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