next-intl 4.4.0 → 4.5.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 (116) hide show
  1. package/dist/cjs/development/plugin.cjs +129 -8
  2. package/dist/esm/development/extractor/ExtractionCompiler.js +41 -0
  3. package/dist/esm/development/extractor/catalog/CatalogLocales.js +117 -0
  4. package/dist/esm/development/extractor/catalog/CatalogManager.js +286 -0
  5. package/dist/esm/development/extractor/catalog/CatalogPersister.js +45 -0
  6. package/dist/esm/development/extractor/catalog/SaveScheduler.js +66 -0
  7. package/dist/esm/development/extractor/catalogLoader.js +35 -0
  8. package/dist/esm/development/extractor/extractMessages.js +8 -0
  9. package/dist/esm/development/extractor/extractionLoader.js +22 -0
  10. package/dist/esm/development/extractor/extractor/ASTScope.js +18 -0
  11. package/dist/esm/development/extractor/extractor/KeyGenerator.js +11 -0
  12. package/dist/esm/development/extractor/extractor/LRUCache.js +30 -0
  13. package/dist/esm/development/extractor/extractor/MessageExtractor.js +402 -0
  14. package/dist/esm/development/extractor/formatters/Formatter.js +3 -0
  15. package/dist/esm/development/extractor/formatters/JSONFormatter.js +42 -0
  16. package/dist/esm/development/extractor/formatters/POFormatter.js +51 -0
  17. package/dist/esm/development/extractor/formatters/index.js +6 -0
  18. package/dist/esm/development/extractor/formatters/utils.js +13 -0
  19. package/dist/esm/development/extractor/source/SourceFileFilter.js +11 -0
  20. package/dist/esm/development/extractor/source/SourceFileScanner.js +27 -0
  21. package/dist/esm/development/extractor/utils/ObjectUtils.js +14 -0
  22. package/dist/esm/development/extractor/utils/POParser.js +222 -0
  23. package/dist/esm/development/extractor.js +1 -0
  24. package/dist/esm/development/index.react-client.js +2 -1
  25. package/dist/esm/development/index.react-server.js +1 -0
  26. package/dist/esm/development/plugin/createNextIntlPlugin.js +1 -1
  27. package/dist/esm/development/plugin/{createMessagesDeclaration.js → declaration/createMessagesDeclaration.js} +2 -2
  28. package/dist/esm/development/plugin/getNextConfig.js +117 -8
  29. package/dist/esm/development/plugin/{hasStableTurboConfig.js → nextFlags.js} +7 -2
  30. package/dist/esm/development/react-client/index.js +0 -1
  31. package/dist/esm/development/react-server/useExtracted.js +9 -0
  32. package/dist/esm/development/server/react-client/index.js +2 -1
  33. package/dist/esm/development/server/react-server/getExtracted.js +24 -0
  34. package/dist/esm/development/server/react-server/getServerExtractor.js +38 -0
  35. package/dist/esm/development/server.react-client.js +1 -1
  36. package/dist/esm/development/server.react-server.js +1 -0
  37. package/dist/esm/production/extractor/ExtractionCompiler.js +1 -0
  38. package/dist/esm/production/extractor/catalog/CatalogLocales.js +1 -0
  39. package/dist/esm/production/extractor/catalog/CatalogManager.js +1 -0
  40. package/dist/esm/production/extractor/catalog/CatalogPersister.js +1 -0
  41. package/dist/esm/production/extractor/catalog/SaveScheduler.js +1 -0
  42. package/dist/esm/production/extractor/catalogLoader.js +1 -0
  43. package/dist/esm/production/extractor/extractMessages.js +1 -0
  44. package/dist/esm/production/extractor/extractionLoader.js +1 -0
  45. package/dist/esm/production/extractor/extractor/ASTScope.js +1 -0
  46. package/dist/esm/production/extractor/extractor/KeyGenerator.js +1 -0
  47. package/dist/esm/production/extractor/extractor/LRUCache.js +1 -0
  48. package/dist/esm/production/extractor/extractor/MessageExtractor.js +1 -0
  49. package/dist/esm/production/extractor/formatters/Formatter.js +1 -0
  50. package/dist/esm/production/extractor/formatters/JSONFormatter.js +1 -0
  51. package/dist/esm/production/extractor/formatters/POFormatter.js +1 -0
  52. package/dist/esm/production/extractor/formatters/index.js +1 -0
  53. package/dist/esm/production/extractor/formatters/utils.js +1 -0
  54. package/dist/esm/production/extractor/source/SourceFileFilter.js +1 -0
  55. package/dist/esm/production/extractor/source/SourceFileScanner.js +1 -0
  56. package/dist/esm/production/extractor/utils/ObjectUtils.js +1 -0
  57. package/dist/esm/production/extractor/utils/POParser.js +1 -0
  58. package/dist/esm/production/extractor.js +1 -0
  59. package/dist/esm/production/index.react-client.js +1 -1
  60. package/dist/esm/production/index.react-server.js +1 -1
  61. package/dist/esm/production/plugin/createNextIntlPlugin.js +1 -1
  62. package/dist/esm/production/plugin/declaration/createMessagesDeclaration.js +1 -0
  63. package/dist/esm/production/plugin/getNextConfig.js +1 -1
  64. package/dist/esm/production/plugin/nextFlags.js +1 -0
  65. package/dist/esm/production/react-client/index.js +1 -1
  66. package/dist/esm/production/react-server/useExtracted.js +1 -0
  67. package/dist/esm/production/server/react-client/index.js +1 -1
  68. package/dist/esm/production/server/react-server/getExtracted.js +1 -0
  69. package/dist/esm/production/server/react-server/getServerExtractor.js +1 -0
  70. package/dist/esm/production/server.react-client.js +1 -1
  71. package/dist/esm/production/server.react-server.js +1 -1
  72. package/dist/types/extractor/ExtractionCompiler.d.ts +14 -0
  73. package/dist/types/extractor/catalog/CatalogLocales.d.ts +31 -0
  74. package/dist/types/extractor/catalog/CatalogManager.d.ts +46 -0
  75. package/dist/types/extractor/catalog/CatalogPersister.d.ts +11 -0
  76. package/dist/types/extractor/catalog/SaveScheduler.d.ts +17 -0
  77. package/dist/types/extractor/extractMessages.d.ts +2 -0
  78. package/dist/types/extractor/extractor/ASTScope.d.ts +12 -0
  79. package/dist/types/extractor/extractor/KeyGenerator.d.ts +3 -0
  80. package/dist/types/extractor/extractor/LRUCache.d.ts +7 -0
  81. package/dist/types/extractor/extractor/MessageExtractor.d.ts +20 -0
  82. package/dist/types/extractor/formatters/Formatter.d.ts +10 -0
  83. package/dist/types/extractor/formatters/JSONFormatter.d.ts +10 -0
  84. package/dist/types/extractor/formatters/POFormatter.d.ts +10 -0
  85. package/dist/types/extractor/formatters/index.d.ts +5 -0
  86. package/dist/types/extractor/formatters/utils.d.ts +2 -0
  87. package/dist/types/extractor/index.d.ts +1 -0
  88. package/dist/types/extractor/source/SourceFileFilter.d.ts +4 -0
  89. package/dist/types/extractor/source/SourceFileScanner.d.ts +4 -0
  90. package/dist/types/extractor/types.d.ts +23 -0
  91. package/dist/types/extractor/utils/ObjectUtils.d.ts +1 -0
  92. package/dist/types/extractor/utils/POParser.d.ts +24 -0
  93. package/dist/types/extractor.d.ts +1 -0
  94. package/dist/types/navigation/react-client/createNavigation.d.ts +9 -9
  95. package/dist/types/navigation/react-server/createNavigation.d.ts +9 -9
  96. package/dist/types/navigation/shared/createSharedNavigationFns.d.ts +10 -10
  97. package/dist/types/plugin/catalog/catalogLoader.d.ts +10 -0
  98. package/dist/types/plugin/declaration/index.d.ts +1 -0
  99. package/dist/types/plugin/extractor/extractionLoader.d.ts +3 -0
  100. package/dist/types/plugin/nextFlags.d.ts +2 -0
  101. package/dist/types/plugin/types.d.ts +18 -0
  102. package/dist/types/react-client/index.d.ts +3 -1
  103. package/dist/types/react-server/index.d.ts +1 -0
  104. package/dist/types/react-server/useExtracted.d.ts +2 -0
  105. package/dist/types/server/react-client/index.d.ts +2 -1
  106. package/dist/types/server/react-server/getExtracted.d.ts +10 -0
  107. package/dist/types/server/react-server/getServerExtractor.d.ts +5 -0
  108. package/dist/types/server/react-server/index.d.ts +1 -0
  109. package/extractor/catalogLoader.d.ts +4 -0
  110. package/extractor/extractionLoader.d.ts +4 -0
  111. package/extractor.d.ts +2 -0
  112. package/package.json +20 -5
  113. package/dist/esm/production/plugin/createMessagesDeclaration.js +0 -1
  114. package/dist/esm/production/plugin/hasStableTurboConfig.js +0 -1
  115. package/dist/types/plugin/hasStableTurboConfig.d.ts +0 -2
  116. /package/dist/types/plugin/{createMessagesDeclaration.d.ts → declaration/createMessagesDeclaration.d.ts} +0 -0
