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) {