Internationalization Best Practices#
Good messages give translators enough context to express the same meaning naturally in every locale. Keep complete thoughts together, format values with locale-aware APIs, and treat terminology as part of your product.
Keep messages inline and complete#
Declare each message next to its use with a defaultMessage and a useful description. Colocation preserves UI context for both developers and translators, and lets extraction tools remove messages when their usages disappear.
<FormattedMessage
defaultMessage="Delete {fileName}?"
description="Confirmation dialog title shown before deleting a file"
values={{fileName}}
/>
Do not concatenate fragments or assemble sentences from multiple messages. Translators may need to reorder words, change inflection, or translate the same English word differently based on its role.
// Avoid: translators only see disconnected fragments.
const greeting = intl.formatMessage({defaultMessage: 'Welcome'})
return `${greeting}, ${name}!`
// Prefer: translators see and can reorder the complete thought.
return intl.formatMessage({defaultMessage: 'Welcome, {name}!'}, {name})
For the same reason, do not reuse one message for a button, page title, and status label only because their English text matches. Give each usage its own message and description.
Put grammar and formatting in the message#
Use ICU plural and select arguments instead of branching in application code. This keeps every grammatical variant together and allows each locale to define the variants it needs.
intl.formatMessage(
{
defaultMessage:
'{count, plural, =0 {No results} one {# result} other {# results}}',
},
{count}
)
Prefer ICU number and date skeletons when formatting is part of a sentence. Skeletons make intent visible to translators while preserving locale-appropriate output.
intl.formatMessage(
{
defaultMessage:
'Your total is {total, number, ::currency/USD}. Delivery is {date, date, ::yyyyMMdd}.',
},
{total, date}
)
Skeletons describe formatting, not sentence structure. Keep surrounding punctuation and words inside the message so translators can move them. See ICU Message Syntax for supported skeletons.
Treat measurements as formatted values. Do not join a placeholder to a unit ({duration}day, {size}MB) or format the number separately and concatenate the unit. Use an ICU number skeleton inside a message, or formatNumber / <FormattedNumber> with style: 'unit' for a standalone value.
// In a message, keep the raw value and its unit format together.
intl.formatMessage(
{defaultMessage: 'Retry in {duration, number, ::measure-unit/duration-day}.'},
{duration}
)
// For a standalone measurement, use the unit formatter directly.
intl.formatNumber(size, {
style: 'unit',
unit: 'megabyte',
unitDisplay: 'short',
})
Calendar names are locale data, not message copy. Do not declare or translate arrays such as ['Mon', 'Tue', ...] or month-name maps. Format an actual date with formatDate, <FormattedDate>, or Intl.DateTimeFormat. For weekday-only controls, use stable reference dates and pin the time zone so the day cannot shift.
const monday = new Date(Date.UTC(2024, 0, 1))
intl.formatDate(monday, {weekday: 'short', timeZone: 'UTC'})
When a value is not part of a message, use FormatJS or the equivalent built-in Intl API instead of constructing locale-sensitive text yourself:
formatNumberor<FormattedNumber>for numbers and currenciesformatDate,formatTime, or their component equivalents for dates and timesformatDateTimeRangeor<FormattedDateTimeRange>for rangesformatListor<FormattedList>for listsformatRelativeTimeor<FormattedRelativeTime>for relative time
Prefer these APIs when ICU skeletons do not cover the format. For example, relative time has no message skeleton; let formatRelativeTime produce the locale-aware phrase instead of recreating it with plural rules.
Separating text is appropriate when the UI presents a semantic label and an independently formatted value rather than one sentence. Keep the label translatable, format the value with its native API, and use layout instead of string concatenation:
<dl className="metadata-row">
<dt>
<FormattedMessage
defaultMessage="Last updated:"
description="Label before a relative time"
/>
</dt>
<dd>
<FormattedRelativeTime value={-1} unit="day" />
</dd>
</dl>
This can render as “Last updated: 1 day ago” while allowing the label and relative-time phrase to adapt independently.
Do not use spaces, non-breaking spaces, or newlines for layout. Different writing systems use whitespace differently. Use CSS for visual spacing.
Keep brand names translatable#
Do not hide a brand behind a placeholder such as {brand}. Brands may stay unchanged in one locale but be transliterated or use an established local name in another. The translator also needs the full sentence to place the brand naturally.
// Avoid: the brand cannot be transliterated, and sentence context is weaker.
intl.formatMessage(
{defaultMessage: '{brand} helps you find a place to stay.'},
{brand: 'Airbnb'}
)
// Prefer: the translator controls the complete message, including the brand.
intl.formatMessage({
defaultMessage: 'Airbnb helps you find a place to stay.',
description: 'Product introduction; Airbnb is a brand name',
})
For example, a Japanese translation may render “Google” as “グーグル”, while a Simplified Chinese translation may use Coca-Cola's established local name “可口可乐”. Product and legal requirements still apply: record whether each brand should be translated, transliterated, or kept unchanged in your glossary.
Maintain a glossary#
A shared glossary keeps important terms consistent across translators, teams, and releases. Add an entry when a term is product-specific, ambiguous, legally sensitive, or intentionally left untranslated.
Each entry should include:
- source term and meaning
- usage context and an example sentence
- approved translation for each locale, when one exists
- terms or translations to avoid
- capitalization, pluralization, and do-not-translate rules
- owner and last review date
Treat meanings separately. For example, “workspace” as a product container may need a different translation from “workspace” as a physical desk area. Review the glossary with translators as product language changes; do not use it to force one translation into every context.
Review messages in context#
Before shipping, review translated UI with realistic data. Check long text, plurals, gender or select variants, right-to-left layouts, narrow screens, and accessibility labels. Pseudolocalization can expose hard-coded strings and layout assumptions, but native-speaker review remains necessary for meaning and tone.
See Message Declaration for extractable message patterns and Application Workflow for the translation lifecycle.