next-intl 4.5.8 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/development/ExtractorCodec-D9Tw618d.cjs +7 -0
- package/dist/cjs/development/JSONCodec-L1_VeQBi.cjs +48 -0
- package/dist/cjs/development/POCodec-Be_UL6jy.cjs +105 -0
- package/dist/cjs/development/plugin-DDtWCyPI.cjs +1373 -0
- package/dist/cjs/development/plugin.cjs +8 -379
- package/dist/esm/development/extractor/ExtractionCompiler.js +23 -26
- package/dist/esm/development/extractor/catalog/CatalogLocales.js +0 -33
- package/dist/esm/development/extractor/catalog/CatalogManager.js +171 -110
- package/dist/esm/development/extractor/catalog/CatalogPersister.js +31 -13
- package/dist/esm/development/extractor/catalog/SaveScheduler.js +1 -1
- package/dist/esm/development/extractor/catalogLoader.js +10 -10
- package/dist/esm/development/extractor/extractMessages.js +9 -2
- package/dist/esm/development/extractor/extractionLoader.js +15 -12
- package/dist/esm/development/extractor/extractor/MessageExtractor.js +5 -4
- package/dist/esm/development/extractor/format/ExtractorCodec.js +5 -0
- package/dist/esm/development/extractor/format/codecs/JSONCodec.js +40 -0
- package/dist/esm/development/extractor/format/codecs/POCodec.js +93 -0
- package/dist/esm/development/extractor/format/index.js +44 -0
- package/dist/esm/development/extractor/source/SourceFileScanner.js +2 -1
- package/dist/esm/development/extractor/source/SourceFileWatcher.js +132 -0
- package/dist/esm/development/extractor/utils.js +16 -1
- package/dist/esm/development/extractor.js +1 -0
- package/dist/esm/development/plugin/createNextIntlPlugin.js +3 -1
- package/dist/esm/development/plugin/declaration/createMessagesDeclaration.js +2 -11
- package/dist/esm/development/plugin/extractor/initExtractionCompiler.js +45 -0
- package/dist/esm/development/plugin/getNextConfig.js +7 -4
- package/dist/esm/development/plugin/utils.js +16 -1
- package/dist/esm/production/extractor/ExtractionCompiler.js +1 -1
- package/dist/esm/production/extractor/catalog/CatalogLocales.js +1 -1
- package/dist/esm/production/extractor/catalog/CatalogManager.js +1 -1
- package/dist/esm/production/extractor/catalog/CatalogPersister.js +1 -1
- package/dist/esm/production/extractor/catalog/SaveScheduler.js +1 -1
- package/dist/esm/production/extractor/catalogLoader.js +1 -1
- package/dist/esm/production/extractor/extractMessages.js +1 -1
- package/dist/esm/production/extractor/extractionLoader.js +1 -1
- package/dist/esm/production/extractor/extractor/MessageExtractor.js +1 -1
- package/dist/esm/production/extractor/format/ExtractorCodec.js +1 -0
- package/dist/esm/production/extractor/format/codecs/JSONCodec.js +1 -0
- package/dist/esm/production/extractor/format/codecs/POCodec.js +1 -0
- package/dist/esm/production/extractor/format/index.js +1 -0
- package/dist/esm/production/extractor/source/SourceFileScanner.js +1 -1
- package/dist/esm/production/extractor/source/SourceFileWatcher.js +1 -0
- package/dist/esm/production/extractor/utils.js +1 -1
- package/dist/esm/production/extractor.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/getNextConfig.js +1 -1
- package/dist/esm/production/plugin/utils.js +1 -1
- package/dist/types/extractor/ExtractionCompiler.d.ts +5 -10
- package/dist/types/extractor/catalog/CatalogLocales.d.ts +0 -2
- package/dist/types/extractor/catalog/CatalogManager.d.ts +26 -15
- package/dist/types/extractor/catalog/CatalogPersister.d.ts +15 -6
- package/dist/types/extractor/catalog/SaveScheduler.d.ts +2 -2
- package/dist/types/extractor/extractor/MessageExtractor.d.ts +6 -6
- package/dist/types/extractor/format/ExtractorCodec.d.ts +33 -0
- package/dist/types/extractor/format/codecs/JSONCodec.d.ts +2 -0
- package/dist/types/extractor/format/codecs/POCodec.d.ts +2 -0
- package/dist/types/extractor/format/codecs/fixtures/JSONCodecStructured.d.ts +2 -0
- package/dist/types/extractor/format/codecs/fixtures/POCodecSourceMessageKey.d.ts +2 -0
- package/dist/types/extractor/format/index.d.ts +15 -0
- package/dist/types/extractor/format/types.d.ts +8 -0
- package/dist/types/extractor/index.d.ts +1 -0
- package/dist/types/extractor/source/SourceFileFilter.d.ts +2 -2
- package/dist/types/extractor/source/SourceFileScanner.d.ts +1 -1
- package/dist/types/extractor/source/SourceFileWatcher.d.ts +15 -0
- package/dist/types/extractor/types.d.ts +2 -2
- package/dist/types/extractor/utils.d.ts +3 -0
- package/dist/types/plugin/extractor/initExtractionCompiler.d.ts +2 -0
- package/dist/types/plugin/types.d.ts +1 -1
- package/dist/types/plugin/utils.d.ts +6 -0
- package/package.json +6 -5
- package/dist/esm/development/extractor/formatters/Formatter.js +0 -3
- package/dist/esm/development/extractor/formatters/JSONFormatter.js +0 -42
- package/dist/esm/development/extractor/formatters/POFormatter.js +0 -51
- package/dist/esm/development/extractor/formatters/index.js +0 -6
- package/dist/esm/development/extractor/formatters/utils.js +0 -15
- package/dist/esm/production/extractor/formatters/Formatter.js +0 -1
- package/dist/esm/production/extractor/formatters/JSONFormatter.js +0 -1
- package/dist/esm/production/extractor/formatters/POFormatter.js +0 -1
- package/dist/esm/production/extractor/formatters/index.js +0 -1
- package/dist/esm/production/extractor/formatters/utils.js +0 -1
- package/dist/types/extractor/extractor/ASTScope.d.ts +0 -12
- package/dist/types/extractor/formatters/Formatter.d.ts +0 -10
- package/dist/types/extractor/formatters/JSONFormatter.d.ts +0 -10
- package/dist/types/extractor/formatters/POFormatter.d.ts +0 -10
- package/dist/types/extractor/formatters/index.d.ts +0 -5
- package/dist/types/extractor/formatters/utils.d.ts +0 -2
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import
|
|
4
|
-
import formatters from '../formatters/index.js';
|
|
3
|
+
import { resolveCodec, getFormatExtension } from '../format/index.js';
|
|
5
4
|
import SourceFileScanner from '../source/SourceFileScanner.js';
|
|
6
|
-
import
|
|
5
|
+
import SourceFileWatcher from '../source/SourceFileWatcher.js';
|
|
6
|
+
import { getDefaultProjectRoot, localeCompare } from '../utils.js';
|
|
7
7
|
import CatalogLocales from './CatalogLocales.js';
|
|
8
8
|
import CatalogPersister from './CatalogPersister.js';
|
|
9
9
|
import SaveScheduler from './SaveScheduler.js';
|
|
10
10
|
|
|
11
11
|
class CatalogManager {
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The source of truth for which messages are used.
|
|
14
|
+
* NOTE: Should be mutated in place to keep `messagesById` and `messagesByFile` in sync.
|
|
15
|
+
*/
|
|
13
16
|
messagesByFile = (() => new Map())();
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
*
|
|
18
|
+
/**
|
|
19
|
+
* Fast lookup for messages by ID across all files,
|
|
20
|
+
* contains the same messages as `messagesByFile`.
|
|
21
|
+
* NOTE: Should be mutated in place to keep `messagesById` and `messagesByFile` in sync.
|
|
22
|
+
*/
|
|
17
23
|
messagesById = (() => new Map())();
|
|
18
24
|
|
|
19
25
|
/**
|
|
@@ -26,104 +32,92 @@ class CatalogManager {
|
|
|
26
32
|
// Cached instances
|
|
27
33
|
|
|
28
34
|
// Resolves when all catalogs are loaded
|
|
29
|
-
// (but doesn't indicate that project scan is done)
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
// Resolves when the initial project scan and processing is complete
|
|
37
|
+
|
|
38
|
+
constructor(config, opts) {
|
|
32
39
|
this.config = config;
|
|
33
40
|
this.saveScheduler = new SaveScheduler(50);
|
|
34
|
-
this.projectRoot = opts.projectRoot
|
|
41
|
+
this.projectRoot = opts.projectRoot ?? getDefaultProjectRoot();
|
|
35
42
|
this.isDevelopment = opts.isDevelopment ?? false;
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
this.extractor = opts.extractor;
|
|
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)
|
|
48
|
+
this.sourceWatcher = new SourceFileWatcher(this.getSrcPaths(), this.handleFileEvents.bind(this));
|
|
49
|
+
void this.sourceWatcher.start();
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
|
-
async
|
|
43
|
-
if (this.
|
|
44
|
-
|
|
45
|
-
} else {
|
|
46
|
-
const FormatterClass = (await formatters[this.config.messages.format]()).default;
|
|
47
|
-
this.formatter = new FormatterClass();
|
|
48
|
-
return this.formatter;
|
|
52
|
+
async getCodec() {
|
|
53
|
+
if (!this.codec) {
|
|
54
|
+
this.codec = await resolveCodec(this.config.messages.format, this.projectRoot);
|
|
49
55
|
}
|
|
56
|
+
return this.codec;
|
|
50
57
|
}
|
|
51
58
|
async getPersister() {
|
|
52
59
|
if (this.persister) {
|
|
53
60
|
return this.persister;
|
|
54
61
|
} else {
|
|
55
|
-
this.persister = new CatalogPersister(
|
|
62
|
+
this.persister = new CatalogPersister({
|
|
63
|
+
messagesPath: this.config.messages.path,
|
|
64
|
+
codec: await this.getCodec(),
|
|
65
|
+
extension: getFormatExtension(this.config.messages.format)
|
|
66
|
+
});
|
|
56
67
|
return this.persister;
|
|
57
68
|
}
|
|
58
69
|
}
|
|
59
|
-
|
|
70
|
+
getCatalogLocales() {
|
|
60
71
|
if (this.catalogLocales) {
|
|
61
72
|
return this.catalogLocales;
|
|
62
73
|
} else {
|
|
63
74
|
const messagesDir = path.join(this.projectRoot, this.config.messages.path);
|
|
64
|
-
const formatter = await this.getFormatter();
|
|
65
75
|
this.catalogLocales = new CatalogLocales({
|
|
66
76
|
messagesDir,
|
|
67
77
|
sourceLocale: this.config.sourceLocale,
|
|
68
|
-
extension:
|
|
78
|
+
extension: getFormatExtension(this.config.messages.format),
|
|
69
79
|
locales: this.config.messages.locales
|
|
70
80
|
});
|
|
71
81
|
return this.catalogLocales;
|
|
72
82
|
}
|
|
73
83
|
}
|
|
74
84
|
async getTargetLocales() {
|
|
75
|
-
|
|
76
|
-
return catalogLocales.getTargetLocales();
|
|
85
|
+
return this.getCatalogLocales().getTargetLocales();
|
|
77
86
|
}
|
|
78
87
|
getSrcPaths() {
|
|
79
88
|
return (Array.isArray(this.config.srcPath) ? this.config.srcPath : [this.config.srcPath]).map(srcPath => path.join(this.projectRoot, srcPath));
|
|
80
89
|
}
|
|
81
90
|
async loadMessages() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Ensure catalogs are loaded before scanning source files.
|
|
85
|
-
// Otherwise, `loadSourceMessages` might overwrite extracted
|
|
86
|
-
// messages if it finishes after source file extraction.
|
|
91
|
+
const sourceDiskMessages = await this.loadSourceMessages();
|
|
92
|
+
this.loadCatalogsPromise = this.loadTargetMessages();
|
|
87
93
|
await this.loadCatalogsPromise;
|
|
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;
|
|
88
100
|
if (this.isDevelopment) {
|
|
89
|
-
const catalogLocales =
|
|
101
|
+
const catalogLocales = this.getCatalogLocales();
|
|
90
102
|
catalogLocales.subscribeLocalesChange(this.onLocalesChange);
|
|
91
103
|
}
|
|
92
|
-
const sourceFiles = await SourceFileScanner.getSourceFiles(this.getSrcPaths());
|
|
93
|
-
await Promise.all(sourceFiles.map(async filePath => this.extractFileMessages(filePath, await fs.readFile(filePath, 'utf8'))));
|
|
94
104
|
}
|
|
95
105
|
async loadSourceMessages() {
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
for (const
|
|
101
|
-
|
|
102
|
-
if (message.references) {
|
|
103
|
-
for (const ref of message.references) {
|
|
104
|
-
const absoluteFilePath = path.join(this.projectRoot, ref.path);
|
|
105
|
-
let fileMessages = messagesByFile.get(absoluteFilePath);
|
|
106
|
-
if (!fileMessages) {
|
|
107
|
-
fileMessages = new Map();
|
|
108
|
-
messagesByFile.set(absoluteFilePath, fileMessages);
|
|
109
|
-
}
|
|
110
|
-
fileMessages.set(message.id, message);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
106
|
+
// Load source catalog to hydrate metadata (e.g. flags) later without
|
|
107
|
+
// treating catalog entries as source of truth.
|
|
108
|
+
const diskMessages = await this.loadLocaleMessages(this.config.sourceLocale);
|
|
109
|
+
const byId = new Map();
|
|
110
|
+
for (const diskMessage of diskMessages) {
|
|
111
|
+
byId.set(diskMessage.id, diskMessage);
|
|
113
112
|
}
|
|
114
|
-
|
|
115
|
-
this.messagesByFile = messagesByFile;
|
|
113
|
+
return byId;
|
|
116
114
|
}
|
|
117
115
|
async loadLocaleMessages(locale) {
|
|
118
116
|
const persister = await this.getPersister();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return messages;
|
|
124
|
-
} catch {
|
|
125
|
-
return [];
|
|
126
|
-
}
|
|
117
|
+
const messages = await persister.read(locale);
|
|
118
|
+
const fileTime = await persister.getLastModified(locale);
|
|
119
|
+
this.lastWriteByLocale.set(locale, fileTime);
|
|
120
|
+
return messages;
|
|
127
121
|
}
|
|
128
122
|
async loadTargetMessages() {
|
|
129
123
|
const targetLocales = await this.getTargetLocales();
|
|
@@ -136,28 +130,70 @@ class CatalogManager {
|
|
|
136
130
|
for (const diskMessage of diskMessages) {
|
|
137
131
|
const prev = this.messagesById.get(diskMessage.id);
|
|
138
132
|
if (prev) {
|
|
133
|
+
// Mutate the existing object instead of creating a copy
|
|
134
|
+
// to keep messagesById and messagesByFile in sync.
|
|
139
135
|
// Unknown properties (like flags): disk wins
|
|
140
136
|
// Known properties: existing (from extraction) wins
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
});
|
|
137
|
+
for (const key of Object.keys(diskMessage)) {
|
|
138
|
+
if (!['id', 'message', 'description', 'references'].includes(key)) {
|
|
139
|
+
// For unknown properties (like flags), disk wins
|
|
140
|
+
prev[key] = diskMessage[key];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
148
143
|
}
|
|
149
144
|
}
|
|
150
145
|
} else {
|
|
151
|
-
// For target: disk wins completely
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
mergeSourceDiskMetadata(diskMessages) {
|
|
166
|
+
for (const [id, diskMessage] of diskMessages) {
|
|
167
|
+
const existing = this.messagesById.get(id);
|
|
168
|
+
if (!existing) continue;
|
|
169
|
+
|
|
170
|
+
// Mutate the existing object instead of creating a copy.
|
|
171
|
+
// This keeps `messagesById` and `messagesByFile` in sync since
|
|
172
|
+
// they reference the same object instance.
|
|
173
|
+
for (const key of Object.keys(diskMessage)) {
|
|
174
|
+
if (existing[key] == null) {
|
|
175
|
+
existing[key] = diskMessage[key];
|
|
176
|
+
}
|
|
155
177
|
}
|
|
156
|
-
this.translationsByTargetLocale.set(locale, translations);
|
|
157
178
|
}
|
|
158
179
|
}
|
|
159
|
-
async
|
|
160
|
-
|
|
180
|
+
async processFile(absoluteFilePath) {
|
|
181
|
+
let messages = [];
|
|
182
|
+
try {
|
|
183
|
+
const content = await fs.readFile(absoluteFilePath, 'utf8');
|
|
184
|
+
let extraction;
|
|
185
|
+
try {
|
|
186
|
+
extraction = await this.extractor.extract(absoluteFilePath, content);
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
messages = extraction.messages;
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (err.code !== 'ENOENT') {
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
// ENOENT -> treat as no messages
|
|
196
|
+
}
|
|
161
197
|
const prevFileMessages = this.messagesByFile.get(absoluteFilePath);
|
|
162
198
|
|
|
163
199
|
// Init with all previous ones
|
|
@@ -165,24 +201,17 @@ class CatalogManager {
|
|
|
165
201
|
|
|
166
202
|
// Replace existing messages with new ones
|
|
167
203
|
const fileMessages = new Map();
|
|
168
|
-
for (let message of
|
|
204
|
+
for (let message of messages) {
|
|
169
205
|
const prevMessage = this.messagesById.get(message.id);
|
|
170
206
|
|
|
171
207
|
// Merge with previous message if it exists
|
|
172
208
|
if (prevMessage) {
|
|
173
|
-
|
|
174
|
-
// reference, which is the current file. We need to merge this with
|
|
175
|
-
// potentially existing references.
|
|
176
|
-
const references = [...(prevMessage.references ?? [])];
|
|
177
|
-
message.references.forEach(ref => {
|
|
178
|
-
if (!references.some(cur => cur.path === ref.path)) {
|
|
179
|
-
references.push(ref);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
references.sort((referenceA, referenceB) => localeCompare(referenceA.path, referenceB.path));
|
|
209
|
+
const validated = prevMessage.references ?? [];
|
|
183
210
|
message = {
|
|
184
211
|
...message,
|
|
185
|
-
references
|
|
212
|
+
references: this.mergeReferences(validated, {
|
|
213
|
+
path: path.relative(this.projectRoot, absoluteFilePath)
|
|
214
|
+
})
|
|
186
215
|
};
|
|
187
216
|
|
|
188
217
|
// Merge other properties like description, or unknown
|
|
@@ -200,31 +229,39 @@ class CatalogManager {
|
|
|
200
229
|
const index = idsToRemove.indexOf(message.id);
|
|
201
230
|
if (index !== -1) idsToRemove.splice(index, 1);
|
|
202
231
|
}
|
|
203
|
-
|
|
204
|
-
// Don't delete IDs still used in other files
|
|
205
232
|
const relativeFilePath = path.relative(this.projectRoot, absoluteFilePath);
|
|
206
|
-
const idsToDelete = idsToRemove.filter(id => {
|
|
207
|
-
const message = this.messagesById.get(id);
|
|
208
|
-
return !message?.references?.some(ref => ref.path !== relativeFilePath);
|
|
209
|
-
});
|
|
210
233
|
|
|
211
234
|
// Clean up removed messages from `messagesById`
|
|
212
|
-
|
|
213
|
-
this.messagesById.
|
|
235
|
+
idsToRemove.forEach(id => {
|
|
236
|
+
const message = this.messagesById.get(id);
|
|
237
|
+
if (!message) return;
|
|
238
|
+
const hasOtherReferences = message.references?.some(ref => ref.path !== relativeFilePath);
|
|
239
|
+
if (!hasOtherReferences) {
|
|
240
|
+
// No other references, delete the message entirely
|
|
241
|
+
this.messagesById.delete(id);
|
|
242
|
+
} else {
|
|
243
|
+
// Message is used elsewhere, remove this file from references
|
|
244
|
+
// Mutate the existing object to keep `messagesById` and `messagesByFile` in sync
|
|
245
|
+
message.references = message.references?.filter(ref => ref.path !== relativeFilePath);
|
|
246
|
+
}
|
|
214
247
|
});
|
|
215
248
|
|
|
216
249
|
// Update the stored messages
|
|
217
|
-
|
|
218
|
-
if (hasMessages) {
|
|
250
|
+
if (messages.length > 0) {
|
|
219
251
|
this.messagesByFile.set(absoluteFilePath, fileMessages);
|
|
220
252
|
} else {
|
|
221
253
|
this.messagesByFile.delete(absoluteFilePath);
|
|
222
254
|
}
|
|
223
255
|
const changed = this.haveMessagesChangedForFile(prevFileMessages, fileMessages);
|
|
224
|
-
return
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
256
|
+
return changed;
|
|
257
|
+
}
|
|
258
|
+
mergeReferences(existing, current) {
|
|
259
|
+
const dedup = new Map();
|
|
260
|
+
for (const ref of existing) {
|
|
261
|
+
dedup.set(ref.path, ref);
|
|
262
|
+
}
|
|
263
|
+
dedup.set(current.path, current);
|
|
264
|
+
return Array.from(dedup.values()).sort((a, b) => localeCompare(a.path, b.path));
|
|
228
265
|
}
|
|
229
266
|
haveMessagesChangedForFile(beforeMessages, afterMessages) {
|
|
230
267
|
// If one exists and the other doesn't, there's a change
|
|
@@ -273,18 +310,21 @@ class CatalogManager {
|
|
|
273
310
|
if (currentFileTime && lastWriteTime && currentFileTime > lastWriteTime) {
|
|
274
311
|
await this.reloadLocaleCatalog(locale);
|
|
275
312
|
}
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
const
|
|
313
|
+
const localeMessages = isSourceLocale ? this.messagesById : this.translationsByTargetLocale.get(locale);
|
|
314
|
+
const messagesToPersist = messages.map(message => {
|
|
315
|
+
const localeMessage = localeMessages?.get(message.id);
|
|
279
316
|
return {
|
|
280
|
-
...
|
|
317
|
+
...localeMessage,
|
|
281
318
|
id: message.id,
|
|
282
319
|
description: message.description,
|
|
283
320
|
references: message.references,
|
|
284
|
-
message: isSourceLocale ? message.message :
|
|
321
|
+
message: isSourceLocale ? message.message : localeMessage?.message ?? ''
|
|
285
322
|
};
|
|
286
323
|
});
|
|
287
|
-
await persister.write(
|
|
324
|
+
await persister.write(messagesToPersist, {
|
|
325
|
+
locale,
|
|
326
|
+
sourceMessagesById: this.messagesById
|
|
327
|
+
});
|
|
288
328
|
|
|
289
329
|
// Update timestamps
|
|
290
330
|
const newTime = await persister.getLastModified(locale);
|
|
@@ -301,8 +341,29 @@ class CatalogManager {
|
|
|
301
341
|
this.lastWriteByLocale.delete(locale);
|
|
302
342
|
}
|
|
303
343
|
};
|
|
304
|
-
|
|
305
|
-
this.
|
|
344
|
+
async handleFileEvents(events) {
|
|
345
|
+
if (this.loadCatalogsPromise) {
|
|
346
|
+
await this.loadCatalogsPromise;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Wait for initial scan to complete to avoid race conditions
|
|
350
|
+
if (this.scanCompletePromise) {
|
|
351
|
+
await this.scanCompletePromise;
|
|
352
|
+
}
|
|
353
|
+
let changed = false;
|
|
354
|
+
const expandedEvents = await this.sourceWatcher.expandDirectoryDeleteEvents(events, Array.from(this.messagesByFile.keys()));
|
|
355
|
+
for (const event of expandedEvents) {
|
|
356
|
+
const hasChanged = await this.processFile(event.path);
|
|
357
|
+
changed ||= hasChanged;
|
|
358
|
+
}
|
|
359
|
+
if (changed) {
|
|
360
|
+
await this.save();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
[Symbol.dispose]() {
|
|
364
|
+
this.sourceWatcher?.stop();
|
|
365
|
+
this.sourceWatcher = undefined;
|
|
366
|
+
this.saveScheduler[Symbol.dispose]();
|
|
306
367
|
if (this.catalogLocales && this.isDevelopment) {
|
|
307
368
|
this.catalogLocales.unsubscribeLocalesChange(this.onLocalesChange);
|
|
308
369
|
}
|
|
@@ -2,25 +2,43 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
4
|
class CatalogPersister {
|
|
5
|
-
constructor(
|
|
6
|
-
this.messagesPath = messagesPath;
|
|
7
|
-
this.
|
|
5
|
+
constructor(params) {
|
|
6
|
+
this.messagesPath = params.messagesPath;
|
|
7
|
+
this.codec = params.codec;
|
|
8
|
+
this.extension = params.extension;
|
|
9
|
+
}
|
|
10
|
+
getFileName(locale) {
|
|
11
|
+
return locale + this.extension;
|
|
8
12
|
}
|
|
9
13
|
getFilePath(locale) {
|
|
10
|
-
return path.join(this.messagesPath,
|
|
14
|
+
return path.join(this.messagesPath, this.getFileName(locale));
|
|
11
15
|
}
|
|
12
16
|
async read(locale) {
|
|
13
17
|
const filePath = this.getFilePath(locale);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
})
|
|
18
|
+
let content;
|
|
19
|
+
try {
|
|
20
|
+
content = await fs.readFile(filePath, 'utf8');
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`Error while reading ${this.getFileName(locale)}:\n> ${error}`, {
|
|
26
|
+
cause: error
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return this.codec.decode(content, {
|
|
31
|
+
locale
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new Error(`Error while decoding ${this.getFileName(locale)}:\n> ${error}`, {
|
|
35
|
+
cause: error
|
|
36
|
+
});
|
|
37
|
+
}
|
|
18
38
|
}
|
|
19
|
-
async write(
|
|
20
|
-
const filePath = this.getFilePath(locale);
|
|
21
|
-
const content = this.
|
|
22
|
-
locale
|
|
23
|
-
});
|
|
39
|
+
async write(messages, context) {
|
|
40
|
+
const filePath = this.getFilePath(context.locale);
|
|
41
|
+
const content = this.codec.encode(messages, context);
|
|
24
42
|
try {
|
|
25
43
|
const outputDir = path.dirname(filePath);
|
|
26
44
|
await fs.mkdir(outputDir, {
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import
|
|
2
|
+
import { getFormatExtension, resolveCodec } from './format/index.js';
|
|
3
3
|
|
|
4
|
-
let
|
|
5
|
-
async function
|
|
6
|
-
if (!
|
|
7
|
-
|
|
8
|
-
cachedFormatter = new FormatterClass();
|
|
4
|
+
let cachedCodec = null;
|
|
5
|
+
async function getCodec(options, projectRoot) {
|
|
6
|
+
if (!cachedCodec) {
|
|
7
|
+
cachedCodec = await resolveCodec(options.messages.format, projectRoot);
|
|
9
8
|
}
|
|
10
|
-
return
|
|
9
|
+
return cachedCodec;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
/**
|
|
@@ -20,9 +19,10 @@ async function getFormatter(options) {
|
|
|
20
19
|
function catalogLoader(source) {
|
|
21
20
|
const options = this.getOptions();
|
|
22
21
|
const callback = this.async();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
22
|
+
const extension = getFormatExtension(options.messages.format);
|
|
23
|
+
getCodec(options, this.rootContext).then(codec => {
|
|
24
|
+
const locale = path.basename(this.resourcePath, extension);
|
|
25
|
+
const jsonString = codec.toJSONString(source, {
|
|
26
26
|
locale
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import ExtractionCompiler from './ExtractionCompiler.js';
|
|
2
|
+
import MessageExtractor from './extractor/MessageExtractor.js';
|
|
3
|
+
import { getDefaultProjectRoot } from './utils.js';
|
|
2
4
|
|
|
3
5
|
async function extractMessages(params) {
|
|
4
|
-
const compiler = new ExtractionCompiler(params
|
|
5
|
-
|
|
6
|
+
const compiler = new ExtractionCompiler(params, {
|
|
7
|
+
extractor: new MessageExtractor({
|
|
8
|
+
isDevelopment: false,
|
|
9
|
+
projectRoot: getDefaultProjectRoot()
|
|
10
|
+
})
|
|
11
|
+
});
|
|
12
|
+
await compiler.extractAll();
|
|
6
13
|
}
|
|
7
14
|
|
|
8
15
|
export { extractMessages as default };
|
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import MessageExtractor from './extractor/MessageExtractor.js';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
let
|
|
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.
|
|
7
|
+
let extractor;
|
|
8
8
|
function extractionLoader(source) {
|
|
9
|
-
const options = this.getOptions();
|
|
10
9
|
const callback = this.async();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const projectRoot = this.rootContext;
|
|
11
|
+
|
|
12
|
+
// Avoid rollup's `replace` plugin to compile this away
|
|
13
|
+
const isDevelopment = process.env['NODE_ENV'.trim()] === 'development';
|
|
14
|
+
if (!extractor) {
|
|
15
|
+
extractor = new MessageExtractor({
|
|
16
|
+
isDevelopment,
|
|
17
|
+
projectRoot,
|
|
15
18
|
sourceMap: this.sourceMap
|
|
16
19
|
});
|
|
17
20
|
}
|
|
18
|
-
|
|
21
|
+
extractor.extract(this.resourcePath, source).then(result => {
|
|
19
22
|
callback(null, result.code, result.map);
|
|
20
23
|
}).catch(callback);
|
|
21
24
|
}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { transform } from '@swc/core';
|
|
4
|
+
import { getDefaultProjectRoot } from '../utils.js';
|
|
4
5
|
import LRUCache from './LRUCache.js';
|
|
5
6
|
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
7
8
|
class MessageExtractor {
|
|
8
9
|
compileCache = (() => new LRUCache(750))();
|
|
9
10
|
constructor(opts) {
|
|
10
|
-
this.isDevelopment = opts.isDevelopment;
|
|
11
|
-
this.projectRoot = opts.projectRoot;
|
|
11
|
+
this.isDevelopment = opts.isDevelopment ?? false;
|
|
12
|
+
this.projectRoot = opts.projectRoot ?? getDefaultProjectRoot();
|
|
12
13
|
this.sourceMap = opts.sourceMap ?? false;
|
|
13
14
|
}
|
|
14
|
-
async
|
|
15
|
-
const cacheKey = source;
|
|
15
|
+
async extract(absoluteFilePath, source) {
|
|
16
|
+
const cacheKey = [source, absoluteFilePath].join('!');
|
|
16
17
|
const cached = this.compileCache.get(cacheKey);
|
|
17
18
|
if (cached) return cached;
|
|
18
19
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getSortedMessages, setNestedProperty } from '../../utils.js';
|
|
2
|
+
import { defineCodec } from '../ExtractorCodec.js';
|
|
3
|
+
|
|
4
|
+
var JSONCodec = defineCodec(() => ({
|
|
5
|
+
decode(source) {
|
|
6
|
+
const json = JSON.parse(source);
|
|
7
|
+
const messages = [];
|
|
8
|
+
traverseMessages(json, (message, id) => {
|
|
9
|
+
messages.push({
|
|
10
|
+
id,
|
|
11
|
+
message
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
return messages;
|
|
15
|
+
},
|
|
16
|
+
encode(messages) {
|
|
17
|
+
const root = {};
|
|
18
|
+
for (const message of getSortedMessages(messages)) {
|
|
19
|
+
setNestedProperty(root, message.id, message.message);
|
|
20
|
+
}
|
|
21
|
+
return JSON.stringify(root, null, 2) + '\n';
|
|
22
|
+
},
|
|
23
|
+
toJSONString(source) {
|
|
24
|
+
return source;
|
|
25
|
+
}
|
|
26
|
+
}));
|
|
27
|
+
function traverseMessages(obj, callback, path = '') {
|
|
28
|
+
const NAMESPACE_SEPARATOR = '.';
|
|
29
|
+
for (const key of Object.keys(obj)) {
|
|
30
|
+
const newPath = path ? path + NAMESPACE_SEPARATOR + key : key;
|
|
31
|
+
const value = obj[key];
|
|
32
|
+
if (typeof value === 'string') {
|
|
33
|
+
callback(value, newPath);
|
|
34
|
+
} else if (typeof value === 'object') {
|
|
35
|
+
traverseMessages(value, callback, newPath);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { JSONCodec as default };
|