{
  "name": "i18n",
  "title": "i18n - multi-language web apps",
  "description": "Multi-language for web apps via `gipity add i18n` - language picker, locale persistence, RTL, plural/translation lookup. Transparently upgrades the app's existing t() copy calls; no code changes.",
  "guid": "sk_kit_i18n",
  "category": "Kits",
  "requiredTools": [
    "add"
  ],
  "content": "# i18n — multi-language web apps\n\n`gipity add i18n` makes a web app multi-language: a language picker, `localStorage` persistence, automatic RTL (`<html dir>`), and plural/translation lookup. It installs into `src/packages/i18n/` and **transparently upgrades the app's existing copy calls** — no edits to `main.js`.\n\n## The seam: keep copy in `strings.js`\n\nWeb starter apps (`web-simple` and friends) keep all user-facing text in `src/js/strings.js` and read it with `t('key')`, importing the reader from the `@gipity/i18n` specifier:\n\n```js\nimport { t, tn, getLang } from '@gipity/i18n';\n\nel.textContent = t('welcome');\nel.textContent = t('greeting', { name: 'Sam' });   // \"Hello, Sam!\" — {placeholder} interpolation\nel.textContent = tn('items', cart.length);          // plural-aware: items_one / items_other\n```\n\n```js\n// src/js/strings.js — the one place copy lives\nexport const strings = {\n  welcome: 'Welcome',\n  greeting: 'Hello, {name}!',\n  items_one: '{count} item',\n  items_other: '{count} items',\n};\n```\n\nOut of the box `@gipity/i18n` resolves to a tiny **monolingual** reader (`src/js/i18n.js`): `t()` just returns the string with interpolation. No picker, no weight. So even a single-language app should route copy through `t()` — it's good structure, and it's what makes step 2 free.\n\n## Add multi-language\n\n```\ngipity add i18n\n```\n\nThe kit's install overrides the `@gipity/i18n` import to its full runtime and adds an `@app/strings` alias pointing at your `src/js/strings.js`. Every `t('key')` call site upgrades at once — translation lookup, the language `<select>` (auto-mounted once translations exist), persistence, and RTL. Then:\n\n> **\"translate strings to Spanish and Japanese\"**\n\nTranslations land in `src/packages/i18n/translations.js`, mirroring the keys in `strings.js`. English stays the base in `strings.js` and never appears there.\n\n```js\nexport const translations = {\n  es: { welcome: 'Bienvenido', items_one: '{count} artículo', items_other: '{count} artículos' },\n  ja: { welcome: 'ようこそ', items_other: '{count}個' },\n};\n```\n\n## The runtime API\n\n```js\nimport { t, tn, getLang, setLang, availableLangs, isRtl } from '@gipity/i18n';\nsetLang('es');          // switch + persist; getLang()/<html lang|dir>/listeners all updated synchronously\navailableLangs();       // ['en', ...languages present in translations.js]\n```\n\nThe picker is a `<select>` **auto-mounted** to `<body>` once at least one translation exists — nothing to wire. Style it via the `.lang-picker` class (the web templates already position it top-right). To place it yourself, `mountLangPicker(i18n, parentEl)`.\n\n## Re-rendering on language change\n\n`setLang()` (and the picker) fires an `i18n:changed` event on `document`, synchronously — by the time it returns, `getLang()` and `<html lang>/<html dir>` are already updated. Re-pull your text in a `render()` you wire to it — the web starter's `main.js` already does:\n\n```js\nfunction render() { /* your t('key') → DOM assignments */ }\ndocument.addEventListener('i18n:changed', render);\nrender();\n```\n\n## Notes\n\n- **Add it early.** Routing copy through `t()` as you build the UI is what makes the install zero-edit. Bolting it onto an app with hard-coded strings means moving that copy into `strings.js` first.\n- **Plurals** use `_one`/`_other` (plus `_zero`/`_two`/`_few`/`_many` where a language needs them) per `Intl.PluralRules`.\n- **RTL** scripts (`ar`, `he`, `fa`, `ur`, `yi`) flip layout automatically.\n- **Default language is predictable.** First load defaults to English (the base copy in `strings.js`); the kit only auto-switches to a detected browser language when the app actually *ships* that language (i.e. it's in `translations.js`). A guest on an `ar` browser visiting an English-only app stays English (no surprise RTL flip) — the always-visible picker lets them switch if the guess was wrong, and that explicit choice persists in `localStorage`. To make a different language the default instead of English, set the owner's language as the first/base copy.\n- **Number/date/currency** aren't wrapped — `Intl.*` is a one-liner with `getLang()`: `new Intl.NumberFormat(getLang(), { style: 'currency', currency: 'USD' }).format(99.5)`.\n- **Web only.** For server-side / function copy, format with `Intl.*` directly.\n"
}
