diff --git a/src/ui/code_editor.rs b/src/ui/code_editor.rs index 4f3fbd9b..9cb9d0a6 100644 --- a/src/ui/code_editor.rs +++ b/src/ui/code_editor.rs @@ -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; diff --git a/src/ui/host_ops.rs b/src/ui/host_ops.rs index a72ca16c..a63255d4 100644 --- a/src/ui/host_ops.rs +++ b/src/ui/host_ops.rs @@ -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 { in_flight: HashSet, stale: HashSet, @@ -322,6 +340,31 @@ impl ByHost { #[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] diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 09cc5b0a..bc7640ba 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -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." } diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index e412f4b6..e341d836 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -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 => { "このウィンドウのサーバーがタブを渡さなかったため、空のまま開きました。失われたものはなく、応答すれば戻ります。戻らない場合はコマンドパレットの「サーバーを再起動」を実行してください" } diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 2663c3f5..e47d5c93 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -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, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index 5c70907e..2ef5317e 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -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 => { "这个窗口的服务器没有交出标签页,所以窗口是空的。什么都没丢,它一响应就会回来。如果一直不回来,在命令面板里执行「重启服务器」。" }