From 926600ed3d05ad9069d43399a1a98da5dd581537 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 28 Jun 2026 19:43:43 +0200 Subject: [PATCH] refactor(locale): draw per-app language from floret-kit core-locale Replace the app-local AppLanguage with the shared de.jeanlucmakiola.floret.locale.AppLanguage. The kit version is identical except supportedTags() takes the locales_config XML res id as a parameter (the kit can't reference the app's R), so the call site now passes R.xml.locales_config. Wire the core-locale module into the app. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/build.gradle.kts | 1 + .../calendula/ui/settings/AppLanguage.kt | 78 ------------------- .../calendula/ui/settings/SettingsScreen.kt | 3 +- 3 files changed, 3 insertions(+), 79 deletions(-) delete mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/AppLanguage.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 29669c5..0c97368 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -160,6 +160,7 @@ dependencies { implementation(libs.kotlinx.datetime) implementation("de.jeanlucmakiola.floret:core-time") + implementation("de.jeanlucmakiola.floret:core-locale") implementation(libs.kotlinx.coroutines.core) debugImplementation(libs.androidx.ui.tooling) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/AppLanguage.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/AppLanguage.kt deleted file mode 100644 index 26e59d5..0000000 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/AppLanguage.kt +++ /dev/null @@ -1,78 +0,0 @@ -package de.jeanlucmakiola.calendula.ui.settings - -import android.content.Context -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.os.LocaleListCompat -import de.jeanlucmakiola.calendula.R -import org.xmlpull.v1.XmlPullParser -import java.util.Locale - -private const val ANDROID_NS = "http://schemas.android.com/apk/res/android" - -/** - * Per-app language via AppCompatDelegate, driven by res/xml/locales_config.xml. - * - * That file is the single source of truth for which languages we ship: dropping - * in a values- translation and adding a matching `` entry makes the - * language show up here and in the system per-app-language settings, with no - * other code change. The system-default choice is represented as `null`. - * - * On API 33+ this delegates to the platform per-app-languages API; below that - * the appcompat backport persists the choice itself (manifest `autoStoreLocales` - * service), so we don't mirror it in DataStore. Setting a locale recreates the - * activity, which re-reads the current value for the picker. - */ -object AppLanguage { - - /** - * The BCP-47 tags the app ships translations for, in declaration order, as - * listed in locales_config.xml. Returns whatever could be parsed; a missing - * or malformed config yields an empty list (the picker then offers only the - * system-default entry rather than crashing). - */ - fun supportedTags(context: Context): List { - val tags = mutableListOf() - val parser = context.resources.getXml(R.xml.locales_config) - try { - var event = parser.eventType - while (event != XmlPullParser.END_DOCUMENT) { - if (event == XmlPullParser.START_TAG && parser.name == "locale") { - parser.getAttributeValue(ANDROID_NS, "name")?.let(tags::add) - } - event = parser.next() - } - } catch (_: Exception) { - // Fall back to whatever was parsed before the failure. - } finally { - parser.close() - } - return tags - } - - /** The applied app language as a BCP-47 tag, or `null` when following the system. */ - fun currentTag(): String? { - val locales = AppCompatDelegate.getApplicationLocales() - return if (locales.isEmpty) null else locales[0]?.toLanguageTag() - } - - /** Apply a BCP-47 tag, or `null` to follow the system languages. */ - fun apply(tag: String?) { - val locales = if (tag == null) { - LocaleListCompat.getEmptyLocaleList() - } else { - LocaleListCompat.forLanguageTags(tag) - } - AppCompatDelegate.setApplicationLocales(locales) - } - - /** - * The autonym for a tag — the language's own name in its own script, e.g. - * "Deutsch", "English", "Français" — so users find their language regardless - * of the current UI language. Capitalised per the language's own rules. - */ - fun displayName(tag: String): String { - val locale = Locale.forLanguageTag(tag) - return locale.getDisplayName(locale) - .replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() } - } -} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index fc1447e..83f52cf 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -50,6 +50,7 @@ import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.Translate import androidx.compose.material.icons.filled.Tune +import de.jeanlucmakiola.floret.locale.AppLanguage import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -298,7 +299,7 @@ private fun LanguageRow(position: Position) { var showDialog by remember { mutableStateOf(false) } // null = follow the system; the rest are BCP-47 tags from locales_config.xml. - val options = remember { listOf(null) + AppLanguage.supportedTags(context) } + val options = remember { listOf(null) + AppLanguage.supportedTags(context, R.xml.locales_config) } GroupedRow( title = stringResource(R.string.settings_language),