baldart 4.51.0 → 4.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,120 @@
1
+ # i18n Protocol
2
+
3
+ ## Purpose
4
+
5
+ Define how agents/skills handle **user-facing text** when a project has opted
6
+ into the internationalization layer. The contract is: **no user-facing string is
7
+ ever hardcoded** — every label goes through the stack's translation function
8
+ (`t('key')` / `<FormattedMessage>` / equivalent), the key is recorded in the
9
+ **context registry** (`paths.i18n_registry`) with a *hyper-brief usage context*,
10
+ and translation into the maintained languages is **context-aware** (the LLM sees
11
+ *what the label is for and where it appears*, not just the source string). The
12
+ 2026 best-practice this encodes: "the void of context" is the #1 threat to
13
+ translation quality — so context travels with every key.
14
+
15
+ ## Scope
16
+
17
+ **In**: externalizing strings; the registry schema + naming convention; ICU usage;
18
+ the BLOCKING anti-hardcoded cascade; the drift/coherence codes; who owns what.
19
+ **Out**: installing the lint/extract tooling (that is `baldart configure` /
20
+ `baldart doctor` / the toolchain `eslint-i18next` adapter — see
21
+ `framework/docs/I18N-LAYER.md`); the actual translation mechanics (the
22
+ `i18n-translator` agent + the `/i18n` skill).
23
+
24
+ ## Gating
25
+
26
+ Conditional on `features.has_i18n: true` in `baldart.config.yml`. When the flag
27
+ is `false`/missing, **every rule below is a no-op** — identical to pre-i18n
28
+ behavior. Never block a task because the registry file or a lint command is
29
+ missing; surface gaps through `baldart doctor`, not mid-task.
30
+
31
+ ## Authority model (avoid the sidecar drift)
32
+
33
+ - **Code + native locale files** are authoritative for **WHICH keys exist** and
34
+ for **the translations**.
35
+ - **The registry** (`paths.i18n_registry`, default `docs/i18n/registry.yml`) is
36
+ authoritative ONLY for the per-key **`context` + `domain`** (the context layer).
37
+ - Reconciliation is **registry ⟷ `t()` calls / source-language file**: the
38
+ registry mirrors keys, it never invents them.
39
+
40
+ ## Registry schema (`paths.i18n_registry`, YAML)
41
+
42
+ ```yaml
43
+ schema_version: 1
44
+ keys:
45
+ checkout.summary.apply_coupon_cta:
46
+ source: "Apply coupon" # in i18n.source_language
47
+ context: "Primary CTA in cart summary; applies the entered discount code"
48
+ domain: "checkout"
49
+ char_limit: 18 # optional — UI constraint (verbose langs, e.g. DE)
50
+ icu: false # true when the string uses ICU plural/select
51
+ status: active # active | deprecated
52
+ ```
53
+
54
+ Translations are NOT stored here — they live in the native locale files.
55
+
56
+ ## Naming convention (default; overridable via `.baldart/overlays/`)
57
+
58
+ - **dot-notation** `namespace.domain.key` (e.g. `forms.buttons.login`, `errors.email_invalid`).
59
+ - **Semantic, not literal**: `welcoming_message`, never the text itself nor a
60
+ generic `heading` — keys must survive copy changes.
61
+ - **`global.*`** namespace for genuinely reused strings (`global.ok`, `global.cancel`);
62
+ do NOT force reuse when the context differs (a "god key" mistranslates).
63
+ - **No concatenation** — use **ICU MessageFormat** for plurals/gender/select/number
64
+ in a single string. Concatenating `actor + " shared " + count + " files"` is
65
+ wrong in any inflected language.
66
+
67
+ ## BLOCKING anti-hardcoded cascade
68
+
69
+ 1. **Deterministic gate (primary)** — a BALDART-owned **standalone ESLint config**
70
+ (`eslint.i18n.config.mjs`, `eslint-plugin-i18next` `no-literal-string`) that
71
+ `baldart configure` / `baldart doctor` write and install. The gate command is
72
+ `i18n.lint_command` (default `npx eslint --config eslint.i18n.config.mjs .`),
73
+ resolved by the runners as: `i18n.lint_command` → the standalone config by
74
+ convention when the file exists → SKIP (never a silent pass). It is a dedicated
75
+ ESLint run **independent of the project's main lint** (so it works even on a
76
+ Biome-only toolchain) and **never touches the user's config**. The default rule
77
+ mode is `jsx-only`: it deterministically blocks **JSX text nodes AND user-facing
78
+ JSX attributes** (`title`/`placeholder`/`alt`/`aria-label`) — verified to FAIL on
79
+ a hardcoded label and PASS on `t()`. Runners: `qa-sentinel` (for `/new`, `/qa`),
80
+ the classic `/new` Phase-2 gate, the `new2` Phase-2 gate, and the `i18n-align`
81
+ routine (pre-commit). This is the HARD enforcement for JSX-rendered strings.
82
+ 2. **Semantic backstop (review)** — non-JSX imperative strings are OUTSIDE the
83
+ linter's deterministic reach, so `code-reviewer` flags them as **HIGH**,
84
+ **syntactically scoped**:
85
+ - *In scope (backstop's job)*: non-JSX user-facing strings — `toast(...)` /
86
+ imperative call args, thrown user-facing errors, strings built by
87
+ concatenation. (JSX text + attributes are already hard-blocked by the linter;
88
+ a linter pass is confirmation there, not a reason to re-flag.)
89
+ - *Excluded*: log lines, dev-facing errors, test files, enum/const keys,
90
+ `className`, internal identifiers, URLs, technical values.
91
+
92
+ ## Post-intervention coherence (drift codes)
93
+
94
+ Whenever a change adds/edits `t()` keys, reconcile the registry in the SAME change:
95
+
96
+ - `I18N_REGISTRY_DRIFT` — a key referenced in `t()` has no registry entry.
97
+ - `I18N_KEY_ORPHANED` — a registry entry has no corresponding `t()` call.
98
+ - `I18N_CONTEXT_MISSING` — a registry entry has an empty/placeholder `context`.
99
+
100
+ Three enforcement layers (mirrors the design-system discipline): **per-task**
101
+ (`coder` STEP 9 writes the stub entry) → **per-merge** (`doc-reviewer` curates
102
+ context/orphans on new keys) → **weekly** (`i18n-align` routine audits + aligns
103
+ target languages). The on-demand `/i18n audit` covers off-cadence checks.
104
+
105
+ ## Ownership (strict specialization)
106
+
107
+ - **`coder`** — externalizes strings + writes the registry STUB (`source`+`context`+`domain`).
108
+ Does NOT translate.
109
+ - **`doc-reviewer`** — curates the registry (context quality, dedup, orphan/missing).
110
+ Does NOT translate, does NOT write app code.
111
+ - **`i18n-translator`** (`model: sonnet`, `effort: low`) — produces context-aware
112
+ translations into the native locale files. Does NOT touch the registry or code;
113
+ flags anomalies (`needs-attention`) instead of guessing.
114
+
115
+ ## Consumers
116
+
117
+ `coder` (STEP 9), `code-reviewer` (anti-hardcoded backstop), `doc-reviewer`
118
+ (registry curation), the `/i18n` skill, the `/i18n-adopt` migration skill, the
119
+ `i18n-align` routine, `/prd` + `/new` (declare/inherit the constraint). All gate
120
+ on `features.has_i18n`.
@@ -25,6 +25,7 @@ Route agents to the right module with minimal reading.
25
25
  - If touching workflow/process/commits/backlog -> read `agents/workflows.md`.
26
26
  - If CREATING or MUTATING a backlog card (any prefix — `FEAT`/`CHORE`/`BUG`/`DOC`/`PERF`/`UI`), or consuming one type-blind (`/new`, `/new2`) -> read `agents/card-schema.md` (the universal, profile-aware baseline) before writing/validating fields.
27
27
  - If running MECHANICAL GATES (lint, format, type-check, test, build, audit) and `features.has_toolchain: true` -> read `agents/toolchain-protocol.md` and run the command from `toolchain.commands.<gate>` verbatim, falling back to the project-standard default when a key is unset. A configured command that FAILS is a real gate failure (do not fall back).
28
+ - If writing/reviewing ANY user-facing text (labels, copy, messages) and `features.has_i18n: true` -> read `agents/i18n-protocol.md`: never hardcode strings, route every label through `t()` with a key recorded in `paths.i18n_registry` (`context`+`domain`), use ICU (no concatenation). The anti-hardcoded linter is the BLOCKING gate; translation is the `i18n-translator` agent / `/i18n` skill. When the flag is `false`, no-op.
28
29
  - If touching testing or QA issues -> read `agents/testing.md` (also documents the scope-aware,
29
30
  profile-driven test-selection strategy consumed by `qa-sentinel`).
30
31
  - If touching GitHub issues or issue workflow -> read `agents/github-issue-subagent.md`.
@@ -67,6 +68,7 @@ When adding or updating agents, update REGISTRY.md — not this file.
67
68
  - `agents/code-search-protocol.md` — Retrieval hierarchy for code search: LSP → Grep → Git (since v3.10.0, gated on `features.has_lsp_layer`)
68
69
  - `agents/code-graph-protocol.md` — Structural/relational retrieval via the Graphify code knowledge graph (since v4.21.0, gated on `features.has_code_graph`)
69
70
  - `agents/toolchain-protocol.md` — Mechanical-gate command resolution (lint/format/typecheck/test/build/audit) from `toolchain.commands.*` with silent project-standard fallback (since v4.41.0, gated on `features.has_toolchain`)
71
+ - `agents/i18n-protocol.md` — Internationalization discipline: no hardcoded user-facing strings, context registry (`paths.i18n_registry`) + ICU + context-aware translation; BLOCKING anti-hardcoded lint gate (since v4.52.0, gated on `features.has_i18n`)
70
72
  - `agents/design-system-protocol.md` — Registry-first discipline for UI work: BLOCKING cascade on `INDEX.md` + `tokens-reference.md` + `components/<Name>.md` (since v3.11.0, gated on `features.has_design_system`)
71
73
  - `agents/card-schema.md` — Atomic Card Baseline Schema: the universal, profile-aware (epic/child/standalone) field contract every backlog card satisfies, plus the consumer HALT/BACK-FILL/WARN contract (since v4.35.0)
72
74
 
@@ -0,0 +1,89 @@
1
+ # i18n Layer — Operator Guide
2
+
3
+ > Since v4.52.0. Opt-in, gated on `features.has_i18n`. The **runtime protocol**
4
+ > (how agents handle user-facing text) lives in
5
+ > [`framework/agents/i18n-protocol.md`](../agents/i18n-protocol.md); this document
6
+ > covers the *plumbing* — config, lifecycle, the moving parts, fallback.
7
+
8
+ ## 1. Why this exists
9
+
10
+ Multi-language products fail on translation quality when the translator (human or
11
+ LLM) sees only the source string. The 2026 best-practice consensus calls this
12
+ "the void of context" — the #1 quality threat. BALDART's i18n layer makes
13
+ internationalization a first-class discipline: **no user-facing string is ever
14
+ hardcoded**, and every key carries a **hyper-brief context** so translation is
15
+ context-aware, not blind.
16
+
17
+ ## 2. What BALDART adds vs what the stack already ships
18
+
19
+ BALDART does NOT reimplement i18n. It **wraps** the consumer's existing framework
20
+ (next-intl / i18next / react-intl / lingui / vue-i18n) and best-in-class OSS:
21
+
22
+ - **Anti-hardcoded gate** — `eslint-plugin-i18next` (`no-literal-string`; the most
23
+ adopted, ~550k weekly downloads) or `i18nGuard` (multi-framework, SARIF).
24
+ - **Key extraction** — `i18next-cli` / `lingui extract` / `formatjs extract`
25
+ (detection-only; BALDART never installs the extractor).
26
+ - **Bulk translation (optional)** — `lingo.dev` / `languine` the `i18n-translator`
27
+ agent may delegate volume to, then review.
28
+
29
+ BALDART's proprietary value is the **context registry** (`paths.i18n_registry`) and
30
+ the agentic orchestration around it.
31
+
32
+ ## 3. The moving parts
33
+
34
+ | Part | Where | Role |
35
+ |---|---|---|
36
+ | Feature flag + block | `baldart.config.yml` | `features.has_i18n` + `paths.i18n_registry` + the `i18n:` block (`framework`, `source_language`, `target_languages`, `locales_root`, `extract_command`, `bulk_translator`, `lint_command`) |
37
+ | Context registry | `${paths.i18n_registry}` (default `docs/i18n/registry.yml`) | stack-agnostic YAML SSOT for per-key `source`+`context`+`domain` (NOT the translations) |
38
+ | Protocol module | `framework/agents/i18n-protocol.md` | authority model, naming/ICU rules, drift codes, ownership |
39
+ | Translator agent | `framework/.claude/agents/i18n-translator.md` | `model: sonnet`, `effort: low` — context-aware translation into native locale files; flag-not-guess |
40
+ | On-demand skill | `framework/.claude/skills/i18n/SKILL.md` | `/i18n audit` + `/i18n translate` |
41
+ | Adoption skill | `framework/.claude/skills/i18n-adopt/SKILL.md` | `/i18n-adopt` — one-shot migration of an existing codebase (find → replace → register → translate → verify); dedicated branch, idempotent, never auto-merges |
42
+ | Weekly routine | `framework/routines/i18n-align.routine.yml` | translates target languages, lints, commits direct to trunk |
43
+ | Anti-hardcoded gate | BALDART-owned standalone `eslint.i18n.config.mjs` (run via `i18n.lint_command`) + `src/utils/i18n-gate.js` | BLOCKING — literal user-facing strings fail; works on Biome projects; never touches the user's lint config |
44
+ | Consumers | `coder` (STEP 9), `code-reviewer` (backstop), `doc-reviewer` (registry curation), `/prd` (HARD RULE 20), `/new` (briefing) | all gate on `features.has_i18n` |
45
+
46
+ ## 4. Install lifecycle
47
+
48
+ ```
49
+ features.has_i18n: true
50
+
51
+ ├─ baldart configure (INTERACTIVE) ── detect framework (package.json)
52
+ │ → prompt source / target languages / locales_root
53
+ │ → populate i18n.extract_command default
54
+ │ → set i18n.lint_command (standalone gate)
55
+ │ → offer gate setup: devDeps + write eslint.i18n.config.mjs
56
+ │ → hint: npx baldart routines install i18n-align
57
+
58
+ ├─ baldart configure --yes / CI ──→ writes the flag + block ONLY; doctor backfills
59
+
60
+ ├─ baldart doctor ─────────────────→ i18n-gate-setup (deps/config missing)
61
+ │ i18n-registry-missing (registry absent)
62
+
63
+ └─ /i18n (on demand) ──────────────→ audit + translate
64
+ ```
65
+
66
+ ## 5. Authority + drift (3 tiers)
67
+
68
+ Code/native files own *which keys exist* + the translations; the registry owns
69
+ *context/domain*. Drift is contained continuously:
70
+
71
+ 1. **Per-task** — `coder` STEP 9 writes the registry stub at key creation.
72
+ 2. **Per-merge** — `doc-reviewer` curates context/orphans on new keys in `/new`.
73
+ 3. **Weekly** — `i18n-align` translates target languages and commits to trunk.
74
+
75
+ On-demand `/i18n audit` covers off-cadence checks. Drift codes:
76
+ `I18N_REGISTRY_DRIFT`, `I18N_KEY_ORPHANED`, `I18N_CONTEXT_MISSING`.
77
+
78
+ ## 6. Invariants
79
+
80
+ - **Gate on `features.has_i18n`** — every rule is a no-op when false. Never assume.
81
+ - **Silent fallback** — never block a task because the registry/lint is missing;
82
+ surface gaps via `baldart doctor`.
83
+ - **Translations live in native files** — the registry never stores them.
84
+ - **Strict specialization** — coder populates, doc-reviewer curates, i18n-translator
85
+ translates; none crosses lanes.
86
+ - **Schema-change propagation** applies to `features.has_i18n` + `paths.i18n_registry`
87
+ + the `i18n.*` block (template + configure + update detector + doctor + CHANGELOG).
88
+ - **No PR for the routine** — translation files are low-risk; direct commit to the
89
+ trunk (`git.trunk_branch`).
@@ -0,0 +1,61 @@
1
+ name: i18n-align
2
+ description: Weekly i18n alignment — translates labels missing/stale in the maintained target languages, using the per-key registry context. Direct commit (no PR).
3
+ since_version: 4.52.0
4
+
5
+ schedule:
6
+ cron: "0 4 * * 1" # Monday 04:00 UTC
7
+ timezone: UTC
8
+ jitter_minutes: 0
9
+ cadence_label: weekly
10
+
11
+ agent: i18n-translator
12
+
13
+ prompt: |
14
+ Run the weekly i18n alignment (only when `features.has_i18n: true` in
15
+ `baldart.config.yml` AND `i18n.target_languages` is non-empty — exit cleanly
16
+ otherwise). Read `framework/agents/i18n-protocol.md` first. You are the
17
+ context-aware translator; you keep the maintained target languages in sync
18
+ with the source language. You do NOT curate the registry (that is
19
+ doc-reviewer's nightly job) and you do NOT touch application code.
20
+
21
+ 1. **Enumerate keys**: run `i18n.extract_command` if set (e.g.
22
+ `npx i18next-cli extract`); otherwise `rg` the codebase for `t('…')` calls.
23
+ Read the source-language locale file under `i18n.locales_root` and the
24
+ context registry at `${paths.i18n_registry}`.
25
+ 2. **Find gaps**: for each language in `i18n.target_languages`, list keys that
26
+ are present in the source language but **missing or stale** (source changed
27
+ since last translation) in that language's native locale file.
28
+ 3. **Translate with context**: for each gap, translate using the registry
29
+ `context` + `domain` (+ `char_limit`, glossary if present). Preserve ICU
30
+ constructs and placeholders EXACTLY. On any anomaly (empty/ambiguous
31
+ context, impossible char_limit, unmappable ICU, technical source) DO NOT
32
+ guess — leave the label untranslated and record a `needs-attention` entry.
33
+ 4. **Write + verify**: write the translations into the native locale files,
34
+ then run the anti-hardcoded / validate gate — `i18n.lint_command` if set,
35
+ else the standalone `eslint.i18n.config.mjs` by convention
36
+ (`npx eslint --config eslint.i18n.config.mjs .`) — so no malformed
37
+ ICU/placeholder or hardcoded string is committed.
38
+ 5. **Report**: write `docs/reports/{{YYYYMMDD}}-i18n-align.md` listing keys
39
+ translated per language and all `needs-attention` records.
40
+
41
+ Commit directly to the current branch (the integration trunk, `${git.trunk_branch}`)
42
+ — these are translation files only, blast radius is minimal, NO pull request.
43
+ The `needs-attention` records go into the commit message and the report.
44
+
45
+ output:
46
+ path: docs/reports/{{YYYYMMDD}}-i18n-align.md
47
+ commit:
48
+ enabled: true
49
+ prefix: "[I18N-ALIGN]"
50
+ branch: ${git.trunk_branch}
51
+
52
+ required_artifacts:
53
+ - ${paths.i18n_registry}
54
+ - .claude/agents/i18n-translator.md
55
+
56
+ optional: true # routine is silently skipped when artifacts missing / feature off
57
+
58
+ backend_hints:
59
+ - claude-code-cloud
60
+ - github-actions
61
+ - cron
@@ -78,3 +78,14 @@ routines:
78
78
  Weekly full SSOT sweep — runs the project's full audit suite
79
79
  (frontmatter, errors, perms, env, imports, etc.) and writes a
80
80
  consolidated drift report.
81
+
82
+ - name: i18n-align
83
+ file: i18n-align.routine.yml
84
+ cadence: weekly
85
+ since_version: 4.52.0
86
+ summary: |
87
+ Weekly i18n alignment — translates labels missing/stale in the
88
+ maintained target languages (i18n.target_languages) using the per-key
89
+ registry context, runs lint, and commits directly to the trunk (no PR).
90
+ Optional (only runs when features.has_i18n is true and target languages
91
+ are configured).
@@ -43,6 +43,12 @@ paths:
43
43
  references_dir: "" # e.g. docs/references
44
44
  wiki_dir: "" # e.g. docs/wiki (LLM-wiki overlay)
45
45
 
46
+ # i18n context registry (since v4.52.0). Stack-agnostic YAML SSOT for the
47
+ # per-key TRANSLATION CONTEXT (source + hyper-brief usage note + domain) — NOT
48
+ # the translations themselves (those live in the stack's native locale files).
49
+ # Set features.has_i18n: true to enable. Default convention "docs/i18n/registry.yml".
50
+ i18n_registry: "" # e.g. docs/i18n/registry.yml
51
+
46
52
  # Test infrastructure.
47
53
  e2e_tests_dir: "" # e.g. tests/e2e
48
54
 
@@ -233,6 +239,18 @@ features:
233
239
  # and framework/docs/TOOLCHAIN-LAYER.md.
234
240
  has_toolchain: false
235
241
 
242
+ # Internationalization layer (since v4.52.0). When true, all user-facing
243
+ # strings MUST be externalized through a translation function (never
244
+ # hardcoded), keys are recorded in paths.i18n_registry with a hyper-brief
245
+ # CONTEXT note so LLM translation is context-aware (not blind), and the
246
+ # anti-hardcoded gate (eslint-plugin-i18next via the toolchain lint command)
247
+ # blocks literal strings. The `/i18n` skill audits + translates on demand; the
248
+ # weekly `i18n-align` routine keeps target languages aligned. coder STEP 9
249
+ # populates the registry; doc-reviewer curates it. Falls back SILENTLY to a
250
+ # no-op everywhere when false. See framework/agents/i18n-protocol.md and
251
+ # framework/docs/I18N-LAYER.md.
252
+ has_i18n: false
253
+
236
254
  # ─── E2E REVIEW ──────────────────────────────────────────────────────────
237
255
  # Tuning for the /e2e-review skill (only consulted when has_e2e_review: true).
238
256
  e2e_review:
@@ -321,6 +339,47 @@ toolchain:
321
339
  # restores any missing default config. Disable on CI if too noisy.
322
340
  auto_verify: true
323
341
 
342
+ # ─── I18N ──────────────────────────────────────────────────────────────────
343
+ # State of the internationalization layer. Populated by `baldart configure` /
344
+ # the `/i18n` skill. Only meaningful when `features.has_i18n: true`. The
345
+ # translations themselves live in the stack's NATIVE locale files; this block
346
+ # only describes the stack + the maintained languages + the on-demand tooling.
347
+ i18n:
348
+ # Canonical i18n framework / library in use. Autodetected from package.json by
349
+ # `baldart configure`. Recognized: "next-intl" | "i18next" | "react-intl" |
350
+ # "lingui" | "vue-i18n" | "custom" | "".
351
+ framework: ""
352
+
353
+ # Source/default language (BCP-47 tag, e.g. "en", "en-US"). The locale the
354
+ # source strings are authored in — the registry `source` field is in this lang.
355
+ source_language: ""
356
+
357
+ # Target languages BALDART MAINTAINS (BCP-47 tags). The weekly `i18n-align`
358
+ # routine and the `/i18n` skill translate missing/stale keys into each of
359
+ # these. Empty = no automated translation (audit still works).
360
+ target_languages: []
361
+
362
+ # Locale files root (e.g. src/locales, public/locales, locales). Autodetected.
363
+ locales_root: ""
364
+
365
+ # Key-extraction command (detection-only — BALDART does NOT install it). Run by
366
+ # `/i18n` + the routine to enumerate t() keys for registry↔code reconciliation.
367
+ # Empty → the skill falls back to an rg-based scan. e.g. "npx i18next-cli extract".
368
+ extract_command: ""
369
+
370
+ # Optional bulk machine-translation backend the i18n-translator agent may
371
+ # delegate VOLUME to (then reviews its output). "" | "lingo.dev" | "languine".
372
+ bulk_translator: ""
373
+
374
+ # Deterministic anti-hardcoded gate command. `baldart configure` / `baldart
375
+ # doctor` set this to a STANDALONE ESLint run BALDART owns
376
+ # (`npx eslint --config eslint.i18n.config.mjs .`) — it works on Biome projects
377
+ # too (a dedicated ESLint invocation just for this gate, never touching your main
378
+ # lint config). The gate runners (qa-sentinel, /new2, the i18n-align routine) run
379
+ # it verbatim; a non-zero exit FAILS the gate. Empty + no config file present →
380
+ # the gate reports SKIP (never a silent pass).
381
+ lint_command: ""
382
+
324
383
  # ─── GIT ─────────────────────────────────────────────────────────────────
325
384
  # Controls how worktree-manager (`/mw`) integrates a worktree's feature
326
385
  # branch back into the integration trunk.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.51.0",
3
+ "version": "4.52.0",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"
@@ -8,6 +8,7 @@ const lspAdapters = require('../utils/lsp-adapters');
8
8
  const GraphifyInstaller = require('../utils/graphify-installer');
9
9
  const ToolchainInstaller = require('../utils/toolchain-installer');
10
10
  const toolchainAdapters = require('../utils/toolchain-adapters');
11
+ const I18nGate = require('../utils/i18n-gate');
11
12
 
12
13
  const CONFIG_FILE = 'baldart.config.yml';
13
14
  // The subtree pull copies the entire BALDART repo (which itself has a
@@ -168,6 +169,25 @@ function detect(cwd = process.cwd()) {
168
169
  const hasDep = (name) => Object.keys(deps).some((d) => d === name || d.startsWith(`${name}/`));
169
170
  const hasAnyDep = (...names) => names.some(hasDep);
170
171
 
172
+ // ---- i18n layer autodetection (since v4.52.0) --------------------------
173
+ // Detection-only: BALDART does not install the i18n framework, it reads which
174
+ // one the consumer already uses to populate the `i18n:` config block.
175
+ const detectI18nFramework = () => {
176
+ if (hasDep('next-intl')) return 'next-intl';
177
+ if (hasDep('react-intl') || hasDep('@formatjs/intl')) return 'react-intl';
178
+ if (hasDep('@lingui/core') || hasDep('@lingui/react')) return 'lingui';
179
+ if (hasDep('vue-i18n')) return 'vue-i18n';
180
+ if (hasDep('i18next')) return 'i18next';
181
+ return '';
182
+ };
183
+ const detectLocalesRoot = () => {
184
+ for (const cand of ['src/locales', 'public/locales', 'locales', 'src/i18n/locales', 'i18n/locales', 'app/locales']) {
185
+ if (exists(cand)) return cand;
186
+ }
187
+ return '';
188
+ };
189
+ const detectedI18nFramework = detectI18nFramework();
190
+
171
191
  // ---- Monorepo awareness ------------------------------------------------
172
192
  const monorepoMarkers = ['pnpm-workspace.yaml', 'lerna.json', 'nx.json', 'turbo.json', 'rush.json'];
173
193
  const isMonorepo = monorepoMarkers.some(exists) || Array.isArray(pkg?.workspaces) ||
@@ -480,6 +500,9 @@ function detect(cwd = process.cwd()) {
480
500
  // Drives the prompt default; install side effects only run interactively.
481
501
  has_toolchain: toolchainAdapters.detectAll(cwd).length > 0
482
502
  && toolchainAdapters.detectIncumbents(cwd).length === 0,
503
+ // i18n recommended when the project already uses a known i18n framework.
504
+ // Drives the prompt default; the config block is populated interactively.
505
+ has_i18n: Boolean(detectedI18nFramework),
483
506
  },
484
507
  tools: {
485
508
  enabled: toolAdapters.defaultEnabled(cwd)
@@ -650,6 +673,7 @@ async function interactivePrompts(merged, detected) {
650
673
  ['has_code_graph', 'Enable Graphify code-knowledge-graph layer? (structural queries + wiki feed; recommended for large codebases)'],
651
674
  ['has_toolchain', 'Enable curated dev toolchain? (Biome format+lint+import; agents run it in the quality gates — recommended for JS/TS)'],
652
675
  ['has_e2e_review', 'Enable BLOCKING end-to-end review (Phase 2.6 of /new invokes /e2e-review — functional + visual fidelity gate)?'],
676
+ ['has_i18n', 'Enable i18n layer? (no hardcoded labels + context registry + context-aware translation — recommended for multi-language products)'],
653
677
  ]) {
654
678
  const [key, question] = flag;
655
679
  const currentVal = merged.features[key];
@@ -1004,6 +1028,85 @@ async function interactivePrompts(merged, detected) {
1004
1028
  merged.toolchain.installed_tools = [];
1005
1029
  }
1006
1030
 
1031
+ // ---- i18n layer (since v4.52.0) ---------------------------------------
1032
+ // Detection-first, install-light. We detect the i18n framework, ask which
1033
+ // languages to maintain, populate the on-demand tooling commands, and OFFER
1034
+ // (interactive only) to install the anti-hardcoded lint plugin + the weekly
1035
+ // i18n-align routine. Translations + key extraction are NOT installed by
1036
+ // BALDART — the i18n framework is the consumer's choice; we wrap it.
1037
+ merged.i18n = merged.i18n || {
1038
+ framework: '', source_language: '', target_languages: [],
1039
+ locales_root: '', extract_command: '', bulk_translator: '', lint_command: '',
1040
+ };
1041
+ if (merged.features.has_i18n === true) {
1042
+ UI.section('i18n layer (context registry + context-aware translation)');
1043
+ merged.i18n.framework = await promptForKey(
1044
+ 'i18n framework (next-intl | i18next | react-intl | lingui | vue-i18n | custom)',
1045
+ merged.i18n.framework || detectedI18nFramework || ''
1046
+ );
1047
+ merged.i18n.source_language = await promptForKey(
1048
+ 'Source language (BCP-47 tag, e.g. en, en-US)',
1049
+ merged.i18n.source_language || (merged.identity && merged.identity.language) || 'en'
1050
+ );
1051
+ const targetDefault = (merged.i18n.target_languages || []).join(', ');
1052
+ const targetInput = await promptForKey(
1053
+ 'Target languages to MAINTAIN (comma-separated BCP-47, e.g. it, es, fr — empty = audit only)',
1054
+ targetDefault
1055
+ );
1056
+ merged.i18n.target_languages = targetInput
1057
+ .split(',').map((l) => l.trim()).filter(Boolean);
1058
+ merged.i18n.locales_root = await promptForKey(
1059
+ 'Native locale files root (e.g. src/locales, public/locales)',
1060
+ merged.i18n.locales_root || detectLocalesRoot() || ''
1061
+ );
1062
+ // Sensible default for the on-demand key-extraction command (detection-only).
1063
+ if (!merged.i18n.extract_command) {
1064
+ const fw = merged.i18n.framework;
1065
+ merged.i18n.extract_command =
1066
+ fw === 'i18next' ? 'npx i18next-cli extract'
1067
+ : fw === 'lingui' ? 'npx lingui extract'
1068
+ : fw === 'react-intl' ? 'npx formatjs extract' : '';
1069
+ }
1070
+ merged.i18n.bulk_translator = await promptForKey(
1071
+ 'Optional bulk machine-translation backend the i18n-translator may delegate to (empty | lingo.dev | languine)',
1072
+ merged.i18n.bulk_translator || ''
1073
+ );
1074
+ // Default the i18n_registry path when unset.
1075
+ if (!merged.paths.i18n_registry) {
1076
+ merged.paths.i18n_registry = await promptForKey(
1077
+ 'i18n context registry path (key+context SSOT)', 'docs/i18n/registry.yml'
1078
+ );
1079
+ }
1080
+ // Anti-hardcoded gate: a BALDART-owned STANDALONE ESLint config (works on
1081
+ // Biome projects too — a dedicated ESLint run just for this gate, never
1082
+ // touching the user's lint config). i18n.lint_command points at it so the
1083
+ // gate runners (qa-sentinel, new2, the routine) execute it deterministically.
1084
+ const gateCwd = process.cwd();
1085
+ const gateDeps = I18nGate.depsFor(gateCwd);
1086
+ const gateConfigFile = I18nGate.configFilename(gateCwd);
1087
+ merged.i18n.lint_command = I18nGate.commandForCwd(gateCwd);
1088
+ const wantGate = await UI.confirm(
1089
+ `Install the deterministic anti-hardcoded gate now (devDeps ${gateDeps.join(', ')} + a standalone ${gateConfigFile})?`, true
1090
+ );
1091
+ if (wantGate) {
1092
+ const res = I18nGate.setup(gateCwd);
1093
+ if (res.error) {
1094
+ UI.warning(`i18n gate setup incomplete: ${res.error}. Run \`baldart doctor\` to retry.`);
1095
+ } else {
1096
+ if (res.installed) UI.success(`Installed ${gateDeps.join(', ')}.`);
1097
+ if (res.config && res.config.status === 'written') UI.success(`Wrote ${res.config.file} (${res.era} ESLint config — standalone, edit freely, never overwritten).`);
1098
+ else if (res.config && res.config.status === 'exists') UI.info(`Kept existing ${res.config.file}.`);
1099
+ UI.success(`Anti-hardcoded gate wired: \`${merged.i18n.lint_command}\` runs in the quality gates.`);
1100
+ }
1101
+ } else {
1102
+ UI.info('Skipping. The gate stays unwired (qa-sentinel reports i18n-lint SKIP) until you run `baldart doctor`.');
1103
+ }
1104
+ UI.info('To enable weekly auto-alignment of target languages, run: `npx baldart routines install i18n-align`.');
1105
+ } else {
1106
+ merged.i18n.framework = '';
1107
+ merged.i18n.target_languages = [];
1108
+ }
1109
+
1007
1110
  UI.section('Stack (autodetected from package.json — confirm or override)');
1008
1111
  const charting = merged.stack.charting;
1009
1112
  const chartingCanonical = await promptForKey(
@@ -34,6 +34,7 @@ const GitHooks = require('../utils/githooks');
34
34
  const LspInstaller = require('../utils/lsp-installer');
35
35
  const GraphifyInstaller = require('../utils/graphify-installer');
36
36
  const ToolchainInstaller = require('../utils/toolchain-installer');
37
+ const I18nGate = require('../utils/i18n-gate');
37
38
  const ToolCurrency = require('../utils/tool-currency');
38
39
  const CodexOrphans = require('../utils/codex-orphans');
39
40
  const UpdateNotifier = require('../utils/update-notifier');
@@ -425,6 +426,29 @@ async function detectState(cwd, opts = {}) {
425
426
  }
426
427
  } catch (_) { /* never block doctor on toolchain probe */ }
427
428
 
429
+ // ---- i18n layer (since v4.52.0) ------------------------------------
430
+ // Detection-only backfill: the i18n framework is the consumer's, so doctor
431
+ // never installs it — it flags a missing context registry and a missing
432
+ // anti-hardcoded lint plugin (the only thing BALDART offers to install).
433
+ state.i18nEnabled = false;
434
+ state.i18nRegistryMissing = false;
435
+ state.i18nGateMissing = false;
436
+ try {
437
+ if (config && !config.__malformed && config.features && config.features.has_i18n === true) {
438
+ state.i18nEnabled = true;
439
+ const regPath = (config.paths && config.paths.i18n_registry) || '';
440
+ if (regPath && !fs.existsSync(path.join(cwd, regPath))) {
441
+ state.i18nRegistryMissing = true;
442
+ }
443
+ // Deterministic anti-hardcoded gate present? (standalone config + deps).
444
+ // Only meaningful for a JS/TS project — skip the gate check otherwise.
445
+ if (I18nGate.hasPackageJson(cwd)
446
+ && (!I18nGate.isConfigPresent(cwd) || !I18nGate.depsInstalled(cwd))) {
447
+ state.i18nGateMissing = true;
448
+ }
449
+ }
450
+ } catch (_) { /* never block doctor on i18n probe */ }
451
+
428
452
  // ---- External-tool version currency (since v4.38.0) ----------------
429
453
  // BALDART pins none of the external tools it installs (graphifyy via pipx,
430
454
  // language servers via npm/system) — pipx/npm never auto-upgrade, so a
@@ -885,6 +909,40 @@ function planActions(state) {
885
909
  });
886
910
  }
887
911
 
912
+ // ---- i18n layer backfill (since v4.52.0) ----------------------------
913
+ // Detection-only: BALDART never installs the consumer's i18n framework. It
914
+ // offers to install the anti-hardcoded lint plugin and flags a missing
915
+ // context registry (which `/i18n` or the consumer scaffolds).
916
+ if (state.i18nEnabled && state.i18nGateMissing) {
917
+ const gateDeps = I18nGate.depsFor(state.cwd);
918
+ const gateFile = I18nGate.configFilename(state.cwd);
919
+ actions.push({
920
+ key: 'i18n-gate-setup',
921
+ label: `Set up the anti-hardcoded gate (${gateDeps.join(', ')} + ${gateFile})`,
922
+ why: `features.has_i18n is true but the deterministic anti-hardcoded gate is not fully set up (missing devDeps or ${gateFile}), so hardcoded user-facing strings are not blocked. Installs the gate dependencies and writes a STANDALONE ESLint config (era-matched to your ESLint; works on Biome projects; never touches your main lint config).`,
923
+ autoOk: false, // installs devDependencies; confirm
924
+ run: async () => {
925
+ const res = I18nGate.setup(state.cwd);
926
+ if (res.error) { UI.warning(`Gate setup incomplete: ${res.error}.`); return; }
927
+ if (res.installed) UI.success(`Installed ${gateDeps.join(', ')}.`);
928
+ if (res.config && res.config.status === 'written') UI.success(`Wrote ${res.config.file} (${res.era} ESLint config).`);
929
+ UI.success(`Anti-hardcoded gate ready: \`${I18nGate.commandForCwd(state.cwd)}\`.`);
930
+ UI.info('If i18n.lint_command is unset in baldart.config.yml, the gate runs by convention via the config file; `baldart configure` records it explicitly.');
931
+ },
932
+ });
933
+ }
934
+ if (state.i18nEnabled && state.i18nRegistryMissing) {
935
+ actions.push({
936
+ key: 'i18n-registry-missing',
937
+ label: 'i18n context registry not found — scaffold it with /i18n',
938
+ why: `features.has_i18n is true but the context registry at paths.i18n_registry does not exist yet. It is the SSOT for per-key translation context. Run the /i18n skill (or create the YAML) to start populating it; coder STEP 9 fills it as keys are added.`,
939
+ autoOk: false, // informational — no safe auto-create without project context
940
+ run: async () => {
941
+ UI.info('Run `/i18n` in your editor to audit + scaffold the registry, or create the YAML at the configured path. See framework/docs/I18N-LAYER.md.');
942
+ },
943
+ });
944
+ }
945
+
888
946
  // External-tool version currency (since v4.38.0). One non-blocking upgrade
889
947
  // action per managed tool confirmed behind upstream — the tool-dependency
890
948
  // analogue of the `baldart` CLI's own UpdateNotifier. `unknown`/`current`
@@ -1312,6 +1312,11 @@ async function update(options = {}, unknownArgs = []) {
1312
1312
  const missingToolchainCmds = Object.keys((tpl.toolchain && tpl.toolchain.commands) || {})
1313
1313
  .filter((k) => !(k in ((cur2.toolchain && cur2.toolchain.commands) || {})))
1314
1314
  .map((k) => `toolchain.commands.${k}`);
1315
+ // `i18n:` (since v4.52.0) — same nested-block contract as `graph:` /
1316
+ // `toolchain:`. Diff its own scalar keys so a new option surfaces.
1317
+ const missingI18n = Object.keys(tpl.i18n || {})
1318
+ .filter((k) => !(k in (cur2.i18n || {})))
1319
+ .map((k) => `i18n.${k}`);
1315
1320
  const allMissing = [
1316
1321
  ...missingPaths,
1317
1322
  ...missingFeatures,
@@ -1320,6 +1325,7 @@ async function update(options = {}, unknownArgs = []) {
1320
1325
  ...missingGraph.map((k) => `graph.${k}`),
1321
1326
  ...missingToolchain,
1322
1327
  ...missingToolchainCmds,
1328
+ ...missingI18n,
1323
1329
  ];
1324
1330
  if (allMissing.length) {
1325
1331
  UI.newline();