diff --git a/plugin-designs/my-upg-100-list/upg-100-list.vue b/plugin-designs/my-upg-100-list/upg-100-list.vue index d0fec33ab..613b75b93 100644 --- a/plugin-designs/my-upg-100-list/upg-100-list.vue +++ b/plugin-designs/my-upg-100-list/upg-100-list.vue @@ -77,7 +77,7 @@ import ThemeToggleButton from '@map/components/map-controls/ThemeToggleButton.vu // edits src/ (zero blast radius — md5 unchanged). (research-map.vue:141,1815.) import GeocoderComponent from '@map/components/map-controls/GeocoderComponent.vue' import { resolveMapProvider } from '@map/composables/useMapProvider.js' -import { ensureEngineScript, engineGlobal, engineStylesheetUrl } from '@map/composables/useMapInstance.js' +import { ensureEngineScript, engineGlobal, ensureEngineCSS, engineStylesheetUrl } from '@map/composables/useMapInstance.js' // Shapes Arabic / Persian / Hebrew BASEMAP labels. A raw map inherits nothing from the bundle // entry, so this profile has to install it itself — see the call site below for why the entry's // registration only ever worked by accident. @@ -1567,6 +1567,22 @@ function selectedLists() { // HERE — once, against one fetch of the people-groups list — and handed on as the type every // consumer already handles. Resolving per zone would refetch the whole catalogue per zone and // still leave the caller to merge. +// The packet's own string block for the reader's locale, per-KEY fallback to English exactly as +// packet.mjs and packet-render do it — a half-translated block must not blank a key that en has. +let _packetStrings = null; +async function packetStrings() { + if (!_packetStrings) { + const all = await import('@map/scripts/posters/packet-strings.json').then((m) => m.default || m); + _packetStrings = { ...(all.en || {}), ...(all[packetLang()] || {}) }; + } + return _packetStrings; +} +/** `{a}` / `{b}` / `{n}` interpolation; null when the pattern is missing so the caller can default. */ +function fillJoin(pattern, params) { + if (!pattern) return null; + return String(pattern).replace(/\{(\w+)\}/g, (m, k) => (params[k] != null ? String(params[k]) : m)); +} + async function selectionAsList() { const lists = selectedLists(); if (!lists.length) throw new Error('nothing selected'); @@ -1587,10 +1603,20 @@ async function selectionAsList() { } // The label is what the reader sees on the cover and in the print dialog's title. Name the // territories they actually chose rather than inventing a title for their selection. + // ⚠ THE JOINERS WERE HARDCODED ENGLISH on a cover an Arabic reader sees: ' + ' and ' +N more', printed + // straight onto the packet's first page and into the print dialog's title. Found while checking whether + // batch selection was already done (dx-shepherd END-DXS-U100B) — the SELECTION path was, but its label + // was not localized, so a reader choosing three territories in Arabic got their names in Arabic joined + // by an English tail. + // zone_join / zone_join_more already exist in all eleven packet-strings blocks (landed with the zone + // title generator), translated once for exactly this shape, so this is a reuse rather than new strings. const names = lists.map((l) => l.label || l.id); + const S = await packetStrings(); + const join2 = (a, b) => fillJoin(S.zone_join, { a, b }) ?? `${a} + ${b}`; const label = names.length <= 3 - ? names.join(' + ') - : `${names.slice(0, 2).join(' + ')} +${names.length - 2} more`; + ? names.reduce(join2) + : fillJoin(S.zone_join_more, { a: names.slice(0, 2).reduce(join2), n: names.length - 2 }) + ?? `${names.slice(0, 2).join(' + ')} +${names.length - 2} more`; return { id: 'selection', label, count: slugs.size, selector: { type: 'pg_slugs', values: [...slugs] } }; } @@ -2042,6 +2068,29 @@ onMounted(async () => { // mapbox-under-maplibre reference in this profile, carried as a named exemption in the // maplibre-embed gate; guarding it lets that exemption be deleted and enforced instead. // MapLibre ships its own ctrl CSS via ensureEngineScript, so nothing is lost. + // ★ THE LIBRARY'S ensureEngineCSS, NOT A HAND-ROLLED COPY (rega END-REGA-SRV / END-DXS-U100C, + // 2026-09-27). Mapbox logged "missing CSS declarations" on the BUILT leaf and in the gallery, and the + // cause was this block, not a missing : + // (a) it only ran when the map element was inside a ShadowRoot — `root instanceof ShadowRoot` — so on + // a standalone leaf, where the element is in LIGHT DOM, no stylesheet was ensured at all; and + // (b) even in the shadow case it FIRED AND FORGOT, so `new gl.Map` could run before the sheet parsed. + // Mapbox checks its declarations AT CONSTRUCTION, which is exactly when the warning appears. + // ensureEngineCSS handles both DOM cases (light DOM returns early because the wrapper covers it) + // and AWAITS the load/error event with a 2s cap, so construction happens after the sheet is live. + // ⚠ AND IT TAKES THE PROVIDER'S OWN URL rather than 'mapbox' hardcoded — the old line asked the registry + // for mapbox's stylesheet even under MapLibre, guarded by !isMaplibre. Passing upgProvider means the + // guard is unnecessary: a maplibre page gets maplibre's sheet (or none) and never touches + // api.mapbox.com, which is the keyless contract the maplibre-embed gate enforces. + // MEASURED as the difference from cities-internet, which does not warn: connectivity-map.vue:398-399 + // calls exactly this pair and this profile called neither. + try { + const css = engineStylesheetUrl(upgProvider) + if (css) await ensureEngineCSS(mapEl.value, css) + } catch (_) { /* a stylesheet must never stop the map painting */ } + // The legacy shadow-root injection stays ONLY as the belt to that braces: ensureEngineCSS returns early + // for light DOM on the assumption a wrapper exists, and a partner embedding the custom element + // directly may have no wrapper at all. Harmless when ensureEngineCSS already added the same href — both + // paths check for it by href before inserting. try { const shadow = !isMaplibre && mapEl.value && mapEl.value.getRootNode() if (shadow instanceof ShadowRoot) {