i18n
Translations for Kotlin Multiplatform. YAML in, typed Kotlin out, mistakes caught at build time.
Why translations are a build problem
Most translation setups look up a string by key at runtime. That means a renamed key, a missing language, or a placeholder the translator dropped are all discovered the same way: a user sees something wrong. The failure is silent, it is in production, and it is usually in the language nobody on the team reads.
The interesting part of the problem is therefore not the lookup — it is everything around it. Which keys exist? Which languages must be complete? Does every language agree on the placeholders? Those are questions a build can answer.
What it looks like
Write a catalog per language. The fallback language defines the API surface.
# src/commonMain/i18n/messages.en.yaml
checkout:
greeting: "Hello {{name}}!"
items_one: "{{count}} item in your basket"
items_other: "{{count}} items in your basket" The Gradle plugin turns that into typed accessors and a baked catalog. Call them like functions:
val i18n = I18n(locale = Locale("de"), fallback = Locale("en")) {
install(AppI18nCatalog)
}
i18n.translate.checkout.greeting(name = "Alice") // named params, no string keys
i18n.translate.checkout.items(count = 3) // plural form picked for you A key that no longer exists will not compile, and a translation that invents a placeholder nobody supplies fails the build rather than the checkout page.
What you get
Typed accessors
Generated from the fallback catalog. Placeholders become named parameters — a forgotten one is a compile error.
Build-time checks
A missing required language, or a translation inventing an
unknown placeholder, fails the build. Softer findings warn, and become errors under
-Pi18n.strict.
Locale fallback chain
de-CH falls back to de, then to the
configured fallback. A regional catalog only carries what differs.
Catalog precedence
Each module ships its own catalog. An app installs later and wins on shared keys, so framework strings can be overridden without forking.
Plurals
_one / _other suffixes, selected by
count, with {{count}} supplied automatically.
Reactive in Kraft
Switching language re-renders subscribed components. The choice persists in localStorage.
Scope, honestly
The locale model covers language and region — de, de-CH. Script and
variant subtags such as zh-Hant are not modelled: the second subtag is always read as a
region, so those tags are mis-parsed. Plural selection is currently the simple
count == 1 rule rather than full CLDR, and i18n.format is a seam with a
default implementation rather than locale-aware date and number formatting.