From c83a9827ff845f75d8696f5adf8b3a9547ed7333 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Tue, 29 Sep 2026 03:59:18 +0000 Subject: [PATCH] Country Posters and the UPG-100 wall read in the page's language, and walls count in each language's plural forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The i18n + RTL audit of Country Posters (the hardcoded-English gate only reads production plugins, so it had never read this one; its scanner was run on the plugin directly and every hit triaged for reachability, since the country map mounts one tab): - 9 strings a reader sees were English in every locale: the search placeholder, the packet title's "— 50 of N", the map-failed message, the wall's heading, tag, intro, soon message and item nouns, and the view switcher's legend and label. All read from the catalogue now, reusing the existing keys where they exist (tabs.selectView/views, the UPG-100 wall's tag and emphasis). - The wall's section headings are the WAGF region labels in the data, English everywhere; they now resolve through the Regions dashboard's own WAGF table, which gains its two missing regions. - The three WAGF tier tooltips had no keys in any locale (legend.tabInfo.region/block/country). - 0 physical-CSS findings. PosterWall counted "N countries" as a number plus a 2-form noun pair and defaulted the plural to noun + "s" — wrong for Russian and Arabic whatever strings it was given. It takes itemCountKey, a plural catalogue key rendered with the count; useOptionalT forwards a plural count to vue-i18n (the CLDR map picks ru 4 forms, ar 6, zh 1), and a host without i18n picks the English form by vue-i18n's own rule. The people-group counts on the section heads and placeholder cards use wall.peopleGroupCount the same way ("people groups" was hardcoded English on the cards). The UPG-100 wall: its Americas and Europe section headings had no key (single words, invisible to the gate), and it counts its lists with wall.listCount. Checked: plural-forms, i18n-parity, keys-exist, never-translated, and i18n-treeshake against both bundles built (26 bundles carry every area they ask for); gates:pre 66/66. --- library/components/poster/PosterWall.vue | 23 +++++++++---- library/composables/useOptionalT.js | 26 +++++++++++--- library/i18n/locales/ar/dashboard.json | 4 ++- library/i18n/locales/ar/legend.json | 5 ++- library/i18n/locales/ar/messages.json | 8 ++++- library/i18n/locales/ar/poster.json | 4 ++- library/i18n/locales/ar/search.json | 3 +- library/i18n/locales/ar/wall.json | 4 ++- library/i18n/locales/de/dashboard.json | 4 ++- library/i18n/locales/de/legend.json | 5 ++- library/i18n/locales/de/messages.json | 8 ++++- library/i18n/locales/de/poster.json | 4 ++- library/i18n/locales/de/search.json | 3 +- library/i18n/locales/de/wall.json | 4 ++- library/i18n/locales/en/dashboard.json | 4 ++- library/i18n/locales/en/legend.json | 5 ++- library/i18n/locales/en/messages.json | 8 ++++- library/i18n/locales/en/poster.json | 4 ++- library/i18n/locales/en/search.json | 3 +- library/i18n/locales/en/wall.json | 4 ++- library/i18n/locales/es/dashboard.json | 4 ++- library/i18n/locales/es/legend.json | 5 ++- library/i18n/locales/es/messages.json | 8 ++++- library/i18n/locales/es/poster.json | 4 ++- library/i18n/locales/es/search.json | 3 +- library/i18n/locales/es/wall.json | 4 ++- library/i18n/locales/fr/dashboard.json | 4 ++- library/i18n/locales/fr/legend.json | 5 ++- library/i18n/locales/fr/messages.json | 8 ++++- library/i18n/locales/fr/poster.json | 4 ++- library/i18n/locales/fr/search.json | 3 +- library/i18n/locales/fr/wall.json | 4 ++- library/i18n/locales/hi/dashboard.json | 4 ++- library/i18n/locales/hi/legend.json | 5 ++- library/i18n/locales/hi/messages.json | 8 ++++- library/i18n/locales/hi/poster.json | 4 ++- library/i18n/locales/hi/search.json | 3 +- library/i18n/locales/hi/wall.json | 4 ++- library/i18n/locales/it/dashboard.json | 4 ++- library/i18n/locales/it/legend.json | 5 ++- library/i18n/locales/it/messages.json | 8 ++++- library/i18n/locales/it/poster.json | 4 ++- library/i18n/locales/it/search.json | 3 +- library/i18n/locales/it/wall.json | 4 ++- library/i18n/locales/pt/dashboard.json | 4 ++- library/i18n/locales/pt/legend.json | 5 ++- library/i18n/locales/pt/messages.json | 8 ++++- library/i18n/locales/pt/poster.json | 4 ++- library/i18n/locales/pt/search.json | 3 +- library/i18n/locales/pt/wall.json | 4 ++- library/i18n/locales/ro/dashboard.json | 4 ++- library/i18n/locales/ro/legend.json | 5 ++- library/i18n/locales/ro/messages.json | 8 ++++- library/i18n/locales/ro/poster.json | 4 ++- library/i18n/locales/ro/search.json | 3 +- library/i18n/locales/ro/wall.json | 4 ++- library/i18n/locales/ru/dashboard.json | 4 ++- library/i18n/locales/ru/legend.json | 5 ++- library/i18n/locales/ru/messages.json | 8 ++++- library/i18n/locales/ru/poster.json | 4 ++- library/i18n/locales/ru/search.json | 3 +- library/i18n/locales/ru/wall.json | 4 ++- library/i18n/locales/zh/dashboard.json | 4 ++- library/i18n/locales/zh/legend.json | 5 ++- library/i18n/locales/zh/messages.json | 8 ++++- library/i18n/locales/zh/poster.json | 4 ++- library/i18n/locales/zh/search.json | 3 +- library/i18n/locales/zh/wall.json | 4 ++- .../country-posters/countries-map.vue | 6 ++-- .../country-posters/country-poster-wall.vue | 34 +++++++++++++------ .../country-posters/country-posters-mfe.vue | 4 +-- .../my-upg-100-list/poster-wall.vue | 6 +++- 72 files changed, 313 insertions(+), 94 deletions(-) diff --git a/library/components/poster/PosterWall.vue b/library/components/poster/PosterWall.vue index 35b909827..7161fabc4 100644 --- a/library/components/poster/PosterWall.vue +++ b/library/components/poster/PosterWall.vue @@ -47,7 +47,7 @@ import { tStatic } from '../../i18n/index.js' const T = (key, en) => tStatic(key) || en import { useShadowStyles } from '@map/composables/useShadowStyles.js' import { usePosterPrints } from '@map/composables/usePosterPrints.js' -import { useOptionalI18n } from '@map/composables/useOptionalT.js' +import { useOptionalI18n, useOptionalT } from '@map/composables/useOptionalT.js' // Export-modal geometry. A fixed;inset:0 scrim covers only the CONTENT PANE in the cap-5 shell // (contain:layout on .vtl-content). Measured 1024x793 @ (256,107) at 1280x900 — rail clickable // behind the "modal". See library/utils/viewportScrim.js for the measurements and rejected fixes. @@ -70,8 +70,12 @@ const props = defineProps({ itemNoun: { type: String, default: 'list' }, // English plurals are not "+s". A naive pluralizer shipped "28 countrys" to the // section headers, so the plural is a PARAMETER — the caller knows its own noun. - // Defaults to itemNoun + 's', which is right for "list"/"poster". + // ⚠ No "+s" default any more: a noun pair is English-only by construction. itemNounPlural: { type: String, default: '' }, + // ★ THE LOCALIZED WAY TO COUNT ITEMS: a catalogue PLURAL key ("{n} country | {n} countries" in en, + // four forms in ru, six in ar, one in zh), rendered with the count so each locale picks its own CLDR + // form. Wins over itemNoun/itemNounPlural, which remain for callers that have not moved yet. + itemCountKey: { type: String, default: '' }, collapsible: { type: Boolean, default: false }, collapsedBy: { type: Function, default: () => false }, // A card may declare itself unsupported (India: 855 groups do not fit a sheet). @@ -246,7 +250,13 @@ const gridStyle = computed(() => { return { gridTemplateColumns: `repeat(${Math.min(perRow.value, fits)}, minmax(0, 1fr))` } }) -const plural = computed(() => props.itemNounPlural || `${props.itemNoun}s`) +const plural = computed(() => props.itemNounPlural || props.itemNoun) +// Counts are PLURAL catalogue keys, rendered with the number so each locale takes its own CLDR form. +// `t` here is useOptionalI18n's, whose bare-host fallback is humanizeKey — right for button labels, +// wrong for a count — so counts use a catalogue translator of their own (tStatic when no i18n). +const tCount = useOptionalT('') +const countOf = (key, n) => tCount(key, { n: Number(n || 0).toLocaleString() }, Number(n || 0)) +const itemCount = (n) => (props.itemCountKey ? countOf(props.itemCountKey, n) : `${n} ${n === 1 ? props.itemNoun : plural.value}`) const base = computed(() => String(props.assetsBase || './assets').replace(/\/+$/, '')) // ★ A SECOND PLACE TO LOOK, because "poster not generated yet" was being shown for posters that @@ -1087,8 +1097,7 @@ useShadowStyles( ▶

{{ sec.name }}

- {{ sec.total.toLocaleString() }} {{ T('gospel.list.peopleGroups', 'people groups') }} · - {{ sec.items.length }} {{ sec.items.length === 1 ? itemNoun : plural }} + {{ countOf('wall.peopleGroupCount', sec.total) }} · {{ itemCount(sec.items.length) }} @@ -1132,7 +1141,7 @@ useShadowStyles(
{{ item.label }}
-
{{ item.count.toLocaleString() }} people groups
+
{{ countOf('wall.peopleGroupCount', item.count) }}
{{ unsupported(item) }}