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..2872bec80 100644 --- a/src/features/settings/components/sections/SettingsAboutSection.tsx +++ b/src/features/settings/components/sections/SettingsAboutSection.tsx @@ -4,8 +4,9 @@ import { isMobileRuntime, type AppBuildType, } from "@services/tauri"; -import { useUpdater } from "@/features/update/hooks/useUpdater"; import { SettingsSection } from "@/features/design-system/components/settings/SettingsPrimitives"; +import { useUpdater } from "@/features/update/hooks/useUpdater"; +import { useI18n } from "@/i18n/useI18n"; function formatBytes(value: number) { if (!Number.isFinite(value) || value <= 0) { @@ -22,6 +23,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 +74,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 +119,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 +149,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..3711e6da6 100644 --- a/src/features/settings/components/sections/SettingsAgentsSection.tsx +++ b/src/features/settings/components/sections/SettingsAgentsSection.tsx @@ -1,18 +1,18 @@ import { useEffect, useMemo, useState } from "react"; import { revealItemInDir } from "@tauri-apps/plugin-opener"; import type { ModelOption } from "@/types"; +import { + SettingsSection, + SettingsToggleRow, + SettingsToggleSwitch, +} from "@/features/design-system/components/settings/SettingsPrimitives"; import { MagicSparkleIcon, MagicSparkleLoaderIcon, } 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 +62,7 @@ export function SettingsAgentsSection({ modelOptionsLoading, modelOptionsError, }: SettingsAgentsSectionProps) { + const { t } = useI18n(); const [openPathError, setOpenPathError] = useState(null); const [maxThreadsDraft, setMaxThreadsDraft] = useState("6"); const [maxDepthDraft, setMaxDepthDraft] = useState("1"); @@ -162,7 +163,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 +182,10 @@ export function SettingsAgentsSection({ setCreateError(null); setEditError(null); setOpenPathError( - `Max threads must be an integer between ${MIN_MAX_THREADS} and ${MAX_MAX_THREADS}.`, + t("settings.agents.maxThreads.invalidRange", { + min: MIN_MAX_THREADS, + max: MAX_MAX_THREADS, + }), ); return; } @@ -216,7 +220,10 @@ export function SettingsAgentsSection({ setCreateError(null); setEditError(null); setOpenPathError( - `Max depth must be an integer between ${MIN_MAX_DEPTH} and ${MAX_MAX_DEPTH}.`, + t("settings.agents.maxDepth.invalidRange", { + min: MIN_MAX_DEPTH, + max: MAX_MAX_DEPTH, + }), ); return; } @@ -247,7 +254,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 +289,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; @@ -351,21 +358,26 @@ export function SettingsAgentsSection({ return (
- 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()}.} + title={t("settings.agents.configFile.title")} + subtitle={t("settings.agents.configFile.subtitle", { fileManager: fileManagerName() })} >
@@ -423,7 +441,7 @@ 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")} > ▲ @@ -431,14 +449,20 @@ export function SettingsAgentsSection({ - Maximum nested spawn depth. Valid range: 1-4. Changes save immediately. + {t("settings.agents.maxDepth.subtitle.before")} + 1-4 + {t("settings.agents.maxDepth.subtitle.after")} } > -
+
@@ -460,25 +484,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")} +