-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial 5 Localization
Goal: understand how LockedIn CLI speaks 33 languages — and practice directing an agent to add another one. Localization is a fantastic agent task: it's mechanical enough to delegate, but it has real constraints (a test gate, layout rules, grammar review) that teach you to review.
← Prev: Tutorial 4 Prompting & Reviewing · Back to Home
Run the CLI in Spanish, Hindi, Japanese, Simplified Chinese, or any shipped language and everything changes — the splash, the help table, every command's output, the chat session, even the legal fine print. Not just the jokes: the whole visible surface.
lockedin --lang es post
LOCKEDIN_LANG=hi lockedin
lockedin --lang zh auraThe language is autodetected on startup, resolved in priority order:
- the
--langflag (--lang es,--lang=fr,-l ja) - the
LOCKEDIN_LANGenvironment variable - your locale (
LC_ALL/LC_MESSAGES/LANG, then the OS/runtime locale) - English, as the fallback
normalizeLang() normally uses the locale's primary subtag. That means de-DE
selects de, but tlh does not accidentally become tl; the real aliases
fil and tgl intentionally map to Tagalog (tl), Norwegian nb and nn
map to no, legacy Indonesian in maps to id, and legacy Hebrew iw
maps to he. Two regional codes are preserved verbatim rather than folded to
their primary subtag: pt-BR/pt_BR select the canonical regional code while
generic pt stays the backward-compatible Brazilian Portuguese bundle (both are
Brazilian Portuguese and share the same pools/UI), and en-SG/en_SG keep
Singlish (generic en stays English). Hong Kong Traditional Chinese is the same
kind of exception:
zh-HK, zh_HK.UTF-8, and zh-Hant-HK select zh-HK, while generic zh
and mainland tags select Simplified Chinese (zh).
Switching mid-flight — the /language panel. The CLI could always start in
another language (--lang, LOCKEDIN_LANG); now you can switch mid-session. Type
/language (aliases /lang and /languages) to list all 33 languages by code,
each in its own script; /language el switches for the rest of the session. The point is the
escape hatch: after a switch it redraws in the new language and then, in the language
you just left, prints the exact way back — /language en now, lockedin --lang en
next time — so accidentally landing in 日本語 or ಕನ್ನಡ never strands you. (Switch twice
and the language your LOCKEDIN_LANG names is offered too.) --lang and LOCKEDIN_LANG
are unchanged. Like /a11y, it's a sincere utility, not part of the satire.
All translatable text lives in bundles, one per language, each shaped like:
{ meta: { lang: 'es', name: 'Español', dir: 'ltr' },
pools: { HOOKS: [ /* ~25 */ ], LESSONS: [ /* ... */ ], /* ... */ },
ui: { buzzwordDensity: 'Densidad de palabrería: ', /* labels, headings */ } }-
poolsare the content arrays (the jokes) you met in Chapter 2. -
uiare the chrome strings: labels, headings, and small templates.
English is the reference bundle inside src/lockedin.js; the other 32 bundle
modules live in src/content/*.js: ar, bn, bo, de, el, en-SG, es,
eu, fa, fi, fr, he, hi, id, is, it, ja, kn, ms, nl,
no, pl, pt, pt-BR, ru, sv, tl, tr, uk, ur, zh, and zh-HK
(pt-BR reuses pt's pools/UI but is registered separately). Each one is
registered in BUNDLES; SUPPORTED_LANGS is generated from those keys, and
renderHelp() prints that generated code list. No UI bundle hard-codes the list.
setLang('fr'); // point the active language at the French bundle
// L = active pools, U = active ui
pick(L.HOOKS) // a French hook
U.buzzwordDensity // "Densité de jargon : "Because every renderer reads L and U (never a hardcoded string),
setLang alone changes the entire experience. That's the whole trick.
Here's the invariant that makes adding a language safe:
Every bundle must expose the exact same
poolsanduikeys as English.
A test enforces it across all 33 bundles. If you add a new UI string in English
and forget to translate it in Ukrainian, npm test goes red and tells you which
key is missing. You cannot silently ship a half-translated language.
Languages stress terminal layout in different ways:
-
Japanese, Simplified Chinese, and Hong Kong Traditional Chinese use East
Asian Wide / Fullwidth characters.
vw()counts those as two columns, andwrap()hard-breaks long spaceless tokens so CJK text stays inside cards and boxes. -
Hindi and Kannada use nonspacing/enclosing combining marks (
Mn/Me), such as matras and viramas.vw()counts those as zero columns so they do not inflate measured width. -
box()wraps each body line before padding it, so a long translated banner can no longer punch through the border. - Each bundle sets
sentenceEndandlistSep(for example./。,,/、) so generator-composed sentences read naturally.
When you add a language, card header strings (cardSubtitle, cardMeta,
cardFooter) must still fit ≤ 60 visible columns. Arabic, Persian, Hebrew,
and Urdu set meta.dir: 'rtl'. Output contains no bidi controls by default, because some
terminals render them as boxed labels. LOCKEDIN_BIDI=on explicitly enables
balanced isolates after wrapping for terminals known to support them, preserving
ANSI, ASCII commands, and logical copy/paste order. Accessible output always
removes those controls. Without the opt-in, mixed RTL/LTR ordering may be less
sophisticated; never probe or infer support.
Some UI templates splice in raw user clauses with placeholders like {cap}. Do
not translate those slot-for-slot. The sentence must remain grammatical when the
placeholder is a phrase the user typed, not a neat noun.
A real cautionary bug: Japanese templates that put は directly after {cap}
can sound wrong when {cap} is a full clause. The fix is not "translate harder";
it is to restructure the template (for example, add a nominalizer or move the
placeholder) so arbitrary user input still fits.
This exercise still works exactly the same way. Pick a language you can sanity-check (or ask the agent to), and drive it end-to-end. Write the spec first:
Add Danish (
da). Createsrc/content/da.jsas a{ meta, pools, ui }bundle with the same keys as English, translating every entry (content pools at ~25 each, all UI strings). RegisterdainBUNDLESinsrc/lockedin.js.--lang daand ada-*locale must select it. Keep card header strings within the width limit.npm testmust stay green, and add Danish invariants + detection tests mirroring the existing localized ones.
Then run the loop from Chapters 3–4:
- Plan first. "Before writing code, tell me the files you'll change and how you'll keep key parity with English."
-
Tests first. "Add failing tests:
dadetection, key parity forda, and a Danish reflect/connect invariant. Don't create the bundle yet." -
Implement. "Now create
src/content/da.jsby translating an existing bundle key for key, register it, and make the tests pass. Onlypick/shufflefor randomness." -
Gate + review.
npm test, thenlockedin --lang da post— and read the diff: did every key get translated? Do the card borders still line up? Do templates with{cap}survive raw user clauses?
Smaller warm-up exercises if a whole language is too much:
- "Add one more
TAGLINEto all 33 language bundles, keeping counts equal." - "Check whether the Kannada
cardFooteris ≤ 60 visible columns and explain how combining marks were measured." - "Show me the test that would fail if I deleted a
uikey fromja.js."
- Skim
src/content/es.js— it is still a friendly template for a new bundle. - Re-read
docs/HANDOFF.md→ "Adding a language". - Enjoy the multilingual jokes in the Command Reference.
That's the full tutorial. You can now direct an AI agent to build features and localize them behind a test gate — in 33 languages and ready for more. Agree? 👇
Tutorial
- 1 · Orientation
- 2 · How the Code Works
- 3 · Your First Agent Task
- 4 · Prompting & Reviewing
- 5 · Localization
Reference
Satire · Sátira · 風刺. Not affiliated with LinkedIn. GPL-3.0-or-later.