…e item
The full-page search header is one ternary with two branches. The query branch
already picked its key on `totalCount === 1` (`search.resultsCount` against
`search.resultsCountPlural`); the browse branch beside it asked for
`search.itemsAvailable` at every count. That key had no plural family and no
sibling key, so `en` shipped `1 items available` at a single searchable item,
and `de`/`es`/`fr`/`pt` read the same way.
Adds `search.itemsAvailableOne` to all ten packs and picks between the two on
`allItems.length === 1` — this repo's two-key plural convention
(`common.itemCount`/`itemCountOne`, `detail.reactionCount`/`reactionCountOne`),
deliberately not an i18next `_one`/`_other` family: key parity caps a family at
base + `_one` + `_other`, so every other CLDR category falls through to the base
key, which here is the plural — what `ru` meets at 2-4 and `ar` at 2, 3-10 and
11-99. Selecting in the component keeps `Intl.PluralRules` and `fallbackLng` out
of the path.
`zh`/`ja`/`ko` repeat the string (the counter word holds the number) and so does
`ru` (its phrasing states no noun). `ar`'s value changed rather than being copied:
its parenthesised marker covered both numbers on a key that can no longer be
reached at one, so it now uses the noun pair `ar`'s own `common.itemCount` pair
already uses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Fixes #9664
The full-page search header is one ternary with two branches. The query branch already chose its key on
totalCount === 1—search.resultsCountagainstsearch.resultsCountPlural. The browse branch on the very next line asked forsearch.itemsAvailableat every count, and that key had neither a plural family nor a sibling to fall to, so the shipped default language read1 items availableat a single searchable item. ⭐ The correct pattern was not elsewhere in the codebase; it was the line above.The route, and why it beat the alternatives on THIS key
search.itemsAvailableOneis added to all ten packs as the singular half, and the browse branch now picks between the two halves onallItems.length === 1— the same device its neighbour uses, in this repo'sxxxOnespelling.This repo's two-key plural convention, not an invention:
common.itemCount/itemCountOne,detail.reactionCount/reactionCountOne,collaboration.commentCount/commentCountOne.useDetailTranslation's defaults-map comment names it in those words, and thecommon.itemCountbadge comment records the reason count labels take two keys rather than an i18next family: zh/ja/ko have no separate singular form, so those packs would legitimately omit an_onehalf, and full key parity reads a legitimately-absent half as a lost key.⛔ Deliberately NOT an i18next
_one/_otherfamily, and that is a measurement of this key rather than a preference:_oneplus_other, so every CLDR category a pack does not spell out falls through to the base key — here the plural.rumeets that at 2 to 4 andarat 2, 3 to 10 and 11 to 99.ru's value states no noun to agree with, so there is nothing for_few/_manyto get right that one string does not already get right.ar: under a family the base key serves 0, 2, 3 to 10 and 11 to 99 — the everyday counts — and the base key is the value that carried the parenthesised marker.Intl.PluralRulesandfallbackLngout of the path entirely: both halves exist in every pack, so no count in any language can reach English — the failure mode objectui#3863 is about.⭐ Why the
xxxOnespelling rather than thexxxPluralone (resultsCount/resultsCountPluralon the adjacent branch):xxxPluralwould have meant re-pointingen's existingsearch.itemsAvailableat the singular. That is anenvalue change, andpnpm check:i18n-driftthen requires the nine translations to follow — but zh/ja/ko/ru have nothing to follow here, so it would have taken a waiver in the drift ledger plus a raise of that ledger's ceiling pin, both outside this card's declared file surface. ThexxxOnespelling renders identically with zero value churn: the gate printsNo en value changed in this range.⛔ objectui#9266 (landed as PR objectui#9663) was read as the worked precedent and not copied. It chose a count-invariant phrase because its call site had one key and rendered a noun beside a number; here the two-key switch is available on the adjacent branch, so English keeps a real sentence instead of giving up agreement.
The ten packs, each at exactly one item
en1 items available1 item availablede1 Elemente verfügbar1 Element verfügbares1 elementos disponibles1 elemento disponiblefr1 éléments disponibles1 élément disponiblept1 itens disponíveis1 item disponívelar1 عنصر(عناصر) متاح(ة)1 عنصر متاحru1 доступно1 доступноja1 件利用可能ko1개 항목 사용 가능zh共 1 项可搜索The one value edit, named rather than ridden in
ar's count-≠-1 value changed from the parenthesised-marker form to the noun pairar's owncommon.itemCount/itemCountOnealready uses. The reason is a consequence of this change, not a sweep: the marker existed to cover both numbers in one string, and that key can no longer be reached at one item, so its singular half became dead weight while the parentheses still rendered at every count the key does serve. ⛔ No new parenthesised marker is introduced in any pack. The key-scoped pin onpreview.history.itemsis untouched and makes no claim about this key.What
all-locales-key-parityactually requires, re-derivedThree separate invariants, all re-read on this branch rather than summarised from elsewhere:
enkey and no pack defines a keyenlacks, minus four outbound-message keys. ⇒ a new key is all ten packs or none; it is whyzh/ja/ko/rucarry a half they do not linguistically need.en's, compared pack againsten. ⇒ both halves carry exactly one count hole in every pack._oneplus_otherand sends every other category to the base value.The new key touches none of the three adversely, and the suite is green.
Evidence
Control reading. The fix was committed first, then the two source surfaces were reverted to the branch base with the new tests left in place, the mutation was verified on disk in both directions (new key: 1 call-site hit and 10 packs before, 0 and 0 after; pre-fix call site back: 1 hit;
armarker back: 1 hit), and the restore was proved by an emptygit diff HEADrather than by an exit code.Unable to find an element with the text: 1 item availableSuites and gates, at
b5b604daf:vitest run packages/i18n/vitest run packages/app-shell/turbo run type-checkfor both packages (with the^buildclosure)turbo run lintfor both packages plus the root legcheck:i18n-driftNo en value changed in this rangecheck:i18n-keys·check:i18n-dead-keys·check:i18n-designer-paritycheck:vi-mock-specifiers·check:vi-mock-inherit·check:vi-mock-override-shape·check:test-path-rootscheck:control-bytes·check:new-line-citationsDeclared narrowings, so the difference between a measurement and a skipped run stays legible:
lintwas run for the two affected packages out of the 46 that carry a lint script, which is the repo's own "only the affected packages" rule rather than a file-level filter; the repo-widepnpm test,pnpm checkand the workflow's coverage/entry-guard/port-parity/bash-floor steps are CI's run and were not taken locally — nothing in this diff reaches what they read.type-checkcovers:tsc --noEmitexcludes__tests__by directory, so the new tests are checked by the second leg of the script (tsc -p tsconfig.test.json), which is part of the green above.Acceptance notes
Two observations found while measuring this card, neither of them fixed here and neither filed by this seat:
search.resultsCountPluralis wrong in Russian at 2 to 4 — and in Arabic at 2. The adjacent branch this card copies its device from is explicitly out of the fence, and it is repaired here in no way. But the same binary switch that is sufficient onitemsAvailableis not sufficient there, becauseru's value on that key does carry an agreeing noun: Russian readsрезультатовwhere 2 to 4 needsрезультата. Named probe: Russian's category at 2 isfew, Arabic's at 2 istwo, and the pack defines neither. Dedupe words:resultsCountPlural·ru few category·search results plural agreement·two-key switch insufficient·binary plural Russian.preview.history.itemsbounds the letters inside the parentheses at four;عنصر(عناصر)has five, so that marker scores absent under it whileمتاح(ة)scores present — both halves of the same retired value. Correct and sufficient for the key it guards today; the bound is the thing that would be false if such a marker arrived there. Measured, and pinned in the other direction in this branch's own test. Dedupe words:parenthesised plural marker·marker regex bound·Unicode property class {1,4}·Arabic broken plural five letters·preview.history.items pin.Noted, not filed: the drift ledger's waiver ceiling is coupled to a test edit by design — that is the ratchet working, not a defect, and the next seat to touch that ledger is the one who meets it. Carrier: none needed.
Generated by Claude Code