diff --git a/crates/tty7-core/src/core/config.rs b/crates/tty7-core/src/core/config.rs index 5d622fcb..b956d56f 100644 --- a/crates/tty7-core/src/core/config.rs +++ b/crates/tty7-core/src/core/config.rs @@ -167,6 +167,10 @@ pub struct Config { /// package manager's copy — and do not want it shadowed. #[serde(default = "default_true")] pub install_cli_on_path: bool, + /// GUI-only locale selection. Values: `en` or `zh-CN`. + /// CLI output stays English so agent/script integrations are stable. + #[serde(default = "default_gui_language")] + pub gui_language: String, #[serde(default = "default_notify_threshold_secs")] pub notify_threshold_secs: u64, #[serde(default = "default_true")] @@ -409,6 +413,7 @@ impl Default for Config { notify_on_command_finish: NotifyMode::Unfocused, check_for_updates: true, install_cli_on_path: true, + gui_language: default_gui_language(), notify_threshold_secs: default_notify_threshold_secs(), restore_session: true, show_tray_icon: true, @@ -496,6 +501,10 @@ impl Config { { self.link_file_command = None; } + match self.gui_language.as_str() { + "en" | "zh-CN" => {} + _ => self.gui_language = default_gui_language(), + } } pub fn save(&self) { @@ -647,6 +656,10 @@ fn default_true() -> bool { true } +fn default_gui_language() -> String { + "en".to_string() +} + fn default_word_separators() -> String { ",│`|:\"' ()[]{}<>\t".to_string() } @@ -1076,6 +1089,19 @@ mod tests { assert_eq!(clamp(100_000), 3600); } + #[test] + fn gui_language_defaults_to_english_and_rejects_unsupported_values() { + let cfg = Config::default(); + assert_eq!(cfg.gui_language, "en"); + + let cfg: Config = serde_json::from_str(r#"{"gui_language": "zh-CN"}"#).unwrap(); + assert_eq!(cfg.gui_language, "zh-CN"); + + let mut cfg: Config = serde_json::from_str(r#"{"gui_language": "ko"}"#).unwrap(); + cfg.sanitize(); + assert_eq!(cfg.gui_language, "en"); + } + #[test] fn keybinding_preset_and_prefix_default_and_round_trip() { let cfg = Config::default(); diff --git a/docs/features.md b/docs/features.md index e4878b7d..bc58f1b8 100644 --- a/docs/features.md +++ b/docs/features.md @@ -120,3 +120,16 @@ a brief pause; `prefix` + an unbound key passes straight through. - The PTY is read at device speed and parsed in large batches, off the render path - Hot paths are lock-free — a big `cat` never waits on drawing - The daemon buffers up to 16 MiB ahead of the window before backpressure applies + +## Localization + +The GUI ships English and Simplified Chinese strings. Pick one in Settings → +Appearance → Language, or in `config.json`: + +```json +{ "gui_language": "zh-CN" } +``` + +`en` and `zh-CN` are the only accepted values; anything else falls back to `en`. +The choice is explicit — the system language is never inferred. CLI output stays +English so agent and script integrations keep a stable, predictable surface. diff --git a/docs/features.zh-CN.md b/docs/features.zh-CN.md index 526d3bba..62ee5ee5 100644 --- a/docs/features.zh-CN.md +++ b/docs/features.zh-CN.md @@ -116,3 +116,15 @@ Aider、Amp、OpenCode 等约 17 个)并在其外围加功能 —— 绝不包 - 以设备速度读取 PTY,在渲染路径之外成批解析 - 热路径全程无锁 —— 再大的 `cat` 也不会阻塞在渲染上 - 触发背压前,守护进程最多可领先窗口缓冲 16 MiB + +## 本地化 + +GUI 目前提供英文和简体中文两套文案。在「设置 → 外观 → 语言」中选择,或直接改 +`config.json`: + +```json +{ "gui_language": "zh-CN" } +``` + +只接受 `en` 和 `zh-CN` 两个值,其它值一律回落到 `en`。语言必须显式指定,不会 +去猜系统语言。CLI 输出保持英文,保证 agent、脚本和开发者工作流的输出稳定可预测。 diff --git a/src/main.rs b/src/main.rs index 78ef59bb..921b35b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,10 +74,17 @@ fn spawn_config_watcher(cx: &mut App) { while rx.try_recv().is_ok() {} cx.update(|cx| { - cx.set_global(Config::load()); + let config = Config::load(); + crate::ui::i18n::set_locale(&config.gui_language); + cx.set_global(config); crate::ui::presets::load_registry(cx); crate::ui::theme::apply_cursor_hide_mode(cx); crate::ui::theme::apply_theme(None, cx); + // The menu bar is built once from the current locale, so editing + // gui_language by hand has to rebuild it the same way the + // in-app language picker does. + crate::ui::theme::set_menus(cx); + crate::ui::windows::WindowRegistry::refresh_locale(cx, None); cx.refresh_windows(); }); } @@ -319,6 +326,7 @@ fn main() { return; } let config = crate::core::config::Config::load(); + let gui_language = config.gui_language.clone(); // After the PATH enrichment above, which is what makes the candidate scan // see the user's real PATH rather than the stub a Finder launch inherits — @@ -344,6 +352,7 @@ fn main() { cx.activate(true); #[cfg(target_os = "macos")] set_dock_icon_for_bare_binary(); + crate::ui::i18n::set_locale(&gui_language); cx.set_global(Config::load()); crate::ui::theme::refresh_system_appearance(cx); crate::core::session::WorkspaceStore::init(cx); diff --git a/src/ui/app.rs b/src/ui/app.rs index 23a90b18..c909ff37 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -27,13 +27,15 @@ use crate::core::window_state::{WindowGeometry as _, WindowState}; use crate::daemon::protocol::{RemoteContext, ShellSpec, ssh_option_takes_value}; use crate::terminal::view::{ChildExited, TerminalView}; use crate::ui::host_registry::HostId; +use crate::ui::i18n::{L10nKey, set_locale, t, t_fmt}; use crate::ui::palette::{ ChromeState, Command, CommandGroup, CommandKind, PaletteEvent, PaletteView, }; use crate::ui::pane::{CloseOutcome, Dir, Pane, PaneSlot}; use crate::ui::presets::Fill; use crate::ui::settings::{ - Recording, SettingsSection, SettingsState, ThemeEditor, humanize_action, + ExplorerContextMenuNote, Recording, SettingsSection, SettingsState, ThemeEditor, + humanize_action, }; use crate::ui::theme::{apply_theme, set_menus, window_background}; @@ -479,27 +481,21 @@ impl Tty7App { }; let ours = crate::daemon::protocol::PROTOCOL_VERSION; let detail = match mismatch.version { - Some(v) => format!( - "The server holding your shells is from another build \ - (v{}, protocol {} — this app speaks {}). You can keep using it and \ - your shells stay, but features whose wire format changed may \ - misbehave until it's restarted. Restarting starts a clean server: \ - tabs reopen with fresh shells and anything running in them is \ - terminated.", - v.build, v.protocol, ours + Some(v) => t_fmt( + L10nKey::AppRestartServerMismatchDetail, + &[ + ("build", &v.build.to_string()), + ("protocol", &v.protocol.to_string()), + ("ours", &ours.to_string()), + ], ), - None => "The server holding your shells is from an older \ - version of the app. You can keep using it and your shells stay, \ - but newer features may misbehave until it's restarted. Restarting \ - starts a clean server: tabs reopen with fresh shells and anything \ - running in them is terminated." - .to_string(), + None => t(L10nKey::AppRestartServerOldDetail).to_string(), }; let answer = window.prompt( PromptLevel::Warning, - "Restart Server?", + t(L10nKey::AppRestartServerTitle), Some(&detail), - &["Keep Shells", "Restart"], + &[t(L10nKey::AppKeepShells), t(L10nKey::AppRestart)], cx, ); cx.spawn(async move |this, cx| { @@ -560,7 +556,9 @@ impl Tty7App { let mf_bind_port = cx.new(|cx| InputState::new(window, cx).placeholder("8080")); let mf_target_host = cx.new(|cx| InputState::new(window, cx).placeholder("127.0.0.1")); let mf_target_port = cx.new(|cx| InputState::new(window, cx).placeholder("80")); - let mf_description = cx.new(|cx| InputState::new(window, cx).placeholder("description")); + let mf_description = cx.new(|cx| { + InputState::new(window, cx).placeholder(t(L10nKey::AppPlaceholderDescription)) + }); let sidebar_width = cx.global::().sidebar_width; let right_panel_width = cx.global::().right_panel_width; let right_panel_visible = cx.global::().right_panel_visible; @@ -627,14 +625,18 @@ impl Tty7App { }, some => tabs_from_session(pane_ws.as_ref(), workspace, some, font_size, window, cx), }; - let sidebar_search = cx.new(|cx| InputState::new(window, cx).placeholder("Search tabs…")); + let sidebar_search = cx.new(|cx| { + InputState::new(window, cx).placeholder(t(crate::ui::i18n::L10nKey::SearchTabs)) + }); let sidebar_search_sub = cx.subscribe_in(&sidebar_search, window, |_this, _i, ev, _w, cx| { if matches!(ev, InputEvent::Change) { cx.notify(); } }); - let file_search = cx.new(|cx| InputState::new(window, cx).placeholder("Search files…")); + let file_search = cx.new(|cx| { + InputState::new(window, cx).placeholder(t(crate::ui::i18n::L10nKey::SearchFiles)) + }); let file_search_sub = cx.subscribe_in(&file_search, window, |_this, _i, ev, _w, cx| { if matches!(ev, InputEvent::Change) { cx.notify(); @@ -753,14 +755,12 @@ impl Tty7App { let answer = window.prompt( PromptLevel::Info, - "Close Window?", - Some( - "Your sessions keep running in the background. This \ - workspace will be waiting on the home page, and in the \ - workspace menu in the title bar, the next time you open \ - tty7.", - ), - &["Cancel", "Close"], + t(crate::ui::i18n::L10nKey::CloseWindowTitle), + Some(t(crate::ui::i18n::L10nKey::CloseWindowBody)), + &[ + t(crate::ui::i18n::L10nKey::Cancel), + t(crate::ui::i18n::L10nKey::Close), + ], cx, ); let close_confirmed = close_confirmed.clone(); @@ -976,7 +976,7 @@ impl Tty7App { window, cx, ) else { - window.push_notification("Could not reopen the tab: no terminal started", cx); + window.push_notification(t(L10nKey::AppReopenTabFailed), cx); self.closed.push(st); return; }; @@ -1105,14 +1105,12 @@ impl Tty7App { window.activate_window(); let answer = window.prompt( PromptLevel::Warning, - "Quit and Stop Server?", - Some( - "This quits tty7 and stops the background server — anything \ - still running in your shells is terminated. Your tabs and \ - layout are kept and reopen with fresh shells next launch. \ - (Plain Quit keeps shells running.)", - ), - &["Cancel", "Quit and Stop"], + t(crate::ui::i18n::L10nKey::QuitStopServerTitle), + Some(t(crate::ui::i18n::L10nKey::QuitStopServerBody)), + &[ + t(crate::ui::i18n::L10nKey::Cancel), + t(crate::ui::i18n::L10nKey::QuitAndStop), + ], cx, ); cx.spawn(async move |_this, cx| { @@ -1135,10 +1133,7 @@ impl Tty7App { let label = crate::ui::remote_connect::label_for(&target, cx); if !target.is_ssh() { window.push_notification( - format!( - "tty7 can only restart the server on machines it reaches over SSH. \ - {label} is served from this computer — stop its workspace instead." - ), + t_fmt(L10nKey::AppRestartServerNotSsh, &[("label", &label)]), cx, ); return; @@ -1149,13 +1144,9 @@ impl Tty7App { pub(crate) fn restart_daemon(&mut self, window: &mut Window, cx: &mut Context) { let answer = window.prompt( PromptLevel::Warning, - "Restart Server?", - Some( - "This stops every running shell on this computer — anything still \ - running in them will be terminated. Your tabs and layout are kept \ - and reopened with fresh shells.", - ), - &["Cancel", "Restart"], + t(L10nKey::AppRestartServerTitle), + Some(t(L10nKey::AppRestartServerBody)), + &[t(crate::ui::i18n::L10nKey::Cancel), t(L10nKey::AppRestart)], cx, ); cx.spawn(async move |this, cx| { @@ -1538,17 +1529,29 @@ impl Tty7App { let seed_specs: [(ThemeEdit, &str, u32); 5] = [ ( ThemeEdit::Background, - "Background", + t(L10nKey::AppThemeColorBackground), theme.background_color(), ), - (ThemeEdit::Foreground, "Foreground", theme.foreground), - (ThemeEdit::Accent, "Accent", theme.accent), + ( + ThemeEdit::Foreground, + t(L10nKey::AppThemeColorForeground), + theme.foreground, + ), + ( + ThemeEdit::Accent, + t(L10nKey::AppThemeColorAccent), + theme.accent, + ), ( ThemeEdit::Cursor, - "Cursor", + t(L10nKey::AppThemeColorCursor), theme.caret.unwrap_or(theme.accent), ), - (ThemeEdit::Selection, "Selection", neutrals.selection), + ( + ThemeEdit::Selection, + t(L10nKey::AppThemeColorSelection), + neutrals.selection, + ), ]; let mut subs = Vec::new(); @@ -1956,13 +1959,10 @@ impl Tty7App { crate::core::explorer_context_menu::unregister() }; let note = match result { - Ok(()) if register => { - "Registered. Right-click a folder or folder background in Explorer to open it in tty7." - .to_string() - } - Ok(()) => "Unregistered from Windows Explorer.".to_string(), - Err(error) if register => format!("Could not register: {error}"), - Err(error) => format!("Could not unregister: {error}"), + Ok(()) if register => ExplorerContextMenuNote::Registered, + Ok(()) => ExplorerContextMenuNote::Unregistered, + Err(error) if register => ExplorerContextMenuNote::RegisterFailed(error.to_string()), + Err(error) => ExplorerContextMenuNote::UnregisterFailed(error.to_string()), }; let status = crate::core::explorer_context_menu::status().map_err(|error| error.to_string()); @@ -2310,7 +2310,10 @@ impl Tty7App { Ok(view) => view, Err(e) => { log::error!("new tab spawn failed: {e}"); - window.push_notification(format!("Could not open a terminal: {e}"), cx); + window.push_notification( + t_fmt(L10nKey::AppOpenTerminalFailed, &[("error", &e.to_string())]), + cx, + ); return; } }; @@ -2339,7 +2342,13 @@ impl Tty7App { Ok(view) => view, Err(e) => { log::error!("native SSH spawn failed: {e}"); - window.push_notification(format!("SSH connection failed: {e}"), cx); + window.push_notification( + t_fmt( + L10nKey::AppSshConnectionFailed, + &[("error", &e.to_string())], + ), + cx, + ); return; } }; @@ -2366,7 +2375,10 @@ impl Tty7App { Ok(view) => view, Err(e) => { log::error!("native SSH respawn failed: {e}"); - window.push_notification(format!("SSH reconnect failed: {e}"), cx); + window.push_notification( + t_fmt(L10nKey::AppSshReconnectFailed, &[("error", &e.to_string())]), + cx, + ); return; } }; @@ -2403,7 +2415,13 @@ impl Tty7App { Ok(view) => PaneSlot::Ready(view), Err(e) => { log::error!("native SSH split spawn failed: {e}"); - window.push_notification(format!("SSH connection failed: {e}"), cx); + window.push_notification( + t_fmt( + L10nKey::AppSshConnectionFailed, + &[("error", &e.to_string())], + ), + cx, + ); return; } } @@ -2422,7 +2440,10 @@ impl Tty7App { Ok(view) => view, Err(e) => { log::error!("split spawn failed: {e}"); - window.push_notification(format!("Could not split the pane: {e}"), cx); + window.push_notification( + t_fmt(L10nKey::AppSplitPaneFailed, &[("error", &e.to_string())]), + cx, + ); return; } } @@ -2701,31 +2722,32 @@ impl Tty7App { }, move |_this, found, cx| { let Some(wt) = found else { return }; + let path = wt.path.display().to_string(); let detail = if wt.dirty { - format!( - "The closed tab's worktree at {} has uncommitted changes.", - wt.path.display() - ) + t_fmt(L10nKey::AppWorktreeRemoveDetailDirty, &[("path", &path)]) } else { - format!( - "The closed tab's worktree at {} is clean.", - wt.path.display() - ) + t_fmt(L10nKey::AppWorktreeRemoveDetailClean, &[("path", &path)]) }; - let title = format!("Remove worktree \"{}\"?", wt.branch); + let title = t_fmt(L10nKey::AppWorktreeRemoveTitle, &[("branch", &wt.branch)]); let level = if wt.dirty { PromptLevel::Warning } else { PromptLevel::Info }; let remove_label = if wt.dirty { - "Discard Changes & Remove" + t(L10nKey::AppWorktreeDiscardAndRemove) } else { - "Remove Worktree" + t(L10nKey::AppWorktreeRemove) }; cx.spawn(async move |this, cx| { let Ok(answer) = this.update_in(cx, |_, window, cx| { - window.prompt(level, &title, Some(&detail), &["Keep", remove_label], cx) + window.prompt( + level, + &title, + Some(&detail), + &[t(L10nKey::AppWorktreeKeep), remove_label], + cx, + ) }) else { return; }; @@ -2742,11 +2764,16 @@ impl Tty7App { move |h| crate::core::worktree::remove(h, &wt, force), move |_this, result, window, cx| match result { Ok(()) => window.push_notification( - format!("Removed worktree \"{branch}\""), + t_fmt(L10nKey::AppWorktreeRemoved, &[("branch", &branch)]), + cx, + ), + Err(e) => window.push_notification( + t_fmt( + L10nKey::AppWorktreeRemoveFailed, + &[("error", &e.to_string())], + ), cx, ), - Err(e) => window - .push_notification(format!("Worktree removal failed: {e}"), cx), }, ); }); @@ -2889,13 +2916,16 @@ impl Tty7App { Ok(view) => view, Err(e) => { log::error!("fork spawn failed: {e}"); - window.push_notification(format!("Could not open a terminal: {e}"), cx); + window.push_notification( + t_fmt(L10nKey::AppOpenTerminalFailed, &[("error", &e.to_string())]), + cx, + ); return; } }; let Some(terminal) = new.terminal() else { log::error!("fork spawn produced a pane that is still connecting"); - window.push_notification("Could not fork: the pane is still connecting", cx); + window.push_notification(t(L10nKey::AppForkStillConnecting), cx); return; }; terminal.read(cx).run_command_line(&cmd); @@ -2961,38 +2991,32 @@ impl Tty7App { let view = source.read(cx); let (agent, session, remote) = (view.agent(), view.agent_session(), view.remote_context()); let Some(agent) = agent else { - window.push_notification("This pane isn't running a coding agent", cx); + window.push_notification(t(L10nKey::AppPaneNoCodingAgent), cx); return None; }; let name = agent.display_name(); if agent.fork_label().is_none() { - window.push_notification(format!("tty7 has no fork command for {name}"), cx); + window.push_notification(t_fmt(L10nKey::AppForkNoCommand, &[("name", &name)]), cx); return None; } if remote.is_some() { - window.push_notification( - format!("{name} sessions can only be forked from a local pane"), - cx, - ); + window.push_notification(t_fmt(L10nKey::AppForkLocalOnly, &[("name", &name)]), cx); return None; } let session = session.unwrap_or_default(); let Some(id) = session.session_id.as_deref() else { - window.push_notification( - format!("tty7 hasn't seen a {name} session id in this pane — install its hooks in Settings → Agents"), - cx, - ); + window.push_notification(t_fmt(L10nKey::AppForkNoSessionId, &[("name", &name)]), cx); return None; }; let Some(cmd) = agent.fork_command(id, session.launch_argv.as_deref()) else { - window.push_notification(format!("{name}'s session id isn't a plain token"), cx); + window.push_notification( + t_fmt(L10nKey::AppForkSessionIdNotToken, &[("name", &name)]), + cx, + ); return None; }; if session.status == AgentStatus::Working { - window.push_notification( - format!("{name} is mid-turn — the fork won't include the turn in flight"), - cx, - ); + window.push_notification(t_fmt(L10nKey::AppForkMidTurn, &[("name", &name)]), cx); } Some(cmd) } @@ -3038,7 +3062,7 @@ impl Tty7App { cx: &mut Context, ) { let Some((host, cwd)) = self.tab_host_cwd(index, window, cx) else { - window.push_notification("This tab has no working directory yet", cx); + window.push_notification(t(L10nKey::AppTabNoWorkingDirectory), cx); return; }; let sheet_host = host.clone(); @@ -3050,7 +3074,10 @@ impl Tty7App { move |h| crate::core::worktree::defaults(h, &probe_cwd), move |this, result, window, cx| match result { Ok(defaults) => this.open_worktree_prompt(sheet_host, cwd, defaults, window, cx), - Err(e) => window.push_notification(format!("New worktree failed: {e}"), cx), + Err(e) => window.push_notification( + t_fmt(L10nKey::AppNewWorktreeFailed, &[("error", &e.to_string())]), + cx, + ), }, ); } @@ -3074,7 +3101,10 @@ impl Tty7App { Ok(view) => view, Err(e) => { log::error!("worktree tab spawn failed: {e}"); - window.push_notification(format!("Could not open a terminal: {e}"), cx); + window.push_notification( + t_fmt(L10nKey::AppOpenTerminalFailed, &[("error", &e.to_string())]), + cx, + ); return; } }; @@ -3214,7 +3244,7 @@ impl Tty7App { }; commands.push( Command::new( - format!("SSH: {title}"), + t_fmt(L10nKey::AppCmdSshProfileTitle, &[("title", &title)]), CommandKind::ConnectSavedProfile(p.id), ) .with_subtitle(subtitle) @@ -3229,7 +3259,7 @@ impl Tty7App { let label = self.tab_label(tab, i, None, cx); commands.push( Command::new( - format!("Switch to Tab: {label}"), + t_fmt(L10nKey::AppCmdSwitchToTab, &[("label", &label)]), CommandKind::ActivateTab(i), ) .in_group(CommandGroup::TabsPanes), @@ -3430,10 +3460,7 @@ impl Tty7App { fn deliver_agent_prompt(&mut self, prompt: &str, window: &mut Window, cx: &mut Context) { let Some(target) = self.agent_target_leaf(cx) else { - crate::terminal::notify_desktop( - Some("tty7"), - "No running coding agent found — start one (claude, codex, …) in a pane first.", - ); + crate::terminal::notify_desktop(Some("tty7"), t(L10nKey::AppNoRunningCodingAgent)); return; }; target.read(cx).send_agent_prompt(prompt); @@ -3456,10 +3483,7 @@ impl Tty7App { None => (None, None), }; let Some(selection) = selection else { - crate::terminal::notify_desktop( - Some("tty7"), - "Nothing selected — select some terminal output first.", - ); + crate::terminal::notify_desktop(Some("tty7"), t(L10nKey::AppNothingSelected)); return; }; let cwd = cwd.map(|c| c.to_string_lossy().into_owned()); @@ -3480,7 +3504,7 @@ impl Tty7App { Some((view.host(cx)?, view.host_cwd()?)) }); let Some((host, cwd)) = target else { - crate::terminal::notify_desktop(Some("tty7"), "This pane has no known directory."); + crate::terminal::notify_desktop(Some("tty7"), t(L10nKey::AppPaneNoKnownDirectory)); return; }; crate::ui::host_ops::HostOps::run_in( @@ -3503,7 +3527,7 @@ impl Tty7App { Some(prompt) => this.deliver_agent_prompt(&prompt, window, cx), None => crate::terminal::notify_desktop( Some("tty7"), - &format!("No uncommitted changes in {cwd_s} (or not a git repository)."), + &t_fmt(L10nKey::AppNoUncommittedChanges, &[("cwd", &cwd_s)]), ), } }, @@ -3520,12 +3544,15 @@ impl Tty7App { let mut subs = Vec::new(); let (font_select, font_bold_select, font_italic_select) = self.build_font_selects(&mut subs, window, cx); + let language_select = self.build_language_select(&mut subs, window, cx); let (shell_program_input, shell_args_input, wd_path_input) = self.build_shell_inputs(&mut subs, window, cx); let link_file_command_input = self.build_link_file_command_input(&mut subs, window, cx); let scroll_slider = self.build_scroll_slider(&mut subs, window, cx); let window_opacity_slider = self.build_window_opacity_slider(&mut subs, window, cx); - let theme_search = cx.new(|cx| InputState::new(window, cx).placeholder("Search themes…")); + let theme_search = cx.new(|cx| { + InputState::new(window, cx).placeholder(t(crate::ui::i18n::L10nKey::SearchThemes)) + }); subs.push( cx.subscribe_in(&theme_search, window, |_this, _i, ev, _w, cx| { if matches!(ev, InputEvent::Change) { @@ -3533,8 +3560,9 @@ impl Tty7App { } }), ); - let settings_search = - cx.new(|cx| InputState::new(window, cx).placeholder("Search settings…")); + let settings_search = cx.new(|cx| { + InputState::new(window, cx).placeholder(t(crate::ui::i18n::L10nKey::SearchSettings)) + }); subs.push( cx.subscribe_in(&settings_search, window, |this, _i, ev, _w, cx| { if matches!(ev, InputEvent::Change) { @@ -3544,7 +3572,9 @@ impl Tty7App { }), ); - let ssh_filter = cx.new(|cx| InputState::new(window, cx).placeholder("Filter hosts…")); + let ssh_filter = cx.new(|cx| { + InputState::new(window, cx).placeholder(t(crate::ui::i18n::L10nKey::FilterHosts)) + }); subs.push( cx.subscribe_in(&ssh_filter, window, |_this, _i, ev, _w, cx| { if matches!(ev, InputEvent::Change) { @@ -3553,8 +3583,9 @@ impl Tty7App { }), ); - let ssh_quick_connect = - cx.new(|cx| InputState::new(window, cx).placeholder("user@host or user@host:port")); + let ssh_quick_connect = cx.new(|cx| { + InputState::new(window, cx).placeholder(t(L10nKey::AppPlaceholderSshQuickConnect)) + }); subs.push( cx.subscribe_in(&ssh_quick_connect, window, |_this, _i, ev, _w, cx| { if matches!(ev, InputEvent::Change) { @@ -3570,6 +3601,7 @@ impl Tty7App { font_select, font_bold_select, font_italic_select, + language_select, shell_program_input, shell_args_input, wd_path_input, @@ -3646,7 +3678,7 @@ impl Tty7App { window: &mut Window, cx: &mut Context| { let mut rows = Vec::with_capacity(names.len() + 1); - rows.push(crate::ui::settings::FONT_DEFAULT_LABEL.to_string()); + rows.push(crate::ui::settings::font_default_label().to_string()); rows.extend(names.iter().cloned()); let selected = value .as_ref() @@ -3694,6 +3726,117 @@ impl Tty7App { (font_select, font_bold_select, font_italic_select) } + fn build_language_select( + &mut self, + subs: &mut Vec, + window: &mut Window, + cx: &mut Context, + ) -> Entity>> { + const CODES: &[&str] = &["en", "zh-CN"]; + let labels = || { + vec![ + t(L10nKey::SettingsLanguageEnglish).to_string(), + t(L10nKey::SettingsLanguageChinese).to_string(), + ] + }; + let cfg = cx.global::(); + let current = Self::normalize_gui_language(&cfg.gui_language); + let rows = labels(); + let selected = CODES.iter().position(|c| *c == current).unwrap_or(0); + let language_select = cx.new(|cx| { + SelectState::new( + SearchableVec::new(rows), + Some(IndexPath::default().row(selected)), + window, + cx, + ) + }); + subs.push(cx.subscribe_in( + &language_select, + window, + move |this, _select, ev: &SelectEvent>, window, cx| { + if let SelectEvent::Confirm(Some(label)) = ev { + let rows = labels(); + if let Some(idx) = rows.iter().position(|r| r == label) { + this.set_gui_language(CODES[idx], window, cx); + } + } + }, + )); + language_select + } + + fn normalize_gui_language(code: &str) -> &'static str { + match code { + "zh-CN" => "zh-CN", + _ => "en", + } + } + + pub(crate) fn set_gui_language( + &mut self, + code: &'static str, + window: &mut Window, + cx: &mut Context, + ) { + let code = Self::normalize_gui_language(code); + { + let cfg = cx.global_mut::(); + cfg.gui_language = code.to_string(); + } + set_locale(code); + cx.global::().save(); + set_menus(cx); + self.refresh_locale_state(window, cx); + crate::ui::windows::WindowRegistry::refresh_locale(cx, Some(self.workspace)); + } + + pub(crate) fn refresh_locale_state(&mut self, window: &mut Window, cx: &mut Context) { + const CODES: &[&str] = &["en", "zh-CN"]; + self.sidebar_search.update(cx, |state, cx| { + state.set_placeholder(t(L10nKey::SearchTabs), window, cx) + }); + self.file_search.update(cx, |state, cx| { + state.set_placeholder(t(L10nKey::SearchFiles), window, cx) + }); + if let Some(s) = self.active_settings() { + let rows = vec![ + t(L10nKey::SettingsLanguageEnglish).to_string(), + t(L10nKey::SettingsLanguageChinese).to_string(), + ]; + s.language_select.update(cx, |state, cx| { + state.set_items(SearchableVec::new(rows), window, cx); + let code = Self::normalize_gui_language(&cx.global::().gui_language); + let selected = CODES.iter().position(|c| *c == code).unwrap_or(0); + state.set_selected_index(Some(IndexPath::default().row(selected)), window, cx); + }); + s.search.update(cx, |state, cx| { + state.set_placeholder(t(L10nKey::SearchSettings), window, cx) + }); + s.theme_search.update(cx, |state, cx| { + state.set_placeholder(t(L10nKey::SearchThemes), window, cx) + }); + s.ssh_filter.update(cx, |state, cx| { + state.set_placeholder(t(L10nKey::FilterHosts), window, cx) + }); + s.ssh_quick_connect.update(cx, |state, cx| { + state.set_placeholder(t(L10nKey::AppPlaceholderSshQuickConnect), window, cx) + }); + s.shell_args_input.update(cx, |state, cx| { + state.set_placeholder(t(L10nKey::AppPlaceholderNone), window, cx) + }); + if !cfg!(windows) { + s.shell_program_input.update(cx, |state, cx| { + state.set_placeholder(t(L10nKey::AppPlaceholderLoginShell), window, cx) + }); + } + s.link_file_command_input.update(cx, |state, cx| { + state.set_placeholder(t(L10nKey::AppPlaceholderOpenInDefaultApp), window, cx) + }); + } + cx.notify(); + } + fn build_shell_inputs( &mut self, subs: &mut Vec, @@ -3709,7 +3852,7 @@ impl Tty7App { let platform_default = if cfg!(windows) { "PowerShell" } else { - "login shell" + t(L10nKey::AppPlaceholderLoginShell) }; let shell_program_input = cx.new(|cx| { InputState::new(window, cx) @@ -3718,7 +3861,7 @@ impl Tty7App { }); let shell_args_input = cx.new(|cx| { InputState::new(window, cx) - .placeholder("none") + .placeholder(t(L10nKey::AppPlaceholderNone)) .default_value(shell_args) }); let wd_path_input = cx.new(|cx| { @@ -3767,7 +3910,7 @@ impl Tty7App { .unwrap_or_default(); let input = cx.new(|cx| { InputState::new(window, cx) - .placeholder("open in default app") + .placeholder(t(L10nKey::AppPlaceholderOpenInDefaultApp)) .default_value(value) }); subs.push( @@ -3927,7 +4070,7 @@ impl Tty7App { } fn commit_font_family_emphasis(&mut self, bold: bool, name: String, cx: &mut Context) { - let family = (name != crate::ui::settings::FONT_DEFAULT_LABEL).then_some(name); + let family = (name != crate::ui::settings::font_default_label()).then_some(name); for tab in &self.tabs { for leaf in tab.pane.terminals() { let family = family.clone(); @@ -4270,7 +4413,7 @@ impl Tty7App { use crate::ui::settings::AgentHooksMachine; let mut out = vec![AgentHooksMachine { host: crate::ui::host_ops::HostId::LOCAL, - label: "This Computer".to_string(), + label: t(L10nKey::AppAgentHooksThisComputer).to_string(), }]; let configured = crate::ui::remote_connect::available_hosts(cx); for id in crate::ui::host_registry::HostRegistry::ids(cx) { @@ -4281,7 +4424,7 @@ impl Tty7App { .iter() .find(|h| h.target.host_id() == id) .map(|h| h.label.clone()) - .unwrap_or_else(|| "Remote machine".to_string()); + .unwrap_or_else(|| t(L10nKey::AppAgentHooksRemoteMachine).to_string()); out.push(AgentHooksMachine { host: id, label }); } out @@ -4332,8 +4475,7 @@ impl Tty7App { }; let Some((host, home)) = self.agent_hooks_link(host_id, cx) else { if let Some(s) = self.settings.as_mut() { - s.agent_hooks_states = - AgentHooksView::Unavailable(Self::AGENT_HOOKS_OFFLINE.into()); + s.agent_hooks_states = AgentHooksView::Unavailable(Self::agent_hooks_offline_msg()); } cx.notify(); return; @@ -4365,9 +4507,7 @@ impl Tty7App { s.agent_hooks_states = match rows { Some(rows) => AgentHooksView::Ready(rows), None => AgentHooksView::Unavailable( - "tty7 could not work out this computer's home directory, so there is \ - nowhere to install to." - .into(), + t(L10nKey::AppAgentHooksNoHomeDir).to_string(), ), }; cx.notify(); @@ -4376,10 +4516,9 @@ impl Tty7App { ); } - const AGENT_HOOKS_OFFLINE: &'static str = concat!( - "Not connected to this machine, so its agent config can't be read or ", - "written. Open a workspace on it and come back." - ); + fn agent_hooks_offline_msg() -> String { + t(L10nKey::AppAgentHooksOffline).to_string() + } fn agent_hooks_link( &self, @@ -4426,9 +4565,9 @@ impl Tty7App { }; let Some((host, home)) = self.agent_hooks_link(host_id, cx) else { if let Some(s) = self.settings.as_mut() { - s.agent_hooks_note = Some((agent, Self::AGENT_HOOKS_OFFLINE.to_string())); + s.agent_hooks_note = Some((agent, Self::agent_hooks_offline_msg())); s.agent_hooks_states = crate::ui::settings::AgentHooksView::Unavailable( - Self::AGENT_HOOKS_OFFLINE.into(), + Self::agent_hooks_offline_msg(), ); } cx.notify(); @@ -4441,8 +4580,9 @@ impl Tty7App { move |h| { let target = match &home { Some(home) => HookTarget::remote(h, home.clone()), - None => HookTarget::local(h) - .ok_or_else(|| anyhow::anyhow!("cannot resolve home directory"))?, + None => HookTarget::local(h).ok_or_else(|| { + anyhow::anyhow!("{}", t(L10nKey::AppAgentHooksHomeDirUnresolved)) + })?, }; if install { crate::core::agent_hooks::install_hooks(&target, agent) @@ -4456,7 +4596,9 @@ impl Tty7App { agent, match result { Ok(summary) => summary, - Err(e) => format!("Failed: {e}"), + Err(e) => { + t_fmt(L10nKey::AppAgentHooksOpFailed, &[("error", &e.to_string())]) + } }, )); } @@ -4600,10 +4742,12 @@ impl Tty7App { .find(|(a, k)| *k == spec && *a != action) .map(|(a, _)| a); let note = displaced.as_ref().map(|other| { - format!( - "{} took the shortcut from {}, which is now unset.", - humanize_action(&action), - humanize_action(other) + t_fmt( + L10nKey::AppKeybindingDisplacedNote, + &[ + ("action", &humanize_action(&action)), + ("previous", &humanize_action(other)), + ], ) }); self.update_config(cx, |cfg| { @@ -5630,7 +5774,7 @@ pub(crate) fn new_terminal( .workspace .as_ref() .map(|w| w.target.to_string()) - .unwrap_or_else(|| "the local server".to_string()); + .unwrap_or_else(|| t(L10nKey::AppLocalServerName).to_string()); let pending = cx.new(|cx| crate::ui::pending_pane::PendingPane::new(machine, spawn, cx)); cx.subscribe_in( &pending, @@ -5796,7 +5940,7 @@ pub(crate) fn parse_ssh_connect_input(input: &str) -> Result Result Result v.clone(), - None => return Err(format!("-{flag} needs a value")), + None => { + return Err(t_fmt( + L10nKey::AppSshParseFlagNeedsValue, + &[("flag", &flag.to_string())], + )); + } } } } else { @@ -5834,7 +5983,9 @@ pub(crate) fn parse_ssh_connect_input(input: &str) -> Result() .ok() .filter(|&p| p != 0) - .ok_or_else(|| format!("Invalid port \u{201c}{value}\u{201d}"))?, + .ok_or_else(|| { + t_fmt(L10nKey::AppSshParseInvalidPort, &[("value", &value)]) + })?, ) } 'l' => user = Some(value), @@ -5844,7 +5995,10 @@ pub(crate) fn parse_ssh_connect_input(input: &str) -> Result {} } } else if word.starts_with('-') { - return Err(format!("Unsupported option \u{201c}{word}\u{201d}")); + return Err(t_fmt( + L10nKey::AppSshParseUnsupportedOption, + &[("option", &word)], + )); } else if target.is_none() { target = Some(word); } else { @@ -5853,9 +6007,9 @@ pub(crate) fn parse_ssh_connect_input(input: &str) -> Result() .ok() .filter(|&p| p != 0) - .ok_or_else(|| format!("Invalid port \u{201c}{val}\u{201d}"))?, + .ok_or_else(|| t_fmt(L10nKey::AppSshParseInvalidPort, &[("value", val)]))?, ) } "proxyjump" => *jump = Some(val.to_string()), diff --git a/src/ui/code_editor.rs b/src/ui/code_editor.rs index a3509a55..4f3fbd9b 100644 --- a/src/ui/code_editor.rs +++ b/src/ui/code_editor.rs @@ -15,6 +15,7 @@ use gpui_component::{ use crate::ui::app::Tty7App; use crate::ui::host_ops::{HostOps, MTime, SharedHost, WatchSub}; +use crate::ui::i18n::{L10nKey, t, t_fmt}; const MAX_FILE_BYTES: u64 = 4 * 1024 * 1024; @@ -362,24 +363,43 @@ impl Tty7App { let path = h.canonicalize(&p).unwrap_or(p); let meta = match h.stat(&path) { Ok(m) => m, - Err(e) => return Err(format!("Can't open {}: {e}", path.display())), + Err(e) => { + return Err(t_fmt( + L10nKey::EditorCantOpen, + &[("path", &path.display().to_string()), ("e", &e.to_string())], + )); + } }; if meta.len > MAX_FILE_BYTES { - return Err(format!( - "\"{}\" is too large for the editor ({} MB)", - path.display(), - meta.len / (1024 * 1024) + return Err(t_fmt( + L10nKey::EditorFileTooLarge, + &[ + ("path", &path.display().to_string()), + ("size", &(meta.len / (1024 * 1024)).to_string()), + ], )); } let bytes = match h.read_file(&path, MAX_FILE_BYTES) { Ok(b) => b, - Err(e) => return Err(format!("Can't read {}: {e}", path.display())), + Err(e) => { + return Err(t_fmt( + L10nKey::EditorCantRead, + &[("path", &path.display().to_string()), ("e", &e.to_string())], + )); + } }; if looks_binary(&bytes) { - return Err(format!("\"{}\" looks like a binary file", path.display())); + return Err(t_fmt( + L10nKey::EditorBinaryFile, + &[("path", &path.display().to_string())], + )); } - let text = String::from_utf8(bytes) - .map_err(|_| format!("\"{}\" is not valid UTF-8", path.display()))?; + let text = String::from_utf8(bytes).map_err(|_| { + t_fmt( + L10nKey::EditorNotUtf8, + &[("path", &path.display().to_string())], + ) + })?; Ok((path, text, meta.mtime)) }, move |app, opened, window, cx| match opened { @@ -603,7 +623,7 @@ impl Tty7App { Ok(mtime) => { f.disk_mtime = mtime; } - Err(e) => HostOps::notify_err(window, cx, "Save failed", &e), + Err(e) => HostOps::notify_err(window, cx, t(L10nKey::EditorSaveFailed), &e), } if landing.clean { f.dirty = false; @@ -658,9 +678,13 @@ impl Tty7App { let name = f.label(); let answer = window.prompt( PromptLevel::Warning, - &format!("\"{name}\" has unsaved changes"), + &t_fmt(L10nKey::EditorUnsavedChanges, &[("name", &name)]), None, - &["Save", "Discard", "Cancel"], + &[ + t(L10nKey::Save), + t(L10nKey::EditorDiscard), + t(L10nKey::Cancel), + ], cx, ); let id = f.input.entity_id(); @@ -937,7 +961,9 @@ impl Tty7App { .when(name.is_none(), |d| { d.text_color(cx.theme().muted_foreground) }) - .child(name.unwrap_or_else(|| SharedString::from("No file open"))), + .child( + name.unwrap_or_else(|| SharedString::from(t(L10nKey::EditorNoFileOpen))), + ), ) .when(dirty, |d| { d.child( @@ -958,7 +984,7 @@ impl Tty7App { cx, ) .rounded_lg() - .tooltip("Back to Terminal (Esc)") + .tooltip(t(L10nKey::EditorBackToTerminal)) .on_click(cx.listener(|this, _, window, cx| { this.toggle_code_panel(window, cx); })), @@ -992,7 +1018,14 @@ impl Tty7App { let active = code.and_then(|c| c.active_file()); let cursor: Option = active.map(|f| { let pos = f.input.read(cx).cursor_position(); - format!("Ln {}, Col {}", pos.line + 1, pos.character + 1).into() + t_fmt( + L10nKey::EditorLnCol, + &[ + ("line", &(pos.line + 1).to_string()), + ("column", &(pos.character + 1).to_string()), + ], + ) + .into() }); let wrap: Option = active.map(|f| f.wrap); let is_markdown = active.is_some_and(|f| language_for_path(&f.path) == "markdown"); @@ -1016,7 +1049,11 @@ impl Tty7App { .when(is_markdown, |this| { this.child( Button::new("status-md-preview") - .label(if preview { "Edit" } else { "Preview" }) + .label(if preview { + t(L10nKey::EditorEdit) + } else { + t(L10nKey::EditorPreview) + }) .custom(crate::ui::tab_strip::chrome_tile_variant(cx)) .xsmall() .on_click(cx.listener(|this, _, _w, cx| { @@ -1033,7 +1070,11 @@ impl Tty7App { .when_some(wrap, |this, wrap| { this.child( Button::new("status-wrap") - .label(if wrap { "Wrap: on" } else { "Wrap: off" }) + .label(if wrap { + t(L10nKey::EditorWrapOn) + } else { + t(L10nKey::EditorWrapOff) + }) .custom(crate::ui::tab_strip::chrome_tile_variant(cx)) .xsmall() .on_click(cx.listener(|this, _, window, cx| { @@ -1069,7 +1110,9 @@ impl Tty7App { div() .text_sm() .text_color(cx.theme().muted_foreground) - .child("Open a file from the file tree"), + .child(crate::ui::i18n::t( + crate::ui::i18n::L10nKey::OpenFileFromTree, + )), ) } @@ -1087,10 +1130,12 @@ impl Tty7App { .border_b_1() .border_color(cx.theme().border) .text_sm() - .child(div().flex_1().child("File changed on disk")) + .child(div().flex_1().child(crate::ui::i18n::t( + crate::ui::i18n::L10nKey::FileChangedOnDisk, + ))) .child( Button::new("editor-conflict-reload") - .label("Reload") + .label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Reload)) .small() .on_click(cx.listener(move |this, _, window, cx| { this.editor_reload_from_disk(tab_ix, ix, window, cx); @@ -1098,7 +1143,7 @@ impl Tty7App { ) .child( Button::new("editor-conflict-keep") - .label("Keep mine") + .label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::KeepMine)) .ghost() .small() .on_click(cx.listener(move |this, _, _w, cx| { diff --git a/src/ui/diff_overlay.rs b/src/ui/diff_overlay.rs index 8662337e..da52eaa7 100644 --- a/src/ui/diff_overlay.rs +++ b/src/ui/diff_overlay.rs @@ -13,6 +13,7 @@ use crate::terminal::git_diff::{ MAX_RENDERED_FILES, Truncation, }; use crate::ui::app::Tty7App; +use crate::ui::i18n::{L10nKey, t, t_fmt, t_plural}; use crate::ui::rounding; use crate::ui::rounding::RoundedCorners as _; @@ -241,14 +242,13 @@ impl Tty7App { let overlay = self.tabs.get(self.active)?.diff_overlay.as_ref()?; let content = match &overlay.load { - DiffLoad::Loading => self.diff_message("Reading diff…", cx), - DiffLoad::NotARepo => self.diff_message("Not a git repository", cx), - DiffLoad::Ready(snap) if empty_snapshot(snap) && snap.read_failed => self.diff_message( - "Couldn't read the working-tree diff — retrying on the next refresh.", - cx, - ), + DiffLoad::Loading => self.diff_message(t(L10nKey::DiffReading), cx), + DiffLoad::NotARepo => self.diff_message(t(L10nKey::DiffNotARepo), cx), + DiffLoad::Ready(snap) if empty_snapshot(snap) && snap.read_failed => { + self.diff_message(t(L10nKey::DiffReadFailed), cx) + } DiffLoad::Ready(snap) if empty_snapshot(snap) => { - self.diff_message("Working tree clean", cx) + self.diff_message(t(L10nKey::DiffWorkingTreeClean), cx) } DiffLoad::Ready(snap) => { self.diff_file_list(snap, &overlay.expanded, focused_file(snap, overlay), cx) @@ -367,13 +367,9 @@ impl Tty7App { .when( matches!(overlay.load, DiffLoad::Ready(_)) && overlay.focus.is_none(), |bar| { - let mut summary = format!( - "{} changed file{}", - files, - if files == 1 { "" } else { "s" } - ); + let mut summary = t_plural(L10nKey::DiffChangedFiles, files, &[]); if untracked > 0 { - summary.push_str(&format!(" · {untracked} untracked")); + summary.push_str(&t_plural(L10nKey::DiffUntrackedCount, untracked, &[])); } bar.child( div() @@ -406,7 +402,7 @@ impl Tty7App { div() .text_xs() .text_color(cx.theme().muted_foreground) - .child("refreshing…"), + .child(t(L10nKey::Refreshing)), ) }, ) @@ -421,7 +417,7 @@ impl Tty7App { cx, ) .rounded_lg() - .tooltip("Close Diff (Esc)") + .tooltip(t(L10nKey::DiffCloseTooltip)) .on_click(cx.listener(|this, _, window, cx| { this.close_diff_overlay(window, cx); })), @@ -478,10 +474,7 @@ impl Tty7App { .py_1p5() .text_xs() .text_color(cx.theme().muted_foreground) - .child(format!( - "… and {rest} more changed file{} — run `git diff` in the terminal to see them.", - if rest == 1 { "" } else { "s" } - )), + .child(t_plural(L10nKey::DiffMoreFiles, rest, &[])), ); } if focused.is_none() && !snap.untracked.is_empty() { @@ -502,10 +495,9 @@ impl Tty7App { stats: &DiffStats, cx: &Context, ) -> AnyElement { - let text = format!( - "This working tree is too large to render efficiently ({}). Every file is \ - collapsed — expand individual files, or run `git diff` in the terminal.", - oversized_summary(snap, stats), + let text = t_fmt( + L10nKey::DiffOversizedNotice, + &[("summary", &oversized_summary(snap, stats))], ); div() .w_full() @@ -607,7 +599,7 @@ impl Tty7App { .flex_shrink_0() .text_xs() .text_color(cx.theme().muted_foreground) - .child("binary"), + .child(t(L10nKey::Binary)), ); } if file.added > 0 { @@ -671,15 +663,11 @@ impl Tty7App { } if let Some(reason) = file.truncated { let note = match reason { - Truncation::PerFile => format!( - "Diff truncated at {} lines — run `git diff` in the terminal for the rest.", - git_diff::MAX_LINES_PER_FILE + Truncation::PerFile => t_fmt( + L10nKey::DiffTruncatedPerFile, + &[("limit", &git_diff::MAX_LINES_PER_FILE.to_string())], ), - Truncation::Budget => { - "Body not loaded — this working tree is past tty7's diff budget. \ - Run `git diff` in the terminal for this file." - .to_string() - } + Truncation::Budget => t(L10nKey::DiffTruncatedBudget).to_string(), }; body = body.child( div() @@ -778,7 +766,7 @@ impl Tty7App { .bg(cx.theme().secondary) .text_xs() .text_color(cx.theme().muted_foreground) - .child(format!("Untracked files ({total})")), + .child(t_plural(L10nKey::DiffUntrackedHeader, total, &[])), ); for path in untracked { section = section.child( @@ -809,9 +797,7 @@ impl Tty7App { .py_1() .text_xs() .text_color(cx.theme().muted_foreground) - .child(format!( - "… and {rest} more — run `git status` in the terminal to see them.", - )), + .child(t_plural(L10nKey::DiffMoreUntracked, rest, &[])), ); } section.into_any_element() @@ -843,31 +829,36 @@ fn file_expanded(file: &FileDiff, expanded: &HashMap, collapse_all } fn oversized_summary(snap: &DiffSnapshot, stats: &DiffStats) -> String { - let mut parts = vec![format!( - "{} changed file{}", - snap.files.len(), - if snap.files.len() == 1 { "" } else { "s" } - )]; + let mut parts = vec![t_plural(L10nKey::DiffChangedFiles, snap.files.len(), &[])]; let (added, removed) = stats.totals; let total_lines = (added + removed) as usize; let loaded = stats.retained_lines; let budget = stats.budget_exhausted; let per_file = stats.per_file_truncated; parts.push(match (budget, per_file) { - (false, false) => format!("{total_lines} diff lines"), + (false, false) => t_plural(L10nKey::DiffLines, total_lines, &[]), _ => { - let cap = match (budget, per_file) { - (true, true) => "tty7's budget and the per-file cap", - (true, false) => "tty7's budget", - _ => "the per-file cap", + let cap_key = match (budget, per_file) { + (true, true) => L10nKey::DiffBudgetAndCap, + (true, false) => L10nKey::DiffBudget, + _ => L10nKey::DiffPerFileCap, }; - format!( - "{total_lines} changed lines, {loaded} diff rows loaded before {cap} cut the rest" + t_fmt( + L10nKey::DiffChangedLines, + &[ + ("total", &total_lines.to_string()), + ("loaded", &loaded.to_string()), + ("cap", t(cap_key)), + ], ) } }); if stats.untracked_count > 0 { - parts.push(format!("{} untracked", stats.untracked_count)); + parts.push(t_plural( + L10nKey::DiffUntrackedSummary, + stats.untracked_count, + &[], + )); } parts.join(", ") } @@ -948,6 +939,7 @@ fn split_hunk(lines: &[git_diff::DiffLine]) -> Vec { mod tests { use super::*; use crate::terminal::git_diff::{DiffLine, LineKind}; + use crate::ui::i18n::set_locale; fn line(kind: LineKind, old: Option, new: Option, text: &str) -> DiffLine { DiffLine { @@ -1037,6 +1029,7 @@ mod tests { } fn banner(snap: &DiffSnapshot) -> String { + set_locale("en"); oversized_summary(snap, &snap.stats()) } diff --git a/src/ui/file_tree.rs b/src/ui/file_tree.rs index bb8a70e4..3e718bfa 100644 --- a/src/ui/file_tree.rs +++ b/src/ui/file_tree.rs @@ -6,6 +6,7 @@ use crate::core::config::RightPanelTab; use crate::ui::app::Tty7App; use crate::ui::host_ops::{ByHost, HostId, HostOps, InFlight, SharedHost, WatchSub}; use crate::ui::host_registry::HostRegistry; +use crate::ui::i18n::{L10nKey, t, t_fmt}; use gpui::prelude::*; use gpui::{ AnyElement, App, Context, Entity, ExternalPaths, FocusHandle, KeyDownEvent, MouseButton, @@ -904,9 +905,9 @@ impl Tty7App { }; let input = cx.new(|cx| { let mut st = InputState::new(window, cx).placeholder(match edit_for { - TreeEditKind::NewFile => "file name", - TreeEditKind::NewFolder => "folder name", - TreeEditKind::Rename => "new name", + TreeEditKind::NewFile => t(L10nKey::FileTreePlaceholderFileName), + TreeEditKind::NewFolder => t(L10nKey::FileTreePlaceholderFolderName), + TreeEditKind::Rename => t(L10nKey::FileTreePlaceholderNewName), }); st.set_value(initial, window, cx); st @@ -1048,15 +1049,15 @@ impl Tty7App { .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| path.display().to_string()); let detail = if is_dir { - "The folder and everything inside it will be deleted." + t(L10nKey::FileTreeDeleteFolderBody) } else { - "The file will be deleted." + t(L10nKey::FileTreeDeleteFileBody) }; let answer = window.prompt( PromptLevel::Warning, - &format!("Delete \"{name}\"?"), + &t_fmt(L10nKey::FileTreeDeleteTitle, &[("name", &name)]), Some(detail), - &["Cancel", "Delete"], + &[t(L10nKey::Cancel), t(L10nKey::Delete)], cx, ); cx.spawn_in(window, async move |app, cx| { @@ -1096,7 +1097,12 @@ impl Tty7App { } Err(e) => { app.file_tree.rollback(id, &parent, rollback); - HostOps::notify_err(window, cx, "Delete failed", &e); + HostOps::notify_err( + window, + cx, + t(L10nKey::FileTreeDeleteFailed), + &e, + ); } } cx.notify(); @@ -1123,10 +1129,7 @@ impl Tty7App { fn file_tree_attach_to_agent(&mut self, path: &Path, cx: &mut Context) { let Some(target) = self.agent_target_leaf(cx) else { - crate::terminal::notify_desktop( - Some("tty7"), - "No running coding agent found — start one (claude, codex, …) in a pane first.", - ); + crate::terminal::notify_desktop(Some("tty7"), t(L10nKey::AppNoRunningCodingAgent)); return; }; let rel = self @@ -1353,86 +1356,110 @@ impl Tty7App { let p = path.to_path_buf(); if !is_dir { - menu = menu.item(PopupMenuItem::new("Open").on_click({ - let app = app.clone(); - let p = p.clone(); - move |_, window, cx| { - let _ = app.update(cx, |this, cx| this.open_file_in_editor(&p, window, cx)); - } - })); + menu = menu.item( + PopupMenuItem::new(t(L10nKey::FileTreeContextOpen)).on_click({ + let app = app.clone(); + let p = p.clone(); + move |_, window, cx| { + let _ = app.update(cx, |this, cx| this.open_file_in_editor(&p, window, cx)); + } + }), + ); } if is_dir { - menu = menu.item(PopupMenuItem::new("cd Here").on_click({ - let app = app.clone(); - let p = p.clone(); - move |_, window, cx| { - let _ = app.update(cx, |this, cx| this.file_tree_cd(&p, window, cx)); - } - })); + menu = menu.item( + PopupMenuItem::new(t(L10nKey::FileTreeContextCdHere)).on_click({ + let app = app.clone(); + let p = p.clone(); + move |_, window, cx| { + let _ = app.update(cx, |this, cx| this.file_tree_cd(&p, window, cx)); + } + }), + ); } menu = menu - .item(PopupMenuItem::new("Insert Path in Terminal").on_click({ - let app = app.clone(); - let p = p.clone(); - move |_, window, cx| { - let _ = app.update(cx, |this, cx| { - if let Some(leaf) = this - .tabs - .get(this.active) - .and_then(|t| t.pane.focused_or_first(window, cx)) - { - leaf.update(cx, |view, cx| view.paste(shell_quote(&p), cx)); - } - }); - } - })) - .item(PopupMenuItem::new("Attach to Agent").on_click({ - let app = app.clone(); - let p = p.clone(); - move |_, _window, cx| { - let _ = app.update(cx, |this, cx| this.file_tree_attach_to_agent(&p, cx)); - } - })) + .item( + PopupMenuItem::new(t(L10nKey::FileTreeContextInsertPath)).on_click({ + let app = app.clone(); + let p = p.clone(); + move |_, window, cx| { + let _ = app.update(cx, |this, cx| { + if let Some(leaf) = this + .tabs + .get(this.active) + .and_then(|t| t.pane.focused_or_first(window, cx)) + { + leaf.update(cx, |view, cx| view.paste(shell_quote(&p), cx)); + } + }); + } + }), + ) + .item( + PopupMenuItem::new(t(L10nKey::FileTreeContextAttachAgent)).on_click({ + let app = app.clone(); + let p = p.clone(); + move |_, _window, cx| { + let _ = app.update(cx, |this, cx| this.file_tree_attach_to_agent(&p, cx)); + } + }), + ) .separator() - .item(PopupMenuItem::new("New File").on_click({ - let app = app.clone(); - let p = p.clone(); - move |_, window, cx| { - let _ = app.update(cx, |this, cx| { - this.file_tree_begin_edit(TreeEditKind::NewFile, &p, is_dir, window, cx) - }); - } - })) - .item(PopupMenuItem::new("New Folder").on_click({ - let app = app.clone(); - let p = p.clone(); - move |_, window, cx| { - let _ = app.update(cx, |this, cx| { - this.file_tree_begin_edit(TreeEditKind::NewFolder, &p, is_dir, window, cx) - }); - } - })); + .item( + PopupMenuItem::new(t(L10nKey::FileTreeContextNewFile)).on_click({ + let app = app.clone(); + let p = p.clone(); + move |_, window, cx| { + let _ = app.update(cx, |this, cx| { + this.file_tree_begin_edit(TreeEditKind::NewFile, &p, is_dir, window, cx) + }); + } + }), + ) + .item( + PopupMenuItem::new(t(L10nKey::FileTreeContextNewFolder)).on_click({ + let app = app.clone(); + let p = p.clone(); + move |_, window, cx| { + let _ = app.update(cx, |this, cx| { + this.file_tree_begin_edit( + TreeEditKind::NewFolder, + &p, + is_dir, + window, + cx, + ) + }); + } + }), + ); if !is_root { - menu = menu.item(PopupMenuItem::new("Rename").on_click({ - let app = app.clone(); - let p = p.clone(); - move |_, window, cx| { - let _ = app.update(cx, |this, cx| { - this.file_tree_begin_edit(TreeEditKind::Rename, &p, is_dir, window, cx) - }); - } - })); + menu = menu.item( + PopupMenuItem::new(t(L10nKey::FileTreeContextRename)).on_click({ + let app = app.clone(); + let p = p.clone(); + move |_, window, cx| { + let _ = app.update(cx, |this, cx| { + this.file_tree_begin_edit(TreeEditKind::Rename, &p, is_dir, window, cx) + }); + } + }), + ); } menu = menu .separator() - .item(PopupMenuItem::new("Copy Path").on_click({ - let p = p.clone(); - move |_, _window, cx| { - cx.write_to_clipboard(gpui::ClipboardItem::new_string(p.display().to_string())); - } - })) + .item( + PopupMenuItem::new(t(L10nKey::FileTreeContextCopyPath)).on_click({ + let p = p.clone(); + move |_, _window, cx| { + cx.write_to_clipboard(gpui::ClipboardItem::new_string( + p.display().to_string(), + )); + } + }), + ) .item( PopupMenuItem::new(crate::ui::right_panel::reveal_label()).on_click({ let p = p.clone(); @@ -1447,7 +1474,7 @@ impl Tty7App { if !is_root { menu = menu.separator().item( PopupMenuItem::element(move |_window, _cx| { - div().text_color(danger).child("Delete") + div().text_color(danger).child(t(L10nKey::Delete)) }) .on_click({ let app = app.clone(); @@ -1466,9 +1493,9 @@ impl Tty7App { fn dotfiles_menu_item(show_hidden: bool, app: &gpui::WeakEntity) -> PopupMenuItem { let app = app.clone(); PopupMenuItem::new(if show_hidden { - "Hide Dotfiles" + t(L10nKey::FileTreeContextHideDotfiles) } else { - "Show Dotfiles" + t(L10nKey::FileTreeContextShowDotfiles) }) .on_click(move |_, _window, cx| { let _ = app.update(cx, |this, cx| { diff --git a/src/ui/forwards.rs b/src/ui/forwards.rs index fcc34131..45ec7faa 100644 --- a/src/ui/forwards.rs +++ b/src/ui/forwards.rs @@ -6,6 +6,7 @@ use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _, h_flex, v_f use crate::daemon::protocol::{ForwardStatus, ManagedForward, SshForwardKind}; use crate::terminal::view::TerminalView; use crate::ui::app::{CONTENT_INSET, Tty7App}; +use crate::ui::i18n::{L10nKey, t, t_fmt}; impl Tty7App { pub(crate) fn render_ssh_status_strip( @@ -45,15 +46,15 @@ impl Tty7App { .font_weight(FontWeight::MEDIUM) .text_color(theme.foreground) .child(if host.is_empty() { - "Disconnected".to_string() + t(L10nKey::ForwardDisconnected).to_string() } else { - format!("Disconnected from {host}") + t_fmt(L10nKey::ForwardDisconnectedFrom, &[("host", &host)]) }), ) .child(div().child("· ⌘⇧R")) .child( Button::new("ssh-reconnect") - .label("Reconnect") + .label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Reconnect)) .primary() .small() .on_click( @@ -92,13 +93,13 @@ impl Tty7App { .child( div() .font_weight(FontWeight::SEMIBOLD) - .child("Close this SSH connection?"), + .child(t(crate::ui::i18n::L10nKey::CloseSshConnectionTitle)), ) .child( div() .text_sm() .text_color(theme.muted_foreground) - .child("The connection is live. Closing will end it."), + .child(t(crate::ui::i18n::L10nKey::CloseSshConnectionBody)), ) .child( h_flex() @@ -106,7 +107,7 @@ impl Tty7App { .gap_2() .child( Button::new("ssh-close-cancel") - .label("Keep") + .label(t(crate::ui::i18n::L10nKey::Keep)) .small() .on_click( cx.listener(|this, _, _window, cx| this.cancel_ssh_close(cx)), @@ -114,7 +115,7 @@ impl Tty7App { ) .child( Button::new("ssh-close-confirm") - .label("Close") + .label(t(crate::ui::i18n::L10nKey::Close)) .primary() .small() .on_click(cx.listener(|this, _, window, cx| { @@ -151,7 +152,11 @@ impl Tty7App { .w(px(24.)) .h(px(24.)) .rounded_md() - .tooltip(if open { "Cancel" } else { "Add forward" }) + .tooltip(if open { + t(L10nKey::Cancel) + } else { + t(L10nKey::ForwardTooltipAdd) + }) .on_click(cx.listener(move |this, _, window, cx| { this.toggle_managed_forward_form(pane_id, window, cx) })) @@ -173,7 +178,7 @@ impl Tty7App { Some( v_flex() - .child(self.panel_subtitle("Forwards", true, Some(add), cx)) + .child(self.panel_subtitle(t(L10nKey::ForwardPanelTitle), true, Some(add), cx)) .when(managed.is_empty() && !open, |this| { this.child( div() @@ -181,7 +186,7 @@ impl Tty7App { .py(px(2.)) .text_size(px(12.)) .text_color(cx.theme().muted_foreground) - .child("None."), + .child(crate::ui::i18n::t(crate::ui::i18n::L10nKey::None)), ) }) .when(!managed.is_empty(), |this| this.child(list)) @@ -293,7 +298,7 @@ impl Tty7App { .w(px(18.)) .h(px(18.)) .rounded(px(4.)) - .tooltip("Remove") + .tooltip(t(L10nKey::ForwardTooltipRemove)) .on_click(cx.listener( move |this, _, _window, cx| { this.remove_managed_forward(pane_id, forward_id, cx) @@ -343,7 +348,11 @@ impl Tty7App { .child(self.segmented_on( sf, "ssh-managed-forward-kind", - &["Local", "Remote", "Dynamic"], + &[ + t(L10nKey::ForwardLocal), + t(L10nKey::ForwardRemote), + t(L10nKey::ForwardDynamic), + ], selected, cx, move |this, ix, _window, cx| { @@ -356,7 +365,7 @@ impl Tty7App { }, )) .child(pair( - "bind", + t(L10nKey::ForwardBindLabel), &self.loopback_panel.mf_bind_host, &self.loopback_panel.mf_bind_port, )) @@ -364,7 +373,11 @@ impl Tty7App { div() .opacity(if needs_target { 1.0 } else { 0.4 }) .child(pair( - if needs_target { "to" } else { "SOCKS" }, + if needs_target { + t(L10nKey::ForwardToLabel) + } else { + t(L10nKey::ForwardSocksLabel) + }, &self.loopback_panel.mf_target_host, &self.loopback_panel.mf_target_port, )), @@ -377,7 +390,7 @@ impl Tty7App { .pt(px(1.)) .child( Button::new(("ssh-managed-forward-cancel", pane_id)) - .label("Cancel") + .label(t(L10nKey::Cancel)) .ghost() .xsmall() .on_click(cx.listener(move |this, _, window, cx| { @@ -386,7 +399,11 @@ impl Tty7App { ) .child( Button::new(("ssh-managed-forward-add", pane_id)) - .label(if editing { "Save" } else { "Add" }) + .label(if editing { + t(L10nKey::Save) + } else { + t(L10nKey::ForwardAdd) + }) .primary() .xsmall() .on_click(cx.listener(move |this, _, window, cx| { diff --git a/src/ui/home.rs b/src/ui/home.rs index d946905a..01e376bd 100644 --- a/src/ui/home.rs +++ b/src/ui/home.rs @@ -10,6 +10,7 @@ use gpui_component::{ActiveTheme as _, IconName, Sizable as _, h_flex, v_flex}; use crate::core::session::{SessionPane, SessionTab}; use crate::ui::app::Tty7App; +use crate::ui::i18n::{L10nKey, t, t_fmt, t_plural}; const LOGO: [&str; 4] = [ " ▄▄▄ ▄▄▄ ▄ ▄ ▄▄▄▄", @@ -20,14 +21,14 @@ const LOGO: [&str; 4] = [ const LOGO_PX: f32 = 20.0; -const HOME_SHORTCUTS: [(&str, &str); 7] = [ - ("NewTab", "New Tab"), - ("ReopenClosedTab", "Reopen Closed Tab"), - ("ToggleSwitcher", "Switch Workspace"), - ("TogglePalette", "Command Palette"), - ("SplitRight", "Split Right"), - ("SplitDown", "Split Down"), - ("OpenSettings", "Settings…"), +const HOME_SHORTCUTS: [&str; 7] = [ + "NewTab", + "ReopenClosedTab", + "ToggleSwitcher", + "TogglePalette", + "SplitRight", + "SplitDown", + "OpenSettings", ]; const CLOSED_LABEL_MAX: usize = 20; @@ -70,17 +71,17 @@ pub(crate) fn now_secs() -> u64 { pub(crate) fn relative_time(now: u64, then: u64) -> String { if then == 0 || then >= now { - return "just now".to_string(); + return t(L10nKey::HomeTimeJustNow).to_string(); } let secs = now - then; match secs { - s if s < 60 => "just now".to_string(), - s if s < 3600 => format!("{} min ago", s / 60), - s if s < 7200 => "1 hour ago".to_string(), - s if s < 86_400 => format!("{} hours ago", s / 3600), - s if s < 172_800 => "yesterday".to_string(), - s if s < 604_800 => format!("{} days ago", s / 86_400), - _ => "over a week ago".to_string(), + s if s < 60 => t(L10nKey::HomeTimeJustNow).to_string(), + s if s < 3_600 => t_plural(L10nKey::HomeTimeMinutesAgo, (s / 60) as usize, &[]), + s if s < 7_200 => t(L10nKey::HomeTimeHourAgo).to_string(), + s if s < 86_400 => t_plural(L10nKey::HomeTimeHoursAgo, (s / 3_600) as usize, &[]), + s if s < 172_800 => t(L10nKey::HomeTimeYesterday).to_string(), + s if s < 604_800 => t_plural(L10nKey::HomeTimeDaysAgo, (s / 86_400) as usize, &[]), + _ => t(L10nKey::HomeTimeOverWeekAgo).to_string(), } } @@ -109,6 +110,25 @@ fn key_hint(action: &str, cx: &App) -> Option { Some(Kbd::format(&stroke)) } +fn home_shortcut_label(action: &str, closed: Option<&str>) -> String { + let label = match action { + "NewTab" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeNewTab), + "ReopenClosedTab" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeReopenClosedTab), + "ToggleSwitcher" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeSwitchWorkspace), + "TogglePalette" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeCommandPalette), + "SplitRight" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeSplitRight), + "SplitDown" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeSplitDown), + "OpenSettings" => crate::ui::i18n::t(crate::ui::i18n::L10nKey::HomeSettings), + _ => action, + }; + if action == "ReopenClosedTab" { + if let Some(name) = closed { + return t_fmt(L10nKey::HomeReopenNamed, &[("name", name)]); + } + } + label.to_string() +} + impl Tty7App { pub(crate) fn render_home(&self, cx: &mut Context) -> impl IntoElement + use<> { let theme = cx.theme(); @@ -133,11 +153,9 @@ impl Tty7App { let closed_hint = self.closed.last().and_then(closed_tab_label); let mut list = v_flex().gap_2().w(px(300.)).text_sm().text_color(muted); - for (action, label) in HOME_SHORTCUTS { - let (label, emphasized) = match (&closed_hint, action) { - (Some(name), "ReopenClosedTab") => (format!("Reopen \u{201c}{name}\u{201d}"), true), - _ => (label.to_string(), false), - }; + for action in HOME_SHORTCUTS { + let emphasized = closed_hint.is_some() && action == "ReopenClosedTab"; + let label = home_shortcut_label(action, closed_hint.as_deref()); list = list.child( h_flex() .items_center() @@ -219,6 +237,7 @@ impl Tty7App { #[cfg(test)] mod tests { use super::*; + use crate::ui::i18n::set_locale; use std::path::PathBuf; fn leaf(cwd: Option<&str>) -> SessionPane { @@ -311,6 +330,7 @@ mod tests { #[test] fn relative_time_reads_coarsely_across_the_ranges() { + set_locale("en"); let now = 10_000_000u64; assert_eq!(relative_time(now, now), "just now"); assert_eq!(relative_time(now, now - 30), "just now"); @@ -320,10 +340,17 @@ mod tests { assert_eq!(relative_time(now, now - 90_000), "yesterday"); assert_eq!(relative_time(now, now - 3 * 86_400), "3 days ago"); assert_eq!(relative_time(now, now - 30 * 86_400), "over a week ago"); + + set_locale("zh-CN"); + assert_eq!(relative_time(now, now - 30), "刚刚"); + assert_eq!(relative_time(now, now - 120), "2 分钟前"); + assert_eq!(relative_time(now, now - 3600), "1 小时前"); + assert_eq!(relative_time(now, now - 90_000), "昨天"); } #[test] fn relative_time_never_renders_a_negative_age() { + set_locale("en"); let now = 1_000_000u64; assert_eq!(relative_time(now, 0), "just now"); assert_eq!(relative_time(now, now + 5_000), "just now"); diff --git a/src/ui/host_ops.rs b/src/ui/host_ops.rs index e27dcd70..a72ca16c 100644 --- a/src/ui/host_ops.rs +++ b/src/ui/host_ops.rs @@ -5,6 +5,8 @@ use std::hash::Hash; use gpui::{App, Context, Window}; use gpui_component::WindowExt as _; +use crate::ui::i18n::{L10nKey, t_fmt}; + #[allow(unused_imports)] pub use tty7_core::host::{ Entry, Host, HostId, MTime, Meta, Output, SearchHit, SharedHost, WatchSub, @@ -200,7 +202,13 @@ impl HostOps { } pub fn notify_err(window: &mut Window, cx: &mut App, context: &str, err: &std::io::Error) { - window.push_notification(format!("{context}: {err}"), cx); + window.push_notification( + t_fmt( + L10nKey::HostOpsError, + &[("context", context), ("error", &err.to_string())], + ), + cx, + ); } } diff --git a/src/ui/i18n.rs b/src/ui/i18n.rs new file mode 100644 index 00000000..fa3a8af0 --- /dev/null +++ b/src/ui/i18n.rs @@ -0,0 +1,3896 @@ +use std::sync::atomic::{AtomicU8, Ordering}; + +const EN: u8 = 0; +const ZH_HANS: u8 = 1; + +static CURRENT: AtomicU8 = AtomicU8::new(EN); + +// Tests run in parallel and every one of them reads the same process-wide +// locale, so a test that switches to Chinese would flip the language out from +// under another thread's English assertions. libtest gives each test its own +// thread, so an override that lives in thread-local storage keeps them apart. +#[cfg(test)] +thread_local! { + static TEST_LOCALE: std::cell::Cell> = const { std::cell::Cell::new(None) }; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum L10nKey { + SearchTabs, + SearchFiles, + SearchThemes, + SearchSettings, + FilterHosts, + SearchCommandsOrHost, + SearchTheme, + Search, + SearchWorkspacesAndMachines, + SearchFonts, + NewFolderName, + NewFileName, + HomeNewTab, + HomeReopenClosedTab, + HomeSwitchWorkspace, + HomeCommandPalette, + HomeSplitRight, + HomeSplitDown, + HomeSettings, + TrayQuitStopServer, + Reconnect, + None, + TryAgain, + Refreshing, + Binary, + Delete, + NoMatchingCommands, + ConnectSshHint, + EditHint, + OpenFileFromTree, + FileChangedOnDisk, + Reload, + KeepMine, + Dismiss, + StoredPasswordRejected, + Trust, + Abort, + HostKeyOverrideMessage, + Override, + RememberKeychain, + CloseWindowTitle, + CloseWindowBody, + Cancel, + Close, + QuitStopServerTitle, + QuitStopServerBody, + QuitAndStop, + CloseSshConnectionTitle, + CloseSshConnectionBody, + Keep, + SettingsNavAppearance, + SettingsNavTerminal, + SettingsNavInput, + SettingsNavSsh, + SettingsNavAgents, + SettingsNavWindowTabs, + SettingsNavKeybindings, + SettingsNavAbout, + SettingsHeader, + Reset, + Save, + Connect, + Download, + Link, + SettingsThemeIntroTitle, + SettingsThemeIntroDesc, + SettingsTypography, + SettingsFontSize, + SettingsFontSizeDesc, + SettingsLineHeight, + SettingsLineHeightDesc, + SettingsFontFamily, + SettingsFontFamilyDesc, + SettingsBoldFont, + SettingsBoldFontDesc, + SettingsItalicFont, + SettingsItalicFontDesc, + SettingsFontLigatures, + SettingsFontLigaturesDesc, + SettingsCursor, + SettingsCursorShape, + SettingsCursorShapeDesc, + SettingsCursorBlink, + SettingsCursorBlinkDesc, + SettingsLanguage, + SettingsLanguageDesc, + SettingsLanguageEnglish, + SettingsLanguageChinese, + SettingsSearchLanguageKeywords, + SettingsTransparency, + SettingsOpacity, + SettingsOpacityDesc, + SettingsBlur, + SettingsBlurDesc, + FollowTheme, + SettingsDimInactivePanes, + SettingsDimInactivePanesDesc, + SettingsOpenThemesFolder, + SettingsChangeThemeImage, + SettingsChooseThemeImage, + SettingsRemoveThemeImage, + SettingsImageOpacity, + SettingsImageOpacityDesc, + SettingsEditTheme, + SettingsEditThemeIntro, + SettingsBackgroundImage, + SettingsBackgroundImageDesc, + SettingsAnsiColors, + SettingsCustomThemes, + SettingsCustomThemesIntro, + SettingsDuplicateToEdit, + SettingsHosts, + SettingsDefaults, + SettingsInheritedByEveryHost, + SettingsNoSavedHosts, + SettingsNothingMatches, + SettingsInTty7, + SettingsImportFromSshConfig, + SettingsExpandAllGroups, + SettingsNoHostsYet, + SettingsNothingSelected, + SettingsTypeAddressToConnect, + SettingsMoreInSshConfig, + SettingsAliasesLinked, + SettingsImportAliases, + SettingsImportAliasesDesc, + SettingsImportNow, + SettingsDefaultsIntro, + SettingsCopyAddress, + SettingsDuplicate, + SettingsForgetPassword, + SettingsForgotPasswordFor, + SettingsCouldntForgetPassword, + SettingsSecurity, + SettingsSecurityIntro, + SettingsVerifyHostKeys, + SettingsVerifyHostKeysDesc, + WarnBeforeClosing, + SettingsWarnBeforeClosingDesc, + SettingsNewHost, + SettingsName, + SettingsNameDesc, + SettingsHost, + SettingsHostDesc, + SettingsUser, + SettingsUserDesc, + SettingsAuth, + SettingsAuthDesc, + SettingsAuthModeAuto, + SettingsAuthModePassword, + SettingsAuthModeKey, + SettingsAuthModeAgent, + SettingsAuthMode2Fa, + SettingsJumpHost, + SettingsJumpHostDesc, + SettingsNoneSummary, + SettingsNoneLower, + SettingsPortForwarding, + SettingsRulesOpenedWithConnection, + SettingsAddRule, + SettingsFwdLegendLocal, + SettingsFwdLegendRemote, + SettingsFwdLegendDynamic, + SettingsFwdNeedsBoth, + SettingsFwdNeedsListen, + SettingsAdvanced, + SettingsAdvancedSummary, + SettingsIdentityFiles, + SettingsIdentityFilesDesc, + SettingsAgentForwarding, + SettingsAgentForwardingDesc, + SettingsProxyCommand, + SettingsProxyCommandDesc, + SettingsSocks5Proxy, + SettingsSocks5ProxyDesc, + SettingsHttpProxy, + SettingsHttpProxyDesc, + SettingsKexAlgorithms, + SettingsKexAlgorithmsDesc, + SettingsCiphers, + SettingsCiphersDesc, + SettingsMacs, + SettingsMacsDesc, + SettingsHostKeyAlgorithms, + SettingsHostKeyAlgorithmsDesc, + SettingsCompression, + SettingsJumpHostVia, + SettingsConnected, + SettingsProfileCopied, + SettingsCompressionDesc, + SettingsKeepaliveInterval, + SettingsKeepaliveIntervalDesc, + SettingsKeepaliveCountMax, + SettingsKeepaliveCountMaxDesc, + SettingsConnectTimeout, + SettingsConnectTimeoutDesc, + SettingsX11Forwarding, + SettingsX11ForwardingDesc, + SettingsShellIntegration, + SettingsShellIntegrationDesc, + SettingsLoginScripts, + SettingsLoginScriptsDesc, + SettingsSkipBanner, + SettingsSkipBannerDesc, + SettingsDefaultFollowsDefaults, + SettingsValueOn, + SettingsValueOff, + SettingsDefault, + SettingsOn, + SettingsOff, + SettingsShell, + SettingsShellIntro, + SettingsProgram, + SettingsProgramDesc, + SettingsArguments, + SettingsArgumentsDesc, + SettingsStartIn, + SettingsStartInDesc, + SettingsCustomPath, + SettingsCustomPathDesc, + SettingsWdInherit, + SettingsWdHome, + SettingsWdCustom, + SettingsShellFooter, + SettingsScrolling, + SettingsScrollback, + SettingsScrollbackDesc, + SettingsScrollSpeed, + SettingsScrollSpeedDesc, + SettingsMouse, + SettingsFocusFollowsMouse, + SettingsFocusFollowsMouseDesc, + SettingsHideMouseWhileTyping, + SettingsHideMouseWhileTypingDesc, + SettingsReportMouseToApps, + SettingsReportMouseToAppsDesc, + SettingsBell, + SettingsTerminalBell, + SettingsTerminalBellDesc, + SettingsLinks, + DetectUrls, + SettingsDetectUrlsDesc, + ForwardSshLoopbackLinks, + SettingsForwardSshLoopbackLinksDesc, + OpenFilesWith, + SettingsOpenFilesWithDesc, + SettingsBellModeOff, + SettingsBellModeVisual, + SettingsBellModeAudible, + SettingsPrompt, + SettingsPromptIntro, + SettingsTabCompletion, + SettingsTabCompletionDesc, + SettingsHistorySearch, + SettingsHistorySearchDesc, + SettingsSelectionClipboard, + SettingsSmartSelection, + SettingsSmartSelectionDesc, + SettingsCopyOnSelect, + SettingsCopyOnSelectDesc, + SettingsTrimTrailingSpaces, + SettingsTrimTrailingSpacesDesc, + SettingsKeyboard, + SettingsOptionAsMeta, + SettingsOptionAsMetaDesc, + SettingsAgentsIntro, + SettingsAgentsIntroDesc, + SettingsReadingAgentConfig, + SettingsStatusNotInstalled, + SettingsStatusInstalled, + SettingsStatusOutdated, + SettingsInstall, + SettingsReinstall, + SettingsUpdate, + SettingsUninstall, + SettingsOfflineMachines, + SettingsSyncWithSystem, + SettingsSyncWithSystemDesc, + SettingsChangeTheme, + SettingsThemes, + SettingsThemePanelManual, + SettingsThemePanelLight, + SettingsThemePanelDark, + SettingsCustom, + SettingsBuiltIn, + SettingsDark, + SettingsLight, + SettingsLightMode, + SettingsDarkMode, + SettingsActive, + SettingsStartupWindow, + SettingsStartupWindowDesc, + SettingsRememberWindowSize, + SettingsRememberWindowSizeDesc, + SettingsRestoreLastLayout, + SettingsRestoreLastLayoutDesc, + SettingsConfirmLastWindowClose, + SettingsConfirmLastWindowCloseDesc, + SettingsShowTrayIcon, + SettingsShowTrayIconDesc, + SettingsTabs, + SettingsNewTabPosition, + SettingsNewTabPositionDesc, + SettingsTabBarPosition, + SettingsTabBarPositionDesc, + SettingsSidebarGrouping, + SettingsSidebarGroupingDesc, + SettingsDiffPreviewFromCounts, + SettingsDiffPreviewFromCountsDesc, + SettingsNotifications, + SettingsWindow, + SettingsNotifyOnCommandFinish, + SettingsNotifyOnCommandFinishDesc, + SettingsNotifyThreshold, + SettingsNotifyThresholdDesc, + NotifyModeNever, + NotifyModeUnfocused, + NotifyModeAlways, + SettingsStartupNormal, + SettingsStartupMaximized, + SettingsStartupFullscreen, + SettingsAfterCurrent, + SettingsAtEnd, + SettingsTop, + SettingsLeft, + SettingsByRepo, + SettingsFlat, + SettingsPreset, + SettingsPresetDesc, + SettingsPrefix, + SettingsPressKeys, + SettingsPauseToSaveEsc, + SettingsKeybindingsIntroDesc, + SettingsPrefixNote, + SettingsRestoreAllDefaults, + SettingsAboutDesc1, + SettingsAboutDesc2, + SettingsAboutTech, + SettingsVersion, + SettingsUpdates, + SettingsVersionAvailable, + SettingsCheckUpdatesDesc, + SettingsCheckUpdatesOnLaunch, + SettingsCommandLine, + SettingsCommandLineDesc, + SettingsInstallCliOnPath, + SettingsExplorerContextMenu, + SettingsExplorerContextMenuDesc, + SettingsExplorerNotRegistered, + SettingsExplorerRegistered, + SettingsExplorerNeedsUpdate, + SettingsExplorerUnavailable, + SettingsExplorerStatusUnavailable, + SettingsExplorerRegister, + SettingsExplorerUpdate, + SettingsExplorerUnregister, + SettingsExplorerRegisteredNote, + SettingsExplorerUnregisteredNote, + SettingsExplorerRegisterFailed, + SettingsExplorerUnregisterFailed, + SettingsExplorerWindows11Note, + SettingsServer, + SettingsServerDesc, + SettingsRestartServer, + SettingsAgentClaudeCode, + SettingsAgentCodex, + SettingsAgentCopilotCli, + SettingsAgentOpencode, + SettingsAgentPi, + SettingsAgentGrokBuild, + SettingsSearchAboutKeywords, + SettingsSearchAnsiColorsKeywords, + SettingsSearchArgumentsKeywords, + SettingsSearchBlurKeywords, + SettingsSearchBoldFontKeywords, + SettingsSearchClaudeCodeKeywords, + SettingsSearchCodexKeywords, + SettingsSearchCommandLineToolKeywords, + SettingsSearchCommandLineToolTitle, + SettingsSearchConfirmLastWindowCloseKeywords, + SettingsSearchCopilotCliKeywords, + SettingsSearchCopyOnSelectKeywords, + SettingsSearchCursorBlinkKeywords, + SettingsSearchCursorShapeKeywords, + SettingsSearchCustomThemesKeywords, + SettingsSearchDetectUrlsKeywords, + SettingsSearchDiffPreviewFromCountsKeywords, + SettingsSearchDimInactivePanesKeywords, + SettingsSearchExplorerContextMenuKeywords, + SettingsSearchFocusFollowsMouseKeywords, + SettingsSearchFontFamilyKeywords, + SettingsSearchFontLigaturesKeywords, + SettingsSearchFontSizeKeywords, + SettingsSearchForwardSshLoopbackLinksKeywords, + SettingsSearchGrokBuildKeywords, + SettingsSearchHideMouseWhileTypingKeywords, + SettingsSearchHistorySearchKeywords, + SettingsSearchHostsKeywords, + SettingsSearchHowShellsWorkKeywords, + SettingsSearchHowShellsWorkTitle, + SettingsSearchItalicFontKeywords, + SettingsSearchKeybindingsKeywords, + SettingsSearchKeybindingsTitle, + SettingsSearchLineHeightKeywords, + SettingsSearchNewTabPositionKeywords, + SettingsSearchNotifyOnCommandFinishKeywords, + SettingsSearchNotifyThresholdKeywords, + SettingsSearchOpacityKeywords, + SettingsSearchOpenFilesWithKeywords, + SettingsSearchOpencodeKeywords, + SettingsSearchOptionAsMetaKeywords, + SettingsSearchPiKeywords, + SettingsSearchPortForwardingKeywords, + SettingsSearchProgramKeywords, + SettingsSearchRememberWindowSizeKeywords, + SettingsSearchReportMouseToAppsKeywords, + SettingsSearchRestoreLastLayoutKeywords, + SettingsSearchScrollSpeedKeywords, + SettingsSearchScrollbackKeywords, + SettingsSearchShowTrayIconKeywords, + SettingsSearchSidebarGroupingKeywords, + SettingsSearchSmartSelectionKeywords, + SettingsSearchStartInKeywords, + SettingsSearchSyncWithSystemKeywords, + SettingsSearchTabBarPositionKeywords, + SettingsSearchTabCompletionKeywords, + SettingsSearchTerminalBellKeywords, + SettingsSearchThemeKeywords, + SettingsSearchTrimTrailingSpacesKeywords, + SettingsSearchVerifyHostKeysKeywords, + SettingsSearchWarnBeforeClosingKeywords, + SettingsSearchStartupWindowKeywords, + SwitcherNoMatch, + AddSshHost, + ClickForNewWindow, + RestartServer, + OtherMachines, + Ok, + SftpNoTransfers, + SftpPanelTitleFiles, + SftpTooltipRefresh, + SftpTooltipMore, + SftpMenuNewFolder, + SftpMenuNewFile, + SftpMenuUpload, + SftpMenuGotoShellCwd, + SftpMenuHideTransferHistory, + SftpMenuTransferHistory, + SftpEditNewFolder, + SftpEditNewFile, + SftpEditRename, + SftpEditPermissions, + SftpLoading, + SftpEmptyDirectory, + SftpContextOpen, + SftpContextFollowSymlink, + SftpContextRename, + SftpContextChmod, + SftpTransferSummaryRunning, + SftpTransferSummaryFailed, + SftpTransferSummaryIdle, + SftpTransferProgress, + SftpTransferDone, + SftpTransferCancelled, + SftpTransferError, + ForwardPanelTitle, + ForwardDisconnected, + ForwardDisconnectedFrom, + ForwardTooltipAdd, + ForwardTooltipRemove, + ForwardLocal, + ForwardRemote, + ForwardDynamic, + ForwardBindLabel, + ForwardToLabel, + ForwardSocksLabel, + ForwardAdd, + FileTreePlaceholderFileName, + FileTreePlaceholderFolderName, + FileTreePlaceholderNewName, + FileTreeDeleteTitle, + FileTreeDeleteFolderBody, + FileTreeDeleteFileBody, + FileTreeDeleteFailed, + FileTreeContextOpen, + FileTreeContextCdHere, + FileTreeContextInsertPath, + FileTreeContextAttachAgent, + FileTreeContextNewFile, + FileTreeContextNewFolder, + FileTreeContextRename, + FileTreeContextCopyPath, + FileTreeContextHideDotfiles, + FileTreeContextShowDotfiles, + SshPromptNewKey, + SshPromptOldKey, + EditorCantOpen, + EditorCantRead, + EditorNotUtf8, + EditorSaveFailed, + EditorUnsavedChanges, + EditorDiscard, + EditorNoFileOpen, + EditorBackToTerminal, + EditorLnCol, + EditorEdit, + EditorPreview, + EditorWrapOn, + EditorWrapOff, + EditorFileTooLarge, + EditorBinaryFile, + PanelInfoTitle, + PanelOutlineTitle, + PanelChangesTitle, + PanelFilesTitle, + PanelNoSession, + PanelNoSessionHint, + PanelNoCommands, + PanelNoCommandsHint, + PanelNoWorkingDirectory, + PanelNoWorkingDirectoryHint, + PanelLoading, + PanelNotAGitRepo, + PanelNotAGitRepoHint, + PanelNoChanges, + PanelNoChangesHint, + PanelMoreChangedFiles, + PanelUntracked, + PanelSessionSubtitle, + PanelProcessesSubtitle, + PanelPortsSubtitle, + PanelCwd, + PanelShell, + PanelSsh, + PanelBranch, + PanelChangesRow, + PanelAgent, + PanelAgentIdle, + PanelAgentWorking, + PanelAgentWaiting, + PanelAgentDone, + PanelRevealInFinder, + PanelOpenFolder, + WindowStop, + WindowDelete, + WindowThisWorkspace, + WindowConfirmTitle, + WindowStopUnreachable, + WindowDeleteUnreachable, + WindowStopShells, + WindowDeleteShells, + DiffReading, + DiffNotARepo, + DiffReadFailed, + DiffWorkingTreeClean, + DiffCloseTooltip, + DiffChangedFiles, + DiffUntrackedCount, + DiffMoreFiles, + DiffOversizedNotice, + DiffTruncatedPerFile, + DiffTruncatedBudget, + DiffUntrackedHeader, + DiffMoreUntracked, + DiffLines, + DiffChangedLines, + DiffBudgetAndCap, + DiffBudget, + DiffPerFileCap, + DiffUntrackedSummary, + PendingConnecting, + PendingUnreachable, + WorktreePromptNeedsName, + WorktreePromptTitle, + WorktreePromptName, + WorktreePromptBranch, + WorktreePromptBase, + WorktreePromptCreating, + WorktreePromptCreate, + AppNewWorktreeFailed, + HomeTimeJustNow, + HomeTimeMinutesAgo, + HomeTimeHourAgo, + HomeTimeHoursAgo, + HomeTimeYesterday, + HomeTimeDaysAgo, + HomeTimeOverWeekAgo, + HomeReopenNamed, + AppMenuAbout, + AppMenuCheckForUpdates, + AppMenuSettings, + AppMenuServices, + AppMenuHideApp, + AppMenuHideOthers, + AppMenuShowAll, + AppMenuQuit, + AppMenuFile, + AppMenuEdit, + AppMenuView, + AppMenuWindow, + AppMenuHelp, + AppMenuNewTab, + AppMenuNewWorkspace, + AppMenuNewWorktreeTab, + AppMenuSplitRight, + AppMenuSplitDown, + AppMenuRenameTab, + AppMenuCopyWorkingDirectory, + AppMenuCopySessionId, + AppMenuForkSession, + AppMenuClosePaneTab, + AppMenuCloseOtherTabs, + AppMenuCloseTabsRight, + AppMenuReopenClosedTab, + AppMenuRenameWorkspace, + AppMenuStopWorkspace, + AppMenuDeleteWorkspace, + AppMenuUndo, + AppMenuRedo, + AppMenuCut, + AppMenuCopy, + AppMenuPaste, + AppMenuSelectAll, + AppMenuFind, + AppMenuFindNext, + AppMenuFindPrevious, + AppMenuCommandPalette, + AppMenuIncreaseFontSize, + AppMenuDecreaseFontSize, + AppMenuResetFontSize, + AppMenuLeftSidebar, + AppMenuRightPanel, + AppMenuCodePanel, + AppMenuTabBarPosition, + AppMenuFocusNextPane, + AppMenuFocusPreviousPane, + AppMenuZoomPane, + AppMenuClearScrollback, + AppMenuEnterFullscreen, + AppMenuDocumentation, + AppMenuKeyboardShortcuts, + AppMenuJoinDiscord, + AppMenuReportIssue, + AppMenuRestartServer, + WindowUntitled, + TrayShowTty7, + TrayNotifications, + TrayAgentNeedsInput, + TabTooltipMore, + TabTooltipShowSidebar, + TabTooltipHideSidebar, + TabTooltipHideDetailPanel, + TabTooltipShowDetailPanel, + TabUnnamedShell, + ShellDefault, + SidebarScratchGroup, + TabContextCloseTab, + TabContextCloseTabsBelow, + TabContextMarkUnread, + RemoteStripDisconnected, + RemoteStripConnecting, + RemoteStripReconnecting, + RemoteStripReconnectingAttempt, + RemoteStripPreempted, + RemoteStripFailed, + RemoteNoticePreempted, + RemoteNoticeDisconnected, + RemoteActionRetryNow, + RemoteActionTakeBack, + RemoteActionConnect, + RemoteActionRetry, + RemoteNoConnectionDetails, + RemoteThisComputer, + RemoteRestartTitle, + RemoteRestartBody, + RemoteReplaceBody, + RemoteRestartFailedTitle, + RemoteRestartFailedBody, + RemoteHostUnreachable, + RemoteInstallTitle, + RemoteInstallDetail, + RemoteInstallPathLabel, + RemoteInstallVersionLabel, + RemoteInstallSizeLabel, + RemoteInstallFromLabel, + RemoteInstallShaLabel, + RemoteInstallSilentUpgrades, + RemoteInstallBytes, + RemoteMismatchTitle, + RemoteMismatchDetail, + RemoteMismatchUnknownBuild, + RemoteMismatchUnknownBuildFromExe, + RemoteDaemonStartFailed, + RemoteDaemonUnreachable, + RemoteDaemonTooOld, + RemoteProfileMissing, + RemoteAliasMissing, + RemoteWslNoSsh, + RemoteLocalStdioNoSsh, + RemoteHostNotTty7, + RemoteWorkspaceListFailed, + RemoteServerRestartFailed, + RemoteNoRouteToHost, + RemoteMachineTreeUnexpectedReply, + RemoteMismatchVersionFromExe, + AppNoRunningCodingAgent, + SwitcherThisComputer, + SwitcherRestartingServer, + SwitcherDownloadingServerWithTotal, + SwitcherDownloadingServerNoTotal, + SwitcherCopyingServer, + SwitcherThisWindow, + SwitcherOpen, + SwitcherDisconnect, + SwitcherOpenInNewWindow, + SwitcherRename, + SshPromptPasswordFor, + SshPromptPassphraseFor, + SshPromptTwoFactor, + SshPromptUnknownHost, + SshPromptHostKeyChanged, + SshPromptHostKeyChangedBody, + SshPromptConnect, + SshPromptUnlock, + SshPromptSubmit, + HostOpsError, + CmdGroupTabsPanes, + CmdGroupWorkspaces, + CmdGroupView, + CmdGroupTerminal, + CmdGroupSsh, + CmdGroupAgents, + CmdGroupApplication, + CmdNewTab, + CmdNewWorktreeTab, + CmdNewWorktreeTabSubtitle, + CmdRenameTab, + CmdSplitRight, + CmdSplitDown, + CmdZoomPane, + CmdNextPane, + CmdPreviousPane, + CmdFocusPaneLeft, + CmdFocusPaneRight, + CmdFocusPaneUp, + CmdFocusPaneDown, + CmdResizePaneLeft, + CmdResizePaneRight, + CmdResizePaneUp, + CmdResizePaneDown, + CmdSwapPaneNext, + CmdSwapPanePrevious, + CmdNextTab, + CmdPreviousTab, + CmdCopyWorkingDirectory, + CmdCopySessionId, + CmdCopySessionIdSubtitle, + CmdForkSession, + CmdForkSessionSubtitle, + CmdMarkTabAsUnread, + CmdClosePaneTab, + CmdCloseOtherTabs, + CmdCloseTabsToTheRight, + CmdReopenClosedTab, + CmdNewWorkspace, + CmdSwitchWorkspace, + CmdRenameWorkspace, + CmdStopWorkspace, + CmdStopWorkspaceSubtitle, + CmdDeleteWorkspace, + CmdDeleteWorkspaceSubtitle, + CmdShowLeftSidebar, + CmdHideLeftSidebar, + CmdHideRightPanel, + CmdShowRightPanel, + CmdShowCodePanel, + CmdTabBarMoveToTop, + CmdTabBarMoveToLeftSidebar, + CmdRightPanelInfo, + CmdRightPanelOutline, + CmdRightPanelChanges, + CmdRightPanelFiles, + CmdChangeTheme, + CmdResetFontSize, + CmdEnterFullScreen, + CmdClearScrollback, + CmdFindInTerminal, + CmdFindNext, + CmdFindPrevious, + CmdCopy, + CmdCut, + CmdPaste, + CmdSelectAll, + CmdSshAddConnection, + CmdSshManageProfiles, + CmdSshReconnect, + CmdSshRemoteFiles, + CmdSshPortForwarding, + CmdSshConnectWithInput, + CmdAgentSendSelection, + CmdAgentSendSelectionSubtitle, + CmdAgentSendGitDiffForReview, + CmdAgentSendGitDiffSubtitle, + CmdSettings, + CmdKeyboardShortcuts, + CmdAboutTty7, + CmdCheckForUpdates, + CmdDocumentation, + CmdJoinDiscord, + CmdReportIssue, + CmdRestartServer, + CmdRestartServerSubtitle, + CmdQuitTty7, + CmdQuitTty7Subtitle, + CmdQuickConnect, + CmdQuickConnectSaveProfile, + CmdRecent, + AppRestartServerTitle, + AppRestartServerMismatchDetail, + AppRestartServerOldDetail, + AppKeepShells, + AppRestart, + AppRestartServerNotSsh, + AppRestartServerBody, + AppWorktreeRemoveDetailDirty, + AppWorktreeRemoveDetailClean, + AppWorktreeRemoveTitle, + AppWorktreeDiscardAndRemove, + AppWorktreeRemove, + AppWorktreeKeep, + AppReopenTabFailed, + AppOpenTerminalFailed, + AppSshConnectionFailed, + AppSshReconnectFailed, + AppSplitPaneFailed, + AppWorktreeRemoved, + AppWorktreeRemoveFailed, + AppForkStillConnecting, + AppPaneNoCodingAgent, + AppForkNoCommand, + AppForkLocalOnly, + AppForkNoSessionId, + AppForkSessionIdNotToken, + AppForkMidTurn, + AppTabNoWorkingDirectory, + AppNothingSelected, + AppPaneNoKnownDirectory, + AppNoUncommittedChanges, + AppCmdSshProfileTitle, + AppCmdSwitchToTab, + AppPlaceholderDescription, + AppPlaceholderSshQuickConnect, + AppPlaceholderLoginShell, + AppPlaceholderNone, + AppPlaceholderOpenInDefaultApp, + AppThemeColorBackground, + AppThemeColorForeground, + AppThemeColorAccent, + AppThemeColorCursor, + AppThemeColorSelection, + AppAgentHooksThisComputer, + AppAgentHooksRemoteMachine, + AppAgentHooksNoHomeDir, + AppAgentHooksOffline, + AppAgentHooksHomeDirUnresolved, + AppAgentHooksOpFailed, + AppKeybindingDisplacedNote, + AppLocalServerName, + AppSshParseUnbalancedQuotes, + AppSshParseNoRemoteCommands, + AppSshParseFlagNeedsValue, + AppSshParseInvalidPort, + AppSshParseUnsupportedOption, + AppSshParseEnterHost, + AppSshParseBadHost, + AppMenuMinimize, + AppMenuZoom, + SwitcherStatusRestarting, + SwitcherStatusInstalling, + SwitcherStatusConnecting, + SwitcherStatusConnectFailed, + SwitcherStatusNotConnected, + SettingsFontDefault, + ForwardDescriptionPlaceholder, + SettingsShellDefaultLoginShell, + SftpErrorUnexpectedReply, + SftpErrorUnsafeRemoteName, + SftpErrorInvalidOctalMode, +} + +pub fn set_locale(gui_language: &str) { + let locale = if gui_language == "zh-CN" { ZH_HANS } else { EN }; + #[cfg(test)] + TEST_LOCALE.with(|slot| slot.set(Some(locale))); + #[cfg(not(test))] + CURRENT.store(locale, Ordering::Relaxed); +} + +pub fn t(key: L10nKey) -> &'static str { + translate(current_locale(), key) +} + +pub fn t_fmt(key: L10nKey, args: &[(&str, &str)]) -> String { + apply_template(t(key), args, None) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PluralCategory { + Zero, + One, + Other, +} + +impl PluralCategory { + pub fn from_count(n: usize) -> Self { + match n { + 0 => Self::Zero, + 1 => Self::One, + _ => Self::Other, + } + } + + pub fn as_str(self) -> &'static str { + match self { + Self::Zero => "zero", + Self::One => "one", + Self::Other => "other", + } + } +} + +/// Select a plural-aware translation and fill placeholders. +/// The template may use `{count}`; it is always substituted first. +pub fn t_plural(key: L10nKey, count: usize, args: &[(&str, &str)]) -> String { + let branch = PluralCategory::from_count(count).as_str(); + apply_template( + translate_variant(current_locale(), key, branch), + args, + Some(count), + ) +} + +/// Select a named branch of a translation and fill placeholders. +pub fn t_select(key: L10nKey, branch: &'static str, args: &[(&str, &str)]) -> String { + apply_template(translate_variant(current_locale(), key, branch), args, None) +} + +fn apply_template(template: &'static str, args: &[(&str, &str)], count: Option) -> String { + let mut text = template.to_string(); + if let Some(n) = count { + text = text.replace("{count}", &n.to_string()); + } + for (name, value) in args { + text = text.replace(&format!("{{{name}}}"), value); + } + text +} + +fn current_locale() -> Locale { + #[cfg(test)] + if let Some(locale) = TEST_LOCALE.with(|slot| slot.get()) { + return locale_of(locale); + } + locale_of(CURRENT.load(Ordering::Relaxed)) +} + +fn locale_of(raw: u8) -> Locale { + if raw == ZH_HANS { + Locale::ZhHans + } else { + Locale::En + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Locale { + En, + ZhHans, +} + +fn translate(locale: Locale, key: L10nKey) -> &'static str { + let (en, zh) = match key { + L10nKey::SearchTabs => ("Search tabs…", "搜索标签页…"), + L10nKey::SearchFiles => ("Search files…", "搜索文件…"), + L10nKey::SearchThemes => ("Search themes…", "搜索主题…"), + L10nKey::SearchSettings => ("Search settings…", "搜索设置…"), + L10nKey::FilterHosts => ("Filter hosts…", "筛选主机…"), + L10nKey::SearchCommandsOrHost => ( + "Search or type user@host to connect…", + "搜索或输入 user@host 连接…", + ), + L10nKey::SearchTheme => ("Search…", "搜索…"), + L10nKey::Search => ("Search", "搜索"), + L10nKey::SearchWorkspacesAndMachines => { + ("Search workspaces and machines", "搜索工作区与机器") + } + L10nKey::SearchFonts => ("Search fonts…", "搜索字体…"), + L10nKey::NewFolderName => ("New folder name", "新文件夹名称"), + L10nKey::NewFileName => ("New file name", "新文件名称"), + L10nKey::HomeNewTab => ("New Tab", "新标签页"), + L10nKey::HomeReopenClosedTab => ("Reopen Closed Tab", "重新打开已关闭的标签页"), + L10nKey::HomeSwitchWorkspace => ("Switch Workspace", "切换工作区"), + L10nKey::HomeCommandPalette => ("Command Palette", "命令面板"), + L10nKey::HomeSplitRight => ("Split Right", "向右分屏"), + L10nKey::HomeSplitDown => ("Split Down", "向下分屏"), + L10nKey::HomeSettings => ("Settings…", "设置…"), + L10nKey::TrayQuitStopServer => ("Quit and Stop Server…", "退出并停止服务器…"), + L10nKey::Reconnect => ("Reconnect", "重新连接"), + L10nKey::None => ("None.", "无。"), + L10nKey::TryAgain => ("Try Again", "重试"), + L10nKey::Refreshing => ("refreshing…", "正在刷新…"), + L10nKey::Binary => ("binary", "二进制文件"), + L10nKey::Delete => ("Delete", "删除"), + L10nKey::NoMatchingCommands => ("No matching commands", "没有匹配的命令"), + L10nKey::ConnectSshHint => ( + "Type user@host to connect over SSH instead.", + "输入 user@host 改为通过 SSH 连接。", + ), + L10nKey::EditHint => ("→ edit", "→ 编辑"), + L10nKey::OpenFileFromTree => ("Open a file from the file tree", "从文件树打开文件"), + L10nKey::FileChangedOnDisk => ("File changed on disk", "文件在磁盘上已被修改"), + L10nKey::Reload => ("Reload", "重新加载"), + L10nKey::KeepMine => ("Keep mine", "保留我的版本"), + L10nKey::Dismiss => ("Dismiss", "关闭"), + L10nKey::StoredPasswordRejected => ( + "The stored password was rejected. Enter a new one.", + "已存储的密码被拒绝,请输入新密码。", + ), + L10nKey::Trust => ("Trust", "信任"), + L10nKey::Abort => ("Abort", "中止"), + L10nKey::HostKeyOverrideMessage => ( + "Type \"yes\" to override and trust the new key, or Esc to abort.", + "输入 yes 覆盖并信任新密钥,或按 Esc 中止。", + ), + L10nKey::Override => ("Override", "覆盖"), + L10nKey::RememberKeychain => ("Remember (keychain)", "记住(钥匙串)"), + L10nKey::CloseWindowTitle => ("Close Window?", "是否关闭窗口?"), + L10nKey::CloseWindowBody => ( + "Your sessions keep running in the background. This workspace will be \ + waiting on the home page, and in the workspace menu in the title bar, the \ + next time you open tty7.", + "你的会话会继续在后台运行。此工作区将保留,下次启动时可在主页和标题栏工作区菜单中找到。", + ), + L10nKey::Cancel => ("Cancel", "取消"), + L10nKey::Close => ("Close", "关闭"), + L10nKey::QuitStopServerTitle => ("Quit and Stop Server?", "退出并停止服务器?"), + L10nKey::QuitStopServerBody => ( + "This quits tty7 and stops the background server — anything still running \ + in your shells is terminated. Your tabs and layout are kept and reopen with \ + fresh shells next launch. (Plain Quit keeps shells running.)", + "这会退出 tty7 并停止后台服务器,所有仍在运行的 shell 都会被终止。你的标签页和布局会被保留,下次启动时以全新的 shell 重新打开。(普通退出会保持 shell 运行。)", + ), + L10nKey::QuitAndStop => ("Quit and Stop", "退出并停止"), + L10nKey::CloseSshConnectionTitle => ("Close this SSH connection?", "关闭这个 SSH 连接?"), + L10nKey::CloseSshConnectionBody => ( + "The connection is live. Closing will end it.", + "连接仍处于活动状态。关闭将结束它。", + ), + L10nKey::Keep => ("Keep", "保留"), + L10nKey::SettingsNavAppearance => ("Appearance", "外观"), + L10nKey::SettingsNavTerminal => ("Terminal", "终端"), + L10nKey::SettingsNavInput => ("Input", "输入"), + L10nKey::SettingsNavSsh => ("SSH", "SSH"), + L10nKey::SettingsNavAgents => ("Agents", "智能体"), + L10nKey::SettingsNavWindowTabs => ("Window & Tabs", "窗口与标签页"), + L10nKey::SettingsNavKeybindings => ("Keybindings", "按键绑定"), + L10nKey::SettingsNavAbout => ("About", "关于"), + L10nKey::SettingsHeader => ("SETTINGS", "设置"), + L10nKey::Reset => ("Reset", "重置"), + L10nKey::Save => ("Save", "保存"), + L10nKey::Connect => ("Connect", "连接"), + L10nKey::Download => ("Download", "下载"), + L10nKey::Link => ("Link", "关联"), + L10nKey::SettingsThemeIntroTitle => ("Theme", "主题"), + L10nKey::SettingsThemeIntroDesc => ( + "Pick a color theme. Each one sets its own light or dark look.", + "选择配色主题。每个主题都有各自的浅色或深色外观。", + ), + L10nKey::SettingsTypography => ("Typography", "字体排版"), + L10nKey::SettingsFontSize => ("Font size", "字号"), + L10nKey::SettingsFontSizeDesc => { + ("Terminal text size in pixels.", "终端文字大小(像素)。") + } + L10nKey::SettingsLineHeight => ("Line height", "行高"), + L10nKey::SettingsLineHeightDesc => ( + "Row spacing as a multiple of the font size.", + "行间距为字号的倍数。", + ), + L10nKey::SettingsFontFamily => ("Font family", "字体族"), + L10nKey::SettingsFontFamilyDesc => ( + "Pick from fonts installed on your system.", + "从系统已安装的字体中选择。", + ), + L10nKey::SettingsBoldFont => ("Bold font", "粗体字体"), + L10nKey::SettingsBoldFontDesc => ( + "Face for bold text; Default synthesizes it from the primary.", + "粗体文字使用的字体;默认由主字体合成。", + ), + L10nKey::SettingsItalicFont => ("Italic font", "斜体字体"), + L10nKey::SettingsItalicFontDesc => ( + "Face for italic text; Default synthesizes it from the primary.", + "斜体文字使用的字体;默认由主字体合成。", + ), + L10nKey::SettingsFontLigatures => ("Font ligatures", "字体连字"), + L10nKey::SettingsFontLigaturesDesc => ( + "Enable common programming ligature features for terminal text.", + "为终端文字启用常见的编程连字特性。", + ), + L10nKey::SettingsCursor => ("Cursor", "光标"), + L10nKey::SettingsCursorShape => ("Cursor shape", "光标形状"), + L10nKey::SettingsCursorShapeDesc => { + ("How the terminal cursor is drawn.", "终端光标的绘制方式。") + } + L10nKey::SettingsCursorBlink => ("Cursor blink", "光标闪烁"), + L10nKey::SettingsCursorBlinkDesc => ( + "Pulse the cursor while the terminal is focused.", + "终端获得焦点时让光标闪烁。", + ), + L10nKey::SettingsLanguage => ("Language", "语言"), + L10nKey::SettingsLanguageDesc => ( + "Choose the language used for the tty7 interface.", + "选择 tty7 界面使用的语言。", + ), + L10nKey::SettingsLanguageEnglish => ("English", "English"), + L10nKey::SettingsLanguageChinese => ("简体中文", "简体中文"), + L10nKey::SettingsSearchLanguageKeywords => ( + "language, locale, english, chinese", + "语言 区域设置 英文 中文 language locale english chinese", + ), + L10nKey::SettingsTransparency => ("Transparency", "透明度"), + L10nKey::SettingsOpacity => ("Opacity", "不透明度"), + L10nKey::SettingsOpacityDesc => ( + "How opaque the window background is, for every theme. Below 100% the desktop shows through.", + "窗口背景的不透明度,适用于所有主题。低于 100% 时可以看到桌面。", + ), + L10nKey::SettingsBlur => ("Blur", "模糊"), + L10nKey::SettingsBlurDesc => ( + "Blur whatever is behind a translucent window (macOS).", + "模糊半透明窗口背后的内容(macOS)。", + ), + L10nKey::FollowTheme => ("Follow theme", "跟随主题"), + L10nKey::SettingsDimInactivePanes => ("Dim inactive panes", "调暗非活动窗格"), + L10nKey::SettingsDimInactivePanesDesc => ( + "Fade unfocused panes in a split so the active one stands out.", + "在分屏中淡化未聚焦的窗格,让活动窗格更突出。", + ), + L10nKey::SettingsOpenThemesFolder => ("Open themes folder", "打开主题文件夹"), + L10nKey::SettingsChangeThemeImage => ("Change…", "更改…"), + L10nKey::SettingsChooseThemeImage => ("Choose…", "选择…"), + L10nKey::SettingsRemoveThemeImage => ("Remove", "移除"), + L10nKey::SettingsImageOpacity => ("Image opacity", "图片不透明度"), + L10nKey::SettingsImageOpacityDesc => ( + "How strongly the image shows over the background color.", + "图片叠加在背景色上的显示强度。", + ), + L10nKey::SettingsEditTheme => ("Edit theme", "编辑主题"), + L10nKey::SettingsEditThemeIntro => ( + "You're editing a copy. Changes save to its file in the themes folder and apply live.", + "你正在编辑一份副本。更改会保存到主题文件夹中的对应文件并实时生效。", + ), + L10nKey::SettingsBackgroundImage => ("Background image", "背景图片"), + L10nKey::SettingsBackgroundImageDesc => ( + "Composited over the background color, under the text.", + "叠加在背景色之上、文字之下。", + ), + L10nKey::SettingsAnsiColors => ("ANSI colors", "ANSI 颜色"), + L10nKey::SettingsCustomThemes => ("Custom themes", "自定义主题"), + L10nKey::SettingsCustomThemesIntro => ( + "Duplicate a theme to edit its colors here, or drop your own in the themes folder: a tty7 YAML theme or an iTerm2 .itermcolors scheme.", + "复制一个主题后可在此编辑其颜色,或者把自定义主题放入主题文件夹:tty7 YAML 主题或 iTerm2 的 .itermcolors 方案。", + ), + L10nKey::SettingsDuplicateToEdit => ("Duplicate to edit", "复制以编辑"), + L10nKey::SettingsHosts => ("Hosts", "主机"), + L10nKey::SettingsDefaults => ("Defaults", "默认值"), + L10nKey::SettingsInheritedByEveryHost => ("Inherited by every host", "所有主机都继承"), + L10nKey::SettingsNoSavedHosts => ("No saved hosts yet.", "还没有保存的主机。"), + L10nKey::SettingsNothingMatches => { + ("Nothing matches {query}.", "没有匹配 {query} 的内容。") + } + L10nKey::SettingsInTty7 => ("In tty7", "在 tty7 中"), + L10nKey::SettingsImportFromSshConfig => { + ("Import from ~/.ssh/config", "从 ~/.ssh/config 导入") + } + L10nKey::SettingsExpandAllGroups => ("Expand all groups", "展开所有分组"), + L10nKey::SettingsNoHostsYet => ("No hosts yet", "还没有主机"), + L10nKey::SettingsNothingSelected => ("Nothing selected", "未选择任何内容"), + L10nKey::SettingsTypeAddressToConnect => ( + "Type an address to connect now — tty7 offers to save it afterwards.", + "输入地址即可立刻连接,之后 tty7 会提示保存。", + ), + L10nKey::SettingsMoreInSshConfig => ( + "{count} more in ~/.ssh/config", + "~/.ssh/config 中还有 {count} 个", + ), + L10nKey::SettingsAliasesLinked => ("{count} aliases linked.", "已关联 {count} 个别名。"), + L10nKey::SettingsImportAliases => ("Import aliases", "导入别名"), + L10nKey::SettingsImportAliasesDesc => ( + "Re-reads the file and adds anything new. Edits you make here are stored by tty7 — the file itself is never written.", + "重新读取文件并添加新内容。你在这里做的编辑由 tty7 保存——不会写入该文件本身。", + ), + L10nKey::SettingsImportNow => ("Import now", "立即导入"), + L10nKey::SettingsDefaultsIntro => ( + "Every host starts from these. Any host can override one under its own Advanced.", + "所有主机都从这些设置开始。每个主机都可以在自己的高级选项中覆盖某项。", + ), + L10nKey::SettingsCopyAddress => ("Copy address", "复制地址"), + L10nKey::SettingsDuplicate => ("Duplicate", "复制"), + L10nKey::SettingsForgetPassword => ("Forget password", "忘记密码"), + L10nKey::SettingsForgotPasswordFor => ( + "Forgot saved password for {endpoint}", + "已忘记 {endpoint} 的已保存密码", + ), + L10nKey::SettingsCouldntForgetPassword => ( + "Couldn't forget password for {endpoint}: {error}", + "无法忘记 {endpoint} 的密码:{error}", + ), + L10nKey::SettingsSecurity => ("Security", "安全"), + L10nKey::SettingsSecurityIntro => ( + "A host can override either of these under its own Advanced.", + "主机可以在自己的高级选项中覆盖这些设置。", + ), + L10nKey::SettingsVerifyHostKeys => ("Verify host keys", "校验主机密钥"), + L10nKey::SettingsVerifyHostKeysDesc => ( + "Check each server's key against known_hosts and confirm unknown or changed keys before connecting. Off connects without checking, so a spoofed server would go unnoticed.", + "在连接前对照 known_hosts 检查每台服务器的密钥,并确认未知或已更改的密钥。关闭时连接不做检查,仿冒服务将无法被发现。", + ), + L10nKey::WarnBeforeClosing => ("Warn before closing", "关闭前警告"), + L10nKey::SettingsWarnBeforeClosingDesc => ( + "Ask for confirmation before closing a tab or pane with a live SSH session.", + "在关闭带有活动 SSH 会话的标签页或窗格前请求确认。", + ), + L10nKey::SettingsNewHost => ("New host", "新主机"), + L10nKey::SettingsName => ("Name", "名称"), + L10nKey::SettingsNameDesc => ("A label for this connection.", "此连接的标签。"), + L10nKey::SettingsHost => ("Host", "主机"), + L10nKey::SettingsHostDesc => ("Hostname or IP address.", "主机名或 IP 地址。"), + L10nKey::SettingsUser => ("User", "用户"), + L10nKey::SettingsUserDesc => ( + "Login user (blank = resolve at connect).", + "登录用户(留空表示连接时解析)。", + ), + L10nKey::SettingsAuth => ("Auth", "认证"), + L10nKey::SettingsAuthDesc => ( + "Authentication method. Auto tries every applicable method.", + "认证方式。自动会依次尝试所有适用的方式。", + ), + L10nKey::SettingsAuthModeAuto => ("Auto", "自动"), + L10nKey::SettingsAuthModePassword => ("Password", "密码"), + L10nKey::SettingsAuthModeKey => ("Key", "密钥"), + L10nKey::SettingsAuthModeAgent => ("Agent", "代理"), + L10nKey::SettingsAuthMode2Fa => ("2FA", "2FA"), + L10nKey::SettingsJumpHost => ("Jump host", "跳板主机"), + L10nKey::SettingsJumpHostDesc => ( + "Name of another profile to tunnel through (blank = direct).", + "用于隧道中转的另一配置文件名称(留空 = 直连)。", + ), + L10nKey::SettingsNoneSummary => ("(none)", "(无)"), + L10nKey::SettingsNoneLower => ("none", "无"), + L10nKey::SettingsPortForwarding => ("Port forwarding", "端口转发"), + L10nKey::SettingsRulesOpenedWithConnection => { + ("1 rule, opened with the connection", "1 条规则,随连接打开") + } + L10nKey::SettingsAddRule => ("+ Add rule", "+ 添加规则"), + L10nKey::SettingsFwdLegendLocal => ( + "L — a local port reaches the remote side", + "L — 本地端口可达远程侧", + ), + L10nKey::SettingsFwdLegendRemote => ( + "R — a remote port reaches this machine", + "R — 远程端口可达本机", + ), + L10nKey::SettingsFwdLegendDynamic => ("D — dynamic SOCKS proxy", "D — 动态 SOCKS 代理"), + L10nKey::SettingsFwdNeedsBoth => ( + "Needs a listen port and a target host:port — won't be saved.", + "需要监听端口和目标 host:port——不会被保存。", + ), + L10nKey::SettingsFwdNeedsListen => ( + "Needs a listen port — won't be saved.", + "需要监听端口——不会被保存。", + ), + L10nKey::SettingsAdvanced => ("Advanced", "高级"), + L10nKey::SettingsAdvancedSummary => ( + "algorithms / keepalive / proxies / X11 / login scripts", + "算法 / 保活 / 代理 / X11 / 登录脚本", + ), + L10nKey::SettingsIdentityFiles => ("Identity files", "身份文件"), + L10nKey::SettingsIdentityFilesDesc => ( + "Private-key paths, one per line (%h/%r expand).", + "私钥路径,每行一个(支持 %h/%r 展开)。", + ), + L10nKey::SettingsAgentForwarding => ("Agent forwarding", "代理转发"), + L10nKey::SettingsAgentForwardingDesc => ( + "Forward the local ssh-agent to the connection.", + "将本机 ssh-agent 转发到该连接。", + ), + L10nKey::SettingsProxyCommand => ("ProxyCommand", "代理命令"), + L10nKey::SettingsProxyCommandDesc => ( + "Transport command (%h/%p/%r substituted).", + "传输命令(%h/%p/%r 会被替换)。", + ), + L10nKey::SettingsSocks5Proxy => ("SOCKS5 proxy", "SOCKS5 代理"), + L10nKey::SettingsSocks5ProxyDesc => { + ("host:port (blank = none).", "host:port(留空 = 无)。") + } + L10nKey::SettingsHttpProxy => ("HTTP proxy", "HTTP 代理"), + L10nKey::SettingsHttpProxyDesc => ("host:port (blank = none).", "host:port(留空 = 无)。"), + L10nKey::SettingsKexAlgorithms => ("KEX algorithms", "KEX 算法"), + L10nKey::SettingsKexAlgorithmsDesc => ( + "Comma-separated (blank = library default).", + "逗号分隔(留空 = 库默认值)。", + ), + L10nKey::SettingsCiphers => ("Ciphers", "加密算法"), + L10nKey::SettingsCiphersDesc => ( + "Comma-separated (blank = default).", + "逗号分隔(留空 = 默认值)。", + ), + L10nKey::SettingsMacs => ("MACs", "MAC 算法"), + L10nKey::SettingsMacsDesc => ( + "Comma-separated (blank = default).", + "逗号分隔(留空 = 默认值)。", + ), + L10nKey::SettingsHostKeyAlgorithms => ("Host-key algorithms", "主机密钥算法"), + L10nKey::SettingsHostKeyAlgorithmsDesc => ( + "Comma-separated (blank = default).", + "逗号分隔(留空 = 默认值)。", + ), + L10nKey::SettingsCompression => ("Compression", "压缩"), + L10nKey::SettingsJumpHostVia => ("via {jump_name}", "经由 {jump_name}"), + L10nKey::SettingsConnected => ("connected", "已连接"), + L10nKey::SettingsProfileCopied => ("{name} (copy)", "{name}(副本)"), + L10nKey::SettingsCompressionDesc => ( + "Comma-separated (blank = default).", + "逗号分隔(留空 = 默认值)。", + ), + L10nKey::SettingsKeepaliveInterval => ("Keepalive interval (s)", "保活间隔(秒)"), + L10nKey::SettingsKeepaliveIntervalDesc => ("Blank = library default.", "留空 = 库默认值。"), + L10nKey::SettingsKeepaliveCountMax => ("Keepalive count max", "最大保活次数"), + L10nKey::SettingsKeepaliveCountMaxDesc => ( + "Missed keepalives before dead.", + "判定断连前允许丢失的保活次数。", + ), + L10nKey::SettingsConnectTimeout => ("Connect timeout (s)", "连接超时(秒)"), + L10nKey::SettingsConnectTimeoutDesc => ("Blank = library default.", "留空 = 库默认值。"), + L10nKey::SettingsX11Forwarding => ("X11 forwarding", "X11 转发"), + L10nKey::SettingsX11ForwardingDesc => ( + "Request X11 forwarding (needs XQuartz on macOS).", + "请求 X11 转发(macOS 上需要 XQuartz)。", + ), + L10nKey::SettingsShellIntegration => ("Shell integration", "Shell 集成"), + L10nKey::SettingsShellIntegrationDesc => ( + "Let the remote shell report prompts, exit codes and directory.", + "让远程 shell 报告提示符、退出码和目录。", + ), + L10nKey::SettingsLoginScripts => ("Login scripts", "登录脚本"), + L10nKey::SettingsLoginScriptsDesc => ( + "Commands sent after the shell opens, one per line.", + "shell 打开后发送的命令,每行一个。", + ), + L10nKey::SettingsSkipBanner => ("Skip banner", "跳过横幅"), + L10nKey::SettingsSkipBannerDesc => { + ("Suppress the server login banner.", "抑制服务器登录横幅。") + } + L10nKey::SettingsDefaultFollowsDefaults => ( + "Default follows Defaults, which is {value}.", + "默认跟随默认设置,当前为 {value}。", + ), + L10nKey::SettingsValueOn => ("on", "开"), + L10nKey::SettingsValueOff => ("off", "关"), + L10nKey::SettingsDefault => ("Default", "默认"), + L10nKey::SettingsOn => ("On", "开"), + L10nKey::SettingsOff => ("Off", "关"), + L10nKey::SettingsShell => ("Shell", "Shell"), + L10nKey::SettingsShellIntro => ( + "The program each new terminal launches. Leave Program empty to use the platform default ({default}).", + "每个新终端启动的程序。将 Program 留空可使用平台默认值({default})。", + ), + L10nKey::SettingsProgram => ("Program", "程序"), + L10nKey::SettingsProgramDesc => ( + "Executable name on PATH or an absolute path. e.g. zsh, fish, pwsh.", + "PATH 中的可执行文件名或绝对路径,例如 zsh、fish、pwsh。", + ), + L10nKey::SettingsArguments => ("Arguments", "参数"), + L10nKey::SettingsArgumentsDesc => ( + "Space-separated launch flags. e.g. -l for a login shell.", + "以空格分隔的启动参数,例如登录 shell 用 -l。", + ), + L10nKey::SettingsStartIn => ("Start in", "起始目录"), + L10nKey::SettingsStartInDesc => ( + "What a fresh shell starts in: tty7's launch directory, your home folder, or a fixed path.", + "新 shell 的启动目录:tty7 的启动目录、主目录或固定路径。", + ), + L10nKey::SettingsCustomPath => ("Custom path", "自定义路径"), + L10nKey::SettingsCustomPathDesc => ( + "The directory new shells start in.", + "新 shell 启动的目录。", + ), + L10nKey::SettingsWdInherit => ("Inherit", "继承"), + L10nKey::SettingsWdHome => ("Home", "主目录"), + L10nKey::SettingsWdCustom => ("Custom", "自定义"), + L10nKey::SettingsShellFooter => ( + "Applies to shells with nothing to inherit — like the first tab of a window. New tabs and splits keep inheriting the active pane's directory, and shells already open keep running.", + "仅适用于没有可继承目录的 shell,例如窗口的第一个标签页。新标签页和分屏仍会继承活动窗格的目录,已经打开的 shell 会继续运行。", + ), + L10nKey::SettingsScrolling => ("Scrolling", "滚动"), + L10nKey::SettingsScrollback => ("Scrollback", "回滚缓冲"), + L10nKey::SettingsScrollbackDesc => ( + "Lines of history kept per pane. Applies to new panes.", + "每个窗格保留的历史行数。仅适用于新窗格。", + ), + L10nKey::SettingsScrollSpeed => ("Scroll speed", "滚动速度"), + L10nKey::SettingsScrollSpeedDesc => ( + "Multiplier applied to mouse-wheel scrolling.", + "应用于鼠标滚轮滚动的倍率。", + ), + L10nKey::SettingsMouse => ("Mouse", "鼠标"), + L10nKey::SettingsFocusFollowsMouse => ("Focus follows mouse", "鼠标聚焦跟随"), + L10nKey::SettingsFocusFollowsMouseDesc => ( + "Hovering a pane focuses it without a click.", + "悬停窗格即聚焦,无需点击。", + ), + L10nKey::SettingsHideMouseWhileTyping => ("Hide mouse while typing", "输入时隐藏鼠标"), + L10nKey::SettingsHideMouseWhileTypingDesc => ( + "Hide the pointer as you type; it returns on the next move.", + "输入时隐藏指针;下次移动鼠标时恢复。", + ), + L10nKey::SettingsReportMouseToApps => ("Report mouse to apps", "向应用报告鼠标"), + L10nKey::SettingsReportMouseToAppsDesc => ( + "Let full-screen apps (vim, tmux) handle clicks and scrolling; hold Shift to keep a gesture local.", + "让全屏应用(如 vim、tmux)处理点击和滚动;按住 Shift 可让操作保持本地。", + ), + L10nKey::SettingsBell => ("Bell", "铃声"), + L10nKey::SettingsTerminalBell => ("Terminal bell", "终端铃声"), + L10nKey::SettingsTerminalBellDesc => ( + "How a bell (^G) is signalled: silenced, a brief flash, or the system sound.", + "铃声(^G)的通知方式:静音、短暂闪烁或系统声音。", + ), + L10nKey::SettingsLinks => ("Links", "链接"), + L10nKey::DetectUrls => ("Detect URLs", "检测 URL"), + L10nKey::SettingsDetectUrlsDesc => ( + "Underline links on hover and open them on {modifier}-click.", + "悬停时给链接加下划线,通过 {modifier}+点击 打开。", + ), + L10nKey::ForwardSshLoopbackLinks => ("Forward SSH loopback links", "转发 SSH 回环链接"), + L10nKey::SettingsForwardSshLoopbackLinksDesc => ( + "When a pane is in SSH, open localhost links through a temporary port forward.", + "当窗格处于 SSH 中时,通过临时端口转发打开 localhost 链接。", + ), + L10nKey::OpenFilesWith => ("Open files with", "打开文件方式"), + L10nKey::SettingsOpenFilesWithDesc => ( + "Command run when {modifier}-clicking a file link, instead of the default app. Use {path}, {line}, {column}; a flag whose value is absent is dropped (e.g. herdr edit {path} --line={line}). Empty uses the default app.", + "{modifier}+点击 文件链接时运行的命令,而不是默认应用。可使用 {path}、{line}、{column};参数值缺失的标志会被丢弃(例如 herdr edit {path} --line={line})。留空使用默认应用。", + ), + L10nKey::SettingsBellModeOff => ("Off", "关"), + L10nKey::SettingsBellModeVisual => ("Visual", "闪烁"), + L10nKey::SettingsBellModeAudible => ("Audible", "声音"), + L10nKey::SettingsPrompt => ("Prompt", "提示符"), + L10nKey::SettingsPromptIntro => ( + "tty7's own menus at the shell prompt. Turn one off to hand the key back to the shell.", + "shell 提示符处的 tty7 自带菜单。关闭某项即可把按键交还给 shell。", + ), + L10nKey::SettingsTabCompletion => ("Tab completion", "Tab 补全"), + L10nKey::SettingsTabCompletionDesc => ( + "Tab at the prompt opens tty7's completion menu. When off, Tab goes to the shell's own completion instead.", + "在提示符按 Tab 打开 tty7 的补全菜单。关闭后 Tab 交由 shell 自身的补全处理。", + ), + L10nKey::SettingsHistorySearch => ("History search", "历史搜索"), + L10nKey::SettingsHistorySearchDesc => ( + "⌃R at the prompt opens tty7's fuzzy history menu. When off, ⌃R goes to the shell instead — its own reverse-i-search, or whatever you've bound there (fzf, percol).", + "在提示符按 ⌃R 打开 tty7 的模糊历史菜单。关闭后 ⌃R 交由 shell 处理——它自带的反向搜索,或你在那里绑定的其它功能(fzf、percol)。", + ), + L10nKey::SettingsSelectionClipboard => ("Selection & clipboard", "选择与剪贴板"), + L10nKey::SettingsSmartSelection => ("Smart selection", "智能选择"), + L10nKey::SettingsSmartSelectionDesc => ( + "Double-click selects the whole URL, file path, email, or bracket pair under the cursor.", + "双击选择光标下的完整 URL、文件路径、邮箱或成对的括号。", + ), + L10nKey::SettingsCopyOnSelect => ("Copy on select", "选中即复制"), + L10nKey::SettingsCopyOnSelectDesc => ( + "Selecting text with the mouse copies it to the clipboard right away, no ⌘C needed.", + "用鼠标选中文本时立即复制到剪贴板,无需按 ⌘C。", + ), + L10nKey::SettingsTrimTrailingSpaces => { + ("Trim trailing spaces on copy", "复制时去除末尾空格") + } + L10nKey::SettingsTrimTrailingSpacesDesc => ( + "Strip trailing whitespace from each copied line.", + "去除每行复制文本末尾的空白。", + ), + L10nKey::SettingsKeyboard => ("Keyboard", "键盘"), + L10nKey::SettingsOptionAsMeta => ("Option (⌥) acts as Meta", "Option (⌥) 作为 Meta"), + L10nKey::SettingsOptionAsMetaDesc => ( + "⌥+key sends the escape chord shells expect (⌥B = back one word) instead of typing a special character (∫).", + "⌥+按键 发送 shell 期望的转义组合键(⌥B = 后退一个词),而不是输入特殊字符(∫)。", + ), + L10nKey::SettingsAgentsIntro => ("Agents", "智能体"), + L10nKey::SettingsAgentsIntroDesc => ( + "Hook integrations give panes running these agents live session status (working / waiting / done) in the tab bar. Only active inside tty7.", + "钩子集成让标签栏中的窗格实时显示这些智能体的会话状态(进行中 / 等待中 / 已完成)。仅在 tty7 内生效。", + ), + L10nKey::SettingsReadingAgentConfig => ( + "Reading this machine's agent config…", + "正在读取这台机器的智能体配置…", + ), + L10nKey::SettingsStatusNotInstalled => ("Not installed", "未安装"), + L10nKey::SettingsStatusInstalled => ("Installed", "已安装"), + L10nKey::SettingsStatusOutdated => ("Outdated", "已过时"), + L10nKey::SettingsInstall => ("Install", "安装"), + L10nKey::SettingsReinstall => ("Reinstall", "重新安装"), + L10nKey::SettingsUpdate => ("Update", "更新"), + L10nKey::SettingsUninstall => ("Uninstall", "卸载"), + L10nKey::SettingsOfflineMachines => ( + "{count} more saved machines are not connected — open a workspace on one to install its hooks there.", + "还有 {count} 个已保存的机器未连接——在其中一个上打开工作区以在那里安装钩子。", + ), + L10nKey::SettingsSyncWithSystem => ("Sync with system", "跟随系统"), + L10nKey::SettingsSyncWithSystemDesc => ( + "Follow the OS appearance with separate light and dark themes.", + "跟随操作系统外观,并分别使用浅色与深色主题。", + ), + L10nKey::SettingsChangeTheme => ("Change theme", "更换主题"), + L10nKey::SettingsThemes => ("Themes", "主题"), + L10nKey::SettingsThemePanelManual => ("Change your current theme.", "更改当前主题。"), + L10nKey::SettingsThemePanelLight => { + ("Choose the theme for light mode.", "选择浅色模式的主题。") + } + L10nKey::SettingsThemePanelDark => { + ("Choose the theme for dark mode.", "选择深色模式的主题。") + } + L10nKey::SettingsCustom => ("Custom", "自定义"), + L10nKey::SettingsBuiltIn => ("Built-in", "内置"), + L10nKey::SettingsDark => ("Dark", "深色"), + L10nKey::SettingsLight => ("Light", "浅色"), + L10nKey::SettingsLightMode => ("Light mode", "浅色模式"), + L10nKey::SettingsDarkMode => ("Dark mode", "深色模式"), + L10nKey::SettingsActive => ("Active", "已激活"), + L10nKey::SettingsStartupWindow => ("Startup window", "启动窗口"), + L10nKey::SettingsStartupWindowDesc => ( + "Window state when tty7 launches.", + "tty7 启动时的窗口状态。", + ), + L10nKey::SettingsRememberWindowSize => { + ("Remember window size & position", "记住窗口大小与位置") + } + L10nKey::SettingsRememberWindowSizeDesc => ( + "Reopen at the size and position the window had when tty7 last quit. Off opens centered at the default size.", + "以 tty7 上次退出时窗口的大小和位置重新打开。关闭时以默认大小居中打开。", + ), + L10nKey::SettingsRestoreLastLayout => ("Restore last layout", "恢复上次布局"), + L10nKey::SettingsRestoreLastLayoutDesc => ( + "Reopen the last window's tabs, splits, and directories on launch. Off starts with a single fresh terminal.", + "启动时恢复上次窗口的标签页、分屏和目录。关闭时从单个新终端开始。", + ), + L10nKey::SettingsConfirmLastWindowClose => ( + "Confirm before closing the last window", + "关闭最后一个窗口前确认", + ), + L10nKey::SettingsConfirmLastWindowCloseDesc => ( + "Ask first, since that close also quits tty7. Off closes straight away — either way your shells keep running in the background.", + "因为关闭它会同时退出 tty7,所以先询问。关闭后直接退出——任何情况下你的 shell 都会在后台继续运行。", + ), + L10nKey::SettingsShowTrayIcon => ("Show tray icon", "显示托盘图标"), + L10nKey::SettingsShowTrayIconDesc => ( + "Keep a status item in the system tray / menu bar: it signals when a coding agent needs your input, and its menu jumps to agent panes.", + "在系统托盘/菜单栏保留状态项:当编码智能体需要输入时发出提示,其菜单可跳转到智能体窗格。", + ), + L10nKey::SettingsTabs => ("Tabs", "标签页"), + L10nKey::SettingsNewTabPosition => ("New tab position", "新标签页位置"), + L10nKey::SettingsNewTabPositionDesc => ( + "Where a freshly opened tab is inserted.", + "新打开的标签页插入的位置。", + ), + L10nKey::SettingsTabBarPosition => ("Tab bar position", "标签栏位置"), + L10nKey::SettingsTabBarPositionDesc => ( + "Show tabs as a horizontal strip on top or a vertical sidebar on the left.", + "将标签页显示为顶部横向条或左侧垂直侧栏。", + ), + L10nKey::SettingsSidebarGrouping => ("Sidebar grouping", "侧栏分组"), + L10nKey::SettingsSidebarGroupingDesc => ( + "Group sidebar tabs under a header per git repository, with non-repo tabs in a Scratch section. Only applies to the left sidebar.", + "按 git 仓库在标题下对侧栏标签页分组,非仓库标签页放在 Scratch 区。仅适用于左侧栏。", + ), + L10nKey::SettingsDiffPreviewFromCounts => ( + "Open diff preview from sidebar counts", + "从侧栏计数打开差异预览", + ), + L10nKey::SettingsDiffPreviewFromCountsDesc => ( + "Click a row's +N −N to open the working-tree diff in an overlay. Off keeps the branch and the counts on the row and just stops them being clickable.", + "点击行上的 +N −N 可在浮层中打开工作树差异。关闭时行上仍显示分支和计数,但不再可点击。", + ), + L10nKey::SettingsNotifications => ("Notifications", "通知"), + L10nKey::SettingsNotifyOnCommandFinish => ("Notify on command finish", "命令完成时通知"), + L10nKey::SettingsNotifyOnCommandFinishDesc => ( + "Desktop alert after a long foreground command completes.", + "较长的前台命令完成后发出桌面提醒。", + ), + L10nKey::SettingsNotifyThreshold => ("Notify threshold", "通知阈值"), + L10nKey::SettingsNotifyThresholdDesc => ( + "How long a command must run to qualify as \"long\".", + "命令需运行多久才能算作\"较长\"。", + ), + L10nKey::SettingsWindow => ("Window", "窗口"), + L10nKey::NotifyModeNever => ("Never", "从不"), + L10nKey::NotifyModeUnfocused => ("When Unfocused", "窗口未聚焦时"), + L10nKey::NotifyModeAlways => ("Always", "总是"), + L10nKey::SettingsStartupNormal => ("Normal", "普通"), + L10nKey::SettingsStartupMaximized => ("Maximized", "最大化"), + L10nKey::SettingsStartupFullscreen => ("Fullscreen", "全屏"), + L10nKey::SettingsAfterCurrent => ("After current", "当前之后"), + L10nKey::SettingsAtEnd => ("At end", "末尾"), + L10nKey::SettingsTop => ("Top", "顶部"), + L10nKey::SettingsLeft => ("Left", "左侧"), + L10nKey::SettingsByRepo => ("By repo", "按仓库"), + L10nKey::SettingsFlat => ("Flat", "平铺"), + L10nKey::SettingsPreset => ("Preset", "预设"), + L10nKey::SettingsPresetDesc => ( + "tmux remaps pane/tab actions onto prefix sequences (e.g. Ctrl-B then C).", + "tmux 预设把窗格/标签页操作映射为前缀序列(例如 Ctrl-B 后按 C)。", + ), + L10nKey::SettingsPrefix => ("Prefix", "前缀"), + L10nKey::SettingsPressKeys => ("Press keys…", "按下按键…"), + L10nKey::SettingsPauseToSaveEsc => ("pause to save · Esc", "暂停以保存 · Esc"), + L10nKey::SettingsKeybindingsIntroDesc => ( + "Click a shortcut, then press the new keys — it saves after a brief pause. Chain keys for a sequence like Ctrl-B then X. Esc cancels; Backspace removes the last key, or resets the shortcut to default when pressed first.", + "点击某个快捷键,然后按下新按键,短暂停顿后便会保存。可连续按键组成序列,例如 Ctrl-B 后按 X。Esc 取消;Backspace 移除最后一个按键,若最先按下则重置为默认。", + ), + L10nKey::SettingsPrefixNote => ( + "With a prefix active, a bare prefix key reaches the shell after a ~1s pause, and prefix + an unbound key is sent through to the terminal.", + "启用前缀后,单独按前缀键约 1 秒后会传给 shell,前缀 + 未绑定的按键会直接发送到终端。", + ), + L10nKey::SettingsRestoreAllDefaults => ("Restore all defaults", "恢复全部默认值"), + L10nKey::SettingsAboutDesc1 => ( + "A terminal workbench: shells, workspaces, SSH, coding agents.", + "一个终端工作台:shell、工作区、SSH、编码智能体。", + ), + L10nKey::SettingsAboutDesc2 => ( + "Editor-grade input in every shell, shells that outlive quits and reboots without tmux, a native SSH stack with profiles and port forwarding, and live status for panes running coding agents.", + "每个 shell 都具备编辑器级输入;无需 tmux 也能让 shell 在退出和重启后继续运行;原生的 SSH 栈支持配置文件和端口转发;为运行编码智能体的窗格提供实时状态。", + ), + L10nKey::SettingsAboutTech => ( + "Pure Rust · GPU rendering on Zed's gpui · VT core from Alacritty", + "纯 Rust · 基于 Zed 的 gpui 进行 GPU 渲染 · 来自 Alacritty 的 VT 核心", + ), + L10nKey::SettingsVersion => ("Version", "版本"), + L10nKey::SettingsUpdates => ("Updates", "更新"), + L10nKey::SettingsVersionAvailable => { + ("Version {version} is available.", "新版本 {version} 可用。") + } + L10nKey::SettingsCheckUpdatesDesc => ( + "Check GitHub for a newer release on launch and show it here. tty7 never updates itself — downloading happens on the Releases page.", + "启动时检查 GitHub 是否有新版本并在此显示。tty7 不会自行更新——下载在 Releases 页面完成。", + ), + L10nKey::SettingsCheckUpdatesOnLaunch => ("Check for updates on launch", "启动时检查更新"), + L10nKey::SettingsCommandLine => ("Command line", "命令行"), + L10nKey::SettingsCommandLineDesc => ( + "Put the bundled `tty7` command on your PATH at launch, so scripts and coding agents can drive tty7 from any terminal. Inside a tty7 pane it works either way. Turn this off if you keep your own `tty7` — one you built or installed yourself — and do not want it shadowed. Takes effect at next launch.", + "启动时将自带的 `tty7` 命令加入 PATH,让脚本和编码智能体可在任意终端驱动 tty7。在 tty7 窗格内两种情况都可用。如果你自己构建或安装了 `tty7` 且不希望被遮蔽,请关闭此选项。下次启动时生效。", + ), + L10nKey::SettingsInstallCliOnPath => ( + "Install the `tty7` command on PATH", + "将 `tty7` 命令安装到 PATH", + ), + L10nKey::SettingsExplorerContextMenu => ("Windows Explorer", "Windows 文件资源管理器"), + L10nKey::SettingsExplorerContextMenuDesc => ( + "Add “Open in tty7” when you right-click a folder and “Open tty7 here” when you right-click a folder background. This is off by default and is registered only for your Windows account.", + "右键单击文件夹时添加“Open in tty7”,右键单击文件夹背景时添加“Open tty7 here”。此功能默认关闭,且只为当前 Windows 帐户注册。", + ), + L10nKey::SettingsExplorerNotRegistered => ("Not registered", "未注册"), + L10nKey::SettingsExplorerRegistered => ("Registered", "已注册"), + L10nKey::SettingsExplorerNeedsUpdate => ("Needs update", "需要更新"), + L10nKey::SettingsExplorerUnavailable => ("Unavailable", "不可用"), + L10nKey::SettingsExplorerStatusUnavailable => ("Status unavailable", "无法获取状态"), + L10nKey::SettingsExplorerRegister => ("Register", "注册"), + L10nKey::SettingsExplorerUpdate => ("Update", "更新"), + L10nKey::SettingsExplorerUnregister => ("Unregister", "取消注册"), + L10nKey::SettingsExplorerRegisteredNote => ( + "Registered. Right-click a folder or folder background in Explorer to open it in tty7.", + "已注册。现在可以在文件资源管理器中右键单击文件夹或文件夹背景,以在 tty7 中打开。", + ), + L10nKey::SettingsExplorerUnregisteredNote => ( + "Unregistered from Windows Explorer.", + "已从 Windows 文件资源管理器中取消注册。", + ), + L10nKey::SettingsExplorerRegisterFailed => { + ("Could not register: {error}", "无法注册:{error}") + } + L10nKey::SettingsExplorerUnregisterFailed => { + ("Could not unregister: {error}", "无法取消注册:{error}") + } + L10nKey::SettingsExplorerWindows11Note => ( + "On Windows 11, classic shell entries may appear under “Show more options”.", + "在 Windows 11 上,经典右键菜单项可能显示在“显示更多选项”中。", + ), + L10nKey::SettingsServer => ("Server", "服务器"), + L10nKey::SettingsServerDesc => ( + "Restart the server on this computer to pick up a newly granted macOS permission, recover if it stops responding, or start from a clean slate. This ends all running shells here; your tabs and layout reopen with fresh shells. A remote machine's server is restarted from its own menu in the workspace switcher.", + "重启这台计算机上的服务器以应用新授予的 macOS 权限,在无响应时恢复,或重新开始。这会结束此处所有正在运行的 shell;你的标签页和布局会以全新的 shell 重新打开。远程机器的服务器可从工作区切换器的对应菜单中重启。", + ), + L10nKey::SettingsRestartServer => ("Restart server…", "重启服务器…"), + L10nKey::SettingsAgentClaudeCode => ("Claude Code", "Claude Code"), + L10nKey::SettingsAgentCodex => ("Codex", "Codex"), + L10nKey::SettingsAgentCopilotCli => ("Copilot CLI", "Copilot CLI"), + L10nKey::SettingsAgentOpencode => ("OpenCode", "OpenCode"), + L10nKey::SettingsAgentPi => ("Pi", "Pi"), + L10nKey::SettingsAgentGrokBuild => ("Grok Build", "Grok Build"), + L10nKey::SettingsSearchAboutKeywords => ( + "version license credits build update check github", + "关于 版本 许可证 致谢 构建 更新 检查 github about version license credits update", + ), + L10nKey::SettingsSearchAnsiColorsKeywords => ( + "palette 16 terminal colours theme", + "ANSI颜色 调色板 终端颜色 主题 ansi colors palette terminal theme", + ), + L10nKey::SettingsSearchArgumentsKeywords => ( + "shell flags login args", + "参数 shell 启动参数 登录参数 arguments shell flags login args", + ), + L10nKey::SettingsSearchBlurKeywords => ( + "transparency translucent frosted vibrancy window background", + "模糊 毛玻璃 半透明 窗口 背景 blur frosted vibrancy window background", + ), + L10nKey::SettingsSearchBoldFontKeywords => ( + "typeface weight", + "粗体 字体粗细 字重 bold font weight typeface", + ), + L10nKey::SettingsSearchClaudeCodeKeywords => ( + "agent integration hooks install uninstall status rich session working waiting tab bar sidebar badge claude", + "Claude Code 智能体 集成 钩子 安装 卸载 状态 会话 claude agent integration hooks install", + ), + L10nKey::SettingsSearchCodexKeywords => ( + "agent integration hooks install openai codex", + "Codex 智能体 集成 钩子 安装 OpenAI codex agent integration hooks install", + ), + L10nKey::SettingsSearchCommandLineToolKeywords => ( + "cli tty7 path shell command install symlink terminal iterm agent script", + "命令行工具 cli tty7 路径 shell 命令 安装 符号链接 terminal command line tool", + ), + L10nKey::SettingsSearchCommandLineToolTitle => ("Command line tool", "命令行工具"), + L10nKey::SettingsSearchConfirmLastWindowCloseKeywords => ( + "close quit confirm prompt dialog ask again warn last window cmd-w ctrl-w", + "关闭最后一个窗口前确认 关闭 退出 确认 提示 最后一个窗口 confirm close last window quit", + ), + L10nKey::SettingsSearchCopilotCliKeywords => ( + "agent integration hooks install github copilot", + "Copilot CLI 智能体 集成 钩子 安装 GitHub copilot agent integration hooks install", + ), + L10nKey::SettingsSearchCopyOnSelectKeywords => ( + "clipboard selection yank mouse", + "选中即复制 复制 剪贴板 选择 鼠标 copy on select clipboard yank", + ), + L10nKey::SettingsSearchCursorBlinkKeywords => ( + "caret blinking flash", + "光标闪烁 闪烁 光标 blink cursor blinking flash", + ), + L10nKey::SettingsSearchCursorShapeKeywords => ( + "caret block bar underline beam", + "光标形状 光标 块 竖线 下划线 cursor shape caret block bar underline beam", + ), + L10nKey::SettingsSearchCustomThemesKeywords => ( + "theme duplicate edit colors folder yaml import", + "自定义主题 复制 编辑 颜色 文件夹 yaml 导入 theme custom edit duplicate colors import", + ), + L10nKey::SettingsSearchDetectUrlsKeywords => ( + "links hyperlink clickable open", + "检测URL 链接 超链接 可点击 打开 detect urls links hyperlink open", + ), + L10nKey::SettingsSearchDiffPreviewFromCountsKeywords => ( + "diff overlay preview sidebar counts git changes click branch lines", + "从侧栏计数打开差异预览 差异 预览 侧栏 git diff preview sidebar counts git changes", + ), + L10nKey::SettingsSearchDimInactivePanesKeywords => ( + "fade unfocused inactive split pane focus opacity highlight active dimming", + "调暗 非活动窗格 淡化 未聚焦 分屏 高亮 active dimming pane focus", + ), + L10nKey::SettingsSearchExplorerContextMenuKeywords => ( + "windows explorer context menu right click folder directory background shell menu register unregister open here", + "Windows 文件资源管理器 右键 菜单 文件夹 目录 背景 注册 取消注册 打开 explorer context menu right click folder directory background shell register unregister open here", + ), + L10nKey::SettingsSearchFocusFollowsMouseKeywords => ( + "pane hover activate", + "鼠标聚焦跟随 悬停 激活 窗格 focus follows mouse hover activate pane", + ), + L10nKey::SettingsSearchFontFamilyKeywords => ( + "typeface monospace typography", + "字体 字体族 等宽 排版 font family monospace typography typeface", + ), + L10nKey::SettingsSearchFontLigaturesKeywords => ( + "typography glyph fira", + "字体连字 连字 字形 typography ligatures glyph fira", + ), + L10nKey::SettingsSearchFontSizeKeywords => ( + "typography text bigger smaller zoom", + "字号 字体大小 文字 放大 缩小 typography font size bigger smaller zoom", + ), + L10nKey::SettingsSearchForwardSshLoopbackLinksKeywords => ( + "ssh remote port tunnel localhost forward links", + "SSH回环链接 端口转发 隧道 localhost 转发 forward ssh loopback links tunnel", + ), + L10nKey::SettingsSearchGrokBuildKeywords => ( + "agent integration hooks install xai grok build", + "Grok Build 智能体 集成 钩子 安装 xai grok build agent integration hooks install", + ), + L10nKey::SettingsSearchHideMouseWhileTypingKeywords => ( + "cursor pointer autohide", + "输入时隐藏鼠标 隐藏鼠标 指针 自动隐藏 hide mouse typing cursor pointer autohide", + ), + L10nKey::SettingsSearchHistorySearchKeywords => ( + "ctrl-r reverse search fuzzy history recall fzf prompt", + "历史搜索 反向搜索 模糊搜索 ctrl-r fzf history search recall", + ), + L10nKey::SettingsSearchHostsKeywords => ( + "ssh host connection saved profile import ssh_config manage add edit quick connect", + "主机 SSH 连接 保存 配置文件 导入 ssh_config 管理 添加 编辑 快速连接 hosts ssh profile import connect", + ), + L10nKey::SettingsSearchHowShellsWorkKeywords => ( + "shell session daemon server detach persist background close quit stop delete workspace layout survive reboot tmux", + "Shell工作原理 shell 会话 守护进程 持久化 后台 工作区 布局 survive reboot daemon how shells work", + ), + L10nKey::SettingsSearchHowShellsWorkTitle => ("How shells work", "Shell 工作原理"), + L10nKey::SettingsSearchItalicFontKeywords => { + ("typeface oblique", "斜体 字体样式 italic oblique typeface") + } + L10nKey::SettingsSearchKeybindingsKeywords => ( + "shortcut hotkey keyboard binding chord tmux preset rebind prefix", + "按键绑定 快捷键 热键 键盘 绑定 前缀 tmux keybindings shortcut hotkey binding prefix", + ), + L10nKey::SettingsSearchKeybindingsTitle => ("Keybindings", "按键绑定"), + L10nKey::SettingsSearchLineHeightKeywords => ( + "typography leading spacing", + "行高 行间距 行距 typography line height spacing leading", + ), + L10nKey::SettingsSearchNewTabPositionKeywords => ( + "tabs order end after current", + "新标签页位置 标签页 顺序 末尾 当前之后 new tab position tabs order end after current", + ), + L10nKey::SettingsSearchNotifyOnCommandFinishKeywords => ( + "notification alert done osc desktop banner long command", + "命令完成时通知 通知 提醒 命令 notify command finish notification alert desktop", + ), + L10nKey::SettingsSearchNotifyThresholdKeywords => ( + "notification alert seconds duration long command delay", + "通知阈值 通知 秒数 时长 命令 notify threshold notification duration seconds", + ), + L10nKey::SettingsSearchOpacityKeywords => ( + "transparency translucent see through window alpha", + "不透明度 透明度 窗口 半透明 alpha opacity transparency translucent window", + ), + L10nKey::SettingsSearchOpenFilesWithKeywords => ( + "links file editor command external app path line column", + "打开文件 链接 编辑器 命令 外部应用 路径 行号 列号 open files editor command path line column", + ), + L10nKey::SettingsSearchOpencodeKeywords => ( + "agent integration plugin install opencode", + "OpenCode 智能体 集成 插件 安装 opencode agent integration plugin install", + ), + L10nKey::SettingsSearchOptionAsMetaKeywords => ( + "alt keyboard modifier escape macos option meta option acts as meta", + "Option作为Meta 修饰键 alt option meta 转义 escape macos keyboard modifier", + ), + L10nKey::SettingsSearchPiKeywords => ( + "agent integration extension install pi", + "Pi 智能体 集成 扩展 安装 pi agent integration extension install", + ), + L10nKey::SettingsSearchPortForwardingKeywords => ( + "ssh tunnel local remote dynamic socks forward rule", + "端口转发 SSH 隧道 本地 远程 动态 SOCKS 转发 port forwarding ssh tunnel local remote", + ), + L10nKey::SettingsSearchProgramKeywords => ( + "shell binary zsh bash fish nu nushell pwsh powershell executable launch", + "程序 shell 二进制 zsh bash fish nu nushell pwsh powershell 可执行文件 启动 program shell binary launch", + ), + L10nKey::SettingsSearchRememberWindowSizeKeywords => ( + "window size position bounds geometry launch startup remember", + "记住窗口大小位置 窗口 大小 位置 启动 记住 remember window size position geometry", + ), + L10nKey::SettingsSearchReportMouseToAppsKeywords => ( + "mouse reporting vim tmux click scroll shift passthrough", + "鼠标报告 鼠标 vim tmux 点击 滚动 shift report mouse apps", + ), + L10nKey::SettingsSearchRestoreLastLayoutKeywords => ( + "restore session previous tabs splits reopen launch startup layout", + "恢复上次布局 恢复 会话 标签页 分屏 布局 restore last layout tabs splits", + ), + L10nKey::SettingsSearchScrollSpeedKeywords => ( + "mouse wheel multiplier scrolling", + "滚动速度 鼠标滚轮 滚动倍率 scroll speed mouse wheel multiplier scrolling", + ), + L10nKey::SettingsSearchScrollbackKeywords => ( + "history buffer lines scroll", + "回滚 历史 缓冲区 行数 scrollback history buffer lines", + ), + L10nKey::SettingsSearchShowTrayIconKeywords => ( + "tray menu bar status item agent attention system icon", + "显示托盘图标 托盘 菜单栏 状态 图标 show tray icon menu bar status", + ), + L10nKey::SettingsSearchSidebarGroupingKeywords => ( + "tabs group repo repository git scratch header sidebar flat", + "侧栏分组 标签页 分组 仓库 git 侧栏 sidebar grouping tabs repo repository", + ), + L10nKey::SettingsSearchSmartSelectionKeywords => ( + "double click word url path select semantic bracket email", + "智能选择 双击 选择 单词 URL 路径 邮箱 括号 smart selection double click", + ), + L10nKey::SettingsSearchStartInKeywords => ( + "cwd working directory start folder path home inherit custom", + "起始目录 工作目录 启动目录 主目录 继承 自定义 cwd working directory start home inherit custom", + ), + L10nKey::SettingsSearchSyncWithSystemKeywords => ( + "theme dark light auto follow os appearance mode", + "主题 跟随系统 自动 深色 浅色 外观 模式 theme dark light auto follow system", + ), + L10nKey::SettingsSearchTabBarPositionKeywords => ( + "tabs vertical sidebar left top layout rail", + "标签栏位置 标签栏 侧边栏 左侧 顶部 布局 tab bar position tabs sidebar left top", + ), + L10nKey::SettingsSearchTabCompletionKeywords => ( + "complete completion menu suggestions tab prompt", + "Tab补全 补全 菜单 建议 tab completion suggestions prompt", + ), + L10nKey::SettingsSearchTerminalBellKeywords => ( + "bell audible visual flash sound silence beep ^g", + "终端铃声 铃声 提示音 闪烁 静音 beep bell terminal audible visual", + ), + L10nKey::SettingsSearchThemeKeywords => ( + "appearance color colours scheme dark light palette background foreground accent sync system os auto follow", + "外观 颜色 主题 配色 深色 浅色 背景 前景 强调色 跟随系统 appearance color scheme dark light palette", + ), + L10nKey::SettingsSearchTrimTrailingSpacesKeywords => ( + "clipboard whitespace copy", + "复制时去除空格 去除末尾空格 剪贴板 空白 trim trailing spaces copy whitespace", + ), + L10nKey::SettingsSearchVerifyHostKeysKeywords => ( + "ssh security known_hosts fingerprint mitm host key verification", + "校验主机密钥 主机密钥 known_hosts 指纹 mitm 安全 verification ssh host keys", + ), + L10nKey::SettingsSearchWarnBeforeClosingKeywords => ( + "ssh confirm close tab pane live session security", + "关闭前警告 确认关闭 SSH 标签页 窗格 会话 warn before closing ssh confirm", + ), + L10nKey::SettingsSearchStartupWindowKeywords => ( + "launch open maximized fullscreen normal", + "启动窗口 启动 最大化 全屏 普通 startup window launch maximized fullscreen normal", + ), + L10nKey::SwitcherNoMatch => ( + "No workspace or machine matches.", + "没有匹配的工作区或机器。", + ), + L10nKey::AddSshHost => ("Add SSH Host…", "添加 SSH 主机…"), + L10nKey::ClickForNewWindow => ("click for a new window", "点击打开新窗口"), + L10nKey::RestartServer => ("Restart Server", "重启服务器"), + L10nKey::OtherMachines => ("Other Machines", "其他机器"), + L10nKey::Ok => ("OK", "确定"), + L10nKey::SftpNoTransfers => ("No transfers yet.", "还没有传输任务。"), + L10nKey::SftpPanelTitleFiles => ("Files", "文件"), + L10nKey::SftpTooltipRefresh => ("Refresh", "刷新"), + L10nKey::SftpTooltipMore => ("More", "更多"), + L10nKey::SftpMenuNewFolder => ("New folder", "新建文件夹"), + L10nKey::SftpMenuNewFile => ("New file", "新建文件"), + L10nKey::SftpMenuUpload => ("Upload…", "上传…"), + L10nKey::SftpMenuGotoShellCwd => ("Go to shell directory", "转到 shell 目录"), + L10nKey::SftpMenuHideTransferHistory => ("Hide transfer history", "隐藏传输历史"), + L10nKey::SftpMenuTransferHistory => ("Transfer history", "传输历史"), + L10nKey::SftpEditNewFolder => ("New folder", "新建文件夹"), + L10nKey::SftpEditNewFile => ("New file", "新建文件"), + L10nKey::SftpEditRename => ("Rename", "重命名"), + L10nKey::SftpEditPermissions => ("Permissions · {mode}", "权限 · {mode}"), + L10nKey::SftpLoading => ("Loading…", "加载中…"), + L10nKey::SftpEmptyDirectory => ("Empty directory.", "空文件夹。"), + L10nKey::SftpContextOpen => ("Open", "打开"), + L10nKey::SftpContextFollowSymlink => ("Follow symlink", "跟随符号链接"), + L10nKey::SftpContextRename => ("Rename", "重命名"), + L10nKey::SftpContextChmod => ("chmod…", "权限…"), + L10nKey::SftpTransferSummaryRunning => { + ("{count} transferring · {pct}%", "{count} 个传输中 · {pct}%") + } + L10nKey::SftpTransferSummaryFailed => ("{count} failed", "{count} 个失败"), + L10nKey::SftpTransferSummaryIdle => ("Transfers", "传输"), + L10nKey::SftpTransferProgress => ("{done} / {total} ({pct}%)", "{done} / {total} ({pct}%)"), + L10nKey::SftpTransferDone => ("done", "完成"), + L10nKey::SftpTransferCancelled => ("cancelled", "已取消"), + L10nKey::SftpTransferError => ("error", "错误"), + L10nKey::ForwardPanelTitle => ("Forwards", "端口转发"), + L10nKey::ForwardDisconnected => ("Disconnected", "已断开"), + L10nKey::ForwardDisconnectedFrom => ("Disconnected from {host}", "与 {host} 的连接已断开"), + L10nKey::ForwardTooltipAdd => ("Add forward", "添加转发"), + L10nKey::ForwardTooltipRemove => ("Remove", "移除"), + L10nKey::ForwardLocal => ("Local", "本地"), + L10nKey::ForwardRemote => ("Remote", "远程"), + L10nKey::ForwardDynamic => ("Dynamic", "动态"), + L10nKey::ForwardBindLabel => ("bind", "绑定"), + L10nKey::ForwardToLabel => ("to", "到"), + L10nKey::ForwardSocksLabel => ("SOCKS", "SOCKS"), + L10nKey::ForwardAdd => ("Add", "添加"), + L10nKey::FileTreePlaceholderFileName => ("file name", "文件名"), + L10nKey::FileTreePlaceholderFolderName => ("folder name", "文件夹名"), + L10nKey::FileTreePlaceholderNewName => ("new name", "新名称"), + L10nKey::FileTreeDeleteTitle => ("Delete \"{name}\"?", "删除\"{name}\"?"), + L10nKey::FileTreeDeleteFolderBody => ( + "The folder and everything inside it will be deleted.", + "该文件夹及其中的所有内容都将被删除。", + ), + L10nKey::FileTreeDeleteFileBody => ("The file will be deleted.", "该文件将被删除。"), + L10nKey::FileTreeDeleteFailed => ("Delete failed", "删除失败"), + L10nKey::FileTreeContextOpen => ("Open", "打开"), + L10nKey::FileTreeContextCdHere => ("cd Here", "在此处 cd"), + L10nKey::FileTreeContextInsertPath => ("Insert Path in Terminal", "在终端中插入路径"), + L10nKey::FileTreeContextAttachAgent => ("Attach to Agent", "附加到智能体"), + L10nKey::FileTreeContextNewFile => ("New File", "新建文件"), + L10nKey::FileTreeContextNewFolder => ("New Folder", "新建文件夹"), + L10nKey::FileTreeContextRename => ("Rename", "重命名"), + L10nKey::FileTreeContextCopyPath => ("Copy Path", "复制路径"), + L10nKey::FileTreeContextHideDotfiles => ("Hide Dotfiles", "隐藏点文件"), + L10nKey::FileTreeContextShowDotfiles => ("Show Dotfiles", "显示点文件"), + L10nKey::SshPromptNewKey => ("new {fingerprint}", "新 {fingerprint}"), + L10nKey::SshPromptOldKey => ("old {old_fingerprint}", "旧 {old_fingerprint}"), + L10nKey::EditorCantOpen => ("Can't open {path}: {e}", "无法打开 {path}:{e}"), + L10nKey::EditorCantRead => ("Can't read {path}: {e}", "无法读取 {path}:{e}"), + L10nKey::EditorNotUtf8 => ( + "\"{path}\" is not valid UTF-8", + "\"{path}\" 不是有效的 UTF-8", + ), + L10nKey::EditorSaveFailed => ("Save failed", "保存失败"), + L10nKey::EditorUnsavedChanges => ( + "\"{name}\" has unsaved changes", + "\"{name}\" 有未保存的更改", + ), + L10nKey::EditorDiscard => ("Discard", "放弃"), + L10nKey::EditorNoFileOpen => ("No file open", "没有打开的文件"), + L10nKey::EditorBackToTerminal => ("Back to Terminal (Esc)", "返回终端 (Esc)"), + L10nKey::EditorLnCol => ("Ln {line}, Col {column}", "行 {line}, 列 {column}"), + L10nKey::EditorEdit => ("Edit", "编辑"), + L10nKey::EditorPreview => ("Preview", "预览"), + L10nKey::EditorWrapOn => ("Wrap: on", "自动换行:开"), + L10nKey::EditorWrapOff => ("Wrap: off", "自动换行:关"), + L10nKey::EditorFileTooLarge => ( + "\"{path}\" is too large for the editor ({size} MB)", + "\"{path}\" 太大,无法在编辑器中打开({size} MB)", + ), + L10nKey::EditorBinaryFile => ( + "\"{path}\" looks like a binary file", + "\"{path}\" 看起来是二进制文件", + ), + L10nKey::PanelInfoTitle => ("Info", "信息"), + L10nKey::PanelOutlineTitle => ("Outline", "大纲"), + L10nKey::PanelChangesTitle => ("Changes", "变更"), + L10nKey::PanelFilesTitle => ("Files", "文件"), + L10nKey::PanelNoSession => ("No active session.", "没有活动会话。"), + L10nKey::PanelNoSessionHint => ( + "Open a tab to see its shell, directory, and processes here.", + "打开一个标签页以在此处查看其 shell、目录和进程。", + ), + L10nKey::PanelNoCommands => ( + "No commands recorded for this pane.", + "此窗格没有记录命令。", + ), + L10nKey::PanelNoCommandsHint => ( + "Run a command — shell integration marks each one so you can jump back to it.", + "运行命令——shell 集成会标记每个命令,方便你跳回。", + ), + L10nKey::PanelNoWorkingDirectory => ("No working directory.", "没有工作目录。"), + L10nKey::PanelNoWorkingDirectoryHint => ( + "This pane has not reported one yet.", + "此窗格尚未报告工作目录。", + ), + L10nKey::PanelLoading => ("Loading…", "加载中…"), + L10nKey::PanelNotAGitRepo => ("Not a git repository.", "不是 git 仓库。"), + L10nKey::PanelNotAGitRepoHint => ( + "cd into one and this tab lists its uncommitted changes.", + "进入 git 仓库后,此标签页会列出未提交的变更。", + ), + L10nKey::PanelNoChanges => ("No uncommitted changes.", "没有未提交的变更。"), + L10nKey::PanelNoChangesHint => ("The working tree is clean.", "工作树是干净的。"), + L10nKey::PanelSessionSubtitle => ("Session", "会话"), + L10nKey::PanelProcessesSubtitle => ("Processes", "进程"), + L10nKey::PanelPortsSubtitle => ("Ports", "端口"), + L10nKey::PanelCwd => ("cwd", "工作目录"), + L10nKey::PanelShell => ("shell", "shell"), + L10nKey::PanelSsh => ("ssh", "ssh"), + L10nKey::PanelBranch => ("branch", "分支"), + L10nKey::PanelChangesRow => ("changes", "变更"), + L10nKey::PanelAgent => ("agent", "智能体"), + L10nKey::PanelAgentIdle => ("idle", "空闲"), + L10nKey::PanelAgentWorking => ("working", "进行中"), + L10nKey::PanelAgentWaiting => ("waiting", "等待中"), + L10nKey::PanelAgentDone => ("done", "已完成"), + L10nKey::PanelRevealInFinder => ("Reveal in Finder", "在 Finder 中显示"), + L10nKey::PanelOpenFolder => ("Open Folder", "打开文件夹"), + L10nKey::WindowStop => ("Stop", "停止"), + L10nKey::WindowDelete => ("Delete", "删除"), + L10nKey::WindowThisWorkspace => ("this workspace", "此工作区"), + L10nKey::WindowConfirmTitle => ("{verb} Workspace \"{name}\"?", "{verb}工作区\"{name}\"?"), + L10nKey::WindowStopUnreachable => ( + "Its machine could not be reached. Any shells still running there will be ended.", + "无法连接到其所在机器。仍在运行的 shell 将会被终止。", + ), + L10nKey::WindowDeleteUnreachable => ( + "Its machine could not be reached. Any shells still running there will be ended, and the layout forgotten.", + "无法连接到其所在机器。仍在运行的 shell 将会被终止,布局也将被清除。", + ), + L10nKey::WindowStopShells => ( + "{count} running shells will be ended.", + "{count} 个正在运行的 shell 将会被终止。", + ), + L10nKey::WindowDeleteShells => ( + "{count} running shells will be ended and the layout forgotten.", + "{count} 个正在运行的 shell 将会被终止,布局也将被清除。", + ), + L10nKey::DiffReading => ("Reading diff…", "正在读取差异…"), + L10nKey::DiffNotARepo => ("Not a git repository", "不是 git 仓库"), + L10nKey::DiffReadFailed => ( + "Couldn't read the working-tree diff — retrying on the next refresh.", + "无法读取工作树差异——下次刷新时重试。", + ), + L10nKey::DiffWorkingTreeClean => ("Working tree clean", "工作树干净"), + L10nKey::DiffCloseTooltip => ("Close Diff (Esc)", "关闭差异 (Esc)"), + L10nKey::DiffChangedFiles => ("{count} changed files", "{count} 个变更文件"), + L10nKey::DiffUntrackedCount => (" · {count} untracked", " · {count} 个未跟踪文件"), + L10nKey::DiffMoreFiles => ( + "… and {count} more changed files — run `git diff` in the terminal to see them.", + "…还有 {count} 个变更文件——在终端中运行 `git diff` 查看。", + ), + L10nKey::DiffOversizedNotice => ( + "This working tree is too large to render efficiently ({summary}). Every file is collapsed — expand individual files, or run `git diff` in the terminal.", + "此工作树太大,无法高效渲染({summary})。每个文件都已折叠——可展开单个文件,或在终端中运行 `git diff`。", + ), + L10nKey::DiffTruncatedPerFile => ( + "Diff truncated at {limit} lines — run `git diff` in the terminal for the rest.", + "差异在 {limit} 行处截断——在终端中运行 `git diff` 查看其余部分。", + ), + L10nKey::DiffTruncatedBudget => ( + "Body not loaded — this working tree is past tty7's diff budget. Run `git diff` in the terminal for this file.", + "内容未加载——此工作树已超出 tty7 的差异预算。在终端中运行 `git diff` 查看此文件。", + ), + L10nKey::DiffUntrackedHeader => ("Untracked files ({count})", "未跟踪文件 ({count})"), + L10nKey::DiffMoreUntracked => ( + "… and {count} more — run `git status` in the terminal to see them.", + "…还有 {count} 个——在终端中运行 `git status` 查看。", + ), + L10nKey::DiffLines => ("{count} diff lines", "{count} 行差异"), + L10nKey::DiffChangedLines => ( + "{total} changed lines, {loaded} diff rows loaded before {cap} cut the rest", + "{total} 行变更,在 {cap} 截断前已加载 {loaded} 行差异", + ), + L10nKey::DiffBudgetAndCap => ( + "tty7's budget and the per-file cap", + "tty7 的预算和单文件上限", + ), + L10nKey::DiffBudget => ("tty7's budget", "tty7 的预算"), + L10nKey::DiffPerFileCap => ("the per-file cap", "单文件上限"), + L10nKey::DiffUntrackedSummary => ("{count} untracked", "{count} 个未跟踪"), + L10nKey::PendingConnecting => ("Connecting to {machine}…", "正在连接 {machine}…"), + L10nKey::PendingUnreachable => ("Couldn't reach {machine}", "无法连接到 {machine}"), + L10nKey::WorktreePromptNeedsName => ("The worktree needs a name", "工作区需要一个名称"), + L10nKey::WorktreePromptTitle => ("New Worktree Tab", "新建工作区标签页"), + L10nKey::WorktreePromptName => ("Worktree Name", "工作区名称"), + L10nKey::WorktreePromptBranch => ("New Branch", "新分支"), + L10nKey::WorktreePromptBase => ("Start From", "起始分支"), + L10nKey::WorktreePromptCreating => ("Creating…", "正在创建…"), + L10nKey::WorktreePromptCreate => ("Create", "创建"), + L10nKey::AppNewWorktreeFailed => { + ("New worktree failed: {error}", "新建工作区失败:{error}") + } + L10nKey::HomeTimeJustNow => ("just now", "刚刚"), + L10nKey::HomeTimeMinutesAgo => ("{count} min ago", "{count} 分钟前"), + L10nKey::HomeTimeHourAgo => ("1 hour ago", "1 小时前"), + L10nKey::HomeTimeHoursAgo => ("{count} hours ago", "{count} 小时前"), + L10nKey::HomeTimeYesterday => ("yesterday", "昨天"), + L10nKey::HomeTimeDaysAgo => ("{count} days ago", "{count} 天前"), + L10nKey::HomeTimeOverWeekAgo => ("over a week ago", "超过一周"), + L10nKey::HomeReopenNamed => ("Reopen \"{name}\"", "重新打开\"{name}\""), + L10nKey::RemoteStripDisconnected => ("Not connected to {machine}", "未连接到 {machine}"), + L10nKey::RemoteStripConnecting => ("Connecting to {machine}…", "正在连接 {machine}…"), + L10nKey::RemoteStripReconnecting => { + ("Reconnecting to {machine}…", "正在重新连接 {machine}…") + } + L10nKey::RemoteStripReconnectingAttempt => ( + "Reconnecting to {machine}… (attempt {count})", + "正在重新连接 {machine}…(第 {count} 次尝试)", + ), + L10nKey::RemoteStripPreempted => ( + "This workspace was opened on {by}", + "此工作区已在 {by} 上打开", + ), + L10nKey::RemoteStripFailed => ( + "Not connected to {machine} — {error}", + "未连接到 {machine} — {error}", + ), + L10nKey::RemoteNoticePreempted => ( + "Opened elsewhere — typing has no effect", + "已在别处打开 — 输入无效", + ), + L10nKey::RemoteNoticeDisconnected => { + ("Not connected — typing has no effect", "未连接 — 输入无效") + } + L10nKey::RemoteActionRetryNow => ("Retry Now", "立即重试"), + L10nKey::RemoteActionTakeBack => ("Take Back", "夺回"), + L10nKey::RemoteActionConnect => ("Connect", "连接"), + L10nKey::RemoteActionRetry => ("Retry", "重试"), + L10nKey::RemoteNoConnectionDetails => ( + "This window is a workspace on {machine}, but tty7 has no connection \ + details for it any more — check that its SSH profile or ~/.ssh/config \ + entry still exists.", + "此窗口是 {machine} 上的工作区,但 tty7 已没有它的连接详情 — \ + 请检查其 SSH 配置文件或 ~/.ssh/config 条目是否仍然存在。", + ), + L10nKey::RemoteThisComputer => ("this computer", "本机"), + L10nKey::RemoteRestartTitle => ( + "Restart tty7's server on \"{machine}\"?", + "重启 \"{machine}\" 上的 tty7 服务器?", + ), + L10nKey::RemoteRestartBody => ( + "This stops every shell on {machine} — anything still running in them \ + will be terminated, including shells this window is not showing. \ + Workspaces and layouts are kept and come back with fresh shells.", + "这将停止 {machine} 上的所有 shell — 其中仍在运行的任何内容都会被终止,\ + 包括此窗口未显示的 shell。工作区和布局会被保留,并以全新的 shell 恢复。", + ), + L10nKey::RemoteReplaceBody => ( + "The tty7-server running on {machine} speaks a protocol this client \ + cannot. tty7 will restart the service there onto one that does, installing it \ + first if {machine} does not already have it.\n\ + \n\ + Every session running on {machine} ends, including any this window is not \ + connected to.", + "{machine} 上运行的 tty7-server 使用了此客户端无法识别的协议。\ + tty7 会在该机器上重启为可识别的服务,如果 {machine} 尚未安装则会先安装。\n\ + \n\ + {machine} 上运行的所有会话都会结束,包括此窗口未连接的会话。", + ), + L10nKey::RemoteRestartFailedTitle => ( + "tty7's server on \"{machine}\" was not restarted", + "\"{machine}\" 上的 tty7 服务器未被重启", + ), + L10nKey::RemoteRestartFailedBody => ( + "{error}\n\ + \n\ + Sessions still running there are on the older build. If they are \ + gone, reconnecting starts this build's server.", + "{error}\n\ + \n\ + 仍在运行的会话仍位于旧版本上。如果它们已消失,重新连接将启动此版本的服务器。", + ), + L10nKey::RemoteHostUnreachable => ( + "could not reach {machine}: {error}", + "无法连接到 {machine}:{error}", + ), + L10nKey::RemoteInstallTitle => ( + "Install tty7's server on \"{machine}\"?", + "在 \"{machine}\" 上安装 tty7 服务器?", + ), + L10nKey::RemoteInstallDetail => ( + "tty7 will write its server binary to {machine} so this machine can host \ + workspaces there. Nothing else on {machine} is touched, and no sudo is used.\n\ + \n\ + {path_label}\u{2003}{path}\n\ + {version_label}\u{2003}{version}\n\ + {size_label}\u{2003}{size}\n\ + {from_label}\u{2003}{from}\n\ + {sha_label}\u{2003}{sha256}\n\ + \n\ + {silent_upgrades}", + "tty7 会将其服务器二进制文件写入 {machine},以便本机可以在那里托管\ + 工作区。{machine} 上的其他内容不会被修改,也不会使用 sudo。\n\ + \n\ + {path_label}\u{2003}{path}\n\ + {version_label}\u{2003}{version}\n\ + {size_label}\u{2003}{size}\n\ + {from_label}\u{2003}{from}\n\ + {sha_label}\u{2003}{sha256}\n\ + \n\ + {silent_upgrades}", + ), + L10nKey::RemoteInstallPathLabel => ("Path", "路径"), + L10nKey::RemoteInstallVersionLabel => ("Version", "版本"), + L10nKey::RemoteInstallSizeLabel => ("Size", "大小"), + L10nKey::RemoteInstallFromLabel => ("From", "来源"), + L10nKey::RemoteInstallShaLabel => ("SHA-256", "SHA-256"), + L10nKey::RemoteInstallSilentUpgrades => ( + "Later upgrades on this machine install silently.", + "此后在该机器上的升级将静默安装。", + ), + L10nKey::RemoteInstallBytes => ("bytes", "字节"), + L10nKey::RemoteMismatchTitle => ( + "Restart tty7's server on \"{machine}\"?", + "重启 \"{machine}\" 上的 tty7 服务器?", + ), + L10nKey::RemoteMismatchDetail => ( + "{machine} is serving tty7 sessions from {running}, which speaks a protocol \ + this client ({wanted}) cannot. tty7 has installed a matching server there, \ + but the one already running is the one your sessions are on.\n\ + \n\ + {restart_server}\u{2003}starts {wanted} there and ends every session it is hosting.\n\ + {cancel}\u{2003}leaves {machine} exactly as it is. This window will not connect.", + "{machine} 正在使用 {running} 提供 tty7 会话,该版本使用的协议无法被\ + 此客户端({wanted})识别。tty7 已在那里安装了匹配的服务器,\ + 但正在运行的是你当前会话所在的版本。\n\ + \n\ + {restart_server}\u{2003}会在该机器上启动 {wanted} 并结束其托管的所有会话。\n\ + {cancel}\u{2003}会保持 {machine} 现状不变。此窗口将不会连接。", + ), + L10nKey::RemoteMismatchUnknownBuild => ("an unknown build", "未知构建"), + L10nKey::RemoteMismatchUnknownBuildFromExe => { + ("an unknown build (from {exe})", "未知构建(来自 {exe})") + } + L10nKey::RemoteDaemonStartFailed => ( + "tty7's local server could not be started: {error}", + "无法启动 tty7 本地服务器:{error}", + ), + L10nKey::RemoteDaemonUnreachable => ( + "could not reach tty7's local server: {error}", + "无法连接到 tty7 本地服务器:{error}", + ), + L10nKey::RemoteDaemonTooOld => ( + "this machine's tty7 daemon is an older build and cannot restart the server on \ + {machine}. Quit tty7 (which stops the daemon) and open it again, then retry.", + "此机器上的 tty7 守护进程版本较旧,无法重启 {machine} 上的服务器。\ + 请退出 tty7(这会停止守护进程)并重新打开,然后重试。", + ), + L10nKey::RemoteProfileMissing => ( + "that saved SSH profile no longer exists", + "该已保存的 SSH 配置文件已不存在", + ), + L10nKey::RemoteAliasMissing => ( + "`{alias}` is no longer in ~/.ssh/config", + "`{alias}` 已不再位于 ~/.ssh/config 中", + ), + L10nKey::RemoteWslNoSsh => ( + "a WSL workspace has no SSH connection", + "WSL 工作区没有 SSH 连接", + ), + L10nKey::RemoteLocalStdioNoSsh => ( + "a local --stdio workspace has no SSH connection", + "本地 --stdio 工作区没有 SSH 连接", + ), + L10nKey::RemoteHostNotTty7 => ( + "{machine} answered, but not as a tty7 server: {error}", + "{machine} 已响应,但并非作为 tty7 服务器:{error}", + ), + L10nKey::RemoteWorkspaceListFailed => ( + "connected to {machine}, but its workspace list failed: {error}", + "已连接到 {machine},但其工作区列表获取失败:{error}", + ), + L10nKey::RemoteServerRestartFailed => ( + "could not restart tty7's server on {machine}: {error}", + "无法重启 {machine} 上的 tty7 服务器:{error}", + ), + L10nKey::RemoteNoRouteToHost => ( + "tty7 no longer has a way to reach {machine}", + "tty7 已无法到达 {machine}", + ), + L10nKey::RemoteMachineTreeUnexpectedReply => ( + "the server answered a machine tree with {reply}", + "服务器用 {reply} 回复了机器树请求", + ), + L10nKey::RemoteMismatchVersionFromExe => { + ("{version} (from {exe})", "{version}(来自 {exe})") + } + L10nKey::AppNoRunningCodingAgent => ( + "No running coding agent found — start one (claude, codex, …) in a pane first.", + "未找到运行中的编码智能体——请先在某个窗格中启动一个(claude、codex 等)。", + ), + L10nKey::SwitcherThisComputer => ("This Computer", "本机"), + L10nKey::SwitcherRestartingServer => ("Restarting tty7's server…", "正在重启 tty7 服务器…"), + L10nKey::SwitcherDownloadingServerWithTotal => ( + "Downloading tty7's server… {done} / {total}", + "正在下载 tty7 服务器… {done} / {total}", + ), + L10nKey::SwitcherDownloadingServerNoTotal => ( + "Downloading tty7's server… {done}", + "正在下载 tty7 服务器… {done}", + ), + L10nKey::SwitcherCopyingServer => ( + "Copying tty7's server… {done} / {total}", + "正在复制 tty7 服务器… {done} / {total}", + ), + L10nKey::SwitcherThisWindow => ("this window", "当前窗口"), + L10nKey::SwitcherOpen => ("open", "已打开"), + L10nKey::SwitcherDisconnect => ("Disconnect", "断开连接"), + L10nKey::SwitcherOpenInNewWindow => ("Open in New Window", "在新窗口中打开"), + L10nKey::SwitcherRename => ("Rename…", "重命名…"), + L10nKey::SshPromptPasswordFor => ("Password for {user}@{host}", "{user}@{host} 的密码"), + L10nKey::SshPromptPassphraseFor => ("Passphrase for {key_path}", "{key_path} 的密码短语"), + L10nKey::SshPromptTwoFactor => ("Two-factor authentication", "双因素认证"), + L10nKey::SshPromptUnknownHost => ("Unknown host {host}", "未知主机 {host}"), + L10nKey::SshPromptHostKeyChanged => ( + "Host key CHANGED — possible man-in-the-middle", + "主机密钥已更改——可能存在中间人攻击", + ), + L10nKey::SshPromptHostKeyChangedBody => ( + "The host key differs from the one previously trusted. This may be an attack.", + "主机密钥与之前信任的密钥不同,这可能是一次攻击。", + ), + L10nKey::SshPromptConnect => ("Connect", "连接"), + L10nKey::SshPromptUnlock => ("Unlock", "解锁"), + L10nKey::SshPromptSubmit => ("Submit", "提交"), + L10nKey::HostOpsError => ("{context}: {error}", "{context}:{error}"), + L10nKey::CmdGroupTabsPanes => ("Tabs & Panes", "标签页与窗格"), + L10nKey::CmdGroupWorkspaces => ("Workspaces", "工作区"), + L10nKey::CmdGroupView => ("View", "视图"), + L10nKey::CmdGroupTerminal => ("Terminal", "终端"), + L10nKey::CmdGroupSsh => ("SSH", "SSH"), + L10nKey::CmdGroupAgents => ("Agents", "智能体"), + L10nKey::CmdGroupApplication => ("Application", "应用"), + L10nKey::CmdNewTab => ("New Tab", "新标签页"), + L10nKey::CmdNewWorktreeTab => ("New Worktree Tab", "新工作树标签页"), + L10nKey::CmdNewWorktreeTabSubtitle => ( + "isolated checkout on a fresh branch", + "在全新分支上独立检出", + ), + L10nKey::CmdRenameTab => ("Rename Tab…", "重命名标签页…"), + L10nKey::CmdSplitRight => ("Split Right", "向右分屏"), + L10nKey::CmdSplitDown => ("Split Down", "向下分屏"), + L10nKey::CmdZoomPane => ("Zoom Pane", "缩放窗格"), + L10nKey::CmdNextPane => ("Next Pane", "下一窗格"), + L10nKey::CmdPreviousPane => ("Previous Pane", "上一窗格"), + L10nKey::CmdFocusPaneLeft => ("Focus Pane Left", "聚焦左侧窗格"), + L10nKey::CmdFocusPaneRight => ("Focus Pane Right", "聚焦右侧窗格"), + L10nKey::CmdFocusPaneUp => ("Focus Pane Up", "聚焦上方窗格"), + L10nKey::CmdFocusPaneDown => ("Focus Pane Down", "聚焦下方窗格"), + L10nKey::CmdResizePaneLeft => ("Resize Pane Left", "向左调整窗格"), + L10nKey::CmdResizePaneRight => ("Resize Pane Right", "向右调整窗格"), + L10nKey::CmdResizePaneUp => ("Resize Pane Up", "向上调整窗格"), + L10nKey::CmdResizePaneDown => ("Resize Pane Down", "向下调整窗格"), + L10nKey::CmdSwapPaneNext => ("Swap Pane Next", "与下一窗格交换"), + L10nKey::CmdSwapPanePrevious => ("Swap Pane Previous", "与上一窗格交换"), + L10nKey::CmdNextTab => ("Next Tab", "下一标签页"), + L10nKey::CmdPreviousTab => ("Previous Tab", "上一标签页"), + L10nKey::CmdCopyWorkingDirectory => ("Copy Working Directory", "复制工作目录"), + L10nKey::CmdCopySessionId => ("Copy Session ID", "复制会话 ID"), + L10nKey::CmdCopySessionIdSubtitle => ( + "the coding agent's own session id", + "编码智能体自身的会话 ID", + ), + L10nKey::CmdForkSession => ("Fork Session", "派生会话"), + L10nKey::CmdForkSessionSubtitle => ( + "branch this agent session into a new tab", + "将此智能体会话派生到新标签页", + ), + L10nKey::CmdMarkTabAsUnread => ("Mark Tab as Unread", "标记为未读"), + L10nKey::CmdClosePaneTab => ("Close Pane / Tab", "关闭窗格/标签页"), + L10nKey::CmdCloseOtherTabs => ("Close Other Tabs", "关闭其他标签页"), + L10nKey::CmdCloseTabsToTheRight => ("Close Tabs to the Right", "关闭右侧标签页"), + L10nKey::CmdReopenClosedTab => ("Reopen Closed Tab", "重新打开已关闭标签页"), + L10nKey::CmdNewWorkspace => ("New Workspace", "新建工作区"), + L10nKey::CmdSwitchWorkspace => ("Switch Workspace…", "切换工作区…"), + L10nKey::CmdRenameWorkspace => ("Rename Workspace…", "重命名工作区…"), + L10nKey::CmdStopWorkspace => ("Stop Workspace…", "停止工作区…"), + L10nKey::CmdStopWorkspaceSubtitle => ( + "ends its shells, keeps the layout", + "结束其 shell,保留布局", + ), + L10nKey::CmdDeleteWorkspace => ("Delete Workspace…", "删除工作区…"), + L10nKey::CmdDeleteWorkspaceSubtitle => ( + "ends its shells and forgets the layout", + "结束其 shell,清除布局", + ), + L10nKey::CmdShowLeftSidebar => ("Show Left Sidebar", "显示左侧边栏"), + L10nKey::CmdHideLeftSidebar => ("Hide Left Sidebar", "隐藏左侧边栏"), + L10nKey::CmdHideRightPanel => ("Hide Right Panel", "隐藏右侧面板"), + L10nKey::CmdShowRightPanel => ("Show Right Panel", "显示右侧面板"), + L10nKey::CmdShowCodePanel => ("Show Code Panel", "显示代码面板"), + L10nKey::CmdTabBarMoveToTop => ("Tab Bar: Move to Top", "标签栏:移到顶部"), + L10nKey::CmdTabBarMoveToLeftSidebar => { + ("Tab Bar: Move to Left Sidebar", "标签栏:移到左侧边栏") + } + L10nKey::CmdRightPanelInfo => ("Right Panel: Info", "右侧面板:信息"), + L10nKey::CmdRightPanelOutline => ("Right Panel: Outline", "右侧面板:大纲"), + L10nKey::CmdRightPanelChanges => ("Right Panel: Changes", "右侧面板:变更"), + L10nKey::CmdRightPanelFiles => ("Right Panel: Files", "右侧面板:文件"), + L10nKey::CmdChangeTheme => ("Change Theme…", "更改主题…"), + L10nKey::CmdResetFontSize => ("Reset Font Size", "重置字号"), + L10nKey::CmdEnterFullScreen => ("Enter Full Screen", "进入全屏"), + L10nKey::CmdClearScrollback => ("Clear Scrollback", "清除回滚"), + L10nKey::CmdFindInTerminal => ("Find in Terminal…", "在终端中查找…"), + L10nKey::CmdFindNext => ("Find Next", "查找下一个"), + L10nKey::CmdFindPrevious => ("Find Previous", "查找上一个"), + L10nKey::CmdCopy => ("Copy", "复制"), + L10nKey::CmdCut => ("Cut", "剪切"), + L10nKey::CmdPaste => ("Paste", "粘贴"), + L10nKey::CmdSelectAll => ("Select All", "全选"), + L10nKey::CmdSshAddConnection => ("SSH: Add Connection…", "SSH:添加连接…"), + L10nKey::CmdSshManageProfiles => ("SSH: Manage Profiles…", "SSH:管理配置文件…"), + L10nKey::CmdSshReconnect => ("SSH: Reconnect", "SSH:重新连接"), + L10nKey::CmdSshRemoteFiles => ("SSH: Remote Files", "SSH:远程文件"), + L10nKey::CmdSshPortForwarding => ("SSH: Port Forwarding", "SSH:端口转发"), + L10nKey::CmdSshConnectWithInput => ("SSH: Connect {input}", "SSH:连接 {input}"), + L10nKey::CmdAgentSendSelection => ("Agent: Send Selection", "智能体:发送选区"), + L10nKey::CmdAgentSendSelectionSubtitle => ( + "selection → running coding agent", + "选区 → 运行中的编码智能体", + ), + L10nKey::CmdAgentSendGitDiffForReview => ( + "Agent: Send Git Diff for Review", + "智能体:发送 Git diff 以供审查", + ), + L10nKey::CmdAgentSendGitDiffSubtitle => ( + "git diff → running coding agent", + "git diff → 运行中的编码智能体", + ), + L10nKey::CmdSettings => ("Settings…", "设置…"), + L10nKey::CmdKeyboardShortcuts => ("Keyboard Shortcuts", "键盘快捷键"), + L10nKey::CmdAboutTty7 => ("About tty7", "关于 tty7"), + L10nKey::CmdCheckForUpdates => ("Check for Updates…", "检查更新…"), + L10nKey::CmdDocumentation => ("Documentation", "文档"), + L10nKey::CmdJoinDiscord => ("Join the Discord", "加入 Discord"), + L10nKey::CmdReportIssue => ("Report an Issue…", "报告问题…"), + L10nKey::CmdRestartServer => ("Restart Server…", "重启服务器…"), + L10nKey::CmdRestartServerSubtitle => ( + "ends every running shell; layout is kept", + "结束所有运行中的 shell;保留布局", + ), + L10nKey::CmdQuitTty7 => ("Quit tty7", "退出 tty7"), + L10nKey::CmdQuitTty7Subtitle => ("shells keep running", "shell 保持运行"), + L10nKey::CmdQuickConnect => ("Connect to \"{target}\"", "连接到 \"{target}\""), + L10nKey::CmdQuickConnectSaveProfile => ( + "Save \"{target}\" as profile…", + "将 \"{target}\" 保存为配置文件…", + ), + L10nKey::CmdRecent => ("Recent", "最近使用"), + L10nKey::AppRestartServerTitle => ("Restart Server?", "重启服务器?"), + L10nKey::AppRestartServerMismatchDetail => ( + "The server holding your shells is from another build (v{build}, protocol {protocol} — this app speaks {ours}). You can keep using it and your shells stay, but features whose wire format changed may misbehave until it's restarted. Restarting starts a clean server: tabs reopen with fresh shells and anything running in them is terminated.", + "保存你 shell 的服务器来自另一个构建(v{build},协议 {protocol} — 此应用使用 {ours})。你可以继续使用,shell 也会保留,但协议格式已变更的功能可能会表现异常,直到重启服务器。重启会启动一个干净的服务器:标签页会以全新的 shell 重新打开,其中正在运行的所有内容都会被终止。", + ), + L10nKey::AppRestartServerOldDetail => ( + "The server holding your shells is from an older version of the app. You can keep using it and your shells stay, but newer features may misbehave until it's restarted. Restarting starts a clean server: tabs reopen with fresh shells and anything running in them is terminated.", + "保存你 shell 的服务器来自应用的旧版本。你可以继续使用,shell 也会保留,但新功能可能会表现异常,直到重启服务器。重启会启动一个干净的服务器:标签页会以全新的 shell 重新打开,其中正在运行的所有内容都会被终止。", + ), + L10nKey::AppKeepShells => ("Keep Shells", "保留 Shell"), + L10nKey::AppRestart => ("Restart", "重启"), + L10nKey::AppRestartServerNotSsh => ( + "tty7 can only restart the server on machines it reaches over SSH. {label} is served from this computer — stop its workspace instead.", + "tty7 只能通过 SSH 重启它能连接到的机器上的服务器。{label} 由本机提供服务 — 请改为停止其工作区。", + ), + L10nKey::AppRestartServerBody => ( + "This stops every running shell on this computer — anything still running in them will be terminated. Your tabs and layout are kept and reopened with fresh shells.", + "这会停止本机上所有正在运行的 shell — 其中仍在运行的任何内容都会被终止。你的标签页和布局会被保留,并以全新的 shell 重新打开。", + ), + L10nKey::AppWorktreeRemoveDetailDirty => ( + "The closed tab's worktree at {path} has uncommitted changes.", + "位于 {path} 的已关闭标签页的工作区有未提交的更改。", + ), + L10nKey::AppWorktreeRemoveDetailClean => ( + "The closed tab's worktree at {path} is clean.", + "位于 {path} 的已关闭标签页的工作区是干净的。", + ), + L10nKey::AppWorktreeRemoveTitle => { + ("Remove worktree \"{branch}\"?", "删除工作区\"{branch}\"?") + } + L10nKey::AppWorktreeDiscardAndRemove => ("Discard Changes & Remove", "放弃更改并删除"), + L10nKey::AppWorktreeRemove => ("Remove Worktree", "删除工作区"), + L10nKey::AppWorktreeKeep => ("Keep", "保留"), + L10nKey::AppReopenTabFailed => ( + "Could not reopen the tab: no terminal started", + "无法重新打开标签页:没有启动终端", + ), + L10nKey::AppOpenTerminalFailed => ( + "Could not open a terminal: {error}", + "无法打开终端:{error}", + ), + L10nKey::AppSshConnectionFailed => { + ("SSH connection failed: {error}", "SSH 连接失败:{error}") + } + L10nKey::AppSshReconnectFailed => { + ("SSH reconnect failed: {error}", "SSH 重新连接失败:{error}") + } + L10nKey::AppSplitPaneFailed => { + ("Could not split the pane: {error}", "无法拆分窗格:{error}") + } + L10nKey::AppWorktreeRemoved => { + ("Removed worktree \"{branch}\"", "已删除工作区\"{branch}\"") + } + L10nKey::AppWorktreeRemoveFailed => ( + "Worktree removal failed: {error}", + "删除工作区失败:{error}", + ), + L10nKey::AppForkStillConnecting => ( + "Could not fork: the pane is still connecting", + "无法派生:窗格仍在连接中", + ), + L10nKey::AppPaneNoCodingAgent => ( + "This pane isn't running a coding agent", + "此窗格未运行编码智能体", + ), + L10nKey::AppForkNoCommand => ( + "tty7 has no fork command for {name}", + "tty7 没有用于 {name} 的派生命令", + ), + L10nKey::AppForkLocalOnly => ( + "{name} sessions can only be forked from a local pane", + "{name} 会话只能从本地窗格派生", + ), + L10nKey::AppForkNoSessionId => ( + "tty7 hasn't seen a {name} session id in this pane — install its hooks in Settings → Agents", + "tty7 尚未在此窗格中看到 {name} 的会话 ID — 请在设置 → 智能体中安装其钩子", + ), + L10nKey::AppForkSessionIdNotToken => ( + "{name}'s session id isn't a plain token", + "{name} 的会话 ID 不是普通令牌", + ), + L10nKey::AppForkMidTurn => ( + "{name} is mid-turn — the fork won't include the turn in flight", + "{name} 正在处理中 — 派生不会包含进行中的这一轮", + ), + L10nKey::AppTabNoWorkingDirectory => ( + "This tab has no working directory yet", + "此标签页还没有工作目录", + ), + L10nKey::AppNothingSelected => ( + "Nothing selected — select some terminal output first.", + "未选择任何内容 — 请先选择一些终端输出。", + ), + L10nKey::AppPaneNoKnownDirectory => ( + "This pane has no known directory.", + "此窗格没有已知的目录。", + ), + L10nKey::AppNoUncommittedChanges => ( + "No uncommitted changes in {cwd} (or not a git repository).", + "{cwd} 中没有未提交的更改(或不是 git 仓库)。", + ), + L10nKey::AppCmdSshProfileTitle => ("SSH: {title}", "SSH:{title}"), + L10nKey::AppCmdSwitchToTab => ("Switch to Tab: {label}", "切换到标签页:{label}"), + L10nKey::AppPlaceholderDescription => ("description", "描述"), + L10nKey::AppPlaceholderSshQuickConnect => ( + "user@host or user@host:port", + "user@host 或 user@host:port", + ), + L10nKey::AppPlaceholderLoginShell => ("login shell", "登录 shell"), + L10nKey::AppPlaceholderNone => ("none", "无"), + L10nKey::AppPlaceholderOpenInDefaultApp => ("open in default app", "在默认应用中打开"), + L10nKey::AppThemeColorBackground => ("Background", "背景"), + L10nKey::AppThemeColorForeground => ("Foreground", "前景"), + L10nKey::AppThemeColorAccent => ("Accent", "强调色"), + L10nKey::AppThemeColorCursor => ("Cursor", "光标"), + L10nKey::AppThemeColorSelection => ("Selection", "选区"), + L10nKey::AppAgentHooksThisComputer => ("This Computer", "本机"), + L10nKey::AppAgentHooksRemoteMachine => ("Remote machine", "远程机器"), + L10nKey::AppAgentHooksNoHomeDir => ( + "tty7 could not work out this computer's home directory, so there is nowhere to install to.", + "tty7 无法确定这台计算机的主目录,因此没有可安装的位置。", + ), + L10nKey::AppAgentHooksOffline => ( + "Not connected to this machine, so its agent config can't be read or written. Open a workspace on it and come back.", + "未连接到这台机器,因此无法读取或写入其智能体配置。请在其上打开一个工作区后再回来。", + ), + L10nKey::AppAgentHooksHomeDirUnresolved => { + ("cannot resolve home directory", "无法解析主目录") + } + L10nKey::AppAgentHooksOpFailed => ("Failed: {error}", "失败:{error}"), + L10nKey::AppKeybindingDisplacedNote => ( + "{action} took the shortcut from {previous}, which is now unset.", + "{action} 占用了 {previous} 的快捷键,{previous} 现在已被取消设置。", + ), + L10nKey::AppLocalServerName => ("the local server", "本地服务器"), + L10nKey::AppSshParseUnbalancedQuotes => ( + "Unbalanced quotes in the SSH command", + "SSH 命令中的引号不匹配", + ), + L10nKey::AppSshParseNoRemoteCommands => ( + "Remote commands aren't supported here", + "此处不支持远程命令", + ), + L10nKey::AppSshParseFlagNeedsValue => ("-{flag} needs a value", "-{flag} 需要一个值"), + L10nKey::AppSshParseInvalidPort => ("Invalid port \"{value}\"", "无效端口 \"{value}\""), + L10nKey::AppSshParseUnsupportedOption => ( + "Unsupported option \"{option}\"", + "不支持的选项 \"{option}\"", + ), + L10nKey::AppSshParseEnterHost => ("Enter a host to connect to", "输入要连接的主机"), + L10nKey::AppSshParseBadHost => ("Can't parse host \"{host}\"", "无法解析主机 \"{host}\""), + L10nKey::AppMenuMinimize => ("Minimize", "最小化"), + L10nKey::AppMenuZoom => ("Zoom", "缩放"), + L10nKey::SwitcherStatusRestarting => ("restarting…", "正在重启…"), + L10nKey::SwitcherStatusInstalling => ("installing…", "正在安装…"), + L10nKey::SwitcherStatusConnecting => ("connecting…", "正在连接…"), + L10nKey::SwitcherStatusConnectFailed => ("couldn't connect", "连接失败"), + L10nKey::SwitcherStatusNotConnected => ("not connected", "未连接"), + L10nKey::SettingsFontDefault => ("Default (match primary)", "默认(匹配主字体)"), + L10nKey::ForwardDescriptionPlaceholder => ("what it's for", "用途说明"), + L10nKey::SettingsShellDefaultLoginShell => ("your login shell", "你的登录 shell"), + L10nKey::SftpErrorUnexpectedReply => ("unexpected reply: {reply}", "意外回复:{reply}"), + L10nKey::SftpErrorUnsafeRemoteName => ( + "refusing unsafe remote name {name}", + "拒绝不安全的远程名称 {name}", + ), + L10nKey::SftpErrorInvalidOctalMode => ("invalid octal mode", "无效的八进制模式"), + L10nKey::PanelMoreChangedFiles => ( + "… and {count} more changed files — run `git diff` to see them.", + "…还有 {count} 个变更文件——运行 `git diff` 查看。", + ), + L10nKey::PanelUntracked => ("{count} untracked", "{count} 个未跟踪文件"), + L10nKey::AppMenuAbout => ("About tty7", "关于 tty7"), + L10nKey::AppMenuCheckForUpdates => ("Check for Updates…", "检查更新…"), + L10nKey::AppMenuSettings => ("Settings…", "设置…"), + L10nKey::AppMenuServices => ("Services", "服务"), + L10nKey::AppMenuHideApp => ("Hide tty7", "隐藏 tty7"), + L10nKey::AppMenuHideOthers => ("Hide Others", "隐藏其他"), + L10nKey::AppMenuShowAll => ("Show All", "显示全部"), + L10nKey::AppMenuQuit => ("Quit tty7", "退出 tty7"), + L10nKey::AppMenuFile => ("File", "文件"), + L10nKey::AppMenuEdit => ("Edit", "编辑"), + L10nKey::AppMenuView => ("View", "视图"), + L10nKey::AppMenuWindow => ("Window", "窗口"), + L10nKey::AppMenuHelp => ("Help", "帮助"), + L10nKey::AppMenuNewTab => ("New Tab", "新标签页"), + L10nKey::AppMenuNewWorkspace => ("New Workspace", "新工作区"), + L10nKey::AppMenuNewWorktreeTab => ("New Worktree Tab", "新工作区标签页"), + L10nKey::AppMenuSplitRight => ("Split Right", "向右分屏"), + L10nKey::AppMenuSplitDown => ("Split Down", "向下分屏"), + L10nKey::AppMenuRenameTab => ("Rename Tab…", "重命名标签页…"), + L10nKey::AppMenuCopyWorkingDirectory => ("Copy Working Directory", "复制工作目录"), + L10nKey::AppMenuCopySessionId => ("Copy Session ID", "复制会话 ID"), + L10nKey::AppMenuForkSession => ("Fork Session", "派生会话"), + L10nKey::AppMenuClosePaneTab => ("Close Pane / Tab", "关闭窗格 / 标签页"), + L10nKey::AppMenuCloseOtherTabs => ("Close Other Tabs", "关闭其他标签页"), + L10nKey::AppMenuCloseTabsRight => ("Close Tabs to the Right", "关闭右侧标签页"), + L10nKey::AppMenuReopenClosedTab => ("Reopen Closed Tab", "重新打开已关闭的标签页"), + L10nKey::AppMenuRenameWorkspace => ("Rename Workspace…", "重命名工作区…"), + L10nKey::AppMenuStopWorkspace => ("Stop Workspace…", "停止工作区…"), + L10nKey::AppMenuDeleteWorkspace => ("Delete Workspace…", "删除工作区…"), + L10nKey::AppMenuUndo => ("Undo", "撤销"), + L10nKey::AppMenuRedo => ("Redo", "重做"), + L10nKey::AppMenuCut => ("Cut", "剪切"), + L10nKey::AppMenuCopy => ("Copy", "复制"), + L10nKey::AppMenuPaste => ("Paste", "粘贴"), + L10nKey::AppMenuSelectAll => ("Select All", "全选"), + L10nKey::AppMenuFind => ("Find…", "查找…"), + L10nKey::AppMenuFindNext => ("Find Next", "查找下一个"), + L10nKey::AppMenuFindPrevious => ("Find Previous", "查找上一个"), + L10nKey::AppMenuCommandPalette => ("Command Palette…", "命令面板…"), + L10nKey::AppMenuIncreaseFontSize => ("Increase Font Size", "增大字号"), + L10nKey::AppMenuDecreaseFontSize => ("Decrease Font Size", "减小字号"), + L10nKey::AppMenuResetFontSize => ("Reset Font Size", "重置字号"), + L10nKey::AppMenuLeftSidebar => ("Left Sidebar", "左侧边栏"), + L10nKey::AppMenuRightPanel => ("Right Panel", "右侧面板"), + L10nKey::AppMenuCodePanel => ("Code Panel", "代码面板"), + L10nKey::AppMenuTabBarPosition => ("Tab Bar Position", "标签栏位置"), + L10nKey::AppMenuFocusNextPane => ("Focus Next Pane", "聚焦下一个窗格"), + L10nKey::AppMenuFocusPreviousPane => ("Focus Previous Pane", "聚焦上一个窗格"), + L10nKey::AppMenuZoomPane => ("Zoom Pane", "缩放窗格"), + L10nKey::AppMenuClearScrollback => ("Clear Scrollback", "清除回滚"), + L10nKey::AppMenuEnterFullscreen => ("Enter Full Screen", "进入全屏"), + L10nKey::AppMenuDocumentation => ("tty7 Documentation", "tty7 文档"), + L10nKey::AppMenuKeyboardShortcuts => ("Keyboard Shortcuts", "键盘快捷键"), + L10nKey::AppMenuJoinDiscord => ("Join the Discord", "加入 Discord"), + L10nKey::AppMenuReportIssue => ("Report an Issue…", "报告问题…"), + L10nKey::AppMenuRestartServer => ("Restart Server…", "重启服务器…"), + L10nKey::WindowUntitled => ("Untitled", "未命名"), + L10nKey::TrayShowTty7 => ("Show tty7", "显示 tty7"), + L10nKey::TrayNotifications => ("Notifications", "通知"), + L10nKey::TrayAgentNeedsInput => ("needs input", "需要输入"), + L10nKey::TabTooltipMore => ("More", "更多"), + L10nKey::TabTooltipShowSidebar => ("Show Sidebar", "显示侧栏"), + L10nKey::TabTooltipHideSidebar => ("Hide Sidebar", "隐藏侧栏"), + L10nKey::TabTooltipHideDetailPanel => ("Hide Detail Panel", "隐藏详情面板"), + L10nKey::TabTooltipShowDetailPanel => ("Show Detail Panel", "显示详情面板"), + L10nKey::TabUnnamedShell => ("Shell {n}", "终端 {n}"), + L10nKey::ShellDefault => ("default", "默认"), + L10nKey::SidebarScratchGroup => ("Scratch", "草稿"), + L10nKey::TabContextCloseTab => ("Close Tab", "关闭标签页"), + L10nKey::TabContextCloseTabsBelow => ("Close Tabs Below", "关闭下方标签页"), + L10nKey::TabContextMarkUnread => ("Mark as Unread", "标记为未读"), + }; + match locale { + Locale::En => en, + Locale::ZhHans => zh, + } +} + +fn translate_variant(locale: Locale, key: L10nKey, branch: &'static str) -> &'static str { + use L10nKey::*; + let (en, zh) = match (key, branch) { + // --- Settings aliases --- + (SettingsAliasesLinked, "zero") => ("No aliases linked yet.", "还没有关联别名。"), + (SettingsAliasesLinked, "one") => ("1 alias linked.", "已关联 1 个别名。"), + (SettingsAliasesLinked, "other") => ("{count} aliases linked.", "已关联 {count} 个别名。"), + + // --- Settings forward rules --- + (SettingsRulesOpenedWithConnection, "zero") => ( + "0 rules, opened with the connection", + "0 条规则,随连接打开", + ), + (SettingsRulesOpenedWithConnection, "one") => { + ("1 rule, opened with the connection", "1 条规则,随连接打开") + } + (SettingsRulesOpenedWithConnection, "other") => ( + "{count} rules, opened with the connection", + "{count} 条规则,随连接打开", + ), + + // --- Offline machines --- + (SettingsOfflineMachines, "zero") => ( + "0 more saved machines are not connected — open a workspace on one to install its hooks there.", + "还有 0 个已保存的机器未连接——在其中一个上打开工作区以在那里安装钩子。", + ), + (SettingsOfflineMachines, "one") => ( + "1 more saved machine is not connected — open a workspace on it to install its hooks there.", + "还有 1 个已保存的机器未连接——在其上打开工作区以在那里安装钩子。", + ), + (SettingsOfflineMachines, "other") => ( + "{count} more saved machines are not connected — open a workspace on one to install its hooks there.", + "还有 {count} 个已保存的机器未连接——在其中一个上打开工作区以在那里安装钩子。", + ), + + // --- Panel untracked files --- + (PanelUntracked, "zero") => ("0 untracked", "0 个未跟踪文件"), + (PanelUntracked, "one") => ("1 untracked", "1 个未跟踪文件"), + (PanelUntracked, "other") => ("{count} untracked", "{count} 个未跟踪文件"), + + // --- Panel more changed files --- + (PanelMoreChangedFiles, "zero") => ( + "… and 0 more changed files — run `git diff` to see them.", + "…还有 0 个变更文件——运行 `git diff` 查看。", + ), + (PanelMoreChangedFiles, "one") => ( + "… and 1 more changed file — run `git diff` to see it.", + "…还有 1 个变更文件——运行 `git diff` 查看。", + ), + (PanelMoreChangedFiles, "other") => ( + "… and {count} more changed files — run `git diff` to see them.", + "…还有 {count} 个变更文件——运行 `git diff` 查看。", + ), + + // --- Diff summary counts --- + (DiffChangedFiles, "zero") => ("0 changed files", "0 个变更文件"), + (DiffChangedFiles, "one") => ("1 changed file", "1 个变更文件"), + (DiffChangedFiles, "other") => ("{count} changed files", "{count} 个变更文件"), + (DiffUntrackedCount, "zero") => (" · 0 untracked", " · 0 个未跟踪文件"), + (DiffUntrackedCount, "one") => (" · 1 untracked", " · 1 个未跟踪文件"), + (DiffUntrackedCount, "other") => (" · {count} untracked", " · {count} 个未跟踪文件"), + (DiffMoreFiles, "zero") => ( + "… and 0 more changed files — run `git diff` in the terminal to see them.", + "…还有 0 个变更文件——在终端中运行 `git diff` 查看。", + ), + (DiffMoreFiles, "one") => ( + "… and 1 more changed file — run `git diff` in the terminal to see it.", + "…还有 1 个变更文件——在终端中运行 `git diff` 查看。", + ), + (DiffMoreFiles, "other") => ( + "… and {count} more changed files — run `git diff` in the terminal to see them.", + "…还有 {count} 个变更文件——在终端中运行 `git diff` 查看。", + ), + (DiffUntrackedHeader, "zero") => ("Untracked files (0)", "未跟踪文件 (0)"), + (DiffUntrackedHeader, "one") => ("Untracked files (1)", "未跟踪文件 (1)"), + (DiffUntrackedHeader, "other") => ("Untracked files ({count})", "未跟踪文件 ({count})"), + (DiffMoreUntracked, "zero") => ( + "… and 0 more — run `git status` in the terminal to see them.", + "…还有 0 个——在终端中运行 `git status` 查看。", + ), + (DiffMoreUntracked, "one") => ( + "… and 1 more — run `git status` in the terminal to see it.", + "…还有 1 个——在终端中运行 `git status` 查看。", + ), + (DiffMoreUntracked, "other") => ( + "… and {count} more — run `git status` in the terminal to see them.", + "…还有 {count} 个——在终端中运行 `git status` 查看。", + ), + (DiffUntrackedSummary, "zero") => ("0 untracked", "0 个未跟踪"), + (DiffUntrackedSummary, "one") => ("1 untracked", "1 个未跟踪"), + (DiffUntrackedSummary, "other") => ("{count} untracked", "{count} 个未跟踪"), + + // --- Home relative time --- + (HomeTimeMinutesAgo, "one") => ("1 min ago", "1 分钟前"), + (HomeTimeMinutesAgo, "other") => ("{count} min ago", "{count} 分钟前"), + (HomeTimeHoursAgo, "one") => ("1 hour ago", "1 小时前"), + (HomeTimeHoursAgo, "other") => ("{count} hours ago", "{count} 小时前"), + (HomeTimeDaysAgo, "one") => ("1 day ago", "1 天前"), + (HomeTimeDaysAgo, "other") => ("{count} days ago", "{count} 天前"), + + // --- Window stop/delete shells --- + (WindowStopShells, "zero") => ( + "Its layout and working directories will be forgotten.", + "其布局和工作目录将被清除。", + ), + (WindowStopShells, "one") => ( + "1 running shell will be ended.", + "1 个正在运行的 shell 将会被终止。", + ), + (WindowStopShells, "other") => ( + "{count} running shells will be ended.", + "{count} 个正在运行的 shell 将会被终止。", + ), + (WindowDeleteShells, "zero") => ( + "Its layout and working directories will be forgotten.", + "其布局和工作目录将被清除。", + ), + (WindowDeleteShells, "one") => ( + "1 running shell will be ended and its layout forgotten.", + "1 个正在运行的 shell 将会被终止,其布局也将被清除。", + ), + (WindowDeleteShells, "other") => ( + "{count} running shells will be ended and the layout forgotten.", + "{count} 个正在运行的 shell 将会被终止,布局也将被清除。", + ), + + _ => return t(key), + }; + match locale { + Locale::En => en, + Locale::ZhHans => zh, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn zh_translations_cover_the_initial_keys() { + for key in [ + L10nKey::SearchTabs, + L10nKey::SearchFiles, + L10nKey::SearchThemes, + L10nKey::SearchSettings, + L10nKey::FilterHosts, + L10nKey::SearchCommandsOrHost, + L10nKey::SearchTheme, + L10nKey::Search, + L10nKey::SearchWorkspacesAndMachines, + L10nKey::SearchFonts, + L10nKey::NewFolderName, + L10nKey::NewFileName, + L10nKey::HomeNewTab, + L10nKey::HomeReopenClosedTab, + L10nKey::HomeSwitchWorkspace, + L10nKey::HomeCommandPalette, + L10nKey::HomeSplitRight, + L10nKey::HomeSplitDown, + L10nKey::HomeSettings, + L10nKey::TrayQuitStopServer, + L10nKey::Reconnect, + L10nKey::None, + L10nKey::TryAgain, + L10nKey::Refreshing, + L10nKey::Binary, + L10nKey::Delete, + L10nKey::NoMatchingCommands, + L10nKey::ConnectSshHint, + L10nKey::EditHint, + L10nKey::OpenFileFromTree, + L10nKey::FileChangedOnDisk, + L10nKey::Reload, + L10nKey::KeepMine, + L10nKey::Dismiss, + L10nKey::StoredPasswordRejected, + L10nKey::Trust, + L10nKey::Abort, + L10nKey::HostKeyOverrideMessage, + L10nKey::Override, + L10nKey::RememberKeychain, + L10nKey::CloseWindowTitle, + L10nKey::CloseWindowBody, + L10nKey::Cancel, + L10nKey::Close, + L10nKey::QuitStopServerTitle, + L10nKey::QuitStopServerBody, + L10nKey::QuitAndStop, + L10nKey::CloseSshConnectionTitle, + L10nKey::CloseSshConnectionBody, + L10nKey::Keep, + L10nKey::SettingsNavAppearance, + L10nKey::SettingsNavTerminal, + L10nKey::SettingsNavInput, + L10nKey::SettingsNavSsh, + L10nKey::SettingsNavAgents, + L10nKey::SettingsNavWindowTabs, + L10nKey::SettingsNavKeybindings, + L10nKey::SettingsNavAbout, + L10nKey::SettingsHeader, + L10nKey::Reset, + L10nKey::Save, + L10nKey::Connect, + L10nKey::Download, + L10nKey::Link, + L10nKey::SettingsThemeIntroTitle, + L10nKey::SettingsThemeIntroDesc, + L10nKey::SettingsTypography, + L10nKey::SettingsFontSize, + L10nKey::SettingsFontSizeDesc, + L10nKey::SettingsLineHeight, + L10nKey::SettingsLineHeightDesc, + L10nKey::SettingsFontFamily, + L10nKey::SettingsFontFamilyDesc, + L10nKey::SettingsBoldFont, + L10nKey::SettingsBoldFontDesc, + L10nKey::SettingsItalicFont, + L10nKey::SettingsItalicFontDesc, + L10nKey::SettingsFontLigatures, + L10nKey::SettingsFontLigaturesDesc, + L10nKey::SettingsCursor, + L10nKey::SettingsCursorShape, + L10nKey::SettingsCursorShapeDesc, + L10nKey::SettingsCursorBlink, + L10nKey::SettingsCursorBlinkDesc, + L10nKey::SettingsTransparency, + L10nKey::SettingsOpacity, + L10nKey::SettingsOpacityDesc, + L10nKey::SettingsBlur, + L10nKey::SettingsBlurDesc, + L10nKey::FollowTheme, + L10nKey::SettingsDimInactivePanes, + L10nKey::SettingsDimInactivePanesDesc, + L10nKey::SettingsOpenThemesFolder, + L10nKey::SettingsChangeThemeImage, + L10nKey::SettingsChooseThemeImage, + L10nKey::SettingsRemoveThemeImage, + L10nKey::SettingsImageOpacity, + L10nKey::SettingsImageOpacityDesc, + L10nKey::SettingsEditTheme, + L10nKey::SettingsEditThemeIntro, + L10nKey::SettingsBackgroundImage, + L10nKey::SettingsBackgroundImageDesc, + L10nKey::SettingsAnsiColors, + L10nKey::SettingsCustomThemes, + L10nKey::SettingsCustomThemesIntro, + L10nKey::SettingsDuplicateToEdit, + L10nKey::SettingsHosts, + L10nKey::SettingsDefaults, + L10nKey::SettingsInheritedByEveryHost, + L10nKey::SettingsNoSavedHosts, + L10nKey::SettingsNothingMatches, + L10nKey::SettingsInTty7, + L10nKey::SettingsImportFromSshConfig, + L10nKey::SettingsExpandAllGroups, + L10nKey::SettingsNoHostsYet, + L10nKey::SettingsNothingSelected, + L10nKey::SettingsTypeAddressToConnect, + L10nKey::SettingsMoreInSshConfig, + L10nKey::SettingsAliasesLinked, + L10nKey::SettingsImportAliases, + L10nKey::SettingsImportAliasesDesc, + L10nKey::SettingsImportNow, + L10nKey::SettingsDefaultsIntro, + L10nKey::SettingsCopyAddress, + L10nKey::SettingsDuplicate, + L10nKey::SettingsForgetPassword, + L10nKey::SettingsForgotPasswordFor, + L10nKey::SettingsCouldntForgetPassword, + L10nKey::SettingsSecurity, + L10nKey::SettingsSecurityIntro, + L10nKey::SettingsVerifyHostKeys, + L10nKey::SettingsVerifyHostKeysDesc, + L10nKey::WarnBeforeClosing, + L10nKey::SettingsWarnBeforeClosingDesc, + L10nKey::SettingsNewHost, + L10nKey::SettingsName, + L10nKey::SettingsNameDesc, + L10nKey::SettingsHost, + L10nKey::SettingsHostDesc, + L10nKey::SettingsUser, + L10nKey::SettingsUserDesc, + L10nKey::SettingsAuth, + L10nKey::SettingsAuthDesc, + L10nKey::SettingsAuthModeAuto, + L10nKey::SettingsAuthModePassword, + L10nKey::SettingsAuthModeKey, + L10nKey::SettingsAuthModeAgent, + L10nKey::SettingsAuthMode2Fa, + L10nKey::SettingsJumpHost, + L10nKey::SettingsJumpHostDesc, + L10nKey::SettingsNoneSummary, + L10nKey::SettingsNoneLower, + L10nKey::SettingsPortForwarding, + L10nKey::SettingsRulesOpenedWithConnection, + L10nKey::SettingsAddRule, + L10nKey::SettingsFwdLegendLocal, + L10nKey::SettingsFwdLegendRemote, + L10nKey::SettingsFwdLegendDynamic, + L10nKey::SettingsFwdNeedsBoth, + L10nKey::SettingsFwdNeedsListen, + L10nKey::SettingsAdvanced, + L10nKey::SettingsAdvancedSummary, + L10nKey::SettingsIdentityFiles, + L10nKey::SettingsIdentityFilesDesc, + L10nKey::SettingsAgentForwarding, + L10nKey::SettingsAgentForwardingDesc, + L10nKey::SettingsProxyCommand, + L10nKey::SettingsProxyCommandDesc, + L10nKey::SettingsSocks5Proxy, + L10nKey::SettingsSocks5ProxyDesc, + L10nKey::SettingsHttpProxy, + L10nKey::SettingsHttpProxyDesc, + L10nKey::SettingsKexAlgorithms, + L10nKey::SettingsKexAlgorithmsDesc, + L10nKey::SettingsCiphers, + L10nKey::SettingsCiphersDesc, + L10nKey::SettingsMacs, + L10nKey::SettingsMacsDesc, + L10nKey::SettingsHostKeyAlgorithms, + L10nKey::SettingsHostKeyAlgorithmsDesc, + L10nKey::SettingsCompression, + L10nKey::SettingsJumpHostVia, + L10nKey::SettingsConnected, + L10nKey::SettingsProfileCopied, + L10nKey::SettingsCompressionDesc, + L10nKey::SettingsKeepaliveInterval, + L10nKey::SettingsKeepaliveIntervalDesc, + L10nKey::SettingsKeepaliveCountMax, + L10nKey::SettingsKeepaliveCountMaxDesc, + L10nKey::SettingsConnectTimeout, + L10nKey::SettingsConnectTimeoutDesc, + L10nKey::SettingsX11Forwarding, + L10nKey::SettingsX11ForwardingDesc, + L10nKey::SettingsShellIntegration, + L10nKey::SettingsShellIntegrationDesc, + L10nKey::SettingsLoginScripts, + L10nKey::SettingsLoginScriptsDesc, + L10nKey::SettingsSkipBanner, + L10nKey::SettingsSkipBannerDesc, + L10nKey::SettingsDefaultFollowsDefaults, + L10nKey::SettingsValueOn, + L10nKey::SettingsValueOff, + L10nKey::SettingsDefault, + L10nKey::SettingsOn, + L10nKey::SettingsOff, + L10nKey::SettingsShell, + L10nKey::SettingsShellIntro, + L10nKey::SettingsProgram, + L10nKey::SettingsProgramDesc, + L10nKey::SettingsArguments, + L10nKey::SettingsArgumentsDesc, + L10nKey::SettingsStartIn, + L10nKey::SettingsStartInDesc, + L10nKey::SettingsCustomPath, + L10nKey::SettingsCustomPathDesc, + L10nKey::SettingsWdInherit, + L10nKey::SettingsWdHome, + L10nKey::SettingsWdCustom, + L10nKey::SettingsShellFooter, + L10nKey::SettingsScrolling, + L10nKey::SettingsScrollback, + L10nKey::SettingsScrollbackDesc, + L10nKey::SettingsScrollSpeed, + L10nKey::SettingsScrollSpeedDesc, + L10nKey::SettingsMouse, + L10nKey::SettingsFocusFollowsMouseDesc, + L10nKey::SettingsHideMouseWhileTypingDesc, + L10nKey::SettingsReportMouseToAppsDesc, + L10nKey::SettingsBell, + L10nKey::SettingsTerminalBellDesc, + L10nKey::SettingsLinks, + L10nKey::SettingsDetectUrlsDesc, + L10nKey::SettingsForwardSshLoopbackLinksDesc, + L10nKey::SettingsOpenFilesWithDesc, + L10nKey::SettingsBellModeOff, + L10nKey::SettingsBellModeVisual, + L10nKey::SettingsBellModeAudible, + L10nKey::SettingsPrompt, + L10nKey::SettingsPromptIntro, + L10nKey::SettingsTabCompletionDesc, + L10nKey::SettingsHistorySearchDesc, + L10nKey::SettingsSelectionClipboard, + L10nKey::SettingsSmartSelectionDesc, + L10nKey::SettingsCopyOnSelectDesc, + L10nKey::SettingsTrimTrailingSpacesDesc, + L10nKey::SettingsKeyboard, + L10nKey::SettingsOptionAsMetaDesc, + L10nKey::SettingsAgentsIntro, + L10nKey::SettingsAgentsIntroDesc, + L10nKey::SettingsReadingAgentConfig, + L10nKey::SettingsStatusNotInstalled, + L10nKey::SettingsStatusInstalled, + L10nKey::SettingsStatusOutdated, + L10nKey::SettingsInstall, + L10nKey::SettingsReinstall, + L10nKey::SettingsUpdate, + L10nKey::SettingsUninstall, + L10nKey::SettingsOfflineMachines, + L10nKey::SettingsSyncWithSystem, + L10nKey::SettingsSyncWithSystemDesc, + L10nKey::SettingsChangeTheme, + L10nKey::SettingsThemes, + L10nKey::SettingsThemePanelManual, + L10nKey::SettingsThemePanelLight, + L10nKey::SettingsThemePanelDark, + L10nKey::SettingsCustom, + L10nKey::SettingsBuiltIn, + L10nKey::SettingsDark, + L10nKey::SettingsLight, + L10nKey::SettingsActive, + L10nKey::SettingsStartupWindow, + L10nKey::SettingsStartupWindowDesc, + L10nKey::SettingsRememberWindowSize, + L10nKey::SettingsRememberWindowSizeDesc, + L10nKey::SettingsRestoreLastLayout, + L10nKey::SettingsRestoreLastLayoutDesc, + L10nKey::SettingsConfirmLastWindowClose, + L10nKey::SettingsConfirmLastWindowCloseDesc, + L10nKey::SettingsShowTrayIcon, + L10nKey::SettingsShowTrayIconDesc, + L10nKey::SettingsTabs, + L10nKey::SettingsNewTabPosition, + L10nKey::SettingsNewTabPositionDesc, + L10nKey::SettingsTabBarPosition, + L10nKey::SettingsTabBarPositionDesc, + L10nKey::SettingsSidebarGrouping, + L10nKey::SettingsSidebarGroupingDesc, + L10nKey::SettingsDiffPreviewFromCounts, + L10nKey::SettingsDiffPreviewFromCountsDesc, + L10nKey::SettingsNotifications, + L10nKey::SettingsNotifyOnCommandFinish, + L10nKey::SettingsNotifyOnCommandFinishDesc, + L10nKey::SettingsNotifyThreshold, + L10nKey::SettingsNotifyThresholdDesc, + L10nKey::NotifyModeNever, + L10nKey::NotifyModeUnfocused, + L10nKey::NotifyModeAlways, + L10nKey::SettingsStartupNormal, + L10nKey::SettingsStartupMaximized, + L10nKey::SettingsStartupFullscreen, + L10nKey::SettingsAfterCurrent, + L10nKey::SettingsAtEnd, + L10nKey::SettingsTop, + L10nKey::SettingsLeft, + L10nKey::SettingsByRepo, + L10nKey::SettingsFlat, + L10nKey::SettingsPreset, + L10nKey::SettingsPresetDesc, + L10nKey::SettingsPrefix, + L10nKey::SettingsPressKeys, + L10nKey::SettingsPauseToSaveEsc, + L10nKey::SettingsKeybindingsIntroDesc, + L10nKey::SettingsPrefixNote, + L10nKey::SettingsRestoreAllDefaults, + L10nKey::SettingsAboutDesc1, + L10nKey::SettingsAboutDesc2, + L10nKey::SettingsAboutTech, + L10nKey::SettingsUpdates, + L10nKey::SettingsVersionAvailable, + L10nKey::SettingsCheckUpdatesDesc, + L10nKey::SettingsCheckUpdatesOnLaunch, + L10nKey::SettingsCommandLine, + L10nKey::SettingsCommandLineDesc, + L10nKey::SettingsInstallCliOnPath, + L10nKey::SettingsExplorerContextMenu, + L10nKey::SettingsExplorerContextMenuDesc, + L10nKey::SettingsExplorerNotRegistered, + L10nKey::SettingsExplorerRegistered, + L10nKey::SettingsExplorerNeedsUpdate, + L10nKey::SettingsExplorerUnavailable, + L10nKey::SettingsExplorerStatusUnavailable, + L10nKey::SettingsExplorerRegister, + L10nKey::SettingsExplorerUpdate, + L10nKey::SettingsExplorerUnregister, + L10nKey::SettingsExplorerRegisteredNote, + L10nKey::SettingsExplorerUnregisteredNote, + L10nKey::SettingsExplorerRegisterFailed, + L10nKey::SettingsExplorerUnregisterFailed, + L10nKey::SettingsExplorerWindows11Note, + L10nKey::SettingsServer, + L10nKey::SettingsServerDesc, + L10nKey::SettingsRestartServer, + L10nKey::SettingsAgentClaudeCode, + L10nKey::SettingsAgentCodex, + L10nKey::SettingsAgentCopilotCli, + L10nKey::SettingsAgentOpencode, + L10nKey::SettingsAgentPi, + L10nKey::SettingsAgentGrokBuild, + L10nKey::SettingsSearchAboutKeywords, + L10nKey::SettingsSearchAnsiColorsKeywords, + L10nKey::SettingsSearchArgumentsKeywords, + L10nKey::SettingsSearchBlurKeywords, + L10nKey::SettingsSearchBoldFontKeywords, + L10nKey::SettingsSearchClaudeCodeKeywords, + L10nKey::SettingsSearchCodexKeywords, + L10nKey::SettingsSearchCommandLineToolKeywords, + L10nKey::SettingsSearchCommandLineToolTitle, + L10nKey::SettingsSearchConfirmLastWindowCloseKeywords, + L10nKey::SettingsSearchCopilotCliKeywords, + L10nKey::SettingsSearchCopyOnSelectKeywords, + L10nKey::SettingsSearchCursorBlinkKeywords, + L10nKey::SettingsSearchCursorShapeKeywords, + L10nKey::SettingsSearchCustomThemesKeywords, + L10nKey::SettingsSearchDetectUrlsKeywords, + L10nKey::SettingsSearchDiffPreviewFromCountsKeywords, + L10nKey::SettingsSearchDimInactivePanesKeywords, + L10nKey::SettingsSearchExplorerContextMenuKeywords, + L10nKey::SettingsSearchFocusFollowsMouseKeywords, + L10nKey::SettingsSearchFontFamilyKeywords, + L10nKey::SettingsSearchFontLigaturesKeywords, + L10nKey::SettingsSearchFontSizeKeywords, + L10nKey::SettingsSearchForwardSshLoopbackLinksKeywords, + L10nKey::SettingsSearchGrokBuildKeywords, + L10nKey::SettingsSearchHideMouseWhileTypingKeywords, + L10nKey::SettingsSearchHistorySearchKeywords, + L10nKey::SettingsSearchHostsKeywords, + L10nKey::SettingsSearchHowShellsWorkKeywords, + L10nKey::SettingsSearchHowShellsWorkTitle, + L10nKey::SettingsSearchItalicFontKeywords, + L10nKey::SettingsSearchKeybindingsKeywords, + L10nKey::SettingsSearchKeybindingsTitle, + L10nKey::SettingsSearchLineHeightKeywords, + L10nKey::SettingsSearchNewTabPositionKeywords, + L10nKey::SettingsSearchNotifyOnCommandFinishKeywords, + L10nKey::SettingsSearchNotifyThresholdKeywords, + L10nKey::SettingsSearchOpacityKeywords, + L10nKey::SettingsSearchOpenFilesWithKeywords, + L10nKey::SettingsSearchOpencodeKeywords, + L10nKey::SettingsSearchOptionAsMetaKeywords, + L10nKey::SettingsSearchPiKeywords, + L10nKey::SettingsSearchPortForwardingKeywords, + L10nKey::SettingsSearchProgramKeywords, + L10nKey::SettingsSearchRememberWindowSizeKeywords, + L10nKey::SettingsSearchReportMouseToAppsKeywords, + L10nKey::SettingsSearchRestoreLastLayoutKeywords, + L10nKey::SettingsSearchScrollSpeedKeywords, + L10nKey::SettingsSearchScrollbackKeywords, + L10nKey::SettingsSearchShowTrayIconKeywords, + L10nKey::SettingsSearchSidebarGroupingKeywords, + L10nKey::SettingsSearchSmartSelectionKeywords, + L10nKey::SettingsSearchStartInKeywords, + L10nKey::SettingsSearchSyncWithSystemKeywords, + L10nKey::SettingsSearchTabBarPositionKeywords, + L10nKey::SettingsSearchTabCompletionKeywords, + L10nKey::SettingsSearchTerminalBellKeywords, + L10nKey::SettingsSearchThemeKeywords, + L10nKey::SettingsSearchTrimTrailingSpacesKeywords, + L10nKey::SettingsSearchVerifyHostKeysKeywords, + L10nKey::SettingsSearchWarnBeforeClosingKeywords, + L10nKey::SettingsSearchStartupWindowKeywords, + L10nKey::SwitcherNoMatch, + L10nKey::AddSshHost, + L10nKey::ClickForNewWindow, + L10nKey::RestartServer, + L10nKey::OtherMachines, + L10nKey::Ok, + L10nKey::SftpNoTransfers, + L10nKey::SftpPanelTitleFiles, + L10nKey::SftpTooltipRefresh, + L10nKey::SftpTooltipMore, + L10nKey::SftpMenuNewFolder, + L10nKey::SftpMenuNewFile, + L10nKey::SftpMenuUpload, + L10nKey::SftpMenuGotoShellCwd, + L10nKey::SftpMenuHideTransferHistory, + L10nKey::SftpMenuTransferHistory, + L10nKey::SftpEditNewFolder, + L10nKey::SftpEditNewFile, + L10nKey::SftpEditRename, + L10nKey::SftpEditPermissions, + L10nKey::SftpLoading, + L10nKey::SftpEmptyDirectory, + L10nKey::SftpContextOpen, + L10nKey::SftpContextFollowSymlink, + L10nKey::SftpContextRename, + L10nKey::SftpContextChmod, + L10nKey::SftpTransferSummaryRunning, + L10nKey::SftpTransferSummaryFailed, + L10nKey::SftpTransferSummaryIdle, + L10nKey::SftpTransferProgress, + L10nKey::SftpTransferDone, + L10nKey::SftpTransferCancelled, + L10nKey::SftpTransferError, + L10nKey::ForwardPanelTitle, + L10nKey::ForwardDisconnected, + L10nKey::ForwardDisconnectedFrom, + L10nKey::ForwardTooltipAdd, + L10nKey::ForwardTooltipRemove, + L10nKey::ForwardLocal, + L10nKey::ForwardRemote, + L10nKey::ForwardDynamic, + L10nKey::ForwardBindLabel, + L10nKey::ForwardToLabel, + L10nKey::ForwardSocksLabel, + L10nKey::ForwardAdd, + L10nKey::FileTreePlaceholderFileName, + L10nKey::FileTreePlaceholderFolderName, + L10nKey::FileTreePlaceholderNewName, + L10nKey::FileTreeDeleteTitle, + L10nKey::FileTreeDeleteFolderBody, + L10nKey::FileTreeDeleteFileBody, + L10nKey::FileTreeDeleteFailed, + L10nKey::FileTreeContextOpen, + L10nKey::FileTreeContextCdHere, + L10nKey::FileTreeContextInsertPath, + L10nKey::FileTreeContextAttachAgent, + L10nKey::FileTreeContextNewFile, + L10nKey::FileTreeContextNewFolder, + L10nKey::FileTreeContextRename, + L10nKey::FileTreeContextCopyPath, + L10nKey::FileTreeContextHideDotfiles, + L10nKey::FileTreeContextShowDotfiles, + L10nKey::SshPromptNewKey, + L10nKey::SshPromptOldKey, + L10nKey::EditorCantOpen, + L10nKey::EditorCantRead, + L10nKey::EditorNotUtf8, + L10nKey::EditorSaveFailed, + L10nKey::EditorUnsavedChanges, + L10nKey::EditorDiscard, + L10nKey::EditorNoFileOpen, + L10nKey::EditorBackToTerminal, + L10nKey::EditorLnCol, + L10nKey::EditorEdit, + L10nKey::EditorPreview, + L10nKey::EditorWrapOn, + L10nKey::EditorWrapOff, + L10nKey::EditorFileTooLarge, + L10nKey::EditorBinaryFile, + L10nKey::PanelInfoTitle, + L10nKey::PanelOutlineTitle, + L10nKey::PanelChangesTitle, + L10nKey::PanelFilesTitle, + L10nKey::PanelNoSession, + L10nKey::PanelNoSessionHint, + L10nKey::PanelNoCommands, + L10nKey::PanelNoCommandsHint, + L10nKey::PanelNoWorkingDirectory, + L10nKey::PanelNoWorkingDirectoryHint, + L10nKey::PanelLoading, + L10nKey::PanelNotAGitRepo, + L10nKey::PanelNotAGitRepoHint, + L10nKey::PanelNoChanges, + L10nKey::PanelNoChangesHint, + L10nKey::PanelMoreChangedFiles, + L10nKey::PanelUntracked, + L10nKey::PanelSessionSubtitle, + L10nKey::PanelProcessesSubtitle, + L10nKey::PanelPortsSubtitle, + L10nKey::PanelCwd, + L10nKey::PanelShell, + L10nKey::PanelSsh, + L10nKey::PanelBranch, + L10nKey::PanelChangesRow, + L10nKey::PanelAgent, + L10nKey::PanelAgentIdle, + L10nKey::PanelAgentWorking, + L10nKey::PanelAgentWaiting, + L10nKey::PanelAgentDone, + L10nKey::PanelRevealInFinder, + L10nKey::PanelOpenFolder, + L10nKey::WindowStop, + L10nKey::WindowDelete, + L10nKey::WindowThisWorkspace, + L10nKey::WindowConfirmTitle, + L10nKey::WindowStopUnreachable, + L10nKey::WindowDeleteUnreachable, + L10nKey::WindowStopShells, + L10nKey::WindowDeleteShells, + L10nKey::DiffReading, + L10nKey::DiffNotARepo, + L10nKey::DiffReadFailed, + L10nKey::DiffWorkingTreeClean, + L10nKey::DiffCloseTooltip, + L10nKey::DiffChangedFiles, + L10nKey::DiffUntrackedCount, + L10nKey::DiffMoreFiles, + L10nKey::DiffOversizedNotice, + L10nKey::DiffTruncatedPerFile, + L10nKey::DiffTruncatedBudget, + L10nKey::DiffUntrackedHeader, + L10nKey::DiffMoreUntracked, + L10nKey::DiffLines, + L10nKey::DiffChangedLines, + L10nKey::DiffBudgetAndCap, + L10nKey::DiffBudget, + L10nKey::DiffPerFileCap, + L10nKey::DiffUntrackedSummary, + L10nKey::PendingConnecting, + L10nKey::PendingUnreachable, + L10nKey::WorktreePromptNeedsName, + L10nKey::WorktreePromptTitle, + L10nKey::WorktreePromptName, + L10nKey::WorktreePromptBranch, + L10nKey::WorktreePromptBase, + L10nKey::WorktreePromptCreating, + L10nKey::WorktreePromptCreate, + L10nKey::AppNewWorktreeFailed, + L10nKey::HomeTimeJustNow, + L10nKey::HomeTimeMinutesAgo, + L10nKey::HomeTimeHourAgo, + L10nKey::HomeTimeHoursAgo, + L10nKey::HomeTimeYesterday, + L10nKey::HomeTimeDaysAgo, + L10nKey::HomeTimeOverWeekAgo, + L10nKey::HomeReopenNamed, + L10nKey::AppMenuAbout, + L10nKey::AppMenuCheckForUpdates, + L10nKey::AppMenuSettings, + L10nKey::AppMenuServices, + L10nKey::AppMenuHideApp, + L10nKey::AppMenuHideOthers, + L10nKey::AppMenuShowAll, + L10nKey::AppMenuQuit, + L10nKey::AppMenuFile, + L10nKey::AppMenuEdit, + L10nKey::AppMenuView, + L10nKey::AppMenuWindow, + L10nKey::AppMenuHelp, + L10nKey::AppMenuNewTab, + L10nKey::AppMenuNewWorkspace, + L10nKey::AppMenuNewWorktreeTab, + L10nKey::AppMenuSplitRight, + L10nKey::AppMenuSplitDown, + L10nKey::AppMenuRenameTab, + L10nKey::AppMenuCopyWorkingDirectory, + L10nKey::AppMenuCopySessionId, + L10nKey::AppMenuForkSession, + L10nKey::AppMenuClosePaneTab, + L10nKey::AppMenuCloseOtherTabs, + L10nKey::AppMenuCloseTabsRight, + L10nKey::AppMenuReopenClosedTab, + L10nKey::AppMenuRenameWorkspace, + L10nKey::AppMenuStopWorkspace, + L10nKey::AppMenuDeleteWorkspace, + L10nKey::AppMenuUndo, + L10nKey::AppMenuRedo, + L10nKey::AppMenuCut, + L10nKey::AppMenuCopy, + L10nKey::AppMenuPaste, + L10nKey::AppMenuSelectAll, + L10nKey::AppMenuFind, + L10nKey::AppMenuFindNext, + L10nKey::AppMenuFindPrevious, + L10nKey::AppMenuCommandPalette, + L10nKey::AppMenuIncreaseFontSize, + L10nKey::AppMenuDecreaseFontSize, + L10nKey::AppMenuResetFontSize, + L10nKey::AppMenuLeftSidebar, + L10nKey::AppMenuRightPanel, + L10nKey::AppMenuCodePanel, + L10nKey::AppMenuTabBarPosition, + L10nKey::AppMenuFocusNextPane, + L10nKey::AppMenuFocusPreviousPane, + L10nKey::AppMenuZoomPane, + L10nKey::AppMenuClearScrollback, + L10nKey::AppMenuEnterFullscreen, + L10nKey::AppMenuDocumentation, + L10nKey::AppMenuKeyboardShortcuts, + L10nKey::AppMenuJoinDiscord, + L10nKey::AppMenuReportIssue, + L10nKey::AppMenuRestartServer, + L10nKey::WindowUntitled, + L10nKey::TrayShowTty7, + L10nKey::TrayNotifications, + L10nKey::TrayAgentNeedsInput, + L10nKey::TabTooltipMore, + L10nKey::TabTooltipShowSidebar, + L10nKey::TabTooltipHideSidebar, + L10nKey::TabTooltipHideDetailPanel, + L10nKey::TabTooltipShowDetailPanel, + L10nKey::TabUnnamedShell, + L10nKey::ShellDefault, + L10nKey::SidebarScratchGroup, + L10nKey::TabContextCloseTab, + L10nKey::TabContextCloseTabsBelow, + L10nKey::TabContextMarkUnread, + L10nKey::RemoteStripDisconnected, + L10nKey::RemoteStripConnecting, + L10nKey::RemoteStripReconnecting, + L10nKey::RemoteStripReconnectingAttempt, + L10nKey::RemoteStripPreempted, + L10nKey::RemoteStripFailed, + L10nKey::RemoteNoticePreempted, + L10nKey::RemoteNoticeDisconnected, + L10nKey::RemoteActionRetryNow, + L10nKey::RemoteActionTakeBack, + L10nKey::RemoteActionConnect, + L10nKey::RemoteActionRetry, + L10nKey::RemoteNoConnectionDetails, + L10nKey::RemoteThisComputer, + L10nKey::RemoteRestartTitle, + L10nKey::RemoteRestartBody, + L10nKey::RemoteReplaceBody, + L10nKey::RemoteRestartFailedTitle, + L10nKey::RemoteRestartFailedBody, + L10nKey::RemoteHostUnreachable, + L10nKey::RemoteInstallTitle, + L10nKey::RemoteInstallDetail, + L10nKey::RemoteInstallPathLabel, + L10nKey::RemoteInstallVersionLabel, + L10nKey::RemoteInstallSizeLabel, + L10nKey::RemoteInstallFromLabel, + L10nKey::RemoteInstallShaLabel, + L10nKey::RemoteInstallSilentUpgrades, + L10nKey::RemoteInstallBytes, + L10nKey::RemoteMismatchTitle, + L10nKey::RemoteMismatchDetail, + L10nKey::RemoteMismatchUnknownBuild, + L10nKey::RemoteMismatchUnknownBuildFromExe, + L10nKey::RemoteDaemonStartFailed, + L10nKey::RemoteDaemonUnreachable, + L10nKey::RemoteDaemonTooOld, + L10nKey::RemoteProfileMissing, + L10nKey::RemoteAliasMissing, + L10nKey::RemoteWslNoSsh, + L10nKey::RemoteLocalStdioNoSsh, + L10nKey::RemoteHostNotTty7, + L10nKey::RemoteWorkspaceListFailed, + L10nKey::RemoteServerRestartFailed, + L10nKey::RemoteNoRouteToHost, + L10nKey::RemoteMachineTreeUnexpectedReply, + L10nKey::RemoteMismatchVersionFromExe, + L10nKey::AppNoRunningCodingAgent, + L10nKey::SwitcherThisComputer, + L10nKey::SwitcherRestartingServer, + L10nKey::SwitcherDownloadingServerWithTotal, + L10nKey::SwitcherDownloadingServerNoTotal, + L10nKey::SwitcherCopyingServer, + L10nKey::SwitcherThisWindow, + L10nKey::SwitcherOpen, + L10nKey::SwitcherDisconnect, + L10nKey::SwitcherOpenInNewWindow, + L10nKey::SwitcherRename, + L10nKey::SshPromptPasswordFor, + L10nKey::SshPromptPassphraseFor, + L10nKey::SshPromptTwoFactor, + L10nKey::SshPromptUnknownHost, + L10nKey::SshPromptHostKeyChanged, + L10nKey::SshPromptHostKeyChangedBody, + L10nKey::SshPromptConnect, + L10nKey::SshPromptUnlock, + L10nKey::SshPromptSubmit, + L10nKey::HostOpsError, + L10nKey::CmdGroupTabsPanes, + L10nKey::CmdGroupWorkspaces, + L10nKey::CmdGroupView, + L10nKey::CmdGroupTerminal, + L10nKey::CmdGroupSsh, + L10nKey::CmdGroupAgents, + L10nKey::CmdGroupApplication, + L10nKey::CmdNewTab, + L10nKey::CmdNewWorktreeTab, + L10nKey::CmdNewWorktreeTabSubtitle, + L10nKey::CmdRenameTab, + L10nKey::CmdSplitRight, + L10nKey::CmdSplitDown, + L10nKey::CmdZoomPane, + L10nKey::CmdNextPane, + L10nKey::CmdPreviousPane, + L10nKey::CmdFocusPaneLeft, + L10nKey::CmdFocusPaneRight, + L10nKey::CmdFocusPaneUp, + L10nKey::CmdFocusPaneDown, + L10nKey::CmdResizePaneLeft, + L10nKey::CmdResizePaneRight, + L10nKey::CmdResizePaneUp, + L10nKey::CmdResizePaneDown, + L10nKey::CmdSwapPaneNext, + L10nKey::CmdSwapPanePrevious, + L10nKey::CmdNextTab, + L10nKey::CmdPreviousTab, + L10nKey::CmdCopyWorkingDirectory, + L10nKey::CmdCopySessionId, + L10nKey::CmdCopySessionIdSubtitle, + L10nKey::CmdForkSession, + L10nKey::CmdForkSessionSubtitle, + L10nKey::CmdMarkTabAsUnread, + L10nKey::CmdClosePaneTab, + L10nKey::CmdCloseOtherTabs, + L10nKey::CmdCloseTabsToTheRight, + L10nKey::CmdReopenClosedTab, + L10nKey::CmdNewWorkspace, + L10nKey::CmdSwitchWorkspace, + L10nKey::CmdRenameWorkspace, + L10nKey::CmdStopWorkspace, + L10nKey::CmdStopWorkspaceSubtitle, + L10nKey::CmdDeleteWorkspace, + L10nKey::CmdDeleteWorkspaceSubtitle, + L10nKey::CmdShowLeftSidebar, + L10nKey::CmdHideLeftSidebar, + L10nKey::CmdHideRightPanel, + L10nKey::CmdShowRightPanel, + L10nKey::CmdShowCodePanel, + L10nKey::CmdTabBarMoveToTop, + L10nKey::CmdTabBarMoveToLeftSidebar, + L10nKey::CmdRightPanelInfo, + L10nKey::CmdRightPanelOutline, + L10nKey::CmdRightPanelChanges, + L10nKey::CmdRightPanelFiles, + L10nKey::CmdChangeTheme, + L10nKey::CmdResetFontSize, + L10nKey::CmdEnterFullScreen, + L10nKey::CmdClearScrollback, + L10nKey::CmdFindInTerminal, + L10nKey::CmdFindNext, + L10nKey::CmdFindPrevious, + L10nKey::CmdCopy, + L10nKey::CmdCut, + L10nKey::CmdPaste, + L10nKey::CmdSelectAll, + L10nKey::CmdSshAddConnection, + L10nKey::CmdSshManageProfiles, + L10nKey::CmdSshReconnect, + L10nKey::CmdSshRemoteFiles, + L10nKey::CmdSshPortForwarding, + L10nKey::CmdSshConnectWithInput, + L10nKey::CmdAgentSendSelection, + L10nKey::CmdAgentSendSelectionSubtitle, + L10nKey::CmdAgentSendGitDiffForReview, + L10nKey::CmdAgentSendGitDiffSubtitle, + L10nKey::CmdSettings, + L10nKey::CmdKeyboardShortcuts, + L10nKey::CmdAboutTty7, + L10nKey::CmdCheckForUpdates, + L10nKey::CmdDocumentation, + L10nKey::CmdJoinDiscord, + L10nKey::CmdReportIssue, + L10nKey::CmdRestartServer, + L10nKey::CmdRestartServerSubtitle, + L10nKey::CmdQuitTty7, + L10nKey::CmdQuitTty7Subtitle, + L10nKey::CmdQuickConnect, + L10nKey::CmdQuickConnectSaveProfile, + L10nKey::CmdRecent, + L10nKey::AppRestartServerTitle, + L10nKey::AppRestartServerMismatchDetail, + L10nKey::AppRestartServerOldDetail, + L10nKey::AppKeepShells, + L10nKey::AppRestart, + L10nKey::AppRestartServerNotSsh, + L10nKey::AppRestartServerBody, + L10nKey::AppWorktreeRemoveDetailDirty, + L10nKey::AppWorktreeRemoveDetailClean, + L10nKey::AppWorktreeRemoveTitle, + L10nKey::AppWorktreeDiscardAndRemove, + L10nKey::AppWorktreeRemove, + L10nKey::AppWorktreeKeep, + L10nKey::AppReopenTabFailed, + L10nKey::AppOpenTerminalFailed, + L10nKey::AppSshConnectionFailed, + L10nKey::AppSshReconnectFailed, + L10nKey::AppSplitPaneFailed, + L10nKey::AppWorktreeRemoved, + L10nKey::AppWorktreeRemoveFailed, + L10nKey::AppForkStillConnecting, + L10nKey::AppPaneNoCodingAgent, + L10nKey::AppForkNoCommand, + L10nKey::AppForkLocalOnly, + L10nKey::AppForkNoSessionId, + L10nKey::AppForkSessionIdNotToken, + L10nKey::AppForkMidTurn, + L10nKey::AppTabNoWorkingDirectory, + L10nKey::AppNothingSelected, + L10nKey::AppPaneNoKnownDirectory, + L10nKey::AppNoUncommittedChanges, + L10nKey::AppCmdSshProfileTitle, + L10nKey::AppCmdSwitchToTab, + L10nKey::AppPlaceholderDescription, + L10nKey::AppPlaceholderSshQuickConnect, + L10nKey::AppPlaceholderLoginShell, + L10nKey::AppPlaceholderNone, + L10nKey::AppPlaceholderOpenInDefaultApp, + L10nKey::AppThemeColorBackground, + L10nKey::AppThemeColorForeground, + L10nKey::AppThemeColorAccent, + L10nKey::AppThemeColorCursor, + L10nKey::AppThemeColorSelection, + L10nKey::AppAgentHooksThisComputer, + L10nKey::AppAgentHooksRemoteMachine, + L10nKey::AppAgentHooksNoHomeDir, + L10nKey::AppAgentHooksOffline, + L10nKey::AppAgentHooksHomeDirUnresolved, + L10nKey::AppAgentHooksOpFailed, + L10nKey::AppKeybindingDisplacedNote, + L10nKey::AppLocalServerName, + L10nKey::AppSshParseUnbalancedQuotes, + L10nKey::AppSshParseNoRemoteCommands, + L10nKey::AppSshParseFlagNeedsValue, + L10nKey::AppSshParseInvalidPort, + L10nKey::AppSshParseUnsupportedOption, + L10nKey::AppSshParseEnterHost, + L10nKey::AppSshParseBadHost, + L10nKey::AppMenuMinimize, + L10nKey::AppMenuZoom, + L10nKey::SwitcherStatusRestarting, + L10nKey::SwitcherStatusInstalling, + L10nKey::SwitcherStatusConnecting, + L10nKey::SwitcherStatusConnectFailed, + L10nKey::SwitcherStatusNotConnected, + L10nKey::SettingsLanguage, + L10nKey::SettingsLanguageDesc, + L10nKey::SettingsLanguageEnglish, + L10nKey::SettingsLanguageChinese, + L10nKey::SettingsSearchLanguageKeywords, + L10nKey::SettingsFontDefault, + L10nKey::ForwardDescriptionPlaceholder, + L10nKey::SettingsShellDefaultLoginShell, + L10nKey::SftpErrorUnexpectedReply, + L10nKey::SftpErrorUnsafeRemoteName, + L10nKey::SftpErrorInvalidOctalMode, + ] { + assert!( + !translate(Locale::ZhHans, key).is_empty(), + "missing zh translation for {key:?}" + ); + assert!( + !translate(Locale::En, key).is_empty(), + "missing en translation for {key:?}" + ); + } + } + + #[test] + fn explicit_languages_select_the_right_locale() { + set_locale("zh-CN"); + assert_eq!(current_locale(), Locale::ZhHans); + set_locale("en"); + assert_eq!(current_locale(), Locale::En); + set_locale("ko"); + assert_eq!(current_locale(), Locale::En); + } + + #[test] + fn explorer_settings_are_translated_with_error_details() { + let keys = [ + L10nKey::SettingsExplorerContextMenu, + L10nKey::SettingsExplorerContextMenuDesc, + L10nKey::SettingsExplorerNotRegistered, + L10nKey::SettingsExplorerRegistered, + L10nKey::SettingsExplorerNeedsUpdate, + L10nKey::SettingsExplorerUnavailable, + L10nKey::SettingsExplorerStatusUnavailable, + L10nKey::SettingsExplorerRegister, + L10nKey::SettingsExplorerUpdate, + L10nKey::SettingsExplorerUnregister, + L10nKey::SettingsExplorerRegisteredNote, + L10nKey::SettingsExplorerUnregisteredNote, + L10nKey::SettingsExplorerRegisterFailed, + L10nKey::SettingsExplorerUnregisterFailed, + L10nKey::SettingsExplorerWindows11Note, + L10nKey::SettingsSearchExplorerContextMenuKeywords, + ]; + for key in keys { + assert_ne!( + translate(Locale::En, key), + translate(Locale::ZhHans, key), + "Simplified Chinese should not fall back to English for {key:?}" + ); + } + + assert_eq!( + apply_template( + translate(Locale::ZhHans, L10nKey::SettingsExplorerRegisterFailed), + &[("error", "access denied")], + None, + ), + "无法注册:access denied" + ); + } + + #[test] + fn plural_and_select_branches_are_translated() { + let plural_keys = [ + L10nKey::SettingsAliasesLinked, + L10nKey::SettingsRulesOpenedWithConnection, + L10nKey::SettingsOfflineMachines, + L10nKey::PanelUntracked, + L10nKey::PanelMoreChangedFiles, + L10nKey::WindowStopShells, + L10nKey::WindowDeleteShells, + L10nKey::DiffChangedFiles, + L10nKey::DiffUntrackedCount, + L10nKey::DiffMoreFiles, + L10nKey::DiffUntrackedHeader, + L10nKey::DiffMoreUntracked, + L10nKey::DiffUntrackedSummary, + L10nKey::HomeTimeMinutesAgo, + L10nKey::HomeTimeHoursAgo, + L10nKey::HomeTimeDaysAgo, + ]; + for key in plural_keys { + for branch in ["zero", "one", "other"] { + assert!( + !translate_variant(Locale::En, key, branch).is_empty(), + "missing en plural/select branch {branch:?} for {key:?}" + ); + assert!( + !translate_variant(Locale::ZhHans, key, branch).is_empty(), + "missing zh plural/select branch {branch:?} for {key:?}" + ); + } + // Smoke-check t_plural does not produce empty strings. + assert!(!t_plural(key, 0, &[]).is_empty()); + assert!(!t_plural(key, 1, &[]).is_empty()); + assert!(!t_plural(key, 5, &[]).is_empty()); + } + } +} diff --git a/src/ui/machine_mirror.rs b/src/ui/machine_mirror.rs index 4de95a64..ea4ad05f 100644 --- a/src/ui/machine_mirror.rs +++ b/src/ui/machine_mirror.rs @@ -6,6 +6,7 @@ use tty7_core::daemon::control::{ControlRequest, ReplyOk}; use tty7_core::host::HostId; use crate::core::session::WorkspaceId; +use crate::ui::i18n::{L10nKey, t}; #[derive(Default)] pub struct MachineMirrors { @@ -264,7 +265,7 @@ pub fn display_name_of(ws: &Workspace, panes: &[PaneRecord]) -> String { .map(|n| n.to_string_lossy().into_owned()) }) .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "Untitled".to_string()) + .unwrap_or_else(|| t(L10nKey::WindowUntitled).to_string()) } pub fn subject_path_of(ws: &Workspace, panes: &[PaneRecord]) -> Option { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 41806060..7be1962c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -10,6 +10,7 @@ pub mod home; pub mod host_ops; #[allow(dead_code)] pub mod host_registry; +pub mod i18n; pub mod keymap; pub mod local_link; pub mod machine_mirror; diff --git a/src/ui/palette.rs b/src/ui/palette.rs index bfcd6bd7..82a04a70 100644 --- a/src/ui/palette.rs +++ b/src/ui/palette.rs @@ -12,6 +12,7 @@ use uuid::Uuid; use crate::core::config::{Config, RightPanelTab, TabBarPosition}; use crate::core::ssh_profile::parse_quick_connect; +use crate::ui::i18n::{L10nKey, t, t_fmt}; #[derive(Clone, PartialEq, Eq)] pub enum CommandKind { @@ -297,13 +298,13 @@ impl CommandGroup { fn title(self) -> &'static str { match self { - CommandGroup::TabsPanes => "Tabs & Panes", - CommandGroup::Workspaces => "Workspaces", - CommandGroup::View => "View", - CommandGroup::Terminal => "Terminal", - CommandGroup::Ssh => "SSH", - CommandGroup::Agents => "Agents", - CommandGroup::Application => "Application", + CommandGroup::TabsPanes => t(L10nKey::CmdGroupTabsPanes), + CommandGroup::Workspaces => t(L10nKey::CmdGroupWorkspaces), + CommandGroup::View => t(L10nKey::CmdGroupView), + CommandGroup::Terminal => t(L10nKey::CmdGroupTerminal), + CommandGroup::Ssh => t(L10nKey::CmdGroupSsh), + CommandGroup::Agents => t(L10nKey::CmdGroupAgents), + CommandGroup::Application => t(L10nKey::CmdGroupApplication), } } } @@ -350,127 +351,134 @@ impl Command { let right_panel_open = chrome.right_panel_visible; let tabs = [ - Command::new("New Tab", NewTab), - Command::new("New Worktree Tab", NewWorktreeTab) - .with_subtitle("isolated checkout on a fresh branch"), - Command::new("Rename Tab…", RenameTab), - Command::new("Split Right", SplitRight), - Command::new("Split Down", SplitDown), - Command::new("Zoom Pane", ToggleMaximizePane), - Command::new("Next Pane", NextPane), - Command::new("Previous Pane", PrevPane), - Command::new("Focus Pane Left", FocusPaneLeft), - Command::new("Focus Pane Right", FocusPaneRight), - Command::new("Focus Pane Up", FocusPaneUp), - Command::new("Focus Pane Down", FocusPaneDown), - Command::new("Resize Pane Left", ResizePaneLeft), - Command::new("Resize Pane Right", ResizePaneRight), - Command::new("Resize Pane Up", ResizePaneUp), - Command::new("Resize Pane Down", ResizePaneDown), - Command::new("Swap Pane Next", SwapPaneNext), - Command::new("Swap Pane Previous", SwapPanePrev), - Command::new("Next Tab", NextTab), - Command::new("Previous Tab", PrevTab), - Command::new("Copy Working Directory", CopyWorkingDirectory), - Command::new("Copy Session ID", CopyAgentSessionId) - .with_subtitle("the coding agent's own session id"), - Command::new("Fork Session", ForkAgentSession) - .with_subtitle("branch this agent session into a new tab"), - Command::new("Mark Tab as Unread", MarkTabUnread), - Command::new("Close Pane / Tab", ClosePane), - Command::new("Close Other Tabs", CloseOtherTabs), - Command::new("Close Tabs to the Right", CloseTabsToTheRight), - Command::new("Reopen Closed Tab", ReopenClosedTab), + Command::new(t(L10nKey::CmdNewTab), NewTab), + Command::new(t(L10nKey::CmdNewWorktreeTab), NewWorktreeTab) + .with_subtitle(t(L10nKey::CmdNewWorktreeTabSubtitle)), + Command::new(t(L10nKey::CmdRenameTab), RenameTab), + Command::new(t(L10nKey::CmdSplitRight), SplitRight), + Command::new(t(L10nKey::CmdSplitDown), SplitDown), + Command::new(t(L10nKey::CmdZoomPane), ToggleMaximizePane), + Command::new(t(L10nKey::CmdNextPane), NextPane), + Command::new(t(L10nKey::CmdPreviousPane), PrevPane), + Command::new(t(L10nKey::CmdFocusPaneLeft), FocusPaneLeft), + Command::new(t(L10nKey::CmdFocusPaneRight), FocusPaneRight), + Command::new(t(L10nKey::CmdFocusPaneUp), FocusPaneUp), + Command::new(t(L10nKey::CmdFocusPaneDown), FocusPaneDown), + Command::new(t(L10nKey::CmdResizePaneLeft), ResizePaneLeft), + Command::new(t(L10nKey::CmdResizePaneRight), ResizePaneRight), + Command::new(t(L10nKey::CmdResizePaneUp), ResizePaneUp), + Command::new(t(L10nKey::CmdResizePaneDown), ResizePaneDown), + Command::new(t(L10nKey::CmdSwapPaneNext), SwapPaneNext), + Command::new(t(L10nKey::CmdSwapPanePrevious), SwapPanePrev), + Command::new(t(L10nKey::CmdNextTab), NextTab), + Command::new(t(L10nKey::CmdPreviousTab), PrevTab), + Command::new(t(L10nKey::CmdCopyWorkingDirectory), CopyWorkingDirectory), + Command::new(t(L10nKey::CmdCopySessionId), CopyAgentSessionId) + .with_subtitle(t(L10nKey::CmdCopySessionIdSubtitle)), + Command::new(t(L10nKey::CmdForkSession), ForkAgentSession) + .with_subtitle(t(L10nKey::CmdForkSessionSubtitle)), + Command::new(t(L10nKey::CmdMarkTabAsUnread), MarkTabUnread), + Command::new(t(L10nKey::CmdClosePaneTab), ClosePane), + Command::new(t(L10nKey::CmdCloseOtherTabs), CloseOtherTabs), + Command::new(t(L10nKey::CmdCloseTabsToTheRight), CloseTabsToTheRight), + Command::new(t(L10nKey::CmdReopenClosedTab), ReopenClosedTab), ]; let workspaces = [ - Command::new("New Workspace", NewWorkspace), - Command::new("Switch Workspace…", OpenWorkspacePicker), - Command::new("Rename Workspace…", RenameWorkspace), - Command::new("Stop Workspace…", StopWorkspace) - .with_subtitle("ends its shells, keeps the layout"), - Command::new("Delete Workspace…", DeleteWorkspace) - .with_subtitle("ends its shells and forgets the layout"), + Command::new(t(L10nKey::CmdNewWorkspace), NewWorkspace), + Command::new(t(L10nKey::CmdSwitchWorkspace), OpenWorkspacePicker), + Command::new(t(L10nKey::CmdRenameWorkspace), RenameWorkspace), + Command::new(t(L10nKey::CmdStopWorkspace), StopWorkspace) + .with_subtitle(t(L10nKey::CmdStopWorkspaceSubtitle)), + Command::new(t(L10nKey::CmdDeleteWorkspace), DeleteWorkspace) + .with_subtitle(t(L10nKey::CmdDeleteWorkspaceSubtitle)), ]; let view = [ Command::new( if sidebar_hidden { - "Show Left Sidebar" + t(L10nKey::CmdShowLeftSidebar) } else { - "Hide Left Sidebar" + t(L10nKey::CmdHideLeftSidebar) }, ToggleLeftPanel, ), Command::new( if right_panel_open { - "Hide Right Panel" + t(L10nKey::CmdHideRightPanel) } else { - "Show Right Panel" + t(L10nKey::CmdShowRightPanel) }, ToggleRightPanel, ), - Command::new("Show Code Panel", ToggleCodePanel), + Command::new(t(L10nKey::CmdShowCodePanel), ToggleCodePanel), Command::new( if tab_bar_left { - "Tab Bar: Move to Top" + t(L10nKey::CmdTabBarMoveToTop) } else { - "Tab Bar: Move to Left Sidebar" + t(L10nKey::CmdTabBarMoveToLeftSidebar) }, ToggleTabSidebar, ), - Command::new("Right Panel: Info", ShowRightPanel(RightPanelTab::Info)), Command::new( - "Right Panel: Outline", + t(L10nKey::CmdRightPanelInfo), + ShowRightPanel(RightPanelTab::Info), + ), + Command::new( + t(L10nKey::CmdRightPanelOutline), ShowRightPanel(RightPanelTab::Outline), ), Command::new( - "Right Panel: Changes", + t(L10nKey::CmdRightPanelChanges), ShowRightPanel(RightPanelTab::Changes), ), - Command::new("Right Panel: Files", ShowRightPanel(RightPanelTab::Files)), - Command::new("Change Theme…", OpenThemePicker), - Command::new("Reset Font Size", ResetFontSize), - Command::new("Enter Full Screen", ToggleFullscreen), + Command::new( + t(L10nKey::CmdRightPanelFiles), + ShowRightPanel(RightPanelTab::Files), + ), + Command::new(t(L10nKey::CmdChangeTheme), OpenThemePicker), + Command::new(t(L10nKey::CmdResetFontSize), ResetFontSize), + Command::new(t(L10nKey::CmdEnterFullScreen), ToggleFullscreen), ]; let terminal = [ - Command::new("Clear Scrollback", ClearTerminal), - Command::new("Find in Terminal…", FindInTerminal), - Command::new("Find Next", FindNext), - Command::new("Find Previous", FindPrevious), - Command::new("Copy", CopyText), - Command::new("Cut", CutText), - Command::new("Paste", PasteText), - Command::new("Select All", SelectAllText), + Command::new(t(L10nKey::CmdClearScrollback), ClearTerminal), + Command::new(t(L10nKey::CmdFindInTerminal), FindInTerminal), + Command::new(t(L10nKey::CmdFindNext), FindNext), + Command::new(t(L10nKey::CmdFindPrevious), FindPrevious), + Command::new(t(L10nKey::CmdCopy), CopyText), + Command::new(t(L10nKey::CmdCut), CutText), + Command::new(t(L10nKey::CmdPaste), PasteText), + Command::new(t(L10nKey::CmdSelectAll), SelectAllText), ]; let ssh = [ - Command::new("SSH: Add Connection…", OpenSshConnectInput), - Command::new("SSH: Manage Profiles…", OpenSshProfiles), - Command::new("SSH: Reconnect", RestartSshSession), - Command::new("SSH: Remote Files", ToggleSftp), - Command::new("SSH: Port Forwarding", ShowSshForwards), + Command::new(t(L10nKey::CmdSshAddConnection), OpenSshConnectInput), + Command::new(t(L10nKey::CmdSshManageProfiles), OpenSshProfiles), + Command::new(t(L10nKey::CmdSshReconnect), RestartSshSession), + Command::new(t(L10nKey::CmdSshRemoteFiles), ToggleSftp), + Command::new(t(L10nKey::CmdSshPortForwarding), ShowSshForwards), ]; let agents = [ - Command::new("Agent: Send Selection", SendSelectionToAgent) - .with_subtitle("selection → running coding agent"), - Command::new("Agent: Send Git Diff for Review", SendGitDiffToAgent) - .with_subtitle("git diff → running coding agent"), + Command::new(t(L10nKey::CmdAgentSendSelection), SendSelectionToAgent) + .with_subtitle(t(L10nKey::CmdAgentSendSelectionSubtitle)), + Command::new(t(L10nKey::CmdAgentSendGitDiffForReview), SendGitDiffToAgent) + .with_subtitle(t(L10nKey::CmdAgentSendGitDiffSubtitle)), ]; let application = [ - Command::new("Settings…", OpenSettings), - Command::new("Keyboard Shortcuts", ShowKeyboardShortcuts), - Command::new("About tty7", About), - Command::new("Check for Updates…", CheckForUpdates), - Command::new("Documentation", OpenDocumentation), - Command::new("Join the Discord", OpenDiscord), - Command::new("Report an Issue…", ReportIssue), - Command::new("Restart Server…", RestartDaemon) - .with_subtitle("ends every running shell; layout is kept"), - Command::new("Quit tty7", Quit).with_subtitle("shells keep running"), + Command::new(t(L10nKey::CmdSettings), OpenSettings), + Command::new(t(L10nKey::CmdKeyboardShortcuts), ShowKeyboardShortcuts), + Command::new(t(L10nKey::CmdAboutTty7), About), + Command::new(t(L10nKey::CmdCheckForUpdates), CheckForUpdates), + Command::new(t(L10nKey::CmdDocumentation), OpenDocumentation), + Command::new(t(L10nKey::CmdJoinDiscord), OpenDiscord), + Command::new(t(L10nKey::CmdReportIssue), ReportIssue), + Command::new(t(L10nKey::CmdRestartServer), RestartDaemon) + .with_subtitle(t(L10nKey::CmdRestartServerSubtitle)), + Command::new(t(L10nKey::CmdQuitTty7), Quit) + .with_subtitle(t(L10nKey::CmdQuitTty7Subtitle)), ]; let mut out = Vec::new(); @@ -504,10 +512,11 @@ impl Command { } fn ssh_connect_command(input: &str) -> Command { - let title = if input.trim().is_empty() { - "SSH: Add Connection…".to_string() + let trimmed = input.trim(); + let title = if trimmed.is_empty() { + t(L10nKey::CmdSshAddConnection).to_string() } else { - format!("SSH: Connect {}", input.trim()) + t_fmt(L10nKey::CmdSshConnectWithInput, &[("input", trimmed)]) }; Command::new(title, CommandKind::OpenSshConnect(input.to_string())) } @@ -644,7 +653,7 @@ impl PaletteDelegate { recent.truncate(RECENT_ROWS); if !recent.is_empty() { sections.push(Section { - title: Some("Recent".into()), + title: Some(t(L10nKey::CmdRecent).into()), commands: recent.into_iter().map(|(_, c)| c.clone()).collect(), }); } @@ -675,11 +684,11 @@ impl PaletteDelegate { let target = query.trim().to_string(); vec![ Command::new( - format!("Connect to \"{target}\""), + t_fmt(L10nKey::CmdQuickConnect, &[("target", &target)]), CommandKind::QuickConnect(target.clone()), ), Command::new( - format!("Save \"{target}\" as profile…"), + t_fmt(L10nKey::CmdQuickConnectSaveProfile, &[("target", &target)]), CommandKind::SaveQuickConnect(target), ), ] @@ -803,11 +812,13 @@ impl ListDelegate for PaletteDelegate { .items_center() .text_sm() .text_color(cx.theme().muted_foreground) - .child("No matching commands") + .child(crate::ui::i18n::t( + crate::ui::i18n::L10nKey::NoMatchingCommands, + )) .child( div() .text_xs() - .child("Type user@host to connect over SSH instead."), + .child(crate::ui::i18n::t(crate::ui::i18n::L10nKey::ConnectSshHint)), ) } @@ -840,7 +851,12 @@ impl ListDelegate for PaletteDelegate { .justify_between() .child(left); if cmd.kind.edit_variant().is_some() { - row = row.child(div().text_xs().text_color(muted).child("→ edit")); + row = row.child( + div() + .text_xs() + .text_color(muted) + .child(crate::ui::i18n::t(crate::ui::i18n::L10nKey::EditHint)), + ); } if let Some(tokens) = keys { row = row.child(h_flex().gap_1().children(tokens.into_iter().map(move |t| { @@ -958,8 +974,8 @@ impl PaletteView { fn search_placeholder(&self) -> &'static str { match self.menu { PaletteMenu::SshConnect => "user@host [-p 2222 -J jump]", - PaletteMenu::Root => "Search or type user@host to connect…", - PaletteMenu::Theme => "Search…", + PaletteMenu::Root => t(crate::ui::i18n::L10nKey::SearchCommandsOrHost), + PaletteMenu::Theme => t(crate::ui::i18n::L10nKey::SearchTheme), } } @@ -1094,6 +1110,7 @@ mod tests { #[test] fn host_like_queries_get_connect_and_save_rows() { + crate::ui::i18n::set_locale("en"); for q in [ "deploy@10.0.0.5", "host.example.com", @@ -1105,8 +1122,8 @@ mod tests { assert_eq!( titles, vec![ - format!("Connect to \"{q}\""), - format!("Save \"{q}\" as profile…"), + t_fmt(L10nKey::CmdQuickConnect, &[("target", q)]), + t_fmt(L10nKey::CmdQuickConnectSaveProfile, &[("target", q)]), ], "query {q:?}" ); diff --git a/src/ui/pending_pane.rs b/src/ui/pending_pane.rs index a54b037c..93ed11c2 100644 --- a/src/ui/pending_pane.rs +++ b/src/ui/pending_pane.rs @@ -9,6 +9,7 @@ use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _, h_flex, v_f use crate::daemon::protocol::ShellSpec; use crate::terminal::PaneWorkspace; +use crate::ui::i18n::{L10nKey, t_fmt}; #[derive(Clone)] pub struct PendingSpawn { @@ -93,23 +94,19 @@ impl Render for PendingPane { }, ), ) - .child( - div() - .text_sm() - .text_color(muted) - .child(format!("Connecting to {}…", self.machine)), - ) + .child(div().text_sm().text_color(muted).child(t_fmt( + L10nKey::PendingConnecting, + &[("machine", &self.machine)], + ))) .into_any_element(), PendingState::Failed(reason) => v_flex() .items_center() .gap(px(10.)) .max_w(px(420.)) - .child( - div() - .text_sm() - .text_color(theme.foreground) - .child(format!("Couldn't reach {}", self.machine)), - ) + .child(div().text_sm().text_color(theme.foreground).child(t_fmt( + L10nKey::PendingUnreachable, + &[("machine", &self.machine)], + ))) .child( div() .text_xs() @@ -119,7 +116,7 @@ impl Render for PendingPane { ) .child( Button::new("pending-pane-retry") - .label("Try Again") + .label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::TryAgain)) .ghost() .small() .on_click(cx.listener(|this, _, _window, cx| { diff --git a/src/ui/presets.rs b/src/ui/presets.rs index a8306847..be0d9787 100644 --- a/src/ui/presets.rs +++ b/src/ui/presets.rs @@ -499,6 +499,9 @@ pub fn fork_to_file(t: &Theme) -> std::io::Result { n += 1; } let mut copy = t.clone(); + // The name lands in the YAML on disk and is matched back with + // `trim_end_matches(" (custom)")`, so it stays English in every locale — + // a translated suffix would survive a language switch and stack up. copy.name = format!("{} (custom)", t.name.trim_end_matches(" (custom)")); crate::core::config::write_atomic( &dir.join(format!("{stem}.yaml")), @@ -592,6 +595,8 @@ fn id_and_name(path: &std::path::Path) -> (String, String) { ( stem, if name.is_empty() { + // Theme names are data — they get written back to the YAML file, + // so the fallback stays English rather than following the GUI. "Theme".into() } else { name @@ -1442,6 +1447,15 @@ ansi: assert_eq!(name, "Solarized Dark"); } + #[test] + fn theme_names_stay_english_under_a_translated_gui() { + crate::ui::i18n::set_locale("zh-CN"); + // A stem of only separators leaves nothing to title-case. + let (_, name) = id_and_name(std::path::Path::new("/x/_.yaml")); + assert_eq!(name, "Theme"); + crate::ui::i18n::set_locale("en"); + } + #[test] fn mix_blends_channels() { assert_eq!(mix(0x000000, 0xffffff, 0.0), 0x000000); diff --git a/src/ui/remote_connect.rs b/src/ui/remote_connect.rs index b06f1d83..d3065467 100644 --- a/src/ui/remote_connect.rs +++ b/src/ui/remote_connect.rs @@ -15,6 +15,7 @@ use crate::daemon::install::{ }; use crate::daemon::protocol::{AuthPromptKind, AuthResponse, NativeSshSpec}; use crate::daemon::router::RouteHeader; +use crate::ui::i18n::{L10nKey, t, t_fmt}; use tty7_core::host::remote::RemoteHost; use tty7_core::host::{Host as _, HostId}; @@ -106,12 +107,14 @@ fn local_stdio_host() -> Option { }; Some(HostChoice { label: format!("{target}"), - detail: format!("{program} --stdio (this computer)"), + detail: format!("{program} --stdio ({})", t(L10nKey::RemoteThisComputer)), target, }) } -const WSL_DETAIL: &str = "WSL · this computer"; +fn wsl_detail() -> String { + format!("WSL · {}", t(L10nKey::RemoteThisComputer)) +} fn wsl_hosts(cx: &App) -> Vec { let names = cx @@ -129,7 +132,7 @@ fn wsl_choices(names: &[String]) -> Vec { distro: distro.clone(), }, label: distro.clone(), - detail: WSL_DETAIL.to_string(), + detail: wsl_detail(), }) .collect() } @@ -197,7 +200,7 @@ pub fn spec_for(target: &RemoteTarget, cx: &App) -> Result Result { let resolved = crate::core::ssh_config::resolve_alias_to_profile(alias) - .ok_or_else(|| format!("`{alias}` is no longer in ~/.ssh/config"))?; + .ok_or_else(|| t_fmt(L10nKey::RemoteAliasMissing, &[("alias", alias)]))?; Ok(crate::ui::ssh_connect::native_spec_from_transient_profile( &resolved.profile, resolved.proxy_jump, @@ -228,10 +231,8 @@ pub fn spec_for(target: &RemoteTarget, cx: &App) -> Result Err("a WSL workspace has no SSH connection".to_string()), - RemoteTarget::LocalStdio { .. } => { - Err("a local --stdio workspace has no SSH connection".to_string()) - } + RemoteTarget::Wsl { .. } => Err(t(L10nKey::RemoteWslNoSsh).to_string()), + RemoteTarget::LocalStdio { .. } => Err(t(L10nKey::RemoteLocalStdioNoSsh).to_string()), } } @@ -260,22 +261,42 @@ pub fn connect_blocking( label: &str, ) -> Result { note_origin(&header.target, target); - crate::daemon::spawn::ensure_running() - .map_err(|e| format!("tty7's local server could not be started: {e}"))?; + crate::daemon::spawn::ensure_running().map_err(|e| { + t_fmt( + L10nKey::RemoteDaemonStartFailed, + &[("error", &e.to_string())], + ) + })?; - let stream = crate::daemon::transport::connect() - .map_err(|e| format!("could not reach tty7's local server: {e}"))?; + let stream = crate::daemon::transport::connect().map_err(|e| { + t_fmt( + L10nKey::RemoteDaemonUnreachable, + &[("error", &e.to_string())], + ) + })?; let mut stream = stream; - crate::daemon::router::negotiate(&mut stream, &header) - .map_err(|e| format!("could not reach {label}: {e}"))?; + crate::daemon::router::negotiate(&mut stream, &header).map_err(|e| { + t_fmt( + L10nKey::RemoteHostUnreachable, + &[("machine", label), ("error", &e.to_string())], + ) + })?; let hello = ControlHello::host_rpc(new_session_token(), client_hostname()); - let host = handshake(stream, &target.connection_key(), &hello) - .map_err(|e| format!("{label} answered, but not as a tty7 server: {e}"))?; + let host = handshake(stream, &target.connection_key(), &hello).map_err(|e| { + t_fmt( + L10nKey::RemoteHostNotTty7, + &[("machine", label), ("error", &e.to_string())], + ) + })?; - let rows = list_workspaces(&host) - .map_err(|e| format!("connected to {label}, but its workspace list failed: {e}"))?; + let rows = list_workspaces(&host).map_err(|e| { + t_fmt( + L10nKey::RemoteWorkspaceListFailed, + &[("machine", label), ("error", &e.to_string())], + ) + })?; let home = host.home(); refresh_agent_hooks_once(&host, &home); Ok(Connected { host, home, rows }) @@ -321,7 +342,10 @@ pub fn list_workspaces(host: &Arc) -> io::Result Ok(rows_from_machine(&machine)), other => Err(io::Error::new( io::ErrorKind::InvalidData, - format!("the server answered a machine tree with {other:?}"), + t_fmt( + L10nKey::RemoteMachineTreeUnexpectedReply, + &[("reply", &format!("{other:?}"))], + ), )), } } @@ -405,36 +429,37 @@ impl HostLinks { } pub fn install_detail(request: &InstallRequest) -> String { - format!( - "tty7 will write its server binary to {machine} so this machine can host \ - workspaces there. Nothing else on {machine} is touched, and no sudo is used.\n\ - \n\ - Path\u{2003}{path}\n\ - Version\u{2003}{version} ({asset})\n\ - Size\u{2003}{size}\n\ - From\u{2003}{url}\n\ - SHA-256\u{2003}{sha}\n\ - \n\ - Later upgrades on this machine install silently.", - machine = request.host, - path = request.remote_path, - version = request.version, - asset = request.asset, - size = human_bytes(request.size_bytes), - url = request.source_url, - sha = request.sha256, + t_fmt( + L10nKey::RemoteInstallDetail, + &[ + ("machine", &request.host), + ("path", &request.remote_path), + ( + "version", + &format!("{} ({})", request.version, request.asset), + ), + ("size", &human_bytes(request.size_bytes)), + ("from", &request.source_url), + ("sha256", &request.sha256), + ("path_label", t(L10nKey::RemoteInstallPathLabel)), + ("version_label", t(L10nKey::RemoteInstallVersionLabel)), + ("size_label", t(L10nKey::RemoteInstallSizeLabel)), + ("from_label", t(L10nKey::RemoteInstallFromLabel)), + ("sha_label", t(L10nKey::RemoteInstallShaLabel)), + ("silent_upgrades", t(L10nKey::RemoteInstallSilentUpgrades)), + ], ) } pub fn install_title(request: &InstallRequest) -> String { - format!("Install tty7's server on \u{201c}{}\u{201d}?", request.host) + t_fmt(L10nKey::RemoteInstallTitle, &[("machine", &request.host)]) } pub fn human_bytes(n: u64) -> String { const KIB: f64 = 1024.0; let n = n as f64; if n < KIB { - return format!("{} bytes", n as u64); + return format!("{} {}", n as u64, t(L10nKey::RemoteInstallBytes)); } let units = ["KiB", "MiB", "GiB"]; let mut value = n / KIB; @@ -615,29 +640,34 @@ pub(crate) fn claim_mailbox() -> std::sync::MutexGuard<'static, ()> { MAILBOX_TURN.lock().unwrap_or_else(|e| e.into_inner()) } -pub const MISMATCH_ANSWERS: [&str; 2] = ["Cancel", "Restart Server"]; +pub fn mismatch_answers() -> [&'static str; 2] { + [t(L10nKey::Cancel), t(L10nKey::RestartServer)] +} pub fn mismatch_detail(m: &MismatchedRemoteDaemon) -> String { let running = match (&m.running_version, &m.running_exe) { - (Some(v), Some(exe)) => format!("{v} (from {exe})"), + (Some(v), Some(exe)) => t_fmt( + L10nKey::RemoteMismatchVersionFromExe, + &[("version", v), ("exe", exe)], + ), (Some(v), None) => v.clone(), - (None, Some(exe)) => format!("an unknown build (from {exe})"), - (None, None) => "an unknown build".to_string(), + (None, Some(exe)) => t_fmt(L10nKey::RemoteMismatchUnknownBuildFromExe, &[("exe", exe)]), + (None, None) => t(L10nKey::RemoteMismatchUnknownBuild).to_string(), }; - format!( - "{host} is serving tty7 sessions from {running}, which speaks a protocol \ - this client ({wanted}) cannot. tty7 has installed a matching server there, \ - but the one already running is the one your sessions are on.\n\ - \n\ - Restart Server\u{2003}starts {wanted} there and ends every session it is hosting.\n\ - Cancel\u{2003}leaves {host} exactly as it is. This window will not connect.", - host = m.host, - wanted = m.wanted_version, + t_fmt( + L10nKey::RemoteMismatchDetail, + &[ + ("machine", &m.host), + ("running", &running), + ("wanted", &m.wanted_version), + ("restart_server", t(L10nKey::RestartServer)), + ("cancel", t(L10nKey::Cancel)), + ], ) } pub fn mismatch_title(m: &MismatchedRemoteDaemon) -> String { - format!("Restart tty7's server on \u{201c}{}\u{201d}?", m.host) + t_fmt(L10nKey::RemoteMismatchTitle, &[("machine", &m.host)]) } pub fn mismatch_target(m: &MismatchedRemoteDaemon) -> Option { @@ -646,17 +676,26 @@ pub fn mismatch_target(m: &MismatchedRemoteDaemon) -> Option { pub fn restart_server_blocking(header: RouteHeader, label: &str) -> Result<(), String> { let action = header.action; - crate::daemon::spawn::ensure_running() - .map_err(|e| format!("tty7's local server could not be started: {e}"))?; - let mut stream = crate::daemon::transport::connect() - .map_err(|e| format!("could not reach tty7's local server: {e}"))?; - let ack = crate::daemon::router::negotiate(&mut stream, &header) - .map_err(|e| format!("could not restart tty7's server on {label}: {e}"))?; + crate::daemon::spawn::ensure_running().map_err(|e| { + t_fmt( + L10nKey::RemoteDaemonStartFailed, + &[("error", &e.to_string())], + ) + })?; + let mut stream = crate::daemon::transport::connect().map_err(|e| { + t_fmt( + L10nKey::RemoteDaemonUnreachable, + &[("error", &e.to_string())], + ) + })?; + let ack = crate::daemon::router::negotiate(&mut stream, &header).map_err(|e| { + t_fmt( + L10nKey::RemoteServerRestartFailed, + &[("machine", label), ("error", &e.to_string())], + ) + })?; if !ack.performed(action) { - return Err(format!( - "this machine's tty7 daemon is an older build and cannot restart the server on \ - {label}. Quit tty7 (which stops the daemon) and open it again, then retry." - )); + return Err(t_fmt(L10nKey::RemoteDaemonTooOld, &[("machine", label)])); } Ok(()) } @@ -679,6 +718,7 @@ mod tests { #[test] fn the_install_prompt_states_every_field_of_the_request() { + crate::ui::i18n::set_locale("en"); let request = request(); let detail = install_detail(&request); for needle in [ @@ -694,12 +734,22 @@ mod tests { ); } assert!(detail.contains("9.0 MiB"), "{detail}"); - assert!(install_title(&request).contains("me@build-box:22")); + assert_eq!( + install_title(&request), + t_fmt( + L10nKey::RemoteInstallTitle, + &[("machine", "me@build-box:22")] + ) + ); } #[test] fn human_bytes_reads_in_binary_units() { - assert_eq!(human_bytes(512), "512 bytes"); + crate::ui::i18n::set_locale("en"); + assert_eq!( + human_bytes(512), + format!("512 {}", t(L10nKey::RemoteInstallBytes)) + ); assert_eq!(human_bytes(1024), "1.0 KiB"); assert_eq!(human_bytes(1_572_864), "1.5 MiB"); assert_eq!(human_bytes(3 * 1024 * 1024 * 1024), "3.0 GiB"); @@ -859,6 +909,7 @@ mod tests { #[test] fn the_mismatch_prompt_names_the_host_and_both_versions() { + crate::ui::i18n::set_locale("en"); let m = MismatchedRemoteDaemon { host: "me@build-box:22".into(), running_version: Some("0.8.0".into()), @@ -869,25 +920,35 @@ mod tests { assert!(detail.contains("0.8.0"), "{detail}"); assert!(detail.contains("0.9.1"), "{detail}"); assert!(detail.contains("me@build-box:22"), "{detail}"); - assert!(mismatch_title(&m).contains("me@build-box:22")); + assert_eq!( + mismatch_title(&m), + t_fmt( + L10nKey::RemoteMismatchTitle, + &[("machine", "me@build-box:22")] + ) + ); let unknown = MismatchedRemoteDaemon { running_version: None, running_exe: None, ..m }; - assert!(mismatch_detail(&unknown).contains("an unknown build")); + assert!( + mismatch_detail(&unknown).contains(t(L10nKey::RemoteMismatchUnknownBuild)), + "{detail}" + ); } #[test] fn the_mismatch_detail_explains_every_answer_the_prompt_offers() { + crate::ui::i18n::set_locale("en"); let detail = mismatch_detail(&MismatchedRemoteDaemon { host: "me@build-box:22".into(), running_version: Some("0.8.0".into()), running_exe: None, wanted_version: "0.9.1".into(), }); - for answer in MISMATCH_ANSWERS { + for answer in mismatch_answers() { assert!(detail.contains(answer), "{answer} is unexplained: {detail}"); } } diff --git a/src/ui/remote_workspace.rs b/src/ui/remote_workspace.rs index fa34bcaf..a49d457a 100644 --- a/src/ui/remote_workspace.rs +++ b/src/ui/remote_workspace.rs @@ -11,6 +11,7 @@ use crate::core::session::{RemoteRef, RemoteTarget, WorkspaceId, WorkspaceStore} use crate::daemon::control::{ControlEvent, ControlRequest, ReplyOk}; use crate::daemon::install::InstallDecision; use crate::ui::app::Tty7App; +use crate::ui::i18n::{L10nKey, t, t_fmt}; use crate::ui::remote_connect::{self, HostChoice, RemoteWorkspaceRow}; pub enum ConnectFlow { @@ -40,35 +41,47 @@ impl RemoteStatus { pub fn strip_message(&self, machine: &str) -> Option { match self { RemoteStatus::Attached => None, - RemoteStatus::Disconnected => Some(format!("Not connected to {machine}")), - RemoteStatus::Connecting => Some(format!("Connecting to {machine}…")), - RemoteStatus::Reconnecting { attempt: 0 } => { - Some(format!("Reconnecting to {machine}…")) + RemoteStatus::Disconnected => Some(t_fmt( + L10nKey::RemoteStripDisconnected, + &[("machine", machine)], + )), + RemoteStatus::Connecting => Some(t_fmt( + L10nKey::RemoteStripConnecting, + &[("machine", machine)], + )), + RemoteStatus::Reconnecting { attempt: 0 } => Some(t_fmt( + L10nKey::RemoteStripReconnecting, + &[("machine", machine)], + )), + RemoteStatus::Reconnecting { attempt } => Some(t_fmt( + L10nKey::RemoteStripReconnectingAttempt, + &[("machine", machine), ("count", &(attempt + 1).to_string())], + )), + RemoteStatus::Preempted { by } => { + Some(t_fmt(L10nKey::RemoteStripPreempted, &[("by", by)])) } - RemoteStatus::Reconnecting { attempt } => Some(format!( - "Reconnecting to {machine}… (attempt {})", - attempt + 1 + RemoteStatus::Failed(e) => Some(t_fmt( + L10nKey::RemoteStripFailed, + &[("machine", machine), ("error", e)], )), - RemoteStatus::Preempted { by } => Some(format!("This workspace was opened on {by}")), - RemoteStatus::Failed(e) => Some(format!("Not connected to {machine} — {e}")), } } pub fn input_notice(&self) -> Option<&'static str> { match self { RemoteStatus::Attached => None, - RemoteStatus::Preempted { .. } => Some("Opened elsewhere — typing has no effect"), - _ => Some("Not connected — typing has no effect"), + RemoteStatus::Preempted { .. } => Some(t(L10nKey::RemoteNoticePreempted)), + _ => Some(t(L10nKey::RemoteNoticeDisconnected)), } } pub fn action_label(&self) -> Option<&'static str> { match self { RemoteStatus::Attached | RemoteStatus::Connecting => None, - RemoteStatus::Reconnecting { .. } => Some("Retry Now"), - RemoteStatus::Preempted { .. } => Some("Take Back"), - RemoteStatus::Disconnected => Some("Connect"), - RemoteStatus::Failed(_) => Some("Retry"), + RemoteStatus::Reconnecting { .. } => Some(t(L10nKey::RemoteActionRetryNow)), + RemoteStatus::Preempted { .. } => Some(t(L10nKey::RemoteActionTakeBack)), + RemoteStatus::Disconnected => Some(t(L10nKey::RemoteActionConnect)), + RemoteStatus::Failed(_) => Some(t(L10nKey::RemoteActionRetry)), } } @@ -184,11 +197,7 @@ impl Tty7App { _ => { let machine = self.remote_machine_label(cx); window.push_notification( - format!( - "This window is a workspace on {machine}, but tty7 has no connection \ - details for it any more — check that its SSH profile or ~/.ssh/config \ - entry still exists." - ), + t_fmt(L10nKey::RemoteNoConnectionDetails, &[("machine", &machine)]), cx, ); false @@ -206,7 +215,7 @@ impl Tty7App { pub(crate) fn remote_machine_label(&self, cx: &gpui::App) -> String { match WorkspaceStore::remote_ref(cx, self.workspace) { Some(host) => host.target.to_string(), - None => "this computer".to_string(), + None => t(L10nKey::RemoteThisComputer).to_string(), } } @@ -437,7 +446,7 @@ impl Tty7App { PromptLevel::Warning, &title, Some(&detail), - &["Cancel", "Install"], + &[t(L10nKey::Cancel), t(L10nKey::SettingsInstall)], cx, ); cx.spawn(async move |_, _| { @@ -458,7 +467,7 @@ impl Tty7App { PromptLevel::Warning, &title, Some(&detail), - &remote_connect::MISMATCH_ANSWERS, + &remote_connect::mismatch_answers(), cx, ); cx.spawn(async move |this, cx| { @@ -481,7 +490,7 @@ impl Tty7App { ) { let label = mismatch.host.clone(); match remote_connect::mismatch_target(&mismatch) - .ok_or_else(|| format!("tty7 no longer has a way to reach {label}")) + .ok_or_else(|| t_fmt(L10nKey::RemoteNoRouteToHost, &[("machine", &label)])) { Ok(target) => self.restart_remote_server(target, label, window, cx), Err(e) => Tty7App::report_restart_failure(&label, &e, window, cx), @@ -497,13 +506,9 @@ impl Tty7App { ) { let answer = window.prompt( PromptLevel::Warning, - &format!("Restart tty7's server on \u{201c}{label}\u{201d}?"), - Some(&format!( - "This stops every shell on {label} — anything still running in them \ - will be terminated, including shells this window is not showing. \ - Workspaces and layouts are kept and come back with fresh shells." - )), - &["Cancel", "Restart Server"], + &t_fmt(L10nKey::RemoteRestartTitle, &[("machine", &label)]), + Some(&t_fmt(L10nKey::RemoteRestartBody, &[("machine", &label)])), + &[t(L10nKey::Cancel), t(L10nKey::RestartServer)], cx, ); cx.spawn(async move |this, cx| { @@ -567,16 +572,9 @@ impl Tty7App { ) { let answer = window.prompt( PromptLevel::Warning, - &format!("Restart tty7's server on \u{201c}{label}\u{201d}?"), - Some(&format!( - "The tty7-server running on {label} speaks a protocol this client cannot. \ - tty7 will restart the service there onto one that does, installing it \ - first if {label} does not already have it.\n\ - \n\ - Every session running on {label} ends, including any this window is not \ - connected to." - )), - &["Cancel", "Restart Server"], + &t_fmt(L10nKey::RemoteRestartTitle, &[("machine", &label)]), + Some(&t_fmt(L10nKey::RemoteReplaceBody, &[("machine", &label)])), + &[t(L10nKey::Cancel), t(L10nKey::RestartServer)], cx, ); cx.spawn(async move |this, cx| { @@ -640,12 +638,12 @@ impl Tty7App { ) { let answer = window.prompt( PromptLevel::Warning, - &format!("tty7's server on \u{201c}{label}\u{201d} was not restarted"), - Some(&format!( - "{error}\n\nSessions still running there are on the older build. If they are \ - gone, reconnecting starts this build's server." + &t_fmt(L10nKey::RemoteRestartFailedTitle, &[("machine", label)]), + Some(&t_fmt( + L10nKey::RemoteRestartFailedBody, + &[("error", error)], )), - &["OK"], + &[t(L10nKey::Ok)], cx, ); cx.spawn(async move |_, _| { @@ -1441,24 +1439,28 @@ mod tests { #[test] fn the_status_strip_speaks_unless_everything_is_working() { + crate::ui::i18n::set_locale("en"); assert_eq!(RemoteStatus::Attached.strip_message("build-box"), None); assert_eq!( - RemoteStatus::Disconnected - .strip_message("build-box") - .as_deref(), - Some("Not connected to build-box") + RemoteStatus::Disconnected.strip_message("build-box"), + Some(t_fmt( + L10nKey::RemoteStripDisconnected, + &[("machine", "build-box")] + )) ); assert_eq!( - RemoteStatus::Connecting - .strip_message("build-box") - .as_deref(), - Some("Connecting to build-box…") + RemoteStatus::Connecting.strip_message("build-box"), + Some(t_fmt( + L10nKey::RemoteStripConnecting, + &[("machine", "build-box")] + )) ); assert_eq!( - RemoteStatus::Failed("connection refused".into()) - .strip_message("build-box") - .as_deref(), - Some("Not connected to build-box — connection refused") + RemoteStatus::Failed("connection refused".into()).strip_message("build-box"), + Some(t_fmt( + L10nKey::RemoteStripFailed, + &[("machine", "build-box"), ("error", "connection refused")] + )) ); } @@ -1626,39 +1628,40 @@ mod tests { #[test] fn every_state_says_what_it_means_for_the_keyboard() { + crate::ui::i18n::set_locale("en"); let cases = [ (RemoteStatus::Attached, true, None, None), ( RemoteStatus::Disconnected, false, - Some("Not connected — typing has no effect"), - Some("Connect"), + Some(t(L10nKey::RemoteNoticeDisconnected)), + Some(t(L10nKey::RemoteActionConnect)), ), ( RemoteStatus::Connecting, false, - Some("Not connected — typing has no effect"), + Some(t(L10nKey::RemoteNoticeDisconnected)), None, ), ( RemoteStatus::Reconnecting { attempt: 2 }, false, - Some("Not connected — typing has no effect"), - Some("Retry Now"), + Some(t(L10nKey::RemoteNoticeDisconnected)), + Some(t(L10nKey::RemoteActionRetryNow)), ), ( RemoteStatus::Preempted { by: "desktop".into(), }, false, - Some("Opened elsewhere — typing has no effect"), - Some("Take Back"), + Some(t(L10nKey::RemoteNoticePreempted)), + Some(t(L10nKey::RemoteActionTakeBack)), ), ( RemoteStatus::Failed("no route to host".into()), false, - Some("Not connected — typing has no effect"), - Some("Retry"), + Some(t(L10nKey::RemoteNoticeDisconnected)), + Some(t(L10nKey::RemoteActionRetry)), ), ]; for (status, accepts, notice, action) in cases { @@ -1670,26 +1673,28 @@ mod tests { #[test] fn the_new_states_name_what_happened() { + crate::ui::i18n::set_locale("en"); assert_eq!( - RemoteStatus::Reconnecting { attempt: 0 } - .strip_message("build-box") - .as_deref(), - Some("Reconnecting to build-box…"), + RemoteStatus::Reconnecting { attempt: 0 }.strip_message("build-box"), + Some(t_fmt( + L10nKey::RemoteStripReconnecting, + &[("machine", "build-box")] + )), "the first attempt does not need a count" ); assert_eq!( - RemoteStatus::Reconnecting { attempt: 3 } - .strip_message("build-box") - .as_deref(), - Some("Reconnecting to build-box… (attempt 4)") + RemoteStatus::Reconnecting { attempt: 3 }.strip_message("build-box"), + Some(t_fmt( + L10nKey::RemoteStripReconnectingAttempt, + &[("machine", "build-box"), ("count", "4")] + )) ); assert_eq!( RemoteStatus::Preempted { by: "desktop".into() } - .strip_message("build-box") - .as_deref(), - Some("This workspace was opened on desktop") + .strip_message("build-box"), + Some(t_fmt(L10nKey::RemoteStripPreempted, &[("by", "desktop")])) ); } diff --git a/src/ui/right_panel.rs b/src/ui/right_panel.rs index 80bfacba..d1f17786 100644 --- a/src/ui/right_panel.rs +++ b/src/ui/right_panel.rs @@ -14,6 +14,7 @@ use crate::ui::app::{ CONTENT_INSET, TILE_GLYPH_SM, TILE_SIZE_SM, Tty7App, tile_trailing_inset, tile_trailing_inset_sm, }; +use crate::ui::i18n::{L10nKey, t, t_plural}; use crate::ui::scrollbar::with_vertical_scrollbar; pub(crate) const MIN_WIDTH: f32 = 216.; @@ -355,7 +356,7 @@ impl Tty7App { } fn render_panel_info(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { - let title = self.panel_title("Info", None, None, window, cx); + let title = self.panel_title(t(L10nKey::PanelInfoTitle), None, None, window, cx); let mut rows: Vec<(&'static str, String)> = Vec::new(); let mut cwd_for_actions: Option = None; let mut pane_id: Option = None; @@ -370,16 +371,16 @@ impl Tty7App { .map(|p| p.to_path_buf()) .or_else(|| view.cwd()) { - rows.push(("cwd", compact_path(&cwd))); + rows.push((t(L10nKey::PanelCwd), compact_path(&cwd))); cwd_for_actions = Some(cwd); } let shell = match view.shell_spec().map(|s| s.program.clone()) { Some(program) => crate::core::shells::default_shell_name(Some(&program)), None => self.default_shell_label(cx), }; - rows.push(("shell", shell)); + rows.push((t(L10nKey::PanelShell), shell)); if let Some(ssh) = view.ssh_spec() { - rows.push(("ssh", ssh.host.clone())); + rows.push((t(L10nKey::PanelSsh), ssh.host.clone())); } let connected_ssh = view .remote_context() @@ -393,8 +394,11 @@ impl Tty7App { } } if let Some(git) = tab.git_status(Some(window), cx) { - rows.push(("branch", git.branch.clone())); - rows.push(("changes", format!("+{} −{}", git.added, git.removed))); + rows.push((t(L10nKey::PanelBranch), git.branch.clone())); + rows.push(( + t(L10nKey::PanelChangesRow), + format!("+{} −{}", git.added, git.removed), + )); } if let Some(agent) = tab.agent(cx) { let name = agent.display_name(); @@ -402,15 +406,15 @@ impl Tty7App { Some(s) => format!("{name} · {}", agent_status_label(s)), None => name.to_string(), }; - rows.push(("agent", status)); + rows.push((t(L10nKey::PanelAgent), status)); } } if rows.is_empty() { return self.panel_scroll( self.panel_empty( - "No active session.", - Some("Open a tab to see its shell, directory, and processes here."), + t(L10nKey::PanelNoSession), + Some(t(L10nKey::PanelNoSessionHint)), cx, ), title, @@ -449,7 +453,7 @@ impl Tty7App { } let inner = v_flex() - .child(self.panel_subtitle("Session", false, None, cx)) + .child(self.panel_subtitle(t(L10nKey::PanelSessionSubtitle), false, None, cx)) .child(list) .when_some(cwd_for_actions, |this, cwd| { this.child(self.cwd_actions(cwd, cx)) @@ -491,7 +495,7 @@ impl Tty7App { cx, ) .rounded_md() - .tooltip("Copy Path") + .tooltip(t(L10nKey::FileTreeContextCopyPath)) .on_click(move |_, _window, cx| { cx.write_to_clipboard(gpui::ClipboardItem::new_string( cwd.display().to_string(), @@ -575,7 +579,7 @@ impl Tty7App { } Some( v_flex() - .child(self.panel_subtitle("Processes", true, None, cx)) + .child(self.panel_subtitle(t(L10nKey::PanelProcessesSubtitle), true, None, cx)) .child(list) .into_any_element(), ) @@ -613,7 +617,7 @@ impl Tty7App { } Some( v_flex() - .child(self.panel_subtitle("Ports", true, None, cx)) + .child(self.panel_subtitle(t(L10nKey::PanelPortsSubtitle), true, None, cx)) .child(list) .into_any_element(), ) @@ -708,11 +712,11 @@ impl Tty7App { .get(self.active) .and_then(|t| t.detail_pane(window, cx)) else { - let title = self.panel_title("Outline", None, None, window, cx); + let title = self.panel_title(t(L10nKey::PanelOutlineTitle), None, None, window, cx); return self.panel_scroll( self.panel_empty( - "No active session.", - Some("Open a tab to see its shell, directory, and processes here."), + t(L10nKey::PanelNoSession), + Some(t(L10nKey::PanelNoSessionHint)), cx, ), title, @@ -720,17 +724,23 @@ impl Tty7App { }; let count = leaf.read(cx).command_marks().len(); if count == 0 { - let title = self.panel_title("Outline", None, None, window, cx); + let title = self.panel_title(t(L10nKey::PanelOutlineTitle), None, None, window, cx); return self.panel_scroll( self.panel_empty( - "No commands recorded for this pane.", - Some("Run a command — shell integration marks each one so you can jump back to it."), + t(L10nKey::PanelNoCommands), + Some(t(L10nKey::PanelNoCommandsHint)), cx, ), title, ); } - let title = self.panel_title("Outline", Some(count.to_string()), None, window, cx); + let title = self.panel_title( + t(L10nKey::PanelOutlineTitle), + Some(count.to_string()), + None, + window, + cx, + ); let mono = cx.theme().mono_font_family.clone(); let mut list = v_flex().px(px(CONTENT_INSET - 4.)).py(px(2.)).gap(px(1.)); @@ -812,11 +822,11 @@ impl Tty7App { }); let Some((host, cwd)) = target else { - let title = self.panel_title("Changes", None, None, window, cx); + let title = self.panel_title(t(L10nKey::PanelChangesTitle), None, None, window, cx); return self.panel_scroll( self.panel_empty( - "No working directory.", - Some("This pane has not reported one yet."), + t(L10nKey::PanelNoWorkingDirectory), + Some(t(L10nKey::PanelNoWorkingDirectoryHint)), cx, ), title, @@ -838,20 +848,20 @@ impl Tty7App { } _ => None, }; - let title = self.panel_title("Changes", count, None, window, cx); + let title = self.panel_title(t(L10nKey::PanelChangesTitle), count, None, window, cx); let mono = cx.theme().mono_font_family.clone(); let inner = match &self.right_panel.diff { - None => self.panel_empty("Loading…", None, cx), + None => self.panel_empty(t(L10nKey::PanelLoading), None, cx), Some(None) => self.panel_empty( - "Not a git repository.", - Some("cd into one and this tab lists its uncommitted changes."), + t(L10nKey::PanelNotAGitRepo), + Some(t(L10nKey::PanelNotAGitRepoHint)), cx, ), Some(Some(snap)) if snap.files.is_empty() && snap.untracked.is_empty() => self .panel_empty( - "No uncommitted changes.", - Some("The working tree is clean."), + t(L10nKey::PanelNoChanges), + Some(t(L10nKey::PanelNoChangesHint)), cx, ), Some(Some(snap)) => { @@ -930,10 +940,7 @@ impl Tty7App { .py(px(3.)) .text_size(px(11.5)) .text_color(cx.theme().muted_foreground) - .child(format!( - "… and {rest} more changed file{} — run `git diff` to see them.", - if rest == 1 { "" } else { "s" } - )), + .child(t_plural(L10nKey::PanelMoreChangedFiles, rest, &[])), ); } if untracked > 0 { @@ -952,7 +959,7 @@ impl Tty7App { div() .text_size(px(11.5)) .text_color(cx.theme().muted_foreground) - .child(format!("{untracked} untracked")), + .child(t_plural(L10nKey::PanelUntracked, untracked, &[])), ), ); } @@ -1007,7 +1014,7 @@ impl Tty7App { return self.render_panel_sftp(host.unwrap_or_default(), window, cx); } - let title = self.panel_title("Files", None, None, window, cx); + let title = self.panel_title(t(L10nKey::PanelFilesTitle), None, None, window, cx); let search = self.panel_search(&self.file_search.clone(), cx); let rows = self.render_file_tree_rows(window, cx); v_flex() @@ -1071,19 +1078,19 @@ pub(crate) fn info_chip( pub fn reveal_label() -> &'static str { if cfg!(target_os = "macos") { - "Reveal in Finder" + t(L10nKey::PanelRevealInFinder) } else { - "Open Folder" + t(L10nKey::PanelOpenFolder) } } fn agent_status_label(status: crate::core::cli_agent::AgentStatus) -> &'static str { use crate::core::cli_agent::AgentStatus::*; match status { - Idle => "idle", - Working => "working", - Waiting => "waiting", - Done => "done", + Idle => t(L10nKey::PanelAgentIdle), + Working => t(L10nKey::PanelAgentWorking), + Waiting => t(L10nKey::PanelAgentWaiting), + Done => t(L10nKey::PanelAgentDone), } } diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 8f37de00..42383c7b 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -32,6 +32,7 @@ use crate::ui::app::{ Tty7App, }; use crate::ui::host_ops::HostId; +use crate::ui::i18n::{L10nKey, t, t_fmt, t_plural}; use crate::ui::presets; use crate::ui::rounding; use crate::ui::rounding::RoundedCorners as _; @@ -80,314 +81,344 @@ impl SettingsSection { struct SearchEntry { section: SettingsSection, - title: &'static str, - keywords: &'static str, + title: L10nKey, + keywords: L10nKey, +} + +#[derive(Clone)] +pub(crate) enum ExplorerContextMenuNote { + Registered, + Unregistered, + RegisterFailed(String), + UnregisterFailed(String), +} + +impl ExplorerContextMenuNote { + fn localized(&self) -> String { + match self { + Self::Registered => t(L10nKey::SettingsExplorerRegisteredNote).to_string(), + Self::Unregistered => t(L10nKey::SettingsExplorerUnregisteredNote).to_string(), + Self::RegisterFailed(error) => { + t_fmt(L10nKey::SettingsExplorerRegisterFailed, &[("error", error)]) + } + Self::UnregisterFailed(error) => t_fmt( + L10nKey::SettingsExplorerUnregisterFailed, + &[("error", error)], + ), + } + } } fn settings_search_entries() -> &'static [SearchEntry] { + use L10nKey::*; use SettingsSection::*; &[ SearchEntry { section: Appearance, - title: "Theme", - keywords: "appearance color colours scheme dark light palette background foreground accent sync system os auto follow", + title: SettingsLanguage, + keywords: SettingsSearchLanguageKeywords, }, SearchEntry { section: Appearance, - title: "Sync with system", - keywords: "theme dark light auto follow os appearance mode", + title: SettingsThemeIntroTitle, + keywords: SettingsSearchThemeKeywords, }, SearchEntry { section: Appearance, - title: "Custom themes", - keywords: "theme duplicate edit colors folder yaml import", + title: SettingsSyncWithSystem, + keywords: SettingsSearchSyncWithSystemKeywords, }, SearchEntry { section: Appearance, - title: "Opacity", - keywords: "transparency translucent see through window alpha", + title: SettingsCustomThemes, + keywords: SettingsSearchCustomThemesKeywords, }, SearchEntry { section: Appearance, - title: "Blur", - keywords: "transparency translucent frosted vibrancy window background", + title: SettingsOpacity, + keywords: SettingsSearchOpacityKeywords, }, SearchEntry { section: Appearance, - title: "Dim inactive panes", - keywords: "fade unfocused inactive split pane focus opacity highlight active dimming", + title: SettingsBlur, + keywords: SettingsSearchBlurKeywords, }, SearchEntry { section: Appearance, - title: "Font size", - keywords: "typography text bigger smaller zoom", + title: SettingsDimInactivePanes, + keywords: SettingsSearchDimInactivePanesKeywords, }, SearchEntry { section: Appearance, - title: "Line height", - keywords: "typography leading spacing", + title: SettingsFontSize, + keywords: SettingsSearchFontSizeKeywords, }, SearchEntry { section: Appearance, - title: "Font family", - keywords: "typeface monospace typography", + title: SettingsLineHeight, + keywords: SettingsSearchLineHeightKeywords, }, SearchEntry { section: Appearance, - title: "Bold font", - keywords: "typeface weight", + title: SettingsFontFamily, + keywords: SettingsSearchFontFamilyKeywords, }, SearchEntry { section: Appearance, - title: "Italic font", - keywords: "typeface oblique", + title: SettingsBoldFont, + keywords: SettingsSearchBoldFontKeywords, }, SearchEntry { section: Appearance, - title: "Font ligatures", - keywords: "typography glyph fira", + title: SettingsItalicFont, + keywords: SettingsSearchItalicFontKeywords, }, SearchEntry { section: Appearance, - title: "Cursor shape", - keywords: "caret block bar underline beam", + title: SettingsFontLigatures, + keywords: SettingsSearchFontLigaturesKeywords, }, SearchEntry { section: Appearance, - title: "Cursor blink", - keywords: "caret blinking flash", + title: SettingsCursorShape, + keywords: SettingsSearchCursorShapeKeywords, }, SearchEntry { section: Appearance, - title: "ANSI colors", - keywords: "palette 16 terminal colours theme", + title: SettingsCursorBlink, + keywords: SettingsSearchCursorBlinkKeywords, + }, + SearchEntry { + section: Appearance, + title: SettingsAnsiColors, + keywords: SettingsSearchAnsiColorsKeywords, }, SearchEntry { section: Terminal, - title: "Program", - keywords: "shell binary zsh bash fish nu nushell pwsh powershell executable launch", + title: SettingsProgram, + keywords: SettingsSearchProgramKeywords, }, SearchEntry { section: Terminal, - title: "Arguments", - keywords: "shell flags login args", + title: SettingsArguments, + keywords: SettingsSearchArgumentsKeywords, }, SearchEntry { section: Terminal, - title: "Start in", - keywords: "cwd working directory start folder path home inherit custom", + title: SettingsStartIn, + keywords: SettingsSearchStartInKeywords, }, SearchEntry { section: Terminal, - title: "Scrollback", - keywords: "history buffer lines scroll", + title: SettingsScrollback, + keywords: SettingsSearchScrollbackKeywords, }, SearchEntry { section: Terminal, - title: "Scroll speed", - keywords: "mouse wheel multiplier scrolling", + title: SettingsScrollSpeed, + keywords: SettingsSearchScrollSpeedKeywords, }, SearchEntry { section: Terminal, - title: "Focus follows mouse", - keywords: "pane hover activate", + title: SettingsFocusFollowsMouse, + keywords: SettingsSearchFocusFollowsMouseKeywords, }, SearchEntry { section: Terminal, - title: "Hide mouse while typing", - keywords: "cursor pointer autohide", + title: SettingsHideMouseWhileTyping, + keywords: SettingsSearchHideMouseWhileTypingKeywords, }, SearchEntry { section: Terminal, - title: "Report mouse to apps", - keywords: "mouse reporting vim tmux click scroll shift passthrough", + title: SettingsReportMouseToApps, + keywords: SettingsSearchReportMouseToAppsKeywords, }, SearchEntry { section: Terminal, - title: "Terminal bell", - keywords: "bell audible visual flash sound silence beep ^g", + title: SettingsTerminalBell, + keywords: SettingsSearchTerminalBellKeywords, }, SearchEntry { section: Terminal, - title: "Detect URLs", - keywords: "links hyperlink clickable open", + title: DetectUrls, + keywords: SettingsSearchDetectUrlsKeywords, }, SearchEntry { section: Terminal, - title: "Forward SSH loopback links", - keywords: "ssh remote port tunnel localhost forward links", + title: ForwardSshLoopbackLinks, + keywords: SettingsSearchForwardSshLoopbackLinksKeywords, }, SearchEntry { section: Terminal, - title: "Open files with", - keywords: "links file editor command external app path line column", + title: OpenFilesWith, + keywords: SettingsSearchOpenFilesWithKeywords, }, SearchEntry { section: Input, - title: "Tab completion", - keywords: "complete completion menu suggestions tab prompt", + title: SettingsTabCompletion, + keywords: SettingsSearchTabCompletionKeywords, }, SearchEntry { section: Input, - title: "History search", - keywords: "ctrl-r reverse search fuzzy history recall fzf prompt", + title: SettingsHistorySearch, + keywords: SettingsSearchHistorySearchKeywords, }, SearchEntry { section: Input, - title: "Option (⌥) acts as Meta", - keywords: "alt keyboard modifier escape macos option meta option acts as meta", + title: SettingsOptionAsMeta, + keywords: SettingsSearchOptionAsMetaKeywords, }, SearchEntry { section: Input, - title: "Smart selection", - keywords: "double click word url path select semantic bracket email", + title: SettingsSmartSelection, + keywords: SettingsSearchSmartSelectionKeywords, }, SearchEntry { section: Input, - title: "Copy on select", - keywords: "clipboard selection yank mouse", + title: SettingsCopyOnSelect, + keywords: SettingsSearchCopyOnSelectKeywords, }, SearchEntry { section: Input, - title: "Trim trailing spaces on copy", - keywords: "clipboard whitespace copy", + title: SettingsTrimTrailingSpaces, + keywords: SettingsSearchTrimTrailingSpacesKeywords, }, SearchEntry { section: Ssh, - title: "Hosts", - keywords: "ssh host connection saved profile import ssh_config manage add edit \ - quick connect", + title: SettingsHosts, + keywords: SettingsSearchHostsKeywords, }, SearchEntry { section: Ssh, - title: "Verify host keys", - keywords: "ssh security known_hosts fingerprint mitm host key verification", + title: SettingsVerifyHostKeys, + keywords: SettingsSearchVerifyHostKeysKeywords, }, SearchEntry { section: Ssh, - title: "Warn before closing", - keywords: "ssh confirm close tab pane live session security", + title: WarnBeforeClosing, + keywords: SettingsSearchWarnBeforeClosingKeywords, }, SearchEntry { section: Ssh, - title: "Port forwarding", - keywords: "ssh tunnel local remote dynamic socks forward rule", + title: SettingsPortForwarding, + keywords: SettingsSearchPortForwardingKeywords, }, SearchEntry { section: Agents, - title: "Claude Code", - keywords: "agent integration hooks install uninstall status rich session working waiting tab bar sidebar badge claude", + title: SettingsAgentClaudeCode, + keywords: SettingsSearchClaudeCodeKeywords, }, SearchEntry { section: Agents, - title: "Codex", - keywords: "agent integration hooks install openai codex", + title: SettingsAgentCodex, + keywords: SettingsSearchCodexKeywords, }, SearchEntry { section: Agents, - title: "Copilot CLI", - keywords: "agent integration hooks install github copilot", + title: SettingsAgentCopilotCli, + keywords: SettingsSearchCopilotCliKeywords, }, SearchEntry { section: Agents, - title: "OpenCode", - keywords: "agent integration plugin install opencode", + title: SettingsAgentOpencode, + keywords: SettingsSearchOpencodeKeywords, }, SearchEntry { section: Agents, - title: "Pi", - keywords: "agent integration extension install pi", + title: SettingsAgentPi, + keywords: SettingsSearchPiKeywords, }, SearchEntry { section: Agents, - title: "Grok Build", - keywords: "agent integration hooks install xai grok build", + title: SettingsAgentGrokBuild, + keywords: SettingsSearchGrokBuildKeywords, }, SearchEntry { section: WindowTabs, - title: "Startup window", - keywords: "launch open maximized fullscreen normal", + title: SettingsStartupWindow, + keywords: SettingsSearchStartupWindowKeywords, }, SearchEntry { section: WindowTabs, - title: "Remember window size & position", - keywords: "window size position bounds geometry launch startup remember", + title: SettingsRememberWindowSize, + keywords: SettingsSearchRememberWindowSizeKeywords, }, SearchEntry { section: WindowTabs, - title: "Restore last layout", - keywords: "restore session previous tabs splits reopen launch startup layout", + title: SettingsRestoreLastLayout, + keywords: SettingsSearchRestoreLastLayoutKeywords, }, SearchEntry { section: WindowTabs, - title: "Confirm before closing the last window", - keywords: "close quit confirm prompt dialog ask again warn last window cmd-w ctrl-w", + title: SettingsConfirmLastWindowClose, + keywords: SettingsSearchConfirmLastWindowCloseKeywords, }, SearchEntry { section: WindowTabs, - title: "Show tray icon", - keywords: "tray menu bar status item agent attention system icon", + title: SettingsShowTrayIcon, + keywords: SettingsSearchShowTrayIconKeywords, }, SearchEntry { section: WindowTabs, - title: "New tab position", - keywords: "tabs order end after current", + title: SettingsNewTabPosition, + keywords: SettingsSearchNewTabPositionKeywords, }, SearchEntry { section: WindowTabs, - title: "Tab bar position", - keywords: "tabs vertical sidebar left top layout rail", + title: SettingsTabBarPosition, + keywords: SettingsSearchTabBarPositionKeywords, }, SearchEntry { section: WindowTabs, - title: "Sidebar grouping", - keywords: "tabs group repo repository git scratch header sidebar flat", + title: SettingsSidebarGrouping, + keywords: SettingsSearchSidebarGroupingKeywords, }, SearchEntry { section: WindowTabs, - title: "Open diff preview from sidebar counts", - keywords: "diff overlay preview sidebar counts git changes click branch lines", + title: SettingsDiffPreviewFromCounts, + keywords: SettingsSearchDiffPreviewFromCountsKeywords, }, SearchEntry { section: WindowTabs, - title: "Notify on command finish", - keywords: "notification alert done osc desktop banner long command", + title: SettingsNotifyOnCommandFinish, + keywords: SettingsSearchNotifyOnCommandFinishKeywords, }, SearchEntry { section: WindowTabs, - title: "Notify threshold", - keywords: "notification alert seconds duration long command delay", + title: SettingsNotifyThreshold, + keywords: SettingsSearchNotifyThresholdKeywords, }, SearchEntry { section: Keybindings, - title: "Keybindings", - keywords: "shortcut hotkey keyboard binding chord tmux preset rebind prefix", + title: SettingsSearchKeybindingsTitle, + keywords: SettingsSearchKeybindingsKeywords, }, SearchEntry { section: About, - title: "About", - keywords: "version license credits build update check github", + title: SettingsNavAbout, + keywords: SettingsSearchAboutKeywords, }, SearchEntry { section: About, - title: "How shells work", - keywords: "shell session daemon server detach persist background close quit stop delete workspace layout survive reboot tmux", + title: SettingsSearchHowShellsWorkTitle, + keywords: SettingsSearchHowShellsWorkKeywords, }, SearchEntry { section: About, - title: "Command line tool", - keywords: "cli tty7 path shell command install symlink terminal iterm agent script", + title: SettingsSearchCommandLineToolTitle, + keywords: SettingsSearchCommandLineToolKeywords, }, SearchEntry { section: About, - title: "Windows Explorer context menu", - keywords: "windows explorer right click folder directory background shell menu register unregister open here", + title: SettingsExplorerContextMenu, + keywords: SettingsSearchExplorerContextMenuKeywords, }, ] } fn entry_matches(entry: &SearchEntry, query: &str) -> bool { - entry.title.to_lowercase().contains(query) || entry.keywords.contains(query) + t(entry.title).to_lowercase().contains(query) + || t(entry.keywords).to_lowercase().contains(query) } pub(crate) fn section_match_count(section: SettingsSection, query: &str) -> usize { @@ -422,6 +453,7 @@ pub(crate) struct SettingsState { pub(crate) font_select: Entity>>, pub(crate) font_bold_select: Entity>>, pub(crate) font_italic_select: Entity>>, + pub(crate) language_select: Entity>>, pub(crate) shell_program_input: Entity, pub(crate) shell_args_input: Entity, pub(crate) wd_path_input: Entity, @@ -436,7 +468,7 @@ pub(crate) struct SettingsState { pub(crate) rebinding_note: Option, pub(crate) explorer_context_menu_status: Result, - pub(crate) explorer_context_menu_note: Option, + pub(crate) explorer_context_menu_note: Option, pub(crate) ssh_form: Option, pub(crate) ssh_detail: SshDetail, pub(crate) ssh_filter: Entity, @@ -490,7 +522,7 @@ fn ssh_group_key(p: &SshProfile) -> &str { fn ssh_group_label(key: &str) -> &str { match key { crate::core::ssh_config::IMPORTED_GROUP => "~/.ssh/config", - "" => "In tty7", + "" => t(L10nKey::SettingsInTty7), other => other, } } @@ -605,7 +637,9 @@ pub(crate) struct Recording { pub(crate) _intercept: Subscription, } -pub(crate) const FONT_DEFAULT_LABEL: &str = "Default (match primary)"; +pub(crate) fn font_default_label() -> &'static str { + t(L10nKey::SettingsFontDefault) +} #[cfg(target_os = "macos")] const LINK_MODIFIER_LABEL: &str = "⌘"; @@ -678,7 +712,12 @@ fn seed_forward_row( bind_port: seed_hinted(window, cx, &port(rule.bind.port), "8080"), target_host: seed_hinted(window, cx, &rule.target.host, "127.0.0.1"), target_port: seed_hinted(window, cx, &port(rule.target.port), "80"), - description: seed_hinted(window, cx, &rule.description, "what it's for"), + description: seed_hinted( + window, + cx, + &rule.description, + t(L10nKey::ForwardDescriptionPlaceholder), + ), } } @@ -762,42 +801,42 @@ impl Tty7App { let nav_body = SidebarMenu::new() .child(nav_item( - "Appearance", + t(L10nKey::SettingsNavAppearance), SettingsSection::Appearance, Icon::new(IconName::Palette), )) .child(nav_item( - "Terminal", + t(L10nKey::SettingsNavTerminal), SettingsSection::Terminal, Icon::new(IconName::SquareTerminal), )) .child(nav_item( - "Input", + t(L10nKey::SettingsNavInput), SettingsSection::Input, Icon::new(IconName::Settings2), )) .child(nav_item( - "SSH", + t(L10nKey::SettingsNavSsh), SettingsSection::Ssh, Icon::new(IconName::Globe), )) .child(nav_item( - "Agents", + t(L10nKey::SettingsNavAgents), SettingsSection::Agents, Icon::new(IconName::Bot), )) .child(nav_item( - "Window & Tabs", + t(L10nKey::SettingsNavWindowTabs), SettingsSection::WindowTabs, Icon::new(IconName::WindowRestore), )) .child(nav_item( - "Keybindings", + t(L10nKey::SettingsNavKeybindings), SettingsSection::Keybindings, Icon::new(IconName::CaseSensitive), )) .child(nav_item( - "About", + t(L10nKey::SettingsNavAbout), SettingsSection::About, Icon::empty().path("icons/circle-info.svg"), )); @@ -817,7 +856,7 @@ impl Tty7App { .text_xs() .font_weight(FontWeight::MEDIUM) .text_color(header_muted) - .child("SETTINGS"), + .child(t(L10nKey::SettingsHeader)), ) .child( h_flex() @@ -1014,7 +1053,7 @@ impl Tty7App { pub(crate) fn segmented( &self, id: impl Into, - options: &'static [&'static str], + options: &[&str], selected: usize, cx: &mut Context, on_pick: impl Fn(&mut Self, usize, &mut Window, &mut Context) + 'static, @@ -1027,7 +1066,7 @@ impl Tty7App { &self, sf: presets::Surface, id: impl Into, - options: &'static [&'static str], + options: &[&str], selected: usize, cx: &mut Context, on_pick: impl Fn(&mut Self, usize, &mut Window, &mut Context) + 'static, @@ -1069,7 +1108,7 @@ impl Tty7App { .hover(|h| h.bg(gpui::rgb(sf.hover))) }) .active(|s| s.bg(gpui::rgb(sf.pressed))) - .child(*label) + .child(label.to_string()) .on_click(cx.listener(move |this, _, window, cx| { on_pick(this, i, window, cx); })) @@ -1084,14 +1123,16 @@ impl Tty7App { let hover_bg = gpui::rgb(cx.global::().window.hover); let stepper_bg = theme.secondary.opacity(0.35); let font_size = self.font_size; - let (font_select, font_bold_select, font_italic_select) = match self.active_settings() { - Some(s) => ( - s.font_select.clone(), - s.font_bold_select.clone(), - s.font_italic_select.clone(), - ), - None => return div().into_any_element(), - }; + let (font_select, font_bold_select, font_italic_select, language_select) = + match self.active_settings() { + Some(s) => ( + s.font_select.clone(), + s.font_bold_select.clone(), + s.font_italic_select.clone(), + s.language_select.clone(), + ), + None => return div().into_any_element(), + }; let cfg = cx.global::(); let cursor_style = cfg.cursor_style; let cursor_blink = cfg.cursor_blink; @@ -1160,7 +1201,7 @@ impl Tty7App { step("font-inc", "+", 2) .on_click(cx.listener(|this, _, _w, cx| this.change_font_size(FONT_SIZE_STEP, cx))), Button::new("font-reset") - .label("Reset") + .label(t(L10nKey::Reset)) .ghost() .small() .on_click(cx.listener(|this, _, _w, cx| this.reset_font_size(cx))), @@ -1176,7 +1217,7 @@ impl Tty7App { cx.listener(|this, _, _w, cx| this.change_line_height(LINE_HEIGHT_STEP, cx)), ), Button::new("lh-reset") - .label("Reset") + .label(t(L10nKey::Reset)) .ghost() .small() .on_click(cx.listener(|this, _, _w, cx| this.reset_line_height(cx))), @@ -1187,7 +1228,7 @@ impl Tty7App { .small() .w(px(180.)) .h(control_h) - .search_placeholder("Search fonts…") + .search_placeholder(crate::ui::i18n::t(crate::ui::i18n::L10nKey::SearchFonts)) .menu_max_h(px(224.)) .into_any_element() }; @@ -1198,6 +1239,12 @@ impl Tty7App { .checked(font_ligatures) .on_click(cx.listener(|this, on: &bool, _w, cx| this.set_font_ligatures(*on, cx))) .into_any_element(); + let language_control = Select::new(&language_select) + .small() + .w(px(180.)) + .h(control_h) + .menu_max_h(px(224.)) + .into_any_element(); let cursor_idx = match cursor_style { CursorStyle::Block => 0, @@ -1225,8 +1272,8 @@ impl Tty7App { v_flex() .child(self.section_intro( - "Theme", - "Pick a color theme. Each one sets its own light or dark look.", + t(L10nKey::SettingsThemeIntroTitle), + t(L10nKey::SettingsThemeIntroDesc), cx, )) .child(self.render_theme_selection(cx)) @@ -1234,54 +1281,62 @@ impl Tty7App { .child(self.section_rule(cx)) .child(self.render_window_section(cx)) .child(self.section_rule(cx)) - .child(self.section_header("Typography", cx)) + .child(self.section_header(t(L10nKey::SettingsLanguage), cx)) .child(self.settings_row( - "Font size", - "Terminal text size in pixels.", + t(L10nKey::SettingsLanguage), + t(L10nKey::SettingsLanguageDesc), + language_control, + cx, + )) + .child(self.section_rule(cx)) + .child(self.section_header(t(L10nKey::SettingsTypography), cx)) + .child(self.settings_row( + t(L10nKey::SettingsFontSize), + t(L10nKey::SettingsFontSizeDesc), font_size_control, cx, )) .child(self.settings_row( - "Line height", - "Row spacing as a multiple of the font size.", + t(L10nKey::SettingsLineHeight), + t(L10nKey::SettingsLineHeightDesc), line_height_control, cx, )) .child(self.settings_row( - "Font family", - "Pick from fonts installed on your system.", + t(L10nKey::SettingsFontFamily), + t(L10nKey::SettingsFontFamilyDesc), font_family_control, cx, )) .child(self.settings_row( - "Bold font", - "Face for bold text; Default synthesizes it from the primary.", + t(L10nKey::SettingsBoldFont), + t(L10nKey::SettingsBoldFontDesc), font_bold_control, cx, )) .child(self.settings_row( - "Italic font", - "Face for italic text; Default synthesizes it from the primary.", + t(L10nKey::SettingsItalicFont), + t(L10nKey::SettingsItalicFontDesc), font_italic_control, cx, )) .child(self.settings_row( - "Font ligatures", - "Enable common programming ligature features for terminal text.", + t(L10nKey::SettingsFontLigatures), + t(L10nKey::SettingsFontLigaturesDesc), ligature_switch, cx, )) .child(self.section_rule(cx)) - .child(self.section_header("Cursor", cx)) + .child(self.section_header(t(L10nKey::SettingsCursor), cx)) .child(self.settings_row( - "Cursor shape", - "How the terminal cursor is drawn.", + t(L10nKey::SettingsCursorShape), + t(L10nKey::SettingsCursorShapeDesc), cursor_style_control, cx, )) .child(self.settings_row( - "Cursor blink", - "Pulse the cursor while the terminal is focused.", + t(L10nKey::SettingsCursorBlink), + t(L10nKey::SettingsCursorBlinkDesc), blink_switch, cx, )) @@ -1327,17 +1382,16 @@ impl Tty7App { .into_any_element(); v_flex() - .child(self.section_header("Transparency", cx)) + .child(self.section_header(t(L10nKey::SettingsTransparency), cx)) .child(self.settings_row( - "Opacity", - "How opaque the window background is, for every theme. Below \ - 100% the desktop shows through.", + t(L10nKey::SettingsOpacity), + t(L10nKey::SettingsOpacityDesc), opacity_control, cx, )) .child(self.settings_row( - "Blur", - "Blur whatever is behind a translucent window (macOS).", + t(L10nKey::SettingsBlur), + t(L10nKey::SettingsBlurDesc), blur_switch, cx, )) @@ -1345,7 +1399,7 @@ impl Tty7App { this.child( h_flex().mt_2().child( Button::new("follow-theme-window") - .label("Follow theme") + .label(t(L10nKey::FollowTheme)) .small() .on_click(cx.listener(|this, _, window, cx| { this.reset_window_overrides(window, cx) @@ -1354,8 +1408,8 @@ impl Tty7App { ) }) .child(self.settings_row( - "Dim inactive panes", - "Fade unfocused panes in a split so the active one stands out.", + t(L10nKey::SettingsDimInactivePanes), + t(L10nKey::SettingsDimInactivePanesDesc), dim_switch, cx, )) @@ -1366,7 +1420,7 @@ impl Tty7App { let editor = self.active_settings().and_then(|s| s.theme_editor.as_ref()); let folder_button = Button::new("open-themes-folder") - .label("Open themes folder") + .label(t(L10nKey::SettingsOpenThemesFolder)) .small() .on_click(cx.listener(|this, _, _w, cx| this.open_themes_folder(cx))); @@ -1398,9 +1452,9 @@ impl Tty7App { .child( Button::new("pick-theme-image") .label(if image.is_some() { - "Change…" + t(L10nKey::SettingsChangeThemeImage) } else { - "Choose…" + t(L10nKey::SettingsChooseThemeImage) }) .small() .on_click(cx.listener(|this, _, _w, cx| this.pick_theme_image(cx))), @@ -1417,7 +1471,7 @@ impl Tty7App { ) .child( Button::new("remove-theme-image") - .label("Remove") + .label(t(L10nKey::SettingsRemoveThemeImage)) .small() .on_click(cx.listener(|this, _, window, cx| { this.remove_theme_image(window, cx) @@ -1441,8 +1495,8 @@ impl Tty7App { ) .into_any_element(); self.settings_row( - "Image opacity", - "How strongly the image shows over the background color.", + t(L10nKey::SettingsImageOpacity), + t(L10nKey::SettingsImageOpacityDesc), control, cx, ) @@ -1451,9 +1505,8 @@ impl Tty7App { return v_flex() .mt_5() .child(self.section_intro( - "Edit theme", - "You're editing a copy. Changes save to its file in the themes \ - folder and apply live.", + t(L10nKey::SettingsEditTheme), + t(L10nKey::SettingsEditThemeIntro), cx, )) .children( @@ -1461,13 +1514,13 @@ impl Tty7App { .map(|(label, state)| self.render_theme_color_row(label, state, cx)), ) .child(self.settings_row( - "Background image", - "Composited over the background color, under the text.", + t(L10nKey::SettingsBackgroundImage), + t(L10nKey::SettingsBackgroundImageDesc), image_control, cx, )) .children(image_opacity_row) - .child(self.section_header("ANSI colors", cx)) + .child(self.section_header(t(L10nKey::SettingsAnsiColors), cx)) .children( ansi.into_iter() .map(|(label, state)| self.render_theme_color_row(label, state, cx)), @@ -1479,9 +1532,8 @@ impl Tty7App { v_flex() .mt_5() .child(self.section_intro( - "Custom themes", - "Duplicate a theme to edit its colors here, or drop your own in the \ - themes folder: a tty7 YAML theme or an iTerm2 .itermcolors scheme.", + t(L10nKey::SettingsCustomThemes), + t(L10nKey::SettingsCustomThemesIntro), cx, )) .child( @@ -1489,7 +1541,7 @@ impl Tty7App { .gap_3() .child( Button::new("duplicate-theme") - .label("Duplicate to edit") + .label(t(L10nKey::SettingsDuplicateToEdit)) .small() .on_click(cx.listener(|this, _, window, cx| { this.fork_active_theme(window, cx) @@ -1564,49 +1616,52 @@ impl Tty7App { let live = self.live_ssh_profiles(cx); let menu_app = cx.entity().downgrade(); - let header = v_flex().gap_2().child(self.header_text("Hosts", cx)).child( - h_flex() - .items_center() - .gap_2() - .child( - Icon::empty() - .path("stock/icons/search.svg") - .size(px(16.)) - .text_color(muted), - ) - .child( - div() - .flex_1() - .min_w_0() - .child(Input::new(&filter).appearance(false).pl_0()), - ) - .child( - h_flex() - .flex_shrink_0() - .gap_0p5() - .child( - Button::new("ssh-profiles-add") - .icon(Icon::new(IconName::Plus)) - .ghost() - .small() - .on_click(cx.listener(|this, _, window, cx| { - this.add_new_profile(window, cx) - })), - ) - .child( - Button::new("ssh-profiles-more") - .icon(Icon::empty().path("stock/icons/ellipsis.svg")) - .ghost() - .small() - .dropdown_menu_with_anchor( - gpui::Anchor::TopRight, - move |menu, _window, _cx| { - Self::ssh_master_menu(menu, &menu_app) - }, - ), - ), - ), - ); + let header = v_flex() + .gap_2() + .child(self.header_text(t(L10nKey::SettingsHosts), cx)) + .child( + h_flex() + .items_center() + .gap_2() + .child( + Icon::empty() + .path("stock/icons/search.svg") + .size(px(16.)) + .text_color(muted), + ) + .child( + div() + .flex_1() + .min_w_0() + .child(Input::new(&filter).appearance(false).pl_0()), + ) + .child( + h_flex() + .flex_shrink_0() + .gap_0p5() + .child( + Button::new("ssh-profiles-add") + .icon(Icon::new(IconName::Plus)) + .ghost() + .small() + .on_click(cx.listener(|this, _, window, cx| { + this.add_new_profile(window, cx) + })), + ) + .child( + Button::new("ssh-profiles-more") + .icon(Icon::empty().path("stock/icons/ellipsis.svg")) + .ghost() + .small() + .dropdown_menu_with_anchor( + gpui::Anchor::TopRight, + move |menu, _window, _cx| { + Self::ssh_master_menu(menu, &menu_app) + }, + ), + ), + ), + ); let mut groups: Vec<(String, Vec)> = Vec::new(); for p in profiles.iter().filter(|p| ssh_row_matches(p, &query)) { @@ -1624,8 +1679,8 @@ impl Tty7App { let mut list = v_flex().gap_0p5().w_full().child(self.render_ssh_row( "ssh-defaults-row", - "Defaults", - "Inherited by every host", + t(L10nKey::SettingsDefaults), + t(L10nKey::SettingsInheritedByEveryHost), detail == SshDetail::Defaults, None, sf, @@ -1640,7 +1695,7 @@ impl Tty7App { .py_4() .text_sm() .text_color(muted) - .child("No saved hosts yet."), + .child(t(L10nKey::SettingsNoSavedHosts)), ); } else if groups.is_empty() { list = list.child( @@ -1648,7 +1703,7 @@ impl Tty7App { .py_4() .text_sm() .text_color(muted) - .child(format!("Nothing matches {query}.")), + .child(t_fmt(L10nKey::SettingsNothingMatches, &[("query", &query)])), ); } @@ -1687,23 +1742,27 @@ impl Tty7App { fn ssh_master_menu(menu: PopupMenu, app: &gpui::WeakEntity) -> PopupMenu { menu.min_w(px(200.)) - .item(PopupMenuItem::new("Import from ~/.ssh/config").on_click({ - let app = app.clone(); - move |_, _window, cx| { - let _ = app.update(cx, |this, cx| this.import_ssh_config_profiles(cx)); - } - })) - .item(PopupMenuItem::new("Expand all groups").on_click({ - let app = app.clone(); - move |_, _window, cx| { - let _ = app.update(cx, |this, cx| { - if let Some(s) = this.active_settings_mut() { - s.ssh_collapsed_groups.clear(); - } - cx.notify(); - }); - } - })) + .item( + PopupMenuItem::new(t(L10nKey::SettingsImportFromSshConfig)).on_click({ + let app = app.clone(); + move |_, _window, cx| { + let _ = app.update(cx, |this, cx| this.import_ssh_config_profiles(cx)); + } + }), + ) + .item( + PopupMenuItem::new(t(L10nKey::SettingsExpandAllGroups)).on_click({ + let app = app.clone(); + move |_, _window, cx| { + let _ = app.update(cx, |this, cx| { + if let Some(s) = this.active_settings_mut() { + s.ssh_collapsed_groups.clear(); + } + cx.notify(); + }); + } + }), + ) } fn render_ssh_group_header( @@ -1992,9 +2051,9 @@ impl Tty7App { }; let heading = if saved == 0 { - "No hosts yet" + t(L10nKey::SettingsNoHostsYet) } else { - "Nothing selected" + t(L10nKey::SettingsNothingSelected) }; let mut body = v_flex() @@ -2004,7 +2063,7 @@ impl Tty7App { div() .text_sm() .text_color(muted) - .child("Type an address to connect now — tty7 offers to save it afterwards."), + .child(t(L10nKey::SettingsTypeAddressToConnect)), ) .child( h_flex() @@ -2013,7 +2072,7 @@ impl Tty7App { .child(div().w(px(320.)).child(Input::new(&input).small())) .child( Button::new("ssh-quick-connect") - .label("Connect") + .label(t(L10nKey::Connect)) .primary() .small() .disabled(parsed.is_none()) @@ -2042,17 +2101,15 @@ impl Tty7App { .flex_1() .min_w_0() .gap_0p5() - .child( - div() - .text_sm() - .font_weight(FontWeight::MEDIUM) - .child(format!("{n} more in ~/.ssh/config")), - ) + .child(div().text_sm().font_weight(FontWeight::MEDIUM).child(t_fmt( + L10nKey::SettingsMoreInSshConfig, + &[("count", &n.to_string())], + ))) .child(div().text_xs().text_color(muted).truncate().child(names)), ) .child( Button::new("ssh-empty-import") - .label("Link") + .label(t(L10nKey::Link)) .small() .on_click( cx.listener(|this, _, _w, cx| this.import_ssh_config_profiles(cx)), @@ -2090,20 +2147,15 @@ impl Tty7App { let config_block = v_flex() .child(self.section_intro( "~/.ssh/config", - match imported { - 0 => "No aliases linked yet.".to_string(), - 1 => "1 alias linked.".to_string(), - n => format!("{n} aliases linked."), - }, + t_plural(L10nKey::SettingsAliasesLinked, imported, &[]), cx, )) .child( self.settings_row( - "Import aliases", - "Re-reads the file and adds anything new. Edits you make here are \ - stored by tty7 — the file itself is never written.", + t(L10nKey::SettingsImportAliases), + t(L10nKey::SettingsImportAliasesDesc), Button::new("ssh-defaults-import") - .label("Import now") + .label(t(L10nKey::SettingsImportNow)) .small() .on_click( cx.listener(|this, _, _w, cx| this.import_ssh_config_profiles(cx)), @@ -2118,11 +2170,13 @@ impl Tty7App { v_flex() .gap_1() .mb_6() - .child(self.header_text("Defaults", cx)) - .child(div().text_sm().text_color(muted).child( - "Every host starts from these. Any host can override one under \ - its own Advanced.", - )), + .child(self.header_text(t(L10nKey::SettingsDefaults), cx)) + .child( + div() + .text_sm() + .text_color(muted) + .child(t(L10nKey::SettingsDefaultsIntro)), + ), ) .child(self.render_ssh_security_block(cx)) .child(self.section_rule(cx)) @@ -2138,7 +2192,7 @@ impl Tty7App { ) -> PopupMenu { let menu = menu .min_w(px(180.)) - .item(PopupMenuItem::new("Connect").on_click({ + .item(PopupMenuItem::new(t(L10nKey::Connect)).on_click({ let app = app.clone(); move |_, window, cx| { let _ = app.update(cx, |this, cx| { @@ -2147,40 +2201,46 @@ impl Tty7App { }); } })) - .item(PopupMenuItem::new("Copy address").on_click({ - let app = app.clone(); - move |_, _window, cx| { - let _ = app.update(cx, |this, cx| this.copy_profile_connect_string(id, cx)); - } - })) - .item(PopupMenuItem::new("Duplicate").on_click({ + .item( + PopupMenuItem::new(t(L10nKey::SettingsCopyAddress)).on_click({ + let app = app.clone(); + move |_, _window, cx| { + let _ = app.update(cx, |this, cx| this.copy_profile_connect_string(id, cx)); + } + }), + ) + .item(PopupMenuItem::new(t(L10nKey::SettingsDuplicate)).on_click({ let app = app.clone(); move |_, window, cx| { let _ = app.update(cx, |this, cx| this.duplicate_profile(id, window, cx)); } })) - .item(PopupMenuItem::new("Forget password").on_click({ - let app = app.clone(); - move |_, window, cx| { - if let Some(msg) = app - .update(cx, |this, cx| this.forget_profile_password(id, cx)) - .ok() - .flatten() - { - window.push_notification(msg, cx); + .item( + PopupMenuItem::new(t(L10nKey::SettingsForgetPassword)).on_click({ + let app = app.clone(); + move |_, window, cx| { + if let Some(msg) = app + .update(cx, |this, cx| this.forget_profile_password(id, cx)) + .ok() + .flatten() + { + window.push_notification(msg, cx); + } } - } - })) + }), + ) .separator(); menu.item( - PopupMenuItem::element(move |_window, _cx| div().text_color(danger).child("Delete")) - .on_click({ - let app = app.clone(); - move |_, _window, cx| { - let _ = app.update(cx, |this, cx| this.delete_profile(id, cx)); - } - }), + PopupMenuItem::element(move |_window, _cx| { + div().text_color(danger).child(t(L10nKey::Delete)) + }) + .on_click({ + let app = app.clone(); + move |_, _window, cx| { + let _ = app.update(cx, |this, cx| this.delete_profile(id, cx)); + } + }), ) } @@ -2199,22 +2259,19 @@ impl Tty7App { v_flex() .child(self.section_intro( - "Security", - "A host can override either of these under its own Advanced.", + t(L10nKey::SettingsSecurity), + t(L10nKey::SettingsSecurityIntro), cx, )) .child(self.settings_row( - "Verify host keys", - "Check each server's key against known_hosts and confirm unknown or \ - changed keys before connecting. Off connects without checking, so a \ - spoofed server would go unnoticed.", + t(L10nKey::SettingsVerifyHostKeys), + t(L10nKey::SettingsVerifyHostKeysDesc), verify_switch, cx, )) .child(self.settings_row( - "Warn before closing", - "Ask for confirmation before closing a tab or pane with a live SSH \ - session.", + t(L10nKey::WarnBeforeClosing), + t(L10nKey::SettingsWarnBeforeClosingDesc), warn_switch, cx, )) @@ -2474,7 +2531,7 @@ impl Tty7App { return; }; profile.id = Uuid::new_v4(); - profile.name = format!("{} (copy)", profile.name); + profile.name = t_fmt(L10nKey::SettingsProfileCopied, &[("name", &profile.name)]); self.update_config(cx, |cfg| cfg.ssh_profiles.push(profile.clone())); self.ssh_form_load(&profile, window, cx); } @@ -2531,8 +2588,14 @@ impl Tty7App { let endpoint = format!("{user}@{host}:{port}"); Some( match OsCredentialStore.delete_password(&user, &host, port) { - Ok(()) => format!("Forgot saved password for {endpoint}"), - Err(e) => format!("Couldn't forget password for {endpoint}: {e}"), + Ok(()) => t_fmt( + L10nKey::SettingsForgotPasswordFor, + &[("endpoint", &endpoint)], + ), + Err(e) => t_fmt( + L10nKey::SettingsCouldntForgetPassword, + &[("endpoint", &endpoint), ("error", &e.to_string())], + ), }, ) } @@ -2564,7 +2627,7 @@ impl Tty7App { let title = match (name.is_empty(), host.is_empty()) { (false, _) => name, (true, false) => host, - (true, true) => "New host".to_string(), + (true, true) => t(L10nKey::SettingsNewHost).to_string(), }; let auth_idx = match form.auth { @@ -2598,10 +2661,17 @@ impl Tty7App { .text_color(muted) .child(div().truncate().child(address)) .when(!jump_name.is_empty(), |r| { - r.child(div().child(format!("· via {jump_name}"))) + r.child(div().child(t_fmt( + L10nKey::SettingsJumpHostVia, + &[("jump_name", &jump_name)], + ))) }) .when(live, |r| { - r.child(div().text_color(success).child("· connected")) + r.child( + div() + .text_color(success) + .child(format!("· {}", t(L10nKey::SettingsConnected))), + ) }), ), ) @@ -2611,14 +2681,14 @@ impl Tty7App { .gap_2() .child( Button::new("ssh-form-save") - .label("Save") + .label(t(L10nKey::Save)) .small() .disabled(!dirty) .on_click(cx.listener(|this, _, _w, cx| this.save_ssh_form(cx))), ) .child( Button::new("ssh-form-connect") - .label("Connect") + .label(t(L10nKey::Connect)) .primary() .small() .on_click(cx.listener(|this, _, window, cx| { @@ -2631,8 +2701,8 @@ impl Tty7App { .gap_3() .child( self.settings_row( - "Name", - "A label for this connection.", + t(L10nKey::SettingsName), + t(L10nKey::SettingsNameDesc), div() .w(px(260.)) .child(Input::new(&form.name).small()) @@ -2642,8 +2712,8 @@ impl Tty7App { ) .child( self.settings_row( - "Host", - "Hostname or IP address.", + t(L10nKey::SettingsHost), + t(L10nKey::SettingsHostDesc), h_flex() .gap_2() .child(div().w(px(172.)).child(Input::new(&form.host).small())) @@ -2654,8 +2724,8 @@ impl Tty7App { ) .child( self.settings_row( - "User", - "Login user (blank = resolve at connect).", + t(L10nKey::SettingsUser), + t(L10nKey::SettingsUserDesc), div() .w(px(260.)) .child(Input::new(&form.user).small()) @@ -2664,11 +2734,18 @@ impl Tty7App { ), ) .child(self.settings_row( - "Auth", - "Authentication method. Auto tries every applicable method.", + t(L10nKey::SettingsAuth), + t(L10nKey::SettingsAuthDesc), self.segmented( "ssh-form-auth", - &["Auto", "GSSAPI", "Password", "Key", "Agent", "2FA"], + &[ + t(L10nKey::SettingsAuthModeAuto), + "GSSAPI", + t(L10nKey::SettingsAuthModePassword), + t(L10nKey::SettingsAuthModeKey), + t(L10nKey::SettingsAuthModeAgent), + t(L10nKey::SettingsAuthMode2Fa), + ], auth_idx, cx, |this, ix, _w, cx| { @@ -2737,14 +2814,14 @@ impl Tty7App { let summary = { let name = form.jump.read(cx).value().trim().to_string(); if name.is_empty() { - "(none)".to_string() + t(L10nKey::SettingsNoneSummary).to_string() } else { name } }; let mut section = v_flex().child(self.disclosure_header( "ssh-sec-jump", - "Jump host", + t(L10nKey::SettingsJumpHost), &summary, form.show_jump, cx, @@ -2758,8 +2835,8 @@ impl Tty7App { if form.show_jump { section = section.child( self.settings_row( - "Jump host", - "Name of another profile to tunnel through (blank = direct).", + t(L10nKey::SettingsJumpHost), + t(L10nKey::SettingsJumpHostDesc), div() .w(px(260.)) .child(Input::new(&form.jump).small()) @@ -2783,13 +2860,12 @@ impl Tty7App { .filter(|r| r.collect(cx).is_some()) .count(); let summary = match count { - 0 => "none".to_string(), - 1 => "1 rule, opened with the connection".to_string(), - n => format!("{n} rules, opened with the connection"), + 0 => t(L10nKey::SettingsNoneLower).to_string(), + _ => t_plural(L10nKey::SettingsRulesOpenedWithConnection, count, &[]), }; let mut section = v_flex().child(self.disclosure_header( "ssh-sec-fwd", - "Port forwarding", + t(L10nKey::SettingsPortForwarding), &summary, form.show_forwards, cx, @@ -2812,7 +2888,7 @@ impl Tty7App { .child( h_flex().pt_1p5().child( Button::new("ssh-fwd-add") - .label("+ Add rule") + .label(t(L10nKey::SettingsAddRule)) .ghost() .small() .on_click( @@ -2826,9 +2902,9 @@ impl Tty7App { .pt_1() .text_xs() .text_color(muted) - .child("L — a local port reaches the remote side") - .child("R — a remote port reaches this machine") - .child("D — dynamic SOCKS proxy"), + .child(t(L10nKey::SettingsFwdLegendLocal)) + .child(t(L10nKey::SettingsFwdLegendRemote)) + .child(t(L10nKey::SettingsFwdLegendDynamic)), ) .into_any_element() } @@ -2909,9 +2985,9 @@ impl Tty7App { ) .when(incomplete, |col| { col.child(div().text_xs().text_color(danger).child(if needs_target { - "Needs a listen port and a target host:port — won't be saved." + t(L10nKey::SettingsFwdNeedsBoth) } else { - "Needs a listen port — won't be saved." + t(L10nKey::SettingsFwdNeedsListen) })) }) .into_any_element() @@ -2953,8 +3029,8 @@ impl Tty7App { ) -> AnyElement { let mut section = v_flex().child(self.disclosure_header( "ssh-sec-adv", - "Advanced", - "algorithms / keepalive / proxies / X11 / login scripts", + t(L10nKey::SettingsAdvanced), + t(L10nKey::SettingsAdvancedSummary), form.show_advanced, cx, |this, cx| { @@ -2984,7 +3060,13 @@ impl Tty7App { ) }; - let on_off = |b: bool| if b { "on" } else { "off" }; + let on_off = |b: bool| { + if b { + t(L10nKey::SettingsValueOn) + } else { + t(L10nKey::SettingsValueOff) + } + }; let vhk_default = on_off(cx.global::().verify_host_keys); let woc_default = on_off(cx.global::().ssh_warn_on_close); let vhk_idx = match form.verify_host_keys { @@ -3001,15 +3083,15 @@ impl Tty7App { section = section .child(text_row( self, - "Identity files", - "Private-key paths, one per line (%h/%r expand).", + t(L10nKey::SettingsIdentityFiles), + t(L10nKey::SettingsIdentityFilesDesc), &form.identity_files, cx, )) .child( self.settings_row( - "Agent forwarding", - "Forward the local ssh-agent to the connection.", + t(L10nKey::SettingsAgentForwarding), + t(L10nKey::SettingsAgentForwardingDesc), crate::ui::theme::switch("ssh-form-agent", cx) .checked(form.agent_forward) .on_click(cx.listener(|this, on: &bool, _w, cx| { @@ -3024,85 +3106,85 @@ impl Tty7App { ) .child(text_row( self, - "ProxyCommand", - "Transport command (%h/%p/%r substituted).", + t(L10nKey::SettingsProxyCommand), + t(L10nKey::SettingsProxyCommandDesc), &form.proxy_command, cx, )) .child(text_row( self, - "SOCKS5 proxy", - "host:port (blank = none).", + t(L10nKey::SettingsSocks5Proxy), + t(L10nKey::SettingsSocks5ProxyDesc), &form.socks, cx, )) .child(text_row( self, - "HTTP proxy", - "host:port (blank = none).", + t(L10nKey::SettingsHttpProxy), + t(L10nKey::SettingsHttpProxyDesc), &form.http, cx, )) .child(text_row( self, - "KEX algorithms", - "Comma-separated (blank = library default).", + t(L10nKey::SettingsKexAlgorithms), + t(L10nKey::SettingsKexAlgorithmsDesc), &form.kex, cx, )) .child(text_row( self, - "Ciphers", - "Comma-separated (blank = default).", + t(L10nKey::SettingsCiphers), + t(L10nKey::SettingsCiphersDesc), &form.cipher, cx, )) .child(text_row( self, - "MACs", - "Comma-separated (blank = default).", + t(L10nKey::SettingsMacs), + t(L10nKey::SettingsMacsDesc), &form.mac, cx, )) .child(text_row( self, - "Host-key algorithms", - "Comma-separated (blank = default).", + t(L10nKey::SettingsHostKeyAlgorithms), + t(L10nKey::SettingsHostKeyAlgorithmsDesc), &form.hostkey, cx, )) .child(text_row( self, - "Compression", - "Comma-separated (blank = default).", + t(L10nKey::SettingsCompression), + t(L10nKey::SettingsCompressionDesc), &form.compression, cx, )) .child(text_row( self, - "Keepalive interval (s)", - "Blank = library default.", + t(L10nKey::SettingsKeepaliveInterval), + t(L10nKey::SettingsKeepaliveIntervalDesc), &form.keepalive_interval, cx, )) .child(text_row( self, - "Keepalive count max", - "Missed keepalives before dead.", + t(L10nKey::SettingsKeepaliveCountMax), + t(L10nKey::SettingsKeepaliveCountMaxDesc), &form.keepalive_count, cx, )) .child(text_row( self, - "Connect timeout (s)", - "Blank = library default.", + t(L10nKey::SettingsConnectTimeout), + t(L10nKey::SettingsConnectTimeoutDesc), &form.connect_timeout, cx, )) .child( self.settings_row( - "X11 forwarding", - "Request X11 forwarding (needs XQuartz on macOS).", + t(L10nKey::SettingsX11Forwarding), + t(L10nKey::SettingsX11ForwardingDesc), crate::ui::theme::switch("ssh-form-x11", cx) .checked(form.x11) .on_click(cx.listener(|this, on: &bool, _w, cx| { @@ -3117,8 +3199,8 @@ impl Tty7App { ) .child( self.settings_row( - "Shell integration", - "Let the remote shell report prompts, exit codes and directory.", + t(L10nKey::SettingsShellIntegration), + t(L10nKey::SettingsShellIntegrationDesc), crate::ui::theme::switch("ssh-form-shell-integration", cx) .checked(form.shell_integration) .on_click(cx.listener(|this, on: &bool, _w, cx| { @@ -3133,15 +3215,15 @@ impl Tty7App { ) .child(text_row( self, - "Login scripts", - "Commands sent after the shell opens, one per line.", + t(L10nKey::SettingsLoginScripts), + t(L10nKey::SettingsLoginScriptsDesc), &form.login_scripts, cx, )) .child( self.settings_row( - "Skip banner", - "Suppress the server login banner.", + t(L10nKey::SettingsSkipBanner), + t(L10nKey::SettingsSkipBannerDesc), crate::ui::theme::switch("ssh-form-banner", cx) .checked(form.skip_banner) .on_click(cx.listener(|this, on: &bool, _w, cx| { @@ -3155,11 +3237,18 @@ impl Tty7App { ), ) .child(self.settings_row( - "Verify host keys", - format!("Default follows Defaults, which is {vhk_default}."), + t(L10nKey::SettingsVerifyHostKeys), + t_fmt( + L10nKey::SettingsDefaultFollowsDefaults, + &[("value", vhk_default)], + ), self.segmented( "ssh-form-vhk", - &["Default", "On", "Off"], + &[ + t(L10nKey::SettingsDefault), + t(L10nKey::SettingsOn), + t(L10nKey::SettingsOff), + ], vhk_idx, cx, |this, ix, _w, cx| { @@ -3176,11 +3265,18 @@ impl Tty7App { cx, )) .child(self.settings_row( - "Warn before closing", - format!("Default follows Defaults, which is {woc_default}."), + t(L10nKey::WarnBeforeClosing), + t_fmt( + L10nKey::SettingsDefaultFollowsDefaults, + &[("value", woc_default)], + ), self.segmented( "ssh-form-woc", - &["Default", "On", "Off"], + &[ + t(L10nKey::SettingsDefault), + t(L10nKey::SettingsOn), + t(L10nKey::SettingsOff), + ], woc_idx, cx, |this, ix, _w, cx| { @@ -3214,7 +3310,7 @@ impl Tty7App { let platform_default = if cfg!(windows) { "PowerShell" } else { - "your login shell" + t(L10nKey::SettingsShellDefaultLoginShell) }; let program_control = div() @@ -3234,7 +3330,11 @@ impl Tty7App { }; let wd_radio = self.segmented( "wd-strategy", - &["Inherit", "Home", "Custom"], + &[ + t(L10nKey::SettingsWdInherit), + t(L10nKey::SettingsWdHome), + t(L10nKey::SettingsWdCustom), + ], wd_idx, cx, |this, ix, _w, cx| { @@ -3257,44 +3357,48 @@ impl Tty7App { v_flex() .child(self.section_intro( - "Shell", - format!( - "The program each new terminal launches. Leave Program empty to use the platform default ({platform_default})." + t(L10nKey::SettingsShell), + t_fmt( + L10nKey::SettingsShellIntro, + &[("default", platform_default)], ), cx, )) .child(self.settings_row( - "Program", - "Executable name on PATH or an absolute path. e.g. zsh, fish, nu, pwsh.", + t(L10nKey::SettingsProgram), + t(L10nKey::SettingsProgramDesc), program_control, cx, )) .child(self.settings_row( - "Arguments", - "Space-separated launch flags. e.g. -l for a login shell.", + t(L10nKey::SettingsArguments), + t(L10nKey::SettingsArgumentsDesc), args_control, cx, )) .child(self.settings_row( - "Start in", - "What a fresh shell starts in: tty7's launch directory, your home folder, or a fixed path.", + t(L10nKey::SettingsStartIn), + t(L10nKey::SettingsStartInDesc), wd_radio, cx, )) - .when(wd_strategy == crate::core::config::WdStrategy::Custom, |v| { - v.child(self.settings_row( - "Custom path", - "The directory new shells start in.", - wd_path_control, - cx, - )) - }) + .when( + wd_strategy == crate::core::config::WdStrategy::Custom, + |v| { + v.child(self.settings_row( + t(L10nKey::SettingsCustomPath), + t(L10nKey::SettingsCustomPathDesc), + wd_path_control, + cx, + )) + }, + ) .child( div() .mt_3() .text_xs() .text_color(muted_fg) - .child("Applies to shells with nothing to inherit — like the first tab of a window. New tabs and splits keep inheriting the active pane's directory, and shells already open keep running."), + .child(t(L10nKey::SettingsShellFooter)), ) .into_any_element() } @@ -3371,7 +3475,11 @@ impl Tty7App { }; let bell_control = self.segmented( "term-bell", - &["Off", "Visual", "Audible"], + &[ + t(L10nKey::SettingsBellModeOff), + t(L10nKey::SettingsBellModeVisual), + t(L10nKey::SettingsBellModeAudible), + ], bell_idx, cx, |this, ix, _w, cx| { @@ -3400,68 +3508,74 @@ impl Tty7App { v_flex() .child(self.render_shell_group(cx)) .child(self.section_rule(cx)) - .child(self.section_header("Scrolling", cx)) + .child(self.section_header(t(L10nKey::SettingsScrolling), cx)) .child(self.settings_row( - "Scrollback", - "Lines of history kept per pane. Applies to new panes.", + t(L10nKey::SettingsScrollback), + t(L10nKey::SettingsScrollbackDesc), scrollback_radio, cx, )) .child(self.settings_row( - "Scroll speed", - "Multiplier applied to mouse-wheel scrolling.", + t(L10nKey::SettingsScrollSpeed), + t(L10nKey::SettingsScrollSpeedDesc), scroll_control, cx, )) .child(self.section_rule(cx)) - .child(self.section_header("Mouse", cx)) + .child(self.section_header(t(L10nKey::SettingsMouse), cx)) .child(self.settings_row( - "Focus follows mouse", - "Hovering a pane focuses it without a click.", + t(L10nKey::SettingsFocusFollowsMouse), + t(L10nKey::SettingsFocusFollowsMouseDesc), focus_switch, cx, )) .child(self.settings_row( - "Hide mouse while typing", - "Hide the pointer as you type; it returns on the next move.", + t(L10nKey::SettingsHideMouseWhileTyping), + t(L10nKey::SettingsHideMouseWhileTypingDesc), mouse_hide_switch, cx, )) .child(self.settings_row( - "Report mouse to apps", - "Let full-screen apps (vim, tmux) handle clicks and scrolling; hold Shift to keep a gesture local.", + t(L10nKey::SettingsReportMouseToApps), + t(L10nKey::SettingsReportMouseToAppsDesc), mouse_report_switch, cx, )) .child(self.section_rule(cx)) - .child(self.section_header("Bell", cx)) + .child(self.section_header(t(L10nKey::SettingsBell), cx)) .child(self.settings_row( - "Terminal bell", - "How a bell (^G) is signalled: silenced, a brief flash, or the system sound.", + t(L10nKey::SettingsTerminalBell), + t(L10nKey::SettingsTerminalBellDesc), bell_control, cx, )) .child(self.section_rule(cx)) - .child(self.section_header("Links", cx)) + .child(self.section_header(t(L10nKey::SettingsLinks), cx)) .child(self.settings_row( - "Detect URLs", - format!("Underline links on hover and open them on {LINK_MODIFIER_LABEL}-click."), + t(L10nKey::DetectUrls), + t_fmt( + L10nKey::SettingsDetectUrlsDesc, + &[("modifier", LINK_MODIFIER_LABEL)], + ), link_switch, cx, )) .child(self.settings_row( - "Forward SSH loopback links", - "When a pane is in SSH, open localhost links through a temporary port forward.", + t(L10nKey::ForwardSshLoopbackLinks), + t(L10nKey::SettingsForwardSshLoopbackLinksDesc), ssh_loopback_switch, cx, )) .child(self.settings_row( - "Open files with", - format!( - "Command run when {LINK_MODIFIER_LABEL}-clicking a file link, instead of \ - the default app. Use {{path}}, {{line}}, {{column}}; a flag whose value \ - is absent is dropped (e.g. herdr edit {{path}} --line={{line}}). Empty \ - uses the default app." + t(L10nKey::OpenFilesWith), + t_fmt( + L10nKey::SettingsOpenFilesWithDesc, + &[ + ("modifier", LINK_MODIFIER_LABEL), + ("path", "{path}"), + ("line", "{line}"), + ("column", "{column}"), + ], ), link_file_command_control, cx, @@ -3506,9 +3620,8 @@ impl Tty7App { ) .into_any_element(); self.settings_row( - "Option (⌥) acts as Meta", - "⌥+key sends the escape chord shells expect (⌥B = back one word) \ - instead of typing a special character (∫).", + t(L10nKey::SettingsOptionAsMeta), + t(L10nKey::SettingsOptionAsMetaDesc), switch, cx, ) @@ -3516,48 +3629,45 @@ impl Tty7App { v_flex() .child(self.section_intro( - "Prompt", - "tty7's own menus at the shell prompt. Turn one off to hand the key back to the shell.", + t(L10nKey::SettingsPrompt), + t(L10nKey::SettingsPromptIntro), cx, )) .child(self.settings_row( - "Tab completion", - "Tab at the prompt opens tty7's completion menu. When off, Tab goes to the \ - shell's own completion instead.", + t(L10nKey::SettingsTabCompletion), + t(L10nKey::SettingsTabCompletionDesc), tab_completion_switch, cx, )) .child(self.settings_row( - "History search", - "⌃R at the prompt opens tty7's fuzzy history menu. When off, ⌃R goes to the \ - shell instead — its own reverse-i-search, or whatever you've bound there \ - (fzf, percol).", + t(L10nKey::SettingsHistorySearch), + t(L10nKey::SettingsHistorySearchDesc), history_search_switch, cx, )) .child(self.section_rule(cx)) - .child(self.section_header("Selection & clipboard", cx)) + .child(self.section_header(t(L10nKey::SettingsSelectionClipboard), cx)) .child(self.settings_row( - "Smart selection", - "Double-click selects the whole URL, file path, email, or bracket pair under the cursor.", + t(L10nKey::SettingsSmartSelection), + t(L10nKey::SettingsSmartSelectionDesc), smart_select_switch, cx, )) .child(self.settings_row( - "Copy on select", - "Selecting text with the mouse copies it to the clipboard right away, no ⌘C needed.", + t(L10nKey::SettingsCopyOnSelect), + t(L10nKey::SettingsCopyOnSelectDesc), copy_on_select_switch, cx, )) .child(self.settings_row( - "Trim trailing spaces on copy", - "Strip trailing whitespace from each copied line.", + t(L10nKey::SettingsTrimTrailingSpaces), + t(L10nKey::SettingsTrimTrailingSpacesDesc), trim_switch, cx, )) .when_some(option_alt_row, |v, row| { v.child(self.section_rule(cx)) - .child(self.section_header("Keyboard", cx)) + .child(self.section_header(t(L10nKey::SettingsKeyboard), cx)) .child(row) }) .into_any_element() @@ -3578,9 +3688,8 @@ impl Tty7App { None => (AgentHooksView::Loading, None, HostId::LOCAL), }; let mut page = v_flex().child(self.section_intro( - "Agents", - "Hook integrations give panes running these agents live session status \ - (working / waiting / done) in the tab bar. Only active inside tty7.", + t(L10nKey::SettingsAgentsIntro), + t(L10nKey::SettingsAgentsIntroDesc), cx, )); @@ -3594,7 +3703,7 @@ impl Tty7App { .py_4() .text_sm() .text_color(muted_fg) - .child("Reading this machine's agent config…"), + .child(t(L10nKey::SettingsReadingAgentConfig)), ) .into_any_element(); } @@ -3607,14 +3716,16 @@ impl Tty7App { for (i, row) in rows.into_iter().enumerate() { let agent = row.agent; let (dot_color, status_text) = match row.state { - HooksState::NotInstalled => (muted_fg, "Not installed"), - HooksState::Installed => (success, "Installed"), - HooksState::Outdated => (warning, "Outdated"), + HooksState::NotInstalled => { + (muted_fg, t(L10nKey::SettingsStatusNotInstalled)) + } + HooksState::Installed => (success, t(L10nKey::SettingsStatusInstalled)), + HooksState::Outdated => (warning, t(L10nKey::SettingsStatusOutdated)), }; let primary_label = match row.state { - HooksState::NotInstalled => "Install", - HooksState::Installed => "Reinstall", - HooksState::Outdated => "Update", + HooksState::NotInstalled => t(L10nKey::SettingsInstall), + HooksState::Installed => t(L10nKey::SettingsReinstall), + HooksState::Outdated => t(L10nKey::SettingsUpdate), }; let row_note = note .as_ref() @@ -3645,7 +3756,7 @@ impl Tty7App { .when(row.state != HooksState::NotInstalled, |r| { r.child( Button::new(("agent-hooks-uninstall", i)) - .label("Uninstall") + .label(t(L10nKey::SettingsUninstall)) .small() .on_click(cx.listener(move |this, _, _w, cx| { this.settings_uninstall_agent_hooks(agent, cx) @@ -3726,10 +3837,10 @@ impl Tty7App { })), ) .when(offline > 0, |col| { - col.child(div().text_xs().text_color(muted_fg).child(format!( - "{offline} more saved machine{} not connected — open a workspace on one to \ - install its hooks there.", - if offline == 1 { " is" } else { "s are" } + col.child(div().text_xs().text_color(muted_fg).child(t_plural( + L10nKey::SettingsOfflineMachines, + offline, + &[], ))) }), ) @@ -3772,7 +3883,11 @@ impl Tty7App { }; let notify_radio = self.segmented( "wt-notify", - &["Never", "When Unfocused", "Always"], + &[ + t(L10nKey::NotifyModeNever), + t(L10nKey::NotifyModeUnfocused), + t(L10nKey::NotifyModeAlways), + ], notify_idx, cx, |this, ix, _w, cx| { @@ -3818,7 +3933,11 @@ impl Tty7App { .into_any_element(); let startup_radio = self.segmented( "wt-startup", - &["Normal", "Maximized", "Fullscreen"], + &[ + t(L10nKey::SettingsStartupNormal), + t(L10nKey::SettingsStartupMaximized), + t(L10nKey::SettingsStartupFullscreen), + ], startup_idx, cx, |this, ix, _w, cx| { @@ -3832,7 +3951,7 @@ impl Tty7App { ); let new_tab_radio = self.segmented( "wt-new-tab-pos", - &["After current", "At end"], + &[t(L10nKey::SettingsAfterCurrent), t(L10nKey::SettingsAtEnd)], new_tab_idx, cx, |this, ix, _w, cx| { @@ -3846,7 +3965,7 @@ impl Tty7App { ); let tab_bar_radio = self.segmented( "wt-tab-bar-pos", - &["Top", "Left"], + &[t(L10nKey::SettingsTop), t(L10nKey::SettingsLeft)], tab_bar_idx, cx, |this, ix, _w, cx| { @@ -3864,7 +3983,7 @@ impl Tty7App { .into_any_element(); let sidebar_grouping_radio = self.segmented( "wt-sidebar-grouping", - &["By repo", "Flat"], + &[t(L10nKey::SettingsByRepo), t(L10nKey::SettingsFlat)], sidebar_grouping_idx, cx, |this, ix, _w, cx| { @@ -3878,78 +3997,74 @@ impl Tty7App { ); v_flex() - .child(self.section_header("Window", cx)) + .child(self.section_header(t(L10nKey::SettingsWindow), cx)) .child(self.settings_row( - "Startup window", - "Window state when tty7 launches.", + t(L10nKey::SettingsStartupWindow), + t(L10nKey::SettingsStartupWindowDesc), startup_radio, cx, )) .child(self.settings_row( - "Remember window size & position", - "Reopen at the size and position the window had when tty7 last quit. Off opens centered at the default size.", + t(L10nKey::SettingsRememberWindowSize), + t(L10nKey::SettingsRememberWindowSizeDesc), remember_window_switch, cx, )) .child(self.settings_row( - "Restore last layout", - "Reopen the last window's tabs, splits, and directories on launch. Off starts with a single fresh terminal.", + t(L10nKey::SettingsRestoreLastLayout), + t(L10nKey::SettingsRestoreLastLayoutDesc), restore_switch, cx, )) .child(self.settings_row( - "Confirm before closing the last window", - "Ask first, since that close also quits tty7. Off closes straight away — \ - either way your shells keep running in the background.", + t(L10nKey::SettingsConfirmLastWindowClose), + t(L10nKey::SettingsConfirmLastWindowCloseDesc), confirm_close_switch, cx, )) .child(self.settings_row( - "Show tray icon", - "Keep a status item in the system tray / menu bar: it signals when a \ - coding agent needs your input, and its menu jumps to agent panes.", + t(L10nKey::SettingsShowTrayIcon), + t(L10nKey::SettingsShowTrayIconDesc), tray_switch, cx, )) .child(self.section_rule(cx)) - .child(self.section_header("Tabs", cx)) + .child(self.section_header(t(L10nKey::SettingsTabs), cx)) .child(self.settings_row( - "New tab position", - "Where a freshly opened tab is inserted.", + t(L10nKey::SettingsNewTabPosition), + t(L10nKey::SettingsNewTabPositionDesc), new_tab_radio, cx, )) .child(self.settings_row( - "Tab bar position", - "Show tabs as a horizontal strip on top or a vertical sidebar on the left.", + t(L10nKey::SettingsTabBarPosition), + t(L10nKey::SettingsTabBarPositionDesc), tab_bar_radio, cx, )) .child(self.settings_row( - "Sidebar grouping", - "Group sidebar tabs under a header per git repository, with non-repo tabs \ - in a Scratch section. Only applies to the left sidebar.", + t(L10nKey::SettingsSidebarGrouping), + t(L10nKey::SettingsSidebarGroupingDesc), sidebar_grouping_radio, cx, )) .child(self.settings_row( - "Open diff preview from sidebar counts", - "Click a row's +N −N to open the working-tree diff in an overlay. Off keeps the \ - branch and the counts on the row and just stops them being clickable.", + t(L10nKey::SettingsDiffPreviewFromCounts), + t(L10nKey::SettingsDiffPreviewFromCountsDesc), sidebar_diff_switch, cx, )) .child(self.section_rule(cx)) - .child(self.section_header("Notifications", cx)) + .child(self.section_header(t(L10nKey::SettingsNotifications), cx)) .child(self.settings_row( - "Notify on command finish", - "Desktop alert after a long foreground command completes.", + t(L10nKey::SettingsNotifyOnCommandFinish), + t(L10nKey::SettingsNotifyOnCommandFinishDesc), notify_radio, cx, )) .child(self.settings_row( - "Notify threshold", - "How long a command must run to qualify as \"long\".", + t(L10nKey::SettingsNotifyThreshold), + t(L10nKey::SettingsNotifyThresholdDesc), threshold_radio, cx, )) @@ -4010,8 +4125,8 @@ impl Tty7App { })) .into_any_element(); let root = v_flex().child(self.settings_row( - "Sync with system", - "Follow the OS appearance with separate light and dark themes.", + t(L10nKey::SettingsSyncWithSystem), + t(L10nKey::SettingsSyncWithSystemDesc), follow_switch, cx, )); @@ -4042,23 +4157,30 @@ impl Tty7App { let active = presets::by_id(cx, &active_id); let name = active.name.clone(); let kind = if active.path.is_some() { - "Custom" + t(L10nKey::SettingsCustom) } else { - "Built-in" + t(L10nKey::SettingsBuiltIn) + }; + let mode = if active.dark { + t(L10nKey::SettingsDark) + } else { + t(L10nKey::SettingsLight) + }; + let mode_label = if active.dark { + t(L10nKey::SettingsDarkMode) + } else { + t(L10nKey::SettingsLightMode) }; let caption = match slot { - ThemeSlot::Manual => { - let mode = if active.dark { "Dark" } else { "Light" }; - format!("{kind} · {mode}") - } + ThemeSlot::Manual => format!("{kind} · {mode}"), ThemeSlot::Light if !crate::ui::theme::system_dark(cx) => { - format!("Light mode · {kind} · Active") + format!("{mode_label} · {kind} · {}", t(L10nKey::SettingsActive)) } - ThemeSlot::Light => format!("Light mode · {kind}"), + ThemeSlot::Light => format!("{mode_label} · {kind}"), ThemeSlot::Dark if crate::ui::theme::system_dark(cx) => { - format!("Dark mode · {kind} · Active") + format!("{mode_label} · {kind} · {}", t(L10nKey::SettingsActive)) } - ThemeSlot::Dark => format!("Dark mode · {kind}"), + ThemeSlot::Dark => format!("{mode_label} · {kind}"), }; let to_u32 = |(r, g, b): (u8, u8, u8)| (r as u32) << 16 | (g as u32) << 8 | b as u32; let swatches = h_flex().gap_1().mt_1p5().children((1..=6).map(|i| { @@ -4115,7 +4237,7 @@ impl Tty7App { .gap_1() .text_sm() .text_color(muted_fg) - .child("Change theme") + .child(t(L10nKey::SettingsChangeTheme)) .child(Icon::new(IconName::ChevronRight).small()), ), ) @@ -4166,7 +4288,7 @@ impl Tty7App { .text_base() .font_weight(FontWeight::SEMIBOLD) .text_color(foreground) - .child("Themes"), + .child(t(L10nKey::SettingsThemes)), ) .child( div().occlude().child( @@ -4184,9 +4306,9 @@ impl Tty7App { .text_xs() .text_color(muted_fg) .child(match slot { - ThemeSlot::Manual => "Change your current theme.", - ThemeSlot::Light => "Choose the theme for light mode.", - ThemeSlot::Dark => "Choose the theme for dark mode.", + ThemeSlot::Manual => t(L10nKey::SettingsThemePanelManual), + ThemeSlot::Light => t(L10nKey::SettingsThemePanelLight), + ThemeSlot::Dark => t(L10nKey::SettingsThemePanelDark), }); let search_box = div().px_4().pb_3().child( @@ -4333,7 +4455,7 @@ impl Tty7App { let preset_control = self.segmented( "kb-preset", - &["Default", "tmux"], + &[t(L10nKey::SettingsDefault), "tmux"], usize::from(tmux), cx, |this, ix, _w, cx| { @@ -4362,11 +4484,14 @@ impl Tty7App { .text_sm() .font_weight(FontWeight::MEDIUM) .text_color(foreground) - .child("Preset"), + .child(t(L10nKey::SettingsPreset)), ) - .child(div().text_xs().text_color(muted).child( - "tmux remaps pane/tab actions onto prefix sequences (e.g. Ctrl-B then C).", - )), + .child( + div() + .text_xs() + .text_color(muted) + .child(t(L10nKey::SettingsPresetDesc)), + ), ) .child(h_flex().flex_shrink_0().child(preset_control)); @@ -4379,7 +4504,7 @@ impl Tty7App { .text_sm() .font_weight(FontWeight::MEDIUM) .text_color(foreground) - .child("Prefix"), + .child(t(L10nKey::SettingsPrefix)), ) .child(h_flex().flex_shrink_0().child(prefix_control)); @@ -4404,13 +4529,18 @@ impl Tty7App { .unwrap_or_default(); let row = h_flex().gap_2().items_center(); let row = if chords.is_empty() { - row.child(div().text_xs().text_color(accent).child("Press keys…")) + row.child( + div() + .text_xs() + .text_color(accent) + .child(t(L10nKey::SettingsPressKeys)), + ) } else { row.child(keycaps(&chords.join(" "))).child( div() .text_xs() .text_color(muted) - .child("pause to save · Esc"), + .child(t(L10nKey::SettingsPauseToSaveEsc)), ) }; row.into_any_element() @@ -4449,7 +4579,7 @@ impl Tty7App { .when(is_overridden, |r| { r.child( Button::new(SharedString::from(format!("reset-{action}"))) - .label("Reset") + .label(t(L10nKey::Reset)) .small() .on_click(cx.listener(move |this, _, _w, cx| { this.reset_keybinding(action_for_reset.clone(), cx) @@ -4475,16 +4605,20 @@ impl Tty7App { v_flex() .child(self.section_intro( - "Keybindings", - "Click a shortcut, then press the new keys — it saves after a brief pause. Chain keys for a sequence like Ctrl-B then X. Esc cancels; Backspace removes the last key, or resets the shortcut to default when pressed first.", + t(L10nKey::SettingsNavKeybindings), + t(L10nKey::SettingsKeybindingsIntroDesc), cx, )) .child(preset_row) .when(tmux, |v| v.child(prefix_row)) .when(tmux, |v| { - v.child(div().py_1().text_xs().text_color(muted).child( - "With a prefix active, a bare prefix key reaches the shell after a ~1s pause, and prefix + an unbound key is sent through to the terminal.", - )) + v.child( + div() + .py_1() + .text_xs() + .text_color(muted) + .child(t(L10nKey::SettingsPrefixNote)), + ) }) .when_some(note, |v, note| { v.child(div().py_1().text_xs().text_color(accent).child(note)) @@ -4492,11 +4626,11 @@ impl Tty7App { .child( h_flex().justify_end().py_2().child( Button::new("kb-restore-all") - .label("Restore all defaults") + .label(t(L10nKey::SettingsRestoreAllDefaults)) .small() - .on_click(cx.listener(|this, _, _w, cx| { - this.restore_default_keybindings(cx) - })), + .on_click( + cx.listener(|this, _, _w, cx| this.restore_default_keybindings(cx)), + ), ), ) .child(list) @@ -4558,21 +4692,46 @@ impl Tty7App { register_disabled, unregister_disabled, ) = match explorer_status.as_ref() { - Ok(crate::core::explorer_context_menu::Status::NotRegistered) => { - ("Not registered", muted_fg, "Register", false, true) - } - Ok(crate::core::explorer_context_menu::Status::Registered) => { - ("Registered", success, "Register", true, false) - } - Ok(crate::core::explorer_context_menu::Status::NeedsUpdate) => { - ("Needs update", warning, "Update", false, false) - } - Ok(crate::core::explorer_context_menu::Status::Unsupported) => { - ("Unavailable", muted_fg, "Register", true, true) - } - Err(_) => ("Status unavailable", warning, "Register", false, false), + Ok(crate::core::explorer_context_menu::Status::NotRegistered) => ( + t(L10nKey::SettingsExplorerNotRegistered), + muted_fg, + t(L10nKey::SettingsExplorerRegister), + false, + true, + ), + Ok(crate::core::explorer_context_menu::Status::Registered) => ( + t(L10nKey::SettingsExplorerRegistered), + success, + t(L10nKey::SettingsExplorerRegister), + true, + false, + ), + Ok(crate::core::explorer_context_menu::Status::NeedsUpdate) => ( + t(L10nKey::SettingsExplorerNeedsUpdate), + warning, + t(L10nKey::SettingsExplorerUpdate), + false, + false, + ), + Ok(crate::core::explorer_context_menu::Status::Unsupported) => ( + t(L10nKey::SettingsExplorerUnavailable), + muted_fg, + t(L10nKey::SettingsExplorerRegister), + true, + true, + ), + Err(_) => ( + t(L10nKey::SettingsExplorerStatusUnavailable), + warning, + t(L10nKey::SettingsExplorerRegister), + false, + false, + ), }; - let explorer_feedback = explorer_note.or_else(|| explorer_status.err()); + let explorer_feedback = explorer_note + .as_ref() + .map(ExplorerContextMenuNote::localized) + .or_else(|| explorer_status.err()); let logo = Arc::new(Image::from_bytes( ImageFormat::Png, @@ -4580,7 +4739,7 @@ impl Tty7App { )); v_flex() - .child(self.section_header("About", cx)) + .child(self.section_header(t(L10nKey::SettingsNavAbout), cx)) .child( h_flex() .gap_4() @@ -4597,7 +4756,8 @@ impl Tty7App { .child("tty7"), ) .child(div().text_sm().text_color(muted_fg).child(format!( - "Version {}", + "{} {}", + t(L10nKey::SettingsVersion), env!("CARGO_PKG_VERSION") ))) .child( @@ -4616,16 +4776,19 @@ impl Tty7App { div() .text_sm() .text_color(foreground) - .child("A terminal workbench: shells, workspaces, SSH, coding agents."), + .child(t(L10nKey::SettingsAboutDesc1)), + ) + .child( + div() + .text_sm() + .text_color(muted_fg) + .child(t(L10nKey::SettingsAboutDesc2)), ) - .child(div().text_sm().text_color(muted_fg).child( - "Editor-grade input in every shell, shells that outlive quits and reboots without tmux, a native SSH stack with profiles and port forwarding, and live status for panes running coding agents.", - )) .child( div() .text_xs() .text_color(muted_fg) - .child("Pure Rust · GPU rendering on Zed's gpui · VT core from Alacritty"), + .child(t(L10nKey::SettingsAboutTech)), ), ) .child( @@ -4638,7 +4801,7 @@ impl Tty7App { .text_sm() .font_weight(FontWeight::MEDIUM) .text_color(foreground) - .child("Updates"), + .child(t(L10nKey::SettingsUpdates)), ) .when_some(update, |this, upd| { let button_label = if upd.installable { @@ -4653,9 +4816,10 @@ impl Tty7App { h_flex() .gap_3() .items_center() - .child(div().text_sm().text_color(foreground).child( - format!("Version {} is available.", upd.version), - )) + .child(div().text_sm().text_color(foreground).child(t_fmt( + L10nKey::SettingsVersionAvailable, + &[("version", &upd.version)], + ))) .child( Button::new("install-update") .label(button_label) @@ -4710,7 +4874,7 @@ impl Tty7App { div() .text_sm() .text_color(foreground) - .child("Check for updates on launch"), + .child(t(L10nKey::SettingsCheckUpdatesOnLaunch)), ), ), ) @@ -4725,11 +4889,14 @@ impl Tty7App { .text_sm() .font_weight(FontWeight::MEDIUM) .text_color(foreground) - .child("Windows Explorer"), + .child(t(L10nKey::SettingsExplorerContextMenu)), + ) + .child( + div() + .text_sm() + .text_color(muted_fg) + .child(t(L10nKey::SettingsExplorerContextMenuDesc)), ) - .child(div().text_sm().text_color(muted_fg).child( - "Add “Open in tty7” when you right-click a folder and “Open tty7 here” when you right-click a folder background. This is off by default and is registered only for your Windows account.", - )) .child( h_flex() .gap_2() @@ -4756,7 +4923,7 @@ impl Tty7App { ) .child( Button::new("explorer-menu-unregister") - .label("Unregister") + .label(t(L10nKey::SettingsExplorerUnregister)) .small() .disabled(unregister_disabled) .on_click(cx.listener(|this, _, _window, cx| { @@ -4772,9 +4939,12 @@ impl Tty7App { .child(message), ) }) - .child(div().text_xs().text_color(muted_fg).child( - "On Windows 11, classic shell entries may appear under “Show more options”.", - )), + .child( + div() + .text_xs() + .text_color(muted_fg) + .child(t(L10nKey::SettingsExplorerWindows11Note)), + ), ) }) .child( @@ -4787,11 +4957,14 @@ impl Tty7App { .text_sm() .font_weight(FontWeight::MEDIUM) .text_color(foreground) - .child("Command line"), + .child(t(L10nKey::SettingsCommandLine)), + ) + .child( + div() + .text_sm() + .text_color(muted_fg) + .child(t(L10nKey::SettingsCommandLineDesc)), ) - .child(div().text_sm().text_color(muted_fg).child( - "Put the bundled `tty7` command on your PATH at launch, so scripts and coding agents can drive tty7 from any terminal. Inside a tty7 pane it works either way. Turn this off if you keep your own `tty7` — one you built or installed yourself — and do not want it shadowed. Takes effect at next launch.", - )) .child( h_flex() .gap_2() @@ -4807,7 +4980,7 @@ impl Tty7App { div() .text_sm() .text_color(foreground) - .child("Install the `tty7` command on PATH"), + .child(t(L10nKey::SettingsInstallCliOnPath)), ), ), ) @@ -4821,15 +4994,18 @@ impl Tty7App { .text_sm() .font_weight(FontWeight::MEDIUM) .text_color(foreground) - .child("Server"), + .child(t(L10nKey::SettingsServer)), + ) + .child( + div() + .text_sm() + .text_color(muted_fg) + .child(t(L10nKey::SettingsServerDesc)), ) - .child(div().text_sm().text_color(muted_fg).child( - "Restart the server on this computer to pick up a newly granted macOS permission, recover if it stops responding, or start from a clean slate. This ends all running shells here; your tabs and layout reopen with fresh shells. A remote machine's server is restarted from its own menu in the workspace switcher.", - )) .child( h_flex().child( Button::new("restart-daemon") - .label("Restart server…") + .label(t(L10nKey::SettingsRestartServer)) .small() .on_click(cx.listener(|this, _, window, cx| { this.restart_daemon(window, cx) @@ -4879,7 +5055,7 @@ mod tests { .iter() .find(|e| e.section == section) .expect("checked by every_section_has_search_entries"); - let query = entry.title.to_lowercase(); + let query = t(entry.title).to_lowercase(); let landed = best_matching_section(&query); assert!( landed.is_some(), @@ -4917,6 +5093,20 @@ mod tests { } } + #[test] + fn explorer_context_menu_search_entry_uses_localized_keys() { + let entry = settings_search_entries() + .iter() + .find(|entry| entry.title == L10nKey::SettingsExplorerContextMenu) + .expect("Explorer settings should be searchable"); + + assert_eq!(entry.section.profile_label(), "settings:about"); + assert_eq!( + entry.keywords, + L10nKey::SettingsSearchExplorerContextMenuKeywords + ); + } + #[test] fn close_confirmation_toggle_is_findable() { for query in [ @@ -4950,7 +5140,9 @@ mod tests { "Option (⌥) acts as Meta", ] { assert!( - settings_search_entries().iter().any(|e| e.title == title), + settings_search_entries() + .iter() + .any(|e| t(e.title) == title), "no index entry titled {title:?}" ); } @@ -4960,9 +5152,10 @@ mod tests { fn agent_rows_are_in_the_search_index() { for agent in crate::core::agent_hooks::HookAgent::ALL { assert!( - settings_search_entries().iter().any( - |e| e.section == SettingsSection::Agents && e.title == agent.display_name() - ), + settings_search_entries() + .iter() + .any(|e| e.section == SettingsSection::Agents + && t(e.title) == agent.display_name()), "no Agents index entry titled {:?}", agent.display_name() ); diff --git a/src/ui/sftp.rs b/src/ui/sftp.rs index f9f7a65a..d1bb1582 100644 --- a/src/ui/sftp.rs +++ b/src/ui/sftp.rs @@ -20,6 +20,7 @@ use crate::daemon::protocol::{ use crate::daemon::ssh::sftp::{remote_basename, remote_join, remote_parent, safe_local_name}; use crate::terminal::RemoteTerminal; use crate::ui::app::{CONTENT_INSET, Tty7App}; +use crate::ui::i18n::{L10nKey, t, t_fmt}; #[derive(Clone, Copy)] enum SftpMenuAction { @@ -70,7 +71,10 @@ impl SftpRoute { }; match RemoteTerminal::on_workspace(req) { Ok(crate::daemon::protocol::DaemonMsg::SftpEntries(e)) => Ok(e), - Ok(other) => Err(format!("unexpected reply: {other:?}")), + Ok(other) => Err(t_fmt( + L10nKey::SftpErrorUnexpectedReply, + &[("reply", &format!("{other:?}"))], + )), Err(e) => Err(e.to_string()), } } @@ -83,7 +87,10 @@ impl SftpRoute { }; match RemoteTerminal::on_workspace(req) { Ok(crate::daemon::protocol::DaemonMsg::SftpOpResult(r)) => r, - Ok(other) => SftpOpResult::Error(format!("unexpected reply: {other:?}")), + Ok(other) => SftpOpResult::Error(t_fmt( + L10nKey::SftpErrorUnexpectedReply, + &[("reply", &format!("{other:?}"))], + )), Err(e) => SftpOpResult::Error(e.to_string()), } } @@ -98,7 +105,10 @@ impl SftpRoute { }; match RemoteTerminal::on_workspace(req) { Ok(crate::daemon::protocol::DaemonMsg::SftpTransferStarted { job_id }) => Ok(job_id), - Ok(other) => Err(format!("unexpected reply: {other:?}")), + Ok(other) => Err(t_fmt( + L10nKey::SftpErrorUnexpectedReply, + &[("reply", &format!("{other:?}"))], + )), Err(e) => Err(e.to_string()), } } @@ -146,7 +156,10 @@ pub(crate) struct SftpPanelState { impl SftpPanelState { pub(crate) fn new(window: &mut Window, cx: &mut Context) -> Self { - let filter_input = cx.new(|cx| InputState::new(window, cx).placeholder("Search")); + let filter_input = cx.new(|cx| { + InputState::new(window, cx) + .placeholder(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Search)) + }); let sub = cx.subscribe_in(&filter_input, window, |_this, _input, ev, _w, cx| { if matches!(ev, gpui_component::input::InputEvent::Change) { cx.notify(); @@ -496,7 +509,10 @@ impl Tty7App { return; }; if !safe_local_name(&entry.name) { - self.sftp_panel.error = Some(format!("refusing unsafe remote name {:?}", entry.name)); + self.sftp_panel.error = Some(t_fmt( + L10nKey::SftpErrorUnsafeRemoteName, + &[("name", &format!("{:?}", entry.name))], + )); cx.notify(); return; } @@ -595,13 +611,19 @@ impl Tty7App { } pub(crate) fn sftp_begin_new_folder(&mut self, window: &mut Window, cx: &mut Context) { - let input = cx.new(|cx| InputState::new(window, cx).placeholder("New folder name")); + let input = cx.new(|cx| { + InputState::new(window, cx) + .placeholder(crate::ui::i18n::t(crate::ui::i18n::L10nKey::NewFolderName)) + }); self.sftp_panel.editing = Some(SftpEdit::NewFolder(input)); cx.notify(); } pub(crate) fn sftp_begin_new_file(&mut self, window: &mut Window, cx: &mut Context) { - let input = cx.new(|cx| InputState::new(window, cx).placeholder("New file name")); + let input = cx.new(|cx| { + InputState::new(window, cx) + .placeholder(crate::ui::i18n::t(crate::ui::i18n::L10nKey::NewFileName)) + }); self.sftp_panel.editing = Some(SftpEdit::NewFile(input)); cx.notify(); } @@ -685,7 +707,8 @@ impl Tty7App { mode, }), Err(_) => { - self.sftp_panel.error = Some("invalid octal mode".to_string()); + self.sftp_panel.error = + Some(t(L10nKey::SftpErrorInvalidOctalMode).to_string()); cx.notify(); return; } @@ -832,7 +855,13 @@ impl Tty7App { cx: &mut Context, ) -> AnyElement { let controls = self.sftp_controls(cx); - let title = self.panel_title("Files", Some(host), Some(controls), window, cx); + let title = self.panel_title( + t(L10nKey::SftpPanelTitleFiles), + Some(host), + Some(controls), + window, + cx, + ); let breadcrumb = self.render_sftp_breadcrumb(cx); let filter = div() .id("panel-sftp-filter") @@ -885,7 +914,7 @@ impl Tty7App { false, cx, ) - .tooltip("Refresh") + .tooltip(t(L10nKey::SftpTooltipRefresh)) .on_click(cx.listener(|this, _, _w, cx| this.sftp_refresh(cx))), ), ) @@ -897,16 +926,19 @@ impl Tty7App { false, cx, ) - .tooltip("More") + .tooltip(t(L10nKey::SftpTooltipMore)) .dropdown_menu_with_anchor(gpui::Anchor::TopRight, { let app = cx.entity().downgrade(); move |menu, _window, _cx| { let mut menu = menu.min_w(px(190.)); for (label, action) in [ - ("New folder", SftpMenuAction::NewFolder), - ("New file", SftpMenuAction::NewFile), - ("Upload…", SftpMenuAction::Upload), - ("Go to shell directory", SftpMenuAction::GotoShellCwd), + (t(L10nKey::SftpMenuNewFolder), SftpMenuAction::NewFolder), + (t(L10nKey::SftpMenuNewFile), SftpMenuAction::NewFile), + (t(L10nKey::SftpMenuUpload), SftpMenuAction::Upload), + ( + t(L10nKey::SftpMenuGotoShellCwd), + SftpMenuAction::GotoShellCwd, + ), ] { menu = menu.item(PopupMenuItem::new(label).on_click({ let app = app.clone(); @@ -919,9 +951,9 @@ impl Tty7App { } menu.separator().item( PopupMenuItem::new(if history { - "Hide transfer history" + t(L10nKey::SftpMenuHideTransferHistory) } else { - "Transfer history" + t(L10nKey::SftpMenuTransferHistory) }) .on_click({ let app = app.clone(); @@ -1027,12 +1059,15 @@ impl Tty7App { let border = cx.theme().border; let foreground = cx.theme().foreground; let (title, input): (String, _) = match self.sftp_panel.editing.as_ref()? { - SftpEdit::NewFolder(input) => ("New folder".to_string(), input), - SftpEdit::NewFile(input) => ("New file".to_string(), input), - SftpEdit::Rename { input, .. } => ("Rename".to_string(), input), + SftpEdit::NewFolder(input) => (t(L10nKey::SftpEditNewFolder).to_string(), input), + SftpEdit::NewFile(input) => (t(L10nKey::SftpEditNewFile).to_string(), input), + SftpEdit::Rename { input, .. } => (t(L10nKey::SftpEditRename).to_string(), input), SftpEdit::Chmod { readable, input, .. - } => (format!("Permissions · {readable}"), input), + } => ( + t_fmt(L10nKey::SftpEditPermissions, &[("mode", readable)]), + input, + ), }; Some( v_flex() @@ -1058,14 +1093,14 @@ impl Tty7App { .justify_end() .child( Button::new("sftp-edit-cancel") - .label("Cancel") + .label(t(L10nKey::Cancel)) .ghost() .xsmall() .on_click(cx.listener(|this, _, _w, cx| this.sftp_cancel_edit(cx))), ) .child( Button::new("sftp-edit-ok") - .label("OK") + .label(t(L10nKey::Ok)) .xsmall() .primary() .on_click(cx.listener(|this, _, _w, cx| this.sftp_commit_edit(cx))), @@ -1105,12 +1140,12 @@ impl Tty7App { let show_go_up = self.sftp_panel.cwd != "/" && filter.trim().is_empty(); if entries.is_empty() && !show_go_up { - let text = if self.sftp_panel.loading { - "Loading…" + let text: gpui::SharedString = if self.sftp_panel.loading { + t(L10nKey::SftpLoading).into() } else { - "Empty directory." + t(L10nKey::SftpEmptyDirectory).into() }; - return container.child(note(text.into(), muted)); + return container.child(note(text, muted)); } let mut list = v_flex().gap(px(1.)).py(px(2.)); @@ -1220,7 +1255,11 @@ impl Tty7App { ) -> gpui_component::menu::PopupMenu { let mut menu = menu.min_w(px(180.)); - let primary_label = if dir_like { "Open" } else { "Download" }; + let primary_label = if dir_like { + t(L10nKey::SftpContextOpen) + } else { + t(L10nKey::Download) + }; menu = menu.item(PopupMenuItem::new(primary_label).on_click({ let app = app.clone(); let entry = entry.clone(); @@ -1231,18 +1270,20 @@ impl Tty7App { })); if is_symlink { - menu = menu.item(PopupMenuItem::new("Follow symlink").on_click({ - let app = app.clone(); - let entry = entry.clone(); - move |_, _window, cx| { + menu = menu.item( + PopupMenuItem::new(t(L10nKey::SftpContextFollowSymlink)).on_click({ + let app = app.clone(); let entry = entry.clone(); - let _ = app.update(cx, |this, cx| this.sftp_follow_symlink(entry, cx)); - } - })); + move |_, _window, cx| { + let entry = entry.clone(); + let _ = app.update(cx, |this, cx| this.sftp_follow_symlink(entry, cx)); + } + }), + ); } menu = menu - .item(PopupMenuItem::new("Rename").on_click({ + .item(PopupMenuItem::new(t(L10nKey::SftpContextRename)).on_click({ let app = app.clone(); let name = entry.name.clone(); move |_, window, cx| { @@ -1250,7 +1291,7 @@ impl Tty7App { let _ = app.update(cx, |this, cx| this.sftp_begin_rename(name, window, cx)); } })) - .item(PopupMenuItem::new("chmod…").on_click({ + .item(PopupMenuItem::new(t(L10nKey::SftpContextChmod)).on_click({ let app = app.clone(); let entry = entry.clone(); move |_, window, cx| { @@ -1261,15 +1302,17 @@ impl Tty7App { .separator(); menu.item( - PopupMenuItem::element(move |_window, _cx| div().text_color(danger).child("Delete")) - .on_click({ - let app = app.clone(); + PopupMenuItem::element(move |_window, _cx| { + div().text_color(danger).child(t(L10nKey::Delete)) + }) + .on_click({ + let app = app.clone(); + let entry = entry.clone(); + move |_, _window, cx| { let entry = entry.clone(); - move |_, _window, cx| { - let entry = entry.clone(); - let _ = app.update(cx, |this, cx| this.sftp_delete_entry(entry, cx)); - } - }), + let _ = app.update(cx, |this, cx| this.sftp_delete_entry(entry, cx)); + } + }), ) } @@ -1312,11 +1355,20 @@ impl Tty7App { 0.0 }; let summary = if running > 0 { - format!("{running} transferring · {pct:.0}%") + t_fmt( + L10nKey::SftpTransferSummaryRunning, + &[ + ("count", &running.to_string()), + ("pct", &format!("{pct:.0}")), + ], + ) } else if failed > 0 { - format!("{failed} failed") + t_fmt( + L10nKey::SftpTransferSummaryFailed, + &[("count", &failed.to_string())], + ) } else { - "Transfers".to_string() + t(L10nKey::SftpTransferSummaryIdle).to_string() }; let summary_color = if running == 0 && failed > 0 { danger @@ -1363,7 +1415,7 @@ impl Tty7App { .w(px(18.)) .h(px(18.)) .rounded(px(4.)) - .tooltip("Dismiss") + .tooltip(t(L10nKey::Dismiss)) .on_click(cx.listener(|this, _, _w, cx| this.sftp_dismiss_tray(cx))), ), ); @@ -1383,7 +1435,7 @@ impl Tty7App { .py(px(3.)) .text_size(px(11.5)) .text_color(muted) - .child("No transfers yet."), + .child(t(L10nKey::SftpNoTransfers)), ) } else { let mut list = v_flex().px(px(CONTENT_INSET)).pb(px(6.)).gap(px(6.)); @@ -1430,14 +1482,20 @@ impl Tty7App { 0.0 }; let status = match job.state { - SftpJobState::Running => format!( - "{} / {} ({pct:.0}%)", - human_size(job.bytes_done), - human_size(job.bytes_total) + SftpJobState::Running => t_fmt( + L10nKey::SftpTransferProgress, + &[ + ("done", &human_size(job.bytes_done)), + ("total", &human_size(job.bytes_total)), + ("pct", &format!("{pct:.0}")), + ], ), - SftpJobState::Done => "done".to_string(), - SftpJobState::Cancelled => "cancelled".to_string(), - SftpJobState::Error => job.error.clone().unwrap_or_else(|| "error".to_string()), + SftpJobState::Done => t(L10nKey::SftpTransferDone).to_string(), + SftpJobState::Cancelled => t(L10nKey::SftpTransferCancelled).to_string(), + SftpJobState::Error => job + .error + .clone() + .unwrap_or_else(|| t(L10nKey::SftpTransferError).to_string()), }; let status_color = match job.state { SftpJobState::Error => danger, diff --git a/src/ui/ssh_prompt.rs b/src/ui/ssh_prompt.rs index 58a12b7b..c1a5c685 100644 --- a/src/ui/ssh_prompt.rs +++ b/src/ui/ssh_prompt.rs @@ -579,7 +579,7 @@ impl Tty7App { .child(div().flex_1().text_sm().child(text.to_string())) .child( Button::new(("ssh-banner-dismiss", ix)) - .label("Dismiss") + .label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Dismiss)) .small() .ghost() .on_click(cx.listener(move |this, _, _w, cx| this.dismiss_ssh_banner(ix, cx))), @@ -590,23 +590,37 @@ impl Tty7App { fn render_ssh_sheet(&self, model: &PromptModel, cx: &mut Context) -> AnyElement { let danger = cx.theme().danger; let (title, danger_sheet) = match model { - PromptModel::Password { user, host, .. } => { - (format!("Password for {user}@{host}"), false) - } - PromptModel::KeyPassphrase { key_path, .. } => { - (format!("Passphrase for {key_path}"), false) - } + PromptModel::Password { user, host, .. } => ( + crate::ui::i18n::t_fmt( + crate::ui::i18n::L10nKey::SshPromptPasswordFor, + &[("user", user), ("host", host)], + ), + false, + ), + PromptModel::KeyPassphrase { key_path, .. } => ( + crate::ui::i18n::t_fmt( + crate::ui::i18n::L10nKey::SshPromptPassphraseFor, + &[("key_path", key_path)], + ), + false, + ), PromptModel::KeyboardInteractive { name, .. } => { let label = if name.is_empty() { - "Two-factor authentication".to_string() + crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshPromptTwoFactor).to_string() } else { name.clone() }; (label, false) } - PromptModel::HostKeyUnknown { host, .. } => (format!("Unknown host {host}"), false), + PromptModel::HostKeyUnknown { host, .. } => ( + crate::ui::i18n::t_fmt( + crate::ui::i18n::L10nKey::SshPromptUnknownHost, + &[("host", host)], + ), + false, + ), PromptModel::HostKeyChanged { .. } => ( - "Host key CHANGED — possible man-in-the-middle".to_string(), + crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshPromptHostKeyChanged).to_string(), true, ), }; @@ -645,16 +659,16 @@ impl Tty7App { PromptModel::Password { rejected, .. } => { let mut c = card; if *rejected { - c = c.child( - div() - .text_xs() - .text_color(danger) - .child("The stored password was rejected. Enter a new one."), - ); + c = c.child(div().text_xs().text_color(danger).child(crate::ui::i18n::t( + crate::ui::i18n::L10nKey::StoredPasswordRejected, + ))); } c.child(self.render_ssh_input(0)) .child(self.render_ssh_remember(cx)) - .child(self.render_ssh_actions("Connect", cx)) + .child(self.render_ssh_actions( + crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshPromptConnect), + cx, + )) } PromptModel::KeyPassphrase { comment, .. } => { let mut c = card; @@ -668,7 +682,10 @@ impl Tty7App { } c.child(self.render_ssh_input(0)) .child(self.render_ssh_remember(cx)) - .child(self.render_ssh_actions("Unlock", cx)) + .child(self.render_ssh_actions( + crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshPromptUnlock), + cx, + )) } PromptModel::KeyboardInteractive { instructions, @@ -683,7 +700,10 @@ impl Tty7App { c = c.child(div().text_xs().child(row.text.clone())); c = c.child(self.render_ssh_input(i)); } - c.child(self.render_ssh_actions("Submit", cx)) + c.child(self.render_ssh_actions( + crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshPromptSubmit), + cx, + )) } PromptModel::HostKeyUnknown { algorithm, @@ -703,16 +723,21 @@ impl Tty7App { .gap_2() .child( Button::new("ssh-hk-trust") - .label("Trust") + .label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Trust)) .small() .primary() .on_click(cx.listener(|this, _, window, cx| { this.trust_ssh_host_key(window, cx) })), ) - .child(Button::new("ssh-hk-abort").label("Abort").small().on_click( - cx.listener(|this, _, window, cx| this.cancel_ssh_prompt(window, cx)), - )), + .child( + Button::new("ssh-hk-abort") + .label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Abort)) + .small() + .on_click(cx.listener(|this, _, window, cx| { + this.cancel_ssh_prompt(window, cx) + })), + ), ), PromptModel::HostKeyChanged { algorithm, @@ -721,35 +746,39 @@ impl Tty7App { port, host, } => card - .child(div().text_xs().text_color(danger).child( - "The host key differs from the one previously trusted. This may be an attack.", - )) + .child(div().text_xs().text_color(danger).child(crate::ui::i18n::t( + crate::ui::i18n::L10nKey::SshPromptHostKeyChangedBody, + ))) .child(div().text_xs().child(format!("{host}:{port} {algorithm}"))) .child( div() .text_xs() .font_family("monospace") - .child(format!("new {fingerprint}")), + .child(crate::ui::i18n::t_fmt( + crate::ui::i18n::L10nKey::SshPromptNewKey, + &[("fingerprint", &fingerprint)], + )), ) .child( div() .text_xs() .font_family("monospace") .text_color(cx.theme().muted_foreground) - .child(format!("old {old_fingerprint}")), - ) - .child( - div() - .text_xs() - .child("Type \"yes\" to override and trust the new key, or Esc to abort."), + .child(crate::ui::i18n::t_fmt( + crate::ui::i18n::L10nKey::SshPromptOldKey, + &[("old_fingerprint", &old_fingerprint)], + )), ) + .child(div().text_xs().child(crate::ui::i18n::t( + crate::ui::i18n::L10nKey::HostKeyOverrideMessage, + ))) .child(self.render_ssh_input(0)) .child( h_flex() .gap_2() .child( Button::new("ssh-hkc-abort") - .label("Abort") + .label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Abort)) .small() .primary() .on_click(cx.listener(|this, _, window, cx| { @@ -758,7 +787,7 @@ impl Tty7App { ) .child( Button::new("ssh-hkc-override") - .label("Override") + .label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Override)) .small() .on_click(cx.listener(|this, _, window, cx| { this.submit_ssh_prompt(window, cx) @@ -781,7 +810,9 @@ impl Tty7App { h_flex() .child( Checkbox::new("ssh-remember") - .label("Remember (keychain)") + .label(crate::ui::i18n::t( + crate::ui::i18n::L10nKey::RememberKeychain, + )) .checked(self.ssh_prompt.remember) .on_click(cx.listener(|this, _, _w, cx| this.toggle_ssh_remember(cx))), ) @@ -802,9 +833,12 @@ impl Tty7App { ), ) .child( - Button::new("ssh-cancel").label("Cancel").small().on_click( - cx.listener(|this, _, window, cx| this.cancel_ssh_prompt(window, cx)), - ), + Button::new("ssh-cancel") + .label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Cancel)) + .small() + .on_click( + cx.listener(|this, _, window, cx| this.cancel_ssh_prompt(window, cx)), + ), ) .into_any_element() } diff --git a/src/ui/switcher.rs b/src/ui/switcher.rs index 9855f0bb..6efcfe29 100644 --- a/src/ui/switcher.rs +++ b/src/ui/switcher.rs @@ -16,6 +16,7 @@ use crate::core::session::WorkspaceStore; use crate::daemon::install::InstallPhase; use crate::terminal::pane_liveness::Liveness; use crate::ui::app::Tty7App; +use crate::ui::i18n::{L10nKey, t, t_fmt}; use crate::ui::remote_connect::{self, HostChoice, RemoteWorkspaceRow, human_bytes}; use crate::ui::remote_workspace::ConnectFlow; @@ -108,8 +109,11 @@ impl Tty7App { pub(crate) fn open_switcher(&mut self, window: &mut Window, cx: &mut Context) { remote_connect::register(cx); remote_connect::sweep_wsl(cx); - let query = - cx.new(|cx| InputState::new(window, cx).placeholder("Search workspaces and machines")); + let query = cx.new(|cx| { + InputState::new(window, cx).placeholder(crate::ui::i18n::t( + crate::ui::i18n::L10nKey::SearchWorkspacesAndMachines, + )) + }); query.update(cx, |state, cx| state.focus(window, cx)); let subs = vec![cx.subscribe_in( &query, @@ -152,7 +156,11 @@ impl Tty7App { let store = WorkspaceStore::all(app); for w in &store.views { let (key, label, target) = match w.host.as_ref() { - None => (String::new(), "This Computer".to_string(), None), + None => ( + String::new(), + t(L10nKey::SwitcherThisComputer).to_string(), + None, + ), Some(r) => { let key = r.target.to_string(); (key.clone(), key, Some(r.target.clone())) @@ -175,7 +183,7 @@ impl Tty7App { groups[slot].rows.push(Row { id: w.id, name: crate::ui::machine_mirror::display_name(app, w) - .unwrap_or_else(|| "Untitled".to_string()), + .unwrap_or_else(|| t(L10nKey::WindowUntitled).to_string()), path: crate::ui::machine_mirror::subject_path(app, w) .map(|p| crate::ui::home::display_path(std::path::Path::new(&p))) .unwrap_or_default(), @@ -213,7 +221,7 @@ impl Tty7App { 0, Group { key: String::new(), - label: "This Computer".to_string(), + label: t(L10nKey::SwitcherThisComputer).to_string(), endpoint: String::new(), target: None, link: Link::Offline, @@ -436,7 +444,7 @@ impl Tty7App { .py(px(14.)) .text_sm() .text_color(cx.theme().muted_foreground) - .child("No workspace or machine matches."), + .child(t(L10nKey::SwitcherNoMatch)), ); } @@ -537,7 +545,7 @@ impl Tty7App { GUTTER, Icon::new(IconName::Plus).size(px(ICON)).text_color(dim), )) - .child("Add SSH Host…") + .child(t(L10nKey::AddSshHost)) .on_click(cx.listener(|this, _, window, cx| { this.close_switcher(window, cx); this.open_settings_section( @@ -563,7 +571,7 @@ impl Tty7App { .border_color(border) .child("⌘"), ) - .child("click for a new window"), + .child(t(L10nKey::ClickForNewWindow)), ) } @@ -630,7 +638,7 @@ impl Tty7App { "switcher-retry:{}", group.key ))) - .label("Try Again") + .label(t(L10nKey::TryAgain)) .ghost() .xsmall() .on_click(cx.listener(move |this, _, _window, cx| { @@ -655,7 +663,7 @@ impl Tty7App { "switcher-replace:{}", group.key ))) - .label("Restart Server") + .label(t(L10nKey::RestartServer)) .ghost() .xsmall() .on_click(cx.listener(move |this, _, window, cx| { @@ -693,19 +701,20 @@ impl Tty7App { let accent = theme.warning; let fraction = phase.fraction().unwrap_or(0.0); let caption = match phase { - InstallPhase::Restarting => "Restarting tty7's server\u{2026}".to_string(), + InstallPhase::Restarting => t(L10nKey::SwitcherRestartingServer).to_string(), InstallPhase::Downloading { done, total } => match total { - Some(total) => format!( - "Downloading tty7's server\u{2026} {} / {}", - human_bytes(done), - human_bytes(total) + Some(total) => t_fmt( + L10nKey::SwitcherDownloadingServerWithTotal, + &[("done", &human_bytes(done)), ("total", &human_bytes(total))], + ), + None => t_fmt( + L10nKey::SwitcherDownloadingServerNoTotal, + &[("done", &human_bytes(done))], ), - None => format!("Downloading tty7's server\u{2026} {}", human_bytes(done)), }, - InstallPhase::Uploading { done, total } => format!( - "Copying tty7's server\u{2026} {} / {}", - human_bytes(done), - human_bytes(total) + InstallPhase::Uploading { done, total } => t_fmt( + L10nKey::SwitcherCopyingServer, + &[("done", &human_bytes(done)), ("total", &human_bytes(total))], ), }; @@ -784,17 +793,25 @@ impl Tty7App { let (dot, word): (Option, Option<&'static str>) = match group.link { Link::Local => (None, None), Link::Connected => (Some(gpui::rgb(crate::ui::tab_strip::LIVE_DOT).into()), None), - Link::Connecting if matches!(group.installing, Some(InstallPhase::Restarting)) => { - (Some(theme.warning), Some("restarting…")) - } - Link::Connecting if group.installing.is_some() => { - (Some(theme.warning), Some("installing…")) - } - Link::Connecting => (Some(theme.warning), Some("connecting…")), - Link::Failed => (Some(theme.danger), Some("couldn't connect")), + Link::Connecting if matches!(group.installing, Some(InstallPhase::Restarting)) => ( + Some(theme.warning), + Some(t(L10nKey::SwitcherStatusRestarting)), + ), + Link::Connecting if group.installing.is_some() => ( + Some(theme.warning), + Some(t(L10nKey::SwitcherStatusInstalling)), + ), + Link::Connecting => ( + Some(theme.warning), + Some(t(L10nKey::SwitcherStatusConnecting)), + ), + Link::Failed => ( + Some(theme.danger), + Some(t(L10nKey::SwitcherStatusConnectFailed)), + ), Link::Offline => ( Some(gpui::rgb(crate::ui::tab_strip::UNKNOWN_DOT).into()), - Some("not connected"), + Some(t(L10nKey::SwitcherStatusNotConnected)), ), }; let word_color = match group.link { @@ -927,9 +944,9 @@ impl Tty7App { let key = row.id.element_key() as usize; let badge = if row.current { - Some(("this window", true)) + Some((t(L10nKey::SwitcherThisWindow), true)) } else if row.open { - Some(("open", false)) + Some((t(L10nKey::SwitcherOpen), false)) } else { None }; @@ -1054,7 +1071,12 @@ impl Tty7App { GUTTER, Icon::new(IconName::Globe).size(px(ICON)).text_color(dim), )) - .child(div().text_sm().text_color(muted).child("Other Machines")) + .child( + div() + .text_sm() + .text_color(muted) + .child(t(L10nKey::OtherMachines)), + ) .child(div().flex_1()) .child( div() @@ -1202,7 +1224,7 @@ fn group_menu( let gref = group.clone(); let can_create = group.target.is_none() || group.home.is_some(); let menu = menu.item( - PopupMenuItem::new("New Workspace") + PopupMenuItem::new(t(L10nKey::AppMenuNewWorkspace)) .disabled(!can_create) .on_click(move |_, window, cx| { let _ = a1.update(cx, |this, cx| this.switcher_new(&gref, window, cx)); @@ -1215,7 +1237,7 @@ fn group_menu( let restartable = target.is_ssh(); let (label, for_restart) = (group.label.clone(), target.clone()); let menu = menu.separator().item( - PopupMenuItem::new("Disconnect") + PopupMenuItem::new(t(L10nKey::SwitcherDisconnect)) .disabled(!connected) .on_click(move |_, _window, cx| { let _ = a2.update(cx, |this, cx| this.switcher_disconnect(&target, cx)); @@ -1225,7 +1247,7 @@ fn group_menu( return menu; } menu.item( - PopupMenuItem::new("Restart Server…").on_click(move |_, window, cx| { + PopupMenuItem::new(t(L10nKey::AppMenuRestartServer)).on_click(move |_, window, cx| { let _ = a3.update(cx, |this, cx| { this.confirm_restart_remote_server(for_restart.clone(), label.clone(), window, cx); }); @@ -1242,14 +1264,14 @@ fn row_menu( let (id, adopt) = (row.id, row.adopt.is_some()); let stoppable = row.live; menu.item( - PopupMenuItem::new("Rename…") + PopupMenuItem::new(t(L10nKey::SwitcherRename)) .disabled(adopt) .on_click(move |_, window, cx| { let _ = a1.update(cx, |this, cx| this.switcher_rename(id, window, cx)); }), ) .item( - PopupMenuItem::new("Open in New Window") + PopupMenuItem::new(t(L10nKey::SwitcherOpenInNewWindow)) .disabled(adopt) .on_click(move |_, window, cx| { let _ = a2.update(cx, |this, cx| { @@ -1260,7 +1282,7 @@ fn row_menu( ) .separator() .item( - PopupMenuItem::new("Stop Workspace…") + PopupMenuItem::new(t(L10nKey::AppMenuStopWorkspace)) .disabled(adopt || !stoppable) .on_click(move |_, window, cx| { let _ = a3.update(cx, |this, cx| { @@ -1270,7 +1292,7 @@ fn row_menu( }), ) .item( - PopupMenuItem::new("Delete Workspace…") + PopupMenuItem::new(t(L10nKey::AppMenuDeleteWorkspace)) .disabled(adopt) .on_click(move |_, window, cx| { let _ = a4.update(cx, |this, cx| { diff --git a/src/ui/tab_sidebar.rs b/src/ui/tab_sidebar.rs index fb33b883..0a7bbe7f 100644 --- a/src/ui/tab_sidebar.rs +++ b/src/ui/tab_sidebar.rs @@ -16,6 +16,7 @@ use crate::core::config::{Config, SidebarGrouping}; use crate::terminal::git_status::GitStatusCache; use crate::ui::app::{TITLE_BAR_HEIGHT, Tty7App}; use crate::ui::hints::tab_badge_label; +use crate::ui::i18n::{L10nKey, t}; use crate::ui::reorder::{self, Reorder, Surface}; use crate::ui::tab_strip::{DragTab, REORDER_SLIDE_MS}; @@ -554,7 +555,7 @@ impl Tty7App { cx, ) .rounded_lg() - .tooltip("Hide Sidebar") + .tooltip(t(L10nKey::TabTooltipHideSidebar)) .on_click(cx.listener(|this, _, _window, cx| this.toggle_left_panel(cx))), ), ); @@ -784,7 +785,7 @@ fn sidebar_sections(keys: &[Option]) -> Vec
{ if !scratch.is_empty() { sections.push(Section { key: None, - name: Some("Scratch".into()), + name: Some(t(L10nKey::SidebarScratchGroup).to_string()), tabs: scratch, }); } diff --git a/src/ui/tab_strip.rs b/src/ui/tab_strip.rs index 11f18f93..ef7d15f9 100644 --- a/src/ui/tab_strip.rs +++ b/src/ui/tab_strip.rs @@ -20,6 +20,7 @@ use crate::core::shells::DetectedShell; use crate::daemon::protocol::ShellSpec; use crate::ui::app::{TILE_GLYPH, TILE_GLYPH_LINE, TILE_SIZE, Tab, Tty7App, tile_trailing_inset}; use crate::ui::hints::tab_badge_label; +use crate::ui::i18n::{L10nKey, t, t_fmt}; use crate::ui::reorder::{self, Reorder, Surface}; pub(crate) const REORDER_SLIDE_MS: u64 = 140; @@ -327,14 +328,14 @@ impl Tty7App { cx, ) .rounded_lg() - .tooltip("More") + .tooltip(t(L10nKey::TabTooltipMore)) .dropdown_menu_with_anchor( gpui::Anchor::TopRight, move |menu, _window, _cx| { menu.min_w(px(200.)) .action_context(action_ctx.clone()) - .menu("Command Palette…", Box::new(TogglePalette)) - .menu("Settings…", Box::new(OpenSettings)) + .menu(t(L10nKey::AppMenuCommandPalette), Box::new(TogglePalette)) + .menu(t(L10nKey::AppMenuSettings), Box::new(OpenSettings)) }, ), ) @@ -362,9 +363,9 @@ impl Tty7App { ) .rounded_lg() .tooltip(if panel_open { - "Hide Detail Panel" + t(L10nKey::TabTooltipHideDetailPanel) } else { - "Show Detail Panel" + t(L10nKey::TabTooltipShowDetailPanel) }) .on_click(cx.listener(|this, _, _window, cx| { this.toggle_right_panel(cx); @@ -387,26 +388,26 @@ impl Tty7App { ( RightPanelTab::Info, Icon::empty().path("icons/info.svg"), - "Info", + L10nKey::PanelInfoTitle, ), ( RightPanelTab::Outline, Icon::empty().path("icons/list.svg"), - "Outline", + L10nKey::PanelOutlineTitle, ), ( RightPanelTab::Changes, Icon::empty().path("icons/git-branch.svg"), - "Changes", + L10nKey::PanelChangesTitle, ), ( RightPanelTab::Files, Icon::new(IconName::FolderClosed), - "Files", + L10nKey::PanelFilesTitle, ), ] .into_iter() - .map(|(tab, icon, label)| { + .map(|(tab, icon, label_key)| { div() .occlude() .flex_shrink_0() @@ -419,9 +420,9 @@ impl Tty7App { .rounded_lg() .tooltip(match (tab, changed) { (RightPanelTab::Changes, Some(n)) => { - SharedString::from(format!("{label} · {n}")) + SharedString::from(format!("{} · {n}", t(label_key))) } - _ => SharedString::from(label), + _ => SharedString::from(t(label_key)), }) .on_click(cx.listener(move |this, _, _window, cx| { this.set_right_panel_tab(tab, cx); @@ -534,7 +535,10 @@ impl Tty7App { let raw = tab.leaf_title(window, cx); let label = short_title(&raw); if label.trim().is_empty() { - format!("Shell {}", index + 1) + t_fmt( + L10nKey::TabUnnamedShell, + &[("n", &((index + 1).to_string()))], + ) } else { label } @@ -565,7 +569,7 @@ impl Tty7App { .child( div() .text_color(cx.theme().muted_foreground) - .child("default"), + .child(t(L10nKey::ShellDefault)), ) }) } else { @@ -581,13 +585,13 @@ impl Tty7App { } if shells.is_empty() { let open_default = app.clone(); - menu = menu.item( - PopupMenuItem::new("New Tab").on_click(move |_, window, cx| { + menu = menu.item(PopupMenuItem::new(t(L10nKey::AppMenuNewTab)).on_click( + move |_, window, cx| { if let Some(app) = open_default.upgrade() { app.update(cx, |this, cx| this.new_tab(window, cx)); } - }), - ); + }, + )); } menu }) @@ -610,7 +614,7 @@ impl Tty7App { let has_cwd = cwd.is_some(); let mut menu = menu.min_w(px(200.)); - menu = menu.item(PopupMenuItem::new("Rename Tab").on_click({ + menu = menu.item(PopupMenuItem::new(t(L10nKey::AppMenuRenameTab)).on_click({ let app = app.clone(); move |_, window, cx| { let _ = app.update(cx, |this, cx| this.start_rename(index, window, cx)); @@ -622,7 +626,7 @@ impl Tty7App { let done = tab.and_then(|t| t.agent_status(cx)) == Some(crate::core::cli_agent::AgentStatus::Done); menu = menu.item( - PopupMenuItem::new("Mark as Unread") + PopupMenuItem::new(t(L10nKey::TabContextMarkUnread)) .disabled(!done) .on_click({ let app = app.clone(); @@ -635,14 +639,14 @@ impl Tty7App { let in_repo = this.tab_is_in_repo(index, window, cx); if in_repo { - menu = menu - .separator() - .item(PopupMenuItem::new("New Worktree Tab").on_click({ + menu = menu.separator().item( + PopupMenuItem::new(t(L10nKey::AppMenuNewWorktreeTab)).on_click({ let app = app.clone(); move |_, window, cx| { let _ = app.update(cx, |this, cx| this.new_worktree_tab(index, window, cx)); } - })); + }), + ); } let agent_session = this.tab_agent_session(index, window, cx); @@ -673,7 +677,7 @@ impl Tty7App { menu = menu .separator() - .item(PopupMenuItem::new("Split Right").on_click({ + .item(PopupMenuItem::new(t(L10nKey::AppMenuSplitRight)).on_click({ let app = app.clone(); move |_, window, cx| { let _ = app.update(cx, |this, cx| { @@ -682,7 +686,7 @@ impl Tty7App { }); } })) - .item(PopupMenuItem::new("Split Down").on_click({ + .item(PopupMenuItem::new(t(L10nKey::AppMenuSplitDown)).on_click({ let app = app.clone(); move |_, window, cx| { let _ = app.update(cx, |this, cx| { @@ -693,7 +697,7 @@ impl Tty7App { })); menu = menu.separator().item( - PopupMenuItem::new("Copy Working Directory") + PopupMenuItem::new(t(L10nKey::AppMenuCopyWorkingDirectory)) .disabled(!has_cwd) .on_click(move |_, _window, cx| { if let Some(cwd) = cwd.as_ref() { @@ -706,7 +710,7 @@ impl Tty7App { if let Some(session_id) = agent_session.map(|(_, s)| s.session_id) { menu = menu.item( - PopupMenuItem::new("Copy Session ID") + PopupMenuItem::new(t(L10nKey::AppMenuCopySessionId)) .disabled(session_id.is_none()) .on_click(move |_, _window, cx| { if let Some(id) = session_id.as_ref() { @@ -717,14 +721,16 @@ impl Tty7App { } menu.separator() - .item(PopupMenuItem::new("Close Tab").on_click({ - let app = app.clone(); - move |_, window, cx| { - let _ = app.update(cx, |this, cx| this.close_tab(index, window, cx)); - } - })) .item( - PopupMenuItem::new("Close Other Tabs") + PopupMenuItem::new(t(L10nKey::TabContextCloseTab)).on_click({ + let app = app.clone(); + move |_, window, cx| { + let _ = app.update(cx, |this, cx| this.close_tab(index, window, cx)); + } + }), + ) + .item( + PopupMenuItem::new(t(L10nKey::AppMenuCloseOtherTabs)) .disabled(tab_count <= 1) .on_click({ let app = app.clone(); @@ -736,9 +742,9 @@ impl Tty7App { ) .item( PopupMenuItem::new(if below_wording { - "Close Tabs Below" + t(L10nKey::TabContextCloseTabsBelow) } else { - "Close Tabs to the Right" + t(L10nKey::AppMenuCloseTabsRight) }) .disabled(index + 1 >= tab_count) .on_click({ @@ -1054,7 +1060,7 @@ impl Tty7App { cx, ) .rounded_lg() - .tooltip("Show Sidebar") + .tooltip(t(L10nKey::TabTooltipShowSidebar)) .on_click(cx.listener(|this, _, _window, cx| this.toggle_left_panel(cx))), ), ) diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 33d51eb8..fe2a242a 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -11,6 +11,7 @@ use crate::terminal::view::{ ClearScrollback, CopyText, CutText, FindInTerminal, FindNext, FindPrevious, PasteText, RedoEdit, SelectAll, UndoEdit, }; +use crate::ui::i18n::{L10nKey, t}; use crate::ui::presets; use crate::ui::presets::Fill; @@ -21,84 +22,87 @@ pub(crate) fn traffic_light_position() -> Point { pub(crate) fn set_menus(cx: &mut App) { cx.set_menus([ Menu::new("tty7").items([ - MenuItem::action("About tty7", About), - MenuItem::action("Check for Updates…", CheckForUpdates), + MenuItem::action(t(L10nKey::AppMenuAbout), About), + MenuItem::action(t(L10nKey::AppMenuCheckForUpdates), CheckForUpdates), MenuItem::separator(), - MenuItem::action("Settings…", OpenSettings), + MenuItem::action(t(L10nKey::AppMenuSettings), OpenSettings), MenuItem::separator(), - MenuItem::os_submenu("Services", SystemMenuType::Services), + MenuItem::os_submenu(t(L10nKey::AppMenuServices), SystemMenuType::Services), MenuItem::separator(), - MenuItem::action("Hide tty7", HideApp), - MenuItem::action("Hide Others", HideOthers), - MenuItem::action("Show All", ShowAll), + MenuItem::action(t(L10nKey::AppMenuHideApp), HideApp), + MenuItem::action(t(L10nKey::AppMenuHideOthers), HideOthers), + MenuItem::action(t(L10nKey::AppMenuShowAll), ShowAll), MenuItem::separator(), - MenuItem::action("Quit tty7", Quit), + MenuItem::action(t(L10nKey::AppMenuQuit), Quit), ]), - Menu::new("File").items([ - MenuItem::action("New Tab", NewTab), - MenuItem::action("New Workspace", NewWorkspace), - MenuItem::action("New Worktree Tab", NewWorktreeTab), + Menu::new(t(L10nKey::AppMenuFile)).items([ + MenuItem::action(t(L10nKey::AppMenuNewTab), NewTab), + MenuItem::action(t(L10nKey::AppMenuNewWorkspace), NewWorkspace), + MenuItem::action(t(L10nKey::AppMenuNewWorktreeTab), NewWorktreeTab), MenuItem::separator(), - MenuItem::action("Split Right", SplitRight), - MenuItem::action("Split Down", SplitDown), + MenuItem::action(t(L10nKey::AppMenuSplitRight), SplitRight), + MenuItem::action(t(L10nKey::AppMenuSplitDown), SplitDown), MenuItem::separator(), - MenuItem::action("Rename Tab…", RenameTab), - MenuItem::action("Copy Working Directory", CopyWorkingDirectory), - MenuItem::action("Copy Session ID", CopyAgentSessionId), - MenuItem::action("Fork Session", ForkAgentSession), + MenuItem::action(t(L10nKey::AppMenuRenameTab), RenameTab), + MenuItem::action( + t(L10nKey::AppMenuCopyWorkingDirectory), + CopyWorkingDirectory, + ), + MenuItem::action(t(L10nKey::AppMenuCopySessionId), CopyAgentSessionId), + MenuItem::action(t(L10nKey::AppMenuForkSession), ForkAgentSession), MenuItem::separator(), - MenuItem::action("Close Pane / Tab", CloseActiveTab), - MenuItem::action("Close Other Tabs", CloseOtherTabs), - MenuItem::action("Close Tabs to the Right", CloseTabsToTheRight), - MenuItem::action("Reopen Closed Tab", ReopenClosedTab), + MenuItem::action(t(L10nKey::AppMenuClosePaneTab), CloseActiveTab), + MenuItem::action(t(L10nKey::AppMenuCloseOtherTabs), CloseOtherTabs), + MenuItem::action(t(L10nKey::AppMenuCloseTabsRight), CloseTabsToTheRight), + MenuItem::action(t(L10nKey::AppMenuReopenClosedTab), ReopenClosedTab), MenuItem::separator(), - MenuItem::action("Rename Workspace…", RenameWorkspace), - MenuItem::action("Stop Workspace…", StopWorkspace), + MenuItem::action(t(L10nKey::AppMenuRenameWorkspace), RenameWorkspace), + MenuItem::action(t(L10nKey::AppMenuStopWorkspace), StopWorkspace), MenuItem::separator(), - MenuItem::action("Delete Workspace…", DeleteWorkspace), + MenuItem::action(t(L10nKey::AppMenuDeleteWorkspace), DeleteWorkspace), ]), - Menu::new("Edit").items([ - MenuItem::os_action("Undo", UndoEdit, OsAction::Undo), - MenuItem::os_action("Redo", RedoEdit, OsAction::Redo), + Menu::new(t(L10nKey::AppMenuEdit)).items([ + MenuItem::os_action(t(L10nKey::AppMenuUndo), UndoEdit, OsAction::Undo), + MenuItem::os_action(t(L10nKey::AppMenuRedo), RedoEdit, OsAction::Redo), MenuItem::separator(), - MenuItem::os_action("Cut", CutText, OsAction::Cut), - MenuItem::os_action("Copy", CopyText, OsAction::Copy), - MenuItem::os_action("Paste", PasteText, OsAction::Paste), - MenuItem::os_action("Select All", SelectAll, OsAction::SelectAll), + MenuItem::os_action(t(L10nKey::AppMenuCut), CutText, OsAction::Cut), + MenuItem::os_action(t(L10nKey::AppMenuCopy), CopyText, OsAction::Copy), + MenuItem::os_action(t(L10nKey::AppMenuPaste), PasteText, OsAction::Paste), + MenuItem::os_action(t(L10nKey::AppMenuSelectAll), SelectAll, OsAction::SelectAll), MenuItem::separator(), - MenuItem::action("Find…", FindInTerminal), - MenuItem::action("Find Next", FindNext), - MenuItem::action("Find Previous", FindPrevious), + MenuItem::action(t(L10nKey::AppMenuFind), FindInTerminal), + MenuItem::action(t(L10nKey::AppMenuFindNext), FindNext), + MenuItem::action(t(L10nKey::AppMenuFindPrevious), FindPrevious), ]), - Menu::new("View").items([ - MenuItem::action("Command Palette…", TogglePalette), + Menu::new(t(L10nKey::AppMenuView)).items([ + MenuItem::action(t(L10nKey::AppMenuCommandPalette), TogglePalette), MenuItem::separator(), - MenuItem::action("Increase Font Size", IncreaseFontSize), - MenuItem::action("Decrease Font Size", DecreaseFontSize), - MenuItem::action("Reset Font Size", ResetFontSize), + MenuItem::action(t(L10nKey::AppMenuIncreaseFontSize), IncreaseFontSize), + MenuItem::action(t(L10nKey::AppMenuDecreaseFontSize), DecreaseFontSize), + MenuItem::action(t(L10nKey::AppMenuResetFontSize), ResetFontSize), MenuItem::separator(), - MenuItem::action("Left Sidebar", ToggleLeftPanel), - MenuItem::action("Right Panel", ToggleRightPanel), - MenuItem::action("Code Panel", ToggleCodePanel), - MenuItem::action("Tab Bar Position", ToggleTabSidebar), + MenuItem::action(t(L10nKey::AppMenuLeftSidebar), ToggleLeftPanel), + MenuItem::action(t(L10nKey::AppMenuRightPanel), ToggleRightPanel), + MenuItem::action(t(L10nKey::AppMenuCodePanel), ToggleCodePanel), + MenuItem::action(t(L10nKey::AppMenuTabBarPosition), ToggleTabSidebar), MenuItem::separator(), - MenuItem::action("Focus Next Pane", FocusNextPane), - MenuItem::action("Focus Previous Pane", FocusPrevPane), - MenuItem::action("Zoom Pane", ToggleMaximizePane), + MenuItem::action(t(L10nKey::AppMenuFocusNextPane), FocusNextPane), + MenuItem::action(t(L10nKey::AppMenuFocusPreviousPane), FocusPrevPane), + MenuItem::action(t(L10nKey::AppMenuZoomPane), ToggleMaximizePane), MenuItem::separator(), - MenuItem::action("Clear Scrollback", ClearScrollback), + MenuItem::action(t(L10nKey::AppMenuClearScrollback), ClearScrollback), MenuItem::separator(), - MenuItem::action("Enter Full Screen", ToggleFullscreen), + MenuItem::action(t(L10nKey::AppMenuEnterFullscreen), ToggleFullscreen), ]), - Menu::new("Window").items(window_menu_items(cx)), - Menu::new("Help").items([ - MenuItem::action("tty7 Documentation", OpenDocumentation), - MenuItem::action("Keyboard Shortcuts", ShowKeyboardShortcuts), + Menu::new(t(L10nKey::AppMenuWindow)).items(window_menu_items(cx)), + Menu::new(t(L10nKey::AppMenuHelp)).items([ + MenuItem::action(t(L10nKey::AppMenuDocumentation), OpenDocumentation), + MenuItem::action(t(L10nKey::AppMenuKeyboardShortcuts), ShowKeyboardShortcuts), MenuItem::separator(), - MenuItem::action("Join the Discord", OpenDiscord), - MenuItem::action("Report an Issue…", ReportIssue), + MenuItem::action(t(L10nKey::AppMenuJoinDiscord), OpenDiscord), + MenuItem::action(t(L10nKey::AppMenuReportIssue), ReportIssue), MenuItem::separator(), - MenuItem::action("Restart Server…", RestartDaemon), + MenuItem::action(t(L10nKey::AppMenuRestartServer), RestartDaemon), ]), ]); } @@ -114,8 +118,8 @@ fn window_menu_items(cx: &App) -> Vec { let slot_action = crate::ui::tab_strip::select_workspace_action; let mut items = vec![ - MenuItem::action("Minimize", MinimizeWindow), - MenuItem::action("Zoom", ZoomWindow), + MenuItem::action(t(L10nKey::AppMenuMinimize), MinimizeWindow), + MenuItem::action(t(L10nKey::AppMenuZoom), ZoomWindow), MenuItem::separator(), ]; let workspace_start = items.len(); @@ -132,7 +136,7 @@ fn window_menu_items(cx: &App) -> Vec { } } let name = crate::ui::machine_mirror::display_name(cx, workspace) - .unwrap_or_else(|| "Untitled".to_string()); + .unwrap_or_else(|| t(L10nKey::WindowUntitled).to_string()); let label = if *open { name } else { @@ -151,7 +155,10 @@ fn window_menu_items(cx: &App) -> Vec { }); } if items.len() == workspace_start { - items.push(MenuItem::action("New Workspace", NewWorkspace)); + items.push(MenuItem::action( + t(L10nKey::AppMenuNewWorkspace), + NewWorkspace, + )); } items } diff --git a/src/ui/tray/mod.rs b/src/ui/tray/mod.rs index 0bbb40a6..cce56cfd 100644 --- a/src/ui/tray/mod.rs +++ b/src/ui/tray/mod.rs @@ -11,6 +11,7 @@ use sni::Backend; use crate::core::cli_agent::AgentStatus; use crate::core::config::{Config, NotifyMode}; +use crate::ui::i18n::{L10nKey, t}; use gpui::App; const POLL: std::time::Duration = std::time::Duration::from_millis(1000); @@ -59,9 +60,18 @@ impl TraySnapshot { let count = |s: AgentStatus| self.agents.iter().filter(|a| a.status == s).count(); let mut parts = Vec::new(); for (n, word) in [ - (count(AgentStatus::Waiting), "waiting"), - (count(AgentStatus::Working), "working"), - (count(AgentStatus::Done), "done"), + ( + count(AgentStatus::Waiting), + t(crate::ui::i18n::L10nKey::PanelAgentWaiting), + ), + ( + count(AgentStatus::Working), + t(crate::ui::i18n::L10nKey::PanelAgentWorking), + ), + ( + count(AgentStatus::Done), + t(crate::ui::i18n::L10nKey::PanelAgentDone), + ), ] { if n > 0 { parts.push(format!("{n} {word}")); @@ -90,19 +100,23 @@ pub(crate) enum SpecItem { } pub(crate) fn menu_spec(snap: &TraySnapshot) -> Vec { - let item = |id: &str, label: String| SpecItem::Item { + let item = |id: &str, label: &str| SpecItem::Item { id: id.to_string(), - label, + label: label.to_string(), checked: None, avatar: None, }; - let mut items = vec![item("show", "Show tty7".into()), SpecItem::Separator]; + let mut items = vec![item("show", t(L10nKey::TrayShowTty7)), SpecItem::Separator]; for a in &snap.agents { let state = match a.status { - AgentStatus::Waiting => " — needs input", - AgentStatus::Working => " — working", - AgentStatus::Done => " — done", - AgentStatus::Idle => "", + AgentStatus::Waiting => { + format!(" — {}", t(crate::ui::i18n::L10nKey::TrayAgentNeedsInput)) + } + AgentStatus::Working => { + format!(" — {}", t(crate::ui::i18n::L10nKey::PanelAgentWorking)) + } + AgentStatus::Done => format!(" — {}", t(crate::ui::i18n::L10nKey::PanelAgentDone)), + AgentStatus::Idle => String::new(), }; items.push(SpecItem::Item { id: format!("agent:{}", a.leaf_id), @@ -121,18 +135,33 @@ pub(crate) fn menu_spec(snap: &TraySnapshot) -> Vec { avatar: None, }; items.push(SpecItem::Submenu { - label: "Notifications".into(), + label: t(L10nKey::TrayNotifications).to_string(), items: vec![ - notify("notify:never", "Never", NotifyMode::Never), - notify("notify:unfocused", "When Unfocused", NotifyMode::Unfocused), - notify("notify:always", "Always", NotifyMode::Always), + notify( + "notify:never", + t(L10nKey::NotifyModeNever), + NotifyMode::Never, + ), + notify( + "notify:unfocused", + t(L10nKey::NotifyModeUnfocused), + NotifyMode::Unfocused, + ), + notify( + "notify:always", + t(L10nKey::NotifyModeAlways), + NotifyMode::Always, + ), ], }); - items.push(item("settings", "Settings…".into())); - items.push(item("updates", "Check for Updates…".into())); + items.push(item("settings", t(L10nKey::AppMenuSettings))); + items.push(item("updates", t(L10nKey::AppMenuCheckForUpdates))); items.push(SpecItem::Separator); - items.push(item("quit", "Quit tty7".into())); - items.push(item("quit-stop", "Quit and Stop Server…".into())); + items.push(item("quit", t(L10nKey::AppMenuQuit))); + items.push(item( + "quit-stop", + crate::ui::i18n::t(crate::ui::i18n::L10nKey::TrayQuitStopServer), + )); items } @@ -199,18 +228,23 @@ mod tests { #[test] fn attention_follows_waiting_and_tooltip_counts() { + crate::ui::i18n::set_locale("en"); assert!(snapshot_with_agent(AgentStatus::Waiting).attention()); assert!(!snapshot_with_agent(AgentStatus::Working).attention()); assert!(!snapshot_with_agent(AgentStatus::Done).attention()); assert_eq!( snapshot_with_agent(AgentStatus::Waiting).tooltip(), - "tty7 — 1 waiting" + format!( + "tty7 — 1 {}", + t(crate::ui::i18n::L10nKey::PanelAgentWaiting) + ) ); assert_eq!(TraySnapshot::default().tooltip(), "tty7"); } #[test] fn menu_spec_shape() { + crate::ui::i18n::set_locale("en"); let empty = menu_spec(&TraySnapshot::default()); let labels: Vec<_> = empty .iter() @@ -223,12 +257,12 @@ mod tests { assert_eq!( labels, [ - "Show tty7", - "Notifications", - "Settings…", - "Check for Updates…", - "Quit tty7", - "Quit and Stop Server…" + t(L10nKey::TrayShowTty7), + t(L10nKey::TrayNotifications), + t(L10nKey::AppMenuSettings), + t(L10nKey::AppMenuCheckForUpdates), + t(L10nKey::AppMenuQuit), + t(L10nKey::TrayQuitStopServer), ] ); assert!( diff --git a/src/ui/windows.rs b/src/ui/windows.rs index 799446e9..f0f6a1f9 100644 --- a/src/ui/windows.rs +++ b/src/ui/windows.rs @@ -8,6 +8,7 @@ use crate::core::config::{Config, StartupMode}; use crate::core::session::{WorkspaceId, WorkspaceStore}; use crate::core::window_state::{WindowGeometry as _, WindowState}; use crate::ui::app::Tty7App; +use crate::ui::i18n::{L10nKey, t, t_fmt, t_plural}; const CASCADE_STEP: f32 = 28.0; @@ -105,6 +106,23 @@ impl WindowRegistry { .map(|w| w.app.clone()) } + pub fn refresh_locale(cx: &mut App, except: Option) { + Self::sweep(cx); + let windows: Vec<_> = cx + .global::() + .windows + .iter() + .filter(|entry| Some(entry.workspace) != except) + .map(|entry| (entry.handle, entry.app.clone())) + .collect(); + for (handle, app) in windows { + let _ = handle.update(cx, |_, window, cx| { + let _ = app.update(cx, |app, cx| app.refresh_locale_state(window, cx)); + window.refresh(); + }); + } + } + fn register( cx: &mut App, workspace: WorkspaceId, @@ -303,22 +321,11 @@ pub fn confirm_and_delete(cx: &mut App, window: &mut Window, workspace: Workspac fn destructive_detail(live: Option, verb: &str) -> String { match (live, verb) { - (None, "Delete") => "Its machine could not be reached. Any shells still running there \ - will be ended, and the layout forgotten." - .to_string(), - (None, _) => { - "Its machine could not be reached. Any shells still running there will be ended." - .to_string() - } - (Some(0), _) => "Its layout and working directories will be forgotten.".to_string(), - (Some(1), "Delete") => { - "1 running shell will be ended and its layout forgotten.".to_string() - } - (Some(n), "Delete") => { - format!("{n} running shells will be ended and the layout forgotten.") - } - (Some(1), _) => "1 running shell will be ended.".to_string(), - (Some(n), _) => format!("{n} running shells will be ended."), + (None, "Delete") => t(L10nKey::WindowDeleteUnreachable).to_string(), + (None, _) => t(L10nKey::WindowStopUnreachable).to_string(), + (Some(0), _) => t_plural(L10nKey::WindowStopShells, 0, &[]), + (Some(n), "Delete") => t_plural(L10nKey::WindowDeleteShells, n, &[]), + (Some(n), _) => t_plural(L10nKey::WindowStopShells, n, &[]), } } @@ -330,7 +337,7 @@ fn confirm_destructive( act: fn(&mut App, WorkspaceId), ) { let name = crate::ui::machine_mirror::display_name_for(cx, workspace) - .unwrap_or_else(|| "this workspace".to_string()); + .unwrap_or_else(|| t(L10nKey::WindowThisWorkspace).to_string()); let query = pane_count_query(cx, workspace); let handle = window.window_handle(); @@ -349,12 +356,22 @@ fn confirm_destructive( } let detail = destructive_detail(live, verb); + let verb_key = if verb == "Delete" { + L10nKey::WindowDelete + } else { + L10nKey::WindowStop + }; + let verb_label = t(verb_key); + let title = t_fmt( + L10nKey::WindowConfirmTitle, + &[("verb", verb_label), ("name", &name)], + ); let Ok(answer) = handle.update(cx, |_, window, cx| { window.prompt( gpui::PromptLevel::Warning, - &format!("{verb} Workspace \u{201c}{name}\u{201d}?"), + &title, Some(&detail), - &["Cancel", verb], + &[t(L10nKey::Cancel), verb_label], cx, ) }) else { @@ -537,6 +554,7 @@ fn cascade(bounds: Bounds, existing: usize) -> Bounds Bounds { Bounds { @@ -614,25 +632,26 @@ mod tests { #[test] fn the_confirmation_says_which_of_the_three_answers_it_got() { + set_locale("en"); assert_eq!( destructive_detail(Some(1), "Stop"), - "1 running shell will be ended." + t_plural(L10nKey::WindowStopShells, 1, &[]) ); assert_eq!( destructive_detail(Some(3), "Stop"), - "3 running shells will be ended." + t_plural(L10nKey::WindowStopShells, 3, &[]) ); assert_eq!( destructive_detail(Some(1), "Delete"), - "1 running shell will be ended and its layout forgotten." + t_plural(L10nKey::WindowDeleteShells, 1, &[]) ); assert_eq!( destructive_detail(Some(3), "Delete"), - "3 running shells will be ended and the layout forgotten." + t_plural(L10nKey::WindowDeleteShells, 3, &[]) ); assert_eq!( destructive_detail(Some(0), "Delete"), - "Its layout and working directories will be forgotten." + t_plural(L10nKey::WindowStopShells, 0, &[]) ); for verb in ["Stop", "Delete"] { diff --git a/src/ui/worktree_prompt.rs b/src/ui/worktree_prompt.rs index 1171364a..0db27ae6 100644 --- a/src/ui/worktree_prompt.rs +++ b/src/ui/worktree_prompt.rs @@ -7,6 +7,7 @@ use gpui_component::{ use crate::core::worktree::{WorktreeDefaults, WorktreeRequest}; use crate::ui::app::Tty7App; +use crate::ui::i18n::{L10nKey, t, t_fmt}; pub(crate) struct WorktreePrompt { host: crate::ui::host_ops::SharedHost, @@ -78,7 +79,7 @@ impl Tty7App { let base = p.base.read(cx).value().trim().to_string(); let (name, branch) = match (name.is_empty(), branch.is_empty()) { (true, true) => { - window.push_notification("The worktree needs a name", cx); + window.push_notification(t(L10nKey::WorktreePromptNeedsName), cx); return; } (true, false) => (branch.clone(), branch), @@ -113,7 +114,10 @@ impl Tty7App { if let Some(p) = this.worktree_prompt.as_mut() { p.busy = false; } - window.push_notification(format!("New worktree failed: {e}"), cx); + window.push_notification( + t_fmt(L10nKey::AppNewWorktreeFailed, &[("error", &e.to_string())]), + cx, + ); cx.notify(); } }, @@ -162,9 +166,9 @@ impl Tty7App { div() .text_sm() .font_weight(gpui::FontWeight::SEMIBOLD) - .child("New Worktree Tab"), + .child(t(L10nKey::WorktreePromptTitle)), ) - .child(field("Worktree Name", &p.name)) + .child(field(t(L10nKey::WorktreePromptName), &p.name)) .child( div() .text_xs() @@ -172,14 +176,18 @@ impl Tty7App { .text_color(muted) .child(preview), ) - .child(field("New Branch", &p.branch)) - .child(field("Start From", &p.base)) + .child(field(t(L10nKey::WorktreePromptBranch), &p.branch)) + .child(field(t(L10nKey::WorktreePromptBase), &p.base)) .child( h_flex() .gap_2() .child( Button::new("worktree-create") - .label(if p.busy { "Creating…" } else { "Create" }) + .label(if p.busy { + t(L10nKey::WorktreePromptCreating) + } else { + t(L10nKey::WorktreePromptCreate) + }) .small() .primary() .disabled(p.busy) @@ -189,7 +197,7 @@ impl Tty7App { ) .child( Button::new("worktree-cancel") - .label("Cancel") + .label(t(L10nKey::Cancel)) .small() .on_click(cx.listener(|this, _, window, cx| { this.cancel_worktree_prompt(window, cx)