next-intl
Version:
Internationalization (i18n) for Next.js
255 lines (239 loc) • 9.55 kB
JavaScript
import fs from 'fs';
import { createRequire } from 'module';
import path from 'path';
import { getFormatExtension } from '../extractor/format/index.js';
import { normalizeMessagesCatalogPaths } from '../extractor/normalizeExtractorConfig.js';
import SourceFileFilter from '../extractor/source/SourceFileFilter.js';
import { isDevelopmentOrNextBuild } from './config.js';
import { isNextJs16OrHigher, hasStableTurboConfig } from './nextFlags.js';
import { throwError } from './utils.js';
const require$1 = createRequire(import.meta.url);
function withExtensions(localPath) {
return [`${localPath}.ts`, `${localPath}.tsx`, `${localPath}.js`, `${localPath}.jsx`];
}
function normalizeTurbopackAliasPath(pathname) {
// Turbopack alias targets should use forward slashes; Windows backslashes can
// break resolution in dev (see `next-intl/config` alias path style).
return pathname.replace(/\\/g, '/');
}
function resolveI18nPath(providedPath, cwd) {
function resolvePath(pathname) {
const parts = [];
if (cwd) parts.push(cwd);
parts.push(pathname);
return path.resolve(...parts);
}
function pathExists(pathname) {
return fs.existsSync(resolvePath(pathname));
}
if (providedPath) {
// We use the `isNextDevOrBuild` condition to avoid throwing errors
// if `next.config.ts` is read by a non-Next.js process.
// https://github.com/amannn/next-intl/discussions/2209#discussioncomment-15650927
if (isDevelopmentOrNextBuild && !pathExists(providedPath)) {
throwError(`Could not find i18n config at ${providedPath}, please provide a valid path.`);
}
return providedPath;
} else {
for (const candidate of [...withExtensions('./i18n/request'), ...withExtensions('./src/i18n/request')]) {
if (pathExists(candidate)) {
return candidate;
}
}
if (isDevelopmentOrNextBuild) {
throwError(`Could not locate request configuration module.\n\nThis path is supported by default: ./(src/)i18n/request.{js,jsx,ts,tsx}\n\nAlternatively, you can specify a custom location in your Next.js config:\n\nconst withNextIntl = createNextIntlPlugin(\n './path/to/i18n/request.tsx'\n);`);
}
// Default as fallback
if (pathExists('./src')) {
return './src/i18n/request.ts';
} else {
return './i18n/request.ts';
}
}
}
function getNextConfig(pluginConfig, nextConfig, extractorConfig) {
const useTurbo = process.env.TURBOPACK != null;
// `experimental-analyze` doesn’t set the TURBOPACK env param. Since Next.js
// 16 doesn't print a warning when we configure both Turbo- and Webpack, just
// always configure Turbopack just in case.
const shouldConfigureTurbo = useTurbo || isNextJs16OrHigher();
const nextIntlConfig = {};
let messageLoadPaths = [];
if (pluginConfig.experimental?.messages) {
messageLoadPaths = normalizeMessagesCatalogPaths(pluginConfig.experimental.messages.path);
}
function getExtractMessagesLoaderConfig(config) {
return {
loader: 'next-intl/extractor/extractionLoader',
options: config
};
}
function getCatalogLoaderConfig() {
const messages = pluginConfig.experimental.messages;
return {
loader: 'next-intl/extractor/catalogLoader',
options: {
messages: {
format: messages.format,
...(messages.precompile !== undefined && {
precompile: messages.precompile
})
}
}
};
}
function getTurboRules() {
return nextConfig?.turbopack?.rules ||
// @ts-expect-error -- For Next.js <16
nextConfig?.experimental?.turbo?.rules || {};
}
function addTurboRule(rules, glob, rule) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (rules[glob]) {
if (Array.isArray(rules[glob])) {
rules[glob].push(rule);
} else {
rules[glob] = [rules[glob], rule];
}
} else {
rules[glob] = rule;
}
}
if (shouldConfigureTurbo) {
if (pluginConfig.requestConfig && path.isAbsolute(pluginConfig.requestConfig)) {
throwError("Turbopack support for next-intl currently does not support absolute paths, please provide a relative one (e.g. './src/i18n/config.ts').\n\nFound: " + pluginConfig.requestConfig);
}
// Assign alias for `next-intl/config`
const resolveAlias = {
// Turbo aliases don't work with absolute
// paths (see error handling above)
'next-intl/config': resolveI18nPath(pluginConfig.requestConfig)
};
// Add alias for precompiled message formatting
if (pluginConfig.experimental?.messages?.precompile) {
// Workaround for https://github.com/vercel/next.js/issues/88540
let formatOnlyPath = path.relative(process.cwd(), require$1.resolve('use-intl/format-message/format-only'));
// Turbopack seems to require this, otherwise `use-intl/format-message` is
// still bundled (despite the code correctly calling into `format-only`).
// Note that in this monorepo this is not necessary, because we'll end
// up with a path like `../…` — but for actual consumers this is required.
if (!formatOnlyPath.startsWith('.')) {
formatOnlyPath = `./${formatOnlyPath}`;
}
resolveAlias['use-intl/format-message'] = normalizeTurbopackAliasPath(formatOnlyPath);
}
// Add loaders
let rules;
// Add loader for extractor
if (pluginConfig.experimental?.extract) {
if (!isNextJs16OrHigher()) {
throwError('Message extraction requires Next.js 16 or higher.');
}
rules ??= getTurboRules();
addTurboRule(rules, `*.{${SourceFileFilter.EXTENSIONS.join(',')}}`, {
loaders: [getExtractMessagesLoaderConfig(extractorConfig)],
condition: {
// We don't filter for `path` here to allow transformation
// of `useExtracted` calls in external packages (e.g. monorepos)
content: /(useExtracted|getExtracted)/
}
});
}
// Add loader for catalog
if (pluginConfig.experimental?.messages) {
if (!isNextJs16OrHigher()) {
throwError('Message catalog loading requires Next.js 16 or higher.');
}
rules ??= getTurboRules();
const extension = getFormatExtension(pluginConfig.experimental.messages.format);
addTurboRule(rules, `*${extension}`, {
loaders: [getCatalogLoaderConfig()],
condition: {
path: `{${messageLoadPaths.join(',')}}/**/*`
},
as: '*.js'
});
}
if (hasStableTurboConfig() &&
// @ts-expect-error -- For Next.js <16
!nextConfig?.experimental?.turbo) {
nextIntlConfig.turbopack = {
...nextConfig?.turbopack,
...(rules && {
rules
}),
resolveAlias: {
...nextConfig?.turbopack?.resolveAlias,
...resolveAlias
}
};
} else {
nextIntlConfig.experimental = {
...nextConfig?.experimental,
// @ts-expect-error -- For Next.js <16
turbo: {
// @ts-expect-error -- For Next.js <16
...nextConfig?.experimental?.turbo,
...(rules && {
rules
}),
resolveAlias: {
// @ts-expect-error -- For Next.js <16
...nextConfig?.experimental?.turbo?.resolveAlias,
...resolveAlias
}
}
};
}
}
if (!useTurbo) {
nextIntlConfig.webpack = function webpack(config, context) {
if (!config.resolve) config.resolve = {};
if (!config.resolve.alias) config.resolve.alias = {};
// Assign alias for `next-intl/config`
// (Webpack requires absolute paths)
config.resolve.alias['next-intl/config'] = path.resolve(config.context, resolveI18nPath(pluginConfig.requestConfig, config.context));
// Add alias for precompiled message formatting
if (pluginConfig.experimental?.messages?.precompile) {
// Use require.resolve to get the actual file path, since
// bundlers don't properly resolve package subpath exports
// when used as alias targets
config.resolve.alias['use-intl/format-message'] = require$1.resolve('use-intl/format-message/format-only');
}
// Add loader for extractor
if (pluginConfig.experimental?.extract) {
if (!config.module) config.module = {};
if (!config.module.rules) config.module.rules = [];
config.module.rules.push({
test: new RegExp(`\\.(${SourceFileFilter.EXTENSIONS.join('|')})$`),
use: [getExtractMessagesLoaderConfig(extractorConfig)]
});
}
// Add loader for catalog
if (pluginConfig.experimental?.messages) {
if (!config.module) config.module = {};
if (!config.module.rules) config.module.rules = [];
const extension = getFormatExtension(pluginConfig.experimental.messages.format);
config.module.rules.push({
test: new RegExp(`${extension.replace(/\./g, '\\.')}$`),
include: messageLoadPaths.map(dirPath => path.resolve(config.context, dirPath)),
use: [getCatalogLoaderConfig()],
type: 'javascript/auto'
});
}
if (typeof nextConfig?.webpack === 'function') {
return nextConfig.webpack(config, context);
}
return config;
};
}
// Forward config
if (nextConfig?.trailingSlash) {
nextIntlConfig.env = {
...nextConfig.env,
_next_intl_trailing_slash: 'true'
};
}
return Object.assign({}, nextConfig, nextIntlConfig);
}
export { getNextConfig as default };