# How Multi-Language Localization Is Implemented with LanguageManager in GSY GitHub App Compose

> Implement multi-language localization in GSY GitHub App Compose using LanguageManager. Discover how this app coordinates enum definitions, persistence, and locale-aware context wrappers.

- Repository: [Shuyu Guo/gsygithubappcompose](https://github.com/carguo/gsygithubappcompose)
- Tags: how-to-guide
- Published: 2026-02-26

---

**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`](https://github.com/carguo/gsygithubappcompose/blob/main/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`](https://github.com/carguo/gsygithubappcompose/blob/main/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`](https://github.com/carguo/gsygithubappcompose/blob/main/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.

```kotlin
// 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`](https://github.com/carguo/gsygithubappcompose/blob/main/core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/util/StringResourceProvider.kt) builds a locale-aware Context on-demand:

```kotlin
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`](https://github.com/carguo/gsygithubappcompose/blob/main/core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/util/LocalizedResources.kt) supplies a wrapped Context via `LocalizedContextWrapper`:

```kotlin
@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:

1. **User selects a language** in the UI, triggering `languageManager.setAppLanguage(AppLanguage.ENGLISH)`.
2. `LanguageManager` updates the in-memory cache and persists the value via `LanguageDataStore`.
3. The `appLanguage` flow emits the new value to collectors.
4. `MainActivity` recomposes, calculates the new `Locale`, and recreates `ProvideLocalizedResources`.
5. 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:

```kotlin
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:

```kotlin
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:

```kotlin
@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

- **`AppLanguage`** enum defines valid language states including system fallback, Chinese, and English variants.
- **`LanguageDataStore`** persists selections using Jetpack DataStore with the key `APP_LANGUAGE` and exposes reactive flows.
- **`LanguageManager`** provides both asynchronous flow access and synchronous blocking reads via an in-memory cache.
- **Context wrapping** through `StringResourceProviderImpl` and `ProvideLocalizedResources` ensures localized resources for both legacy code and Jetpack Compose.
- **Reactive updates** are driven by collecting `LanguageManager.appLanguage` in `MainActivity`, 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.