Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f5ba099
enable locale config generation
314systems Dec 26, 2025
e1172e6
add AppLocalesMetadataHolderService to manifest
314systems Dec 26, 2025
b672093
refactor LocaleUtils and update settings references
314systems Dec 27, 2025
49d205d
add system_default string to translations
314systems Dec 27, 2025
e17243c
update locale utils and language preference
314systems Dec 27, 2025
353cfa0
refactor locale handling to use AppCompatDelegate for application loc…
314systems Dec 27, 2025
960d7dd
update LocaleUtils and refactor language preference tests
314systems Dec 28, 2025
f6db85d
reorder and update supported locales in LocaleUtils and LocaleUtilsTest
314systems Jan 1, 2026
c83cba2
rename variable
314systems Jan 1, 2026
3fbc740
remove createContext from CompatUtils and CompatUtilsTest
314systems Jan 1, 2026
2dce2a1
add support for Chinese language tags and update tests
314systems Jan 1, 2026
391da3b
update SettingsTest with appLocale and syncLanguage tests
314systems Feb 24, 2026
0da39a1
refactor LocaleUtils to use a getter for countriesLocales
314systems Feb 24, 2026
c72e3e4
refactor LocaleUtils to extract findSupportedLocale method
314systems Feb 24, 2026
afbf046
add tests for locale matching logic in LocaleUtils
314systems May 4, 2026
d70b542
add tests for language preference change handling with application lo…
314systems May 5, 2026
55975dc
register and unregister shared preference change listener in MainActi…
314systems May 5, 2026
e1052a4
Merge branch 'main' into lang
VREMSoftwareDevelopment May 8, 2026
69119d3
sort and include system default in language preference options
314systems May 11, 2026
9ff5379
fix language preference tests
314systems May 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ android {
lint {
lintConfig = file("lint.xml")
}

androidResources {
generateLocaleConfig = true
}
}

allOpen {
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,14 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
Comment thread
314systems marked this conversation as resolved.
</application>
</manifest>
10 changes: 0 additions & 10 deletions app/src/main/kotlin/com/vrem/util/CompatUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,9 @@ package com.vrem.util
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager.PackageInfoFlags
import android.content.res.Configuration
import android.content.res.Resources
import android.net.wifi.ScanResult
import android.os.Build
import androidx.annotation.RequiresApi
import java.util.Locale

fun Context.createContext(newLocale: Locale): Context {
val resources: Resources = resources
val configuration: Configuration = resources.configuration
configuration.setLocale(newLocale)
return createConfigurationContext(configuration)
}

fun Context.packageInfo(): PackageInfo =
if (buildMinVersionT()) {
Expand Down
129 changes: 76 additions & 53 deletions app/src/main/kotlin/com/vrem/util/LocaleUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,80 +20,103 @@ package com.vrem.util
import java.util.Locale
import java.util.SortedMap

private object SyncAvoid {
val defaultLocale: Locale = Locale.getDefault()
val countryCodes: Set<String> = Locale.getISOCountries().toSet()
val availableLocales: List<Locale> = Locale.getAvailableLocales().filter { countryCodes.contains(it.country) }

val countriesLocales: SortedMap<String, Locale> =
private val currentLocale: Locale get() = Locale.getDefault()
private val countryCodes: Set<String> = Locale.getISOCountries().toSet()
private val availableLocales: List<Locale> = Locale.getAvailableLocales().filter { countryCodes.contains(it.country) }
private val countriesLocales: SortedMap<String, Locale>
get() =
availableLocales
.associateBy { it.country.toCapitalize(Locale.getDefault()) }
.associateBy { it.country.toCapitalize(currentLocale) }
.toSortedMap()
Comment on lines +23 to 30
val supportedLocales: List<Locale> =
setOf(
BULGARIAN,
DUTCH,
GREEK,
HUNGARIAN,
Locale.SIMPLIFIED_CHINESE,
Locale.TRADITIONAL_CHINESE,
Locale.ENGLISH,
Locale.FRENCH,
Locale.GERMAN,
Locale.ITALIAN,
Locale.JAPANESE,
POLISH,
PORTUGUESE_BRAZIL,
PORTUGUESE_PORTUGAL,
SPANISH,
RUSSIAN,
TURKISH,
UKRAINIAN,
defaultLocale,
).toList()
}

val BULGARIAN: Locale = Locale.forLanguageTag("bg")
val CHINESE: Locale = Locale.forLanguageTag("zh")
val CHINESE_SIMPLIFIED: Locale = Locale.forLanguageTag("zh-Hans")
val CHINESE_TRADITIONAL: Locale = Locale.forLanguageTag("zh-Hant")
val DUTCH: Locale = Locale.forLanguageTag("nl")
val ENGLISH: Locale = Locale.forLanguageTag("en")
val FRENCH: Locale = Locale.forLanguageTag("fr")
val GERMAN: Locale = Locale.forLanguageTag("de")
val GREEK: Locale = Locale.forLanguageTag("el")
val HUNGARIAN: Locale = Locale.forLanguageTag("hu")
val ITALIAN: Locale = Locale.forLanguageTag("it")
val JAPANESE: Locale = Locale.forLanguageTag("ja")
val POLISH: Locale = Locale.forLanguageTag("pl")
val PORTUGUESE_PORTUGAL: Locale = Locale.forLanguageTag("pt-PT")
val PORTUGUESE_BRAZIL: Locale = Locale.forLanguageTag("pt-BR")
val SPANISH: Locale = Locale.forLanguageTag("es")
val PORTUGUESE_PORTUGAL: Locale = Locale.forLanguageTag("pt-PT")
val RUSSIAN: Locale = Locale.forLanguageTag("ru")
val SPANISH: Locale = Locale.forLanguageTag("es")
val TURKISH: Locale = Locale.forLanguageTag("tr")
val UKRAINIAN: Locale = Locale.forLanguageTag("uk")

private const val SEPARATOR: String = "_"
val baseSupportedLocales: List<Locale> =
listOf(
BULGARIAN,
CHINESE_SIMPLIFIED,
CHINESE_TRADITIONAL,
DUTCH,
ENGLISH,
FRENCH,
GERMAN,
GREEK,
HUNGARIAN,
ITALIAN,
JAPANESE,
POLISH,
PORTUGUESE_BRAZIL,
PORTUGUESE_PORTUGAL,
RUSSIAN,
SPANISH,
TURKISH,
UKRAINIAN,
)

fun findByCountryCode(countryCode: String): Locale =
SyncAvoid.availableLocales.firstOrNull { countryCode.toCapitalize(Locale.getDefault()) == it.country }
?: SyncAvoid.defaultLocale
availableLocales.firstOrNull { countryCode.uppercase(Locale.ROOT) == it.country }
?: currentLocale

fun allCountries(): List<Locale> = SyncAvoid.countriesLocales.values.toList()
fun allCountries(): List<Locale> = countriesLocales.values.toList()

fun findByLanguageTag(languageTag: String): Locale {
val languageTagPredicate: (Locale) -> Boolean = {
val locale: Locale = fromLanguageTag(languageTag)
it.language == locale.language && it.country == locale.country
}
return SyncAvoid.supportedLocales.firstOrNull(languageTagPredicate) ?: SyncAvoid.defaultLocale
}
fun supportedLanguages(): List<Locale> = (baseSupportedLocales + currentLocale).distinct()

fun supportedLanguages(): List<Locale> = SyncAvoid.supportedLocales
fun supportedLanguageTags(): List<String> = listOf("") + baseSupportedLocales.map { it.toLanguageTag() }

fun defaultCountryCode(): String = SyncAvoid.defaultLocale.country
private fun normalizeLanguageTag(languageTag: String): String = languageTag.replace('_', '-').trim()

fun defaultLanguageTag(): String = toLanguageTag(SyncAvoid.defaultLocale)
private val chineseCountryToLocale: Map<String, Locale> =
mapOf(
"CN" to CHINESE_SIMPLIFIED,
"SG" to CHINESE_SIMPLIFIED,
"TW" to CHINESE_TRADITIONAL,
"HK" to CHINESE_TRADITIONAL,
"MO" to CHINESE_TRADITIONAL,
)

fun toLanguageTag(locale: Locale): String = locale.language + SEPARATOR + locale.country
fun findByLanguageTag(languageTag: String): Locale {
val normalizedLanguageTag = normalizeLanguageTag(languageTag)
if (normalizedLanguageTag.isEmpty()) return currentLocale
return findSupportedLocale(Locale.forLanguageTag(normalizedLanguageTag))
}

private fun fromLanguageTag(languageTag: String): Locale {
val codes: Array<String> = languageTag.split(SEPARATOR).toTypedArray()
return when (codes.size) {
1 -> Locale.forLanguageTag(codes[0])
2 -> Locale.forLanguageTag("${codes[0]}-${codes[1].toCapitalize(Locale.getDefault())}")
else -> SyncAvoid.defaultLocale
fun findSupportedLocale(target: Locale): Locale {
if (target.language.isEmpty()) return currentLocale

if (target.language == "zh" && target.script.isEmpty()) {
if (target.country.isEmpty()) return CHINESE
return chineseCountryToLocale[target.country] ?: CHINESE
}

return baseSupportedLocales.find { it == target }
?: baseSupportedLocales.find { it.language == target.language && it.script == target.script }
?: baseSupportedLocales.find { it.language == target.language && it.country == target.country }
?: baseSupportedLocales.find { it.language == target.language }
?: currentLocale
}

fun currentCountryCode(): String = currentLocale.country

fun currentLanguageTag(): String = currentLocale.toLanguageTag()

fun toLanguageTag(locale: Locale): String = locale.toLanguageTag()

fun Locale.toSupportedLocaleTag(): String = findSupportedLocale(this).toLanguageTag()
2 changes: 2 additions & 0 deletions app/src/main/kotlin/com/vrem/util/StringUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ fun String.Companion.nullToEmpty(value: String?): String = value ?: String.EMPTY
fun String.specialTrim(): String = this.trim { it <= ' ' }.replace(" +".toRegex(), String.SPACE_SEPARATOR)

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

fun String.titlecaseFirst(locale: Locale): String = replaceFirstChar { it.titlecase(locale) }
Comment thread
314systems marked this conversation as resolved.
30 changes: 22 additions & 8 deletions app/src/main/kotlin/com/vrem/wifianalyzer/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
*/
package com.vrem.wifianalyzer

import android.content.Context
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.res.Configuration
Expand All @@ -26,18 +25,17 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import com.google.android.material.navigation.NavigationView
import com.vrem.annotation.OpenClass
import com.vrem.util.createContext
import com.vrem.wifianalyzer.navigation.NavigationMenu
import com.vrem.wifianalyzer.navigation.NavigationMenuControl
import com.vrem.wifianalyzer.navigation.NavigationMenuController
import com.vrem.wifianalyzer.navigation.options.OptionMenu
import com.vrem.wifianalyzer.settings.Repository
import com.vrem.wifianalyzer.settings.Settings
import com.vrem.wifianalyzer.wifi.accesspoint.ConnectionView
import com.vrem.wifianalyzer.wifi.scanner.ScannerService

Expand All @@ -52,15 +50,13 @@ class MainActivity :
internal lateinit var optionMenu: OptionMenu
internal lateinit var connectionView: ConnectionView

override fun attachBaseContext(newBase: Context) =
super.attachBaseContext(newBase.createContext(Settings(Repository(newBase)).languageLocale()))

override fun onCreate(savedInstanceState: Bundle?) {
val mainContext = MainContext.INSTANCE
mainContext.initialize(this, largeScreen)

val settings = mainContext.settings
settings.initializeDefaultValues()
settings.syncLanguage()
settings.themeStyle().setTheme(this)
Comment on lines 57 to 60

mainReload = MainReload(settings)
Expand All @@ -69,7 +65,6 @@ class MainActivity :
installSplashScreen()
setContentView(R.layout.main_activity)

settings.registerOnSharedPreferenceChangeListener(this)
optionMenu = OptionMenu()

keepScreenOn()
Expand All @@ -84,9 +79,16 @@ class MainActivity :

connectionView = ConnectionView(this)

settings.registerOnSharedPreferenceChangeListener(this)

onBackPressedDispatcher.addCallback(this, MainActivityBackPressed(this))
}

override fun onDestroy() {
MainContext.INSTANCE.settings.unregisterOnSharedPreferenceChangeListener(this)
super.onDestroy()
}

public override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
drawerNavigation.syncState()
Expand Down Expand Up @@ -120,6 +122,18 @@ class MainActivity :
sharedPreferences: SharedPreferences,
key: String?,
) {
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()

Comment thread
314systems marked this conversation as resolved.
AppCompatDelegate.setApplicationLocales(locales)
}
Comment thread
314systems marked this conversation as resolved.

val mainContext = MainContext.INSTANCE
if (mainReload.shouldReload(mainContext.settings)) {
MainContext.INSTANCE.scannerService.stop()
Expand Down
16 changes: 1 addition & 15 deletions app/src/main/kotlin/com/vrem/wifianalyzer/MainReload.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package com.vrem.wifianalyzer
import com.vrem.wifianalyzer.settings.Settings
import com.vrem.wifianalyzer.settings.ThemeStyle
import com.vrem.wifianalyzer.wifi.accesspoint.ConnectionViewType
import java.util.Locale

class MainReload(
settings: Settings,
Expand All @@ -29,11 +28,8 @@ class MainReload(
private set
var connectionViewType: ConnectionViewType
private set
var languageLocale: Locale
private set

fun shouldReload(settings: Settings): Boolean =
themeChanged(settings) || connectionViewTypeChanged(settings) || languageChanged(settings)
fun shouldReload(settings: Settings): Boolean = themeChanged(settings) || connectionViewTypeChanged(settings)

private fun connectionViewTypeChanged(settings: Settings): Boolean {
val currentConnectionViewType = settings.connectionViewType()
Expand All @@ -53,18 +49,8 @@ class MainReload(
return themeChanged
}

private fun languageChanged(settings: Settings): Boolean {
val settingLanguageLocale = settings.languageLocale()
val languageLocaleChanged = languageLocale != settingLanguageLocale
if (languageLocaleChanged) {
languageLocale = settingLanguageLocale
}
return languageLocaleChanged
}

init {
themeStyle = settings.themeStyle()
connectionViewType = settings.connectionViewType()
languageLocale = settings.languageLocale()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ package com.vrem.wifianalyzer.settings

import android.content.Context
import android.util.AttributeSet
import com.vrem.util.defaultCountryCode
import com.vrem.util.currentCountryCode
import com.vrem.wifianalyzer.MainContext
import com.vrem.wifianalyzer.wifi.band.WiFiChannelCountry
import java.util.Locale

private fun data(): List<Data> {
val currentLocale: Locale = MainContext.INSTANCE.settings.languageLocale()
val currentLocale: Locale = MainContext.INSTANCE.settings.appLocale()
return WiFiChannelCountry
.findAll()
.map { Data(it.countryCode, it.countryName(currentLocale)) }
Expand All @@ -35,4 +35,4 @@ private fun data(): List<Data> {
class CountryPreference(
context: Context,
attrs: AttributeSet,
) : CustomPreference(context, attrs, data(), defaultCountryCode())
) : CustomPreference(context, attrs, data(), currentCountryCode())
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,25 @@ package com.vrem.wifianalyzer.settings

import android.content.Context
import android.util.AttributeSet
import com.vrem.util.defaultLanguageTag
import com.vrem.util.supportedLanguages
import com.vrem.util.toCapitalize
import com.vrem.util.toLanguageTag
import com.vrem.util.supportedLanguageTags
import com.vrem.util.titlecaseFirst
import com.vrem.wifianalyzer.R
import java.util.Locale

private fun data(): List<Data> =
supportedLanguages()
.map { map(it) }
.sorted()
private fun data(context: Context): List<Data> {
val systemDefault = Data("", context.getString(R.string.system_default))
val languages =
supportedLanguageTags()
.filter { it.isNotEmpty() }
.map { tag ->
val locale = Locale.forLanguageTag(tag)
Data(tag, locale.getDisplayName(locale).titlecaseFirst(locale))
}.sortedBy { it.name }

private fun map(it: Locale): Data = Data(toLanguageTag(it), it.getDisplayName(it).toCapitalize(Locale.getDefault()))
return listOf(systemDefault) + languages
}

class LanguagePreference(
context: Context,
attrs: AttributeSet,
) : CustomPreference(context, attrs, data(), defaultLanguageTag())
) : CustomPreference(context, attrs, data(context), "")
Loading
Loading