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 <area>.<key>-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.
109 lines
6.1 KiB
JavaScript
109 lines
6.1 KiB
JavaScript
/**
|
|
* ★ NO `<area>.<key>`-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.
|
|
*
|
|
* `useI18n()` THROWS ("Need to install with `app.use` function") on a host without the plugin, and
|
|
* seven bundles here install none — big-picture, doxa-maps-spa, doxa-vision-card, golden-path-check,
|
|
* people-groups-grid, plugin-studio, stack-demo. An unguarded call in shared code is a blank subtree
|
|
* waiting for the first bare host: that is exactly how library FilterDropdown blanked
|
|
* people-groups-grid, a production plugin.
|
|
*
|
|
* ★ WHY THIS DOES NOT TAKE A FALLBACK CATALOGUE, though the curation spec specified
|
|
* `(namespace, fallbackCatalogue)`. Every existing copy imports its own English JSON
|
|
* (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 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 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.
|
|
*
|
|
* Order of resolution, and each step earns its place:
|
|
* 1. the host's own vue-i18n, when it installed one — so a host that manages locale keeps control;
|
|
* 2. `tStatic`, which honours the runtime/page locale and falls back to English;
|
|
* 3. the key itself, so a missing string is visible as a key rather than as a blank.
|
|
* 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 name (a bare area, no dotted key) — used only to prefix bare keys
|
|
* @returns {(key: string, params?: object) => string}
|
|
*/
|
|
import { useI18n } from 'vue-i18n'
|
|
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)
|
|
// ★ `!= 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 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
|
|
}
|
|
|
|
/**
|
|
* ★ `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() — 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
|
|
* an improvement, but it is a separate decision from "store this fact once", so each site passes the
|
|
* fallback it already had and renders byte-identically.
|
|
* @param {(key:string, params?:object)=>string} [opts.fallback] used instead of tStatic when the host
|
|
* has no i18n, or when its catalogue lacks the key. Omit it to get the catalogue (the default).
|
|
*/
|
|
export function useOptionalT(namespace, opts = {}) {
|
|
const viaFallback = typeof opts.fallback === 'function'
|
|
? (key, params) => fillParams(opts.fallback(qualify(namespace, key), params) ?? key, params)
|
|
: (key, params) => fillParams(tStatic(qualify(namespace, key)) ?? key, params)
|
|
try {
|
|
const i18n = useI18n()
|
|
return (key, params) => {
|
|
const q = qualify(namespace, key)
|
|
const v = i18n.t(q, params || {})
|
|
return v === q ? viaFallback(key, params) : v
|
|
}
|
|
} catch (_) {
|
|
// No i18n on this host. Not an error: seven bundles deliberately install none.
|
|
return viaFallback
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The same guard, for callers that need more than `t`.
|
|
*
|
|
* ★ `te` KEEPS ITS EXISTING CONTRACT DELIBERATELY: on a host with no i18n it answers FALSE, never
|
|
* "true because the catalogue has it". PinHint and SemanticTreeLegend both record why — "te becomes a
|
|
* 'no, I don't have it' so every caller falls through to its own default". Making it consult tStatic
|
|
* would silently take those callers off their own defaults, which is the same class of mistake as
|
|
* moving a fallback.
|
|
*
|
|
* @returns {{ t: Function, te: (key:string)=>boolean, locale: any }}
|
|
*/
|
|
export function useOptionalI18n(namespace, opts = {}) {
|
|
const t = useOptionalT(namespace, opts)
|
|
try {
|
|
const i18n = useI18n()
|
|
return { t, te: (key) => i18n.te(qualify(namespace, key)), locale: i18n.locale }
|
|
} catch (_) {
|
|
return { t, te: () => false, locale: opts.locale ?? 'en' }
|
|
}
|
|
}
|
|
|
|
export default useOptionalT
|