diff --git a/app/components/landing/header/language-selector.tsx b/app/components/landing/header/language-selector.tsx index 61a654b3..c51eb50a 100644 --- a/app/components/landing/header/language-selector.tsx +++ b/app/components/landing/header/language-selector.tsx @@ -1,6 +1,6 @@ import i18next from 'i18next' import { Globe } from 'lucide-react' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { useFetcher, useLoaderData } from 'react-router' import { Button } from '~/components/ui/button' import { type loader } from '~/root' @@ -8,23 +8,28 @@ import { type loader } from '~/root' export default function LanguageSelector() { const data = useLoaderData() const fetcher = useFetcher() - + const [locale, setLocale] = useState(data.locale || 'en') // When loader locale changes (e.g. after login), sync state useEffect(() => { if (!data?.locale) return - const updateLang = async () => { - await i18next.changeLanguage(data.locale) - } - void updateLang() + setLocale(data.locale) + void (async () => { + try { + await i18next.changeLanguage(data.locale) + } catch (e) { + // Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler + } + })() }, [data.locale]) - const toggleLanguage = async () => { - const newLocale = (data?.locale ?? 'en') === 'en' ? 'de' : 'en' // Toggle between "en" and "de" + const toggleLanguage = () => { + const newLocale = locale === 'en' ? 'de' : 'en' // Toggle between "en" and "de" + setLocale(newLocale) + void i18next.changeLanguage(newLocale) // Change the language in the app void fetcher.submit( { language: newLocale }, { method: 'post', action: '/action/set-language' }, // Persist the new language ) - await i18next.changeLanguage(newLocale) // Change the language in the app } return ( @@ -35,7 +40,7 @@ export default function LanguageSelector() { className="hover:bg-transparent hover:text-black dark:hover:text-white" > - {(data?.locale ?? 'en') === 'de' ?

DE

:

EN

} + {locale === 'de' ?

DE

:

EN

} ) } diff --git a/app/components/map/map.tsx b/app/components/map/map.tsx index 07e04b47..c5071d18 100644 --- a/app/components/map/map.tsx +++ b/app/components/map/map.tsx @@ -1,87 +1,119 @@ -import type { Map as MapboxMap, AnyLayer, MapboxEvent } from "mapbox-gl"; -import { forwardRef, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { type MapProps, type MapRef, NavigationControl, Map as ReactMap } from "react-map-gl"; +import { + type Map as MapboxMap, + type AnyLayer, + type MapboxEvent, +} from 'mapbox-gl' +import { forwardRef, useEffect, useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { + type MapProps, + type MapRef, + NavigationControl, + Map as ReactMap, +} from 'react-map-gl' +const Map = forwardRef( + ({ children, mapStyle, ...props }, ref) => { + const [theme] = 'light' + const { i18n } = useTranslation() + const updateMapLanguage = useCallback( + (map: MapboxMap, locale: string): void => { + if (!map) return -const Map = forwardRef( - ( - { children, mapStyle, ...props }, - ref, - ) => { - const [theme] = "light"; - const [, i18n] = useTranslation(); - const updateMapLanguage = (map: MapboxMap, locale: string) => { - if (!map) return; - - const style = map.getStyle(); - if (!style || !style.layers) return; - - style.layers.forEach((layer: AnyLayer) => { - // Uses type guarding instead of casting to any - if (!("layout" in layer)) return; - - const layout = layer.layout; - if (layout && typeof layout === "object" && 'text-field' in layout) { - map.setLayoutProperty(layer.id, 'text-field', [ - 'coalesce', - ['get', `name_${locale}`], - ['get', 'name'], - ]); - } - }); - }; - - const handleMapLoad = (event: MapboxEvent) => { - updateMapLanguage(event.target as MapboxMap, i18n.language); - }; - - // Update language when it changes - useEffect(() => { - if (ref && typeof ref !== 'function' && ref.current) { - updateMapLanguage(ref.current.getMap(), i18n.language); - } - }, [i18n.language, ref]); - - return ( - - {children} - - - ); - }, -); + const style = map.getStyle() + if (!style?.layers) return + + const mapboxLocale = locale.split('-')[0] + + style.layers.forEach((layer: AnyLayer) => { + if (!('layout' in layer) || !layer.layout) return + + const layout = layer.layout as Record + const textField = layout['text-field'] + if (!textField) return + + // Skip layers that don't have localized names + if ( + layer.id.includes('shield') || + layer.id.includes('road-number') || + layer.id.includes('exit') || + layer.id.includes('ref') + ) { + return + } + + try { + map.setLayoutProperty(layer.id, 'text-field', [ + 'coalesce', + ['get', `name_${mapboxLocale}`], + ['get', 'name_en'], + ['get', 'name'], + ]) + } catch (e) { + console.warn(`Could not set text-field for layer ${layer.id}:`, e) + } + }) + }, + [], + ) + + const handleMapLoad = useCallback( + (event: MapboxEvent) => { + updateMapLanguage(event.target as MapboxMap, i18n.language) + }, + [updateMapLanguage, i18n.language], + ) + + // Update language when it changes + useEffect(() => { + if (ref && typeof ref !== 'function' && ref.current) { + const map = ref.current.getMap() + if (map.isStyleLoaded()) { + updateMapLanguage(map, i18n.language) + } + } + }, [i18n.language, ref, updateMapLanguage]) + + return ( + + {children} + + + ) + }, +) -Map.displayName = "Map"; +Map.displayName = 'Map' -export default Map; \ No newline at end of file +export default Map