@zohodesk/client_build_tool 0.0.1-0.exp.0.0.3 → 0.0.1-0.exp.0.0.8

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.
@@ -9,20 +9,76 @@ const {
9
9
  Compilation
10
10
  } = require('webpack');
11
11
 
12
+ const {
13
+ parseProperties,
14
+ decodeUnicodeEscapes
15
+ } = require('../../utils/propertiesParser');
16
+
17
+ let createHashFunction;
18
+
12
19
  const {
13
20
  createHash
14
21
  } = require("../I18nSplitPlugin/createHash");
15
22
 
23
+ createHashFunction = createHash;
16
24
  const {
17
25
  RawSource
18
26
  } = sources;
19
27
  const pluginName = 'I18nNumericIndexPlugin';
20
28
 
21
29
  class I18nNumericIndexPlugin {
22
- constructor(options) {
30
+ constructor(options = {}) {
23
31
  this.options = options;
24
32
  this.numericMap = null;
25
- this.numericMapPath = options.numericMapPath;
33
+ this.stringPool = new Map(); // String interning for deduplication
34
+ // Default emitLiteralUnicode to true if not specified
35
+
36
+ if (typeof this.options.emitLiteralUnicode === 'undefined') {
37
+ this.options.emitLiteralUnicode = true;
38
+ } // Default to compact mode for memory optimization
39
+
40
+
41
+ if (typeof this.options.useCompactFormat === 'undefined') {
42
+ this.options.useCompactFormat = true;
43
+ }
44
+ } // String interning to reduce memory usage
45
+
46
+
47
+ intern(str) {
48
+ if (typeof str !== 'string') {
49
+ return str;
50
+ }
51
+
52
+ if (!this.stringPool.has(str)) {
53
+ this.stringPool.set(str, str);
54
+ }
55
+
56
+ return this.stringPool.get(str);
57
+ } // Generate compact numeric data structure - returns just an array of values
58
+
59
+
60
+ generateCompactNumericData(numericDataArray) {
61
+ if (!this.options.useCompactFormat) {
62
+ return numericDataArray;
63
+ } // Create a sparse array with only non-null values
64
+
65
+
66
+ const compactArray = [];
67
+
68
+ for (let i = 0; i < numericDataArray.length; i++) {
69
+ if (numericDataArray[i] !== null) {
70
+ compactArray[i] = this.intern(numericDataArray[i]);
71
+ }
72
+ }
73
+
74
+ return compactArray;
75
+ } // Compact serializer for JSON
76
+
77
+
78
+ compactSerialize(data) {
79
+ return JSON.stringify(data, null, 0).replace(/"(\d+)":/g, '$1:') // Remove quotes from numeric keys
80
+ .replace(/,null/g, ',0') // Replace null with 0 for smaller size
81
+ .replace(/null,/g, '0,'); // Replace null with 0 for smaller size
26
82
  }
27
83
 
28
84
  loadNumericMapOnce(compilation) {
@@ -30,34 +86,58 @@ class I18nNumericIndexPlugin {
30
86
  return this.numericMap;
31
87
  }
32
88
 
89
+ const numericMapPathOpt = this.options.numericMapPath;
90
+ const numericMapPath = numericMapPathOpt ? path.resolve(compilation.compiler.context, numericMapPathOpt) : null;
91
+
33
92
  try {
34
- if (this.numericMapPath && fs.existsSync(this.numericMapPath)) {
35
- const fileContent = fs.readFileSync(this.numericMapPath, 'utf-8');
36
- const parsedData = JSON.parse(fileContent);
93
+ if (numericMapPath && fs.existsSync(numericMapPath)) {
94
+ const fileContent = fs.readFileSync(numericMapPath, 'utf-8');
95
+ const parsedData = JSON.parse(fileContent); // Handle both old format (sortedOriginalKeys) and new format (originalKeyToNumericId)
37
96
 
38
97
  if (parsedData && parsedData.sortedOriginalKeys && parsedData.totalKeys !== undefined) {
98
+ // Old format
39
99
  this.numericMap = {
40
100
  sortedOriginalKeys: parsedData.sortedOriginalKeys,
41
101
  totalKeys: parsedData.totalKeys
42
102
  };
43
- compilation.logger.info(`${pluginName}: Loaded numeric map from ${this.numericMapPath}.`);
103
+ return this.numericMap;
104
+ } else if (parsedData && parsedData.originalKeyToNumericId && parsedData.totalKeysInMap !== undefined) {
105
+ // New format - convert originalKeyToNumericId to sortedOriginalKeys
106
+ const keyToIdMap = parsedData.originalKeyToNumericId;
107
+ const sortedOriginalKeys = new Array(parsedData.totalKeysInMap); // Fill the array with keys at their numeric positions
108
+
109
+ Object.keys(keyToIdMap).forEach(key => {
110
+ const numericId = keyToIdMap[key];
111
+
112
+ if (numericId >= 0 && numericId < parsedData.totalKeysInMap) {
113
+ sortedOriginalKeys[numericId] = key;
114
+ }
115
+ });
116
+ this.numericMap = {
117
+ sortedOriginalKeys: sortedOriginalKeys,
118
+ totalKeys: parsedData.totalKeysInMap
119
+ };
44
120
  return this.numericMap;
45
121
  } else {
46
- compilation.logger.error(`${pluginName}: Invalid format in numeric map file at ${this.numericMapPath}.`);
122
+ compilation.warnings.push(new Error(`${pluginName}: numericMap file (${numericMapPath}) parsed but seems malformed. Expected 'sortedOriginalKeys' + 'totalKeys' or 'originalKeyToNumericId' + 'totalKeysInMap'. Using empty map.`));
47
123
  this.numericMap = {
48
124
  sortedOriginalKeys: [],
49
125
  totalKeys: 0
50
126
  };
51
127
  }
52
128
  } else {
53
- compilation.logger.warn(`${pluginName}: Numeric map path not provided or file not found at ${this.numericMapPath}.`);
129
+ if (numericMapPathOpt) {
130
+ // Only warn if a path was actually provided
131
+ compilation.warnings.push(new Error(`${pluginName}: numericMapPath (${numericMapPath}) not found. Using empty map.`));
132
+ }
133
+
54
134
  this.numericMap = {
55
135
  sortedOriginalKeys: [],
56
136
  totalKeys: 0
57
137
  };
58
138
  }
59
139
  } catch (err) {
60
- compilation.logger.error(`${pluginName}: Error loading numeric map from ${this.numericMapPath}: ${err.message}`);
140
+ compilation.errors.push(new Error(`${pluginName}: Error loading/parsing numericMapPath ${numericMapPath}: ${err.message}`));
61
141
  this.numericMap = {
62
142
  sortedOriginalKeys: [],
63
143
  totalKeys: 0
@@ -67,117 +147,344 @@ class I18nNumericIndexPlugin {
67
147
  return this.numericMap;
68
148
  }
69
149
 
70
- emitFile(compilation, baseFilenameTemplate, locale, fileContentData, jsonpFunc, typeSuffix = '') {
71
- const fileContent = `${jsonpFunc}(${JSON.stringify(fileContentData)});`;
150
+ loadJSResourceBaseFile(compilation) {
151
+ const compilerContext = compilation.compiler.context;
152
+ const jsResourcePathOpt = this.options.jsResourcePath;
153
+
154
+ if (!jsResourcePathOpt) {
155
+ return {};
156
+ }
157
+
158
+ const jsResourcePath = path.resolve(compilerContext, jsResourcePathOpt);
159
+
160
+ try {
161
+ if (fs.existsSync(jsResourcePath)) {
162
+ const fileContent = fs.readFileSync(jsResourcePath, 'utf-8');
163
+ return parseProperties(fileContent);
164
+ } else {
165
+ compilation.warnings.push(new Error(`${pluginName}: JS Resource base file not found: ${jsResourcePath}`));
166
+ return {};
167
+ }
168
+ } catch (err) {
169
+ compilation.errors.push(new Error(`${pluginName}: Error loading JS Resource base file ${jsResourcePath}: ${err.message}`));
170
+ return {};
171
+ }
172
+ }
173
+
174
+ loadAllI18nData(compilation) {
175
+ if (this.options.allI18nObject && this.options.locales) {
176
+ return {
177
+ allI18nObject: this.options.allI18nObject,
178
+ locales: this.options.locales,
179
+ jsResourceBase: this.loadJSResourceBaseFile(compilation)
180
+ };
181
+ }
182
+
183
+ const allI18nObject = {};
184
+ const discoveredLocales = new Set();
185
+ const compilerContext = compilation.compiler.context;
186
+ const propertiesFolderPathOpt = this.options.propertiesFolderPath;
187
+ const jsResourcePathOpt = this.options.jsResourcePath;
188
+
189
+ if (!propertiesFolderPathOpt) {
190
+ compilation.errors.push(new Error(`${pluginName}: 'propertiesFolderPath' option is missing, cannot load translations.`));
191
+ return {
192
+ allI18nObject,
193
+ locales: [],
194
+ jsResourceBase: {}
195
+ };
196
+ }
197
+
198
+ const propertiesFolderPath = path.resolve(compilerContext, propertiesFolderPathOpt);
199
+ const baseJsResourcePath = jsResourcePathOpt ? path.resolve(compilerContext, jsResourcePathOpt) : null;
200
+
201
+ if (!fs.existsSync(propertiesFolderPath)) {
202
+ compilation.errors.push(new Error(`${pluginName}: propertiesFolderPath does not exist: ${propertiesFolderPath}`));
203
+ return {
204
+ allI18nObject,
205
+ locales: [],
206
+ jsResourceBase: {}
207
+ };
208
+ }
209
+
210
+ const baseFileName = baseJsResourcePath ? path.basename(baseJsResourcePath, path.extname(baseJsResourcePath)) : null;
211
+ const baseExtension = baseJsResourcePath ? path.extname(baseJsResourcePath) : '.properties';
212
+
213
+ try {
214
+ const files = fs.readdirSync(propertiesFolderPath);
215
+ files.forEach(file => {
216
+ const filePath = path.join(propertiesFolderPath, file);
217
+
218
+ if (fs.statSync(filePath).isFile()) {
219
+ let locale = null;
220
+ const ext = path.extname(file);
221
+ const nameWithoutExt = path.basename(file, ext);
222
+
223
+ if (baseFileName && nameWithoutExt.startsWith(baseFileName + '_')) {
224
+ locale = nameWithoutExt.substring((baseFileName + '_').length);
225
+ } else if (baseFileName && nameWithoutExt === baseFileName) {
226
+ locale = this.options.defaultLocaleForBaseFile || 'en';
227
+ } else if (!baseFileName && nameWithoutExt.includes('_')) {
228
+ // Generic pattern: messages_fr_CA.properties -> fr_CA
229
+ const parts = nameWithoutExt.split('_');
230
+
231
+ if (parts.length > 1) {
232
+ locale = parts.slice(1).join('_');
233
+ }
234
+ } else if (!baseFileName && !nameWithoutExt.includes('_') && ext.toLowerCase() === baseExtension.toLowerCase()) {
235
+ // Fallback for files like 'fr.properties' if no baseFileName is defined
236
+ locale = nameWithoutExt;
237
+ }
238
+
239
+ if (locale && ext.toLowerCase() === baseExtension.toLowerCase()) {
240
+ try {
241
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
242
+ allI18nObject[locale] = parseProperties(fileContent);
243
+ discoveredLocales.add(locale);
244
+ } catch (readErr) {
245
+ compilation.errors.push(new Error(`${pluginName}: Error reading/parsing properties file ${filePath}: ${readErr.message}`));
246
+ }
247
+ }
248
+ }
249
+ });
250
+ } catch (err) {
251
+ compilation.errors.push(new Error(`${pluginName}: Error reading propertiesFolderPath ${propertiesFolderPath}: ${err.message}`));
252
+ } // Load JS Resource base file separately
253
+
254
+
255
+ const jsResourceBase = this.loadJSResourceBaseFile(compilation);
256
+ return {
257
+ allI18nObject,
258
+ locales: Array.from(discoveredLocales),
259
+ jsResourceBase
260
+ };
261
+ }
262
+
263
+ validateAndSanitizeData(data, chunkType, locale, compilation) {
264
+ // Placeholder for any future validation or sanitization logic
265
+ return data;
266
+ }
267
+
268
+ emitChunkFile(compilation, filenameTemplate, locale, fileContentData, jsonpFunc, chunkType) {
269
+ const dataToStringify = this.validateAndSanitizeData(fileContentData, chunkType, locale, compilation);
270
+ let stringifiedData;
271
+
272
+ try {
273
+ // Use compact serializer for smaller output
274
+ if (this.options.useCompactFormat) {
275
+ stringifiedData = this.compactSerialize(dataToStringify);
276
+ } else {
277
+ stringifiedData = JSON.stringify(dataToStringify, null, 0);
278
+ } // If emitLiteralUnicode is true (default), convert \uXXXX sequences back to actual Unicode characters
279
+ // This ensures the output file contains literal UTF-8 characters if desired.
280
+
281
+
282
+ if (this.options.emitLiteralUnicode) {
283
+ stringifiedData = decodeUnicodeEscapes(stringifiedData);
284
+ }
285
+ } catch (e) {
286
+ compilation.errors.push(new Error(`${pluginName}: Failed to stringify or post-process data for ${chunkType} chunk (${locale}): ${e.message}`));
287
+ return;
288
+ } // Create content in the format: window.loadI18nChunk({data})
289
+
290
+
291
+ const fileContent = `${jsonpFunc}(${stringifiedData});`;
72
292
  const source = new RawSource(fileContent);
73
- const actualContentHash = createHash({
74
- //
293
+ const actualContentHash = createHashFunction({
294
+ // Ensure createHashFunction is loaded
75
295
  outputOptions: compilation.outputOptions,
76
- content: fileContent
296
+ content: fileContent // Hash is based on the final content
297
+
77
298
  });
78
- let processedFilenameTemplate = baseFilenameTemplate.replace(/\[locale\]/g, locale).replace(/\[name\]/g, locale).replace(/\[id\]/g, locale);
299
+ let processedFilenameTemplate = filenameTemplate.replace(/\[locale\]/g, locale).replace(/\[name\]/g, locale).replace(/\[id\]/g, locale);
79
300
  const finalFileName = compilation.getAssetPath(processedFilenameTemplate, {
80
301
  locale: locale,
81
302
  contentHash: actualContentHash,
82
303
  hash: compilation.hash,
304
+ // Webpack compilation hash
83
305
  chunk: {
84
- id: `i18n-${typeSuffix || 'data'}-${locale}`,
85
- name: `${typeSuffix || 'data'}-${locale}`,
306
+ // Mock chunk object for placeholders
307
+ id: `i18n-${chunkType}-${locale}`,
308
+ name: `${chunkType}-${locale}`,
86
309
  hash: actualContentHash,
310
+ // Chunk-specific hash
87
311
  contentHash: {
88
- [this.options.moduleType || 'i18n/mini-extract']: actualContentHash
312
+ [this.options.moduleType || 'i18n/data-extract']: actualContentHash
89
313
  }
90
314
  },
91
- contentHashType: this.options.moduleType || 'i18n/mini-extract'
315
+ contentHashType: this.options.moduleType || 'i18n/data-extract'
92
316
  });
93
317
  compilation.emitAsset(finalFileName, source);
94
- compilation.logger.info(`${pluginName}: Emitted ${typeSuffix} i18n file for locale ${locale} to ${finalFileName}`);
95
318
  }
96
319
 
97
320
  apply(compiler) {
98
321
  compiler.hooks.thisCompilation.tap(pluginName, compilation => {
99
322
  compilation.hooks.processAssets.tapAsync({
100
323
  name: pluginName,
101
- stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE
102
- }, (assets, callback) => {
324
+ stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE // Or a later stage if needed
325
+
326
+ }, (unusedAssets, callback) => {
327
+ if (!this.options.enable) {
328
+ return callback();
329
+ }
330
+
103
331
  const {
104
- sortedOriginalKeys,
105
- totalKeys
106
- } = this.loadNumericMapOnce(compilation);
332
+ allI18nObject,
333
+ locales,
334
+ jsResourceBase
335
+ } = this.loadAllI18nData(compilation);
336
+
337
+ if (!locales || locales.length === 0) {
338
+ if (this.options.propertiesFolderPath) {
339
+ // Only warn if path was given
340
+ compilation.warnings.push(new Error(`${pluginName}: No locales found or no translation data loaded from ${this.options.propertiesFolderPath}.`));
341
+ }
107
342
 
108
- if (totalKeys === 0 && (!sortedOriginalKeys || sortedOriginalKeys.length === 0)) {
109
- compilation.logger.info(`${pluginName}: No numeric keys to process. Skipping numeric file emission.`);
343
+ return callback();
110
344
  }
111
345
 
112
- const globallyUsedLiteralKeys = new Set();
346
+ const numericMapData = this.loadNumericMapOnce(compilation);
347
+ const sortedOriginalKeys = numericMapData.sortedOriginalKeys || [];
348
+ const totalKeys = numericMapData.totalKeys || 0;
113
349
  const globallyUsedCommentKeys = new Set();
114
350
 
115
351
  for (const module of compilation.modules) {
116
- if (module.buildInfo) {
117
- if (Array.isArray(module.buildInfo.loaderIdentifiedLiteralI18nKeys)) {
118
- module.buildInfo.loaderIdentifiedLiteralI18nKeys.forEach(key => globallyUsedLiteralKeys.add(key));
119
- }
120
-
121
- if (Array.isArray(module.buildInfo.loaderIdentifiedCommentI18nKeys)) {
122
- module.buildInfo.loaderIdentifiedCommentI18nKeys.forEach(key => globallyUsedCommentKeys.add(key));
123
- }
352
+ if (module.buildInfo && Array.isArray(module.buildInfo.loaderIdentifiedCommentI18nKeys)) {
353
+ module.buildInfo.loaderIdentifiedCommentI18nKeys.forEach(key => globallyUsedCommentKeys.add(key));
124
354
  }
125
355
  }
126
356
 
127
- compilation.logger.info(`${pluginName}: Globally identified Literal Keys: ${globallyUsedLiteralKeys.size}, Comment Keys: ${globallyUsedCommentKeys.size}`);
128
- const {
129
- locales,
130
- allI18nObject,
131
- numericFilenameTemplate,
132
- dynamicFilenameTemplate,
133
- numericJsonpFunc,
134
- dynamicJsonpFunc
135
- } = this.options;
136
-
137
- if (!locales || !allI18nObject || !numericFilenameTemplate || !dynamicFilenameTemplate || !numericJsonpFunc || !dynamicJsonpFunc) {
138
- compilation.errors.push(new Error(`${pluginName}: Missing some required options (locales, allI18nObject, filename templates, jsonp funcs).`));
139
- return callback();
357
+ const numericFilenameTemplate = this.options.numericFilenameTemplate;
358
+ const dynamicFilenameTemplate = this.options.dynamicFilenameTemplate;
359
+ const jsonpFunction = this.options.jsonpFunc || 'window.loadI18nChunk';
360
+
361
+ if (!numericFilenameTemplate) {
362
+ compilation.errors.push(new Error(`${pluginName}: Missing required option 'numericFilenameTemplate' in plugin options.`)); // return callback(); // Allow processing other chunks if one template is missing
140
363
  }
141
364
 
142
- if (totalKeys > 0 && sortedOriginalKeys && sortedOriginalKeys.length > 0) {
143
- for (const locale of locales) {
144
- const localeTranslations = allI18nObject[locale] || {};
145
- const orderedNumericTranslations = new Array(totalKeys);
365
+ if (!dynamicFilenameTemplate) {
366
+ compilation.errors.push(new Error(`${pluginName}: Missing required option 'dynamicFilenameTemplate' in plugin options.`)); // return callback();
367
+ }
368
+
369
+ if (typeof numericFilenameTemplate === 'string' && !numericFilenameTemplate.includes('[locale]') && !numericFilenameTemplate.includes('[name]') && !numericFilenameTemplate.includes('[id]')) {
370
+ compilation.warnings.push(new Error( // Changed to warning as it might be intentional for a single combined file not per-locale
371
+ `${pluginName}: The 'numericFilenameTemplate' ("${numericFilenameTemplate}") ` + `does not include '[locale]', '[name]', or '[id]'. All locales will overwrite the same file if multiple locales exist.`));
372
+ }
373
+
374
+ if (typeof dynamicFilenameTemplate === 'string' && !dynamicFilenameTemplate.includes('[locale]') && !dynamicFilenameTemplate.includes('[name]') && !dynamicFilenameTemplate.includes('[id]')) {
375
+ compilation.warnings.push(new Error(`${pluginName}: The 'dynamicFilenameTemplate' ("${dynamicFilenameTemplate}") ` + `does not include '[locale]', '[name]', or '[id]'. All locales will overwrite the same file if multiple locales exist.`));
376
+ }
377
+
378
+ const numericKeysSet = new Set(sortedOriginalKeys); // Use JS resource base file as the master reference for keys
379
+
380
+ const jsResourceBaseTranslations = jsResourceBase || {}; // Get English translations for fallback
381
+
382
+ const englishTranslations = allI18nObject.en || allI18nObject[this.options.defaultLocaleForBaseFile || 'en'] || jsResourceBaseTranslations; // Log base file and numeric map stats
146
383
 
384
+ console.log(`${pluginName}: === DEBUGGING STATS ===`);
385
+ console.log(`${pluginName}: JS Resource base file keys count: ${Object.keys(jsResourceBaseTranslations).length}`);
386
+ console.log(`${pluginName}: English fallback keys count: ${Object.keys(englishTranslations).length}`);
387
+ console.log(`${pluginName}: Numeric map total keys: ${totalKeys}`);
388
+ console.log(`${pluginName}: Numeric map actual keys: ${sortedOriginalKeys.filter(key => key !== undefined).length}`);
389
+ console.log(`${pluginName}: Global comment keys count: ${globallyUsedCommentKeys.size}`);
390
+
391
+ for (const locale of locales) {
392
+ const localeTranslations = allI18nObject[locale] || {};
393
+ const numericDataForLocale = []; // Fill numericDataForLocale based on sortedOriginalKeys
394
+
395
+ if (totalKeys > 0 && sortedOriginalKeys.length > 0) {
147
396
  for (let i = 0; i < totalKeys; i++) {
148
- const originalKey = sortedOriginalKeys[i];
397
+ // Iterate up to totalKeys (max expected length)
398
+ const originalKey = sortedOriginalKeys[i]; // Get key if available
399
+ // If originalKey is undefined (i < sortedOriginalKeys.length but originalKey is not there due to sparse array or shorter sortedKeys)
400
+ // or if the translation is missing, use English fallback if available, otherwise null.
149
401
 
150
- if (globallyUsedLiteralKeys.has(originalKey)) {
151
- orderedNumericTranslations[i] = localeTranslations[originalKey] !== undefined ? localeTranslations[originalKey] : 0; // Use 0 or null as placeholder for missing keys
152
- } else {
153
- orderedNumericTranslations[i] = 0; // Placeholder for keys not identified as literals or not used
402
+ let value = null;
403
+
404
+ if (originalKey !== undefined && jsResourceBaseTranslations[originalKey] !== undefined) {
405
+ if (localeTranslations[originalKey] !== undefined) {
406
+ value = this.intern(localeTranslations[originalKey]);
407
+ } else if (englishTranslations[originalKey] !== undefined) {
408
+ // Fallback to English value for missing key
409
+ value = this.intern(englishTranslations[originalKey]);
410
+ }
154
411
  }
412
+
413
+ numericDataForLocale.push(value);
155
414
  }
415
+ } // Convert to compact format
156
416
 
157
- this.emitFile(compilation, numericFilenameTemplate, locale, orderedNumericTranslations, numericJsonpFunc, 'numeric');
158
- }
159
- }
160
417
 
161
- if (globallyUsedCommentKeys.size > 0) {
162
- for (const locale of locales) {
163
- const localeTranslations = allI18nObject[locale] || {};
164
- const dynamicKeyTranslations = {};
165
- globallyUsedCommentKeys.forEach(originalKey => {
418
+ const compactNumericData = this.generateCompactNumericData(numericDataForLocale);
419
+ const dynamicDataForLocale = Object.create(null); // Use null prototype for cleaner object
420
+ // Process globally used comment keys first (only if they exist in JS resource base)
421
+
422
+ globallyUsedCommentKeys.forEach(originalKey => {
423
+ if (!numericKeysSet.has(originalKey) && jsResourceBaseTranslations[originalKey] !== undefined) {
424
+ let value = null;
425
+
166
426
  if (localeTranslations[originalKey] !== undefined) {
167
- dynamicKeyTranslations[originalKey] = localeTranslations[originalKey];
168
- } else {
169
- dynamicKeyTranslations[originalKey] = null;
427
+ value = this.intern(localeTranslations[originalKey]);
428
+ } else if (englishTranslations[originalKey] !== undefined) {
429
+ // Fallback to English value for missing key
430
+ value = this.intern(englishTranslations[originalKey]);
170
431
  }
171
- });
172
432
 
173
- if (Object.keys(dynamicKeyTranslations).length > 0) {
174
- this.emitFile(compilation, dynamicFilenameTemplate, locale, dynamicKeyTranslations, dynamicJsonpFunc, 'dynamic');
433
+ dynamicDataForLocale[originalKey] = value;
175
434
  }
435
+ }); // Process remaining keys from JS resource base (keys in JS resource but not in numeric mapping)
436
+ // Only include keys that exist in the JS resource base file
437
+
438
+ Object.keys(jsResourceBaseTranslations).forEach(originalKey => {
439
+ // Add if not in numeric set AND not already added from globallyUsedCommentKeys
440
+ if (!numericKeysSet.has(originalKey) && !Object.prototype.hasOwnProperty.call(dynamicDataForLocale, originalKey)) {
441
+ let value = null;
442
+
443
+ if (localeTranslations[originalKey] !== undefined) {
444
+ value = this.intern(localeTranslations[originalKey]);
445
+ } else if (englishTranslations[originalKey] !== undefined) {
446
+ // Use English fallback for missing key
447
+ value = this.intern(englishTranslations[originalKey]);
448
+ }
449
+
450
+ dynamicDataForLocale[originalKey] = value;
451
+ }
452
+ }); // Log per-locale stats
453
+
454
+ const numericKeysWithValues = numericDataForLocale.filter(v => v !== null).length;
455
+ const numericKeysWithFallbacks = numericDataForLocale.filter((v, i) => {
456
+ const originalKey = sortedOriginalKeys[i];
457
+ return v !== null && originalKey && localeTranslations[originalKey] === undefined && englishTranslations[originalKey] !== undefined;
458
+ }).length;
459
+ const dynamicKeysCount = Object.keys(dynamicDataForLocale).length;
460
+ const dynamicKeysWithFallbacks = Object.keys(dynamicDataForLocale).filter(key => localeTranslations[key] === undefined && englishTranslations[key] !== undefined).length; // Count how many locale keys are filtered out (not in JS resource base)
461
+
462
+ const localeKeysCount = Object.keys(localeTranslations).length;
463
+ const localeKeysInBase = Object.keys(localeTranslations).filter(key => jsResourceBaseTranslations[key] !== undefined).length;
464
+ const localeKeysFiltered = localeKeysCount - localeKeysInBase;
465
+ console.log(`${pluginName}: ${locale} - Total keys in locale file: ${localeKeysCount}`);
466
+ console.log(`${pluginName}: ${locale} - Keys from locale matching JS resource base: ${localeKeysInBase}`);
467
+ console.log(`${pluginName}: ${locale} - Keys filtered out (not in base): ${localeKeysFiltered}`);
468
+ console.log(`${pluginName}: ${locale} - Numeric: ${numericKeysWithValues}/${numericDataForLocale.length} (${numericKeysWithFallbacks} fallbacks)`);
469
+ console.log(`${pluginName}: ${locale} - Dynamic: ${dynamicKeysCount} (${dynamicKeysWithFallbacks} fallbacks)`);
470
+ console.log(`${pluginName}: ${locale} - Total emitted: ${numericKeysWithValues + dynamicKeysCount}`);
471
+ console.log(`${pluginName}: ---`); // Keep original debug logging for compatibility
472
+
473
+ if (this.options.logKeyCounts && this.options.isDevelopment && locale === (this.options.logKeyCountsForLocale || 'en_US')) {
474
+ console.log(`${pluginName}: ${locale} - Numeric: ${numericKeysWithValues}/${numericDataForLocale.length}, Dynamic: ${Object.keys(dynamicDataForLocale).length}`);
176
475
  }
177
- } else {
178
- compilation.logger.info(`${pluginName}: No globally used comment keys found. Skipping dynamic file emission.`);
179
- }
180
476
 
477
+ if (numericFilenameTemplate && (this.options.useCompactFormat ? compactNumericData.length > 0 : numericDataForLocale.length > 0)) {
478
+ this.emitChunkFile(compilation, numericFilenameTemplate, locale, this.options.useCompactFormat ? compactNumericData : numericDataForLocale, jsonpFunction, 'numeric');
479
+ }
480
+
481
+ if (dynamicFilenameTemplate && Object.keys(dynamicDataForLocale).length > 0) {
482
+ this.emitChunkFile(compilation, dynamicFilenameTemplate, locale, dynamicDataForLocale, jsonpFunction, 'dynamic');
483
+ }
484
+ } // Clear string pool after processing to free memory
485
+
486
+
487
+ this.stringPool.clear();
181
488
  callback();
182
489
  });
183
490
  });
@@ -7,6 +7,10 @@ exports.I18nFilesEmitPlugin = void 0;
7
7
 
8
8
  var _webpack = require("webpack");
9
9
 
10
+ var _fs = _interopRequireDefault(require("fs"));
11
+
12
+ var _path = _interopRequireDefault(require("path"));
13
+
10
14
  var _createHash = require("./createHash");
11
15
 
12
16
  var _pathCreator = require("./pathCreator");
@@ -15,6 +19,8 @@ var _propertiesUtils = require("./utils/propertiesUtils");
15
19
 
16
20
  var _LocaleChunkAssetsStore = require("./LocaleChunkAssetsStore");
17
21
 
22
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
23
+
18
24
  /* eslint-disable no-restricted-syntax */
19
25
  const pluginName = 'I18nFilesEmitPlugin';
20
26
  const {
@@ -23,18 +29,57 @@ const {
23
29
 
24
30
  class I18nFilesEmitPlugin {
25
31
  constructor(options) {
26
- this.options = options; // this.options = {
32
+ this.options = options;
33
+ this.numericIdMap = null; // this.options = {
27
34
  // locales: options.locales,
28
35
  // chunkFilename: options.chunkFilename,
29
36
  // filename: options.filename,
30
37
  // allI18nObject: options.allI18nObject,
31
- // jsonpFunc: options.jsonpFunc
38
+ // jsonpFunc: options.jsonpFunc,
39
+ // useNumericIndexing: options.useNumericIndexing, // NEW OPTION
40
+ // numericMapPath: options.numericMapPath // NEW OPTION
32
41
  // };
33
42
  }
34
43
 
44
+ loadNumericIdMap(compilation) {
45
+ if (this.numericIdMap || !this.options.useNumericIndexing || !this.options.numericMapPath) {
46
+ return this.numericIdMap;
47
+ }
48
+
49
+ const absoluteMapPath = _path.default.isAbsolute(this.options.numericMapPath) ? this.options.numericMapPath : _path.default.resolve(compilation.compiler.context, this.options.numericMapPath);
50
+
51
+ try {
52
+ if (_fs.default.existsSync(absoluteMapPath)) {
53
+ const fileContent = _fs.default.readFileSync(absoluteMapPath, 'utf-8');
54
+
55
+ const parsedData = JSON.parse(fileContent);
56
+
57
+ if (parsedData && parsedData.originalKeyToNumericId && typeof parsedData.originalKeyToNumericId === 'object') {
58
+ this.numericIdMap = parsedData.originalKeyToNumericId;
59
+ } else {
60
+ compilation.warnings.push(new Error(`${pluginName}: Invalid numeric map format in ${absoluteMapPath}`));
61
+ this.numericIdMap = {};
62
+ }
63
+ } else {
64
+ compilation.warnings.push(new Error(`${pluginName}: Numeric map file not found: ${absoluteMapPath}`));
65
+ this.numericIdMap = {};
66
+ }
67
+ } catch (err) {
68
+ compilation.errors.push(new Error(`${pluginName}: Error loading numeric map from ${absoluteMapPath}: ${err.message}`));
69
+ this.numericIdMap = {};
70
+ }
71
+
72
+ return this.numericIdMap;
73
+ }
74
+
35
75
  apply(compiler) {
36
76
  compiler.hooks.thisCompilation.tap(pluginName, compilation => {
37
- // Get store for cache
77
+ // Load numeric map if needed
78
+ if (this.options.useNumericIndexing) {
79
+ this.loadNumericIdMap(compilation);
80
+ } // Get store for cache
81
+
82
+
38
83
  this.store = (0, _LocaleChunkAssetsStore.getLocaleChunkAssetsStore)(compilation);
39
84
  const i18nStore = this.store;
40
85
  compilation.hooks.beforeHash.tap(pluginName, () => {
@@ -150,12 +195,28 @@ class I18nFilesEmitPlugin {
150
195
 
151
196
  getI18nContentForkeys(i18nKeys, locale) {
152
197
  const {
153
- allI18nObject
198
+ allI18nObject,
199
+ useNumericIndexing
154
200
  } = this.options;
155
201
  const data = {};
156
202
 
157
203
  for (const key of i18nKeys) {
158
- data[key] = allI18nObject[locale][key];
204
+ const value = allI18nObject[locale][key];
205
+
206
+ if (useNumericIndexing && this.numericIdMap) {
207
+ // Use numeric ID as key if available, otherwise keep original key
208
+ const numericId = this.numericIdMap[key];
209
+
210
+ if (numericId !== undefined) {
211
+ data[numericId] = value;
212
+ } else {
213
+ // Keep original key if no numeric mapping exists
214
+ data[key] = value;
215
+ }
216
+ } else {
217
+ // Default behavior: use original key
218
+ data[key] = value;
219
+ }
159
220
  }
160
221
 
161
222
  return data;