You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(cli): report os i18n extract key counts off the emitted bytes (#16247)
* fix(cli): report i18n extract key counts off the emitted bytes
`extractTranslations` returned `counts[locale]` as a walk counter and the
command spent it as the size of the file it had just written. Under the
default `--objects-only` those are different numbers: on a one-object,
one-app stack the run announced `Wrote …objects.generated.ts (776 keys)`
for a file holding 2 leaves, and summarised it as `776 key(s) (of 776
expected) + 773 metadataForms key(s)` — appending a number the 776
already contained.
`counts` is now a leaf count of the locale's skeleton, taken off the tree
rather than off the walk, and documented as not being any file's size.
Every count the command reports is `countTranslationLeaves` of that
module's own payload, selected with the new `translationModulePayload` —
the same function the renderer renders from, so a count and its bytes
cannot drift apart, including for a sub-tree mode added later. The
summary is a partition of the skeleton (`E of S key(s) emitted` plus a
per-module breakdown), never a sum over it, and nothing subtracts one
count from another at a print site.
Two consequences of the same conflation go with it: the emit gate is now
the module's own leaf count, so a stack with no objects no longer writes
an empty module under `--objects-only`; and `--json`'s `counts` now
counts the `bundles` payload beside it, as `metadataFormsCounts` already
counted `metadataForms`.
The pin spawns the real CLI in four flag states and compares each printed
number against a structural leaf count of the module parsed back off
disk — the comparison the defect precluded.
Fixes#16121
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
* test(cli): fix the grammar of the key-count pin's case title
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
* fix(cli): unbreak the pin's typecheck, drop a false symmetry claim, report suppressed modules
Four repairs from contract review, none of them a rethink of the count design.
1. `runExtract`/`runJson` took `flags: string[]` while the partition table is
`as const`, so the call site handed them a readonly tuple: TS2345 at
test/i18n-extract-key-count.e2e.test.ts(203,59). The parameters only ever
read, so they are `readonly string[]` now — the table keeps its literal
types. Present since the first commit, and green under every gate that was
re-run at that head because each either skips the test layer or is
type-blind.
2. The changeset, the PR body and the `--json` comment all claimed the new
`counts`/`bundles` relationship was "the relationship `metadataFormsCounts`
already had to `metadataForms`". It is not: `metadataFormsCounts` reports
the baseline as BUILT whether or not it is emitted, so under
`--no-metadata-forms` the payload carries a positive count beside an empty
`metadataForms` map. The claim is corrected in all three places and nothing
about either face moves; whether `--json` SHOULD carry two count semantics
is left to the maintainer. The `--json` case now drives `--metadata-forms`
in both states, which is what would have caught the claim.
3. The summary dropped an operator reading on the commonest path:
`--no-metadata-forms` is what 8 of this repo's 9 extract configs pass, and
there the row named nothing at all. A module a flag suppressed is now a
CANDIDATE that is reported but not written — named with its size and the
words `not emitted`, so it stays out of the total:
zh-CN 2 of 776 key(s) emitted objects 2 · metadataForms 773 not emitted
That is mode-agnostic: a later sub-tree mode is a candidate like any other.
The tone also read green on `0 of 774 emitted`; green means there is nothing
to translate, which is a property of the skeleton, so it now reads that.
4. The emitted-files mirror judged its `--no-objects-only` arm on the whole
bundle while the command judges it on the stack-authored subtree. They
diverge on a bundle with no authored surface; the mirror subtracts the
baseline too, and a case drives that input class.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
---------
Co-authored-by: Claude <noreply@anthropic.com>
`os i18n extract` reports key counts that describe the bytes it emitted, and its summary is a partition of the skeleton rather than a sum over it.
6
+
7
+
`extractTranslations` returned `counts[locale]` as a WALK counter — `count += 1` once per expected entry, unconditionally — and the command spent it as the number of keys in the file it had just written. Under the default `--objects-only` the module holds only the `objects` sub-tree, so the two are different numbers. Driven on a one-object, one-app stack with `i18n.defaultLocale: 'zh-CN'`:
The file that run wrote holds **2** leaves. The true split of the 776 is 2 objects + 1 app + 773 metadata-form baseline, so the summary appended a number the 776 already contained and read as 1549 out of 776 — an operator could not derive the truth from it, and the `(776 keys)` described no file the run produced. Both lines now read off the emitted tree:
Wrote OUT/zh-CN.metadata-forms.generated.ts (773 keys)
22
+
```
23
+
24
+
**What each number now means.**`ExtractResult.counts[locale]` is a leaf count of `bundles[locale]` — the whole skeleton built for that locale, taken off the tree instead of off the walk that built it. It is explicitly not the size of any one file: which sections of the skeleton become committed modules is the caller's decision. The command therefore takes every count it reports off that module's own payload, selected with `translationModulePayload` — the same function `renderTranslationModule` renders from, so the number and the bytes cannot drift apart, including for a sub-tree mode added later. Nothing subtracts one count from another at a print site: that would repair today's two modes and leave the third wrong in the same way.
25
+
26
+
**The summary line's shape changed** from `N key(s) (of N expected) + M metadataForms key(s)` to `E of S key(s) emitted` with a per-module breakdown. `E` is what this run's modules hold together and `S` is what the locale's skeleton holds, so `E ≤ S` always and the gap is exactly the keys a flag excluded — one app label under the default `--objects-only`, and nothing at all under `--no-objects-only`. A module a flag SUPPRESSED is named in the breakdown too, with its size and the words `not emitted` that keep it out of `E`: under `--no-metadata-forms` the row reads `2 of 776 key(s) emitted objects 2 · metadataForms 773 not emitted`, so the operator still sees how big the baseline they switched off is — which the old, double-counting line did tell them.
27
+
28
+
**A module with no leaves is no longer written.** The emit gate was `counts[locale] > 0`, a property of the skeleton: on a stack whose only surface is apps, the default `--objects-only` wrote a `<locale>.objects.generated.ts` holding `{}` and announced it as 774 keys. The gate is now the module's own leaf count.
29
+
30
+
**`--json`**: `counts` is now the leaf count of the `bundles` payload printed beside it, instead of the extractor's skeleton size. The skeleton total is unchanged and still reported, under its own name, as `totalExpected`.
31
+
32
+
⚠️ That is **not** the relationship `metadataFormsCounts` has to `metadataForms`, and nothing here changes the latter. `metadataFormsCounts` reports the baseline as BUILT, emitted or not: under `--no-metadata-forms` the payload carries `metadataFormsCounts: { 'zh-CN': 773 }` beside an empty `metadataForms`, deliberately, and a pin holds it there. So the payload carries two count semantics — `counts` is what was emitted, `metadataFormsCounts` is what was built. Both faces are unchanged by this note; it exists because an earlier draft of it claimed a symmetry that does not hold.
33
+
34
+
**No committed bundle moves.** All nine extract configs in this repository run under the default `--objects-only` on stacks that do author objects, and every emitted module is byte-for-byte unchanged; `pnpm check:i18n` stays green on the committed tree. What changed is stdout, the `--json` counts, and the emission of a module that would have been empty.
35
+
36
+
The regression pin spawns the real CLI in four flag states and compares each printed count against a structural leaf count of the module it wrote, parsed back off disk. That comparison is the thing the defect precluded: a walk counter cannot disagree with the walk, so no assertion over `ExtractResult` could have failed while the printed number was wrong by two orders of magnitude. Its `--json` case drives `--metadata-forms` in both states, because a case that drives one state of a flag cannot see what that flag does — driving it ON only is exactly how the symmetry claim above survived unmeasured into a first draft.
0 commit comments