From 3a04a941bc83e452c571effbf578c093a3224c84 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sun, 27 Sep 2026 04:37:23 +0000 Subject: [PATCH] The wall intro composes its own markup, so the catalogue holds plain text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit studio landed upg100WallIntroA / Emph / B in all eleven locales (landing 10, f4871401) and kept the legacy upg100WallIntro, so this consumer can land without a flag day. dx-shepherd UPGHTML / studio END-STU-WALLINTRO. ★ THE CATALOGUE NO LONGER CARRIES MARKUP. vue-i18n warned "[intlify] Detected HTML in 'Hand away the prayer task…'" twice per tab pass, and it was right: I measured every en catalogue area and upg100WallIntro was 1 of 1 — the only key in the whole tree with a tag in it. Not a convention I was following; my outlier. ⚠ THE OBVIOUS FIXES DID NOT APPLY, which is why this took a design rather than a one-liner. "Markup in the template" and " interpolation" both assume this profile renders the sentence. It does not: `intro` is a STRING PROP on the library PosterWall, rendered there with v-html (PosterWall.vue:955), and that component has no slots. So the markup must arrive inside the string; the only question was who builds it. Building it here keeps the catalogue clean with no library change. ★ AND IT SHRINKS WHAT v-html TRUSTS, which matters more than the warning. v-html on a whole catalogue string trusts the CATALOGUE: any tag a translator or a bad merge puts in any of the eleven files renders as markup. Composing from parts escapes each part and emits exactly one tag I control, so the trusted surface goes from "every translation of this key" to this line. Verified: a tag planted in a part comes out as <img …>, not as an element. THREE PARTS, NOT A HEAD+EMPHASIS WRAPPER, because the emphasis is not last in every language. All eleven have exactly one and no other tags, but de, hi and zh carry real text AFTER it (" beten.", " प्रार्थना करते हुए।", "為他們禱告。") where the others end on a full stop. A two-key split would have silently dropped those tails. RENDERS IDENTICALLY BEFORE AND AFTER: while the three keys were absent it fell through to the legacy single key, unescaped, exactly as before — escaping THAT would have printed a literal "" to the reader, a visible regression traded for a console warning. Verified against main's catalogue: all eleven locales recompose A + Emph + B to the current upg100WallIntro BYTE-FOR-BYTE, which is the same check studio said they would run before committing, run independently here rather than taken on trust. check:sfc-parse PASS. check:i18n-keys-exist still reports the three keys missing IN THIS WORKTREE, which is correct and expected — they are on main and this branch has not merged it yet; the merge in my landing window resolves it. --- .../my-upg-100-list/poster-wall.vue | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/plugin-designs/my-upg-100-list/poster-wall.vue b/plugin-designs/my-upg-100-list/poster-wall.vue index 2879c81e7..92e20aa25 100644 --- a/plugin-designs/my-upg-100-list/poster-wall.vue +++ b/plugin-designs/my-upg-100-list/poster-wall.vue @@ -192,7 +192,46 @@ async function fetchListRecords(list) { // Product call taken by dx-shepherd (UPGHK, 2026-09-27) under the Driver's standing goal that the // production maps be localized. const t = useOptionalT('') -const intro = computed(() => t('messages.upg100WallIntro')) + +// ★ THE MARKUP IS COMPOSED HERE, so the CATALOGUE holds plain text (dx-shepherd UPGHTML, 2026-09-27). +// vue-i18n warns "[intlify] Detected HTML in '…'" twice per tab pass, and it is right to: a translated +// string carrying is the only one in this tree that does — I measured every en catalogue area +// and upg100WallIntro is 1 of 1, so this was not a convention, it was my outlier. +// +// ⚠ WHY THE OBVIOUS FIXES DO NOT APPLY HERE. The shepherd's suggestion — markup in the template, or +// interpolation — assumes this profile renders the sentence. It does not: `intro` is a STRING +// PROP on the library PosterWall, rendered there with v-html (PosterWall.vue:955), and that component +// has no slots at all. So the markup has to arrive inside the string, and the only question is who +// builds the string. Building it HERE keeps the catalogue clean without a library change. +// +// ★ AND IT IS STRICTLY SAFER THAN WHAT IT REPLACES, which is the part worth keeping. v-html on a whole +// catalogue string TRUSTS THE CATALOGUE: any tag a translator (or a bad merge) puts in any of the 11 +// locale files renders as markup. Composing here escapes each part and emits exactly one tag I control, +// so the trusted surface shrinks from "every translation of this key" to "this line". +// +// THREE PARTS, NOT A WRAPPER AROUND ONE, because the emphasis is not in the same place in every +// language. Measured across all 11 locales: each has exactly one and no other tags, and de, hi +// and zh carry real text AFTER the emphasised run (" beten.", " प्रार्थना करते हुए।", "為他們禱告。") +// where the others end on a full stop. A head+emphasis wrapper would have silently dropped those tails. +const escHtml = (s) => String(s) + .replace(/&/g, '&').replace(//g, '>') +const intro = computed(() => { + // The three plain-text keys studio adds (rule 5B — the catalogue is theirs). Until they land, the + // single legacy key is still the source of truth, so this renders identically before AND after: no + // flag day, and my landing does not have to be ordered against theirs. + const a = t('messages.upg100WallIntroA') + const b = t('messages.upg100WallIntroEmph') + const c = t('messages.upg100WallIntroB') + const missing = (v, key) => !v || v === key + if (missing(a, 'messages.upg100WallIntroA') || missing(b, 'messages.upg100WallIntroEmph')) { + // ⚠ THE LEGACY KEY STILL CONTAINS THE MARKUP, so it is passed through unescaped exactly as before. + // Escaping it here would print a literal "" to the reader — a visible regression traded for + // a console warning, which is the wrong way round. + return t('messages.upg100WallIntro') + } + const tail = missing(c, 'messages.upg100WallIntroB') ? '' : escHtml(c) + return `${escHtml(a)}${escHtml(b)}${tail}` +}) const heading = computed(() => t('messages.upg100WallHeading')) const wallTag = computed(() => t('messages.upg100WallTag'))