diff --git a/src/components/map/index.tsx b/src/components/map/index.tsx index 856a33d3..2129890c 100644 --- a/src/components/map/index.tsx +++ b/src/components/map/index.tsx @@ -29,6 +29,7 @@ import OSMIcon from '@/images/osm_icon.png'; import { ToolButton } from './parts/tool-button'; import { MapStyleControl } from './map-style-control'; +import { updateMapLabels } from './map-language-control'; import { getInitialMapStyle, getCustomStyle, getMapStyleUrl } from './utils'; import { CLICK_DELAY_MS, @@ -787,7 +788,19 @@ export const MapComponent = () => { {...viewState} onMove={(evt) => setViewState(evt.viewState)} onMoveEnd={handleMoveEnd} - onLoad={() => setMapReady(true)} + onLoad={() => { + setMapReady(true); + const lang = localStorage.getItem('directions_language'); + if (lang) { + updateMapLabels(mapRef.current?.getMap(), lang); + } + }} + onStyleData={() => { + const lang = localStorage.getItem('directions_language'); + if (lang) { + updateMapLabels(mapRef.current?.getMap(), lang); + } + }} onClick={handleMapClick} onDblClick={handleMapDblClick} onContextMenu={handleMapContextMenu} diff --git a/src/components/map/map-language-control.tsx b/src/components/map/map-language-control.tsx new file mode 100644 index 00000000..6fd5dd40 --- /dev/null +++ b/src/components/map/map-language-control.tsx @@ -0,0 +1,56 @@ +function getMapLanguageKey(directionsLanguage: string): string | null { + const base = directionsLanguage.split('-')[0]; + if (base === 'en' || base === 'de') { + return base; + } + return null; +} + +export function updateMapLabels( + map: maplibregl.Map | undefined, + directionsLanguage: string +) { + if (!map) return; + + const style = map.getStyle(); + if (!style?.layers) return; + + const langKey = getMapLanguageKey(directionsLanguage); + + for (const layer of style.layers) { + if (layer.type !== 'symbol') continue; + + const textField = (layer.layout as Record)?.['text-field']; + + // Shortbread: simple string like "{name}", "{name_en}", "{name_de}" + if (typeof textField === 'string') { + if (!textField.match(/\{name(_[a-z]{2})?\}/)) continue; + + const newTextField = langKey ? `{name_${langKey}}` : '{name}'; + map.setLayoutProperty(layer.id, 'text-field', newTextField); + continue; + } + + // Alidade Smooth: expression-based, e.g. ["get", "name:latin"] + // or ["coalesce", ["get", "name:en"], ["get", "name:latin"]] + if (Array.isArray(textField)) { + const json = JSON.stringify(textField); + if (!json.includes('"name:') && !json.includes('"name"')) continue; + + if (langKey) { + map.setLayoutProperty(layer.id, 'text-field', [ + 'coalesce', + ['get', `name:${langKey}`], + ['get', 'name:latin'], + ['get', 'name'], + ]); + } else { + map.setLayoutProperty(layer.id, 'text-field', [ + 'coalesce', + ['get', 'name:latin'], + ['get', 'name'], + ]); + } + } + } +} diff --git a/src/components/settings-panel/settings-panel.spec.tsx b/src/components/settings-panel/settings-panel.spec.tsx index 2a6c1c88..98e65184 100644 --- a/src/components/settings-panel/settings-panel.spec.tsx +++ b/src/components/settings-panel/settings-panel.spec.tsx @@ -82,6 +82,14 @@ vi.mock('@/stores/common-store', () => ({ }), })); +vi.mock('react-map-gl/maplibre', () => ({ + useMap: vi.fn(() => ({ mainMap: { getMap: vi.fn() } })), +})); + +vi.mock('@/components/map/map-language-control', () => ({ + updateMapLabels: vi.fn(), +})); + vi.mock('@/hooks/use-directions-queries', () => ({ useDirectionsQuery: vi.fn(() => ({ refetch: mockRefetchDirections, @@ -558,16 +566,15 @@ describe('SettingsPanel', () => { }); describe('Language Picker', () => { - it('should render Directions Language section when activeTab is directions', () => { + it('should render Language section on all tabs', () => { renderWithQueryClient(); - expect(screen.getByText('Directions Language')).toBeInTheDocument(); - expect(screen.getByText('Language')).toBeInTheDocument(); + expect(screen.getAllByText('Language').length).toBeGreaterThan(0); }); - it('should not render Directions Language section when activeTab is isochrones', () => { + it('should render Language section when activeTab is isochrones', () => { mockUseParams.mockReturnValue({ activeTab: 'isochrones' }); renderWithQueryClient(); - expect(screen.queryByText('Directions Language')).not.toBeInTheDocument(); + expect(screen.getAllByText('Language').length).toBeGreaterThan(0); }); it('should use system locale when no language is stored', () => { @@ -598,7 +605,7 @@ describe('SettingsPanel', () => { it('should render language description in help tooltip', () => { renderWithQueryClient(); - expect(screen.getByText('Language')).toBeInTheDocument(); + expect(screen.getAllByText('Language').length).toBeGreaterThan(0); }); }); }); diff --git a/src/components/settings-panel/settings-panel.tsx b/src/components/settings-panel/settings-panel.tsx index 38968588..87bb1cca 100644 --- a/src/components/settings-panel/settings-panel.tsx +++ b/src/components/settings-panel/settings-panel.tsx @@ -11,6 +11,7 @@ import { getDirectionsLanguage, setDirectionsLanguage, } from '@/utils/directions-language'; +import { updateMapLabels } from '@/components/map/map-language-control'; import type { PossibleSettings } from '@/components/types'; import { SliderSetting } from '@/components/ui/slider-setting'; @@ -33,6 +34,7 @@ import { Settings2, } from 'lucide-react'; import { useParams, useSearch } from '@tanstack/react-router'; +import { useMap } from 'react-map-gl/maplibre'; import { useDirectionsQuery } from '@/hooks/use-directions-queries'; import { useIsochronesQuery } from '@/hooks/use-isochrones-queries'; import { CollapsibleSection } from '@/components/ui/collapsible-section'; @@ -50,6 +52,7 @@ export const SettingsPanel = () => { const resetSettings = useCommonStore((state) => state.resetSettings); const toggleSettings = useCommonStore((state) => state.toggleSettings); const [copied, setCopied] = useState(false); + const { mainMap: map } = useMap(); const { refetch: refetchDirections } = useDirectionsQuery(); const { refetch: refetchIsochrones } = useIsochronesQuery(); @@ -66,9 +69,10 @@ export const SettingsPanel = () => { const newLanguage = value as DirectionsLanguage; setDirectionsLanguage(newLanguage); setLanguage(newLanguage); + updateMapLabels(map?.getMap(), newLanguage); refetchDirections(); }, - [refetchDirections] + [refetchDirections, map] ); const handleMakeRequest = useCallback(() => { @@ -144,24 +148,22 @@ export const SettingsPanel = () => {
- {activeTab === 'directions' && ( - - - - )} + + + {hasProfileSettings && ( opt.value === stored); if (!isValid) { - return getSystemLanguage(); + const language = getSystemLanguage(); + localStorage.setItem(DIRECTIONS_LANGUAGE_STORAGE_KEY, language); + return language; } return stored as DirectionsLanguage;