How Multi-Language Localization Is Implemented with LanguageManager in GSY GitHub App Compose
The GSY GitHub App Compose implements multi-language localization through a three-tier architecture using LanguageManager as a façade that coordinates AppLanguage enum definitions, LanguageDataStore persistence, and locale-aware Context wrappers for both Compose and non-Compose code.
The carguo/gsygithubappcompose repository demonstrates a production-ready approach to multi-language localization in modern Android development. By combining Jetpack DataStore for persistence, Kotlin Flow for reactive updates, and strategic Context wrapping, the implementation ensures seamless language switching throughout the application lifecycle. This architecture decouples language state management from UI components while supporting both synchronous access for early startup and asynchronous flows for reactive recompositions.
Core Architecture Components
The localization pipeline rests on three foundational components that handle definition, persistence, and runtime management.
AppLanguage Enum Definition
The AppLanguage enum in core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/datastore/AppLanguage.kt defines the supported language codes. It includes three variants: SYSTEM (follows device settings), CHINESE, and ENGLISH. This enum serves as the single source of truth for valid language states throughout the application.
LanguageDataStore Persistence
LanguageDataStore handles disk persistence using Jetpack DataStore. Located in core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/datastore/LanguageDataStore.kt, it creates a DataStore<Preferences> named language_preferences and maps the string key APP_LANGUAGE to AppLanguage instances. The class exposes appLanguage as a Flow<AppLanguage>, enabling reactive observation of language changes across the app.
LanguageManager Façade
LanguageManager, implemented in core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/manager/LanguageManager.kt, acts as the central coordinator. It maintains an in-memory cache (inMemoryLanguage) for synchronous access and provides three critical functions: setAppLanguage() for updates, getAppLanguageSync() for blocking reads, and appLanguageToLocale() for converting enum values to java.util.Locale objects.
Synchronous and Asynchronous Language Access
The implementation supports both reactive streams and immediate access patterns required by different Android lifecycle stages.
Flow-Based Observation for UI
LanguageManager exposes appLanguage as a public Flow<AppLanguage> that UI layers collect for reactive updates. When setAppLanguage() is called, it writes to LanguageDataStore and updates the in-memory cache simultaneously, causing the flow to emit the new value.
// In MainActivity.kt
val appLanguage by languageManager.appLanguage.collectAsState(languageManager.getAppLanguageSync())
val currentLocale = remember(appLanguage) {
languageManager.appLanguageToLocale(appLanguage)
}
Blocking Reads for Early Startup
During attachBaseContext or the initial Compose frame, synchronous access is required. LanguageManager.getAppLanguageSync() first checks the in-memory cache; if empty, it blocks on languageDataStore.appLanguage.first() using runBlocking to retrieve the persisted value without launching a coroutine scope.
Locale-Aware Resource Provision
Providing localized resources requires wrapping the Android Context to inject the correct Locale configuration.
StringResourceProvider for Non-Compose Code
For ViewModels, Services, or other non-Compose components, StringResourceProviderImpl in core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/util/StringResourceProvider.kt builds a locale-aware Context on-demand:
private fun getLocalizedContext(): Context {
val currentLocale = languageManager.appLanguageToLocale(languageManager.getAppLanguageSync())
val configuration = Configuration(context.resources.configuration)
configuration.setLocale(currentLocale)
return context.createConfigurationContext(configuration)
}
All getString() calls delegate to this wrapped Context, ensuring that background operations and notification builders receive properly localized strings.
ProvideLocalizedResources for Compose UI
For Jetpack Compose, ProvideLocalizedResources in core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/util/LocalizedResources.kt supplies a wrapped Context via LocalizedContextWrapper:
@Composable
fun ProvideLocalizedResources(
locale: Locale,
content: @Composable () -> Unit
) {
val context = LocalContext.current
val localizedContext = remember(context, locale) {
LocalizedContextWrapper(context, locale)
}
CompositionLocalProvider(
LocalContext provides localizedContext,
LocalAppLocale provides locale,
content = content
)
}
The wrapper preserves the original activity type to maintain Hilt ViewModel resolution while overriding locale configuration.
End-to-End Implementation Flow
The complete data flow demonstrates how user interaction propagates through the system:
- User selects a language in the UI, triggering
languageManager.setAppLanguage(AppLanguage.ENGLISH). LanguageManagerupdates the in-memory cache and persists the value viaLanguageDataStore.- The
appLanguageflow emits the new value to collectors. MainActivityrecomposes, calculates the newLocale, and recreatesProvideLocalizedResources.- All downstream
stringResource()calls pull from the newly wrapped Context, rendering the UI in the selected language.
Practical Usage Examples
Changing Language from a ViewModel
ViewModels interact with LanguageManager to persist user selection:
class SettingsViewModel @Inject constructor(
private val languageManager: LanguageManager
) : ViewModel() {
fun selectLanguage(lang: AppLanguage) {
viewModelScope.launch {
languageManager.setAppLanguage(lang)
}
}
}
Reading Strings Outside Compose
Components like notification helpers inject StringResourceProvider for automatic localization:
class NotificationHelper @Inject constructor(
private val stringProvider: StringResourceProvider
) {
fun getWelcomeMessage(): String =
stringProvider.getString(R.string.welcome_message)
}
Observing Changes in MainActivity
The root activity wires everything together by collecting state and providing the localized Context:
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val language by viewModel.currentLanguage.collectAsState()
Column {
Text(text = stringResource(R.string.choose_language))
LanguageOption(AppLanguage.SYSTEM, language, viewModel::selectLanguage)
LanguageOption(AppLanguage.CHINESE, language, viewModel::selectLanguage)
LanguageOption(AppLanguage.ENGLISH, language, viewModel::selectLanguage)
}
}
Summary
AppLanguageenum defines valid language states including system fallback, Chinese, and English variants.LanguageDataStorepersists selections using Jetpack DataStore with the keyAPP_LANGUAGEand exposes reactive flows.LanguageManagerprovides both asynchronous flow access and synchronous blocking reads via an in-memory cache.- Context wrapping through
StringResourceProviderImplandProvideLocalizedResourcesensures localized resources for both legacy code and Jetpack Compose. - Reactive updates are driven by collecting
LanguageManager.appLanguageinMainActivity, triggering automatic recompositions when users change languages.
Frequently Asked Questions
What is LanguageManager in the GSY GitHub App?
LanguageManager is a singleton façade class that coordinates all language-related operations in the app. It caches the current language in memory, provides synchronous access via getAppLanguageSync(), handles persistence through LanguageDataStore, and converts AppLanguage enums to Locale objects for resource loading.
How does the app persist language selection across sessions?
The app uses Jetpack DataStore via LanguageDataStore, which writes the selected AppLanguage value to a preferences file named language_preferences under the key APP_LANGUAGE. This survives process death and device restarts, with LanguageManager reading this value during application startup.
Can LanguageManager be used outside of Jetpack Compose UI components?
Yes. For non-Compose contexts such as ViewModels, Services, or BroadcastReceivers, the app provides StringResourceProvider (implemented by StringResourceProviderImpl). This utility creates a locale-configured Context on-demand, allowing background operations to retrieve localized strings without direct Compose dependencies.
How does the app handle system language fallback?
When AppLanguage.SYSTEM is selected, LanguageManager.appLanguageToLocale() returns the device's default Locale. This ensures that if the user has not explicitly chosen Chinese or English, the app automatically adopts the operating system's language settings, falling back to the default Locale configuration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →