next-intl 4.5.8 → 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.
Files changed (88) hide show
  1. package/dist/cjs/development/ExtractorCodec-D9Tw618d.cjs +7 -0
  2. package/dist/cjs/development/JSONCodec-L1_VeQBi.cjs +48 -0
  3. package/dist/cjs/development/POCodec-Be_UL6jy.cjs +105 -0
  4. package/dist/cjs/development/plugin-DDtWCyPI.cjs +1373 -0
  5. package/dist/cjs/development/plugin.cjs +8 -379
  6. package/dist/esm/development/extractor/ExtractionCompiler.js +23 -26
  7. package/dist/esm/development/extractor/catalog/CatalogLocales.js +0 -33
  8. package/dist/esm/development/extractor/catalog/CatalogManager.js +171 -110
  9. package/dist/esm/development/extractor/catalog/CatalogPersister.js +31 -13
  10. package/dist/esm/development/extractor/catalog/SaveScheduler.js +1 -1
  11. package/dist/esm/development/extractor/catalogLoader.js +10 -10
  12. package/dist/esm/development/extractor/extractMessages.js +9 -2
  13. package/dist/esm/development/extractor/extractionLoader.js +15 -12
  14. package/dist/esm/development/extractor/extractor/MessageExtractor.js +5 -4
  15. package/dist/esm/development/extractor/format/ExtractorCodec.js +5 -0
  16. package/dist/esm/development/extractor/format/codecs/JSONCodec.js +40 -0
  17. package/dist/esm/development/extractor/format/codecs/POCodec.js +93 -0
  18. package/dist/esm/development/extractor/format/index.js +44 -0
  19. package/dist/esm/development/extractor/source/SourceFileScanner.js +2 -1
  20. package/dist/esm/development/extractor/source/SourceFileWatcher.js +132 -0
  21. package/dist/esm/development/extractor/utils.js +16 -1
  22. package/dist/esm/development/extractor.js +1 -0
  23. package/dist/esm/development/plugin/createNextIntlPlugin.js +3 -1
  24. package/dist/esm/development/plugin/declaration/createMessagesDeclaration.js +2 -11
  25. package/dist/esm/development/plugin/extractor/initExtractionCompiler.js +45 -0
  26. package/dist/esm/development/plugin/getNextConfig.js +7 -4
  27. package/dist/esm/development/plugin/utils.js +16 -1
  28. package/dist/esm/production/extractor/ExtractionCompiler.js +1 -1
  29. package/dist/esm/production/extractor/catalog/CatalogLocales.js +1 -1
  30. package/dist/esm/production/extractor/catalog/CatalogManager.js +1 -1
  31. package/dist/esm/production/extractor/catalog/CatalogPersister.js +1 -1
  32. package/dist/esm/production/extractor/catalog/SaveScheduler.js +1 -1
  33. package/dist/esm/production/extractor/catalogLoader.js +1 -1
  34. package/dist/esm/production/extractor/extractMessages.js +1 -1
  35. package/dist/esm/production/extractor/extractionLoader.js +1 -1
  36. package/dist/esm/production/extractor/extractor/MessageExtractor.js +1 -1
  37. package/dist/esm/production/extractor/format/ExtractorCodec.js +1 -0
  38. package/dist/esm/production/extractor/format/codecs/JSONCodec.js +1 -0
  39. package/dist/esm/production/extractor/format/codecs/POCodec.js +1 -0
  40. package/dist/esm/production/extractor/format/index.js +1 -0
  41. package/dist/esm/production/extractor/source/SourceFileScanner.js +1 -1
  42. package/dist/esm/production/extractor/source/SourceFileWatcher.js +1 -0
  43. package/dist/esm/production/extractor/utils.js +1 -1
  44. package/dist/esm/production/extractor.js +1 -1
  45. package/dist/esm/production/plugin/createNextIntlPlugin.js +1 -1
  46. package/dist/esm/production/plugin/declaration/createMessagesDeclaration.js +1 -1
  47. package/dist/esm/production/plugin/extractor/initExtractionCompiler.js +1 -0
  48. package/dist/esm/production/plugin/getNextConfig.js +1 -1
  49. package/dist/esm/production/plugin/utils.js +1 -1
  50. package/dist/types/extractor/ExtractionCompiler.d.ts +5 -10
  51. package/dist/types/extractor/catalog/CatalogLocales.d.ts +0 -2
  52. package/dist/types/extractor/catalog/CatalogManager.d.ts +26 -15
  53. package/dist/types/extractor/catalog/CatalogPersister.d.ts +15 -6
  54. package/dist/types/extractor/catalog/SaveScheduler.d.ts +2 -2
  55. package/dist/types/extractor/extractor/MessageExtractor.d.ts +6 -6
  56. package/dist/types/extractor/format/ExtractorCodec.d.ts +33 -0
  57. package/dist/types/extractor/format/codecs/JSONCodec.d.ts +2 -0
  58. package/dist/types/extractor/format/codecs/POCodec.d.ts +2 -0
  59. package/dist/types/extractor/format/codecs/fixtures/JSONCodecStructured.d.ts +2 -0
  60. package/dist/types/extractor/format/codecs/fixtures/POCodecSourceMessageKey.d.ts +2 -0
  61. package/dist/types/extractor/format/index.d.ts +15 -0
  62. package/dist/types/extractor/format/types.d.ts +8 -0
  63. package/dist/types/extractor/index.d.ts +1 -0
  64. package/dist/types/extractor/source/SourceFileFilter.d.ts +2 -2
  65. package/dist/types/extractor/source/SourceFileScanner.d.ts +1 -1
  66. package/dist/types/extractor/source/SourceFileWatcher.d.ts +15 -0
  67. package/dist/types/extractor/types.d.ts +2 -2
  68. package/dist/types/extractor/utils.d.ts +3 -0
  69. package/dist/types/plugin/extractor/initExtractionCompiler.d.ts +2 -0
  70. package/dist/types/plugin/types.d.ts +1 -1
  71. package/dist/types/plugin/utils.d.ts +6 -0
  72. package/package.json +6 -5
  73. package/dist/esm/development/extractor/formatters/Formatter.js +0 -3
  74. package/dist/esm/development/extractor/formatters/JSONFormatter.js +0 -42
  75. package/dist/esm/development/extractor/formatters/POFormatter.js +0 -51
  76. package/dist/esm/development/extractor/formatters/index.js +0 -6
  77. package/dist/esm/development/extractor/formatters/utils.js +0 -15
  78. package/dist/esm/production/extractor/formatters/Formatter.js +0 -1
  79. package/dist/esm/production/extractor/formatters/JSONFormatter.js +0 -1
  80. package/dist/esm/production/extractor/formatters/POFormatter.js +0 -1
  81. package/dist/esm/production/extractor/formatters/index.js +0 -1
  82. package/dist/esm/production/extractor/formatters/utils.js +0 -1
  83. package/dist/types/extractor/extractor/ASTScope.d.ts +0 -12
  84. package/dist/types/extractor/formatters/Formatter.d.ts +0 -10
  85. package/dist/types/extractor/formatters/JSONFormatter.d.ts +0 -10
  86. package/dist/types/extractor/formatters/POFormatter.d.ts +0 -10
  87. package/dist/types/extractor/formatters/index.d.ts +0 -5
  88. package/dist/types/extractor/formatters/utils.d.ts +0 -2
