fix(i18n): convert plural templates from ICU to i18next native format #264

Merged
james merged 1 commit from fix-plural-templates into main 2026-06-23 20:56:44 +00:00
Owner

Summary

The Jobs screen rendered 0 of {# of positions}} instead of 0 positions — the catalog had ICU MessageFormat plural templates ({count, plural, one {# position} other {# positions}}) but the universal client uses i18next, which doesn't parse ICU MessageFormat without the i18next-icu plugin. So the template body leaked through literal after the leading {count} substitution.

The ICU syntax was inherited from the API side, where next-intl consumed it. These keys are client-only.

Fix

Convert to i18next-native plural format — separate _one / _other keys with regular {count} interpolation:

- "positionsCount": "{count, plural, one {# position} other {# positions}}",
+ "positionsCount_one": "{count} position",
+ "positionsCount_other": "{count} positions",

Same for contributionsCount. i18next picks the right key automatically based on the count argument.

Spanish catalog (es.json) didn't have these keys; falls back to English per ADR-0025.

Test plan

  • pnpm -F @carol/client typecheck / test (97 passing).
  • jq verifies the four new keys parse and resolve correctly.
  • Manual on Android via Expo Go: Jobs screen renders 0 positions / 1 position / N positions correctly.

Future

If more ICU-shaped strings creep into the catalog, consider pulling in i18next-icu to handle them uniformly across both runtimes (API uses next-intl which is ICU-native; client uses i18next which needs the plugin). For now the catalog's only ICU users were these two keys.

## Summary The Jobs screen rendered `0 of {# of positions}}` instead of `0 positions` — the catalog had ICU MessageFormat plural templates (`{count, plural, one {# position} other {# positions}}`) but the universal client uses **i18next**, which doesn't parse ICU MessageFormat without the i18next-icu plugin. So the template body leaked through literal after the leading `{count}` substitution. The ICU syntax was inherited from the API side, where `next-intl` consumed it. These keys are client-only. ## Fix Convert to i18next-native plural format — separate `_one` / `_other` keys with regular `{count}` interpolation: ```diff - "positionsCount": "{count, plural, one {# position} other {# positions}}", + "positionsCount_one": "{count} position", + "positionsCount_other": "{count} positions", ``` Same for `contributionsCount`. i18next picks the right key automatically based on the `count` argument. Spanish catalog (`es.json`) didn't have these keys; falls back to English per ADR-0025. ## Test plan - [x] `pnpm -F @carol/client typecheck` / `test` (97 passing). - [x] `jq` verifies the four new keys parse and resolve correctly. - [ ] Manual on Android via Expo Go: Jobs screen renders `0 positions` / `1 position` / `N positions` correctly. ## Future If more ICU-shaped strings creep into the catalog, consider pulling in `i18next-icu` to handle them uniformly across both runtimes (API uses next-intl which is ICU-native; client uses i18next which needs the plugin). For now the catalog's only ICU users were these two keys.
fix(i18n): convert plural templates from ICU to i18next native format
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 13s
PR / OSV-Scanner (pull_request) Successful in 22s
PR / Static analysis (pull_request) Successful in 37s
PR / Trivy (image) (pull_request) Successful in 1m31s
PR / Package age policy (soft) (pull_request) Successful in 13s
Secrets / gitleaks (pull_request) Successful in 14s
PR / pnpm audit (pull_request) Successful in 2m57s
PR / Typecheck (pull_request) Successful in 3m3s
PR / Test (postgres) (pull_request) Successful in 3m11s
PR / OpenAPI (pull_request) Successful in 3m29s
PR / Build (pull_request) Successful in 3m32s
PR / Test (sqlite) (pull_request) Successful in 3m41s
PR / Coverage (soft) (pull_request) Successful in 3m25s
PR / Lint (pull_request) Successful in 4m29s
PR / Client (web export smoke) (pull_request) Successful in 4m59s
9417737091
The catalog had ICU MessageFormat plurals (`{count, plural, one {#
position} other {# positions}}`) authored when the API rendered them
via next-intl. The universal client uses i18next, which doesn't parse
ICU MessageFormat without the i18next-icu plugin — so the literal
template body leaked through to the UI ("0 of {# of positions}}" in
the user-facing wording).

Convert to i18next's native plural format: separate `_one` / `_other`
keys per plural. i18next picks the right key based on `count` and
applies the regular {count} interpolation.

Closes the visible regression on the Jobs screen.
james merged commit 574b4d2cd7 into main 2026-06-23 20:56:44 +00:00
james deleted branch fix-plural-templates 2026-06-23 20:56:44 +00:00

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 82.7% ≥ 50%
Branches 75.1% ≥ 75%
Functions 91.7% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 82.7% ✅ | ≥ 50% | | Branches | 75.1% ✅ | ≥ 75% | | Functions | 91.7% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
Sign in to join this conversation.
No description provided.