From 0e2ea66dac7c85154a81e54f4167f53052fe71b5 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Tue, 29 Apr 2025 16:38:13 +0200 Subject: [PATCH] Improved i18n handling --- app/localization/README.md | 5 +++++ app/localization/cookie.server.ts | 11 +++++++++++ app/localization/i18n.server.ts | 3 +++ app/root.tsx | 21 +++++++++++++++++++-- 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 app/localization/cookie.server.ts diff --git a/app/localization/README.md b/app/localization/README.md index b1bfd974..5251025e 100644 --- a/app/localization/README.md +++ b/app/localization/README.md @@ -13,6 +13,11 @@ Due to the fact that the server does not care about loading in additional resour pass in `resources` to the `i18next` instance. This provides all the languages to your server which allows it to render the correct language on the server. +The server side also first checks the search param for the `lng` param and uses that as the language, otherwise it checks +the cookie and if it has the users preferred language set. If the cookie is not set it defaults to the fallback language. + +This is useful if the user navigates to a page without the query param set, the server will still be able to render the page in the correct language. + ## Client-side The client-side is a bit more complicated. We do not want to load in all the languages on the client side as it would diff --git a/app/localization/cookie.server.ts b/app/localization/cookie.server.ts new file mode 100644 index 00000000..54816cc6 --- /dev/null +++ b/app/localization/cookie.server.ts @@ -0,0 +1,11 @@ +import { createCookie } from "react-router"; +import { getServerEnv } from "~/env.server"; + +const env = getServerEnv() + +export const localeCookie = createCookie("lng", { + path: "/", + sameSite: "lax", + secure: env.NODE_ENV === "production", + httpOnly: true, +}); \ No newline at end of file diff --git a/app/localization/i18n.server.ts b/app/localization/i18n.server.ts index f92e0b7d..f74a065d 100644 --- a/app/localization/i18n.server.ts +++ b/app/localization/i18n.server.ts @@ -1,11 +1,14 @@ import { RemixI18Next } from "remix-i18next/server" import i18n from "~/localization/i18n" // your i18n configuration file +import { localeCookie } from "./cookie.server" import { resources } from "./resource" const i18next = new RemixI18Next({ detection: { supportedLanguages: i18n.supportedLngs, fallbackLanguage: i18n.fallbackLng, + cookie: localeCookie, + order: ["searchParams", "cookie"] }, // This is the configuration for i18next used // when translating messages server-side only diff --git a/app/root.tsx b/app/root.tsx index 0d50a40c..2d9ba4fc 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,16 +1,33 @@ import { useTranslation } from "react-i18next" -import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse, useRouteError } from "react-router" +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + data, + isRouteErrorResponse, + useRouteError, +} from "react-router" import type { LinksFunction } from "react-router" import { useChangeLanguage } from "remix-i18next/react" import type { Route } from "./+types/root" import { LanguageSwitcher } from "./library/language-switcher" +import { localeCookie } from "./localization/cookie.server" import { ClientHintCheck, getHints } from "./services/client-hints" import tailwindcss from "./tailwind.css?url" export async function loader({ context, request }: Route.LoaderArgs) { const { lang, clientEnv } = context const hints = getHints(request) - return { lang, clientEnv, hints } + return data( + { lang, clientEnv, hints }, + { + headers: { + "Set-Cookie": await localeCookie.serialize(lang), + }, + } + ) } export const links: LinksFunction = () => [{ rel: "stylesheet", href: tailwindcss }]