diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-24 02:50:57 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-01-24 02:52:05 +0900 |
| commit | 59f947dfd8c19859cba6cb3939c859c1fa64e634 (patch) | |
| tree | 4818f6b25297eadd6c11f00fb18f4a9025660746 /platform-paper | |
| parent | a822c76bdf264c4887ebe4cc354341337ca13f30 (diff) | |
| download | LunaticChat-59f947dfd8c19859cba6cb3939c859c1fa64e634.tar.gz LunaticChat-59f947dfd8c19859cba6cb3939c859c1fa64e634.tar.bz2 LunaticChat-59f947dfd8c19859cba6cb3939c859c1fa64e634.zip | |
test: Add i18n test-case
Diffstat (limited to 'platform-paper')
7 files changed, 616 insertions, 4 deletions
diff --git a/platform-paper/build.gradle.kts b/platform-paper/build.gradle.kts index 7ed91aa..67265e6 100644 --- a/platform-paper/build.gradle.kts +++ b/platform-paper/build.gradle.kts @@ -20,6 +20,9 @@ dependencies { compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") implementation("com.charleskorn.kaml:kaml:0.104.0") // YAML configuration implementation("org.jetbrains.kotlin:kotlin-reflect:2.3.0") // Annotation processing + + // Test dependencies + testImplementation("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") } tasks { 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 index 42072fa..e13ebc7 100644 --- 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 @@ -30,15 +30,23 @@ private sealed class YamlValue { * - Providing message retrieval with placeholder substitution * - Fallback to English if the selected language is unavailable * - * @property plugin The JavaPlugin instance + * @property plugin The JavaPlugin instance (required unless resourceLoader is provided) * @property logger The logger for this manager * @property selectedLanguage The currently selected language + * @property resourceLoader Optional resource loader function for testing (overrides plugin.getResource) */ class LanguageManager( - private val plugin: JavaPlugin, + private val plugin: JavaPlugin? = null, private val logger: Logger, private val selectedLanguage: Language, + private val resourceLoader: ((String) -> java.io.InputStream?)? = null, ) { + init { + require(plugin != null || resourceLoader != null) { + "Either plugin or resourceLoader must be provided" + } + } + private val languageCache = mutableMapOf<Language, Map<String, String>>() /** @@ -70,9 +78,10 @@ class LanguageManager( * @throws IllegalStateException if the language file is not found */ private fun loadLanguageFile(language: Language): Map<String, String> { + val resourcePath = "languages/${language.fileName}" val stream = - plugin.getResource("languages/${language.fileName}") - ?: throw IllegalStateException("Language file not found: languages/${language.fileName}") + (resourceLoader?.invoke(resourcePath) ?: plugin?.getResource(resourcePath)) + ?: throw IllegalStateException("Language file not found: $resourcePath") val yamlContent = stream.bufferedReader().use { it.readText() } val yamlNode = Yaml.default.parseToYamlNode(yamlContent) diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageManagerTest.kt new file mode 100644 index 0000000..158d985 --- /dev/null +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageManagerTest.kt @@ -0,0 +1,308 @@ +package dev.m1sk9.lunaticChat.paper.i18n + +import java.io.InputStream +import java.util.logging.Logger +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Test for LanguageManager using test resources. + */ +class LanguageManagerTest { + /** + * Mock Logger that collects log messages. + */ + private class TestLogger : Logger("test", null) { + val infoMessages = mutableListOf<String>() + val warningMessages = mutableListOf<String>() + + override fun info(msg: String) { + infoMessages.add(msg) + } + + override fun warning(msg: String) { + warningMessages.add(msg) + } + } + + /** + * Resource loader that loads files from test classpath. + */ + private val testResourceLoader: (String) -> InputStream? = { path -> + this::class.java.classLoader.getResourceAsStream(path) + } + + private fun createLanguageManager(language: Language): Pair<LanguageManager, TestLogger> { + val logger = TestLogger() + val manager = + LanguageManager( + plugin = null, + logger = logger, + selectedLanguage = language, + resourceLoader = testResourceLoader, + ) + return Pair(manager, logger) + } + + @Test + fun `initialize should load English language file`() { + val (manager, logger) = createLanguageManager(Language.EN) + manager.initialize() + + assertTrue(logger.infoMessages.any { it.contains("en.yml") }) + } + + @Test + fun `initialize should load Japanese language file`() { + val (manager, logger) = createLanguageManager(Language.JA) + manager.initialize() + + assertTrue(logger.infoMessages.any { it.contains("ja.yml") }) + } + + @Test + fun `initialize should load both language files`() { + val (manager, logger) = createLanguageManager(Language.EN) + manager.initialize() + + // Both en.yml and ja.yml should be loaded + assertTrue(logger.infoMessages.any { it.contains("en.yml") }) + assertTrue(logger.infoMessages.any { it.contains("ja.yml") }) + } + + @Test + fun `getMessage should return English message for EN language`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + val message = manager.getMessage("commandDescription.tell") + assertEquals("Send a direct message to another player", message) + } + + @Test + fun `getMessage should return Japanese message for JA language`() { + val (manager, _) = createLanguageManager(Language.JA) + manager.initialize() + + val message = manager.getMessage("commandDescription.tell") + assertEquals("他のプレイヤーにダイレクトメッセージを送信します", message) + } + + @Test + fun `getMessage should handle nested keys with dot notation`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + val onMessage = manager.getMessage("toggle.on") + val offMessage = manager.getMessage("toggle.off") + + assertEquals("Enabled", onMessage) + assertEquals("Disabled", offMessage) + } + + @Test + fun `getMessage should substitute placeholders`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + val message = + manager.getMessage( + "tellTargetOffline", + mapOf("target" to "Steve"), + ) + + assertEquals("Player 'Steve' is currently offline.", message) + } + + @Test + fun `getMessage should substitute multiple placeholders`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + val message = + manager.getMessage( + "directMessageNoticeToggle", + mapOf("toggle" to "Enabled"), + ) + + assertEquals("Direct message notifications have been set to Enabled", message) + } + + @Test + fun `getMessage should handle Japanese placeholders`() { + val (manager, _) = createLanguageManager(Language.JA) + manager.initialize() + + val message = + manager.getMessage( + "tellTargetOffline", + mapOf("target" to "太郎"), + ) + + assertEquals("プレイヤー '太郎' は現在オフラインです。", message) + } + + @Test + fun `getMessage should return key when message not found`() { + val (manager, logger) = createLanguageManager(Language.EN) + manager.initialize() + + val message = manager.getMessage("nonexistent.key") + + assertEquals("nonexistent.key", message) + assertTrue(logger.warningMessages.any { it.contains("Message key not found: nonexistent.key") }) + } + + @Test + fun `getMessage should handle empty placeholders map`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + val message = manager.getMessage("playerOnlyCommand", emptyMap()) + + assertEquals("This command can only be executed by players.", message) + } + + @Test + fun `getToggleText should return 'on' text when enabled`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + val text = manager.getToggleText(true) + + assertEquals("Enabled", text) + } + + @Test + fun `getToggleText should return 'off' text when disabled`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + val text = manager.getToggleText(false) + + assertEquals("Disabled", text) + } + + @Test + fun `getToggleText should return Japanese text for JA language`() { + val (manager, _) = createLanguageManager(Language.JA) + manager.initialize() + + val onText = manager.getToggleText(true) + val offText = manager.getToggleText(false) + + assertEquals("有効", onText) + assertEquals("無効", offText) + } + + @Test + fun `getMessage should retrieve all command descriptions`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + val commands = listOf("tell", "reply", "jp", "notice") + commands.forEach { command -> + val message = manager.getMessage("commandDescription.$command") + assertNotNull(message) + assertTrue(message.isNotEmpty()) + } + } + + @Test + fun `getMessage should retrieve all standard messages`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + val keys = + listOf( + "playerOnlyCommand", + "replyTargetNotFound", + "tellYourself", + ) + + keys.forEach { key -> + val message = manager.getMessage(key) + assertNotNull(message) + assertTrue(message.isNotEmpty()) + } + } + + @Test + fun `getMessage should handle placeholder not provided`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + // Get message with placeholder but don't provide value + val message = manager.getMessage("tellTargetOffline") + + // Should still return message with placeholder intact + assertEquals("Player '{target}' is currently offline.", message) + } + + @Test + fun `initialize should log number of loaded keys`() { + val (manager, logger) = createLanguageManager(Language.EN) + manager.initialize() + + // Should log number of keys for each language file + assertTrue(logger.infoMessages.any { it.contains("keys)") }) + } + + @Test + fun `getMessage should be case sensitive for keys`() { + val (manager, logger) = createLanguageManager(Language.EN) + manager.initialize() + + val message = manager.getMessage("PlayerOnlyCommand") // Wrong case + + assertEquals("PlayerOnlyCommand", message) // Should return key as-is + assertTrue(logger.warningMessages.any { it.contains("Message key not found") }) + } + + @Test + fun `getMessage should handle special characters in placeholder values`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + val message = + manager.getMessage( + "tellTargetOffline", + mapOf("target" to "Player@123!"), + ) + + assertEquals("Player 'Player@123!' is currently offline.", message) + } + + @Test + fun `getMessage should replace all occurrences of placeholder`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + // Create a message with the same placeholder multiple times (if such exists) + val message = + manager.getMessage( + "directMessageNoticeToggle", + mapOf("toggle" to "ON"), + ) + + // Should replace the placeholder + assertTrue(message.contains("ON")) + } + + @Test + fun `flattened keys should handle nested structure correctly`() { + val (manager, _) = createLanguageManager(Language.EN) + manager.initialize() + + // Verify that nested keys work + val commandTell = manager.getMessage("commandDescription.tell") + val toggleOn = manager.getMessage("toggle.on") + + assertNotNull(commandTell) + assertNotNull(toggleOn) + assertTrue(commandTell.isNotEmpty()) + assertTrue(toggleOn.isNotEmpty()) + } +} diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageTest.kt new file mode 100644 index 0000000..c8b2c3b --- /dev/null +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageTest.kt @@ -0,0 +1,75 @@ +package dev.m1sk9.lunaticChat.paper.i18n + +import kotlin.test.Test +import kotlin.test.assertEquals + +class LanguageTest { + @Test + fun `should have correct code for EN`() { + assertEquals("en", Language.EN.code) + } + + @Test + fun `should have correct code for JA`() { + assertEquals("ja", Language.JA.code) + } + + @Test + fun `should have correct fileName for EN`() { + assertEquals("en.yml", Language.EN.fileName) + } + + @Test + fun `should have correct fileName for JA`() { + assertEquals("ja.yml", Language.JA.fileName) + } + + @Test + fun `fromCode should return EN for 'en'`() { + assertEquals(Language.EN, Language.fromCode("en")) + } + + @Test + fun `fromCode should return JA for 'ja'`() { + assertEquals(Language.JA, Language.fromCode("ja")) + } + + @Test + fun `fromCode should be case insensitive for EN`() { + assertEquals(Language.EN, Language.fromCode("EN")) + assertEquals(Language.EN, Language.fromCode("En")) + assertEquals(Language.EN, Language.fromCode("eN")) + } + + @Test + fun `fromCode should be case insensitive for JA`() { + assertEquals(Language.JA, Language.fromCode("JA")) + assertEquals(Language.JA, Language.fromCode("Ja")) + assertEquals(Language.JA, Language.fromCode("jA")) + } + + @Test + fun `fromCode should fallback to EN for unknown code`() { + assertEquals(Language.EN, Language.fromCode("unknown")) + assertEquals(Language.EN, Language.fromCode("fr")) + assertEquals(Language.EN, Language.fromCode("de")) + assertEquals(Language.EN, Language.fromCode("")) + } + + @Test + fun `fromCode should fallback to EN for empty string`() { + assertEquals(Language.EN, Language.fromCode("")) + } + + @Test + fun `should have exactly two language entries`() { + assertEquals(2, Language.entries.size) + } + + @Test + fun `entries should contain EN and JA`() { + val entries = Language.entries + assertEquals(true, entries.contains(Language.EN)) + assertEquals(true, entries.contains(Language.JA)) + } +} diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageFormatterTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageFormatterTest.kt new file mode 100644 index 0000000..04ef02a --- /dev/null +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageFormatterTest.kt @@ -0,0 +1,181 @@ +package dev.m1sk9.lunaticChat.paper.i18n + +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class MessageFormatterTest { + private val plainSerializer = PlainTextComponentSerializer.plainText() + + @Test + fun `format should add LC prefix`() { + val result = MessageFormatter.format("test message") + val plainText = plainSerializer.serialize(result) + assertTrue(plainText.startsWith("[LC] ")) + } + + @Test + fun `format should include message text`() { + val result = MessageFormatter.format("test message") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] test message", plainText) + } + + @Test + fun `format should preserve placeholder braces in plain text`() { + val result = MessageFormatter.format("Hello {name}") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] Hello {name}", plainText) + } + + @Test + fun `format should handle multiple placeholders`() { + val result = MessageFormatter.format("Player {player} sent message to {target}") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] Player {player} sent message to {target}", plainText) + } + + @Test + fun `format should handle message without placeholders`() { + val result = MessageFormatter.format("Simple message", highlightPlaceholders = false) + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] Simple message", plainText) + } + + @Test + fun `formatError should add LC prefix`() { + val result = MessageFormatter.formatError("error message") + val plainText = plainSerializer.serialize(result) + assertTrue(plainText.startsWith("[LC] ")) + } + + @Test + fun `formatError should include error message text`() { + val result = MessageFormatter.formatError("Something went wrong") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] Something went wrong", plainText) + } + + @Test + fun `formatError should preserve placeholders in plain text`() { + val result = MessageFormatter.formatError("Player {player} not found") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] Player {player} not found", plainText) + } + + @Test + fun `formatSuccess should add LC prefix`() { + val result = MessageFormatter.formatSuccess("success message") + val plainText = plainSerializer.serialize(result) + assertTrue(plainText.startsWith("[LC] ")) + } + + @Test + fun `formatSuccess should include success message text`() { + val result = MessageFormatter.formatSuccess("Operation completed") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] Operation completed", plainText) + } + + @Test + fun `formatSuccess should preserve placeholders in plain text`() { + val result = MessageFormatter.formatSuccess("Setting {setting} enabled") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] Setting {setting} enabled", plainText) + } + + @Test + fun `format should handle empty message`() { + val result = MessageFormatter.format("") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] ", plainText) + } + + @Test + fun `format should handle message with only placeholder`() { + val result = MessageFormatter.format("{value}") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] {value}", plainText) + } + + @Test + fun `format should handle nested braces`() { + val result = MessageFormatter.format("{{nested}}") + val plainText = plainSerializer.serialize(result) + // Only {nested} should be treated as placeholder, outer braces are literal + assertTrue(plainText.contains("{nested}")) + } + + @Test + fun `format should handle Japanese text`() { + val result = MessageFormatter.format("こんにちは {name}") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] こんにちは {name}", plainText) + } + + @Test + fun `formatError should handle Japanese text`() { + val result = MessageFormatter.formatError("エラーが発生しました") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] エラーが発生しました", plainText) + } + + @Test + fun `formatSuccess should handle Japanese text`() { + val result = MessageFormatter.formatSuccess("設定を有効にしました") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] 設定を有効にしました", plainText) + } + + @Test + fun `format should handle very long message`() { + val longMessage = "a".repeat(1000) + val result = MessageFormatter.format(longMessage) + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] $longMessage", plainText) + } + + @Test + fun `format should handle special characters`() { + val result = MessageFormatter.format("Special: !@#$%^&*()") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] Special: !@#$%^&*()", plainText) + } + + @Test + fun `format should handle message with newlines`() { + val result = MessageFormatter.format("Line 1\nLine 2") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] Line 1\nLine 2", plainText) + } + + @Test + fun `format should handle unclosed brace`() { + val result = MessageFormatter.format("Unclosed {placeholder") + val plainText = plainSerializer.serialize(result) + // Unclosed braces should not be treated as placeholders + assertEquals("[LC] Unclosed {placeholder", plainText) + } + + @Test + fun `format should handle placeholder at start`() { + val result = MessageFormatter.format("{player} joined the game") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] {player} joined the game", plainText) + } + + @Test + fun `format should handle placeholder at end`() { + val result = MessageFormatter.format("Welcome {player}") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] Welcome {player}", plainText) + } + + @Test + fun `format should handle consecutive placeholders`() { + val result = MessageFormatter.format("{player1}{player2}") + val plainText = plainSerializer.serialize(result) + assertEquals("[LC] {player1}{player2}", plainText) + } +} diff --git a/platform-paper/src/test/resources/languages/en.yml b/platform-paper/src/test/resources/languages/en.yml new file mode 100644 index 0000000..b7d2223 --- /dev/null +++ b/platform-paper/src/test/resources/languages/en.yml @@ -0,0 +1,18 @@ +commandDescription: + tell: "Send a direct message to another player" + reply: "Reply to the player who last sent/received a direct message" + jp: "Toggle romaji-to-kana conversion on/off" + notice: "Toggle direct message notifications on/off" + +directMessageNoticeStatus: "Direct message notifications are currently {toggle}" +directMessageNoticeToggle: "Direct message notifications have been set to {toggle}" +playerOnlyCommand: "This command can only be executed by players." +replyTargetNotFound: "Reply target player not found." +romajiConversionToggle: "Romaji-to-kana conversion has been set to {toggle}" +romajiConversionStatus: "Romaji-to-kana conversion is currently {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/test/resources/languages/ja.yml b/platform-paper/src/test/resources/languages/ja.yml new file mode 100644 index 0000000..9588375 --- /dev/null +++ b/platform-paper/src/test/resources/languages/ja.yml @@ -0,0 +1,18 @@ +commandDescription: + tell: "他のプレイヤーにダイレクトメッセージを送信します" + reply: "最後にダイレクトメッセージを送受信したプレイヤーに返信します" + jp: "ローマ字→かな変換のオン/オフを切り替えます" + notice: "ダイレクトメッセージ通知のオン/オフを切り替えます" + +directMessageNoticeStatus: "ダイレクトメッセージ通知は現在 {toggle} です" +directMessageNoticeToggle: "ダイレクトメッセージ通知を {toggle} に設定しました" +playerOnlyCommand: "このコマンドはプレイヤーのみが実行できます。" +replyTargetNotFound: "返信先のプレイヤーが見つかりませんでした。" +romajiConversionToggle: "ローマ字→かな変換を {toggle} に設定しました" +romajiConversionStatus: "ローマ字→かな変換は現在 {toggle} です" +tellTargetOffline: "プレイヤー '{target}' は現在オフラインです。" +tellYourself: "自分自身にメッセージを送信することはできません。" + +toggle: + off: "無効" + on: "有効" |
