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') }, +)))