Resolve "EU" through platform locale data instead of hardcoding it
Verified directly (java.util.Locale, NSLocale, and Intl.DisplayNames all tested outside the app) that every platform's locale data resolves "EU" correctly on its own — "European Union" in en, "Union européenne" in fr, "Europäische Union" in de, etc. — via CLDR, which defines "EU" as a grouping in its own right despite it not being a real ISO 3166-1 country code. The hardcoded English fallback was based on an assumption that was never actually verified. Removes the special case from the shared code entirely. That required two platform-side fixes to stop blocking "EU" from ever reaching the platform lookup: - JVM/Android: dropped the Locale.getISOCountries() membership guard (which excluded "EU") in favor of detecting an unresolvable code by getDisplayCountry() echoing it back unchanged. - Web: jsRegionDisplayName() was hardcoding locale 'en' for Intl.DisplayNames — changed to undefined (the browser's own locale), matching what the other platforms already did. Without this fix, "EU" would resolve, but only ever in English on web. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
This commit is contained in:
@@ -38,10 +38,12 @@ actual fun currencyDecimalDigits(code: String): Int = try {
|
|||||||
2
|
2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Doesn't gate on Locale.getISOCountries() — that would incorrectly exclude "EU", which isn't a
|
||||||
|
// real ISO 3166-1 code but which getDisplayCountry() resolves correctly anyway (CLDR defines it
|
||||||
|
// as a grouping in its own right). A genuinely-unresolvable code's display name just echoes the
|
||||||
|
// code back unchanged, so that's the signal used to report "unknown" instead.
|
||||||
@Suppress("DEPRECATION") // Locale(language, country) still works fine; Locale.of() needs newer Android API levels.
|
@Suppress("DEPRECATION") // Locale(language, country) still works fine; Locale.of() needs newer Android API levels.
|
||||||
actual fun regionDisplayName(regionCode: String): String? {
|
actual fun regionDisplayName(regionCode: String): String? =
|
||||||
if (regionCode !in Locale.getISOCountries()) return null
|
Locale("", regionCode).getDisplayCountry(Locale.getDefault()).takeIf { it != regionCode }
|
||||||
return Locale("", regionCode).getDisplayCountry(Locale.getDefault())
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun supportsFlagEmoji(): Boolean = true
|
actual fun supportsFlagEmoji(): Boolean = true
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ fun matchesCurrencySearch(info: CurrencyInfo, query: String): Boolean {
|
|||||||
if (info.code.contains(query, ignoreCase = true)) return true
|
if (info.code.contains(query, ignoreCase = true)) return true
|
||||||
if (info.displayName.contains(query, ignoreCase = true)) return true
|
if (info.displayName.contains(query, ignoreCase = true)) return true
|
||||||
return issuingCountryCodes(info.code).any { regionCode ->
|
return issuingCountryCodes(info.code).any { regionCode ->
|
||||||
localizedRegionName(regionCode)?.contains(query, ignoreCase = true) == true
|
cachedRegionDisplayName(regionCode)?.contains(query, ignoreCase = true) == true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,20 +24,20 @@ fun matchesCurrencySearch(info: CurrencyInfo, query: String): Boolean {
|
|||||||
// being retried every time, which getOrPut would do for a null value.
|
// being retried every time, which getOrPut would do for a null value.
|
||||||
private val regionDisplayNameCache = mutableMapOf<String, String?>()
|
private val regionDisplayNameCache = mutableMapOf<String, String?>()
|
||||||
|
|
||||||
/**
|
private fun cachedRegionDisplayName(regionCode: String): String? {
|
||||||
* A localized display name for ISO 3166-1 alpha-2 [regionCode] (e.g. "Canada"), or null if the
|
|
||||||
* platform doesn't recognize it. "EU" is handled directly since it's not a real ISO 3166-1
|
|
||||||
* country code — it's [issuingCountryCodes]' own stand-in for EUR's region/flag — and platform
|
|
||||||
* locale data doesn't reliably resolve it to a name the way it does real country codes.
|
|
||||||
*/
|
|
||||||
private fun localizedRegionName(regionCode: String): String? {
|
|
||||||
if (regionCode in regionDisplayNameCache) return regionDisplayNameCache[regionCode]
|
if (regionCode in regionDisplayNameCache) return regionDisplayNameCache[regionCode]
|
||||||
val name = if (regionCode == "EU") "European Union" else regionDisplayName(regionCode)
|
val name = regionDisplayName(regionCode)
|
||||||
regionDisplayNameCache[regionCode] = name
|
regionDisplayNameCache[regionCode] = name
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The current platform's localized display name for ISO 3166-1 alpha-2 [regionCode], if known. */
|
/**
|
||||||
|
* The current platform's localized display name for ISO 3166-1 alpha-2 [regionCode], if known.
|
||||||
|
* Also handles "EU" — [issuingCountryCodes]' own stand-in for EUR's region/flag, and not a real
|
||||||
|
* ISO 3166-1 code — since every platform's locale data resolves it correctly regardless (e.g.
|
||||||
|
* "European Union" in en, "Union européenne" in fr): CLDR, which all of their locale data is
|
||||||
|
* ultimately sourced from, defines "EU" as a grouping in its own right.
|
||||||
|
*/
|
||||||
expect fun regionDisplayName(regionCode: String): String?
|
expect fun regionDisplayName(regionCode: String): String?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +52,7 @@ fun warmRegionDisplayNameCache(currencies: List<CurrencyInfo>) {
|
|||||||
currencies.asSequence()
|
currencies.asSequence()
|
||||||
.flatMap { issuingCountryCodes(it.code).asSequence() }
|
.flatMap { issuingCountryCodes(it.code).asSequence() }
|
||||||
.distinct()
|
.distinct()
|
||||||
.forEach { regionCode -> localizedRegionName(regionCode) }
|
.forEach { regionCode -> cachedRegionDisplayName(regionCode) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -51,13 +51,16 @@ class SystemCurrenciesTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun matchesCurrencySearch_matchesEurozoneByHardcodedEuropeanUnionName() {
|
fun matchesCurrencySearch_matchesEurozoneByEuropeanUnionName() {
|
||||||
// "EU" isn't a real ISO 3166-1 country code, so platform locale data can't be relied on
|
// "EU" isn't a real ISO 3166-1 country code, but every platform's locale data resolves it
|
||||||
// to name it — this is hardcoded rather than delegated to [regionDisplayName].
|
// correctly anyway (CLDR defines it as a grouping in its own right) — regionDisplayName()
|
||||||
|
// isn't special-cased for it.
|
||||||
val eur = CurrencyInfo("EUR", "Euro")
|
val eur = CurrencyInfo("EUR", "Euro")
|
||||||
|
val euName = regionDisplayName("EU")
|
||||||
|
assertTrue(euName != null && euName.isNotBlank(), "expected a display name for EU")
|
||||||
|
|
||||||
assertTrue(matchesCurrencySearch(eur, "European"))
|
assertTrue(matchesCurrencySearch(eur, euName))
|
||||||
assertTrue(matchesCurrencySearch(eur, "union"))
|
assertTrue(matchesCurrencySearch(eur, euName.lowercase()))
|
||||||
assertFalse(matchesCurrencySearch(eur, "Germany"))
|
assertFalse(matchesCurrencySearch(eur, "Definitely not a matching country name"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,12 @@ actual fun currencyDecimalDigits(code: String): Int = try {
|
|||||||
2
|
2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Doesn't gate on Locale.getISOCountries() — that would incorrectly exclude "EU", which isn't a
|
||||||
|
// real ISO 3166-1 code but which getDisplayCountry() resolves correctly anyway (CLDR defines it
|
||||||
|
// as a grouping in its own right). A genuinely-unresolvable code's display name just echoes the
|
||||||
|
// code back unchanged, so that's the signal used to report "unknown" instead.
|
||||||
@Suppress("DEPRECATION") // Locale(language, country) still works fine; Locale.of() needs newer Android API levels.
|
@Suppress("DEPRECATION") // Locale(language, country) still works fine; Locale.of() needs newer Android API levels.
|
||||||
actual fun regionDisplayName(regionCode: String): String? {
|
actual fun regionDisplayName(regionCode: String): String? =
|
||||||
if (regionCode !in Locale.getISOCountries()) return null
|
Locale("", regionCode).getDisplayCountry(Locale.getDefault()).takeIf { it != regionCode }
|
||||||
return Locale("", regionCode).getDisplayCountry(Locale.getDefault())
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun supportsFlagEmoji(): Boolean = true
|
actual fun supportsFlagEmoji(): Boolean = true
|
||||||
|
|||||||
@@ -37,13 +37,15 @@ actual fun currencyDecimalDigits(code: String): Int = try {
|
|||||||
2
|
2
|
||||||
}
|
}
|
||||||
|
|
||||||
// Falls back to the region code itself (rather than a JS null/undefined) when unrecognized, since
|
// `undefined` (rather than a fixed locale like 'en') uses the browser's own locale, matching the
|
||||||
// a plain `-> String?` return type doesn't reliably round-trip through js() interop here; that
|
// other platforms' regionDisplayName(). Falls back to the region code itself (rather than a JS
|
||||||
// fallback is filtered back out to null actual-side below, same as the other platforms.
|
// null/undefined) when unrecognized, since a plain `-> String?` return type doesn't reliably
|
||||||
|
// round-trip through js() interop here; that fallback is filtered back out to null actual-side
|
||||||
|
// below, same as the other platforms.
|
||||||
private fun jsRegionDisplayName(regionCode: String): String = js(
|
private fun jsRegionDisplayName(regionCode: String): String = js(
|
||||||
"""(function() {
|
"""(function() {
|
||||||
try {
|
try {
|
||||||
var names = new Intl.DisplayNames(['en'], { type: 'region' });
|
var names = new Intl.DisplayNames(undefined, { type: 'region' });
|
||||||
return names.of(regionCode) || regionCode;
|
return names.of(regionCode) || regionCode;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return regionCode;
|
return regionCode;
|
||||||
|
|||||||
Reference in New Issue
Block a user