diff --git a/client/src/components/Journey/JourneyMapGL.tsx b/client/src/components/Journey/JourneyMapGL.tsx index ce4abc81..3eb4f1cc 100644 --- a/client/src/components/Journey/JourneyMapGL.tsx +++ b/client/src/components/Journey/JourneyMapGL.tsx @@ -5,7 +5,7 @@ import 'mapbox-gl/dist/mapbox-gl.css' import 'maplibre-gl/dist/maplibre-gl.css' import { useSettingsStore } from '../../store/settingsStore' import { isStandardFamily, supportsCustom3d, wantsTerrain, addCustom3dBuildings, addTerrainAndSky } from '../Map/mapboxSetup' -import { MAPBOX_DEFAULT_STYLE, styleForActiveProvider, type GlMapProvider } from '../Map/glProviders' +import { MAPBOX_DEFAULT_STYLE, styleForActiveProvider, basemapLanguage, type GlMapProvider } from '../Map/glProviders' export interface JourneyMapGLHandle { highlightMarker: (id: string | null) => void @@ -215,6 +215,7 @@ const JourneyMapGL = forwardRef(function JourneyMapGL const mapboxToken = useSettingsStore(s => s.settings.mapbox_access_token || '') const mapbox3d = useSettingsStore(s => s.settings.mapbox_3d_enabled !== false) const mapboxQuality = useSettingsStore(s => s.settings.mapbox_quality_mode === true) + const mapLang = useSettingsStore(s => s.settings.language) const isMapLibre = glProvider === 'maplibre-gl' const gl = (isMapLibre ? maplibregl : mapboxgl) as any const glStyle = styleForActiveProvider(glProvider, rawMapboxStyle, rawMaplibreStyle) @@ -375,6 +376,11 @@ const JourneyMapGL = forwardRef(function JourneyMapGL if (glStyle === MAPBOX_DEFAULT_STYLE) { try { map.setTerrain(null) } catch { /* noop */ } } + // Pin the basemap label language to the UI language so labels don't fall back to the + // browser/OS locale and stack multiple scripts per place (#1299). + if (!isMapLibre && isStandardFamily(glStyle)) { + try { map.setConfigProperty('basemap', 'language', basemapLanguage(mapLang)) } catch { /* style/SDK may not support it */ } + } // route trail — dashed line connecting entries in time order if (items.length > 1) { diff --git a/client/src/components/Map/MapViewGL.tsx b/client/src/components/Map/MapViewGL.tsx index aad764f6..65e790a3 100644 --- a/client/src/components/Map/MapViewGL.tsx +++ b/client/src/components/Map/MapViewGL.tsx @@ -11,7 +11,7 @@ import { CATEGORY_ICON_MAP } from '../shared/categoryIcons' import { isStandardFamily, supportsCustom3d, wantsTerrain, addCustom3dBuildings, addTerrainAndSky } from './mapboxSetup' import { attachLocationMarker, type LocationMarkerHandle } from './locationMarkerMapbox' import { ReservationMapboxOverlay } from './reservationsMapbox' -import { MAPBOX_DEFAULT_STYLE, styleForActiveProvider, type GlMapProvider } from './glProviders' +import { MAPBOX_DEFAULT_STYLE, styleForActiveProvider, basemapLanguage, type GlMapProvider } from './glProviders' import LocationButton from './LocationButton' import { useGeolocation } from '../../hooks/useGeolocation' import type { Place, Reservation } from '../../types' @@ -183,6 +183,7 @@ export function MapViewGL({ const mapbox3d = useSettingsStore(s => s.settings.mapbox_3d_enabled !== false) const mapboxQuality = useSettingsStore(s => s.settings.mapbox_quality_mode === true) const showEndpointLabels = useSettingsStore(s => s.settings.map_booking_labels) !== false + const mapLang = useSettingsStore(s => s.settings.language) const isMapLibre = glProvider === 'maplibre-gl' const gl = (isMapLibre ? maplibregl : mapboxgl) as any const glStyle = styleForActiveProvider(glProvider, rawMapboxStyle, rawMaplibreStyle) @@ -412,6 +413,16 @@ export function MapViewGL({ } }, [glProvider, glStyle, mapboxToken, enableMapbox3d, mapboxQuality]) // rebuild on provider/style changes only + // Pin the basemap label language to the UI language so labels don't fall back to the + // browser/OS locale and stack multiple scripts per place (e.g. "India/भारत/India", #1299). + // Mapbox Standard exposes this via a basemap config property; classic and MapLibre styles + // are left as-is. Runs on load (mapReady) and whenever the UI language changes. + useEffect(() => { + const map = mapRef.current + if (!map || !mapReady || isMapLibre || !isStandardFamily(glStyle)) return + try { map.setConfigProperty('basemap', 'language', basemapLanguage(mapLang)) } catch { /* style/SDK may not support the basemap language property */ } + }, [mapLang, mapReady, isMapLibre, glStyle]) + // Photo loading — mirrors the Leaflet MapView. Updates via RAF to batch // simultaneous thumb arrivals into one re-render. const pendingThumbsRef = useRef>({}) diff --git a/client/src/components/Map/glProviders.test.ts b/client/src/components/Map/glProviders.test.ts index 1c138686..899c9f83 100644 --- a/client/src/components/Map/glProviders.test.ts +++ b/client/src/components/Map/glProviders.test.ts @@ -5,6 +5,7 @@ import { isOpenFreeMapStyle, normalizeStyleForProvider, styleForActiveProvider, + basemapLanguage, } from './glProviders' describe('glProviders', () => { @@ -52,4 +53,20 @@ describe('glProviders', () => { // An empty MapLibre slot falls back to the OpenFreeMap default, leaving mapbox untouched. expect(styleForActiveProvider('maplibre-gl', mb, '')).toBe(OPENFREEMAP_DEFAULT_STYLE) }) + + it('basemapLanguage maps TREK UI codes to basemap label codes (#1299)', () => { + // Pass-through for plain ISO 639-1 codes. + expect(basemapLanguage('en')).toBe('en') + expect(basemapLanguage('de')).toBe('de') + expect(basemapLanguage('fr')).toBe('fr') + // TREK-specific overrides. + expect(basemapLanguage('br')).toBe('pt') + expect(basemapLanguage('gr')).toBe('el') + expect(basemapLanguage('zh')).toBe('zh-Hans') + expect(basemapLanguage('zhTw')).toBe('zh-Hant') + expect(basemapLanguage('zh-TW')).toBe('zh-Hant') + // Falls back to English when unset. + expect(basemapLanguage(undefined)).toBe('en') + expect(basemapLanguage('')).toBe('en') + }) }) diff --git a/client/src/components/Map/glProviders.ts b/client/src/components/Map/glProviders.ts index e120edcd..05710e5e 100644 --- a/client/src/components/Map/glProviders.ts +++ b/client/src/components/Map/glProviders.ts @@ -66,3 +66,22 @@ export function styleForActiveProvider( ): string { return normalizeStyleForProvider(provider, provider === 'maplibre-gl' ? maplibreStyle : mapboxStyle) } + +// A few TREK UI language codes differ from what the GL basemap expects for its labels. +const BASEMAP_LANG_OVERRIDES: Record = { + br: 'pt', // TREK 'br' = Brazilian Portuguese + gr: 'el', // TREK 'gr' = Greek + zh: 'zh-Hans', + zhTw: 'zh-Hant', + 'zh-TW': 'zh-Hant', +} + +/** + * Maps a TREK UI language code to the label language the GL basemap expects. Used to pin + * Mapbox Standard's basemap labels to the user's language so they don't fall back to the + * browser/OS locale and stack multiple scripts per place (#1299). + */ +export function basemapLanguage(uiLang: string | undefined): string { + const code = (uiLang || 'en').trim() + return BASEMAP_LANG_OVERRIDES[code] ?? code +}