doxa-maps/library/maps/localize-labels.js
Doxa Maps 501e2699ce
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source acf67c59b
2026-09-28 02:59:32 +00:00

74 lines
3.8 KiB
JavaScript

/**
* localize-labels.js — show the BASEMAP's place names in the reader's language, on both engines.
*
* WHY. Every map translated its own chrome but left the basemap's country, city and street names in
* the style's default (English for Mapbox, Latin + native script for OpenFreeMap), in all eleven
* locales. A reader in Arabic saw an Arabic legend over an English map.
*
* TWO ENGINES, TWO MECHANISMS — each the engine's own supported one:
* Mapbox map.setLanguage(lang). The engine localizes its own styles, including the Standard
* style's IMPORTED basemap, which a layer-by-layer rewrite cannot reach.
* MapLibre OpenFreeMap styles label from OSM name tags, so each basemap symbol layer's text-field
* is pointed at ['coalesce', name:<lang>, name:latin, name]: the translated name where
* OSM has one, else the style's Latin name, else the local name. English restores each
* layer's ORIGINAL expression (remembered per map), so switching back loses nothing.
*
* ★ ONLY THE BASEMAP'S LABELS. Plugins add their own symbol layers (zone titles, people-group names,
* counts) that read their OWN properties. A layer is rewritten only when its text-field reads an OSM
* name field (name, name:latin, name:nonlatin, name_en, name_int, name:en). Anything else is not a
* basemap label and is never touched.
*
* Fail-soft everywhere: a label that stays in the default language is a nicety lost, never a broken
* map.
*/
const OSM_NAME_FIELD = /\[\s*"get"\s*,\s*"name(?::latin|:nonlatin|:en|_en|_int)?"\s*\]|\{name(?::latin|:nonlatin|:en|_en|_int)?\}/
const originals = new WeakMap() // map -> Map(layerId -> original text-field)
function isMaplibreMap(map) {
return typeof window !== 'undefined' && !!window.maplibregl && map instanceof window.maplibregl.Map
}
/**
* Point the basemap's place-name labels at `lang` (every language, English included).
* Call after style.load, and again whenever the reader's language changes.
* @returns {number} how many label layers were localized (0 on Mapbox, which localizes internally)
*/
export function localizeBasemapLabels(map, lang) {
if (!map || !lang) return 0
const code = String(lang).split('-')[0].toLowerCase()
try {
if (!isMaplibreMap(map)) {
if (typeof map.setLanguage === 'function') map.setLanguage(code)
return 0
}
const style = map.getStyle && map.getStyle()
if (!style || !Array.isArray(style.layers)) return 0
let saved = originals.get(map)
if (!saved) { saved = new Map(); originals.set(map, saved) }
let n = 0
for (const layer of style.layers) {
if (layer.type !== 'symbol' || !layer.layout || layer.layout['text-field'] == null) continue
const current = layer.layout['text-field']
const json = JSON.stringify(current)
// Our own expression is recognisable by shape. Anything ELSE is the style's current original —
// re-saved every time, so a setStyle() that reuses layer ids with new expressions (a theme
// switch) never restores the PREVIOUS style's labels.
if (!json.startsWith('["coalesce",["get","name:')) {
if (!OSM_NAME_FIELD.test(json)) continue // a plugin's own label: leave it
saved.set(layer.id, current)
} else if (!saved.has(layer.id)) continue
// English too, not the style's own bilingual labels ("Россия" under "Russia"): those drew every
// native script at world view, so an English page fetched 13 extra glyph ranges (~600 KB, rega:
// Prayer Progress 18 requests / 777 KB in en against 5 / 185 KB in ar). One language per label.
const next = ['coalesce', ['get', 'name:' + code], ['get', 'name:latin'], ['get', 'name']]
map.setLayoutProperty(layer.id, 'text-field', next)
n++
}
return n
} catch (_) {
return 0
}
}
export default localizeBasemapLabels