From 503c8a93adb990b35ef7bbbdd6c558fe23879d9e Mon Sep 17 00:00:00 2001 From: huahai0202 <2023217276@mail.hfut.edu.cn> Date: Thu, 26 Feb 2026 13:24:07 +0800 Subject: [PATCH 1/4] feat(settings): add i18n support and translate settings UI to zh-CN --- .gitignore | 1 + src-tauri/src/types.rs | 8 + src/App.tsx | 17 +- .../settings/components/SettingsNav.tsx | 28 +- .../settings/components/SettingsView.test.tsx | 1 + .../settings/components/SettingsView.tsx | 12 +- .../sections/SettingsAboutSection.tsx | 46 +- .../sections/SettingsAgentsSection.tsx | 257 ++++---- .../sections/SettingsCodexSection.tsx | 208 ++++--- .../sections/SettingsComposerSection.tsx | 276 +++++--- .../sections/SettingsDictationSection.tsx | 111 ++-- .../sections/SettingsDisplaySection.test.tsx | 50 ++ .../sections/SettingsDisplaySection.tsx | 332 ++++++---- .../sections/SettingsEnvironmentsSection.tsx | 40 +- .../sections/SettingsFeaturesSection.tsx | 240 ++++--- .../sections/SettingsGitSection.tsx | 84 +-- .../sections/SettingsOpenAppsSection.tsx | 64 +- .../sections/SettingsProjectsSection.tsx | 57 +- .../sections/SettingsServerSection.tsx | 222 ++++--- .../sections/SettingsShortcutsSection.tsx | 93 +-- .../components/settingsViewConstants.ts | 26 +- src/features/settings/hooks/useAppSettings.ts | 5 + src/i18n/index.test.ts | 33 + src/i18n/index.ts | 44 ++ src/i18n/locales/en.ts | 589 ++++++++++++++++++ src/i18n/locales/zh-CN.ts | 521 ++++++++++++++++ src/i18n/provider.tsx | 53 ++ src/i18n/types.ts | 9 + src/i18n/useI18n.ts | 6 + src/types.ts | 2 + 30 files changed, 2571 insertions(+), 864 deletions(-) create mode 100644 src/i18n/index.test.ts create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/locales/en.ts create mode 100644 src/i18n/locales/zh-CN.ts create mode 100644 src/i18n/provider.tsx create mode 100644 src/i18n/types.ts create mode 100644 src/i18n/useI18n.ts diff --git a/.gitignore b/.gitignore index 05b62d7c2..e9eef9f36 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ public/assets/material-icons/ # Nix result skills-lock.json +.vs/ diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index cbb8afbaa..87b05408f 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -487,6 +487,8 @@ pub(crate) struct AppSettings { pub(crate) ui_scale: f64, #[serde(default = "default_theme", rename = "theme")] pub(crate) theme: String, + #[serde(default = "default_ui_language", rename = "uiLanguage")] + pub(crate) ui_language: String, #[serde( default = "default_usage_show_remaining", rename = "usageShowRemaining" @@ -698,6 +700,10 @@ fn default_theme() -> String { "system".to_string() } +fn default_ui_language() -> String { + "system".to_string() +} + fn default_usage_show_remaining() -> bool { false } @@ -1142,6 +1148,7 @@ impl Default for AppSettings { last_composer_reasoning_effort: None, ui_scale: 1.0, theme: default_theme(), + ui_language: default_ui_language(), usage_show_remaining: default_usage_show_remaining(), show_message_file_path: default_show_message_file_path(), chat_history_scrollback_items: default_chat_history_scrollback_items(), @@ -1306,6 +1313,7 @@ mod tests { assert!(settings.last_composer_reasoning_effort.is_none()); assert!((settings.ui_scale - 1.0).abs() < f64::EPSILON); assert_eq!(settings.theme, "system"); + assert_eq!(settings.ui_language, "system"); assert!(!settings.usage_show_remaining); assert!(settings.show_message_file_path); assert_eq!(settings.chat_history_scrollback_items, Some(200)); diff --git a/src/App.tsx b/src/App.tsx index 1a0694b5d..ec63e86af 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -157,6 +157,7 @@ import { resolveWorkspaceRuntimeCodexArgsOverride, } from "@threads/utils/threadCodexParamsSeed"; import { setWorkspaceRuntimeCodexArgs } from "@services/tauri"; +import { I18nProvider } from "@/i18n/provider"; const AboutView = lazy(() => import("@/features/about/components/AboutView").then((module) => ({ @@ -2630,7 +2631,12 @@ function MainApp() { ); return ( -
+ +
@@ -2776,10 +2782,11 @@ function MainApp() { onRemoveDictationModel: dictationModel.remove, }} /> - {showMobileSetupWizard && ( - - )} -
+ {showMobileSetupWizard && ( + + )} +
+
); } diff --git a/src/features/settings/components/SettingsNav.tsx b/src/features/settings/components/SettingsNav.tsx index 9e51920b0..0c636d327 100644 --- a/src/features/settings/components/SettingsNav.tsx +++ b/src/features/settings/components/SettingsNav.tsx @@ -12,6 +12,7 @@ import ServerCog from "lucide-react/dist/esm/icons/server-cog"; import Bot from "lucide-react/dist/esm/icons/bot"; import Info from "lucide-react/dist/esm/icons/info"; import { PanelNavItem, PanelNavList } from "@/features/design-system/components/panel/PanelPrimitives"; +import { useI18n } from "@/i18n/useI18n"; import type { CodexSection } from "./settingsTypes"; type SettingsNavProps = { @@ -25,6 +26,7 @@ export function SettingsNav({ onSelectSection, showDisclosure = false, }: SettingsNavProps) { + const { t } = useI18n(); return ( diff --git a/src/features/settings/components/SettingsView.test.tsx b/src/features/settings/components/SettingsView.test.tsx index e8d5a7e03..ace7a5f31 100644 --- a/src/features/settings/components/SettingsView.test.tsx +++ b/src/features/settings/components/SettingsView.test.tsx @@ -101,6 +101,7 @@ const baseSettings: AppSettings = { lastComposerReasoningEffort: null, uiScale: 1, theme: "system", + uiLanguage: "system", usageShowRemaining: false, showMessageFilePath: true, chatHistoryScrollbackItems: 200, diff --git a/src/features/settings/components/SettingsView.tsx b/src/features/settings/components/SettingsView.tsx index 62327c2a8..74fe08741 100644 --- a/src/features/settings/components/SettingsView.tsx +++ b/src/features/settings/components/SettingsView.tsx @@ -13,6 +13,7 @@ import { useSettingsViewCloseShortcuts } from "@settings/hooks/useSettingsViewCl import { useSettingsViewNavigation } from "@settings/hooks/useSettingsViewNavigation"; import { useSettingsViewOrchestration } from "@settings/hooks/useSettingsViewOrchestration"; import { ModalShell } from "@/features/design-system/components/modal/ModalShell"; +import { useI18n } from "@/i18n/useI18n"; import { SettingsNav } from "./SettingsNav"; import type { CodexSection } from "./settingsTypes"; import { SETTINGS_SECTION_LABELS } from "./settingsViewConstants"; @@ -97,6 +98,7 @@ export function SettingsView({ onRemoveDictationModel, initialSection, }: SettingsViewProps) { + const { t } = useI18n(); const { activeSection, showMobileDetail, @@ -137,7 +139,7 @@ export function SettingsView({ useSettingsViewCloseShortcuts(onClose); - const activeSectionLabel = SETTINGS_SECTION_LABELS[activeSection]; + const activeSectionLabel = t(`settings.nav.${SETTINGS_SECTION_LABELS[activeSection]}`); const settingsBodyClassName = `settings-body${ useMobileMasterDetail ? " settings-body-mobile-master-detail" : "" }${useMobileMasterDetail && showMobileDetail ? " is-detail-visible" : ""}`; @@ -151,13 +153,13 @@ export function SettingsView({ >
- Settings + {t("settings.shell.title")}
@@ -180,10 +182,10 @@ export function SettingsView({ type="button" className="settings-mobile-back" onClick={() => setShowMobileDetail(false)} - aria-label="Back to settings sections" + aria-label={t("settings.shell.mobile.back")} > - Sections + {t("settings.shell.mobile.sections")}
{activeSectionLabel}
diff --git a/src/features/settings/components/sections/SettingsAboutSection.tsx b/src/features/settings/components/sections/SettingsAboutSection.tsx index f78e35d35..ebb7234a9 100644 --- a/src/features/settings/components/sections/SettingsAboutSection.tsx +++ b/src/features/settings/components/sections/SettingsAboutSection.tsx @@ -5,7 +5,7 @@ import { type AppBuildType, } from "@services/tauri"; import { useUpdater } from "@/features/update/hooks/useUpdater"; -import { SettingsSection } from "@/features/design-system/components/settings/SettingsPrimitives"; +import { useI18n } from "@/i18n/useI18n"; function formatBytes(value: number) { if (!Number.isFinite(value) || value <= 0) { @@ -22,6 +22,7 @@ function formatBytes(value: number) { } export function SettingsAboutSection() { + const { t } = useI18n(); const [appBuildType, setAppBuildType] = useState("unknown"); const [updaterEnabled, setUpdaterEnabled] = useState(false); const { state: updaterState, checkForUpdates, startUpdate } = useUpdater({ @@ -72,42 +73,42 @@ export function SettingsAboutSection() { const buildDateValue = __APP_BUILD_DATE__.trim(); const parsedBuildDate = Date.parse(buildDateValue); const buildDateLabel = Number.isNaN(parsedBuildDate) - ? buildDateValue || "unknown" + ? buildDateValue || t("settings.about.unknown") : new Date(parsedBuildDate).toLocaleString(); return ( - +
- Version: {__APP_VERSION__} + {t("settings.about.version")}: {__APP_VERSION__}
- Build type: {appBuildType} + {t("settings.about.buildType")}: {appBuildType}
- Branch: {__APP_GIT_BRANCH__ || "unknown"} + {t("settings.about.branch")}: {__APP_GIT_BRANCH__ || t("settings.about.unknown")}
- Commit: {__APP_COMMIT_HASH__ || "unknown"} + {t("settings.about.commit")}: {__APP_COMMIT_HASH__ || t("settings.about.unknown")}
- Build date: {buildDateLabel} + {t("settings.about.buildDate")}: {buildDateLabel}
-
App Updates
+
{t("settings.about.updates.title")}
- Currently running version {__APP_VERSION__} + {t("settings.about.updates.currentVersion")} {__APP_VERSION__}
{!updaterEnabled && (
- Updates are unavailable in this runtime. + {t("settings.about.updates.unavailable")}
)} {updaterState.stage === "error" && (
- Update failed: {updaterState.error} + {t("settings.about.updates.error")}: {updaterState.error}
)} @@ -117,23 +118,26 @@ export function SettingsAboutSection() {
{updaterState.stage === "downloading" ? ( <> - Downloading update...{" "} + {t("settings.about.updates.downloading")}{" "} {updaterState.progress?.totalBytes ? `${Math.round((updaterState.progress.downloadedBytes / updaterState.progress.totalBytes) * 100)}%` : formatBytes(updaterState.progress?.downloadedBytes ?? 0)} ) : updaterState.stage === "installing" ? ( - "Installing update..." + t("settings.about.updates.installing") ) : ( - "Restarting..." + t("settings.about.updates.restarting") )}
) : updaterState.stage === "available" ? (
- Version {updaterState.version} is available. + {t("settings.about.updates.available.prefix")} {updaterState.version}{" "} + {t("settings.about.updates.available.suffix")}
) : updaterState.stage === "latest" ? ( -
You are on the latest version.
+
+ {t("settings.about.updates.latest")} +
) : null}
@@ -144,7 +148,7 @@ export function SettingsAboutSection() { disabled={!updaterEnabled} onClick={() => void startUpdate()} > - Download & Install + {t("settings.about.updates.downloadInstall")} ) : ( )}
- +
); } diff --git a/src/features/settings/components/sections/SettingsAgentsSection.tsx b/src/features/settings/components/sections/SettingsAgentsSection.tsx index 85b588cb2..413e216cc 100644 --- a/src/features/settings/components/sections/SettingsAgentsSection.tsx +++ b/src/features/settings/components/sections/SettingsAgentsSection.tsx @@ -7,12 +7,7 @@ import { } from "@/features/shared/components/MagicSparkleIcon"; import type { SettingsAgentsSectionProps } from "@settings/hooks/useSettingsAgentsSection"; import { fileManagerName, openInFileManagerLabel } from "@utils/platformPaths"; -import { - SettingsSection, - SettingsSubsection, - SettingsToggleRow, - SettingsToggleSwitch, -} from "@/features/design-system/components/settings/SettingsPrimitives"; +import { useI18n } from "@/i18n/useI18n"; const FALLBACK_AGENT_MODELS: ModelOption[] = [ { @@ -62,6 +57,8 @@ export function SettingsAgentsSection({ modelOptionsLoading, modelOptionsError, }: SettingsAgentsSectionProps) { + const { locale, t } = useI18n(); + const zh = locale === "zh-CN"; const [openPathError, setOpenPathError] = useState(null); const [maxThreadsDraft, setMaxThreadsDraft] = useState("6"); const [maxDepthDraft, setMaxDepthDraft] = useState("1"); @@ -162,7 +159,7 @@ export function SettingsAgentsSection({ await revealItemInDir(path); } catch (openError) { setOpenPathError( - openError instanceof Error ? openError.message : "Unable to open path.", + openError instanceof Error ? openError.message : t("settings.agents.openPathError"), ); } }; @@ -181,7 +178,9 @@ export function SettingsAgentsSection({ setCreateError(null); setEditError(null); setOpenPathError( - `Max threads must be an integer between ${MIN_MAX_THREADS} and ${MAX_MAX_THREADS}.`, + zh + ? `最大线程数必须是 ${MIN_MAX_THREADS} 到 ${MAX_MAX_THREADS} 之间的整数。` + : `Max threads must be an integer between ${MIN_MAX_THREADS} and ${MAX_MAX_THREADS}.`, ); return; } @@ -216,7 +215,9 @@ export function SettingsAgentsSection({ setCreateError(null); setEditError(null); setOpenPathError( - `Max depth must be an integer between ${MIN_MAX_DEPTH} and ${MAX_MAX_DEPTH}.`, + zh + ? `最大深度必须是 ${MIN_MAX_DEPTH} 到 ${MAX_MAX_DEPTH} 之间的整数。` + : `Max depth must be an integer between ${MIN_MAX_DEPTH} and ${MAX_MAX_DEPTH}.`, ); return; } @@ -247,7 +248,7 @@ export function SettingsAgentsSection({ const handleCreateAgent = async () => { const name = createName.trim(); if (!name) { - setCreateError("Agent name is required."); + setCreateError(t("settings.agents.nameRequired")); return; } setCreateError(null); @@ -282,7 +283,7 @@ export function SettingsAgentsSection({ } const nextName = editNameDraft.trim(); if (!nextName) { - setEditError("Agent name is required."); + setEditError(t("settings.agents.nameRequired")); return; } const editingAgent = settings?.agents.find((agent) => agent.name === editingName) ?? null; @@ -350,22 +351,29 @@ export function SettingsAgentsSection({ }; return ( - +
+
{t("settings.agents.sectionTitle")}
+
{t("settings.agents.sectionSubtitle")}
- Built-in roles from Codex are still available: default, explorer, - and worker. + {t("settings.agents.builtins.before")} + default + {t("settings.agents.builtins.middle1")} + explorer + {t("settings.agents.builtins.middle2")} + worker + {t("settings.agents.builtins.after")}
- Open global Codex config in {fileManagerName()}.} - > +
+
+
{t("settings.agents.configFile.title")}
+
+ {t("settings.agents.configFile.subtitle", { fileManager: fileManagerName() })} +
+
- +
- +
+
+
{t("settings.agents.multiAgent.title")}
+
Writes features.multi_agent in config.toml. - - } - > - +
+ +
+ +
+
+
{t("settings.agents.maxThreads.title")}
+
+ {t("settings.agents.maxThreads.subtitle.before")} + 1-12 + {t("settings.agents.maxThreads.subtitle.after")} +
+
+
@@ -423,22 +439,27 @@ export function SettingsAgentsSection({ void handleMaxThreadsStep(1); }} disabled={!settings || isUpdatingCore || currentMaxThreads >= MAX_MAX_THREADS} - aria-label="Increase max threads" + aria-label={t("settings.agents.maxThreads.increaseAria")} > ▲
- - - - Maximum nested spawn depth. Valid range: 1-4. Changes save immediately. - - } - > -
+
+ +
+
+
{t("settings.agents.maxDepth.title")}
+
+ {t("settings.agents.maxDepth.subtitle.before")} + 1-4 + {t("settings.agents.maxDepth.subtitle.after")} +
+
+
@@ -460,25 +481,23 @@ export function SettingsAgentsSection({ void handleMaxDepthStep(1); }} disabled={!settings || isUpdatingCore || currentMaxDepth >= MAX_MAX_DEPTH} - aria-label="Increase max depth" + aria-label={t("settings.agents.maxDepth.increaseAria")} > ▲
- - - - Add a custom role under [agents.<name>] and create its config file. - - } - /> +
+ +
{t("settings.agents.create.title")}
+
+ {t("settings.agents.create.subtitle.before")} + [agents.<name>] + {t("settings.agents.create.subtitle.after")} +