From fc6cfbd7e3c699460d940a84877119993cde13a5 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Thu, 23 Oct 2025 11:30:50 +0200 Subject: [PATCH 1/8] added language selection in the explore route --- app/components/header/index.tsx | 5 +++++ app/components/landing/header/language-selector.tsx | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/components/header/index.tsx b/app/components/header/index.tsx index 6f049ee1..9b1eeef0 100644 --- a/app/components/header/index.tsx +++ b/app/components/header/index.tsx @@ -1,3 +1,5 @@ +import { Globe } from "lucide-react"; +import LanguageSelector from "../landing/header/language-selector"; import Download from "./download"; import Home from "./home"; import Menu from "./menu"; @@ -17,6 +19,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..b6d669cf 100644 --- a/app/components/landing/header/language-selector.tsx +++ b/app/components/landing/header/language-selector.tsx @@ -1,4 +1,5 @@ import i18next from "i18next"; +import { Globe } from "lucide-react"; import { useState } from "react"; import { useFetcher, useLoaderData } from "react-router"; import { Button } from "~/components/ui/button"; @@ -26,7 +27,7 @@ export default function LanguageSelector() { onClick={toggleLanguage} className="hover:bg-transparent dark:hover:text-white hover:text-black" > - {locale === "de" ?

DE

:

EN

} + {locale === "de" ?

DE

:

EN

} ); } From b99cb0610c7e0ac17bc6b02aad6bb456aa99273b Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 12 Nov 2025 12:05:24 +0100 Subject: [PATCH 2/8] User can now change the language of the map labels from the explore route. --- app/components/header/index.tsx | 2 +- app/components/map/map.tsx | 56 +++++++++++++++++++++++++++------ app/routes/explore.tsx | 4 ++- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/app/components/header/index.tsx b/app/components/header/index.tsx index 9b1eeef0..fd2a0803 100644 --- a/app/components/header/index.tsx +++ b/app/components/header/index.tsx @@ -19,7 +19,7 @@ export default function Header(props: HeaderProps) {
-
+
diff --git a/app/components/map/map.tsx b/app/components/map/map.tsx index bff47f1a..a084e041 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 { forwardRef, useEffect } 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"; -const Map = forwardRef( +interface CustomMapProps extends MapProps { + language?: string; // 'de', 'en', 'fr', etc. +} + +const Map = forwardRef( ( - // take fog and terrain out of props to resolve error - { children, mapStyle, fog = null, terrain = null, ...props }, + { children, mapStyle, language="en", fog = null, terrain = null, ...props }, ref, ) => { - // get theme from tailwind - const [theme] = "light"; //useTheme(); + const [theme] = "light"; + + const updateMapLanguage = (map: MapboxMap, locale: string) => { + if (!map) return; + + const style = map.getStyle(); + if (!style || !style.layers) return; + + style.layers.forEach((layer: AnyLayer) => { + const layerAny = layer as any; + const layout = layerAny.layout; + if (layout && typeof layout === "object" && 'text-field' in layout) { + const layerId = layerAny.id; + map.setLayoutProperty(layerId, 'text-field', [ + 'coalesce', + ['get', `name_${locale}`], + ['get', 'name'], + ]); + } + }); + }; + + const handleMapLoad = (event: MapboxEvent) => { + updateMapLanguage(event.target as MapboxMap, language); + }; + + // Update language when it changes + useEffect(() => { + if (ref && typeof ref !== 'function' && ref.current) { + updateMapLanguage(ref.current.getMap(), language); + } + }, [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/routes/explore.tsx b/app/routes/explore.tsx index 7ca0b611..5df54c6d 100644 --- a/app/routes/explore.tsx +++ b/app/routes/explore.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { type FeatureCollection, type Point } from "geojson"; import mapboxglcss from "mapbox-gl/dist/mapbox-gl.css?url"; +import { locale } from "moment"; import { useState, useRef } from "react"; import { type MapLayerMouseEvent, @@ -170,7 +171,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); @@ -360,6 +361,7 @@ export default function Explore() { onClick={onMapClick} onMouseMove={handleMouseMove} ref={mapRef} + language={locale} initialViewState={ deviceId ? { latitude: deviceLoc[0], longitude: deviceLoc[1], zoom: 10 } From 618249e4bd93b8b081ff6583cce78ba63626958b Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 12 Nov 2025 16:13:29 +0100 Subject: [PATCH 3/8] This commit fixes the language switching problem and avoids passing custom props. --- app/components/map/map.tsx | 16 +++++++--------- app/routes/explore.tsx | 5 ++--- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/app/components/map/map.tsx b/app/components/map/map.tsx index a084e041..7792f623 100644 --- a/app/components/map/map.tsx +++ b/app/components/map/map.tsx @@ -1,18 +1,16 @@ import { forwardRef, useEffect } 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 { useTranslation } from "react-i18next"; -interface CustomMapProps extends MapProps { - language?: string; // 'de', 'en', 'fr', etc. -} -const Map = forwardRef( +const Map = forwardRef( ( - { children, mapStyle, language="en", fog = null, terrain = null, ...props }, + { children, mapStyle, fog = null, terrain = null, ...props }, ref, ) => { const [theme] = "light"; - + const [, i18n] = useTranslation(); const updateMapLanguage = (map: MapboxMap, locale: string) => { if (!map) return; @@ -34,15 +32,15 @@ const Map = forwardRef( }; const handleMapLoad = (event: MapboxEvent) => { - updateMapLanguage(event.target as MapboxMap, language); + updateMapLanguage(event.target as MapboxMap, i18n.language); }; // Update language when it changes useEffect(() => { if (ref && typeof ref !== 'function' && ref.current) { - updateMapLanguage(ref.current.getMap(), language); + updateMapLanguage(ref.current.getMap(), i18n.language); } - }, [language, ref]); + }, [i18n.language, ref]); return ( Date: Wed, 12 Nov 2025 16:27:38 +0100 Subject: [PATCH 4/8] This commit uses type guardind for the layer instead of casting to any. --- app/components/map/map.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/components/map/map.tsx b/app/components/map/map.tsx index 7792f623..bbf775a7 100644 --- a/app/components/map/map.tsx +++ b/app/components/map/map.tsx @@ -18,11 +18,12 @@ const Map = forwardRef( if (!style || !style.layers) return; style.layers.forEach((layer: AnyLayer) => { - const layerAny = layer as any; - const layout = layerAny.layout; + // 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) { - const layerId = layerAny.id; - map.setLayoutProperty(layerId, 'text-field', [ + map.setLayoutProperty(layer.id, 'text-field', [ 'coalesce', ['get', `name_${locale}`], ['get', 'name'], From 464b112557fd1d8bcf4a0d2a30fe4dd38b8502b1 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Wed, 19 Nov 2025 14:26:38 +0100 Subject: [PATCH 5/8] Fix the bug that the app is not reflecting the default user language setting. --- .../landing/header/language-selector.tsx | 16 ++++++++++++---- app/routes/explore.tsx | 6 +++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/components/landing/header/language-selector.tsx b/app/components/landing/header/language-selector.tsx index b6d669cf..4c5d11f1 100644 --- a/app/components/landing/header/language-selector.tsx +++ b/app/components/landing/header/language-selector.tsx @@ -1,15 +1,23 @@ import i18next from "i18next"; import { Globe } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useFetcher, useLoaderData } from "react-router"; import { Button } from "~/components/ui/button"; -import { type loader } from "~/root"; +// import { type loader } from "~/root"; export default function LanguageSelector() { - const data = useLoaderData(); + const data = useLoaderData(); const fetcher = useFetcher(); const [locale, setLocale] = useState(data.locale || "en"); - + console.log("Locale in LanguageSelector:", locale); + useEffect(() => { + setLocale(data.locale || "en"); + i18next.changeLanguage(data.locale || "en"); + void fetcher.submit( + { language: locale }, + { method: "post", action: "/action/set-language" }, // Persist the new language + ); + }, [data.locale,data.user]); const toggleLanguage = () => { const newLocale = locale === "en" ? "de" : "en"; // Toggle between "en" and "de" setLocale(newLocale); diff --git a/app/routes/explore.tsx b/app/routes/explore.tsx index 339b34ec..50eeafb3 100644 --- a/app/routes/explore.tsx +++ b/app/routes/explore.tsx @@ -132,13 +132,17 @@ export async function loader({ request }: LoaderFunctionArgs) { if (user) { const profile = await getProfileByUserId(user.id); + const userLocale = user.language + ? user.language.split(/[_-]/)[0].toLowerCase() + : "en"; + console.log("User locale:", userLocale); return { devices, user, profile, filteredDevices, filterParams, - locale + locale: userLocale, //phenomena }; } From 0301f0853cb9f352f5fad287ff625eae68625dbc Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Mon, 24 Nov 2025 15:14:45 +0100 Subject: [PATCH 6/8] 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. --- .../landing/header/language-selector.tsx | 14 ++++++-------- app/lib/set-language.server.ts | 7 +++++++ app/routes/_index.tsx | 8 ++++++-- app/routes/explore.login.tsx | 9 ++++++++- app/routes/explore.tsx | 1 - app/utils/session.server.ts | 2 ++ 6 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 app/lib/set-language.server.ts diff --git a/app/components/landing/header/language-selector.tsx b/app/components/landing/header/language-selector.tsx index 4c5d11f1..2fc3dddc 100644 --- a/app/components/landing/header/language-selector.tsx +++ b/app/components/landing/header/language-selector.tsx @@ -9,15 +9,13 @@ export default function LanguageSelector() { const data = useLoaderData(); const fetcher = useFetcher(); const [locale, setLocale] = useState(data.locale || "en"); - console.log("Locale in LanguageSelector:", locale); + // When loader locale changes (e.g. after login), sync state useEffect(() => { - setLocale(data.locale || "en"); - i18next.changeLanguage(data.locale || "en"); - void fetcher.submit( - { language: locale }, - { method: "post", action: "/action/set-language" }, // Persist the new language - ); - }, [data.locale,data.user]); + if (data.locale) { + setLocale(data.locale); + i18next.changeLanguage(data.locale); + } + }, [data.locale]); const toggleLanguage = () => { const newLocale = locale === "en" ? "de" : "en"; // Toggle between "en" and "de" setLocale(newLocale); 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 50eeafb3..88cc6478 100644 --- a/app/routes/explore.tsx +++ b/app/routes/explore.tsx @@ -135,7 +135,6 @@ export async function loader({ request }: LoaderFunctionArgs) { const userLocale = user.language ? user.language.split(/[_-]/)[0].toLowerCase() : "en"; - console.log("User locale:", userLocale); return { devices, user, 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); From 7332bbe672143b3bd45c8a275c37bbc0a8521bf0 Mon Sep 17 00:00:00 2001 From: JerryVincent Date: Mon, 24 Nov 2025 15:58:58 +0100 Subject: [PATCH 7/8] This commit will removes the eslint warnings and a test error which was happening in one of the test routes. --- app/components/header/index.tsx | 1 - app/components/landing/header/language-selector.tsx | 13 +++++++++---- app/components/map/map.tsx | 7 ++++--- tests/routes/api.measurements.spec.ts | 4 ++-- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/components/header/index.tsx b/app/components/header/index.tsx index fd2a0803..67b8fd6f 100644 --- a/app/components/header/index.tsx +++ b/app/components/header/index.tsx @@ -1,4 +1,3 @@ -import { Globe } from "lucide-react"; import LanguageSelector from "../landing/header/language-selector"; import Download from "./download"; import Home from "./home"; diff --git a/app/components/landing/header/language-selector.tsx b/app/components/landing/header/language-selector.tsx index 2fc3dddc..87fb9611 100644 --- a/app/components/landing/header/language-selector.tsx +++ b/app/components/landing/header/language-selector.tsx @@ -11,10 +11,15 @@ export default function LanguageSelector() { const [locale, setLocale] = useState(data.locale || "en"); // When loader locale changes (e.g. after login), sync state useEffect(() => { - if (data.locale) { - setLocale(data.locale); - i18next.changeLanguage(data.locale); - } + if (!data?.locale) return; + 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 = () => { const newLocale = locale === "en" ? "de" : "en"; // Toggle between "en" and "de" diff --git a/app/components/map/map.tsx b/app/components/map/map.tsx index bbf775a7..07e04b47 100644 --- a/app/components/map/map.tsx +++ b/app/components/map/map.tsx @@ -1,12 +1,13 @@ -import { forwardRef, useEffect } 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( ( - { children, mapStyle, fog = null, terrain = null, ...props }, + { children, mapStyle, ...props }, ref, ) => { const [theme] = "light"; 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, } ); From c606e5c1d71c9f4b284bc6918b391f46e4a3029a Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Wed, 26 Nov 2025 12:23:06 +0100 Subject: [PATCH 8/8] refactor: remove unused state from selector --- .../landing/header/language-selector.tsx | 77 +++++++++---------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/app/components/landing/header/language-selector.tsx b/app/components/landing/header/language-selector.tsx index 87fb9611..61a654b3 100644 --- a/app/components/landing/header/language-selector.tsx +++ b/app/components/landing/header/language-selector.tsx @@ -1,44 +1,41 @@ -import i18next from "i18next"; -import { Globe } from "lucide-react"; -import { useEffect, 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"); - // When loader locale changes (e.g. after login), sync state - useEffect(() => { - if (!data?.locale) return; - 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 = () => { - 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 - ); - }; + const data = useLoaderData() + const fetcher = useFetcher() - return ( - - ); + // 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]) + + 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 ( + + ) }