@@ -0,0 +1,51 @@
1
+ import { setNestedProperty } from '../utils/ObjectUtils.js';
2
+ import POParser from '../utils/POParser.js';
3
+ import Formatter from './Formatter.js';
4
+ import { getSortedMessages } from './utils.js';
5
+
6
+ class POFormatter extends Formatter {
7
+ // See also https://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html
8
+ static 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
+ EXTENSION = '.po';
18
+
19
+ // Metadata is stored so it can be retained when writing
20
+ metadataByLocale = (() => new Map())();
21
+ parse(content, context) {
22
+ const catalog = POParser.parse(content);
23
+
24
+ // Store metadata for this locale
25
+ if (catalog.meta) {
26
+ this.metadataByLocale.set(context.locale, catalog.meta);
27
+ }
28
+ return catalog.messages || [];
29
+ }
30
+ serialize(messages, context) {
31
+ const meta = {
32
+ Language: context.locale,
33
+ ...POFormatter.DEFAULT_METADATA,
34
+ ...this.metadataByLocale.get(context.locale)
35
+ };
36
+ return POParser.serialize({
37
+ meta,
38
+ messages: getSortedMessages(messages)
39
+ });
40
+ }
41
+ toJSONString(source, context) {
42
+ const parsed = this.parse(source, context);
43
+ const messagesObject = {};
44
+ for (const message of parsed) {
45
+ setNestedProperty(messagesObject, message.id, message.message);
46
+ }
47
+ return JSON.stringify(messagesObject, null, 2);
48
+ }
49
+ }
50
+
51
+ export { POFormatter as default };
@@ -0,0 +1,6 @@
1
+ const formatters = {
2
+ json: () => import('./JSONFormatter.js'),
3
+ po: () => import('./POFormatter.js')
4
+ };
5
+
6
+ export { formatters as default };
@@ -0,0 +1,13 @@
1
+ function getSortedMessages(messages) {
2
+ return messages.toSorted((a, b) => {
3
+ const aPath = a.references?.[0]?.path ?? a.message;
4
+ const bPath = b.references?.[0]?.path ?? b.message;
5
+ if (aPath === bPath) {
6
+ return a.message.localeCompare(b.message);
7
+ } else {
8
+ return aPath.localeCompare(bPath);
9
+ }
10
+ });
11
+ }
12
+
13
+ export { getSortedMessages };
@@ -0,0 +1,11 @@
1
+ import path from 'path';
2
+
3
+ class SourceFileFilter {
4
+ static EXTENSIONS = ['ts', 'tsx', 'js', 'jsx'];
5
+ static isSourceFile(filePath) {
6
+ const ext = path.extname(filePath);
7
+ return SourceFileFilter.EXTENSIONS.map(cur => '.' + cur).includes(ext);
8
+ }
9
+ }
10
+
11
+ export { SourceFileFilter as default };
@@ -0,0 +1,27 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import SourceFileFilter from './SourceFileFilter.js';
4
+
5
+ class SourceFileScanner {
6
+ static async walkSourceFiles(dir, srcPaths, acc = []) {
7
+ const entries = await fs.readdir(dir, {
8
+ withFileTypes: true
9
+ });
10
+ for (const entry of entries) {
11
+ const entryPath = path.join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ await SourceFileScanner.walkSourceFiles(entryPath, srcPaths, acc);
14
+ } else {
15
+ if (SourceFileFilter.isSourceFile(entry.name)) {
16
+ acc.push(entryPath);
17
+ }
18
+ }
19
+ }
20
+ return acc;
21
+ }
22
+ static async getSourceFiles(srcPaths) {
23
+ return (await Promise.all(srcPaths.map(srcPath => SourceFileScanner.walkSourceFiles(srcPath, srcPaths)))).flat();
24
+ }
25
+ }
26
+
27
+ export { SourceFileScanner as default };
@@ -0,0 +1,14 @@
1
+ function setNestedProperty(obj, keyPath, value) {
2
+ const keys = keyPath.split('.');
3
+ let current = obj;
4
+ for (let i = 0; i < keys.length - 1; i++) {
5
+ const key = keys[i];
6
+ if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
7
+ current[key] = {};
8
+ }
9
+ current = current[key];
10
+ }
11
+ current[keys[keys.length - 1]] = value;
12
+ }
13
+
14
+ export { setNestedProperty };
@@ -0,0 +1,222 @@
1
+ class POParser {
2
+ static KEYWORDS = (() => ({
3
+ MSGID: 'msgid',
4
+ MSGSTR: 'msgstr',
5
+ MSGCTXT: 'msgctxt',
6
+ MSGID_PLURAL: 'msgid_plural'
7
+ }))();
8
+ static COMMENTS = (() => ({
9
+ REFERENCE: '#:',
10
+ EXTRACTED: '#.',
11
+ TRANSLATOR: '#',
12
+ FLAG: '#,',
13
+ PREVIOUS: '#|'
14
+ }))();
15
+ static NAMESPACE_SEPARATOR = '.';
16
+ static QUOTE = '"';
17
+ static NEWLINE = '\\n';
18
+ static FILE_COLUMN_SEPARATOR = ':';
19
+ static META_SEPARATOR = ':';
20
+ static parse(content) {
21
+ const lines = POParser.splitLines(content);
22
+ const messages = [];
23
+ const meta = {};
24
+ let state = 'entry';
25
+ let entry;
26
+ for (let i = 0; i < lines.length; i++) {
27
+ const line = lines[i].trim();
28
+
29
+ // An empty line indicates the end of an entry
30
+ if (!line) {
31
+ if (state === 'entry' && entry) {
32
+ messages.push(POParser.finishEntry(entry));
33
+ entry = undefined;
34
+ }
35
+ state = 'entry';
36
+ continue;
37
+ }
38
+ if (state === 'meta') {
39
+ if (line.startsWith(POParser.QUOTE)) {
40
+ const metaLine = POParser.extractQuotedString(line, state);
41
+ const cleaned = metaLine.endsWith(POParser.NEWLINE) ? metaLine.slice(0, -2) : metaLine;
42
+ const separatorIndex = cleaned.indexOf(POParser.META_SEPARATOR);
43
+ if (separatorIndex > 0) {
44
+ const key = cleaned.substring(0, separatorIndex).trim();
45
+ const value = cleaned.substring(separatorIndex + 1).trim();
46
+ meta[key] = value;
47
+ }
48
+ } else {
49
+ POParser.throwWithLine('Encountered unexpected non-quoted metadata line', line);
50
+ }
51
+ } else {
52
+ // Unsupported comment types
53
+ if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.TRANSLATOR)) {
54
+ POParser.throwWithLine('Translator comments (#) are not supported, use inline descriptions instead', line);
55
+ }
56
+ if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.FLAG)) {
57
+ POParser.throwWithLine('Flag comments (#,) are not supported', line);
58
+ }
59
+ if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.PREVIOUS)) {
60
+ POParser.throwWithLine('Previous string key comments (#|) are not supported', line);
61
+ }
62
+
63
+ // Reference comments
64
+ if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.REFERENCE)) {
65
+ entry = POParser.ensureEntry(entry);
66
+ // Only use the path part, ignore line and column numbers
67
+ const path = line.substring(POParser.COMMENTS.REFERENCE.length).trim().split(POParser.FILE_COLUMN_SEPARATOR).at(0);
68
+ entry.references ??= [];
69
+ entry.references.push({
70
+ path
71
+ });
72
+ continue;
73
+ }
74
+
75
+ // Extracted comments
76
+ if (POParser.lineStartsWithPrefix(line, POParser.COMMENTS.EXTRACTED)) {
77
+ entry = POParser.ensureEntry(entry);
78
+ entry.description = line.substring(POParser.COMMENTS.EXTRACTED.length).trim();
79
+ continue;
80
+ }
81
+
82
+ // Check for unsupported features
83
+ if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGID_PLURAL)) {
84
+ POParser.throwWithLine('Plural forms (msgid_plural) are not supported, use ICU pluralization instead', line);
85
+ }
86
+
87
+ // msgctxt
88
+ if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGCTXT)) {
89
+ entry = POParser.ensureEntry(entry);
90
+ entry.msgctxt = POParser.extractQuotedString(line.substring(POParser.KEYWORDS.MSGCTXT.length + 1), state);
91
+ continue;
92
+ }
93
+
94
+ // msgid
95
+ if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGID)) {
96
+ entry = POParser.ensureEntry(entry);
97
+ entry.msgid = POParser.extractQuotedString(line.substring(POParser.KEYWORDS.MSGID.length + 1), state);
98
+ if (POParser.isMetaEntry(entry, messages)) {
99
+ state = 'meta';
100
+ entry = undefined;
101
+ }
102
+ continue;
103
+ }
104
+
105
+ // msgstr
106
+ if (POParser.lineStartsWithPrefix(line, POParser.KEYWORDS.MSGSTR)) {
107
+ entry = POParser.ensureEntry(entry);
108
+ entry.msgstr = POParser.extractQuotedString(line.substring(POParser.KEYWORDS.MSGSTR.length + 1), state);
109
+ if (POParser.isMetaEntry(entry, messages)) {
110
+ state = 'meta';
111
+ entry = undefined;
112
+ }
113
+ continue;
114
+ }
115
+
116
+ // Multi-line strings are not supported in entry mode
117
+ if (line.startsWith(POParser.QUOTE)) {
118
+ POParser.throwWithLine('Multi-line strings are not supported, use single-line strings instead', line);
119
+ }
120
+ }
121
+ }
122
+
123
+ // Finish any remaining entry
124
+ if (state === 'entry' && entry) {
125
+ messages.push(POParser.finishEntry(entry));
126
+ }
127
+ return {
128
+ meta: Object.keys(meta).length > 0 ? meta : undefined,
129
+ messages: messages.length > 0 ? messages : undefined
130
+ };
131
+ }
132
+ static isMetaEntry(entry, messages) {
133
+ return messages.length === 0 && entry.msgid === '' && entry.msgstr === '';
134
+ }
135
+ static serialize(catalog) {
136
+ const lines = [];
137
+
138
+ // Metadata
139
+ if (catalog.meta) {
140
+ lines.push(`${POParser.KEYWORDS.MSGID} ${POParser.QUOTE}${POParser.QUOTE}`);
141
+ lines.push(`${POParser.KEYWORDS.MSGSTR} ${POParser.QUOTE}${POParser.QUOTE}`);
142
+ for (const [key, value] of Object.entries(catalog.meta)) {
143
+ lines.push(`${POParser.QUOTE}${key}${POParser.META_SEPARATOR} ${value}${POParser.NEWLINE}${POParser.QUOTE}`);
144
+ }
145
+ lines.push('');
146
+ }
147
+
148
+ // Messages
149
+ if (catalog.messages) {
150
+ for (const message of catalog.messages) {
151
+ if (message.description) {
152
+ lines.push(`${POParser.COMMENTS.EXTRACTED} ${message.description}`);
153
+ }
154
+ if (message.references && message.references.length > 0) {
155
+ for (const ref of message.references) {
156
+ lines.push(`${POParser.COMMENTS.REFERENCE} ${ref.path}`);
157
+ }
158
+ }
159
+ let msgctxt;
160
+ let msgid;
161
+ const lastDotIndex = message.id.lastIndexOf(POParser.NAMESPACE_SEPARATOR);
162
+ if (lastDotIndex > 0) {
163
+ msgctxt = message.id.substring(0, lastDotIndex);
164
+ msgid = message.id.substring(lastDotIndex + 1);
165
+ } else {
166
+ msgid = message.id;
167
+ }
168
+ if (msgctxt) {
169
+ lines.push(`${POParser.KEYWORDS.MSGCTXT} ${POParser.QUOTE}${msgctxt}${POParser.QUOTE}`);
170
+ }
171
+ lines.push(`${POParser.KEYWORDS.MSGID} ${POParser.QUOTE}${msgid}${POParser.QUOTE}`);
172
+ lines.push(`${POParser.KEYWORDS.MSGSTR} ${POParser.QUOTE}${message.message}${POParser.QUOTE}`);
173
+ lines.push('');
174
+ }
175
+ }
176
+ return lines.join('\n');
177
+ }
178
+ static lineStartsWithPrefix(line, prefix) {
179
+ return line.startsWith(prefix + ' ');
180
+ }
181
+ static throwWithLine(message, line) {
182
+ throw new Error(`${message}:\n> ${line}`);
183
+ }
184
+ static splitLines(content) {
185
+ // Avoid overhead for Unix newlines only
186
+ if (content.includes('\r')) {
187
+ content = content.replace(/\r\n/g, '\n');
188
+ }
189
+ return content.split('\n');
190
+ }
191
+ static ensureEntry(entry) {
192
+ return entry || {};
193
+ }
194
+ static finishEntry(entry) {
195
+ if (entry.msgid == null || entry.msgstr == null) {
196
+ throw new Error('Incomplete message entry: both msgid and msgstr are required');
197
+ }
198
+ let fullId = entry.msgid;
199
+ if (entry.msgctxt) {
200
+ fullId = entry.msgctxt + POParser.NAMESPACE_SEPARATOR + entry.msgid;
201
+ }
202
+ return {
203
+ id: fullId,
204
+ message: entry.msgstr,
205
+ description: entry.description,
206
+ references: entry.references
207
+ };
208
+ }
209
+ static extractQuotedString(line, state) {
210
+ const trimmed = line.trim();
211
+ const endIndex = trimmed.indexOf(POParser.QUOTE, POParser.QUOTE.length);
212
+ if (endIndex === -1) {
213
+ if (state === 'meta') {
214
+ return trimmed.substring(POParser.QUOTE.length);
215
+ }
216
+ POParser.throwWithLine('Incomplete quoted string', line);
217
+ }
218
+ return trimmed.substring(POParser.QUOTE.length, endIndex);
219
+ }
220
+ }
221
+
222
+ export { POParser as default };
@@ -0,0 +1 @@
1
+ export { default as unstable_extractMessages } from './extractor/extractMessages.js';
@@ -1,3 +1,4 @@
1
1
  export { useFormatter, useTranslations } from './react-client/index.js';
