3.9 KiB
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(inar/de/en/es/fr/pt/ru;hi/it/ro/zhfall back to English) andpopup(inen/fronly). So the on-disk matrix is:en/fr10 files,ar/de/es/pt/ru9,hi/it/ro/zh8. 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/missingWarnare 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 ofindex.js).
Add a custom locale (checklist)
- Copy
locales/en/tolocales/<code>/(ISO-639-1, e.g.sw). Keep all 10 area files (en/carries the fullest set, includingtabs+popup). - 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.
- Add
<code>toSUPPORTED_LOCALESinindex.js(and toRTL_LOCALESif it reads right-to-left). This is the onlyindex.jsedit a new locale needs. - Verify the glob picked it up and keys resolve:
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.*.