From fe168c2af02a7cf00f5320bc160203f2b262ef0b Mon Sep 17 00:00:00 2001 From: allen Date: Fri, 24 Apr 2026 05:33:57 +0800 Subject: [PATCH 1/2] feat(i18n): add app language switching --- src-tauri/src/types.rs | 8 + src/features/about/components/AboutView.tsx | 38 +- .../app/components/ApprovalToasts.tsx | 18 +- .../app/components/LaunchScriptButton.tsx | 32 +- .../components/LaunchScriptEntryButton.tsx | 14 +- .../app/components/LaunchScriptIconPicker.tsx | 5 +- src/features/app/components/MainApp.tsx | 7 +- src/features/app/components/MainHeader.tsx | 55 +- .../app/components/MainHeaderActions.tsx | 15 +- src/features/app/components/OpenAppMenu.tsx | 18 +- .../components/PlanReadyFollowupMessage.tsx | 14 +- .../components/RequestUserInputMessage.tsx | 18 +- src/features/app/components/Sidebar.tsx | 18 +- .../app/components/SidebarBottomRail.tsx | 26 +- src/features/app/components/SidebarHeader.tsx | 40 +- .../app/components/SidebarSearchBar.tsx | 9 +- .../components/SidebarThreadsOnlySection.tsx | 11 +- .../app/components/SidebarWorkspaceGroups.tsx | 34 +- src/features/app/components/ThreadList.tsx | 10 +- src/features/app/components/ThreadLoading.tsx | 6 +- src/features/app/components/ThreadRow.tsx | 14 +- src/features/app/components/WorkspaceCard.tsx | 10 +- .../app/components/WorkspaceGroup.tsx | 13 +- src/features/app/components/WorktreeCard.tsx | 10 +- .../app/components/WorktreeSection.tsx | 5 +- .../app/hooks/useMainAppDisplayNodes.tsx | 9 +- src/features/composer/components/Composer.tsx | 32 +- .../components/ComposerAttachments.tsx | 10 +- .../composer/components/ComposerInput.tsx | 22 +- .../composer/components/ComposerMetaBar.tsx | 46 +- .../components/ComposerMobileActionsMenu.tsx | 11 +- .../composer/components/ComposerQueue.tsx | 21 +- .../components/ComposerSuggestionsPopover.tsx | 4 +- .../components/ReviewInlinePrompt.tsx | 60 +- .../hooks/useComposerDictationControls.ts | 18 +- src/features/debug/components/DebugPanel.tsx | 12 +- .../components/DictationWaveform.tsx | 4 +- .../files/components/FilePreviewPopover.tsx | 27 +- .../files/components/FileTreePanel.tsx | 50 +- .../git/components/BranchSwitcherPrompt.tsx | 12 +- src/features/git/components/GitDiffPanel.tsx | 101 +- .../git/components/GitDiffPanelDiffMode.tsx | 57 +- .../git/components/GitDiffPanelListModes.tsx | 30 +- .../git/components/GitDiffPanelOverview.tsx | 27 +- .../git/components/GitDiffPanelShared.tsx | 70 +- src/features/git/components/GitDiffViewer.tsx | 27 +- .../git/components/GitDiffViewerDiffCard.tsx | 32 +- .../GitDiffViewerPullRequestSummary.tsx | 42 +- src/features/git/components/ImageDiffCard.tsx | 16 +- .../git/components/InitGitRepoPrompt.tsx | 35 +- .../git/components/PierreDiffBlock.tsx | 4 +- src/features/home/components/Home.tsx | 7 +- .../components/HomeLatestAgentsSection.tsx | 15 +- .../home/components/HomeUsageSection.tsx | 47 +- .../layout/components/DesktopLayout.tsx | 10 +- src/features/layout/components/PanelTabs.tsx | 9 +- .../components/SidebarToggleControls.tsx | 29 +- .../layout/components/TabletLayout.tsx | 5 +- .../components/WindowCaptionControls.tsx | 10 +- .../hooks/layoutNodes/buildPrimaryNodes.tsx | 8 +- .../hooks/layoutNodes/buildSecondaryNodes.tsx | 27 +- src/features/layout/hooks/useLayoutNodes.tsx | 6 +- src/features/messages/components/Markdown.tsx | 10 +- .../messages/components/MessageRows.tsx | 86 +- src/features/messages/components/Messages.tsx | 22 +- .../messages/hooks/useFileLinkOpener.ts | 20 +- .../components/MobileServerSetupWizard.tsx | 30 +- .../notifications/components/ErrorToasts.tsx | 7 +- src/features/plan/components/PlanPanel.tsx | 6 +- .../prompts/components/PromptPanel.tsx | 98 +- .../settings/components/SettingsNav.tsx | 31 +- .../settings/components/SettingsView.test.tsx | 1 + .../settings/components/SettingsView.tsx | 14 +- .../sections/SettingsAboutSection.tsx | 45 +- .../sections/SettingsAgentsSection.tsx | 147 +- .../sections/SettingsCodexSection.tsx | 121 +- .../sections/SettingsComposerSection.tsx | 77 +- .../sections/SettingsDictationSection.tsx | 85 +- .../sections/SettingsDisplaySection.test.tsx | 51 + .../sections/SettingsDisplaySection.tsx | 139 +- .../sections/SettingsEnvironmentsSection.tsx | 56 +- .../sections/SettingsFeaturesSection.tsx | 46 +- .../sections/SettingsGitSection.tsx | 31 +- .../sections/SettingsOpenAppsSection.tsx | 66 +- .../sections/SettingsProjectsSection.tsx | 44 +- .../sections/SettingsServerSection.tsx | 154 +- .../sections/SettingsShortcutsSection.tsx | 67 +- .../components/settingsViewConstants.ts | 17 + src/features/settings/hooks/useAppSettings.ts | 5 + .../shared/components/FileEditorCard.tsx | 17 +- .../terminal/components/TerminalDock.tsx | 13 +- .../threads/components/RenameThreadPrompt.tsx | 16 +- .../update/components/UpdateToast.tsx | 48 +- .../workspaces/components/ClonePrompt.tsx | 30 +- .../MobileRemoteWorkspacePrompt.tsx | 18 +- .../components/WorkspaceFromUrlPrompt.tsx | 24 +- .../workspaces/components/WorkspaceHome.tsx | 18 +- .../components/WorkspaceHomeGitInitBanner.tsx | 11 +- .../components/WorkspaceHomeHistory.tsx | 38 +- .../components/WorkspaceHomeRunControls.tsx | 24 +- .../workspaces/components/WorktreePrompt.tsx | 27 +- src/i18n/i18n.test.ts | 30 + src/i18n/index.tsx | 1255 +++++++++++++++++ src/types.ts | 2 + 104 files changed, 3147 insertions(+), 1300 deletions(-) create mode 100644 src/i18n/i18n.test.ts create mode 100644 src/i18n/index.tsx diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 6b595106b..10051f475 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -489,6 +489,8 @@ pub(crate) struct AppSettings { pub(crate) ui_scale: f64, #[serde(default = "default_theme", rename = "theme")] pub(crate) theme: String, + #[serde(default = "default_app_language", rename = "appLanguage")] + pub(crate) app_language: String, #[serde( default = "default_usage_show_remaining", rename = "usageShowRemaining" @@ -707,6 +709,10 @@ fn default_theme() -> String { "system".to_string() } +fn default_app_language() -> String { + "system".to_string() +} + fn default_usage_show_remaining() -> bool { false } @@ -1155,6 +1161,7 @@ impl Default for AppSettings { last_composer_reasoning_effort: None, ui_scale: 1.0, theme: default_theme(), + app_language: default_app_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(), @@ -1321,6 +1328,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.app_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/features/about/components/AboutView.tsx b/src/features/about/components/AboutView.tsx index 12ab9696e..d44ae6e61 100644 --- a/src/features/about/components/AboutView.tsx +++ b/src/features/about/components/AboutView.tsx @@ -1,11 +1,39 @@ import { useEffect, useState } from "react"; import { getVersion } from "@tauri-apps/api/app"; import { openUrl } from "@tauri-apps/plugin-opener"; +import type { AppLanguagePreference } from "@/types"; +import { getAppSettings } from "@services/tauri"; +import { I18nProvider, useI18n } from "@/i18n"; const GITHUB_URL = "https://github.com/Dimillian/CodexMonitor"; const TWITTER_URL = "https://x.com/dimillian"; export function AboutView() { + const [language, setLanguage] = useState("system"); + + useEffect(() => { + let active = true; + void getAppSettings() + .then((settings) => { + if (active) { + setLanguage(settings.appLanguage); + } + }) + .catch(() => {}); + return () => { + active = false; + }; + }, []); + + return ( + + + + ); +} + +function AboutViewContent() { + const { tx } = useI18n(); const [version, setVersion] = useState(null); const handleOpenGitHub = () => { @@ -44,15 +72,15 @@ export function AboutView() { Codex Monitor icon -
Codex Monitor
+
{tx("Codex Monitor")}
- {version ? `Version ${version}` : "Version —"} + {version ? tx("Version {version}", { version }) : tx("Version —")}
- Monitor the situation of your Codex agents + {tx("Monitor the situation of your Codex agents")}
@@ -72,7 +100,7 @@ export function AboutView() { Twitter
-
Made with ♥ by Codex & Dimillian
+
{tx("Made with ♥ by Codex & Dimillian")}
); diff --git a/src/features/app/components/ApprovalToasts.tsx b/src/features/app/components/ApprovalToasts.tsx index 67a702399..46093bc84 100644 --- a/src/features/app/components/ApprovalToasts.tsx +++ b/src/features/app/components/ApprovalToasts.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo } from "react"; +import { useI18n } from "@/i18n"; import type { ApprovalRequest, WorkspaceInfo } from "../../../types"; import { getApprovalCommandInfo } from "../../../utils/approvalRules"; import { @@ -24,6 +25,7 @@ export function ApprovalToasts({ onDecision, onRemember, }: ApprovalToastsProps) { + const { tx } = useI18n(); const workspaceLabels = useMemo( () => new Map(workspaces.map((workspace) => [workspace.id, workspace.name])), [workspaces], @@ -75,7 +77,7 @@ export function ApprovalToasts({ const renderParamValue = (value: unknown) => { if (value === null || value === undefined) { - return { text: "None", isCode: false }; + return { text: tx("None"), isCode: false }; } if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { return { text: String(value), isCode: false }; @@ -103,7 +105,7 @@ export function ApprovalToasts({ role="alert" > - Approval needed + {tx("Approval needed")} {workspaceName ? (
{workspaceName}
) : null} @@ -132,7 +134,7 @@ export function ApprovalToasts({ }) ) : (
- No extra details. + {tx("No extra details.")}
)} @@ -141,22 +143,24 @@ export function ApprovalToasts({ className="secondary" onClick={() => onDecision(request, "decline")} > - Decline + {tx("Decline")} {commandInfo && onRemember ? ( ) : null} diff --git a/src/features/app/components/LaunchScriptButton.tsx b/src/features/app/components/LaunchScriptButton.tsx index 8e2d82f2d..72ebc66f5 100644 --- a/src/features/app/components/LaunchScriptButton.tsx +++ b/src/features/app/components/LaunchScriptButton.tsx @@ -1,4 +1,5 @@ import Play from "lucide-react/dist/esm/icons/play"; +import { useI18n } from "@/i18n"; import type { LaunchScriptIconId } from "../../../types"; import { PopoverSurface } from "../../design-system/components/popover/PopoverPrimitives"; import { useMenuController } from "../hooks/useMenuController"; @@ -54,6 +55,7 @@ export function LaunchScriptButton({ onNewDraftLabelChange, onCreateNew, }: LaunchScriptButtonProps) { + const { tx } = useI18n(); const editorMenu = useMenuController({ open: editorOpen, onDismiss: () => { @@ -76,9 +78,9 @@ export function LaunchScriptButton({ onOpenEditor(); }} data-tauri-drag-region="false" - aria-label={hasLaunchScript ? "Run launch script" : "Set launch script"} - title={hasLaunchScript ? "Run launch script" : "Set launch script"} - data-tooltip={hasLaunchScript ? "Run launch script" : "Set launch script"} + aria-label={hasLaunchScript ? tx("Run launch script") : tx("Set launch script")} + title={hasLaunchScript ? tx("Run launch script") : tx("Set launch script")} + data-tooltip={hasLaunchScript ? tx("Run launch script") : tx("Set launch script")} data-tooltip-placement="bottom" > @@ -86,10 +88,10 @@ export function LaunchScriptButton({ {editorOpen && ( -
Launch script
+
{tx("Launch script")}