doxa-maps/library/packet/mapLabels.js
Doxa Maps 87c59e3a1f Name the map's countries on page 1 in the packet's language
The map sheets are generated once, in English, so in an Arabic or
Chinese packet page 1's map labelled every country in Latin capitals,
and its privacy line was English.

The labels are now replaced as the sheet is lifted into the packet.
They are recognised by the generator's own rule rather than a list:
generate.mjs draws each label as esc(ISO3_NAME[iso3].toUpperCase())
from the zone data's ISO3_NAME table, so inverting that table under
that transform names the country behind every label it can draw. The
inverse is unambiguous (162 labels, no two countries sharing one), so a
label is known exactly or left as drawn. The map's privacy line is word
for word the packet's privacy_note, so it takes this language's.

A width guard: the generator placed every label and then dropped any
that would collide, measuring the English name, so a translation is
fitted into that same box - estimated wider, it shrinks in proportion
and cannot reach a neighbour the English name did not. On the Sudan
poster, six Arabic names shrink (the longest, Saudi Arabia, from 15px
to 10px); none of the narrower Chinese ones do. Capitals' tracking is
dropped for scripts without capitals, where it breaks Arabic joining.

English is untouched: its relabeller is null, and the lifted sheet is
byte-identical with and without it. No sheet or poster is regenerated -
the change is in the lift, so the tracked 3x4 posters page 1 now uses
are relabelled as they are.

Both print paths supply the relabeller: the UPG 100 profile from its
own ISO3_NAME and the library's country-name resolver, the offline
generator from the same table and the injected ISO-3 to ISO-2 table.

check-map-labels (gates:pre), on the tracked Sudan poster: English
byte-identical; in ar and zh no known English name left, halo matching
fill, privacy localized; the guard fires for Arabic, not Chinese, and
grows nothing; Arabic tracking off, Spanish kept; an unknown label left
alone. Mutation-tested five ways, each caught.
2026-09-29 02:16:33 +00:00

39 lines
2.4 KiB
JavaScript

/**
* mapLabels — name the countries on a generated map sheet in the packet's language, at print time.
*
* The map sheets (posters, and the map-only packet page) are generated once, in English: every country
* on the map is labelled with its English name in capitals. In an Arabic or Chinese packet those labels
* printed in Latin capitals. Regenerating a sheet per language would multiply the artwork by eleven; so
* instead the label is recognised on the way into the packet and replaced.
*
* ★ RECOGNISED BY THE GENERATOR'S OWN RULE, NOT A HAND-MADE LIST. generate.mjs writes each label as
* esc(ISO3_NAME[iso3].toUpperCase()), from the ISO3_NAME table in the zone data. Inverting that same
* table under that same transform gives, for every label the generator can draw, the country it names.
* The inverse is unambiguous (no two countries share a label), so a label is either known exactly or
* left alone — never guessed.
*
* Pure and zero-import: the caller injects the table, the country-name resolver its runtime can load,
* and the language, so the browser print and the offline generator run the same code.
*
* @param {{iso3Name: Object<string,string>, countryName: (iso3:string, lang:string)=>string, lang: string}} o
* @returns {null | ((labelText:string) => (string|null))}
* null for English — the sheets are already English, and returning no relabeller is what keeps an
* English packet's output byte-identical. Otherwise a function from a label as it appears in the
* sheet to the localized name, or null for a label it does not know (which is then left as drawn).
*/
export function makeCountryLabelFor({ iso3Name, countryName, lang } = {}) {
const lc = String(lang || 'en').split('-')[0].toLowerCase()
if (lc === 'en' || !iso3Name || typeof countryName !== 'function') return null
const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
const byLabel = new Map()
for (const [iso3, name] of Object.entries(iso3Name)) byLabel.set(esc(String(name).toUpperCase()), iso3)
return (labelText) => {
const iso3 = byLabel.get(String(labelText))
if (!iso3) return null
const name = countryName(iso3, lc)
if (!name || name === iso3) return null
// Capitals as the sheet draws them, in the language's own casing rules; scripts without case
// (Arabic, Devanagari, Chinese) come back unchanged.
return esc(name.toLocaleUpperCase(lc))
}
}