Skip to content

Commit 0e67933

Browse files
Language fix (#662)
* added language selection in the explore route * User can now change the language of the map labels from the explore route. * This commit fixes the language switching problem and avoids passing custom props. * This commit uses type guardind for the layer instead of casting to any. * Fix the bug that the app is not reflecting the default user language setting. * This commit will fix the issues related to switching the language with the user-preferred language and falls back to the default language when a user is logged out. * This commit will removes the eslint warnings and a test error which was happening in one of the test routes. * This commit will fix the issue with the labels that are currently empty for the road markers. * this commit will fix the formatting issues and removed the text-field names that might change in the future mapbox update.. * fix: resolve eslint warning * refactor: reformat language selector * refactor: use type for loader data --------- Co-authored-by: David Scheidt <[email protected]>
1 parent 6153b69 commit 0e67933

File tree

2 files changed

+129
-92
lines changed

2 files changed

+129
-92
lines changed
Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
11
import i18next from 'i18next'
22
import { Globe } from 'lucide-react'
3-
import { useEffect } from 'react'
3+
import { useEffect, useState } from 'react'
44
import { useFetcher, useLoaderData } from 'react-router'
55
import { Button } from '~/components/ui/button'
66
import { type loader } from '~/root'
77

88
export default function LanguageSelector() {
99
const data = useLoaderData<typeof loader>()
1010
const fetcher = useFetcher()
11-
11+
const [locale, setLocale] = useState(data.locale || 'en')
1212
// When loader locale changes (e.g. after login), sync state
1313
useEffect(() => {
1414
if (!data?.locale) return
15-
const updateLang = async () => {
16-
await i18next.changeLanguage(data.locale)
17-
}
18-
void updateLang()
15+
setLocale(data.locale)
16+
void (async () => {
17+
try {
18+
await i18next.changeLanguage(data.locale)
19+
} catch (e) {
20+
// Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler
21+
}
22+
})()
1923
}, [data.locale])
2024

21-
const toggleLanguage = async () => {
22-
const newLocale = (data?.locale ?? 'en') === 'en' ? 'de' : 'en' // Toggle between "en" and "de"
25+
const toggleLanguage = () => {
26+
const newLocale = locale === 'en' ? 'de' : 'en' // Toggle between "en" and "de"
27+
setLocale(newLocale)
28+
void i18next.changeLanguage(newLocale) // Change the language in the app
2329
void fetcher.submit(
2430
{ language: newLocale },
2531
{ method: 'post', action: '/action/set-language' }, // Persist the new language
2632
)
27-
await i18next.changeLanguage(newLocale) // Change the language in the app
2833
}
2934

3035
return (
@@ -35,7 +40,7 @@ export default function LanguageSelector() {
3540
className="hover:bg-transparent hover:text-black dark:hover:text-white"
3641
>
3742
<Globe />
38-
{(data?.locale ?? 'en') === 'de' ? <p>DE</p> : <p>EN</p>}
43+
{locale === 'de' ? <p>DE</p> : <p>EN</p>}
3944
</Button>
4045
)
4146
}

app/components/map/map.tsx

Lines changed: 114 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,119 @@
1-
import type { Map as MapboxMap, AnyLayer, MapboxEvent } from "mapbox-gl";
2-
import { forwardRef, useEffect } from "react";
3-
import { useTranslation } from "react-i18next";
4-
import { type MapProps, type MapRef, NavigationControl, Map as ReactMap } from "react-map-gl";
1+
import {
2+
type Map as MapboxMap,
3+
type AnyLayer,
4+
type MapboxEvent,
5+
} from 'mapbox-gl'
6+
import { forwardRef, useEffect, useCallback } from 'react'
7+
import { useTranslation } from 'react-i18next'
8+
import {
9+
type MapProps,
10+
type MapRef,
11+
NavigationControl,
12+
Map as ReactMap,
13+
} from 'react-map-gl'
514

15+
const Map = forwardRef<MapRef, MapProps>(
16+
({ children, mapStyle, ...props }, ref) => {
17+
const [theme] = 'light'
18+
const { i18n } = useTranslation()
619

20+
const updateMapLanguage = useCallback(
21+
(map: MapboxMap, locale: string): void => {
22+
if (!map) return
723

8-
const Map = forwardRef<MapRef, MapProps>(
9-
(
10-
{ children, mapStyle, ...props },
11-
ref,
12-
) => {
13-
const [theme] = "light";
14-
const [, i18n] = useTranslation();
15-
const updateMapLanguage = (map: MapboxMap, locale: string) => {
16-
if (!map) return;
17-
18-
const style = map.getStyle();
19-
if (!style || !style.layers) return;
20-
21-
style.layers.forEach((layer: AnyLayer) => {
22-
// Uses type guarding instead of casting to any
23-
if (!("layout" in layer)) return;
24-
25-
const layout = layer.layout;
26-
if (layout && typeof layout === "object" && 'text-field' in layout) {
27-
map.setLayoutProperty(layer.id, 'text-field', [
28-
'coalesce',
29-
['get', `name_${locale}`],
30-
['get', 'name'],
31-
]);
32-
}
33-
});
34-
};
35-
36-
const handleMapLoad = (event: MapboxEvent<undefined>) => {
37-
updateMapLanguage(event.target as MapboxMap, i18n.language);
38-
};
39-
40-
// Update language when it changes
41-
useEffect(() => {
42-
if (ref && typeof ref !== 'function' && ref.current) {
43-
updateMapLanguage(ref.current.getMap(), i18n.language);
44-
}
45-
}, [i18n.language, ref]);
46-
47-
return (
48-
<ReactMap
49-
id="osem"
50-
dragRotate={false}
51-
initialViewState={{
52-
longitude: 7.628202,
53-
latitude: 51.961563,
54-
zoom: 2,
55-
}}
56-
mapStyle={
57-
mapStyle || (theme === "dark"
58-
? "mapbox://styles/mapbox/dark-v11"
59-
: "mapbox://styles/mapbox/streets-v12")
60-
}
61-
mapboxAccessToken={ENV.MAPBOX_ACCESS_TOKEN}
62-
pitchWithRotate={false}
63-
projection={{ name: "globe" }}
64-
preserveDrawingBuffer
65-
hash={true}
66-
ref={ref}
67-
style={{
68-
width: "100%",
69-
height: "100%",
70-
position: "fixed",
71-
top: 0,
72-
left: 0,
73-
}}
74-
touchZoomRotate={false}
75-
onLoad={handleMapLoad}
76-
{...props}
77-
>
78-
{children}
79-
<NavigationControl position="bottom-right" showCompass={false} />
80-
</ReactMap>
81-
);
82-
},
83-
);
24+
const style = map.getStyle()
25+
if (!style?.layers) return
26+
27+
const mapboxLocale = locale.split('-')[0]
28+
29+
style.layers.forEach((layer: AnyLayer) => {
30+
if (!('layout' in layer) || !layer.layout) return
31+
32+
const layout = layer.layout as Record<string, unknown>
33+
const textField = layout['text-field']
34+
if (!textField) return
35+
36+
// Skip layers that don't have localized names
37+
if (
38+
layer.id.includes('shield') ||
39+
layer.id.includes('road-number') ||
40+
layer.id.includes('exit') ||
41+
layer.id.includes('ref')
42+
) {
43+
return
44+
}
45+
46+
try {
47+
map.setLayoutProperty(layer.id, 'text-field', [
48+
'coalesce',
49+
['get', `name_${mapboxLocale}`],
50+
['get', 'name_en'],
51+
['get', 'name'],
52+
])
53+
} catch (e) {
54+
console.warn(`Could not set text-field for layer ${layer.id}:`, e)
55+
}
56+
})
57+
},
58+
[],
59+
)
60+
61+
const handleMapLoad = useCallback(
62+
(event: MapboxEvent<undefined>) => {
63+
updateMapLanguage(event.target as MapboxMap, i18n.language)
64+
},
65+
[updateMapLanguage, i18n.language],
66+
)
67+
68+
// Update language when it changes
69+
useEffect(() => {
70+
if (ref && typeof ref !== 'function' && ref.current) {
71+
const map = ref.current.getMap()
72+
if (map.isStyleLoaded()) {
73+
updateMapLanguage(map, i18n.language)
74+
}
75+
}
76+
}, [i18n.language, ref, updateMapLanguage])
77+
78+
return (
79+
<ReactMap
80+
id="osem"
81+
dragRotate={false}
82+
initialViewState={{
83+
longitude: 7.628202,
84+
latitude: 51.961563,
85+
zoom: 2,
86+
}}
87+
mapStyle={
88+
mapStyle ||
89+
(theme === 'dark'
90+
? 'mapbox://styles/mapbox/dark-v11'
91+
: 'mapbox://styles/mapbox/streets-v12')
92+
}
93+
mapboxAccessToken={ENV.MAPBOX_ACCESS_TOKEN}
94+
pitchWithRotate={false}
95+
projection={{ name: 'globe' }}
96+
preserveDrawingBuffer
97+
hash={true}
98+
ref={ref}
99+
style={{
100+
width: '100%',
101+
height: '100%',
102+
position: 'fixed',
103+
top: 0,
104+
left: 0,
105+
}}
106+
touchZoomRotate={false}
107+
onLoad={handleMapLoad}
108+
{...props}
109+
>
110+
{children}
111+
<NavigationControl position="bottom-right" showCompass={false} />
112+
</ReactMap>
113+
)
114+
},
115+
)
84116

85-
Map.displayName = "Map";
117+
Map.displayName = 'Map'
86118

87-
export default Map;
119+
export default Map

0 commit comments

Comments
 (0)