Skip to content

API

This page describes the public building blocks exported by @nano_kit/intl: context helpers, message formats, translation utilities, and common composition patterns.

Creates an internationalization context with a bound messages method.

import { intl } from '@nano_kit/intl'
const { messages, $loading, $error } = intl(
$locale,
loader
)

The loader can return full translation data or load namespaces on demand.

Creates reactive messages for a namespace.

const [$t, $pending, $error] = messages('home', {
title: text(),
attendees: plural('count')
})

Returns the input value as-is. Use it for objects, arrays, dictionaries, or values that do not need formatting.

const [$t] = messages('home', {
categories: raw<Record<string, string>>()
})
$t().categories?.conference

Formats string messages and optional string fallbacks.

const [$t] = messages('layout', {
title: text('Untitled')
})
$t().title
// string | 'Untitled'

uppercase(format), lowercase(format), capitalize(format)

Section titled “uppercase(format), lowercase(format), capitalize(format)”

Transforms text formatter output with the active locale.

const [$t] = messages('home', {
title: capitalize(text())
})
$t().title
// 'Hello'

Replaces {name} placeholders with formatted values.

const [$t] = messages('home', {
greeting: params({
name: text()
})
})
$t().greeting({
name: 'Ada'
})

params can preformat another formatter before replacing placeholders. That makes it possible to share parameters across nested formats.

const translations = {
home: {
mailbox: {
one: '{name} has {count} message',
other: '{name} has {count} messages'
}
}
}
const [$t] = messages('home', {
mailbox: params({
name: text()
}, plural('count', forms({
one: text(),
other: text()
})))
})
$t().mailbox({
name: 'Ada',
count: 2
})
// 'Ada has 2 messages'

Use format() when the message should format an application value instead of formatting a value from translation data. It binds another format to the current locale and returns a callable message.

const [$t] = messages('home', {
formatPrice: format(number({
style: 'currency',
currency: 'USD'
})),
eventDate: format(capitalize(datetime({
dateStyle: 'medium',
timeZone: 'UTC'
})))
})
$t().formatPrice(12)
// '$12.00'
$t().eventDate(new Date('2024-01-02T00:00:00.000Z'))
// 'Jan 2, 2024'

This is useful for dates, numbers, and other UI values that come from app state.

The formatter reads a signal or an accessor passed as the value, so the $t.eventDate($startsAt) shortcut is a computed message that follows $startsAt. A null value formats like a missing one, so data that is still loading needs no guard:

const $startsAt = signal<Date | null>(null)
const $eventDate = $t.eventDate($startsAt)
$eventDate()
// undefined
$startsAt(new Date('2024-01-02T00:00:00.000Z'))
$eventDate()
// 'Jan 2, 2024'

Intl formats can be used inside params(...) for translated templates, or wrapped with format(...) when the value comes from runtime state, API data, or a database.

A fallback is always passed together with options, for example number(0, {}) or datetime(fallback, false): a lone argument is always read as options.

Formats numbers with Intl.NumberFormat.

const [$t] = messages('stats', {
formatCount: format(number({
maximumFractionDigits: 1
}))
})
$t().formatCount(1234.56)
// '1,234.6'

Formats Date or timestamp values with Intl.DateTimeFormat.

const [$t] = messages('event', {
formatDate: format(datetime({
dateStyle: 'medium',
timeZone: 'UTC'
}))
})
$t().formatDate(new Date('2024-01-02T00:00:00.000Z'))
// 'Jan 2, 2024'

Formats the distance from now to a date, a timestamp or a date string with Intl.RelativeTimeFormat, in the largest unit the distance fills: years, months, weeks, days, hours, minutes or seconds.

const [$t] = messages('event', {
formatStartsIn: format(relativetime({
numeric: 'auto'
}))
})
$t().formatStartsIn(event.startsAt)
// 'tomorrow' for an event that starts in 30 hours

largestUnit and smallestUnit bound the unit, and both set to one unit format in that unit only. With numeric: 'auto', a distance below the smallest unit reads as “now” when that unit is an hour or less, and as “today” or “this week” for calendar units, so a feed with smallestUnit: 'minute' shows “now” for anything newer than a minute:

const [$t] = messages('activity', {
formatTime: format(relativetime({
numeric: 'auto',
smallestUnit: 'minute'
}))
})
$t().formatTime(comment.createdAt)
// 'now', then '5 minutes ago', 'yesterday', 'last week'

relativeTo sets the moment to count from instead of the current time. It takes a signal or an accessor as well, so a message follows a ticking clock, for example one built on interval(). A month counts as 30 days and a year as 365: the unit comes from the distance, not from the calendar.

