# ICU reference

What the Comvi ICU compiler does not implement, the apostrophe and quoted-literal rules, and the exact E_ICU_SYNTAX texts a development and a production build produce.

Canonical: https://comvi.io/docs/i18n/icu-reference

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](/docs/i18n/pluralization), together with how to turn the compiler on.

## What ICU does not implement

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:

```text
[@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`

```ts

// 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:**

```text
[@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"`.

## Dev throws, production renders literally

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:

```text
[@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`

```ts

// 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.

## Apostrophes and quoted literals

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`

```json
{
"cant": "I can't do that.",
"clock": "o' clock",
"behavior": "Superiors' behavior",
"rating": "Gib' eine Bewertung"
}
```

`src/quotes.ts`

```ts

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`

```json
{
"help.syntax": "Use the syntax '{'key'}' to reference a variable.",
"weather": "It''s a beautiful day."
}
```

`src/help.ts`

```ts

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`.

### Escaping rules summary

| 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](/docs/i18n/tag-interpolation).

## Read next

- [Pluralize with ICU](/docs/i18n/pluralization): composing the compiler, and plural, select and selectordinal per framework.
- [Error codes](/docs/i18n/errors): every code a production build can emit, with its cause and fix.
- [Format numbers, dates and currency](/docs/i18n/formatting): the four free Intl functions that replace ICU skeletons.
