diff --git a/apps/web/package.json b/apps/web/package.json index 7a1d10f02..3e528fa03 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -57,6 +57,7 @@ "crypto": "^1.0.1", "dayjs": "^1.11.10", "i18next": "^24.2.2", + "i18next-http-backend": "^3.0.2", "i18next-icu": "^2.3.0", "i18next-resources-to-backend": "^1.2.1", "ics": "^3.1.0", diff --git a/apps/web/src/app/[locale]/(space)/settings/components/language-preference.tsx b/apps/web/src/app/[locale]/(space)/settings/components/language-preference.tsx index cbdfc9d66..e124d52eb 100644 --- a/apps/web/src/app/[locale]/(space)/settings/components/language-preference.tsx +++ b/apps/web/src/app/[locale]/(space)/settings/components/language-preference.tsx @@ -3,7 +3,6 @@ import { Button } from "@rallly/ui/button"; import { Form, FormField, FormItem, FormLabel } from "@rallly/ui/form"; import { ArrowUpRight } from "lucide-react"; import Link from "next/link"; -import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -20,7 +19,6 @@ type FormData = z.infer; export const LanguagePreference = () => { const { i18n } = useTranslation(); - const router = useRouter(); const form = useForm({ defaultValues: { language: i18n.language, @@ -34,7 +32,8 @@ export const LanguagePreference = () => {
{ await updatePreferences({ locale: data.language }); - router.refresh(); + i18n.changeLanguage(data.language); + form.reset({ language: data.language }); })} > - + - - - - - - - - - - {children} - - - - - - - - - + + + + + + + + + {children} + + + + + + + + diff --git a/apps/web/src/i18n/client.tsx b/apps/web/src/i18n/client.tsx index 5e3da29d8..6bcbf388c 100644 --- a/apps/web/src/i18n/client.tsx +++ b/apps/web/src/i18n/client.tsx @@ -1,5 +1,5 @@ "use client"; -import { useParams } from "next/navigation"; +import httpBackend from "i18next-http-backend"; import React from "react"; import { I18nextProvider, @@ -14,12 +14,20 @@ export function useTranslation() { return useTranslationOrg("app"); } -export function I18nProvider({ children }: { children: React.ReactNode }) { - const params = useParams<{ locale: string }>(); - const locale = params?.locale ?? defaultNS; - +export function I18nProvider({ + children, + locale, +}: { + children: React.ReactNode; + locale: string; +}) { const res = useAsync(async () => { - return await initI18next(locale, "app"); + return await initI18next({ + lng: locale, + middleware: (i18n) => { + i18n.use(httpBackend); + }, + }); }); if (!res.value) { @@ -27,7 +35,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) { } return ( - + {children} ); diff --git a/apps/web/src/i18n/i18n.ts b/apps/web/src/i18n/i18n.ts index 3609c720a..590a55175 100644 --- a/apps/web/src/i18n/i18n.ts +++ b/apps/web/src/i18n/i18n.ts @@ -1,22 +1,21 @@ -import type { Namespace } from "i18next"; +import type { i18n, Namespace } from "i18next"; import { createInstance } from "i18next"; import ICU from "i18next-icu"; -import resourcesToBackend from "i18next-resources-to-backend"; import { initReactI18next } from "react-i18next/initReactI18next"; import { getOptions } from "./settings"; -export const initI18next = async (lng: string, ns: Namespace) => { - const i18nInstance = createInstance(); - await i18nInstance - .use(initReactI18next) - .use(ICU) - .use( - resourcesToBackend( - (language: string, namespace: string) => - import(`../../public/locales/${language}/${namespace}.json`), - ), - ) - .init(getOptions(lng, ns)); - return i18nInstance; +export const initI18next = async ({ + lng, + ns, + middleware, +}: { + lng: string; + ns?: Namespace; + middleware: (i18n: i18n) => void; +}) => { + const i18nInstance = createInstance().use(initReactI18next).use(ICU); + middleware(i18nInstance); + const t = await i18nInstance.init(getOptions(lng, ns)); + return { t, i18n: i18nInstance }; }; diff --git a/apps/web/src/i18n/server.ts b/apps/web/src/i18n/server.ts index 8834f719d..f4916a9f3 100644 --- a/apps/web/src/i18n/server.ts +++ b/apps/web/src/i18n/server.ts @@ -1,14 +1,25 @@ +import resourcesToBackend from "i18next-resources-to-backend"; + import { defaultNS } from "@/i18n/settings"; -import { getLocaleFromPath } from "@/utils/locale/get-locale-from-path"; import { initI18next } from "./i18n"; +import { getLocale } from "./server/get-locale"; export async function getTranslation(localeOverride?: string) { - const localeFromPath = getLocaleFromPath(); - const locale = localeOverride || localeFromPath; - const i18nextInstance = await initI18next(locale, defaultNS); + const locale = localeOverride || getLocale(); + const { i18n } = await initI18next({ + lng: locale, + middleware: (i18n) => { + i18n.use( + resourcesToBackend( + (language: string, namespace: string) => + import(`../../public/locales/${language}/${namespace}.json`), + ), + ); + }, + }); return { - t: i18nextInstance.getFixedT(locale, defaultNS), - i18n: i18nextInstance, + t: i18n.getFixedT(locale, defaultNS), + i18n, }; } diff --git a/apps/web/src/i18n/server/get-locale.ts b/apps/web/src/i18n/server/get-locale.ts new file mode 100644 index 000000000..dd1acac63 --- /dev/null +++ b/apps/web/src/i18n/server/get-locale.ts @@ -0,0 +1,11 @@ +import { defaultLocale } from "@rallly/languages"; +import { headers } from "next/headers"; + +export function getLocale() { + const headersList = headers(); + const localeFromHeader = headersList.get("x-locale"); + if (!localeFromHeader) { + return defaultLocale; + } + return localeFromHeader; +} diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index f8ce3ae85..c90fefbe7 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -10,30 +10,23 @@ const supportedLocales = Object.keys(languages); export const middleware = withAuth(async (req) => { const { nextUrl } = req; const newUrl = nextUrl.clone(); + const pathname = newUrl.pathname; const isLoggedIn = req.auth?.user?.email; // if the user is already logged in, don't let them access the login page - if ( - /^\/(login)/.test(newUrl.pathname) && - isLoggedIn && - !newUrl.searchParams.get("invalidSession") - ) { + if (/^\/(login)/.test(pathname) && isLoggedIn) { newUrl.pathname = "/"; return NextResponse.redirect(newUrl); } - // Check if locale is specified in cookie - let locale = req.auth?.user?.locale; - if (locale && supportedLocales.includes(locale)) { - newUrl.pathname = `/${locale}${newUrl.pathname}`; - } else { - // Check if locale is specified in header - locale = getPreferredLocale(req); - newUrl.pathname = `/${locale}${newUrl.pathname}`; + const locale = req.auth?.user?.locale || getPreferredLocale(req); + if (supportedLocales.includes(locale)) { + newUrl.pathname = `/${locale}${pathname}`; } const res = NextResponse.rewrite(newUrl); - res.headers.set("x-pathname", newUrl.pathname); + res.headers.set("x-locale", locale); + res.headers.set("x-pathname", pathname); if (req.auth?.user?.id) { await withPostHog(res, { distinctID: req.auth.user.id }); diff --git a/apps/web/src/utils/locale/get-locale-from-path.ts b/apps/web/src/utils/locale/get-locale-from-path.ts deleted file mode 100644 index 0b4608746..000000000 --- a/apps/web/src/utils/locale/get-locale-from-path.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defaultLocale, supportedLngs } from "@rallly/languages"; -import { headers } from "next/headers"; - -export function getLocaleFromPath() { - const headersList = headers(); - const pathname = headersList.get("x-pathname") || defaultLocale; - const localeFromPath = pathname.split("/")[1]; - return supportedLngs.includes(localeFromPath) - ? localeFromPath - : defaultLocale; -} diff --git a/yarn.lock b/yarn.lock index 5b6a70c88..e6dcc7e52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8102,6 +8102,13 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" +cross-fetch@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz" @@ -9925,6 +9932,13 @@ hyphenate-style-name@^1.0.3: resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz" integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== +i18next-http-backend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz#7c8daa31aa69679e155ec1f96a37846225bdf907" + integrity sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g== + dependencies: + cross-fetch "4.0.0" + i18next-icu@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/i18next-icu/-/i18next-icu-2.3.0.tgz" @@ -11583,6 +11597,13 @@ nice-try@^1.0.4: resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-fetch@^2.6.7: version "2.6.9" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz"