diff --git a/next.config.ts b/next.config.ts index 1d73d67..33648b1 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,7 +3,17 @@ import createNextIntlPlugin from "next-intl/plugin" const isProd = process.env.NODE_ENV === "production" const internalHost = process.env.TAURI_DEV_HOST || "localhost" -const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts") +const withNextIntl = createNextIntlPlugin({ + requestConfig: "./src/i18n/request.ts", + experimental: { + messages: { + path: "./src/i18n/messages", + format: "json", + locales: ["en", "zh-CN", "zh-TW"], + precompile: true, + }, + }, +}) const nextConfig: NextConfig = { output: "export", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4195e01..9e69c47 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( - + void } -const MESSAGES_BY_LOCALE: Record = { - en: enMessages, - zh_cn: zhCNMessages, - zh_tw: zhTWMessages, -} - const AppI18nContext = createContext(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(DEFAULT_LANGUAGE_SETTINGS) + useState( + () => loadPersistedLanguageSettings() ?? DEFAULT_LANGUAGE_SETTINGS + ) const [languageSettingsLoaded, setLanguageSettingsLoaded] = useState(false) + const [messages, setMessages] = useState( + 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 ( - + {children} diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx index 64d46fa..a676534 100644 --- a/src/components/settings/settings-shell.tsx +++ b/src/components/settings/settings-shell.tsx @@ -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) {