Name the country cards, and give India's reason, in the reader's language

Two untranslated strings on a production print surface, both reported by the
Country Posters plugin after it fixed the same pair on its own wall.

THE CARD NAMES. postersFor returned the country-posters.json record unchanged,
and that record carries its label in English only -- so every card with a poster
was named in English in every locale. label is not decoration: it is the card's
accessible name, the print HUD's title, and the title on the printed list, deck
and packet. It now goes through countryName(iso3, locale) for a non-English
reader. English keeps the data's own names, because Intl's English differs from
them for 32 of these 175 countries and that swap is a recorded product decision.
The sheet generator still receives the original record; the poster itself is
English by the same decision. The locale REF is read, not a snapshot, so a
runtime language switch renames the cards.

INDIA'S REASON. posterUnsupported returned the record's own English note, so one
untranslated sentence sat on the wall in every locale. The record still decides
WHETHER a country is unsupported; only the wording now comes from the catalogue.
The key needs no number in it because the card shows the count directly above.

★ AND IT NEEDED A SECOND TRANSLATOR, which is the part that would have shipped
broken. This page's t() is useRegionsI18n's: it resolves DX-life's English
sentences as keys inside the `regions` namespace, because that is what the page
is cloned from. messages.countryPosterTooMany lives in another area, so t()
would have missed and rendered the literal string "messages.countryPosterTooMany"
on the wall. tKey is vue-i18n's own lookup, added for keys outside this page's
namespace.

Verified in the built bundle rather than the source: DisplayNames present,
countryPosterTooMany present. A probe over the real alpha-2 table confirms the
localisation across ar/ru/zh/de/fr and that an unknown iso3 falls back instead
of throwing. gates:pre 75 PASS / 0 FAIL / 1 NOT MEASURED of 76; i18n-parity and
i18n-keys-exist both pass. Cost +244 B.
This commit is contained in:
driver 2026-09-29 09:34:51 +00:00
parent 92c5422d1c
commit dd7230a029

View file

@ -44,6 +44,8 @@ import { applyPeopleGroupList } from '@map/lists/peopleGroupList.js'
import { SHADOW_BOX_SIZING_RESET } from './src/shadowReset.js'
import { splitLastWord } from './src/useLastWordHighlight.js'
import { useRegionsI18n } from '@map/i18n/useRegionsI18n.js'
import { countryName } from '@map/utils/countryName.js'
import { useOptionalT } from '@map/composables/useOptionalT.js'
// ar renders right-to-left. RTL_LOCALES is the shared set and :dir on our own root is the
// house pattern (doxa-simple-map.vue:18, :246, :1346) — an EMBEDDED element must not reach for
// document.documentElement.dir, which belongs to the host page.
@ -129,6 +131,12 @@ const isRTL = computed(() => RTL_LOCALES.has(i18nLocale.value))
// invisible inside the composite, where the tab strip names the view, and plainly missing on
// /doxa-maps/regions-dashboard/regions-list/index.html, which is a page in its own right.
const { t, tf } = useRegionsI18n()
// ★ A SECOND TRANSLATOR, DELIBERATELY. useRegionsI18n's t() resolves DX-life's English sentences as
// keys inside the `regions` namespace only, which is what this page is cloned from. A dotted key in
// another area — messages.countryPosterTooMany — misses there and would render the literal string
// "messages.countryPosterTooMany" on the wall. tKey is vue-i18n's own lookup, for keys that live
// outside this page's namespace.
const tKey = useOptionalT('')
const headTitle = computed(() => splitLastWord(t('Regions')))
// ★ REGISTER BEBAS NEUE AT DOCUMENT LEVEL, not in the shadow root: @font-face is document-scoped
@ -311,10 +319,27 @@ const POSTER_BY_ISO3 = new Map((posterData.countries || []).map(c => [String(c.i
// Every country in the region gets a card, in PosterWall's own shape. A country with no
// generated poster is NOT dropped — it is passed with a reason, which is what the wall's
// `unsupported` slot is for, so the grid never silently loses a country.
// ★ EACH CARD NAMED IN THE READER'S LANGUAGE. item.label is the card's accessible name, the print
// HUD's title, and the title on the printed list, deck and packet — and country-posters.json carries
// it in English only, so every card was English in every locale. Same fix as the country-posters
// wall, and the same two caveats travel with it:
// · ENGLISH KEEPS THE DATA'S OWN NAMES. Intl's English differs from them for 32 of these 175
// countries, and countryName.js records that swap as a product decision rather than a refactor.
// · THE SHEET GENERATOR STILL GETS THE ORIGINAL RECORD. The poster itself is English by recorded
// decision, so only the card's label is localised — postersFor's result feeds the wall, while
// countrySheet resolves its own country entry from the same json by iso3.
// The i18n locale REF is read, not a snapshot, so a runtime language switch renames the cards.
function localisedPoster(rec) {
const locale = i18nLocale.value || 'en'
if (/^en\b/i.test(locale)) return rec
const name = countryName(rec.iso3, locale)
return name && name !== rec.iso3 ? { ...rec, label: name } : rec
}
function postersFor(region) {
return (region.countries || []).map((c) => {
const rec = POSTER_BY_ISO3.get(String(c.code || '').toUpperCase())
if (rec) return rec
if (rec) return localisedPoster(rec)
return {
id: `country-${String(c.code || '').toLowerCase()}`,
iso3: c.code,
@ -325,10 +350,14 @@ function postersFor(region) {
})
}
// Their own not-yet reason travels WITH the record, so the number the wall prints can never
// drift from the data — country-poster-wall.vue:63 does exactly this. India is the only
// country in the dataset that carries one (855 people groups, no sheet); China is fine at 3x4.
const posterUnsupported = (c) => c.unsupportedReason || null
// ★ THE REASON IS TRANSLATED, THE PRESENCE OF ONE IS STILL THE DATA'S. The record carries its own
// English note ("India has 855 people groups — too many…"), and returning it put one untranslated
// sentence on the wall in every locale. The record still DECIDES whether a country is unsupported;
// only the wording comes from the catalogue now. messages.countryPosterTooMany exists in all
// eleven, and needs no number in it: the card already shows the count directly above.
// India is the only country in the dataset carrying one (855 people groups, no sheet); China is
// fine at 3x4. My own not-yet reason, for a country with no record at all, is already through t().
const posterUnsupported = (c) => (c.unsupportedReason ? tKey('messages.countryPosterTooMany') : null)
// One region's wall is already inside a region section, so it needs no second level of
// collapsing and no grouping: everything in it is that one region.