doxa-maps/documentation/architecture/i18n.md
doxa-maps-88655c70 9dc4dda8cf
Some checks are pending
build-gate / build (push) Waiting to run
consolidate docs into documentation/ and deployment shells into deployment/
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.

docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.

The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.

Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
  - join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
  - tauri frontendDist '../public' -> '../../public', since src-tauri is now one
    level deeper; check:tauri caught this exactly as the card predicted
  - the WordPress contract test's dirname(__DIR__,2) -> 3, same reason

Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.

Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.
2026-09-21 23:23:23 +00:00

68 lines
3.9 KiB
Markdown

# Architecture — i18n (the full contract)
> The simple overview is in `library/i18n/README.md`. This is the complete contract for
> **adding or overriding** a locale.
The `i18n/` seam owns every user-facing string in the map. Translations are **folder-per-locale,
one JSON file per area**, composed at build time into a Vue I18n instance — one instance per
`<doxa-map>` element so multiple embeds on a page don't bleed state.
## Layout
```
i18n/
index.js ← engine (don't edit to add a locale): glob-loads locales, builds createAppI18n()
locales/
en/ ← one folder per locale (the fallback; fullest set — 10 areas)
buttons.json legend.json detail.json options.json
search.json messages.json aria.json prayerLaps.json
tabs.json popup.json
es/ ar/ zh/ … ← same key structure; some locales carry fewer area
files (see the locale contract below)
```
| Export (`index.js`) | What it is |
|---|---|
| `createAppI18n()` | Builds a fresh Vue I18n instance. Called once per `<doxa-map>` (`app.use(createAppI18n())`). |
| `SUPPORTED_LOCALES` | `['ar','de','en','es','fr','hi','it','pt','ro','ru','zh']` — the 11 shipped locales. |
| `RTL_LOCALES` | `Set(['ar'])` — locales that render right-to-left. |
| `detectLocale()` | Reads `document.documentElement.lang` (set by Polylang on WordPress); falls back to `en`. |
## The locale contract
- **8 core area files in every locale**, identical names across locales: `buttons`, `legend`,
`detail`, `options`, `search`, `messages`, `aria`, `prayerLaps`. Two more areas are partially
translated: `tabs` (in `ar`/`de`/`en`/`es`/`fr`/`pt`/`ru`; `hi`/`it`/`ro`/`zh` fall back to
English) and `popup` (in `en`/`fr` only). So the on-disk matrix is: `en`/`fr` 10 files,
`ar`/`de`/`es`/`pt`/`ru` 9, `hi`/`it`/`ro`/`zh` 8. The filename (minus `.json`) becomes the
top-level message namespace — `buttons.json` → `$t('buttons.prayForThem')`.
- **`en/` is the source of truth and the fallback.** Mirror its key structure exactly; keys may
nest (`buttons.speedOption.slow`). Missing keys — and whole missing area files — fall back to
English silently (`fallbackWarn`/`missingWarn` are off in the IIFE bundle).
- All catalogs are **eager-globbed and inlined** (`import.meta.glob('./locales/*/*.json', { eager:true })`)
— required because Vite can't code-split an IIFE bundle. Don't convert to lazy/dynamic import:
the 2026-04-26 experiment GREW the bundle ~22 KB (see the note at the top of `index.js`).
## Add a custom locale (checklist)
1. **Copy `locales/en/` to `locales/<code>/`** (ISO-639-1, e.g. `sw`). Keep all 10 area files
(`en/` carries the fullest set, including `tabs` + `popup`).
2. **Translate the values**, leaving every key in place. Don't rename or drop keys — a missing key
reverts that one string to English; a renamed key is dead.
3. **Add `<code>` to `SUPPORTED_LOCALES`** in `index.js` (and to `RTL_LOCALES` if it reads
right-to-left). This is the only `index.js` edit a new locale needs.
4. **Verify** the glob picked it up and keys resolve:
```js
import { createAppI18n, SUPPORTED_LOCALES } from '@map/i18n/index.js'
const i18n = createAppI18n()
console.log(SUPPORTED_LOCALES.includes('<code>')) // true
console.log(i18n.global.t('buttons.prayForThem', 1, { locale: '<code>' })) // your translation
```
## How it integrates with the component library
The app-profile entry (`plugin-designs/<name>/index.js`) does `app.use(createAppI18n())` alongside
`createPinia()`, so every component reaches strings through the standard `$t('area.key')` /
`useI18n()`. Initial locale comes from `detectLocale()` (the host page's `<html lang>`); the
fallback chain is `<detected> → en`. Because area files are namespaced by filename, a component
only needs the namespace it uses — e.g. a button reads `buttons.*`, the legend reads `legend.*`.