next-intl and uses a custom subfolder-per-locale content structure rather than Next.js’s built-in internationalization features.
Why Not Next.js Built-in i18n?
Next.js offers a built-in i18n routing system, but the Node.js website does not use it. The reasons:- Content structure mismatch — The site uses a subfolder per locale (
/pages/en/,/pages/fr/) rather than file extensions (file.en.md). Next.js’s built-in system doesn’t support this model. - Page listing requirements — The custom system needs to enumerate all pages per locale for static generation, which is not straightforward with the Next.js built-in approach.
- Historical consistency — The subfolder approach matches the structure of the previous Node.js website, aiding migration and long-term maintainability.
- Long-term control — A custom solution ensures the team is not dependent on the direction Next.js takes with its built-in i18n features.
next-intl
Package:next-intl@~4.8.3
next-intl is the i18n library. It is initialized via the next-intl/plugin in next.config.mjs:
useTranslations() hook:
Always call
useTranslations() with no arguments and pass the full key path to t(). Do not call useTranslations('some.prefix') and then t('suffix') — the full path enables static analysis tools to validate that keys exist.ICU Message Syntax
Translation values follow the ICU Message Syntax, which supports:- Simple strings:
"Hello, world!" - Variable interpolation:
"Hello, {name}!" - Pluralization:
- Select (gender, conditional text):
Content Structure
English source content lives inapps/site/pages/en/. Translated content lives in parallel subdirectories:
packages/i18n/locales/{locale}.json.
Locale Configuration
Supported locales are declared inpackages/i18n/src/config.json. Each entry is an object:
enabled flag controls whether a locale is active. The default: true entry is English.
Locale Detection and Routing
The middleware fileapps/site/middleware.ts handles browser locale detection:
- The browser sends an
Accept-Languageheader - The middleware reads this header and finds the best matching locale from the enabled locales in
config.json - The user is redirected to the appropriate locale prefix (e.g.,
/es/download) - If no match is found, the request falls back to
/en
Fallback Behavior
Not every page is translated into every language. For untranslated pages:- The English body content is served
- Navigation, sidebar, and UI strings are shown in the user’s locale
- No additional configuration is needed —
next.dynamic.mjshandles fallback path generation automatically
Translation Keys
Translation keys for UI components follow a canonical path convention:- Keys are nested JSON matching the component file path
- Prefix: component canonical path (e.g.,
components.common.myComponent) - Suffix: semantic description (e.g.,
copyButton.title) - Keys use camelCase only
- New keys are added only to
packages/i18n/locales/en.json— Crowdin syncs to other locales