next-intl 4.5.7 → 4.6.0

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 (78) hide show
  1. package/dist/cjs/development/ExtractorCodec-DZKNn0Zq.cjs +37 -0
  2. package/dist/cjs/development/JSONCodec-Dlcx71xz.cjs +41 -0
  3. package/dist/cjs/development/POCodec-BW-UDNcq.cjs +94 -0
  4. package/dist/cjs/development/plugin.cjs +28 -5
  5. package/dist/esm/development/extractor/ExtractionCompiler.js +22 -25
  6. package/dist/esm/development/extractor/catalog/CatalogLocales.js +0 -33
  7. package/dist/esm/development/extractor/catalog/CatalogManager.js +134 -102
  8. package/dist/esm/development/extractor/catalog/CatalogPersister.js +31 -13
  9. package/dist/esm/development/extractor/catalog/SaveScheduler.js +33 -14
  10. package/dist/esm/development/extractor/catalogLoader.js +10 -10
  11. package/dist/esm/development/extractor/extractMessages.js +9 -2
  12. package/dist/esm/development/extractor/extractionLoader.js +27 -4
  13. package/dist/esm/development/extractor/extractor/MessageExtractor.js +5 -4
  14. package/dist/esm/development/extractor/format/ExtractorCodec.js +5 -0
  15. package/dist/esm/development/extractor/format/codecs/JSONCodec.js +40 -0
  16. package/dist/esm/development/extractor/format/codecs/POCodec.js +93 -0
  17. package/dist/esm/development/extractor/format/index.js +44 -0
  18. package/dist/esm/development/extractor/source/SourceFileScanner.js +2 -1
  19. package/dist/esm/development/extractor/source/SourceFileWatcher.js +38 -0
  20. package/dist/esm/development/extractor/utils.js +16 -1
  21. package/dist/esm/development/extractor.js +1 -0
  22. package/dist/esm/development/plugin/createNextIntlPlugin.js +1 -1
  23. package/dist/esm/development/plugin/getNextConfig.js +7 -4
  24. package/dist/esm/production/extractor/ExtractionCompiler.js +1 -1
  25. package/dist/esm/production/extractor/catalog/CatalogLocales.js +1 -1
  26. package/dist/esm/production/extractor/catalog/CatalogManager.js +1 -1
  27. package/dist/esm/production/extractor/catalog/CatalogPersister.js +1 -1
  28. package/dist/esm/production/extractor/catalog/SaveScheduler.js +1 -1
  29. package/dist/esm/production/extractor/catalogLoader.js +1 -1
  30. package/dist/esm/production/extractor/extractMessages.js +1 -1
  31. package/dist/esm/production/extractor/extractionLoader.js +1 -1
  32. package/dist/esm/production/extractor/extractor/MessageExtractor.js +1 -1
  33. package/dist/esm/production/extractor/format/ExtractorCodec.js +1 -0
  34. package/dist/esm/production/extractor/format/codecs/JSONCodec.js +1 -0
  35. package/dist/esm/production/extractor/format/codecs/POCodec.js +1 -0
  36. package/dist/esm/production/extractor/format/index.js +1 -0
  37. package/dist/esm/production/extractor/source/SourceFileScanner.js +1 -1
  38. package/dist/esm/production/extractor/source/SourceFileWatcher.js +1 -0
  39. package/dist/esm/production/extractor/utils.js +1 -1
  40. package/dist/esm/production/extractor.js +1 -1
  41. package/dist/esm/production/plugin/createNextIntlPlugin.js +1 -1
  42. package/dist/esm/production/plugin/getNextConfig.js +1 -1
  43. package/dist/types/extractor/ExtractionCompiler.d.ts +5 -10
  44. package/dist/types/extractor/catalog/CatalogLocales.d.ts +0 -2
  45. package/dist/types/extractor/catalog/CatalogManager.d.ts +21 -11
  46. package/dist/types/extractor/catalog/CatalogPersister.d.ts +15 -6
  47. package/dist/types/extractor/catalog/SaveScheduler.d.ts +1 -0
  48. package/dist/types/extractor/extractor/MessageExtractor.d.ts +6 -6
  49. package/dist/types/extractor/format/ExtractorCodec.d.ts +33 -0
  50. package/dist/types/extractor/format/codecs/JSONCodec.d.ts +2 -0
  51. package/dist/types/extractor/format/codecs/POCodec.d.ts +2 -0
  52. package/dist/types/extractor/format/codecs/fixtures/JSONCodecStructured.d.ts +2 -0
  53. package/dist/types/extractor/format/codecs/fixtures/POCodecSourceMessageKey.d.ts +2 -0
  54. package/dist/types/extractor/format/index.d.ts +15 -0
  55. package/dist/types/extractor/format/types.d.ts +8 -0
  56. package/dist/types/extractor/index.d.ts +1 -0
  57. package/dist/types/extractor/source/SourceFileFilter.d.ts +1 -1
  58. package/dist/types/extractor/source/SourceFileScanner.d.ts +1 -1
  59. package/dist/types/extractor/source/SourceFileWatcher.d.ts +12 -0
  60. package/dist/types/extractor/types.d.ts +2 -2
  61. package/dist/types/extractor/utils.d.ts +3 -0
  62. package/dist/types/plugin/types.d.ts +1 -1
  63. package/package.json +6 -5
  64. package/dist/esm/development/extractor/formatters/Formatter.js +0 -3
  65. package/dist/esm/development/extractor/formatters/JSONFormatter.js +0 -42
  66. package/dist/esm/development/extractor/formatters/POFormatter.js +0 -51
  67. package/dist/esm/development/extractor/formatters/index.js +0 -6
  68. package/dist/esm/development/extractor/formatters/utils.js +0 -15
  69. package/dist/esm/production/extractor/formatters/Formatter.js +0 -1
  70. package/dist/esm/production/extractor/formatters/JSONFormatter.js +0 -1
  71. package/dist/esm/production/extractor/formatters/POFormatter.js +0 -1
  72. package/dist/esm/production/extractor/formatters/index.js +0 -1
  73. package/dist/esm/production/extractor/formatters/utils.js +0 -1
  74. package/dist/types/extractor/formatters/Formatter.d.ts +0 -10
  75. package/dist/types/extractor/formatters/JSONFormatter.d.ts +0 -10
  76. package/dist/types/extractor/formatters/POFormatter.d.ts +0 -10
  77. package/dist/types/extractor/formatters/index.d.ts +0 -5
  78. package/dist/types/extractor/formatters/utils.d.ts +0 -2
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ // Essentialls lodash/set, but we avoid this dependency
4
+ function setNestedProperty(obj, keyPath, value) {
5
+ const keys = keyPath.split('.');
6
+ let current = obj;
7
+ for (let i = 0; i < keys.length - 1; i++) {
8
+ const key = keys[i];
9
+ if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
10
+ current[key] = {};
11
+ }
12
+ current = current[key];
13
+ }
14
+ current[keys[keys.length - 1]] = value;
15
+ }
16
+ function getSortedMessages(messages) {
17
+ return messages.toSorted((messageA, messageB) => {
18
+ const pathA = messageA.references?.[0]?.path ?? '';
19
+ const pathB = messageB.references?.[0]?.path ?? '';
20
+ if (pathA === pathB) {
21
+ return localeCompare(messageA.id, messageB.id);
22
+ } else {
23
+ return localeCompare(pathA, pathB);
24
+ }
25
+ });
26
+ }
27
+ function localeCompare(a, b) {
28
+ return a.localeCompare(b, 'en');
29
+ }
30
+
31
+ function defineCodec(factory) {
32
+ return factory;
33
+ }
34
+
35
+ exports.defineCodec = defineCodec;
36
+ exports.getSortedMessages = getSortedMessages;
37
+ exports.setNestedProperty = setNestedProperty;
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ var ExtractorCodec = require('./ExtractorCodec-DZKNn0Zq.cjs');
4
+
5
+ var JSONCodec = ExtractorCodec.defineCodec(() => ({
6
+ decode(source) {
7
+ const json = JSON.parse(source);
8
+ const messages = [];
9
+ traverseMessages(json, (message, id) => {
10
+ messages.push({
11
+ id,
12
+ message
13
+ });
14
+ });
15
+ return messages;
16
+ },
17
+ encode(messages) {
18
+ const root = {};
19
+ for (const message of ExtractorCodec.getSortedMessages(messages)) {
20
+ ExtractorCodec.setNestedProperty(root, message.id, message.message);
21
+ }
22
+ return JSON.stringify(root, null, 2) + '\n';
23
+ },
24
+ toJSONString(source) {
25
+ return source;
26
+ }
27
+ }));
28
+ function traverseMessages(obj, callback, path = '') {
29
+ const NAMESPACE_SEPARATOR = '.';
30
+ for (const key of Object.keys(obj)) {
31
+ const newPath = path ? path + NAMESPACE_SEPARATOR + key : key;
32
+ const value = obj[key];
33
+ if (typeof value === 'string') {
34
+ callback(value, newPath);
35
+ } else if (typeof value === 'object') {
36
+ traverseMessages(value, callback, newPath);
37
+ }
38
+ }
39
+ }
40
+
41
+ exports.default = JSONCodec;
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ var POParser = require('po-parser');
4
+ var ExtractorCodec = require('./ExtractorCodec-DZKNn0Zq.cjs');
5
+
6
+ var POCodec = ExtractorCodec.defineCodec(() => {
7
+ // See also https://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html
8
+ const DEFAULT_METADATA = {
9
+ // Recommended by spec
10
+ 'Content-Type': 'text/plain; charset=utf-8',
11
+ 'Content-Transfer-Encoding': '8bit',
12
+ // Otherwise other tools might set this
13
+ 'X-Generator': 'next-intl',
14
+ // Crowdin defaults to using msgid as source key
15
+ 'X-Crowdin-SourceKey': 'msgstr'
16
+ };
17
+
18
+ // Move all parts before the last dot to msgctxt
19
+ const NAMESPACE_SEPARATOR = '.';
20
+
21
+ // Metadata is stored so it can be retained when writing
22
+ const metadataByLocale = new Map();
23
+ return {
24
+ decode(content, context) {
25
+ const catalog = POParser.parse(content);
26
+ if (catalog.meta) {
27
+ metadataByLocale.set(context.locale, catalog.meta);
28
+ }
29
+ const messages = catalog.messages || [];
30
+ return messages.map(msg => {
31
+ const {
32
+ extractedComments,
33
+ msgctxt,
34
+ msgid,
35
+ msgstr,
36
+ ...rest
37
+ } = msg;
38
+ if (extractedComments && extractedComments.length > 1) {
39
+ throw new Error(`Multiple extracted comments are not supported. Found ${extractedComments.length} comments for msgid "${msgid}".`);
40
+ }
41
+ return {
42
+ ...rest,
43
+ id: msgctxt ? [msgctxt, msgid].join(NAMESPACE_SEPARATOR) : msgid,
44
+ message: msgstr,
45
+ ...(extractedComments && extractedComments.length > 0 && {
46
+ description: extractedComments[0]
47
+ })
48
+ };
49
+ });
50
+ },
51
+ encode(messages, context) {
52
+ const encodedMessages = ExtractorCodec.getSortedMessages(messages).map(msg => {
53
+ const {
54
+ description,
55
+ id,
56
+ message,
57
+ ...rest
58
+ } = msg;
59
+ const lastDotIndex = id.lastIndexOf(NAMESPACE_SEPARATOR);
60
+ const hasNamespace = id.includes(NAMESPACE_SEPARATOR);
61
+ const msgid = hasNamespace ? id.slice(lastDotIndex + NAMESPACE_SEPARATOR.length) : id;
62
+ return {
63
+ msgid,
64
+ msgstr: message,
65
+ ...(description && {
66
+ extractedComments: [description]
67
+ }),
68
+ ...(hasNamespace && {
69
+ msgctxt: id.slice(0, lastDotIndex)
70
+ }),
71
+ ...rest
72
+ };
73
+ });
74
+ return POParser.serialize({
75
+ meta: {
76
+ Language: context.locale,
77
+ ...DEFAULT_METADATA,
78
+ ...metadataByLocale.get(context.locale)
79
+ },
80
+ messages: encodedMessages
81
+ });
82
+ },
83
+ toJSONString(source, context) {
84
+ const parsed = this.decode(source, context);
85
+ const messagesObject = {};
86
+ for (const message of parsed) {
87
+ ExtractorCodec.setNestedProperty(messagesObject, message.id, message.message);
88
+ }
89
+ return JSON.stringify(messagesObject);
90
+ }
91
+ };
92
+ });
93
+
94
+ exports.default = POCodec;
@@ -105,6 +105,27 @@ export default messages;`;
105
105
  fs.writeFileSync(declarationPath, createDeclaration(content));
106
106
  }
107
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
+
108
129
  class SourceFileFilter {
109
130
  static EXTENSIONS = ['ts', 'tsx', 'js', 'jsx'];
110
131
 
@@ -198,7 +219,7 @@ function getNextConfig(pluginConfig, nextConfig) {
198
219
  const nextIntlConfig = {};
199
220
  function getExtractMessagesLoaderConfig() {
200
221
  const experimental = pluginConfig.experimental;
201
- if (!experimental.srcPath || !experimental.messages) {
222
+ if (!experimental.srcPath || !pluginConfig.experimental?.messages) {
202
223
  throwError('`srcPath` and `messages` are required when using `extractor`.');
203
224
  }
204
225
  return {
@@ -206,7 +227,7 @@ function getNextConfig(pluginConfig, nextConfig) {
206
227
  options: {
207
228
  srcPath: experimental.srcPath,
208
229
  sourceLocale: experimental.extract.sourceLocale,
209
- messages: experimental.messages
230
+ messages: pluginConfig.experimental.messages
210
231
  }
211
232
  };
212
233
  }
@@ -274,7 +295,8 @@ function getNextConfig(pluginConfig, nextConfig) {
274
295
  throwError('Message catalog loading requires Next.js 16 or higher.');
275
296
  }
276
297
  rules ??= getTurboRules();
277
- addTurboRule(rules, `*.${pluginConfig.experimental.messages.format}`, {
298
+ const extension = getFormatExtension(pluginConfig.experimental.messages.format);
299
+ addTurboRule(rules, `*${extension}`, {
278
300
  loaders: [getCatalogLoaderConfig()],
279
301
  condition: {
280
302
  path: `${pluginConfig.experimental.messages.path}/**/*`
@@ -338,8 +360,9 @@ function getNextConfig(pluginConfig, nextConfig) {
338
360
  if (pluginConfig.experimental?.messages) {
339
361
  if (!config.module) config.module = {};
340
362
  if (!config.module.rules) config.module.rules = [];
363
+ const extension = getFormatExtension(pluginConfig.experimental.messages.format);
341
364
  config.module.rules.push({
342
- test: new RegExp(`\\.${pluginConfig.experimental.messages.format}$`),
365
+ test: new RegExp(`${extension.replace(/\./g, '\\.')}$`),
343
366
  include: path.resolve(config.context, pluginConfig.experimental.messages.path),
344
367
  use: [getCatalogLoaderConfig()],
345
368
  type: 'javascript/auto'
@@ -364,7 +387,7 @@ function getNextConfig(pluginConfig, nextConfig) {
364
387
 
365
388
  function initPlugin(pluginConfig, nextConfig) {
366
389
  if (nextConfig?.i18n != null) {
367
- warn("\n[next-intl] 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");
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");
368
391
  }
369
392
  const messagesPathOrPaths = pluginConfig.experimental?.createMessagesDeclaration;
370
393
  if (messagesPathOrPaths) {
@@ -1,41 +1,38 @@
1
1
  import CatalogManager from './catalog/CatalogManager.js';
2
+ import MessageExtractor from './extractor/MessageExtractor.js';
2
3
 
3
4
  class ExtractionCompiler {
4
- isDevelopment = false;
5
5
  constructor(config, opts = {}) {
6
- this.manager = new CatalogManager(config, opts);
7
- this.isDevelopment = opts.isDevelopment ?? false;
8
-
9
- // Kick off the initial scan as early as possible,
10
- // while awaiting it in `compile`. This also ensures
11
- // we're only scanning once.
12
- this.initialScanPromise = this.performInitialScan();
13
- }
14
- async compile(resourcePath, source) {
15
- if (this.initialScanPromise) {
16
- await this.initialScanPromise;
17
- this.initialScanPromise = undefined;
18
- }
19
- const result = await this.manager.extractFileMessages(resourcePath, source);
20
- if (this.isDevelopment && result.changed) {
21
- // While we await the AST modification, we
22
- // don't need to await the persistence
23
- void this.manager.save();
24
- }
25
- return result;
6
+ const extractor = opts.extractor ?? new MessageExtractor(opts);
7
+ this.manager = new CatalogManager(config, {
8
+ ...opts,
9
+ extractor
10
+ });
11
+ this[Symbol.dispose] = this[Symbol.dispose].bind(this);
12
+ this.installExitHandlers();
26
13
  }
27
- async performInitialScan() {
14
+ async extractAll() {
28
15
  // We can't rely on all files being compiled (e.g. due to persistent
29
16
  // caching), so loading the messages initially is necessary.
30
17
  await this.manager.loadMessages();
31
18
  await this.manager.save();
32
19
  }
33
- async extract() {
34
- await this.initialScanPromise;
35
- }
36
20
  [Symbol.dispose]() {
21
+ this.uninstallExitHandlers();
37
22
  this.manager.destroy();
38
23
  }
24
+ installExitHandlers() {
25
+ const cleanup = this[Symbol.dispose];
26
+ process.on('exit', cleanup);
27
+ process.on('SIGINT', cleanup);
28
+ process.on('SIGTERM', cleanup);
29
+ }
30
+ uninstallExitHandlers() {
31
+ const cleanup = this[Symbol.dispose];
32
+ process.off('exit', cleanup);
33
+ process.off('SIGINT', cleanup);
34
+ process.off('SIGTERM', cleanup);
35
+ }
39
36
  }
40
37
 
41
38
  export { ExtractionCompiler as default };
@@ -3,7 +3,6 @@ import fs from 'fs/promises';
3
3
  import path from 'path';
4
4
 
5
5
  class CatalogLocales {
6
- cleanupHandlers = [];
7
6
  onChangeCallbacks = (() => new Set())();
8
7
  constructor(params) {
9
8
  this.messagesDir = params.messagesDir;
@@ -58,17 +57,12 @@ class CatalogLocales {
58
57
  void this.onChange();
59
58
  }
60
59
  });
61
- this.setupCleanupHandlers();
62
60
  }
63
61
  stopWatcher() {
64
62
  if (this.watcher) {
65
63
  this.watcher.close();
66
64
  this.watcher = undefined;
67
65
  }
68
- for (const handler of this.cleanupHandlers) {
69
- handler();
70
- }
71
- this.cleanupHandlers = [];
72
66
  }
73
67
  async onChange() {
74
68
  const oldLocales = new Set(this.targetLocales || []);
@@ -85,33 +79,6 @@ class CatalogLocales {
85
79
  }
86
80
  }
87
81
  }
88
- setupCleanupHandlers() {
89
- const cleanup = () => {
90
- if (this.watcher) {
91
- this.watcher.close();
92
- this.watcher = undefined;
93
- }
94
- };
95
- function exitHandler() {
96
- cleanup();
97
- }
98
- function sigintHandler() {
99
- cleanup();
100
- process.exit(0);
101
- }
102
- function sigtermHandler() {
103
- cleanup();
104
- process.exit(0);
105
- }
106
- process.once('exit', exitHandler);
107
- process.once('SIGINT', sigintHandler);
108
- process.once('SIGTERM', sigtermHandler);
109
- this.cleanupHandlers.push(() => {
110
- process.removeListener('exit', exitHandler);
111
- process.removeListener('SIGINT', sigintHandler);
112
- process.removeListener('SIGTERM', sigtermHandler);
113
- });
114
- }
115
82
  }
116
83
 
117
84
  export { CatalogLocales as default };