baldart 4.52.0 → 4.52.2
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 +16 -0
- package/VERSION +1 -1
- package/framework/agents/i18n-protocol.md +5 -3
- package/package.json +1 -1
- package/src/commands/configure.js +7 -4
- package/src/utils/i18n-gate.js +15 -6
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to BALDART will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.52.2] - 2026-06-18
|
|
9
|
+
|
|
10
|
+
**Anti-hardcoded gate: kill the false positives — the generated ESLint config now allowlists only user-facing JSX attributes.** v4.52.0's `mode: 'jsx-only'` flagged *every* JSX attribute string, so on a real component-heavy codebase it drowned the signal in technical props — `variant="inset"`, `size="lg"`, `href="/login"`, `padding="md"` were all reported as "hardcoded user-facing strings". Measured on a production app: **3260 raw findings, mostly false positives.** Fixed by adding `'jsx-attributes': { include: ['placeholder','title','alt','label','aria-label'] }` to the generated `eslint.i18n.config.mjs` / `.eslintrc.i18n.json`, so attribute checks are restricted to the genuinely user-facing ones while JSX text nodes are still fully checked. Same codebase after the fix: **~1500 findings, 0 technical-attribute false positives** — every remaining finding is a real hardcoded label. Verified against `eslint-plugin-i18next`'s `shouldSkip` include-allowlist semantics. **PATCH** (tightens the generated gate config to be usable on real codebases; no new key, no behaviour change beyond fewer false positives).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`src/utils/i18n-gate.js`** — both the flat (ESLint 9) and legacy (ESLint 8) generated configs now carry the `jsx-attributes.include` user-facing allowlist; `framework/agents/i18n-protocol.md` cascade updated to describe the precise coverage (JSX text + 5 user-facing attributes; technical props excluded).
|
|
15
|
+
|
|
16
|
+
## [4.52.1] - 2026-06-18
|
|
17
|
+
|
|
18
|
+
**Hotfix: `baldart configure` (and the `configure-drift` doctor action) crashed with `detectedI18nFramework is not defined` whenever the i18n block ran.** The v4.52.0 i18n config block lives in `interactivePrompts()`, but referenced two helpers (`detectedI18nFramework`, `detectLocalesRoot`) that are scoped to the separate `detect()` function — a `ReferenceError` the moment `features.has_i18n` was true (so the framework / locales-root prompt defaults threw, taking down configure and the doctor drift action). Fixed by passing the detected values through the `detected` object (`detected.i18n.{framework,locales_root}` — real snake_case keys that also seed `merged.i18n.*` via the existing merge, no junk serialized). Also **broadened i18n framework autodetection** to `next-i18next` / `react-i18next` (→ i18next) and `@nuxtjs/i18n` / `@nuxt/i18n` (→ vue-i18n), so the `has_i18n` flag defaults to Yes on those stacks instead of being silently skipped. **PATCH** (bugfix to a v4.52.0 CLI path; no behaviour change beyond fixing the crash + wider detection; no new `baldart.config.yml` key).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`src/commands/configure.js`** — i18n config block no longer references `detect()`-scoped helpers from `interactivePrompts()`; detected framework + locales root flow through `detected.i18n`. Autodetection covers `next-i18next`, `react-i18next`, `@nuxtjs/i18n`, `@nuxt/i18n`.
|
|
23
|
+
|
|
8
24
|
## [4.52.0] - 2026-06-18
|
|
9
25
|
|
|
10
26
|
**BALDART becomes opinionated about internationalization: a new opt-in `features.has_i18n` layer makes consumers develop multi-language by default — zero hardcoded labels + a stack-agnostic context registry that makes LLM translation context-aware instead of blind.** The differentiator vs ordinary i18n is the **per-key context**: a stack-agnostic YAML registry (`paths.i18n_registry`, default `docs/i18n/registry.yml`) holds `source` + a *hyper-brief usage context* + `domain` for every label, so the translator (the new `i18n-translator` agent, or a delegated bulk backend) sees *what the label is for and where it appears* — the 2026 best-practice fix for "the void of context", the #1 translation-quality threat. **Translations live in the stack's NATIVE locale files** (i18next / next-intl / react-intl / lingui / vue-i18n); BALDART wraps the consumer's framework + best-in-class OSS rather than reimplementing i18n. The anti-hardcoded gate is a **deterministic, BALDART-owned standalone ESLint config** (`eslint.i18n.config.mjs` running `eslint-plugin-i18next` `no-literal-string` — the most-adopted at ~550k weekly downloads; framework-agnostic so it covers next-intl/vue too) written + installed by `configure`/`doctor` (`src/utils/i18n-gate.js`). It is a **dedicated ESLint run independent of the project's main lint** (so it works even on a **Biome-only toolchain**, which does not execute ESLint plugins) and **never touches the user's config**. The gate runs via `i18n.lint_command` in `qa-sentinel` (`/new`, `/qa`), the `new2` Phase-2 gate, and the `i18n-align` routine; `coder` STEP 9 and `code-reviewer` enforce the same rule at the agent level (real layers, not hollow — they do not assume the linter is wired). Key extraction reuses the stack's extractor (`i18next-cli`/`lingui`/`formatjs`, detection-only). Drift is contained in **3 tiers** mirroring the design-system discipline: per-task (`coder` STEP 9 populates the registry stub), per-merge (`doc-reviewer` curates context/orphans in `/new`), weekly (`i18n-align` routine translates the maintained `i18n.target_languages`, lints, and commits **directly to the trunk — no PR**, since translation files are low-risk). The always-on HARD rule ("never hardcode a user-facing string") lives in `framework/AGENTS.md` § 4.b gated on the flag. Strict specialization throughout: `coder` externalizes + populates, `doc-reviewer` curates, `i18n-translator` (`model: sonnet`, `effort: low`, bounded flag-not-guess) translates — none crosses lanes. **MINOR** (additive, opt-in: new agent + two new skills (`/i18n`, `/i18n-adopt`) + new routine + new flag/`i18n:` block + `paths.i18n_registry`; no removed surface). Schema-change propagation rule applies (`features.has_i18n` + `paths.i18n_registry` + `i18n.*` through template + configure + update detector + doctor + CHANGELOG).
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.52.
|
|
1
|
+
4.52.2
|
|
@@ -74,9 +74,11 @@ Translations are NOT stored here — they live in the native locale files.
|
|
|
74
74
|
convention when the file exists → SKIP (never a silent pass). It is a dedicated
|
|
75
75
|
ESLint run **independent of the project's main lint** (so it works even on a
|
|
76
76
|
Biome-only toolchain) and **never touches the user's config**. The default rule
|
|
77
|
-
mode is `jsx-only
|
|
78
|
-
JSX attributes**
|
|
79
|
-
|
|
77
|
+
mode is `jsx-only` with a `jsx-attributes.include` allowlist: it deterministically
|
|
78
|
+
blocks **JSX text nodes AND the user-facing attributes** `placeholder`/`title`/`alt`/
|
|
79
|
+
`label`/`aria-label`, while **technical props** (`variant`/`size`/`href`/`className`/
|
|
80
|
+
`type`/…) are NOT flagged — verified on a real codebase (3260 raw findings → ~1500
|
|
81
|
+
genuine, 0 technical-attribute false positives). Runners: `qa-sentinel` (for `/new`, `/qa`),
|
|
80
82
|
the classic `/new` Phase-2 gate, the `new2` Phase-2 gate, and the `i18n-align`
|
|
81
83
|
routine (pre-commit). This is the HARD enforcement for JSX-rendered strings.
|
|
82
84
|
2. **Semantic backstop (review)** — non-JSX imperative strings are OUTSIDE the
|
package/package.json
CHANGED
|
@@ -176,8 +176,8 @@ function detect(cwd = process.cwd()) {
|
|
|
176
176
|
if (hasDep('next-intl')) return 'next-intl';
|
|
177
177
|
if (hasDep('react-intl') || hasDep('@formatjs/intl')) return 'react-intl';
|
|
178
178
|
if (hasDep('@lingui/core') || hasDep('@lingui/react')) return 'lingui';
|
|
179
|
-
if (hasDep('vue-i18n')) return 'vue-i18n';
|
|
180
|
-
if (hasDep('i18next')) return 'i18next';
|
|
179
|
+
if (hasDep('@nuxtjs/i18n') || hasDep('@nuxt/i18n') || hasDep('vue-i18n')) return 'vue-i18n';
|
|
180
|
+
if (hasDep('next-i18next') || hasDep('react-i18next') || hasDep('i18next')) return 'i18next';
|
|
181
181
|
return '';
|
|
182
182
|
};
|
|
183
183
|
const detectLocalesRoot = () => {
|
|
@@ -504,6 +504,9 @@ function detect(cwd = process.cwd()) {
|
|
|
504
504
|
// Drives the prompt default; the config block is populated interactively.
|
|
505
505
|
has_i18n: Boolean(detectedI18nFramework),
|
|
506
506
|
},
|
|
507
|
+
// Detected i18n framework string (for the interactive prompt default — kept
|
|
508
|
+
// off `features` so it's a reference, never serialized to config).
|
|
509
|
+
i18n: { framework: detectedI18nFramework, locales_root: detectLocalesRoot() },
|
|
507
510
|
tools: {
|
|
508
511
|
enabled: toolAdapters.defaultEnabled(cwd)
|
|
509
512
|
},
|
|
@@ -1042,7 +1045,7 @@ async function interactivePrompts(merged, detected) {
|
|
|
1042
1045
|
UI.section('i18n layer (context registry + context-aware translation)');
|
|
1043
1046
|
merged.i18n.framework = await promptForKey(
|
|
1044
1047
|
'i18n framework (next-intl | i18next | react-intl | lingui | vue-i18n | custom)',
|
|
1045
|
-
merged.i18n.framework ||
|
|
1048
|
+
merged.i18n.framework || (detected.i18n && detected.i18n.framework) || ''
|
|
1046
1049
|
);
|
|
1047
1050
|
merged.i18n.source_language = await promptForKey(
|
|
1048
1051
|
'Source language (BCP-47 tag, e.g. en, en-US)',
|
|
@@ -1057,7 +1060,7 @@ async function interactivePrompts(merged, detected) {
|
|
|
1057
1060
|
.split(',').map((l) => l.trim()).filter(Boolean);
|
|
1058
1061
|
merged.i18n.locales_root = await promptForKey(
|
|
1059
1062
|
'Native locale files root (e.g. src/locales, public/locales)',
|
|
1060
|
-
merged.i18n.locales_root ||
|
|
1063
|
+
merged.i18n.locales_root || (detected.i18n && detected.i18n.locales_root) || ''
|
|
1061
1064
|
);
|
|
1062
1065
|
// Sensible default for the on-demand key-extraction command (detection-only).
|
|
1063
1066
|
if (!merged.i18n.extract_command) {
|
package/src/utils/i18n-gate.js
CHANGED
|
@@ -47,10 +47,11 @@ const FLAT_BODY = `// BALDART-owned i18n anti-hardcoded gate (generated by \`bal
|
|
|
47
47
|
// main ESLint setup. ESLint 9 flat config. Run via: ${FLAT_COMMAND}
|
|
48
48
|
//
|
|
49
49
|
// Fails when a user-facing string is hardcoded instead of routed through your i18n
|
|
50
|
-
// translation function. \`mode: '${MODE}'\`
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
50
|
+
// translation function. \`mode: '${MODE}'\` checks JSX text; \`jsx-attributes.include\`
|
|
51
|
+
// restricts ATTRIBUTE checks to the user-facing ones only (placeholder/title/alt/
|
|
52
|
+
// label/aria-label) so technical props (variant/size/href/className/type/…) are NOT
|
|
53
|
+
// flagged as false positives. Broaden the include list (or \`mode: 'all'\`) for stricter
|
|
54
|
+
// coverage — BALDART writes this file only when absent and never overwrites your edits.
|
|
54
55
|
import i18next from 'eslint-plugin-i18next';
|
|
55
56
|
import tseslint from 'typescript-eslint';
|
|
56
57
|
|
|
@@ -67,7 +68,10 @@ export default [
|
|
|
67
68
|
},
|
|
68
69
|
plugins: { i18next },
|
|
69
70
|
rules: {
|
|
70
|
-
'i18next/no-literal-string': ['error', {
|
|
71
|
+
'i18next/no-literal-string': ['error', {
|
|
72
|
+
mode: '${MODE}',
|
|
73
|
+
'jsx-attributes': { include: ['placeholder', 'title', 'alt', 'label', 'aria-label'] },
|
|
74
|
+
}],
|
|
71
75
|
},
|
|
72
76
|
},
|
|
73
77
|
];
|
|
@@ -78,7 +82,12 @@ const LEGACY_BODY = JSON.stringify({
|
|
|
78
82
|
parser: '@typescript-eslint/parser',
|
|
79
83
|
parserOptions: { ecmaFeatures: { jsx: true }, sourceType: 'module' },
|
|
80
84
|
plugins: ['i18next'],
|
|
81
|
-
rules: {
|
|
85
|
+
rules: {
|
|
86
|
+
'i18next/no-literal-string': ['error', {
|
|
87
|
+
mode: MODE,
|
|
88
|
+
'jsx-attributes': { include: ['placeholder', 'title', 'alt', 'label', 'aria-label'] },
|
|
89
|
+
}],
|
|
90
|
+
},
|
|
82
91
|
ignorePatterns: ['node_modules/', 'dist/', 'build/', '.next/', 'out/', 'coverage/', '*.test.*', '*.spec.*'],
|
|
83
92
|
}, null, 2) + '\n';
|
|
84
93
|
|