设置、快捷键、窗口壳的文案接入多语言,打包元数据补英文

设置页按语言实时切换(分类、搜索关键词、规则表的提示),快捷键表改存键,
冲突提示按快捷键 id 找名称;状态栏、标题栏、开始页、确认框和通知换成整句。
macOS 包声明 en / zh-Hans 两种语言并翻译本地网络说明,Linux 桌面文件补英文说明。
新增界面语言实时切换的 UI 测试。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XuzandClaude Opus 5.5 committed 2026-10-07 21:42:09 +08:00
1 parent 1c855f1508
commit 352ffcef55
30 files changed
+1403 -418

No files matched your search

+1 -1
View File
@@ -104,7 +104,7 @@ ShellRS 同一时间只运行一个:已经开着时再打开一次,会把已
侧栏最下方的「设置」按钮(或 ⌘,,其他平台 Ctrl+,)在中间区打开「设置」标签页;已经打开时切换到该标签,不会重复打开。设置页分两栏:左边是分类(外观、终端、关键字高亮、键盘快捷键、外部 CLI、应用、关于),上方可搜索,栏宽可拖动;右边是所选分类的设置。设置改动立即生效并保存在数据目录的 `settings.json` 里。
- **外观 → 常规 → 界面语言**:跟随系统、简体中文、English,默认简体中文。英文界面尚在翻译中,目前只有 GPUI Kit 组件自带的文字(如搜索框提示)会切换。
- **外观 → 常规 → 界面语言**:跟随系统、简体中文、English,默认跟随系统:系统语言是中文时用简体中文,其他语言用英文。改动立即生效,不用重启:窗口里的文字、已打开的标签和搜索框提示一起换成新的语言;已经写进终端的提示和已经弹出的通知保持原样。外部 CLI 的输出也用这里的语言。
- **外观 → 常规 → 应用外观**:深色、浅色、跟随系统,默认跟随系统;选「跟随系统」时随系统的深浅色切换。标题栏的主题按钮也会改写这一项。
- **外观 → 主题**:左栏是浅色主题,右栏是深色主题,各选一套:应用外观为浅色时用左栏选中的那套,深色时用右栏的;跟随系统时随系统一起切换,标题栏的主题按钮切换时也一样。默认只有终端按所选主题配色,界面保持中性的灰色调(深色是不刺眼的炭灰,不是纯黑);打开上方的「界面跟随主题」,界面也按主题配色:侧栏、标签栏、对话框等用主题的底色和文字色,成功、警告、错误等状态用主题的绿、黄、红。每张卡片用该主题的颜色显示一段示例,选中的右上角有蓝色对勾。默认是 ShellRS Light 和 ShellRS Dark,与界面同色。内置浅色、深色各 10 套:浅色 ShellRS Light、Solarized Light、Tokyo Day、Catppuccin Latte、Flexoki Light、One Half Light、Gruvbox Light、Kanagawa Lotus、Rosé Pine Dawn、Everforest Light;深色 ShellRS Dark、Solarized Dark、Tokyo Night、Catppuccin Mocha、Flexoki Dark、One Half Dark、Gruvbox Dark、Kanagawa Wave、Dracula、Nord。同一主题的浅色版和深色版在两栏的同一行。改动立即作用于所有已打开的终端,字体和关键字高亮的预览也用当前主题;vim 等程序询问终端的背景色时,得到的是当前主题的颜色。**尚未实现:自定义主题、导入配色、按主机指定主题。**
- **终端 → 字体配置**:字体(只列出本机安装的等宽字体,默认是系统的等宽字体,macOS 上为 Menlo)、字号(10–28 像素,默认 13)、行高(字号的 1.0–2.0 倍,默认 1.54,即原来的 20 像素行高)。改动立即作用于所有已打开的终端;在终端里按 ⌘= / ⌘-(其他平台 Ctrl)也能逐级调字号,⌘0 回到默认。下方的预览按终端的排法显示字母、数字、易混字符、符号、中文、制表符和一行命令。设置文件里的字体在本机没有安装时,使用默认字体。
+109
View File
@@ -3,3 +3,112 @@ _version: 2
app.database_unavailable:
zh-CN: 无法打开本地数据库,本次运行的改动不会被保存:%{error}
en: "Couldn’t open the local database, so changes last only until ShellRS quits: %{error}"
# 设置 › 键盘快捷键: each shortcut by its id, in the order the page lists them.
app.shortcut.new_host:
zh-CN: 新建主机
en: New host
app.shortcut.new_group:
zh-CN: 新建分组
en: New group
app.shortcut.open_settings:
zh-CN: 打开设置
en: Open settings
app.shortcut.toggle_sidebar:
zh-CN: 显示或隐藏侧栏
en: Show or hide sidebar
app.shortcut.toggle_tool_sidebar:
zh-CN: 显示或隐藏右侧栏
en: Show or hide right sidebar
app.shortcut.focus_search:
zh-CN: 聚焦搜索
en: Focus search
app.shortcut.zoom_in:
zh-CN: 放大界面
en: Zoom in
app.shortcut.zoom_out:
zh-CN: 缩小界面
en: Zoom out
app.shortcut.zoom_reset:
zh-CN: 界面实际大小
en: Actual size
app.shortcut.quit:
zh-CN: 退出
en: Quit
app.shortcut.new_local_terminal:
zh-CN: 新建本地终端
en: New local terminal
app.shortcut.close_tab:
zh-CN: 关闭标签
en: Close tab
app.shortcut.next_tab:
zh-CN: 下一个标签
en: Next tab
app.shortcut.previous_tab:
zh-CN: 上一个标签
en: Previous tab
app.shortcut.terminal_copy:
zh-CN: 复制
en: Copy
app.shortcut.terminal_paste:
zh-CN: 粘贴
en: Paste
app.shortcut.terminal_find:
zh-CN: 查找
en: Find
app.shortcut.terminal_find_next:
zh-CN: 查找下一个
en: Find next
app.shortcut.terminal_find_previous:
zh-CN: 查找上一个
en: Find previous
app.shortcut.terminal_clear:
zh-CN: 清屏
en: Clear
app.shortcut.terminal_zoom_in:
zh-CN: 放大字号
en: Bigger text
app.shortcut.terminal_zoom_out:
zh-CN: 缩小字号
en: Smaller text
app.shortcut.terminal_zoom_reset:
zh-CN: 默认字号
en: Default text size
app.shortcut.editor_save:
zh-CN: 保存
en: Save
app.shortcut.switch_to_tab:
zh-CN: 切换到标签 1…9
en: Switch to tab 1…9
app.shortcut_group.general:
zh-CN: 通用
en: General
app.shortcut_group.tabs:
zh-CN: 标签
en: Tabs
app.shortcut_group.terminal:
zh-CN: 终端
en: Terminal
app.shortcut_group.editor:
zh-CN: 编辑器
en: Editor
app.shortcut_refusal.needs_modifier_mac:
zh-CN: 快捷键要带 ⌘、⌥ 或 ⌃,或者使用 F1–F12。
en: A shortcut needs ⌘, ⌥ or ⌃, or one of F1–F12.
app.shortcut_refusal.needs_modifier:
zh-CN: 快捷键要带 Ctrl 或 Alt,或者使用 F1–F12。
en: A shortcut needs Ctrl or Alt, or one of F1–F12.
app.shortcut_refusal.needs_digit_mac:
zh-CN: 这一项要按带修饰键的数字 1,例如 ⌘1。
en: Press the digit 1 with a modifier, such as ⌘1.
app.shortcut_refusal.needs_digit:
zh-CN: 这一项要按带修饰键的数字 1,例如 Alt+1。
en: Press the digit 1 with a modifier, such as Alt+1.
app.shortcut_refusal.used_by:
zh-CN: "%{keys} 已用于「%{label}」。"
en: "%{keys} is already used by “%{label}”."
app.shortcut_refusal.reserved:
zh-CN: "%{keys} 已被输入框等内置功能使用。"
en: "%{keys} is used by text fields and other built-in features."
+338
View File
@@ -22,3 +22,341 @@ settings.store.unsaved:
settings.store.save_failed:
zh-CN: 无法保存设置:%{error}
en: "Couldn’t save the settings: %{error}"
settings.tab.title:
zh-CN: 设置
en: Settings
settings.preview:
zh-CN: 预览
en: Preview
# A choice that is what ShellRS uses unless told otherwise: 「13(默认)」.
settings.default_option:
zh-CN: "%{value}(默认)"
en: "%{value} (default)"
settings.category.appearance:
zh-CN: 外观
en: Appearance
settings.category.terminal:
zh-CN: 终端
en: Terminal
settings.category.highlight:
zh-CN: 关键字高亮
en: Highlighting
settings.category.shortcuts:
zh-CN: 键盘快捷键
en: Keyboard shortcuts
settings.category.external_cli:
zh-CN: 外部 CLI
en: External CLI
settings.category.application:
zh-CN: 应用
en: General
settings.category.about:
zh-CN: 关于
en: About
settings.appearance.interface:
zh-CN: 常规
en: Interface
settings.appearance.language:
zh-CN: 界面语言
en: Language
settings.appearance.mode:
zh-CN: 应用外观
en: Appearance
settings.appearance.theme:
zh-CN: 主题
en: Theme
settings.appearance.theme_description:
zh-CN: 浅色外观时用左栏选中的主题,深色外观时用右栏的。
en: The light appearance uses the theme chosen on the left, the dark appearance the one on the right.
settings.terminal.font:
zh-CN: 字体配置
en: Font
settings.terminal.family:
zh-CN: 字体
en: Font family
settings.terminal.family_description:
zh-CN: 只列出等宽字体。
en: Only monospaced fonts are listed.
settings.terminal.size:
zh-CN: 字号
en: Font size
settings.terminal.size_description:
zh-CN: 以像素计。
en: In pixels.
settings.terminal.line_height:
zh-CN: 行高
en: Line height
settings.terminal.line_height_description:
zh-CN: 字号的倍数。
en: A multiple of the font size.
settings.terminal.preview_keywords:
zh-CN: 预览,字体,字号,行高
en: preview,font,size,line height
settings.notifications.title:
zh-CN: 通知
en: Notifications
settings.notifications.description:
zh-CN: ShellRS 不在前台时发系统通知;在前台但终端在别的标签时,在窗口右上角提示,点一下切过去。
en: When ShellRS is in the background, you get a system notification. When it’s in front but the terminal is in another tab, a notice appears at the top right of the window; click it to go there.
settings.notifications.programs:
zh-CN: 程序发送的通知
en: Notifications from programs
settings.notifications.programs_description:
zh-CN: 远程或本地程序用 OSC 9 / 777 请求的通知,例如 printf '\e]9;完成\a'。
en: Notifications that remote or local programs ask for with OSC 9 / 777, for example printf '\e]9;Done\a'.
settings.notifications.bell:
zh-CN: 响铃时通知
en: Notify on bell
settings.notifications.bell_description:
zh-CN: 终端就在当前标签时不提示。
en: Not when the terminal is the current tab.
settings.highlight.options:
zh-CN: 常规
en: Options
settings.highlight.enabled:
zh-CN: 启用关键字高亮
en: Highlight keywords
settings.highlight.enabled_description:
zh-CN: 全屏程序(vim、less、tmux 等)里不高亮。
en: Not in full-screen programs such as vim, less and tmux.
settings.highlight.preview_description:
zh-CN: 使用当前终端主题
en: In the current terminal theme
settings.highlight.preview_keywords:
zh-CN: 预览,高亮
en: preview,highlight
settings.highlight.rules:
zh-CN: 规则(%{count})
en: Rules (%{count})
settings.highlight.rules_description:
zh-CN: 规则按从上到下应用,勾选「通知」的规则在看不到终端时弹出提醒。
en: Rules apply from top to bottom. A rule with Notify checked alerts you when its terminal isn’t in view.
settings.highlight.rules_keywords:
zh-CN: 规则,正则,关键字,高亮,颜色,备注,通知,排序
en: rule,regex,keyword,highlight,color,note,notify,order
settings.window.title:
zh-CN: 窗口
en: Window
settings.window.remember_size:
zh-CN: 记住窗口大小
en: Remember window size
settings.window.remember_size_description:
zh-CN: 下次启动时恢复上次关闭时的窗口大小和最大化状态。
en: The next launch restores the window’s size, maximized or not, as it was when it closed.
settings.window.remember_position:
zh-CN: 记住窗口位置
en: Remember window position
settings.window.remember_position_description:
zh-CN: 下次启动时把窗口放回上次所在的屏幕和位置;那块屏幕不在时放在主屏幕中间。
en: The next launch puts the window back on the same screen and in the same place. If that screen is gone, it opens in the middle of the main screen.
settings.cli.access:
zh-CN: 访问控制
en: Access
settings.cli.enabled:
zh-CN: 启用外部 CLI
en: Enable external CLI
settings.cli.enabled_description:
zh-CN: 开启后,本机当前用户下的程序(如 AI Agent)可以通过 shellrs 命令,在已保存的主机上执行命令、传输和同步文件,并查看、新建、修改和删除主机与凭据,无需知道密码。
en: When on, programs running as you on this computer, such as AI agents, can use the shellrs command to run commands and transfer and sync files on saved hosts, and to view, create, change and delete hosts and credentials, without knowing any password.
settings.cli.binary:
zh-CN: CLI 二进制
en: Command
settings.cli.binary_description:
zh-CN: 把 shellrs 命令加入 PATH,方便在终端和 Agent 中直接调用。
en: Puts the shellrs command on the PATH so terminals and agents can call it directly.
settings.cli.binary_description_windows:
zh-CN: 把 shellrs 命令加入当前用户的 PATH,方便在终端和 Agent 中直接调用。重新打开的终端和 Agent 才能找到它。
en: Puts the shellrs command on your user PATH so terminals and agents can call it directly. Terminals and agents find it once reopened.
settings.cli.skills_description:
zh-CN: 安装 shellrs skill,让外部 Agent 在连接服务器、执行远程命令和传输文件时使用 shellrs CLI。可安装到通用目录或指定 Agent 的专用目录。
en: Install the shellrs skill so agents use the shellrs CLI to connect to servers, run remote commands and transfer files. Install it to the shared folder or to a particular agent’s own folder.
settings.cli.status:
zh-CN: 状态:
en: "Status:"
settings.cli.checking:
zh-CN: 正在检查…
en: Checking…
settings.cli.binary_unsupported:
zh-CN: 此系统暂不支持安装 shellrs 命令
en: The shellrs command can’t be installed on this system yet
settings.cli.skill_unsupported:
zh-CN: 此系统暂不支持安装
en: Can’t be installed on this system yet
settings.cli.missing:
zh-CN: 未安装(%{path})
en: Not installed (%{path})
settings.cli.outdated:
zh-CN: 已安装旧版本(%{path})
en: An older version is installed (%{path})
settings.cli.binary_stale:
zh-CN: "%{path} 指向 %{target},已失效或不是这个 ShellRS"
en: "%{path} points to %{target}, which is gone or isn’t this ShellRS"
settings.cli.binary_occupied_by:
zh-CN: "%{path} 已被其他程序占用(指向 %{target})"
en: "%{path} is taken by another program (it points to %{target})"
settings.cli.binary_occupied:
zh-CN: "%{path} 已被其他程序占用"
en: "%{path} is taken by another program"
settings.cli.binary_not_on_path:
zh-CN: "%{path} 所在的文件夹不在 PATH 中"
en: The folder of %{path} isn’t on the PATH
settings.cli.binary_unavailable:
zh-CN: 找不到 shellrs-cli.exe:它应在 ShellRS 程序旁边(开发时请先 cargo build)
en: Can’t find shellrs-cli.exe. It belongs next to the ShellRS program (in development, run cargo build first).
settings.cli.installed_at:
zh-CN: 已安装于
en: Installed at
settings.cli.installed_at_path:
zh-CN: 已安装于 %{path}
en: Installed at %{path}
settings.cli.install:
zh-CN: 安装
en: Install
settings.cli.reinstall:
zh-CN: 重新安装
en: Reinstall
settings.cli.update:
zh-CN: 更新
en: Update
settings.cli.remove:
zh-CN: 移除
en: Remove
settings.cli.binary_keywords:
zh-CN: CLI,PATH,shellrs,命令,安装
en: CLI,PATH,shellrs,command,install
settings.cli.check:
zh-CN: 检查状态
en: Check status
settings.cli.copy_skills:
zh-CN: 复制 skills
en: Copy skills
settings.cli.skills_keywords:
zh-CN: skill,Agent,检查,复制
en: skill,agent,check,copy
settings.about.updates:
zh-CN: 应用更新
en: Updates
settings.about.version:
zh-CN: 当前版本
en: Current version
settings.about.version_keywords:
zh-CN: 当前版本,检查更新,更新,升级,版本,下载
en: current version,check for updates,update,upgrade,version,download
settings.about.check:
zh-CN: 检查更新
en: Check for updates
settings.about.restart:
zh-CN: 重启并安装…
en: Restart to update…
settings.about.download_page:
zh-CN: 前往下载页
en: Go to downloads
settings.about.retry_download:
zh-CN: 重试下载
en: Retry download
settings.about.download:
zh-CN: 下载更新
en: Download update
settings.about.local_build:
zh-CN: 本机构建
en: Local build
# The version's tooltip: the build, then the system it is for.
settings.about.build:
zh-CN: "%{build}(%{platform})"
en: "%{build} (%{platform})"
settings.about.channel:
zh-CN: 更新渠道
en: Update channel
settings.about.channel_description:
zh-CN: 稳定版适合日常使用,Beta 可提前接收测试版本。
en: Stable is for everyday use. Beta gets test versions early.
settings.about.automatic:
zh-CN: 自动升级
en: Automatic updates
settings.about.automatic_description:
zh-CN: 检测到新版本后自动下载,下载完成后点击「重启并安装」完成更新。
en: A new version downloads by itself. Once it’s ready, click Restart to update.
settings.about.privacy:
zh-CN: 隐私
en: Privacy
settings.about.analytics:
zh-CN: 发送匿名使用统计
en: Send anonymous usage statistics
settings.about.analytics_description:
zh-CN: 发送应用版本、操作系统/架构,用于评估安装量、版本分布,用于改进 ShellRS。不会发送任何主机、凭据、命令或会话内容。
en: Sends the app version, operating system and architecture, to count installs and versions and to improve ShellRS. No host, credential, command or session content is ever sent.
settings.about.website:
zh-CN: 官网
en: Website
settings.about.website_description:
zh-CN: 功能介绍、下载和更新内容。
en: Features, downloads and what’s new.
settings.highlight.pattern_placeholder:
zh-CN: 例如:ERROR|WARN 或 failed\s+\d+
en: For example ERROR|WARN or failed\s+\d+
settings.highlight.note_placeholder:
zh-CN: 例如:错误告警
en: For example Error alert
settings.highlight.add_rule:
zh-CN: 添加规则
en: Add rule
settings.highlight.column_enabled:
zh-CN: 启用
en: "On"
settings.highlight.column_pattern:
zh-CN: 正则表达式
en: Regular expression
settings.highlight.column_note:
zh-CN: 备注
en: Note
settings.highlight.column_color:
zh-CN: 颜色
en: Color
settings.highlight.column_notify:
zh-CN: 通知
en: Notify
settings.highlight.drag:
zh-CN: 拖动调整顺序
en: Drag to reorder
settings.highlight.empty:
zh-CN: 还没有规则
en: No rules yet
settings.shortcuts.recording:
zh-CN: 按下组合键
en: Press a key combination
settings.shortcuts.none:
zh-CN: 无
en: None
settings.shortcuts.change:
zh-CN: 修改快捷键
en: Change shortcut
settings.shortcuts.enable:
zh-CN: 启用
en: Enable
settings.shortcuts.disable:
zh-CN: 禁用
en: Disable
settings.shortcuts.reset:
zh-CN: 恢复默认
en: Restore default
settings.theme.keywords:
zh-CN: 主题,终端主题,配色,颜色,浅色,深色
en: theme,terminal theme,color scheme,color,light,dark
settings.theme.app_follows:
zh-CN: 界面跟随主题
en: Match interface to theme
settings.theme.app_follows_description:
zh-CN: 关闭时界面用中性的黑白灰,只有终端按所选主题配色。
en: When off, the interface stays neutral black, white and gray, and only terminals take the theme’s colors.
+260
View File
@@ -0,0 +1,260 @@
_version: 2
# The title bar.
workspace.title_bar.hosts:
zh-CN: 主机
en: Hosts
workspace.title_bar.forwards:
zh-CN: 端口转发
en: Port forwarding
workspace.title_bar.forwards_running:
zh-CN: 端口转发,%{count} 条运行中
en: Port forwarding, %{count} running
workspace.title_bar.credentials:
zh-CN: 凭据
en: Credentials
workspace.title_bar.hide_sidebar:
zh-CN: 隐藏侧栏
en: Hide sidebar
workspace.title_bar.show_sidebar:
zh-CN: 显示侧栏
en: Show sidebar
workspace.title_bar.quick_connect:
zh-CN: 临时连接…
en: Quick connect…
workspace.title_bar.quick_connect_tooltip:
zh-CN: 连接一台主机,不保存
en: Connect to a host without saving it
workspace.title_bar.local_terminal:
zh-CN: 本地终端
en: Local terminal
workspace.title_bar.new_local_terminal:
zh-CN: 新建本地终端
en: New local terminal
workspace.title_bar.to_light:
zh-CN: 切换为浅色
en: Switch to light
workspace.title_bar.to_dark:
zh-CN: 切换为深色
en: Switch to dark
workspace.sidebar.settings:
zh-CN: 设置
en: Settings
workspace.sidebar.open_settings:
zh-CN: 打开设置
en: Open settings
# The status bar.
workspace.status.connected_to:
zh-CN: 已连接 %{name}
en: Connected to %{name}
workspace.status.connecting_to:
zh-CN: 连接中 %{name}
en: Connecting to %{name}
workspace.status.disconnected_from:
zh-CN: 未连接 %{name}
en: Not connected to %{name}
workspace.status.not_connected:
zh-CN: 未连接
en: Not connected
# How the connection stands, then what went wrong.
workspace.status.problem:
zh-CN: "%{state}:%{problem}"
en: "%{state}: %{problem}"
workspace.status.local_file:
zh-CN: 本机文件
en: Local file
workspace.status.local_starting:
zh-CN: 正在启动 本地终端
en: Local terminal starting
workspace.status.local_running:
zh-CN: 运行中 本地终端
en: Local terminal running
workspace.status.local_exited:
zh-CN: 已退出 本地终端
en: Local terminal exited
workspace.status.local_failed:
zh-CN: 启动失败 本地终端
en: Local terminal failed to start
workspace.status.local_closing:
zh-CN: 正在关闭 本地终端
en: Local terminal closing
workspace.status.encoding:
zh-CN: 编码 UTF-8
en: Encoding UTF-8
workspace.status.terminal_type:
zh-CN: 终端 xterm-256color
en: Terminal xterm-256color
workspace.status.terminal_size:
zh-CN: 终端尺寸:%{columns} 列,%{rows} 行
en: "Terminal size: %{columns} columns, %{rows} rows"
# 最近连接, the start page.
workspace.recent.title:
zh-CN: 最近连接
en: Recent
workspace.recent.hint:
zh-CN: 双击主机连接,或新建一台主机
en: Double-click a host to connect, or create a new one
workspace.recent.find_host:
zh-CN: 快速连接
en: Find a host
workspace.recent.new_host:
zh-CN: 新建主机…
en: New host…
workspace.recent.empty:
zh-CN: 还没有连接过的主机
en: No hosts connected yet
workspace.recent.connected:
zh-CN: 已连接
en: Connected
workspace.copied:
zh-CN: 已复制 %{text}
en: Copied %{text}
workspace.copied_id:
zh-CN: 已复制 ID %{id}
en: Copied ID %{id}
workspace.link.open_failed:
zh-CN: 无法打开链接
en: Couldn’t open the link
# Questions while connecting.
workspace.host_key.unknown_title:
zh-CN: 首次连接此主机
en: First connection to this host
workspace.host_key.trust:
zh-CN: 信任并连接
en: Trust and connect
workspace.host_key.changed_title:
zh-CN: 服务器主机密钥已变更
en: Server host key changed
workspace.host_key.changed:
zh-CN: "主机:%{host}:%{port}\n算法:%{algorithm}\n已保存指纹:%{old}\n服务器当前指纹:%{fingerprint}\n\n连接已阻断。请核验服务器身份后手动处理:%{path}"
en: "Host: %{host}:%{port}\nAlgorithm: %{algorithm}\nSaved fingerprint: %{old}\nServer’s fingerprint now: %{fingerprint}\n\nThe connection was blocked. Verify the server’s identity, then sort it out by hand in %{path}"
workspace.host_key.changed_via_jump_host:
zh-CN: "跳板主机:%{jump_host}\n主机:%{host}:%{port}\n算法:%{algorithm}\n已保存指纹:%{old}\n服务器当前指纹:%{fingerprint}\n\n连接已阻断。请核验服务器身份后手动处理:%{path}"
en: "Jump host: %{jump_host}\nHost: %{host}:%{port}\nAlgorithm: %{algorithm}\nSaved fingerprint: %{old}\nServer’s fingerprint now: %{fingerprint}\n\nThe connection was blocked. Verify the server’s identity, then sort it out by hand in %{path}"
workspace.auth.title:
zh-CN: SSH 认证
en: SSH authentication
workspace.auth.continue:
zh-CN: 继续
en: Continue
# 外部 CLI.
workspace.cli.start_failed:
zh-CN: 外部 CLI 无法启动
en: The external CLI couldn’t start
workspace.cli.install_command_failed:
zh-CN: 无法安装 shellrs 命令
en: Couldn’t install the shellrs command
workspace.cli.remove_command_failed:
zh-CN: 无法移除 shellrs 命令
en: Couldn’t remove the shellrs command
workspace.cli.install_skill_failed:
zh-CN: 无法安装 skill
en: Couldn’t install the skill
workspace.cli.remove_skill_failed:
zh-CN: 无法移除 skill
en: Couldn’t remove the skill
workspace.cli.skill_copied:
zh-CN: 已复制 shellrs skill
en: Copied the shellrs skill
# Changes not saved yet, which a close or a quit would lose. `files` is
# one of the workspace.files phrases.
workspace.files.single:
zh-CN: “%{name}”
en: “%{name}”
workspace.files.pair:
zh-CN: “%{first}”和“%{second}”
en: “%{first}” and “%{second}”
workspace.files.several:
zh-CN: “%{first}”等 %{count} 个文件
en: "%{count} files including “%{first}”"
workspace.unsaved.quit_title:
zh-CN: 退出 ShellRS?
en: Quit ShellRS?
workspace.unsaved.quit:
zh-CN: "%{files}有未保存的修改,退出后会丢失。"
en: Unsaved changes to %{files} will be lost.
workspace.unsaved.discard_and_quit:
zh-CN: 放弃修改并退出
en: Discard changes and quit
workspace.unsaved.close_tabs_title.one:
en: Close 1 tab?
workspace.unsaved.close_tabs_title.other:
zh-CN: 关闭 %{count} 个标签?
en: Close %{count} tabs?
workspace.unsaved.close_tabs:
zh-CN: "%{files}有未保存的修改,关闭后会丢失。"
en: Unsaved changes to %{files} will be lost.
workspace.unsaved.close_sftp_title:
zh-CN: 关闭 SFTP 标签?
en: Close the SFTP tab?
workspace.unsaved.close_sftp:
zh-CN: "%{files}有未保存的修改,关闭 SFTP 标签会一起关掉它们,修改会丢失。"
en: Closing the SFTP tab also closes %{files}. Unsaved changes will be lost.
workspace.unsaved.discard_and_close:
zh-CN: 放弃修改并关闭
en: Discard changes and close
# Terminal notices: the tab first, so a notice says which terminal it is
# from.
workspace.notice.titled:
zh-CN: "%{tab}:%{title}"
en: "%{tab}: %{title}"
workspace.notice.bell:
zh-CN: "%{tab}:响铃"
en: "%{tab}: Bell"
workspace.credential.public_key_copied:
zh-CN: 已复制公钥
en: Copied the public key
workspace.credential.public_key_unreadable:
zh-CN: 无法从 %{path} 读出公钥
en: Couldn’t read a public key from %{path}
workspace.credential.in_use_then_password.one:
en: 1 host uses this credential. Once it’s deleted, that host switches to Password, asks for the password when connecting, and keeps its username.
workspace.credential.in_use_then_password.other:
zh-CN: 有 %{count} 台主机正在使用此凭据。删除后它们改为「密码」,连接时询问密码,用户名不变。
en: "%{count} hosts use this credential. Once it’s deleted, they switch to Password, ask for the password when connecting, and keep their usernames."
workspace.credential.in_use_then_no_password.one:
en: 1 host uses this credential. Once it’s deleted, that host switches to No password, signs in with the SSH agent and the private keys in ~/.ssh, and keeps its username.
workspace.credential.in_use_then_no_password.other:
zh-CN: 有 %{count} 台主机正在使用此凭据。删除后它们改为「无密码」,用 SSH Agent 和 ~/.ssh 中的私钥登录,用户名不变。
en: "%{count} hosts use this credential. Once it’s deleted, they switch to No password, sign in with the SSH agent and the private keys in ~/.ssh, and keep their usernames."
workspace.forward.needs_host:
zh-CN: 端口转发经由一台主机的 SSH 连接,请先新建主机。
en: A port forward runs over a host’s SSH connection. Create a host first.
workspace.forward.delete_running:
zh-CN: 这条转发正在运行,会先停止。
en: This forward is running and will be stopped first.
workspace.forward.stopped:
zh-CN: 端口转发“%{title}”已停止
en: Port forward “%{title}” stopped
workspace.forward.prompt_origin:
zh-CN: 端口转发“%{title}”正在通过 %{host} 连接。
en: Port forward “%{title}” is connecting through %{host}.
workspace.snippets.delete_category.one:
en: The 1 snippet in the category is deleted with it.
workspace.snippets.delete_category.other:
zh-CN: 分类里的 %{count} 个片段会一起删除。
en: The %{count} snippets in the category are deleted with it.
workspace.update.done_title:
zh-CN: 更新完成
en: Update complete
workspace.update.done:
zh-CN: ShellRS 已更新到 %{version}。
en: ShellRS is now %{version}.
workspace.update.failed_title:
zh-CN: 更新没有完成
en: Update not completed
workspace.update.failed:
zh-CN: 没有装上 %{to},ShellRS 仍是 %{from}。可以到官网下载安装。
en: "%{to} wasn’t installed, so ShellRS is still %{from}. You can download it from the website."
+2 -1
View File
@@ -1,7 +1,8 @@
[Desktop Entry]
Type=Application
Name=ShellRS
Comment=SSH 主机管理:终端、SFTP 与端口转发
Comment=SSH host manager: terminal, SFTP and port forwarding
Comment[zh_CN]=SSH 主机管理:终端、SFTP 与端口转发
Exec=shellrs
Icon=shellrs
Terminal=false
+11 -2
View File
@@ -5,11 +5,20 @@
change: the keychain grants each saved password to them, so a new
identifier would have macOS ask about every password again.
bundle.sh fills in @VERSION@ and @BUILD@ (a number that only goes up).
The languages are the interface's: the system's own panels (the file
picker, the menu bar's standard items) follow whichever of them the user
prefers, and the *.lproj folders beside this file translate the
descriptions below.
-->
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>zh_CN</string>
<string>en</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>zh-Hans</string>
</array>
<key>CFBundleDisplayName</key>
<string>ShellRS</string>
<key>CFBundleExecutable</key>
@@ -35,7 +44,7 @@
<key>NSHighResolutionCapable</key>
<true/>
<key>NSLocalNetworkUsageDescription</key>
<string>ShellRS 需要访问本地网络,才能连接局域网中的主机。</string>
<string>ShellRS needs the local network to connect to hosts on your LAN.</string>
<key>NSHumanReadableCopyright</key>
<string>© ShellRS</string>
</dict>
+5
View File
@@ -24,6 +24,11 @@ chmod 755 "$app/Contents/MacOS/shellrs"
sed -e "s/@VERSION@/$version/g" -e "s/@BUILD@/$build/g" \
"$here/Info.plist.in" > "$app/Contents/Info.plist"
plutil -lint "$app/Contents/Info.plist" >/dev/null
# The Info.plist descriptions in each language of the interface.
for lproj in "$here"/*.lproj; do
cp -R "$lproj" "$app/Contents/Resources/"
plutil -lint "$app/Contents/Resources/$(basename "$lproj")/InfoPlist.strings" >/dev/null
done
icns="$root/assets/logo/shellrs.icns"
icon="$root/assets/logo/shellrs.png"
@@ -0,0 +1,2 @@
/* What macOS shows when ShellRS first reaches for the local network. */
NSLocalNetworkUsageDescription = "ShellRS needs the local network to connect to hosts on your LAN.";
@@ -0,0 +1,2 @@
/* ShellRS 第一次访问本地网络时,macOS 显示的说明。 */
NSLocalNetworkUsageDescription = "ShellRS 需要访问本地网络,才能连接局域网中的主机。";
+58 -43
View File
@@ -14,6 +14,7 @@ use gpui_kit::*;
use serde::{Deserialize, Deserializer, Serialize};
use super::*;
use crate::i18n::t;
use crate::terminal::{TERMINAL_FIND_KEY_CONTEXT, TERMINAL_KEY_CONTEXT, terminal_key_bindings};
/// The groups of 设置 › 键盘快捷键, in the order the page shows them.
@@ -28,12 +29,12 @@ pub enum ShortcutGroup {
impl ShortcutGroup {
pub const ALL: [Self; 4] = [Self::General, Self::Tabs, Self::Terminal, Self::Editor];
pub fn title(self) -> &'static str {
pub fn title(self) -> SharedString {
match self {
Self::General => "通用",
Self::Tabs => "标签",
Self::Terminal => "终端",
Self::Editor => "编辑器",
Self::General => t!("app.shortcut_group.general"),
Self::Tabs => t!("app.shortcut_group.tabs"),
Self::Terminal => t!("app.shortcut_group.terminal"),
Self::Editor => t!("app.shortcut_group.editor"),
}
}
}
@@ -42,6 +43,7 @@ impl ShortcutGroup {
pub struct Shortcut {
/// What the settings file stores. Never changes once released.
id: &'static str,
/// The key of what it is called.
label: &'static str,
group: ShortcutGroup,
/// Where it works: anywhere, or in these key contexts.
@@ -82,7 +84,7 @@ macro_rules! shortcut {
pub static SHORTCUTS: [Shortcut; 25] = [
shortcut!(
"new-host",
"新建主机",
"app.shortcut.new_host",
General,
&[],
"cmd-n",
@@ -91,7 +93,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"new-group",
"新建分组",
"app.shortcut.new_group",
General,
&[],
"cmd-shift-n",
@@ -100,7 +102,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"open-settings",
"打开设置",
"app.shortcut.open_settings",
General,
&[],
"cmd-,",
@@ -109,7 +111,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"toggle-sidebar",
"显示或隐藏侧栏",
"app.shortcut.toggle_sidebar",
General,
&[],
"cmd-b",
@@ -118,7 +120,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"toggle-tool-sidebar",
"显示或隐藏右侧栏",
"app.shortcut.toggle_tool_sidebar",
General,
&[],
"cmd-alt-b",
@@ -128,7 +130,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
// In a terminal ⌘K clears instead, as in other macOS terminals.
shortcut!(
"focus-search",
"聚焦搜索",
"app.shortcut.focus_search",
General,
&[],
"cmd-k",
@@ -138,7 +140,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
// In a terminal the same keys size its text instead, as terminals do.
shortcut!(
"zoom-in",
"放大界面",
"app.shortcut.zoom_in",
General,
&[],
"cmd-=",
@@ -147,7 +149,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"zoom-out",
"缩小界面",
"app.shortcut.zoom_out",
General,
&[],
"cmd--",
@@ -156,17 +158,25 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"zoom-reset",
"界面实际大小",
"app.shortcut.zoom_reset",
General,
&[],
"cmd-0",
"ctrl-0",
ZoomReset
),
shortcut!("quit", "退出", General, &[], "cmd-q", "ctrl-q", Quit),
shortcut!(
"quit",
"app.shortcut.quit",
General,
&[],
"cmd-q",
"ctrl-q",
Quit
),
shortcut!(
"new-local-terminal",
"新建本地终端",
"app.shortcut.new_local_terminal",
Tabs,
&[],
"cmd-t",
@@ -175,7 +185,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"close-tab",
"关闭标签",
"app.shortcut.close_tab",
Tabs,
&[],
"cmd-w",
@@ -185,7 +195,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
// macOS reports ⌘⇧] as ⌘}, as Safari and Terminal take it.
shortcut!(
"next-tab",
"下一个标签",
"app.shortcut.next_tab",
Tabs,
&[],
"cmd-}",
@@ -194,7 +204,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"previous-tab",
"上一个标签",
"app.shortcut.previous_tab",
Tabs,
&[],
"cmd-{",
@@ -203,7 +213,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
Shortcut {
id: "switch-to-tab",
label: "切换到标签 1…9",
label: "app.shortcut.switch_to_tab",
group: ShortcutGroup::Tabs,
contexts: &[],
mac: "cmd-1",
@@ -213,7 +223,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
},
shortcut!(
"terminal-copy",
"复制",
"app.shortcut.terminal_copy",
Terminal,
TERMINAL,
"cmd-c",
@@ -222,7 +232,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"terminal-paste",
"粘贴",
"app.shortcut.terminal_paste",
Terminal,
TERMINAL,
"cmd-v",
@@ -231,7 +241,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"terminal-find",
"查找",
"app.shortcut.terminal_find",
Terminal,
TERMINAL_AND_FIND,
"cmd-f",
@@ -240,7 +250,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"terminal-find-next",
"查找下一个",
"app.shortcut.terminal_find_next",
Terminal,
TERMINAL_AND_FIND,
"cmd-g",
@@ -249,7 +259,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"terminal-find-previous",
"查找上一个",
"app.shortcut.terminal_find_previous",
Terminal,
TERMINAL_AND_FIND,
"cmd-shift-g",
@@ -258,7 +268,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"terminal-clear",
"清屏",
"app.shortcut.terminal_clear",
Terminal,
TERMINAL,
"cmd-k",
@@ -269,7 +279,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
// iTerm2 and Windows Terminal.
shortcut!(
"terminal-zoom-in",
"放大字号",
"app.shortcut.terminal_zoom_in",
Terminal,
TERMINAL_AND_FIND,
"cmd-=",
@@ -278,7 +288,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"terminal-zoom-out",
"缩小字号",
"app.shortcut.terminal_zoom_out",
Terminal,
TERMINAL_AND_FIND,
"cmd--",
@@ -287,7 +297,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"terminal-zoom-reset",
"默认字号",
"app.shortcut.terminal_zoom_reset",
Terminal,
TERMINAL_AND_FIND,
"cmd-0",
@@ -296,7 +306,7 @@ pub static SHORTCUTS: [Shortcut; 25] = [
),
shortcut!(
"editor-save",
"保存",
"app.shortcut.editor_save",
Editor,
EDITOR,
"cmd-s",
@@ -320,8 +330,8 @@ impl Shortcut {
self.id
}
pub fn label(&self) -> &'static str {
self.label
pub fn label(&self) -> SharedString {
t!(self.label)
}
pub fn is_numbered(&self) -> bool {
@@ -481,7 +491,7 @@ pub enum Refusal {
NeedsModifier,
/// 切换到标签 1…9 takes a modifier with a digit.
NeedsDigit,
/// Another shortcut has it where this one works.
/// Another shortcut, this id's, has it where this one works.
UsedBy(&'static str),
/// A text field, list or dialog has it.
Reserved,
@@ -492,15 +502,20 @@ impl Refusal {
pub fn message(&self, keys: &str) -> String {
match self {
Self::NeedsModifier if cfg!(target_os = "macos") => {
"快捷键要带 ⌘、⌥ 或 ⌃,或者使用 F1–F12。".into()
t!("app.shortcut_refusal.needs_modifier_mac").to_string()
}
Self::NeedsModifier => "快捷键要带 Ctrl 或 Alt,或者使用 F1–F12。".into(),
Self::NeedsModifier => t!("app.shortcut_refusal.needs_modifier").to_string(),
Self::NeedsDigit if cfg!(target_os = "macos") => {
"这一项要按带修饰键的数字 1,例如 ⌘1。".into()
t!("app.shortcut_refusal.needs_digit_mac").to_string()
}
Self::NeedsDigit => "这一项要按带修饰键的数字 1,例如 Alt+1。".into(),
Self::UsedBy(label) => format!("{keys} 已用于「{label}」。"),
Self::Reserved => format!("{keys} 已被输入框等内置功能使用。"),
Self::NeedsDigit => t!("app.shortcut_refusal.needs_digit").to_string(),
Self::UsedBy(id) => t!(
"app.shortcut_refusal.used_by",
keys = keys,
label = Shortcut::find(id).map(Shortcut::label).unwrap_or_default()
)
.to_string(),
Self::Reserved => t!("app.shortcut_refusal.reserved", keys = keys).to_string(),
}
}
}
@@ -547,7 +562,7 @@ pub fn check(
.iter()
.any(|(taken, _)| same_keystroke(pressed, taken))
}) {
return Err(Refusal::UsedBy(other.label));
return Err(Refusal::UsedBy(other.id));
}
}
// One that works anywhere would win over a text field's or a list's
@@ -1032,7 +1047,7 @@ mod tests {
let terminal = shortcut("new-local-terminal");
assert_eq!(
check(new_host, terminal.default_keys()),
Err(Refusal::UsedBy("新建本地终端"))
Err(Refusal::UsedBy("new-local-terminal"))
);
// A text field's key.
assert_eq!(check(new_host, &primary("z")), Err(Refusal::Reserved));
@@ -1047,7 +1062,7 @@ mod tests {
let clear = shortcut("terminal-clear");
assert_eq!(
check(shortcut("terminal-copy"), clear.default_keys()),
Err(Refusal::UsedBy("清屏"))
Err(Refusal::UsedBy("terminal-clear"))
);
// 切换到标签 1…9 takes any digit for all nine, and holds them all.
@@ -1057,7 +1072,7 @@ mod tests {
assert_eq!(check(tabs, "ctrl-alt-5"), Ok("ctrl-alt-1".into()));
assert_eq!(
check(new_host, &tabs.default_keys().replace('1', "4")),
Err(Refusal::UsedBy("切换到标签 1…9"))
Err(Refusal::UsedBy("switch-to-tab"))
);
}
+20 -2
View File
@@ -177,10 +177,28 @@ mod tests {
/// every text in them goes through `t!`. Grows as modules are
/// translated, until it is all of `src`.
const TRANSLATED: &[&str] = &[
"src/app",
"src/i18n.rs",
"src/settings/model.rs",
"src/settings/store.rs",
"src/main.rs",
"src/settings",
"src/shared",
"src/workspace/analytics.rs",
"src/workspace/credentials.rs",
"src/workspace/dock_skin.rs",
"src/workspace/editors.rs",
"src/workspace/forwards.rs",
"src/workspace/links.rs",
"src/workspace/mod.rs",
"src/workspace/notices.rs",
"src/workspace/recent_hosts.rs",
"src/workspace/sidebar.rs",
"src/workspace/snippets.rs",
"src/workspace/status_bar.rs",
"src/workspace/tabs.rs",
"src/workspace/title_bar.rs",
"src/workspace/updates.rs",
"src/workspace/window_state.rs",
"src/workspace/workspace_view.rs",
];
/// Marks a line whose Chinese literal is meant to stay, such as a
+10 -1
View File
@@ -42,7 +42,16 @@ fn main() {
// Read before anything is built: the settings choose the language
// every text is in, and 应用 › 窗口 where the window opens.
let (settings, settings_problem) = open_settings();
shellrs::i18n::set_locale(settings.settings().language.resolved());
let locale = settings.settings().language.resolved();
shellrs::i18n::set_locale(locale);
// Defaults that become the user's own, the example highlight rules
// a first start fills in, are made while reading: read again, now
// in the language just chosen.
let (settings, settings_problem) = if locale == shellrs::i18n::LOCALES[0] {
(settings, settings_problem)
} else {
open_settings()
};
shellrs::init(cx);
shellrs::app::show_logo_when_unbundled(cx);
cx.activate(true);
+54 -12
View File
@@ -16,6 +16,7 @@ use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::*;
use crate::app::CatalogIcon;
use crate::i18n::{UiLocale, t};
use crate::terminal::{HighlightColor, HighlightRule};
use super::SettingsStore;
@@ -51,6 +52,7 @@ pub struct HighlightRulesEditor {
/// row keeps its fields.
next_id: usize,
_store_changes: Subscription,
_locale: Subscription,
}
struct RuleRow {
@@ -105,11 +107,24 @@ impl HighlightRulesEditor {
cx.notify();
}
});
// The rows' inputs keep their placeholders; a new language gives
// them again.
let locale = cx.observe_global_in::<UiLocale>(window, |this, window, cx| {
for row in &this.rows {
row.pattern.update(cx, |input, cx| {
input.set_placeholder(t!("settings.highlight.pattern_placeholder"), window, cx)
});
row.note.update(cx, |input, cx| {
input.set_placeholder(t!("settings.highlight.note_placeholder"), window, cx)
});
}
});
let mut this = Self {
store: store.clone(),
rows: Vec::new(),
next_id: 0,
_store_changes: store_changes,
_locale: locale,
};
for rule in store.read(cx).settings().terminal_highlight.rules {
let row = this.new_row(rule, window, cx);
@@ -132,12 +147,12 @@ impl HighlightRulesEditor {
self.next_id += 1;
let pattern = cx.new(|cx| {
InputState::new(window, cx)
.placeholder(r"例如:ERROR|WARN 或 failed\s+\d+")
.placeholder(t!("settings.highlight.pattern_placeholder"))
.default_value(rule.pattern.clone())
});
let note = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("例如:错误告警")
.placeholder(t!("settings.highlight.note_placeholder"))
.default_value(rule.note.clone())
});
let hex = cx.new(|cx| InputState::new(window, cx).default_value(rule.color.to_hex()));
@@ -280,7 +295,7 @@ impl HighlightRulesEditor {
.outline()
.small()
.icon(Icon::new(IconName::Plus))
.label("添加规则")
.label(t!("settings.highlight.add_rule"))
.on_click(cx.listener(|this, _, window, cx| this.add(window, cx))),
)
}
@@ -294,11 +309,36 @@ impl HighlightRulesEditor {
.text_color(cx.theme().muted_foreground)
// Over the drag handles.
.child(div().w_4().flex_shrink_0())
.child(div().w_8().flex_shrink_0().child("启用"))
.child(div().flex_1().min_w_0().child("正则表达式"))
.child(div().w_40().flex_shrink_0().child("备注"))
.child(div().w_32().flex_shrink_0().child("颜色"))
.child(div().w_8().flex_shrink_0().child("通知"))
.child(
div()
.w_8()
.flex_shrink_0()
.child(t!("settings.highlight.column_enabled")),
)
.child(
div()
.flex_1()
.min_w_0()
.child(t!("settings.highlight.column_pattern")),
)
.child(
div()
.w_40()
.flex_shrink_0()
.child(t!("settings.highlight.column_note")),
)
.child(
div()
.w_32()
.flex_shrink_0()
.child(t!("settings.highlight.column_color")),
)
.child(
div()
.w_8()
.flex_shrink_0()
.child(t!("settings.highlight.column_notify")),
)
.child(div().w_6().flex_shrink_0())
}
@@ -336,7 +376,9 @@ impl HighlightRulesEditor {
.flex_shrink_0()
.cursor_grab()
.text_color(cx.theme().muted_foreground)
.tooltip(|window, cx| Tooltip::new("拖动调整顺序").build(window, cx))
.tooltip(|window, cx| {
Tooltip::new(t!("settings.highlight.drag")).build(window, cx)
})
.child(Icon::new(CatalogIcon::GripVertical).small())
.on_drag(
DraggedRule {
@@ -404,7 +446,7 @@ impl HighlightRulesEditor {
FEATURED_COLORS.iter().map(|color| color.hsla()).collect(),
)
.small()
.accessibility_label("颜色"),
.accessibility_label(t!("settings.highlight.column_color")),
),
)
.child(
@@ -433,7 +475,7 @@ impl HighlightRulesEditor {
.ghost()
.xsmall()
.icon(Icon::new(CatalogIcon::Trash))
.tooltip("删除")
.tooltip(t!("common.delete"))
.on_click(cx.listener(move |this, _, _, cx| this.remove(id, cx))),
),
)
@@ -453,7 +495,7 @@ impl Render for HighlightRulesEditor {
div()
.text_sm()
.text_color(cx.theme().muted_foreground)
.child("还没有规则")
.child(t!("settings.highlight.empty"))
.into_any_element()
} else {
let rows: Vec<_> = self
+253 -231
View File
@@ -26,6 +26,7 @@ use crate::app::{
RemoveAgentSkill, RemoveCliCommand, ShowUpdate,
};
use crate::cli::{AgentKind, BinaryStatus, CliIntegration, IntegrationStatus, SkillStatus};
use crate::i18n::t;
use crate::shared::{ClosableTabTitle, close_tab_items};
use crate::terminal::{
FONT_SIZE_RANGE, LINE_HEIGHT_RANGE, TerminalColors, TerminalFont, TerminalFontPreview,
@@ -172,7 +173,7 @@ impl Panel for SettingsPanel {
ClosableTabTitle::new(
"settings-tab",
Icon::new(CatalogIcon::Settings).small(),
"设置",
t!("settings.tab.title"),
)
.closable("close-settings", Box::new(CloseSettings))
.context_menu(move |menu, _, cx| tab_menu(menu, group.clone(), panel, cx))
@@ -225,6 +226,7 @@ impl Render for SettingsPanel {
/// One category of the left column.
struct Category {
/// The key of the title.
title: &'static str,
icon: CatalogIcon,
/// The category's settings. `Settings` leaves out a page without groups,
@@ -238,37 +240,37 @@ type CategoryGroups = fn(&SettingsPanel, &App) -> Vec<SettingGroup>;
/// The categories, in the order the left column lists them.
const CATEGORIES: [Category; 7] = [
Category {
title: "外观",
title: "settings.category.appearance",
icon: CatalogIcon::Palette,
groups: appearance_groups,
},
Category {
title: "终端",
title: "settings.category.terminal",
icon: CatalogIcon::Terminal,
groups: terminal_groups,
},
Category {
title: "关键字高亮",
title: "settings.category.highlight",
icon: CatalogIcon::Highlighter,
groups: highlight_groups,
},
Category {
title: "键盘快捷键",
title: "settings.category.shortcuts",
icon: CatalogIcon::Keyboard,
groups: shortcuts_groups,
},
Category {
title: "外部 CLI",
title: "settings.category.external_cli",
icon: CatalogIcon::SquareTerminal,
groups: external_cli_groups,
},
Category {
title: "应用",
title: "settings.category.application",
icon: CatalogIcon::AppWindow,
groups: application_groups,
},
Category {
title: "关于",
title: "settings.category.about",
icon: CatalogIcon::Info,
groups: about_groups,
},
@@ -278,7 +280,7 @@ impl Category {
/// The page of the category at `ix` in `CATEGORIES`.
fn page(&self, ix: usize, panel: &SettingsPanel, cx: &App) -> SettingPage {
let shown = panel.shown_category.clone();
SettingPage::new(self.title)
SettingPage::new(t!(self.title))
.icon(Icon::new(self.icon))
// Drawn only for the page on display: nothing to see, it notes
// which one that is.
@@ -294,27 +296,29 @@ impl Category {
fn appearance_groups(panel: &SettingsPanel, _: &App) -> Vec<SettingGroup> {
let store = &panel.store;
vec![
SettingGroup::new().title("常规").items([
SettingItem::new(
"界面语言",
choice_field(
store,
|settings| settings.language,
|settings, language| settings.language = language,
),
),
SettingItem::new(
"应用外观",
choice_field(
store,
|settings| settings.appearance,
|settings, appearance| settings.appearance = appearance,
),
),
]),
SettingGroup::new()
.title("主题")
.description("浅色外观时用左栏选中的主题,深色外观时用右栏的。")
.title(t!("settings.appearance.interface"))
.items([
SettingItem::new(
t!("settings.appearance.language"),
choice_field(
store,
|settings| settings.language,
|settings, language| settings.language = language,
),
),
SettingItem::new(
t!("settings.appearance.mode"),
choice_field(
store,
|settings| settings.appearance,
|settings, appearance| settings.appearance = appearance,
),
),
]),
SettingGroup::new()
.title(t!("settings.appearance.theme"))
.description(t!("settings.appearance.theme_description"))
.items([app_follows_item(store), terminal_theme_item(store)]),
]
}
@@ -327,7 +331,7 @@ const PREVIEW_LINES: &[&str] = &[
"abcdefghijklmnopqrstuvwxyz",
"0123456789 0O 1lI| ,.;:'\"`",
"~!@#$%^&*()-_=+[]{}<>/\\?",
"中文预览:你好,世界!",
"中文预览:你好,世界!", // i18n: keep (shows how two-cell characters look)
"┌──┬──┐ ░▒▓█ ←↑→↓ ✓",
"root@web-01:~$ tail -f /var/log/syslog",
];
@@ -337,46 +341,50 @@ fn terminal_groups(panel: &SettingsPanel, cx: &App) -> Vec<SettingGroup> {
let store = &panel.store;
let defaults = AppSettings::default().terminal_font;
vec![
SettingGroup::new().title("字体配置").items([
SettingItem::new("字体", font_family_field(panel, cx)).description("只列出等宽字体。"),
SettingItem::new("字号", font_size_field(store)).description("以像素计。"),
SettingItem::new(
"行高",
number_field(
store,
NumberFieldOptions {
min: *LINE_HEIGHT_RANGE.start() as f64,
max: *LINE_HEIGHT_RANGE.end() as f64,
step: 0.1,
},
|settings| settings.terminal_font.line_height,
|settings, line_height| settings.terminal_font.line_height = line_height,
)
.default_value(defaults.line_height as f64),
)
.description("字号的倍数。"),
SettingItem::render(|_, _, cx| {
v_flex()
.w_full()
.gap_2()
.child(div().text_sm().child("预览"))
.child(
div()
.w_full()
.p_3()
.rounded(cx.theme().radius)
.border_1()
.border_color(cx.theme().border)
.bg(TerminalColors::current(cx).background())
.overflow_hidden()
.child(TerminalFontPreview::new(
"terminal-font-preview",
PREVIEW_LINES,
)),
SettingGroup::new()
.title(t!("settings.terminal.font"))
.items([
SettingItem::new(t!("settings.terminal.family"), font_family_field(panel, cx))
.description(t!("settings.terminal.family_description")),
SettingItem::new(t!("settings.terminal.size"), font_size_field(store))
.description(t!("settings.terminal.size_description")),
SettingItem::new(
t!("settings.terminal.line_height"),
number_field(
store,
NumberFieldOptions {
min: *LINE_HEIGHT_RANGE.start() as f64,
max: *LINE_HEIGHT_RANGE.end() as f64,
step: 0.1,
},
|settings| settings.terminal_font.line_height,
|settings, line_height| settings.terminal_font.line_height = line_height,
)
})
.keywords(["预览", "字体", "字号", "行高"]),
]),
.default_value(defaults.line_height as f64),
)
.description(t!("settings.terminal.line_height_description")),
SettingItem::render(|_, _, cx| {
v_flex()
.w_full()
.gap_2()
.child(div().text_sm().child(t!("settings.preview")))
.child(
div()
.w_full()
.p_3()
.rounded(cx.theme().radius)
.border_1()
.border_color(cx.theme().border)
.bg(TerminalColors::current(cx).background())
.overflow_hidden()
.child(TerminalFontPreview::new(
"terminal-font-preview",
PREVIEW_LINES,
)),
)
})
.keywords(keywords(t!("settings.terminal.preview_keywords"))),
]),
notification_group(store),
]
}
@@ -398,24 +406,22 @@ fn notification_group(store: &Entity<SettingsStore>) -> SettingGroup {
.default_value(true)
};
SettingGroup::new()
.title("通知")
.description(
"ShellRS 不在前台时发系统通知;在前台但终端在别的标签时,在窗口右上角提示,点一下切过去。",
)
.title(t!("settings.notifications.title"))
.description(t!("settings.notifications.description"))
.items([
SettingItem::new(
"程序发送的通知",
t!("settings.notifications.programs"),
switch(
|settings| settings.programs,
|settings, on| settings.programs = on,
),
)
.description("远程或本地程序用 OSC 9 / 777 请求的通知,例如 printf '\\e]9;完成\\a'。"),
.description(t!("settings.notifications.programs_description")),
SettingItem::new(
"响铃时通知",
t!("settings.notifications.bell"),
switch(|settings| settings.bell, |settings, on| settings.bell = on),
)
.description("终端就在当前标签时不提示。"),
.description(t!("settings.notifications.bell_description")),
])
}
@@ -443,38 +449,33 @@ fn highlight_groups(panel: &SettingsPanel, cx: &App) -> Vec<SettingGroup> {
.len();
let editor = panel.highlight_rules.clone();
vec![
SettingGroup::new().title("常规").items([SettingItem::new(
"启用关键字高亮",
SettingField::switch(
move |cx| reader.read(cx).settings().terminal_highlight.enabled,
move |enabled, cx| {
writer.update(cx, |store, cx| {
store.update(|settings| settings.terminal_highlight.enabled = enabled, cx)
});
},
)
.default_value(true),
)
.description("全屏程序(vim、less、tmux 等)里不高亮。")]),
SettingGroup::new()
.title("预览")
.description("使用当前终端主题")
.title(t!("settings.highlight.options"))
.items([SettingItem::new(
t!("settings.highlight.enabled"),
SettingField::switch(
move |cx| reader.read(cx).settings().terminal_highlight.enabled,
move |enabled, cx| {
writer.update(cx, |store, cx| {
store.update(
|settings| settings.terminal_highlight.enabled = enabled,
cx,
)
});
},
)
.default_value(true),
)
.description(t!("settings.highlight.enabled_description"))]),
SettingGroup::new()
.title(t!("settings.preview"))
.description(t!("settings.highlight.preview_description"))
.items([highlight_preview_item()]),
SettingGroup::new()
.title(format!("规则({count})"))
.description("规则按从上到下应用,勾选「通知」的规则在看不到终端时弹出提醒。")
.items([
SettingItem::render(move |_, _, _| editor.clone()).keywords([
"规则",
"正则",
"关键字",
"高亮",
"颜色",
"备注",
"通知",
"排序",
]),
]),
.title(t!("settings.highlight.rules", count = count))
.description(t!("settings.highlight.rules_description"))
.items([SettingItem::render(move |_, _, _| editor.clone())
.keywords(keywords(t!("settings.highlight.rules_keywords")))]),
]
}
@@ -513,7 +514,7 @@ fn highlight_preview_item() -> SettingItem {
.child(StyledText::new(*line).with_highlights(highlights))
}))
})
.keywords(["预览", "高亮"])
.keywords(keywords(t!("settings.highlight.preview_keywords")))
}
/// 键盘快捷键.
@@ -537,26 +538,26 @@ fn application_groups(panel: &SettingsPanel, _: &App) -> Vec<SettingGroup> {
.default_value(true)
};
vec![
SettingGroup::new().title("窗口").items([
SettingItem::new(
"记住窗口大小",
switch(
|window| window.remember_size,
|window, on| window.remember_size = on,
),
)
.description("下次启动时恢复上次关闭时的窗口大小和最大化状态。"),
SettingItem::new(
"记住窗口位置",
switch(
|window| window.remember_position,
|window, on| window.remember_position = on,
),
)
.description(
"下次启动时把窗口放回上次所在的屏幕和位置;那块屏幕不在时放在主屏幕中间。",
),
]),
SettingGroup::new()
.title(t!("settings.window.title"))
.items([
SettingItem::new(
t!("settings.window.remember_size"),
switch(
|window| window.remember_size,
|window, on| window.remember_size = on,
),
)
.description(t!("settings.window.remember_size_description")),
SettingItem::new(
t!("settings.window.remember_position"),
switch(
|window| window.remember_position,
|window, on| window.remember_position = on,
),
)
.description(t!("settings.window.remember_position_description")),
]),
]
}
@@ -577,9 +578,9 @@ fn external_cli_groups(panel: &SettingsPanel, cx: &App) -> Vec<SettingGroup> {
};
vec![
SettingGroup::new()
.title("访问控制")
.title(t!("settings.cli.access"))
.items([SettingItem::new(
"启用外部 CLI",
t!("settings.cli.enabled"),
SettingField::switch(
move |cx| reader.read(cx).settings().external_cli.enabled,
move |enabled, cx| {
@@ -590,26 +591,18 @@ fn external_cli_groups(panel: &SettingsPanel, cx: &App) -> Vec<SettingGroup> {
)
.default_value(false),
)
.description(
"开启后,本机当前用户下的程序(如 AI Agent)可以通过 shellrs 命令,\
在已保存的主机上执行命令、传输和同步文件,并查看、新建、修改和删除主机与凭据,\
无需知道密码。",
)]),
.description(t!("settings.cli.enabled_description"))]),
SettingGroup::new()
.title("CLI 二进制")
.title(t!("settings.cli.binary"))
.description(if cfg!(windows) {
"把 shellrs 命令加入当前用户的 PATH,方便在终端和 Agent 中直接调用。\
重新打开的终端和 Agent 才能找到它。"
t!("settings.cli.binary_description_windows")
} else {
"把 shellrs 命令加入 PATH,方便在终端和 Agent 中直接调用。"
t!("settings.cli.binary_description")
})
.items([binary_item(bin_link, status.clone())]),
SettingGroup::new()
.title("Agent Skills")
.description(
"安装 shellrs skill,让外部 Agent 在连接服务器、执行远程命令和传输文件时\
使用 shellrs CLI。可安装到通用目录或指定 Agent 的专用目录。",
)
.description(t!("settings.cli.skills_description"))
.items([
skill_actions_item(installable),
skills_item(skill_files, status),
@@ -624,51 +617,52 @@ fn about_groups(panel: &SettingsPanel, cx: &App) -> Vec<SettingGroup> {
let (reader, writer) = (panel.store.clone(), panel.store.clone());
let (analytics_reader, analytics_writer) = (panel.store.clone(), panel.store.clone());
vec![
SettingGroup::new().title("应用更新").items([
current_version_item(snapshot),
SettingItem::new(
"更新渠道",
choice_field(
&panel.store,
|settings| settings.update.channel,
|settings, channel| settings.update.channel = channel,
),
)
.description("稳定版适合日常使用,Beta 可提前接收测试版本。"),
SettingItem::new(
"自动升级",
SettingGroup::new()
.title(t!("settings.about.updates"))
.items([
current_version_item(snapshot),
SettingItem::new(
t!("settings.about.channel"),
choice_field(
&panel.store,
|settings| settings.update.channel,
|settings, channel| settings.update.channel = channel,
),
)
.description(t!("settings.about.channel_description")),
SettingItem::new(
t!("settings.about.automatic"),
SettingField::switch(
move |cx| reader.read(cx).settings().update.automatic,
move |automatic, cx| {
writer.update(cx, |store, cx| {
store.update(|settings| settings.update.automatic = automatic, cx)
});
},
)
.default_value(true),
)
.description(t!("settings.about.automatic_description")),
]),
SettingGroup::new()
.title(t!("settings.about.privacy"))
.items([SettingItem::new(
t!("settings.about.analytics"),
SettingField::switch(
move |cx| reader.read(cx).settings().update.automatic,
move |automatic, cx| {
writer.update(cx, |store, cx| {
store.update(|settings| settings.update.automatic = automatic, cx)
move |cx| analytics_reader.read(cx).settings().analytics.enabled,
move |enabled, cx| {
analytics_writer.update(cx, |store, cx| {
store.update(|settings| settings.analytics.enabled = enabled, cx)
});
},
)
.default_value(true),
)
.description("检测到新版本后自动下载,下载完成后点击「重启并安装」完成更新。"),
]),
SettingGroup::new().title("隐私").items([SettingItem::new(
"发送匿名使用统计",
SettingField::switch(
move |cx| analytics_reader.read(cx).settings().analytics.enabled,
move |enabled, cx| {
analytics_writer.update(cx, |store, cx| {
store.update(|settings| settings.analytics.enabled = enabled, cx)
});
},
)
.default_value(true),
)
.description(
"发送应用版本、操作系统/架构,用于评估安装量、版本分布,用于改进 ShellRS。\
不会发送任何主机、凭据、命令或会话内容。",
)]),
.description(t!("settings.about.analytics_description"))]),
SettingGroup::new()
.title("ShellRS")
.items([SettingItem::new(
"官网",
t!("settings.about.website"),
SettingField::render(|options, _, _| {
Button::new("open-website")
.outline()
@@ -678,7 +672,7 @@ fn about_groups(panel: &SettingsPanel, cx: &App) -> Vec<SettingGroup> {
.on_click(|_, window, cx| window.dispatch_action(Box::new(OpenWebsite), cx))
}),
)
.description("功能介绍、下载和更新内容。")]),
.description(t!("settings.about.website_description"))]),
]
}
@@ -697,18 +691,22 @@ fn current_version_item(snapshot: UpdateSnapshot) -> SettingItem {
let build = SharedString::from(build_description());
let step = snapshot.step().map(|step| -> (_, _, Box<dyn Action>) {
match step {
UpdateStep::Restart => ("show-update", "重启并安装…", Box::new(ShowUpdate)),
UpdateStep::Restart => (
"show-update",
t!("settings.about.restart"),
Box::new(ShowUpdate),
),
UpdateStep::DownloadPage => (
"open-download-page",
"前往下载页",
t!("settings.about.download_page"),
Box::new(OpenDownloadPage),
),
UpdateStep::Download { retry } => (
"download-update",
if retry {
"重试下载"
t!("settings.about.retry_download")
} else {
"下载更新"
t!("settings.about.download")
},
Box::new(DownloadUpdate),
),
@@ -729,7 +727,7 @@ fn current_version_item(snapshot: UpdateSnapshot) -> SettingItem {
.outline()
.with_size(options.size())
.icon(Icon::new(CatalogIcon::RefreshCw))
.label("检查更新")
.label(t!("settings.about.check"))
.loading(snapshot.phase == Phase::Checking)
.disabled(!snapshot.can_check())
.on_click(|_, window, cx| window.dispatch_action(Box::new(CheckForUpdates), cx))
@@ -753,7 +751,7 @@ fn current_version_item(snapshot: UpdateSnapshot) -> SettingItem {
text.flex_1().max_w_3_5()
}
})
.child(Label::new("当前版本").text_sm())
.child(Label::new(t!("settings.about.version")).text_sm())
.child(
div()
.id("update-status")
@@ -784,7 +782,7 @@ fn current_version_item(snapshot: UpdateSnapshot) -> SettingItem {
.children(button),
)
})
.keywords(["当前版本", "检查更新", "更新", "升级", "版本", "下载"])
.keywords(keywords(t!("settings.about.version_keywords")))
}
/// The version's tooltip: which build this is, and for which system.
@@ -792,9 +790,14 @@ fn build_description() -> String {
let build = match (build_info::COMMIT, build_info::BUILD_DATE) {
(Some(commit), Some(date)) => format!("{} · {date}", commit.get(..7).unwrap_or(commit)),
(Some(commit), None) => commit.get(..7).unwrap_or(commit).to_string(),
_ => "本机构建".into(),
_ => t!("settings.about.local_build").to_string(),
};
format!("{build}({})", platform::platform_label())
t!(
"settings.about.build",
build = build,
platform = platform::platform_label()
)
.to_string()
}
/// Where the `shellrs` command stands, with the one thing to do about it.
@@ -803,46 +806,51 @@ fn binary_item(bin_link: Option<String>, status: Option<IntegrationStatus>) -> S
let binary = status.as_ref().map(|status| &status.binary);
let (line, action) = match (&bin_link, binary) {
(None, _) => (
RowStatus::Plain("此系统暂不支持安装 shellrs 命令".into()),
RowStatus::Plain(t!("settings.cli.binary_unsupported").into()),
None,
),
(Some(_), None) => (RowStatus::Plain("正在检查…".into()), None),
(Some(path), Some(BinaryStatus::Installed)) => {
(RowStatus::Installed(path.clone()), Some(("移除", false)))
}
(Some(_), None) => (RowStatus::Plain(t!("settings.cli.checking").into()), None),
(Some(path), Some(BinaryStatus::Installed)) => (
RowStatus::Installed(path.clone()),
Some((t!("settings.cli.remove"), false)),
),
(Some(path), Some(BinaryStatus::Missing)) => (
RowStatus::Plain(format!("未安装({path})")),
Some(("安装", true)),
RowStatus::Plain(t!("settings.cli.missing", path = path).into()),
Some((t!("settings.cli.install"), true)),
),
(Some(path), Some(BinaryStatus::Stale { target })) => (
RowStatus::Warning(format!(
"{path} 指向 {},已失效或不是这个 ShellRS",
target.display()
)),
Some(("重新安装", true)),
RowStatus::Warning(
t!(
"settings.cli.binary_stale",
path = path,
target = target.display()
)
.into(),
),
Some((t!("settings.cli.reinstall"), true)),
),
(Some(path), Some(BinaryStatus::Occupied { target })) => (
RowStatus::Warning(match target {
Some(target) => {
format!("{path} 已被其他程序占用(指向 {})", target.display())
}
None => format!("{path} 已被其他程序占用"),
Some(target) => t!(
"settings.cli.binary_occupied_by",
path = path,
target = target.display()
)
.into(),
None => t!("settings.cli.binary_occupied", path = path).into(),
}),
None,
),
(Some(path), Some(BinaryStatus::Outdated)) => (
RowStatus::Warning(format!("已安装旧版本({path})")),
Some(("更新", true)),
RowStatus::Warning(t!("settings.cli.outdated", path = path).into()),
Some((t!("settings.cli.update"), true)),
),
(Some(path), Some(BinaryStatus::NotOnPath)) => (
RowStatus::Warning(format!("{path} 所在的文件夹不在 PATH 中")),
Some(("安装", true)),
RowStatus::Warning(t!("settings.cli.binary_not_on_path", path = path).into()),
Some((t!("settings.cli.install"), true)),
),
(Some(_), Some(BinaryStatus::Unavailable)) => (
RowStatus::Warning(
"找不到 shellrs-cli.exe:它应在 ShellRS 程序旁边(开发时请先 cargo build)"
.into(),
),
RowStatus::Warning(t!("settings.cli.binary_unavailable").into()),
None,
),
};
@@ -855,7 +863,7 @@ fn binary_item(bin_link: Option<String>, status: Option<IntegrationStatus>) -> S
v_flex()
.gap_1()
.min_w_0()
.child(div().text_sm().child("状态:"))
.child(div().text_sm().child(t!("settings.cli.status")))
.child(line.render("cli-binary-status", cx)),
)
.children(action.map(|(label, install)| {
@@ -868,7 +876,7 @@ fn binary_item(bin_link: Option<String>, status: Option<IntegrationStatus>) -> S
})
}))
})
.keywords(["CLI", "PATH", "shellrs", "命令", "安装"])
.keywords(keywords(t!("settings.cli.binary_keywords")))
}
/// 检查状态 and 复制 skills, above the agents.
@@ -881,7 +889,7 @@ fn skill_actions_item(installable: bool) -> SettingItem {
.outline()
.small()
.icon(Icon::new(CatalogIcon::RefreshCw))
.label("检查状态")
.label(t!("settings.cli.check"))
.disabled(!installable)
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(RefreshCliIntegration), cx)
@@ -892,11 +900,11 @@ fn skill_actions_item(installable: bool) -> SettingItem {
.outline()
.small()
.icon(Icon::new(IconName::Copy))
.label("复制 skills")
.label(t!("settings.cli.copy_skills"))
.on_click(|_, window, cx| window.dispatch_action(Box::new(CopyAgentSkill), cx)),
)
})
.keywords(["skill", "Agent", "检查", "复制"])
.keywords(keywords(t!("settings.cli.skills_keywords")))
}
/// One row per agent, each with where its skill goes and whether it is
@@ -928,18 +936,22 @@ fn skill_row(
cx: &App,
) -> impl IntoElement {
let (line, action) = match (&file, status) {
(None, _) => (RowStatus::Plain("此系统暂不支持安装".into()), None),
(Some(_), None) => (RowStatus::Plain("正在检查…".into()), None),
(Some(file), Some(SkillStatus::Installed)) => {
(RowStatus::Installed(file.clone()), Some(("移除", false)))
}
(None, _) => (
RowStatus::Plain(t!("settings.cli.skill_unsupported").into()),
None,
),
(Some(_), None) => (RowStatus::Plain(t!("settings.cli.checking").into()), None),
(Some(file), Some(SkillStatus::Installed)) => (
RowStatus::Installed(file.clone()),
Some((t!("settings.cli.remove"), false)),
),
(Some(file), Some(SkillStatus::Missing)) => (
RowStatus::Plain(format!("未安装({file})")),
Some(("安装", true)),
RowStatus::Plain(t!("settings.cli.missing", path = file).into()),
Some((t!("settings.cli.install"), true)),
),
(Some(file), Some(SkillStatus::Outdated)) => (
RowStatus::Warning(format!("已安装旧版本({file})")),
Some(("更新", true)),
RowStatus::Warning(t!("settings.cli.outdated", path = file).into()),
Some((t!("settings.cli.update"), true)),
),
};
h_flex()
@@ -981,7 +993,7 @@ fn skill_row(
/// Installing is the step the row asks for, so it is the primary button;
/// removing is not.
fn row_button(id: impl Into<ElementId>, label: &'static str, install: bool) -> Button {
fn row_button(id: impl Into<ElementId>, label: SharedString, install: bool) -> Button {
let button = Button::new(id).small().label(label);
if install {
button.primary()
@@ -1004,13 +1016,13 @@ impl RowStatus {
let line = div().id(id).test_support().text_sm();
match self {
RowStatus::Installed(path) => line
.aria_label(format!("已安装于 {path}"))
.aria_label(t!("settings.cli.installed_at_path", path = path))
.text_color(cx.theme().success)
.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::Check).small())
.child("已安装于")
.child(t!("settings.cli.installed_at"))
.child(
div()
.font_family(cx.theme().mono_font_family.clone())
@@ -1043,7 +1055,7 @@ fn font_family_field(panel: &SettingsPanel, cx: &App) -> SettingField<SharedStri
.into_iter()
.map(|family| {
let label = if family == default_family {
format!("{family}(默认)").into()
t!("settings.default_option", value = family)
} else {
family.clone()
};
@@ -1089,7 +1101,7 @@ fn font_size_field(store: &Entity<SettingsStore>) -> SettingField<SharedString>
.map(|size| {
let key = SharedString::from(size.to_string());
let label = if size as f32 == default {
format!("{size}(默认)").into()
t!("settings.default_option", value = size)
} else {
key.clone()
};
@@ -1146,6 +1158,16 @@ fn number_field(
)
}
/// A row's search words, from a translation that lists them separated by
/// commas: they are in the interface language like everything the search
/// matches.
pub(super) fn keywords(words: SharedString) -> Vec<SharedString> {
words
.split(',')
.map(|word| SharedString::from(word.trim().to_string()))
.collect()
}
/// A dropdown over one `Choice` of the settings. Its reset goes back to the
/// default, which is what the page's 「重置全部」 uses.
fn choice_field<T: Choice>(
+11 -6
View File
@@ -16,6 +16,7 @@ use crate::app::{
CatalogIcon, Shortcut, ShortcutGroup, ShortcutOverrides, check, fixed_bindings, key_caps,
key_text,
};
use crate::i18n::t;
use super::SettingsStore;
@@ -206,7 +207,7 @@ fn render_field(
.bg(theme.background)
.text_sm()
.text_color(theme.muted_foreground)
.child("按下组合键")
.child(t!("settings.shortcuts.recording"))
.into_any_element()
} else {
let start = editor.clone();
@@ -214,12 +215,12 @@ fn render_field(
let spoken = keys
.as_deref()
.map(|keys| key_text(keys, shortcut.is_numbered()))
.unwrap_or_else(|| "无".into());
.unwrap_or_else(|| t!("settings.shortcuts.none").to_string());
Button::new(element_id("keys", shortcut))
.ghost()
.with_size(size)
.accessibility_label(spoken)
.tooltip("修改快捷键")
.tooltip(t!("settings.shortcuts.change"))
.child(key_caps_element(
keys.as_deref(),
shortcut.is_numbered(),
@@ -241,7 +242,11 @@ fn render_field(
.with_size(size)
.icon(Icon::new(CatalogIcon::Ban))
.selected(disabled)
.tooltip(if disabled { "启用" } else { "禁用" })
.tooltip(if disabled {
t!("settings.shortcuts.enable")
} else {
t!("settings.shortcuts.disable")
})
.on_click(move |_, _, cx| {
toggle.update(cx, |editor, cx| {
editor.change(
@@ -262,7 +267,7 @@ fn render_field(
.ghost()
.with_size(size)
.icon(Icon::new(CatalogIcon::RotateCcw))
.tooltip("恢复默认")
.tooltip(t!("settings.shortcuts.reset"))
.disabled(is_default)
.on_click(move |_, _, cx| {
reset.update(cx, |editor, cx| {
@@ -280,7 +285,7 @@ fn key_caps_element(keys: Option<&str>, numbered: bool, cx: &App) -> AnyElement
return div()
.text_sm()
.text_color(theme.muted_foreground)
.child("无")
.child(t!("settings.shortcuts.none"))
.into_any_element();
};
let caps = key_caps(keys, numbered);
+8 -6
View File
@@ -14,8 +14,10 @@ use gpui_kit::component::{
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::*;
use crate::i18n::t;
use crate::terminal::{TerminalFont, TerminalTheme};
use super::settings_panel::keywords;
use super::{SettingsStore, TerminalThemeSettings};
/// How tall each column's list is, in rems: about three cards, the third
@@ -54,9 +56,9 @@ pub(super) fn terminal_theme_item(store: &Entity<SettingsStore>) -> SettingItem
.child(column(ThemeMode::Dark, &chosen, &reader, cx))
})
.keywords(
["主题", "终端主题", "配色", "颜色", "浅色", "深色"]
keywords(t!("settings.theme.keywords"))
.into_iter()
.chain(TerminalTheme::all().iter().map(TerminalTheme::name)),
.chain(TerminalTheme::all().iter().map(|theme| theme.name().into())),
)
.on_reset(
move |cx| !dirty.read(cx).settings().terminal_theme.is_default_choice(),
@@ -73,7 +75,7 @@ pub(super) fn terminal_theme_item(store: &Entity<SettingsStore>) -> SettingItem
pub(super) fn app_follows_item(store: &Entity<SettingsStore>) -> SettingItem {
let (reader, writer) = (store.clone(), store.clone());
SettingItem::new(
"界面跟随主题",
t!("settings.theme.app_follows"),
SettingField::switch(
move |cx| reader.read(cx).settings().terminal_theme.app_follows,
move |on, cx| {
@@ -84,7 +86,7 @@ pub(super) fn app_follows_item(store: &Entity<SettingsStore>) -> SettingItem {
)
.default_value(false),
)
.description("关闭时界面用中性的黑白灰,只有终端按所选主题配色。")
.description(t!("settings.theme.app_follows_description"))
}
/// One appearance's themes under its name, scrolling on their own.
@@ -95,8 +97,8 @@ fn column(
cx: &App,
) -> impl IntoElement {
let title = match mode {
ThemeMode::Light => "浅色",
ThemeMode::Dark => "深色",
ThemeMode::Light => t!("settings.appearance.light"),
ThemeMode::Dark => t!("settings.appearance.dark"),
};
let chosen = chosen.theme(mode);
let themes: Vec<_> = TerminalTheme::for_mode(mode).collect();
+16 -9
View File
@@ -13,6 +13,7 @@ use crate::app::{
};
use crate::credential::{CredentialDialog, open_credential_dialog};
use crate::host::{AuthKind, CredentialKind, read_public_key};
use crate::i18n::{t, tn};
use crate::shared::confirm_delete;
use super::{sidebar::SidebarMode, workspace_view::Workspace};
@@ -91,10 +92,18 @@ impl Workspace {
cx.update(|window, cx| match line {
Some(line) => {
cx.write_to_clipboard(ClipboardItem::new_string(line));
window.push_notification(Notification::success("已复制公钥"), cx);
window.push_notification(
Notification::success(t!("workspace.credential.public_key_copied")),
cx,
);
}
None => window
.push_notification(Notification::error(format!("无法从 {path} 读出公钥")), cx),
None => window.push_notification(
Notification::error(t!(
"workspace.credential.public_key_unreadable",
path = path
)),
cx,
),
})
.ok();
})
@@ -136,12 +145,10 @@ impl Workspace {
/// What the delete dialog says happens to the hosts using the credential.
/// `None` when no host uses it.
fn describe_credential_delete(hosts: usize, kind: CredentialKind) -> Option<SharedString> {
let after = match kind.without_credential() {
AuthKind::Password => "改为「密码」,连接时询问密码",
AuthKind::NoPassword => "改为「无密码」,用 SSH Agent 和 ~/.ssh 中的私钥登录",
};
(hosts > 0)
.then(|| format!("有 {hosts} 台主机正在使用此凭据。删除后它们{after},用户名不变。").into())
(hosts > 0).then(|| match kind.without_credential() {
AuthKind::Password => tn!("workspace.credential.in_use_then_password", hosts),
AuthKind::NoPassword => tn!("workspace.credential.in_use_then_no_password", hosts),
})
}
#[cfg(test)]
+16 -9
View File
@@ -12,6 +12,7 @@ use crate::analytics::Counter;
use crate::app::{CenterTab, CloseEditor, EditorAction, EditorCommand, EditorShortcut};
use crate::editor::{EditorId, EditorKey, EditorPanel, EditorPanelEvent, EditorSource};
use crate::explorer::{ExplorerId, FileLocation};
use crate::i18n::{t, tn};
use crate::sftp::TextFile;
use crate::shared::confirm_danger;
@@ -255,9 +256,12 @@ impl Workspace {
// A hidden window (closed on macOS) would ask where nobody sees it.
crate::app::bring_forward(window, cx);
confirm_danger(
"退出 ShellRS?".into(),
Some(format!("{}有未保存的修改,退出后会丢失。", describe_files(&unsaved)).into()),
"放弃修改并退出",
t!("workspace.unsaved.quit_title"),
Some(t!(
"workspace.unsaved.quit",
files = describe_files(&unsaved)
)),
t!("workspace.unsaved.discard_and_quit"),
Rc::new(|_, cx| cx.quit()),
window,
cx,
@@ -291,9 +295,12 @@ impl Workspace {
}
let this = cx.weak_entity();
confirm_danger(
format!("关闭 {} 个标签?", tabs.len()).into(),
Some(format!("{}有未保存的修改,关闭后会丢失。", describe_files(&unsaved)).into()),
"放弃修改并关闭",
tn!("workspace.unsaved.close_tabs_title", tabs.len()),
Some(t!(
"workspace.unsaved.close_tabs",
files = describe_files(&unsaved)
)),
t!("workspace.unsaved.discard_and_close"),
Rc::new(move |window, cx| {
let tabs = tabs.clone();
let _ = this.update(cx, |this, cx| {
@@ -317,9 +324,9 @@ impl Workspace {
pub(super) fn describe_files(names: &[String]) -> String {
match names {
[] => String::new(),
[one] => format!("“{one}”"),
[one, two] => format!("“{one}”和“{two}”"),
[one, ..] => format!("“{one}”等 {} 个文件", names.len()),
[one] => t!("workspace.files.single", name = one).to_string(),
[one, two] => t!("workspace.files.pair", first = one, second = two).to_string(),
[one, ..] => t!("workspace.files.several", first = one, count = names.len()).to_string(),
}
}
+13 -8
View File
@@ -11,6 +11,7 @@ use crate::app::{
};
use crate::forward::{ForwardManagerEvent, open_forward_dialog};
use crate::host::ForwardId;
use crate::i18n::t;
use crate::shared::confirm_delete;
use super::{
@@ -72,7 +73,7 @@ impl Workspace {
let store = self.store.read(cx);
if store.hosts().is_empty() {
window.push_notification(
Notification::warning("端口转发经由一台主机的 SSH 连接,请先新建主机。"),
Notification::warning(t!("workspace.forward.needs_host")),
cx,
);
return;
@@ -108,7 +109,7 @@ impl Workspace {
.forwards
.read(cx)
.is_active(id)
.then(|| SharedString::from("这条转发正在运行,会先停止。"));
.then(|| t!("workspace.forward.delete_running"));
let store = self.store.clone();
confirm_delete(
&title,
@@ -180,7 +181,8 @@ impl Workspace {
.map(|rule| rule.title())
.unwrap_or_default();
window.push_notification(
Notification::error(reason.clone()).title(format!("端口转发“{title}”已停止")),
Notification::error(reason.clone())
.title(t!("workspace.forward.stopped", title = title)),
cx,
);
}
@@ -215,10 +217,13 @@ impl Workspace {
let store = self.store.read(cx);
let rule = store.forward(id)?;
let host = store.host(rule.host)?;
Some(format!(
"端口转发“{}”正在通过 {} 连接。",
rule.title(),
host.name
))
Some(
t!(
"workspace.forward.prompt_origin",
title = rule.title(),
host = host.name
)
.to_string(),
)
}
}
+5 -3
View File
@@ -8,6 +8,7 @@ use gpui_kit::*;
use crate::cli::OpenLink;
use crate::host::{HostId, LinkKind, SshLink};
use crate::i18n::t;
use super::workspace_view::Workspace;
@@ -34,9 +35,10 @@ impl Workspace {
});
}
}
Err(problem) => {
window.push_notification(Notification::error(problem).title("无法打开链接"), cx)
}
Err(problem) => window.push_notification(
Notification::error(problem).title(t!("workspace.link.open_failed")),
cx,
),
}
}
+13 -3
View File
@@ -11,6 +11,7 @@ use gpui_kit::*;
use super::Workspace;
use crate::analytics::Counter;
use crate::app::CenterTab;
use crate::i18n::t;
use crate::settings::NotificationSettings;
use crate::shared::RenamableTab as _;
use crate::terminal::{LocalTerminalId, RemoteTerminalId, TerminalNotice};
@@ -66,10 +67,19 @@ pub(super) fn notice_text(tab: &str, notice: &TerminalNotice) -> (String, String
TerminalNotice::Program {
title: Some(title),
body,
} => (format!("{tab}:{title}"), body.clone()),
} => (
t!("workspace.notice.titled", tab = tab, title = title).to_string(),
body.clone(),
),
TerminalNotice::Program { title: None, body } => (tab.to_string(), body.clone()),
TerminalNotice::Bell { line } => (format!("{tab}:响铃"), line.clone()),
TerminalNotice::Keyword { rule, line } => (format!("{tab}:{rule}"), line.clone()),
TerminalNotice::Bell { line } => (
t!("workspace.notice.bell", tab = tab).to_string(),
line.clone(),
),
TerminalNotice::Keyword { rule, line } => (
t!("workspace.notice.titled", tab = tab, title = rule).to_string(),
line.clone(),
),
}
}
+7 -6
View File
@@ -16,6 +16,7 @@ use crate::app::{
CatalogIcon, ConnectHost, ConnectSelected, NewHost, QuickConnect, RECENT_HOSTS_CONTEXT,
};
use crate::host::{HostId, HostOs, HostStore, host_menu};
use crate::i18n::t;
use crate::shared::HostMark;
/// The center's empty state: shown in place of the tabs while none is open.
@@ -100,13 +101,13 @@ impl RecentHosts {
div()
.text_lg()
.font_weight(FontWeight::MEDIUM)
.child("最近连接"),
.child(t!("workspace.recent.title")),
)
.child(
div()
.text_sm()
.text_color(cx.theme().muted_foreground)
.child("双击主机连接,或新建一台主机"),
.child(t!("workspace.recent.hint")),
),
)
.child(
@@ -117,7 +118,7 @@ impl RecentHosts {
.primary()
.small()
.icon(IconName::Search)
.label("快速连接")
.label(t!("workspace.recent.find_host"))
.on_click({
let target = target.clone();
move |_, window, cx| {
@@ -129,7 +130,7 @@ impl RecentHosts {
Button::new("recent-new-host")
.small()
.icon(IconName::Plus)
.label("新建主机…")
.label(t!("workspace.recent.new_host"))
.on_click(move |_, window, cx| {
target.dispatch_action(&NewHost, window, cx)
}),
@@ -145,7 +146,7 @@ impl RecentHosts {
.py_8()
.text_color(muted)
.child(Icon::new(CatalogIcon::Server).large())
.child(div().text_sm().child("还没有连接过的主机"))
.child(div().text_sm().child(t!("workspace.recent.empty")))
}
fn on_connect_selected(
@@ -233,7 +234,7 @@ impl RecentHosts {
.xsmall()
.text_color(cx.theme().success),
)
.child("已连接")
.child(t!("workspace.recent.connected"))
})
})
// A right click selects the row, as in Finder and Explorer, so
+7 -2
View File
@@ -10,6 +10,7 @@ use crate::app::{CatalogIcon, OpenSettings};
use crate::credential::CredentialPanel;
use crate::forward::ForwardPanel;
use crate::host::HostPanel;
use crate::i18n::t;
/// Which list the left dock is showing.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@@ -164,8 +165,12 @@ impl Render for Sidebar {
.ghost()
.small()
.icon(Icon::new(CatalogIcon::Settings))
.label("设置")
.tooltip_with_action("打开设置", &OpenSettings, None)
.label(t!("workspace.sidebar.settings"))
.tooltip_with_action(
t!("workspace.sidebar.open_settings"),
&OpenSettings,
None,
)
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(OpenSettings), cx)
}),
+3 -2
View File
@@ -9,6 +9,7 @@ use crate::app::{
DeleteSnippet, DeleteSnippetCategory, EditSnippet, NewSnippet, NewSnippetCategory,
NewSnippetIn, RenameSnippetCategory, ToggleSnippetCategory,
};
use crate::i18n::tn;
use crate::shared::confirm_delete;
use crate::snippets::{CategoryDialog, SnippetDialog, open_category_dialog, open_snippet_dialog};
@@ -124,8 +125,8 @@ impl Workspace {
return;
};
let snippets = store.snippets_in(id);
let description = (snippets > 0)
.then(|| SharedString::from(format!("分类里的 {snippets} 个片段会一起删除。")));
let description =
(snippets > 0).then(|| tn!("workspace.snippets.delete_category", snippets));
let store = self.store.clone();
confirm_delete(
&name,
+35 -12
View File
@@ -8,6 +8,7 @@ use gpui_kit::*;
use crate::app::CatalogIcon;
use crate::explorer::ExplorerStatus;
use crate::host::{ConnectionState, Host};
use crate::i18n::t;
use crate::terminal::{TerminalLifecycle, TerminalStatus};
/// The active connection/process state on the left and terminal facts on the
@@ -46,6 +47,15 @@ fn state_icon(state: ConnectionState, cx: &App) -> Icon {
}
}
/// 「已连接 web-01」: where the connection to `name` stands.
fn connection_text(state: ConnectionState, name: &str) -> SharedString {
match state {
ConnectionState::Disconnected => t!("workspace.status.disconnected_from", name = name),
ConnectionState::Connecting => t!("workspace.status.connecting_to", name = name),
ConnectionState::Connected => t!("workspace.status.connected_to", name = name),
}
}
impl RenderOnce for WorkspaceStatus {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let muted = cx.theme().muted_foreground;
@@ -74,14 +84,14 @@ impl RenderOnce for WorkspaceStatus {
.as_ref()
.map_or_else(|| "SFTP".into(), |host| host.name.clone());
(
format!("{} {name}", state.label()).into(),
connection_text(state, &name),
state_icon(state, cx),
host.map(|host| host.endpoint()),
None,
)
}
None => (
"本机文件".into(),
t!("workspace.status.local_file"),
Icon::new(CatalogIcon::Laptop).text_color(muted),
None,
None,
@@ -92,14 +102,14 @@ impl RenderOnce for WorkspaceStatus {
Some(host) => {
alarming = host.state == ConnectionState::Disconnected;
(
format!("{} {}", host.state.label(), host.name).into(),
connection_text(host.state, &host.name),
state_icon(host.state, cx),
Some(host.endpoint()),
terminal,
)
}
None => (
ConnectionState::Disconnected.label().into(),
t!("workspace.status.not_connected"),
Icon::new(CatalogIcon::Unplug).text_color(muted),
None,
terminal,
@@ -109,17 +119,20 @@ impl RenderOnce for WorkspaceStatus {
let name = host
.as_ref()
.map_or_else(|| "SFTP".into(), |host| host.name.clone());
let state = format!("{} {name}", status.state.label());
let state = connection_text(status.state, &name);
let (text, icon) = match (status.state, status.problem) {
(ConnectionState::Disconnected, Some(problem)) => {
alarming = true;
(format!("{state}:{problem}"), state_icon(status.state, cx))
(
t!("workspace.status.problem", state = state, problem = problem),
state_icon(status.state, cx),
)
}
// Connected, but a directory could not be read.
(ConnectionState::Connected, Some(problem)) => {
alarming = true;
(
problem.to_string(),
problem,
Icon::new(IconName::CircleX).text_color(cx.theme().danger),
)
}
@@ -128,7 +141,7 @@ impl RenderOnce for WorkspaceStatus {
(state, state_icon(now, cx))
}
};
(text.into(), icon, host.map(|host| host.endpoint()), None)
(text, icon, host.map(|host| host.endpoint()), None)
}
WorkspaceStatus::Local(status) => {
let lifecycle = status.lifecycle();
@@ -146,7 +159,13 @@ impl RenderOnce for WorkspaceStatus {
}
};
(
format!("{} 本地终端", lifecycle.label()).into(),
match lifecycle {
TerminalLifecycle::Starting => t!("workspace.status.local_starting"),
TerminalLifecycle::Running => t!("workspace.status.local_running"),
TerminalLifecycle::Exited { .. } => t!("workspace.status.local_exited"),
TerminalLifecycle::Failed(_) => t!("workspace.status.local_failed"),
TerminalLifecycle::Closing => t!("workspace.status.local_closing"),
},
icon,
None,
Some(status),
@@ -190,14 +209,18 @@ impl RenderOnce for WorkspaceStatus {
.child(format),
),
None => bar
.right("编码 UTF-8")
.right(t!("workspace.status.encoding"))
.right(Separator::vertical())
.right("终端 xterm-256color"),
.right(t!("workspace.status.terminal_type")),
})
.when_some(terminal, |bar, terminal| {
let (columns, rows) = (terminal.columns(), terminal.rows());
let size = format!("{columns}×{rows}");
let explained = format!("终端尺寸:{columns} 列,{rows} 行");
let explained = t!(
"workspace.status.terminal_size",
columns = columns,
rows = rows
);
bar.right(Separator::vertical()).right(
div()
.id("status-terminal-size")
+27 -19
View File
@@ -10,6 +10,7 @@ use crate::app::{
CatalogIcon, NewLocalTerminal, NewTemporaryConnection, ShowCredentials, ShowForwards,
ShowHosts, ShowUpdate, ToggleHostPanel, ToggleTheme,
};
use crate::i18n::t;
use crate::shared::tinted;
use crate::update::UpdateBadge;
@@ -62,31 +63,34 @@ pub fn render_title_bar(
.child(
Button::new("show-hosts")
.icon(CatalogIcon::Server)
.tooltip("主机")
.accessibility_label("主机")
.tooltip(t!("workspace.title_bar.hosts"))
.accessibility_label(t!("workspace.title_bar.hosts"))
.selected(sidebar == Some(SidebarMode::Hosts)),
)
.child(
Button::new("show-forwards")
.icon(CatalogIcon::ArrowLeftRight)
.tooltip("端口转发")
.tooltip(t!("workspace.title_bar.forwards"))
.selected(sidebar == Some(SidebarMode::Forwards))
// Forwards run whether or not their list
// is showing, so the count is said here.
.map(|button| match forwards {
0 => button.accessibility_label("端口转发"),
running => {
button.label(running.to_string()).accessibility_label(
format!("端口转发,{running} 条运行中"),
)
}
0 => button.accessibility_label(t!(
"workspace.title_bar.forwards"
)),
running => button
.label(running.to_string())
.accessibility_label(t!(
"workspace.title_bar.forwards_running",
count = running
)),
}),
)
.child(
Button::new("show-credentials")
.icon(CatalogIcon::KeyRound)
.tooltip("凭据")
.accessibility_label("凭据")
.tooltip(t!("workspace.title_bar.credentials"))
.accessibility_label(t!("workspace.title_bar.credentials"))
.selected(sidebar == Some(SidebarMode::Credentials)),
)
.on_click(move |picked: &Vec<usize>, window, cx| {
@@ -112,9 +116,9 @@ pub fn render_title_bar(
.small()
.icon(IconName::PanelLeft)
.tooltip(if sidebar.is_some() {
"隐藏侧栏"
t!("workspace.title_bar.hide_sidebar")
} else {
"显示侧栏"
t!("workspace.title_bar.show_sidebar")
})
.on_click(move |_, window, cx| {
toggle.dispatch_action(&ToggleHostPanel, window, cx)
@@ -127,8 +131,8 @@ pub fn render_title_bar(
.ghost()
.small()
.icon(CatalogIcon::Plug)
.label("临时连接…")
.tooltip("连接一台主机,不保存")
.label(t!("workspace.title_bar.quick_connect"))
.tooltip(t!("workspace.title_bar.quick_connect_tooltip"))
.on_click(move |_, window, cx| {
temporary.dispatch_action(&NewTemporaryConnection, window, cx)
}),
@@ -138,8 +142,12 @@ pub fn render_title_bar(
.ghost()
.small()
.icon(CatalogIcon::Terminal)
.label("本地终端")
.tooltip_with_action("新建本地终端", &NewLocalTerminal, None)
.label(t!("workspace.title_bar.local_terminal"))
.tooltip_with_action(
t!("workspace.title_bar.new_local_terminal"),
&NewLocalTerminal,
None,
)
.on_click(move |_, window, cx| {
new_local.dispatch_action(&NewLocalTerminal, window, cx)
}),
@@ -170,9 +178,9 @@ pub fn render_title_bar(
.small()
.icon(if dark { IconName::Sun } else { IconName::Moon })
.tooltip(if dark {
"切换为浅色"
t!("workspace.title_bar.to_light")
} else {
"切换为深色"
t!("workspace.title_bar.to_dark")
})
.on_click(move |_, window, cx| {
theme.dispatch_action(&ToggleTheme, window, cx)
+5 -5
View File
@@ -11,6 +11,7 @@ use crate::app::{
ShowUpdate,
};
use crate::host::ConnectionState;
use crate::i18n::t;
use crate::terminal::TerminalLifecycle;
use crate::update::build_info::{CHANGELOG_PAGE, WEBSITE};
use crate::update::{RestartImpact, UpdaterEvent, open_update_dialog};
@@ -114,14 +115,13 @@ impl Workspace {
completed: true,
..
} => window.push_notification(
Notification::success(format!("ShellRS 已更新到 {to}。")).title("更新完成"),
Notification::success(t!("workspace.update.done", version = to))
.title(t!("workspace.update.done_title")),
cx,
),
UpdaterEvent::Updated { from, to, .. } => window.push_notification(
Notification::error(format!(
"没有装上 {to},ShellRS 仍是 {from}。可以到官网下载安装。"
))
.title("更新没有完成"),
Notification::error(t!("workspace.update.failed", to = to, from = from))
.title(t!("workspace.update.failed_title")),
cx,
),
}
+1 -1
View File
@@ -121,7 +121,7 @@ impl Workspace {
/// Saving the window's place is a convenience: a failure is only logged.
fn write(state: &WindowState, path: &Path) {
if let Err(error) = state.save(path) {
eprintln!("shellrs: 无法保存窗口位置:{error}");
eprintln!("shellrs: cannot save the window's place: {error}");
}
}
+58 -34
View File
@@ -55,6 +55,7 @@ use crate::host::{
HostStoreEvent, confirm_delete_group, confirm_delete_host, open_group_dialog, open_host_dialog,
open_quick_connect_dialog, open_temporary_connection_dialog,
};
use crate::i18n::t;
use crate::settings::{
Appearance, SettingsPanel, SettingsPanelEvent, SettingsStore, SettingsStoreEvent,
WindowSettings,
@@ -260,7 +261,7 @@ impl Workspace {
this.serve_cli(window, cx);
}
Err(error) => notify_once_open(
Notification::error(error.to_string()).title("外部 CLI 无法启动"),
Notification::error(error.to_string()).title(t!("workspace.cli.start_failed")),
window,
cx,
),
@@ -759,12 +760,12 @@ impl Workspace {
let description = introduced(prompt.description());
window.open_alert_dialog(cx, move |alert, _, _| {
alert
.title("首次连接此主机")
.title(t!("workspace.host_key.unknown_title"))
.description(description.clone())
.button_props(
DialogButtonProps::default()
.ok_text("信任并连接")
.cancel_text("取消"),
.ok_text(t!("workspace.host_key.trust"))
.cancel_text(t!("common.cancel")),
)
.show_cancel(true)
.on_ok({
@@ -788,26 +789,39 @@ impl Workspace {
});
}
ConnectionPromptKind::HostKeyChanged(prompt) => {
let old = prompt.old_fingerprints().join("、");
let jump_host = prompt
.jump_host()
.map(|name| format!("跳板主机:{name}\n"))
.unwrap_or_default();
let description = introduced(format!(
"{jump_host}主机:{}:{}\n算法:{}\n已保存指纹:{old}\n服务器当前指纹:{}\n\n连接已阻断。请核验服务器身份后手动处理:{}",
prompt.host(),
prompt.port(),
prompt.algorithm(),
prompt.fingerprint(),
prompt.known_hosts_path().display(),
));
let old = prompt.old_fingerprints().join(&t!("common.list.separator"));
let path = prompt.known_hosts_path().display().to_string();
let description = introduced(
match prompt.jump_host() {
Some(jump_host) => t!(
"workspace.host_key.changed_via_jump_host",
jump_host = jump_host,
host = prompt.host(),
port = prompt.port(),
algorithm = prompt.algorithm(),
old = old,
fingerprint = prompt.fingerprint(),
path = path
),
None => t!(
"workspace.host_key.changed",
host = prompt.host(),
port = prompt.port(),
algorithm = prompt.algorithm(),
old = old,
fingerprint = prompt.fingerprint(),
path = path
),
}
.to_string(),
);
window.open_alert_dialog(cx, move |alert, _, _| {
alert
.title("服务器主机密钥已变更")
.title(t!("workspace.host_key.changed_title"))
.description(description.clone())
.button_props(
DialogButtonProps::default()
.ok_text("关闭")
.ok_text(t!("common.close"))
.ok_variant(ButtonVariant::Danger),
)
.on_ok({
@@ -828,7 +842,7 @@ impl Workspace {
let instructions = introduced(prompt.instructions().to_string());
let fields = prompt.fields().to_vec();
let dialog_title: SharedString = if title.trim().is_empty() {
"SSH 认证".into()
t!("workspace.auth.title")
} else {
title.clone().into()
};
@@ -844,7 +858,10 @@ impl Workspace {
})
.child(form.clone()),
)
.footer(commit_footer("ssh-auth-submit", "继续"))
.footer(commit_footer(
"ssh-auth-submit",
t!("workspace.auth.continue"),
))
.on_ok({
let answer = answer.clone();
let form = form.clone();
@@ -1170,7 +1187,7 @@ impl Workspace {
cx: &mut Context<Self>,
) {
self.change_cli_integration(
"无法安装 shellrs 命令",
t!("workspace.cli.install_command_failed"),
crate::cli::install_binary,
window,
cx,
@@ -1184,7 +1201,7 @@ impl Workspace {
cx: &mut Context<Self>,
) {
self.change_cli_integration(
"无法移除 shellrs 命令",
t!("workspace.cli.remove_command_failed"),
crate::cli::remove_binary,
window,
cx,
@@ -1199,7 +1216,7 @@ impl Workspace {
) {
let agent = action.0;
self.change_cli_integration(
"无法安装 skill",
t!("workspace.cli.install_skill_failed"),
move |paths| crate::cli::install_skill(paths, agent),
window,
cx,
@@ -1214,7 +1231,7 @@ impl Workspace {
) {
let agent = action.0;
self.change_cli_integration(
"无法移除 skill",
t!("workspace.cli.remove_skill_failed"),
move |paths| crate::cli::remove_skill(paths, agent),
window,
cx,
@@ -1238,7 +1255,7 @@ impl Workspace {
cx: &mut Context<Self>,
) {
cx.write_to_clipboard(ClipboardItem::new_string(crate::cli::SKILL.to_string()));
window.push_notification(Notification::success("已复制 shellrs skill"), cx);
window.push_notification(Notification::success(t!("workspace.cli.skill_copied")), cx);
}
/// Install or remove part of the external CLI off the main thread, then
@@ -1246,7 +1263,7 @@ impl Workspace {
/// system's administrator prompt.
fn change_cli_integration(
&mut self,
failure: &'static str,
failure: SharedString,
change: impl FnOnce(&IntegrationPaths) -> std::io::Result<()> + Send + 'static,
window: &mut Window,
cx: &mut Context<Self>,
@@ -1351,10 +1368,11 @@ impl Workspace {
Vec::new()
};
let unsaved = (!unsaved.is_empty()).then(|| {
format!(
"{}有未保存的修改,关闭 SFTP 标签会一起关掉它们,修改会丢失。",
describe_files(&unsaved)
t!(
"workspace.unsaved.close_sftp",
files = describe_files(&unsaved)
)
.to_string()
});
if panel.read(cx).is_transferring() {
let direction = panel.read(cx).transfer_direction();
@@ -1370,9 +1388,9 @@ impl Workspace {
} else if let Some(unsaved) = unsaved {
let dispatch = self.focus_handle.clone();
confirm_danger(
"关闭 SFTP 标签?".into(),
t!("workspace.unsaved.close_sftp_title"),
Some(unsaved.into()),
"放弃修改并关闭",
t!("workspace.unsaved.discard_and_close"),
Rc::new(move |window, cx| {
dispatch.dispatch_explorer_action(
&ExplorerAction::new(id, ExplorerCommand::CloseConfirmed)
@@ -1637,7 +1655,10 @@ impl Workspace {
return;
};
cx.write_to_clipboard(ClipboardItem::new_string(host.to_string()));
window.push_notification(Notification::success(format!("已复制 {host}")), cx);
window.push_notification(
Notification::success(t!("workspace.copied", text = host)),
cx,
);
}
fn on_copy_host_id(
@@ -1655,7 +1676,10 @@ impl Workspace {
return;
};
cx.write_to_clipboard(ClipboardItem::new_string(public_id.to_string()));
window.push_notification(Notification::success(format!("已复制 ID {public_id}")), cx);
window.push_notification(
Notification::success(t!("workspace.copied_id", id = public_id)),
cx,
);
}
fn on_restart_local_terminal(
+53
View File
@@ -657,3 +657,56 @@ async fn highlight_rules_are_edited_in_place_on_their_page(cx: &mut TestAppConte
);
});
}
/// 界面语言 changes the window at once, to the language just chosen and
/// back. The fixture keeps this test's language to itself.
#[gpui_kit::test]
fn the_interface_language_changes_the_window_at_once(cx: &mut TestAppContext) {
let (handle, workspace) = open_workspace(cx);
let settings = cx.update(|cx| workspace.read(cx).settings().clone());
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
window.click("open-settings", cx);
})
.unwrap();
cx.run_until_parked();
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
window.within("settings").click("0-0", cx);
})
.unwrap();
cx.run_until_parked();
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
assert_eq!(window.find("settings-tab").label(), Some("设置"));
assert_eq!(window.find("show-hosts").label(), Some("主机"));
assert_eq!(appearance_dropdown(window, 1).as_deref(), Some("跟随系统"));
})
.unwrap();
let choose = |language, cx: &mut TestAppContext| {
cx.update(|cx| {
settings.update(cx, |settings, cx| {
settings.update(|settings| settings.language = language, cx)
})
});
cx.run_until_parked();
};
choose(InterfaceLanguage::English, cx);
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
assert_eq!(window.find("settings-tab").label(), Some("Settings"));
assert_eq!(window.find("show-hosts").label(), Some("Hosts"));
assert_eq!(appearance_dropdown(window, 0).as_deref(), Some("English"));
assert_eq!(appearance_dropdown(window, 1).as_deref(), Some("System"));
})
.unwrap();
choose(InterfaceLanguage::SimplifiedChinese, cx);
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
assert_eq!(window.find("settings-tab").label(), Some("设置"));
assert_eq!(appearance_dropdown(window, 1).as_deref(), Some("跟随系统"));
})
.unwrap();
}