By Dan & Katya · May 24, 2026
Nine Locales, One Watch
Eight non-English languages. Three weeks of evening work. A handful of cultural discoveries that turned out to be more anthropology than engineering.
We localized Run Plan into eight non-English languages. It was Katya’s idea. The argument: Apple Watch fitness apps that don’t speak your language feel rude. We agreed. Then I read about Japanese line-break rules and lost a weekend.
This is the short writeup. What worked. Where we tripped. The one French verb we shipped wrong. And a few cultural gems hiding inside the running vocabulary of other languages.
What ships
en-GB (default), es-ES, de-DE, fr-FR, it, nl, ru, pt-BR, ja.
572 keys per locale. Every notification. Every plan-creation phrase. Every watch screen. The Privacy and Terms pages. The App Store descriptions. The screenshots. All of it.
It’s not the words. It’s the register.
Naive idea of localizing an app: find the strings, hand them to a translator, paste back.
Reality: every language has a register — formal vs informal, runner-slang vs clinical, anglicism-friendly vs puristic — and you have to pick one, per surface. And the picks are not consistent across languages.
Here’s what we landed on:
| Locale | App body | Privacy / Terms |
|---|---|---|
| German | du | Sie |
| French | tu | vous |
| Italian | tu | Lei (capitalized — IT legal convention, not voi) |
| Spanish | tú | usted |
| Dutch | je / jij | u |
| Russian | ты | Вы (capitalized — Apple RU / premium convention) |
| Portuguese (BR) | você | você |
| Japanese | です/ます | です/ます |
Two outliers. BR and JP collapse the register down to one — for opposite reasons. BR drops formal because BR doesn’t use formal “o senhor” in app/legal text (Apple’s BR App Store uses você throughout). Japanese keeps です/ます because it IS the polite default — not a casual fallback. Same shape, opposite logic.
Runner-slang isn’t translation. It’s anthropology.
Each running culture has its own vocabulary that dictionaries miss. Examples we got wrong before we got right:
Brazilian Portuguese. Long run = Longão (substantive, “the big one”). Intervals = Tiros (literally shots). Training plan = Planilha (literally spreadsheet) — inheritance from the era when coaches shared plans as Excel files. BR runners say “minha planilha,” never “meu plano de treino.” We translated to plano first. Reviewer flagged it. Of course you call it the spreadsheet — that’s where it came from.
Italian. Italian runners are puristic — they translate everything instead of importing. Pace = Passo (not Pace). Intervals = Ripetute (not Intervalli). Strides = Allunghi. Splits = Parziali. Threshold = Soglia. And the substantives are bare: il lungo, la Mezza — “the long one,” “the half.”
Russian. Лонгран (Cyrillic anglicism — modern Strava RU). Темп for pace (native, dominant — RU runners actually translate this). Ускорения for strides, NOT страйды. Сплиты (anglicism). And one we love: Забег for race — NOT гонка, because гонка means Formula 1.
Japanese. Western training concepts → katakana (ペース, ロングラン, ファルトレク). Body and sensation → kanji (流し for strides — the beautiful native term, dominant in JP running magazines; 閾値 for threshold; 心拍数 for heart rate). And the untranslatable one: 本番 for race day. Literally “the real thing, the main event.” No English word fits.
(Side note: my running vocabulary in five languages improved more in two weekends of translation review than in years of casual reading.)
The plan-loading disaster
The plan-creation screen shows a 3-line loading status while the engine generates your plan:
Tuning Your Intervals
I localized this naively — 31 unique English tokens, each mapped per locale: Tuning → Настраиваем, Your → Твою, Intervals → Интервальная тренировка. Test catalog passed. Shipped to beta.
A bilingual user — Russian native, runs in St. Petersburg — emailed within a day. Roughly: “the loading screen reads like an exchange student wrote it.”
I went and looked. Oh.
Oh no.
The 3-line stack in Russian was reading “Настраиваем / Твою / Интервальная тренировка” — which is grammatically broken. Твою is accusative-feminine (“your” as direct object); Интервальная тренировка is nominative (“interval training” as subject). The case mismatch is what an LLM-translated dictionary would produce. Russian doesn’t slot words into boxes; it inflects them according to the whole sentence.
Audit across all locales:
- German: separable verbs split across slots. “Stimme ab / Deine / Intervalle” is ungrammatical — abstimmen literally cannot be interrupted by another word.
- Italian: gender clash. “Tuoi” (masculine) + “Ripetute” (feminine). Can’t agree.
- Russian: case mismatch, as above.
- Japanese: verb-first when verb belongs last. “仕上げ中 / の / テーパー” reads like word salad.
The architecture fix: phrase-level keys instead of token-level. One catalog value per phrase, three segments joined by a |. Each language puts its words in slot 1/2/3 however its native grammar requires. Loading screen still renders three visual lines. Each language is finally free.
// Before — 31 token keys, slotted independently: phraseKeys = [["plan_loading.tuning"], ["plan_loading.your"], ["plan_loading.intervals"]] // After — one phrase key, split on render: phraseKey = "plan_loading.tuning_intervals" // catalog value (RU): "Настраиваем|твои|интервалы" // ↑ slot 1 ↑ slot 2 ↑ slot 3 // 1st-person plural verb, accusative-plural possessive, // accusative-plural object. Native case agreement.
Six commits across the catalog. LocalizationCatalogTests 6/6 green. Three native-speaker reviewers signed off. Shipped.
The architectural lesson: slots are not words. Anywhere a phrase reads sequentially in English ({verb} {possessive} {object}), every other language has its own opinion on the order, the case, the agreement. Localize the phrase, not the tokens.
Typography rules are load-bearing
Japanese line-breaking has a rule called Kinsoku Shori (禁則処理 — “forbidden character handling”). You cannot end a line with an opening bracket. You cannot start a line with a closing bracket, a period, or a small kana. You cannot break between a number and its counter.
SwiftUI’s default text wrapper does not know about Kinsoku Shori.
I learned this when one of our Watch screens broke a Japanese phrase between an opening 「 and the noun it was wrapping. Looked deranged. Like dropping a comma in front of a word for no reason.
Fix is mostly: trust the system text engine on iOS 17+ (it handles Kinsoku Shori natively for most Japanese strings). For tight Watch labels you sometimes have to insert U+2060 (word joiner) or use non-breaking spaces. We have ~five spots in the watchOS app where we force non-breaking on Japanese-specific strings.
I now sit two clicks closer to understanding Microsoft Word’s “automatic Japanese typography” preferences than I did a year ago. Of all the skills I expected this project to give me, this was not one.
The brand mark stays English
Our App Store tagline is PLAN. RUN. REPEAT. Three imperatives. Visual rhythm.
First instinct: translate it. Russian = ПЛАНИРУЙ. БЕГАЙ. ПОВТОРЯЙ. German = PLANEN. LAUFEN. WIEDERHOLEN. All correct. All terrible — the rhythm is gone, the letter widths fight, half of them no longer fit the visual width of the badge.
So we kept English across all 9 locales. Strava does the same with “Run. Race. Repeat.” — every locale keeps the English imperatives. Brand-mark treatment, not body copy.
There is a category of strings that should not be localized. Knowing which strings belong in it is the entire skill.
How we keep it from rotting: LocalizationCatalogTests
Every commit runs a catalog gate. Six checks, automatically:
✓ missing translations — every key has a value in every locale ✓ empty translated values — no "" sneaking in ✓ format-argument multiset — %@ and %lld counts match across locales ✓ format-argument positional — %1$@ reorder is allowed if multiset matches ✓ plural-category coverage — one, other, few, many — all gaps caught ✓ knownRegions parity — every locale is registered
Dormant until the second language landed. Then auto-active. Now load-bearing: every commit message in the localization saga ends with LocalizationCatalogTests 6/6 green, and no translation merges without it.
And — yes — we used Claude for this
Translation review for nine languages is not a 2-person-team task without help. The workflow was:
- We pick a locale.
- Find a native-speaker reviewer (friend, friend-of-friend, sometimes paid).
- I did the first pass with Claude in a chat window. Catalog dump in. Locale-specific glossary attached (Strava-RU vocab list / FIDAL IT terms / what BR runners actually say).
- Reviewer reads the output. Marks the misses.
- I escalate the misses back to Claude with the reviewer’s context. (“Reviewer says Educativos means technique drills, not strides. Pick a replacement that means 50-100m end-of-run accelerations.” Claude: “Acelerações.” Reviewer: “Yes.”)
- Catalog test, ship.
Six languages translated this way. Claude was the first-draft engine. Native reviewer was the final authority. Claude was usefully wrong about cultural nuance (offered Educativos for strides); usefully right about grammatical agreement; useful at proposing 3-4 candidates instead of one and letting the reviewer pick.
Two notes from a year of this:
- AI can translate. AI cannot review. The reviewer is doing different work — they’re matching the output to the cultural register of a specific subcommunity (Strava RU vs literary RU, FIDAL IT vs vacation-Italian IT). Claude does not have that.
- The 5-minute “is this what runners say?” question is the entire value-add of a human reviewer. Catalogs of “what runners actually say” are not on the internet in any indexed form.
What it cost
Wall time per language, roughly:
- First pass (Claude + us): ~3 hours.
- Reviewer pass: ~2 hours of their time, spread over a week.
- Integration + glossary lock + catalog test fixes: ~4 hours.
- Total per locale: about a working day, spread.
For nine locales: about nine working days. Spread across two months of evenings.
The “cost” math people skip: the glossary you build for the first locale pays dividends for every subsequent one. By Japanese, the eighth locale, we knew which categories needed locale-specific judgment — running-slang vocab, address forms, plural strategy, gender-neutrality strategy — and which would resolve automatically: numbers, dates, format strings.
What we got wrong
Three for the record:
- French: shipped tu courses for ~two weeks before a French native pointed out it should be tu cours — the present indicative of courir is irregular and we’d conjugated the wrong verb entirely (courser, “to chase”). Embarrassing.
- Russian “экономичность бега” → “эффективность бега”. The first version was the academic sport-science calque. Russian-speaking runners actually say эффективность.
- Dutch consistency: we had hardloopschema in the subtitle and hardloopplannen in the screenshots. Both are correct. Pick one. (We picked hardloopschema.)
None of these were catastrophic. All of them would have been catastrophic without native review.
What this changed in how we think
Two things, durable:
1. Localization is a cultural decision, not a technical one. The engineering layer (catalog tests, key naming, format-arg parity) is the easy part. The hard part is what register, what subcommunity, what slang. You can ship a technically-perfect localization that reads like a press release. We almost did.
2. The catalog test is the seatbelt, not the steering wheel. LocalizationCatalogTests 6/6 green tells you the strings exist. It does not tell you they’re good. Native reviewers tell you they’re good. Build the catalog test anyway — it’ll catch the missing keys you didn’t notice.
Further reading
- Why We Are Building Run Plan Without a Subscription — the related “indie bet on respecting users” decision.
- Apple — Localizable.xcstrings — the format we use for the catalog.
- Kinsoku Shori (W3C, Japanese line composition) — the typography rules.
- Strava brand guidelines — for the “keep English imperatives across locales” reference.
Run Plan is an indie iOS + Apple Watch training planner built by a 2-person team in Amsterdam. No accounts, no ads, no subscription. Your data stays on your device. Also in: español, deutsch, français, italiano, nederlands, русский, português (BR), 日本語.