package summer.phrasebook import java.nio.file.Path class PhrasebookTranslatorSpec extends munit.FunSuite: val langDir: Path = Path.of(getClass.getClassLoader.getResource("lang/en/lang.conf").toURI).getParent.getParent val overridesDir: Path = Path.of(getClass.getClassLoader.getResource("overrides/en/lang.conf").toURI).getParent.getParent def makeTranslator(): PhrasebookTranslator = val t = new PhrasebookTranslator( initialLocale = LocaleTag("en"), initialFallback = Some(LocaleTag("en")), ) t.addNamespace("test.plugin", langDir) t override def beforeEach(context: BeforeEach): Unit = KeyParser.clearCache() // --- Basic get --- test("get simple translation") { val t = makeTranslator() assertEquals( t.get("test.plugin::lang.labels.pluginName"), "Blog", ) } test("get nested translation") { val t = makeTranslator() assertEquals( t.get("test.plugin::lang.post.create_title"), "Create Post", ) } test("get returns raw key when not found") { val t = makeTranslator() assertEquals( t.get("test.plugin::lang.nonexistent.key"), "test.plugin::lang.nonexistent.key", ) } test("get from different group (validation)") { val t = makeTranslator() assertEquals( t.get("test.plugin::validation.required"), "The :attribute field is required.", ) } // --- Parameter replacement --- test("get with replacements") { val t = makeTranslator() assertEquals( t.get("test.plugin::lang.welcome", Map("name" -> "Alice")), "Welcome, Alice!", ) } test("get with multiple replacements and case variants") { val t = makeTranslator() assertEquals( t.get("test.plugin::lang.greeting", Map("name" -> "alice", "count" -> "3")), "Hello Alice, you have 3 new messages", ) } test("get with uppercase replacement") { val t = makeTranslator() assertEquals( t.get("test.plugin::lang.shout", Map("name" -> "alice")), "ATTENTION ALICE!", ) } test("validation message with attribute replacement") { val t = makeTranslator() assertEquals( t.get("test.plugin::validation.required", Map("attribute" -> "email")), "The email field is required.", ) } // --- Locale switching --- test("get translation in Polish") { val t = makeTranslator() assertEquals( t.get("test.plugin::lang.labels.posts", locale = Some(LocaleTag("pl"))), "Wpisy", ) } test("get translation in German") { val t = makeTranslator() assertEquals( t.get("test.plugin::lang.labels.posts", locale = Some(LocaleTag("de"))), "Beiträge", ) } test("setLocale changes default locale") { val t = makeTranslator() t.setLocale(LocaleTag("pl")) assertEquals( t.get("test.plugin::lang.post.create_title"), "Utwórz wpis", ) } // --- Fallback --- test("falls back to English when key missing in Polish") { val t = makeTranslator() // Polish has no "shout" key, should fall back to English assertEquals( t.get("test.plugin::lang.shout", Map("name" -> "alice"), locale = Some(LocaleTag("pl"))), "ATTENTION ALICE!", ) } test("falls back to English when key missing in German") { val t = makeTranslator() assertEquals( t.get("test.plugin::lang.post.edit_title", locale = Some(LocaleTag("de"))), "Edit Post", ) } // --- Pluralization via choice --- test("choice English singular") { val t = makeTranslator() assertEquals( t.choice("test.plugin::lang.posts_count", 1), "1 post", ) } test("choice English plural") { val t = makeTranslator() assertEquals( t.choice("test.plugin::lang.posts_count", 5), "5 posts", ) } test("choice Polish forms") { val t = makeTranslator() // 1 wpis (one) assertEquals( t.choice("test.plugin::lang.posts_count", 1, locale = Some(LocaleTag("pl"))), "1 wpis", ) // 2 wpisy (few) assertEquals( t.choice("test.plugin::lang.posts_count", 2, locale = Some(LocaleTag("pl"))), "2 wpisy", ) // 5 wpisów (many) assertEquals( t.choice("test.plugin::lang.posts_count", 5, locale = Some(LocaleTag("pl"))), "5 wpisów", ) // 22 wpisy (few: n%10=2, n%100=22) assertEquals( t.choice("test.plugin::lang.posts_count", 22, locale = Some(LocaleTag("pl"))), "22 wpisy", ) } test("choice with explicit range syntax") { val t = makeTranslator() assertEquals( t.choice("test.plugin::lang.items_range", 0), "No items", ) assertEquals( t.choice("test.plugin::lang.items_range", 1), "One item", ) assertEquals( t.choice("test.plugin::lang.items_range", 3), "A few items", ) assertEquals( t.choice("test.plugin::lang.items_range", 10), "Many items", ) } // --- has --- test("has returns true for existing key") { val t = makeTranslator() assert(t.has("test.plugin::lang.labels.pluginName")) } test("has returns false for missing key") { val t = makeTranslator() assert(!t.has("test.plugin::lang.nonexistent")) } // --- Namespace alias --- test("namespace alias resolves correctly") { val t = makeTranslator() t.addAlias("blog", "test.plugin") assertEquals( t.get("blog::lang.labels.pluginName"), "Blog", ) } // --- Runtime set --- test("set overrides a translation at runtime") { val t = makeTranslator() t.set("test.plugin::lang.labels.pluginName", "My Blog") assertEquals( t.get("test.plugin::lang.labels.pluginName"), "My Blog", ) } test("set override is locale-specific") { val t = makeTranslator() t.set("test.plugin::lang.labels.pluginName", "My Blog", Some(LocaleTag("en"))) // Polish should still return original assertEquals( t.get("test.plugin::lang.labels.pluginName", locale = Some(LocaleTag("pl"))), "Blog", ) } // --- clearCache --- test("clearCache resets loaded translations") { val t = makeTranslator() // Load something t.get("test.plugin::lang.labels.pluginName") // Clear t.clearCache() // Should still work (reloads) assertEquals( t.get("test.plugin::lang.labels.pluginName"), "Blog", ) } // --- setLocale validation --- test("setLocale rejects path traversal") { val t = makeTranslator() intercept[IllegalArgumentException] { t.setLocale(LocaleTag("../etc/passwd")) } } // --- getLocale / getFallback --- test("getLocale returns current locale") { val t = makeTranslator() assertEquals(t.getLocale.value, "en") t.setLocale(LocaleTag("pl")) assertEquals(t.getLocale.value, "pl") } test("getFallback returns configured fallback") { val t = makeTranslator() assertEquals(t.getFallback.map(_.value), Some("en")) } test("setFallback changes fallback locale") { val t = makeTranslator() t.setFallback(LocaleTag("de")) assertEquals(t.getFallback.map(_.value), Some("de")) } // --- Unknown namespace --- test("unknown namespace returns raw key") { val t = makeTranslator() assertEquals( t.get("unknown.ns::lang.key"), "unknown.ns::lang.key", ) }