feat(i18n): 增强管理端多语言支持,优化语言初始化与恢复逻辑

This commit is contained in:
2026-05-25 14:53:15 +08:00
parent ddedef824e
commit 84bf924378
4 changed files with 56 additions and 9 deletions

View File

@@ -22,6 +22,9 @@ export const metadata: Metadata = {
description: "Lottery administration console", description: "Lottery administration console",
}; };
/** 在 React 水合前恢复 `<html lang>`,避免刷新后先闪中文再切换 */
const ADMIN_LOCALE_BOOTSTRAP = `(function(){try{var s=localStorage.getItem("lottery_admin_ui_locale");var m={zh:"zh-Hans",en:"en",ne:"ne"};if(s&&m[s])document.documentElement.lang=m[s];}catch(e){}})();`;
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
@@ -33,6 +36,9 @@ export default function RootLayout({
suppressHydrationWarning suppressHydrationWarning
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
> >
<head>
<script dangerouslySetInnerHTML={{ __html: ADMIN_LOCALE_BOOTSTRAP }} />
</head>
<body className="flex min-h-full flex-col"> <body className="flex min-h-full flex-col">
<Providers>{children}</Providers> <Providers>{children}</Providers>
</body> </body>

View File

@@ -13,13 +13,29 @@ type ProvidersProps = {
children: ReactNode; children: ReactNode;
}; };
function applyStoredAdminLanguage() {
const locale = hydrateAdminUiLocale();
if (!locale) {
return;
}
const current = i18n.resolvedLanguage ?? i18n.language;
if (locale !== current) {
void i18n.changeLanguage(locale);
}
}
function AdminSessionHydrator() { function AdminSessionHydrator() {
useEffect(() => { useEffect(() => {
const locale = hydrateAdminUiLocale(); if (i18n.isInitialized) {
if (locale && i18n.resolvedLanguage !== locale) { applyStoredAdminLanguage();
void i18n.changeLanguage(locale); } else {
i18n.on("initialized", applyStoredAdminLanguage);
} }
useAdminSessionStore.getState().rehydrate(); useAdminSessionStore.getState().rehydrate();
return () => {
i18n.off("initialized", applyStoredAdminLanguage);
};
}, []); }, []);
return null; return null;

View File

@@ -3,7 +3,13 @@
import i18n from "i18next"; import i18n from "i18next";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import { adminHtmlLang, applyAdminUiLocale, type AdminApiLocale } from "@/lib/admin-locale"; import {
adminHtmlLang,
applyAdminUiLocale,
getStoredAdminUiLocale,
hydrateAdminUiLocale,
type AdminApiLocale,
} from "@/lib/admin-locale";
import enAudit from "@/i18n/locales/en/audit.json"; import enAudit from "@/i18n/locales/en/audit.json";
import enAdminUsers from "@/i18n/locales/en/adminUsers.json"; import enAdminUsers from "@/i18n/locales/en/adminUsers.json";
import enAuth from "@/i18n/locales/en/auth.json"; import enAuth from "@/i18n/locales/en/auth.json";
@@ -118,10 +124,15 @@ export function normalizeAdminLanguage(lang: string | undefined): AdminLanguage
} }
function getInitialAdminLanguage(): AdminLanguage { function getInitialAdminLanguage(): AdminLanguage {
if (typeof document === "undefined") { if (typeof window === "undefined") {
return ADMIN_DEFAULT_LANGUAGE; return ADMIN_DEFAULT_LANGUAGE;
} }
const stored = getStoredAdminUiLocale();
if (stored) {
return stored;
}
return normalizeAdminLanguage(document.documentElement.lang); return normalizeAdminLanguage(document.documentElement.lang);
} }
@@ -133,11 +144,17 @@ function syncAdminLanguage(lang: AdminLanguage): void {
} }
if (!i18n.isInitialized) { if (!i18n.isInitialized) {
if (typeof window !== "undefined") {
hydrateAdminUiLocale();
}
const initialLanguage = getInitialAdminLanguage();
void i18n void i18n
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
resources, resources,
lng: getInitialAdminLanguage(), lng: initialLanguage,
fallbackLng: ADMIN_DEFAULT_LANGUAGE, fallbackLng: ADMIN_DEFAULT_LANGUAGE,
supportedLngs: [...ADMIN_SUPPORTED_LANGUAGES], supportedLngs: [...ADMIN_SUPPORTED_LANGUAGES],
defaultNS: "common", defaultNS: "common",
@@ -149,9 +166,16 @@ if (!i18n.isInitialized) {
react: { react: {
useSuspense: false, useSuspense: false,
}, },
})
.then(() => {
const lang = getInitialAdminLanguage();
if (normalizeAdminLanguage(i18n.language) !== lang) {
void i18n.changeLanguage(lang);
}
syncAdminLanguage(normalizeAdminLanguage(i18n.resolvedLanguage ?? i18n.language));
}); });
syncAdminLanguage(normalizeAdminLanguage(i18n.resolvedLanguage ?? i18n.language)); syncAdminLanguage(initialLanguage);
i18n.on("languageChanged", (lang) => { i18n.on("languageChanged", (lang) => {
syncAdminLanguage(normalizeAdminLanguage(lang)); syncAdminLanguage(normalizeAdminLanguage(lang));
}); });

View File

@@ -69,7 +69,8 @@ export function adminHtmlLang(loc: AdminApiLocale): string {
return "en"; return "en";
} }
function readStoredUiLocale(): AdminApiLocale | null { /** 读取用户上次选择的界面语言(无则 null */
export function getStoredAdminUiLocale(): AdminApiLocale | null {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return null; return null;
} }
@@ -104,7 +105,7 @@ export function hydrateAdminUiLocale(): AdminApiLocale | null {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return null; return null;
} }
const stored = readStoredUiLocale(); const stored = getStoredAdminUiLocale();
if (stored) { if (stored) {
setAdminRequestLocale(stored); setAdminRequestLocale(stored);
document.documentElement.lang = adminHtmlLang(stored); document.documentElement.lang = adminHtmlLang(stored);