Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

πŸ› fix: fix language hydration on ssr #6091

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@
"sideEffects": false,
"scripts": {
"build": "next build",
"build:analyze": "ANALYZE=true next build",
"build:docker": "DOCKER=true next build && npm run build-sitemap",
"postbuild": "npm run build-sitemap && npm run build-migrate-db",
"build-migrate-db": "bun run db:migrate",
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
"build:analyze": "ANALYZE=true next build",
"build:docker": "DOCKER=true next build && npm run build-sitemap",
"db:generate": "drizzle-kit generate && npm run db:generate-client",
"db:generate-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts",
"db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts",
Expand Down Expand Up @@ -59,11 +59,11 @@
"start": "next start -p 3210",
"stylelint": "stylelint \"src/**/*.{js,jsx,ts,tsx}\" --fix",
"test": "npm run test-app && npm run test-server",
"test:update": "vitest -u",
"test-app": "vitest run --config vitest.config.ts",
"test-app:coverage": "vitest run --config vitest.config.ts --coverage",
"test-server": "vitest run --config vitest.server.config.ts",
"test-server:coverage": "vitest run --config vitest.server.config.ts --coverage",
"test:update": "vitest -u",
"type-check": "tsc --noEmit",
"webhook:ngrok": "ngrok http http://localhost:3011",
"workflow:cdn": "tsx ./scripts/cdnWorkflow/index.ts",
Expand Down Expand Up @@ -162,7 +162,6 @@
"framer-motion": "^11.16.0",
"gpt-tokenizer": "^2.8.1",
"i18next": "^24.2.1",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-resources-to-backend": "^1.2.1",
"idb-keyval": "^6.2.1",
"immer": "^10.1.1",
Expand Down
42 changes: 20 additions & 22 deletions src/layout/GlobalProvider/Locale.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { ConfigProvider } from 'antd';
import dayjs from 'dayjs';
import { PropsWithChildren, memo, useEffect, useState } from 'react';
import { PropsWithChildren, memo, useEffect, useMemo, useState } from 'react';
import { isRtlLang } from 'rtl-detect';

import { createI18nNext } from '@/locales/create';
Expand Down Expand Up @@ -32,30 +32,28 @@ interface LocaleLayoutProps extends PropsWithChildren {
}

const Locale = memo<LocaleLayoutProps>(({ children, defaultLang, antdLocale }) => {
const [i18n] = useState(createI18nNext(defaultLang));
const [lang, setLang] = useState(defaultLang);
const [locale, setLocale] = useState(antdLocale);

// if run on server side, init i18n instance everytime
if (isOnServerSide) {
i18n.init();

// load the dayjs locale
// if (lang) {
// const dayJSLocale = require(`dayjs/locale/${lang!.toLowerCase()}.js`);
//
// dayjs.locale(dayJSLocale);
// }
} else {
// if on browser side, init i18n instance only once
if (!i18n.instance.isInitialized)
// console.debug('locale', lang);
i18n.init().then(async () => {
if (!lang) return;

await updateDayjs(lang);
});
}
const i18n = useMemo(() => {
const instance = createI18nNext(defaultLang);

// if run on server side, init i18n instance everytime
if (isOnServerSide) {
instance.init();
} else {
// if on browser side, init i18n instance only once
if (!instance.instance.isInitialized) {
instance.init().then(async () => {
if (!lang) return;

await updateDayjs(lang);
});
}
}

return instance;
}, [defaultLang]);

// handle i18n instance language change
useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions src/layout/GlobalProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const GlobalLayout = async ({
// get default feature flags to use with ssr
const serverFeatureFlags = getServerFeatureFlagsValue();
const serverConfig = await getServerGlobalConfig();

return (
<StyleRegistry>
<Locale antdLocale={antdLocale} defaultLang={userLocale}>
Expand Down
37 changes: 10 additions & 27 deletions src/locales/create.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import resourcesToBackend from 'i18next-resources-to-backend';
import { initReactI18next } from 'react-i18next';
import { isRtlLang } from 'rtl-detect';

import { getDebugConfig } from '@/config/debug';
import { DEFAULT_LANG } from '@/const/locale';
import { normalizeLocale } from '@/locales/resources';
import { isDev, isOnServerSide } from '@/utils/env';

const { I18N_DEBUG, I18N_DEBUG_BROWSER, I18N_DEBUG_SERVER } = getDebugConfig();
const debugMode = (I18N_DEBUG ?? isOnServerSide) ? I18N_DEBUG_SERVER : I18N_DEBUG_BROWSER;

export const createI18nNext = (lang?: string) => {
const instance = i18n
.use(initReactI18next)
.use(LanguageDetector)
.use(
resourcesToBackend(async (lng: string, ns: string) => {
if (isDev && lng === 'zh-CN') return import(`./default/${ns}`);
const instance = i18n.use(initReactI18next).use(
resourcesToBackend(async (lng: string, ns: string) => {
if (isDev && lng === 'zh-CN') return import(`./default/${ns}`);

return import(`@/../locales/${normalizeLocale(lng)}/${ns}.json`);
}),
);
return import(`@/../locales/${normalizeLocale(lng)}/${ns}.json`);
}),
);
// Dynamically set HTML direction on language change
instance.on('languageChanged', (lng) => {
if (typeof window !== 'undefined') {
Expand All @@ -34,26 +29,14 @@ export const createI18nNext = (lang?: string) => {
init: () =>
instance.init({
debug: debugMode,
defaultNS: ['error', 'common', 'chat'],
// detection: {
// caches: ['cookie'],
// cookieMinutes: 60 * 24 * COOKIE_CACHE_DAYS,
// /**
// Set `sameSite` to `lax` so that the i18n cookie can be passed to the
// server side when returning from the OAuth authorization website.
// ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
// discussion: https://github.com/lobehub/lobe-chat/pull/1474
// */
// cookieOptions: {
// sameSite: 'lax',
// },
// lookupCookie: LOBE_LOCALE_COOKIE,
// },
fallbackLng: DEFAULT_LANG,
fallbackLng: lang,
interpolation: {
escapeValue: false,
},
lng: lang,
// only load current lang
load: 'currentOnly',
ns: ['error', 'common', 'chat'],
}),
instance,
};
Expand Down