This library contains core intl API that is used by react-intl.
Installation#
npm i -S @formatjs/intl
The intl object#
The core of @formatjs/intl is the intl object (of type IntlShape), which is the instance to store a cache of all Intl.* APIs, configurations, compiled messages and such. The lifecycle of the intl object is typically tied to the locale & the list of messages that it contains, which means when you switch locale, this object should be recreated.
Tip
The intl object should be reused as much as possible for performance.
createIntl#
This allows you to create an IntlShape object that contains all format* methods. For example:
// This is optional but highly recommended
// since it prevents memory leak
const cache = createIntlCache()
const intl = createIntl(
{
locale: 'fr-FR',
messages: {},
},
cache
)
// Call imperatively
intl.formatNumber(20)
createIntlCache#
Creates a cache instance to be used globally across locales. This memoizes previously created Intl.* constructors for performance and is only an in-memory cache.
IntlShape#
interface IntlConfig {
locale: string
timeZone?: string
fallbackOnEmptyString?: boolean
formats: CustomFormats
messages: Record<string, string> | Record<string, MessageFormatElement[]>
defaultLocale: string
defaultRichTextElements?: Record<string, FormatXMLElementFn<React.ReactNode>>
defaultFormats: CustomFormats
onError(err: string): void
onWarn(warning: string): void
}
interface IntlFormatters {
formatDuration(
value: Parameters<Intl.DurationFormat['format']>[0],
opts?: FormatDurationOptions
): string
formatDurationToParts(
value: Parameters<Intl.DurationFormat['format']>[0],
opts?: FormatDurationOptions
): ReturnType<Intl.DurationFormat['formatToParts']>
formatDate(value: number | Date | string, opts?: FormatDateOptions): string
formatTime(value: number | Date | string, opts?: FormatDateOptions): string
formatDateToParts(
value: number | Date | string,
opts?: FormatDateOptions
): Intl.DateTimeFormatPart[]
formatTimeToParts(
value: number | Date | string,
opts?: FormatDateOptions
): Intl.DateTimeFormatPart[]
formatRelativeTime(
value: number,
unit?: FormattableUnit,
opts?: FormatRelativeTimeOptions
): string
formatNumber(value: number, opts?: FormatNumberOptions): string
formatNumberToParts(
value: number,
opts?: FormatNumberOptions
): Intl.NumberFormatPart[]
formatPlural(
value: number | string,
opts?: FormatPluralOptions
): ReturnType<Intl.PluralRules['select']>
formatMessage(
descriptor: MessageDescriptor,
values?: Record<string, PrimitiveType | FormatXMLElementFn<string, string>>
): string
formatMessage(
descriptor: MessageDescriptor,
values?: Record<string, PrimitiveType | T | FormatXMLElementFn<T, R>>
): R
formatList(values: Iterable<string>, opts?: FormatListOptions): string
formatList(
values: Iterable<string | T>,
opts?: FormatListOptions
): T | string | Array<string | T>
formatListToParts(
values: Iterable<string | T>,
opts?: FormatListOptions
): Part[]
formatDisplayName(
value: string,
opts?: FormatDisplayNameOptions
): string | undefined
}
type IntlShape = IntlConfig & IntlFormatters
The definition above shows what the intl object will look like. It's made up of two parts:
IntlConfig: The intl metadata passed as props into the parent<IntlProvider>.IntlFormatters: The imperative formatting API described below.
locale, formats, and messages#
The user's current locale and what the app should be rendered in. While defaultLocale and defaultFormats are for fallbacks or during development and represent the app's default. Notice how there is no defaultMessages, that's because each Message Descriptor provides a defaultMessage.
defaultLocale and defaultFormats#
Default locale & formats for when a message is not translated (missing from messages). defaultLocale should be the locale that defaultMessages are declared in so that a sentence is coherent in a single locale. Without defaultLocale and/or if it's set incorrectly, you might run into scenario where a sentence is in English but embedded date/time is in Spanish.
onError#
Allows the user to provide a custom error handler. By default, error messages are logged using console.error if NODE_ENV is not set to production.
defaultRichTextElements#
A map of tag to rich text formatting function. This is meant to provide a centralized way to format common tags such as <b>, <p>... or enforcing certain Design System in the codebase (e.g standardized <a> or <button>...). See https://github.com/formatjs/formatjs/issues/1752 for more context.
fallbackOnEmptyString#
Defaults to true.
This boolean option can be useful if you want to intentionally provide empty values for certain locales via empty strings. When fallbackOnEmptyString is false, empty strings will be returned instead of triggering the fallback procedure. This behaviour can be leveraged to "skip" content in specific locales.
See this issue for more context.
formatDate#
function formatDate(
value: number | Date,
options?: Intl.DateTimeFormatOptions & {format?: string}
): string
This function will return a formatted date string. It expects a value which can be parsed as a date (i.e., isFinite(new Date(value))), and accepts options that conform to DateTimeFormatOptions.
intl.formatDate(Date.now(), { year: 'numeric', month: 'numeric', day: 'numeric', })
formatTime#
function formatTime(
value: number | Date,
options?: Intl.DateTimeFormatOptions & {format?: string}
): string
This function will return a formatted date string, but it differs from formatDate by having the following default options:
{
hour: 'numeric',
minute: 'numeric',
}
It expects a value which can be parsed as a date (i.e., isFinite(new Date(value))), and accepts options that conform to DateTimeFormatOptions.
intl.formatTime(Date.now()) // "4:03 PM"
formatRelativeTime#
browser support
This requires Intl.RelativeTimeFormat which has limited browser support. Please use our polyfill if you plan to support them.
type Unit =
'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'
type RelativeTimeFormatOptions = {
numeric?: 'always' | 'auto'
style?: 'long' | 'short' | 'narrow'
}
function formatRelativeTime(
value: number,
unit: Unit,
options?: Intl.RelativeTimeFormatOptions & {
format?: string
}
): string
This function will return a formatted relative time string (e.g., "1 hour ago"). It expects a value which is a number, a unit and options that conform to Intl.RelativeTimeFormatOptions.
intl.formatRelativeTime(0)
intl.formatRelativeTime(-24, 'hour', {style: 'narrow'})
formatNumber#
This function uses Intl.NumberFormat options.
function formatNumber(
value: number,
options?: Intl.NumberFormatOptions & {format?: string}
): string
This function will return a formatted number string. It expects a value which can be parsed as a number, and accepts options that conform to NumberFormatOptions.
intl.formatNumber(1000, {style: 'currency', currency: 'USD'})
Formatting Number using unit
Currently this is part of ES2020 NumberFormat.
We've provided a polyfill here and @formatjs/intl types allow users to pass
in a sanctioned unit:
intl.formatNumber(1000, { style: 'unit', unit: 'kilobyte', unitDisplay: 'narrow', })
intl.formatNumber(1000, { unit: 'fahrenheit', unitDisplay: 'long', style: 'unit', })
formatPlural#
type PluralFormatOptions = {
type?: 'cardinal' | 'ordinal' = 'cardinal'
}
function formatPlural(
value: number,
options?: Intl.PluralFormatOptions
): 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'
This function will return a plural category string: "zero", "one", "two", "few", "many", or "other". It expects a value which can be parsed as a number, and accepts options that conform to PluralFormatOptions.
This is a low-level utility whose output could be provided to a switch statement to select a particular string to display.
intl.formatPlural(1)
intl.formatPlural(3, {style: 'ordinal'})
intl.formatPlural(4, {style: 'ordinal'})
multiple language support
This function should only be used in apps that only need to support one
language. If your app supports multiple languages use
formatMessage instead.
formatList#
browser support
This requires Intl.ListFormat which has limited browser support. Please use our polyfill if you plan to support them.
type ListFormatOptions = {
type?: 'disjunction' | 'conjunction' | 'unit'
style?: 'long' | 'short' | 'narrow'
}
function formatList(
elements: Iterable<string | React.ReactNode>,
options?: Intl.ListFormatOptions
): string | React.ReactNode[]
This function allows you to join list of things together in an i18n-safe way. For example, when the locale is en:
intl.formatList(['Me', 'myself', 'I'], {type: 'conjunction'})
intl.formatList(['5 hours', '3 minutes'], {type: 'unit'})
formatDisplayName#
browser support
This requires Intl.DisplayNames which has limited browser support. Please use our polyfill if you plan to support them.
type FormatDisplayNameOptions = {
style?: 'narrow' | 'short' | 'long'
type?: 'language' | 'region' | 'script' | 'currency'
fallback?: 'code' | 'none'
}
function formatDisplayName(
value: string | number | Record<string, unknown>,
options: FormatDisplayNameOptions
): string | undefined
Usage examples:
intl.formatDisplayName('zh-Hans-SG', {type: 'language'})
// ISO-15924 four letters script code to localized display name intl.formatDisplayName('Deva', {type: 'script'})
// ISO-4217 currency code to localized display name intl.formatDisplayName('CNY', {type: 'currency'})
// ISO-3166 two letters region code to localized display name intl.formatDisplayName('UN', {type: 'region'})
formatMessage#
For TypeScript argument checks and generic defaults, see Typed message arguments.
Message Syntax#
String/Message formatting is a paramount feature of React Intl and it builds on ICU Message Formatting by using the ICU Message Syntax. This message syntax allows for simple to complex messages to be defined, translated, and then formatted at runtime.
Simple Message:
Hello, {name}
Complex Message:
Hello, {name}, you have {itemCount, plural,
=0 {no items}
one {# item}
other {# items}
}.
See: The Message Syntax Guide.
Message Descriptor#
React Intl has a Message Descriptor concept which is used to define your app's default messages/strings and is passed into formatMessage. The Message Descriptors work very well for providing the data necessary for having the strings/messages translated, and they contain the following properties:
id: A unique, stable identifier for the messagedescription: Context for the translator about how it's used in the UIdefaultMessage: The default message (probably in English)
type MessageDescriptor = {
id: string
defaultMessage?: string
description?: string | object
}
Extracting Message Descriptor
You can extract inline-declared messages from source files using our CLI.
Message Formatting Fallbacks#
The message formatting APIs go the extra mile to provide fallbacks for the common situations where formatting fails; at the very least a non-empty string should always be returned. Here's the message formatting fallback algorithm:
- Lookup and format the translated message at
id, passed to<IntlProvider>. - Fallback to formatting the
defaultMessage. - Fallback to source of translated message at
id. - Fallback to source of
defaultMessage. - Fallback to the literal message
id.
Above, "source" refers to using the template as is, without any substitutions made.
Usage#
type MessageFormatPrimitiveValue = string | number | boolean | null | undefined
function formatMessage(
descriptor: MessageDescriptor,
values?: Record<string, MessageFormatPrimitiveValue>
): string
function formatMessage(
descriptor: MessageDescriptor,
values?: Record<
string,
MessageFormatPrimitiveValue | React.ReactElement | FormatXMLElementFn
>
): string | React.ReactNode[]
This function will return a formatted message string. It expects a MessageDescriptor with at least an id property, and accepts a shallow values object which are used to fill placeholders in the message.
If a translated message with the id has been passed to the <IntlProvider> via its messages prop it will be formatted, otherwise it will fallback to formatting defaultMessage. See: Message Formatting Fallbacks for more details.
function () { const messages = defineMessages({ greeting: { id: 'app.greeting', defaultMessage: 'Hello, {name}!', description: 'Greeting to welcome the user to the app', }, }) return intl.formatMessage(messages.greeting, {name: 'Eric'}) }
with ReactElement
const messages = defineMessages({ greeting: { id: 'app.greeting', defaultMessage: 'Hello, {name}!', description: 'Greeting to welcome the user to the app', }, }) return intl.formatMessage(messages.greeting, {name: <b>Eric</b>})
with rich text formatting
function () { const messages = defineMessages({ greeting: { id: 'app.greeting', defaultMessage: 'Hello, <bold>{name}</bold>!', description: 'Greeting to welcome the user to the app', }, }) return intl.formatMessage(messages.greeting, { name: 'Eric', bold: str => <b>{str}</b>, }) }
The message we defined using defineMessages to support extraction via babel-plugin-formatjs, but it doesn't have to be if you're not using the Babel plugin.
simple message
Messages can be simple strings without placeholders, and that's the most common type of message.
defineMessages/defineMessage#
interface MessageDescriptor {
id?: string
description?: string | object
defaultMessage?: string
}
function defineMessages(
messageDescriptors: Record<string, MessageDescriptor>
): Record<string, MessageDescriptor>
function defineMessage(messageDescriptor: MessageDescriptor): MessageDescriptor
These functions are exported by the @formatjs/intl package and are simply a hook for our CLI & babel/TS plugin to use when compiling default messages defined in JavaScript source files. This function simply returns the Message Descriptor map object that's passed-in.
const messages = defineMessages({
greeting: {
id: 'app.home.greeting',
description: 'Message to greet the user.',
defaultMessage: 'Hello, {name}!',
},
})
const msg = defineMessage({
id: 'single',
defaultMessage: 'single message',
description: 'header',
})
formatDuration#
Formats a duration object using the configured locale and Intl.DurationFormat.
Values are objects with units such as {hours: 1, minutes: 30}, not timestamps
or a total number of milliseconds.
const intl = createIntl({
locale: 'en',
formats: {duration: {clock: {style: 'digital'}}},
})
intl.formatDuration({hours: 1, minutes: 30}, {style: 'long'})
// "1 hour, 30 minutes"
intl.formatDuration({hours: 1, minutes: 30}, {format: 'clock'})
// "1:30:00"
FormatDurationOptions accepts Intl.DurationFormatOptions except
localeMatcher, plus format to select a named entry from formats.duration.
Explicit options override the named format. Formatter instances are cached by
locale and options, including across intl instances sharing createIntlCache().
This API requires native Intl.DurationFormat support or the
@formatjs/intl-durationformat polyfill.
The polyfill is not loaded automatically. Other formatting APIs do not require it.
If formatting fails, onError receives the error and the result is an empty string.
formatDurationToParts#
Accepts the same duration and options as formatDuration, and returns
Intl.DurationFormat.prototype.formatToParts() output for custom rendering.
Each numeric part includes its duration unit; literal parts preserve localized
separators. If formatting fails, onError receives the error and the result is
an empty array.
intl.formatDurationToParts({minutes: 3, seconds: 42}, {style: 'long'})
TypeScript requirements#
TypeScript 5.4 or newer is required. The formatter declarations use NoInfer
to keep typed message contracts independent of the supplied values. Existing
JavaScript usage is unchanged.
Typed message arguments#
formatMessage<Values, RichOutput> and its alias $t<Values, RichOutput>
accept an ICU argument contract first and an optional rich-output type second.
RichOutput defaults to the type supplied to createIntl<T>() (string by
default); an explicit output type must fit that type.
With no generics, a plain descriptor keeps the permissive API: TypeScript checks the descriptor and supported value types, but does not infer required placeholder names or their types from the ICU string. Missing values can still fail at runtime. A typed helper descriptor carries its contract, so no call-site generic is needed.
import {defineMessage} from '@formatjs/intl'
// Accepted by TypeScript; no ICU argument checks.
intl.$t({defaultMessage: '{count, number}'})
intl.$t({defaultMessage: '{count, number}'}, {count: 'two'})
// Explicit contract: count is required and must be numeric.
intl.$t<{count: number | bigint}>(
{defaultMessage: '{count, number}'},
{count: 2}
)
// The helper carries the contract into calls without generics.
const count = defineMessage<{count: number | bigint}>(
{defaultMessage: '{count, number}'},
{typed: true}
)
intl.$t(count, {count: 2})
intl.$t(count) // TypeScript error: missing count
defineMessages<Contracts>(catalog, {typed: true}) works the same way for a
message map. Without generics, helpers carry an empty ICU contract and reject
extra values at formatting sites. TypedMessageDescriptor also defaults to an
empty contract. For messages with arguments, supply the contract or generate it
with formatjs/enforce-message-types and generateTypes: true.
TypeScript does not verify that a handwritten contract matches the message.
These types do not change runtime formatting.
For migration from the old rich-output generic positions, supply Values
first and move the output type to the second generic.