next-intl 4.11.2 → 4.12.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/{JSONCodec-B-lAnRTg.cjs → JSONCodec-CzA8ubPy.cjs} +4 -2
- package/dist/cjs/development/{POCodec-0XdsL-1F.cjs → POCodec-CWGHK-Gp.cjs} +16 -10
- package/dist/cjs/development/{plugin-0S9vVrVM.cjs → plugin-DlFYUFWh.cjs} +281 -150
- package/dist/cjs/development/plugin.cjs +1 -1
- package/dist/esm/development/extractor/catalog/CatalogManager.js +146 -95
- package/dist/esm/development/extractor/extractMessages.js +9 -2
- package/dist/esm/development/extractor/format/codecs/JSONCodec.js +3 -1
- package/dist/esm/development/extractor/format/codecs/POCodec.js +15 -9
- package/dist/esm/development/extractor/normalizeExtractorConfig.js +70 -0
- package/dist/esm/development/extractor/source/SourceFileWatcher.js +1 -1
- package/dist/esm/development/extractor/utils.js +29 -5
- package/dist/esm/development/plugin/createNextIntlPlugin.js +13 -2
- package/dist/esm/development/plugin/extractor/initExtractionCompiler.js +3 -8
- package/dist/esm/development/plugin/getNextConfig.js +21 -34
- package/dist/esm/development/server/react-server/getServerExtractor.js +3 -3
- package/dist/esm/production/extractor/catalog/CatalogManager.js +1 -1
- package/dist/esm/production/extractor/extractMessages.js +1 -1
- package/dist/esm/production/extractor/format/codecs/JSONCodec.js +1 -1
- package/dist/esm/production/extractor/format/codecs/POCodec.js +1 -1
- package/dist/esm/production/extractor/normalizeExtractorConfig.js +1 -0
- 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/extractor/initExtractionCompiler.js +1 -1
- package/dist/esm/production/plugin/getNextConfig.js +1 -1
- package/dist/esm/production/server/react-server/getServerExtractor.js +1 -1
- package/dist/types/extractor/ExtractionCompiler.d.ts +2 -1
- package/dist/types/extractor/catalog/CatalogLocales.d.ts +2 -2
- package/dist/types/extractor/catalog/CatalogManager.d.ts +27 -10
- package/dist/types/extractor/extractMessages.d.ts +2 -2
- package/dist/types/extractor/extractor/MessageExtractor.d.ts +2 -6
- package/dist/types/extractor/normalizeExtractorConfig.d.ts +5 -0
- package/dist/types/extractor/types.d.ts +62 -11
- package/dist/types/extractor/utils.d.ts +2 -1
- package/dist/types/plugin/extractor/initExtractionCompiler.d.ts +2 -2
- package/dist/types/plugin/getNextConfig.d.ts +2 -1
- package/dist/types/plugin/types.d.ts +10 -14
- package/package.json +5 -5
|
@@ -39,6 +39,73 @@ function once(namespace) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function stripTrailingSlash(dirPath) {
|
|
43
|
+
if (dirPath.endsWith('/')) {
|
|
44
|
+
return dirPath.slice(0, -1);
|
|
45
|
+
} else {
|
|
46
|
+
return dirPath;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function normalizeMessagesCatalogPaths(messagesPath) {
|
|
50
|
+
const rawPaths = Array.isArray(messagesPath) ? messagesPath : [messagesPath];
|
|
51
|
+
return rawPaths.map(dirPath => stripTrailingSlash(String(dirPath).trim())).filter(dirPath => dirPath.length > 0);
|
|
52
|
+
}
|
|
53
|
+
function normalizeExtractorConfig(input) {
|
|
54
|
+
if (input.messages == null) {
|
|
55
|
+
throwError('`messages` is required when extracting messages.');
|
|
56
|
+
}
|
|
57
|
+
const extract = input.extract;
|
|
58
|
+
let extractPath;
|
|
59
|
+
let sourceLocale;
|
|
60
|
+
if (extract !== undefined && extract !== true) {
|
|
61
|
+
if (extract.sourceLocale) {
|
|
62
|
+
warn('`extract.sourceLocale` is deprecated in favor of `messages.sourceLocale`.');
|
|
63
|
+
sourceLocale = extract.sourceLocale;
|
|
64
|
+
}
|
|
65
|
+
if (extract.path) {
|
|
66
|
+
extractPath = stripTrailingSlash(extract.path);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const locales = input.messages.locales;
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
71
|
+
if (!locales) {
|
|
72
|
+
throwError('`messages.locales` is required when extracting messages.');
|
|
73
|
+
}
|
|
74
|
+
if (input.messages.sourceLocale) {
|
|
75
|
+
sourceLocale = input.messages.sourceLocale;
|
|
76
|
+
}
|
|
77
|
+
if (!sourceLocale) {
|
|
78
|
+
throwError('`messages.sourceLocale` is required when extracting messages.');
|
|
79
|
+
}
|
|
80
|
+
const srcPath = input.srcPath;
|
|
81
|
+
if (srcPath == null) {
|
|
82
|
+
throwError('`srcPath` is required when extracting messages.');
|
|
83
|
+
}
|
|
84
|
+
const pathIsArray = Array.isArray(input.messages.path);
|
|
85
|
+
const messagesPath = normalizeMessagesCatalogPaths(input.messages.path);
|
|
86
|
+
if (messagesPath.length === 0) {
|
|
87
|
+
throwError('`messages.path` must not be empty.');
|
|
88
|
+
}
|
|
89
|
+
if (extractPath == null) {
|
|
90
|
+
if (pathIsArray) {
|
|
91
|
+
throwError('When `messages.path` is an array, `extract.path` is required to select the writable catalog directory.');
|
|
92
|
+
}
|
|
93
|
+
extractPath = messagesPath[0];
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
extract: {
|
|
97
|
+
locales,
|
|
98
|
+
path: extractPath,
|
|
99
|
+
sourceLocale,
|
|
100
|
+
srcPath
|
|
101
|
+
},
|
|
102
|
+
messages: {
|
|
103
|
+
format: input.messages.format,
|
|
104
|
+
path: messagesPath
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
42
109
|
/**
|
|
43
110
|
* Wrapper around `fs.watch` that provides a workaround
|
|
44
111
|
* for https://github.com/nodejs/node/issues/5039.
|
|
@@ -122,11 +189,11 @@ export default messages;`;
|
|
|
122
189
|
|
|
123
190
|
const formats = {
|
|
124
191
|
json: {
|
|
125
|
-
codec: () => Promise.resolve().then(function () { return require('./JSONCodec-
|
|
192
|
+
codec: () => Promise.resolve().then(function () { return require('./JSONCodec-CzA8ubPy.cjs'); }),
|
|
126
193
|
extension: '.json'
|
|
127
194
|
},
|
|
128
195
|
po: {
|
|
129
|
-
codec: () => Promise.resolve().then(function () { return require('./POCodec-
|
|
196
|
+
codec: () => Promise.resolve().then(function () { return require('./POCodec-CWGHK-Gp.cjs'); }),
|
|
130
197
|
extension: '.po'
|
|
131
198
|
}
|
|
132
199
|
};
|
|
@@ -231,7 +298,7 @@ class SourceFileWatcher {
|
|
|
231
298
|
}
|
|
232
299
|
const filtered = await this.normalizeEvents(events);
|
|
233
300
|
if (filtered.length > 0) {
|
|
234
|
-
|
|
301
|
+
await this.onChange(filtered);
|
|
235
302
|
}
|
|
236
303
|
}, {
|
|
237
304
|
ignore
|
|
@@ -346,6 +413,12 @@ const FORBIDDEN_OBJECT_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
|
346
413
|
function isForbiddenObjectKey(key) {
|
|
347
414
|
return FORBIDDEN_OBJECT_KEYS.has(key);
|
|
348
415
|
}
|
|
416
|
+
function hasLocalesToExtract(config) {
|
|
417
|
+
const {
|
|
418
|
+
locales
|
|
419
|
+
} = config.extract;
|
|
420
|
+
return locales === 'infer' || locales.length > 0;
|
|
421
|
+
}
|
|
349
422
|
|
|
350
423
|
// Essentialls lodash/set, but we avoid this dependency
|
|
351
424
|
function setNestedProperty(obj, keyPath, value) {
|
|
@@ -366,17 +439,34 @@ function setNestedProperty(obj, keyPath, value) {
|
|
|
366
439
|
current[keys[keys.length - 1]] = value;
|
|
367
440
|
}
|
|
368
441
|
function getSortedMessages(messages) {
|
|
442
|
+
const warnedMissingReferenceIds = new Set();
|
|
369
443
|
return messages.toSorted((messageA, messageB) => {
|
|
370
|
-
const refA = messageA.references
|
|
371
|
-
const refB = messageB.references
|
|
444
|
+
const refA = messageA.references[0];
|
|
445
|
+
const refB = messageB.references[0];
|
|
372
446
|
|
|
373
|
-
//
|
|
374
|
-
if (
|
|
447
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
448
|
+
if (refA == null) {
|
|
449
|
+
warnAboutMissingReference(messageA.id, warnedMissingReferenceIds);
|
|
450
|
+
}
|
|
451
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
452
|
+
if (refB == null) {
|
|
453
|
+
warnAboutMissingReference(messageB.id, warnedMissingReferenceIds);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
457
|
+
if (refA == null || refB == null) {
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
375
460
|
|
|
376
461
|
// Sort by path, then line. Same path+line: preserve original order
|
|
377
462
|
return compareReferences(refA, refB);
|
|
378
463
|
});
|
|
379
464
|
}
|
|
465
|
+
function warnAboutMissingReference(id, warnedMissingReferenceIds) {
|
|
466
|
+
if (warnedMissingReferenceIds.has(id)) return;
|
|
467
|
+
warnedMissingReferenceIds.add(id);
|
|
468
|
+
warn(`Missing file reference for extracted message: ${id}`);
|
|
469
|
+
}
|
|
380
470
|
function localeCompare(a, b) {
|
|
381
471
|
return a.localeCompare(b, 'en');
|
|
382
472
|
}
|
|
@@ -613,15 +703,27 @@ class SaveScheduler {
|
|
|
613
703
|
|
|
614
704
|
class CatalogManager {
|
|
615
705
|
/**
|
|
616
|
-
*
|
|
617
|
-
*
|
|
706
|
+
* Extraction-derived fields aggregated into `ExtractorMessage`.
|
|
707
|
+
* Source code is the source of truth for these fields, only ancillary
|
|
708
|
+
* codec fields may merge from disk (e.g. flags).
|
|
709
|
+
*/
|
|
710
|
+
static extractorOwnedAggregatorKeys = new Set(['description', 'id', 'message', 'references']);
|
|
711
|
+
/**
|
|
712
|
+
* Source of truth for statically extracted source messages,
|
|
713
|
+
* grouped by file and message ID.
|
|
618
714
|
*/
|
|
619
|
-
|
|
715
|
+
sourceMessagesByFile = new Map();
|
|
620
716
|
|
|
621
717
|
/**
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
718
|
+
* Reverse index for rebuilding aggregated messages without scanning all files.
|
|
719
|
+
* Contains the same `SourceMessage` arrays as `sourceMessagesByFile` and is
|
|
720
|
+
* kept in sync with it.
|
|
721
|
+
*/
|
|
722
|
+
sourceMessagesById = new Map();
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Fast lookup for messages by ID, aggregated across all files. This combines
|
|
726
|
+
* metadata from `sourceMessagesById`, e.g. references and descriptions.
|
|
625
727
|
*/
|
|
626
728
|
messagesById = new Map();
|
|
627
729
|
|
|
@@ -640,7 +742,7 @@ class CatalogManager {
|
|
|
640
742
|
|
|
641
743
|
constructor(config, opts) {
|
|
642
744
|
this.config = config;
|
|
643
|
-
this.saveScheduler = new SaveScheduler(50);
|
|
745
|
+
this.saveScheduler = new SaveScheduler(opts.saveDebounceMs ?? 50);
|
|
644
746
|
this.projectRoot = opts.projectRoot ?? getDefaultProjectRoot();
|
|
645
747
|
this.isDevelopment = opts.isDevelopment ?? false;
|
|
646
748
|
this.extractor = opts.extractor;
|
|
@@ -663,7 +765,7 @@ class CatalogManager {
|
|
|
663
765
|
return this.persister;
|
|
664
766
|
} else {
|
|
665
767
|
this.persister = new CatalogPersister({
|
|
666
|
-
messagesPath: this.config.
|
|
768
|
+
messagesPath: this.config.extract.path,
|
|
667
769
|
codec: await this.getCodec(),
|
|
668
770
|
extension: getFormatExtension(this.config.messages.format)
|
|
669
771
|
});
|
|
@@ -674,12 +776,12 @@ class CatalogManager {
|
|
|
674
776
|
if (this.catalogLocales) {
|
|
675
777
|
return this.catalogLocales;
|
|
676
778
|
} else {
|
|
677
|
-
const messagesDir = path__default.default.join(this.projectRoot, this.config.
|
|
779
|
+
const messagesDir = path__default.default.join(this.projectRoot, this.config.extract.path);
|
|
678
780
|
this.catalogLocales = new CatalogLocales({
|
|
679
781
|
messagesDir,
|
|
680
|
-
sourceLocale: this.config.sourceLocale,
|
|
681
782
|
extension: getFormatExtension(this.config.messages.format),
|
|
682
|
-
locales: this.config.
|
|
783
|
+
locales: this.config.extract.locales,
|
|
784
|
+
sourceLocale: this.config.extract.sourceLocale
|
|
683
785
|
});
|
|
684
786
|
return this.catalogLocales;
|
|
685
787
|
}
|
|
@@ -688,15 +790,28 @@ class CatalogManager {
|
|
|
688
790
|
return this.getCatalogLocales().getTargetLocales();
|
|
689
791
|
}
|
|
690
792
|
getSrcPaths() {
|
|
691
|
-
return (Array.isArray(this.config.srcPath) ? this.config.srcPath : [this.config.srcPath]).map(srcPath => path__default.default.join(this.projectRoot, srcPath));
|
|
793
|
+
return (Array.isArray(this.config.extract.srcPath) ? this.config.extract.srcPath : [this.config.extract.srcPath]).map(srcPath => path__default.default.join(this.projectRoot, srcPath));
|
|
692
794
|
}
|
|
693
795
|
async loadMessages() {
|
|
694
796
|
const sourceDiskMessages = await this.loadSourceMessages();
|
|
695
797
|
this.loadCatalogsPromise = this.loadTargetMessages();
|
|
696
798
|
await this.loadCatalogsPromise;
|
|
697
799
|
this.scanCompletePromise = (async () => {
|
|
698
|
-
const sourceFiles = await SourceFileScanner.getSourceFiles(this.getSrcPaths())
|
|
699
|
-
|
|
800
|
+
const sourceFiles = Array.from(await SourceFileScanner.getSourceFiles(this.getSrcPaths()))
|
|
801
|
+
// Stable file order keeps catalog ties independent of processing timing
|
|
802
|
+
.toSorted(localeCompare);
|
|
803
|
+
const extractedFiles = await Promise.all(sourceFiles.map(async filePath => ({
|
|
804
|
+
filePath,
|
|
805
|
+
messages: await this.extractFile(filePath)
|
|
806
|
+
})));
|
|
807
|
+
for (const {
|
|
808
|
+
filePath,
|
|
809
|
+
messages
|
|
810
|
+
} of extractedFiles) {
|
|
811
|
+
if (messages) {
|
|
812
|
+
this.applyFileMessages(filePath, messages);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
700
815
|
this.mergeSourceDiskMetadata(sourceDiskMessages);
|
|
701
816
|
})();
|
|
702
817
|
await this.scanCompletePromise;
|
|
@@ -708,7 +823,7 @@ class CatalogManager {
|
|
|
708
823
|
async loadSourceMessages() {
|
|
709
824
|
// Load source catalog to hydrate metadata (e.g. flags) later without
|
|
710
825
|
// treating catalog entries as source of truth.
|
|
711
|
-
const diskMessages = await this.loadLocaleMessages(this.config.sourceLocale);
|
|
826
|
+
const diskMessages = await this.loadLocaleMessages(this.config.extract.sourceLocale);
|
|
712
827
|
const byId = new Map();
|
|
713
828
|
for (const diskMessage of diskMessages) {
|
|
714
829
|
byId.set(diskMessage.id, diskMessage);
|
|
@@ -728,17 +843,13 @@ class CatalogManager {
|
|
|
728
843
|
}
|
|
729
844
|
async reloadLocaleCatalog(locale) {
|
|
730
845
|
const diskMessages = await this.loadLocaleMessages(locale);
|
|
731
|
-
if (locale === this.config.sourceLocale) {
|
|
846
|
+
if (locale === this.config.extract.sourceLocale) {
|
|
732
847
|
// For source: Merge additional properties like flags
|
|
733
848
|
for (const diskMessage of diskMessages) {
|
|
734
849
|
const prev = this.messagesById.get(diskMessage.id);
|
|
735
850
|
if (prev) {
|
|
736
|
-
// Mutate the existing object instead of creating a copy
|
|
737
|
-
// to keep messagesById and messagesByFile in sync.
|
|
738
|
-
// Unknown properties (like flags): disk wins
|
|
739
|
-
// Known properties: existing (from extraction) wins
|
|
740
851
|
for (const key of Object.keys(diskMessage)) {
|
|
741
|
-
if (!
|
|
852
|
+
if (!CatalogManager.extractorOwnedAggregatorKeys.has(key)) {
|
|
742
853
|
// For unknown properties (like flags), disk wins
|
|
743
854
|
prev[key] = diskMessage[key];
|
|
744
855
|
}
|
|
@@ -770,17 +881,23 @@ class CatalogManager {
|
|
|
770
881
|
const existing = this.messagesById.get(id);
|
|
771
882
|
if (!existing) continue;
|
|
772
883
|
|
|
773
|
-
//
|
|
774
|
-
// This keeps `messagesById` and `messagesByFile` in sync since
|
|
775
|
-
// they reference the same object instance.
|
|
884
|
+
// Fill unknown metadata from disk without replacing extraction-owned fields.
|
|
776
885
|
for (const key of Object.keys(diskMessage)) {
|
|
777
|
-
if (existing[key] == null) {
|
|
886
|
+
if (!CatalogManager.extractorOwnedAggregatorKeys.has(key) && existing[key] == null) {
|
|
778
887
|
existing[key] = diskMessage[key];
|
|
779
888
|
}
|
|
780
889
|
}
|
|
781
890
|
}
|
|
782
891
|
}
|
|
783
892
|
async processFile(absoluteFilePath) {
|
|
893
|
+
const messages = await this.extractFile(absoluteFilePath);
|
|
894
|
+
|
|
895
|
+
// `undefined` only when `extractFile()` throws. An empty array is success
|
|
896
|
+
// and must still run `applyFileMessages` to clear stale ids for this file.
|
|
897
|
+
if (!messages) return false;
|
|
898
|
+
return this.applyFileMessages(absoluteFilePath, messages);
|
|
899
|
+
}
|
|
900
|
+
async extractFile(absoluteFilePath) {
|
|
784
901
|
let messages = [];
|
|
785
902
|
try {
|
|
786
903
|
const content = await fs__default$1.default.readFile(absoluteFilePath, 'utf8');
|
|
@@ -788,7 +905,7 @@ class CatalogManager {
|
|
|
788
905
|
try {
|
|
789
906
|
extraction = await this.extractor.extract(absoluteFilePath, content);
|
|
790
907
|
} catch {
|
|
791
|
-
return
|
|
908
|
+
return undefined;
|
|
792
909
|
}
|
|
793
910
|
messages = extraction.messages;
|
|
794
911
|
} catch (err) {
|
|
@@ -797,71 +914,91 @@ class CatalogManager {
|
|
|
797
914
|
}
|
|
798
915
|
// ENOENT -> treat as no messages
|
|
799
916
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
917
|
+
return messages;
|
|
918
|
+
}
|
|
919
|
+
applyFileMessages(absoluteFilePath, messages) {
|
|
920
|
+
const prevFileMessages = this.sourceMessagesByFile.get(absoluteFilePath);
|
|
921
|
+
const nextFileMessages = this.groupSourceMessagesById(messages);
|
|
922
|
+
const affectedIds = new Set([...(prevFileMessages?.keys() ?? []), ...nextFileMessages.keys()]);
|
|
923
|
+
if (nextFileMessages.size > 0) {
|
|
924
|
+
this.sourceMessagesByFile.set(absoluteFilePath, nextFileMessages);
|
|
925
|
+
} else {
|
|
926
|
+
this.sourceMessagesByFile.delete(absoluteFilePath);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Clear this file's contribution from the reverse index, then re-insert
|
|
930
|
+
// fresh rows and rebuild aggregates (messagesById) per touched id.
|
|
931
|
+
for (const id of affectedIds) {
|
|
932
|
+
const sourceMessagesForId = this.sourceMessagesById.get(id);
|
|
933
|
+
if (sourceMessagesForId) {
|
|
934
|
+
sourceMessagesForId.delete(absoluteFilePath);
|
|
935
|
+
// No files left for this id: drop the reverse-index entry.
|
|
936
|
+
if (sourceMessagesForId.size === 0) {
|
|
937
|
+
this.sourceMessagesById.delete(id);
|
|
818
938
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
939
|
+
}
|
|
940
|
+
const nextSourceMessagesForId = nextFileMessages.get(id);
|
|
941
|
+
if (nextSourceMessagesForId) {
|
|
942
|
+
let sourceMessagesByFile = this.sourceMessagesById.get(id);
|
|
943
|
+
if (!sourceMessagesByFile) {
|
|
944
|
+
sourceMessagesByFile = new Map();
|
|
945
|
+
this.sourceMessagesById.set(id, sourceMessagesByFile);
|
|
826
946
|
}
|
|
947
|
+
sourceMessagesByFile.set(absoluteFilePath, nextSourceMessagesForId);
|
|
827
948
|
}
|
|
828
|
-
this.
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
if (!message) return;
|
|
840
|
-
const hasOtherReferences = message.references?.some(ref => ref.path !== relativeFilePath);
|
|
841
|
-
if (!hasOtherReferences) {
|
|
842
|
-
// No other references, delete the message entirely
|
|
843
|
-
this.messagesById.delete(id);
|
|
949
|
+
this.rebuildMessageById(id);
|
|
950
|
+
}
|
|
951
|
+
const changed = this.haveMessagesChangedForFile(prevFileMessages, nextFileMessages);
|
|
952
|
+
return changed;
|
|
953
|
+
}
|
|
954
|
+
groupSourceMessagesById(messages) {
|
|
955
|
+
const result = new Map();
|
|
956
|
+
for (const message of messages) {
|
|
957
|
+
const messagesById = result.get(message.id);
|
|
958
|
+
if (messagesById) {
|
|
959
|
+
messagesById.push(message);
|
|
844
960
|
} else {
|
|
845
|
-
|
|
846
|
-
// Mutate the existing object to keep `messagesById` and `messagesByFile` in sync
|
|
847
|
-
message.references = message.references?.filter(ref => ref.path !== relativeFilePath);
|
|
961
|
+
result.set(message.id, [message]);
|
|
848
962
|
}
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
// Update the stored messages
|
|
852
|
-
if (messages.length > 0) {
|
|
853
|
-
this.messagesByFile.set(absoluteFilePath, fileMessages);
|
|
854
|
-
} else {
|
|
855
|
-
this.messagesByFile.delete(absoluteFilePath);
|
|
856
963
|
}
|
|
857
|
-
|
|
858
|
-
return changed;
|
|
964
|
+
return result;
|
|
859
965
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
966
|
+
rebuildMessageById(id) {
|
|
967
|
+
const sourceMessages = Array.from(this.sourceMessagesById.get(id)?.values() ?? []).flat();
|
|
968
|
+
if (sourceMessages.length === 0) {
|
|
969
|
+
this.messagesById.delete(id);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const previousMessage = this.messagesById.get(id);
|
|
973
|
+
const aggregate = {
|
|
974
|
+
description: this.mergeDescriptions(sourceMessages),
|
|
975
|
+
id,
|
|
976
|
+
message: sourceMessages[0].message,
|
|
977
|
+
references: sourceMessages.map(message => message.reference).sort(compareReferences)
|
|
978
|
+
};
|
|
979
|
+
if (previousMessage) {
|
|
980
|
+
for (const key of Object.keys(previousMessage)) {
|
|
981
|
+
// Preserve extra fields (e.g. from disk/codec) across rebuilds; the
|
|
982
|
+
// four core fields above are always recomputed from source messages.
|
|
983
|
+
if (!CatalogManager.extractorOwnedAggregatorKeys.has(key) && aggregate[key] == null) {
|
|
984
|
+
aggregate[key] = previousMessage[key];
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
this.messagesById.set(id, aggregate);
|
|
989
|
+
}
|
|
990
|
+
mergeDescriptions(messages) {
|
|
991
|
+
const sortedByReference = messages.toSorted((a, b) => compareReferences(a.reference, b.reference));
|
|
992
|
+
const merged = [];
|
|
993
|
+
for (const message of sortedByReference) {
|
|
994
|
+
const {
|
|
995
|
+
description
|
|
996
|
+
} = message;
|
|
997
|
+
if (description != null && !merged.includes(description)) {
|
|
998
|
+
merged.push(description);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return merged;
|
|
865
1002
|
}
|
|
866
1003
|
haveMessagesChangedForFile(beforeMessages, afterMessages) {
|
|
867
1004
|
// If one exists and the other doesn't, there's a change
|
|
@@ -875,25 +1012,28 @@ class CatalogManager {
|
|
|
875
1012
|
}
|
|
876
1013
|
|
|
877
1014
|
// Check differences in beforeMessages vs afterMessages
|
|
878
|
-
for (const [id,
|
|
879
|
-
const
|
|
880
|
-
if (!
|
|
1015
|
+
for (const [id, prevSourceMessages] of beforeMessages) {
|
|
1016
|
+
const nextSourceMessages = afterMessages.get(id);
|
|
1017
|
+
if (!nextSourceMessages) {
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
if (!this.areSourceMessageArraysEqual(prevSourceMessages, nextSourceMessages)) {
|
|
881
1021
|
return true; // Early exit on first difference
|
|
882
1022
|
}
|
|
883
1023
|
}
|
|
884
1024
|
return false;
|
|
885
1025
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
return msg1.id === msg2.id && msg1.message === msg2.message && msg1.description === msg2.description;
|
|
1026
|
+
areSourceMessageArraysEqual(messages1, messages2) {
|
|
1027
|
+
return messages1.length === messages2.length && messages1.every((message, index) => this.areSourceMessagesEqual(message, messages2[index]));
|
|
1028
|
+
}
|
|
1029
|
+
areSourceMessagesEqual(msg1, msg2) {
|
|
1030
|
+
return msg1.id === msg2.id && msg1.message === msg2.message && msg1.description === msg2.description && msg1.reference.path === msg2.reference.path && msg1.reference.line === msg2.reference.line;
|
|
891
1031
|
}
|
|
892
1032
|
async save() {
|
|
893
1033
|
return this.saveScheduler.schedule(() => this.saveImpl());
|
|
894
1034
|
}
|
|
895
1035
|
async saveImpl() {
|
|
896
|
-
await this.saveLocale(this.config.sourceLocale);
|
|
1036
|
+
await this.saveLocale(this.config.extract.sourceLocale);
|
|
897
1037
|
const targetLocales = await this.getTargetLocales();
|
|
898
1038
|
await Promise.all(targetLocales.map(locale => this.saveLocale(locale)));
|
|
899
1039
|
}
|
|
@@ -901,7 +1041,7 @@ class CatalogManager {
|
|
|
901
1041
|
await this.loadCatalogsPromise;
|
|
902
1042
|
const messages = Array.from(this.messagesById.values());
|
|
903
1043
|
const persister = await this.getPersister();
|
|
904
|
-
const isSourceLocale = locale === this.config.sourceLocale;
|
|
1044
|
+
const isSourceLocale = locale === this.config.extract.sourceLocale;
|
|
905
1045
|
|
|
906
1046
|
// Check if file was modified externally (poll-at-save is cheaper than
|
|
907
1047
|
// watchers here since stat() is fast and avoids continuous overhead)
|
|
@@ -951,8 +1091,9 @@ class CatalogManager {
|
|
|
951
1091
|
await this.scanCompletePromise;
|
|
952
1092
|
}
|
|
953
1093
|
let changed = false;
|
|
954
|
-
const expandedEvents = await this.sourceWatcher.expandDirectoryDeleteEvents(events, Array.from(this.
|
|
955
|
-
|
|
1094
|
+
const expandedEvents = await this.sourceWatcher.expandDirectoryDeleteEvents(events, Array.from(this.sourceMessagesByFile.keys()));
|
|
1095
|
+
// Stable file order keeps catalog ties independent of event timing.
|
|
1096
|
+
for (const event of expandedEvents.toSorted((a, b) => localeCompare(a.path, b.path))) {
|
|
956
1097
|
const hasChanged = await this.processFile(event.path);
|
|
957
1098
|
changed ||= hasChanged;
|
|
958
1099
|
}
|
|
@@ -999,7 +1140,7 @@ class LRUCache {
|
|
|
999
1140
|
}
|
|
1000
1141
|
}
|
|
1001
1142
|
|
|
1002
|
-
const require$2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('plugin-
|
|
1143
|
+
const require$2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('plugin-DlFYUFWh.cjs', document.baseURI).href)));
|
|
1003
1144
|
class MessageExtractor {
|
|
1004
1145
|
compileCache = new LRUCache(750);
|
|
1005
1146
|
constructor(opts) {
|
|
@@ -1104,9 +1245,8 @@ const isDevelopmentOrNextBuild = isDevelopment || isNextBuild;
|
|
|
1104
1245
|
// Single compiler instance, initialized once per process
|
|
1105
1246
|
let compiler;
|
|
1106
1247
|
const runOnce = once('_NEXT_INTL_EXTRACT');
|
|
1107
|
-
function initExtractionCompiler(
|
|
1108
|
-
|
|
1109
|
-
if (!experimental?.extract) {
|
|
1248
|
+
function initExtractionCompiler(extractorConfig) {
|
|
1249
|
+
if (!extractorConfig || !hasLocalesToExtract(extractorConfig)) {
|
|
1110
1250
|
return;
|
|
1111
1251
|
}
|
|
1112
1252
|
|
|
@@ -1125,11 +1265,6 @@ function initExtractionCompiler(pluginConfig) {
|
|
|
1125
1265
|
const shouldRun = isDevelopment || isNextBuild;
|
|
1126
1266
|
if (!shouldRun) return;
|
|
1127
1267
|
runOnce(() => {
|
|
1128
|
-
const extractorConfig = {
|
|
1129
|
-
srcPath: experimental.srcPath,
|
|
1130
|
-
sourceLocale: experimental.extract.sourceLocale,
|
|
1131
|
-
messages: experimental.messages
|
|
1132
|
-
};
|
|
1133
1268
|
compiler = new ExtractionCompiler(extractorConfig, {
|
|
1134
1269
|
isDevelopment,
|
|
1135
1270
|
projectRoot: process.cwd()
|
|
@@ -1157,7 +1292,7 @@ function initExtractionCompiler(pluginConfig) {
|
|
|
1157
1292
|
|
|
1158
1293
|
function getCurrentVersion() {
|
|
1159
1294
|
try {
|
|
1160
|
-
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-
|
|
1295
|
+
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-DlFYUFWh.cjs', document.baseURI).href)));
|
|
1161
1296
|
const pkg = require$1('next/package.json');
|
|
1162
1297
|
return pkg.version;
|
|
1163
1298
|
} catch (error) {
|
|
@@ -1184,7 +1319,7 @@ function isNextJs16OrHigher() {
|
|
|
1184
1319
|
return compareVersions(getCurrentVersion(), '16.0.0') >= 0;
|
|
1185
1320
|
}
|
|
1186
1321
|
|
|
1187
|
-
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-
|
|
1322
|
+
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-DlFYUFWh.cjs', document.baseURI).href)));
|
|
1188
1323
|
function withExtensions(localPath) {
|
|
1189
1324
|
return [`${localPath}.ts`, `${localPath}.tsx`, `${localPath}.js`, `${localPath}.jsx`];
|
|
1190
1325
|
}
|
|
@@ -1229,7 +1364,7 @@ function resolveI18nPath(providedPath, cwd) {
|
|
|
1229
1364
|
}
|
|
1230
1365
|
}
|
|
1231
1366
|
}
|
|
1232
|
-
function getNextConfig(pluginConfig, nextConfig) {
|
|
1367
|
+
function getNextConfig(pluginConfig, nextConfig, extractorConfig) {
|
|
1233
1368
|
const useTurbo = process.env.TURBOPACK != null;
|
|
1234
1369
|
|
|
1235
1370
|
// `experimental-analyze` doesn’t set the TURBOPACK env param. Since Next.js
|
|
@@ -1237,25 +1372,27 @@ function getNextConfig(pluginConfig, nextConfig) {
|
|
|
1237
1372
|
// always configure Turbopack just in case.
|
|
1238
1373
|
const shouldConfigureTurbo = useTurbo || isNextJs16OrHigher();
|
|
1239
1374
|
const nextIntlConfig = {};
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1375
|
+
let messageLoadPaths = [];
|
|
1376
|
+
if (pluginConfig.experimental?.messages) {
|
|
1377
|
+
messageLoadPaths = normalizeMessagesCatalogPaths(pluginConfig.experimental.messages.path);
|
|
1378
|
+
}
|
|
1379
|
+
function getExtractMessagesLoaderConfig(config) {
|
|
1245
1380
|
return {
|
|
1246
1381
|
loader: 'next-intl/extractor/extractionLoader',
|
|
1247
|
-
options:
|
|
1248
|
-
srcPath: experimental.srcPath,
|
|
1249
|
-
sourceLocale: experimental.extract.sourceLocale,
|
|
1250
|
-
messages: pluginConfig.experimental.messages
|
|
1251
|
-
}
|
|
1382
|
+
options: config
|
|
1252
1383
|
};
|
|
1253
1384
|
}
|
|
1254
1385
|
function getCatalogLoaderConfig() {
|
|
1386
|
+
const messages = pluginConfig.experimental.messages;
|
|
1255
1387
|
return {
|
|
1256
1388
|
loader: 'next-intl/extractor/catalogLoader',
|
|
1257
1389
|
options: {
|
|
1258
|
-
messages:
|
|
1390
|
+
messages: {
|
|
1391
|
+
format: messages.format,
|
|
1392
|
+
...(messages.precompile !== undefined && {
|
|
1393
|
+
precompile: messages.precompile
|
|
1394
|
+
})
|
|
1395
|
+
}
|
|
1259
1396
|
}
|
|
1260
1397
|
};
|
|
1261
1398
|
}
|
|
@@ -1276,18 +1413,6 @@ function getNextConfig(pluginConfig, nextConfig) {
|
|
|
1276
1413
|
rules[glob] = rule;
|
|
1277
1414
|
}
|
|
1278
1415
|
}
|
|
1279
|
-
|
|
1280
|
-
// Validate messages config
|
|
1281
|
-
if (pluginConfig.experimental?.messages) {
|
|
1282
|
-
const messages = pluginConfig.experimental.messages;
|
|
1283
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- For non-TS consumers
|
|
1284
|
-
if (!messages.format) {
|
|
1285
|
-
throwError('`format` is required when using `messages`.');
|
|
1286
|
-
}
|
|
1287
|
-
if (!messages.path) {
|
|
1288
|
-
throwError('`path` is required when using `messages`.');
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
1416
|
if (shouldConfigureTurbo) {
|
|
1292
1417
|
if (pluginConfig.requestConfig && path__default.default.isAbsolute(pluginConfig.requestConfig)) {
|
|
1293
1418
|
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);
|
|
@@ -1324,13 +1449,11 @@ function getNextConfig(pluginConfig, nextConfig) {
|
|
|
1324
1449
|
throwError('Message extraction requires Next.js 16 or higher.');
|
|
1325
1450
|
}
|
|
1326
1451
|
rules ??= getTurboRules();
|
|
1327
|
-
const srcPaths = (Array.isArray(pluginConfig.experimental.srcPath) ? pluginConfig.experimental.srcPath : [pluginConfig.experimental.srcPath]).map(srcPath => srcPath.endsWith('/') ? srcPath.slice(0, -1) : srcPath);
|
|
1328
1452
|
addTurboRule(rules, `*.{${SourceFileFilter.EXTENSIONS.join(',')}}`, {
|
|
1329
|
-
loaders: [getExtractMessagesLoaderConfig()],
|
|
1453
|
+
loaders: [getExtractMessagesLoaderConfig(extractorConfig)],
|
|
1330
1454
|
condition: {
|
|
1331
|
-
//
|
|
1332
|
-
//
|
|
1333
|
-
path: `{${srcPaths.join(',')}}` + '/**/*',
|
|
1455
|
+
// We don't filter for `path` here to allow transformation
|
|
1456
|
+
// of `useExtracted` calls in external packages (e.g. monorepos)
|
|
1334
1457
|
content: /(useExtracted|getExtracted)/
|
|
1335
1458
|
}
|
|
1336
1459
|
});
|
|
@@ -1346,7 +1469,7 @@ function getNextConfig(pluginConfig, nextConfig) {
|
|
|
1346
1469
|
addTurboRule(rules, `*${extension}`, {
|
|
1347
1470
|
loaders: [getCatalogLoaderConfig()],
|
|
1348
1471
|
condition: {
|
|
1349
|
-
path:
|
|
1472
|
+
path: `{${messageLoadPaths.join(',')}}/**/*`
|
|
1350
1473
|
},
|
|
1351
1474
|
as: '*.js'
|
|
1352
1475
|
});
|
|
@@ -1404,11 +1527,9 @@ function getNextConfig(pluginConfig, nextConfig) {
|
|
|
1404
1527
|
if (pluginConfig.experimental?.extract) {
|
|
1405
1528
|
if (!config.module) config.module = {};
|
|
1406
1529
|
if (!config.module.rules) config.module.rules = [];
|
|
1407
|
-
const srcPath = pluginConfig.experimental.srcPath;
|
|
1408
1530
|
config.module.rules.push({
|
|
1409
1531
|
test: new RegExp(`\\.(${SourceFileFilter.EXTENSIONS.join('|')})$`),
|
|
1410
|
-
|
|
1411
|
-
use: [getExtractMessagesLoaderConfig()]
|
|
1532
|
+
use: [getExtractMessagesLoaderConfig(extractorConfig)]
|
|
1412
1533
|
});
|
|
1413
1534
|
}
|
|
1414
1535
|
|
|
@@ -1419,7 +1540,7 @@ function getNextConfig(pluginConfig, nextConfig) {
|
|
|
1419
1540
|
const extension = getFormatExtension(pluginConfig.experimental.messages.format);
|
|
1420
1541
|
config.module.rules.push({
|
|
1421
1542
|
test: new RegExp(`${extension.replace(/\./g, '\\.')}$`),
|
|
1422
|
-
include: path__default.default.resolve(config.context,
|
|
1543
|
+
include: messageLoadPaths.map(dirPath => path__default.default.resolve(config.context, dirPath)),
|
|
1423
1544
|
use: [getCatalogLoaderConfig()],
|
|
1424
1545
|
type: 'javascript/auto'
|
|
1425
1546
|
});
|
|
@@ -1450,10 +1571,20 @@ function initPlugin(pluginConfig, nextConfig) {
|
|
|
1450
1571
|
if (messagesPathOrPaths && !skipWatchers) {
|
|
1451
1572
|
createMessagesDeclaration(typeof messagesPathOrPaths === 'string' ? [messagesPathOrPaths] : messagesPathOrPaths);
|
|
1452
1573
|
}
|
|
1574
|
+
let extractorConfig;
|
|
1575
|
+
const experimental = pluginConfig.experimental;
|
|
1576
|
+
const extract = experimental?.extract;
|
|
1577
|
+
if (extract) {
|
|
1578
|
+
extractorConfig = normalizeExtractorConfig({
|
|
1579
|
+
extract,
|
|
1580
|
+
messages: experimental.messages,
|
|
1581
|
+
srcPath: experimental.srcPath
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1453
1584
|
if (!skipWatchers) {
|
|
1454
|
-
initExtractionCompiler(
|
|
1585
|
+
initExtractionCompiler(extractorConfig);
|
|
1455
1586
|
}
|
|
1456
|
-
return getNextConfig(pluginConfig, nextConfig);
|
|
1587
|
+
return getNextConfig(pluginConfig, nextConfig, extractorConfig);
|
|
1457
1588
|
}
|
|
1458
1589
|
function createNextIntlPlugin(i18nPathOrConfig = {}) {
|
|
1459
1590
|
const config = typeof i18nPathOrConfig === 'string' ? {
|