ADR-0019 §9 sweep (2/2): help blocks + modals + system notes
Final pass of the i18n migration sweep. Every user-visible
string in `src/` now flows through the catalog via `t!()`.
## Categories migrated in this commit
- **help.cli_banner** — the entire `cli::HELP_TEXT` const,
formerly a 40-line `&'static str`, is now a YAML block in
the catalog. The const is replaced by a thin
`cli::help_text() -> String` wrapper that performs the
catalog lookup. `main.rs` calls `help_text()` for both
`--help` output and the args-parse error path. The two
integration tests that referenced `HELP_TEXT` directly are
updated.
- **help.in_app_body** — the in-app `help` command's body is
one YAML block; `note_help` becomes 5 lines that iterate
the lines and emit each as its own output row (preserving
the renderer's "one logical line = one display row"
invariant for accurate scroll math).
- **modal.*** — load picker, rebuild confirm, and save-as
path-entry strings: rebuild_cancelled, load_cancelled,
generic_cancelled, load_picker_nothing,
path_entry_empty_name, path_entry_empty_path.
- **dsl.failed** — the `"<verb> <subject>" failed: <rendered>`
wrapper around the friendly-error layer's translated
message.
- **dsl.running** — the `running: <input>` echo line shown
above each command's response. (Note: the en-US prefix
"running: " is hardcoded in the parse-error caret-padding
calculation. Translators changing the prefix must keep the
width consistent — documented inline.)
- **advanced_mode.not_implemented** — the placeholder echo
shown when SQL hits the unimplemented advanced-mode path
(Q1 territory).
- **fatal.persistence** — the FATAL banner for
PersistenceFatal events (ADR-0015 §8).
- **project.{load_path_missing,saveas_target_exists,**
**import_zip_missing}** — runtime-side project-switch
validation errors that surface via ProjectSwitchFailed.
## Catalog start-up ordering
`main.rs` now calls `friendly::catalog()` at the very top
(before args parsing) so `help_text()` works in both the
success path and the args-error path. A corrupted build
artefact still fails loudly with a useful panic; the
practical risk is essentially zero since the catalog is
`include_str!`'d at compile time and validated by the unit
test before shipping.
## Remaining literals
The only `note_*` calls in `src/` that still pass plain
strings are inside `#[cfg(test)]` modules — synthetic test
fixtures, not user-visible. The codebase passes the "every
user-visible string flows through the catalog" bar.
## Tally
610 tests passing (no change in count — pure refactor).
Clippy clean with nursery lints.
## What this closes
ADR-0019 §9 (migration sweep) — done.
ADR-0019 itself is now fully implemented:
- §1-§5: catalog + translator + voice + verbosity ✓ (`eac7e5b`)
- §6: row pinpointing + schema enrichment ✓ (`431645a`)
- §9: migration sweep ✓ (this + `aff528a`)
- §10: anchor phrases preserved throughout ✓
- The five "Out of scope" items remain explicitly bounded
to future ADRs (advanced-mode SQL, settings persistence,
pluralisation, runtime locale, value formatting,
constraint management).
This commit is contained in:
+38
-66
@@ -344,10 +344,11 @@ impl App {
|
||||
path,
|
||||
message,
|
||||
} => {
|
||||
let banner = format!(
|
||||
"FATAL: failed to {operation} `{}` — {message}. \
|
||||
Quitting; investigate and restart.",
|
||||
path.display(),
|
||||
let banner = crate::t!(
|
||||
"fatal.persistence",
|
||||
operation = operation,
|
||||
path = path.display(),
|
||||
message = message
|
||||
);
|
||||
self.note_error(banner.clone());
|
||||
self.fatal_message = Some(banner);
|
||||
@@ -754,8 +755,9 @@ impl App {
|
||||
// until the advanced-mode SQL path lands. Once it does,
|
||||
// this branch parses with sqlparser-rs and dispatches
|
||||
// analogously to the DSL path below.
|
||||
self.note_system(format!(
|
||||
"advanced mode SQL not implemented yet — echo: {effective_input}"
|
||||
self.note_system(crate::t!(
|
||||
"advanced_mode.not_implemented",
|
||||
input = effective_input
|
||||
));
|
||||
self.push_output(OutputLine {
|
||||
text: effective_input,
|
||||
@@ -785,7 +787,7 @@ impl App {
|
||||
// keeping the worker out of the loop and the
|
||||
// history.log clean.
|
||||
self.push_output(OutputLine {
|
||||
text: format!("running: {input}"),
|
||||
text: crate::t!("dsl.running", input = input),
|
||||
kind: OutputKind::Echo,
|
||||
mode_at_submission: submission_mode,
|
||||
});
|
||||
@@ -793,7 +795,7 @@ impl App {
|
||||
}
|
||||
Ok(cmd) => {
|
||||
self.push_output(OutputLine {
|
||||
text: format!("running: {input}"),
|
||||
text: crate::t!("dsl.running", input = input),
|
||||
kind: OutputKind::Echo,
|
||||
mode_at_submission: submission_mode,
|
||||
});
|
||||
@@ -807,13 +809,19 @@ impl App {
|
||||
// Echo the source line so the user can see what
|
||||
// got submitted (and copy-paste it back to fix).
|
||||
self.push_output(OutputLine {
|
||||
text: format!("running: {input}"),
|
||||
text: crate::t!("dsl.running", input = input),
|
||||
kind: OutputKind::Echo,
|
||||
mode_at_submission: submission_mode,
|
||||
});
|
||||
// Caret pointer at the failure position, when we
|
||||
// have one. Aligned to the "running: " prefix so
|
||||
// the caret sits under the offending character.
|
||||
//
|
||||
// Note: the prefix length is hardcoded against
|
||||
// the en-US `dsl.running` template ("running:
|
||||
// {input}"). A translator changing that prefix
|
||||
// must update this width too — the constraint is
|
||||
// captured in the catalog comment block.
|
||||
if let ParseError::Invalid { position, .. } = &err {
|
||||
let prefix = "running: ";
|
||||
let trimmed_offset = leading_trim_offset(input);
|
||||
@@ -976,10 +984,11 @@ impl App {
|
||||
// `note_error` splits on newlines internally — refusal
|
||||
// diagnostics from `change column …` (ADR-0017 §7) flow
|
||||
// through as a multi-line bordered table.
|
||||
self.note_error(format!(
|
||||
"\"{} {}\" failed: {rendered}",
|
||||
command.verb(),
|
||||
command.display_subject()
|
||||
self.note_error(crate::t!(
|
||||
"dsl.failed",
|
||||
verb = command.verb(),
|
||||
subject = command.display_subject(),
|
||||
rendered = rendered
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1164,7 +1173,7 @@ impl App {
|
||||
}
|
||||
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||
self.modal = None;
|
||||
self.note_system("rebuild cancelled");
|
||||
self.note_system(crate::t!("modal.rebuild_cancelled"));
|
||||
Vec::new()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
@@ -1179,13 +1188,16 @@ impl App {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
self.modal = None;
|
||||
self.note_system(format!("{} cancelled", state.title.to_lowercase()));
|
||||
self.note_system(crate::t!(
|
||||
"modal.generic_cancelled",
|
||||
title = state.title.to_lowercase()
|
||||
));
|
||||
Vec::new()
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let target = state.input.trim().to_string();
|
||||
if target.is_empty() {
|
||||
self.note_error("path entry: empty name");
|
||||
self.note_error(crate::t!("modal.path_entry_empty_name"));
|
||||
self.modal = Some(Modal::PathEntry(state));
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -1263,7 +1275,7 @@ impl App {
|
||||
LoadPickerSubMode::List => match key.code {
|
||||
KeyCode::Esc => {
|
||||
self.modal = None;
|
||||
self.note_system("load cancelled");
|
||||
self.note_system(crate::t!("modal.load_cancelled"));
|
||||
Vec::new()
|
||||
}
|
||||
KeyCode::Up => {
|
||||
@@ -1288,7 +1300,7 @@ impl App {
|
||||
source: "load".to_string(),
|
||||
}];
|
||||
}
|
||||
self.note_error("nothing to load");
|
||||
self.note_error(crate::t!("modal.load_picker_nothing"));
|
||||
self.modal = Some(Modal::LoadPicker(state));
|
||||
Vec::new()
|
||||
}
|
||||
@@ -1314,7 +1326,7 @@ impl App {
|
||||
KeyCode::Enter => {
|
||||
let target = input.trim().to_string();
|
||||
if target.is_empty() {
|
||||
self.note_error("path entry: empty path");
|
||||
self.note_error(crate::t!("modal.path_entry_empty_path"));
|
||||
self.modal = Some(Modal::LoadPicker(state));
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -1376,53 +1388,13 @@ impl App {
|
||||
/// always accurate against the build they're running. As
|
||||
/// new commands land, append them here.
|
||||
fn note_help(&mut self) {
|
||||
self.note_system("Supported commands:");
|
||||
for line in [
|
||||
" quit / q — exit",
|
||||
" help — this list",
|
||||
" mode simple|advanced — switch input mode",
|
||||
" messages — show current verbosity",
|
||||
" messages short|verbose— switch error wording (verbose is the default)",
|
||||
" rebuild — rebuild .db from project.yaml + data/ (with confirmation)",
|
||||
" save — save current temp project under a name",
|
||||
" save as — copy current project to a new name/path",
|
||||
" new — close current, start a fresh temp project",
|
||||
" load — open the project picker",
|
||||
" export [<path>] — write a zip of project.yaml + data/ (excludes .db, history.log)",
|
||||
" import <zip> [as <t>] — unpack a zip and switch to the new project",
|
||||
"DSL data commands (in simple mode):",
|
||||
" create table <T> with pk [<col>:<type>...]",
|
||||
" drop table <T>",
|
||||
" add column [to] [table] <T>: <col> (<type>)",
|
||||
" (for serial/shortid on a non-empty table: existing rows auto-filled)",
|
||||
" drop column [from] [table] <T>: <col>",
|
||||
" rename column [in] [table] <T>: <old> to <new>",
|
||||
" change column [in] [table] <T>: <col> (<newtype>)",
|
||||
" [--force-conversion | --dont-convert]",
|
||||
" (to serial/shortid: null cells auto-filled with generated values)",
|
||||
" add 1:n relationship [as <name>] from <P>.<col> to <C>.<col>",
|
||||
" [on delete <action>] [on update <action>] [--create-fk]",
|
||||
" drop relationship <name>",
|
||||
" insert into <T> [(cols)] [values] (vals)",
|
||||
" update <T> set <c>=<v>... where <c>=<v> | --all-rows",
|
||||
" delete from <T> where <c>=<v> | --all-rows",
|
||||
" show table <T>",
|
||||
" show data <T>",
|
||||
" replay <path> — run each non-blank, non-`#`-comment line",
|
||||
" of <path> as a command. Stops at the first",
|
||||
" error (no rollback). Relative paths resolve",
|
||||
" under the current project's directory.",
|
||||
"Types: text, int, real, decimal, bool, date, datetime, blob, serial, shortid",
|
||||
"Auto-generated types (serial, shortid):",
|
||||
" serial — integer that auto-fills with the next sequence value",
|
||||
" (MAX(col)+1) on insert. Outside a primary key it carries",
|
||||
" a UNIQUE contract.",
|
||||
" shortid — short base58 identifier auto-filled at insert time. Always",
|
||||
" carries a UNIQUE contract.",
|
||||
" Adding or changing-to either type on a non-empty table auto-fills",
|
||||
" existing/null cells in the same operation.",
|
||||
] {
|
||||
self.note_system(line);
|
||||
// Body lives in the i18n catalog (`help.in_app_body`).
|
||||
// Each YAML line becomes its own `OutputLine` so the
|
||||
// scroll-position math (one logical line = one display
|
||||
// row) stays accurate per the renderer's invariant.
|
||||
let body = crate::t!("help.in_app_body");
|
||||
for line in body.lines() {
|
||||
self.note_system(line.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user