An open API service indexing awesome lists of open source software.

https://github.com/hoc081098/jetpack-compose-localization

A production-ready Android application demonstrating advanced localization techniques including runtime language switching, locale-aware datetime formatting with ICU skeletons, and intelligent caching—all built with Jetpack Compose.
https://github.com/hoc081098/jetpack-compose-localization

android-compose android-jetpack-compose android-localization compose-localization jetpack-compose jetpack-compose-localization

Last synced: 5 months ago
JSON representation

A production-ready Android application demonstrating advanced localization techniques including runtime language switching, locale-aware datetime formatting with ICU skeletons, and intelligent caching—all built with Jetpack Compose.

Awesome Lists containing this project

README

          

# Jetpack Compose Localization

A production-ready Android application demonstrating advanced localization techniques including runtime language switching, locale-aware datetime formatting with ICU skeletons, and intelligent caching—all built with Jetpack Compose.

[![Android CI](https://github.com/hoc081098/Jetpack-Compose-Localization/actions/workflows/android.yml/badge.svg)](https://github.com/hoc081098/Jetpack-Compose-Localization/actions/workflows/android.yml)

## Overview

This project showcases **best practices** for implementing localization in modern Android applications. Beyond basic language switching, it demonstrates production-ready patterns including:
- ⚡ **Zero-restart language switching** using AndroidX AppCompat's per-app language preferences API
- 🕐 **Locale-aware datetime formatting** with ICU skeleton patterns and intelligent caching
- 🎨 **Material 3** design with edge-to-edge display
- 🔄 **Follow system locale** option for seamless integration with device settings

## Features

- **🚀 Runtime Language Switching**: Change app language instantly without restarting
- **⚡ DateTimeFormatter Caching**: Production-ready cache system for optimal performance
- **🌍 ICU Skeleton Support**: Locale-aware date/time formatting using ICU skeleton patterns
- **📱 Modern Jetpack Compose UI**: Pure Compose implementation with Material 3
- **🔀 Follow System Option**: Seamlessly follow device locale settings
- **🎯 Per-App Language Settings**: Uses AndroidX AppCompat's `setApplicationLocales()` API (API 27+)
- **📊 Live Locale Information**: Real-time display of current locale details
- **🌐 Accept-Language Header Demo**: HTTP request demonstration with automatic locale-aware Accept-Language headers

## Supported Languages

- **English** (en)
- **Vietnamese** (vi-VN)

The app dynamically displays available languages from `BuildConfig` and highlights the currently selected one.

https://github.com/user-attachments/assets/1dfaab2c-747b-4974-a1d9-96c1a57fe52a

## Quick Start

```bash
git clone https://github.com/hoc081098/Jetpack-Compose-Localization.git
cd Jetpack-Compose-Localization
./gradlew installDebug
```

Run the app and tap on a language to see instant language switching with locale-aware datetime formatting!

## Requirements & Tech Stack

### Core Requirements
- **Min SDK**: API 27 (Android 8.1)
- **Target/Compile SDK**: API 36 (Android 14)
- **Kotlin**: 2.0.21
- **Gradle**: 8.13.0
- **Java**: 11+

### Key Technologies
- **Jetpack Compose** - Modern declarative UI
- **Material 3** - Material Design 3 components
- **AndroidX AppCompat** - Per-app language preferences API
- **AndroidX Lifecycle** - Lifecycle-aware components
- **Java Time API** - Modern date/time handling with ICU patterns
- **Retrofit** - Type-safe HTTP client for network requests
- **Moshi** - Modern JSON library for Kotlin
- **OkHttp** - HTTP client with interceptor support

## Project Structure

```
app/src/main/
├── java/com/hoc081098/jetpackcomposelocalization/
│ ├── MainActivity.kt # Main activity with language switching
│ ├── DemoAcceptLanguageHeader.kt # Accept-Language header demo
│ ├── MyApplication.kt # Application class for initialization
│ ├── data/
│ │ ├── AcceptedLanguageInterceptor.kt # OkHttp interceptor for Accept-Language
│ │ ├── ApiService.kt # Retrofit API interface
│ │ └── NetworkServiceLocator.kt # Network service configuration
│ └── ui/
│ ├── locale/
│ │ ├── AppLocaleManager.kt # Locale management and state
│ │ └── currentLocale.kt # Composable to get current locale
│ ├── text/
│ │ └── DateTimeFormatterCache.kt # 🔥 Intelligent formatter caching
│ ├── time/
│ │ └── Instant.kt # Extension functions for time formatting
│ └── theme/
│ ├── Color.kt # Color definitions
│ ├── Theme.kt # Material Theme configuration
│ └── Type.kt # Typography definitions
└── res/
├── values/ # Default resources (English)
│ └── strings.xml
└── values-vi/ # Vietnamese resources
└── strings.xml
```

## Setup and Installation

### Prerequisites

- Android Studio (latest version)
- JDK 11 or higher
- Android emulator or physical device

### Build & Run

```bash
# Clone the repository
git clone https://github.com/hoc081098/Jetpack-Compose-Localization.git
cd Jetpack-Compose-Localization

# Build and install
./gradlew build
./gradlew installDebug
```

**Or** open in Android Studio → Sync → Run (Shift + F10)

## How It Works

### Language Switching

The app uses `AppLocaleManager` with AndroidX AppCompat's per-app language preferences API with support for "Follow System" mode:

```kotlin
@Stable
class AppLocaleManager {
fun changeLanguage(locale: AppLocaleState.AppLocale) {
val target = when (locale) {
AppLocaleState.AppLocale.FollowSystem ->
// Set empty locale list to follow system
LocaleListCompat.getEmptyLocaleList()

is AppLocaleState.AppLocale.Language ->
LocaleListCompat.create(locale.locale)
}
AppCompatDelegate.setApplicationLocales(target)
}
}
```

**Key benefits:**
- ✅ Works on Android 8.1+ (API 27)
- ✅ Persists preference across app restarts
- ✅ Integrates with Android 13+ system language settings
- ✅ No app restart required
- ✅ Seamlessly follows system locale when user prefers

### Getting Current Locale in Compose

Utility function to reactively observe locale changes in Compose:

```kotlin
@Composable
@ReadOnlyComposable
fun currentLocale(): Locale =
ConfigurationCompat.getLocales(LocalConfiguration.current)[0]
?: LocaleListCompat.getAdjustedDefault()[0]!!
```

### 🔥 DateTimeFormatter Caching (Production-Ready)

One of the **coolest features** is the intelligent `DateTimeFormatterCache` that provides:
- **Thread-safe caching** of immutable DateTimeFormatter instances
- **ICU skeleton support** for locale-aware patterns (e.g., "yMd", "jm", "yMMMdjm")
- **Automatic 12h/24h normalization** based on user preference
- **Localized styles** (SHORT, MEDIUM, LONG, FULL)
- **Per-locale cache management** for optimal memory usage

#### Using ICU Skeletons

```kotlin
val formatter = DateTimeFormatterCache.getFormatterFromSkeleton(
locale = locale,
skeleton = "yMMMddHmss" // Year, abbreviated month, day, hours, minutes, seconds
)

val formattedTime = formatter.formatInstant(Instant.now(), ZoneId.systemDefault())
// Example outputs:
// English: "Jan 15, 2024, 2:30:45 PM"
// Vietnamese: "15 thg 1, 2024, 14:30:45"
```

**Why ICU skeletons?**
- 🌍 Automatically adapt to locale conventions
- 🎯 More flexible than rigid patterns
- 🔒 Safer than `DateTimeFormatter.ofPattern()` for user-facing text
- ⚡ Cached for optimal performance

#### Cache Management

```kotlin
// Clear cache when locale changes (optional, for memory management)
DateTimeFormatterCache.clear()

// Remove formatters for specific locale
DateTimeFormatterCache.removeLocale(locale)
```

#### Additional Formatter Options

```kotlin
// Localized date formatter
val dateFormatter = DateTimeFormatterCache.getLocalizedDateFormatter(
locale = locale,
dateStyle = FormatStyle.MEDIUM
)

// Localized time formatter
val timeFormatter = DateTimeFormatterCache.getLocalizedTimeFormatter(
locale = locale,
timeStyle = FormatStyle.SHORT
)

// Localized date-time formatter
val dateTimeFormatter = DateTimeFormatterCache.getLocalizedDateTimeFormatter(
locale = locale,
dateStyle = FormatStyle.MEDIUM,
timeStyle = FormatStyle.SHORT
)
```

### Time Formatting Extensions

Convenient extension functions for working with `Instant`:

```kotlin
// Format an Instant with a specific zone
val formatted = formatter.formatInstant(instant, zoneId)

// Convert Instant to ZonedDateTime
val zonedDateTime = instant.toZonedDateTime(ZoneId.systemDefault())
```

### Locale Configuration

The build configuration defines supported locales:

```kotlin
object Locales {
val localeFilters = listOf(
"en",
"vi-rVN",
)

val supportedLocales: String =
localeFilters.joinToString(
separator = ",",
prefix = "\"",
postfix = "\""
) {
it.replace(
oldValue = "-r",
newValue = "-"
)
}
}
```

These are automatically exposed via `BuildConfig.SUPPORTED_LOCALES` (comma-separated string: `"en,vi-VN"`).

## Advanced Features

### 🔥 Why DateTimeFormatterCache is Production-Ready

The `DateTimeFormatterCache` implementation demonstrates enterprise-grade patterns:

1. **Thread-Safety**: Uses `ConcurrentHashMap` for safe concurrent access
2. **Immutability**: DateTimeFormatter instances are immutable and thread-safe
3. **Smart Key Generation**: Combines locale + descriptor + flags for precise caching
4. **Memory Management**: Per-locale removal for granular cache control
5. **ICU Skeleton Normalization**: Automatically handles 12h/24h preferences

**Performance benefits:**
- Avoids repeated expensive pattern compilation
- Reduces garbage collection pressure
- Ideal for apps with frequent datetime formatting

**When to clear the cache:**
```kotlin
// Optional: Clear when locale changes
AppCompatDelegate.setApplicationLocales(newLocaleList)
DateTimeFormatterCache.clear() // Free up memory if needed
```

### Follow System Locale

The app provides a "Follow System" option that:
- Sets empty locale list: `AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())`
- Automatically adopts system locale changes
- Integrates seamlessly with Android 13+ per-app language settings

### BuildConfig Integration

Supported locales are automatically exposed via BuildConfig:

```kotlin
object Locales {
val localeFilters = listOf("en", "vi-rVN")
val supportedLocales: String = localeFilters.joinToString(",", "\"", "\"") {
it.replace("-r", "-")
}
}
// Available at runtime as: BuildConfig.SUPPORTED_LOCALES = "en,vi-VN"
```

The `AppLocaleManager` parses this string to dynamically generate language options without hardcoding.

### Accept-Language Header Demo

The app includes a practical demonstration of sending locale-aware HTTP requests with the Accept-Language header:

**Key Components:**

1. **AcceptedLanguageInterceptor** - OkHttp interceptor that automatically adds Accept-Language header:
```kotlin
internal class AcceptedLanguageInterceptor(
private val localeProvider: LocaleProvider,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val locales = localeProvider.provide()
val request = chain.request()
.newBuilder()
.addHeader("Accept-Language", locales.toLanguageTags())
.build()
return chain.proceed(request)
}
}
```

2. **NetworkServiceLocator** - Configures OkHttp with the interceptor:
```kotlin
object NetworkServiceLocator {
private val localeProvider: AcceptedLanguageInterceptor.LocaleProvider
get() = AcceptedLanguageInterceptor.LocaleProvider {
LocaleManagerCompat.getApplicationLocales(application)
.takeIf { it.size() > 0 }
?: LocaleManagerCompat.getSystemLocales(application)
}

private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.addInterceptor(AcceptedLanguageInterceptor(localeProvider))
.build()
}
}
```

3. **DemoAcceptLanguageHeader** - Composable UI that calls httpbin.org/get:
- Press "GET" to make a request to httpbin.org
- The server echoes back the Accept-Language header
- Shows how different locales result in different Accept-Language values
- Example: English → `"en"`, Vietnamese → `"vi-VN"`

4. **MyApplication** - Initializes the network service locator:
```kotlin
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
NetworkServiceLocator.init(this)
}
}
```

**Why this matters:**
- Demonstrates real-world usage of locale information in API calls
- Shows proper architecture for locale-aware networking
- Useful pattern for apps that need server-side localization
- The Accept-Language header helps servers return content in the user's preferred language

## Adding New Languages

Adding a new language is straightforward:

**1. Update build configuration** (`app/build.gradle.kts`):
```kotlin
object Locales {
val localeFilters = listOf(
"en",
"vi-rVN",
"fr-rFR", // ← Add new locale
)
// ...
}
```

**2. Create resource directory** `app/src/main/res/values-{lang}/`

**3. Add `strings.xml`** with translated strings:
```xml

Votre Nom d\'App
Locale actuelle: %1$s, langue: %2$s, pays: %3$s, languageTag: %4$s
Suivre le système
Maintenant c\'est %1s

```

**4. Rebuild** → Language appears automatically in the app! ✨

## Key Features Implementation

### Live DateTime Demo

The app includes a live demonstration of locale-aware datetime formatting:

```kotlin
@Composable
private fun DemoDateTimeFormatter(
locale: Locale,
modifier: Modifier = Modifier,
clock: Clock = Clock.systemDefaultZone(),
) {
val now: Instant = remember(clock) { Instant.now(clock) }
val timeFormatter = DateTimeFormatterCache.getFormatterFromSkeleton(
locale = locale,
skeleton = "yMMMddHmss"
)

Text(
text = stringResource(
R.string.demo_datetime_formatter,
timeFormatter.formatInstant(now, clock.zone),
),
style = MaterialTheme.typography.bodyLarge,
)
}
```

This demonstrates how date/time formatting automatically adapts to the selected locale without any manual formatting logic.

### Edge-to-Edge Display

Modern edge-to-edge display with proper window insets handling:

```kotlin
enableEdgeToEdge()
```

### Material 3 Theming

Implements Material You design (dynamic color disabled for consistency):

```kotlin
JetpackComposeLocalizationTheme(dynamicColor = false) {
// Content
}
```

### Lifecycle Awareness

The app logs lifecycle events for debugging:

```kotlin
lifecycle.eventFlow
.onEach { Log.d("MainActivity", ">>> lifecycle event: $it") }
.launchIn(lifecycleScope)
```

## Testing

```bash
# Unit tests
./gradlew test

# Instrumentation tests
./gradlew connectedAndroidTest
```

## Building for Release

```bash
./gradlew assembleRelease
# APK output: app/build/outputs/apk/release/
```

## Contributing

Contributions are welcome! Please:
1. Open an issue first for major changes
2. Follow Kotlin conventions
3. Add tests for new features
4. Update documentation
5. Ensure tests pass before submitting PR

## License

Available for educational and demonstration purposes. See repository for license details.

## Acknowledgments

- Built with [Jetpack Compose](https://developer.android.com/jetpack/compose)
- [AndroidX AppCompat](https://developer.android.com/jetpack/androidx/releases/appcompat) for per-app language preferences
- [Material Design 3](https://m3.material.io/) guidelines
- [ICU](https://developer.android.com/guide/topics/resources/internationalization) for locale-aware patterns

## Resources

- [Android Localization Guide](https://developer.android.com/guide/topics/resources/localization)
- [Per-app language preferences](https://developer.android.com/guide/topics/resources/app-languages)
- [Jetpack Compose Documentation](https://developer.android.com/jetpack/compose/documentation)
- [Material Design 3](https://m3.material.io/)

## Author

**hoc081098**

- GitHub: [@hoc081098](https://github.com/hoc081098)

## Support

If you find this project helpful, please consider giving it a ⭐️ on GitHub!

For issues, questions, or suggestions, please open an issue on the [GitHub repository](https://github.com/hoc081098/Jetpack-Compose-Localization/issues).