Skip to content
fluenti

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.

The simplest message is plain text with no special syntax:

Hello World

Plain-text messages compile to a static string — zero runtime overhead.

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 {…}.

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.

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.

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"
StyleOutput (en)Description
(none)1,234.56Locale-aware decimal grouping
currency$1,234.56Currency symbol from locale’s default currency
percent12%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%"

Format Date objects or timestamps:

Last login: {date, date, short}
StyleOutput (en-US)Description
(none)3/23/2026Default format
short3/23/26Short date
longMarch 23, 2026Long date
fullMonday, March 23, 2026Full 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"

Format times from Date objects:

Meeting at {time, time, short}
StyleOutput (en-US)Description
(none)2:30:00 PMMedium time (default)
short2:30 PMHours and minutes
long2:30:00 PM ESTWith timezone
t('Meeting at {time, time, short}', { time: new Date('2026-03-23T14:30:00') }, 'en')
// → "Meeting at 2:30 PM"

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"

The Unicode CLDR defines six plural categories. Each language uses a different subset:

CategoryWhen usedExample languages
zeroZero quantityArabic, Welsh
oneSingular or “one-like”English, French, Russian
twoDualArabic, Welsh
fewSmall numbersRussian (2-4), Polish, Arabic (3-10)
manyLarge numbersRussian (5-20), Arabic (11-99), French (1M+)
otherEverything 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.

LanguageCategories needed
Englishone, other
Chinese, Japanese, Koreanother only
Frenchone, many, other
Russianone, few, many, other
Arabiczero, one, two, few, many, other

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}
}

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}}
}

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

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 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, select,
male {He changed his profile picture}
female {She changed her profile picture}
other {They changed their profile picture}
}

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

Plurals and selects can be nested inside each other to handle complex grammatical requirements. Fluenti supports up to 10 levels of nesting.

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"

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}
}}
}

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}
}

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.

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 }"

Use two consecutive single quotes to produce one literal quote:

it''s a beautiful day
t("it''s a beautiful day")
// → "it's a beautiful day"

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.

You can combine escape types in the same message:

it''s a '{'test'}'
t("it''s a '{'test'}'")
// → "it's a {test}"
SyntaxOutputDescription
'''Literal single quote
'{'{Literal opening brace
'}'}Literal closing brace
'any text'any textQuoted sequence (everything literal)

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.

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.

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 text
t({ 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.

ApproachWhen to use
Auto-generated (default)Most cases — natural-language keys are self-documenting
Explicit IDLegacy 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:

  1. Explicit id (if non-empty string) → use it
  2. message exists → hash-based ID from hashMessage(message, context)
  3. Fallback → empty string

For the full identity API, see hashMessage, createMessageId, and resolveDescriptorId in the core reference.

A compact summary of all ICU MessageFormat syntax supported by Fluenti:

PatternSyntaxExample
Plain textHello WorldHello 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}
Offsetoffset:N in pluraloffset:1
Hash# in plural# items
Escape brace'{' or '}''{' renders as {
Escape quote''it''s
Quoted sequence'…''literal text'