Files
summer-phrasebook/src/test/scala/summer/phrasebook/PluralRulesSpec.scala
Jakub Zych 8684d2fb7b Implement Phase 1 core translation engine (95 tests passing)
Custom i18n module with HOCON translation files, CLDR-aware pluralization
for 40+ locales, namespace::group.item key resolution, :name/:Name/:NAME
parameter interpolation, and locale fallback chains. No effect system deps.
2026-02-22 23:30:16 +01:00

214 lines
9.5 KiB
Scala

package summer.phrasebook
class PluralRulesSpec extends munit.FunSuite:
// --- getPluralIndex tests ---
test("English: 2 forms (singular/plural)") {
assertEquals(PluralRules.getPluralIndex("en", 0), 1)
assertEquals(PluralRules.getPluralIndex("en", 1), 0)
assertEquals(PluralRules.getPluralIndex("en", 2), 1)
assertEquals(PluralRules.getPluralIndex("en", 100), 1)
}
test("French: 0 and 1 are singular") {
assertEquals(PluralRules.getPluralIndex("fr", 0), 0)
assertEquals(PluralRules.getPluralIndex("fr", 1), 0)
assertEquals(PluralRules.getPluralIndex("fr", 2), 1)
assertEquals(PluralRules.getPluralIndex("fr", 5), 1)
}
test("Polish: 3 forms (one/few/many)") {
assertEquals(PluralRules.getPluralIndex("pl", 1), 0) // one
assertEquals(PluralRules.getPluralIndex("pl", 2), 1) // few
assertEquals(PluralRules.getPluralIndex("pl", 3), 1) // few
assertEquals(PluralRules.getPluralIndex("pl", 4), 1) // few
assertEquals(PluralRules.getPluralIndex("pl", 5), 2) // many
assertEquals(PluralRules.getPluralIndex("pl", 10), 2) // many
assertEquals(PluralRules.getPluralIndex("pl", 11), 2) // many
assertEquals(PluralRules.getPluralIndex("pl", 12), 2) // many (12-14 are exceptions)
assertEquals(PluralRules.getPluralIndex("pl", 13), 2) // many
assertEquals(PluralRules.getPluralIndex("pl", 14), 2) // many
assertEquals(PluralRules.getPluralIndex("pl", 22), 1) // few
assertEquals(PluralRules.getPluralIndex("pl", 23), 1) // few
assertEquals(PluralRules.getPluralIndex("pl", 24), 1) // few
assertEquals(PluralRules.getPluralIndex("pl", 25), 2) // many
assertEquals(PluralRules.getPluralIndex("pl", 100), 2) // many
assertEquals(PluralRules.getPluralIndex("pl", 112), 2) // many (n%100=12, exception)
}
test("Russian: 3 forms (Slavic rule)") {
assertEquals(PluralRules.getPluralIndex("ru", 1), 0) // one
assertEquals(PluralRules.getPluralIndex("ru", 2), 1) // few
assertEquals(PluralRules.getPluralIndex("ru", 5), 2) // many
assertEquals(PluralRules.getPluralIndex("ru", 11), 2) // many (exception: 11-19)
assertEquals(PluralRules.getPluralIndex("ru", 21), 0) // one
assertEquals(PluralRules.getPluralIndex("ru", 22), 1) // few
assertEquals(PluralRules.getPluralIndex("ru", 25), 2) // many
assertEquals(PluralRules.getPluralIndex("ru", 111), 2) // many (n%100=11)
assertEquals(PluralRules.getPluralIndex("ru", 121), 0) // one (n%10=1, n%100=21)
}
test("Czech: 3 forms") {
assertEquals(PluralRules.getPluralIndex("cs", 1), 0)
assertEquals(PluralRules.getPluralIndex("cs", 2), 1)
assertEquals(PluralRules.getPluralIndex("cs", 4), 1)
assertEquals(PluralRules.getPluralIndex("cs", 5), 2)
assertEquals(PluralRules.getPluralIndex("cs", 100), 2)
}
test("Japanese: no plural (always 0)") {
assertEquals(PluralRules.getPluralIndex("ja", 0), 0)
assertEquals(PluralRules.getPluralIndex("ja", 1), 0)
assertEquals(PluralRules.getPluralIndex("ja", 100), 0)
}
test("Chinese: no plural (always 0)") {
assertEquals(PluralRules.getPluralIndex("zh", 0), 0)
assertEquals(PluralRules.getPluralIndex("zh", 1), 0)
assertEquals(PluralRules.getPluralIndex("zh", 42), 0)
}
test("Arabic: 6 forms") {
assertEquals(PluralRules.getPluralIndex("ar", 0), 0) // zero
assertEquals(PluralRules.getPluralIndex("ar", 1), 1) // one
assertEquals(PluralRules.getPluralIndex("ar", 2), 2) // two
assertEquals(PluralRules.getPluralIndex("ar", 3), 3) // few (3-10)
assertEquals(PluralRules.getPluralIndex("ar", 10), 3) // few
assertEquals(PluralRules.getPluralIndex("ar", 11), 4) // many (11-99)
assertEquals(PluralRules.getPluralIndex("ar", 99), 4) // many
assertEquals(PluralRules.getPluralIndex("ar", 100), 5) // other (100+)
assertEquals(PluralRules.getPluralIndex("ar", 1000), 5) // other
}
test("Slovenian: 4 forms") {
assertEquals(PluralRules.getPluralIndex("sl", 1), 0) // n%100==1
assertEquals(PluralRules.getPluralIndex("sl", 2), 1) // n%100==2
assertEquals(PluralRules.getPluralIndex("sl", 3), 2) // n%100==3 or 4
assertEquals(PluralRules.getPluralIndex("sl", 4), 2)
assertEquals(PluralRules.getPluralIndex("sl", 5), 3) // other
assertEquals(PluralRules.getPluralIndex("sl", 101), 0) // n%100==1
assertEquals(PluralRules.getPluralIndex("sl", 102), 1) // n%100==2
}
test("Welsh: 4 forms") {
assertEquals(PluralRules.getPluralIndex("cy", 1), 0)
assertEquals(PluralRules.getPluralIndex("cy", 2), 1)
assertEquals(PluralRules.getPluralIndex("cy", 8), 2)
assertEquals(PluralRules.getPluralIndex("cy", 11), 2)
assertEquals(PluralRules.getPluralIndex("cy", 5), 3)
}
test("Maltese: 4 forms") {
assertEquals(PluralRules.getPluralIndex("mt", 1), 0) // one
assertEquals(PluralRules.getPluralIndex("mt", 0), 1) // few (0, or n%100 2-10)
assertEquals(PluralRules.getPluralIndex("mt", 2), 1) // few
assertEquals(PluralRules.getPluralIndex("mt", 10), 1) // few
assertEquals(PluralRules.getPluralIndex("mt", 11), 2) // many (n%100 11-19)
assertEquals(PluralRules.getPluralIndex("mt", 19), 2) // many
assertEquals(PluralRules.getPluralIndex("mt", 20), 3) // other
}
test("Lithuanian: 3 forms") {
assertEquals(PluralRules.getPluralIndex("lt", 1), 0) // one (n%10==1, not 11)
assertEquals(PluralRules.getPluralIndex("lt", 2), 1) // few (n%10>=2, n%100<10 or >=20)
assertEquals(PluralRules.getPluralIndex("lt", 10), 2) // other
assertEquals(PluralRules.getPluralIndex("lt", 11), 2) // exception
assertEquals(PluralRules.getPluralIndex("lt", 21), 0) // one
assertEquals(PluralRules.getPluralIndex("lt", 12), 2) // exception (n%100 in 10-19)
}
test("Latvian: 3 forms") {
assertEquals(PluralRules.getPluralIndex("lv", 0), 0) // zero
assertEquals(PluralRules.getPluralIndex("lv", 1), 1) // one (n%10==1, not 11)
assertEquals(PluralRules.getPluralIndex("lv", 2), 2) // other
assertEquals(PluralRules.getPluralIndex("lv", 11), 2) // other (exception)
assertEquals(PluralRules.getPluralIndex("lv", 21), 1) // one
}
test("Romanian: 3 forms") {
assertEquals(PluralRules.getPluralIndex("ro", 1), 0) // one
assertEquals(PluralRules.getPluralIndex("ro", 0), 1) // few (0 or n%100 1-19)
assertEquals(PluralRules.getPluralIndex("ro", 19), 1) // few
assertEquals(PluralRules.getPluralIndex("ro", 20), 2) // other
assertEquals(PluralRules.getPluralIndex("ro", 100), 2) // other
}
test("Macedonian: last digit rule") {
assertEquals(PluralRules.getPluralIndex("mk", 1), 0) // n%10==1
assertEquals(PluralRules.getPluralIndex("mk", 2), 1)
assertEquals(PluralRules.getPluralIndex("mk", 11), 0) // n%10==1
assertEquals(PluralRules.getPluralIndex("mk", 21), 0) // n%10==1
}
test("Irish: 3 forms") {
assertEquals(PluralRules.getPluralIndex("ga", 1), 0)
assertEquals(PluralRules.getPluralIndex("ga", 2), 1)
assertEquals(PluralRules.getPluralIndex("ga", 3), 2)
}
test("locale with region uses language portion") {
// pt_BR should use "pt" rules (2 forms)
assertEquals(PluralRules.getPluralIndex("pt_BR", 1), 0)
assertEquals(PluralRules.getPluralIndex("pt_BR", 2), 1)
}
test("unknown locale returns 0") {
assertEquals(PluralRules.getPluralIndex("xx", 5), 0)
}
test("negative numbers use absolute value") {
assertEquals(PluralRules.getPluralIndex("en", -1), 0)
assertEquals(PluralRules.getPluralIndex("en", -5), 1)
}
// --- choose() tests ---
test("choose with simple English plural") {
val en = LocaleTag("en")
assertEquals(PluralRules.choose("item|items", 1, en), "item")
assertEquals(PluralRules.choose("item|items", 0, en), "items")
assertEquals(PluralRules.choose("item|items", 5, en), "items")
}
test("choose with explicit conditions") {
val en = LocaleTag("en")
assertEquals(PluralRules.choose("{0} No items|{1} One item|[2,*] Many items", 0, en), "No items")
assertEquals(PluralRules.choose("{0} No items|{1} One item|[2,*] Many items", 1, en), "One item")
assertEquals(PluralRules.choose("{0} No items|{1} One item|[2,*] Many items", 5, en), "Many items")
assertEquals(PluralRules.choose("{0} No items|{1} One item|[2,*] Many items", 100, en), "Many items")
}
test("choose with explicit range") {
val en = LocaleTag("en")
assertEquals(PluralRules.choose("[1,3] Few|[4,*] Many", 2, en), "Few")
assertEquals(PluralRules.choose("[1,3] Few|[4,*] Many", 4, en), "Many")
}
test("choose with Polish 3 forms") {
val pl = LocaleTag("pl")
val line = "wpis|wpisy|wpisów"
assertEquals(PluralRules.choose(line, 1, pl), "wpis")
assertEquals(PluralRules.choose(line, 2, pl), "wpisy")
assertEquals(PluralRules.choose(line, 5, pl), "wpisów")
assertEquals(PluralRules.choose(line, 22, pl), "wpisy")
assertEquals(PluralRules.choose(line, 12, pl), "wpisów")
}
test("choose with Arabic 6 forms") {
val ar = LocaleTag("ar")
val line = "zero|one|two|few|many|other"
assertEquals(PluralRules.choose(line, 0, ar), "zero")
assertEquals(PluralRules.choose(line, 1, ar), "one")
assertEquals(PluralRules.choose(line, 2, ar), "two")
assertEquals(PluralRules.choose(line, 5, ar), "few")
assertEquals(PluralRules.choose(line, 50, ar), "many")
assertEquals(PluralRules.choose(line, 100, ar), "other")
}
test("choose falls back to last segment if index out of bounds") {
val en = LocaleTag("en")
// Only 1 segment but English wants index 1 for n!=1
assertEquals(PluralRules.choose("only one form", 5, en), "only one form")
}