Guides
Message format
One small syntax, powered by native Intl underneath: plurals, gender and formatting without learning full ICU.
Interpolation
"Hola {name}" → t('saludo', { name: 'Aron' })Values format themselves by type: numbers via Intl.NumberFormat, dates via Intl.DateTimeFormat, strings as-is. A missing param shows the placeholder literally and warns once naming the message, so it’s easy to spot and never a crash.
Formatters
Append :formatter (with optional /arg) to a param:
| Syntax | Result |
|---|---|
| {n:number} | Locale number: 1,234.56 / 1234,56 |
| {n:integer} | Rounded, no decimals |
| {n:percent} | 0.5 → 50% |
| {n:currency/EUR} | Currency with ISO code |
| {d:date/long} | dateStyle: short · medium · long · full |
| {d:time/short} | timeStyle variants |
Custom formatters plug in at instance creation. The full signature is (value, locale, arg?) => string, where locale is the active locale and arg is whatever follows the / in the placeholder:
createVerbaly({
formatters: { upper: (v) => String(v).toUpperCase() },
});
// "{word:upper}" → HOLARelative time, lists & units
The format surface covers the rest of modern Intl, still with zero dependencies:
"Updated {when:relative}" → Updated 2 hours ago · yesterday
"Ready {n:relative/day}" → Ready in 3 days
"Works in {langs:list}" → Works in English, Spanish, and Portuguese
"Pick {opts:list/or}" → Pick red, green, or blue
"{d:unit/kilometer}" → 3 kmrelative: pass aDateand the unit picks itself against now (Intl.RelativeTimeFormatwithnumeric: 'auto', so you get yesterday, not 1 day ago); pass a number with an explicit unit (relative/hour).list: arrays localize viaIntl.ListFormat, conjunction by default,/orfor disjunction,/unitfor unit lists. Each item auto-formats per locale.unit: any CLDR unit id (kilometer,megabyte,liter…) throughIntl.NumberFormat.- Bad data never crashes a render, and never slips by either: an unknown format, an invalid unit, a placeholder missing its argument or a list that is not a list all warn once, naming the message, and fall back to
String(value).
Plurals and selects: one syntax
"{count | =0: sin mensajes | one: un mensaje | other: # mensajes}"
"{gender | male: él | female: ella | other: elle}"- Numbers match
=Nexact values first, thenIntl.PluralRulescategories (zero one two few many other). - Strings match their variant key, falling back to
other. #renders the number, locale-formatted. Variants nest placeholders freely.- Always include
otherin a plural block. It is the catch-all, so a block without it renders nothing for any count it does not list.verbaly checkfails the build on that, and also warns when a language needs forms your message lacks: Polish reachesfewandmanywhere English only needsoneandother. If a catalog reaches your app without passing through the build, from a lazy loader or a CMS, the runtime warns in the console naming the message instead of rendering an empty string quietly. A gender or role block is different: leavingotherout there is your choice, and Verbaly leaves it alone.
ICU escape-hatch
Have existing ICU MessageFormat strings, or need its exact syntax? Write ICU and Verbaly detects it automatically and parses it into the same engine, with zero extra dependencies.
"{count, plural, one {# item} other {# items}}"
"{gender, select, male {he} female {she} other {they}}"
"{n, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}"- Auto-detected per message: no config, no marker. Native and ICU messages coexist in the same catalog.
- Supports
plural,select,selectordinal(real ordinal rules),{n, number/date/time, style},#,=N, nesting and'…'quoting. - Prefer the native syntax by default; reach for ICU only when you need it.
- You only pay for it if you use it. Verbaly reads your catalogs at build time, and the ICU parser goes into your app only when one of your messages needs it. That is about 544 bytes, and there is nothing to switch on. Relative time works the same way: another 318 bytes that only travel when a message asks for them.
Escapes
| Write | Get |
|---|---|
| {{ | Literal { |
| }} | Literal } |
| || | Literal | inside variants |
| ## | Literal # inside variants |
In rich messages (data-verbaly-rich or <Trans>) the numeric entity { also shows a literal brace: it decodes after parsing, so it never touches the placeholder syntax.