Add setup screen (#1682)

This commit is contained in:
Luke Vella 2025-04-23 13:10:05 +01:00 committed by GitHub
parent 42d0077045
commit 843dfc4a7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 526 additions and 93 deletions

View file

@ -57,6 +57,7 @@
"crypto": "^1.0.1", "crypto": "^1.0.1",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"i18next": "^24.2.2", "i18next": "^24.2.2",
"i18next-http-backend": "^3.0.2",
"i18next-icu": "^2.3.0", "i18next-icu": "^2.3.0",
"i18next-resources-to-backend": "^1.2.1", "i18next-resources-to-backend": "^1.2.1",
"ics": "^3.1.0", "ics": "^3.1.0",

View file

@ -328,5 +328,10 @@
"unconfirmedEventsEmptyStateTitle": "No Unconfirmed Events", "unconfirmedEventsEmptyStateTitle": "No Unconfirmed Events",
"unconfirmedEventsEmptyStateDescription": "Unconfirmed events will show up here.", "unconfirmedEventsEmptyStateDescription": "Unconfirmed events will show up here.",
"canceledEventsEmptyStateTitle": "No Canceled Events", "canceledEventsEmptyStateTitle": "No Canceled Events",
"canceledEventsEmptyStateDescription": "Canceled events will show up here." "canceledEventsEmptyStateDescription": "Canceled events will show up here.",
"setupFormTitle": "Setup",
"setupFormDescription": "Finish setting up your account.",
"errorNotAuthenticated": "Not authenticated",
"errorInvalidFields": "Invalid fields. Please check your input.",
"errorDatabaseUpdateFailed": "Database error: Failed to update settings."
} }

View file

