Message Format
Fluenti uses a subset of ICU MessageFormat for message strings. ICU MessageFormat is the industry standard for translatable strings — it encodes plurals, gender, number/date formatting, and arbitrary selections into a single, self-contained pattern. Fluenti parses these patterns at build time and compiles them into optimized JavaScript functions.
This page is a syntax reference. For framework-specific translation APIs (t, v-t, <Trans>, <Plural>, <Select>), see Translating Content. For real-world patterns in Arabic, Russian, and Japanese, see Complex Languages.
Plain Text
Section titled “Plain Text”The simplest message is plain text with no special syntax:
Hello WorldPlain-text messages compile to a static string — zero runtime overhead.
Variables (Interpolation)
Section titled “Variables (Interpolation)”Wrap a variable name in curly braces to insert a dynamic value:
Hello {name}, welcome to {place}!Pass values to the translation function as a key-value object:
t('Hello {name}, welcome to {place}!', { name: 'Alice', place: 'Fluenti' })// → "Hello Alice, welcome to Fluenti!"Variable names must contain only letters, digits, and underscores ([a-zA-Z0-9_]). Spaces and special characters are not allowed inside {…}.
Missing values
Section titled “Missing values”If a variable is not provided at runtime, the placeholder is rendered verbatim as a fallback:
t('Hello {name}!', {})// → "Hello {name}!"This makes missing translations easy to spot during development.
Formatted Variables (Functions)
Section titled “Formatted Variables (Functions)”ICU MessageFormat supports built-in formatting functions. The syntax is:
{variable, function}{variable, function, style}Fluenti supports three built-in functions: number, date, and time. You can also register custom formatters.
Number formatting
Section titled “Number formatting”Format numbers with locale-aware grouping, currency symbols, and percent signs:
Your balance is {amount, number}t('Your balance is {amount, number}', { amount: 1234567.89 }, 'de')// → "Your balance is 1.234.567,89"Number styles
Section titled “Number styles”| Style | Output (en) | Description |
|---|---|---|
| (none) | 1,234.56 | Locale-aware decimal grouping |
currency | $1,234.56 | Currency symbol from locale’s default currency |
percent | 12% | Value formatted as percentage |
Price: {price, number, currency}Discount: {rate, number, percent}t('Price: {price, number, currency}', { price: 49.99 }, 'ja')// → "Price: ¥50"
t('Discount: {rate, number, percent}', { rate: 0.15 }, 'en')// → "Discount: 15%"Date formatting
Section titled “Date formatting”Format Date objects or timestamps:
Last login: {date, date, short}Date styles
Section titled “Date styles”| Style | Output (en-US) | Description |
|---|---|---|
| (none) | 3/23/2026 | Default format |
short | 3/23/26 | Short date |
long | March 23, 2026 | Long date |
full | Monday, March 23, 2026 | Full date with day of week |
t('Last login: {date, date, long}', { date: new Date('2026-03-23') }, 'en')// → "Last login: March 23, 2026"
t('Last login: {date, date, short}', { date: new Date('2026-03-23') }, 'ja')// → "Last login: 2026/03/23"Time formatting
Section titled “Time formatting”Format times from Date objects:
Meeting at {time, time, short}Time styles
Section titled “Time styles”| Style | Output (en-US) | Description |
|---|---|---|
| (none) | 2:30:00 PM | Medium time (default) |
short | 2:30 PM | Hours and minutes |
long | 2:30:00 PM EST | With timezone |
t('Meeting at {time, time, short}', { time: new Date('2026-03-23T14:30:00') }, 'en')// → "Meeting at 2:30 PM"Plural Forms
Section titled “Plural Forms”Pluralization is the most common use of ICU MessageFormat. The syntax is:
{variable, plural, ...branches}Each branch is a keyword followed by a message in curly braces:
{count, plural, =0 {No items} one {# item} other {# items}}t('{count, plural, =0 {No items} one {# item} other {# items}}', { count: 0 })// → "No items"
t('{count, plural, =0 {No items} one {# item} other {# items}}', { count: 1 })// → "1 item"
t('{count, plural, =0 {No items} one {# item} other {# items}}', { count: 42 })// → "42 items"CLDR plural categories
Section titled “CLDR plural categories”The Unicode CLDR defines six plural categories. Each language uses a different subset:
| Category | When used | Example languages |
|---|---|---|
zero | Zero quantity | Arabic, Welsh |
one | Singular or “one-like” | English, French, Russian |
two | Dual | Arabic, Welsh |
few | Small numbers | Russian (2-4), Polish, Arabic (3-10) |
many | Large numbers | Russian (5-20), Arabic (11-99), French (1M+) |
other | Everything else (required) | All languages |
Fluenti uses Intl.PluralRules to resolve which category applies for a given number and locale. You only need to provide the categories your source language uses — translators add the categories their target language requires.
| Language | Categories needed |
|---|---|
| English | one, other |
| Chinese, Japanese, Korean | other only |
| French | one, many, other |
| Russian | one, few, many, other |
| Arabic | zero, one, two, few, many, other |
Exact matches
Section titled “Exact matches”Exact matches (=0, =1, =2, etc.) are checked before CLDR categories. This lets you override specific values with custom text:
{count, plural, =0 {Your inbox is empty} =1 {You have one new message} one {You have # new message} other {You have # new messages}}Exact matches compare against the raw numeric value (before any offset is applied). You can use any non-negative integer:
{count, plural, =0 {Nobody is here} =1 {Just you} =2 {You and one other} other {You and # others}}The # symbol
Section titled “The # symbol”Inside plural branches, the # symbol is replaced with the numeric value of the plural variable. If an offset is applied, # reflects the adjusted value (count minus offset):
{count, plural, =0 {No items in your cart} one {# item in your cart} other {# items in your cart}}t('{count, plural, one {# item} other {# items}}', { count: 1 })// → "1 item"
t('{count, plural, one {# item} other {# items}}', { count: 5 })// → "5 items"# can appear multiple times in the same branch and can be mixed with other variables:
{count, plural, one {# message from {sender}} other {# messages from {sender}}}Offset
Section titled “Offset”The offset keyword subtracts a value from the count before CLDR category selection. This is useful for “X and Y others” patterns:
{count, plural, offset:1 =0 {Nobody liked this} =1 {Only {name} liked this} one {{name} and # other person liked this} other {{name} and # others liked this}}t(msg, { count: 0, name: 'Alice' }) // → "Nobody liked this"t(msg, { count: 1, name: 'Alice' }) // → "Only Alice liked this"t(msg, { count: 2, name: 'Alice' }) // → "Alice and 1 other person liked this"t(msg, { count: 5, name: 'Alice' }) // → "Alice and 4 others liked this"Key behaviors with offset:
- Exact matches (
=0,=1) compare against the raw count - CLDR category selection uses the adjusted count (raw minus offset)
#renders the adjusted count
Selectordinal
Section titled “Selectordinal”For ordinal numbers (1st, 2nd, 3rd), use selectordinal instead of plural. It uses Intl.PluralRules with type: 'ordinal':
{position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}t('{position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}', { position: 1 })// → "1st"
t('{position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}', { position: 22 })// → "22nd"
t('{position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}', { position: 33 })// → "33rd"Selectordinal supports exact matches and offset just like plural.
Select
Section titled “Select”Select patterns choose text based on a string value. Common use cases include gender, user roles, and status:
{gender, select, male {He} female {She} other {They}} liked your post.t('{gender, select, male {He} female {She} other {They}} liked your post.', { gender: 'female' })// → "She liked your post."Gender-based selection
Section titled “Gender-based selection”{gender, select, male {He changed his profile picture} female {She changed her profile picture} other {They changed their profile picture}}Role-based selection
Section titled “Role-based selection”Select is not limited to gender — use it for any string-based branching:
{role, select, admin {You have full access to all settings} editor {You can edit and publish content} viewer {You can view content only} other {Your role is not recognized}}Status-based selection
Section titled “Status-based selection”{status, select, active {Your account is active} suspended {Your account has been suspended — contact support} other {Unknown account status}}The value is matched as an exact string comparison. If no key matches, the other branch is used.
Nesting
Section titled “Nesting”Plurals and selects can be nested inside each other to handle complex grammatical requirements. Fluenti supports up to 10 levels of nesting.
Plural inside select
Section titled “Plural inside select”Combine gender with quantity — the outer select picks the gender, and the inner plural handles the count:
{gender, select, male {{count, plural, one {He has # item} other {He has # items} }} female {{count, plural, one {She has # item} other {She has # items} }} other {{count, plural, one {They have # item} other {They have # items} }}}t(msg, { gender: 'female', count: 3 })// → "She has 3 items"Select inside plural
Section titled “Select inside plural”The reverse pattern — the outer plural picks the count, and the inner select picks the gender:
{count, plural, one {{gender, select, male {He has # item} other {She has # item} }} other {{gender, select, male {He has # items} other {She has # items} }}}Deeper nesting
Section titled “Deeper nesting”Languages like Russian and Arabic sometimes require 3 or more nesting levels. Fluenti supports up to 10:
{count, plural, one {{gender, select, male {{formality, select, formal {Sir, you have # item} other {Dude, you have # item} }} other {{formality, select, formal {Ma'am, you have # item} other {You have # item} }} }} other {You have # items}}Escaping Special Characters
Section titled “Escaping Special Characters”ICU MessageFormat uses curly braces { } as delimiters and ' (single quote) as the escape character. When you need these as literal text, use the escaping rules below.
Literal curly braces
Section titled “Literal curly braces”Wrap a curly brace in single quotes to render it as text:
'{' renders as a literal {'}' renders as a literal }t("The set notation is '{' a, b, c '}'")// → "The set notation is { a, b, c }"Literal single quotes
Section titled “Literal single quotes”Use two consecutive single quotes to produce one literal quote:
it''s a beautiful dayt("it''s a beautiful day")// → "it's a beautiful day"Quoted sequences
Section titled “Quoted sequences”A single quote followed by any character (other than {, }, or another ') begins a quoted sequence that runs until the next single quote:
'This entire sequence is literal text'Everything inside the quotes is treated as plain text — no variable substitution or special syntax processing occurs.
Mixed escaping
Section titled “Mixed escaping”You can combine escape types in the same message:
it''s a '{'test'}'t("it''s a '{'test'}'")// → "it's a {test}"Escaping reference
Section titled “Escaping reference”| Syntax | Output | Description |
|---|---|---|
'' | ' | Literal single quote |
'{' | { | Literal opening brace |
'}' | } | Literal closing brace |
'any text' | any text | Quoted sequence (everything literal) |
Message IDs
Section titled “Message IDs”Fluenti uses content-based hashing to generate message IDs automatically. You write natural-language messages in your source code, and Fluenti generates a deterministic short ID from the content using the FNV-1a algorithm.
How IDs are generated
Section titled “How IDs are generated”Source message: "Hello {name}" → hashMessage("Hello {name}") → "k4f2x9"The hash is a 32-bit FNV-1a hash converted to base-36, producing short alphanumeric IDs. The same message always produces the same ID.
Context-sensitive hashing
Section titled “Context-sensitive hashing”When two identical messages need different translations (e.g., “Save” as a verb vs. noun), use context to differentiate:
// These produce different IDs despite identical message textt({ message: 'Save', context: 'button' }, {}) // → ID "a1b2c3"t({ message: 'Save', context: 'noun' }, {}) // → ID "x7y8z9"In PO catalogs, context maps to the msgctxt field — a standard gettext concept.
Explicit vs auto-generated IDs
Section titled “Explicit vs auto-generated IDs”| Approach | When to use |
|---|---|
| Auto-generated (default) | Most cases — natural-language keys are self-documenting |
| Explicit ID | Legacy migration, external translation management systems that require stable IDs |
To use explicit IDs, pass id in a message descriptor:
t({ id: 'nav.save_button', message: 'Save' })When an explicit id is provided, it takes precedence over the auto-generated hash. The resolution order is:
- Explicit
id(if non-empty string) → use it messageexists → hash-based ID fromhashMessage(message, context)- Fallback → empty string
For the full identity API, see hashMessage, createMessageId, and resolveDescriptorId in the core reference.
Quick Reference
Section titled “Quick Reference”A compact summary of all ICU MessageFormat syntax supported by Fluenti:
| Pattern | Syntax | Example |
|---|---|---|
| Plain text | Hello World | Hello World |
| Variable | {name} | Hello {name} |
| Number format | {n, number} | {n, number, currency} |
| Date format | {d, date} | {d, date, long} |
| Time format | {t, time} | {t, time, short} |
| Plural | {n, plural, ...} | {n, plural, one {# item} other {# items}} |
| Select | {v, select, ...} | {v, select, a {A} other {X}} |
| Selectordinal | {n, selectordinal, ...} | {n, selectordinal, one {#st} other {#th}} |
| Exact match | =N in plural | =0 {None} |
| Offset | offset:N in plural | offset:1 |
| Hash | # in plural | # items |
| Escape brace | '{' or '}' | '{' renders as { |
| Escape quote | '' | it''s |
| Quoted sequence | '…' | 'literal text' |