diff --git a/internal/check-optional-t-equivalence.mjs b/internal/check-optional-t-equivalence.mjs index 29953db81..6b0950414 100644 --- a/internal/check-optional-t-equivalence.mjs +++ b/internal/check-optional-t-equivalence.mjs @@ -63,6 +63,10 @@ const hostI18n = (table) => () => ({ t: (k) => (k in table ? table[k] : k) }) const { fillParams, qualify } = make(tStatic, throwingI18n) ok(fillParams('a {x} b', { x: 1 }) === 'a 1 b', 'fillParams interpolates') ok(fillParams('a {x} b', {}) === 'a {x} b', 'fillParams leaves an unknown placeholder visible rather than blanking it') + ok(fillParams('a {x} b', { x: null }) === 'a {x} b', + 'fillParams leaves the placeholder for a NULL value -- a key-presence test would render the string "null" into the sentence') + ok(fillParams('a {x} b', { x: 0 }) === 'a 0 b', + 'but 0 is a real value and must still substitute (a truthiness test would drop it)') ok(fillParams('plain') === 'plain', 'fillParams with no params is a no-op') ok(qualify('pplr', 'x') === 'pplr.x' && qualify('pplr', 'pplr.x') === 'pplr.x', 'qualify prefixes once, never twice') } @@ -107,12 +111,31 @@ const SITES = [ ['library/components/hints/PinHint.vue', 'useOptionalI18n', 'PINHINT_FALLBACK[key]'], ['plugin-designs/pplr-data-maps/src/composables/usePplrT.js', 'useOptionalT', null], ['plugin-designs/upg-neighbor/src/composables/useNeighborT.js', 'useOptionalT', null], + // wave 3a — gospel-resources (owner: gospel-plugin). All four fell back to enLookup, i.e. the + // 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'], + ['plugin-designs/gospel-resources/resources-map.vue', 'useOptionalT', 'fallback: enLookup'], ] +// ★ 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 +// engine's language tag. That is a different fact from translating text: `locale` arrives as a ref OR +// a plain string depending on the host (PosterWall.vue records the same variance), so normalising it +// belongs in a deliberate helper change, not in a translator migration. Asserting EXACTLY 2 means a +// third bare call — a new unguarded translator — still fails this gate. +const ALLOWED_BARE = { 'plugin-designs/gospel-resources/resources-map.vue': 2 } for (const [file, helper, fallbackFrag] of SITES) { const code = codeOnly(rf(new URL('../' + file, import.meta.url), 'utf8')) const short = file.split('/').pop() ok(new RegExp(`\\b${helper}\\(`).test(code), `${short}: routes through ${helper}()`) - ok(!/useI18n\(/.test(code), `${short}: no bare useI18n() left in CODE (comments explaining it are fine)`) + const bare = (code.match(/useI18n\(/g) || []).length + const allowed = ALLOWED_BARE[file] || 0 + ok(bare === allowed, allowed + ? `${short}: exactly ${allowed} bare useI18n() left, the documented locale read(s) -- found ${bare}` + : `${short}: no bare useI18n() left in CODE (comments explaining it are fine)`) if (fallbackFrag) { ok(code.includes(fallbackFrag), `${short}: still passes its OWN fallback (${fallbackFrag}) -- migrating it to the catalogue would change what a bare host renders`) diff --git a/library/composables/useOptionalT.js b/library/composables/useOptionalT.js index 32152c8c5..7d886b808 100644 --- a/library/composables/useOptionalT.js +++ b/library/composables/useOptionalT.js @@ -38,7 +38,11 @@ import { tStatic } from '@map/i18n/index.js' /** `{n}` interpolation. Exported so a gate can test it without a Vue setup scope. */ export function fillParams(str, params) { if (!params) return String(str) - return String(str).replace(/\{(\w+)\}/g, (m, k) => (k in params ? String(params[k]) : m)) + // ★ `!= null`, NOT `k in params`. Gospel's interpolate() had it right and my first version did not: + // with { count: null } a key-presence test renders the STRING "null" into the sentence, while this + // leaves `{count}` visible. A visible placeholder is a bug you can see and fix; "null" reads as + // content. Found by comparing against charts.vue before migrating it, not after. + 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 either 'x.y' or 'pplr.x.y'. */ diff --git a/plugin-designs/gospel-resources/charts.vue b/plugin-designs/gospel-resources/charts.vue index c33280267..1e9d84346 100644 --- a/plugin-designs/gospel-resources/charts.vue +++ b/plugin-designs/gospel-resources/charts.vue @@ -45,7 +45,7 @@ * DATA: fetched at runtime from assets/tables/ (people-group-resources.json is 1.2MB; * inlining it into the IIFE would block first paint for every profile in the bundle). */ -import { useI18n } from 'vue-i18n' +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' @@ -64,14 +64,10 @@ const enLookup = (key) => 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 -let tr = (key, params) => interpolate(enLookup(key) ?? String(key), params) -try { - const i18n = useI18n() - tr = (key, params) => { - const v = params ? i18n.t(key, params) : i18n.t(key) - return v === key ? interpolate(enLookup(key) ?? key, params) : v - } -} catch (_) { /* no i18n on this host */ } +// 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 }) 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 caede68f1..8d48acb4f 100644 --- a/plugin-designs/gospel-resources/data-tables.vue +++ b/plugin-designs/gospel-resources/data-tables.vue @@ -20,7 +20,7 @@ * the table's own meta so the method travels with the number. */ import { computed, inject, ref, watch } from 'vue' -import { useI18n } from 'vue-i18n' +import { useOptionalT } from '@map/composables/useOptionalT.js' import { EN_MESSAGES } from '@map/i18n/index.js' import DataTable from '@map/components/tables/DataTable.vue' import { useShadowStyles } from '@map/composables/useShadowStyles.js' @@ -37,11 +37,10 @@ const assetsBase = computed(() => resolveAssetsBase(profileConfig.value)) 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 || {}) -let tr = (key) => enLookup(key) ?? String(key) -try { - const i18n = useI18n() - tr = (key) => { const v = i18n.t(key); return v === key ? (enLookup(key) ?? key) : v } -} catch (_) { /* no i18n on this host: the shipped English catalogue is the fallback */ } +// 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 }) 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 e6e9628d1..c67d3c2df 100644 --- a/plugin-designs/gospel-resources/resources-list.vue +++ b/plugin-designs/gospel-resources/resources-list.vue @@ -1,5 +1,5 @@