/** * 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:, 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` ('en' = the style's own labels). * 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 const next = code === 'en' ? saved.get(layer.id) : ['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