多语言优化
This commit is contained in:
@@ -3,7 +3,17 @@ import createNextIntlPlugin from "next-intl/plugin"
|
|||||||
|
|
||||||
const isProd = process.env.NODE_ENV === "production"
|
const isProd = process.env.NODE_ENV === "production"
|
||||||
const internalHost = process.env.TAURI_DEV_HOST || "localhost"
|
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 = {
|
const nextConfig: NextConfig = {
|
||||||
output: "export",
|
output: "export",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { JetBrains_Mono } from "next/font/google"
|
|||||||
import { NextIntlClientProvider } from "next-intl"
|
import { NextIntlClientProvider } from "next-intl"
|
||||||
import { AppI18nProvider } from "@/components/i18n-provider"
|
import { AppI18nProvider } from "@/components/i18n-provider"
|
||||||
import { ThemeProvider } from "@/components/theme-provider"
|
import { ThemeProvider } from "@/components/theme-provider"
|
||||||
import enMessages from "@/i18n/messages/en.json"
|
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -24,7 +23,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en" className={jetbrainsMono.variable} suppressHydrationWarning>
|
<html lang="en" className={jetbrainsMono.variable} suppressHydrationWarning>
|
||||||
<body>
|
<body>
|
||||||
<NextIntlClientProvider locale="en" messages={enMessages}>
|
<NextIntlClientProvider locale="en" messages={null}>
|
||||||
<AppI18nProvider>
|
<AppI18nProvider>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
|
|||||||
@@ -10,13 +10,12 @@ import {
|
|||||||
useSyncExternalStore,
|
useSyncExternalStore,
|
||||||
} from "react"
|
} from "react"
|
||||||
import { NextIntlClientProvider, type AbstractIntlMessages } from "next-intl"
|
import { NextIntlClientProvider, type AbstractIntlMessages } from "next-intl"
|
||||||
import enMessages from "@/i18n/messages/en.json"
|
import { getFallbackMessages, getMessagesForLocale } from "@/i18n/messages"
|
||||||
import zhCNMessages from "@/i18n/messages/zh-CN.json"
|
|
||||||
import zhTWMessages from "@/i18n/messages/zh-TW.json"
|
|
||||||
import {
|
import {
|
||||||
APP_LOCALE_TO_INTL_LOCALE,
|
APP_LOCALE_TO_INTL_LOCALE,
|
||||||
DEFAULT_LANGUAGE_SETTINGS,
|
DEFAULT_LANGUAGE_SETTINGS,
|
||||||
getSystemLocaleCandidates,
|
getSystemLocaleCandidates,
|
||||||
|
LANGUAGE_SETTINGS_STORAGE_KEY,
|
||||||
normalizeLanguageSettings,
|
normalizeLanguageSettings,
|
||||||
resolveAppLocale,
|
resolveAppLocale,
|
||||||
} from "@/lib/i18n"
|
} from "@/lib/i18n"
|
||||||
@@ -30,12 +29,6 @@ interface AppI18nContextValue {
|
|||||||
setLanguageSettings: (settings: SystemLanguageSettings) => void
|
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)
|
const AppI18nContext = createContext<AppI18nContextValue | null>(null)
|
||||||
|
|
||||||
function subscribeSystemLocale(onStoreChange: () => void) {
|
function subscribeSystemLocale(onStoreChange: () => void) {
|
||||||
@@ -55,6 +48,31 @@ function getSystemLocaleServerSnapshot(): string {
|
|||||||
return ""
|
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() {
|
export function useAppI18n() {
|
||||||
const context = useContext(AppI18nContext)
|
const context = useContext(AppI18nContext)
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@@ -65,8 +83,13 @@ export function useAppI18n() {
|
|||||||
|
|
||||||
export function AppI18nProvider({ children }: { children: React.ReactNode }) {
|
export function AppI18nProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [languageSettings, setLanguageSettingsState] =
|
const [languageSettings, setLanguageSettingsState] =
|
||||||
useState<SystemLanguageSettings>(DEFAULT_LANGUAGE_SETTINGS)
|
useState<SystemLanguageSettings>(
|
||||||
|
() => loadPersistedLanguageSettings() ?? DEFAULT_LANGUAGE_SETTINGS
|
||||||
|
)
|
||||||
const [languageSettingsLoaded, setLanguageSettingsLoaded] = useState(false)
|
const [languageSettingsLoaded, setLanguageSettingsLoaded] = useState(false)
|
||||||
|
const [messages, setMessages] = useState<AbstractIntlMessages>(
|
||||||
|
getFallbackMessages()
|
||||||
|
)
|
||||||
|
|
||||||
const systemLocaleSnapshot = useSyncExternalStore(
|
const systemLocaleSnapshot = useSyncExternalStore(
|
||||||
subscribeSystemLocale,
|
subscribeSystemLocale,
|
||||||
@@ -80,7 +103,9 @@ export function AppI18nProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const setLanguageSettings = useCallback(
|
const setLanguageSettings = useCallback(
|
||||||
(settings: SystemLanguageSettings) => {
|
(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]
|
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(() => {
|
useEffect(() => {
|
||||||
document.documentElement.lang = intlLocale
|
document.documentElement.lang = intlLocale
|
||||||
}, [intlLocale])
|
}, [intlLocale])
|
||||||
@@ -130,10 +173,7 @@ export function AppI18nProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppI18nContext.Provider value={contextValue}>
|
<AppI18nContext.Provider value={contextValue}>
|
||||||
<NextIntlClientProvider
|
<NextIntlClientProvider locale={intlLocale} messages={messages}>
|
||||||
locale={intlLocale}
|
|
||||||
messages={MESSAGES_BY_LOCALE[appLocale]}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</AppI18nContext.Provider>
|
</AppI18nContext.Provider>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { AppTitleBar } from "@/components/layout/app-title-bar"
|
|||||||
|
|
||||||
interface SettingsNavItem {
|
interface SettingsNavItem {
|
||||||
href: string
|
href: string
|
||||||
labelKey: string
|
labelKey: "appearance" | "agents" | "mcp" | "skills" | "shortcuts" | "system"
|
||||||
icon: ComponentType<{ className?: string }>
|
icon: ComponentType<{ className?: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +115,7 @@ export function SettingsShell({ children }: SettingsShellProps) {
|
|||||||
<nav className="space-y-1">
|
<nav className="space-y-1">
|
||||||
{SETTINGS_NAV_ITEMS.map((item) => {
|
{SETTINGS_NAV_ITEMS.map((item) => {
|
||||||
const Icon = item.icon
|
const Icon = item.icon
|
||||||
|
const translationKey = `nav.${item.labelKey}` as const
|
||||||
const active =
|
const active =
|
||||||
normalizedPathname === item.href ||
|
normalizedPathname === item.href ||
|
||||||
normalizedPathname.startsWith(`${item.href}/`)
|
normalizedPathname.startsWith(`${item.href}/`)
|
||||||
@@ -130,7 +131,7 @@ export function SettingsShell({ children }: SettingsShellProps) {
|
|||||||
>
|
>
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
<Icon className="h-3.5 w-3.5" />
|
<Icon className="h-3.5 w-3.5" />
|
||||||
{t(`nav.${item.labelKey}`)}
|
{t(translationKey)}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|||||||
8
src/i18n/global.d.ts
vendored
Normal file
8
src/i18n/global.d.ts
vendored
Normal 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
34
src/i18n/messages.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -2,13 +2,15 @@ import type { AppLocale, SystemLanguageSettings } from "@/lib/types"
|
|||||||
|
|
||||||
export const APP_LOCALES: readonly AppLocale[] = ["en", "zh_cn", "zh_tw"]
|
export const APP_LOCALES: readonly AppLocale[] = ["en", "zh_cn", "zh_tw"]
|
||||||
const FALLBACK_APP_LOCALE: AppLocale = "en"
|
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 = {
|
export const DEFAULT_LANGUAGE_SETTINGS: SystemLanguageSettings = {
|
||||||
mode: "system",
|
mode: "system",
|
||||||
language: FALLBACK_APP_LOCALE,
|
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",
|
en: "en",
|
||||||
zh_cn: "zh-CN",
|
zh_cn: "zh-CN",
|
||||||
zh_tw: "zh-TW",
|
zh_tw: "zh-TW",
|
||||||
|
|||||||
Reference in New Issue
Block a user