# en-US catalog (ADR-0019).
#
# Hierarchical groups flatten to dot-paths internally:
#   error.unique.insert.headline
#   error.unique.insert.hint
#   help.cli_banner
#   replay.completed
# … etc.
#
# Each error entry has a `headline` (one line, used in both
# short and verbose modes) and may have a `hint` (one or more
# lines, surfaced only in verbose mode). Short mode = headline
# only; verbose mode = headline + hint + (if present) the
# diagnostic table the translator built (ADR-0019 §7).
#
# Anchor phrases per ADR-0019 §10 are kept stable across
# wording changes:
#   "no such table"
#   "no such column"
#   "no such relationship"
#   "already exists"
#   "already has the value"
#   "cannot be converted"
#   "discard information"
#   "referenced by"
#   "[client-side]"
#
# Placeholders use `{name}` substitution; format specifiers
# (`{name:08.2}`, `{name:>10}`, …) are explicitly rejected by
# the substitution helper (ADR-0019 §8.4).

# Sanity entry exercised by the loader's unit tests; not
# user-facing.
_test:
  hello: "Hello, {name}!"

# ---- Error category --------------------------------------------------
error:

  # UNIQUE constraint violations. Anchor: "already has the value".
  unique:
    insert:
      headline: "`{table}.{column}` already has the value `{value}`."
      hint: "The `{column}` column on `{table}` is unique — pick a different value, or update the existing row instead."
    update:
      headline: "`{table}.{column}` already has the value `{value}`."
      hint: "The `{column}` column on `{table}` is unique — your update would create a duplicate."
    # Primary-key collisions get distinct wording — the user
    # learns that PK is the canonical unique constraint.
    pk:
      insert:
        headline: "`{table}` already has a row with primary key `{value}`."
        hint: "Primary keys must be unique — pick a different value or update the existing row."
      update:
        headline: "`{table}` already has a row with primary key `{value}`."
        hint: "Primary keys must be unique — your update would create a duplicate."

  # FOREIGN KEY violations. Anchor: "referenced by".
  foreign_key:
    # Child-side: insert/update sets a value that has no parent.
    child_side:
      insert:
        headline: "no parent row in `{parent_table}` has `{parent_column}` = `{value}`."
        hint: "Foreign keys must point at an existing parent row. Insert a matching parent first, or pick a value that already exists in `{parent_table}.{parent_column}`."
      update:
        headline: "no parent row in `{parent_table}` has `{parent_column}` = `{value}`."
        hint: "Foreign keys must point at an existing parent row. Pick a value that already exists in `{parent_table}.{parent_column}`."
    # Parent-side: delete/update on a row referenced by children.
    # The engine refuses unless the relationship's `on delete /
    # on update` says cascade or set null.
    parent_side:
      delete:
        headline: "`{table}` rows are referenced by `{child_table}`."
        hint: "Deleting these rows would orphan the children. Delete the children first, or change the relationship's `on delete` action to `cascade` or `set null`."
      update:
        headline: "`{table}` rows are referenced by `{child_table}`."
        hint: "Updating the referenced column would orphan the children. Update the children first, or change the relationship's `on update` action to `cascade`."

  # NOT NULL constraint violations.
  not_null:
    insert:
      headline: "`{table}.{column}` cannot be null."
      hint: "The `{column}` column is required — provide a value for it in the row you are inserting."
    update:
      headline: "`{table}.{column}` cannot be null."
      hint: "The `{column}` column is required — pick a non-null value, or do not include `{column}` in your `set` list."

  # CHECK constraint violations (ADR-0029 §10). When the
  # runtime resolves the column's compiled CHECK expression,
  # `hint_with_rule` names both the offending value and the
  # rule; the plain `hint` is the fallback when enrichment
  # could not resolve the rule.
  check:
    insert:
      headline: "check constraint refused `{table}.{column}`."
      hint: "A check constraint requires `{column}` to satisfy a rule the inserted value did not."
      hint_with_rule: "The value {value} breaks the rule `{rule}` — `{column}` must satisfy it."
    update:
      headline: "check constraint refused `{table}.{column}`."
      hint: "A check constraint requires `{column}` to satisfy a rule the new value did not."
      hint_with_rule: "The new value {value} breaks the rule `{rule}` — `{column}` must satisfy it."

  # Type mismatch — engine-side STRICT refusal of a wrong-shape
  # value. Mostly the `change column ... --dont-convert` path
  # today (subsumes `friendly_change_column_engine_error`).
  type_mismatch:
    change_column:
      headline: "cannot change `{table}.{column}` from `{src_type}` to `{target_type}` with `--dont-convert`."
      hint: "The database refused at least one cell as the wrong shape for `{target_type}`. Re-run without `--dont-convert` to see which rows."
    insert:
      headline: "value `{value}` is not a `{expected_type}`."
      hint: "The `{column}` column on `{table}` is `{expected_type}` — provide a `{expected_type}` value, or change the column's type with `change column`."
    update:
      headline: "value `{value}` is not a `{expected_type}`."
      hint: "The `{column}` column on `{table}` is `{expected_type}` — provide a `{expected_type}` value, or change the column's type with `change column`."

  # Object-not-found errors. Anchor: "no such ...". These are
  # genuinely single-line errors — no hint adds value.
  not_found:
    table:
      headline: "no such table: `{name}`"
    column:
      headline: "no such column: `{table}.{column}`"
    column_unqualified:
      headline: "no such column: `{column}`"
    relationship:
      headline: "no such relationship: `{name}`"

  # Name-collision errors. Anchor: "already exists".
  already_exists:
    table:
      headline: "table `{name}` already exists"
    column:
      headline: "column `{table}.{column}` already exists"
    relationship:
      headline: "relationship `{name}` already exists"

  # Generic catch-all when the translator can't classify the
  # engine error into a known category. The wording stays
  # engine-neutral; the message text from the engine is NOT
  # surfaced (ADR-0002 user-facing posture).
  generic:
    headline: "the database refused this `{operation}`."
    hint: "The operation could not be completed against the current state of `{table}`."
    # Used when no table is in context (e.g. contextless `friendly_message()`
    # callsites: replay, undo, rebuild, export) so the hint never leaks a
    # literal `{table}` placeholder.
    hint_no_table: "The operation could not be completed against the current database state."

  # Errors that are specifically about value validation
  # (DbError::InvalidValue) — wrong arity, wrong literal
  # form, etc. Pre-engine; the catalog covers what the
  # parser / validator already decided was wrong.
  invalid_value:
    arity:
      headline: "expected {expected} value(s), got {actual}."
    empty_insert:
      headline: "INSERT requires at least one column value."
    empty_update:
      headline: "UPDATE requires at least one assignment."

# ---- Help text (CLI banner + in-app `help` command) ------------------
# ---- CLI argument-parsing errors (stderr before TUI starts) ---------
cli:
  # Version line for `--version` / `-V` and the in-app `version` command
  # (ADR-0054). `{version}` is `CARGO_PKG_VERSION` — the single source of
  # truth, equal to the `v*` release tag (release CI guards the match).
  version_line: "rdbms-playground {version}"
  missing_value: "missing value for --{flag}"
  invalid_value: "invalid value for --{flag}: {value} (expected one of: {expected})"
  unknown_argument: "unknown argument: {arg}"
  multiple_paths: "only one project path may be supplied; got both `{first}` and `{second}`"
  resume_with_path: |-
    --resume and a positional <project-path> are mutually exclusive; pass one or the other

