-
Notifications
You must be signed in to change notification settings - Fork 710
Support Per-app language preferences #579
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements per-app language preferences by migrating from a custom locale management approach to Android's native AppCompatDelegate API. The changes align with Android's modern language preference system introduced in API 33+, enabling users to select a language specifically for this app independent of the system language.
Key changes:
- Migrated from manual context creation with custom locale to AppCompatDelegate.setApplicationLocales()
- Removed language change tracking from MainReload since AppCompat handles activity recreation automatically
- Updated locale utility functions to use BCP-47 language tags and improved Chinese locale handling with script-based differentiation
Reviewed changes
Copilot reviewed 37 out of 38 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| app/src/main/kotlin/com/vrem/wifianalyzer/MainActivity.kt | Removed attachBaseContext override; added onSharedPreferenceChanged logic to handle language changes via AppCompatDelegate; added syncLanguage call in onCreate |
| app/src/main/kotlin/com/vrem/wifianalyzer/MainReload.kt | Removed language locale tracking since AppCompat handles locale changes automatically |
| app/src/main/kotlin/com/vrem/wifianalyzer/settings/Settings.kt | Replaced languageLocale() with appLocale() using AppCompatDelegate.getApplicationLocales(); added syncLanguage() method |
| app/src/main/kotlin/com/vrem/wifianalyzer/settings/LanguagePreference.kt | Updated to support "System default" option with empty string tag; refactored data() to accept Context |
| app/src/main/kotlin/com/vrem/wifianalyzer/settings/CountryPreference.kt | Updated method call from languageLocale() to appLocale(); renamed utility function reference |
| app/src/main/kotlin/com/vrem/wifianalyzer/wifi/channelavailable/ChannelAvailableFragment.kt | Updated method call from languageLocale() to appLocale() |
| app/src/main/kotlin/com/vrem/wifianalyzer/vendor/model/VendorService.kt | Changed uppercase() to use Locale.ROOT for locale-independent string operations |
| app/src/main/kotlin/com/vrem/util/LocaleUtils.kt | Major refactor: added BCP-47 language tag support, improved Chinese locale handling with script detection, renamed functions for clarity, removed legacy underscore format generation |
| app/src/main/kotlin/com/vrem/util/CompatUtils.kt | Removed createContext() extension function no longer needed with AppCompat |
| app/src/main/kotlin/com/vrem/util/StringUtils.kt | Added titlecaseFirst() extension function for proper title casing of locale display names |
| app/src/main/AndroidManifest.xml | Added AppLocalesMetadataHolderService configuration with autoStoreLocales enabled |
| app/src/main/res/resources.properties | Added unqualifiedResLocale=en-US to specify base locale for resources |
| app/build.gradle | Added generateLocaleConfig=true for automatic locale configuration generation |
| app/src/main/res/values*/strings.xml | Added "system_default" string translations in all supported languages |
| app/src/test/kotlin/**/*Test.kt | Updated tests to reflect API changes: renamed method calls, updated locale handling tests, removed language tracking tests |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| fun toLanguageTag(locale: Locale): String = locale.toLanguageTag() | ||
|
|
||
| fun Locale.toSupportedLocaleTag(): String = findByLanguageTag(this.toLanguageTag()).toLanguageTag() |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The toSupportedLocaleTag() extension function performs a round-trip conversion: it converts the locale to a language tag, calls findByLanguageTag which parses it back to a Locale, and then converts that back to a language tag. This is inefficient and could be simplified. Consider creating a direct method that maps a Locale to its supported equivalent without the intermediate string conversions, or at least add a comment explaining why this approach is necessary if there's a specific reason for it.
| private val countriesLocales: SortedMap<String, Locale> = | ||
| availableLocales |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initialization of countriesLocales on line 28 uses currentLocale which calls Locale.getDefault() at initialization time. This means the map keys will be capitalized using the default locale at app startup. If the user later changes the app's language preference, the map keys will still be in the original locale's capitalization, which could cause inconsistencies in country name display. Consider making countriesLocales a function that computes the map dynamically, or ensure this is the intended behavior and document it.
| private val countriesLocales: SortedMap<String, Locale> = | |
| availableLocales | |
| private val countriesLocales: SortedMap<String, Locale> | |
| get() = availableLocales |
| fun syncLanguage() { | ||
| val appLocaleTag = appLocale().toSupportedLocaleTag() | ||
| if (appLocaleTag != repository.string(R.string.language_key, "")) { | ||
| repository.save(R.string.language_key, appLocaleTag) | ||
| } | ||
| } |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new syncLanguage() method in Settings class lacks test coverage. This method is called during app startup and handles syncing the application locale from AppCompatDelegate back to SharedPreferences. Consider adding a test case that verifies this synchronization behavior, especially since it involves interaction between the Android framework (AppCompatDelegate) and the app's settings storage.
| val languageKey = getString(R.string.language_key) | ||
| if (key == languageKey) { | ||
| val languageTag = sharedPreferences.getString(languageKey, "") | ||
| val locales = | ||
| languageTag | ||
| ?.takeIf { it.isNotEmpty() } | ||
| ?.let(LocaleListCompat::forLanguageTags) | ||
| ?: LocaleListCompat.getEmptyLocaleList() | ||
|
|
||
| AppCompatDelegate.setApplicationLocales(locales) | ||
| } |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new language preference change handling logic in onSharedPreferenceChanged (lines 119-129) lacks test coverage. This is critical functionality that calls AppCompatDelegate.setApplicationLocales() when the language preference changes. Consider adding test cases that verify the locale is correctly set when the language_key preference changes, including edge cases like empty strings and null values.
|
|
||
| fun String.toCapitalize(locale: Locale): String = this.replaceFirstChar { word -> word.uppercase(locale) } | ||
|
|
||
| fun String.titlecaseFirst(locale: Locale): String = replaceFirstChar { it.titlecase(locale) } |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new titlecaseFirst() extension function in StringUtils.kt lacks test coverage. This function is used to capitalize language display names in the language preference list. Consider adding test cases to verify its behavior with various inputs, including edge cases like empty strings, strings with different Unicode characters, and strings in different locales to ensure proper title casing.
| ?.takeIf { it.isNotEmpty() } | ||
| ?.let(LocaleListCompat::forLanguageTags) | ||
| ?: LocaleListCompat.getEmptyLocaleList() | ||
|
|
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The language preference change handling logic could benefit from a comment explaining the flow. Specifically, it would be helpful to document that when the language preference is changed, AppCompatDelegate.setApplicationLocales() is called, which triggers a configuration change and activity recreation, so the syncLanguage() call in onCreate will sync the preference value back to shared preferences after the recreation.
| // When the language preference changes, update the application locales. | |
| // This call triggers a configuration change and causes the activity to be recreated. | |
| // After recreation, onCreate() is invoked again and settings.syncLanguage() is called, | |
| // which syncs the effective language value back to shared preferences. |
| fun syncLanguage() { | ||
| val appLocaleTag = appLocale().toSupportedLocaleTag() | ||
| if (appLocaleTag != repository.string(R.string.language_key, "")) { | ||
| repository.save(R.string.language_key, appLocaleTag) | ||
| } | ||
| } |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The syncLanguage method could create an unnecessary write to SharedPreferences on every app startup when the locale hasn't changed. Consider adding a check to only save if the value in preferences is different from the current appLocaleTag. The current implementation compares against an empty string default, which means if the preference is not set (null), it will still match an empty string and not save, but if the preference was previously set to a value that matches appLocaleTag, it will still perform the comparison on every startup. Consider optimizing by reading the current value first before saving.
Summary
What does this implement/fix?
Does this close any issues?
How was this tested?
Example test commands:
Checklist (required before marking ready)
app/src/test/)Additional context