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, checked into the source tree so downstreams on non-Gradle build systems can compile as-is
- Drift Protection:
lokalizeCheckGenerated(wired intocheck) fails the build if the committed generated sources fall out of sync with the JSON resources - 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
Renders Kotlin code from JSON string resources for compile-time safe access.
./gradlew :module:generateMultipazStrings- Only does anything when
outputFormat.set(OutputFormat.JSON)(skips for XML modules) - Scans all
values*/strings.jsonfiles - Writes Kotlin files with embedded string maps into the checked-in source
directory (
generatedSourceDir, defaultsrc/commonMain/generated/) - Creates a central access object with
getString(),getMapForLocale(), andcontainsKey()methods - Useful for platforms where file access is unreliable (e.g., iOS)
The generated Kotlin is committed to version control, not produced into build/
on every compile. Compiling reads the committed sources directly and never
regenerates. After editing any strings.json, run this task and commit the
result alongside the JSON change — otherwise lokalizeCheckGenerated will fail
(see below). This keeps the source tree compilable as-is for downstreams that
build without Gradle (issue #1811).
Guards against the committed generated sources drifting from the JSON resources.
./gradlew :module:lokalizeCheckGenerated- Renders the Kotlin in memory and compares it against the committed files under
generatedSourceDir; fails the build (printing the exactgenerateMultipazStringscommand to run) if they differ - Wired into
check, so./gradlew check— and./gradlew build, which CI runs — enforce it on every PR. It is the only thing standing between astrings.jsonedit and stale committed sources, since generation is off the compile path.
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 |
generatedSourceDir |
Property<String> |
"src/commonMain/generated" |
Checked-in directory the JSON code generator writes into (empty for XML modules) |
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
# Regenerate the checked-in Kotlin from the JSON resources, then commit the result
# (both the strings.json changes and src/commonMain/generated/ together)
./gradlew :multipaz-doctypes:generateMultipazStrings
# Verify the committed sources are in sync (also runs as part of `check`)
./gradlew :multipaz-doctypes:lokalizeCheckGeneratedmultipaz-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 # then commit src/commonMain/generated/
./gradlew :multipaz-utopia:lokalizeCheckGenerated # verify (also part of `check`)- name: Check Translations
run: ./gradlew :module:lokalizeCheck
env:
LOKALIZE_API_KEY: ${{ secrets.LOKALIZE_API_KEY }}
# Do NOT regenerate in CI. The generated sources are committed; CI only verifies
# they are in sync. lokalizeCheckGenerated is wired into `check`, so a plain
# `./gradlew build` already enforces this — run it explicitly only if you skip check.
- name: Verify Generated Sources (JSON format)
run: ./gradlew :module:lokalizeCheckGeneratedtranslation-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: Renders Kotlin code from JSON resources into the checked-in source tree
- LokalizeVerifyGeneratedTask: Backs
lokalizeCheckGenerated; renders in memory and fails on drift from the committed sources
Worker isolation ensures classpath isolation between the plugin and project dependencies.
Copyright (c) 2024 Multipaz Contributors
Licensed under the Apache License, Version 2.0