From e53907f42634cbef429ed46de1a51d81a01db6ab Mon Sep 17 00:00:00 2001 From: Rohan Date: Wed, 18 Mar 2026 18:57:29 +0530 Subject: [PATCH 1/4] feat:add map language control --- src/components/map/index.tsx | 16 +++- src/components/map/map-language-control.tsx | 95 +++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/components/map/map-language-control.tsx diff --git a/src/components/map/index.tsx b/src/components/map/index.tsx index 63b3f85..f9fdbc1 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 { MapLanguageControl, updateMapLabels } from './map-language-control'; import { getInitialMapStyle, getCustomStyle, getMapStyleUrl } from './utils'; import { CLICK_DELAY_MS, @@ -770,7 +771,19 @@ export const MapComponent = () => { {...viewState} onMove={(evt) => setViewState(evt.viewState)} onMoveEnd={handleMoveEnd} - onLoad={() => setMapReady(true)} + onLoad={() => { + setMapReady(true); + const lang = localStorage.getItem('map_label_language') ?? 'default'; + if (lang !== 'default') { + updateMapLabels(mapRef.current?.getMap(), lang); + } + }} + onStyleData={() => { + const lang = localStorage.getItem('map_label_language') ?? 'default'; + if (lang !== 'default') { + updateMapLabels(mapRef.current?.getMap(), lang); + } + }} onClick={handleMapClick} onDblClick={handleMapDblClick} onContextMenu={handleMapContextMenu} @@ -803,6 +816,7 @@ export const MapComponent = () => { onStyleChange={handleStyleChange} onCustomStyleLoaded={handleCustomStyleLoaded} /> + diff --git a/src/components/map/map-language-control.tsx b/src/components/map/map-language-control.tsx new file mode 100644 index 0000000..2efdd42 --- /dev/null +++ b/src/components/map/map-language-control.tsx @@ -0,0 +1,95 @@ +import { useState, useCallback } from 'react'; +import { LanguagesIcon } from 'lucide-react'; +import { useMap } from 'react-map-gl/maplibre'; +import { ControlButton, CustomControl } from './custom-control'; +import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; + +const MAP_LANGUAGE_STORAGE_KEY = 'map_label_language'; + +// Shortbread OSM vector tiles only provide name, name_en, and name_de +const mapLanguageOptions = [ + { key: 'default', text: 'Local (Default)', value: 'default' }, + { key: 'en', text: 'English', value: 'en' }, + { key: 'de', text: 'German', value: 'de' }, +]; + +function getInitialMapLanguage(): string { + if (typeof window === 'undefined') return 'default'; + return localStorage.getItem(MAP_LANGUAGE_STORAGE_KEY) ?? 'default'; +} + +export function updateMapLabels( + map: maplibregl.Map | undefined, + language: string +) { + if (!map) return; + + const style = map.getStyle(); + if (!style?.layers) return; + + for (const layer of style.layers) { + if (layer.type !== 'symbol') continue; + + const textField = (layer.layout as Record)?.['text-field']; + if (typeof textField !== 'string') continue; + + // Only update layers that use {name}, {name_en}, or {name_de} + if (!textField.match(/\{name(_[a-z]{2})?\}/)) continue; + + const newTextField = + language === 'default' ? '{name}' : `{name_${language}}`; + + map.setLayoutProperty(layer.id, 'text-field', newTextField); + } +} + +export const MapLanguageControl = () => { + const [selectedLanguage, setSelectedLanguage] = useState( + getInitialMapLanguage + ); + const [open, setOpen] = useState(false); + const { current: map } = useMap(); + + const handleLanguageChange = useCallback( + (langCode: string) => { + setSelectedLanguage(langCode); + localStorage.setItem(MAP_LANGUAGE_STORAGE_KEY, langCode); + setOpen(false); + + const mapInstance = map?.getMap(); + updateMapLabels(mapInstance, langCode); + }, + [map] + ); + + return ( + + + + } + /> + + +
+ {mapLanguageOptions.map((option) => ( + + ))} +
+
+
+
+ ); +}; From c114189d68abaf93c42021bf08498f3482bf1bbc Mon Sep 17 00:00:00 2001 From: Rohan Date: Thu, 19 Mar 2026 12:32:16 +0530 Subject: [PATCH 2/4] feat:added support for lang in existing comp & removed duplicate comp --- src/components/map/index.tsx | 11 +- src/components/map/map-language-control.tsx | 117 ++++++------------ .../settings-panel/settings-panel.tsx | 40 +++--- 3 files changed, 65 insertions(+), 103 deletions(-) diff --git a/src/components/map/index.tsx b/src/components/map/index.tsx index f9fdbc1..80dd25b 100644 --- a/src/components/map/index.tsx +++ b/src/components/map/index.tsx @@ -29,7 +29,7 @@ import OSMIcon from '@/images/osm_icon.png'; import { ToolButton } from './parts/tool-button'; import { MapStyleControl } from './map-style-control'; -import { MapLanguageControl, updateMapLabels } from './map-language-control'; +import { updateMapLabels } from './map-language-control'; import { getInitialMapStyle, getCustomStyle, getMapStyleUrl } from './utils'; import { CLICK_DELAY_MS, @@ -773,14 +773,14 @@ export const MapComponent = () => { onMoveEnd={handleMoveEnd} onLoad={() => { setMapReady(true); - const lang = localStorage.getItem('map_label_language') ?? 'default'; - if (lang !== 'default') { + const lang = localStorage.getItem('directions_language'); + if (lang) { updateMapLabels(mapRef.current?.getMap(), lang); } }} onStyleData={() => { - const lang = localStorage.getItem('map_label_language') ?? 'default'; - if (lang !== 'default') { + const lang = localStorage.getItem('directions_language'); + if (lang) { updateMapLabels(mapRef.current?.getMap(), lang); } }} @@ -816,7 +816,6 @@ export const MapComponent = () => { onStyleChange={handleStyleChange} onCustomStyleLoaded={handleCustomStyleLoaded} /> - diff --git a/src/components/map/map-language-control.tsx b/src/components/map/map-language-control.tsx index 2efdd42..6fd5dd4 100644 --- a/src/components/map/map-language-control.tsx +++ b/src/components/map/map-language-control.tsx @@ -1,95 +1,56 @@ -import { useState, useCallback } from 'react'; -import { LanguagesIcon } from 'lucide-react'; -import { useMap } from 'react-map-gl/maplibre'; -import { ControlButton, CustomControl } from './custom-control'; -import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; - -const MAP_LANGUAGE_STORAGE_KEY = 'map_label_language'; - -// Shortbread OSM vector tiles only provide name, name_en, and name_de -const mapLanguageOptions = [ - { key: 'default', text: 'Local (Default)', value: 'default' }, - { key: 'en', text: 'English', value: 'en' }, - { key: 'de', text: 'German', value: 'de' }, -]; - -function getInitialMapLanguage(): string { - if (typeof window === 'undefined') return 'default'; - return localStorage.getItem(MAP_LANGUAGE_STORAGE_KEY) ?? 'default'; +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, - language: string + 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']; - if (typeof textField !== 'string') continue; - - // Only update layers that use {name}, {name_en}, or {name_de} - if (!textField.match(/\{name(_[a-z]{2})?\}/)) continue; - const newTextField = - language === 'default' ? '{name}' : `{name_${language}}`; - - map.setLayoutProperty(layer.id, 'text-field', newTextField); + // 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'], + ]); + } + } } } - -export const MapLanguageControl = () => { - const [selectedLanguage, setSelectedLanguage] = useState( - getInitialMapLanguage - ); - const [open, setOpen] = useState(false); - const { current: map } = useMap(); - - const handleLanguageChange = useCallback( - (langCode: string) => { - setSelectedLanguage(langCode); - localStorage.setItem(MAP_LANGUAGE_STORAGE_KEY, langCode); - setOpen(false); - - const mapInstance = map?.getMap(); - updateMapLabels(mapInstance, langCode); - }, - [map] - ); - - return ( - - - - } - /> - - -
- {mapLanguageOptions.map((option) => ( - - ))} -
-
-
-
- ); -}; diff --git a/src/components/settings-panel/settings-panel.tsx b/src/components/settings-panel/settings-panel.tsx index 3896858..87bb1cc 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 && ( Date: Thu, 19 Mar 2026 13:22:56 +0530 Subject: [PATCH 3/4] fix: update settings panel tests --- .../settings-panel/settings-panel.spec.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/settings-panel/settings-panel.spec.tsx b/src/components/settings-panel/settings-panel.spec.tsx index 2a6c1c8..98e6518 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); }); }); }); From fbce0907b322a0b1d74167d71a10530663713557 Mon Sep 17 00:00:00 2001 From: Rohan Date: Thu, 19 Mar 2026 14:56:59 +0530 Subject: [PATCH 4/4] fix: persist default language to localStorage on first load --- src/utils/directions-language.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils/directions-language.ts b/src/utils/directions-language.ts index e918f28..f09014e 100644 --- a/src/utils/directions-language.ts +++ b/src/utils/directions-language.ts @@ -38,13 +38,17 @@ export function getDirectionsLanguage(): DirectionsLanguage { const stored = localStorage.getItem(DIRECTIONS_LANGUAGE_STORAGE_KEY); if (!stored) { - return getSystemLanguage(); + const language = getSystemLanguage(); + localStorage.setItem(DIRECTIONS_LANGUAGE_STORAGE_KEY, language); + return language; } const isValid = languageOptions.some((opt) => opt.value === stored); if (!isValid) { - return getSystemLanguage(); + const language = getSystemLanguage(); + localStorage.setItem(DIRECTIONS_LANGUAGE_STORAGE_KEY, language); + return language; } return stored as DirectionsLanguage;