summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-01-23 20:28:35 +0900
committerGitHub <noreply@github.com>2026-01-23 20:28:35 +0900
commit60a8cd9b262ae4935d0938a2cbdf5a6330ef7f3a (patch)
tree4357a4f8e68e606557ec3f37b18eb8acadca27e5
parent78a57f9eb5286828512cc25c6c55288bb4944079 (diff)
parent21870d02216954d7e291f2179f4efd4ed6482166 (diff)
downloadLunaticChat-60a8cd9b262ae4935d0938a2cbdf5a6330ef7f3a.tar.gz
LunaticChat-60a8cd9b262ae4935d0938a2cbdf5a6330ef7f3a.tar.bz2
LunaticChat-60a8cd9b262ae4935d0938a2cbdf5a6330ef7f3a.zip
Merge pull request #60 from m1sk9/feat/i18n
feat: Support i18n
-rw-r--r--CHANGELOG.md9
-rw-r--r--build.gradle.kts2
-rw-r--r--docs/src/guide/configuration.md15
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt20
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt12
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/DirectMessageNoticeToggleCommand.kt27
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt17
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/RomajiConvertToggleCommand.kt27
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt23
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt5
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/Language.kt27
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageConfig.kt58
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageManager.kt104
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageFormatter.kt129
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageKey.kt53
-rw-r--r--platform-paper/src/main/resources/config.yml3
-rw-r--r--platform-paper/src/main/resources/languages/en.yml30
-rw-r--r--platform-paper/src/main/resources/languages/ja.yml30
19 files changed, 541 insertions, 52 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ea8c21..92c102f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,11 @@
# LunaticChat Changelog
-## v1 (Minecraft 1.21.X)
-
-### v1.0.0
+## v0
-- Dokka currently references only the platform-paper module.
+### v0.5.0
-## v0
+- Added i18n support for English and Japanese languages.
+- Fixed a Dokka currently references only the platform-paper module.
### v0.4.1
diff --git a/build.gradle.kts b/build.gradle.kts
index 31b2f87..26570b3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -14,7 +14,7 @@ plugins {
allprojects {
group = "dev.m1sk9"
- version = "1.0.0"
+ version = "0.5.0"
repositories {
mavenCentral()
diff --git a/docs/src/guide/configuration.md b/docs/src/guide/configuration.md
index 07458b3..3b7bd1e 100644
--- a/docs/src/guide/configuration.md
+++ b/docs/src/guide/configuration.md
@@ -24,6 +24,9 @@ userSettingsFilePath: "player-settings.yaml"
# If enabled, LunaticChat will check for updates on startup.
checkForUpdates: true
+# Plugin Configuration Language. This setting applies only to player feedback and does not affect plugin logs or similar outputs.
+language: "en"
+
# ----------------------------------------------
# ----------- Features Settings ------------
# ----------------------------------------------
@@ -88,6 +91,18 @@ LunaticChat がプレイヤーの設定を保存する YAML ファイルのパ
LunaticChat の起動時・権限を持ったプレイヤーがサーバに参加した際に,LunaticChat のアップデートを促すかどうか設定します.
+### `language`
+
+- Type: `string`
+- Default: `en`
+
+LunaticChat のプレイヤー向けメッセージの言語を指定します.
+
+### Supported languages:
+
+- `en`: English
+- `ja`: 日本語
+
## Features Settings
### `features.quickReplies.enabled`
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
index 7a7365e..9664340 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
@@ -14,6 +14,7 @@ import dev.m1sk9.lunaticChat.paper.config.ConfigManager
import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration
import dev.m1sk9.lunaticChat.paper.converter.ConversionCache
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.listener.PlayerChatListener
import dev.m1sk9.lunaticChat.paper.listener.PlayerPresenceListener
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
@@ -30,6 +31,7 @@ class LunaticChat :
JavaPlugin(),
Listener {
lateinit var directMessageHandler: DirectMessageHandler
+ lateinit var languageManager: LanguageManager
private lateinit var commandRegistry: CommandRegistry
private var updateChecker: UpdateChecker? = null
@@ -47,6 +49,16 @@ class LunaticChat :
logger.info("Debug: $configuration")
}
+ // Initialize language manager (BEFORE commands)
+ languageManager =
+ LanguageManager(
+ plugin = this,
+ logger = logger,
+ selectedLanguage = configuration.language,
+ )
+ languageManager.initialize()
+ logger.info("Language system initialized: ${configuration.language.code}")
+
val httpClient = HttpClient(CIO)
// Initialize player settings manager (always needed for DM notifications)
@@ -176,21 +188,21 @@ class LunaticChat :
commandRegistry = CommandRegistry(this)
commandRegistry.registerAll(
- TellCommand(this, directMessageHandler),
- DirectMessageNoticeToggleCommand(this, playerSettingsManager!!),
+ TellCommand(this, directMessageHandler, languageManager),
+ DirectMessageNoticeToggleCommand(this, playerSettingsManager!!, languageManager),
)
// Register /reply command if quick replies are enabled
if (configuration.features.quickRepliesEnabled.enabled) {
commandRegistry.registerAll(
- ReplyCommand(this, directMessageHandler),
+ ReplyCommand(this, directMessageHandler, languageManager),
)
}
// Register /jp command if Japanese conversion is enabled
if (configuration.features.japaneseConversion.enabled) {
commandRegistry.registerAll(
- RomajiConvertToggleCommand(this, playerSettingsManager!!),
+ RomajiConvertToggleCommand(this, playerSettingsManager!!, languageManager),
)
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt
index 8bbd1bd..d8ca412 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt
@@ -6,6 +6,8 @@ import dev.m1sk9.lunaticChat.paper.LunaticChat
import dev.m1sk9.lunaticChat.paper.command.annotation.Command
import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import dev.m1sk9.lunaticChat.paper.i18n.MessageKey
import io.papermc.paper.command.brigadier.CommandSourceStack
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
@@ -38,8 +40,8 @@ abstract class LunaticCommand(
/** Command aliases */
val aliases: List<String> get() = commandAnnotation.aliases.toList()
- /** Command description for help text */
- val description: String get() = commandAnnotation.description
+ /** Command description for help text - can be overridden for i18n */
+ open val description: String get() = commandAnnotation.description
/** Required permission node, if any */
val permission: String? get() = permissionAnnotation?.value?.objectInstance?.permissionNode
@@ -75,9 +77,9 @@ abstract class LunaticCommand(
protected fun checkPlayerOnly(ctx: CommandContext): CommandResult? {
if (isPlayerOnly && !ctx.isPlayer) {
return CommandResult.Failure(
- Component
- .text("This command can only be executed by a player.")
- .color(NamedTextColor.RED),
+ MessageFormatter.formatError(
+ plugin.languageManager.getMessage(MessageKey.PlayerOnlyCommand),
+ ),
)
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/DirectMessageNoticeToggleCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/DirectMessageNoticeToggleCommand.kt
index 7906ca9..e838ee0 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/DirectMessageNoticeToggleCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/DirectMessageNoticeToggleCommand.kt
@@ -9,23 +9,28 @@ import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import dev.m1sk9.lunaticChat.paper.i18n.MessageKey
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.Commands
-import net.kyori.adventure.text.Component
-import net.kyori.adventure.text.format.NamedTextColor
@Command(
name = "notice",
aliases = ["dmnotice"],
- description = "Toggle direct message notification",
+ description = "",
)
@Permission(LunaticChatPermissionNode.NoticeToggle::class)
@PlayerOnly
class DirectMessageNoticeToggleCommand(
plugin: LunaticChat,
private val settingsManager: PlayerSettingsManager,
+ private val languageManager: LanguageManager,
) : LunaticCommand(plugin) {
+ override val description: String
+ get() = languageManager.getMessage(MessageKey.CommandDescriptionNotice)
+
override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
Commands
.literal(name)
@@ -63,11 +68,11 @@ class DirectMessageNoticeToggleCommand(
val updatedSettings = currentSettings.copy(directMessageNotificationEnabled = enable)
settingsManager.updateSettings(updatedSettings)
- val statusText = if (enable) "enabled" else "disabled"
+ val toggleText = languageManager.getToggleText(enable)
val message =
- Component
- .text("Direct message notification has been $statusText.")
- .color(NamedTextColor.GREEN)
+ MessageFormatter.formatSuccess(
+ languageManager.getMessage(MessageKey.DirectMessageNoticeToggle(toggleText)),
+ )
player.sendMessage(message)
return CommandResult.Success
@@ -77,11 +82,11 @@ class DirectMessageNoticeToggleCommand(
val player = ctx.requirePlayer()
val settings = settingsManager.getSettings(player.uniqueId)
- val statusText = if (settings.directMessageNotificationEnabled) "enabled" else "disabled"
+ val toggleText = languageManager.getToggleText(settings.directMessageNotificationEnabled)
val message =
- Component
- .text("Direct message notification is currently $statusText.")
- .color(NamedTextColor.YELLOW)
+ MessageFormatter.format(
+ languageManager.getMessage(MessageKey.DirectMessageNoticeStatus(toggleText)),
+ )
player.sendMessage(message)
return CommandResult.Success
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt
index 3d2fa88..0108f37 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt
@@ -11,23 +11,28 @@ import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import dev.m1sk9.lunaticChat.paper.i18n.MessageKey
import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.Commands
import kotlinx.coroutines.runBlocking
-import net.kyori.adventure.text.Component
-import net.kyori.adventure.text.format.NamedTextColor
@Command(
name = "reply",
aliases = ["r"],
- description = "Reply to the last person who messaged you",
+ description = "",
)
@Permission(LunaticChatPermissionNode.Reply::class)
@PlayerOnly
class ReplyCommand(
plugin: LunaticChat,
private val dmHandler: DirectMessageHandler,
+ private val languageManager: LanguageManager,
) : LunaticCommand(plugin) {
+ override val description: String
+ get() = languageManager.getMessage(MessageKey.CommandDescriptionReply)
+
override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
Commands
.literal(name)
@@ -53,9 +58,9 @@ class ReplyCommand(
val target =
dmHandler.getReplyTarget(sender)
?: return CommandResult.Failure(
- Component
- .text("You have no one to reply to.")
- .color(NamedTextColor.RED),
+ MessageFormatter.formatError(
+ languageManager.getMessage(MessageKey.ReplyTargetNotFound),
+ ),
)
runBlocking {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/RomajiConvertToggleCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/RomajiConvertToggleCommand.kt
index 5cf0492..b25667d 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/RomajiConvertToggleCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/RomajiConvertToggleCommand.kt
@@ -9,23 +9,28 @@ import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import dev.m1sk9.lunaticChat.paper.i18n.MessageKey
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.Commands
-import net.kyori.adventure.text.Component
-import net.kyori.adventure.text.format.NamedTextColor
@Command(
name = "jp",
aliases = [],
- description = "Toggle romaji to Japanese conversion for your messages",
+ description = "",
)
@Permission(LunaticChatPermissionNode.JapaneseToggle::class)
@PlayerOnly
class RomajiConvertToggleCommand(
plugin: LunaticChat,
private val settingsManager: PlayerSettingsManager,
+ private val languageManager: LanguageManager,
) : LunaticCommand(plugin) {
+ override val description: String
+ get() = languageManager.getMessage(MessageKey.CommandDescriptionJp)
+
override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
Commands
.literal(name)
@@ -63,11 +68,11 @@ class RomajiConvertToggleCommand(
val updatedSettings = currentSettings.copy(japaneseConversionEnabled = enable)
settingsManager.updateSettings(updatedSettings)
- val statusText = if (enable) "enabled" else "disabled"
+ val toggleText = languageManager.getToggleText(enable)
val message =
- Component
- .text("Japanese conversion has been $statusText.")
- .color(NamedTextColor.GREEN)
+ MessageFormatter.formatSuccess(
+ languageManager.getMessage(MessageKey.RomajiConversionToggle(toggleText)),
+ )
player.sendMessage(message)
return CommandResult.Success
@@ -77,11 +82,11 @@ class RomajiConvertToggleCommand(
val player = ctx.requirePlayer()
val settings = settingsManager.getSettings(player.uniqueId)
- val statusText = if (settings.japaneseConversionEnabled) "enabled" else "disabled"
+ val toggleText = languageManager.getToggleText(settings.japaneseConversionEnabled)
val message =
- Component
- .text("Japanese conversion is currently $statusText.")
- .color(NamedTextColor.YELLOW)
+ MessageFormatter.format(
+ languageManager.getMessage(MessageKey.RomajiConversionStatus(toggleText)),
+ )
player.sendMessage(message)
return CommandResult.Success
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt
index 36617a3..d958b23 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt
@@ -13,25 +13,30 @@ import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import dev.m1sk9.lunaticChat.paper.i18n.MessageKey
import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.Commands
import kotlinx.coroutines.runBlocking
-import net.kyori.adventure.text.Component
-import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.Bukkit
import java.util.concurrent.CompletableFuture
@Command(
name = "tell",
aliases = ["t", "msg", "m", "w", "whisper"],
- description = "Send a private message to another player",
+ description = "",
)
@Permission(LunaticChatPermissionNode.Tell::class)
@PlayerOnly
class TellCommand(
plugin: LunaticChat,
private val directMessageHandler: DirectMessageHandler,
+ private val languageManager: LanguageManager,
) : LunaticCommand(plugin) {
+ override val description: String
+ get() = languageManager.getMessage(MessageKey.CommandDescriptionTell)
+
override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
Commands
.literal(name)
@@ -64,16 +69,16 @@ class TellCommand(
val recipient =
Bukkit.getPlayer(targetName)
?: return CommandResult.Failure(
- Component
- .text("Player '$targetName' is not online.")
- .color(NamedTextColor.RED),
+ MessageFormatter.formatError(
+ languageManager.getMessage(MessageKey.TellTargetOffline(targetName)),
+ ),
)
if (recipient.uniqueId == sender.uniqueId) {
return CommandResult.Failure(
- Component
- .text("You cannot send a message to yourself.")
- .color(NamedTextColor.RED),
+ MessageFormatter.formatError(
+ languageManager.getMessage(MessageKey.TellYourself),
+ ),
)
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
index 5f3a1b0..7661089 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
@@ -4,6 +4,7 @@ import dev.m1sk9.lunaticChat.paper.config.key.FeaturesConfig
import dev.m1sk9.lunaticChat.paper.config.key.JapaneseConversionFeatureConfig
import dev.m1sk9.lunaticChat.paper.config.key.MessageFormatConfig
import dev.m1sk9.lunaticChat.paper.config.key.QuickRepliesFeatureConfig
+import dev.m1sk9.lunaticChat.paper.i18n.Language
import org.bukkit.configuration.file.FileConfiguration
object ConfigManager {
@@ -59,6 +60,10 @@ object ConfigManager {
"userSettingsFilePath",
"player-settings.yaml",
)!!,
+ language =
+ Language.fromCode(
+ configFile.getString("language", "en")!!,
+ ),
)
lunaticChatConfiguration = loadedConfig
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt
index 0dea2ac..92bc157 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt
@@ -2,6 +2,7 @@ package dev.m1sk9.lunaticChat.paper.config
import dev.m1sk9.lunaticChat.paper.config.key.FeaturesConfig
import dev.m1sk9.lunaticChat.paper.config.key.MessageFormatConfig
+import dev.m1sk9.lunaticChat.paper.i18n.Language
data class LunaticChatConfiguration(
val features: FeaturesConfig,
@@ -9,4 +10,5 @@ data class LunaticChatConfiguration(
val debug: Boolean = false,
val userSettingsFilePath: String = "player-settings.yaml",
val checkForUpdates: Boolean = true,
+ val language: Language = Language.EN,
)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/Language.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/Language.kt
new file mode 100644
index 0000000..a25ecab
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/Language.kt
@@ -0,0 +1,27 @@
+package dev.m1sk9.lunaticChat.paper.i18n
+
+/**
+ * Represents a supported language in LunaticChat.
+ *
+ * @property code The language code (e.g., "en", "ja")
+ * @property fileName The name of the language file (e.g., "en.yml", "ja.yml")
+ */
+enum class Language(
+ val code: String,
+ val fileName: String,
+) {
+ EN("en", "en.yml"),
+ JA("ja", "ja.yml"),
+ ;
+
+ companion object {
+ /**
+ * Converts a language code string to a [Language] enum.
+ * If the code is not recognized, returns [EN] as a fallback.
+ *
+ * @param code The language code to convert
+ * @return The corresponding [Language] enum, or [EN] if not found
+ */
+ fun fromCode(code: String): Language = entries.find { it.code.equals(code, ignoreCase = true) } ?: EN
+ }
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageConfig.kt
new file mode 100644
index 0000000..cc531ab
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageConfig.kt
@@ -0,0 +1,58 @@
+package dev.m1sk9.lunaticChat.paper.i18n
+
+import kotlinx.serialization.Serializable
+
+/**
+ * Represents the structure of a language file.
+ * This data class mirrors the YAML structure of language files (en.yml, ja.yml).
+ *
+ * @property commandDescription Command descriptions for various commands
+ * @property directMessageNoticeStatus Status message for direct message notifications
+ * @property directMessageNoticeToggle Toggle message for direct message notifications
+ * @property replyTargetNotFound Error message when reply target is not found
+ * @property romajiConversionStatus Status message for romaji conversion
+ * @property romajiConversionToggle Toggle message for romaji conversion
+ * @property tellTargetOffline Error message when tell target is offline
+ * @property tellYourself Error message when trying to message yourself
+ * @property toggle Toggle state messages (on/off)
+ */
+@Serializable
+data class LanguageConfig(
+ val commandDescription: CommandDescriptions,
+ val directMessageNoticeStatus: String,
+ val directMessageNoticeToggle: String,
+ val replyTargetNotFound: String,
+ val romajiConversionStatus: String,
+ val romajiConversionToggle: String,
+ val tellTargetOffline: String,
+ val tellYourself: String,
+ val toggle: ToggleMessages,
+)
+
+/**
+ * Command descriptions for all LunaticChat commands.
+ *
+ * @property jp Description for the /jp command
+ * @property notice Description for the /notice command
+ * @property reply Description for the /reply command
+ * @property tell Description for the /tell command
+ */
+@Serializable
+data class CommandDescriptions(
+ val jp: String,
+ val notice: String,
+ val reply: String,
+ val tell: String,
+)
+
+/**
+ * Toggle state messages.
+ *
+ * @property on Message for "enabled" state
+ * @property off Message for "disabled" state
+ */
+@Serializable
+data class ToggleMessages(
+ val on: String,
+ val off: String,
+)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageManager.kt
new file mode 100644
index 0000000..f355808
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageManager.kt
@@ -0,0 +1,104 @@
+package dev.m1sk9.lunaticChat.paper.i18n
+
+import com.charleskorn.kaml.Yaml
+import org.bukkit.plugin.java.JavaPlugin
+import java.util.logging.Logger
+
+/**
+ * Manages language files and provides message retrieval with type safety.
+ *
+ * This class handles:
+ * - Loading language files from resources/languages/
+ * - Caching language configurations in memory
+ * - Providing message retrieval with placeholder substitution
+ * - Fallback to English if the selected language is unavailable
+ *
+ * @property plugin The JavaPlugin instance
+ * @property logger The logger for this manager
+ * @property selectedLanguage The currently selected language
+ */
+class LanguageManager(
+ private val plugin: JavaPlugin,
+ private val logger: Logger,
+ private val selectedLanguage: Language,
+) {
+ private val languageCache = mutableMapOf<Language, LanguageConfig>()
+
+ /**
+ * Initializes the language manager by loading all language files.
+ * This should be called during plugin initialization.
+ *
+ * @throws IllegalStateException if the English fallback file is missing or cannot be loaded
+ */
+ fun initialize() {
+ Language.entries.forEach { lang ->
+ try {
+ val config = loadLanguageFile(lang)
+ languageCache[lang] = config
+ logger.info("Loaded language file: ${lang.fileName}")
+ } catch (e: Exception) {
+ logger.warning("Failed to load ${lang.fileName}: ${e.message}")
+ if (lang == Language.EN) {
+ throw IllegalStateException("English fallback missing", e)
+ }
+ }
+ }
+ }
+
+ /**
+ * Loads a language file from resources/languages/.
+ *
+ * @param language The language to load
+ * @return The parsed LanguageConfig
+ * @throws IllegalStateException if the language file is not found
+ */
+ private fun loadLanguageFile(language: Language): LanguageConfig {
+ val stream =
+ plugin.getResource("languages/${language.fileName}")
+ ?: throw IllegalStateException("Language file not found: languages/${language.fileName}")
+
+ val yamlContent = stream.bufferedReader().use { it.readText() }
+ return Yaml.default.decodeFromString(LanguageConfig.serializer(), yamlContent)
+ }
+
+ /**
+ * Retrieves a message for the given message key.
+ * If the selected language is unavailable, falls back to English.
+ *
+ * @param key The message key to retrieve
+ * @return The formatted message with placeholders substituted
+ */
+ fun getMessage(key: MessageKey): String {
+ val config = languageCache[selectedLanguage] ?: languageCache[Language.EN]!!
+
+ return when (key) {
+ MessageKey.CommandDescriptionTell -> config.commandDescription.tell
+ MessageKey.CommandDescriptionReply -> config.commandDescription.reply
+ MessageKey.CommandDescriptionJp -> config.commandDescription.jp
+ MessageKey.CommandDescriptionNotice -> config.commandDescription.notice
+ is MessageKey.DirectMessageNoticeStatus ->
+ config.directMessageNoticeStatus.replace("{toggle}", key.toggle)
+ is MessageKey.DirectMessageNoticeToggle ->
+ config.directMessageNoticeToggle.replace("{toggle}", key.toggle)
+ is MessageKey.RomajiConversionStatus ->
+ config.romajiConversionStatus.replace("{toggle}", key.toggle)
+ is MessageKey.RomajiConversionToggle ->
+ config.romajiConversionToggle.replace("{toggle}", key.toggle)
+ MessageKey.ReplyTargetNotFound -> config.replyTargetNotFound
+ is MessageKey.TellTargetOffline ->
+ config.tellTargetOffline.replace("{target}", key.targetName)
+ MessageKey.TellYourself -> config.tellYourself
+ MessageKey.ToggleOn -> config.toggle.on
+ MessageKey.ToggleOff -> config.toggle.off
+ MessageKey.PlayerOnlyCommand -> "This command can only be executed by players."
+ }
+ }
+
+ /**
+ * Gets the translated text for a toggle state (enabled/disabled).
+ *
+ * @param enabled The toggle state
+ * @return The translated "on" or "off" text
+ */
+ fun getToggleText(enabled: Boolean): String = getMessage(if (enabled) MessageKey.ToggleOn else MessageKey.ToggleOff)
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageFormatter.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageFormatter.kt
new file mode 100644
index 0000000..862ab61
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageFormatter.kt
@@ -0,0 +1,129 @@
+package dev.m1sk9.lunaticChat.paper.i18n
+
+import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.format.NamedTextColor
+
+/**
+ * Formats messages with the prefix and applies color styling.
+ *
+ * All messages follow this format:
+ * - Prefix: `LC`, AQUA + BOLD
+ * - Message: Main text in GRAY
+ * - Placeholders: Text in {braces} highlighted in YELLOW
+ */
+object MessageFormatter {
+ private val PREFIX_COLOR = NamedTextColor.LIGHT_PURPLE
+ private val MESSAGE_COLOR = NamedTextColor.WHITE
+ private val PLACEHOLDER_COLOR = NamedTextColor.YELLOW
+ private val ERROR_COLOR = NamedTextColor.RED
+ private val SUCCESS_COLOR = NamedTextColor.GREEN
+
+ /**
+ * Formats a message with the standard prefix and gray text.
+ * Placeholders in {braces} are highlighted in yellow if [highlightPlaceholders] is true.
+ *
+ * @param message The message text to format
+ * @param highlightPlaceholders Whether to highlight {placeholder} text in yellow
+ * @return A formatted Component with the prefix
+ */
+ fun format(
+ message: String,
+ highlightPlaceholders: Boolean = true,
+ ): Component {
+ val prefix =
+ Component
+ .text("[LC] ")
+ .color(PREFIX_COLOR)
+
+ val messageComponent =
+ if (highlightPlaceholders) {
+ formatWithPlaceholders(message, MESSAGE_COLOR)
+ } else {
+ Component.text(message).color(MESSAGE_COLOR)
+ }
+
+ return prefix.append(messageComponent)
+ }
+
+ /**
+ * Formats an error message with the prefix and red text.
+ *
+ * @param message The error message text
+ * @return A formatted Component with red text
+ */
+ fun formatError(message: String): Component {
+ val prefix =
+ Component
+ .text("[LC] ")
+ .color(PREFIX_COLOR)
+
+ val messageComponent = formatWithPlaceholders(message, ERROR_COLOR)
+
+ return prefix.append(messageComponent)
+ }
+
+ /**
+ * Formats a success message with the prefix and green text.
+ *
+ * @param message The success message text
+ * @return A formatted Component with green text
+ */
+ fun formatSuccess(message: String): Component {
+ val prefix =
+ Component
+ .text("[LC] ")
+ .color(PREFIX_COLOR)
+
+ val messageComponent = formatWithPlaceholders(message, SUCCESS_COLOR)
+
+ return prefix.append(messageComponent)
+ }
+
+ /**
+ * Parses a message and highlights placeholders in {braces} with yellow color.
+ * Text outside braces uses the specified base color.
+ *
+ * @param message The message text to parse
+ * @param baseColor The color for non-placeholder text
+ * @return A Component with highlighted placeholders
+ */
+ private fun formatWithPlaceholders(
+ message: String,
+ baseColor: NamedTextColor,
+ ): Component {
+ val result = Component.text()
+ val regex = Regex("""\{([^}]+)}""")
+ var lastIndex = 0
+
+ regex.findAll(message).forEach { match ->
+ // Add text before the placeholder
+ if (match.range.first > lastIndex) {
+ result.append(
+ Component
+ .text(message.substring(lastIndex, match.range.first))
+ .color(baseColor),
+ )
+ }
+
+ // Add the placeholder in yellow (including braces)
+ result.append(
+ Component
+ .text(match.value)
+ .color(PLACEHOLDER_COLOR),
+ )
+
+ lastIndex = match.range.last + 1
+ }
+
+ // Add remaining text after the last placeholder
+ if (lastIndex < message.length) {
+ result.append(
+ Component
+ .text(message.substring(lastIndex))
+ .color(baseColor),
+ )
+ }
+
+ return result.build()
+ }
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageKey.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageKey.kt
new file mode 100644
index 0000000..b6fe8dc
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageKey.kt
@@ -0,0 +1,53 @@
+package dev.m1sk9.lunaticChat.paper.i18n
+
+/**
+ * Type-safe message keys for LunaticChat i18n system.
+ *
+ * This sealed class ensures compile-time safety when referencing messages,
+ * preventing typos and missing translations. Each message key can have
+ * associated parameters that will be substituted in the final message.
+ */
+sealed class MessageKey {
+ // Command descriptions
+ data object CommandDescriptionTell : MessageKey()
+
+ data object CommandDescriptionReply : MessageKey()
+
+ data object CommandDescriptionJp : MessageKey()
+
+ data object CommandDescriptionNotice : MessageKey()
+
+ // Toggle messages (with placeholder)
+ data class DirectMessageNoticeStatus(
+ val toggle: String,
+ ) : MessageKey()
+
+ data class DirectMessageNoticeToggle(
+ val toggle: String,
+ ) : MessageKey()
+
+ data class RomajiConversionStatus(
+ val toggle: String,
+ ) : MessageKey()
+
+ data class RomajiConversionToggle(
+ val toggle: String,
+ ) : MessageKey()
+
+ // Error messages
+ data object ReplyTargetNotFound : MessageKey()
+
+ data class TellTargetOffline(
+ val targetName: String,
+ ) : MessageKey()
+
+ data object TellYourself : MessageKey()
+
+ // Toggle values
+ data object ToggleOn : MessageKey()
+
+ data object ToggleOff : MessageKey()
+
+ // System messages
+ data object PlayerOnlyCommand : MessageKey()
+}
diff --git a/platform-paper/src/main/resources/config.yml b/platform-paper/src/main/resources/config.yml
index e6a372d..331f690 100644
--- a/platform-paper/src/main/resources/config.yml
+++ b/platform-paper/src/main/resources/config.yml
@@ -21,6 +21,9 @@ userSettingsFilePath: "player-settings.yaml"
# If enabled, LunaticChat will check for updates on startup.
checkForUpdates: true
+# Plugin Configuration Language. This setting applies only to player feedback and does not affect plugin logs or similar outputs.
+language: "en"
+
# ----------------------------------------------
# ----------- Features Settings ------------
# ----------------------------------------------
diff --git a/platform-paper/src/main/resources/languages/en.yml b/platform-paper/src/main/resources/languages/en.yml
new file mode 100644
index 0000000..17743f2
--- /dev/null
+++ b/platform-paper/src/main/resources/languages/en.yml
@@ -0,0 +1,30 @@
+# ----------------------------------------------
+# ------ LunaticChat Languages Files --------
+# ----------------- English -----------------
+# ----------------------------------------------
+#
+# English language file for LunaticChat i18n (internationalization)
+#
+# These translations may affect the UI/UX of LunaticChat, so please translate accurately.
+#
+# When adding new translation keys, please submit a pull request to GitHub.
+#
+# ----------------------------------------------
+
+commandDescription:
+ jp: "Toggle romaji-to-kana conversion on/off"
+ notice: "Toggle direct message notifications on/off"
+ reply: "Reply to the player who last sent/received a direct message"
+ tell: "Send a direct message to another player"
+
+directMessageNoticeStatus: "Direct message notifications are currently {toggle}"
+directMessageNoticeToggle: "Direct message notifications have been set to {toggle}"
+replyTargetNotFound: "Reply target player not found."
+romajiConversionStatus: "Romaji-to-kana conversion is currently {toggle}"
+romajiConversionToggle: "Romaji-to-kana conversion has been set to {toggle}"
+tellTargetOffline: "Player '{target}' is currently offline."
+tellYourself: "You cannot send a message to yourself."
+
+toggle:
+ off: "Disabled"
+ on: "Enabled"
diff --git a/platform-paper/src/main/resources/languages/ja.yml b/platform-paper/src/main/resources/languages/ja.yml
new file mode 100644
index 0000000..e8770f8
--- /dev/null
+++ b/platform-paper/src/main/resources/languages/ja.yml
@@ -0,0 +1,30 @@
+# ----------------------------------------------
+# ------ LunaticChat Languages Files --------
+# ----------------- 日本語 -------------------
+# ----------------------------------------------
+#
+# LunaticChat の i18n (国際化) 用の日本語言語ファイル
+#
+# 以下の翻訳は LunaticChat の UI/UX に影響を与える可能性があるため、正確に翻訳してください。
+#
+# また,新しい翻訳キーを追加する際は GitHub へのプルリクエストの提出をお願いします。
+#
+# ----------------------------------------------
+
+toggle:
+ on: "有効"
+ off: "無効"
+
+commandDescription:
+ tell: "他のプレイヤーにダイレクトメッセージを送信します"
+ reply: "最後にダイレクトメッセージを送信/受信したプレイヤーに返信します"
+ jp: "かな・ローマ字変換機能のオン/オフを切り替えます"
+ notice: "ダイレクトメッセージ通知のオン/オフを切り替えます"
+
+directMessageNoticeToggle: "ダイレクトメッセージ通知を{toggle}にしました"
+directMessageNoticeStatus: "現在ダイレクトメッセージ通知は{toggle}です"
+replyTargetNotFound: "返信対象のプレイヤーが見つかりません"
+romajiConversionToggle: "かな・ローマ字変換機能を{toggle}にしました"
+romajiConversionStatus: "現在かな・ローマ字変換機能は{toggle}です"
+tellTargetOffline: "プレイヤー '{target}' は現在オフラインです"
+tellYourself: "自分自身にメッセージを送信することはできません"