ADR-0019 §9 sweep (3/3): ui.rs prose strings (caught in manual sanity)

Surprise gap from the post-sweep sanity check — `ui.rs` had a
substantial set of TUI-rendered strings that the previous two
sweep passes didn't cover. Caught by grepping for capitalised
literals in `ui.rs` after running the binary smoke check.

## Migrated

- **modal.*** — load picker title / empty state / path
  prompt; rebuild confirm title / "Continue?" prompt.
  (modal.path_entry's title comes from `save.*` since it's
  the save / save-as dialog.)
- **save.*** — `save` no-op hint, modal titles for
  Save / Save as, modal prompt body.
- **status.*** — status bar `Project:` label and the
  `(no project)` placeholder.
- **panel.*** — `Tables` panel title, `(none yet)`
  placeholder for empty tables, `(no active hint)`
  placeholder for the hint panel.
- **shortcut.*** — the bottom-bar keyboard hint labels
  (submit, confirm, cancel, yes, no, load, select,
  browse_path, back_to_list, switch, advanced_once,
  cancel_one_shot, quit). Each is a translatable label
  paired with a key name (Enter / Esc / Ctrl-C / etc.) at
  the call site. Keystroke names are deliberately left as
  literals — translating them would mean retraining users
  away from what their keyboard says.

The `push_shortcut` closure's parameter type changed from
`&'static str` to `&str` so it accepts the catalog-returned
String.

## Deliberately left

- **Echo prefix tags**: `[simple] `, `[advanced] `,
  `[system] `, `[error]  `. Their column widths are
  hardcoded into the wrap-width calculation in
  `render_output_panel`; translating them would silently
  break alignment. Worth a follow-up pass if a future locale
  needs different prefixes (would need `mode.label()` and
  the echo-tag widths to live behind a single locale-aware
  function).
- **Mode labels**: `SIMPLE` / `ADVANCED` / `Advanced:`
  rendered in the input panel border. Same alignment
  reasoning as the echo tags — also they're keywords (the
  user types `mode simple` to switch), so translating the
  display label without translating the command word would
  be confusing. Left as is.
- **Visual decoration**: `[Y]`, `[N]`, `[TEMP] `, `>`
  cursor markers, `█` cursor block, `↑↓` arrow glyph,
  `›` selection marker. Universal symbols / labels rather
  than translatable prose.

## Catalog totals

The catalog now has ~170 entries across 16 categories.
`tests/engine_vocabulary_audit` passes — no engine
vocabulary leaks anywhere user-reachable.

## Tally

610 tests passing (no change — pure refactor with
identical-output catalog substitutions). Clippy clean
with nursery lints. Release builds at 7.8 MB.

