Gå til hovedinnhold

Plurals and select

<Plural> and <Select> let you write count-aware and choice-based text as React children, one child per form. Your source holds no ICU strings, and translators still receive the ICU template their tools expect.

Plurals: the authoring form​

import { Plural, Zero, One, Two, Few, Many, Other } from "@neokapi/i18n-react/runtime";

<p>
<Plural count={n}>
<Zero>No messages</Zero>
<One>1 message</One>
<Other>{n} messages</Other>
</Plural>
</p>;

At render time, <Plural> consults Intl.PluralRules for the active locale and picks the matching form. For English n = 1 resolves to <One>, everything else to <Other>. For Arabic, <Zero>, <One>, <Two>, <Few>, <Many>, <Other> all have distinct rules.

Select: choice-based text​

import { Select, Case, Other } from "@neokapi/i18n-react/runtime";

<p>
<Select value={role}>
<Case when="admin">Admin access</Case>
<Case when="editor">Editor access</Case>
<Case when="viewer">Viewer access</Case>
<Other>No access</Other>
</Select>
</p>;

<Select> picks the <Case> whose when prop equals the source value, or falls back to <Other>.

Why children, not props​

Traditional i18n libraries express plurals as a single ICU message:

{count, plural, zero {No messages} one {1 message} other {# messages}}

That form puts every source-language form inside one opaque string literal. neokapi-i18n keeps each form as a JSX child instead, which gives you:

  • Source readability: the English forms look like English in source, translators see English in the extract.
  • Inline elements per form: you can write <Other><strong>{n}</strong> items</Other> with the strong tag preserved as a position token. ICU has no natural way to express that.
  • Placeholders per form: {n} is an expression, handled by the same placeholder machinery as any JSX text.

The extractor writes the canonical ICU template for each plural or select block. You work in React, and translators work against ICU in their CAT tool.

Move the count and change the locale to see which form Intl.PluralRules resolves (the same lookup <Plural> does at render time) next to the ICU template the extractor emits:

Rendered — the source author's React
1 message
Intl.PluralRules("en") selects one → <One>
Emitted ICU — the translator's CAT tool
{count, plural, one {1 message} other {# messages}}
English (en) uses one, other.

Mixing inline elements inside a plural form​

<p>
<Plural count={items.length}>
<Zero>Your cart is empty</Zero>
<One>
<strong>1</strong> item in your cart
</One>
<Other>
<strong>{items.length}</strong> items in your cart
</Other>
</Plural>
</p>

Each form is extracted as a typed run sequence: <strong> becomes a position token ({=m0}), {items.length} becomes a placeholder ({items.length} or deduped like {itemsLength}). The translator can reorder the bold element freely within each form.

Which forms to declare​

Declare the forms your product ships. You can add a language later without a source change: its translation supplies that language's forms, and the runtime picks the right one.

LanguageForms used
Englishone, other
Frenchone, other
Russianone, few, many, other
Arabiczero, one, two, few, many, other
Japanese, Korean, Chineseother only

Source authors usually only need one + other, plus optional zero for UX polish ("No messages" vs. "0 messages"). Translators fill the rest per locale.

<Plural> with a source pivot​

Sometimes the count variable's name doesn't match what you want translators to reference:

<Plural count={cart.items.length}>
<One>1 item</One>
<Other>{cart.items.length} items</Other>
</Plural>

The pivot variable becomes a placeholder named after the expression (items.length → length). If that name means little to a translator, assign the count to a local first:

const n = cart.items.length;
<Plural count={n}>
<One>1 item</One>
<Other>{n} items</Other>
</Plural>;

Translator-authored plurals​

Not every language needs plurals for every string. Italian uses the same form for 1 vs many in some constructs, Japanese has only one form, while German might want a count form where English didn't.

neokapi-i18n's editor UI (<PluralTargetEditor>, shipped with @neokapi/ui-primitives) lets translators upgrade a flat target into a per-form target without any source change. A translator's German target for <p>You have {n} messages</p> can become:

zero: Keine Nachrichten
one: 1 Nachricht
other: Sie haben {n} Nachrichten

while the English source stays flat. @neokapi/kapi-format's upgradeTargetToPlural / downgradePluralTarget helpers implement the target-side data model. See C-01 for the on-disk shape.

Runtime resolution​

<Plural> / <Select> are authoring components. At render time, a locale switch re-evaluates the form through Intl.PluralRules or the case map, without remounting them. Intl.PluralRules ships in every modern browser and in Node, so you need no polyfill.

The pivot (the count or the case value) is a runtime value, so the form cannot be chosen at build time. Plural and select blocks therefore keep a runtime call in both modes: inline mode bakes the translated ICU template in as the call's fallback, so the dictionary fetch disappears, but the ~2 kB ICU resolver stays. This is the one documented exception to inline mode's zero-runtime rule (see Runtime vs. inline modes).

Inside a plural branch, # is replaced by the count formatted for the active locale: 1.234 in de, 1,234 in en.