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