Improve tree shaking and dynamic loading, fix translations in production build. Moved i18n settings to the application's side.
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.43.0",
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@types/node": "^20.12.7",
|
||||
"node-html-parser": "^6.1.13"
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import { EmbeddedCheckout, PlanPicker } from '@kit/billing-gateway/components';
|
||||
import { PlanPicker } from '@kit/billing-gateway/components';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
Card,
|
||||
@@ -20,6 +22,19 @@ import billingConfig from '~/config/billing.config';
|
||||
|
||||
import { createPersonalAccountCheckoutSession } from '../server-actions';
|
||||
|
||||
const EmbeddedCheckout = dynamic(
|
||||
async () => {
|
||||
const { EmbeddedCheckout } = await import('@kit/billing-gateway/checkout');
|
||||
|
||||
return {
|
||||
default: EmbeddedCheckout,
|
||||
};
|
||||
},
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export function PersonalAccountCheckoutForm(props: {
|
||||
customerId: string | null | undefined;
|
||||
}) {
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
import { EmbeddedCheckout, PlanPicker } from '@kit/billing-gateway/components';
|
||||
import { PlanPicker } from '@kit/billing-gateway/components';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -18,6 +19,19 @@ import billingConfig from '~/config/billing.config';
|
||||
|
||||
import { createTeamAccountCheckoutSession } from '../server-actions';
|
||||
|
||||
const EmbeddedCheckout = dynamic(
|
||||
async () => {
|
||||
const { EmbeddedCheckout } = await import('@kit/billing-gateway/checkout');
|
||||
|
||||
return {
|
||||
default: EmbeddedCheckout,
|
||||
};
|
||||
},
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export function TeamAccountCheckoutForm(params: {
|
||||
accountId: string;
|
||||
customerId: string | null | undefined;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
|
||||
import { CaptchaProvider, CaptchaTokenSetter } from '@kit/auth/captcha/client';
|
||||
import { CaptchaProvider } from '@kit/auth/captcha/client';
|
||||
import { I18nProvider } from '@kit/i18n/provider';
|
||||
import { AuthChangeListener } from '@kit/supabase/components/auth-change-listener';
|
||||
|
||||
@@ -12,24 +14,39 @@ import appConfig from '~/config/app.config';
|
||||
import authConfig from '~/config/auth.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { i18nResolver } from '~/lib/i18n/i18n.resolver';
|
||||
import { getI18nSettings } from '~/lib/i18n/i18n.settings';
|
||||
|
||||
const captchaSiteKey = authConfig.captchaTokenSiteKey;
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const CaptchaTokenSetter = dynamic(async () => {
|
||||
if (!captchaSiteKey) {
|
||||
return Promise.resolve(() => null);
|
||||
}
|
||||
|
||||
const { CaptchaTokenSetter } = await import('@kit/auth/captcha/client');
|
||||
|
||||
return {
|
||||
default: CaptchaTokenSetter,
|
||||
};
|
||||
});
|
||||
|
||||
export function RootProviders({
|
||||
lang,
|
||||
children,
|
||||
}: React.PropsWithChildren<{
|
||||
lang: string;
|
||||
}>) {
|
||||
const i18nSettings = getI18nSettings(lang);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryStreamedHydration>
|
||||
<CaptchaProvider>
|
||||
<CaptchaTokenSetter siteKey={captchaSiteKey} />
|
||||
<I18nProvider settings={i18nSettings} resolver={i18nResolver}>
|
||||
<CaptchaProvider>
|
||||
<CaptchaTokenSetter siteKey={captchaSiteKey} />
|
||||
|
||||
<AuthChangeListener appHomePath={pathsConfig.app.home}>
|
||||
<I18nProvider lang={lang} resolver={i18nResolver}>
|
||||
<AuthChangeListener appHomePath={pathsConfig.app.home}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
enableSystem
|
||||
@@ -38,9 +55,9 @@ export function RootProviders({
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
</AuthChangeListener>
|
||||
</CaptchaProvider>
|
||||
</AuthChangeListener>
|
||||
</CaptchaProvider>
|
||||
</I18nProvider>
|
||||
</ReactQueryStreamedHydration>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -3,15 +3,9 @@
|
||||
*
|
||||
*/
|
||||
export async function i18nResolver(language: string, namespace: string) {
|
||||
try {
|
||||
const { default: data } = await import(
|
||||
`../../public/locales/${language}/${namespace}.json`
|
||||
const data = await import(
|
||||
`../../public/locales/${language}/${namespace}.json`,
|
||||
);
|
||||
|
||||
return data as Record<string, string>;
|
||||
} catch (e) {
|
||||
console.error('Could not load translation file', e);
|
||||
|
||||
return {} as Record<string, string>;
|
||||
}
|
||||
return data as Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { cookies, headers } from 'next/headers';
|
||||
|
||||
import {
|
||||
getLanguageCookie,
|
||||
initializeServerI18n,
|
||||
parseAcceptLanguageHeader,
|
||||
} from '@kit/i18n/server';
|
||||
|
||||
import {
|
||||
I18N_COOKIE_NAME,
|
||||
getI18nSettings,
|
||||
languages,
|
||||
} from '~/lib/i18n/i18n.settings';
|
||||
|
||||
import { i18nResolver } from './i18n.resolver';
|
||||
|
||||
/**
|
||||
@@ -18,10 +23,22 @@ import { i18nResolver } from './i18n.resolver';
|
||||
*/
|
||||
export function createI18nServerInstance() {
|
||||
const acceptLanguage = headers().get('accept-language');
|
||||
const cookie = getLanguageCookie(cookies());
|
||||
const cookie = cookies().get(I18N_COOKIE_NAME)?.value;
|
||||
|
||||
const language =
|
||||
cookie ?? parseAcceptLanguageHeader(acceptLanguage)[0] ?? undefined;
|
||||
let language =
|
||||
cookie ??
|
||||
parseAcceptLanguageHeader(acceptLanguage, languages)[0] ??
|
||||
languages[0];
|
||||
|
||||
return initializeServerI18n(language, i18nResolver);
|
||||
if (!languages.includes(language ?? '')) {
|
||||
console.warn(
|
||||
`Language "${language}" is not supported. Falling back to "${languages[0]}"`,
|
||||
);
|
||||
|
||||
language = languages[0];
|
||||
}
|
||||
|
||||
const settings = getI18nSettings(language);
|
||||
|
||||
return initializeServerI18n(settings, i18nResolver);
|
||||
}
|
||||
|
||||
65
apps/web/lib/i18n/i18n.settings.ts
Normal file
65
apps/web/lib/i18n/i18n.settings.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { InitOptions } from 'i18next';
|
||||
|
||||
/**
|
||||
* The default language of the application.
|
||||
* This is used as a fallback language when the selected language is not supported.
|
||||
*
|
||||
*/
|
||||
const defaultLanguage = process.env.NEXT_PUBLIC_LOCALE ?? 'en';
|
||||
|
||||
/**
|
||||
* The list of supported languages.
|
||||
* By default, only the default language is supported.
|
||||
* Add more languages here if needed.
|
||||
*/
|
||||
export const languages: string[] = [defaultLanguage];
|
||||
|
||||
/**
|
||||
* The name of the cookie that stores the selected language.
|
||||
*/
|
||||
export const I18N_COOKIE_NAME = 'lang';
|
||||
|
||||
/**
|
||||
* The default array of Internationalization (i18n) namespaces.
|
||||
* These namespaces are commonly used in the application for translation purposes.
|
||||
*
|
||||
* Add your own namespaces here
|
||||
**/
|
||||
export const defaultI18nNamespaces = [
|
||||
'common',
|
||||
'auth',
|
||||
'account',
|
||||
'teams',
|
||||
'billing',
|
||||
'marketing',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the i18n settings for the given language and namespaces.
|
||||
* If the language is not supported, it will fall back to the default language.
|
||||
* @param language
|
||||
* @param ns
|
||||
*/
|
||||
export function getI18nSettings(
|
||||
language: string | undefined,
|
||||
ns: string | string[] = defaultI18nNamespaces,
|
||||
): InitOptions {
|
||||
let lng = language ?? defaultLanguage;
|
||||
|
||||
if (!languages.includes(lng)) {
|
||||
console.warn(
|
||||
`Language "${lng}" is not supported. Falling back to "${defaultLanguage}"`,
|
||||
);
|
||||
|
||||
lng = defaultLanguage;
|
||||
}
|
||||
|
||||
return {
|
||||
supportedLngs: languages,
|
||||
fallbackLng: defaultLanguage,
|
||||
lng,
|
||||
fallbackNS: defaultI18nNamespaces,
|
||||
defaultNS: defaultI18nNamespaces,
|
||||
ns,
|
||||
};
|
||||
}
|
||||
@@ -24,7 +24,6 @@ const INTERNAL_PACKAGES = [
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
/** Enables hot reloading for local packages without a build step */
|
||||
transpilePackages: INTERNAL_PACKAGES,
|
||||
pageExtensions: ['ts', 'tsx'],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true pnpm run build",
|
||||
"build": "pnpm with-env next build",
|
||||
"build:test": "NODE_ENV=test next build",
|
||||
"build:test": "pnpm with-env:test next build",
|
||||
"clean": "git clean -xdf .next .turbo node_modules",
|
||||
"dev": "pnpm with-env next dev --turbo",
|
||||
"next:lint": "next lint",
|
||||
@@ -54,7 +54,7 @@
|
||||
"@supabase/supabase-js": "^2.42.3",
|
||||
"@tanstack/react-query": "5.29.0",
|
||||
"@tanstack/react-query-next-experimental": "^5.29.2",
|
||||
"@tanstack/react-table": "^8.15.3",
|
||||
"@tanstack/react-table": "^8.16.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"edge-csrf": "^1.0.9",
|
||||
"i18next": "^23.11.1",
|
||||
|
||||
Reference in New Issue
Block a user