Skip to content

Commit 2df9d10

Browse files
Anty0claude
andcommitted
fix: case-insensitive locale tag comparison in resolveLocale
Locale tags with region or script (e.g. zh-Hans, pt-BR) failed to match because Locale.equals() compares fields case-sensitively. Use lowercase tag-based lookup instead, preserving the original locale casing for CDN file paths. Closes #12 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b6d4e7b commit 2df9d10

File tree

3 files changed

+105
-4
lines changed

3 files changed

+105
-4
lines changed

core/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ kotlin {
216216
implementation(libs.coroutines)
217217
}
218218

219+
commonTest.dependencies {
220+
implementation(kotlin("test"))
221+
}
222+
219223
androidMain.dependencies {
220224
implementation(libs.android)
221225

core/src/commonMain/kotlin/io/tolgee/Tolgee.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,19 @@ open class Tolgee(
236236
return locale
237237
}
238238

239+
// Build a map keyed by lowercase tag for case-insensitive matching.
240+
// Returns the original available locale to preserve the exact casing
241+
// needed for CDN file paths.
242+
val localesByTag = availableLocales.associateBy { it.toTag("-").lowercase() }
243+
239244
// Generate progressive fallback variations
240245
val fallbackCandidates = generateLocaleFallbacks(locale)
241246

242-
// Try each fallback candidate in order
247+
// Try each fallback candidate in order (case-insensitive)
243248
for (candidate in fallbackCandidates) {
244-
if (candidate in availableLocales) {
245-
return candidate
249+
val matchedLocale = localesByTag[candidate.toTag("-").lowercase()]
250+
if (matchedLocale != null) {
251+
return matchedLocale
246252
}
247253
}
248254

@@ -457,7 +463,8 @@ open class Tolgee(
457463
?: emptyList()
458464

459465
val currentLocale = resolveLocale(localeFlow.value)
460-
val otherLocales = availableLocales.filter { it != currentLocale }
466+
val currentLocaleTag = currentLocale?.toTag("-")?.lowercase()
467+
val otherLocales = availableLocales.filter { it.toTag("-").lowercase() != currentLocaleTag }
461468

462469
otherLocales.forEach { locale ->
463470
suspendCatching {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package io.tolgee
2+
3+
import de.comahe.i18n4k.Locale
4+
import de.comahe.i18n4k.forLocaleTag
5+
import de.comahe.i18n4k.toTag
6+
import kotlin.test.Test
7+
import kotlin.test.assertEquals
8+
import kotlin.test.assertNull
9+
10+
class ResolveLocaleTest {
11+
12+
/**
13+
* Testable subclass that exposes resolveLocale for testing.
14+
*/
15+
private class TestableTolgee(config: Tolgee.Config) : Tolgee(config) {
16+
fun testResolveLocale(locale: Locale?): Locale? = resolveLocale(locale)
17+
}
18+
19+
private fun createTolgee(
20+
availableLocales: List<Locale>,
21+
defaultLanguage: Locale? = null,
22+
): TestableTolgee {
23+
val config = Tolgee.Config.Builder()
24+
.availableLocales(availableLocales)
25+
.apply { defaultLanguage?.let { defaultLanguage(it) } }
26+
.build()
27+
return TestableTolgee(config)
28+
}
29+
30+
@Test
31+
fun exactMatchWorks() {
32+
val tolgee = createTolgee(listOf(forLocaleTag("en"), forLocaleTag("zh-Hans")))
33+
val result = tolgee.testResolveLocale(forLocaleTag("zh-Hans"))
34+
assertEquals("zh-Hans", result?.toTag("-"))
35+
}
36+
37+
@Test
38+
fun caseInsensitiveMatchForScript() {
39+
val tolgee = createTolgee(listOf(forLocaleTag("en"), forLocaleTag("zh-Hans")))
40+
val result = tolgee.testResolveLocale(forLocaleTag("zh-hans"))
41+
assertEquals("zh-Hans", result?.toTag("-"))
42+
}
43+
44+
@Test
45+
fun caseInsensitiveMatchForRegion() {
46+
val tolgee = createTolgee(listOf(forLocaleTag("en"), forLocaleTag("pt-BR")))
47+
val result = tolgee.testResolveLocale(forLocaleTag("pt-br"))
48+
assertEquals("pt-BR", result?.toTag("-"))
49+
}
50+
51+
@Test
52+
fun fallbackFromRegionToBaseLanguage() {
53+
val tolgee = createTolgee(listOf(forLocaleTag("en"), forLocaleTag("pt")))
54+
val result = tolgee.testResolveLocale(forLocaleTag("pt-BR"))
55+
assertEquals("pt", result?.toTag("-"))
56+
}
57+
58+
@Test
59+
fun fallbackFromScriptRegionToScript() {
60+
val tolgee = createTolgee(listOf(forLocaleTag("en"), forLocaleTag("zh-Hans")))
61+
val result = tolgee.testResolveLocale(forLocaleTag("zh-Hans-CN"))
62+
assertEquals("zh-Hans", result?.toTag("-"))
63+
}
64+
65+
@Test
66+
fun fallsBackToDefaultLanguageWhenNoMatch() {
67+
val defaultLang = forLocaleTag("en")
68+
val tolgee = createTolgee(
69+
listOf(forLocaleTag("en"), forLocaleTag("fr")),
70+
defaultLanguage = defaultLang,
71+
)
72+
val result = tolgee.testResolveLocale(forLocaleTag("ja"))
73+
assertEquals("en", result?.toTag("-"))
74+
}
75+
76+
@Test
77+
fun nullLocaleReturnsNull() {
78+
val tolgee = createTolgee(listOf(forLocaleTag("en")))
79+
assertNull(tolgee.testResolveLocale(null))
80+
}
81+
82+
@Test
83+
fun returnsInputLocaleWhenNoAvailableLocales() {
84+
val config = Tolgee.Config.Builder().build()
85+
val tolgee = TestableTolgee(config)
86+
val locale = forLocaleTag("zh-Hans")
87+
val result = tolgee.testResolveLocale(locale)
88+
assertEquals("zh-Hans", result?.toTag("-"))
89+
}
90+
}

0 commit comments

Comments
 (0)