Global configuration
Configuration properties that you use across your Next.js app can be set globally.
Client- and Server Components
Depending on if you handle internationalization in Client- or Server Components, the configuration from NextIntlClientProvider
or i18n.ts
will be applied respectively.
NextIntlClientProvider
can be used to provide configuration for Client Components.
import {NextIntlClientProvider} from 'next-intl';
import {notFound} from 'next/navigation';
export default async function LocaleLayout({children, params: {locale}}) {
let messages;
try {
messages = (await import(`../../messages/${locale}.json`)).default;
} catch (error) {
notFound();
}
return (
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Note that NextIntlClientProvider
inherits the props locale
, now
and timeZone
when the component is rendered from a Server Component. Other configuration like messages
and formats
can be provided as necessary.
Messages
The most crucial aspect of internationalization is providing labels based on the user's language. The recommended workflow is to store your messages in your repository along with the code.
├── messages
│ ├── en.json
│ ├── de-AT.json
│ └── ...
...
import {NextIntlClientProvider} from 'next-intl';
import {notFound} from 'next/navigation';
export default async function LocaleLayout({children, params: {locale}}) {
let messages;
try {
messages = (await import(`../../messages/${locale}.json`)).default;
} catch (error) {
notFound();
}
return (
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Colocating your messages with app code is beneficial because it allows developers to make changes to messages quickly. Additionally, you can use the shape of your local messages for type checking.
Translators can collaborate on messages by using CI tools, such as Crowdin's GitHub integration (opens in a new tab), which allows changes to be synchronized directly into your code repository.
How can I load messages from remote sources?
While it's recommended to colocate at least the messages for the default locale, you can also load messages from remote sources, e.g. with the Crowdin OTA JS Client (opens in a new tab).
import OtaClient from '@crowdin/ota-client';
const defaultLocale = 'en';
const client = new OtaClient('<distribution-hash>');
const messages =
locale === defaultLocale
? (await import(`../../messages/en.json`)).default
: await client.getStringsByLocale(locale);
How can I split my messages into multiple files?
Since the messages can be freely defined and loaded, you can split your messages into multiple files and merge them later at runtime if you prefer:
const messages = {
...(await import(`../../messages/${locale}/login.json`)).default,
...(await import(`../../messages/${locale}/dashboard.json`)).default
};
How can I use messages from another locale as fallbacks?
If you have incomplete messages for a given locale and would like to use messages from another locale as a fallback, you can merge the two accordingly.
import deepmerge from 'deepmerge';
const userMessages = (await import(`../../messages/${locale}.json`)).default;
const defaultMessages = (await import(`../../messages/en.json`)).default;
const messages = deepmerge(defaultMessages, userMessages);
Time zone
Specifying a time zone affects the rendering of dates and times. By default, the available time zone of the runtime will be used: In Node.js this is the time zone of the server and in the browser, this is the local time zone of the user. As these can differ, this can lead to markup mismatches when your components render both on the server as well as on the client side.
To ensure consistency, you can globally define a time zone:
// The time zone can either be statically defined, read from the
// user profile if you store such a setting, or based on dynamic
// request information like the locale or headers.
const timeZone = 'Europe/Vienna';
<NextIntlClientProvider timeZone={timeZone}>...<NextIntlClientProvider>
The available time zone names can be looked up in the tz database (opens in a new tab).
Now value
To avoid mismatches between the server and client environment, it is recommended to configure a global value for now
:
<NextIntlClientProvider
// This value can be generated in data fetching
// functions to make sure it's consistent
now={now}
...
>
<App />
</NextIntlClientProvider>
This value will be used as the default for the relativeTime
function as well as returned during the initial render of useNow
.
Tip: For consistent results in end-to-end tests, it can be helpful to mock this value to a constant value, e.g. based on an environment parameter.
Important: When you cache the rendered markup (e.g. when using static
rendering (opens in a new tab)),
the formatted value will become stale. Therefore either regenerate these pages
regularly or use the updateInterval
option of
useNow
on the client side.
Formats
To achieve consistent date, time, number and list formatting, you can define a set of global formats.
<NextIntlClientProvider
...
formats={{
dateTime: {
short: {
day: 'numeric',
month: 'short',
year: 'numeric'
}
},
number: {
precise: {
maximumFractionDigits: 5
}
},
list: {
enumeration: {
style: 'long',
type: 'conjunction'
}
}
}}
>
<App />
</NextIntlClientProvider>
import {useFormatter} from 'next-intl';
function Component() {
const format = useFormatter();
format.dateTime(new Date('2020-11-20T10:36:01.516Z'), 'short');
format.number(47.414329182, 'precise');
format.list(['HTML', 'CSS', 'JavaScript'], 'enumeration');
}
Global formats for numbers, dates and times can be referenced in messages too.
{
"ordered": "You've ordered this product on {orderDate, date, short}",
"latitude": "Latitude: {latitude, number, precise}"
}
import {useTranslations} from 'next-intl';
function Component() {
const t = useTranslations();
t('ordered', {orderDate: new Date('2020-11-20T10:36:01.516Z')});
t('latitude', {latitude: 47.414329182});
}
Default translation values
To achieve consistent usage of translation values and reduce redundancy, you can define a set of global default values. This configuration can also be used to apply consistent styling of commonly used rich text elements.
<NextIntlClientProvider
...
defaultTranslationValues={{
important: (chunks) => <b>{chunks}</b>,
value: 123
}}
>
<App />
</NextIntlClientProvider>
The defaults will be overridden by locally provided values.
Error handling
By default, when a message fails to resolve or when the formatting failed, an error will be printed on the console. In this case ${namespace}.${key}
will be rendered instead to keep your app running.
This behavior can be customized with the onError
and getMessageFallback
configuration option.
import {NextIntlClientProvider, IntlErrorCode} from 'next-intl';
function onError(error) {
if (error.code === IntlErrorCode.MISSING_MESSAGE) {
// Missing translations are expected and should only log an error
console.error(error);
} else {
// Other errors indicate a bug in the app and should be reported
reportToErrorTracking(error);
}
}
function getMessageFallback({namespace, key, error}) {
const path = [namespace, key].filter((part) => part != null).join('.');
if (error.code === IntlErrorCode.MISSING_MESSAGE) {
return path + ' is not yet translated';
} else {
return 'Dear developer, please fix this message: ' + path;
}
}
<NextIntlClientProvider ... onError={onError} getMessageFallback={getMessageFallback}>
<App />
</NextIntlClientProvider>
Locale
NextIntlClientProvider
requires a locale
that it provides to all hooks from next-intl
.
If you render NextIntlClientProvider
from a Server Component, the locale
will automatically be received.
In all other cases, e.g. when being rendered from a Client Component, a unit test or within the Pages Router, you need to pass this prop explicitly. Be sure to also set a timeZone
and a value for now
in this case to avoid potential markup mismatches.
I'm using the Pages Router, how can I provide the locale?
If you use internationalized routing with the Pages Router (opens in a new tab), you can receive the locale from the router in order to pass it to NextIntlClientProvider
:
import {useRouter} from 'next/router';
// ...
const router = useRouter();
return (
<NextIntlClientProvider locale={router.locale}>
...
</NextIntlClientProvider>;
);
Retrieve global configuration
As a convenience, there are a couple of hooks that allow you to read global configuration.
import {useLocale, useTimeZone, useMessages} from 'next-intl';
function Component() {
const locale = useLocale();
const timeZone = useTimeZone();
const messages = useMessages();
}