@@ -0,0 +1,1373 @@
1
+ 'use strict';
2
+
3
+ var fs$1 = require('fs/promises');
4
+ var path = require('path');
5
+ var watcher = require('@parcel/watcher');
6
+ var fs = require('fs');
7
+ var module$1 = require('module');
8
+ var core = require('@swc/core');
9
+
10
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
11
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
12
+
13
+ var fs__default$1 = /*#__PURE__*/_interopDefaultCompat(fs$1);
14
+ var path__default = /*#__PURE__*/_interopDefaultCompat(path);
15
+ var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
16
+
17
+ function formatMessage(message) {
18
+ return `\n[next-intl] ${message}\n`;
19
+ }
20
+ function throwError(message) {
21
+ throw new Error(formatMessage(message));
22
+ }
23
+ function warn(message) {
24
+ console.warn(formatMessage(message));
25
+ }
26
+
27
+ /**
28
+ * Returns a function that runs the provided callback only once per process.
29
+ * Next.js can call the config multiple times - this ensures we only run once.
30
+ * Uses an environment variable to track execution across config loads.
31
+ */
32
+ function once(namespace) {
33
+ return function runOnce(fn) {
34
+ if (process.env[namespace] === '1') {
35
+ return;
36
+ }
37
+ process.env[namespace] = '1';
38
+ fn();
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Wrapper around `fs.watch` that provides a workaround
44
+ * for https://github.com/nodejs/node/issues/5039.
45
+ */
46
+ function watchFile(filepath, callback) {
47
+ const directory = path__default.default.dirname(filepath);
48
+ const filename = path__default.default.basename(filepath);
49
+ return fs__default.default.watch(directory, {
50
+ persistent: false,
51
+ recursive: false
52
+ }, (event, changedFilename) => {
53
+ if (changedFilename === filename) {
54
+ callback();
55
+ }
56
+ });
57
+ }
58
+
59
+ const runOnce$1 = once('_NEXT_INTL_COMPILE_MESSAGES');
60
+ function createMessagesDeclaration(messagesPaths) {
61
+ // Instead of running _only_ in certain cases, it's
62
+ // safer to _avoid_ running for certain known cases.
63
+ // https://github.com/amannn/next-intl/issues/2006
64
+ const shouldBailOut = ['info', 'start'
65
+
66
+ // Note: These commands don't consult the
67
+ // Next.js config, so we can't detect them here.
68
+ // - telemetry
69
+ // - lint
70
+ //
71
+ // What remains are:
72
+ // - dev
73
+ // - build
74
+ // - typegen
75
+ ].some(arg => process.argv.includes(arg));
76
+ if (shouldBailOut) {
77
+ return;
78
+ }
79
+ runOnce$1(() => {
80
+ for (const messagesPath of messagesPaths) {
81
+ const fullPath = path__default.default.resolve(messagesPath);
82
+ if (!fs__default.default.existsSync(fullPath)) {
83
+ throwError(`\`createMessagesDeclaration\` points to a non-existent file: ${fullPath}`);
84
+ }
85
+ if (!fullPath.endsWith('.json')) {
86
+ throwError(`\`createMessagesDeclaration\` needs to point to a JSON file. Received: ${fullPath}`);
87
+ }
88
+
89
+ // Keep this as a runtime check and don't replace
90
+ // this with a constant during the build process
91
+ const env = process.env['NODE_ENV'.trim()];
92
+ compileDeclaration(messagesPath);
93
+ if (env === 'development') {
94
+ startWatching(messagesPath);
95
+ }
96
+ }
97
+ });
98
+ }
99
+ function startWatching(messagesPath) {
100
+ const watcher = watchFile(messagesPath, () => {
101
+ compileDeclaration(messagesPath, true);
102
+ });
103
+ process.on('exit', () => {
104
+ watcher.close();
105
+ });
106
+ }
107
+ function compileDeclaration(messagesPath, async = false) {
108
+ const declarationPath = messagesPath.replace(/\.json$/, '.d.json.ts');
109
+ function createDeclaration(content) {
110
+ return `// This file is auto-generated by next-intl, do not edit directly.
111
+ // See: https://next-intl.dev/docs/workflows/typescript#messages-arguments
112
+
113
+ declare const messages: ${content.trim()};
114
+ export default messages;`;
115
+ }
116
+ if (async) {
117
+ return fs__default.default.promises.readFile(messagesPath, 'utf-8').then(content => fs__default.default.promises.writeFile(declarationPath, createDeclaration(content)));
118
+ }
119
+ const content = fs__default.default.readFileSync(messagesPath, 'utf-8');
120
+ fs__default.default.writeFileSync(declarationPath, createDeclaration(content));
121
+ }
122
+
123
+ const formats = {
124
+ json: {
125
+ codec: () => Promise.resolve().then(function () { return require('./JSONCodec-L1_VeQBi.cjs'); }),
126
+ extension: '.json'
127
+ },
128
+ po: {
129
+ codec: () => Promise.resolve().then(function () { return require('./POCodec-Be_UL6jy.cjs'); }),
130
+ extension: '.po'
131
+ }
132
+ };
133
+ function isBuiltInFormat(format) {
134
+ return typeof format === 'string' && format in formats;
135
+ }
136
+ function getFormatExtension(format) {
137
+ if (isBuiltInFormat(format)) {
138
+ return formats[format].extension;
139
+ } else {
140
+ return format.extension;
141
+ }
142
+ }
143
+ async function resolveCodec(format, projectRoot) {
144
+ if (isBuiltInFormat(format)) {
145
+ const factory = (await formats[format].codec()).default;
146
+ return factory();
147
+ } else {
148
+ const resolvedPath = path__default.default.isAbsolute(format.codec) ? format.codec : path__default.default.resolve(projectRoot, format.codec);
149
+ let module;
150
+ try {
151
+ module = await import(resolvedPath);
152
+ } catch (error) {
153
+ throwError(`Could not load codec from "${resolvedPath}".\n${error}`);
154
+ }
155
+ const factory = module.default;
156
+ if (!factory || typeof factory !== 'function') {
157
+ throwError(`Codec at "${resolvedPath}" must have a default export returned from \`defineCodec\`.`);
158
+ }
159
+ return factory();
160
+ }
161
+ }
162
+
163
+ class SourceFileFilter {
164
+ static EXTENSIONS = ['ts', 'tsx', 'js', 'jsx'];
165
+
166
+ // Will not be entered, except if explicitly asked for
167
+ // TODO: At some point we should infer these from .gitignore
168
+ static IGNORED_DIRECTORIES = ['node_modules', '.next', '.git'];
169
+ static isSourceFile(filePath) {
170
+ const ext = path__default.default.extname(filePath);
171
+ return SourceFileFilter.EXTENSIONS.map(cur => '.' + cur).includes(ext);
172
+ }
173
+ static shouldEnterDirectory(dirPath, srcPaths) {
174
+ const dirName = path__default.default.basename(dirPath);
175
+ if (SourceFileFilter.IGNORED_DIRECTORIES.includes(dirName)) {
176
+ return SourceFileFilter.isIgnoredDirectoryExplicitlyIncluded(dirPath, srcPaths);
177
+ }
178
+ return true;
179
+ }
180
+ static isIgnoredDirectoryExplicitlyIncluded(ignoredDirPath, srcPaths) {
181
+ return srcPaths.some(srcPath => SourceFileFilter.isWithinPath(srcPath, ignoredDirPath));
182
+ }
183
+ static isWithinPath(targetPath, basePath) {
184
+ const relativePath = path__default.default.relative(basePath, targetPath);
185
+ return relativePath === '' || !relativePath.startsWith('..');
186
+ }
187
+ }
188
+
189
+ class SourceFileScanner {
190
+ static async walkSourceFiles(dir, srcPaths, acc = []) {
191
+ const entries = await fs__default$1.default.readdir(dir, {
192
+ withFileTypes: true
193
+ });
194
+ for (const entry of entries) {
195
+ const entryPath = path__default.default.join(dir, entry.name);
196
+ if (entry.isDirectory()) {
197
+ if (!SourceFileFilter.shouldEnterDirectory(entryPath, srcPaths)) {
198
+ continue;
199
+ }
200
+ await SourceFileScanner.walkSourceFiles(entryPath, srcPaths, acc);
201
+ } else {
202
+ if (SourceFileFilter.isSourceFile(entry.name)) {
203
+ acc.push(entryPath);
204
+ }
205
+ }
206
+ }
207
+ return acc;
208
+ }
209
+ static async getSourceFiles(srcPaths) {
210
+ const files = (await Promise.all(srcPaths.map(srcPath => SourceFileScanner.walkSourceFiles(srcPath, srcPaths)))).flat();
211
+ return new Set(files);
212
+ }
213
+ }
214
+
215
+ class SourceFileWatcher {
216
+ subscriptions = [];
217
+ constructor(roots, onChange) {
218
+ this.roots = roots;
219
+ this.onChange = onChange;
220
+ }
221
+ async start() {
222
+ if (this.subscriptions.length > 0) {
223
+ return;
224
+ }
225
+ const ignore = SourceFileFilter.IGNORED_DIRECTORIES.map(dir => `**/${dir}/**`);
226
+ for (const root of this.roots) {
227
+ const sub = await watcher.subscribe(root, async (err, events) => {
228
+ if (err) {
229
+ console.error(err);
230
+ return;
231
+ }
232
+ const filtered = await this.normalizeEvents(events);
233
+ if (filtered.length > 0) {
234
+ void this.onChange(filtered);
235
+ }
236
+ }, {
237
+ ignore
238
+ });
239
+ this.subscriptions.push(sub);
240
+ }
241
+ }
242
+ async normalizeEvents(events) {
243
+ const directoryCreatePaths = [];
244
+ const otherEvents = [];
245
+
246
+ // We need to expand directory creates because during rename operations,
247
+ // @parcel/watcher emits a directory create event but may not emit individual
248
+ // file events for the moved files
249
+ await Promise.all(events.map(async event => {
250
+ if (event.type === 'create') {
251
+ try {
252
+ const stats = await fs__default$1.default.stat(event.path);
253
+ if (stats.isDirectory()) {
254
+ directoryCreatePaths.push(event.path);
255
+ return;
256
+ }
257
+ } catch {
258
+ // Path doesn't exist or is inaccessible, treat as file
259
+ }
260
+ }
261
+ otherEvents.push(event);
262
+ }));
263
+
264
+ // Expand directory create events to find source files inside
265
+ let expandedCreateEvents = [];
266
+ if (directoryCreatePaths.length > 0) {
267
+ try {
268
+ const sourceFiles = await SourceFileScanner.getSourceFiles(directoryCreatePaths);
269
+ expandedCreateEvents = Array.from(sourceFiles).map(filePath => ({
270
+ type: 'create',
271
+ path: filePath
272
+ }));
273
+ } catch {
274
+ // Directories might have been deleted or are inaccessible
275
+ }
276
+ }
277
+
278
+ // Combine original events with expanded directory creates.
279
+ // Deduplicate by path to avoid processing the same file twice
280
+ // in case @parcel/watcher also emitted individual file events.
281
+ const allEvents = [...otherEvents, ...expandedCreateEvents];
282
+ const seenPaths = new Set();
283
+ const deduplicated = [];
284
+ for (const event of allEvents) {
285
+ const key = `${event.type}:${event.path}`;
286
+ if (!seenPaths.has(key)) {
287
+ seenPaths.add(key);
288
+ deduplicated.push(event);
289
+ }
290
+ }
291
+ return deduplicated.filter(event => {
292
+ // Keep all delete events (might be deleted directories that no longer exist)
293
+ if (event.type === 'delete') {
294
+ return true;
295
+ }
296
+ // Keep source files
297
+ return SourceFileFilter.isSourceFile(event.path);
298
+ });
299
+ }
300
+ async expandDirectoryDeleteEvents(events, prevKnownFiles) {
301
+ const expanded = [];
302
+ for (const event of events) {
303
+ if (event.type === 'delete' && !SourceFileFilter.isSourceFile(event.path)) {
304
+ const dirPath = path__default.default.resolve(event.path);
305
+ const filesInDirectory = [];
306
+ for (const filePath of prevKnownFiles) {
307
+ if (SourceFileFilter.isWithinPath(filePath, dirPath)) {
308
+ filesInDirectory.push(filePath);
309
+ }
310
+ }
311
+
312
+ // If we found files within this path, it was a directory
313
+ if (filesInDirectory.length > 0) {
314
+ for (const filePath of filesInDirectory) {
315
+ expanded.push({
316
+ type: 'delete',
317
+ path: filePath
318
+ });
319
+ }
320
+ } else {
321
+ // Not a directory or no files in it, pass through as-is
322
+ expanded.push(event);
323
+ }
324
+ } else {
325
+ // Pass through as-is
326
+ expanded.push(event);
327
+ }
328
+ }
329
+ return expanded;
330
+ }
331
+ async stop() {
332
+ await Promise.all(this.subscriptions.map(sub => sub.unsubscribe()));
333
+ this.subscriptions = [];
334
+ }
335
+ [Symbol.dispose]() {
336
+ void this.stop();
337
+ }
338
+ }
339
+
340
+ // Essentialls lodash/set, but we avoid this dependency
341
+ function setNestedProperty(obj, keyPath, value) {
342
+ const keys = keyPath.split('.');
343
+ let current = obj;
344
+ for (let i = 0; i < keys.length - 1; i++) {
345
+ const key = keys[i];
346
+ if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
347
+ current[key] = {};
348
+ }
349
+ current = current[key];
350
+ }
351
+ current[keys[keys.length - 1]] = value;
352
+ }
353
+ function getSortedMessages(messages) {
354
+ return messages.toSorted((messageA, messageB) => {
355
+ const pathA = messageA.references?.[0]?.path ?? '';
356
+ const pathB = messageB.references?.[0]?.path ?? '';
357
+ if (pathA === pathB) {
358
+ return localeCompare(messageA.id, messageB.id);
359
+ } else {
360
+ return localeCompare(pathA, pathB);
361
+ }
362
+ });
363
+ }
364
+ function localeCompare(a, b) {
365
+ return a.localeCompare(b, 'en');
366
+ }
367
+ function getDefaultProjectRoot() {
368
+ return process.cwd();
369
+ }
370
+
371
+ class CatalogLocales {
372
+ onChangeCallbacks = (() => new Set())();
373
+ constructor(params) {
374
+ this.messagesDir = params.messagesDir;
375
+ this.sourceLocale = params.sourceLocale;
376
+ this.extension = params.extension;
377
+ this.locales = params.locales;
378
+ }
379
+ async getTargetLocales() {
380
+ if (this.targetLocales) {
381
+ return this.targetLocales;
382
+ }
383
+ if (this.locales === 'infer') {
384
+ this.targetLocales = await this.readTargetLocales();
385
+ } else {
386
+ this.targetLocales = this.locales.filter(locale => locale !== this.sourceLocale);
387
+ }
388
+ return this.targetLocales;
389
+ }
390
+ async readTargetLocales() {
391
+ try {
392
+ const files = await fs__default$1.default.readdir(this.messagesDir);
393
+ return files.filter(file => file.endsWith(this.extension)).map(file => path__default.default.basename(file, this.extension)).filter(locale => locale !== this.sourceLocale);
394
+ } catch {
395
+ return [];
396
+ }
397
+ }
398
+ subscribeLocalesChange(callback) {
399
+ this.onChangeCallbacks.add(callback);
400
+ if (this.locales === 'infer' && !this.watcher) {
401
+ void this.startWatcher();
402
+ }
403
+ }
404
+ unsubscribeLocalesChange(callback) {
405
+ this.onChangeCallbacks.delete(callback);
406
+ if (this.onChangeCallbacks.size === 0) {
407
+ this.stopWatcher();
408
+ }
409
+ }
410
+ async startWatcher() {
411
+ if (this.watcher) {
412
+ return;
413
+ }
414
+ await fs__default$1.default.mkdir(this.messagesDir, {
415
+ recursive: true
416
+ });
417
+ this.watcher = fs__default.default.watch(this.messagesDir, {
418
+ persistent: false,
419
+ recursive: false
420
+ }, (event, filename) => {
421
+ const isCatalogFile = filename != null && filename.endsWith(this.extension) && !filename.includes(path__default.default.sep);
422
+ if (isCatalogFile) {
423
+ void this.onChange();
424
+ }
425
+ });
426
+ }
427
+ stopWatcher() {
428
+ if (this.watcher) {
429
+ this.watcher.close();
430
+ this.watcher = undefined;
431
+ }
432
+ }
433
+ async onChange() {
434
+ const oldLocales = new Set(this.targetLocales || []);
435
+ this.targetLocales = await this.readTargetLocales();
436
+ const newLocalesSet = new Set(this.targetLocales);
437
+ const added = this.targetLocales.filter(locale => !oldLocales.has(locale));
438
+ const removed = Array.from(oldLocales).filter(locale => !newLocalesSet.has(locale));
439
+ if (added.length > 0 || removed.length > 0) {
440
+ for (const callback of this.onChangeCallbacks) {
441
+ callback({
442
+ added,
443
+ removed
444
+ });
445
+ }
446
+ }
447
+ }
448
+ }
449
+
450
+ class CatalogPersister {
451
+ constructor(params) {
452
+ this.messagesPath = params.messagesPath;
453
+ this.codec = params.codec;
454
+ this.extension = params.extension;
455
+ }
456
+ getFileName(locale) {
457
+ return locale + this.extension;
458
+ }
459
+ getFilePath(locale) {
460
+ return path__default.default.join(this.messagesPath, this.getFileName(locale));
461
+ }
462
+ async read(locale) {
463
+ const filePath = this.getFilePath(locale);
464
+ let content;
465
+ try {
466
+ content = await fs__default$1.default.readFile(filePath, 'utf8');
467
+ } catch (error) {
468
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
469
+ return [];
470
+ }
471
+ throw new Error(`Error while reading ${this.getFileName(locale)}:\n> ${error}`, {
472
+ cause: error
473
+ });
474
+ }
475
+ try {
476
+ return this.codec.decode(content, {
477
+ locale
478
+ });
479
+ } catch (error) {
480
+ throw new Error(`Error while decoding ${this.getFileName(locale)}:\n> ${error}`, {
481
+ cause: error
482
+ });
483
+ }
484
+ }
485
+ async write(messages, context) {
486
+ const filePath = this.getFilePath(context.locale);
487
+ const content = this.codec.encode(messages, context);
488
+ try {
489
+ const outputDir = path__default.default.dirname(filePath);
490
+ await fs__default$1.default.mkdir(outputDir, {
491
+ recursive: true
492
+ });
493
+ await fs__default$1.default.writeFile(filePath, content);
494
+ } catch (error) {
495
+ console.error(`❌ Failed to write catalog: ${error}`);
496
+ }
497
+ }
498
+ async getLastModified(locale) {
499
+ const filePath = this.getFilePath(locale);
500
+ try {
501
+ const stats = await fs__default$1.default.stat(filePath);
502
+ return stats.mtime;
503
+ } catch {
504
+ return undefined;
505
+ }
506
+ }
507
+ }
508
+
509
+ /**
510
+ * De-duplicates excessive save invocations,
511
+ * while keeping a single one instant.
512
+ */
513
+ class SaveScheduler {
514
+ isSaving = false;
515
+ pendingResolvers = [];
516
+ constructor(delayMs = 50) {
517
+ this.delayMs = delayMs;
518
+ }
519
+ async schedule(saveTask) {
520
+ return new Promise((resolve, reject) => {
521
+ this.pendingResolvers.push({
522
+ resolve,
523
+ reject
524
+ });
525
+ this.nextSaveTask = saveTask;
526
+ if (!this.isSaving && !this.saveTimeout) {
527
+ // Not currently saving and no scheduled save, save immediately
528
+ this.executeSave();
529
+ } else if (this.saveTimeout) {
530
+ // A save is already scheduled, reschedule to debounce
531
+ this.scheduleSave();
532
+ }
533
+ // If isSaving is true and no timeout is scheduled, the current save
534
+ // will check for pending resolvers when it completes and schedule
535
+ // another save if needed (see finally block in executeSave)
536
+ });
537
+ }
538
+ scheduleSave() {
539
+ if (this.saveTimeout) {
540
+ clearTimeout(this.saveTimeout);
541
+ }
542
+ this.saveTimeout = setTimeout(() => {
543
+ this.saveTimeout = undefined;
544
+ this.executeSave();
545
+ }, this.delayMs);
546
+ }
547
+ async executeSave() {
548
+ if (this.isSaving) {
549
+ return;
550
+ }
551
+ const saveTask = this.nextSaveTask;
552
+ if (!saveTask) {
553
+ return;
554
+ }
555
+
556
+ // Capture current pending resolvers for this save
557
+ const resolversForThisSave = this.pendingResolvers;
558
+ this.pendingResolvers = [];
559
+ this.nextSaveTask = undefined;
560
+ this.isSaving = true;
561
+ try {
562
+ const result = await saveTask();
563
+
564
+ // Resolve only the promises that were pending when this save started
565
+ resolversForThisSave.forEach(({
566
+ resolve
567
+ }) => resolve(result));
568
+ } catch (error) {
569
+ // Reject only the promises that were pending when this save started
570
+ resolversForThisSave.forEach(({
571
+ reject
572
+ }) => reject(error));
573
+ } finally {
574
+ this.isSaving = false;
575
+
576
+ // If new saves were requested during this save, schedule another
577
+ if (this.pendingResolvers.length > 0) {
578
+ this.scheduleSave();
579
+ }
580
+ }
581
+ }
582
+ [Symbol.dispose]() {
583
+ if (this.saveTimeout) {
584
+ clearTimeout(this.saveTimeout);
585
+ this.saveTimeout = undefined;
586
+ }
587
+ this.pendingResolvers = [];
588
+ this.nextSaveTask = undefined;
589
+ this.isSaving = false;
590
+ }
591
+ }
592
+
593
+ class CatalogManager {
594
+ /**
595
+ * The source of truth for which messages are used.
596
+ * NOTE: Should be mutated in place to keep `messagesById` and `messagesByFile` in sync.
597
+ */
598
+ messagesByFile = (() => new Map())();
599
+
600
+ /**
601
+ * Fast lookup for messages by ID across all files,
602
+ * contains the same messages as `messagesByFile`.
603
+ * NOTE: Should be mutated in place to keep `messagesById` and `messagesByFile` in sync.
604
+ */
605
+ messagesById = (() => new Map())();
606
+
607
+ /**
608
+ * This potentially also includes outdated ones that were initially available,
609
+ * but are not used anymore. This allows to restore them if they are used again.
610
+ **/
611
+ translationsByTargetLocale = (() => new Map())();
612
+ lastWriteByLocale = (() => new Map())();
613
+
614
+ // Cached instances
615
+
616
+ // Resolves when all catalogs are loaded
617
+
618
+ // Resolves when the initial project scan and processing is complete
619
+
620
+ constructor(config, opts) {
621
+ this.config = config;
622
+ this.saveScheduler = new SaveScheduler(50);
623
+ this.projectRoot = opts.projectRoot ?? getDefaultProjectRoot();
624
+ this.isDevelopment = opts.isDevelopment ?? false;
625
+ this.extractor = opts.extractor;
626
+ if (this.isDevelopment) {
627
+ // We kick this off as early as possible, so we get notified about changes
628
+ // that happen during the initial project scan (while awaiting it to
629
+ // complete though)
630
+ this.sourceWatcher = new SourceFileWatcher(this.getSrcPaths(), this.handleFileEvents.bind(this));
631
+ void this.sourceWatcher.start();
632
+ }
633
+ }
634
+ async getCodec() {
635
+ if (!this.codec) {
636
+ this.codec = await resolveCodec(this.config.messages.format, this.projectRoot);
637
+ }
638
+ return this.codec;
639
+ }
640
+ async getPersister() {
641
+ if (this.persister) {
642
+ return this.persister;
643
+ } else {
644
+ this.persister = new CatalogPersister({
645
+ messagesPath: this.config.messages.path,
646
+ codec: await this.getCodec(),
647
+ extension: getFormatExtension(this.config.messages.format)
648
+ });
649
+ return this.persister;
650
+ }
651
+ }
652
+ getCatalogLocales() {
653
+ if (this.catalogLocales) {
654
+ return this.catalogLocales;
655
+ } else {
656
+ const messagesDir = path__default.default.join(this.projectRoot, this.config.messages.path);
657
+ this.catalogLocales = new CatalogLocales({
658
+ messagesDir,
659
+ sourceLocale: this.config.sourceLocale,
660
+ extension: getFormatExtension(this.config.messages.format),
661
+ locales: this.config.messages.locales
662
+ });
663
+ return this.catalogLocales;
664
+ }
665
+ }
666
+ async getTargetLocales() {
667
+ return this.getCatalogLocales().getTargetLocales();
668
+ }
669
+ getSrcPaths() {
670
+ return (Array.isArray(this.config.srcPath) ? this.config.srcPath : [this.config.srcPath]).map(srcPath => path__default.default.join(this.projectRoot, srcPath));
671
+ }
672
+ async loadMessages() {
673
+ const sourceDiskMessages = await this.loadSourceMessages();
674
+ this.loadCatalogsPromise = this.loadTargetMessages();
675
+ await this.loadCatalogsPromise;
676
+ this.scanCompletePromise = (async () => {
677
+ const sourceFiles = await SourceFileScanner.getSourceFiles(this.getSrcPaths());
678
+ await Promise.all(Array.from(sourceFiles).map(async filePath => this.processFile(filePath)));
679
+ this.mergeSourceDiskMetadata(sourceDiskMessages);
680
+ })();
681
+ await this.scanCompletePromise;
682
+ if (this.isDevelopment) {
683
+ const catalogLocales = this.getCatalogLocales();
684
+ catalogLocales.subscribeLocalesChange(this.onLocalesChange);
685
+ }
686
+ }
687
+ async loadSourceMessages() {
688
+ // Load source catalog to hydrate metadata (e.g. flags) later without
689
+ // treating catalog entries as source of truth.
690
+ const diskMessages = await this.loadLocaleMessages(this.config.sourceLocale);
691
+ const byId = new Map();
692
+ for (const diskMessage of diskMessages) {
693
+ byId.set(diskMessage.id, diskMessage);
694
+ }
695
+ return byId;
696
+ }
697
+ async loadLocaleMessages(locale) {
698
+ const persister = await this.getPersister();
699
+ const messages = await persister.read(locale);
700
+ const fileTime = await persister.getLastModified(locale);
701
+ this.lastWriteByLocale.set(locale, fileTime);
702
+ return messages;
703
+ }
704
+ async loadTargetMessages() {
705
+ const targetLocales = await this.getTargetLocales();
706
+ await Promise.all(targetLocales.map(locale => this.reloadLocaleCatalog(locale)));
707
+ }
708
+ async reloadLocaleCatalog(locale) {
709
+ const diskMessages = await this.loadLocaleMessages(locale);
710
+ if (locale === this.config.sourceLocale) {
711
+ // For source: Merge additional properties like flags
712
+ for (const diskMessage of diskMessages) {
713
+ const prev = this.messagesById.get(diskMessage.id);
714
+ if (prev) {
715
+ // Mutate the existing object instead of creating a copy
716
+ // to keep messagesById and messagesByFile in sync.
717
+ // Unknown properties (like flags): disk wins
718
+ // Known properties: existing (from extraction) wins
719
+ for (const key of Object.keys(diskMessage)) {
720
+ if (!['id', 'message', 'description', 'references'].includes(key)) {
721
+ // For unknown properties (like flags), disk wins
722
+ prev[key] = diskMessage[key];
723
+ }
724
+ }
725
+ }
726
+ }
727
+ } else {
728
+ // For target: disk wins completely, BUT preserve existing translations
729
+ // if we read empty (likely a write in progress by an external tool
730
+ // that causes the file to temporarily be empty)
731
+ const existingTranslations = this.translationsByTargetLocale.get(locale);
732
+ const hasExistingTranslations = existingTranslations && existingTranslations.size > 0;
733
+ if (diskMessages.length > 0) {
734
+ // We got content from disk, replace with it
735
+ const translations = new Map();
736
+ for (const message of diskMessages) {
737
+ translations.set(message.id, message);
738
+ }
739
+ this.translationsByTargetLocale.set(locale, translations);
740
+ } else if (hasExistingTranslations) ; else {
741
+ // We read empty and have no existing translations
742
+ const translations = new Map();
743
+ this.translationsByTargetLocale.set(locale, translations);
744
+ }
745
+ }
746
+ }
747
+ mergeSourceDiskMetadata(diskMessages) {
748
+ for (const [id, diskMessage] of diskMessages) {
749
+ const existing = this.messagesById.get(id);
750
+ if (!existing) continue;
751
+
752
+ // Mutate the existing object instead of creating a copy.
753
+ // This keeps `messagesById` and `messagesByFile` in sync since
754
+ // they reference the same object instance.
755
+ for (const key of Object.keys(diskMessage)) {
756
+ if (existing[key] == null) {
757
+ existing[key] = diskMessage[key];
758
+ }
759
+ }
760
+ }
761
+ }
762
+ async processFile(absoluteFilePath) {
763
+ let messages = [];
764
+ try {
765
+ const content = await fs__default$1.default.readFile(absoluteFilePath, 'utf8');
766
+ let extraction;
767
+ try {
768
+ extraction = await this.extractor.extract(absoluteFilePath, content);
769
+ } catch {
770
+ return false;
771
+ }
772
+ messages = extraction.messages;
773
+ } catch (err) {
774
+ if (err.code !== 'ENOENT') {
775
+ throw err;
776
+ }
777
+ // ENOENT -> treat as no messages
778
+ }
779
+ const prevFileMessages = this.messagesByFile.get(absoluteFilePath);
780
+
781
+ // Init with all previous ones
782
+ const idsToRemove = Array.from(prevFileMessages?.keys() ?? []);
783
+
784
+ // Replace existing messages with new ones
785
+ const fileMessages = new Map();
786
+ for (let message of messages) {
787
+ const prevMessage = this.messagesById.get(message.id);
788
+
789
+ // Merge with previous message if it exists
790
+ if (prevMessage) {
791
+ const validated = prevMessage.references ?? [];
792
+ message = {
793
+ ...message,
794
+ references: this.mergeReferences(validated, {
795
+ path: path__default.default.relative(this.projectRoot, absoluteFilePath)
796
+ })
797
+ };
798
+
799
+ // Merge other properties like description, or unknown
800
+ // attributes like flags that are opaque to us
801
+ for (const key of Object.keys(prevMessage)) {
802
+ if (message[key] == null) {
803
+ message[key] = prevMessage[key];
804
+ }
805
+ }
806
+ }
807
+ this.messagesById.set(message.id, message);
808
+ fileMessages.set(message.id, message);
809
+
810
+ // This message continues to exist in this file
811
+ const index = idsToRemove.indexOf(message.id);
812
+ if (index !== -1) idsToRemove.splice(index, 1);
813
+ }
814
+ const relativeFilePath = path__default.default.relative(this.projectRoot, absoluteFilePath);
815
+
816
+ // Clean up removed messages from `messagesById`
817
+ idsToRemove.forEach(id => {
818
+ const message = this.messagesById.get(id);
819
+ if (!message) return;
820
+ const hasOtherReferences = message.references?.some(ref => ref.path !== relativeFilePath);
821
+ if (!hasOtherReferences) {
822
+ // No other references, delete the message entirely
823
+ this.messagesById.delete(id);
824
+ } else {
825
+ // Message is used elsewhere, remove this file from references
826
+ // Mutate the existing object to keep `messagesById` and `messagesByFile` in sync
827
+ message.references = message.references?.filter(ref => ref.path !== relativeFilePath);
828
+ }
829
+ });
830
+
831
+ // Update the stored messages
832
+ if (messages.length > 0) {
833
+ this.messagesByFile.set(absoluteFilePath, fileMessages);
834
+ } else {
835
+ this.messagesByFile.delete(absoluteFilePath);
836
+ }
837
+ const changed = this.haveMessagesChangedForFile(prevFileMessages, fileMessages);
838
+ return changed;
839
+ }
840
+ mergeReferences(existing, current) {
841
+ const dedup = new Map();
842
+ for (const ref of existing) {
843
+ dedup.set(ref.path, ref);
844
+ }
845
+ dedup.set(current.path, current);
846
+ return Array.from(dedup.values()).sort((a, b) => localeCompare(a.path, b.path));
847
+ }
848
+ haveMessagesChangedForFile(beforeMessages, afterMessages) {
849
+ // If one exists and the other doesn't, there's a change
850
+ if (!beforeMessages) {
851
+ return afterMessages.size > 0;
852
+ }
853
+
854
+ // Different sizes means changes
855
+ if (beforeMessages.size !== afterMessages.size) {
856
+ return true;
857
+ }
858
+
859
+ // Check differences in beforeMessages vs afterMessages
860
+ for (const [id, msg1] of beforeMessages) {
861
+ const msg2 = afterMessages.get(id);
862
+ if (!msg2 || !this.areMessagesEqual(msg1, msg2)) {
863
+ return true; // Early exit on first difference
864
+ }
865
+ }
866
+ return false;
867
+ }
868
+ areMessagesEqual(msg1, msg2) {
869
+ // Note: We intentionally don't compare references here.
870
+ // References are aggregated metadata from multiple files and comparing
871
+ // them would cause false positives due to parallel extraction order.
872
+ return msg1.id === msg2.id && msg1.message === msg2.message && msg1.description === msg2.description;
873
+ }
874
+ async save() {
875
+ return this.saveScheduler.schedule(() => this.saveImpl());
876
+ }
877
+ async saveImpl() {
878
+ await this.saveLocale(this.config.sourceLocale);
879
+ const targetLocales = await this.getTargetLocales();
880
+ await Promise.all(targetLocales.map(locale => this.saveLocale(locale)));
881
+ }
882
+ async saveLocale(locale) {
883
+ await this.loadCatalogsPromise;
884
+ const messages = Array.from(this.messagesById.values());
885
+ const persister = await this.getPersister();
886
+ const isSourceLocale = locale === this.config.sourceLocale;
887
+
888
+ // Check if file was modified externally (poll-at-save is cheaper than
889
+ // watchers here since stat() is fast and avoids continuous overhead)
890
+ const lastWriteTime = this.lastWriteByLocale.get(locale);
891
+ const currentFileTime = await persister.getLastModified(locale);
892
+ if (currentFileTime && lastWriteTime && currentFileTime > lastWriteTime) {
893
+ await this.reloadLocaleCatalog(locale);
894
+ }
895
+ const localeMessages = isSourceLocale ? this.messagesById : this.translationsByTargetLocale.get(locale);
896
+ const messagesToPersist = messages.map(message => {
897
+ const localeMessage = localeMessages?.get(message.id);
898
+ return {
899
+ ...localeMessage,
900
+ id: message.id,
901
+ description: message.description,
902
+ references: message.references,
903
+ message: isSourceLocale ? message.message : localeMessage?.message ?? ''
904
+ };
905
+ });
906
+ await persister.write(messagesToPersist, {
907
+ locale,
908
+ sourceMessagesById: this.messagesById
909
+ });
910
+
911
+ // Update timestamps
912
+ const newTime = await persister.getLastModified(locale);
913
+ this.lastWriteByLocale.set(locale, newTime);
914
+ }
915
+ onLocalesChange = async params => {
916
+ // Chain to existing promise
917
+ this.loadCatalogsPromise = Promise.all([this.loadCatalogsPromise, ...params.added.map(locale => this.reloadLocaleCatalog(locale))]);
918
+ for (const locale of params.added) {
919
+ await this.saveLocale(locale);
920
+ }
921
+ for (const locale of params.removed) {
922
+ this.translationsByTargetLocale.delete(locale);
923
+ this.lastWriteByLocale.delete(locale);
924
+ }
925
+ };
926
+ async handleFileEvents(events) {
927
+ if (this.loadCatalogsPromise) {
928
+ await this.loadCatalogsPromise;
929
+ }
930
+
931
+ // Wait for initial scan to complete to avoid race conditions
932
+ if (this.scanCompletePromise) {
933
+ await this.scanCompletePromise;
934
+ }
935
+ let changed = false;
936
+ const expandedEvents = await this.sourceWatcher.expandDirectoryDeleteEvents(events, Array.from(this.messagesByFile.keys()));
937
+ for (const event of expandedEvents) {
938
+ const hasChanged = await this.processFile(event.path);
939
+ changed ||= hasChanged;
940
+ }
941
+ if (changed) {
942
+ await this.save();
943
+ }
944
+ }
945
+ [Symbol.dispose]() {
946
+ this.sourceWatcher?.stop();
947
+ this.sourceWatcher = undefined;
948
+ this.saveScheduler[Symbol.dispose]();
949
+ if (this.catalogLocales && this.isDevelopment) {
950
+ this.catalogLocales.unsubscribeLocalesChange(this.onLocalesChange);
951
+ }
952
+ }
953
+ }
954
+
955
+ class LRUCache {
956
+ constructor(maxSize) {
957
+ this.maxSize = maxSize;
958
+ this.cache = new Map();
959
+ }
960
+ set(key, value) {
961
+ const isNewKey = !this.cache.has(key);
962
+ if (isNewKey && this.cache.size >= this.maxSize) {
963
+ const lruKey = this.cache.keys().next().value;
964
+ if (lruKey !== undefined) {
965
+ this.cache.delete(lruKey);
966
+ }
967
+ }
968
+ this.cache.set(key, {
969
+ key,
970
+ value
971
+ });
972
+ }
973
+ get(key) {
974
+ const item = this.cache.get(key);
975
+ if (item) {
976
+ this.cache.delete(key);
977
+ this.cache.set(key, item);
978
+ return item.value;
979
+ }
980
+ return undefined;
981
+ }
982
+ }
983
+
984
+ 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-DDtWCyPI.cjs', document.baseURI).href)));
985
+ class MessageExtractor {
986
+ compileCache = (() => new LRUCache(750))();
987
+ constructor(opts) {
988
+ this.isDevelopment = opts.isDevelopment ?? false;
989
+ this.projectRoot = opts.projectRoot ?? getDefaultProjectRoot();
990
+ this.sourceMap = opts.sourceMap ?? false;
991
+ }
992
+ async extract(absoluteFilePath, source) {
993
+ const cacheKey = [source, absoluteFilePath].join('!');
994
+ const cached = this.compileCache.get(cacheKey);
995
+ if (cached) return cached;
996
+
997
+ // Shortcut parsing if hook is not used. The Turbopack integration already
998
+ // pre-filters this, but for webpack this feature doesn't exist, so we need
999
+ // to do it here.
1000
+ if (!source.includes('useExtracted') && !source.includes('getExtracted')) {
1001
+ return {
1002
+ messages: [],
1003
+ code: source
1004
+ };
1005
+ }
1006
+ const filePath = path__default.default.relative(this.projectRoot, absoluteFilePath);
1007
+ const result = await core.transform(source, {
1008
+ jsc: {
1009
+ target: 'esnext',
1010
+ parser: {
1011
+ syntax: 'typescript',
1012
+ tsx: true,
1013
+ decorators: true
1014
+ },
1015
+ experimental: {
1016
+ cacheRoot: 'node_modules/.cache/swc',
1017
+ disableBuiltinTransformsForInternalTesting: true,
1018
+ disableAllLints: true,
1019
+ plugins: [[require$1.resolve('next-intl-swc-plugin-extractor'), {
1020
+ isDevelopment: this.isDevelopment,
1021
+ filePath
1022
+ }]]
1023
+ }
1024
+ },
1025
+ sourceMaps: this.sourceMap,
1026
+ sourceFileName: filePath,
1027
+ filename: filePath
1028
+ });
1029
+
1030
+ // TODO: Improve the typing of @swc/core
1031
+ const output = result.output;
1032
+ const messages = JSON.parse(JSON.parse(output).results);
1033
+ const extractionResult = {
1034
+ code: result.code,
1035
+ map: result.map,
1036
+ messages
1037
+ };
1038
+ this.compileCache.set(cacheKey, extractionResult);
1039
+ return extractionResult;
1040
+ }
1041
+ }
1042
+
1043
+ class ExtractionCompiler {
1044
+ constructor(config, opts = {}) {
1045
+ const extractor = opts.extractor ?? new MessageExtractor(opts);
1046
+ this.manager = new CatalogManager(config, {
1047
+ ...opts,
1048
+ extractor
1049
+ });
1050
+ this[Symbol.dispose] = this[Symbol.dispose].bind(this);
1051
+ this.installExitHandlers();
1052
+ }
1053
+ async extractAll() {
1054
+ // We can't rely on all files being compiled (e.g. due to persistent
1055
+ // caching), so loading the messages initially is necessary.
1056
+ await this.manager.loadMessages();
1057
+ await this.manager.save();
1058
+ }
1059
+ [Symbol.dispose]() {
1060
+ this.uninstallExitHandlers();
1061
+ this.manager[Symbol.dispose]();
1062
+ }
1063
+ installExitHandlers() {
1064
+ const cleanup = this[Symbol.dispose];
1065
+ process.on('exit', cleanup);
1066
+ process.on('SIGINT', cleanup);
1067
+ process.on('SIGTERM', cleanup);
1068
+ }
1069
+ uninstallExitHandlers() {
1070
+ const cleanup = this[Symbol.dispose];
1071
+ process.off('exit', cleanup);
1072
+ process.off('SIGINT', cleanup);
1073
+ process.off('SIGTERM', cleanup);
1074
+ }
1075
+ }
1076
+
1077
+ // Single compiler instance, initialized once per process
1078
+ let compiler;
1079
+ const runOnce = once('_NEXT_INTL_EXTRACT');
1080
+ function initExtractionCompiler(pluginConfig) {
1081
+ const experimental = pluginConfig.experimental;
1082
+ if (!experimental?.extract) {
1083
+ return;
1084
+ }
1085
+ runOnce(() => {
1086
+ // Avoid rollup's `replace` plugin to compile this away
1087
+ const isDevelopment = process.env['NODE_ENV'.trim()] === 'development';
1088
+ const extractorConfig = {
1089
+ srcPath: experimental.srcPath,
1090
+ sourceLocale: experimental.extract.sourceLocale,
1091
+ messages: experimental.messages
1092
+ };
1093
+ compiler = new ExtractionCompiler(extractorConfig, {
1094
+ isDevelopment,
1095
+ projectRoot: process.cwd()
1096
+ });
1097
+
1098
+ // Fire-and-forget: Start extraction, don't block config return.
1099
+ // In dev mode, this also starts the file watcher.
1100
+ // In prod, ideally we would wait until the extraction is complete,
1101
+ // but we can't `await` anywhere (at least for Turbopack).
1102
+ // The result is ok though, as if we encounter untranslated messages,
1103
+ // we'll simply add empty messages to the catalog. So for actually
1104
+ // running the app, there is no difference.
1105
+ compiler.extractAll();
1106
+ function cleanup() {
1107
+ if (compiler) {
1108
+ compiler[Symbol.dispose]();
1109
+ compiler = undefined;
1110
+ }
1111
+ }
1112
+ process.on('exit', cleanup);
1113
+ process.on('SIGINT', cleanup);
1114
+ process.on('SIGTERM', cleanup);
1115
+ });
1116
+ }
1117
+
1118
+ function getCurrentVersion() {
1119
+ try {
1120
+ 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-DDtWCyPI.cjs', document.baseURI).href)));
1121
+ const pkg = require$1('next/package.json');
1122
+ return pkg.version;
1123
+ } catch (error) {
1124
+ 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.', {
1125
+ cause: error
1126
+ });
1127
+ }
1128
+ }
1129
+ function compareVersions(version1, version2) {
1130
+ const v1Parts = version1.split('.').map(Number);
1131
+ const v2Parts = version2.split('.').map(Number);
1132
+ for (let i = 0; i < 3; i++) {
1133
+ const v1 = v1Parts[i] || 0;
1134
+ const v2 = v2Parts[i] || 0;
1135
+ if (v1 > v2) return 1;
1136
+ if (v1 < v2) return -1;
1137
+ }
1138
+ return 0;
1139
+ }
1140
+ function hasStableTurboConfig() {
1141
+ return compareVersions(getCurrentVersion(), '15.3.0') >= 0;
1142
+ }
1143
+ function isNextJs16OrHigher() {
1144
+ return compareVersions(getCurrentVersion(), '16.0.0') >= 0;
1145
+ }
1146
+
1147
+ function withExtensions(localPath) {
1148
+ return [`${localPath}.ts`, `${localPath}.tsx`, `${localPath}.js`, `${localPath}.jsx`];
1149
+ }
1150
+ function resolveI18nPath(providedPath, cwd) {
1151
+ function resolvePath(pathname) {
1152
+ const parts = [];
1153
+ if (cwd) parts.push(cwd);
1154
+ parts.push(pathname);
1155
+ return path__default.default.resolve(...parts);
1156
+ }
1157
+ function pathExists(pathname) {
1158
+ return fs__default.default.existsSync(resolvePath(pathname));
1159
+ }
1160
+ if (providedPath) {
1161
+ if (!pathExists(providedPath)) {
1162
+ throwError(`Could not find i18n config at ${providedPath}, please provide a valid path.`);
1163
+ }
1164
+ return providedPath;
1165
+ } else {
1166
+ for (const candidate of [...withExtensions('./i18n/request'), ...withExtensions('./src/i18n/request')]) {
1167
+ if (pathExists(candidate)) {
1168
+ return candidate;
1169
+ }
1170
+ }
1171
+ 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(
1172
+
1173
+ Alternatively, you can specify a custom location in your Next.js config:
1174
+
1175
+ const withNextIntl = createNextIntlPlugin(
1176
+ './path/to/i18n/request.tsx'
1177
+ );`);
1178
+ }
1179
+ }
1180
+ function getNextConfig(pluginConfig, nextConfig) {
1181
+ const useTurbo = process.env.TURBOPACK != null;
1182
+ const nextIntlConfig = {};
1183
+ function getExtractMessagesLoaderConfig() {
1184
+ const experimental = pluginConfig.experimental;
1185
+ if (!experimental.srcPath || !pluginConfig.experimental?.messages) {
1186
+ throwError('`srcPath` and `messages` are required when using `extractor`.');
1187
+ }
1188
+ return {
1189
+ loader: 'next-intl/extractor/extractionLoader',
1190
+ options: {
1191
+ srcPath: experimental.srcPath,
1192
+ sourceLocale: experimental.extract.sourceLocale,
1193
+ messages: pluginConfig.experimental.messages
1194
+ }
1195
+ };
1196
+ }
1197
+ function getCatalogLoaderConfig() {
1198
+ return {
1199
+ loader: 'next-intl/extractor/catalogLoader',
1200
+ options: {
1201
+ messages: pluginConfig.experimental.messages
1202
+ }
1203
+ };
1204
+ }
1205
+ function getTurboRules() {
1206
+ return nextConfig?.turbopack?.rules ||
1207
+ // @ts-expect-error -- For Next.js <16
1208
+ nextConfig?.experimental?.turbo?.rules || {};
1209
+ }
1210
+ function addTurboRule(rules, glob, rule) {
1211
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1212
+ if (rules[glob]) {
1213
+ if (Array.isArray(rules[glob])) {
1214
+ rules[glob].push(rule);
1215
+ } else {
1216
+ rules[glob] = [rules[glob], rule];
1217
+ }
1218
+ } else {
1219
+ rules[glob] = rule;
1220
+ }
1221
+ }
1222
+ if (useTurbo) {
1223
+ if (pluginConfig.requestConfig && path__default.default.isAbsolute(pluginConfig.requestConfig)) {
1224
+ 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);
1225
+ }
1226
+
1227
+ // Assign alias for `next-intl/config`
1228
+ const resolveAlias = {
1229
+ // Turbo aliases don't work with absolute
1230
+ // paths (see error handling above)
1231
+ 'next-intl/config': resolveI18nPath(pluginConfig.requestConfig)
1232
+ };
1233
+
1234
+ // Add loaders
1235
+ let rules;
1236
+
1237
+ // Add loader for extractor
1238
+ if (pluginConfig.experimental?.extract) {
1239
+ if (!isNextJs16OrHigher()) {
1240
+ throwError('Message extraction requires Next.js 16 or higher.');
1241
+ }
1242
+ rules ??= getTurboRules();
1243
+ const srcPaths = (Array.isArray(pluginConfig.experimental.srcPath) ? pluginConfig.experimental.srcPath : [pluginConfig.experimental.srcPath]).map(srcPath => srcPath.endsWith('/') ? srcPath.slice(0, -1) : srcPath);
1244
+ addTurboRule(rules, `*.{${SourceFileFilter.EXTENSIONS.join(',')}}`, {
1245
+ loaders: [getExtractMessagesLoaderConfig()],
1246
+ condition: {
1247
+ // Note: We don't need `not: 'foreign'`, because this is
1248
+ // implied by the filter based on `srcPath`.
1249
+ path: `{${srcPaths.join(',')}}` + '/**/*',
1250
+ content: /(useExtracted|getExtracted)/
1251
+ }
1252
+ });
1253
+ }
1254
+
1255
+ // Add loader for catalog
1256
+ if (pluginConfig.experimental?.messages) {
1257
+ if (!isNextJs16OrHigher()) {
1258
+ throwError('Message catalog loading requires Next.js 16 or higher.');
1259
+ }
1260
+ rules ??= getTurboRules();
1261
+ const extension = getFormatExtension(pluginConfig.experimental.messages.format);
1262
+ addTurboRule(rules, `*${extension}`, {
1263
+ loaders: [getCatalogLoaderConfig()],
1264
+ condition: {
1265
+ path: `${pluginConfig.experimental.messages.path}/**/*`
1266
+ },
1267
+ as: '*.js'
1268
+ });
1269
+ }
1270
+ if (hasStableTurboConfig() &&
1271
+ // @ts-expect-error -- For Next.js <16
1272
+ !nextConfig?.experimental?.turbo) {
1273
+ nextIntlConfig.turbopack = {
1274
+ ...nextConfig?.turbopack,
1275
+ ...(rules && {
1276
+ rules
1277
+ }),
1278
+ resolveAlias: {
1279
+ ...nextConfig?.turbopack?.resolveAlias,
1280
+ ...resolveAlias
1281
+ }
1282
+ };
1283
+ } else {
1284
+ nextIntlConfig.experimental = {
1285
+ ...nextConfig?.experimental,
1286
+ // @ts-expect-error -- For Next.js <16
1287
+ turbo: {
1288
+ // @ts-expect-error -- For Next.js <16
1289
+ ...nextConfig?.experimental?.turbo,
1290
+ ...(rules && {
1291
+ rules
1292
+ }),
1293
+ resolveAlias: {
1294
+ // @ts-expect-error -- For Next.js <16
1295
+ ...nextConfig?.experimental?.turbo?.resolveAlias,
1296
+ ...resolveAlias
1297
+ }
1298
+ }
1299
+ };
1300
+ }
1301
+ } else {
1302
+ nextIntlConfig.webpack = function webpack(config, context) {
1303
+ if (!config.resolve) config.resolve = {};
1304
+ if (!config.resolve.alias) config.resolve.alias = {};
1305
+
1306
+ // Assign alias for `next-intl/config`
1307
+ // (Webpack requires absolute paths)
1308
+ config.resolve.alias['next-intl/config'] = path__default.default.resolve(config.context, resolveI18nPath(pluginConfig.requestConfig, config.context));
1309
+
1310
+ // Add loader for extractor
1311
+ if (pluginConfig.experimental?.extract) {
1312
+ if (!config.module) config.module = {};
1313
+ if (!config.module.rules) config.module.rules = [];
1314
+ const srcPath = pluginConfig.experimental.srcPath;
1315
+ config.module.rules.push({
1316
+ test: new RegExp(`\\.(${SourceFileFilter.EXTENSIONS.join('|')})$`),
1317
+ include: Array.isArray(srcPath) ? srcPath.map(cur => path__default.default.resolve(config.context, cur)) : path__default.default.resolve(config.context, srcPath || ''),
1318
+ use: [getExtractMessagesLoaderConfig()]
1319
+ });
1320
+ }
1321
+
1322
+ // Add loader for catalog
1323
+ if (pluginConfig.experimental?.messages) {
1324
+ if (!config.module) config.module = {};
1325
+ if (!config.module.rules) config.module.rules = [];
1326
+ const extension = getFormatExtension(pluginConfig.experimental.messages.format);
1327
+ config.module.rules.push({
1328
+ test: new RegExp(`${extension.replace(/\./g, '\\.')}$`),
1329
+ include: path__default.default.resolve(config.context, pluginConfig.experimental.messages.path),
1330
+ use: [getCatalogLoaderConfig()],
1331
+ type: 'javascript/auto'
1332
+ });
1333
+ }
1334
+ if (typeof nextConfig?.webpack === 'function') {
1335
+ return nextConfig.webpack(config, context);
1336
+ }
1337
+ return config;
1338
+ };
1339
+ }
1340
+
1341
+ // Forward config
1342
+ if (nextConfig?.trailingSlash) {
1343
+ nextIntlConfig.env = {
1344
+ ...nextConfig.env,
1345
+ _next_intl_trailing_slash: 'true'
1346
+ };
1347
+ }
1348
+ return Object.assign({}, nextConfig, nextIntlConfig);
1349
+ }
1350
+
1351
+ function initPlugin(pluginConfig, nextConfig) {
1352
+ if (nextConfig?.i18n != null) {
1353
+ 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");
1354
+ }
1355
+ const messagesPathOrPaths = pluginConfig.experimental?.createMessagesDeclaration;
1356
+ if (messagesPathOrPaths) {
1357
+ createMessagesDeclaration(typeof messagesPathOrPaths === 'string' ? [messagesPathOrPaths] : messagesPathOrPaths);
1358
+ }
1359
+ initExtractionCompiler(pluginConfig);
1360
+ return getNextConfig(pluginConfig, nextConfig);
1361
+ }
1362
+ function createNextIntlPlugin(i18nPathOrConfig = {}) {
1363
+ const config = typeof i18nPathOrConfig === 'string' ? {
1364
+ requestConfig: i18nPathOrConfig
1365
+ } : i18nPathOrConfig;
1366
+ return function withNextIntl(nextConfig) {
1367
+ return initPlugin(config, nextConfig);
1368
+ };
1369
+ }
1370
+
1371
+ exports.createNextIntlPlugin = createNextIntlPlugin;
1372
+ exports.getSortedMessages = getSortedMessages;
1373
+ exports.setNestedProperty = setNestedProperty;