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.

The seam: keep copy in strings.js

Web 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:

import { t, tn, getLang } from '@gipity/i18n';

el.textContent = t('welcome');
el.textContent = t('greeting', { name: 'Sam' });   // "Hello, Sam!" — {placeholder} interpolation
el.textContent = tn('items', cart.length);          // plural-aware: items_one / items_other
// src/js/strings.js — the one place copy lives
export const strings = {
  welcome: 'Welcome',
  greeting: 'Hello, {name}!',
  items_one: '{count} item',
  items_other: '{count} items',
};

Out 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.

Add multi-language

gipity add i18n

The 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:

"translate strings to Spanish and Japanese"

Translations land in src/packages/i18n/translations.js, mirroring the keys in strings.js. English stays the base in strings.js and never appears there.

export const translations = {
  es: { welcome: 'Bienvenido', items_one: '{count} artículo', items_other: '{count} artículos' },
  ja: { welcome: 'ようこそ', items_other: '{count}個' },
};

The runtime API

import { t, tn, getLang, setLang, availableLangs, isRtl } from '@gipity/i18n';
setLang('es');          // switch + persist; getLang()/<html lang|dir>/listeners all updated synchronously
availableLangs();       // ['en', ...languages present in translations.js]

The 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).

Re-rendering on language change

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:

function render() { /* your t('key') → DOM assignments */ }
document.addEventListener('i18n:changed', render);
render();

Notes