Skip to content
Comvi

ICU reference

View .md

Look up what the ICU compiler refuses and what it says when it does. The grammar it accepts, plural, select, selectordinal and their choice lists, is on Pluralize with ICU, together with how to turn the compiler on.

Two forms are not implemented and not planned, and each is an E_ICU_SYNTAX failure rather than a wrong render.

1. Argument types other than plural, selectordinal and select, such as {price, number}, {d, date} and {t, time}. Format the value in code and interpolate the result as a plain {param}, or quote the braces to keep the text literal:

[@comvi/core] ICU argument `{price, number}` is not supported: this compiler implements `plural`, `selectordinal` and `select`, each with a choice list like `other {…}`. Quote the braces ('{'…'}') or format the value in code and pass it as a plain {param}
src/price.ts
import { formatCurrency } from '@comvi/core';
import { i18n } from './i18n';
// Format in code, interpolate the result. `formatCurrency` is a root export
// taking the i18n instance as argument 1; there is no i18n.formatCurrency on the instance.
i18n.t('total', { price: formatCurrency(i18n, 1234.5, 'USD') });

2. A choice list whose selectors are missing their braced sub-messages:

[@comvi/core] The `plural` choice list `one {# item} other` is malformed; every selector needs a braced sub-message, as in `other {…}`

Give every selector a {…} sub-message. The plural offset: form fails the same way: offset:N parses as a selector with no braced sub-message.

Both messages are development-only text and fold out of production.

E_ICU_SYNTAX carries two fields: a stable code and the parsed argumentType, "plural", "select" or "selectordinal", or an unimplemented one such as "number" or "date".

The same rule covers ICU syntax on the simple compiler and the two unimplemented forms.

Development is eager. Ingesting a catalog walks its string leaves, so a bad template throws where it entered, before a single render. A template that never passes through ingestion, such as a per-call params.fallback, throws at its first compile instead. On the simple compiler the message names the fix:

[@comvi/core] "{count, plural, one {# item} other {# items}}" in "…" is ICU "plural" syntax; the default compiler supports {param} only. Pass compiler: icuCompiler from "@comvi/core/icu" in the constructor options.

Production never crashes. On the compilation that hits the syntax, the braced segment renders literally, as authored, and E_ICU_SYNTAX is reported through onError, or through console.error when no handler is configured. The report is best-effort, process-wide, and never on cached renders: the parsed template is cached, so a hot key costs one report rather than one per render. A literal {count, plural, …} on the page is visibly broken and takes nothing down with it.

src/i18n.ts
import { createI18n } from '@comvi/core';
// Your error tracker: Sentry, Rollbar, a POST to your own endpoint.
declare function reportToSentry(error: unknown, context: unknown): void;
const i18n = createI18n({
locale: 'en',
onError(error, context) {
// context: { source: "compile", key, namespace, locale }
if ((error as { code?: string }).code === 'E_ICU_SYNTAX') {
reportToSentry(error, context);
}
},
});

The development walk costs the production bundle nothing: it sits behind the __DEV__ fold, and the identifier occurs in no production artifact.

Apostrophe quoting is core grammar, not part of the ICU capability, so it works on the simple compiler too. Comvi uses ICU’s DOUBLE_OPTIONAL mode: a single apostrophe starts quoted literal text only when it immediately precedes an ICU syntax character. Everywhere else it is an ordinary apostrophe, including before spaces and at word boundaries:

locales/en/common.json
{
"cant": "I can't do that.",
"clock": "o' clock",
"behavior": "Superiors' behavior",
"rating": "Gib' eine Bewertung"
}
src/quotes.ts
import { i18n } from './i18n';
i18n.t('cant'); // "I can't do that."
i18n.t('clock'); // "o' clock"
i18n.t('behavior'); // "Superiors' behavior"
i18n.t('rating'); // "Gib' eine Bewertung"

None of those apostrophes precedes a {, } or an in-plural #, so none starts a quoted literal. The syntax characters are { and } everywhere. # is syntax only inside a plural or selectordinal sub-message, including a select nested inside one.

Use quoted literals when the output needs ICU syntax characters without parsing them:

locales/en/common.json
{
"help.syntax": "Use the syntax '{'key'}' to reference a variable.",
"weather": "It''s a beautiful day."
}
src/help.ts
import { i18n } from './i18n';
i18n.t('help.syntax'); // "Use the syntax {key} to reference a variable."
i18n.t('weather'); // "It's a beautiful day."

An apostrophe directly before { starts quoted literal text, so French and Italian elision in front of a parameter needs the apostrophe doubled: l'{name} renders as l{name}, and l''{name} renders as l'hôtel.

Input/context Output
o' clock o' clock
'' '
'{' / '}' { / }
'{name}' {name} as literal text, without interpolation
l'{name} l{name}: the apostrophe opens a quoted literal
l''{name} l' followed by the value of name
'#' inside plural / selectordinal # instead of the plural number
'#' at top level or in a standalone select '#' (both apostrophes remain literal)

The &lt; / &gt; / &amp; / \< escapes are a different axis: they belong to the tag grammar and only decode where < is syntax. ICU apostrophe quoting is unaffected by whether a tag extension is in the graph. See Tag interpolation.