diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-23 20:28:35 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-23 20:28:35 +0900 |
| commit | 60a8cd9b262ae4935d0938a2cbdf5a6330ef7f3a (patch) | |
| tree | 4357a4f8e68e606557ec3f37b18eb8acadca27e5 | |
| parent | 78a57f9eb5286828512cc25c6c55288bb4944079 (diff) | |
| parent | 21870d02216954d7e291f2179f4efd4ed6482166 (diff) | |
| download | LunaticChat-60a8cd9b262ae4935d0938a2cbdf5a6330ef7f3a.tar.gz LunaticChat-60a8cd9b262ae4935d0938a2cbdf5a6330ef7f3a.tar.bz2 LunaticChat-60a8cd9b262ae4935d0938a2cbdf5a6330ef7f3a.zip | |
Merge pull request #60 from m1sk9/feat/i18n
feat: Support i18n
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: "自分自身にメッセージを送信することはできません" |