help:
  # CLI usage banner. Printed by `--help` / `-h` and on
  # argument-parse failure. Multi-line block; consumers
  # iterate lines or print as-is.
  cli_banner: |
    rdbms-playground — a TUI playground for relational database concepts

    Usage:
        rdbms-playground [options] [<project-path>]

    Arguments:
        <project-path>           Path to an existing project directory.
                                 Without this, a fresh auto-named temp
                                 project is created in the data dir.

    Options:
        -h, --help               Print this help and exit.
        -V, --version            Print the version and exit.
            --theme <light|dark> Override theme (default: auto-detect).
            --data-dir <PATH>    Use PATH as the data root instead of
                                 the OS-standard location for this run.
            --log-file <PATH>    Write tracing output to PATH.
            --resume             Open the most-recently-used project
                                 (path tracked under <data-root>/last_project).
                                 Errors out if no previous project is
                                 recorded. Mutually exclusive with
                                 <project-path>.
            --no-undo            Disable the undo machinery for this run:
                                 no snapshot is taken before each change
                                 (no per-command overhead), and undo/redo
                                 report that undo is turned off.
            --mode <simple|advanced>
                                 Start in this input mode, overriding the
                                 project's stored mode. Without it, the
                                 project's last-used mode is restored
                                 (default: simple).
            --demo               Demonstration mode: show on-screen badges
                                 for otherwise-invisible keys (Tab, Enter,
                                 ...) — for screencasts and live teaching.

    App-level commands (typed inside the app, available in both modes):
        quit                     Exit cleanly.
        version                  Print the application version.
        mode simple|advanced     Switch input mode.
        help                     Show this list of commands in-app.
        save                     Save the current temp project under a
                                 chosen name (or `save as` to copy a
                                 named project to a new location).
        save as                  Always prompt for a target name/path.
        new                      Close current, create a fresh temp.
        load                     Open the project picker.
        rebuild                  Rebuild playground.db from project.yaml
                                 + data/, with confirmation.
        export [<path>]          Write a zip of project.yaml + data/ to
                                 <path> (relative paths under the data
                                 root). Excludes playground.db and
                                 history.log.
        import <zip> [as <t>]    Unpack <zip> into a new project and
                                 switch to it. <t> overrides the target
                                 name (else taken from the zip).
        copy [all|last]          Copy the output panel to the system
                                 clipboard (`copy last` copies just the
                                 most recent command's output).
  # In-app `help` command output (ADR-0024 §help_id). The
  # renderer iterates the command REGISTRY and translates each
  # CommandNode's `help_id` — so a newly-registered command
  # appears in `help` automatically, with no edit here. `intro`
  # and `dsl_section` frame the two command groups; each
  # `app.*` / `ddl.*` / `data.*` entry is keyed by a command's
  # `help_id`; `types_reference` closes the block. All entries
  # are multi-line-capable — the renderer emits one output row
  # per line so scroll math stays accurate.
  intro: "Supported commands:"
  # Issue #36: the command list groups by mode. App-lifecycle commands list
  # first (unlabelled, under the intro — they work in either mode); the rest
  # split into these two sections by command category. (Replaces the old
  # single "DSL data commands (in simple mode):" header, which used the banned
  # "DSL" term and mis-labelled the advanced SQL forms it already contained.)
  simple_section: "Simple-mode commands:"
  advanced_section: "Advanced-mode (SQL) commands:"
  # H3: footer on the full `help` list, and the not-found note
  # for `help <topic>`. `{topic}` is the word the user typed.
  detail_hint: "Type `help <command>` for detail on one command (e.g. `help insert`), or `help types` for the type reference."
  unknown_topic: "No help for `{topic}`. Type `help` for the full command list, or `help types` for the type reference."
  # Per-command help, keyed by `CommandNode.help_id`. Block
  # scalars (`|-`) so the column alignment survives — the
  # double-quoted form trips a libyml scanner bug on long
  # internal space runs. The renderer emits one output row per
  # line, so multi-form commands list each form on its own line.
  app:
    quit: |-
        quit                       — exit the app
    help: |-
        help                       — show this command list
        help <command>             — detailed help for one command (e.g. `help insert`)
    hint: |-
        hint                       — explain the most recent error (press F1 for a hint on what you're typing)
    version: |-
        version                    — print the application version (same as the `--version` command-line flag)
    rebuild: |-
        rebuild                    — rebuild the project database from project.yaml + data/ (with confirmation)
    save: |-
        save                       — save the current temp project under a name
        save as                    — copy the current project to a new name/path
    new: |-
        new                        — close the current project, start a fresh temp project
    load: |-
        load                       — open the project picker
    export: |-
        export [<path>]            — write a zip of project.yaml + data/ (excludes the database file and history.log)
    import: |-
        import <zip> [as <target>] — unpack a zip into a new project and switch to it
    mode: |-
        mode simple|advanced       — switch input mode
    messages: |-
        messages [short|verbose]   — show or switch error-message verbosity (verbose is the default)
    undo: |-
        undo                       — undo the last change (with confirmation)
    redo: |-
        redo                       — redo the last undone change (with confirmation)
    copy: |-
        copy [all|last]            — copy the output panel to the clipboard (`copy last` = the most recent command)
  ddl:
    create: |-
        create table <T> with pk [<col>(<type>), ...]    — create a table
    create_m2n: |-
        create m:n relationship from <T1> to <T2> [as <name>]
                                                         — build a junction table linking two tables
    sql_create_table: |-
        create table [if not exists] <T> (
          <col> <type> [not null] [unique] [primary key] [default <expr>] [check (<expr>)] [references <P>[(<col>)]], ...
          [, primary key (<col>, ...)] [, unique (<col>, ...)] [, check (<expr>)]
          [, [constraint <name>] foreign key (<col>) references <P>[(<col>)]])
                                                         — create a table (advanced SQL)
    sql_drop_table: |-
        drop table [if exists] <T>                       — remove a table (advanced SQL)
    sql_create_index: |-
        create [unique] index [if not exists] [<name>] on <T> (<col>, ...)
                                                         — create an index (advanced SQL)
    sql_drop_index: |-
        drop index [if exists] <name>                    — remove an index (advanced SQL)
    sql_alter_table: |-
        alter table <T> add column <col> <type> [not null] [unique] [default …] [check …]
        alter table <T> drop column <col>
        alter table <T> rename column <old> to <new>
        alter table <T> rename to <new>
        alter table <T> alter column <col> type <type>
        alter table <T> add [constraint <name>] check (<expr>) | unique (<col>, …) | foreign key (<col>) references <P>[(<col>)]
        alter table <T> drop constraint <name>   — evolve a table's columns and constraints (advanced SQL)
    drop: |-
        drop table <T>                                  — remove a table
        drop column [from] [table] <T>: <col> [--cascade]   — remove a column
          (--cascade also drops any index that covers the column)
        drop relationship <name>                        — remove a relationship
        drop index <name>                               — remove an index
        drop index on <T> (<col>, ...)                  — remove an index by its columns
    add: |-
        add column [to] [table] <T>: <col> (<type>)     — add a column
          (for serial/shortid on a non-empty table: existing rows auto-filled)
        add 1:n relationship [as <name>] from <P>.<col> to <C>.<col>
          [on delete <action>] [on update <action>] [--create-fk]    — declare a relationship
        add index [as <name>] on <T> (<col>, ...)       — create an index
    rename: |-
        rename column [in] [table] <T>: <old> to <new>  — rename a column
    change: |-
        change column [in] [table] <T>: <col> (<newtype>) [--force-conversion | --dont-convert]
          — change a column's type (to serial/shortid: null cells auto-filled with generated values)
  data:
    show: |-
        show table <T>             — show a table's structure
        show data <T>              — show a table's rows
        show tables                — list all tables
        show relationships         — list all relationships
        show indexes               — list all indexes
        show relationship <name>   — show one relationship's detail
        show index <name>          — show one index's detail
    seed: |-
        seed <T> [<count>]         — fill a table with generated sample rows
                                     (default 20). Existing rows are kept;
                                     foreign keys draw from existing parent rows.
        seed <T> ... set <c> = 'v' | in ('a','b') | as <gen> | between x and y
                                   — pin how a column is generated: a fixed
                                     value, a pick-list, a named generator
                                     (email, name, product, ...), or a range.
        seed <T>.<col> [set ...]   — fill one column across the EXISTING rows
                                     (the follow-up to `add column`).
        seed <T> ... --seed <n>    — reproducible: same data for the same n.
    insert: |-
        insert into <T> [(cols)] [values] (vals)        — add a row
    update: |-
        update <T> set <c>=<v>, ... where <c>=<v> | --all-rows    — change matching rows
    delete: |-
        delete from <T> where <c>=<v> | --all-rows      — remove matching rows
    replay: |-
        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 project directory.
    explain: |-
        explain show data <T> | explain update <T> ... | explain delete from <T> ...
                                   — show how the database would run a query, without
                                     running it (safe even for update / delete)
    # Issue #36: advanced-mode (SQL) forms. Each has its own help page, listed
    # under the "Advanced-mode (SQL) commands:" section and shown by
    # `help <topic>` alongside its simple-mode sibling — so `help insert` shows
    # both the simple form and `sql_insert` (like `help create` already does).
    select: |-
        select <cols> | * from <T> [where …] [group by …] [order by …] [limit n]
                                                         — query rows (advanced SQL)
    with: |-
        with <name> as (<select>) [, …] <select>        — query through a named
                                                           sub-query / CTE (advanced SQL)
    sql_insert: |-
        insert into <T> (col, …) values (val, …)        — add a row (advanced SQL)
    sql_update: |-
        update <T> set <col> = <val>, … where <expr>    — change matching rows (advanced SQL)
    sql_delete: |-
        delete from <T> where <expr>                    — remove matching rows (advanced SQL)
    explain_sql: |-
        explain <select|with|insert|update|delete …>    — show the plan for a SQL statement,
                                                           without running it (advanced SQL)
  # Type reference, appended after the command list.
  types_reference: |
    Types: text, int, real, decimal, bool, date, datetime, 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.

# ---- DSL parse error rendering --------------------------------------
# ---- Hint panel ambient typing assistance (ADR-0022 §6) -------------
hint:
  # The hint panel goes ambient as soon as the user types
  # anything — empty input keeps the existing
  # `panel.hint_empty` content.
  # Hint sentences are full standalone phrases, capitalised
  # at the start. Inline `{message}` substitutions inherit
  # whatever case the source produced (parser errors,
  # engine messages) — they're embedded mid-sentence so they
  # stay lowercase by convention.
  ambient_complete: "Submit with Enter"
  ambient_expected: "Next: {expected}"
  ambient_error_with_usage: "{message} — usage: {usage}"
  # H2 / ADR-0053: shown by `hint` / F1 when there is nothing specific
  # to expand on (no recent error, empty input).
  getting_started: "Start typing a command and press F1 for a hint, or type `help` for the full command list."
  # Tier-3 block scaffolding (ADR-0053 D4): the heading + the labels the
  # `what` / `example` / `concept` parts render under.
  block:
    heading: "Hint"
    what: "What"
    example: "Example"
    concept: "Concept"
  # ── Tier-3 teaching blocks (ADR-0053 D3) ──────────────────────────
  # Per-form command hints (`hint.cmd.<form>`) and per-class error
  # hints (`hint.err.<class>`), each a `what` (1–2 sentences) / `example`
  # (one runnable, mode-correct line) / `concept` (the relational idea —
  # the teaching part). Phase B seeds the three approved exemplars; the
  # rest are authored in Phase C.
  cmd:
    insert:
      what: "Add one or more rows to a table."
      example: "insert into Customers values ('Ann', 'ann@example.io')"
      concept: "A row is one record; each value lines up with a column, in order. Columns typed `serial`/`shortid` fill themselves — leave them out."
    add_relationship:
      what: "Link two tables so a parent row can own many child rows."
      example: "add 1:n relationship from Customers.id to Orders.customer_id"
      concept: "The \"1:n\" means one parent, many children. The child column holds the foreign key; add `--create-fk` to create that column if it doesn't exist yet."
    # App-lifecycle commands (Phase C batch 1). Reference-leaning, so
    # `concept` appears only where there's a real idea to teach.
    quit:
      what: "Leave the playground. Your project is already saved to disk."
      example: "quit"
    help:
      what: "List every command, or show the detail for one."
      example: "help insert"
      concept: "`help` is the reference; press F1 while typing for a hint about the command you're building right now."
    hint:
      what: "Explain the most recent error — or, pressing F1 while typing, the command you're building."
      example: "hint"
    version:
      what: "Print the application version."
      example: "version"
    rebuild:
      what: "Rebuild the project database from its saved text files."
      example: "rebuild"
      concept: "The text files (project.yaml + the data folder) are the source of truth; the database is derived and can always be rebuilt from them."
    save:
      what: "Save the current project; `save as` copies it to a new name or location."
      example: "save as"
      concept: "On a temporary project, `save` opens a prompt to give it a permanent name; a named project auto-saves as you work, so `save` on one is already done. `save as` always prompts for a new name or path — use it to copy a project."
    new:
      what: "Close the current project and start a fresh temporary one."
      example: "new"
    load:
      what: "Open the project picker to switch to a saved project."
      example: "load"
    export:
      what: "Write a shareable zip of the project — its text files only, never the database."
      example: "export my-shop.zip"
      concept: "The zip carries the schema and data as text, so anyone can rebuild the very same database from it."
    import:
      what: "Unpack a project zip into a new project and switch to it."
      example: "import my-shop.zip as shop_copy"
    mode:
      what: "Switch between simple mode (the guided teaching commands) and advanced mode (raw SQL)."
      example: "mode advanced"
      concept: "Simple mode uses keyword commands; advanced mode lets you write SQL directly. A leading `:` runs a single advanced command without switching modes."
    messages:
      what: "Show or set how much detail error messages give."
      example: "messages short"
      concept: "Verbose (the default) adds a fix-it hint under each error headline; short shows just the headline."
    undo:
      what: "Undo the most recent change, after a confirmation."
      example: "undo"
      concept: "Every data or schema change is snapshotted first, so you can step back; `redo` re-applies what you undid."
    redo:
      what: "Re-apply the most recently undone change."
      example: "redo"
    copy:
      what: "Copy the output panel to the clipboard — all of it, or just the last command's output."
      example: "copy last"
    # DDL — schema-shaping commands (Phase C batch 2).
    create_table:
      what: "Create a new table and declare its primary key."
      example: "create table Customers with pk id(serial)"
      concept: "A table is a set of rows sharing the same columns. `with pk` declares the primary key — one column, or several for a compound key; add the other columns afterwards with `add column`. A `serial` key numbers the rows for you."
    create_m2n:
      what: "Create a junction table linking two tables many-to-many."
      example: "create m:n relationship from Students to Courses"
      concept: "A many-to-many link (a student takes many courses; a course has many students) can't live in either table, so it gets its own junction table holding a foreign key to each side."
    add_column:
      what: "Add a new column to an existing table."
      example: "add column Customers: phone (text)"
      concept: "Existing rows take the column's default, or null. A `not null` column with no default can't be added to a table that already has rows — there'd be nothing to put in them."
    add_index:
      what: "Create an index on one or more columns to speed up lookups."
      example: "add index as idx_email on Customers (email)"
      concept: "An index is a sorted side-structure that makes a lookup like `where email = …` fast, at the cost of a little space and slightly slower writes."
    add_constraint:
      what: "Add a constraint — not null, unique, default, or check — to an existing column."
      example: "add constraint not null to Customers.email"
      concept: "A constraint is a rule the database enforces on every row. Adding one fails if existing rows already break it, so you fix the data first."
    drop_table:
      what: "Remove a table and all of its rows."
      example: "drop table Customers"
      concept: "If other tables reference this one through a relationship, drop those relationships (or their child rows) first — the database won't orphan them."
    drop_column:
      what: "Remove a column from a table."
      example: "drop column Customers: phone"
      concept: "The column's values are lost. You can't drop a primary-key column, or one a relationship depends on."
    drop_relationship:
      what: "Remove a relationship between two tables."
      example: "drop relationship customer_orders"
      concept: "This drops the foreign-key link and stops the database enforcing it; the tables and their rows stay. The foreign-key column itself remains unless you also drop it."
    drop_index:
      what: "Remove an index by name."
      example: "drop index idx_email"
      concept: "Only the lookup shortcut goes — the data is untouched. Queries still work, just without that speed-up."
    drop_constraint:
      what: "Remove a constraint from a column."
      example: "drop constraint not null from Customers.email"
      concept: "The rule stops being enforced from now on; rows already stored are left as they are."
    rename_column:
      what: "Rename a column, keeping its values and type."
      example: "rename column Customers: email to contact_email"
      concept: "Only the name changes — the stored data is the same. References to the column are reconciled so nothing breaks."
    change_column:
      what: "Change a column's type, converting the existing values."
      example: "change column Customers: status (int)"
      concept: "The database converts each stored value to the new type; if a value can't convert it refuses the change, so you don't silently lose data. Flags let you force or skip the conversion."
    # DML — querying and changing data (Phase C batch 3).
    update:
      what: "Change values in the rows that match a condition."
      example: "update Customers set email = 'new@example.io' where id = 1"
      concept: "The `where` clause picks which rows change, and it's required — pass `--all-rows` to change the whole table on purpose — so you never update more than you meant to."
    delete:
      what: "Remove the rows that match a condition."
      example: "delete from Orders where status = 'cancelled'"
      concept: "A `where` is required (use `--all-rows` to clear the table on purpose). Rows a relationship points at may be blocked or cascade-deleted, per its `on delete` action."
    show_data:
      what: "Show the rows stored in a table."
      example: "show data Customers"
      concept: "This reads the data and never changes it. Add a `where` to show only matching rows."
    show_table:
      what: "Show a table's structure — its columns, types, keys, and relationships."
      example: "show table Customers"
      concept: "Structure, not data: the column definitions and how this table links to others. Use `show data` to see the rows themselves."
    show_tables:
      what: "List all the tables in the project."
      example: "show tables"
    show_relationships:
      what: "List all the relationships between tables."
      example: "show relationships"
      concept: "Each relationship is a foreign-key link from a child column to a parent's key, with an `on delete` / `on update` rule."
    show_indexes:
      what: "List all the indexes in the project."
      example: "show indexes"
      concept: "Indexes speed up lookups; this shows which columns each one covers and whether it enforces uniqueness."
    seed:
      what: "Fill a table with generated sample rows, or fill one column on existing rows."
      example: "seed Customers 50"
      concept: "Seeding invents realistic-looking data so you have something to query. Pin a value with `set col = …`, choose a generator with `as`, or give a numeric range with `between`."
    explain:
      what: "Show how the database will run a query — without running it."
      example: "explain show data Customers where email = 'a@example.io'"
      concept: "The plan reveals whether the database scans the whole table or jumps straight to rows through an index — the payoff of `add index`. `explain` never executes, so it's safe even on a delete."
    replay:
      what: "Re-run the commands recorded in a history file."
      example: "replay session.log"
      concept: "Every successful command is journalled, so replaying re-applies them in order to reproduce a project's state — handy for scripting or redoing a sequence."
    # Advanced-mode SQL forms (Phase C batch 4). Examples are SQL, the
    # advanced surface — distinct from their simple-mode siblings.
    sql_create_table:
      what: "Create a table using SQL syntax (advanced mode)."
      example: "create table Customers (id int primary key, name text, email text)"
      concept: "Advanced mode speaks SQL: constraints go inline (`primary key`, `not null`, `unique`, `check`). This is the raw form of simple mode's `create table … with pk …`."
    sql_alter_table:
      what: "Change a table's structure with SQL `alter table` (advanced mode)."
      example: "alter table Customers add column phone text"
      concept: "`alter table` adds or drops columns, renames, and adds constraints — the SQL equivalent of simple mode's `add column` / `drop column` / `change column`."
    sql_create_index:
      what: "Create an index with SQL (advanced mode)."
      example: "create index ix_email on Customers (email)"
      concept: "Add `unique` to also forbid duplicate values. The simple-mode equivalent is `add index`."
    sql_drop_index:
      what: "Remove an index with SQL (advanced mode)."
      example: "drop index ix_email"
      concept: "Only the lookup shortcut goes; the data is untouched. Add `if exists` to ignore a missing index."
    sql_drop_table:
      what: "Remove a table with SQL (advanced mode)."
      example: "drop table Customers"
      concept: "Add `if exists` to avoid an error when the table might not be there. Relationships pointing at it may block the drop."
    sql_insert:
      what: "Insert rows with SQL (advanced mode)."
      example: "insert into Customers (name, email) values ('Ann', 'ann@example.io')"
      concept: "Naming the columns lets you supply them in any order and skip ones that have a default — the SQL form of simple mode's `insert`."
    sql_update:
      what: "Update rows with SQL (advanced mode)."
      example: "update Customers set email = 'new@example.io' where id = 1"
      concept: "`set` lists the new values; `where` picks which rows change. The SQL form of simple mode's `update`."
    sql_delete:
      what: "Delete rows with SQL (advanced mode)."
      example: "delete from Orders where status = 'cancelled'"
      concept: "`where` picks the rows to remove; foreign-key rules still apply. The SQL form of simple mode's `delete`."
    select:
      what: "Query rows with SQL `select` (advanced mode)."
      example: "select name, email from Customers where id = 1"
      concept: "`select` is read-only: choose columns (or `*`), filter with `where`, sort with `order by`, cap with `limit`. This is the heart of SQL — and the reason advanced mode exists."
    with:
      what: "Name a sub-query (a CTE) and read from it in a `select` (advanced mode)."
      example: "with recent as (select * from Orders where id > 100) select * from recent"
      concept: "A `with` clause (Common Table Expression) names a query so the main `select` can use it like a temporary table — handy for breaking a complex query into readable steps."
    explain_sql:
      what: "Show how the database will run a SQL query, without running it (advanced mode)."
      example: "explain select * from Customers where email = 'a@example.io'"
      concept: "Like simple mode's `explain`, but wraps a raw SQL statement. It reveals whether an index is used, and never executes."
  err:
    # Runtime error classes (Phase C batch 5), keyed by
    # friendly::error_hint_class. `example` is a fix recipe rather than a
    # runnable line; `concept` is the relational idea behind the rule.
    foreign_key:
      child_side:
        what: "The value you gave for the child column doesn't match any parent row, so the foreign key has nothing to point at."
        example: "First insert the parent (insert into Customers …), then the child that references it."
        concept: "A foreign key is a promise that every child points at a real parent, so the parent must exist before a child can reference it. (`on delete` actions like `cascade` or `set null` govern the other direction — what happens to children when their parent is removed — not this one.)"
      parent_side:
        what: "You're deleting or changing a row that other rows point at, which would orphan those children."
        example: "Delete the child rows first, or set the relationship's `on delete` to `cascade` (remove them too) or `set null` (keep them, unlinked)."
        concept: "A foreign key guarantees every child has a real parent, so the database won't remove a parent out from under its children unless the relationship says what should happen to them."
    unique:
      what: "A value you're inserting — or updating to — already exists in a column that must be unique."
      example: "Pick a different value, or update the existing row instead of inserting a new one."
      concept: "A unique constraint (and every primary key) forbids duplicates, so each value identifies at most one row."
    not_null:
      what: "You left a column empty that is required to have a value."
      example: "Supply a value for the column, or give it a default so new rows fill it automatically."
      concept: "A `not null` constraint means every row must have a value there — it's how you mark a fact as mandatory."
    check:
      what: "A value broke a `check` rule defined on the column."
      example: "Use a value the rule allows — for example a positive number, or one of the permitted options."
      concept: "A `check` constraint is a condition every row must satisfy, so the database enforces business rules like \"price ≥ 0\" for you."
    type_mismatch:
      what: "A value doesn't fit the column's type — for instance text where a number is expected."
      example: "Give a value of the right type: a number for `int`/`real`, a quoted string for `text`, true/false for `bool`."
      concept: "Every column has a type, and the database rejects values that don't fit, so a column's data stays consistent and comparable."
    not_found:
      what: "You named a table or column that doesn't exist."
      example: "Check the spelling, or run `show tables` (or `show table <name>`) to see what's there."
      concept: "A command can only refer to tables and columns that already exist — create them first if you need them."
    already_exists:
      what: "You tried to create a table, column, relationship, or index whose name is already taken."
      example: "Pick a different name, or drop the existing one first if you meant to replace it."
      concept: "Names must be unique within their kind so a command is never ambiguous about what it refers to."
    generic:
      what: "The database refused the command for the reason shown above."
      example: "Read that message for the specifics, adjust the command, and try again."
    invalid_value:
      what: "A value or option in the command wasn't valid for where it was used."
      example: "Check the value against the column's type and the command's accepted options."
  # Invalid identifier in a schema slot (ADR-0022 stage 8e
  # + the user's #5). Voice mirrors ADR-0019's "no such
  # {kind}" wording for consistency with engine errors.
  ambient_invalid_ident: "No such {kind}: `{found}`"
  # User-invented-name slot (NewName per IdentSlot). The
  # probe-derived `{next}` is what comes after the name —
  # e.g. `(` after a new column name. Empty/unknown `next`
  # falls through to `ambient_typing_name` instead.
  ambient_typing_name: "Type a name"
  ambient_typing_name_then: "Type a name, then {next}"
  # Issue #4 — advanced-mode CREATE TABLE element slot. Surfaced
  # at `create table T (` so the column-name role is visible
  # alongside the table-level constraint keywords.
  create_table_element: "Type a column name, or a table-level constraint: `primary`, `unique`, `check`, `constraint`, `foreign`"
  # Issue #26: the `seed <table> ▮` position. The optional row count is
  # a bare number with no Tab candidate, so it (and the `.column`
  # column-fill form) would be invisible next to the `set` / `--seed`
  # chips. Names every option so the most common next move (a count) is
  # discoverable.
  seed_count: "Optionally a row count, e.g. `50` (default 20); `.column` to fill one column on existing rows; `set` to pin a column; `--seed` to fix the RNG"
  # Value-literal slot — `insert ... values (`, `update ... set
  # col=`, `where col=`. Replaces the misleading "null true
  # false" keyword candidate list with format guidance for all
  # accepted literal forms. Used when the walker can't resolve a
  # column type (schemaless parse, missing table, unknown column).
  value_literal_slot: "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')"
  # Per-column-type value-slot hints (ADR-0024 §Phase D §typed-value-slots).
  # Fired when the walker resolved the column's user-facing type
  # at the current value slot; narrows the prose to the relevant
  # literal forms for that type. Falls back to
  # `value_literal_slot` when the type can't be resolved.
  value_slot_int: "Type an integer (e.g. 42, -7) or null"
  value_slot_real: "Type a number (e.g. 3.14, -0.5) or null"
  value_slot_decimal: "Type a number (e.g. 19.95, -2.50) or null"
  value_slot_bool: "Type true, false, or null"
  value_slot_text: "Type a quoted string (e.g. 'Alice') or null"
  value_slot_date: "Type a quoted date as 'YYYY-MM-DD' or null"
  value_slot_datetime: "Type a quoted datetime as 'YYYY-MM-DD HH:MM:SS' or null"
  # Serial / shortid in `values (…)` form: the user must enter
  # something at this position (no "skip column" syntax). `null`
  # is the auto-fill path (ADR-0018: serial / shortid columns
  # auto-fill null cells on insert), so the prose leads with
  # null and offers an explicit value as the alternative.
  value_slot_serial: "Type null to auto-generate, or an explicit integer"
  value_slot_shortid: "Type null to auto-generate, or a quoted shortid"
  # Wrapper that prefixes a per-column-type slot hint with the
  # actual column name so the user sees "for `Email`: Type a
  # quoted string …" instead of the generic type prose.
  value_slot_for_column: "for `{column}`: {detail}"
  # Pedagogical note appended at the first value slot of a
  # Form B `insert into T values (…)` when T has auto-generated
  # columns the value list skips — points the user at the
  # explicit-column form so the skipped column is discoverable.
  value_slot_autogen_skipped: "({columns} auto-generated — skipped here; list columns explicitly, e.g. `insert into T (...) values (...)`, to set it.)"

# Advanced-mode SQL DDL execution notes (ADR-0035).
ddl:
  # `create table if not exists <T>` where the table is already
  # present: a no-op that succeeds with this note instead of an
  # "already exists" error.
  create_skipped_exists: "table '{name}' already exists — skipped (no changes made)"
  # `drop table if exists <T>` where the table is absent: a no-op that
  # succeeds with this note instead of a "doesn't exist" error.
  drop_skipped_absent: "table '{name}' doesn't exist — skipped (no changes made)"
  # `create [unique] index if not exists <name> …` where the index name
  # already exists: a no-op that succeeds with this note (ADR-0035 §4d).
  create_index_skipped_exists: "index '{name}' already exists — skipped (no changes made)"
  # `drop index if exists <name>` where the index is absent: a no-op that
  # succeeds with this note instead of a "doesn't exist" error.
  drop_index_skipped_absent: "index '{name}' doesn't exist — skipped (no changes made)"

parse:
  # Wrapper around chumsky's structural error message. The
  # caret pointer (visualising the failure column) is printed
  # on its own preceding line via `parse.caret`.
  error: "parse error: {detail}"
  # Wrapper used by `ParseError::Display` (so any to_string()
  # call on a parse error renders consistently with the in-app
  # error rendering).
  error_wrapper: "could not parse command: {detail}"
  # Custom (try_map / source-slice) error messages raised by
  # the DSL parser. These were hand-written strings in
  # `src/dsl/parser.rs` until the catalog migration brought
  # them under the same roof as the rest of the user-facing
  # vocabulary. Wording is unchanged from the inline source
  # form so existing anchor-phrase tests still match.
  custom:
    replay_path_expected: "expected a path after `replay`"
    create_table_needs_pk: |-
      tables need at least one column. Add `with pk` for a default `id INTEGER PRIMARY KEY`, or `with pk <name>(<type>)` to choose. Use a comma-separated list for compound primary keys.
    # ADR-0026 §1: the recursion-depth guard on the
    # WHERE-expression grammar. Input nested past the cap
    # (`((((…))))`) stops here with a friendly error instead
    # of overflowing the parser stack.
    expression_too_deep: "expression nested too deeply"
    on_action_specified_twice: "`on {target}` specified twice"
    change_column_flags_exclusive: "`--force-conversion` and `--dont-convert` are mutually exclusive — pick one."
    # ADR-0035 §4g: adding a primary key to an existing table is not
    # supported — every table is created with its primary key.
    alter_add_primary_key: "a table's primary key is fixed at creation — `alter table … add primary key` is not supported."
    # ADR-0035 §4g: composite UNIQUE constraints are unnamed in this
    # tool, so a `constraint <name>` prefix on UNIQUE has nowhere to go.
    alter_named_unique: "a UNIQUE constraint cannot be named — use `alter table <T> add unique (<col>, …)` without `constraint <name>`."
    unknown_type: "unknown type '{found}' (expected one of: {expected})"
    unknown_action: "unknown referential action '{found}' (expected one of: {expected})"
    # Phase D typed-value-slot mismatch (ADR-0024 §Phase D):
    # surfaced when a column's value slot rejects the literal
    # the user typed (e.g. `3.14` at an `int` column). `{found}`
    # is the literal text; `{expected}` names the required
    # shape (`integer`, `number`, …).
    bind_type_mismatch: "value '{found}' is not a valid {expected}"
    # `insert into T (col)` with no `values (...)` afterwards
    # — the shared `(…)` opener matched as a Form-A column
    # list but the user hasn't typed the value clause yet.
    # Surfaced as a parse-time error so the input renderer
    # classifies the input as mid-typing rather than
    # dispatching a logically-empty Form C insert.
    insert_form_a_missing_values: "`insert into ...({columns})` looks like Form A — add `values (...)` to supply the matching values."
    # ADR-0029 §9: a primary-key column is already NOT NULL,
    # and a single-column primary key is already UNIQUE —
    # declaring either explicitly is redundant.
    constraint_redundant_on_pk: "`{column}` is a primary-key column, so it is already {constraint} — drop the redundant constraint."
  # Caret pointer showing where in the input the parser
  # failed. `{padding}` is the leading whitespace; the
  # template appends `^` so the rendered line places the
  # marker under the offending character.
  caret: "{padding}^"
  # Default for the `ParseError::Empty` variant — surfaces as
  # `{detail}` inside the wrapper.
  empty: "empty input"
  # No-prefix fallback (ADR-0021 §5): when the parse fails
  # before any keyword is consumed, the renderer lists every
  # command-entry keyword instead of attempting a per-command
  # usage block. `{commands}` is an oxford-joined list of
  # command-keyword renderings (each from
  # `parse.token.keyword.*`).
  available_commands: "available commands: {commands}"
  # ADR-0042 G2: collapse the SELECT projection-start expression
  # first-set (14 expression-starters plus the `distinct`/`all`
  # quantifiers) into one learner-sized gloss in the error
  # message. The detector keys on `distinct` AND `all` being
  # jointly expectable, which only happens at a projection start —
  # so the raw set is replaced *in the error line only*;
  # completion/hints still expand the full first-set.
  expect:
    select_projection: "a projection: `*`, a column, or an expression"
  # ADR-0042 §3: a CROSS JOIN pairs every row and takes no ON
  # clause. The grammar rejects a following `on`; this message
  # (rendered in place of the generic structural error when the
  # most recent join is a CROSS join and the failing token is `on`)
  # teaches the distinction instead of just "expected end of input".
  cross_join_no_on: "a CROSS JOIN has no ON clause — it pairs every row; for a join condition use `JOIN … ON`, or filter with `WHERE`"
  # Per-command usage templates (ADR-0021 §1). Rendered under a
  # "usage:" prefix when a parse fails after consuming a
  # known command-entry keyword. The bracket convention `[...]`
  # marks optional parts; angle-bracket `<...>` marks
  # placeholders. ADR-0009's surface conventions apply.
  usage:
    create_table: "create table <Name> with pk [<col>(<type>)[, ...]]"
    create_m2n: "create m:n relationship from <Table1> to <Table2> [as <Name>]"
    # Terse one-line synopsis (issue #12): the full grammar — every
    # column- and table-level constraint — lives in `help.ddl.sql_create_table`.
    sql_create_table: "create table [if not exists] <Name> (<col> <type> [constraints], ...)"
    sql_drop_table: "drop table [if exists] <Name>"
    sql_create_index: "create [unique] index [if not exists] [<Name>] on <Table> (<col>[, ...])"
    sql_drop_index: "drop index [if exists] <Name>"
    sql_alter_table: |-
      alter table <Table> add column <Name> <Type> [not null] [unique] [default <expr>] [check (<expr>)]
      alter table <Table> drop column <Name>
      alter table <Table> rename column <Old> to <New>
      alter table <Table> rename to <NewName>
      alter table <Table> alter column <Name> type <Type>
      alter table <Table> add [constraint <Name>] check (<expr>) | unique (<col>, ...) | foreign key (<col>) references <Parent>[(<col>)]
      alter table <Table> drop constraint <Name>
    drop_table: "drop table <Name>"
    drop_column: "drop column [from] [table] <Table>: <Name>"
    drop_relationship: |-
      drop relationship <Name>
      drop relationship from <Parent>.<col> to <Child>.<col>
    drop_index: |-
      drop index <Name>
      drop index on <Table> (<col>[, ...])
    drop_constraint: "drop constraint (not null | unique | default | check) from <Table>.<col>"
    add_column: "add column [to] [table] <Table>: <Name> (<Type>)"
    add_relationship: |-
      add 1:n relationship [as <Name>]
        from <Parent>.<col> to <Child>.<col>
        [on delete <action>] [on update <action>]
        [--create-fk]
    add_index: "add index [as <Name>] on <Table> (<col>[, ...])"
    add_constraint: |-
      add constraint not null to <Table>.<col>
      add constraint unique to <Table>.<col>
      add constraint default <value> to <Table>.<col>
      add constraint check (<expr>) to <Table>.<col>
    rename_column: "rename column [in] [table] <Table>: <Old> to <New>"
    change_column: |-
      change column [in] [table] <Table>: <Name> (<Type>)
        [--force-conversion | --dont-convert]
    seed: "seed <Table> [count] [set <col> = ... | in (...) | as <gen> | between x and y]  |  seed <Table>.<col>"
    show_data: "show data <Table>"
    show_table: "show table <Table>"
    show_tables: "show tables"
    show_relationships: "show relationships"
    show_indexes: "show indexes"
    show_relationship: "show relationship <name>"
    show_index: "show index <name>"
    insert: "insert into <Table> [(<col>[, ...])] [values] (<value>[, ...])"
    update: "update <Table> set <col>=<value>[, ...] (where <col>=<value> | --all-rows)"
    delete: "delete from <Table> (where <col>=<value> | --all-rows)"
    explain: |-
      explain show data <Table> [where <expr>] [limit <n>]
      explain update <Table> set <col>=<value>[, ...] (where <expr> | --all-rows)
      explain delete from <Table> (where <expr> | --all-rows)
      explain <SQL select | with | insert | update | delete>   (advanced mode)
    replay: "replay <path>  |  replay '<path with spaces>'"
    # SQL `SELECT` (advanced mode; ADR-0030 / ADR-0031).
    select: "select (* | <expr>[ as <alias>][, ...]) from <Table> [where <expr>] [order by <expr>[ asc|desc][, ...]] [limit <n>]"
    # SQL `WITH` / CTE (advanced mode; ADR-0032). G4 (ADR-0042):
    # its own template — `with` previously borrowed `select`'s,
    # which never showed the CTE shape.
    with: "with [recursive] <Name> [(<col>[, ...])] as (<query>)[, ...] select ..."
    # App-lifecycle commands (per ADR-0003, surfaced through
    # the parser so they participate in usage templates +
    # completion). Templates here describe the surface
    # grammar that the parser accepts; the in-app `help`
    # listing in `help.in_app_body` carries the user-facing
    # description.
    quit: "quit"
    help: "help [<command>]"
    hint: "hint"
    version: "version"
    rebuild: "rebuild"
    save: "save  |  save as"
    new: "new"
    load: "load"
    export: "export [<path>]"
    import: "import <zip-path> [as <target>]"
    mode: "mode simple  |  mode advanced"
    messages: "messages  |  messages short  |  messages verbose"
    undo: "undo"
    redo: "redo"
    copy: "copy  |  copy all  |  copy last"

# ---- Pre-submit diagnostics (ADR-0027) -------------------------------
# Surfaced by the validity indicator and the hint panel before
# a command is submitted, so a learner sees the problem without
# having to run the command and read the engine's error.
diagnostic:
  unknown_table: "no such table: `{name}`"
  unknown_column: "no such column `{name}` on table `{table}`"
  # WARNING-severity flags (ADR-0026 §7) — the command still
  # runs, but the comparison is very likely not intended.
  type_mismatch: "`{column}` is {type} — this comparison uses a value of a different type"
  eq_null: "`= NULL` is never true — use `IS NULL` or `IS NOT NULL`"
  # ADR-0027 Amendment 1: LIKE is a text-pattern match.
  like_numeric: "`{column}` is {type} — `LIKE` is a text-pattern match, not a {type} comparison"
  # Phase-2 diagnostic keys (ADR-0032 §11.5).
  unknown_qualifier: "no such table or alias in scope: `{qualifier}`"
  ambiguous_column: "`{column}` is ambiguous — appears in {qualifiers}"
  projection_alias_misplaced: "alias `{alias}` cannot be used in {clause} — aliases are not bound until after `SELECT`'s projection list"
  # Issue #31: a bare table alias / table name used where the grammar
  # expects a column (e.g. `GROUP BY o`). The name *is* in scope — it
  # is the alias of a FROM source — so calling it an "unknown column"
  # misleads. Point the learner at the qualified `alias.column` form.
  alias_used_as_column: "`{name}` is a table alias — write `{name}.<column>` to reference one of its columns"
  table_used_as_column: "`{name}` is a table — write `{name}.<column>` to reference one of its columns"
  cte_arity_mismatch: "CTE `{cte}` declares {declared} columns but its body has {actual}"
  compound_arity_mismatch: "`{op}` requires both sides to have the same number of columns — left has {left_n}, right has {right_n}"
  duplicate_cte: "duplicate `WITH` table name: `{name}`"
  # ADR-0033 §8 — Phase-3 DML diagnostic keys.
  auto_column_overridden: "column `{column}` is auto-generated (`{type}`); providing an explicit value bypasses the auto-counter and may collide with later auto-generated values"
  insert_arity_mismatch: "the column list names {expected} column(s) but {actual} value(s) are given"
  # ADR-0033 §8.1 / Amendment 5: Form B (no column list) variant
  # (advanced mode — auto-fills nothing, so every column needs a value).
  insert_arity_mismatch_form_b: "with no column list, all {expected} column(s) need a value but {actual} value(s) are given"
  # ADR-0036 Amendment 1 / issue #17: simple-mode Form B. The DSL
  # auto-fills serial/shortid columns, so only the user-fillable columns
  # take values — name both sets so the learner understands the skip.
  insert_arity_mismatch_form_b_simple: "without a column list, supply {expected} value(s) for {columns} — {skipped} auto-generated; {actual} given"
  # ADR-0036 Amendment 1 / issue #17: simple-mode Form B where every
  # column is auto-generated, so the values list takes nothing.
  insert_arity_mismatch_all_auto: "every column of `{table}` is auto-generated, so no values are needed, but {actual} value(s) are given"
  not_null_missing: "column `{column}` is required (`NOT NULL`, no default); the statement will fail when run"

# Engine-error translations: an engine-rejected SQL statement
# reaches the friendly-error layer (ADR-0019) and these keys
# render its message in engine-neutral wording (ADR-0030 §7).
# The keys are reached only after a parse-time pass let the
# query through and the engine rejected at execution.
engine:
  no_such_table: "no such table: `{name}`"
  no_such_column: "no such column: `{name}`"
  ambiguous_column: "`{column}` is ambiguous — qualify it with a table or alias"
  aggregate_misuse: "`{name}()` cannot be used here — aggregates belong in projection or `HAVING`, not `WHERE`"
  group_by_required: "non-aggregated columns must appear in a `GROUP BY` clause or be wrapped in an aggregate function"
  compound_arity_mismatch: "`{op}` requires both sides to have the same number of columns"
  scalar_subquery_too_many_rows: "scalar subquery returned more than one row — use `IN (…)` or limit the inner query"
  recursive_cte_malformed: "recursive CTE shape is invalid — needs a non-recursive base case combined with `UNION`/`UNION ALL`"

# ---- Project lifecycle event notes -----------------------------------
project:
  rebuild_ok: "[ok] rebuild — {summary}"
  rebuild_failed: "rebuild failed: {error}"
  switched_ok: "[ok] now editing: {display_name}"
  switch_failed: "project switch failed: {error}"
  export_ok: "[ok] export — wrote {path}"
  export_failed: "export failed: {error}"
  # Usage / argument-parsing errors for app-level commands.
  export_usage: "usage: export [<path>]"
  import_usage: "usage: import <zip-path> [as <target>]"
  import_empty_target: "import: empty target after `as`"
  # Project-switch validation failures (load / save-as /
  # import). Returned from the runtime as Err(String) and
  # surfaced via project.switch_failed.
  load_path_missing: "path `{path}` does not exist"
  saveas_target_exists: "`{path}` already exists; pick a different name or remove it first"
  import_zip_missing: "zip `{path}` does not exist"
  # --resume CLI failures printed to stderr before the TUI
  # starts (ADR-0015 §7). Wording stays one line for clean
  # piping; the runtime prepends `rdbms-playground: ` from
  # the calling code itself.
  resume_recorded_missing: "--resume: recorded project `{path}` no longer exists"
  resume_no_previous: "--resume: no previous project recorded under `{data_root}`"
  # Project-lock errors (single-instance enforcement, ADR-0015 §6).
  lock:
    already_held: |-
      project is already open in another rdbms-playground process (pid {pid} on host `{hostname}`); close that process or remove `{path}` if you're sure it's not running
    write: "could not write lock file `{path}`: {source}"
    read: "could not read existing lock file `{path}`: {source}"
  # Temp-project name generation failures.
  naming:
    wordlist_too_small: "wordlist must contain at least 3 entries; found {count}"
    too_many_collisions: |-
      could not generate a non-colliding temp project name after {attempts} attempts
  # User-typed project name validation failures.
  user_name:
    empty: "project name cannot be empty"
    leading_dot: "project name cannot start with `.`"
    invalid_char: "project name cannot contain `{ch}`; use letters, digits, `-`, `_`, or `.` only"
  # ProjectError variants (ProjectError Display path).
  data_root_unavailable: |-
    could not determine the OS-standard data directory; pass --data-dir to override
  path_not_found: "project path `{path}` does not exist"
  not_a_project: |-
    path `{path}` does not look like a project directory (no `project.yaml` and no `playground.db`)
  already_exists: "path `{path}` already exists; pick a different name or remove it first"
  io: "filesystem error at `{path}`: {source}"
  # SafeDeleteError — surfaces in logs when temp-project cleanup
  # refuses to delete a path (ADR-0015 §13).
  safe_delete:
    refused: "refusing to delete `{path}`: {reason}"
    io: "io error on `{path}`: {source}"

# ---- DSL failure wrapper + advanced-mode placeholder + fatal --------
dsl:
  # The friendly-error layer's translated reason, shown beneath the
  # failed command's echo line. ADR-0040: the redundant
  # `"<verb> <subject>" failed:` prefix was dropped — the echo line
  # carries the ✗ marker and names the command.
  failed: "{rendered}"
  # Echo line `running: <input>` shown above each command's
  # response while it executes; ADR-0040 resolves it to
  # `<input> ✓` / `<input> ✗` on completion (the marker replaces
  # the old `[ok] <verb> <subject>` summary line).
  running: "running: {input}"

# ---- Value-validation errors (per-column at bind time) --------------
value:
  type_mismatch: "column `{column}` expects {expected_human}, got {got}"
  format: "column `{column}`: {message}"

# ---- Archive / zip errors (export / import) -------------------------
archive:
  io: "io error on `{path}`: {source}"
  zip: "zip error on `{path}`: {message}"
  export_sequence_exhausted: |-
    could not pick an export filename for `{project}` in `{target_dir}`: all sequence numbers up to {limit} are taken
  import_collision_exhausted: |-
    destination `{path}` already exists and the auto-suffix retries (-02 through -{limit}) are also taken; use `import <zip> as <target>` to choose a different name
  invalid_zip: "zip is malformed: {detail}"
  not_a_project_archive: |-
    zip does not contain a project (no `project.yaml` under a single top-level folder)
  multiple_top_folders: "zip contains more than one top-level folder; refusing to extract"
  unsafe_entry: "zip entry `{entry}` would escape the target directory; refusing to extract"

# ---- Persistence-layer errors (CSV/YAML/IO) -------------------------
# These were thiserror Display attributes pre-round-6. Most surface
# only as the inner `{message}` of `fatal.persistence` or as the
# wrapped detail inside `DbError::PersistenceFatal`.
persistence:
  io: "could not {operation} `{path}`: {source}"
  encode: "could not encode {kind} for `{path}`: {message}"
  csv:
    empty: "CSV is empty"
    invalid_utf8: "invalid UTF-8 in CSV body"
    unterminated_quote: "unterminated quoted field"
  yaml:
    syntax: "project.yaml syntax error: {detail}"
    unsupported_version: "unsupported project.yaml version: {version} (expected 1)"
    unknown_type: "unknown user-facing column type `{raw}` for `{table}.{column}`"
    unknown_action: "unknown referential action `{raw}`"
  migrate:
    version_parse: "could not read version field from project.yaml: {detail}"
    newer_than_supported: |-
      project.yaml is at version {file} but this build only understands up to version {latest}; upgrade the application or restore an older project.yaml
    no_migrator: |-
      no migrator registered for version {version} (programmer error: registry latest_version disagrees with migrators length)
    step_failed: "migrator from v{from} to v{to} failed: {source}"
    bad_output: "migrator produced an unparseable result: {detail}"
    io: "io error during migration on `{path}`: {source}"

# ---- Advanced-mode SQL surface (ADR-0030) ---------------------------
advanced_mode:
  not_implemented: "advanced mode SQL not implemented yet — echo: {input}"
  sql_in_simple: "`{command}` is SQL — available in advanced mode. Switch with `mode advanced`, or prefix the line with `:` to run it once."
  # Appended to a simple-mode DSL error when the same line would run
  # in advanced mode — keeps the actionable DSL fix and adds the mode
  # pointer (ADR-0033 Amendment 3). Suppressed by `advanced_alternative_note`
  # for cases the advanced engine would also reject (issue #1).
  also_valid_sql: "(trying to write SQL? switch with `mode advanced`, or prefix `:` to run once)"

# ---- Insert education hints (issue #1 sub-task 2) -------------------
# Appended to a simple-mode INSERT Form B parse error when the user
# supplied more values than Form B's non-auto-generated columns expect
# (but not more than every column). Teaches the contract that excludes
# `serial` / `shortid` columns and points at the column-list form.
insert:
  form_b_extra_values_note: "`insert into {table} values (…)` expects {expected_phrase} — {auto_phrase}. To set them explicitly, list every column: `insert into {table} ({all_cols}) values (…)`."
  # Pre-flight teaching note for advanced-mode positional
  # `INSERT INTO T VALUES (…)` (no column list) when the value count
  # doesn't match the column count (issue #1 sub-task 3). The override
  # uses the non-auto columns so the user can omit serial/shortid.
  form_b_positional_count_mismatch_note: "Positional `insert into {table} values (…)` requires a value for every column. `{table}` has {col_count} columns ({col_list}); you supplied {supplied}. To omit auto-generated columns, list the columns you want: `insert into {table} ({non_auto_csv}) values (…)`."

# ---- SQL SELECT (advanced mode; ADR-0030 / ADR-0031) ----------------
select:
  internal_table: "`{table}` is an internal system table and cannot be queried."

# ---- Persistence-fatal banner (ADR-0015 §8) -------------------------
fatal:
  persistence: "FATAL: failed to {operation} `{path}` — {message}. Quitting; investigate and restart."

# ---- Modal labels (load picker, rebuild confirm, save-as path) ------
modal:
  rebuild_cancelled: "rebuild cancelled"
  load_cancelled: "load cancelled"
  generic_cancelled: "{title} cancelled"
  path_entry_empty_name: "path entry: empty name"
  path_entry_empty_path: "path entry: empty path"
  load_picker_nothing: "nothing to load"
  # Modal titles + body prose rendered by ui.rs.
  load_picker_title: "Load project"
  load_picker_empty: "(no projects in data directory)"
  load_picker_path_prompt: "Path to project directory:"
  rebuild_confirm_title: "Rebuild project"
  rebuild_confirm_prompt: "Continue?"
  # Undo / redo confirmation (ADR-0006 Amendment 1).
  undo_confirm_title: "Undo last change"
  redo_confirm_title: "Redo last undone change"
  undo_confirm_command: "This will undo:"
  redo_confirm_command: "This will re-apply:"
  undo_confirm_when: "Snapshot taken {timestamp}"
  undo_confirm_prompt: "Restore that earlier state?"
  undo_cancelled: "undo cancelled"
  redo_cancelled: "redo cancelled"

# ---- Undo / redo command surfaces (ADR-0006 Amendment 1) -------------
undo:
  disabled: "undo is turned off for this session (started with --no-undo)"
  nothing_to_undo: "nothing to undo"
  nothing_to_redo: "nothing to redo"
  undone_ok: "undone: {command}"
  redone_ok: "re-applied: {command}"
  undo_failed: "could not undo: {error}"
  redo_failed: "could not redo: {error}"

# ---- Save / save-as command surfaces ---------------------------------
save:
  # `save` on a named project is a no-op with a friendly hint.
  already_saved: "already auto-saved; use `save as` to copy to a different location"
  # Modal titles for `save` (on a temp) vs `save as`.
  title_as: "Save as"
  title_save: "Save"
  # Prompt body for the path-entry modal opened by save / save as.
  path_prompt: "Name (under data dir/projects) or absolute path:"

# ---- Status bar (project label) and panels ---------------------------
status:
  no_project: "(no project)"
  project_label: "Project: "

panel:
  tables_title: "Tables"
  tables_empty: "(none yet)"
  relationships_title: "Relationships"
  relationships_empty: "(none)"
  hint_empty: "Type a command — press Tab for options, `help` for a list"
  # Mode-discovery pointer appended to the empty-input hint in SIMPLE
  # mode (ADR-0051): the `mode advanced` switch left the keybinding
  # strip, so the hint advertises it. Leading separator continues the
  # prompt line. Advanced mode shows no pointer — users know how they
  # got there, and `help` covers the way back.
  hint_mode_advanced: " · `mode advanced` for SQL"
  # Panel titles for the output and hint panels (rendered inside
  # the rounded border, hence the leading/trailing space).
  output_title: "Output"
  hint_title: "Hint"

# ---- Shortcut hints (paired with key names in the bottom bar) -------
# The bottom strip is keystrokes-only and state-aware (ADR-0051). Labels
# pair with a key name in the renderer (e.g. `Enter` + `run`).
shortcut:
  confirm: "confirm"
  cancel: "cancel"
  yes: "Yes"
  no: "No"
  load: "load"
  select: "select"
  browse_path: "browse path"
  back_to_list: "back to list"
  # Status-strip labels (ADR-0051, issue #27).
  run: "run"
  nav: "sidebar"
  next_pane: "next pane"
  scroll: "scroll"
  to_input: "input"
  cycle: "cycle"
  browse: "browse"
  clear: "clear"
  complete: "complete"
  hint: "hint"
  history: "history"
  home_end: "home/end"
  del_word: "del word"

# ---- mode / messages banners (app-level commands) -------------------
mode:
  set_simple: "mode: simple"
  set_advanced: "mode: advanced"
  show_simple: "mode: simple"
  show_advanced: "mode: advanced"
  usage: "usage: mode simple | mode advanced"
  unknown: "unknown mode '{value}' (expected 'simple' or 'advanced')"
  # Labels rendered inside the input panel's border to mark the
  # current input mode. `label_advanced_one_shot` is shown
  # while a `:` one-shot is in flight from simple mode.
  label_simple: "SIMPLE"
  label_advanced: "ADVANCED"
  label_advanced_one_shot: "Advanced:"

messages:
  show: "messages: {current}"
  set_short: "messages: short"
  set_verbose: "messages: verbose"
  unknown: "unknown messages mode '{value}' (expected 'short' or 'verbose')"

# ---- copy (app-level command, ADR-0041) -----------------------------
copy:
  done: "copied {count} line(s) to the clipboard"
  nothing: "nothing to copy — the output panel is empty"
  unknown: "unknown copy target '{value}' (expected 'all' or 'last')"

# ---- Database-error fallback wording + cascade summaries ------------
db:
  # DbError variants — fallback Display wording for paths that
  # bypass the structured friendly translator (fatal banners,
  # plain to_string() calls). The normal path goes through
  # `friendly::translate_error` which routes by the `kind`
  # field and renders catalog wording from `error.*` instead.
  error:
    sqlite: "database error: {message}"
    unsupported: "operation not supported: {detail}"
    invalid_value: "invalid value: {detail}"
    persistence_fatal: "could not {operation} `{path}`: {message}"
    rebuild_row_failed: |-
      unable to load row {row_number} from `{csv_path}` into table `{table}`: {detail}
    worker_gone: "database worker is no longer available"
    io: "io error: {detail}"

  cascade:
    # Per-relationship cascade summary appended to a delete
    # success note. The same template handles cascade,
    # set-null, and restrict/no-action cases — `{action}` is
    # one of the three action phrases below.
    summary: "  related: {count} row(s) {action} in `{child_table}` for relationship `{rel}` (on delete {on_delete})"
    action_deleted: "deleted"
    action_set_null: "had FK set to null"
    action_blocked: "blocked"

  # `change column ... (newtype)` dry-run diagnostics (ADR-0017).
  # Surface when the migration would lose information (lossy),
  # produce values the target type can't represent
  # (incompatible), or violate a uniqueness contract (collision).
  diagnostic:
    # Column headers for the diagnostic tables.
    header_from: "From"
    header_to: "To"
    header_reason: "Reason"
    header_value: "Value"
    header_becomes: "Becomes"
    header_source_rows: "Source rows ({pk_label})"
    header_source_values: "Source values"
    # Summary lines printed above each diagnostic table.
    lossy_summary: |-
      Cannot change `{table}.{column}` from {src_ty} to {target_ty}: {total} row(s) would discard information.
    incompatible_summary: |-
      Cannot change `{table}.{column}` from {src_ty} to {target_ty}: {total} row(s) cannot be converted.
    uniqueness_summary: |-
      Cannot change `{table}.{column}` from {src_ty} to {target_ty}: {total} collision(s) would violate uniqueness.
    # Follow-up suggestion appended to the lossy diagnostic
    # (only — incompatibles can't be force-overridden).
    force_conversion_hint: "if you want to execute this conversion in spite of the problems, re-run with `--force-conversion`."
    # `add constraint ...` dry-run refusals (ADR-0029 §5).
    # Surface when the column's existing rows would violate the
    # constraint being added; the offending rows follow in a
    # diagnostic table. There is no force override — the user
    # fixes the data and retries.
    add_not_null_summary: |-
      Cannot add NOT NULL to `{table}.{column}`: {total} row(s) hold a null value.
    add_unique_summary: |-
      Cannot add UNIQUE to `{table}.{column}`: {total} value(s) appear in more than one row.
    add_check_summary: |-
      Cannot add this CHECK to `{table}.{column}`: {total} row(s) do not satisfy `{rule}`.

# ---- DSL command success summaries (ADR-0019 §9 sweep) --------------
# (The DSL → SQL teaching echo's `Executing SQL:` prefix used to live
# here as `echo.executing_sql`; with the ADR-0038 §4 styled-runs polish
# the line is now built from a hardcoded constant in `crate::echo`
# because the dim-prefix + lex-the-rest rendering path in
# `ui::render_output_line` needs a fixed byte boundary the i18n
# template couldn't provide. Re-introduce a key here if a non-English
# locale lands.)

# Seed-command notes (ADR-0048): the cap note when the unique-value
# space is exhausted, and the advisory that flags columns filled with
# generic text that look like fixed value sets.
seed:
  capped: "(of {requested} requested — ran out of distinct value combinations)"
  # ADR-0048 D13 (Phase 2/3 wording): name the generically-filled
  # enum-ish / CHECK columns and point at the concrete repairs — the
  # `set` clause on a fresh seed, or the column-fill form for the rows
  # just created.
  advisory_generic: "{columns} filled with generic text — they look like fixed value sets. Pin them next time with `set {column} in ('…', '…')`, or fix these rows with `seed {table}.{column} set {column} in ('…', '…')`."

ok:
  # ADR-0040: the generic `[ok] <verb> <subject>` summary line was
  # retired — a successful command's echo line now carries a ✓
  # marker (the verb+subject duplicated the echo above it). The
  # per-operation row-count footers below still convey real payload
  # and are unchanged.
  rows_inserted: "  {count} row(s) inserted"
  rows_seeded: "  {count} row(s) seeded into {table}"
  rows_updated: "  {count} row(s) updated"
  rows_deleted: "  {count} row(s) deleted"
  # Shown beneath a `drop column --cascade` summary, once per
  # index removed because it covered the dropped column.
  index_dropped_with_column: "  also dropped index `{index}` (it covered the column)"

# ---- Client-side success notes (ADR-0017 §6, ADR-0018 §9) ------------
client_side:
  # Per-cell transformation notice when `change column ...` rewrote
  # at least one stored value (storage-class change or non-identity
  # mapping). `lossy` variant fires when information was discarded —
  # under simple-mode `--force-conversion`, and under advanced-mode
  # `alter table … alter column … type …`, which always converts
  # (ADR-0035 §7).
  transformed: |-
    [client-side] {count} row(s) were transformed before being stored. In raw SQL this would need an explicit `CAST` or application-level code.
  transformed_lossy: |-
    [client-side] {count} row(s) transformed; {lossy} of those discarded information (lossy). In raw SQL this would need an explicit `CAST` or application-level code.
  # Auto-fill notice when null cells were populated by the
  # serial/shortid auto-generation contract (change column path).
  auto_fill_transition: |-
    [client-side] {count} null cell(s) given auto-generated {kind} values. In raw SQL this would need an explicit UPDATE to populate.
  # Auto-fill notice for `add column T: x (serial)` on a
  # non-empty table — values run 1..N.
  auto_fill_add_serial: |-
    [client-side] {count} row(s) given auto-generated serial values 1..{count}. In raw SQL this would need an explicit UPDATE to populate.
  # Auto-fill notice for `add column T: x (shortid)` on a
  # non-empty table.
  auto_fill_add_shortid: |-
    [client-side] {count} row(s) given auto-generated shortid values. In raw SQL this would need an explicit UPDATE to populate.
  # `change column … --dont-convert` caveat (ADR-0038 §6 category 3,
  # Phase 3). The only category-3 *caveat* — the others are illuminating
  # (the headline echo already matches the effect). Here the headline
  # SQL (`ALTER TABLE … SET DATA TYPE …`) *would* convert if run, but
  # `--dont-convert` skipped the client-side layer, so the line above is
  # not equivalent. Fires only in an advanced effective mode (the line
  # references "the line above," i.e. the echo).
  dont_convert_caveat: |-
    [client-side] `--dont-convert` kept the stored values as-is; standard SQL always converts, so running the line above would transform them instead.

# ---- Replay command surfaces (ADR-0019 §9 sweep) ---------------------
replay:
  # Success summary printed when `replay <path>` completes
  # without per-line failure.
  completed: "[ok] replay {path} — {count} command(s) run"
  # File-open failure (line_number == 0 from the runtime).
  failed_open: "replay {path} failed: {error}"
  # Per-line failure header. The command echo is on a
  # follow-up `command_echo` line so the renderer can format
  # them as separate output rows.
  failed_at_line: "replay {path} failed at line {line_number}: {error}"
  # Indented echo of the offending command line, shown after
  # `failed_at_line` when the runtime supplied the source.
  command_echo: "  > {command}"
  # Errors the runtime constructs inside ReplayFailed.error
  # before forwarding to App. Carried as plain text so they
  # compose with `failed_at_line`'s `{error}` placeholder.
  error_could_not_open: "could not open `{path}`: {detail}"
  error_parse: "parse error: {detail}"
  # Skipped during replay (ADR-0034): app-lifecycle commands are
  # not re-applied. Most skip silently; `import` and a nested
  # `replay` warn because skipping them can leave the replayed
  # state incomplete (imported data / the nested file's commands
  # are not reconstructed).
  skipped_import: "[skip] line {line}: `{command}` — replay does not re-import; the imported data is not reconstructed"
  skipped_replay: "[skip] line {line}: nested `{command}` — its commands were not replayed"
