Pluralization & ICU Message Format
Comvi supports the ICU MessageFormat syntax needed for pluralization, ordinal plurals, select branches, variable interpolation, nesting, and quoted literals. ICU is the international standard used by Android, iOS, and most professional localization tools — so your translators already know the syntax.
This page covers the ICU subset Comvi evaluates at runtime. Number, date, and currency formatting are handled by Comvi’s formatting APIs rather than ICU skeletons inside translation strings.
Basic Pluralization
Section titled “Basic Pluralization”ICU’s plural type adapts a message based on a numeric value:
{ "cart.items": "{count, plural, =0 {Your cart is empty} one {# item in your cart} other {# items in your cart}}"}const { t } = useI18n();
t('cart.items', { count: 0 }); // "Your cart is empty"t('cart.items', { count: 1 }); // "1 item in your cart"t('cart.items', { count: 42 }); // "42 items in your cart"The # symbol is replaced with the numeric value of count.
Plural Categories
Section titled “Plural Categories”ICU defines six plural categories. Which categories a language uses depends on its plural rules:
| Category | Description | Used by |
|---|---|---|
zero | Zero items | Arabic, Latvian, Welsh |
one | Singular | English, German, French, most languages |
two | Dual | Arabic, Hebrew, Slovenian |
few | Paucal / small numbers | Ukrainian (2-4), Polish (2-4), Czech |
many | Large numbers | Ukrainian (5-20), Polish (5-21), Arabic |
other | General plural (required) | All languages |
The other category is always required. It serves as the fallback when no other category matches.
Exact Value Matches
Section titled “Exact Value Matches”Use =N to match exact numbers, overriding the plural category:
{ "notifications": "{count, plural, =0 {No new notifications} =1 {One new notification} other {# new notifications}}"}Exact matches are checked before plural categories, so =1 takes priority over one.
How It Works
Section titled “How It Works”When you call t('cart.items', { count: 5 }):
- Comvi looks up the key in the current locale and namespace
- It passes
counttoIntl.PluralRulesto determine the plural category (one,other, etc.) - The matching branch is selected (exact
=Nmatch first, then category, thenother) #is replaced with the numeric value- The final string is returned
Comvi uses JavaScript’s built-in Intl.PluralRules API, which provides correct plural rules for every language defined by the Unicode CLDR.
Select (Gender and Variants)
Section titled “Select (Gender and Variants)”The select type chooses a message branch based on a string value. This is commonly used for gender but works for any categorical choice:
{ "user.greeting": "{gender, select, male {He left a comment} female {She left a comment} other {They left a comment}}"}t('user.greeting', { gender: 'male' }); // "He left a comment"t('user.greeting', { gender: 'female' }); // "She left a comment"t('user.greeting', { gender: 'other' }); // "They left a comment"t('user.greeting', { gender: 'unknown' }); // "They left a comment" (falls back to other)Non-Gender Use Cases
Section titled “Non-Gender Use Cases”select works for any enumerated value — formality level, order status, user role, and more.
Formal vs. informal addressing — important for languages like German, French, or Ukrainian where “you” has formal and informal forms:
{ "welcome": "{formality, select, formal {Welcome. How may we assist you?} informal {Hey! What can we help you with?} other {Welcome!}}"}{ "welcome": "{formality, select, formal {Willkommen. Wie können wir Ihnen helfen?} informal {Hey! Wie können wir dir helfen?} other {Willkommen!}}"}t('welcome', { formality: 'formal' });// EN: "Welcome. How may we assist you?"// DE: "Willkommen. Wie können wir Ihnen helfen?"Order status:
{ "order.status": "{status, select, pending {Your order is being processed} shipped {Your order is on its way} delivered {Your order has been delivered} other {Order status: {status}}}"}t('order.status', { status: 'shipped' }); // "Your order is on its way"Ordinal Plurals
Section titled “Ordinal Plurals”For numbers that need ordinal forms (1st, 2nd, 3rd), use selectordinal:
{ "place": "You finished in {position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} place"}t('place', { position: 1 }); // "You finished in 1st place"t('place', { position: 2 }); // "You finished in 2nd place"t('place', { position: 21 }); // "You finished in 21st place"Ordinal categories (one, two, few, other) are language-specific, just like plural categories. The # symbol is replaced with the numeric value.
Nested Messages
Section titled “Nested Messages”You can combine plural and select for complex messages:
{ "activity": "{gender, select, male {{count, plural, one {He wrote # comment} other {He wrote # comments}}} female {{count, plural, one {She wrote # comment} other {She wrote # comments}}} other {{count, plural, one {They wrote # comment} other {They wrote # comments}}}}"}t('activity', { gender: 'female', count: 3 }); // "She wrote 3 comments"t('activity', { gender: 'male', count: 1 }); // "He wrote 1 comment"Framework Examples
Section titled “Framework Examples”<script setup lang="ts">import { useI18n } from '@comvi/vue';import { ref } from 'vue';
const { t } = useI18n();const itemCount = ref(3);</script>
<template> <div> <p>{{ t('cart.items', { count: itemCount }) }}</p> <p>{{ t('cart.total', { amount: 49.99 }) }}</p>
<button @click="itemCount++"> {{ t('cart.add') }} </button> </div></template>import { useState } from 'react';import { useI18n } from '@comvi/react';
export function CartSummary() { const { t } = useI18n(); const [itemCount, setItemCount] = useState(3);
return ( <div> <p>{t('cart.items', { count: itemCount })}</p> <p>{t('cart.total', { amount: 49.99 })}</p>
<button onClick={() => setItemCount((c) => c + 1)}> {t('cart.add')} </button> </div> );}'use client';import { useState } from 'react';import { useI18n } from '@comvi/next/client';
export function CartSummary() { const { t } = useI18n(); const [itemCount, setItemCount] = useState(3);
return ( <div> <p>{t('cart.items', { count: itemCount })}</p> <p>{t('cart.total', { amount: 49.99 })}</p>
<button onClick={() => setItemCount((c) => c + 1)}> {t('cart.add')} </button> </div> );}<script setup lang="ts">import { ref } from 'vue';
const { t } = useI18n();const itemCount = ref(3);</script>
<template> <div> <p>{{ t('cart.items', { count: itemCount }) }}</p> <p>{{ t('cart.total', { amount: 49.99 }) }}</p>
<button @click="itemCount++"> {{ t('cart.add') }} </button> </div></template>import { createSignal } from 'solid-js';import { useI18n } from '@comvi/solid';
export function CartSummary() { const { t } = useI18n(); const [itemCount, setItemCount] = createSignal(3);
return ( <div> <p>{t('cart.items', { count: itemCount() })}</p> <p>{t('cart.total', { amount: 49.99 })}</p>
<button onClick={() => setItemCount((c) => c + 1)}> {t('cart.add')} </button> </div> );}<script lang="ts">import { useI18n } from '@comvi/svelte';
const { t } = useI18n();let itemCount = 3;</script>
<div> <p>{$t('cart.items', { count: itemCount })}</p> <p>{$t('cart.total', { amount: 49.99 })}</p>
<button on:click={() => itemCount++}> {$t('cart.add')} </button></div>import { i18n } from './i18n';
let itemCount = 3;
function render() { const items = i18n.t('cart.items', { count: itemCount }); const total = i18n.t('cart.total', { amount: 49.99 }); const add = i18n.t('cart.add');
container.textContent = ''; const p1 = document.createElement('p'); p1.textContent = items; const p2 = document.createElement('p'); p2.textContent = total; const btn = document.createElement('button'); btn.textContent = add; btn.addEventListener('click', () => { itemCount++; render(); });
container.append(p1, p2, btn);}Escaping Braces
Section titled “Escaping Braces”If your translation needs to include literal { or } characters, wrap them in single quotes:
Use the syntax '{'key'}' to reference a variable.t('help.syntax'); // "Use the syntax {key} to reference a variable."To include a literal single quote, double it:
It''s a beautiful day.t('weather'); // "It's a beautiful day."Escaping Rules Summary
Section titled “Escaping Rules Summary”| Input | Output |
|---|---|
'{' and '}' | { and } (literal braces) |
'' | ' (literal single quote) |
'any text' | any text (literal, no parsing inside) |
Apostrophes Inside Words
Section titled “Apostrophes Inside Words”When an apostrophe sits between two word characters, Comvi treats it as a literal character — no escaping required:
t('cant'); // "I can't do that." (source: "I can't do that.")t('summer'); // "C'est l'été" (source: "C'est l'été")You only need to double the apostrophe ('') at positions where a single ' would otherwise start a quoted literal — typically next to {, <, or at word boundaries (e.g. 'morning, o' clock).
For example, o' clock needs a doubled apostrophe because the apostrophe is followed by a space:
t('clock.unescaped'); // "o clock" (source: "o' clock")t('clock.escaped'); // "o' clock" (source: "o'' clock")Common Patterns Reference
Section titled “Common Patterns Reference”Ready-to-use patterns for frequent translation scenarios:
| Scenario | ICU Message |
|---|---|
| Simple greeting | Hello, {name}! |
| Item count | {count, plural, =0 {No items} one {# item} other {# items}} |
| Relative count | {count, plural, =0 {No new messages} one {# new message} other {# new messages}} |
| Gender-aware | {gender, select, male {His profile} female {Her profile} other {Their profile}} |
| Optional plural text | {count, plural, =0 {Get started} other {Continue ({count})}} |
| Enumerated status | {status, select, pending {Processing} shipped {On its way} delivered {Delivered} other {Unknown}} |
Common Pitfalls
Section titled “Common Pitfalls”Do not hardcode English plural logic. Never write count === 1 ? t('item') : t('items'). Use ICU pluralization and let the message format handle the logic for every language.
// Wrong — breaks for languages with more than two plural formsconst label = count === 1 ? t('item.one') : t('item.other');
// Correct — ICU handles all plural rulesconst label = t('items', { count });Always include the other category. It is the required fallback. If you omit it, values that do not match a more specific category resolve to an empty branch.
Use =0 for “no items” messages. The zero plural category is not the same as the number 0 in most languages. English does not have a zero category, so {count, plural, zero {...} ...} will never match in English. Use =0 instead:
{ "items": "{count, plural, =0 {No items} one {# item} other {# items}}"}Keep ICU messages in one line. Some JSON parsers and editors break when ICU syntax spans multiple lines. Keep the entire message on a single line in your JSON file.