round-5 follow-up: completion + i18n sweep

Four user-reported gaps from the round-4 testing pass:

1. Empty-prompt hint reworded from "(no active hint)" to
   "Type a command — press Tab for options, `help` for a
   list" (6 snapshots updated to reflect 80-col truncation).

2. App-lifecycle commands (quit/q, help, rebuild, save/save as,
   new, load, export, import, mode, messages) now flow through
   the DSL parser:
   - 15 new keywords + catalog token entries
   - new Command::App(AppCommand) AST with 11 variants
   - parse-first dispatch in submit() (app commands work in
     both simple and advanced modes)
   - pre-chumsky source-slice for `export <path>` /
     `import <zip> [as <target>]` mirrors the replay precedent
   - UsageEntry registry entries so parse errors surface
     relevant usage templates
   - `mode <bad>` / `messages <bad>` use try_map for the
     friendly "unknown mode/messages" wording

3. DSL completion gaps:
   - `1:n` surfaces as a composite candidate at `add `
   - --all-rows / --create-fk / --force-conversion /
     --dont-convert surface as new CandidateKind::Flag
     candidates (coloured with tok_flag in hint panel)
   - filter_clause .labelled() wrap removed so chumsky's
     expected-set surfaces the constituent options

4. Hardcoded user-facing strings migrated to catalog:
   - 4 parser custom errors (incl. the known "tables need at
     least one column" wart)
   - UnknownType Display now via parse.custom.unknown_type
   - UI panel titles + mode labels (Output / Hint / SIMPLE /
     ADVANCED / Advanced:)
   - app.rs cascade rendering (action labels + summary)
   - runtime --resume CLI stderr
   - db.rs change-column diagnostic tables (7 headers + 3
     wrapper summaries + force-conversion hint)

Tests: 765 → 769 passing, 0 failed, 1 ignored (same doctest
as before). Clippy clean with -D warnings.

Deferred:
- ~25 thiserror #[error] attributes still hand-rolled
  (DbError, ArgsError, ArchiveError, PersistenceError,
  LockError). Tracked separately.
- DSL/SQL relationship in advanced mode — clarified
  implicitly via parse-first dispatch; broader ADR
  amendment to follow.
- Post-complete-parse completion gap (e.g. `save ` Tab
  can't offer `as` because `save` parses bare; same shape
  as `--create-fk` after a complete `add relationship`).
This commit is contained in:
claude@clouddev1
2026-05-13 15:58:29 +00:00
parent 1eb2e0d01f
commit 1e06490572
22 changed files with 1077 additions and 189 deletions
+40 -15
View File
@@ -2632,7 +2632,11 @@ fn render_lossy_diagnostic(
lossies: &[&Outcome],
) -> String {
let mut headers = pk_header_cells(pk_columns);
headers.extend(["From".to_string(), "To".to_string(), "Reason".to_string()]);
headers.extend([
crate::t!("db.diagnostic.header_from"),
crate::t!("db.diagnostic.header_to"),
crate::t!("db.diagnostic.header_reason"),
]);
let mut alignments = pk_header_alignments(pk_columns, old_schema);
alignments.extend([
@@ -2662,18 +2666,22 @@ fn render_lossy_diagnostic(
}
let mut out = format!(
"Cannot change `{table}.{column}` from {src_ty} to {target_ty}: \
{total} row(s) would discard information.\n\n"
"{}\n\n",
crate::t!(
"db.diagnostic.lossy_summary",
table = table,
column = column,
src_ty = src_ty,
target_ty = target_ty,
total = total,
),
);
for line in render_diagnostic_table(&headers, &rows, &alignments) {
out.push_str(&line);
out.push('\n');
}
out.push('\n');
out.push_str(
"if you want to execute this conversion in spite of the problems, \
re-run with `--force-conversion`.",
);
out.push_str(&crate::t!("db.diagnostic.force_conversion_hint"));
out
}
@@ -2688,7 +2696,10 @@ fn render_incompatible_diagnostic(
incompatibles: &[&Outcome],
) -> String {
let mut headers = pk_header_cells(pk_columns);
headers.extend(["Value".to_string(), "Reason".to_string()]);
headers.extend([
crate::t!("db.diagnostic.header_value"),
crate::t!("db.diagnostic.header_reason"),
]);
let mut alignments = pk_header_alignments(pk_columns, old_schema);
alignments.extend([
@@ -2714,8 +2725,15 @@ fn render_incompatible_diagnostic(
}
let mut out = format!(
"Cannot change `{table}.{column}` from {src_ty} to {target_ty}: \
{total} row(s) cannot be converted.\n\n"
"{}\n\n",
crate::t!(
"db.diagnostic.incompatible_summary",
table = table,
column = column,
src_ty = src_ty,
target_ty = target_ty,
total = total,
),
);
for line in render_diagnostic_table(&headers, &rows, &alignments) {
out.push_str(&line);
@@ -2766,9 +2784,9 @@ fn check_uniqueness_collisions(
let pk_label = pk_columns.join(", ");
let headers = vec![
"Becomes".to_string(),
format!("Source rows ({pk_label})"),
"Source values".to_string(),
crate::t!("db.diagnostic.header_becomes"),
crate::t!("db.diagnostic.header_source_rows", pk_label = pk_label),
crate::t!("db.diagnostic.header_source_values"),
];
let alignments = vec![
@@ -2814,8 +2832,15 @@ fn check_uniqueness_collisions(
let _ = old_schema;
let mut out = format!(
"Cannot change `{table}.{column}` from {src_ty} to {target_ty}: \
{total} collision(s) would violate uniqueness.\n\n"
"{}\n\n",
crate::t!(
"db.diagnostic.uniqueness_summary",
table = table,
column = column,
src_ty = src_ty,
target_ty = target_ty,
total = total,
),
);
for line in render_diagnostic_table(&headers, &rows, &alignments) {
out.push_str(&line);