mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-03 00:19:03 +02:00
♻️ Update locale implementation
This commit is contained in:
parent
42d0077045
commit
99ca9a180d
10 changed files with 132 additions and 85 deletions
|
@ -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",
|
||||
|
|
|
@ -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<typeof formSchema>;
|
|||
|
||||
export const LanguagePreference = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const router = useRouter();
|
||||
const form = useForm<FormData>({
|
||||
defaultValues: {
|
||||
language: i18n.language,
|
||||
|
@ -34,7 +32,8 @@ export const LanguagePreference = () => {
|
|||
<form
|
||||
onSubmit={form.handleSubmit(async (data) => {
|
||||
await updatePreferences({ locale: data.language });
|
||||
router.refresh();
|
||||
i18n.changeLanguage(data.language);
|
||||
form.reset({ language: data.language });
|
||||
})}
|
||||
>
|
||||
<FormField
|
||||
|
|
|
@ -4,7 +4,6 @@ import "../../style.css";
|
|||
import { PostHogProvider } from "@rallly/posthog/client";
|
||||
import { Toaster } from "@rallly/ui/toaster";
|
||||
import { TooltipProvider } from "@rallly/ui/tooltip";
|
||||
import { dehydrate, Hydrate } from "@tanstack/react-query";
|
||||
import { domAnimation, LazyMotion } from "motion/react";
|
||||
import type { Viewport } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
|
@ -13,14 +12,16 @@ import React from "react";
|
|||
|
||||
import { TimeZoneChangeDetector } from "@/app/[locale]/timezone-change-detector";
|
||||
import { UserProvider } from "@/components/user-provider";
|
||||
import { getUser } from "@/data/get-user";
|
||||
import { TimezoneProvider } from "@/features/timezone/client/context";
|
||||
import { I18nProvider } from "@/i18n/client";
|
||||
import { auth } from "@/next-auth";
|
||||
import { getLocale } from "@/i18n/server/get-locale";
|
||||
import { auth, getUserId } from "@/next-auth";
|
||||
import { TRPCProvider } from "@/trpc/client/provider";
|
||||
import { createSSRHelper } from "@/trpc/server/create-ssr-helper";
|
||||
import { ConnectedDayjsProvider } from "@/utils/dayjs";
|
||||
|
||||
import { PostHogPageView } from "../posthog-page-view";
|
||||
import { defaultLocale, supportedLngs } from "@rallly/languages";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
|
@ -32,44 +33,58 @@ export const viewport: Viewport = {
|
|||
initialScale: 1,
|
||||
};
|
||||
|
||||
async function loadLocale() {
|
||||
let locale = getLocale();
|
||||
|
||||
const userId = await getUserId();
|
||||
|
||||
if (userId) {
|
||||
const user = await getUser();
|
||||
if (user.locale) {
|
||||
locale = user.locale;
|
||||
}
|
||||
}
|
||||
|
||||
if (!supportedLngs.includes(locale)) {
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
export default async function Root({
|
||||
children,
|
||||
params: { locale },
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const session = await auth();
|
||||
const trpc = await createSSRHelper();
|
||||
await trpc.user.subscription.prefetch();
|
||||
const locale = await loadLocale();
|
||||
|
||||
return (
|
||||
<html lang={locale} className={inter.className}>
|
||||
<body>
|
||||
<Toaster />
|
||||
<I18nProvider>
|
||||
<I18nProvider locale={locale}>
|
||||
<TRPCProvider>
|
||||
<Hydrate state={dehydrate(trpc.queryClient)}>
|
||||
<LazyMotion features={domAnimation}>
|
||||
<SessionProvider session={session}>
|
||||
<PostHogProvider>
|
||||
<PostHogPageView />
|
||||
<TooltipProvider>
|
||||
<UserProvider>
|
||||
<TimezoneProvider
|
||||
initialTimezone={session?.user?.timeZone ?? undefined}
|
||||
>
|
||||
<ConnectedDayjsProvider>
|
||||
{children}
|
||||
<TimeZoneChangeDetector />
|
||||
</ConnectedDayjsProvider>
|
||||
</TimezoneProvider>
|
||||
</UserProvider>
|
||||
</TooltipProvider>
|
||||
</PostHogProvider>
|
||||
</SessionProvider>
|
||||
</LazyMotion>
|
||||
</Hydrate>
|
||||
<LazyMotion features={domAnimation}>
|
||||
<SessionProvider session={session}>
|
||||
<PostHogProvider>
|
||||
<PostHogPageView />
|
||||
<TooltipProvider>
|
||||
<UserProvider>
|
||||
<TimezoneProvider
|
||||
initialTimezone={session?.user?.timeZone ?? undefined}
|
||||
>
|
||||
<ConnectedDayjsProvider>
|
||||
{children}
|
||||
<TimeZoneChangeDetector />
|
||||
</ConnectedDayjsProvider>
|
||||
</TimezoneProvider>
|
||||
</UserProvider>
|
||||
</TooltipProvider>
|
||||
</PostHogProvider>
|
||||
</SessionProvider>
|
||||
</LazyMotion>
|
||||
</TRPCProvider>
|
||||
</I18nProvider>
|
||||
</body>
|
||||
|
|
|
@ -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 (
|
||||
<I18nextProvider i18n={res.value} defaultNS={defaultNS}>
|
||||
<I18nextProvider i18n={res.value.i18n} defaultNS={defaultNS}>
|
||||
{children}
|
||||
</I18nextProvider>
|
||||
);
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
11
apps/web/src/i18n/server/get-locale.ts
Normal file
11
apps/web/src/i18n/server/get-locale.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -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 });
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue