Merge branch 'fix/io-error-messages' into integration/polish

This commit is contained in:
l0ng-ai
2026-08-08 04:21:36 +08:00
6 changed files with 89 additions and 5 deletions
+12 -1
View File
@@ -623,7 +623,18 @@ impl Tty7App {
Ok(mtime) => {
f.disk_mtime = mtime;
}
Err(e) => HostOps::notify_err(window, cx, t(L10nKey::EditorSaveFailed), &e),
Err(e) => {
// "Save failed" did not say which file, and with more
// than one editor tab open that is the first thing you
// need to know.
let name = f
.path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| f.path.display().to_string());
let context = t_fmt(L10nKey::EditorSaveFailed, &[("name", &name)]);
HostOps::notify_err(window, cx, &context, &e);
}
}
if landing.clean {
f.dirty = false;
+44 -1
View File
@@ -205,13 +205,31 @@ impl HostOps {
window.push_notification(
t_fmt(
L10nKey::HostOpsError,
&[("context", context), ("error", &err.to_string())],
&[("context", context), ("error", &explain_io(err))],
),
cx,
);
}
}
/// A sentence for the failures someone can act on, and the raw error for the
/// rest. `Display` on an `io::Error` answers "what happened" for a developer
/// reading a log; it does not answer "what now" for the person who just lost
/// a save, and "Permission denied (os error 13)" is the shape of that gap.
pub fn explain_io(err: &std::io::Error) -> String {
use std::io::ErrorKind;
let key = match err.kind() {
ErrorKind::PermissionDenied => L10nKey::IoDenied,
ErrorKind::NotFound => L10nKey::IoGone,
ErrorKind::StorageFull => L10nKey::IoNoSpace,
ErrorKind::ReadOnlyFilesystem => L10nKey::IoReadOnly,
ErrorKind::ResourceBusy => L10nKey::IoBusy,
ErrorKind::TimedOut => L10nKey::IoTimedOut,
_ => return err.to_string(),
};
crate::ui::i18n::t(key).to_string()
}
pub struct InFlight<K: Eq + Hash + Clone> {
in_flight: HashSet<K>,
stale: HashSet<K>,
@@ -322,6 +340,31 @@ impl<K: Eq + Hash, V> ByHost<K, V> {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_failures_people_can_act_on_get_a_sentence() {
use std::io::{Error, ErrorKind};
crate::ui::i18n::set_locale("en");
// The kinds that change what you would do next.
for kind in [
ErrorKind::PermissionDenied,
ErrorKind::StorageFull,
ErrorKind::ReadOnlyFilesystem,
ErrorKind::NotFound,
] {
let text = explain_io(&Error::new(kind, "os error 13"));
assert!(!text.contains("os error"), "{kind:?}: {text}");
assert!(text.ends_with('.'), "{kind:?}: {text}");
}
// Anything unclassified keeps its detail rather than losing it to a
// vague house sentence.
assert_eq!(
explain_io(&Error::other("the widget frobnicated")),
"the widget frobnicated"
);
}
use std::path::{Path, PathBuf};
#[test]
+7 -1
View File
@@ -806,7 +806,7 @@ pub fn translate_en(key: L10nKey) -> &'static str {
L10nKey::EditorCantOpen => "Can't open {path}: {e}",
L10nKey::EditorCantRead => "Can't read {path}: {e}",
L10nKey::EditorNotUtf8 => "\"{path}\" is not valid UTF-8",
L10nKey::EditorSaveFailed => "Save failed",
L10nKey::EditorSaveFailed => "Could not save {name}",
L10nKey::EditorUnsavedChanges => "\"{name}\" has unsaved changes",
L10nKey::EditorDiscard => "Discard",
L10nKey::EditorNoFileOpen => "No file open",
@@ -1048,6 +1048,12 @@ pub fn translate_en(key: L10nKey) -> &'static str {
L10nKey::SshPromptUnlock => "Unlock",
L10nKey::SshPromptSubmit => "Submit",
L10nKey::HostOpsError => "{context}: {error}",
L10nKey::IoDenied => "You do not have permission.",
L10nKey::IoGone => "It is not there any more.",
L10nKey::IoNoSpace => "There is no space left on the disk.",
L10nKey::IoReadOnly => "That location is read-only.",
L10nKey::IoBusy => "Something else has it open.",
L10nKey::IoTimedOut => "The machine did not answer in time.",
L10nKey::TreeWindowOpenedEmpty => {
"This window's server never handed over its tabs, so it opened empty. Nothing was lost — they come back when it answers. If it doesn't, run \"Restart Server\" from the command palette."
}
+7 -1
View File
@@ -847,7 +847,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
L10nKey::EditorCantOpen => "{path} を開けません: {e}",
L10nKey::EditorCantRead => "{path} を読み取れません: {e}",
L10nKey::EditorNotUtf8 => "「{path}」は有効な UTF-8 ではありません",
L10nKey::EditorSaveFailed => "保存に失敗しました",
L10nKey::EditorSaveFailed => "{name} を保存できませんでした",
L10nKey::EditorUnsavedChanges => "「{name}」には保存されていない変更があります",
L10nKey::EditorDiscard => "破棄",
L10nKey::EditorNoFileOpen => "開かれているファイルはありません",
@@ -1078,6 +1078,12 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
L10nKey::SshPromptUnlock => "ロック解除",
L10nKey::SshPromptSubmit => "送信",
L10nKey::HostOpsError => "{context}: {error}",
L10nKey::IoDenied => "権限がありません。",
L10nKey::IoGone => "もう存在しません。",
L10nKey::IoNoSpace => "ディスクに空き容量がありません。",
L10nKey::IoReadOnly => "その場所は読み取り専用です。",
L10nKey::IoBusy => "他のプログラムが使用中です。",
L10nKey::IoTimedOut => "時間内に応答がありませんでした。",
L10nKey::TreeWindowOpenedEmpty => {
"このウィンドウのサーバーがタブを渡さなかったため、空のまま開きました。失われたものはなく、応答すれば戻ります。戻らない場合はコマンドパレットの「サーバーを再起動」を実行してください"
}
+12
View File
@@ -877,6 +877,12 @@ l10n_keys! {
SshPromptUnlock,
SshPromptSubmit,
HostOpsError,
IoDenied,
IoGone,
IoNoSpace,
IoReadOnly,
IoBusy,
IoTimedOut,
TreeWindowOpenedEmpty,
CmdGroupTabsPanes,
CmdGroupWorkspaces,
@@ -1740,6 +1746,12 @@ mod tests {
L10nKey::SshPromptUnlock,
L10nKey::SshPromptSubmit,
L10nKey::HostOpsError,
L10nKey::IoDenied,
L10nKey::IoGone,
L10nKey::IoNoSpace,
L10nKey::IoReadOnly,
L10nKey::IoBusy,
L10nKey::IoTimedOut,
L10nKey::TreeWindowOpenedEmpty,
L10nKey::CmdGroupTabsPanes,
L10nKey::CmdGroupWorkspaces,
+7 -1
View File
@@ -772,7 +772,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
L10nKey::EditorCantOpen => "无法打开 {path}{e}",
L10nKey::EditorCantRead => "无法读取 {path}{e}",
L10nKey::EditorNotUtf8 => "\"{path}\" 不是有效的 UTF-8",
L10nKey::EditorSaveFailed => "保存失败",
L10nKey::EditorSaveFailed => "保存 {name} 失败",
L10nKey::EditorUnsavedChanges => "\"{name}\" 有未保存的更改",
L10nKey::EditorDiscard => "放弃",
L10nKey::EditorNoFileOpen => "没有打开的文件",
@@ -986,6 +986,12 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
L10nKey::SshPromptUnlock => "解锁",
L10nKey::SshPromptSubmit => "提交",
L10nKey::HostOpsError => "{context}{error}",
L10nKey::IoDenied => "没有权限。",
L10nKey::IoGone => "它已经不在了。",
L10nKey::IoNoSpace => "磁盘没有空间了。",
L10nKey::IoReadOnly => "那个位置是只读的。",
L10nKey::IoBusy => "有别的程序正占着它。",
L10nKey::IoTimedOut => "对方没有在规定时间内响应。",
L10nKey::TreeWindowOpenedEmpty => {
"这个窗口的服务器没有交出标签页,所以窗口是空的。什么都没丢,它一响应就会回来。如果一直不回来,在命令面板里执行「重启服务器」。"
}