mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08:02:24 +00:00
feat(ui): GUI localization for en and zh-Hans (#303)
* feat(ui): add GUI localization for en and zh-Hans
* feat(ui): localize search placeholders and relative time
* feat(ui): localize palette, switcher, and sftp strings
* feat(ui): localize home shortcut labels
* feat(ui): localize tray, ssh prompt, and editor strings
* feat(ui): add plural/select i18n helpers and localize sftp/settings labels
* feat(ui): localize settings search, forwards panel, and file tree
* feat(ui): localize code editor and right panel
* feat(ui): localize stop/delete workspace confirmations with plural support
* feat(ui): localize diff overlay with plural-aware summary
* feat(ui): localize pending pane, worktree prompt, and home time strings
* feat(ui): localize app menus, tray, tab strip/sidebar, and remote status strings
* feat(ui): localize switcher, file_tree, machine_mirror fallback strings
* feat(ui): localize ssh prompts, theme presets, host error wrapper, and finish remote strings
* feat(ui): localize command palette strings
* feat(ui): localize app.rs notifications, prompts, placeholders, and parse errors
* feat(ui): localize remaining theme, switcher, settings, and sftp strings
* style: cargo fmt
* feat(ui): add language selector to settings
* fix(ui): refresh locales across windows
* refactor(ui): make GUI language selection explicit
* fix(ui): localize Explorer settings after merge
* fix(ui): keep persisted theme names out of the GUI locale
A theme's name is data, not chrome: it is written into the theme YAML and
matched back with `trim_end_matches(" (custom)")`. Translating it meant a
Chinese GUI forked "Nord" into "Nord(自定义)", the next fork stacked a second
suffix on it, and the name stayed Chinese after switching back to English. The
derived-name fallback had the same problem. Both are English again.
Also in this pass:
- Give each test thread its own locale override. The locale is process-wide and
tests run in parallel, so the two tests that switched to zh-CN could flip the
language out from under another thread's English assertions.
- Rebuild the menu bar when gui_language changes in config.json, the way the
in-app picker already does — otherwise the menus kept the old language.
- Document the values the setting actually accepts. The docs still described
`auto` and `zh-Hans`, which sanitize() resets to `en`.
- Put the English words back into the Chinese search keywords for the language
setting; the other 58 keyword sets keep them.
- Drop the unused is_zh_hans helper.
---------
Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
co-authored by
thomas
l0ng-ai
parent
e8525e1b33
commit
8a342f2ca9
+66
-21
@@ -15,6 +15,7 @@ use gpui_component::{
|
||||
|
||||
use crate::ui::app::Tty7App;
|
||||
use crate::ui::host_ops::{HostOps, MTime, SharedHost, WatchSub};
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
|
||||
const MAX_FILE_BYTES: u64 = 4 * 1024 * 1024;
|
||||
|
||||
@@ -362,24 +363,43 @@ impl Tty7App {
|
||||
let path = h.canonicalize(&p).unwrap_or(p);
|
||||
let meta = match h.stat(&path) {
|
||||
Ok(m) => m,
|
||||
Err(e) => return Err(format!("Can't open {}: {e}", path.display())),
|
||||
Err(e) => {
|
||||
return Err(t_fmt(
|
||||
L10nKey::EditorCantOpen,
|
||||
&[("path", &path.display().to_string()), ("e", &e.to_string())],
|
||||
));
|
||||
}
|
||||
};
|
||||
if meta.len > MAX_FILE_BYTES {
|
||||
return Err(format!(
|
||||
"\"{}\" is too large for the editor ({} MB)",
|
||||
path.display(),
|
||||
meta.len / (1024 * 1024)
|
||||
return Err(t_fmt(
|
||||
L10nKey::EditorFileTooLarge,
|
||||
&[
|
||||
("path", &path.display().to_string()),
|
||||
("size", &(meta.len / (1024 * 1024)).to_string()),
|
||||
],
|
||||
));
|
||||
}
|
||||
let bytes = match h.read_file(&path, MAX_FILE_BYTES) {
|
||||
Ok(b) => b,
|
||||
Err(e) => return Err(format!("Can't read {}: {e}", path.display())),
|
||||
Err(e) => {
|
||||
return Err(t_fmt(
|
||||
L10nKey::EditorCantRead,
|
||||
&[("path", &path.display().to_string()), ("e", &e.to_string())],
|
||||
));
|
||||
}
|
||||
};
|
||||
if looks_binary(&bytes) {
|
||||
return Err(format!("\"{}\" looks like a binary file", path.display()));
|
||||
return Err(t_fmt(
|
||||
L10nKey::EditorBinaryFile,
|
||||
&[("path", &path.display().to_string())],
|
||||
));
|
||||
}
|
||||
let text = String::from_utf8(bytes)
|
||||
.map_err(|_| format!("\"{}\" is not valid UTF-8", path.display()))?;
|
||||
let text = String::from_utf8(bytes).map_err(|_| {
|
||||
t_fmt(
|
||||
L10nKey::EditorNotUtf8,
|
||||
&[("path", &path.display().to_string())],
|
||||
)
|
||||
})?;
|
||||
Ok((path, text, meta.mtime))
|
||||
},
|
||||
move |app, opened, window, cx| match opened {
|
||||
@@ -603,7 +623,7 @@ impl Tty7App {
|
||||
Ok(mtime) => {
|
||||
f.disk_mtime = mtime;
|
||||
}
|
||||
Err(e) => HostOps::notify_err(window, cx, "Save failed", &e),
|
||||
Err(e) => HostOps::notify_err(window, cx, t(L10nKey::EditorSaveFailed), &e),
|
||||
}
|
||||
if landing.clean {
|
||||
f.dirty = false;
|
||||
@@ -658,9 +678,13 @@ impl Tty7App {
|
||||
let name = f.label();
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
&format!("\"{name}\" has unsaved changes"),
|
||||
&t_fmt(L10nKey::EditorUnsavedChanges, &[("name", &name)]),
|
||||
None,
|
||||
&["Save", "Discard", "Cancel"],
|
||||
&[
|
||||
t(L10nKey::Save),
|
||||
t(L10nKey::EditorDiscard),
|
||||
t(L10nKey::Cancel),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
let id = f.input.entity_id();
|
||||
@@ -937,7 +961,9 @@ impl Tty7App {
|
||||
.when(name.is_none(), |d| {
|
||||
d.text_color(cx.theme().muted_foreground)
|
||||
})
|
||||
.child(name.unwrap_or_else(|| SharedString::from("No file open"))),
|
||||
.child(
|
||||
name.unwrap_or_else(|| SharedString::from(t(L10nKey::EditorNoFileOpen))),
|
||||
),
|
||||
)
|
||||
.when(dirty, |d| {
|
||||
d.child(
|
||||
@@ -958,7 +984,7 @@ impl Tty7App {
|
||||
cx,
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip("Back to Terminal (Esc)")
|
||||
.tooltip(t(L10nKey::EditorBackToTerminal))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.toggle_code_panel(window, cx);
|
||||
})),
|
||||
@@ -992,7 +1018,14 @@ impl Tty7App {
|
||||
let active = code.and_then(|c| c.active_file());
|
||||
let cursor: Option<SharedString> = active.map(|f| {
|
||||
let pos = f.input.read(cx).cursor_position();
|
||||
format!("Ln {}, Col {}", pos.line + 1, pos.character + 1).into()
|
||||
t_fmt(
|
||||
L10nKey::EditorLnCol,
|
||||
&[
|
||||
("line", &(pos.line + 1).to_string()),
|
||||
("column", &(pos.character + 1).to_string()),
|
||||
],
|
||||
)
|
||||
.into()
|
||||
});
|
||||
let wrap: Option<bool> = active.map(|f| f.wrap);
|
||||
let is_markdown = active.is_some_and(|f| language_for_path(&f.path) == "markdown");
|
||||
@@ -1016,7 +1049,11 @@ impl Tty7App {
|
||||
.when(is_markdown, |this| {
|
||||
this.child(
|
||||
Button::new("status-md-preview")
|
||||
.label(if preview { "Edit" } else { "Preview" })
|
||||
.label(if preview {
|
||||
t(L10nKey::EditorEdit)
|
||||
} else {
|
||||
t(L10nKey::EditorPreview)
|
||||
})
|
||||
.custom(crate::ui::tab_strip::chrome_tile_variant(cx))
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, _w, cx| {
|
||||
@@ -1033,7 +1070,11 @@ impl Tty7App {
|
||||
.when_some(wrap, |this, wrap| {
|
||||
this.child(
|
||||
Button::new("status-wrap")
|
||||
.label(if wrap { "Wrap: on" } else { "Wrap: off" })
|
||||
.label(if wrap {
|
||||
t(L10nKey::EditorWrapOn)
|
||||
} else {
|
||||
t(L10nKey::EditorWrapOff)
|
||||
})
|
||||
.custom(crate::ui::tab_strip::chrome_tile_variant(cx))
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -1069,7 +1110,9 @@ impl Tty7App {
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Open a file from the file tree"),
|
||||
.child(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::OpenFileFromTree,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1087,10 +1130,12 @@ impl Tty7App {
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.text_sm()
|
||||
.child(div().flex_1().child("File changed on disk"))
|
||||
.child(div().flex_1().child(crate::ui::i18n::t(
|
||||
crate::ui::i18n::L10nKey::FileChangedOnDisk,
|
||||
)))
|
||||
.child(
|
||||
Button::new("editor-conflict-reload")
|
||||
.label("Reload")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::Reload))
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.editor_reload_from_disk(tab_ix, ix, window, cx);
|
||||
@@ -1098,7 +1143,7 @@ impl Tty7App {
|
||||
)
|
||||
.child(
|
||||
Button::new("editor-conflict-keep")
|
||||
.label("Keep mine")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::KeepMine))
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, _w, cx| {
|
||||
|
||||
Reference in New Issue
Block a user