@ -4,8 +4,8 @@ import { SidebarInset, SidebarTrigger } from "@rallly/ui/sidebar";
import Link from "next/link"; import Link from "next/link";
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image"; import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
import { getUser } from "@/data/get-user";
import { CommandMenu } from "@/features/navigation/command-menu"; import { CommandMenu } from "@/features/navigation/command-menu";
import { getOnboardedUser } from "@/features/setup/api";
import { TimezoneProvider } from "@/features/timezone/client/context"; import { TimezoneProvider } from "@/features/timezone/client/context";
import { AppSidebar } from "./components/sidebar/app-sidebar"; import { AppSidebar } from "./components/sidebar/app-sidebar";
@ -17,7 +17,8 @@ export default async function Layout({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const user = await getUser(); const user = await getOnboardedUser();
return ( return (
<TimezoneProvider initialTimezone={user.timeZone}> <TimezoneProvider initialTimezone={user.timeZone}>
<AppSidebarProvider> <AppSidebarProvider>

View file

@ -3,7 +3,6 @@ import { Button } from "@rallly/ui/button";
import { Form, FormField, FormItem, FormLabel } from "@rallly/ui/form"; import { Form, FormField, FormItem, FormLabel } from "@rallly/ui/form";
import { ArrowUpRight } from "lucide-react"; import { ArrowUpRight } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@ -20,7 +19,6 @@ type FormData = z.infer<typeof formSchema>;
export const LanguagePreference = () => { export const LanguagePreference = () => {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const router = useRouter();
const form = useForm<FormData>({ const form = useForm<FormData>({
defaultValues: { defaultValues: {
language: i18n.language, language: i18n.language,
@ -34,7 +32,8 @@ export const LanguagePreference = () => {
<form <form
onSubmit={form.handleSubmit(async (data) => { onSubmit={form.handleSubmit(async (data) => {
await updatePreferences({ locale: data.language }); await updatePreferences({ locale: data.language });
router.refresh(); i18n.changeLanguage(data.language);
form.reset({ language: data.language });
})} })}
> >
<FormField <FormField

View file

@ -1,10 +1,10 @@
import "tailwindcss/tailwind.css"; import "tailwindcss/tailwind.css";
import "../../style.css"; import "../../style.css";
import { defaultLocale, supportedLngs } from "@rallly/languages";
import { PostHogProvider } from "@rallly/posthog/client"; import { PostHogProvider } from "@rallly/posthog/client";
import { Toaster } from "@rallly/ui/toaster"; import { Toaster } from "@rallly/ui/toaster";
import { TooltipProvider } from "@rallly/ui/tooltip"; import { TooltipProvider } from "@rallly/ui/tooltip";
import { dehydrate, Hydrate } from "@tanstack/react-query";
import { domAnimation, LazyMotion } from "motion/react"; import { domAnimation, LazyMotion } from "motion/react";
import type { Viewport } from "next"; import type { Viewport } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
@ -13,11 +13,12 @@ import React from "react";
import { TimeZoneChangeDetector } from "@/app/[locale]/timezone-change-detector"; import { TimeZoneChangeDetector } from "@/app/[locale]/timezone-change-detector";
import { UserProvider } from "@/components/user-provider"; import { UserProvider } from "@/components/user-provider";
import { getUser } from "@/data/get-user";
import { TimezoneProvider } from "@/features/timezone/client/context"; import { TimezoneProvider } from "@/features/timezone/client/context";
import { I18nProvider } from "@/i18n/client"; 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 { TRPCProvider } from "@/trpc/client/provider";
import { createSSRHelper } from "@/trpc/server/create-ssr-helper";
import { ConnectedDayjsProvider } from "@/utils/dayjs"; import { ConnectedDayjsProvider } from "@/utils/dayjs";
import { PostHogPageView } from "../posthog-page-view"; import { PostHogPageView } from "../posthog-page-view";
@ -32,44 +33,58 @@ export const viewport: Viewport = {
initialScale: 1, 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({ export default async function Root({
children, children,
params: { locale },
}: { }: {
children: React.ReactNode; children: React.ReactNode;
params: { locale: string };
}) { }) {
const session = await auth(); const session = await auth();
const trpc = await createSSRHelper(); const locale = await loadLocale();
await trpc.user.subscription.prefetch();
return ( return (
<html lang={locale} className={inter.className}> <html lang={locale} className={inter.className}>
<body> <body>
<Toaster /> <Toaster />
<I18nProvider> <I18nProvider locale={locale}>
<TRPCProvider> <TRPCProvider>
<Hydrate state={dehydrate(trpc.queryClient)}> <LazyMotion features={domAnimation}>
<LazyMotion features={domAnimation}> <SessionProvider session={session}>
<SessionProvider session={session}> <PostHogProvider>
<PostHogProvider> <PostHogPageView />
<PostHogPageView /> <TooltipProvider>
<TooltipProvider> <UserProvider>
<UserProvider> <TimezoneProvider
<TimezoneProvider initialTimezone={session?.user?.timeZone ?? undefined}
initialTimezone={session?.user?.timeZone ?? undefined} >
> <ConnectedDayjsProvider>
<ConnectedDayjsProvider> {children}
{children} <TimeZoneChangeDetector />
<TimeZoneChangeDetector /> </ConnectedDayjsProvider>
</ConnectedDayjsProvider> </TimezoneProvider>
</TimezoneProvider> </UserProvider>
</UserProvider> </TooltipProvider>
</TooltipProvider> </PostHogProvider>
</PostHogProvider> </SessionProvider>
</SessionProvider> </LazyMotion>
</LazyMotion>
</Hydrate>
</TRPCProvider> </TRPCProvider>
</I18nProvider> </I18nProvider>
</body> </body>

View file

@ -0,0 +1,61 @@
import { redirect } from "next/navigation";
import { ProfilePicture } from "@/app/[locale]/(space)/settings/profile/profile-picture";
import { Logo } from "@/components/logo";
import { Trans } from "@/components/trans";
import { getUser } from "@/data/get-user";
import { SetupForm } from "@/features/setup/components/setup-form";
import { onboardedUserSchema } from "@/features/setup/schema";
import { getTranslation } from "@/i18n/server";
export default async function SetupPage() {
const user = await getUser();
const isUserOnboarded = onboardedUserSchema.safeParse(user).success;
if (isUserOnboarded) {
redirect("/");
}
return (
<div className="bg-background flex min-h-dvh justify-center p-4 sm:items-center">
<div className="w-full max-w-sm">
<article className="space-y-8 lg:space-y-10">
<div className="py-8">
<Logo className="mx-auto" />
</div>
<header className="text-center">
<h1 className="text-2xl font-bold">
<Trans i18nKey="setupFormTitle" defaults="Setup" />
</h1>
<p className="text-muted-foreground mt-1">
<Trans
i18nKey="setupFormDescription"
defaults="Finish setting up your account."
/>
</p>
</header>
<main className="space-y-4">
<ProfilePicture name={user.name} image={user.image ?? undefined} />
<SetupForm
defaultValues={{
name: user.name ?? undefined,
timeZone: user.timeZone ?? undefined,
locale: user.locale ?? undefined,
}}
/>
</main>
</article>
</div>
</div>
);
}
export async function generateMetadata() {
const { t } = await getTranslation();
return {
title: t("setupFormTitle", {
defaultValue: "Setup",
}),
};
}

View file

@ -0,0 +1,99 @@
"use server";
import { prisma } from "@rallly/database";
import { posthog } from "@rallly/posthog/server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { getTranslation } from "@/i18n/server";
import { auth } from "@/next-auth";
import { setupSchema } from "./schema";
export type SetupFormState = {
message?: string | null;
errors?: {
name?: string[];
timeZone?: string[];
locale?: string[];
};
};
export async function updateUserSetup(
formData: FormData,
): Promise<SetupFormState> {
const { t } = await getTranslation();
const session = await auth();
if (!session?.user?.id) {
return {
message: t("errorNotAuthenticated", {
defaultValue: "Not authenticated",
}),
};
}
const validatedFields = setupSchema.safeParse({
name: formData.get("name"),
timeZone: formData.get("timeZone"),
locale: formData.get("locale"),
});
if (!validatedFields.success) {
const errors = validatedFields.error.flatten().fieldErrors;
const translatedErrors = Object.entries(errors).reduce(
(acc, [key, value]) => {
acc[key as keyof typeof errors] = value?.map((errKey) =>
t(errKey, { defaultValue: `Invalid ${key}` }),
);
return acc;
},
{} as Required<SetupFormState>["errors"],
);
return {
errors: translatedErrors,
message: t("errorInvalidFields", {
defaultValue: "Invalid fields. Please check your input.",
}),
};
}
const { name, timeZone, locale } = validatedFields.data;
try {
await prisma.user.update({
where: { id: session.user.id },
data: {
name,
timeZone,
locale,
},
});
} catch (error) {
console.error("Failed to update user setup:", error);
return {
message: t("errorDatabaseUpdateFailed", {
defaultValue: "Database error: Failed to update settings.",
}),
};
}
posthog?.capture({
event: "user_setup_completed",
distinctId: session.user.id,
properties: {
$set: {
name,
timeZone,
locale,
},
},
});
await posthog?.shutdown();
revalidatePath("/", "layout");
redirect("/");
}

View file

@ -0,0 +1,30 @@
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { cache } from "react";
import { getUser } from "@/data/get-user";
import { onboardedUserSchema } from "./schema";
export const getOnboardedUser = cache(async () => {
const user = await getUser();
const onboardedUser = onboardedUserSchema.safeParse(user);
if (!onboardedUser.success) {
const headerList = headers();
const pathname = headerList.get("x-pathname");
const searchParams =
pathname && pathname !== "/"
? `?redirectTo=${encodeURIComponent(pathname)}`
: "";
redirect(`/setup${searchParams}`);
}
return {
...user,
timeZone: onboardedUser.data.timeZone,
locale: onboardedUser.data.locale,
name: onboardedUser.data.name,
};
});

View file

@ -0,0 +1,145 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@rallly/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@rallly/ui/form";
import { Input } from "@rallly/ui/input";
import * as React from "react";
import { useForm } from "react-hook-form";
import { LanguageSelect } from "@/components/poll/language-selector";
import { TimeZoneSelect } from "@/components/time-zone-picker/time-zone-select";
import { Trans } from "@/components/trans";
import { useTimezone } from "@/features/timezone";
import { useTranslation } from "@/i18n/client";
import { updateUserSetup } from "../actions";
import { type SetupFormValues, setupSchema } from "../schema";
interface SetupFormProps {
defaultValues?: Partial<SetupFormValues>;
}
export function SetupForm({ defaultValues }: SetupFormProps) {
const { timezone } = useTimezone();
const { i18n } = useTranslation();
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [serverError, setServerError] = React.useState<string | null>(null);
const form = useForm<SetupFormValues>({
resolver: zodResolver(setupSchema),
defaultValues: {
name: defaultValues?.name ?? "",
timeZone: defaultValues?.timeZone || timezone,
locale: defaultValues?.locale || i18n.language,
},
});
async function onSubmit(data: SetupFormValues) {
setIsSubmitting(true);
setServerError(null);
// Construct FormData for the server action
const formData = new FormData();
formData.append("name", data.name);
formData.append("timeZone", data.timeZone);
formData.append("locale", data.locale);
const result = await updateUserSetup(formData);
setIsSubmitting(false);
if (result?.message) {
setServerError(result.message);
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{serverError && (
<p aria-live="polite" className="text-destructive text-sm">
{serverError}
</p>
)}
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey="name" defaults="Name" />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="timeZone"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey="timeZone" defaults="Time Zone" />
</FormLabel>
<FormControl>
<TimeZoneSelect
className="w-full"
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="locale"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey="language" defaults="Language" />
</FormLabel>
<FormControl>
<LanguageSelect
className="w-full"
value={field.value}
onChange={(value) => {
field.onChange(value);
i18n.changeLanguage(value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="mt-6">
<Button
variant="primary"
type="submit"
loading={isSubmitting}
className="w-full"
>
<Trans i18nKey="save" defaults="Save" />
</Button>
</div>
</form>
</Form>
);
}

View file

@ -0,0 +1,17 @@
import { z } from "zod";
export const setupSchema = z.object({
name: z.string().min(1),
timeZone: z.string().min(1),
locale: z.string().min(1),
});
export type SetupFormValues = z.infer<typeof setupSchema>;
export const onboardedUserSchema = z.object({
name: z.string().min(1),
timeZone: z.string().min(1),
locale: z.string().min(1),
});
export type OnboardedUser = z.infer<typeof onboardedUserSchema>;

View file

@ -0,0 +1,14 @@
// Assuming you have a base User type somewhere, e.g., in prisma types or a shared types file
import type { User as BaseUser } from "@prisma/client"; // Or wherever your base User type is
// Define the type for a user who has completed onboarding
// It extends the base user but makes required onboarding fields non-nullable
export interface OnboardedUser extends BaseUser {
name: string; // Guaranteed to be a string
timeZone: string; // Guaranteed to be a string
locale: string; // Guaranteed to be a string
}
// You might also want a type for the potentially un-onboarded user,
// which could just be your BaseUser type
export type PotentiallyUnonboardedUser = BaseUser;

View file

@ -1,5 +1,5 @@
"use client"; "use client";
import { useParams } from "next/navigation"; import httpBackend from "i18next-http-backend";
import React from "react"; import React from "react";
import { import {
I18nextProvider, I18nextProvider,
@ -14,12 +14,20 @@ export function useTranslation() {
return useTranslationOrg("app"); return useTranslationOrg("app");
} }
export function I18nProvider({ children }: { children: React.ReactNode }) { export function I18nProvider({
const params = useParams<{ locale: string }>(); children,
const locale = params?.locale ?? defaultNS; locale,
}: {
children: React.ReactNode;
locale: string;
}) {
const res = useAsync(async () => { const res = useAsync(async () => {
return await initI18next(locale, "app"); return await initI18next({
lng: locale,
middleware: (i18n) => {
i18n.use(httpBackend);
},
});
}); });
if (!res.value) { if (!res.value) {
@ -27,7 +35,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) {
} }
return ( return (
<I18nextProvider i18n={res.value} defaultNS={defaultNS}> <I18nextProvider i18n={res.value.i18n} defaultNS={defaultNS}>
{children} {children}
</I18nextProvider> </I18nextProvider>
); );

View file

@ -1,22 +1,21 @@
import type { Namespace } from "i18next"; import type { i18n, Namespace } from "i18next";
import { createInstance } from "i18next"; import { createInstance } from "i18next";
import ICU from "i18next-icu"; import ICU from "i18next-icu";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next"; import { initReactI18next } from "react-i18next/initReactI18next";
import { getOptions } from "./settings"; import { getOptions } from "./settings";
export const initI18next = async (lng: string, ns: Namespace) => { export const initI18next = async ({
const i18nInstance = createInstance(); lng,
await i18nInstance ns,
.use(initReactI18next) middleware,
.use(ICU) }: {
.use( lng: string;
resourcesToBackend( ns?: Namespace;
(language: string, namespace: string) => middleware: (i18n: i18n) => void;
import(`../../public/locales/${language}/${namespace}.json`), }) => {
), const i18nInstance = createInstance().use(initReactI18next).use(ICU);
) middleware(i18nInstance);
.init(getOptions(lng, ns)); const t = await i18nInstance.init(getOptions(lng, ns));
return i18nInstance; return { t, i18n: i18nInstance };
}; };

View file

@ -1,14 +1,25 @@
import resourcesToBackend from "i18next-resources-to-backend";
import { defaultNS } from "@/i18n/settings"; import { defaultNS } from "@/i18n/settings";
import { getLocaleFromPath } from "@/utils/locale/get-locale-from-path";
import { initI18next } from "./i18n"; import { initI18next } from "./i18n";
import { getLocale } from "./server/get-locale";
export async function getTranslation(localeOverride?: string) { export async function getTranslation(localeOverride?: string) {
const localeFromPath = getLocaleFromPath(); const locale = localeOverride || getLocale();
const locale = localeOverride || localeFromPath; const { i18n } = await initI18next({
const i18nextInstance = await initI18next(locale, defaultNS); lng: locale,
middleware: (i18n) => {
i18n.use(
resourcesToBackend(
(language: string, namespace: string) =>
import(`../../public/locales/${language}/${namespace}.json`),
),
);
},
});
return { return {
t: i18nextInstance.getFixedT(locale, defaultNS), t: i18n.getFixedT(locale, defaultNS),
i18n: i18nextInstance, i18n,
}; };
} }

View 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;
}

View file

@ -10,30 +10,23 @@ const supportedLocales = Object.keys(languages);
export const middleware = withAuth(async (req) => { export const middleware = withAuth(async (req) => {
const { nextUrl } = req; const { nextUrl } = req;
const newUrl = nextUrl.clone(); const newUrl = nextUrl.clone();
const pathname = newUrl.pathname;
const isLoggedIn = req.auth?.user?.email; const isLoggedIn = req.auth?.user?.email;
// if the user is already logged in, don't let them access the login page // if the user is already logged in, don't let them access the login page
if ( if (/^\/(login)/.test(pathname) && isLoggedIn) {
/^\/(login)/.test(newUrl.pathname) &&
isLoggedIn &&
!newUrl.searchParams.get("invalidSession")
) {
newUrl.pathname = "/"; newUrl.pathname = "/";
return NextResponse.redirect(newUrl); return NextResponse.redirect(newUrl);
} }
// Check if locale is specified in cookie const locale = req.auth?.user?.locale || getPreferredLocale(req);
let locale = req.auth?.user?.locale; if (supportedLocales.includes(locale)) {
if (locale && supportedLocales.includes(locale)) { newUrl.pathname = `/${locale}${pathname}`;
newUrl.pathname = `/${locale}${newUrl.pathname}`;
} else {
// Check if locale is specified in header
locale = getPreferredLocale(req);
newUrl.pathname = `/${locale}${newUrl.pathname}`;
} }
const res = NextResponse.rewrite(newUrl); 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) { if (req.auth?.user?.id) {
await withPostHog(res, { distinctID: req.auth.user.id }); await withPostHog(res, { distinctID: req.auth.user.id });

View file

@ -66,6 +66,22 @@ const {
}, },
}, },
events: { events: {
createUser({ user }) {
if (user.id) {
posthog?.capture({
distinctId: user.id,
event: "register",
properties: {
$set: {
name: user.name,
email: user.email,
timeZone: user.timeZone ?? undefined,
locale: user.locale ?? undefined,
},
},
});
}
},
signIn({ user, account }) { signIn({ user, account }) {
if (user.id) { if (user.id) {
posthog?.capture({ posthog?.capture({
@ -76,8 +92,8 @@ const {
$set: { $set: {
name: user.name, name: user.name,
email: user.email, email: user.email,
timeZone: user.timeZone, timeZone: user.timeZone ?? undefined,
locale: user.locale, locale: user.locale ?? undefined,
}, },
}, },
}); });
@ -168,7 +184,6 @@ const {
select: { select: {
name: true, name: true,
email: true, email: true,
locale: true,
timeFormat: true, timeFormat: true,
timeZone: true, timeZone: true,
weekStart: true, weekStart: true,
@ -180,7 +195,6 @@ const {
token.name = user.name; token.name = user.name;
token.email = user.email; token.email = user.email;
token.picture = user.image; token.picture = user.image;
token.locale = user.locale;
token.timeFormat = user.timeFormat; token.timeFormat = user.timeFormat;
token.timeZone = user.timeZone; token.timeZone = user.timeZone;
token.weekStart = user.weekStart; token.weekStart = user.weekStart;
@ -218,7 +232,7 @@ const requireUser = async () => {
*/ */
export const getUserId = async () => { export const getUserId = async () => {
const session = await auth(); const session = await auth();
return session?.user?.email ? session.user.id : null; return session?.user?.email ? session.user.id : undefined;
}; };
export const getLoggedIn = async () => { export const getLoggedIn = async () => {

View file

@ -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;
}

View file

@ -8102,6 +8102,13 @@ cross-env@^7.0.3:
dependencies: dependencies:
cross-spawn "^7.0.1" 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: cross-spawn@^6.0.5:
version "6.0.5" version "6.0.5"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz" 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" resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz"
integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== 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: i18next-icu@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.npmjs.org/i18next-icu/-/i18next-icu-2.3.0.tgz" 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" resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== 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: node-fetch@^2.6.7:
version "2.6.9" version "2.6.9"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz"