As a user I want count-based messages to be grammatically correct so the UI reads naturally in every supported language #287

Open
opened 2026-04-20 12:42:32 +02:00 by marcel · 8 comments
Owner

Context

Pluralization is currently handled three different ways in frontend/messages/{de,en,es}.json:

  1. Hand-rolled _one / _many pairs — e.g. person_card_doc_count_one ("1 doc.") / person_card_doc_count_many ("{count} docs."). Selection happens in components via a ternary.
  2. (s) / (n) suffix hacks — e.g. ES hace {count} minuto(s), {count} documento(s) creado(s), {count} página(s) omitida(s).
  3. Plural-only messages that break for count === 1 — e.g. {count} documentos renders as "1 documentos".

The result is inconsistent, duplicated, and a steady source of small rendering bugs.

Proposal

Adopt Paraglide's message variants for every count-bearing message.

Variants use CLDR plural rules, live entirely in the translation files, and are type-safe at build time. Call sites go from count === 1 ? m.foo_one() : m.foo_many({ count }) to a single m.foo({ count }).

Scope

  1. Migrate every existing count-bearing message in de.json, en.json, es.json to variant syntax — every _one / _many pair, every (s) / (n) message, every plural-only message that takes a {count}.
  2. Update call sites — replace ternary-based plural selection in components with a single variant call.
  3. Delete the old keys — remove _one / _many pairs after all call sites are migrated.

Known affected key groups (non-exhaustive — grep {count} to get the full list):

  • docs_result_count, docs_empty_for_term
  • person_card_doc_count_*, person_show_more, persons_stats_persons_many, persons_stats_documents_many
  • upload_success, enrich_needs_metadata_count, enrich_progress
  • ocr_status_creating_blocks, ocr_status_done_blocks, ocr_status_done_skipped
  • transcription_status_sections, transcription_reviewed_count
  • comment_time_minutes / _hours / _days
  • admin_groups_permission_count, admin_system_backfill_success, admin_system_import_status_done
  • admin_tag_children_more, admin_tag_merge_preview_docs, admin_tag_merge_preview_children, admin_tag_delete_subtree_warn
  • topbar_overflow_more, topbar_overflow_show, doc_details_more_receivers
  • conv_letters_count, notification_bell_unread_label
  • mission_control_ready_subtitle, mission_control_weekly_pulse, dashboard_blocks

Acceptance criteria

  • No remaining _one / _many key pairs in any messages file.
  • No remaining (s) / (n) suffix hacks in any translation.
  • No component renders "1 documentos"-style output — spot-check search results, upload success, person card counts, OCR status, admin stats.
  • count === 0, count === 1, count === 2 all render correctly for every migrated key in de / en / es.
  • npm run check, npm run lint, and npm run test pass.

Non-goals

  • Adding new languages (only de / en / es in scope — all simple one / other).
  • Gender or other non-count variants — separate issue if needed later.

Notes / drawbacks

  • Payoff is modest for our locales (only one / other forms), but the (s) hacks are indefensible and variants kill them for free.
  • JSON becomes nested and slightly less grep-friendly — acceptable trade.
  • Best migrated incrementally per message area (search → persons → admin → ocr → …), one logical group per commit per our atomic-commit rule. Not as one monster PR.
## Context Pluralization is currently handled three different ways in `frontend/messages/{de,en,es}.json`: 1. **Hand-rolled `_one` / `_many` pairs** — e.g. `person_card_doc_count_one` ("1 doc.") / `person_card_doc_count_many` ("{count} docs."). Selection happens in components via a ternary. 2. **`(s)` / `(n)` suffix hacks** — e.g. ES `hace {count} minuto(s)`, `{count} documento(s) creado(s)`, `{count} página(s) omitida(s)`. 3. **Plural-only messages that break for `count === 1`** — e.g. `{count} documentos` renders as "1 documentos". The result is inconsistent, duplicated, and a steady source of small rendering bugs. ## Proposal Adopt Paraglide's [message variants](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/variants#pluralization) for every count-bearing message. Variants use CLDR plural rules, live entirely in the translation files, and are type-safe at build time. Call sites go from `count === 1 ? m.foo_one() : m.foo_many({ count })` to a single `m.foo({ count })`. ## Scope 1. **Migrate every existing count-bearing message** in `de.json`, `en.json`, `es.json` to variant syntax — every `_one` / `_many` pair, every `(s)` / `(n)` message, every plural-only message that takes a `{count}`. 2. **Update call sites** — replace ternary-based plural selection in components with a single variant call. 3. **Delete the old keys** — remove `_one` / `_many` pairs after all call sites are migrated. Known affected key groups (non-exhaustive — grep `{count}` to get the full list): - `docs_result_count`, `docs_empty_for_term` - `person_card_doc_count_*`, `person_show_more`, `persons_stats_persons_many`, `persons_stats_documents_many` - `upload_success`, `enrich_needs_metadata_count`, `enrich_progress` - `ocr_status_creating_blocks`, `ocr_status_done_blocks`, `ocr_status_done_skipped` - `transcription_status_sections`, `transcription_reviewed_count` - `comment_time_minutes` / `_hours` / `_days` - `admin_groups_permission_count`, `admin_system_backfill_success`, `admin_system_import_status_done` - `admin_tag_children_more`, `admin_tag_merge_preview_docs`, `admin_tag_merge_preview_children`, `admin_tag_delete_subtree_warn` - `topbar_overflow_more`, `topbar_overflow_show`, `doc_details_more_receivers` - `conv_letters_count`, `notification_bell_unread_label` - `mission_control_ready_subtitle`, `mission_control_weekly_pulse`, `dashboard_blocks` ## Acceptance criteria - No remaining `_one` / `_many` key pairs in any messages file. - No remaining `(s)` / `(n)` suffix hacks in any translation. - No component renders `"1 documentos"`-style output — spot-check search results, upload success, person card counts, OCR status, admin stats. - `count === 0`, `count === 1`, `count === 2` all render correctly for every migrated key in de / en / es. - `npm run check`, `npm run lint`, and `npm run test` pass. ## Non-goals - Adding new languages (only de / en / es in scope — all simple `one` / `other`). - Gender or other non-count variants — separate issue if needed later. ## Notes / drawbacks - Payoff is modest for our locales (only `one` / `other` forms), but the `(s)` hacks are indefensible and variants kill them for free. - JSON becomes nested and slightly less grep-friendly — acceptable trade. - Best migrated **incrementally per message area** (search → persons → admin → ocr → …), one logical group per commit per our atomic-commit rule. Not as one monster PR.
marcel added the featurerefactorui labels 2026-04-20 12:42:36 +02:00
Author
Owner

🏛️ Markus Keller — Senior Application Architect

Observations

  • Frontend-only refactor, zero backend/infra impact. No module-boundary concerns, no ADR needed for this size.
  • Paraglide 2.5 + plugin-message-format@4 is already in place (frontend/project.inlang/settings.json) — variants compile at build time into typed functions. No runtime cost, no new dependency.
  • 96 keys across de/en/es to migrate (32 per locale). That's big enough to need discipline; small enough to not warrant staged infra.
  • At least one message has two independent count variables: ocr_status_done_skipped: "{count} Blöcke erstellt, {skipped} Seite(n) übersprungen". Variant syntax supports multi-selector matches — don't collapse this to a single-selector message or you'll lose the grammar win for skipped.

Recommendations

  • Keep the per-area commit plan stated in the issue — don't consolidate into one mega-PR. Each commit is one area (search / persons / admin / ocr / …), greppable and revertible. This matches our atomic-commit rule.
  • Standardize on {count} as the selector name across all variant messages. Any message with a countable noun uses {count} — no {n}, no {amount}, no {total}. One convention, easier to grep, easier for future translators.
  • For the multi-count message (ocr_status_done_skipped), use a two-dimensional variant and add a frontmatter comment or ADR note if you find yourself introducing a third. Two dimensions is fine; three is a design smell that probably wants sentence composition.
  • Delete _one / _many keys in the same commit as the call-site migration, never in a separate cleanup commit. Leaving dead keys "for later" guarantees drift between the three locale files.
  • No ADR — this is a tactical refactor replacing a demonstrably broken pattern. Save ADR weight for structural decisions.

Open Decisions

  • None from my angle. Scope, sequencing, and mechanics are all clear.
## 🏛️ Markus Keller — Senior Application Architect ### Observations - Frontend-only refactor, zero backend/infra impact. No module-boundary concerns, no ADR needed for this size. - Paraglide 2.5 + `plugin-message-format@4` is already in place (`frontend/project.inlang/settings.json`) — variants compile at build time into typed functions. No runtime cost, no new dependency. - **96 keys** across `de/en/es` to migrate (32 per locale). That's big enough to need discipline; small enough to not warrant staged infra. - At least one message has **two independent count variables**: `ocr_status_done_skipped: "{count} Blöcke erstellt, {skipped} Seite(n) übersprungen"`. Variant syntax supports multi-selector matches — don't collapse this to a single-selector message or you'll lose the grammar win for `skipped`. ### Recommendations - **Keep the per-area commit plan stated in the issue** — don't consolidate into one mega-PR. Each commit is one area (search / persons / admin / ocr / …), greppable and revertible. This matches our atomic-commit rule. - **Standardize on `{count}` as the selector name** across all variant messages. Any message with a countable noun uses `{count}` — no `{n}`, no `{amount}`, no `{total}`. One convention, easier to grep, easier for future translators. - **For the multi-count message** (`ocr_status_done_skipped`), use a two-dimensional variant and add a frontmatter comment or ADR note if you find yourself introducing a third. Two dimensions is fine; three is a design smell that probably wants sentence composition. - **Delete `_one` / `_many` keys in the same commit as the call-site migration**, never in a separate cleanup commit. Leaving dead keys "for later" guarantees drift between the three locale files. - **No ADR** — this is a tactical refactor replacing a demonstrably broken pattern. Save ADR weight for structural decisions. ### Open Decisions - None from my angle. Scope, sequencing, and mechanics are all clear.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Observations

  • Surveyed existing call sites: only two places use the ternary plural pattern — frontend/src/routes/persons/PersonsStatsBar.svelte:13-19 and frontend/src/routes/persons/+page.svelte:162-164. The rest are plural-only messages called with { count } and silently rendering "1 Dokumente" / "1 documentos" style for count=1. Those are the quiet-failure sites; the ternaries are the loud ones.
  • PersonsStatsBar.svelte is a special case: its labels are called without a count argument (just the word "Personen" / "Person"). After migration, the component needs to pass { count: totalPersons } into the variant call — not "total people labeled", but "pluralize this noun using the count I'm already displaying next to it". Don't forget the prop.
  • Two Spanish bugs worth naming explicitly: hace {count} minuto(s) → count=0 Spanish uses plural ("hace 0 minutos"), which CLDR other already handles correctly. Variants fix this for free.

Recommendations

  • Red first, then green, per key. For each message being migrated, write a Vitest unit test that imports the compiled message function and asserts the rendered string for count=0, count=1, count=2, in all three locales. Run it red before editing the JSON, green after. Seven lines per test, fast feedback, proves the migration actually works.
  • One area, one commit, one test file. E.g. commit 1: person_card_doc_count_* + persons_stats_* + their test file. Never mix two areas in one commit — violates the atomic-commit rule, also makes revert painful if one area regresses.
  • Delete the _one / _many keys in the same commit as the call-site + variant edit. Do not leave a "cleanup" commit for later — dead keys in locale files rot.
  • Don't introduce new ternaries during migration. If you catch yourself writing count === 1 ? m.foo_one() : m.foo({ count }), that means the variant isn't wired up. The whole point is a single call.
  • For ocr_status_done_skipped (two count variables), write the multi-selector variant by hand first, test it with 2×2 combinations (1/1, 1/n, n/1, n/n) — this is where variant authoring goes wrong and one failing test will catch it.

Open Decisions

  • None. The plan and sequencing in the issue are concrete and correct; implementation is mechanical once the test-first discipline is in place.
## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Observations - Surveyed existing call sites: only two places use the ternary plural pattern — `frontend/src/routes/persons/PersonsStatsBar.svelte:13-19` and `frontend/src/routes/persons/+page.svelte:162-164`. The rest are **plural-only** messages called with `{ count }` and silently rendering "1 Dokumente" / "1 documentos" style for count=1. Those are the quiet-failure sites; the ternaries are the loud ones. - `PersonsStatsBar.svelte` is a special case: its labels are called **without a count argument** (just the word "Personen" / "Person"). After migration, the component needs to pass `{ count: totalPersons }` into the variant call — not "total people labeled", but "pluralize this noun using the count I'm already displaying next to it". Don't forget the prop. - Two Spanish bugs worth naming explicitly: `hace {count} minuto(s)` → count=0 Spanish uses plural ("hace 0 minutos"), which CLDR `other` already handles correctly. Variants fix this for free. ### Recommendations - **Red first, then green, per key.** For each message being migrated, write a Vitest unit test that imports the compiled message function and asserts the rendered string for `count=0`, `count=1`, `count=2`, in all three locales. Run it red before editing the JSON, green after. Seven lines per test, fast feedback, proves the migration actually works. - **One area, one commit, one test file.** E.g. commit 1: `person_card_doc_count_*` + `persons_stats_*` + their test file. Never mix two areas in one commit — violates the atomic-commit rule, also makes revert painful if one area regresses. - **Delete the `_one` / `_many` keys in the same commit** as the call-site + variant edit. Do not leave a "cleanup" commit for later — dead keys in locale files rot. - **Don't introduce new ternaries** during migration. If you catch yourself writing `count === 1 ? m.foo_one() : m.foo({ count })`, that means the variant isn't wired up. The whole point is a single call. - **For `ocr_status_done_skipped`** (two count variables), write the multi-selector variant by hand first, test it with 2×2 combinations (1/1, 1/n, n/1, n/n) — this is where variant authoring goes wrong and one failing test will catch it. ### Open Decisions - None. The plan and sequencing in the issue are concrete and correct; implementation is mechanical once the test-first discipline is in place.
Author
Owner

🔒 Nora Steiner — Application Security Engineer

Observations

  • Pure translation-file refactor. Paraglide compiles variants into TypeScript functions at build time; interpolation happens via string concatenation of statically-defined literals with the passed count value — no innerHTML, no eval, no runtime template parsing. Zero new injection surface.
  • No user-controlled text flows into the translation files or the variant branches. All {count} values are integers computed from backend data or client state.
  • Error-message translations (error_* keys) are not in scope of this migration — they don't carry counts — so no impact on the getErrorMessage() mapping layer.

Recommendations

  • No action required from a security perspective. If a future translation key ever needs to embed user-provided strings (names, titles), that's the moment to revisit — but this refactor doesn't open that door.

Open Decisions

(none)

## 🔒 Nora Steiner — Application Security Engineer ### Observations - Pure translation-file refactor. Paraglide compiles variants into TypeScript functions at build time; interpolation happens via string concatenation of statically-defined literals with the passed `count` value — no `innerHTML`, no `eval`, no runtime template parsing. Zero new injection surface. - No user-controlled text flows into the translation files or the variant branches. All `{count}` values are integers computed from backend data or client state. - Error-message translations (`error_*` keys) are not in scope of this migration — they don't carry counts — so no impact on the `getErrorMessage()` mapping layer. ### Recommendations - **No action required from a security perspective.** If a future translation key ever needs to embed user-provided strings (names, titles), that's the moment to revisit — but this refactor doesn't open that door. ### Open Decisions _(none)_
Author
Owner

🧪 Sara Holt — Senior QA Engineer

Observations

  • This refactor is entirely a correctness problem, so tests are the whole deliverable, not a bolt-on. The user-visible bugs ("1 Dokumente", "1 documentos", "minuto(s)") are trivial to introduce and trivial to miss in code review of three JSON files.
  • The acceptance criteria already name the three count boundaries that matter (0, 1, 2). Every migrated key needs evidence it works at all three — not "we spot-checked". Spot checks are how "1 Dokumente" shipped in the first place.
  • Paraglide variants can silently regress: a malformed variant declaration falls through to the default branch, which often looks fine in the editor preview but renders wrong for edge counts.

Recommendations

  • Build one systematic test harness up front, before migrating any key. A single frontend/src/lib/paraglide/variants.test.ts:
    • enumerates every migrated key + its required plural args,
    • renders each at count=0, count=1, count=2 across de, en, es,
    • asserts no output contains the regression signatures: /^1 \w+en\b/, /^1 \w+os\b/, /^1 \w+s\b/, \(s\), \(n\), \(es\).
      This runs in milliseconds, stays in the suite forever, catches anyone reintroducing the pattern in a future locale addition.
  • Add a targeted test for the multi-selector message ocr_status_done_skipped — render all four combinations (1/1, 1/many, many/1, many/many) in each locale. This is the single highest-risk key in the migration.
  • Per-commit CI discipline. Because the issue splits the migration into per-area commits, npm run test must pass on every commit. The test harness above should be added in the first commit with only the keys being migrated in that commit registered. Grows incrementally with each subsequent commit.
  • Do not rely on npm run check alone. TypeScript happily accepts a variant call signature that's grammatically broken at runtime — type-checking catches the call shape, not the rendered output.
  • Explicit tests for the PersonsStatsBar.svelte call-site change — its labels (Person/Personen) have no {count} placeholder in the rendered string, only act as a plural selector. Easy to misimplement as m.persons_stats_label_persons() (no args) vs m.persons_stats_label_persons({ count }). Unit-test the component.

Open Decisions

(none — systematic variant regression test is the right call here; it's a $1 investment for permanent protection.)

## 🧪 Sara Holt — Senior QA Engineer ### Observations - **This refactor is entirely a correctness problem, so tests are the whole deliverable**, not a bolt-on. The user-visible bugs ("1 Dokumente", "1 documentos", "minuto(s)") are trivial to introduce and trivial to miss in code review of three JSON files. - The acceptance criteria already name the three count boundaries that matter (0, 1, 2). Every migrated key needs evidence it works at all three — not "we spot-checked". Spot checks are how "1 Dokumente" shipped in the first place. - Paraglide variants can silently regress: a malformed variant declaration falls through to the default branch, which often *looks* fine in the editor preview but renders wrong for edge counts. ### Recommendations - **Build one systematic test harness up front, before migrating any key.** A single `frontend/src/lib/paraglide/variants.test.ts`: - enumerates every migrated key + its required plural args, - renders each at `count=0`, `count=1`, `count=2` across `de`, `en`, `es`, - asserts no output contains the regression signatures: `/^1 \w+en\b/`, `/^1 \w+os\b/`, `/^1 \w+s\b/`, `\(s\)`, `\(n\)`, `\(es\)`. This runs in milliseconds, stays in the suite forever, catches anyone reintroducing the pattern in a future locale addition. - **Add a targeted test for the multi-selector message `ocr_status_done_skipped`** — render all four combinations (1/1, 1/many, many/1, many/many) in each locale. This is the single highest-risk key in the migration. - **Per-commit CI discipline.** Because the issue splits the migration into per-area commits, `npm run test` must pass on every commit. The test harness above should be added in the first commit with only the keys being migrated in that commit registered. Grows incrementally with each subsequent commit. - **Do not rely on `npm run check` alone.** TypeScript happily accepts a variant call signature that's grammatically broken at runtime — type-checking catches the call shape, not the rendered output. - **Explicit tests for the `PersonsStatsBar.svelte` call-site change** — its labels (`Person`/`Personen`) have no `{count}` placeholder in the rendered string, only act as a plural selector. Easy to misimplement as `m.persons_stats_label_persons()` (no args) vs `m.persons_stats_label_persons({ count })`. Unit-test the component. ### Open Decisions _(none — systematic variant regression test is the right call here; it's a $1 investment for permanent protection.)_
Author
Owner

🎨 Leonie Voss — UX Designer & Accessibility Strategist

Observations

  • This matters more for brand quality than the issue suggests. "1 Dokumente" and "hace 1 minuto(s)" are the kind of small copy failures that disproportionately erode the handmade, archival feel of a family archive. Fixing them is a brand-quality win, not just a refactor.
  • Screen reader output improves for free: "+1 weitere" → "+1 weiterer" in German is debatable, but (s) / (n) literally announced by VoiceOver as "paren-s" / "paren-n" is a real accessibility hit that variants remove.
  • The CLDR other rule covers count=0 by default: "0 Dokumente" / "0 documents" / "0 documentos" are all grammatically correct. But grammatically-correct is not the same as good empty-state copy.
  • Layout/truncation risk is minimal — the before/after string lengths are roughly equivalent, nothing will unexpectedly overflow.

Recommendations

  • Proceed with the migration as scoped. The grammar fix is the floor; everything else is upside.
  • Don't try to solve empty-state copy inside variant messages. A variant that renders "Keine Dokumente" for count=0 and "{count} Dokumente" for ≥1 is a layering violation — empty-state phrasing is a UI decision, not a grammar one. Keep that work separate (and it's often per-component: "Keine Treffer" in search is different from "Noch keine Dokumente" in a person detail).
  • Spot-check at 320px viewport after migration, specifically the long ones: enrich_needs_metadata_count, admin_tag_delete_subtree_warn, ocr_status_done_skipped. Plural forms in German and Spanish can be a character or two longer than the _many variant was — minor, but worth a look.
  • Verify the noun-only labels (persons_stats_label_persons, persons_stats_label_documents) still render the UPPERCASE typographic style after the ternary is gone. That's a styling check on PersonsStatsBar.svelte, not a translation concern, but easy to overlook during refactor.

Open Decisions

  • Empty-state copy treatment: once variants ship, places that currently show "0 Dokumente" will look slightly cold. Decide whether to follow up with a separate issue to replace zero-count displays with tailored empty-state messages ("Keine Dokumente bisher"), or accept the grammatical "0 Dokumente" as-is. Both are valid — the cost is incremental PR work vs slightly utilitarian copy in 6–8 places.
## 🎨 Leonie Voss — UX Designer & Accessibility Strategist ### Observations - This matters more for brand quality than the issue suggests. "1 Dokumente" and "hace 1 minuto(s)" are the kind of small copy failures that disproportionately erode the handmade, archival feel of a family archive. Fixing them is a brand-quality win, not just a refactor. - Screen reader output improves for free: "+1 weitere" → "+1 weitere*r*" in German is debatable, but `(s)` / `(n)` literally announced by VoiceOver as "paren-s" / "paren-n" is a real accessibility hit that variants remove. - **The CLDR `other` rule covers count=0 by default**: "0 Dokumente" / "0 documents" / "0 documentos" are all grammatically correct. But grammatically-correct is not the same as good empty-state copy. - Layout/truncation risk is minimal — the before/after string lengths are roughly equivalent, nothing will unexpectedly overflow. ### Recommendations - **Proceed with the migration as scoped.** The grammar fix is the floor; everything else is upside. - **Don't try to solve empty-state copy inside variant messages.** A variant that renders "Keine Dokumente" for count=0 and "{count} Dokumente" for ≥1 is a layering violation — empty-state phrasing is a UI decision, not a grammar one. Keep that work separate (and it's often per-component: "Keine Treffer" in search is different from "Noch keine Dokumente" in a person detail). - **Spot-check at 320px viewport after migration**, specifically the long ones: `enrich_needs_metadata_count`, `admin_tag_delete_subtree_warn`, `ocr_status_done_skipped`. Plural forms in German and Spanish can be a character or two longer than the `_many` variant was — minor, but worth a look. - **Verify the noun-only labels** (`persons_stats_label_persons`, `persons_stats_label_documents`) still render the UPPERCASE typographic style after the ternary is gone. That's a styling check on `PersonsStatsBar.svelte`, not a translation concern, but easy to overlook during refactor. ### Open Decisions - **Empty-state copy treatment**: once variants ship, places that currently show "0 Dokumente" will look slightly cold. Decide whether to follow up with a separate issue to replace zero-count displays with tailored empty-state messages ("Keine Dokumente bisher"), or accept the grammatical "0 Dokumente" as-is. Both are valid — the cost is incremental PR work vs slightly utilitarian copy in 6–8 places.
Author
Owner

⚙️ Tobias Wendt — DevOps & Platform Engineer

Observations

  • No infrastructure, CI, or deployment impact. Paraglide variants compile inside the existing Vite build (paraglideVitePlugin in vite.config.ts) — no new pipeline steps, no new env vars, no container changes.
  • Existing CI gates (npm run check, npm run lint, npm run test) already cover the work. Nothing to add to docker-compose.ci.yml or Gitea Actions.
  • Paraglide 2.5.0 is pinned in package.json — variants have been stable in this major since 2.0, no upgrade risk for the refactor.

Recommendations

  • No CI changes. The per-area commit cadence means each commit stays small enough that CI diagnostics are trivial if something regresses. Don't add any new pipeline steps for this.
  • If Sara ships the variant regression harness (see her comment), make sure it runs in the existing Vitest job — not as a separate job. One more test file, zero config change.
  • Renovate awareness: when @inlang/paraglide-js gets a minor bump, diff the generated src/lib/paraglide/messages.js output as part of the review, because variant compilation semantics are the surface most likely to shift. Non-blocking for this issue.

Open Decisions

(none)

## ⚙️ Tobias Wendt — DevOps & Platform Engineer ### Observations - No infrastructure, CI, or deployment impact. Paraglide variants compile inside the existing Vite build (`paraglideVitePlugin` in `vite.config.ts`) — no new pipeline steps, no new env vars, no container changes. - Existing CI gates (`npm run check`, `npm run lint`, `npm run test`) already cover the work. Nothing to add to `docker-compose.ci.yml` or Gitea Actions. - Paraglide 2.5.0 is pinned in `package.json` — variants have been stable in this major since 2.0, no upgrade risk for the refactor. ### Recommendations - **No CI changes.** The per-area commit cadence means each commit stays small enough that CI diagnostics are trivial if something regresses. Don't add any new pipeline steps for this. - **If Sara ships the variant regression harness** (see her comment), make sure it runs in the existing Vitest job — not as a separate job. One more test file, zero config change. - **Renovate awareness**: when `@inlang/paraglide-js` gets a minor bump, diff the generated `src/lib/paraglide/messages.js` output as part of the review, because variant compilation semantics are the surface most likely to shift. Non-blocking for this issue. ### Open Decisions _(none)_
Author
Owner

🗳️ Decision Queue — Action Required

1 decision needs your input before implementation starts.

UX

  • Empty-state copy treatment for zero counts. Once variants ship, places that currently render "0 Dokumente" / "0 documents" / "0 documentos" will be grammatically correct but read coldly in empty states (search with no results, a person with no documents). Options:
    • Accept grammatical "0 Dokumente" as-is — zero additional work, slightly utilitarian copy in ~6–8 places.
    • Follow up with a separate issue that branches at the component level to show tailored empty-state messages ("Keine Dokumente bisher", "Noch keine Treffer") instead of count-prefixed strings. Higher polish, requires per-component work and UX decisions on phrasing.
      (Raised by: Leonie)
## 🗳️ Decision Queue — Action Required _1 decision needs your input before implementation starts._ ### UX - **Empty-state copy treatment for zero counts.** Once variants ship, places that currently render "0 Dokumente" / "0 documents" / "0 documentos" will be grammatically correct but read coldly in empty states (search with no results, a person with no documents). Options: - **Accept grammatical "0 Dokumente" as-is** — zero additional work, slightly utilitarian copy in ~6–8 places. - **Follow up with a separate issue** that branches at the component level to show tailored empty-state messages ("Keine Dokumente bisher", "Noch keine Treffer") instead of count-prefixed strings. Higher polish, requires per-component work and UX decisions on phrasing. _(Raised by: Leonie)_
Author
Owner

🎨 Leonie Voss — UX Designer & Accessibility Strategist

Summary of UX-scoped discussion. All items resolved.

Resolved

  • Zero-case empty-state copy — grammatically-correct "0 Dokumente" / "0 documents" / "0 documentos" stays in scope for this migration as the correct rendering of count-zero stats (e.g. PersonsStatsBar, counters, status chips). Tailored empty-state messages for search-no-results and similar contexts become a separate follow-up issue with per-component UI branching. Scope creep kept out of this refactor.

  • German "weitere" adjective morphology — the four affected keys (person_show_more, topbar_overflow_more, doc_details_more_receivers, admin_tag_children_more) migrate as plural-only variants using only the other form. "+1 weitere" is a deliberate tradeoff: grammatically incorrect in isolation, but the "+N weitere" chrome-text pattern is visually more valuable than gender-agreed singular forms in space-constrained UI. Recorded here so future-us knows it was a choice, not an oversight.

  • 320px overflow verification — added to acceptance criteria. The following five keys must be verified at 320px viewport in de / en / es post-migration (no overflow, no truncation, no layout-breaking wrap):

    • enrich_needs_metadata_count
    • admin_tag_delete_subtree_warn
    • ocr_status_done_skipped
    • mission_control_ready_subtitle
    • admin_system_import_status_done
  • UPPERCASE noun-only labelspersons_stats_label_persons / persons_stats_label_documents and similar stay in natural case in the translation files ("Person" / "Personen"). Typographic uppercase styling stays on the wrapper span in the Svelte component. Typography is not an i18n concern and must not leak into locale files.

Overall read

Grammar fix first, empty-state polish later. The boundary is clean: this migration owns plural correctness, a follow-up issue owns empty-state copy. No blockers from my side.

## 🎨 Leonie Voss — UX Designer & Accessibility Strategist _Summary of UX-scoped discussion. All items resolved._ ### Resolved - **Zero-case empty-state copy** — grammatically-correct "0 Dokumente" / "0 documents" / "0 documentos" stays in scope for this migration as the correct rendering of count-zero stats (e.g. `PersonsStatsBar`, counters, status chips). **Tailored empty-state messages** for search-no-results and similar contexts become a **separate follow-up issue** with per-component UI branching. Scope creep kept out of this refactor. - **German "weitere" adjective morphology** — the four affected keys (`person_show_more`, `topbar_overflow_more`, `doc_details_more_receivers`, `admin_tag_children_more`) migrate as **plural-only variants** using only the `other` form. "+1 weitere" is a deliberate tradeoff: grammatically incorrect in isolation, but the "+N weitere" chrome-text pattern is visually more valuable than gender-agreed singular forms in space-constrained UI. Recorded here so future-us knows it was a choice, not an oversight. - **320px overflow verification — added to acceptance criteria.** The following five keys must be verified at 320px viewport in `de / en / es` post-migration (no overflow, no truncation, no layout-breaking wrap): - `enrich_needs_metadata_count` - `admin_tag_delete_subtree_warn` - `ocr_status_done_skipped` - `mission_control_ready_subtitle` - `admin_system_import_status_done` - **UPPERCASE noun-only labels** — `persons_stats_label_persons` / `persons_stats_label_documents` and similar **stay in natural case in the translation files** ("Person" / "Personen"). Typographic `uppercase` styling stays on the wrapper span in the Svelte component. Typography is not an i18n concern and must not leak into locale files. ### Overall read Grammar fix first, empty-state polish later. The boundary is clean: this migration owns plural correctness, a follow-up issue owns empty-state copy. No blockers from my side.
Sign in to join this conversation.
No Label feature refactor ui
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#287