Skip to content

Conversation

@314systems
Copy link
Contributor

@314systems 314systems commented Dec 29, 2025

Summary

  • Support Per-app language preferences

What does this implement/fix?

  • Support Per-app language preferences.

Does this close any issues?

How was this tested?

  • Devices / OS (e.g. Android 13 emulator, Pixel 6):
  • Platform / Build variant (e.g. debug/release):
  • Toolchain / Gradle / SDK version(s):
  • Steps to reproduce / test (provide exact steps so reviewers can verify):

Example test commands:

./gradlew ktlintCheck

Checklist (required before marking ready)

  • I added or updated unit tests (see app/src/test/)
  • I followed the project's coding style (ktlint) and formatting
  • I ran lint and addressed or documented any warnings
  • CI checks pass (unit tests, coverage, lint)
  • No sensitive data, keys, or secrets are included

Additional context

  • Screenshots, logs, or other context that helps reviewers understand the change.
  • Migration notes or compatibility considerations (if applicable).

@314systems 314systems marked this pull request as ready for review January 1, 2026 09:25
Copilot AI review requested due to automatic review settings January 1, 2026 09:25
Copy link

Copilot AI left a 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()
Copy link

Copilot AI Jan 1, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +27
private val countriesLocales: SortedMap<String, Locale> =
availableLocales
Copy link

Copilot AI Jan 1, 2026

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.

Suggested change
private val countriesLocales: SortedMap<String, Locale> =
availableLocales
private val countriesLocales: SortedMap<String, Locale>
get() = availableLocales

Copilot uses AI. Check for mistakes.
Comment on lines +75 to 80
fun syncLanguage() {
val appLocaleTag = appLocale().toSupportedLocaleTag()
if (appLocaleTag != repository.string(R.string.language_key, "")) {
repository.save(R.string.language_key, appLocaleTag)
}
}
Copy link

Copilot AI Jan 1, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +129
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)
}
Copy link

Copilot AI Jan 1, 2026

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.

Copilot uses AI. Check for mistakes.

fun String.toCapitalize(locale: Locale): String = this.replaceFirstChar { word -> word.uppercase(locale) }

fun String.titlecaseFirst(locale: Locale): String = replaceFirstChar { it.titlecase(locale) }
Copy link

Copilot AI Jan 1, 2026

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.

Copilot uses AI. Check for mistakes.
?.takeIf { it.isNotEmpty() }
?.let(LocaleListCompat::forLanguageTags)
?: LocaleListCompat.getEmptyLocaleList()

Copy link

Copilot AI Jan 1, 2026

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.

Suggested change
// 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.

Copilot uses AI. Check for mistakes.
Comment on lines +75 to 80
fun syncLanguage() {
val appLocaleTag = appLocale().toSupportedLocaleTag()
if (appLocaleTag != repository.string(R.string.language_key, "")) {
repository.save(R.string.language_key, appLocaleTag)
}
}
Copy link

Copilot AI Jan 1, 2026

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Support Per-app language preferences

2 participants