How to load translations from files in Next.js (Pages Router) v16
Separate translatable content from code
Problem
Hardcoding user-facing strings directly into components creates a tight coupling between content and code. Every time copy changes, developers must locate and modify implementation files. Adding support for a second language requires finding every hardcoded string and wrapping it in conditional logic. A third language extends that logic further. This approach makes translation workflows dependent on code deployments and prevents non-technical team members from updating content independently.
As the application grows, managing translations becomes increasingly difficult. Strings scattered across dozens of components are hard to audit, duplicate, or update consistently. Translators cannot work in parallel with developers because they need access to the codebase itself.
Solution
Store all user-facing text in external resource files, organized by language, with one JSON file per locale. Each message is identified by a unique key rather than its literal text. Components reference these keys instead of hardcoded strings.
Load the appropriate translation file based on the locale received from Next.js and pass the messages to react-intl's provider. The application can then switch languages by loading a different file without changing any component code. This decouples content from implementation, allowing translators to work in standard JSON files while developers reference stable message keys.
Steps
1. Create translation files organized by locale
Put translation messages in JSON files separated by locale. Each file contains key-value pairs where keys are stable identifiers and values are the translated strings for that language.
{
"welcome": "Welcome back",
"greeting": "Hello, {name}",
"itemCount": "You have {count, plural, one {# item} other {# items}}"
}
Create one file per supported locale (for example, messages/en.json, messages/es.json, messages/fr.json) in a messages directory at your project root. Use the same keys across all files so react-intl can look up the correct translation for the active locale.
2. Load messages in getStaticProps
Read the translation file based on the locale received from Next.js in getStaticProps. This ensures messages are available server-side and passed to the page as props.
import { GetStaticProps } from "next";
export const getStaticProps: GetStaticProps = async (context) => {
const locale = context.locale || "en";
const messages = (await import(`../messages/${locale}.json`)).default;
return {
props: {
messages,
},
};
};
The dynamic import loads only the file for the current locale. Next.js provides the locale value automatically based on the URL or user preferences.
3. Pass messages to IntlProvider in _app
Wrap your root component with IntlProvider and configure it with the user's current locale and the corresponding translated messages. Access the messages from pageProps so every page can provide its own translations.
import { AppProps } from "next/app";
import { IntlProvider } from "react-intl";
import { useRouter } from "next/router";
export default function App({ Component, pageProps }: AppProps) {
const { locale, defaultLocale } = useRouter();
return (
<IntlProvider
locale={locale || "en"}
defaultLocale={defaultLocale || "en"}
messages={pageProps.messages}
>
<Component {...pageProps} />
</IntlProvider>
);
}
The provider makes the messages available to all components in the tree. Each page loads its own message file via getStaticProps, and _app receives those messages through pageProps.
4. Reference messages by key in components
Use react-intl's FormattedMessage component or useIntl hook to display translated text. Reference messages by their key rather than hardcoding strings.
import { FormattedMessage, useIntl } from "react-intl";
export default function HomePage() {
const intl = useIntl();
const userName = "Alice";
return (
<div>
<h1>
<FormattedMessage id="welcome" />
</h1>
<p>
<FormattedMessage id="greeting" values={{ name: userName }} />
</p>
<input placeholder={intl.formatMessage({ id: "searchPlaceholder" })} />
</div>
);
}
React-intl looks up and formats the translated message at the given id. If a translation is missing, it falls back to the defaultMessage if provided. Variables passed in the values prop are interpolated into the message string.