Improve tree shaking and dynamic loading, fix translations in production build. Moved i18n settings to the application's side.

This commit is contained in:
giancarlo
2024-04-13 12:43:02 +08:00
parent 31a8d68809
commit 7f11905fc1
28 changed files with 277 additions and 288 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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'],

View File

@@ -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",