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.
- package/CHANGELOG.md +29 -0
- package/README.md +10 -3
- package/VERSION +1 -1
- package/framework/.claude/agents/REGISTRY.md +1 -0
- package/framework/.claude/agents/code-reviewer.md +16 -0
- package/framework/.claude/agents/coder.md +6 -0
- package/framework/.claude/agents/doc-reviewer.md +24 -0
- package/framework/.claude/agents/i18n-translator.md +56 -0
- package/framework/.claude/agents/qa-sentinel.md +10 -0
- package/framework/.claude/skills/i18n/SKILL.md +109 -0
- package/framework/.claude/skills/i18n-adopt/SKILL.md +105 -0
- package/framework/.claude/skills/new/references/implement.md +14 -1
- package/framework/.claude/skills/prd/SKILL.md +12 -2
- package/framework/.claude/workflows/new-final-review.js +1 -1
- package/framework/.claude/workflows/new2.js +3 -0
- package/framework/AGENTS.md +4 -0
- package/framework/agents/i18n-protocol.md +120 -0
- package/framework/agents/index.md +2 -0
- package/framework/docs/I18N-LAYER.md +89 -0
- package/framework/routines/i18n-align.routine.yml +61 -0
- package/framework/routines/index.yml +11 -0
- package/framework/templates/baldart.config.template.yml +59 -0
- package/package.json +1 -1
- package/src/commands/configure.js +103 -0
- package/src/commands/doctor.js +58 -0
- package/src/commands/update.js +6 -0
- package/src/utils/i18n-gate.js +195 -0
|
@@ -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
|
@@ -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(
|
package/src/commands/doctor.js
CHANGED
|
@@ -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`
|
package/src/commands/update.js
CHANGED
|
@@ -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();
|