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.
Files changed (32) hide show
  1. package/dist/cjs/development/ExtractorCodec-D9Tw618d.cjs +7 -0
  2. package/dist/cjs/development/{JSONCodec-Dlcx71xz.cjs → JSONCodec-L1_VeQBi.cjs} +10 -3
  3. package/dist/cjs/development/{POCodec-BW-UDNcq.cjs → POCodec-Be_UL6jy.cjs} +16 -5
  4. package/dist/cjs/development/plugin-DDtWCyPI.cjs +1373 -0
  5. package/dist/cjs/development/plugin.cjs +8 -402
  6. package/dist/esm/development/extractor/ExtractionCompiler.js +1 -1
  7. package/dist/esm/development/extractor/catalog/CatalogManager.js +42 -13
  8. package/dist/esm/development/extractor/catalog/SaveScheduler.js +1 -1
  9. package/dist/esm/development/extractor/extractionLoader.js +5 -25
  10. package/dist/esm/development/extractor/source/SourceFileWatcher.js +99 -5
  11. package/dist/esm/development/plugin/createNextIntlPlugin.js +2 -0
  12. package/dist/esm/development/plugin/declaration/createMessagesDeclaration.js +2 -11
  13. package/dist/esm/development/plugin/extractor/initExtractionCompiler.js +45 -0
  14. package/dist/esm/development/plugin/utils.js +16 -1
  15. package/dist/esm/production/extractor/ExtractionCompiler.js +1 -1
  16. package/dist/esm/production/extractor/catalog/CatalogManager.js +1 -1
  17. package/dist/esm/production/extractor/catalog/SaveScheduler.js +1 -1
  18. package/dist/esm/production/extractor/extractionLoader.js +1 -1
  19. package/dist/esm/production/extractor/source/SourceFileWatcher.js +1 -1
  20. package/dist/esm/production/plugin/createNextIntlPlugin.js +1 -1
  21. package/dist/esm/production/plugin/declaration/createMessagesDeclaration.js +1 -1
  22. package/dist/esm/production/plugin/extractor/initExtractionCompiler.js +1 -0
  23. package/dist/esm/production/plugin/utils.js +1 -1
  24. package/dist/types/extractor/catalog/CatalogManager.d.ts +6 -5
  25. package/dist/types/extractor/catalog/SaveScheduler.d.ts +2 -2
  26. package/dist/types/extractor/source/SourceFileFilter.d.ts +1 -1
  27. package/dist/types/extractor/source/SourceFileWatcher.d.ts +3 -0
  28. package/dist/types/plugin/extractor/initExtractionCompiler.d.ts +2 -0
  29. package/dist/types/plugin/utils.d.ts +6 -0
  30. package/package.json +4 -4
  31. package/dist/cjs/development/ExtractorCodec-DZKNn0Zq.cjs +0 -37
  32. package/dist/types/extractor/extractor/ASTScope.d.ts +0 -12
@@ -1,407 +1,13 @@
1
1
  'use strict';
2
2
 
3
- var fs = require('fs');
4
- var path = require('path');
5
- var module$1 = require('module');
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
- function runOnce(fn) {
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;
@@ -19,7 +19,7 @@ class ExtractionCompiler {
19
19
  }
20
20
  [Symbol.dispose]() {
21
21
  this.uninstallExitHandlers();
22
- this.manager.destroy();
22
+ this.manager[Symbol.dispose]();
23
23
  }
24
24
  installExitHandlers() {
25
25
  const cleanup = this[Symbol.dispose];
@@ -32,7 +32,8 @@ class CatalogManager {
32
32
  // Cached instances
33
33
 
34
34
  // Resolves when all catalogs are loaded
35
- // (but doesn't indicate that project scan is done)
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
- const sourceFiles = await SourceFileScanner.getSourceFiles(this.getSrcPaths());
91
- await Promise.all(Array.from(sourceFiles).map(async filePath => this.processFile(filePath)));
92
- this.mergeSourceDiskMetadata(sourceDiskMessages);
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
- const translations = new Map();
141
- for (const message of diskMessages) {
142
- translations.set(message.id, message);
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
- const extraction = await this.extractor.extract(absoluteFilePath, content);
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
- for (const event of events) {
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
- destroy() {
363
+ [Symbol.dispose]() {
335
364
  this.sourceWatcher?.stop();
336
365
  this.sourceWatcher = undefined;
337
- this.saveScheduler.destroy();
366
+ this.saveScheduler[Symbol.dispose]();
338
367
  if (this.catalogLocales && this.isDevelopment) {
339
368
  this.catalogLocales.unsubscribeLocalesChange(this.onLocalesChange);
340
369
  }
@@ -71,7 +71,7 @@ class SaveScheduler {
71
71
  }
72
72
  }
73
73
  }
74
- destroy() {
74
+ [Symbol.dispose]() {
75
75
  if (this.saveTimeout) {
76
76
  clearTimeout(this.saveTimeout);
77
77
  this.saveTimeout = undefined;
@@ -1,44 +1,24 @@
1
- import ExtractionCompiler from './ExtractionCompiler.js';
2
1
  import MessageExtractor from './extractor/MessageExtractor.js';
3
2
 
4
- // This instance:
5
- // - Remains available through HMR
6
- // - Is the same across react-client and react-server
7
- // - Is only lost when the dev server restarts (e.g. due to change to Next.js config)
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
- if (!compiler) {
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
  }