2
2
  export { default as NextIntlClientProvider } from './shared/NextIntlClientProvider.js';
3
- export * from 'use-intl';
3
+ export { IntlProvider, _useExtracted as useExtracted, useLocale, useMessages, useNow, useTimeZone } from 'use-intl/react';
4
+ export * from 'use-intl/core';
@@ -5,4 +5,5 @@ export { default as useNow } from './react-server/useNow.js';
5
5
  export { default as useTimeZone } from './react-server/useTimeZone.js';
6
6
  export { default as useMessages } from './react-server/useMessages.js';
7
7
  export { default as NextIntlClientProvider } from './react-server/NextIntlClientProviderServer.js';
8
+ export { default as useExtracted } from './react-server/useExtracted.js';
8
9
  export * from 'use-intl/core';
@@ -1,6 +1,6 @@
1
- import createMessagesDeclaration from './createMessagesDeclaration.js';
2
1
  import getNextConfig from './getNextConfig.js';
3
2
  import { warn } from './utils.js';
3
+ import createMessagesDeclaration from './declaration/createMessagesDeclaration.js';
4
4
 
5
5
  function initPlugin(pluginConfig, nextConfig) {
6
6
  if (nextConfig?.i18n != null) {
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { throwError } from './utils.js';
4
- import watchFile from './watchFile.js';
3
+ import { throwError } from '../utils.js';
4
+ import watchFile from '../watchFile.js';
5
5
 
6
6
  function runOnce(fn) {
7
7
  if (process.env._NEXT_INTL_COMPILE_MESSAGES === '1') {
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import hasStableTurboConfig from './hasStableTurboConfig.js';
3
+ import SourceFileFilter from '../extractor/source/SourceFileFilter.js';
4
+ import { isNextJs16OrHigher, hasStableTurboConfig } from './nextFlags.js';
4
5
  import { throwError } from './utils.js';
5
6
 
6
7
  function withExtensions(localPath) {
@@ -39,22 +40,99 @@ const withNextIntl = createNextIntlPlugin(
39
40
  function getNextConfig(pluginConfig, nextConfig) {
40
41
  const useTurbo = process.env.TURBOPACK != null;
41
42
  const nextIntlConfig = {};
42
-
43
- // Assign alias for `next-intl/config`
43
+ function getExtractMessagesLoaderConfig() {
44
+ const experimental = pluginConfig.experimental;
45
+ if (!experimental.srcPath || !experimental.messages) {
46
+ throwError('`srcPath` and `messages` are required when using `extractor`.');
47
+ }
48
+ return {
49
+ loader: 'next-intl/extractor/extractionLoader',
50
+ options: {
51
+ srcPath: experimental.srcPath,
52
+ sourceLocale: experimental.extract.sourceLocale,
53
+ messages: experimental.messages
54
+ }
55
+ };
56
+ }
57
+ function getCatalogLoaderConfig() {
58
+ return {
59
+ loader: 'next-intl/extractor/catalogLoader',
60
+ options: {
61
+ messages: pluginConfig.experimental.messages
62
+ }
63
+ };
64
+ }
65
+ function getTurboRules() {
66
+ return nextConfig?.turbopack?.rules ||
67
+ // @ts-expect-error -- For Next.js <16
68
+ nextConfig?.experimental?.turbo?.rules || {};
69
+ }
70
+ function addTurboRule(rules, glob, rule) {
71
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
72
+ if (rules[glob]) {
73
+ if (Array.isArray(rules[glob])) {
74
+ rules[glob].push(rule);
75
+ } else {
76
+ rules[glob] = [rules[glob], rule];
77
+ }
78
+ } else {
79
+ rules[glob] = rule;
80
+ }
81
+ }
44
82
  if (useTurbo) {
45
- if (pluginConfig.requestConfig?.startsWith('/')) {
83
+ if (pluginConfig.requestConfig && path.isAbsolute(pluginConfig.requestConfig)) {
46
84
  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);
47
85
  }
86
+
87
+ // Assign alias for `next-intl/config`
48
88
  const resolveAlias = {
49
89
  // Turbo aliases don't work with absolute
50
90
  // paths (see error handling above)
51
91
  'next-intl/config': resolveI18nPath(pluginConfig.requestConfig)
52
92
  };
53
- if (hasStableTurboConfig &&
93
+
94
+ // Add loaders
95
+ let rules;
96
+
97
+ // Add loader for extractor
98
+ if (pluginConfig.experimental?.extract) {
99
+ if (!isNextJs16OrHigher()) {
100
+ throwError('Message extraction requires Next.js 16 or higher.');
101
+ }
102
+ rules ??= getTurboRules();
103
+ addTurboRule(rules, `*.{${SourceFileFilter.EXTENSIONS.join(',')}}`, {
104
+ loaders: [getExtractMessagesLoaderConfig()],
105
+ condition: {
106
+ // Note: We don't need `not: 'foreign'`, because this is
107
+ // implied by the filter based on `srcPath`.
108
+ path: (Array.isArray(pluginConfig.experimental.srcPath) ? `{${pluginConfig.experimental.srcPath.join(',')}}` : pluginConfig.experimental.srcPath) + '/**/*',
109
+ content: /(useExtracted|getExtracted)/
110
+ }
111
+ });
112
+ }
113
+
114
+ // Add loader for catalog
115
+ if (pluginConfig.experimental?.messages) {
116
+ if (!isNextJs16OrHigher()) {
117
+ throwError('Message catalog loading requires Next.js 16 or higher.');
118
+ }
119
+ rules ??= getTurboRules();
120
+ addTurboRule(rules, `*.${pluginConfig.experimental.messages.format}`, {
121
+ loaders: [getCatalogLoaderConfig()],
122
+ condition: {
123
+ path: `${pluginConfig.experimental.messages.path}/**/*`
124
+ },
125
+ as: '*.js'
126
+ });
127
+ }
128
+ if (hasStableTurboConfig() &&
54
129
  // @ts-expect-error -- For Next.js <16
55
130
  !nextConfig?.experimental?.turbo) {
56
131
  nextIntlConfig.turbopack = {
57
132
  ...nextConfig?.turbopack,
133
+ ...(rules && {
134
+ rules
135
+ }),
58
136
  resolveAlias: {
59
137
  ...nextConfig?.turbopack?.resolveAlias,
60
138
  ...resolveAlias
@@ -67,6 +145,9 @@ function getNextConfig(pluginConfig, nextConfig) {
67
145
  turbo: {
68
146
  // @ts-expect-error -- For Next.js <16
69
147
  ...nextConfig?.experimental?.turbo,
148
+ ...(rules && {
149
+ rules
150
+ }),
70
151
  resolveAlias: {
71
152
  // @ts-expect-error -- For Next.js <16
72
153
  ...nextConfig?.experimental?.turbo?.resolveAlias,
@@ -76,11 +157,39 @@ function getNextConfig(pluginConfig, nextConfig) {
76
157
  };
77
158
  }
78
159
  } else {
79
- nextIntlConfig.webpack = function webpack(...[config, options]) {
80
- // Webpack requires absolute paths
160
+ nextIntlConfig.webpack = function webpack(config, context) {
161
+ if (!config.resolve) config.resolve = {};
162
+ if (!config.resolve.alias) config.resolve.alias = {};
163
+
164
+ // Assign alias for `next-intl/config`
165
+ // (Webpack requires absolute paths)
81
166
  config.resolve.alias['next-intl/config'] = path.resolve(config.context, resolveI18nPath(pluginConfig.requestConfig, config.context));
167
+
168
+ // Add loader for extractor
169
+ if (pluginConfig.experimental?.extract) {
170
+ if (!config.module) config.module = {};
171
+ if (!config.module.rules) config.module.rules = [];
172
+ const srcPath = pluginConfig.experimental.srcPath;
173
+ config.module.rules.push({
174
+ test: new RegExp(`\\.(${SourceFileFilter.EXTENSIONS.join('|')})$`),
175
+ include: Array.isArray(srcPath) ? srcPath.map(cur => path.resolve(config.context, cur)) : path.resolve(config.context, srcPath || ''),
176
+ use: [getExtractMessagesLoaderConfig()]
177
+ });
178
+ }
179
+
180
+ // Add loader for catalog
181
+ if (pluginConfig.experimental?.messages) {
182
+ if (!config.module) config.module = {};
183
+ if (!config.module.rules) config.module.rules = [];
184
+ config.module.rules.push({
185
+ test: new RegExp(`\\.${pluginConfig.experimental.messages.format}$`),
186
+ include: path.resolve(config.context, pluginConfig.experimental.messages.path),
187
+ use: [getCatalogLoaderConfig()],
188
+ type: 'javascript/auto'
189
+ });
190
+ }
82
191
  if (typeof nextConfig?.webpack === 'function') {
83
- return nextConfig.webpack(config, options);
192
+ return nextConfig.webpack(config, context);
84
193
  }
85
194
  return config;
86
195
  };
@@ -22,6 +22,11 @@ function compareVersions(version1, version2) {
22
22
  }
23
23
  return 0;
24
24
  }
25
- const hasStableTurboConfig = compareVersions(getCurrentVersion(), '15.3.0') >= 0;
25
+ function hasStableTurboConfig() {
26
+ return compareVersions(getCurrentVersion(), '15.3.0') >= 0;
27
+ }
28
+ function isNextJs16OrHigher() {
29
+ return compareVersions(getCurrentVersion(), '16.0.0') >= 0;
30
+ }
26
31
 
27
- export { hasStableTurboConfig as default };
32
+ export { hasStableTurboConfig, isNextJs16OrHigher };
@@ -1,5 +1,4 @@
1
1
  import { useFormatter as useFormatter$1, useTranslations as useTranslations$1 } from 'use-intl';
2
- export * from 'use-intl';
3
2
 
4
3
  /**
5
4
  * This is the main entry file when non-'react-server'
@@ -0,0 +1,9 @@
1
+ import getServerExtractor from '../server/react-server/getServerExtractor.js';
2
+ import useConfig from './useConfig.js';
3
+
4
+ function useExtracted(namespace) {
5
+ const config = useConfig('useExtracted');
6
+ return getServerExtractor(config, namespace);
7
+ }
8
+
9
+ export { useExtracted as default };
@@ -21,6 +21,7 @@ const getNow = notSupported('getNow');
21
21
  const getTimeZone = notSupported('getTimeZone');
22
22
  const getMessages = notSupported('getMessages');
23
23
  const getLocale = notSupported('getLocale');
24
+ const getExtracted = notSupported('getExtracted');
24
25
 
25
26
  // The type of `getTranslations` is not assigned here because it
26
27
  // causes a type error. The types use the `react-server` entry
@@ -28,4 +29,4 @@ const getLocale = notSupported('getLocale');
28
29
  const getTranslations = notSupported('getTranslations');
29
30
  const setRequestLocale = notSupported('setRequestLocale');
30
31
 
31
- export { getFormatter, getLocale, getMessages, getNow, getRequestConfig, getTimeZone, getTranslations, setRequestLocale };
32
+ export { getExtracted, getFormatter, getLocale, getMessages, getNow, getRequestConfig, getTimeZone, getTranslations, setRequestLocale };
@@ -0,0 +1,24 @@
1
+ import { cache } from 'react';
2
+ import getConfig from './getConfig.js';
3
+ import getServerExtractor from './getServerExtractor.js';
4
+
5
+ // Call signature 1: `getExtracted(namespace)`
6
+
7
+ // Call signature 2: `getExtracted({locale, namespace})`
8
+
9
+ // Implementation
10
+ async function getExtractedImpl(namespaceOrOpts) {
11
+ let namespace;
12
+ let locale;
13
+ if (typeof namespaceOrOpts === 'string') {
14
+ namespace = namespaceOrOpts;
15
+ } else if (namespaceOrOpts) {
16
+ locale = namespaceOrOpts.locale;
17
+ namespace = namespaceOrOpts.namespace;
18
+ }
19
+ const config = await getConfig(locale);
20
+ return getServerExtractor(config, namespace);
21
+ }
22
+ const getExtracted = cache(getExtractedImpl);
23
+
24
+ export { getExtracted as default };
@@ -0,0 +1,38 @@
1
+ import { cache } from 'react';
2
+ import getServerTranslator from './getServerTranslator.js';
3
+
4
+ // Note: This API is usually compiled into `useTranslations`,
5
+ // but there is some fallback handling which allows this hook
6
+ // to still work when not being compiled.
7
+ //
8
+ // This is relevant for:
9
+ // - Isolated environments like tests, Storybook, etc.
10
+ // - Fallbacks in case an extracted message is not yet available
11
+ function getServerExtractorImpl(config, namespace) {
12
+ const t = getServerTranslator(config, namespace);
13
+ function translateFn(...[message, values, formats]) {
14
+ return t(undefined, values, formats,
15
+ // @ts-expect-error -- Secret fallback parameter
16
+ message );
17
+ }
18
+ translateFn.rich = function translateRichFn(...[message, values, formats]) {
19
+ return t.rich(undefined, values, formats,
20
+ // @ts-expect-error -- Secret fallback parameter
21
+ message );
22
+ };
23
+ translateFn.markup = function translateMarkupFn(...[message, values, formats]) {
24
+ return t.markup(undefined, values, formats,
25
+ // @ts-expect-error -- Secret fallback parameter
26
+ message );
27
+ };
28
+ translateFn.has = function translateHasFn(
29
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
30
+ ...[message]) {
31
+ // Not really something better we can do here
32
+ return true;
33
+ };
34
+ return translateFn;
35
+ }
36
+ var getServerExtractor = cache(getServerExtractorImpl);
37
+
38
+ export { getServerExtractor as default };
@@ -1 +1 @@
1
- export { getFormatter, getLocale, getMessages, getNow, getRequestConfig, getTimeZone, getTranslations, setRequestLocale } from './server/react-client/index.js';
1
+ export { getExtracted, getFormatter, getLocale, getMessages, getNow, getRequestConfig, getTimeZone, getTranslations, setRequestLocale } from './server/react-client/index.js';