Skip to content
Docs
Usage guide
Messages

Rendering i18n messages

The main part of handling internationalization (typically referred to as i18n) in your Next.js app is to provide messages based on the language of the user.

Terminology

  • Locale: We use this term to describe an identifier that contains the language and formatting preferences of users. Apart from the language, this includes optional regional information (e.g. en-US, de).
  • Messages: These are collections of namespace-label pairs that provide grouping by locale (e.g. en-US.json, de.json).

Structuring messages

To group your messages within a locale, it's recommended to use component names as namespaces and embrace them as the primary unit of code organization in your app.

en.json
{
  "About": {
    "title": "About us"
  }
}

Now, you can render messages from within a React component via the useTranslations hook:

About.tsx
import {useTranslations} from 'next-intl';
 
function About() {
  const t = useTranslations('About');
  return <h1>{t('title')}</h1>;
}

To retrieve all available messages in a component, you can omit the namespace path:

const t = useTranslations();
 
t('About.title');
How can I provide more structure for messages?

Optionally, you can structure your messages as nested objects.

en.json
{
  "auth": {
    "SignUp": {
      "title": "Sign up",
      "form": {
        "placeholder": "Please enter your name",
        "submit": "Submit"
      }
    }
  }
}
SignUp.tsx
import {useTranslations} from 'next-intl';
 
function SignUp() {
  // Provide the lowest common denominator that contains
  // all messages this component needs to consume.
  const t = useTranslations('auth.SignUp');
 
  return (
    <>
      <h1>{t('title')}</h1>
      <form>
        <input
          // The remaining hierarchy can be resolved by
          // using `.` to access nested messages.
          placeholder={t('form.placeholder')}
        />
        <button type="submit">{t('form.submit')}</button>
      </form>
    </>
  );
}
How can I reuse messages?

As your app grows, you'll want to reuse messages among your components. If you use component names as namespaces to structure your messages, you'll automatically benefit from reusable messages by reusing your components.

Examples:

  • You're displaying products in your app and often need to resolve a category identifier to a human readable label (e.g. new → "Just in"). To ensure consistency, you can add a ProductCategory component that turns the category into a string.
  • You commonly need a dialog that displays a "confirm" and "cancel" button. In this case, consider adding a ConfirmDialog component to reuse the messages along with the functionality.

There might be cases where you want to use the same message in different components, but there's no reasonable opportunity for sharing the message via a component. This might be symptom that these components should use separate messages, even if they happen to be the same for the time being. By using separate labels, you gain the flexibility to use more specific labels (e.g. "not now" instead of "cancel").

How can I use translations outside of components?

next-intl only offers an API to use translations from within React components.

This may seem like an unnecessary limitation, but this is intentional and aims to encourage the use of proven patterns that avoid potential issues—especially if they are easy to overlook.

If you're interested to dive deeper into this topic, there's a blog post that discusses the background of this design decision: How (not) to use translations outside of React components.

There's one exception however: Using next-intl with the Next.js Metadata API and Route Handlers.

Rendering ICU messages

next-intl uses ICU message syntax that allows you to express language nuances and separates state handling within messages from your app code.

💡

To work with ICU messages, it can be helpful to use an editor that supports this syntax. E.g. the Crowdin Editor (opens in a new tab) can be used by translators to work on translations without having to change app code.

Static messages

en.json
"message": "Hello world!"
t('message'); // "Hello world!"

Interpolation of dynamic values

en.json
"message": "Hello {name}!"
t('message', {name: 'Jane'}); // "Hello Jane!"

Pluralization

en.json
"message": "You have {count, plural, =0 {no followers yet} =1 {one follower} other {# followers}}."
t('message', {count: 3580}); // "You have 3,580 followers."

Ordinal pluralization

en.json
"message": "It's your {year, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} birthday!"
t('message', {year: 21}); // "It's your 21st birthday!"

Selecting enum-based values

en.json
"message": "{gender, select, female {She} male {He} other {They}} is online."
t('message', {gender: 'female'}); // "She is online."

Escaping

en.json
"message": "Escape curly braces with single quotes (e.g. '{name'})"
t('message'); // "Escape curly braces with single quotes (e.g. {name})"

Rich text

You can format rich text with custom tags and map them to React components.

en.json
{
  "richText": "This is <important><very>very</very> important</important>"
}
t.rich('richText', {
  important: (chunks) => <b>{chunks}</b>,
  very: (chunks) => <i>{chunks}</i>
});

If you want to use the same tag across your app, you can configure it via the default translation values.

