UNPKG

next-intl

Version:

Internationalization (i18n) for Next.js

213 lines (200 loc) 7.65 kB
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 };