From 2276eef19186cd7ed2b42e4b440000fc3b1f20c8 Mon Sep 17 00:00:00 2001 From: TomZz Date: Tue, 9 Jun 2026 19:36:18 +0800 Subject: [PATCH] feat: complete internationalization and ui adjustments - Add full i18n support for UI components including settings and sidebar - Add connection progress tracking strings (Connecting, Failed, etc.) - Fix 'No session' display issue when no SSH/Local tab is active - Align SFTP 'show hidden files' checkbox text size with adjacent buttons - Add border to language settings dropdown for better visibility --- Cargo.lock | 94 ++++++++++++--- Cargo.toml | 2 + locales/en.yml | 60 +++++++++ locales/zh-CN.yml | 60 +++++++++ src/config.rs | 18 +++ src/main.rs | 301 +++++++++++++++++++++++++++++++++------------- 6 files changed, 433 insertions(+), 102 deletions(-) create mode 100644 locales/en.yml create mode 100644 locales/zh-CN.yml diff --git a/Cargo.lock b/Cargo.lock index 6313638..f9a3408 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "accesskit" @@ -302,9 +302,11 @@ dependencies = [ "russh", "russh-keys", "russh-sftp", + "rust-i18n 3.1.5", "serde", "serde_json", "ssh-key", + "sys-locale", "sysinfo 0.33.1", "tar", "thiserror 1.0.69", @@ -753,7 +755,7 @@ dependencies = [ "bitflags 2.12.1", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -1682,7 +1684,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1942,7 +1944,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2563,7 +2565,7 @@ dependencies = [ "log", "presser", "thiserror 2.0.18", - "windows 0.58.0", + "windows 0.62.2", ] [[package]] @@ -2697,7 +2699,7 @@ dependencies = [ "paste", "regex", "ropey", - "rust-i18n", + "rust-i18n 4.1.0", "schemars", "serde", "serde_json", @@ -3258,7 +3260,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.58.0", + "windows-core 0.62.2", ] [[package]] @@ -4122,7 +4124,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4308,7 +4310,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5984,6 +5986,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust-i18n" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332" +dependencies = [ + "globwalk", + "once_cell", + "regex", + "rust-i18n-macro 3.1.5", + "rust-i18n-support 3.1.5", + "smallvec", +] + [[package]] name = "rust-i18n" version = "4.1.0" @@ -5992,11 +6008,28 @@ checksum = "55691a65892c33ee2de49c15ea5600c6f4a70e8eeb8e6c3cd96d2a231d230c40" dependencies = [ "globwalk", "regex", - "rust-i18n-macro", - "rust-i18n-support", + "rust-i18n-macro 4.1.0", + "rust-i18n-support 4.1.0", "smallvec", ] +[[package]] +name = "rust-i18n-macro" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965" +dependencies = [ + "glob", + "once_cell", + "proc-macro2", + "quote", + "rust-i18n-support 3.1.5", + "serde", + "serde_json", + "serde_yaml", + "syn", +] + [[package]] name = "rust-i18n-macro" version = "4.1.0" @@ -6006,13 +6039,36 @@ dependencies = [ "glob", "proc-macro2", "quote", - "rust-i18n-support", + "rust-i18n-support 4.1.0", "serde", "serde_json", "serde_yaml", "syn", ] +[[package]] +name = "rust-i18n-support" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19" +dependencies = [ + "arc-swap", + "base62", + "globwalk", + "itertools 0.11.0", + "lazy_static", + "normpath", + "once_cell", + "proc-macro2", + "regex", + "serde", + "serde_json", + "serde_yaml", + "siphasher", + "toml 0.8.23", + "triomphe", +] + [[package]] name = "rust-i18n-support" version = "4.1.0" @@ -6069,7 +6125,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6082,7 +6138,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6737,7 +6793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6845,7 +6901,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7205,7 +7261,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7682,7 +7738,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset 0.9.1", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8442,7 +8498,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8c4ec06..a5e82d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ rfd = "0.15" russh = "0.49" russh-keys = "0.49" russh-sftp = "2" +rust-i18n = "3" +sys-locale = "0.3" serde = { version = "1", features = ["derive"] } serde_json = "1" ssh-key = "0.6" diff --git a/locales/en.yml b/locales/en.yml new file mode 100644 index 0000000..828b43d --- /dev/null +++ b/locales/en.yml @@ -0,0 +1,60 @@ +remote_files: "REMOTE FILES" +open_ssh_tab_sftp: "Open an SSH tab to browse SFTP files." +sftp_connecting: "sftp connecting..." +refresh: "Refresh" +upload_file: "Upload File" +upload_folder: "Upload Folder" +hidden: "Hidden" +download: "Download" +download_count: "Download (%{count})" +name: "NAME" +size: "SIZE" +modified: "MODIFIED" +new_ssh_connection: "New SSH Connection" +create_or_edit_ssh_session: "Create or edit an SSH session" +local_terminal: "Local Terminal" +open_local_shell_tab: "Open a new local shell tab" +connect: "Connect" +edit: "Edit" +delete: "Delete" +host: "Host" +port: "Port" +user: "User" +password: "Password" +key: "Key" +private_key_path: "Private Key Path" +private_key_data: "Private Key Data" +save_and_connect: "Save & Connect" +save: "Save" +cancel: "Cancel" +host_and_user_required: "Host and user are required" +language: "Language" +follow_system: "Follow System" +light_theme: "Light Theme" +dark_theme: "Dark Theme" +use_light_mode: "Use Light Mode" +use_dark_mode: "Use Dark Mode" +english: "English" +chinese: "简体中文" +idle_no_session: "idle / No session" +system: "SYSTEM" +cpu: "CPU" +mem: "MEM" +swap: "SWAP" +net: "NET" +disk: "DISK" +global_load: "global load" +live: "Live" +idle: "idle" +saved: "SAVED" +add_ssh: "+ ssh" +open_local_or_ssh: "Open a local terminal or pick an SSH session." +open_session: "Open Session" +settings: "Settings" +terminal_font_size: "Terminal Font Size" +theme_management_hint: "Theme mode and light/dark theme families are managed from the top-bar theme button." +local_host: "local host" +local_terminal_opened: "local terminal opened" +connecting: "Connecting" +starting_connection: "starting connection..." +connection_failed: "Connection Failed" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml new file mode 100644 index 0000000..7835831 --- /dev/null +++ b/locales/zh-CN.yml @@ -0,0 +1,60 @@ +remote_files: "远程文件" +open_ssh_tab_sftp: "打开一个 SSH 会话面板以浏览 SFTP 文件。" +sftp_connecting: "正在连接 SFTP..." +refresh: "刷新" +upload_file: "上传文件" +upload_folder: "上传文件夹" +hidden: "显示隐藏文件" +download: "下载" +download_count: "下载 (%{count})" +name: "名称" +size: "大小" +modified: "修改时间" +new_ssh_connection: "新建 SSH 连接" +create_or_edit_ssh_session: "创建或编辑一个 SSH 会话" +local_terminal: "本地终端" +open_local_shell_tab: "打开一个新的本地终端页面" +connect: "连接" +edit: "编辑" +delete: "删除" +host: "主机 (Host)" +port: "端口 (Port)" +user: "用户 (User)" +password: "密码 (Password)" +key: "密钥 (Key)" +private_key_path: "私钥路径" +private_key_data: "私钥内容" +save_and_connect: "保存并连接" +save: "保存" +cancel: "取消" +host_and_user_required: "主机名和用户名不能为空" +language: "显示语言" +follow_system: "跟随系统" +light_theme: "浅色主题" +dark_theme: "深色主题" +use_light_mode: "使用浅色模式" +use_dark_mode: "使用深色模式" +english: "English" +chinese: "简体中文" +idle_no_session: "空闲 / 未连接会话" +system: "系统" +cpu: "CPU" +mem: "内存" +swap: "虚拟内存" +net: "网络" +disk: "磁盘" +global_load: "全局负载" +live: "实时" +idle: "空闲" +saved: "已保存会话" +add_ssh: "+ SSH" +open_local_or_ssh: "打开本地终端或选择一个 SSH 会话。" +open_session: "打开会话" +settings: "设置" +terminal_font_size: "终端字体大小" +theme_management_hint: "主题模式和亮/暗主题系列可通过顶栏的主题按钮管理。" +local_host: "本地主机" +local_terminal_opened: "已打开本地终端" +connecting: "正在连接" +starting_connection: "开始连接..." +connection_failed: "连接失败" diff --git a/src/config.rs b/src/config.rs index d29f6e4..2553ef3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -78,12 +78,18 @@ pub struct ConfigFile { pub light_theme_name: String, #[serde(default)] pub dark_theme_name: String, + #[serde(default = "default_locale")] + pub locale: String, #[serde(default = "default_terminal_font_size")] pub terminal_font_size: f32, #[serde(default)] pub sessions: Vec, } +fn default_locale() -> String { + "system".to_string() +} + fn default_terminal_font_size() -> f32 { 13.0 } @@ -144,6 +150,18 @@ impl ConfigStore { &self.cache.dark_theme_name } + pub fn locale(&self) -> &str { + if self.cache.locale.is_empty() { + "system" + } else { + &self.cache.locale + } + } + + pub fn set_locale(&mut self, locale: &str) { + self.cache.locale = locale.to_string(); + } + pub fn terminal_font_size(&self) -> f32 { if self.cache.terminal_font_size <= 0.0 { default_terminal_font_size() diff --git a/src/main.rs b/src/main.rs index f78bb70..0f1f4a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ #![windows_subsystem = "windows"] use std::{ - borrow::Cow, cell::{Cell, RefCell}, collections::HashMap, ops::Range, @@ -15,7 +14,7 @@ use alacritty_terminal::index::Side; use alacritty_terminal::selection::SelectionType; use anyhow::{Context as _, Result}; use gpui::{ - Anchor, App, AppContext as _, Bounds, ClipboardItem, Context, Entity, FocusHandle, ElementId, + Anchor, App, AppContext as _, Bounds, ClipboardItem, Context, ElementId, Entity, FocusHandle, Focusable as _, FontWeight, Hsla, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, PathPromptOptions, Pixels, Point, QuitMode, Render, ScrollDelta, ScrollWheelEvent, @@ -23,8 +22,8 @@ use gpui::{ prelude::FluentBuilder as _, px, size, uniform_list, }; use gpui_component::{ - ActiveTheme as _, ElementExt, IconName, Root, Sizable as _, Disableable, Theme, ThemeMode, ThemeRegistry, - WindowExt as _, + ActiveTheme as _, Disableable, ElementExt, IconName, Root, Sizable as _, Theme, ThemeMode, + ThemeRegistry, WindowExt as _, button::{Button, ButtonVariants as _}, checkbox::Checkbox, dialog::Dialog, @@ -55,6 +54,8 @@ use system::{DiskSample, SystemSampler, SystemSnapshot, format_bytes}; use terminal::{BackendCommand, BackendEvent, TabKind, TerminalTab, encode_key}; use terminal_element::TerminalElement; +rust_i18n::i18n!("locales", fallback = "en"); + const DEFAULT_COLS: u16 = 100; const DEFAULT_ROWS: u16 = 30; const APP_FONT_FAMILY: &str = "Maple Mono NF CN"; @@ -201,14 +202,15 @@ struct SftpContextMenuState { impl Ashell { fn new(window: &mut Window, cx: &mut Context) -> Self { - let host_input = cx.new(|cx| InputState::new(window, cx).placeholder("host")); + let host_input = + cx.new(|cx| InputState::new(window, cx).placeholder(rust_i18n::t!("host"))); let session_name_input = cx.new(|cx| InputState::new(window, cx).placeholder("name (optional)")); let port_input = cx.new(|cx| InputState::new(window, cx).default_value("22")); let user_input = cx.new(|cx| InputState::new(window, cx).default_value("root")); let password_input = cx.new(|cx| { InputState::new(window, cx) - .placeholder("password") + .placeholder(rust_i18n::t!("password")) .masked(true) }); let key_path_input = @@ -259,6 +261,19 @@ impl Ashell { } else { config.dark_theme_name().into() }; + + let configured_locale = config.locale(); + let mut active_locale = configured_locale.to_string(); + if active_locale == "system" { + active_locale = sys_locale::get_locale().unwrap_or_else(|| "en".to_string()); + if active_locale.starts_with("zh") { + active_locale = "zh-CN".to_string(); + } else { + active_locale = "en".to_string(); + } + } + rust_i18n::set_locale(&active_locale); + gpui_component::set_locale(&active_locale); let mut this = Self { focus_handle: cx.focus_handle(), selector_focus_handle: cx.focus_handle(), @@ -454,7 +469,7 @@ impl Ashell { progress.lines.push(reason.clone().into()); let _ = session_label; let _ = tab_title; - progress.title = "Connection Failed".into(); + progress.title = rust_i18n::t!("connection_failed").into(); progress.failed = true; } } @@ -565,7 +580,7 @@ impl Ashell { let key_inline = self.key_inline_input.read(cx).value().to_string(); if host.is_empty() || user.is_empty() { - self.status = "host and user are required".into(); + self.status = rust_i18n::t!("host_and_user_required").into(); cx.notify(); return; } @@ -706,7 +721,7 @@ impl Ashell { window.open_dialog(cx, move |dialog: Dialog, _window, _cx| { dialog - .title("SSH Session") + .title(rust_i18n::t!("new_ssh_connection")) .w(px(520.)) .overlay_closable(true) .content({ @@ -737,7 +752,7 @@ impl Ashell { .gap_2() .child( Button::new("ssh-auth-password") - .label("password") + .label(rust_i18n::t!("password").to_string()) .when(is_password, |button| button.primary()) .on_click(window.listener_for( &view, @@ -751,7 +766,7 @@ impl Ashell { ) .child( Button::new("ssh-auth-key") - .label("key") + .label(rust_i18n::t!("key").to_string()) .when(!is_password, |button| button.primary()) .on_click(window.listener_for( &view, @@ -780,13 +795,17 @@ impl Ashell { .gap_2() .child( Button::new("connect-ssh-cancel") - .label("cancel") + .label(rust_i18n::t!("cancel").to_string()) .on_click(|_, window, cx| window.close_dialog(cx)), ) .child( Button::new("connect-ssh-confirm") .primary() - .label(if is_editing { "save" } else { "connect" }) + .label(if is_editing { + rust_i18n::t!("save") + } else { + rust_i18n::t!("connect") + }) .on_click(window.listener_for( &view, |this, _, window, cx| { @@ -873,13 +892,15 @@ impl Ashell { div() .text_size(px(12.)) .font_weight(FontWeight::SEMIBOLD) - .child("Local Terminal"), + .child(rust_i18n::t!("local_terminal")), ) .child( div() .text_size(px(11.)) .text_color(_cx.theme().muted_foreground) - .child("Open a new local shell tab"), + .child(rust_i18n::t!( + "open_local_shell_tab" + )), ), ), ) @@ -915,13 +936,15 @@ impl Ashell { div() .text_size(px(12.)) .font_weight(FontWeight::SEMIBOLD) - .child("New SSH Connection"), + .child(rust_i18n::t!("new_ssh_connection")), ) .child( div() .text_size(px(11.)) .text_color(_cx.theme().muted_foreground) - .child("Create or edit an SSH session"), + .child(rust_i18n::t!( + "create_or_edit_ssh_session" + )), ), ), ) @@ -1007,7 +1030,7 @@ impl Ashell { let view = cx.entity(); window.open_dialog(cx, move |dialog: Dialog, _window, _| { dialog - .title("Settings") + .title(rust_i18n::t!("settings").to_string()) .w(px(480.)) .content({ let view = view.clone(); @@ -1019,7 +1042,7 @@ impl Ashell { h_flex() .items_center() .gap_3() - .child(div().w(px(180.)).child("Terminal Font Size")) + .child(div().w(px(180.)).child(rust_i18n::t!("terminal_font_size").to_string())) .child( Button::new("font-size-down") .label("-") @@ -1041,11 +1064,63 @@ impl Ashell { })), ), ) + .child( + h_flex() + .items_center() + .gap_3() + .child(div().w(px(180.)).child(rust_i18n::t!("language").to_string())) + .child( + Button::new("language-dropdown") + .small() + .icon(IconName::Globe) + .label({ + let current_locale = view.read(cx).config.locale().to_string(); + if current_locale == "en" { + rust_i18n::t!("english").to_string() + } else if current_locale == "zh-CN" { + rust_i18n::t!("chinese").to_string() + } else { + rust_i18n::t!("follow_system").to_string() + } + }) + .dropdown_menu_with_anchor(Anchor::BottomRight, { + let view = view.clone(); + move |mut menu, window, cx| { + let current_locale = view.read(cx).config.locale().to_string(); + menu = menu + .min_w(160.) + .item( + PopupMenuItem::new(rust_i18n::t!("follow_system").to_string()) + .checked(current_locale == "system") + .on_click(window.listener_for(&view, |this, _, window, cx| { + this.set_display_language("system", window, cx) + })), + ) + .separator() + .item( + PopupMenuItem::new(rust_i18n::t!("english").to_string()) + .checked(current_locale == "en") + .on_click(window.listener_for(&view, |this, _, window, cx| { + this.set_display_language("en", window, cx) + })), + ) + .item( + PopupMenuItem::new(rust_i18n::t!("chinese").to_string()) + .checked(current_locale == "zh-CN") + .on_click(window.listener_for(&view, |this, _, window, cx| { + this.set_display_language("zh-CN", window, cx) + })), + ); + menu + } + }) + ) + ) .child( div() .text_size(px(12.)) .text_color(cx.theme().muted_foreground) - .child("Theme mode and light/dark theme families are managed from the top-bar theme button."), + .child(rust_i18n::t!("theme_management_hint")), ), ) } @@ -1175,8 +1250,8 @@ impl Ashell { self.pending_sftp_path_sync = Some("/".into()); self.connection_progress = Some(ConnectionProgress { tab_id: id, - title: "Connecting".into(), - lines: vec!["starting connection...".into()], + title: rust_i18n::t!("connecting").into(), + lines: vec![rust_i18n::t!("starting_connection").into()], failed: false, }); self.status = "ssh tab opened".into(); @@ -1529,6 +1604,26 @@ impl Ashell { cx.notify(); } + fn set_display_language(&mut self, locale: &str, window: &mut Window, cx: &mut Context) { + self.config.set_locale(locale); + let mut active_locale = locale.to_string(); + if active_locale == "system" { + active_locale = sys_locale::get_locale().unwrap_or_else(|| "en".to_string()); + if active_locale.starts_with("zh") { + active_locale = "zh-CN".to_string(); + } else { + active_locale = "en".to_string(); + } + } + rust_i18n::set_locale(&active_locale); + gpui_component::set_locale(&active_locale); + if let Err(err) = self.config.save() { + tracing::warn!("failed to save language preferences: {err:#}"); + } + window.refresh(); + cx.notify(); + } + fn apply_theme_preferences(&mut self, window: &mut Window, cx: &mut Context) { let light_theme = ThemeRegistry::global(cx) .themes() @@ -1914,7 +2009,7 @@ impl Ashell { div() .text_size(px(13.)) .text_color(cx.theme().muted_foreground) - .child("Open a local terminal or pick an SSH session."), + .child(rust_i18n::t!("open_local_or_ssh")), ) .child( h_flex() @@ -1922,13 +2017,13 @@ impl Ashell { .child( Button::new("home-open-local") .primary() - .label("Local Terminal") + .label(rust_i18n::t!("local_terminal").to_string()) .on_click(cx.listener(|this, _, _, cx| this.open_local(cx))), ) .child( Button::new("home-open-session") .ghost() - .label("Open Session") + .label(rust_i18n::t!("open_session").to_string()) .on_click(cx.listener(|this, _, window, cx| { this.show_selector_dialog(window, cx) })), @@ -1972,7 +2067,7 @@ impl Ashell { .as_ref() .and_then(|id| self.tabs.iter().find(|t| &t.id == id)) .map(|t| t.title.clone()) - .unwrap_or_else(|| "No session".into()) + .unwrap_or_else(|| rust_i18n::t!("idle_no_session").into()) } fn active_ssh_session(&self) -> Option<(String, Session)> { @@ -2033,7 +2128,7 @@ impl Ashell { fn system_card( &self, - label: &'static str, + label: String, percent: f32, detail: String, fill: Hsla, @@ -2056,7 +2151,7 @@ impl Ashell { .text_size(px(11.)) .font_weight(FontWeight::SEMIBOLD) .text_color(fill) - .child(label), + .child(label.clone()), ) .child(div().flex_1()) .child( @@ -2102,14 +2197,14 @@ impl Ashell { .text_size(px(12.)) .font_weight(FontWeight::SEMIBOLD) .text_color(cx.theme().chart_4) - .child("NET"), + .child(rust_i18n::t!("net")), ) .child(div().flex_1()) .child( div() .text_size(px(11.)) .text_color(cx.theme().muted_foreground) - .child("live"), + .child(rust_i18n::t!("live")), ), ) .child( @@ -2226,11 +2321,17 @@ impl Ashell { } fn download_selected_sftp_entries(&mut self, window: &mut Window, cx: &mut Context) { - let Some(sftp) = self.active_sftp() else { return }; + let Some(sftp) = self.active_sftp() else { + return; + }; let selected: Vec = sftp.selected_entries.iter().cloned().collect(); - if selected.is_empty() { return } + if selected.is_empty() { + return; + } - let Some(handle) = self.active_sftp_handle().cloned() else { return }; + let Some(handle) = self.active_sftp_handle().cloned() else { + return; + }; let path_prompt = cx.prompt_for_paths(PathPromptOptions { files: false, @@ -2249,7 +2350,7 @@ impl Ashell { local_dir: local_dir.clone(), }); } - + let _ = this.update(cx, |this, cx| { if let Some(sftp_mut) = this.active_sftp_mut() { sftp_mut.selected_entries.clear(); @@ -2264,7 +2365,9 @@ impl Ashell { } fn upload_sftp_files_batch(&mut self, paths: Vec, _cx: &mut Context) { - if paths.is_empty() { return } + if paths.is_empty() { + return; + } if let Some(sftp) = self.active_sftp() { if let Some(handle) = self.active_sftp_handle() { let _ = handle.commands.send(crate::sftp::SftpCommand::UploadPaths { @@ -2312,28 +2415,28 @@ impl Ashell { menu = menu .min_w(220.) .item( - PopupMenuItem::new("Follow System") + PopupMenuItem::new(rust_i18n::t!("follow_system")) .checked(follow_system) .on_click(window.listener_for(&view, |this, _, window, cx| { this.set_follow_system_theme(true, window, cx) })), ) .item( - PopupMenuItem::new("Use Light Mode") + PopupMenuItem::new(rust_i18n::t!("use_light_mode")) .checked(!follow_system && !is_dark_mode) .on_click(window.listener_for(&view, |this, _, window, cx| { this.switch_theme_mode(ThemeMode::Light, window, cx) })), ) .item( - PopupMenuItem::new("Use Dark Mode") + PopupMenuItem::new(rust_i18n::t!("use_dark_mode")) .checked(!follow_system && is_dark_mode) .on_click(window.listener_for(&view, |this, _, window, cx| { this.switch_theme_mode(ThemeMode::Dark, window, cx) })), ) .separator() - .label("Light Theme"); + .label(rust_i18n::t!("light_theme").to_string()); for theme_name in light_themes.clone() { let checked = theme_name == light_theme_name; @@ -2347,7 +2450,7 @@ impl Ashell { } menu = menu.separator(); - menu = menu.label("Dark Theme"); + menu = menu.label(rust_i18n::t!("dark_theme").to_string()); for theme_name in dark_themes.clone() { let checked = theme_name == dark_theme_name; @@ -2381,13 +2484,13 @@ impl Ashell { .text_size(px(12.)) .font_weight(FontWeight::SEMIBOLD) .text_color(cx.theme().primary) - .child("REMOTE FILES"), + .child(rust_i18n::t!("remote_files")), ) .child( div() .text_size(px(12.)) .text_color(cx.theme().muted_foreground) - .child("Open an SSH tab to browse SFTP files."), + .child(rust_i18n::t!("open_ssh_tab_sftp")), ); }; @@ -2400,7 +2503,10 @@ impl Ashell { .collect::>(); let status = sftp.status.clone(); let selected_entries = sftp.selected_entries.clone(); - let all_selected = !entries.is_empty() && entries.iter().all(|e| selected_entries.contains(&e.full_path)); + let all_selected = !entries.is_empty() + && entries + .iter() + .all(|e| selected_entries.contains(&e.full_path)); let parent_path = Self::sftp_parent_path(&sftp.current_path); let view = cx.entity(); let icon_col_width = px(14.); @@ -2412,10 +2518,16 @@ impl Ashell { .gap_0() .border_color(cx.theme().border) .bg(cx.theme().background) - .on_drop(cx.listener(|this, paths: &gpui::ExternalPaths, window, cx| { - let paths_to_upload: Vec = paths.0.iter().map(|p| p.to_string_lossy().to_string()).collect(); - this.upload_sftp_files_batch(paths_to_upload, cx); - })) + .on_drop( + cx.listener(|this, paths: &gpui::ExternalPaths, window, cx| { + let paths_to_upload: Vec = paths + .0 + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + this.upload_sftp_files_batch(paths_to_upload, cx); + }), + ) .child( h_flex() .h(px(34.)) @@ -2430,7 +2542,7 @@ impl Ashell { .text_size(px(12.)) .font_weight(FontWeight::SEMIBOLD) .text_color(cx.theme().primary) - .child("REMOTE FILES"), + .child(rust_i18n::t!("remote_files")), ) .child(div().flex_1()) .child( @@ -2438,7 +2550,7 @@ impl Ashell { .ghost() .small() .icon(IconName::ArrowRight) - .label("refresh") + .label(rust_i18n::t!("refresh").to_string()) .on_click(cx.listener(|this, _, _, cx| this.refresh_sftp(cx))), ) .child( @@ -2446,7 +2558,7 @@ impl Ashell { .ghost() .small() .icon(IconName::Plus) - .label("upload file") + .label(rust_i18n::t!("upload_file").to_string()) .on_click(cx.listener(|this, _, window, cx| { this.upload_sftp_files(window, cx) })), @@ -2456,7 +2568,7 @@ impl Ashell { .ghost() .small() .icon(IconName::Folder) - .label("upload folder") + .label(rust_i18n::t!("upload_folder").to_string()) .on_click(cx.listener(|this, _, window, cx| { this.upload_sftp_folder(window, cx) })), @@ -2466,7 +2578,11 @@ impl Ashell { .ghost() .small() .icon(IconName::ArrowDown) - .label(format!("download ({})", selected_entries.len())) + .label(if selected_entries.is_empty() { + rust_i18n::t!("download").to_string() + } else { + rust_i18n::t!("download_count", count = selected_entries.len()).to_string() + }) .disabled(selected_entries.is_empty()) .on_click(cx.listener(|this, _, window, cx| { this.download_selected_sftp_entries(window, cx); @@ -2474,7 +2590,8 @@ impl Ashell { ) .child( Checkbox::new("sftp-show-hidden") - .label("hidden") + .small() + .label(rust_i18n::t!("hidden").to_string()) .checked(self.show_hidden_files) .tab_stop(false) .on_click(cx.listener(|this, checked, _, cx| { @@ -2525,7 +2642,7 @@ impl Ashell { .on_click(cx.listener(move |this, checked, _, cx| { this.toggle_all_sftp_entries(*checked, cx); })), - ) + ), ) .child( h_flex() @@ -2539,7 +2656,7 @@ impl Ashell { .flex_1() .text_size(px(11.)) .text_color(cx.theme().muted_foreground) - .child("NAME"), + .child(rust_i18n::t!("name")), ), ) .child( @@ -2548,7 +2665,7 @@ impl Ashell { .flex_none() .text_size(px(11.)) .text_color(cx.theme().muted_foreground) - .child("SIZE"), + .child(rust_i18n::t!("size")), ) .child( div() @@ -2556,7 +2673,7 @@ impl Ashell { .flex_none() .text_size(px(11.)) .text_color(cx.theme().muted_foreground) - .child("MODIFIED"), + .child(rust_i18n::t!("modified")), ) .child(div().w(px(12.)).flex_none()), ) @@ -2578,7 +2695,8 @@ impl Ashell { let left_row = entry.clone(); let right_row = entry.clone(); let remote_path = entry.full_path.clone(); - let is_checked = selected_entries.contains(&entry.full_path); + let is_checked = + selected_entries.contains(&entry.full_path); let is_selected = selected_path .as_deref() .map(|path| path == entry.full_path) @@ -2642,18 +2760,30 @@ impl Ashell { .flex_none() .items_center() .justify_center() - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_mouse_down(MouseButton::Right, |_, _, cx| cx.stop_propagation()) - .child( - Checkbox::new(ElementId::Name(format!("check-{}", entry.full_path).into())) - .checked(is_checked) - .on_click(window.listener_for(&view, { - let path = entry.full_path.clone(); - move |this, checked, _, cx| { - this.toggle_sftp_entry(path.clone(), *checked, cx); - } - })), + .on_mouse_down(MouseButton::Left, |_, _, cx| { + cx.stop_propagation() + }) + .on_mouse_down( + MouseButton::Right, + |_, _, cx| cx.stop_propagation(), ) + .child( + Checkbox::new(ElementId::Name( + format!("check-{}", entry.full_path) + .into(), + )) + .checked(is_checked) + .on_click(window.listener_for(&view, { + let path = entry.full_path.clone(); + move |this, checked, _, cx| { + this.toggle_sftp_entry( + path.clone(), + *checked, + cx, + ); + } + })), + ), ) .child( h_flex() @@ -2747,11 +2877,6 @@ impl Ashell { fn sidebar(&self, cx: &mut Context) -> impl IntoElement { let sessions = self.config.sessions().to_vec(); let active_session_id = self.active_session_id().map(ToOwned::to_owned); - let active_kind = match self.active_kind() { - Some(TabKind::Local) => "local", - Some(TabKind::Ssh) => "ssh", - None => "idle", - }; let connection_color = self.active_connection_color(cx); v_flex() @@ -2789,7 +2914,17 @@ impl Ashell { div() .text_size(px(11.)) .text_color(cx.theme().muted_foreground) - .child(format!("{} / {}", active_kind, self.active_title())), + .child({ + if let Some(kind) = self.active_kind() { + let kind_str = match kind { + TabKind::Local => rust_i18n::t!("local_terminal").to_string(), + TabKind::Ssh => "ssh".to_string(), + }; + format!("{} / {}", kind_str, self.active_title()) + } else { + self.active_title() + } + }), ), ), ) @@ -2804,7 +2939,7 @@ impl Ashell { .text_size(px(12.)) .font_weight(FontWeight::SEMIBOLD) .text_color(cx.theme().primary) - .child("SYSTEM"), + .child(rust_i18n::t!("system")), ) .child(div().flex_1()) .child( @@ -2823,21 +2958,21 @@ impl Ashell { ) }) .child(self.system_card( - "CPU", + rust_i18n::t!("cpu").to_string(), self.system.cpu_percent, - "global load".into(), + rust_i18n::t!("global_load").into(), cx.theme().chart_1, cx, )) .child(self.system_card( - "MEM", + rust_i18n::t!("mem").to_string(), self.system.mem_percent, self.system.mem_detail.clone(), cx.theme().chart_2, cx, )) .child(self.system_card( - "SWAP", + rust_i18n::t!("swap").to_string(), self.system.swap_percent, self.system.swap_detail.clone(), cx.theme().chart_3, @@ -2856,7 +2991,7 @@ impl Ashell { div() .text_size(px(11.)) .text_color(cx.theme().chart_5) - .child("DISK"), + .child(rust_i18n::t!("disk")), ) .children({ let mut disk_elements = Vec::new(); @@ -2870,7 +3005,7 @@ impl Ashell { .child( Button::new("open-ssh-panel") .primary() - .label("+ ssh") + .label(rust_i18n::t!("add_ssh").to_string()) .on_click( cx.listener(|this, _, window, cx| this.open_new_ssh_dialog(window, cx)), ), @@ -2883,7 +3018,7 @@ impl Ashell { .text_size(px(12.)) .font_weight(FontWeight::SEMIBOLD) .text_color(cx.theme().primary) - .child("SAVED"), + .child(rust_i18n::t!("saved")), ) .child( v_flex()