next-intl
Version:
Internationalization (i18n) for Next.js
213 lines (200 loc) • 7.65 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { getFormatExtension } from '../extractor/format/index.js';
import SourceFileFilter from '../extractor/source/SourceFileFilter.js';
import { isNextJs16OrHigher, hasStableTurboConfig } from './nextFlags.js';
import { throwError } from './utils.js';
function withExtensions(localPath) {
return [`${localPath}.ts`, `${localPath}.tsx`, `${localPath}.js`, `${localPath}.jsx`];
}
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) {
if (!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;
}
}
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(
Alternatively, you can specify a custom location in your Next.js config:
const withNextIntl = createNextIntlPlugin(
'./path/to/i18n/request.tsx'
);`);
}
}
function getNextConfig(pluginConfig, nextConfig) {
const useTurbo = process.env.TURBOPACK != null;
const nextIntlConfig = {};
function getExtractMessagesLoaderConfig() {
const experimental = pluginConfig.experimental;
if (!experimental.srcPath || !pluginConfig.experimental?.messages) {
throwError('`srcPath` and `messages` are required when using `extractor`.');
}
return {
loader: 'next-intl/extractor/extractionLoader',
options: {
srcPath: experimental.srcPath,
sourceLocale: experimental.extract.sourceLocale,
messages: pluginConfig.experimental.messages
}
};
}
function getCatalogLoaderConfig() {
return {
loader: 'next-intl/extractor/catalogLoader',
options: {
messages: pluginConfig.experimental.messages
}
};
}
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 (useTurbo) {
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 loaders
let rules;
// Add loader for extractor
if (pluginConfig.experimental?.extract) {
if (!isNextJs16OrHigher()) {
throwError('Message extraction requires Next.js 16 or higher.');
}
rules ??= getTurboRules();
const srcPaths = (Array.isArray(pluginConfig.experimental.srcPath) ? pluginConfig.experimental.srcPath : [pluginConfig.experimental.srcPath]).map(srcPath => srcPath.endsWith('/') ? srcPath.slice(0, -1) : srcPath);
addTurboRule(rules, `*.{${SourceFileFilter.EXTENSIONS.join(',')}}`, {
loaders: [getExtractMessagesLoaderConfig()],
condition: {
// Note: We don't need `not: 'foreign'`, because this is
// implied by the filter based on `srcPath`.
path: `{${srcPaths.join(',')}}` + '/**/*',
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: `${pluginConfig.experimental.messages.path}/**/*`
},
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
}
}
};
}
} else {
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 loader for extractor
if (pluginConfig.experimental?.extract) {
if (!config.module) config.module = {};
if (!config.module.rules) config.module.rules = [];
const srcPath = pluginConfig.experimental.srcPath;
config.module.rules.push({
test: new RegExp(`\\.(${SourceFileFilter.EXTENSIONS.join('|')})$`),
include: Array.isArray(srcPath) ? srcPath.map(cur => path.resolve(config.context, cur)) : path.resolve(config.context, srcPath || ''),
use: [getExtractMessagesLoaderConfig()]
});
}
// 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: path.resolve(config.context, pluginConfig.experimental.messages.path),
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 };