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>
);
}
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 also 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.
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);
Since the messages can be freely defined and loaded, you can also 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
};
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
If possible, you should configure an explicit time zone, as this 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 that is configured for the server and in the browser, this is the local time zone of the user. As the time zone of the server and the one from the user can be different, this might be problematic when your app is both rendered 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).
Global now value
To avoid mismatches between the server and client environment, it is recommended to configure a static global now
value on the provider:
<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
.
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.
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.
Global 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 also can 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>
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();
}