How Kofte Ships 16 Languages With Zero Libraries

Terje Rutgersen · 2026 · android · localization

Kofte is a knitting counter. It has no accounts, no servers, no sync. It ships in 16 languages. Those two facts are connected.

When I went to localize Kofte, I had one hard rule: no runtime dependencies. No i18n frameworks. No cloud translation services making requests in the background. The app is offline by design, and the localization has to be offline too.

Android already solved this decades ago. You just have to let it.

The architecture is a folder

Android handles localization through directory naming. Base strings live in values/strings.xml. French goes in values-fr/strings.xml. Japanese in values-ja/. Traditional Chinese for Hong Kong in values-zh-rHK/. The system picks the right file at runtime based on the device locale. No code.

Every string in the app is a key:

<string name="welcome_subtitle">Ready to cozy up and knit?</string>

The French file has the same key, different value:

<string name="welcome_subtitle">Prêt(e) à vous installer et tricoter ?</string>

Japanese:

<string name="welcome_subtitle">のんびり編み物を始めましょう。</string>

That is it. No mapping objects, no fallback chains you wire up yourself, no locale negotiation code. Android does the lookup. You write XML.

Three touch points per language

When I add a new language to Kofte, the code changes are small:

1. A new values-xx/strings.xml file. A full translation of every string key. This is the real work, but it is content, not code.

2. A locale mapping in MainActivity. One line in a when block that maps the preference code to a Locale object. This drives the in-app language switcher so users can override their device locale.

3. A label in the settings dropdown. One entry in the language picker so the new option shows up.

No dependency updates. No config files. No build plugin changes. The last four languages I added — Japanese, Malay, Indonesian, and Traditional Chinese — touched exactly three Kotlin/XML files beyond the translation files.

The in-app switcher

Most Android apps rely on the system locale and call it done. Kofte has an in-app language switcher because Google Play's language splitting strips out locales the user did not select at install time.

The implementation overrides attachBaseContext to wrap the context with the user's preferred locale before the activity inflates. The choice persists in DataStore. Pick a new language, the activity recreates. No library.

One gotcha: Google Play's App Bundle format splits resources by language by default. If you rely on an in-app switcher, you need to disable that splitting in your build config, or users will only see languages that match their device settings. Learned that one by shipping a broken build.

Knitting vocabulary is not generic

Knitting has real terminology. A gusset is not a general-purpose word. Heel turns, yarn overs, stitch counts — these need actual craft vocabulary in every target language, not whatever Google Translate spits out.

The Japanese translation uses 目 for stitches and 段 for rows. The Hong Kong Chinese file uses 冷衣 for sweater and 毛冷 for yarn — Cantonese terms, not Mandarin. The Indonesian file uses "merajut" for knitting, "tusukan" for stitches. Getting the craft words right is the whole point.

Each translation file mirrors the English source: same keys, same structure, same comments. A diff between any two locale files should show only value changes. Makes it obvious what is translated and what is not.

The numbers

Kofte currently ships 16 languages across roughly 212 string keys per locale. Total footprint of all translation files is around 3,400 lines of XML. No runtime overhead. No network calls. No parsing. The APK bundles every locale and Android handles selection at zero cost.

Adding the last batch of four languages took less time than debugging the edge-to-edge deprecation warning that prompted the release.

Why bother

I am one developer. I do not have a localization team or a budget for translation platforms. But the architecture means I do not need one. Adding a language is a content task. The barrier is "can you translate 212 strings with correct knitting vocabulary," not "can you wire up another service."

Offline apps should have offline localization. Android gives you that for free.