Formats duration objects with Intl.DurationFormat.

const [$t] = messages('media', {
formatRuntime: format(duration({
style: 'short'
}))
})
$t().formatRuntime({
hours: 1,
minutes: 30
})
// '1 hr, 30 min'

Formats string iterables with Intl.ListFormat.

const [$t] = messages('filters', {
formatTags: format(list({
type: 'conjunction'
}))
})
$t().formatTags(['react', 'ssr', 'signals'])
// 'react, ssr, and signals'

Formats ranges through an Intl formatter that supports formatRange.

const [$t] = messages('price', {
formatAmount: format(range(number, {
style: 'currency',
currency: 'USD'
}))
})
$t().formatAmount([1, 5])
// '$1.00 – $5.00'

Matches an LDML plural form with Intl.PluralRules. If forms(...) is omitted, plural reads forms directly from the translation input. A plain string translation serves every form, which suits locales without plural distinctions.

const [$t] = messages('home', {
attendees: plural('count')
})
$t().attendees({
count: 3
})
// '3 going'
/* Also works with the value itself. */
$t().attendees(3)
// '3 going'

Use forms(...) when nested forms need their own formatters.

const [$t] = messages('home', {
attendees: plural('count', forms({
one: text('{count} attendee'),
other: text('{count} attendees')
}))
})
$t().attendees({
count: 1
})
// '1 attendee'

Exact numeric forms are checked before plural rules.

const [$t] = messages('home', {
guests: plural('count', forms({
0: text('No guests'),
one: text('{count} guest'),
other: text('{count} guests')
}))
})
$t().guests({
count: 0
})
// 'No guests'

Matches a case by parameter value. If cases(...) is omitted, match reads cases directly from the translation input. A plain string translation serves every case.

const [$t] = messages('invite', {
message: match('gender')
})
$t().message({
gender: 'female'
})
// 'She invited female.'
/* Also works with the value itself. */
$t().message('female')
// 'She invited female.'

Use cases(...) when cases need their own formatters.

const [$t] = messages('account', {
status: match('status', cases({
active: text('Active {status}'),
disabled: text('Disabled {status}')
}))
})
$t().status({
status: 'active'
})
// 'Active active'

Use other(key) when unknown values should fall back to a known case instead of returning undefined.

const [$t] = messages('account', {
role: match('role', other('user'))
})
$t().role({
role: 'guest'
})
// 'User area'

It also works together with cases(...).

const [$t] = messages('account', {
role: match('role', cases({
admin: text(),
user: text()
}), other('user'))
})
$t().role({
role: 'guest'
})
// 'User area'

match can compose with plural when one case depends on another formatter.

const [$t] = messages('tasks', {
message: plural('count', forms({
one: match('gender'),
other: match('gender')
}))
})
$t().message({
gender: 'female',
count: 3
})
// 'She has 3 tasks'

match can also wrap plural when the translation shape starts with cases and nests forms inside them.

const [$t] = messages('tasks', {
message: match('gender', cases({
male: plural('count', forms({
one: text('He has one task'),
other: text('He has {count} tasks')
})),
female: plural('count', forms({
one: text('She has one task'),
other: text('She has {count} tasks')
}))
}))
})
$t().message({
gender: 'female',
count: 3
})
// 'She has 3 tasks'

Maps lightweight tags inside translated strings to rich chunks. Self-closing tags like <br/> are supported and mapped with empty chunks. Each tag handler also receives an unique index, usable as a key for framework nodes.

const [$t] = messages('docs', {
message: rich('Read <link>the docs</link><br/>today', {
link: (chunks, i) => <a key={i} href='/docs'>{chunks}</a>,
br: (_, i) => <br key={i} />
})
})
$t().message
// ['Read ', <a href='/docs'>the docs</a>, <br />, 'today']

Maps lightweight tags to strings.

const [$t] = messages('docs', {
message: markup('Read <strong>the docs</strong>', {
strong: chunks => `<strong>${chunks}</strong>`
})
})
$t().message
// 'Read <strong>the docs</strong>'

Turns dotted object keys into nested objects.

const translations = deflat({
'layout.title': 'Event Board',
'home.title': 'Find your next frontend event',
'home.attendees.one': '{count} going',
'home.attendees.other': '{count} going'
})

direction($locale) and getDirection(locale)

Section titled “direction($locale) and getDirection(locale)”

Use direction($locale) when the UI needs to react to left-to-right and right-to-left locales.

import { direction } from '@nano_kit/intl'
const $dir = direction($locale)
$dir()
// 'ltr' or 'rtl'

getDirection(locale) is available for non-reactive code.