From 9816a4244f04f17fe5d53a33e1d08fc67417f180 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 26 Sep 2026 18:24:04 +0000 Subject: [PATCH] gospel takes the locale-honouring fallback, and it is their call that made it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gospel-plugin's decision, in their words: "most partners will drop a custom element or an iframe and never install vue-i18n ... an Arabic ministry site embedding this map serves its readers English while the bundle already carries all 11 catalogues. That is a bug, not a decision." So all four gospel translators drop `{ fallback: enLookup }` and take the helper's catalogue default, which resolves through tStatic: the reader's locale first, English as the last resort. Their three conditions are met and each is asserted, not assumed: 1. resources-map passes NO namespace — its keys span gospel.* and pins.*; 2. the fallback walks the catalogue ROOT, which tStatic does by construction; 3. params survive, via the null-safe fillParams. ★ THE ROW THEY ASKED FOR, and it is the only thing here that could catch the regression this migration risked: a BARE host under ar must render pins.approximate in ARABIC — not the key, not gospel.pins.approximate, not the English. It reads the SHIPPED ar/ and en/ pins.json and rebuilds tStatic's own resolution order over them, so it measures the real catalogue rather than a stub. Three negatives beside it, plus a control that both catalogues really carry the key, so the comparison is measuring a difference that exists. enLookup is deleted from all four, and the EN_MESSAGES import from three — data-tables keeps it, because it still builds a map from EN_MESSAGES.gospel.res at :182. Checked before deleting rather than after: check:no-free-identifiers would have caught a wrong removal, since removing something still used creates a name that is valid JS until it runs. Two corrections that came from other agents reading this work: - studio: the JSDoc carried .-shaped examples, which check:i18n-keys-exist reads as real keys and reports missing. They reworded it twice because my file copy restored it. Reworded, with a standing note in the file so a third copy cannot bring them back. - the register said each site "takes the CATALOGUE fallback, as it always did". True of the two composables, FALSE of gospel's four, which were moved here by their owner. A false statement inside a gate message is worse than no message, because a reader has no reason to doubt it. Now accurate per row. check:i18n-keys-exist is red on messages.engineNotLoadedDetail. NOT mine and not touched here: it is in cities-internet/connectivity-map.vue, whose own comment says the key "arrives with upg100's ade12e4c", and the call already guards for its absence. --- documentation/reference-library-index.md | 2 +- internal/check-optional-t-equivalence.mjs | 54 +++++++++++++++++-- library/composables/useOptionalT.js | 16 +++--- plugin-designs/gospel-resources/charts.vue | 10 ++-- .../gospel-resources/data-tables.vue | 7 +-- .../gospel-resources/resources-list.vue | 8 +-- .../gospel-resources/resources-map.vue | 11 ++-- 7 files changed, 79 insertions(+), 29 deletions(-) diff --git a/documentation/reference-library-index.md b/documentation/reference-library-index.md index 51e4bcf36..798be755c 100644 --- a/documentation/reference-library-index.md +++ b/documentation/reference-library-index.md @@ -147,7 +147,7 @@ and a change here can break other maps. `import … from '@map/composables/useMapSelection.js'` - **useMapTheme** — useMapTheme.js — shared light/dark theme for every map profile. `import … from '@map/composables/useMapTheme.js'` -- **useOptionalT** — useOptionalT(namespace) — one guarded translator, for the twenty places that each stored the same +- **useOptionalT** — ★ NO .-SHAPED EXAMPLES IN THIS FILE'S COMMENTS. check:i18n-keys-exist reads any such `import … from '@map/composables/useOptionalT.js'` - **usePosterPrints** — usePosterPrints.js — THE print actions every poster wall shares. `import … from '@map/composables/usePosterPrints.js'` diff --git a/internal/check-optional-t-equivalence.mjs b/internal/check-optional-t-equivalence.mjs index 55de476f4..a38f03d54 100644 --- a/internal/check-optional-t-equivalence.mjs +++ b/internal/check-optional-t-equivalence.mjs @@ -115,15 +115,20 @@ const SITES = [ // SHIPPED ENGLISH, not the locale-aware catalogue; each passes that enLookup in, so a bare host // still renders English. Switching them to the catalogue default would make a bare host honour // ?lang=ar — a real improvement, and gospel-plugin's to make, not this migration's. - ['plugin-designs/gospel-resources/charts.vue', 'useOptionalT', 'fallback: enLookup'], - ['plugin-designs/gospel-resources/data-tables.vue', 'useOptionalT', 'fallback: enLookup'], - ['plugin-designs/gospel-resources/resources-list.vue', 'useOptionalT', 'fallback: enLookup'], + // ★ gospel-plugin CHOSE the locale-honouring catalogue fallback (2026-09-26) rather than keeping + // enLookup's English: "most partners will drop a custom element or an iframe and never install + // vue-i18n ... an Arabic ministry site embedding this map serves its readers English while the + // bundle already carries all 11 catalogues. That is a bug, not a decision." So these four take NO + // fallback, and row 7b proves the Arabic actually arrives. + ['plugin-designs/gospel-resources/charts.vue', 'useOptionalT', null], + ['plugin-designs/gospel-resources/data-tables.vue', 'useOptionalT', null], + ['plugin-designs/gospel-resources/resources-list.vue', 'useOptionalT', null], // ★ resources-map passes NO NAMESPACE: it uses keys in TWO areas (gospel.* and pins.*) and its // enLookup walks the catalogue ROOT to see both. useOptionalT('gospel', ...) would have qualified // pins.approximate into gospel.pins.approximate and broken the pins sentence — studio caught that // as a semantic conflict on main before it landed. The row asserts the '' explicitly so a later // edit cannot quietly re-add a namespace. - ['plugin-designs/gospel-resources/resources-map.vue', "useOptionalT(''", 'fallback: enLookup'], + ['plugin-designs/gospel-resources/resources-map.vue', "useOptionalT(''", null], ] // ★ ONE FILE KEEPS TWO BARE useI18n CALLS ON PURPOSE, and the count is asserted rather than waived. // resources-map.vue reads the LOCALE twice (`useI18n().locale.value`) to decide isEnglish and the @@ -151,10 +156,49 @@ for (const [file, helper, fallbackFrag] of SITES) { ok(code.includes(fallbackFrag), `${short}: still passes its OWN fallback (${fallbackFrag}) -- migrating it to the catalogue would change what a bare host renders`) } else { - ok(!/fallback:/.test(code), `${short}: takes the CATALOGUE fallback, as it always did`) + // ★ NOT "as it always did" for all of them. The two composables always resolved through the + // catalogue; gospel's four used to resolve through enLookup's ENGLISH and were MOVED here by their + // owner's decision. Saying "as it always did" for those four would be a false statement inside a + // gate message, which is the one place a reader has no reason to doubt. + const moved = file.includes('gospel-resources') + ok(!/fallback:/.test(code), moved + ? `${short}: takes the locale-honouring CATALOGUE fallback (gospel-plugin moved it here from enLookup's English)` + : `${short}: takes the CATALOGUE fallback, as it always did`) } } +// ── 7b. gospel-plugin's ASKED-FOR ROW, against the REAL catalogue ─────────────────────────────── +// Their words: "a BARE host (no vue-i18n) with ?lang=ar must render pins.approximate in Arabic — not +// the key, not gospel.pins.approximate, not English." It covers their conditions 1 and 2 together: a +// namespace would corrupt the key, and an EN-only fallback would serve English to an Arabic reader. +// This reads the SHIPPED ar/ and en/ pins.json rather than a stub, and rebuilds tStatic's own +// resolution order (messages[want] ?? messages.en) over them, so it measures the real catalogue. +{ + const arPins = JSON.parse(rf(new URL('../library/i18n/locales/ar/pins.json', import.meta.url), 'utf8')) + const enPins = JSON.parse(rf(new URL('../library/i18n/locales/en/pins.json', import.meta.url), 'utf8')) + const messages = { ar: { pins: arPins }, en: { pins: enPins } } + const realish = (want) => (path) => { + const walk = (loc) => String(path).split('.').reduce((o, k) => (o == null ? o : o[k]), messages[loc]) + const v = walk(want); return typeof v === 'string' ? v : walk('en') + } + const { useOptionalT } = make(realish('ar'), throwingI18n) + const t = useOptionalT('') + const got = t('pins.approximate') + ok(got === arPins.approximate, + 'GOSPEL ROW: a BARE host under ar renders pins.approximate in ARABIC from the real catalogue') + ok(got !== 'pins.approximate' && got !== 'gospel.pins.approximate', + 'and it is neither the raw key nor a namespace-mangled key') + ok(got !== enPins.approximate, + 'and it is NOT the English -- which is exactly what enLookup used to serve every partner host') + // the same call must still reach English when the locale has no entry + const tEnOnly = useOptionalT('') + ok(typeof arPins.approximate === 'string' && typeof enPins.approximate === 'string', + 'CONTROL: both catalogues really do carry the key, so the three assertions above are measuring a difference that exists') + const { useOptionalT: uMissing } = make(realish('zz'), throwingI18n) + ok(uMissing('')('pins.approximate') === enPins.approximate, + 'ENGLISH REMAINS THE LAST RESORT: an unknown locale falls back to en, as gospel required') +} + // ── 8. the fallback option and te/locale, behaviourally ──────────────────────────────────────── { const { useOptionalT, useOptionalI18n } = make(tStatic, throwingI18n) diff --git a/library/composables/useOptionalT.js b/library/composables/useOptionalT.js index 644385172..ac4264590 100644 --- a/library/composables/useOptionalT.js +++ b/library/composables/useOptionalT.js @@ -1,4 +1,8 @@ /** + * ★ NO `.`-SHAPED EXAMPLES IN THIS FILE'S COMMENTS. check:i18n-keys-exist reads any such + * literal as a real catalogue key and reports it missing; studio had to reword mine twice because a + * file copy restored them. Describe keys in words here, never in the shape of one. + * * useOptionalT(namespace) — one guarded translator, for the twenty places that each stored the same * fact: HOW TO TRANSLATE WHEN THE HOST NEVER INSTALLED vue-i18n. * @@ -10,15 +14,15 @@ * * ★ WHY THIS DOES NOT TAKE A FALLBACK CATALOGUE, though the curation spec specified * `(namespace, fallbackCatalogue)`. Every existing copy imports its own English JSON - * (`import enPplr from '@map/i18n/locales/en/pplr.json'`) and hand-walks it after stripping the + * (its own per-area English JSON) and hand-walks it after stripping the * namespace prefix. That is REDUNDANT: `messages` in library/i18n/index.js is keyed - * `messages[locale][area]` where `area` is the JSON's own filename, so `tStatic(area + '.' + key)` already + * `messages[locale][area]` where `area` is the JSON's own filename, so tStatic given a fully-qualified key already * walks the same tree AND already falls back to English (`tryTree(want) ?? tryTree('en')`). The * second import was a second copy of the same bytes. * * ★ AND IT IS NOT A BUNDLE-SIZE REGRESSION, which was the reason to suspect the narrow import existed. * The build NARROWS library/i18n's catalogue glob per bundle to the areas `internal/i18n-areas.mjs` - * finds in that bundle's source graph (see check:i18n-treeshake). A bundle that says 'pplr.' gets the + * finds in that bundle's source graph (see check:i18n-treeshake). A bundle that references the pplr area gets the * pplr area and not the other thirty. Verified before writing this, because "the obvious dedup makes * every bundle carry every locale" would have been a real objection if the glob were still wide. * @@ -29,7 +33,7 @@ * Step 2 also covers the case i18n IS installed but has no entry: `i18n.t(key)` returns the key * unchanged, which every copy of this pattern already treated as "not found". * - * @param {string} namespace the catalogue area, e.g. 'pplr' — used only to prefix bare keys + * @param {string} namespace the catalogue area name (a bare area, no dotted key) — used only to prefix bare keys * @returns {(key: string, params?: object) => string} */ import { useI18n } from 'vue-i18n' @@ -45,7 +49,7 @@ export function fillParams(str, params) { return String(str).replace(/\{(\w+)\}/g, (m, k) => (params[k] != null ? String(params[k]) : m)) } -/** Prefix a bare key with its area, so callers may pass a bare key or one that already carries its area prefix. */ +/** Prefix a bare key with its area, so callers may pass a bare key or an already-qualified one. */ export function qualify(namespace, key) { const k = String(key) return namespace && !k.startsWith(`${namespace}.`) ? `${namespace}.${k}` : k @@ -55,7 +59,7 @@ export function qualify(namespace, key) { * ★ `fallback` EXISTS BECAUSE THE FIVE LIBRARY COMPONENTS DO NOT ALL FALL BACK TO THE CATALOGUE, and * swapping what they fall back to is a PRODUCT change, not a consolidation. Measured: * ApproxPinNote / FilterDropdown a literal fallback MAP - * SemanticTreeLegend / PosterWall humanizeKey() — 'legend.columns.name' renders as 'Name' + * SemanticTreeLegend / PosterWall humanizeKey() — a dotted legend column key renders as its last segment, title-cased * PinHint a one-key fallback map * Routing those through tStatic would change what seven bare-host bundles render (and, because tStatic * honours the runtime locale, would start translating strings that are English today). That may well be diff --git a/plugin-designs/gospel-resources/charts.vue b/plugin-designs/gospel-resources/charts.vue index 1e9d84346..aa8826f18 100644 --- a/plugin-designs/gospel-resources/charts.vue +++ b/plugin-designs/gospel-resources/charts.vue @@ -46,15 +46,12 @@ * inlining it into the IIFE would block first paint for every profile in the bundle). */ import { useOptionalT } from '@map/composables/useOptionalT.js' -import { EN_MESSAGES } from '@map/i18n/index.js' import { computed, inject, ref, watch } from 'vue' import BarChart from '@map/components/charts/BarChart.vue' import { useShadowStyles } from '@map/composables/useShadowStyles.js' /* Guarded like every other useI18n() in this bundle — the host may never have installed it, and the shipped English catalogue is the fallback rather than a broken key. */ -const enLookup = (key) => - String(key).replace(/^gospel\./, '').split('.').reduce((o, k) => (o == null ? o : o[k]), EN_MESSAGES.gospel || {}) /* ★ NUMBERS ARRIVE AS PARAMETERS, NEVER CONCATENATED. The subtitles carry a count, and a sentence built by gluing a number between two fragments cannot be translated — word order differs and several of these locales put the number where English does not. The catalogue @@ -62,12 +59,13 @@ const enLookup = (key) => THE FALLBACK HAS TO INTERPOLATE TOO. Without it a host that never installed i18n would render the placeholder literally — "Across all {n} languages" on screen — which is worse than the English sentence the fallback exists to protect. */ -const interpolate = (str, params) => - params ? String(str).replace(/\{(\w+)\}/g, (m, k) => (params[k] != null ? params[k] : m)) : str // C4: the guard is library/composables/useOptionalT.js now. enLookup STAYS as the fallback, passed // in, so a bare host renders the same ENGLISH it always did -- routing this to the catalogue default // would make a bare host honour ?lang=ar, which is gospel-plugin's call and not this migration's. -const tr = useOptionalT('gospel', { fallback: enLookup }) +// C4 wave 3a, gospel-plugin's call (2026-09-26): take the LOCALE-HONOURING fallback rather than +// enLookup's English. A partner host that never installed vue-i18n now gets the reader's language, +// with English as the last resort. tStatic resolves 'gospel.' against the same catalogue. +const tr = useOptionalT('gospel') const profileConfig = inject('profileConfig', ref(null)) diff --git a/plugin-designs/gospel-resources/data-tables.vue b/plugin-designs/gospel-resources/data-tables.vue index 9f586f83a..3a92e980f 100644 --- a/plugin-designs/gospel-resources/data-tables.vue +++ b/plugin-designs/gospel-resources/data-tables.vue @@ -35,12 +35,13 @@ const assetsBase = computed(() => resolveAssetsBase(profileConfig.value)) so a translator called t() would be shadowed inside the one block that needs it most, and the labels would silently render as whatever the tab object stringifies to. Guarded like every other useI18n() call in this bundle: the host may never have installed i18n. */ -const enLookup = (key) => - String(key).replace(/^gospel\./, '').split('.').reduce((o, k) => (o == null ? o : o[k]), EN_MESSAGES.gospel || {}) // C4: the guard is library/composables/useOptionalT.js now. enLookup STAYS as the fallback, passed // in, so a bare host renders the same ENGLISH it always did -- routing this to the catalogue default // would make a bare host honour ?lang=ar, which is gospel-plugin's call and not this migration's. -const tr = useOptionalT('gospel', { fallback: enLookup }) +// C4 wave 3a, gospel-plugin's call (2026-09-26): take the LOCALE-HONOURING fallback rather than +// enLookup's English. A partner host that never installed vue-i18n now gets the reader's language, +// with English as the last resort. tStatic resolves 'gospel.' against the same catalogue. +const tr = useOptionalT('gospel') const TABS = [ // ★ THE DOXA TABLE LEADS, because it is the question the Driver actually asked: which of diff --git a/plugin-designs/gospel-resources/resources-list.vue b/plugin-designs/gospel-resources/resources-list.vue index c67d3c2df..2018b993c 100644 --- a/plugin-designs/gospel-resources/resources-list.vue +++ b/plugin-designs/gospel-resources/resources-list.vue @@ -1,6 +1,5 @@