Skip to content

Count with a plural rather than an apologetic "(s)" - #199

Merged
vmillet-dev merged 3 commits into
mainfrom
counts-that-agree
Sep 18, 2026
Merged

vmillet-dev merged 3 commits into
mainfrom
counts-that-agree

Conversation

@vmillet-dev

Copy link
Copy Markdown
Owner

Closes #185.

⚠️ This one carries a parser I wrote rather than a library, and that is the part worth your eyes. Everything else is mechanical.

The dependency was installed, and it cannot run here

You steered me to @jsverse/transloco-messageformat, and the reasoning held: 18 of the 28 strings carry more than one (s), four of them three independent counts in a single sentence with three separate agreements. A key per form would have meant eight variants of those alone.

It compiles each message into a function with new Function. tauri.conf.json is script-src 'self'. The application booted onto a CSP error banner, seeded no sample note, and 19 of 21 e2e spec files failed.

⚠️ The 1339 unit tests passed the whole time. Vitest runs under jsdom, where there is no policy to violate — this is a class of failure only the assembled application can show, and I would have opened this pull request without it had I not run the suite.

What replaces it

plural-transpiler.ts, ~60 lines, reading exactly the ICU syntax already written — so the 28 converted strings are kept as they are. No dependency, no eval, no CSP change.

Two choices that bound the risk of hand-rolling this:

  • It knows plural, exact =N, the categories Intl.PluralRules answers, and #. It does not know select, selectordinal, or a plural inside a plural. Nothing here needs them, and a parser that guesses at a grammar it does not implement is worse than one that refuses it.
  • Plain interpolation stays {{name}} — the convention of the other 75 strings — so DefaultTranspiler keeps handling it and this code never touches it. That includes the sibling-key resolution {{app}} depends on.

Eleven tests, the two that matter most being:

expect(fr('{count, plural, one {# note} other {# notes}}', { count: 0 })).toBe('0 note');
expect(en('{count, plural, one {# note} other {# notes}}', { count: 0 })).toBe('0 notes');
expect(new Intl.PluralRules('fr').select(0)).toBe('one');

Zero taking the singular in French is the thing no doubled key of mine got right, and the third line keeps the reason written down rather than in a commit message.

⚠️ One debt, deliberate and commented: TranslocoService asks for the transpiler in its own constructor, so taking it by injection would be a cycle. The service is resolved on the first render instead.

The rest

  • 28 strings to ICU in both locales, 0 (s) left, 506 keys in parity. Two guards on the shipped files: no (s) may come back, and no brace may appear that is neither an interpolation nor a plural.
  • shortcuts.unavailable is the only one that needed the call site changed: it agrees three times over and had no count at all, only a list.
  • The trash off-by-one. ⚠️ Not the cause the ticket gave. ClockService ticks every 30 s, so the now a card renders against can be behind an instant Rust has just stamped; the gap reads as 30 days and change, and rounding up made the change a whole day. The ticket proposed floor, which says 29 for the same note — the same contradiction reversed. It rounds now, and the test reproduces the exact string from the screenshot.
  • 34 tooltips, each from the same key as its aria-label. ⚠️ The ticket said 84; that counted every occurrence of the attribute rather than the buttons a mouse user cannot read.
  • One empty state, not five. ⚠️ The ticket listed five as if each had an exit to offer. Only noResults is a dead end — clearFilters() was sitting there unoffered — and it is now conditioned on hasUserFilters, never isFiltering, which is true inside a folder.

🤖 Generated with Claude Code

vmillet-dev and others added 2 commits September 18, 2026 11:38
Twenty-eight strings refused to choose a plural. Eighteen carried more than one
"(s)", and four carried three independent counts in a single sentence, each with
its own agreement — a key per form would have been eight variants of those alone.

The library for this cannot run here. @jsverse/transloco-messageformat compiles
every message with new Function, the CSP is script-src 'self', and the
application boots onto an error banner and seeds nothing. The unit suite passed
throughout: vitest runs under jsdom, where there is no policy to violate. Only
the e2e run said so, with 19 spec files out of 21 red.

So the transpiler is ours, sixty lines over Intl.PluralRules, reading the ICU the
strings were already written in. It knows plural, exact =N, the categories and #,
and nothing else — a parser that guesses at a grammar it does not implement is
worse than one that refuses it. Plain interpolation stays {{name}} and stays the
default transpiler's, which is what keeps {{app}} resolving against a sibling key.

Three more from the same ticket. The trash said "erased in 31 d" under a sentence
promising 30: the clock ticks every thirty seconds, so the now a card renders
against can be behind an instant Rust has just stamped, and rounding up made the
difference a whole day. Thirty-four icon buttons gained a tooltip, from the same
key as their label. And the one empty state that was a dead end now offers the
clearing that was already sitting there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vmillet-dev vmillet-dev added this to the v0.3.1 — Room to work milestone Sep 18, 2026
@vmillet-dev vmillet-dev added the bug Something isn't working label Sep 18, 2026
The zero case is written out in words, and the scenario asserted the French
ones. The e2e suite runs in whatever language the machine is set to: French
here, English on both runners. It passed locally and could not pass on CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vmillet-dev
vmillet-dev merged commit 837be2c into main Sep 18, 2026
9 checks passed
@vmillet-dev
vmillet-dev deleted the counts-that-agree branch September 18, 2026 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The counts say "(s)", the trash contradicts itself, and no icon button has a tooltip

1 participant