Note that the ICU parser doesn't support self-closing tags at this point, therefore you have to use syntax like <br></br> if your rich text function doesn't intend to receive any chunks (e.g. br: () => <br />).

Arrays of messages

If you need to render a list of messages, the recommended approach is to map an array of keys to the corresponding messages.

en.json
{
  "Benefits": {
    "zero-config": "Works with zero config",
    "customizable": "Easy to customize",
    "fast": "Blazingly fast"
  }
}
Benefits.tsx
import {useTranslations} from 'next-intl';
 
function Benefits() {
  const t = useTranslations('Benefits');
  const keys = ['zero-config', 'customizable', 'fast'] as const;
 
  return (
    <ul>
      {keys.map((key) => (
        <li key={key}>{t(key)}</li>
      ))}
    </ul>
  );
}

If the number of items varies between locales, you can solve this by using rich text.

en.json
{
  "Benefits": {
    "items": "<item>Works with zero config</item><item>Easy to customize</item><item>Blazingly fast</item>"
  }
}
Benefits.tsx
import {useTranslations} from 'next-intl';
 
function Benefits() {
  const t = useTranslations('Benefits');
  return (
    <ul>
      {t.rich('items', {
        item: (chunks) => <li>{chunks}</li>
      })}
    </ul>
  );
}
Why can't I just use arrays in my messages?

The advantage of this approach over supporting arrays in messages is that this way you can use the formatting capabilities, e.g. to interpolate values into individual messages.

en.json
{
  "Benefits": {
    "zero-config": "Works with <b>zero config</b>",
    "customizable": "Easy to <b>customize</b>",
    "fast": "Blazingly <b>fast</b>"
  }
}
Benefits.tsx
import {useTranslations} from 'next-intl';
 
function Benefits() {
  const t = useTranslations('Benefits');
  return (
    <ul>
      {['zero-config', 'customizable', 'fast'].map((key, index, arr) => {
        const color = `hsl(${(index / (arr.length - 1)) * 360}deg 70% 50%)`;
        return (
          <li key={key}>
            {t(key, {b: (chunks) => <b style={{color}}>{chunks}</b>})}
          </li>
        );
      })}
    </ul>
  );
}

Raw messages

Messages are always parsed and therefore e.g. for rich text you need to supply the necessary tags. If you want to avoid the parsing, e.g. because you have raw HTML stored in a message, there's a separate API for this use case.

en.json
{
  "content": "<h1>Headline<h1><p>This is raw HTML</p>"
}
<div dangerouslySetInnerHTML={{__html: t.raw('content')}} />
⚠️

Important: You should always sanitize the content that you pass to dangerouslySetInnerHTML (opens in a new tab) to avoid cross-site scripting attacks.

The value of a raw message can be any valid JSON value: strings, booleans, objects and arrays.

Right-to-left languages

Languages such as Arabic, Hebrew and Persian use right-to-left script (opens in a new tab) (often abbreviated as RTL). For these languages, writing begins on the right side of the page and continues to the left.

Example:

النص في اللغة العربية _مثلا_ يُقرأ من اليمين لليسار

In addition to providing translated messages, proper RTL localization requires:

  1. Providing the dir attribute (opens in a new tab) on the document
  2. Layout mirroring, e.g. by using CSS logical properties (opens in a new tab)
  3. Element mirroring, e.g. by customizing icons

To handle these cases in your components, you can set up a reusable hook:

useTextDirection.tsx
import {isRtlLang} from 'rtl-detect';
import {useLocale} from 'next-intl';
 
export default function useTextDirection(locale) {
  const defaultLocale = useLocale();
  if (!locale) locale = defaultLocale;
  return isRtlLang(locale) ? 'rtl' : 'ltr';
}

… and use it accordingly:

app/[locale]/layout.tsx
import useTextDirection from './useTextDirection';
 
export default function Layout({children, params: {locale}}) {
  const direction = useTextDirection(locale);
 
  return (
    <html lang={locale} dir={direction}>
      {/* ... */}
    </html>
  );
}
components/Breadcrumbs.tsx
import {useTranslations} from 'next-intl';
import useTextDirection from './useTextDirection';
 
export default function Breadcrumbs({children, params}) {
  const t = useTranslations('Breadcrumbs');
  const direction = useTextDirection();
 
  return (
    <div style={{display: 'flex'}}>
      <p>{t('home')}</p>
      <div style={{marginInlineStart: 10}}>
        {direction === 'ltr' ? <ArrowRight /> : <ArrowLeft />}
      </div>
      <p style={{marginInlineStart: 10}}>{t('about')}</p>
    </div>
  );
}