next-intl 4.6.0 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/development/ExtractorCodec-D9Tw618d.cjs +7 -0
- package/dist/cjs/development/{JSONCodec-Dlcx71xz.cjs → JSONCodec-L1_VeQBi.cjs} +10 -3
- package/dist/cjs/development/{POCodec-BW-UDNcq.cjs → POCodec-Be_UL6jy.cjs} +16 -5
- package/dist/cjs/development/plugin-DDtWCyPI.cjs +1373 -0
- package/dist/cjs/development/plugin.cjs +8 -402
- package/dist/esm/development/extractor/ExtractionCompiler.js +1 -1
- package/dist/esm/development/extractor/catalog/CatalogManager.js +42 -13
- package/dist/esm/development/extractor/catalog/SaveScheduler.js +1 -1
- package/dist/esm/development/extractor/extractionLoader.js +5 -25
- package/dist/esm/development/extractor/source/SourceFileWatcher.js +99 -5
- package/dist/esm/development/plugin/createNextIntlPlugin.js +2 -0
- package/dist/esm/development/plugin/declaration/createMessagesDeclaration.js +2 -11
- package/dist/esm/development/plugin/extractor/initExtractionCompiler.js +45 -0
- package/dist/esm/development/plugin/utils.js +16 -1
- package/dist/esm/production/extractor/ExtractionCompiler.js +1 -1
- package/dist/esm/production/extractor/catalog/CatalogManager.js +1 -1
- package/dist/esm/production/extractor/catalog/SaveScheduler.js +1 -1
- package/dist/esm/production/extractor/extractionLoader.js +1 -1
- package/dist/esm/production/extractor/source/SourceFileWatcher.js +1 -1
- package/dist/esm/production/plugin/createNextIntlPlugin.js +1 -1
- package/dist/esm/production/plugin/declaration/createMessagesDeclaration.js +1 -1
- package/dist/esm/production/plugin/extractor/initExtractionCompiler.js +1 -0
- package/dist/esm/production/plugin/utils.js +1 -1
- package/dist/types/extractor/catalog/CatalogManager.d.ts +6 -5
- package/dist/types/extractor/catalog/SaveScheduler.d.ts +2 -2
- package/dist/types/extractor/source/SourceFileFilter.d.ts +1 -1
- package/dist/types/extractor/source/SourceFileWatcher.d.ts +3 -0
- package/dist/types/plugin/extractor/initExtractionCompiler.d.ts +2 -0
- package/dist/types/plugin/utils.d.ts +6 -0
- package/package.json +4 -4
- package/dist/cjs/development/ExtractorCodec-DZKNn0Zq.cjs +0 -37
- package/dist/types/extractor/extractor/ASTScope.d.ts +0 -12
|
@@ -1,407 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
var plugin = require('./plugin-DDtWCyPI.cjs');
|
|
4
|
+
require('fs/promises');
|
|
5
|
+
require('path');
|
|
6
|
+
require('@parcel/watcher');
|
|
7
|
+
require('fs');
|
|
8
|
+
require('module');
|
|
9
|
+
require('@swc/core');
|
|
6
10
|
|
|
7
|
-
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
8
|
-
function formatMessage(message) {
|
|
9
|
-
return `\n[next-intl] ${message}\n`;
|
|
10
|
-
}
|
|
11
|
-
function throwError(message) {
|
|
12
|
-
throw new Error(formatMessage(message));
|
|
13
|
-
}
|
|
14
|
-
function warn(message) {
|
|
15
|
-
console.warn(formatMessage(message));
|
|
16
|
-
}
|
|
17
11
|
|
|
18
|
-
/**
|
|
19
|
-
* Wrapper around `fs.watch` that provides a workaround
|
|
20
|
-
* for https://github.com/nodejs/node/issues/5039.
|
|
21
|
-
*/
|
|
22
|
-
function watchFile(filepath, callback) {
|
|
23
|
-
const directory = path.dirname(filepath);
|
|
24
|
-
const filename = path.basename(filepath);
|
|
25
|
-
return fs.watch(directory, {
|
|
26
|
-
persistent: false,
|
|
27
|
-
recursive: false
|
|
28
|
-
}, (event, changedFilename) => {
|
|
29
|
-
if (changedFilename === filename) {
|
|
30
|
-
callback();
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
12
|
|
|
35
|
-
|
|
36
|
-
if (process.env._NEXT_INTL_COMPILE_MESSAGES === '1') {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
process.env._NEXT_INTL_COMPILE_MESSAGES = '1';
|
|
40
|
-
fn();
|
|
41
|
-
}
|
|
42
|
-
function createMessagesDeclaration(messagesPaths) {
|
|
43
|
-
// Instead of running _only_ in certain cases, it's
|
|
44
|
-
// safer to _avoid_ running for certain known cases.
|
|
45
|
-
// https://github.com/amannn/next-intl/issues/2006
|
|
46
|
-
const shouldBailOut = ['info', 'start'
|
|
47
|
-
|
|
48
|
-
// Note: These commands don't consult the
|
|
49
|
-
// Next.js config, so we can't detect them here.
|
|
50
|
-
// - telemetry
|
|
51
|
-
// - lint
|
|
52
|
-
//
|
|
53
|
-
// What remains are:
|
|
54
|
-
// - dev
|
|
55
|
-
// - build
|
|
56
|
-
// - typegen
|
|
57
|
-
].some(arg => process.argv.includes(arg));
|
|
58
|
-
if (shouldBailOut) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Next.js can call the Next.js config multiple
|
|
63
|
-
// times - ensure we only run once.
|
|
64
|
-
runOnce(() => {
|
|
65
|
-
for (const messagesPath of messagesPaths) {
|
|
66
|
-
const fullPath = path.resolve(messagesPath);
|
|
67
|
-
if (!fs.existsSync(fullPath)) {
|
|
68
|
-
throwError(`\`createMessagesDeclaration\` points to a non-existent file: ${fullPath}`);
|
|
69
|
-
}
|
|
70
|
-
if (!fullPath.endsWith('.json')) {
|
|
71
|
-
throwError(`\`createMessagesDeclaration\` needs to point to a JSON file. Received: ${fullPath}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Keep this as a runtime check and don't replace
|
|
75
|
-
// this with a constant during the build process
|
|
76
|
-
const env = process.env['NODE_ENV'.trim()];
|
|
77
|
-
compileDeclaration(messagesPath);
|
|
78
|
-
if (env === 'development') {
|
|
79
|
-
startWatching(messagesPath);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
function startWatching(messagesPath) {
|
|
85
|
-
const watcher = watchFile(messagesPath, () => {
|
|
86
|
-
compileDeclaration(messagesPath, true);
|
|
87
|
-
});
|
|
88
|
-
process.on('exit', () => {
|
|
89
|
-
watcher.close();
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
function compileDeclaration(messagesPath, async = false) {
|
|
93
|
-
const declarationPath = messagesPath.replace(/\.json$/, '.d.json.ts');
|
|
94
|
-
function createDeclaration(content) {
|
|
95
|
-
return `// This file is auto-generated by next-intl, do not edit directly.
|
|
96
|
-
// See: https://next-intl.dev/docs/workflows/typescript#messages-arguments
|
|
97
|
-
|
|
98
|
-
declare const messages: ${content.trim()};
|
|
99
|
-
export default messages;`;
|
|
100
|
-
}
|
|
101
|
-
if (async) {
|
|
102
|
-
return fs.promises.readFile(messagesPath, 'utf-8').then(content => fs.promises.writeFile(declarationPath, createDeclaration(content)));
|
|
103
|
-
}
|
|
104
|
-
const content = fs.readFileSync(messagesPath, 'utf-8');
|
|
105
|
-
fs.writeFileSync(declarationPath, createDeclaration(content));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const formats = {
|
|
109
|
-
json: {
|
|
110
|
-
codec: () => Promise.resolve().then(function () { return require('./JSONCodec-Dlcx71xz.cjs'); }),
|
|
111
|
-
extension: '.json'
|
|
112
|
-
},
|
|
113
|
-
po: {
|
|
114
|
-
codec: () => Promise.resolve().then(function () { return require('./POCodec-BW-UDNcq.cjs'); }),
|
|
115
|
-
extension: '.po'
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
function isBuiltInFormat(format) {
|
|
119
|
-
return typeof format === 'string' && format in formats;
|
|
120
|
-
}
|
|
121
|
-
function getFormatExtension(format) {
|
|
122
|
-
if (isBuiltInFormat(format)) {
|
|
123
|
-
return formats[format].extension;
|
|
124
|
-
} else {
|
|
125
|
-
return format.extension;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
class SourceFileFilter {
|
|
130
|
-
static EXTENSIONS = ['ts', 'tsx', 'js', 'jsx'];
|
|
131
|
-
|
|
132
|
-
// Will not be entered, except if explicitly asked for
|
|
133
|
-
// TODO: At some point we should infer these from .gitignore
|
|
134
|
-
static IGNORED_DIRECTORIES = ['node_modules', '.next', '.git'];
|
|
135
|
-
static isSourceFile(filePath) {
|
|
136
|
-
const ext = path.extname(filePath);
|
|
137
|
-
return SourceFileFilter.EXTENSIONS.map(cur => '.' + cur).includes(ext);
|
|
138
|
-
}
|
|
139
|
-
static shouldEnterDirectory(dirPath, srcPaths) {
|
|
140
|
-
const dirName = path.basename(dirPath);
|
|
141
|
-
if (SourceFileFilter.IGNORED_DIRECTORIES.includes(dirName)) {
|
|
142
|
-
return SourceFileFilter.isIgnoredDirectoryExplicitlyIncluded(dirPath, srcPaths);
|
|
143
|
-
}
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
static isIgnoredDirectoryExplicitlyIncluded(ignoredDirPath, srcPaths) {
|
|
147
|
-
return srcPaths.some(srcPath => SourceFileFilter.isWithinPath(srcPath, ignoredDirPath));
|
|
148
|
-
}
|
|
149
|
-
static isWithinPath(targetPath, basePath) {
|
|
150
|
-
const relativePath = path.relative(basePath, targetPath);
|
|
151
|
-
return relativePath === '' || !relativePath.startsWith('..');
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function getCurrentVersion() {
|
|
156
|
-
try {
|
|
157
|
-
const require$1 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('plugin.cjs', document.baseURI).href)));
|
|
158
|
-
const pkg = require$1('next/package.json');
|
|
159
|
-
return pkg.version;
|
|
160
|
-
} catch (error) {
|
|
161
|
-
throw new Error('Failed to get current Next.js version. This can happen if next-intl/plugin is imported into your app code outside of your next.config.js.', {
|
|
162
|
-
cause: error
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
function compareVersions(version1, version2) {
|
|
167
|
-
const v1Parts = version1.split('.').map(Number);
|
|
168
|
-
const v2Parts = version2.split('.').map(Number);
|
|
169
|
-
for (let i = 0; i < 3; i++) {
|
|
170
|
-
const v1 = v1Parts[i] || 0;
|
|
171
|
-
const v2 = v2Parts[i] || 0;
|
|
172
|
-
if (v1 > v2) return 1;
|
|
173
|
-
if (v1 < v2) return -1;
|
|
174
|
-
}
|
|
175
|
-
return 0;
|
|
176
|
-
}
|
|
177
|
-
function hasStableTurboConfig() {
|
|
178
|
-
return compareVersions(getCurrentVersion(), '15.3.0') >= 0;
|
|
179
|
-
}
|
|
180
|
-
function isNextJs16OrHigher() {
|
|
181
|
-
return compareVersions(getCurrentVersion(), '16.0.0') >= 0;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function withExtensions(localPath) {
|
|
185
|
-
return [`${localPath}.ts`, `${localPath}.tsx`, `${localPath}.js`, `${localPath}.jsx`];
|
|
186
|
-
}
|
|
187
|
-
function resolveI18nPath(providedPath, cwd) {
|
|
188
|
-
function resolvePath(pathname) {
|
|
189
|
-
const parts = [];
|
|
190
|
-
if (cwd) parts.push(cwd);
|
|
191
|
-
parts.push(pathname);
|
|
192
|
-
return path.resolve(...parts);
|
|
193
|
-
}
|
|
194
|
-
function pathExists(pathname) {
|
|
195
|
-
return fs.existsSync(resolvePath(pathname));
|
|
196
|
-
}
|
|
197
|
-
if (providedPath) {
|
|
198
|
-
if (!pathExists(providedPath)) {
|
|
199
|
-
throwError(`Could not find i18n config at ${providedPath}, please provide a valid path.`);
|
|
200
|
-
}
|
|
201
|
-
return providedPath;
|
|
202
|
-
} else {
|
|
203
|
-
for (const candidate of [...withExtensions('./i18n/request'), ...withExtensions('./src/i18n/request')]) {
|
|
204
|
-
if (pathExists(candidate)) {
|
|
205
|
-
return candidate;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
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(
|
|
209
|
-
|
|
210
|
-
Alternatively, you can specify a custom location in your Next.js config:
|
|
211
|
-
|
|
212
|
-
const withNextIntl = createNextIntlPlugin(
|
|
213
|
-
'./path/to/i18n/request.tsx'
|
|
214
|
-
);`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
function getNextConfig(pluginConfig, nextConfig) {
|
|
218
|
-
const useTurbo = process.env.TURBOPACK != null;
|
|
219
|
-
const nextIntlConfig = {};
|
|
220
|
-
function getExtractMessagesLoaderConfig() {
|
|
221
|
-
const experimental = pluginConfig.experimental;
|
|
222
|
-
if (!experimental.srcPath || !pluginConfig.experimental?.messages) {
|
|
223
|
-
throwError('`srcPath` and `messages` are required when using `extractor`.');
|
|
224
|
-
}
|
|
225
|
-
return {
|
|
226
|
-
loader: 'next-intl/extractor/extractionLoader',
|
|
227
|
-
options: {
|
|
228
|
-
srcPath: experimental.srcPath,
|
|
229
|
-
sourceLocale: experimental.extract.sourceLocale,
|
|
230
|
-
messages: pluginConfig.experimental.messages
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
function getCatalogLoaderConfig() {
|
|
235
|
-
return {
|
|
236
|
-
loader: 'next-intl/extractor/catalogLoader',
|
|
237
|
-
options: {
|
|
238
|
-
messages: pluginConfig.experimental.messages
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
function getTurboRules() {
|
|
243
|
-
return nextConfig?.turbopack?.rules ||
|
|
244
|
-
// @ts-expect-error -- For Next.js <16
|
|
245
|
-
nextConfig?.experimental?.turbo?.rules || {};
|
|
246
|
-
}
|
|
247
|
-
function addTurboRule(rules, glob, rule) {
|
|
248
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
249
|
-
if (rules[glob]) {
|
|
250
|
-
if (Array.isArray(rules[glob])) {
|
|
251
|
-
rules[glob].push(rule);
|
|
252
|
-
} else {
|
|
253
|
-
rules[glob] = [rules[glob], rule];
|
|
254
|
-
}
|
|
255
|
-
} else {
|
|
256
|
-
rules[glob] = rule;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
if (useTurbo) {
|
|
260
|
-
if (pluginConfig.requestConfig && path.isAbsolute(pluginConfig.requestConfig)) {
|
|
261
|
-
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);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Assign alias for `next-intl/config`
|
|
265
|
-
const resolveAlias = {
|
|
266
|
-
// Turbo aliases don't work with absolute
|
|
267
|
-
// paths (see error handling above)
|
|
268
|
-
'next-intl/config': resolveI18nPath(pluginConfig.requestConfig)
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// Add loaders
|
|
272
|
-
let rules;
|
|
273
|
-
|
|
274
|
-
// Add loader for extractor
|
|
275
|
-
if (pluginConfig.experimental?.extract) {
|
|
276
|
-
if (!isNextJs16OrHigher()) {
|
|
277
|
-
throwError('Message extraction requires Next.js 16 or higher.');
|
|
278
|
-
}
|
|
279
|
-
rules ??= getTurboRules();
|
|
280
|
-
const srcPaths = (Array.isArray(pluginConfig.experimental.srcPath) ? pluginConfig.experimental.srcPath : [pluginConfig.experimental.srcPath]).map(srcPath => srcPath.endsWith('/') ? srcPath.slice(0, -1) : srcPath);
|
|
281
|
-
addTurboRule(rules, `*.{${SourceFileFilter.EXTENSIONS.join(',')}}`, {
|
|
282
|
-
loaders: [getExtractMessagesLoaderConfig()],
|
|
283
|
-
condition: {
|
|
284
|
-
// Note: We don't need `not: 'foreign'`, because this is
|
|
285
|
-
// implied by the filter based on `srcPath`.
|
|
286
|
-
path: `{${srcPaths.join(',')}}` + '/**/*',
|
|
287
|
-
content: /(useExtracted|getExtracted)/
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Add loader for catalog
|
|
293
|
-
if (pluginConfig.experimental?.messages) {
|
|
294
|
-
if (!isNextJs16OrHigher()) {
|
|
295
|
-
throwError('Message catalog loading requires Next.js 16 or higher.');
|
|
296
|
-
}
|
|
297
|
-
rules ??= getTurboRules();
|
|
298
|
-
const extension = getFormatExtension(pluginConfig.experimental.messages.format);
|
|
299
|
-
addTurboRule(rules, `*${extension}`, {
|
|
300
|
-
loaders: [getCatalogLoaderConfig()],
|
|
301
|
-
condition: {
|
|
302
|
-
path: `${pluginConfig.experimental.messages.path}/**/*`
|
|
303
|
-
},
|
|
304
|
-
as: '*.js'
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
if (hasStableTurboConfig() &&
|
|
308
|
-
// @ts-expect-error -- For Next.js <16
|
|
309
|
-
!nextConfig?.experimental?.turbo) {
|
|
310
|
-
nextIntlConfig.turbopack = {
|
|
311
|
-
...nextConfig?.turbopack,
|
|
312
|
-
...(rules && {
|
|
313
|
-
rules
|
|
314
|
-
}),
|
|
315
|
-
resolveAlias: {
|
|
316
|
-
...nextConfig?.turbopack?.resolveAlias,
|
|
317
|
-
...resolveAlias
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
} else {
|
|
321
|
-
nextIntlConfig.experimental = {
|
|
322
|
-
...nextConfig?.experimental,
|
|
323
|
-
// @ts-expect-error -- For Next.js <16
|
|
324
|
-
turbo: {
|
|
325
|
-
// @ts-expect-error -- For Next.js <16
|
|
326
|
-
...nextConfig?.experimental?.turbo,
|
|
327
|
-
...(rules && {
|
|
328
|
-
rules
|
|
329
|
-
}),
|
|
330
|
-
resolveAlias: {
|
|
331
|
-
// @ts-expect-error -- For Next.js <16
|
|
332
|
-
...nextConfig?.experimental?.turbo?.resolveAlias,
|
|
333
|
-
...resolveAlias
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
} else {
|
|
339
|
-
nextIntlConfig.webpack = function webpack(config, context) {
|
|
340
|
-
if (!config.resolve) config.resolve = {};
|
|
341
|
-
if (!config.resolve.alias) config.resolve.alias = {};
|
|
342
|
-
|
|
343
|
-
// Assign alias for `next-intl/config`
|
|
344
|
-
// (Webpack requires absolute paths)
|
|
345
|
-
config.resolve.alias['next-intl/config'] = path.resolve(config.context, resolveI18nPath(pluginConfig.requestConfig, config.context));
|
|
346
|
-
|
|
347
|
-
// Add loader for extractor
|
|
348
|
-
if (pluginConfig.experimental?.extract) {
|
|
349
|
-
if (!config.module) config.module = {};
|
|
350
|
-
if (!config.module.rules) config.module.rules = [];
|
|
351
|
-
const srcPath = pluginConfig.experimental.srcPath;
|
|
352
|
-
config.module.rules.push({
|
|
353
|
-
test: new RegExp(`\\.(${SourceFileFilter.EXTENSIONS.join('|')})$`),
|
|
354
|
-
include: Array.isArray(srcPath) ? srcPath.map(cur => path.resolve(config.context, cur)) : path.resolve(config.context, srcPath || ''),
|
|
355
|
-
use: [getExtractMessagesLoaderConfig()]
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Add loader for catalog
|
|
360
|
-
if (pluginConfig.experimental?.messages) {
|
|
361
|
-
if (!config.module) config.module = {};
|
|
362
|
-
if (!config.module.rules) config.module.rules = [];
|
|
363
|
-
const extension = getFormatExtension(pluginConfig.experimental.messages.format);
|
|
364
|
-
config.module.rules.push({
|
|
365
|
-
test: new RegExp(`${extension.replace(/\./g, '\\.')}$`),
|
|
366
|
-
include: path.resolve(config.context, pluginConfig.experimental.messages.path),
|
|
367
|
-
use: [getCatalogLoaderConfig()],
|
|
368
|
-
type: 'javascript/auto'
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
if (typeof nextConfig?.webpack === 'function') {
|
|
372
|
-
return nextConfig.webpack(config, context);
|
|
373
|
-
}
|
|
374
|
-
return config;
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Forward config
|
|
379
|
-
if (nextConfig?.trailingSlash) {
|
|
380
|
-
nextIntlConfig.env = {
|
|
381
|
-
...nextConfig.env,
|
|
382
|
-
_next_intl_trailing_slash: 'true'
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
return Object.assign({}, nextConfig, nextIntlConfig);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function initPlugin(pluginConfig, nextConfig) {
|
|
389
|
-
if (nextConfig?.i18n != null) {
|
|
390
|
-
warn("An `i18n` property was found in your Next.js config. This likely causes conflicts and should therefore be removed if you use the App Router.\n\nIf you're in progress of migrating from the Pages Router, you can refer to this example: https://next-intl.dev/examples#app-router-migration\n");
|
|
391
|
-
}
|
|
392
|
-
const messagesPathOrPaths = pluginConfig.experimental?.createMessagesDeclaration;
|
|
393
|
-
if (messagesPathOrPaths) {
|
|
394
|
-
createMessagesDeclaration(typeof messagesPathOrPaths === 'string' ? [messagesPathOrPaths] : messagesPathOrPaths);
|
|
395
|
-
}
|
|
396
|
-
return getNextConfig(pluginConfig, nextConfig);
|
|
397
|
-
}
|
|
398
|
-
function createNextIntlPlugin(i18nPathOrConfig = {}) {
|
|
399
|
-
const config = typeof i18nPathOrConfig === 'string' ? {
|
|
400
|
-
requestConfig: i18nPathOrConfig
|
|
401
|
-
} : i18nPathOrConfig;
|
|
402
|
-
return function withNextIntl(nextConfig) {
|
|
403
|
-
return initPlugin(config, nextConfig);
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
module.exports = createNextIntlPlugin;
|
|
13
|
+
module.exports = plugin.createNextIntlPlugin;
|
|
@@ -32,7 +32,8 @@ class CatalogManager {
|
|
|
32
32
|
// Cached instances
|
|
33
33
|
|
|
34
34
|
// Resolves when all catalogs are loaded
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
// Resolves when the initial project scan and processing is complete
|
|
36
37
|
|
|
37
38
|
constructor(config, opts) {
|
|
38
39
|
this.config = config;
|
|
@@ -41,6 +42,9 @@ class CatalogManager {
|
|
|
41
42
|
this.isDevelopment = opts.isDevelopment ?? false;
|
|
42
43
|
this.extractor = opts.extractor;
|
|
43
44
|
if (this.isDevelopment) {
|
|
45
|
+
// We kick this off as early as possible, so we get notified about changes
|
|
46
|
+
// that happen during the initial project scan (while awaiting it to
|
|
47
|
+
// complete though)
|
|
44
48
|
this.sourceWatcher = new SourceFileWatcher(this.getSrcPaths(), this.handleFileEvents.bind(this));
|
|
45
49
|
void this.sourceWatcher.start();
|
|
46
50
|
}
|
|
@@ -87,9 +91,12 @@ class CatalogManager {
|
|
|
87
91
|
const sourceDiskMessages = await this.loadSourceMessages();
|
|
88
92
|
this.loadCatalogsPromise = this.loadTargetMessages();
|
|
89
93
|
await this.loadCatalogsPromise;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
this.scanCompletePromise = (async () => {
|
|
95
|
+
const sourceFiles = await SourceFileScanner.getSourceFiles(this.getSrcPaths());
|
|
96
|
+
await Promise.all(Array.from(sourceFiles).map(async filePath => this.processFile(filePath)));
|
|
97
|
+
this.mergeSourceDiskMetadata(sourceDiskMessages);
|
|
98
|
+
})();
|
|
99
|
+
await this.scanCompletePromise;
|
|
93
100
|
if (this.isDevelopment) {
|
|
94
101
|
const catalogLocales = this.getCatalogLocales();
|
|
95
102
|
catalogLocales.subscribeLocalesChange(this.onLocalesChange);
|
|
@@ -136,12 +143,23 @@ class CatalogManager {
|
|
|
136
143
|
}
|
|
137
144
|
}
|
|
138
145
|
} else {
|
|
139
|
-
// For target: disk wins completely
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
// For target: disk wins completely, BUT preserve existing translations
|
|
147
|
+
// if we read empty (likely a write in progress by an external tool
|
|
148
|
+
// that causes the file to temporarily be empty)
|
|
149
|
+
const existingTranslations = this.translationsByTargetLocale.get(locale);
|
|
150
|
+
const hasExistingTranslations = existingTranslations && existingTranslations.size > 0;
|
|
151
|
+
if (diskMessages.length > 0) {
|
|
152
|
+
// We got content from disk, replace with it
|
|
153
|
+
const translations = new Map();
|
|
154
|
+
for (const message of diskMessages) {
|
|
155
|
+
translations.set(message.id, message);
|
|
156
|
+
}
|
|
157
|
+
this.translationsByTargetLocale.set(locale, translations);
|
|
158
|
+
} else if (hasExistingTranslations) ; else {
|
|
159
|
+
// We read empty and have no existing translations
|
|
160
|
+
const translations = new Map();
|
|
161
|
+
this.translationsByTargetLocale.set(locale, translations);
|
|
143
162
|
}
|
|
144
|
-
this.translationsByTargetLocale.set(locale, translations);
|
|
145
163
|
}
|
|
146
164
|
}
|
|
147
165
|
mergeSourceDiskMetadata(diskMessages) {
|
|
@@ -163,7 +181,12 @@ class CatalogManager {
|
|
|
163
181
|
let messages = [];
|
|
164
182
|
try {
|
|
165
183
|
const content = await fs.readFile(absoluteFilePath, 'utf8');
|
|
166
|
-
|
|
184
|
+
let extraction;
|
|
185
|
+
try {
|
|
186
|
+
extraction = await this.extractor.extract(absoluteFilePath, content);
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
167
190
|
messages = extraction.messages;
|
|
168
191
|
} catch (err) {
|
|
169
192
|
if (err.code !== 'ENOENT') {
|
|
@@ -322,8 +345,14 @@ class CatalogManager {
|
|
|
322
345
|
if (this.loadCatalogsPromise) {
|
|
323
346
|
await this.loadCatalogsPromise;
|
|
324
347
|
}
|
|
348
|
+
|
|
349
|
+
// Wait for initial scan to complete to avoid race conditions
|
|
350
|
+
if (this.scanCompletePromise) {
|
|
351
|
+
await this.scanCompletePromise;
|
|
352
|
+
}
|
|
325
353
|
let changed = false;
|
|
326
|
-
|
|
354
|
+
const expandedEvents = await this.sourceWatcher.expandDirectoryDeleteEvents(events, Array.from(this.messagesByFile.keys()));
|
|
355
|
+
for (const event of expandedEvents) {
|
|
327
356
|
const hasChanged = await this.processFile(event.path);
|
|
328
357
|
changed ||= hasChanged;
|
|
329
358
|
}
|
|
@@ -331,10 +360,10 @@ class CatalogManager {
|
|
|
331
360
|
await this.save();
|
|
332
361
|
}
|
|
333
362
|
}
|
|
334
|
-
|
|
363
|
+
[Symbol.dispose]() {
|
|
335
364
|
this.sourceWatcher?.stop();
|
|
336
365
|
this.sourceWatcher = undefined;
|
|
337
|
-
this.saveScheduler.
|
|
366
|
+
this.saveScheduler[Symbol.dispose]();
|
|
338
367
|
if (this.catalogLocales && this.isDevelopment) {
|
|
339
368
|
this.catalogLocales.unsubscribeLocalesChange(this.onLocalesChange);
|
|
340
369
|
}
|
|
@@ -1,44 +1,24 @@
|
|
|
1
|
-
import ExtractionCompiler from './ExtractionCompiler.js';
|
|
2
1
|
import MessageExtractor from './extractor/MessageExtractor.js';
|
|
3
2
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
let compiler;
|
|
3
|
+
// Module-level extractor instance for transformation caching.
|
|
4
|
+
// Note: Next.js/Turbopack may create multiple loader instances, but each
|
|
5
|
+
// only handles file transformation. The ExtractionCompiler (which manages
|
|
6
|
+
// catalogs) is initialized separately in createNextIntlPlugin.
|
|
9
7
|
let extractor;
|
|
10
|
-
let extractAllPromise;
|
|
11
8
|
function extractionLoader(source) {
|
|
12
|
-
const options = this.getOptions();
|
|
13
9
|
const callback = this.async();
|
|
14
10
|
const projectRoot = this.rootContext;
|
|
15
11
|
|
|
16
12
|
// Avoid rollup's `replace` plugin to compile this away
|
|
17
13
|
const isDevelopment = process.env['NODE_ENV'.trim()] === 'development';
|
|
18
14
|
if (!extractor) {
|
|
19
|
-
// This instance is shared with the compiler to enable caching
|
|
20
|
-
// across code transformations and catalog extraction
|
|
21
15
|
extractor = new MessageExtractor({
|
|
22
16
|
isDevelopment,
|
|
23
17
|
projectRoot,
|
|
24
18
|
sourceMap: this.sourceMap
|
|
25
19
|
});
|
|
26
20
|
}
|
|
27
|
-
|
|
28
|
-
compiler = new ExtractionCompiler(options, {
|
|
29
|
-
isDevelopment,
|
|
30
|
-
projectRoot,
|
|
31
|
-
sourceMap: this.sourceMap,
|
|
32
|
-
extractor
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
if (!extractAllPromise) {
|
|
36
|
-
extractAllPromise = compiler.extractAll();
|
|
37
|
-
}
|
|
38
|
-
extractor.extract(this.resourcePath, source).then(async result => {
|
|
39
|
-
if (!isDevelopment) {
|
|
40
|
-
await extractAllPromise;
|
|
41
|
-
}
|
|
21
|
+
extractor.extract(this.resourcePath, source).then(result => {
|
|
42
22
|
callback(null, result.code, result.map);
|
|
43
23
|
}).catch(callback);
|
|
44
24
|
}
|