Resolve the applied locale back onto a shipped tag
The per-app-language store re-parses the applied locale through ICU and fills in the implied script, so a zh-CN choice reads back as zh-Hans-CN and no longer matches the tag the picker offers — the selected row lost its mark after a restart. currentTag(supported) maps it back.
This commit is contained in:
@@ -122,10 +122,11 @@ fun LanguagePickerRow(
|
||||
leading: @Composable () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var current by remember { mutableStateOf(AppLanguage.currentTag()) }
|
||||
val supported = remember { AppLanguage.supportedTags(context, localesConfig) }
|
||||
var current by remember { mutableStateOf(AppLanguage.currentTag(supported)) }
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
// null = follow the system; the rest are BCP-47 tags from locales_config.xml.
|
||||
val options = remember { listOf<String?>(null) + AppLanguage.supportedTags(context, localesConfig) }
|
||||
val options = remember(supported) { listOf<String?>(null) + supported }
|
||||
val label: (String?) -> String = { tag -> tag?.let { AppLanguage.displayName(it) } ?: autoLabel }
|
||||
|
||||
GroupedRow(
|
||||
|
||||
@@ -57,6 +57,35 @@ object AppLanguage {
|
||||
return if (locales.isEmpty) null else locales[0]?.toLanguageTag()
|
||||
}
|
||||
|
||||
/**
|
||||
* The applied app language as one of [supported], or `null` when following
|
||||
* the system.
|
||||
*
|
||||
* Prefer this over [currentTag] whenever the result is compared against
|
||||
* [supportedTags]: the platform store re-parses the applied locale through
|
||||
* ICU and fills in the implied script, so a "zh-CN" choice reads back as
|
||||
* "zh-Hans-CN" and no longer string-matches the tag the picker offers.
|
||||
*/
|
||||
fun currentTag(supported: List<String>): String? = resolveTag(currentTag(), supported)
|
||||
|
||||
/**
|
||||
* Maps [tag] onto the entry of [supported] naming the same language, or
|
||||
* returns it unchanged when nothing matches. Matching widens in steps —
|
||||
* exact tag, then language plus region, then language alone — so a more
|
||||
* specific entry always wins over a looser one.
|
||||
*/
|
||||
fun resolveTag(tag: String?, supported: List<String>): String? {
|
||||
if (tag == null) return null
|
||||
val locale = Locale.forLanguageTag(tag)
|
||||
return supported.firstOrNull { it == tag }
|
||||
?: supported.firstOrNull {
|
||||
val other = Locale.forLanguageTag(it)
|
||||
other.language == locale.language && other.country == locale.country
|
||||
}
|
||||
?: supported.firstOrNull { Locale.forLanguageTag(it).language == locale.language }
|
||||
?: tag
|
||||
}
|
||||
|
||||
/** Apply a BCP-47 tag, or `null` to follow the system languages. */
|
||||
fun apply(tag: String?) {
|
||||
val locales = if (tag == null) {
|
||||
|
||||
@@ -17,4 +17,44 @@ class AppLanguageTest {
|
||||
// them title-cased so each language reads as a proper name.
|
||||
assertThat(AppLanguage.displayName("fr")).isEqualTo("Français")
|
||||
}
|
||||
|
||||
private val supported = listOf("en", "ar", "de", "es", "fr", "it", "pl", "pt-BR", "zh-CN")
|
||||
|
||||
@Test
|
||||
fun `a script the platform filled in still resolves to the shipped tag`() {
|
||||
// The per-app-language store re-parses "zh-CN" through ICU likely
|
||||
// subtags and hands it back as "zh-Hans-CN"; the picker only has
|
||||
// "zh-CN" to offer, so without this the selected row loses its mark.
|
||||
assertThat(AppLanguage.resolveTag("zh-Hans-CN", supported)).isEqualTo("zh-CN")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tags that round-trip unchanged are returned as they are`() {
|
||||
assertThat(AppLanguage.resolveTag("pt-BR", supported)).isEqualTo("pt-BR")
|
||||
assertThat(AppLanguage.resolveTag("de", supported)).isEqualTo("de")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a region the app does not distinguish falls back to the language`() {
|
||||
assertThat(AppLanguage.resolveTag("en-US", supported)).isEqualTo("en")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an exact match wins over a looser one`() {
|
||||
// "pt" and "pt-BR" both name Portuguese; the regional entry must not
|
||||
// swallow the plain one just by being listed first.
|
||||
val both = listOf("pt-BR", "pt")
|
||||
assertThat(AppLanguage.resolveTag("pt", both)).isEqualTo("pt")
|
||||
assertThat(AppLanguage.resolveTag("pt-BR", both)).isEqualTo("pt-BR")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unshipped language is left alone`() {
|
||||
assertThat(AppLanguage.resolveTag("ja", supported)).isEqualTo("ja")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following the system stays null`() {
|
||||
assertThat(AppLanguage.resolveTag(null, supported)).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user