68 lines
3.9 KiB
Markdown
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.*`.
|