---
title: "ICU Formatting"
description: "ICU Message Formatting Guide"
type: "docs"
category: "doc"
tags: []
authors: [Anonymous]
date: "2026-09-09"
last_update: "2026-09-09"
time_minutes: 4
draft: false
unlisted: false
url: "https://www.derafu.dev/docs/core/translation/icu-formatting"
---

# ICU Message Formatting Guide

`derafu/translation` doesn't implement its own message formatter. ICU
formatting comes directly from PHP's `intl` extension (`\MessageFormatter`),
used in two places:

1. **`TranslatableMessage::__toString()`** — formats the raw message
   eagerly, without a translator. This is what makes `getMessage()` on a
   translatable exception return a fully formatted string with zero setup.
2. **`symfony/translation`'s own ICU formatter** — used when translating
   through a real `Translator`, for any domain whose name ends in
   `+intl-icu` (see [Resource Providers](custom-providers) and the
   [API Reference](api-reference)).

Both paths accept the exact same ICU MessageFormat syntax described below.

## Basic Placeholders

The simplest form uses named placeholders:

```php
throw new ValidationException(
    'The field {field} is required.',
    ['field' => 'email'],
);
// getMessage(): "The field email is required."
```

Multiple placeholders are supported:

```php
throw new ValidationException(
    'Value {value} for field {field} is invalid.',
    ['value' => 'test@', 'field' => 'email'],
);
// getMessage(): "Value test@ for field email is invalid."
```

## Pluralization

```php
$id = '{count, plural, =0{No messages} one{# message} other{# messages}}';

new TranslatableMessage($id, ['count' => 2]); // "2 messages"
new TranslatableMessage($id, ['count' => 1]); // "1 message"
new TranslatableMessage($id, ['count' => 0]); // "No messages"
```

Available plural categories:

- `zero`: for languages with a special zero form.
- `one`: singular form.
- `two`: dual form (for languages that have it).
- `few` / `many`: for languages with special handling of small/large
  numbers.
- `other`: default form (**required** — always provide it).
- `=n`: exact number matches (e.g. `=0`).

## Gender / Select

```php
$id = '{gender, select, female {She liked your post} male {He liked your post} other {They liked your post}}';

(string) new TranslatableMessage($id, ['gender' => 'female']);
// "She liked your post"
```

Nested placeholders work too:

```php
$id = '{gender, select, female {{name} added her comment} male {{name} added his comment} other {{name} added their comment}}';

(string) new TranslatableMessage($id, ['gender' => 'female', 'name' => 'Alice']);
// "Alice added her comment"
```

## Number Formatting

```php
'{value, number}'          // Plain number.
'{value, number, percent}' // Percentage.
'{value, number, currency}' // Currency, using the message's own locale.
```

Currency formatting depends on the locale carrying a specific currency
association (e.g. `en_US`, not just `en`):

```php
new TranslatableMessage(
    'Balance must be greater than {min, number, currency}.',
    ['min' => 100],
    null,
    'en_US',
);
// "Balance must be greater than $100.00."
```

## Nested Plural + Select

Patterns can be nested for combined scenarios:

```php
$id = '{gender, select,
    female {{count, plural, =0{She has no messages} one{She has # message} other{She has # messages}}}
    male {{count, plural, =0{He has no messages} one{He has # message} other{He has # messages}}}
    other {{count, plural, =0{They have no messages} one{They have # message} other{They have # messages}}}
}';

(string) new TranslatableMessage($id, ['gender' => 'female', 'count' => 5]);
// "She has 5 messages"
```

## Common Patterns

```php
// Range validation.
'Value must be between {min} and {max}.'

// List validation.
'{count, plural, =0{List cannot be empty} one{At least one item is required} other{At least # items are required}}'

// Status messages.
'{status, select, pending{Waiting for approval} approved{Approved on {date}} rejected{Rejected: {reason}} other{Unknown status}}'
```

## Troubleshooting

1. **Missing the `other` category** — required for every `select`/`plural`
   pattern. Without it, a value that doesn't match any listed category
   falls back to the raw, unformatted message (same as an invalid pattern,
   see below):

    ```php
    // Wrong.
    '{gender, select, male{He} female{She}}'

    // Correct.
    '{gender, select, male{He} female{She} other{They}}'
    ```

2. **Unmatched braces:**

    ```php
    // Wrong.
    'Hello {name'

    // Correct.
    'Hello {name}'
    ```

   `TranslatableMessage::__toString()` catches this: an invalid ICU pattern
   returns the raw message unchanged rather than throwing.

3. **Missing parameters** don't throw either — the placeholder is simply
   left unexpanded:

    ```php
    (string) new TranslatableMessage('{count} items'); // "{count} items"
    ```

---

For more on ICU MessageFormat syntax:

- [ICU User Guide](https://unicode-org.github.io/icu/userguide/format_parse/messages/)
- [PHP `MessageFormatter` manual](https://www.php.net/manual/en/class.messageformatter.php)



---
Last updated on 09/09/2026

