diff --git a/app/components/header/index.tsx b/app/components/header/index.tsx
index 6f049ee1..67b8fd6f 100644
--- a/app/components/header/index.tsx
+++ b/app/components/header/index.tsx
@@ -1,3 +1,4 @@
+import LanguageSelector from "../landing/header/language-selector";
import Download from "./download";
import Home from "./home";
import Menu from "./menu";
@@ -17,6 +18,9 @@ export default function Header(props: HeaderProps) {
+
+
+
{/* {data?.user?.email ?
: null} */}
diff --git a/app/components/landing/header/language-selector.tsx b/app/components/landing/header/language-selector.tsx
index d4ef7e29..61a654b3 100644
--- a/app/components/landing/header/language-selector.tsx
+++ b/app/components/landing/header/language-selector.tsx
@@ -1,32 +1,41 @@
-import i18next from "i18next";
-import { useState } from "react";
-import { useFetcher, useLoaderData } from "react-router";
-import { Button } from "~/components/ui/button";
-import { type loader } from "~/root";
+import i18next from 'i18next'
+import { Globe } from 'lucide-react'
+import { useEffect } from 'react'
+import { useFetcher, useLoaderData } from 'react-router'
+import { Button } from '~/components/ui/button'
+import { type loader } from '~/root'
export default function LanguageSelector() {
- const data = useLoaderData
();
- const fetcher = useFetcher();
- const [locale, setLocale] = useState(data.locale || "en");
+ const data = useLoaderData()
+ const fetcher = useFetcher()
- 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
- );
- };
+ // 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()
+ }, [data.locale])
- return (
-
- );
+ const toggleLanguage = async () => {
+ const newLocale = (data?.locale ?? 'en') === 'en' ? 'de' : 'en' // Toggle between "en" and "de"
+ 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 (
+
+ )
}
diff --git a/app/components/map/map.tsx b/app/components/map/map.tsx
index bff47f1a..07e04b47 100644
--- a/app/components/map/map.tsx
+++ b/app/components/map/map.tsx
@@ -1,14 +1,49 @@
-import { forwardRef } from "react";
-import { type MapProps, type MapRef, NavigationControl, Map as ReactMap } from "react-map-gl";
+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";
+
+
const Map = forwardRef(
(
- // take fog and terrain out of props to resolve error
- { children, mapStyle, fog = null, terrain = null, ...props },
+ { children, mapStyle, ...props },
ref,
) => {
- // get theme from tailwind
- const [theme] = "light"; //useTheme();
+ 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 (
(
zoom: 2,
}}
mapStyle={
- theme === "dark"
+ mapStyle || (theme === "dark"
? "mapbox://styles/mapbox/dark-v11"
- : "mapbox://styles/mapbox/streets-v12"
+ : "mapbox://styles/mapbox/streets-v12")
}
mapboxAccessToken={ENV.MAPBOX_ACCESS_TOKEN}
pitchWithRotate={false}
@@ -37,6 +72,7 @@ const Map = forwardRef(
left: 0,
}}
touchZoomRotate={false}
+ onLoad={handleMapLoad}
{...props}
>
{children}
@@ -48,4 +84,4 @@ const Map = forwardRef(
Map.displayName = "Map";
-export default Map;
+export default Map;
\ No newline at end of file
diff --git a/app/lib/set-language.server.ts b/app/lib/set-language.server.ts
new file mode 100644
index 00000000..7775368c
--- /dev/null
+++ b/app/lib/set-language.server.ts
@@ -0,0 +1,7 @@
+// app/lib/set-language.server.ts
+//used for setting language cookie during login
+import { i18nCookie } from "~/cookies";
+
+export async function setLanguageCookie(lang: string) {
+ return await i18nCookie.serialize(lang);
+}
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index a177a6ba..5badc116 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -22,6 +22,7 @@ import { type supportedLanguages } from "~/i18next-options";
import i18next from "~/i18next.server";
import { type Partner, getDirectusClient } from "~/lib/directus";
import { getLatestDevices } from "~/models/device.server";
+import { getUserByUsername } from "~/models/user.server";
import { getUserId, getUserName } from "~/utils/session.server";
const sections = [
@@ -52,7 +53,7 @@ const sections = [
];
export const loader = async ({ request }: LoaderFunctionArgs) => {
- const locale = (await i18next.getLocale(
+ let locale = (await i18next.getLocale(
request,
)) as (typeof supportedLanguages)[number];
const directus = getDirectusClient();
@@ -84,7 +85,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
//* Get user Id from session
const userId = await getUserId(request);
const userName = await getUserName(request);
-
+ const user = userName ? await getUserByUsername(userName) : null;
+ if(user){
+ locale = user.language?.split(/[_-]/)[0].toLowerCase() as (typeof supportedLanguages)[number];
+ }//update the locale in the index route loader if user is logged in
const stats = await fetch("https://api.opensensemap.org/stats").then(
(res) => {
return res.json();
diff --git a/app/routes/explore.login.tsx b/app/routes/explore.login.tsx
index f27fcc6a..10d95633 100644
--- a/app/routes/explore.login.tsx
+++ b/app/routes/explore.login.tsx
@@ -29,6 +29,7 @@ import { Checkbox } from "~/components/ui/checkbox";
import { verifyLogin } from "~/models/user.server";
import { safeRedirect, validateEmail } from "~/utils";
import { createUserSession, getUserId } from "~/utils/session.server";
+import { setLanguageCookie } from "~/lib/set-language.server";
export async function loader({ request }: LoaderFunctionArgs) {
const userId = await getUserId(request);
@@ -65,7 +66,10 @@ export async function action({ request }: ActionFunctionArgs) {
}
const user = await verifyLogin(email, password);
-
+ const userLocale = user?.language
+ ? user.language.split(/[_-]/)[0].toLowerCase()
+ : "en";
+
if (!user) {
return data(
{ errors: { email: "Invalid email or password", password: null } },
@@ -78,6 +82,9 @@ export async function action({ request }: ActionFunctionArgs) {
userId: user.id,
remember: remember === "on" ? true : false,
redirectTo,
+ headers: {
+ "Set-Cookie": await setLanguageCookie(userLocale),
+ }
});
}
diff --git a/app/routes/explore.tsx b/app/routes/explore.tsx
index 7ca0b611..88cc6478 100644
--- a/app/routes/explore.tsx
+++ b/app/routes/explore.tsx
@@ -132,13 +132,16 @@ export async function loader({ request }: LoaderFunctionArgs) {
if (user) {
const profile = await getProfileByUserId(user.id);
+ const userLocale = user.language
+ ? user.language.split(/[_-]/)[0].toLowerCase()
+ : "en";
return {
devices,
user,
profile,
filteredDevices,
filterParams,
- locale,
+ locale: userLocale,
//phenomena
};
}
@@ -149,6 +152,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
filterParams,
filteredDevices,
message,
+ locale
//phenomena,
};
}
@@ -170,7 +174,7 @@ if (process.env.NODE_ENV === "production") {
export default function Explore() {
// data from our loader
- const { devices, user, profile, filterParams, filteredDevices, message } =
+ const { devices, user, profile, filterParams, filteredDevices, message,locale } =
useLoaderData();
const mapRef = useRef(null);
diff --git a/app/utils/session.server.ts b/app/utils/session.server.ts
index 3229d5c3..9fc7394a 100644
--- a/app/utils/session.server.ts
+++ b/app/utils/session.server.ts
@@ -117,11 +117,13 @@ export async function createUserSession({
userId,
remember,
redirectTo,
+ headers,
}: {
request: Request;
userId: string;
remember: boolean;
redirectTo: string;
+ headers?: HeadersInit;// added optional headers parameter
}) {
const session = await getUserSession(request);
session.set(USER_SESSION_KEY, userId);
diff --git a/tests/routes/api.measurements.spec.ts b/tests/routes/api.measurements.spec.ts
index cd0e810e..22add2f2 100644
--- a/tests/routes/api.measurements.spec.ts
+++ b/tests/routes/api.measurements.spec.ts
@@ -262,7 +262,7 @@ describe("multiple CSV POST /boxes/:id/data", () => {
"Content-Type": "application/sbx-bytes",
Authorization: mockAccessToken,
},
- body: byteSubmitData(sensors),
+ body: byteSubmitData(sensors) as unknown as BodyInit,
}
);
@@ -302,7 +302,7 @@ describe("multiple CSV POST /boxes/:id/data", () => {
"Content-Type": "application/sbx-bytes-ts",
Authorization: mockAccessToken,
},
- body: byteSubmitData(sensors, true),
+ body: byteSubmitData(sensors, true) as unknown as BodyInit,
}
);