多语言优化

This commit is contained in:
xintaofei
2026-03-07 10:24:13 +08:00
parent 934f689b08
commit 28babff52c
7 changed files with 115 additions and 21 deletions

View File

@@ -4,7 +4,6 @@ import { JetBrains_Mono } from "next/font/google"
import { NextIntlClientProvider } from "next-intl"
import { AppI18nProvider } from "@/components/i18n-provider"
import { ThemeProvider } from "@/components/theme-provider"
import enMessages from "@/i18n/messages/en.json"
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
@@ -24,7 +23,7 @@ export default function RootLayout({
return (
<html lang="en" className={jetbrainsMono.variable} suppressHydrationWarning>
<body>
<NextIntlClientProvider locale="en" messages={enMessages}>
<NextIntlClientProvider locale="en" messages={null}>
<AppI18nProvider>
<ThemeProvider
attribute="class"

View File

@@ -10,13 +10,12 @@ import {
useSyncExternalStore,
} from "react"
import { NextIntlClientProvider, type AbstractIntlMessages } from "next-intl"
import enMessages from "@/i18n/messages/en.json"
import zhCNMessages from "@/i18n/messages/zh-CN.json"
import zhTWMessages from "@/i18n/messages/zh-TW.json"
import { getFallbackMessages, getMessagesForLocale } from "@/i18n/messages"
import {
APP_LOCALE_TO_INTL_LOCALE,
DEFAULT_LANGUAGE_SETTINGS,
getSystemLocaleCandidates,
LANGUAGE_SETTINGS_STORAGE_KEY,
normalizeLanguageSettings,
resolveAppLocale,
} from "@/lib/i18n"
@@ -30,12 +29,6 @@ interface AppI18nContextValue {
setLanguageSettings: (settings: SystemLanguageSettings) => void
}
const MESSAGES_BY_LOCALE: Record<AppLocale, AbstractIntlMessages> = {
en: enMessages,
zh_cn: zhCNMessages,
zh_tw: zhTWMessages,
}
const AppI18nContext = createContext<AppI18nContextValue | null>(null)
function subscribeSystemLocale(onStoreChange: () => void) {
@@ -55,6 +48,31 @@ function getSystemLocaleServerSnapshot(): string {
return ""
}
function loadPersistedLanguageSettings(): SystemLanguageSettings | null {
if (typeof window === "undefined") return null
try {
const raw = window.localStorage.getItem(LANGUAGE_SETTINGS_STORAGE_KEY)
if (!raw) return null
return normalizeLanguageSettings(JSON.parse(raw) as SystemLanguageSettings)
} catch {
return null
}
}
function persistLanguageSettings(settings: SystemLanguageSettings) {
if (typeof window === "undefined") return
try {
window.localStorage.setItem(
LANGUAGE_SETTINGS_STORAGE_KEY,
JSON.stringify(settings)
)
} catch {
// Ignore write failures (e.g. disabled storage).
}
}
export function useAppI18n() {
const context = useContext(AppI18nContext)
if (!context) {
@@ -65,8 +83,13 @@ export function useAppI18n() {
export function AppI18nProvider({ children }: { children: React.ReactNode }) {
const [languageSettings, setLanguageSettingsState] =
useState<SystemLanguageSettings>(DEFAULT_LANGUAGE_SETTINGS)
useState<SystemLanguageSettings>(
() => loadPersistedLanguageSettings() ?? DEFAULT_LANGUAGE_SETTINGS
)
const [languageSettingsLoaded, setLanguageSettingsLoaded] = useState(false)
const [messages, setMessages] = useState<AbstractIntlMessages>(
getFallbackMessages()
)
const systemLocaleSnapshot = useSyncExternalStore(
subscribeSystemLocale,
@@ -80,7 +103,9 @@ export function AppI18nProvider({ children }: { children: React.ReactNode }) {
const setLanguageSettings = useCallback(
(settings: SystemLanguageSettings) => {
setLanguageSettingsState(normalizeLanguageSettings(settings))
const normalized = normalizeLanguageSettings(settings)
setLanguageSettingsState(normalized)
persistLanguageSettings(normalized)
},
[]
)
@@ -114,6 +139,24 @@ export function AppI18nProvider({ children }: { children: React.ReactNode }) {
const intlLocale = APP_LOCALE_TO_INTL_LOCALE[appLocale]
useEffect(() => {
let cancelled = false
getMessagesForLocale(appLocale)
.then((nextMessages) => {
if (!cancelled) {
setMessages(nextMessages)
}
})
.catch((err) => {
console.error("[i18n] load locale messages failed:", err)
})
return () => {
cancelled = true
}
}, [appLocale])
useEffect(() => {
document.documentElement.lang = intlLocale
}, [intlLocale])
@@ -130,10 +173,7 @@ export function AppI18nProvider({ children }: { children: React.ReactNode }) {
return (
<AppI18nContext.Provider value={contextValue}>
<NextIntlClientProvider
locale={intlLocale}
messages={MESSAGES_BY_LOCALE[appLocale]}
>
<NextIntlClientProvider locale={intlLocale} messages={messages}>
{children}
</NextIntlClientProvider>
</AppI18nContext.Provider>

View File

@@ -19,7 +19,7 @@ import { AppTitleBar } from "@/components/layout/app-title-bar"
interface SettingsNavItem {
href: string
labelKey: string
labelKey: "appearance" | "agents" | "mcp" | "skills" | "shortcuts" | "system"
icon: ComponentType<{ className?: string }>
}
@@ -115,6 +115,7 @@ export function SettingsShell({ children }: SettingsShellProps) {
<nav className="space-y-1">
{SETTINGS_NAV_ITEMS.map((item) => {
const Icon = item.icon
const translationKey = `nav.${item.labelKey}` as const
const active =
normalizedPathname === item.href ||
normalizedPathname.startsWith(`${item.href}/`)
@@ -130,7 +131,7 @@ export function SettingsShell({ children }: SettingsShellProps) {
>
<span className="inline-flex items-center gap-1">
<Icon className="h-3.5 w-3.5" />
{t(`nav.${item.labelKey}`)}
{t(translationKey)}
</span>
</Button>
)

8
src/i18n/global.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
import type enMessages from "@/i18n/messages/en.json"
declare module "next-intl" {
interface AppConfig {
Locale: "en" | "zh-CN" | "zh-TW"
Messages: typeof enMessages
}
}

34
src/i18n/messages.ts Normal file
View File

@@ -0,0 +1,34 @@
import type { AbstractIntlMessages } from "next-intl"
import enMessages from "@/i18n/messages/en.json"
import type { AppLocale } from "@/lib/types"
const MESSAGE_CACHE = new Map<AppLocale, AbstractIntlMessages>([
["en", enMessages],
])
async function loadMessages(locale: AppLocale): Promise<AbstractIntlMessages> {
switch (locale) {
case "zh_cn":
return (await import("@/i18n/messages/zh-CN.json")).default
case "zh_tw":
return (await import("@/i18n/messages/zh-TW.json")).default
case "en":
default:
return enMessages
}
}
export function getFallbackMessages(): AbstractIntlMessages {
return enMessages
}
export async function getMessagesForLocale(
locale: AppLocale
): Promise<AbstractIntlMessages> {
const cached = MESSAGE_CACHE.get(locale)
if (cached) return cached
const messages = await loadMessages(locale)
MESSAGE_CACHE.set(locale, messages)
return messages
}

View File

@@ -2,13 +2,15 @@ import type { AppLocale, SystemLanguageSettings } from "@/lib/types"
export const APP_LOCALES: readonly AppLocale[] = ["en", "zh_cn", "zh_tw"]
const FALLBACK_APP_LOCALE: AppLocale = "en"
export const LANGUAGE_SETTINGS_STORAGE_KEY = "codeg.system_language_settings"
export type IntlLocale = "en" | "zh-CN" | "zh-TW"
export const DEFAULT_LANGUAGE_SETTINGS: SystemLanguageSettings = {
mode: "system",
language: FALLBACK_APP_LOCALE,
}
export const APP_LOCALE_TO_INTL_LOCALE: Record<AppLocale, string> = {
export const APP_LOCALE_TO_INTL_LOCALE: Record<AppLocale, IntlLocale> = {
en: "en",
zh_cn: "zh-CN",
zh_tw: "zh-TW",