ADR-0019 §9 is now genuinely complete.
This commit is contained in:
claude@clouddev1
2026-05-09 22:41:06 +00:00
parent 720511ef29
commit a6fd26d15a
4 changed files with 138 additions and 36 deletions
+58 -30
View File
@@ -120,9 +120,12 @@ fn render_path_entry(
text_lines.push(Line::from(""));
text_lines.push(Line::from(vec![
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" confirm "),
Span::raw(format!(" {} ", crate::t!("shortcut.confirm"))),
Span::styled("Esc", Style::default().fg(theme.muted)),
Span::styled(" cancel", Style::default().fg(theme.muted)),
Span::styled(
format!(" {}", crate::t!("shortcut.cancel")),
Style::default().fg(theme.muted),
),
]));
let paragraph = Paragraph::new(text_lines)
@@ -157,7 +160,10 @@ fn render_load_picker(
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.fg))
.title(Line::from(vec![Span::styled(" Load project ", title_style)]))
.title(Line::from(vec![Span::styled(
format!(" {} ", crate::t!("modal.load_picker_title")),
title_style,
)]))
.style(Style::default().bg(theme.bg).fg(theme.fg));
let mut text_lines: Vec<Line<'_>> = Vec::new();
@@ -166,7 +172,7 @@ fn render_load_picker(
match &m.sub_mode {
LoadPickerSubMode::List => {
if m.entries.is_empty() {
text_lines.push(Line::from("(no projects in data directory)"));
text_lines.push(Line::from(crate::t!("modal.load_picker_empty")));
} else {
for (i, entry) in m.entries.iter().enumerate() {
let marker = if i == m.selected { "" } else { " " };
@@ -189,17 +195,20 @@ fn render_load_picker(
text_lines.push(Line::from(""));
text_lines.push(Line::from(vec![
Span::styled("↑↓", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" select "),
Span::raw(format!(" {} ", crate::t!("shortcut.select"))),
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" load "),
Span::raw(format!(" {} ", crate::t!("shortcut.load"))),
Span::styled("b", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" browse path "),
Span::raw(format!(" {} ", crate::t!("shortcut.browse_path"))),
Span::styled("Esc", Style::default().fg(theme.muted)),
Span::styled(" cancel", Style::default().fg(theme.muted)),
Span::styled(
format!(" {}", crate::t!("shortcut.cancel")),
Style::default().fg(theme.muted),
),
]));
}
LoadPickerSubMode::PathEntry { input, cursor } => {
text_lines.push(Line::from("Path to project directory:"));
text_lines.push(Line::from(crate::t!("modal.load_picker_path_prompt")));
text_lines.push(Line::from(""));
let cursor_marker = "";
let display_input = if *cursor == input.len() {
@@ -215,9 +224,12 @@ fn render_load_picker(
text_lines.push(Line::from(""));
text_lines.push(Line::from(vec![
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" load "),
Span::raw(format!(" {} ", crate::t!("shortcut.load"))),
Span::styled("Esc", Style::default().fg(theme.muted)),
Span::styled(" back to list", Style::default().fg(theme.muted)),
Span::styled(
format!(" {}", crate::t!("shortcut.back_to_list")),
Style::default().fg(theme.muted),
),
]));
}
}
@@ -262,7 +274,10 @@ fn render_rebuild_confirm(summary: &str, theme: &Theme, frame: &mut Frame<'_>, a
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.fg))
.title(Line::from(vec![Span::styled(" Rebuild project ", title_style)]))
.title(Line::from(vec![Span::styled(
format!(" {} ", crate::t!("modal.rebuild_confirm_title")),
title_style,
)]))
.style(Style::default().bg(theme.bg).fg(theme.fg));
let mut text_lines: Vec<Line<'_>> = Vec::new();
@@ -271,7 +286,7 @@ fn render_rebuild_confirm(summary: &str, theme: &Theme, frame: &mut Frame<'_>, a
text_lines.push(Line::from(line));
}
text_lines.push(Line::from(""));
text_lines.push(Line::from("Continue?"));
text_lines.push(Line::from(crate::t!("modal.rebuild_confirm_prompt")));
text_lines.push(Line::from(""));
text_lines.push(Line::from(vec![
Span::styled(
@@ -280,16 +295,19 @@ fn render_rebuild_confirm(summary: &str, theme: &Theme, frame: &mut Frame<'_>, a
.fg(theme.fg)
.add_modifier(Modifier::BOLD),
),
Span::raw(" yes "),
Span::raw(format!(" {} ", crate::t!("shortcut.yes"))),
Span::styled(
"[N]",
Style::default()
.fg(theme.fg)
.add_modifier(Modifier::BOLD),
),
Span::raw(" no "),
Span::raw(format!(" {} ", crate::t!("shortcut.no"))),
Span::styled("Esc", Style::default().fg(theme.muted)),
Span::styled(" cancel", Style::default().fg(theme.muted)),
Span::styled(
format!(" {}", crate::t!("shortcut.cancel")),
Style::default().fg(theme.muted),
),
]));
let paragraph = Paragraph::new(text_lines)
@@ -332,8 +350,12 @@ fn render_project_label(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: R
.add_modifier(Modifier::BOLD);
let bar_style = Style::default().bg(theme.bg).fg(theme.muted);
let display = app.project_name.as_deref().unwrap_or("(no project)");
let mut spans: Vec<Span<'_>> = vec![Span::styled("Project: ", label_style)];
let no_project = crate::t!("status.no_project");
let display = app.project_name.as_deref().unwrap_or(no_project.as_str());
let mut spans: Vec<Span<'_>> = vec![Span::styled(
crate::t!("status.project_label"),
label_style,
)];
if app.project_is_temp {
spans.push(Span::styled(
"[TEMP] ",
@@ -374,7 +396,7 @@ fn render_items_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rec
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.border))
.title(Span::styled(
" Tables ",
format!(" {} ", crate::t!("panel.tables_title")),
Style::default()
.fg(theme.fg)
.add_modifier(Modifier::BOLD),
@@ -383,7 +405,7 @@ fn render_items_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rec
if app.tables.is_empty() {
let placeholder = Paragraph::new(Line::from(Span::styled(
"(none yet)",
crate::t!("panel.tables_empty"),
Style::default()
.fg(theme.muted)
.add_modifier(Modifier::ITALIC),
@@ -597,9 +619,10 @@ fn render_hint_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect
))
.style(Style::default().bg(theme.bg).fg(theme.fg));
let body = app.hint.as_deref().unwrap_or("(no active hint)");
let empty_hint = crate::t!("panel.hint_empty");
let body = app.hint.as_deref().unwrap_or(empty_hint.as_str());
let paragraph = Paragraph::new(Line::from(Span::styled(
body,
body.to_string(),
Style::default().fg(theme.muted),
)))
.block(block)
@@ -619,29 +642,34 @@ fn render_status_bar(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect
let separator = Span::styled(" · ", sep_style);
let mut spans: Vec<Span<'_>> = Vec::new();
let push_shortcut = |spans: &mut Vec<Span<'_>>, key: &'static str, label: &'static str| {
let push_shortcut = |spans: &mut Vec<Span<'_>>, key: &'static str, label: &str| {
if !spans.is_empty() {
spans.push(separator.clone());
}
spans.push(Span::styled(key, key_style));
spans.push(Span::raw(" "));
spans.push(Span::styled(label, label_style));
spans.push(Span::styled(label.to_string(), label_style));
};
push_shortcut(&mut spans, "Enter", "submit");
let submit = crate::t!("shortcut.submit");
push_shortcut(&mut spans, "Enter", &submit);
let switch = crate::t!("shortcut.switch");
let advanced_once = crate::t!("shortcut.advanced_once");
let cancel_one_shot = crate::t!("shortcut.cancel_one_shot");
let quit = crate::t!("shortcut.quit");
match app.effective_mode() {
EffectiveMode::Simple => {
push_shortcut(&mut spans, ":", "advanced once");
push_shortcut(&mut spans, "mode advanced", "switch");
push_shortcut(&mut spans, ":", &advanced_once);
push_shortcut(&mut spans, "mode advanced", &switch);
}
EffectiveMode::AdvancedPersistent => {
push_shortcut(&mut spans, "mode simple", "switch");
push_shortcut(&mut spans, "mode simple", &switch);
}
EffectiveMode::AdvancedOneShot => {
push_shortcut(&mut spans, "Backspace", "cancel one-shot");
push_shortcut(&mut spans, "Backspace", &cancel_one_shot);
}
}
push_shortcut(&mut spans, "Ctrl-C", "quit");
push_shortcut(&mut spans, "Ctrl-C", &quit);
let paragraph = Paragraph::new(Line::from(spans)).style(bar_style);
frame.render_widget(paragraph, area);