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, } );