Skip to content

Commit 6153b69

Browse files
Changing label language of the map and the app language from explore page (#653)
* 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. * refactor: remove unused state from selector --------- Co-authored-by: David Scheidt <[email protected]> Co-authored-by: David Scheidt <[email protected]>
1 parent 9023c79 commit 6153b69

File tree

9 files changed

+116
-43
lines changed

9 files changed

+116
-43
lines changed

app/components/header/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import LanguageSelector from "../landing/header/language-selector";
12
import Download from "./download";
23
import Home from "./home";
34
import Menu from "./menu";
@@ -17,6 +18,9 @@ export default function Header(props: HeaderProps) {
1718
<Home />
1819
<NavBar devices={props.devices} />
1920
<div className="flex gap-2">
21+
<div className="flex pointer-events-auto items-center h-10 w-16 rounded-full bg-white justify-center shadow-md">
22+
<LanguageSelector/>
23+
</div>
2024
<Download devices={props.devices} />
2125
{/* {data?.user?.email ? <Notification /> : null} */}
2226
<Menu />
Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,41 @@
1-
import i18next from "i18next";
2-
import { useState } from "react";
3-
import { useFetcher, useLoaderData } from "react-router";
4-
import { Button } from "~/components/ui/button";
5-
import { type loader } from "~/root";
1+
import i18next from 'i18next'
2+
import { Globe } from 'lucide-react'
3+
import { useEffect } from 'react'
4+
import { useFetcher, useLoaderData } from 'react-router'
5+
import { Button } from '~/components/ui/button'
6+
import { type loader } from '~/root'
67

78
export default function LanguageSelector() {
8-
const data = useLoaderData<typeof loader>();
9-
const fetcher = useFetcher();
10-
const [locale, setLocale] = useState(data.locale || "en");
9+
const data = useLoaderData<typeof loader>()
10+
const fetcher = useFetcher()
1111

12-
const toggleLanguage = () => {
13-
const newLocale = locale === "en" ? "de" : "en"; // Toggle between "en" and "de"
14-
setLocale(newLocale);
15-
void i18next.changeLanguage(newLocale); // Change the language in the app
16-
void fetcher.submit(
17-
{ language: newLocale },
18-
{ method: "post", action: "/action/set-language" }, // Persist the new language
19-
);
20-
};
12+
// When loader locale changes (e.g. after login), sync state
13+
useEffect(() => {
14+
if (!data?.locale) return
15+
const updateLang = async () => {
16+
await i18next.changeLanguage(data.locale)
17+
}
18+
void updateLang()
19+
}, [data.locale])
2120

22-
return (
23-
<Button
24-
variant="ghost"
25-
size="icon"
26-
onClick={toggleLanguage}
27-
className="hover:bg-transparent dark:hover:text-white hover:text-black"
28-
>
29-
{locale === "de" ? <p>DE</p> : <p>EN</p>}
30-
</Button>
31-
);
21+
const toggleLanguage = async () => {
22+
const newLocale = (data?.locale ?? 'en') === 'en' ? 'de' : 'en' // Toggle between "en" and "de"
23+
void fetcher.submit(
24+
{ language: newLocale },
25+
{ method: 'post', action: '/action/set-language' }, // Persist the new language
26+
)
27+
await i18next.changeLanguage(newLocale) // Change the language in the app
28+
}
29+
30+
return (
31+
<Button
32+
variant="ghost"
33+
size="icon"
34+
onClick={toggleLanguage}
35+
className="hover:bg-transparent hover:text-black dark:hover:text-white"
36+
>
37+
<Globe />
38+
{(data?.locale ?? 'en') === 'de' ? <p>DE</p> : <p>EN</p>}
39+
</Button>
40+
)
3241
}

app/components/map/map.tsx

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,49 @@
1-
import { forwardRef } from "react";
2-
import { type MapProps, type MapRef, NavigationControl, Map as ReactMap } from "react-map-gl";
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";
5+
6+
37

48
const Map = forwardRef<MapRef, MapProps>(
59
(
6-
// take fog and terrain out of props to resolve error
7-
{ children, mapStyle, fog = null, terrain = null, ...props },
10+
{ children, mapStyle, ...props },
811
ref,
912
) => {
10-
// get theme from tailwind
11-
const [theme] = "light"; //useTheme();
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+
1247
return (
1348
<ReactMap
1449
id="osem"
@@ -19,9 +54,9 @@ const Map = forwardRef<MapRef, MapProps>(
1954
zoom: 2,
2055
}}
2156
mapStyle={
22-
theme === "dark"
57+
mapStyle || (theme === "dark"
2358
? "mapbox://styles/mapbox/dark-v11"
24-
: "mapbox://styles/mapbox/streets-v12"
59+
: "mapbox://styles/mapbox/streets-v12")
2560
}
2661
mapboxAccessToken={ENV.MAPBOX_ACCESS_TOKEN}
2762
pitchWithRotate={false}
@@ -37,6 +72,7 @@ const Map = forwardRef<MapRef, MapProps>(
3772
left: 0,
3873
}}
3974
touchZoomRotate={false}
75+
onLoad={handleMapLoad}
4076
{...props}
4177
>
4278
{children}
@@ -48,4 +84,4 @@ const Map = forwardRef<MapRef, MapProps>(
4884

4985
Map.displayName = "Map";
5086

51-
export default Map;
87+
export default Map;

app/lib/set-language.server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// app/lib/set-language.server.ts
2+
//used for setting language cookie during login
3+
import { i18nCookie } from "~/cookies";
4+
5+
export async function setLanguageCookie(lang: string) {
6+
return await i18nCookie.serialize(lang);
7+
}

app/routes/_index.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { type supportedLanguages } from "~/i18next-options";
2222
import i18next from "~/i18next.server";
2323
import { type Partner, getDirectusClient } from "~/lib/directus";
2424
import { getLatestDevices } from "~/models/device.server";
25+
import { getUserByUsername } from "~/models/user.server";
2526
import { getUserId, getUserName } from "~/utils/session.server";
2627

2728
const sections = [
@@ -52,7 +53,7 @@ const sections = [
5253
];
5354

5455
export const loader = async ({ request }: LoaderFunctionArgs) => {
55-
const locale = (await i18next.getLocale(
56+
let locale = (await i18next.getLocale(
5657
request,
5758
)) as (typeof supportedLanguages)[number];
5859
const directus = getDirectusClient();
@@ -84,7 +85,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
8485
//* Get user Id from session
8586
const userId = await getUserId(request);
8687
const userName = await getUserName(request);
87-
88+
const user = userName ? await getUserByUsername(userName) : null;
89+
if(user){
90+
locale = user.language?.split(/[_-]/)[0].toLowerCase() as (typeof supportedLanguages)[number];
91+
}//update the locale in the index route loader if user is logged in
8892
const stats = await fetch("https://api.opensensemap.org/stats").then(
8993
(res) => {
9094
return res.json();

app/routes/explore.login.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { Checkbox } from "~/components/ui/checkbox";
2929
import { verifyLogin } from "~/models/user.server";
3030
import { safeRedirect, validateEmail } from "~/utils";
3131
import { createUserSession, getUserId } from "~/utils/session.server";
32+
import { setLanguageCookie } from "~/lib/set-language.server";
3233

3334
export async function loader({ request }: LoaderFunctionArgs) {
3435
const userId = await getUserId(request);
@@ -65,7 +66,10 @@ export async function action({ request }: ActionFunctionArgs) {
6566
}
6667

6768
const user = await verifyLogin(email, password);
68-
69+
const userLocale = user?.language
70+
? user.language.split(/[_-]/)[0].toLowerCase()
71+
: "en";
72+
6973
if (!user) {
7074
return data(
7175
{ errors: { email: "Invalid email or password", password: null } },
@@ -78,6 +82,9 @@ export async function action({ request }: ActionFunctionArgs) {
7882
userId: user.id,
7983
remember: remember === "on" ? true : false,
8084
redirectTo,
85+
headers: {
86+
"Set-Cookie": await setLanguageCookie(userLocale),
87+
}
8188
});
8289
}
8390

app/routes/explore.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,16 @@ export async function loader({ request }: LoaderFunctionArgs) {
132132

133133
if (user) {
134134
const profile = await getProfileByUserId(user.id);
135+
const userLocale = user.language
136+
? user.language.split(/[_-]/)[0].toLowerCase()
137+
: "en";
135138
return {
136139
devices,
137140
user,
138141
profile,
139142
filteredDevices,
140143
filterParams,
141-
locale,
144+
locale: userLocale,
142145
//phenomena
143146
};
144147
}
@@ -149,6 +152,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
149152
filterParams,
150153
filteredDevices,
151154
message,
155+
locale
152156
//phenomena,
153157
};
154158
}
@@ -170,7 +174,7 @@ if (process.env.NODE_ENV === "production") {
170174

171175
export default function Explore() {
172176
// data from our loader
173-
const { devices, user, profile, filterParams, filteredDevices, message } =
177+
const { devices, user, profile, filterParams, filteredDevices, message,locale } =
174178
useLoaderData<typeof loader>();
175179

176180
const mapRef = useRef<MapRef | null>(null);

app/utils/session.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,13 @@ export async function createUserSession({
117117
userId,
118118
remember,
119119
redirectTo,
120+
headers,
120121
}: {
121122
request: Request;
122123
userId: string;
123124
remember: boolean;
124125
redirectTo: string;
126+
headers?: HeadersInit;// added optional headers parameter
125127
}) {
126128
const session = await getUserSession(request);
127129
session.set(USER_SESSION_KEY, userId);

tests/routes/api.measurements.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ describe("multiple CSV POST /boxes/:id/data", () => {
262262
"Content-Type": "application/sbx-bytes",
263263
Authorization: mockAccessToken,
264264
},
265-
body: byteSubmitData(sensors),
265+
body: byteSubmitData(sensors) as unknown as BodyInit,
266266
}
267267
);
268268

@@ -302,7 +302,7 @@ describe("multiple CSV POST /boxes/:id/data", () => {
302302
"Content-Type": "application/sbx-bytes-ts",
303303
Authorization: mockAccessToken,
304304
},
305-
body: byteSubmitData(sensors, true),
305+
body: byteSubmitData(sensors, true) as unknown as BodyInit,
306306
}
307307
);
308308

0 commit comments

Comments
 (0)