Gradle plugin for Android/Kotlin Multiplatform internationalization that enforces translation completeness and provides AI-assisted translation.
- Translation Validation: Ensures all target locales have complete translations
- AI-Powered Translation: Automatically generates missing translations using LLMs (OpenAI, Google Gemini, Anthropic)
- Smart Resource Handling: Supports strings, plurals, and string-arrays in both XML and JSON formats
- Build Integration: Fails builds on missing translations (configurable)
- Plural Support: Properly translates all plural quantity variants (one, other, few, many)
- Worker Isolation: Uses Gradle Worker API for safe classpath isolation
- Code Generation: Generates Kotlin constants from JSON resources for compile-time safe access
- Convention Plugin: Pre-configured settings for consistent usage across modules
In your module's build.gradle.kts:
plugins {
id("org.multipaz.lokalize")
}Or use the convention plugin for pre-configured defaults:
plugins {
id("org.multipaz.lokalize.convention")
}import org.multipaz.lokalize.util.LLMProvider
import org.multipaz.lokalize.util.LLmModel
import org.multipaz.lokalize.util.OutputFormat
lokalize {
defaultLocale = "en"
targetLocales = listOf("es", "fr", "de")
failOnMissing = true
// Resource format: XML (Android strings.xml) or JSON
outputFormat.set(OutputFormat.JSON)
// Optional: Configure AI translation
llmApiKey.set("your-api-key") // Or use environment variable
llmProvider.set(LLMProvider.GOOGLE) // GOOGLE, OPENAI, or ANTHROPIC
llModel.set(LLmModel.GEMINI2_0_FLASH)
// Optional: Custom resources directory.
// For Kotlin Multiplatform JSON projects, prefer a path that is NOT a
// recognized KMP source-set directory (e.g. "src/commonMain/lokalize"
// rather than "src/commonMain/resources"), so the per-locale JSONs are
// not auto-bundled into the JVM JAR as Java resources at the root.
resourcesDir.set("src/commonMain/lokalize")
// Optional: Override the packages used for the generated code so that
// multiple modules (e.g. multipaz-doctypes and multipaz-utopia) can each
// produce their own GeneratedTranslations / GeneratedStringKeys without
// colliding. Defaults target multipaz-doctypes.
generatedTranslationsPackageName.set("org.multipaz.utopia.generated")
stringKeysPackageName.set("org.multipaz.utopia.localization")
}Option 1 - Environment variable (recommended):
export LOKALIZE_API_KEY="your-api-key"Option 2 - local.properties file:
lokalize.api.key=your-api-key
lokalize.provider=GOOGLE
lokalize.model=GEMINI2_0_FLASHValidates that all target locales have complete translations.
./gradlew :module:lokalizeCheck- Compares target locale files against the base (default) locale
- Reports missing strings, plurals, and arrays
- Fails the build if
failOnMissing = trueand translations are incomplete - Supports both XML and JSON resource formats
Generates missing translations using AI.
./gradlew :module:lokalizeFix- Detects missing entries
- Sends them to the configured LLM for translation
- Updates the target locale files with translated content
- Fails if the API returns errors (does not silently use fallback text)
- Supports both XML and JSON resource formats
Generates Kotlin code from JSON string resources for compile-time safe access.
./gradlew :module:generateMultipazStrings- Only available when
outputFormat.set(OutputFormat.JSON) - Scans all
values*/strings.jsonfiles - Generates Kotlin files with embedded string maps
- Creates a central access object with
getString(),getMapForLocale(), andcontainsKey()methods - Useful for platforms where file access is unreliable (e.g., iOS)
Generated API:
// Get string for a specific language
val text = GeneratedTranslations.getString("my_key", "es")
// Get entire map for a locale
val map = GeneratedTranslations.getMapForLocale("de")
// Check if key exists
val exists = GeneratedTranslations.containsKey("my_key", "fr")
// List all available languages
val languages = GeneratedTranslations.allLanguages| Option | Type | Default | Description |
|---|---|---|---|
defaultLocale |
String |
"en" |
Base/source locale |
targetLocales |
List<String> |
[] |
Locales to validate/translate |
failOnMissing |
Boolean |
true |
Fail build on missing translations |
outputFormat |
OutputFormat |
XML |
Resource format (XML or JSON) |
llmApiKey |
Property<String> |
Environment lookup | API key for translation |
llmProvider |
LLMProvider |
GOOGLE |
LLM provider to use |
llModel |
LLmModel |
GEMINI2_0_FLASH |
Specific model |
resourcesDir |
Property<String> |
"src/commonMain/composeResources" |
Resources base path |
generatedTranslationsPackageName |
Property<String> |
"org.multipaz.doctypes.generated" |
Package for the generated GeneratedTranslations and per-language Strings_* files |
stringKeysPackageName |
Property<String> |
"org.multipaz.doctypes.localization" |
Package for the generated GeneratedStringKeys object |
Standard Android strings.xml format:
<!-- values/strings.xml -->
<resources>
<string name="app_name">My App</string>
<string name="welcome_message">Welcome, %1$s!</string>
<plurals name="items_count">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
<string-array name="months">
<item>January</item>
<item>February</item>
</string-array>
</resources>Flat JSON with key-value pairs:
{
"app_name": "My App",
"welcome_message": "Welcome, %1$s!",
"items_count_one": "%d item",
"items_count_other": "%d items"
}JSON format is recommended when:
- You need compile-time access to strings via code generation
- Working with Kotlin Multiplatform projects
- Using the
generateMultipazStringstask
Available models:
GEMINI2_0_FLASH- Fast, efficient (recommended)GEMINI2_0_FLASH_LITE- Most efficient for low-latencyGEMINI2_5_PRO- Advanced capabilitiesGEMINI2_5_FLASH- Balance of speed and capability
Available models:
GPT4O- Versatile flagship modelGPT4O_MINI- Cost-effective versionGPT5- Latest flagshipGPT5_MINI/GPT5_NANO- Faster, cost-efficient
Available models:
CLAUDE_SONNET_4- High-performance reasoningCLAUDE_OPUS_4- Most powerful for complex tasksCLAUDE_HAIKU_4_5- Fastest, most compact
XML:
<string name="app_name">My App</string>
<string name="welcome_message">Welcome, %1$s!</string>JSON:
{
"app_name": "My App",
"welcome_message": "Welcome, %1$s!"
}XML:
<plurals name="items_count">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>JSON:
{
"items_count_one": "%d item",
"items_count_other": "%d items"
}The plugin will translate each quantity variant (one, other, few, many) separately based on the target locale's plural rules.
XML:
<string-array name="months">
<item>January</item>
<item>February</item>
</string-array>JSON:
{
"months_0": "January",
"months_1": "February"
}- Scan: Reads base locale resources (XML or JSON) and extracts all entries
- Compare: Checks each target locale for missing or incomplete entries
- Batch: Groups missing entries for efficient API usage
- Translate: Sends to LLM with context-aware prompts
- Parse: Extracts translations from API response
- Write: Updates target locale files preserving existing content
Different locales have different plural rules:
- English, Spanish:
one,other - French:
one,other(treats 0 as "one") - Russian, Polish:
one,few,many,other - Arabic:
zero,one,two,few,many,other - Japanese, Korean:
only(no plurals)
The plugin uses CLDR plural rules to determine required quantities for each locale.
If you see 429 Too Many Requests:
Google Gemini Free Tier:
- Very limited daily quota
- Wait 24 hours for reset, or
- Upgrade to paid API key
OpenAI:
- Check your plan's rate limits
- Consider using
GPT4O_MINIfor cost savings
Anthropic:
- Claude has different rate limits per tier
- Check your API key's tier status
Ensure your resource directory structure follows standard conventions:
XML format:
src/commonMain/composeResources/
values/strings.xml (base/default)
values-es/strings.xml (Spanish)
values-fr/strings.xml (French)
JSON format:
src/commonMain/lokalize/
values/strings.json (base/default)
values-es/strings.json (Spanish)
values-fr/strings.json (French)
The plugin uses Gradle Worker API with classpath isolation. If you see Koog/Kotlin version conflicts:
./gradlew clean
./gradlew --stop # Stop Gradle daemon
./gradlew :module:lokalizeFixIf generateMultipazStrings is skipped:
- Verify
outputFormat.set(OutputFormat.JSON)is configured - Check that JSON files exist in
resourcesDir - Ensure directory names start with "values"
-
Commit base locale first: Always ensure base strings are complete before running
lokalizeFix -
Review AI translations: While AI is accurate, review translations for:
- Brand-specific terminology
- Cultural context
- UI space constraints
-
Use environment variables for API keys: Don't commit API keys to version control
export LOKALIZE_API_KEY="your-key"
-
Run check before commit: Add to pre-commit hooks:
./gradlew :module:lokalizeCheck
-
Batch translations: For cost efficiency, accumulate several missing strings before running
lokalizeFix -
Choose format wisely:
- Use XML for Android-only projects with standard resource handling
- Use JSON for Kotlin Multiplatform projects needing code generation
The org.multipaz.lokalize.convention plugin provides pre-configured settings:
plugins {
id("org.multipaz.lokalize.convention")
}
// Only override if needed
lokalize {
outputFormat.set(OutputFormat.JSON)
resourcesDir.set("src/commonMain/lokalize")
}When using the base plugin directly:
plugins {
id("org.multipaz.lokalize")
}
lokalize {
defaultLocale = "en"
targetLocales = listOf("es", "fr", "de", "ja")
outputFormat.set(OutputFormat.JSON)
resourcesDir.set("src/commonMain/lokalize")
}# Check translations
./gradlew :multipaz-compose:lokalizeCheck
# Generate missing translations (requires API key)
export LOKALIZE_API_KEY="your-key"
./gradlew :multipaz-compose:lokalizeFix# Check translations
./gradlew :multipaz-doctypes:lokalizeCheck
# Generate missing translations
export LOKALIZE_API_KEY="your-key"
./gradlew :multipaz-doctypes:lokalizeFix
# Generate Kotlin code from JSON resources
./gradlew :multipaz-doctypes:generateMultipazStringsmultipaz-utopia carries its own copy of the lokalize pipeline so that
applications consuming only multipaz-doctypes don't ship Utopia-specific
strings. It overrides the generated package names so its
GeneratedTranslations / GeneratedStringKeys live alongside the Utopia
code instead of in the doctypes namespace:
// multipaz-utopia/build.gradle.kts
lokalize {
outputFormat.set(OutputFormat.JSON)
resourcesDir.set("src/commonMain/lokalize")
generatedTranslationsPackageName.set("org.multipaz.utopia.generated")
stringKeysPackageName.set("org.multipaz.utopia.localization")
}./gradlew :multipaz-utopia:lokalizeCheck
./gradlew :multipaz-utopia:lokalizeFix # requires LOKALIZE_API_KEY
./gradlew :multipaz-utopia:generateMultipazStrings- name: Check Translations
run: ./gradlew :module:lokalizeCheck
env:
LOKALIZE_API_KEY: ${{ secrets.LOKALIZE_API_KEY }}
- name: Generate Code (JSON format)
run: ./gradlew :module:generateMultipazStringstranslation-check:
script:
- ./gradlew :module:lokalizeCheck
variables:
LOKALIZE_API_KEY: $LOKALIZE_API_KEYThe plugin uses a layered architecture:
- ResourceScanner: Parses XML and JSON resources using pluggable strategies
- ResourceScannerStrategy: Interface for format-specific scanning (XML/JSON)
- TranslationComparator: Finds missing/incomplete translations
- BatchTranslator: Groups translations for efficient API usage
- TranslationWorkAction: Runs in isolated Gradle Worker process
- ResourceWriter: Merges translations preserving existing content
- ResourceWriterStrategy: Interface for format-specific writing (XML/JSON)
- GenerateStringsTask: Generates Kotlin code from JSON resources
Worker isolation ensures classpath isolation between the plugin and project dependencies.
Copyright (c) 2024 Multipaz Contributors
Licensed under the Apache License, Version 2.0