From 10275c36f8dff56c62f34ba4d6e89d028122ee35 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Tue, 29 Sep 2026 09:15:03 +0000 Subject: [PATCH] Template bundle: every string on screen is a key, and the template shows the three habits that keep it translated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The starter every new plugin copies had 7 English strings outside the catalogue: profile A's "map engine not loaded" and all of profile B. Now profile A keeps its error's KEY in state (errorKey) and translates it in the template, with the engine's own message passed in as a value; profile B's heading, its note line and its sentence are keys, and "Served from {file} in the {bundle} bundle." is ONE key rendered with useSentence, the file and bundle names passed in as their own islands so each language orders the sentence itself. t() is called in the template throughout, never frozen into a constant at setup. The README's pieces table and "Your own text" section say the same three habits. Headless, SSR with the real catalogue (_qa-cycle/exp-shepherd/check-template-i18n.mjs): profile B renders in en, ar and zh from template.json, with the sentence in each language's own order (zh puts the bundle first), both islands intact, and no raw key or placeholder. The experimental English scan: template-bundle 7 → 0. --- plugin-designs/template-bundle/README.md | 24 +++++++++++----- .../template-bundle/template-map-a.vue | 17 +++++++---- .../template-bundle/template-map-b.vue | 28 +++++++++++++++---- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/plugin-designs/template-bundle/README.md b/plugin-designs/template-bundle/README.md index 8a6b2e096..72060690d 100644 --- a/plugin-designs/template-bundle/README.md +++ b/plugin-designs/template-bundle/README.md @@ -6,11 +6,12 @@ Your starting point. **Copy this folder, then build your map inside the copy** blank. It's a tiny working bundle plus a `src/` sandbox laid out for your real code: - **`template-map-a.vue` is a real map.** It draws the points in `data/places.geojson` on a basemap, - shows one translated title, and follows the page's engine, language (including right-to-left) and + shows its text translated, and follows the page's engine, language (including right-to-left) and Mapbox token. **It needs no account**: with no token it runs on the free MapLibre engine. Swap in your own points and it is your map. -- **`template-map-b.vue`** is a second, trivial screen (a coloured box). It only shows that one - bundle can hold several screens. Delete it if you want one. +- **`template-map-b.vue`** is a second, trivial screen: a heading and two lines of translated text, + one of them a sentence that carries markup. It shows that one bundle can hold several screens, and + how a sentence is translated. Delete it if you want one. ## In plain English (read this first) @@ -100,11 +101,20 @@ Everything builds into **one** file: `public/dx-maps/template-bundle/template-bu | the pin colour | `src/colors/colors.json` → `pins.selected` | edit the hex | | the engine (Mapbox or MapLibre) | the page: `profile-config` `provider`, `?provider=`, or `window.MAP_PROVIDER`; no token → MapLibre | nothing: leave it to the page | | the language and right-to-left | the page's locale, given by the library's ProfileLoader | nothing: leave it to the page | -| the title | `t('template.title')` from `library/i18n/locales//template.json` | see below | +| the words on screen (the title, the two error lines) | keys in `library/i18n/locales//template.json`, shown with `t()` in the template | see below | -**Your own text.** Translations live in `library/` (every locale in one place), so a new key there -is a library change: ask the build owner to land it. Until then, a plain English string in your -`.vue` is fine. +**Your own text.** Every string a reader sees is a key, in all eleven locales. Translations live in +`library/` (every locale in one place), so a new key there is a library change: ask the build owner +to land it. Until then, a plain English string in your `.vue` is fine. Three habits keep it right: + +- **Call `t()` in the template**, not once at setup. `const title = t('template.title')` freezes the + first language; `{{ t('template.title') }}` follows a page that switches language later. +- **Keep a message's KEY in state, not its words.** An error stores `'template.engineMissing'`, and the + template translates it when it shows (see `errorKey` in `template-map-a.vue`). +- **A sentence is ONE key, even with markup in it.** `template-map-b.vue` renders "Served from {file} + in the {bundle} bundle." with `@map/composables/useSentence.js`; the two names go in as their own + `` elements, so each language can put them where its grammar wants. Never build a sentence + from pieces ("Served from" + name + "in the" …): that fixes English word order in every language. ## The two ways you import diff --git a/plugin-designs/template-bundle/template-map-a.vue b/plugin-designs/template-bundle/template-map-a.vue index 2019dcb89..5254fda41 100644 --- a/plugin-designs/template-bundle/template-map-a.vue +++ b/plugin-designs/template-bundle/template-map-a.vue @@ -19,7 +19,11 @@ * - WITH NO TOKEN it runs on keyless MapLibre, so a first copy works with no account at all. * - one DATA layer from this bundle's own data/ folder (a static import ships it inside the bundle, * so it loads on any host with no extra request); - * - one TRANSLATED string: t('template.title'), from library/i18n/locales//template.json; + * - its TEXT from the catalogue: every string a reader sees is a key in + * library/i18n/locales//template.json (title, and the two error lines), in all eleven locales; + * t() runs IN THE TEMPLATE, so the words follow a host that changes the language after mount. A + * label computed once at setup (`const title = t('template.title')`) stays in the first language + * forever; an error keeps its KEY in state (errorKey), not its English sentence; * - label fonts and emissive paint that are right on either engine (labelFont, emissivePaint). * - a pin colour from THIS bundle's src/colors/colors.json: your own code is a RELATIVE import; * @map/... is only for the shared library. Edit the hex and rebuild to recolour. @@ -44,7 +48,10 @@ const asked = resolveMapProvider(profileConfig) const provider = asked === 'mapbox' && !mapboxToken.value ? 'maplibre' : asked const mapEl = ref(null) -const error = ref('') +// The KEY of what went wrong, not its words: the template translates it, in whatever language is +// current when it is shown. errorDetail is the engine's own message, passed in as a value. +const errorKey = ref('') +const errorDetail = ref('') const { map, initializeMap, destroy } = useMapInstance({ containerRef: mapEl, accessToken: mapboxToken.value, @@ -69,9 +76,9 @@ function addPlaces(m) { } onMounted(async () => { - try { await initializeMap() } catch (e) { error.value = String(e?.message || e); return } + try { await initializeMap() } catch (e) { errorKey.value = 'template.mapFailed'; errorDetail.value = String(e?.message || e); return } const m = map.value - if (!m) { error.value = 'map engine not loaded'; return } + if (!m) { errorKey.value = 'template.engineMissing'; return } // add the data once the style is ready (it may already be, so do not wait for an event that passed) if (m.isStyleLoaded()) addPlaces(m) else m.once('load', () => addPlaces(m)) @@ -83,7 +90,7 @@ onBeforeUnmount(() => destroy())
{{ t('template.title') }}
-
{{ error }}
+
diff --git a/plugin-designs/template-bundle/template-map-b.vue b/plugin-designs/template-bundle/template-map-b.vue index a4443a437..114fdbabd 100644 --- a/plugin-designs/template-bundle/template-map-b.vue +++ b/plugin-designs/template-bundle/template-map-b.vue @@ -14,18 +14,36 @@ * The host chooses which one to show with the `profile` field: * * + * + * Its text shows the two ways a string reaches the catalogue (library/i18n/locales//template.json): + * - a plain label or line: t('template.profileB.title') in the template; + * - a SENTENCE that carries markup: ONE key with named placeholders ("Served from {file} in the + * {bundle} bundle."), rendered by library/composables/useSentence.js. The file and bundle names + * are passed in as their own left-to-right islands, so a translator can move them anywhere + * in the sentence and never has to copy markup. Building it from pieces ("Served from" + + + * "in the" …) would fix English word order into every language. + * The host's note is content, not interface: it is shown as given, inside a translated line. */ -import { inject, computed } from 'vue' +import { inject, computed, h } from 'vue' +import { useI18n } from 'vue-i18n' +import { sentenceParts, keep } from '@map/composables/useSentence.js' +const { t } = useI18n() const config = inject('profileConfig', null) -const note = computed(() => config?.value?.note || '(no note passed in profile-config)') +const note = computed(() => config?.value?.note || '') +const code = (text) => h('code', { dir: 'ltr' }, text) +// A computed VNode, so it re-renders when the language changes (t() reads the current locale). +const servedFrom = computed(() => h('p', sentenceParts( + t('template.profileB.servedFrom', keep('file', 'bundle')), + { file: code('template-map-b.vue'), bundle: code('template-bundle') }, +)))