@zohodesk/client_build_tool 0.0.11-exp.18 → 0.0.11-exp.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +130 -0
  3. package/README_backup.md +102 -0
  4. package/docs/DYNAMIC_TEMPLATE_EXAMPLE.md +129 -0
  5. package/docs/I18N_NUMERIC_INDEXING_PLUGIN.md +225 -0
  6. package/docs/I18N_SINGLE_FILE_MODE.md +126 -0
  7. package/docs/client_build_tool_source_doc.md +390 -0
  8. package/lib/schemas/defaultConfigValues.js +46 -1
  9. package/lib/schemas/defaultConfigValuesOnly.js +31 -4
  10. package/lib/shared/babel/getBabelPlugin.js +3 -4
  11. package/lib/shared/babel/runBabelForTsFile.js +1 -1
  12. package/lib/shared/bundler/webpack/custom_plugins/I18nNumericIndexPlugin/I18nGroupRuntimeModule.js +139 -0
  13. package/lib/shared/bundler/webpack/custom_plugins/I18nNumericIndexPlugin/I18nNumericIndexHtmlInjectorPlugin.js +98 -0
  14. package/lib/shared/bundler/webpack/custom_plugins/I18nNumericIndexPlugin/I18nNumericIndexPlugin.js +399 -0
  15. package/lib/shared/bundler/webpack/custom_plugins/I18nNumericIndexPlugin/utils/i18nDataLoader.js +141 -0
  16. package/lib/shared/bundler/webpack/custom_plugins/I18nSplitPlugin/utils/propertiesUtils.js +19 -1
  17. package/lib/shared/bundler/webpack/jsLoaders.js +24 -1
  18. package/lib/shared/bundler/webpack/loaderConfigs/i18nIdReplaceLoaderConfig.js +90 -0
  19. package/lib/shared/bundler/webpack/loaders/i18nIdReplaceLoader.js +146 -0
  20. package/lib/shared/bundler/webpack/pluginConfigs/configI18nIndexingPlugin.js +42 -0
  21. package/lib/shared/bundler/webpack/pluginConfigs/configI18nNumericHtmlInjector.js +92 -0
  22. package/lib/shared/bundler/webpack/pluginConfigs/configI18nNumericIndexPlugin.js +112 -0
  23. package/lib/shared/bundler/webpack/plugins.js +3 -1
  24. package/lib/shared/postcss/custom_postcss_plugins/VariableModificationPlugin/index.js +1 -1
  25. package/lib/shared/server/mockApiHandler.js +7 -0
  26. package/lib/shared/server/urlConcat.js +4 -3
  27. package/npm-shrinkwrap.json +8225 -32
  28. package/package.json +6 -5
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.I18nGroupRuntimeModule = void 0;
7
+
8
+ var _webpack = require("webpack");
9
+
10
+ class I18nGroupRuntimeModule extends _webpack.RuntimeModule {
11
+ constructor(options) {
12
+ super('i18n-group-loader');
13
+ this.options = options;
14
+ }
15
+
16
+ generate() {
17
+ const {
18
+ customGroups,
19
+ localeVarName,
20
+ jsonpFunc
21
+ } = this.options; // Build chunk-to-group mapping from config
22
+
23
+ const chunkToGroup = {};
24
+ Object.entries(customGroups || {}).forEach(([groupName, config]) => {
25
+ (config.chunks || []).forEach(chunkName => {
26
+ chunkToGroup[chunkName] = groupName;
27
+ });
28
+ });
29
+ return `
30
+ // I18n Group Loading Runtime
31
+ (function() {
32
+ var loadedGroups = {};
33
+ var chunkToGroup = ${JSON.stringify(chunkToGroup)};
34
+
35
+ // Function to load i18n group
36
+ function loadI18nGroup(groupName) {
37
+ if (loadedGroups[groupName]) {
38
+ return Promise.resolve();
39
+ }
40
+
41
+ var locale = ${localeVarName} || 'en_US';
42
+ var groupConfig = ${JSON.stringify(customGroups)};
43
+ var config = groupConfig[groupName];
44
+
45
+ if (!config) {
46
+ return Promise.resolve();
47
+ }
48
+
49
+ return new Promise(function(resolve, reject) {
50
+ var i18nUrl = config.filenameTemplate
51
+ .replace('[locale]', locale)
52
+ .replace('i18n-chunk/', __webpack_require__.p + 'i18n-chunk/');
53
+
54
+
55
+ var script = document.createElement('script');
56
+ script.src = i18nUrl;
57
+ script.async = true;
58
+
59
+ script.onload = function() {
60
+ loadedGroups[groupName] = true;
61
+ resolve();
62
+ };
63
+
64
+ script.onerror = function() {
65
+ reject(new Error('Failed to load i18n group: ' + groupName));
66
+ };
67
+
68
+ document.head.appendChild(script);
69
+ });
70
+ }
71
+
72
+ // Store original webpack chunk loading function
73
+ var originalEnsureChunk = __webpack_require__.e;
74
+
75
+ // Override webpack's chunk loading if it exists
76
+ if (originalEnsureChunk) {
77
+ __webpack_require__.e = function(chunkId) {
78
+ var promise = originalEnsureChunk.apply(this, arguments);
79
+
80
+ // Check if this chunk needs an i18n group
81
+ var groupName = chunkToGroup[chunkId];
82
+ if (groupName && !loadedGroups[groupName]) {
83
+ // Load the i18n group before the chunk
84
+ var i18nPromise = loadI18nGroup(groupName);
85
+ // Chain the promises so i18n loads first
86
+ promise = Promise.all([promise, i18nPromise]).then(function(results) {
87
+ return results[0]; // Return the original chunk promise result
88
+ });
89
+ }
90
+
91
+ return promise;
92
+ };
93
+ }
94
+
95
+ // Also check for webpackI18nGroup comments in dynamic imports
96
+ if (typeof __webpack_require__.l !== 'undefined') {
97
+ var originalLoadScript = __webpack_require__.l;
98
+ __webpack_require__.l = function(url, done, key, chunkId) {
99
+ // Check if chunk has i18n group
100
+ var groupName = chunkToGroup[chunkId];
101
+ if (groupName && !loadedGroups[groupName]) {
102
+ // Load i18n before main chunk
103
+ var locale = ${localeVarName} || 'en_US';
104
+ var groupConfig = ${JSON.stringify(customGroups)};
105
+ var config = groupConfig[groupName];
106
+
107
+ if (config) {
108
+ var i18nUrl = config.filenameTemplate
109
+ .replace('[locale]', locale)
110
+ .replace('i18n-chunk/', __webpack_require__.p + 'i18n-chunk/');
111
+
112
+ // Load i18n first, then the chunk
113
+ var i18nScript = document.createElement('script');
114
+ i18nScript.src = i18nUrl;
115
+ i18nScript.onload = function() {
116
+ loadedGroups[groupName] = true;
117
+ // Now load the original chunk
118
+ originalLoadScript.call(__webpack_require__, url, done, key, chunkId);
119
+ };
120
+ i18nScript.onerror = function() {
121
+ // Continue even if i18n fails
122
+ originalLoadScript.call(__webpack_require__, url, done, key, chunkId);
123
+ };
124
+ document.head.appendChild(i18nScript);
125
+ return;
126
+ }
127
+ }
128
+
129
+ // Default behavior
130
+ return originalLoadScript.call(__webpack_require__, url, done, key, chunkId);
131
+ };
132
+ }
133
+ })();
134
+ `;
135
+ }
136
+
137
+ }
138
+
139
+ exports.I18nGroupRuntimeModule = I18nGroupRuntimeModule;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+
3
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
4
+
5
+ const path = require('path');
6
+
7
+ const pluginName = 'I18nNumericIndexHtmlInjectorPlugin';
8
+
9
+ class I18nNumericIndexHtmlInjectorPlugin {
10
+ constructor(options) {
11
+ this.options = { ...options,
12
+ injectI18nUrlInIndex: options.injectI18nUrlInIndex !== undefined ? options.injectI18nUrlInIndex : true // Default to true
13
+
14
+ };
15
+ }
16
+
17
+ apply(compiler) {
18
+ compiler.hooks.thisCompilation.tap(pluginName, compilation => {
19
+ HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(pluginName, (hookData, cb) => {
20
+ // Skip HTML injection if injectI18nUrlInIndex is false
21
+ if (!this.options.injectI18nUrlInIndex) {
22
+ return cb(null, hookData);
23
+ }
24
+
25
+ const {
26
+ assets
27
+ } = hookData;
28
+ const {
29
+ outputFolder,
30
+ numericFilenameTemplate,
31
+ dynamicFilenameTemplate,
32
+ singleFileTemplate,
33
+ htmlTemplateLabel,
34
+ singleFile,
35
+ i18nAssetsPublicPathPrefix = ''
36
+ } = this.options;
37
+ const newI18nAssetUrlsToAdd = []; // Construct full paths using outputFolder
38
+
39
+ const constructFullPath = (template, isSingleFile = false) => {
40
+ if (!template) return null; // Replace locale placeholder
41
+
42
+ let filePath = template.replace(/\[locale\]/g, htmlTemplateLabel); // Remove [contenthash] placeholder for HTML injection
43
+ // The actual hash will be determined at build time
44
+
45
+ filePath = filePath.replace(/\.\[contenthash\]/g, ''); // If template already contains outputFolder or starts with a path separator, use as-is
46
+
47
+ if (filePath.includes(outputFolder) || filePath.startsWith('/')) {
48
+ return filePath.replace(/\\/g, '/');
49
+ } // For single-file mode with a simple template like '[locale].js',
50
+ // put it directly in outputFolder without subdirectories
51
+
52
+
53
+ if (isSingleFile && !filePath.includes('/')) {
54
+ return path.join(outputFolder || 'i18n-chunk', filePath).replace(/\\/g, '/');
55
+ } // For other cases, preserve subdirectories
56
+
57
+
58
+ return path.join(outputFolder || 'i18n-chunk', filePath).replace(/\\/g, '/');
59
+ };
60
+
61
+ if (singleFile) {
62
+ // In single file mode, use singleFileTemplate
63
+ const singleTemplate = singleFileTemplate || '[locale].js';
64
+ const combinedFilename = constructFullPath(singleTemplate, true);
65
+
66
+ if (combinedFilename) {
67
+ newI18nAssetUrlsToAdd.push(combinedFilename);
68
+ }
69
+ } else {
70
+ // Add both numeric and dynamic files
71
+ const numericFilename = constructFullPath(numericFilenameTemplate);
72
+
73
+ if (numericFilename) {
74
+ newI18nAssetUrlsToAdd.push(numericFilename);
75
+ }
76
+
77
+ const dynamicFilename = constructFullPath(dynamicFilenameTemplate);
78
+
79
+ if (dynamicFilename) {
80
+ newI18nAssetUrlsToAdd.push(dynamicFilename);
81
+ }
82
+ }
83
+
84
+ if (newI18nAssetUrlsToAdd.length > 0) {
85
+ // Add i18n assets to the beginning of JS assets for early loading
86
+ assets.js = [...newI18nAssetUrlsToAdd, ...assets.js];
87
+ }
88
+
89
+ cb(null, hookData);
90
+ });
91
+ });
92
+ }
93
+
94
+ }
95
+
96
+ module.exports = {
97
+ I18nNumericIndexHtmlInjectorPlugin
98
+ };
@@ -0,0 +1,399 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+
8
+ var _fs = _interopRequireDefault(require("fs"));
9
+
10
+ var _path = _interopRequireDefault(require("path"));
11
+
12
+ var _webpack = require("webpack");
13
+
14
+ var _propertiesUtils = require("../I18nSplitPlugin/utils/propertiesUtils");
15
+
16
+ var _I18nGroupRuntimeModule = require("./I18nGroupRuntimeModule");
17
+
18
+ var _i18nDataLoader = require("./utils/i18nDataLoader");
19
+
20
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
21
+
22
+ const {
23
+ RawSource
24
+ } = _webpack.sources;
25
+ const pluginName = 'I18nNumericIndexPlugin';
26
+
27
+ class I18nNumericIndexPlugin {
28
+ constructor(options) {
29
+ this.options = { ...options,
30
+ singleFile: options.singleFile || false,
31
+ singleFileTemplate: options.singleFileTemplate || '[locale].js',
32
+ includeContentHash: options.includeContentHash || false,
33
+ generateManifest: options.generateManifest || false,
34
+ outputFolder: options.outputFolder || 'i18n-chunk',
35
+ manifestPath: options.manifestPath || null,
36
+ emitFiles: options.emitFiles !== undefined ? options.emitFiles : true
37
+ };
38
+ this.numericMap = {};
39
+ this.customGroups = {};
40
+ this.manifest = {};
41
+ }
42
+
43
+ apply(compiler) {
44
+ // Detect webpackI18nGroup comments in code
45
+ this.detectI18nGroupComments(compiler);
46
+ compiler.hooks.thisCompilation.tap(pluginName, compilation => {
47
+ // Add runtime module for group-based loading
48
+ if (this.options.customGroups) {
49
+ compilation.hooks.additionalTreeRuntimeRequirements.tap(pluginName, (chunk, set) => {
50
+ // Only add to the main/entry chunk to avoid duplication
51
+ if (chunk.name === 'main' || chunk.hasRuntime()) {
52
+ compilation.addRuntimeModule(chunk, new _I18nGroupRuntimeModule.I18nGroupRuntimeModule({
53
+ customGroups: this.options.customGroups,
54
+ localeVarName: this.options.localeVarName,
55
+ jsonpFunc: this.options.jsonpFunc
56
+ }));
57
+ }
58
+ });
59
+ }
60
+
61
+ compilation.hooks.processAssets.tap({
62
+ name: pluginName,
63
+ stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE
64
+ }, () => {
65
+ this.processI18nFiles(compilation);
66
+ });
67
+ });
68
+ }
69
+
70
+ detectI18nGroupComments(compiler) {
71
+ compiler.hooks.normalModuleFactory.tap(pluginName, factory => {
72
+ factory.hooks.parser.for('javascript/auto').tap(pluginName, parser => {
73
+ parser.hooks.importCall.tap(pluginName, expr => {
74
+ // Check for webpackI18nGroup comment
75
+ const comments = expr.leadingComments || [];
76
+ comments.forEach(comment => {
77
+ if (comment.value && comment.value.includes('webpackI18nGroup')) {
78
+ const match = comment.value.match(/webpackI18nGroup:\s*["']([^"']+)["']/);
79
+
80
+ if (match) {
81
+ const groupName = match[1]; // Store this information for later use
82
+
83
+ if (!this.detectedGroups) {
84
+ this.detectedGroups = {};
85
+ } // Extract chunk name from webpackChunkName comment
86
+
87
+
88
+ const chunkNameMatch = comment.value.match(/webpackChunkName:\s*["']([^"']+)["']/);
89
+
90
+ if (chunkNameMatch) {
91
+ const chunkName = chunkNameMatch[1];
92
+ this.detectedGroups[chunkName] = groupName;
93
+ }
94
+ }
95
+ }
96
+ });
97
+ });
98
+ });
99
+ });
100
+ }
101
+
102
+ processI18nFiles(compilation) {
103
+ const {
104
+ jsResourcePath,
105
+ propertiesFolderPath,
106
+ numericMapPath,
107
+ customGroups,
108
+ jsonpFunc,
109
+ numericFilenameTemplate,
110
+ dynamicFilenameTemplate
111
+ } = this.options;
112
+
113
+ if (!jsResourcePath || !propertiesFolderPath) {
114
+ return;
115
+ } // Load existing numeric map if available
116
+
117
+
118
+ if (numericMapPath) {
119
+ const mapData = (0, _i18nDataLoader.loadNumericMap)(numericMapPath, compilation);
120
+
121
+ if (mapData && mapData.sortedKeys) {
122
+ // Initialize numericMap from existing data
123
+ mapData.sortedKeys.forEach((key, id) => {
124
+ if (key) {
125
+ this.numericMap[key] = id;
126
+ }
127
+ });
128
+ }
129
+ } // Read JSResources.properties
130
+
131
+
132
+ let jsResourceKeys = (0, _propertiesUtils.getPropertiesAsJSON)(jsResourcePath);
133
+ jsResourceKeys = this.normalizeObjectValues(jsResourceKeys); // Parse custom groups from banner markers
134
+
135
+ if (customGroups) {
136
+ this.parseCustomGroups(jsResourcePath, customGroups);
137
+ } // Get all locale translations
138
+
139
+
140
+ const allI18nObject = (0, _propertiesUtils.getAllI18n)({
141
+ folderPath: propertiesFolderPath,
142
+ disableDefault: false,
143
+ jsResourceI18nKeys: jsResourceKeys
144
+ }); // Normalize locale values
145
+
146
+ Object.keys(allI18nObject).forEach(loc => {
147
+ allI18nObject[loc] = this.normalizeObjectValues(allI18nObject[loc]);
148
+ }); // For en_US, use only JSResources (don't merge with ApplicationResources_en_US)
149
+
150
+ if (allI18nObject['en_US']) {
151
+ allI18nObject['en_US'] = jsResourceKeys;
152
+ } else {
153
+ // If en_US doesn't exist in the folder, create it from JSResources
154
+ allI18nObject['en_US'] = jsResourceKeys;
155
+ } // If requested, restrict all locales to base (English) keys only
156
+
157
+
158
+ if (this.options.restrictToBaseKeys) {
159
+ const baseKeys = Object.keys(jsResourceKeys);
160
+ Object.keys(allI18nObject).forEach(locale => {
161
+ const merged = { ...jsResourceKeys,
162
+ ...allI18nObject[locale]
163
+ };
164
+ const filtered = {};
165
+ baseKeys.forEach(k => {
166
+ filtered[k] = merged[k];
167
+ });
168
+ allI18nObject[locale] = filtered;
169
+ });
170
+ }
171
+
172
+ const locales = Object.keys(allI18nObject); // Process each locale
173
+
174
+ locales.forEach(locale => {
175
+ const localeData = allI18nObject[locale];
176
+ const numericData = {};
177
+ const dynamicData = {};
178
+ const groupData = {}; // Initialize custom groups
179
+
180
+ Object.keys(customGroups || {}).forEach(groupName => {
181
+ groupData[groupName] = {};
182
+ }); // Process each key
183
+
184
+ Object.keys(localeData).forEach(key => {
185
+ const value = localeData[key]; // Simple logic: if has numeric ID use it, otherwise it's dynamic
186
+
187
+ if (this.numericMap[key]) {
188
+ const numericKey = String(this.numericMap[key]); // Check if belongs to a custom group
189
+
190
+ const belongsToGroup = this.getKeyGroup(key);
191
+
192
+ if (belongsToGroup) {
193
+ groupData[belongsToGroup][numericKey] = value;
194
+ } else {
195
+ numericData[numericKey] = value;
196
+ }
197
+ } else {
198
+ // No numeric ID = dynamic key (regardless of placeholders)
199
+ dynamicData[key] = value;
200
+ }
201
+ }); // Handle single-file mode or separate files
202
+
203
+ if (this.options.singleFile) {
204
+ // Combine numeric and dynamic data into a single file
205
+ const combinedData = { ...numericData,
206
+ ...dynamicData
207
+ };
208
+
209
+ if (Object.keys(combinedData).length > 0) {
210
+ // Use singleFileTemplate for combined file - no fileType suffix needed
211
+ const singleFileTemplate = this.options.singleFileTemplate || '[locale].js';
212
+ this.emitChunk(compilation, singleFileTemplate, locale, combinedData, jsonpFunc, null, null);
213
+ }
214
+ } else {
215
+ // Emit numeric chunk
216
+ if (Object.keys(numericData).length > 0) {
217
+ this.emitChunk(compilation, numericFilenameTemplate, locale, numericData, jsonpFunc, null, 'numeric');
218
+ } // Emit dynamic chunk
219
+
220
+
221
+ if (Object.keys(dynamicData).length > 0) {
222
+ this.emitChunk(compilation, dynamicFilenameTemplate, locale, dynamicData, jsonpFunc, null, 'dynamic');
223
+ }
224
+ } // Emit custom group chunks (always separate)
225
+
226
+
227
+ Object.entries(groupData).forEach(([groupName, data]) => {
228
+ const groupConfig = customGroups[groupName];
229
+
230
+ if (groupConfig && Object.keys(data).length > 0) {
231
+ const groupTemplate = groupConfig.filenameTemplate || `[locale]/${groupName}.i18n.js`;
232
+ this.emitChunk(compilation, groupTemplate, locale, data, jsonpFunc, groupName, `group-${groupName}`);
233
+ }
234
+ });
235
+ }); // Generate manifest file if enabled
236
+
237
+ if (this.options.generateManifest && Object.keys(this.manifest).length > 0) {
238
+ const manifestPath = this.options.manifestPath || _path.default.join(this.options.outputFolder, 'manifest.json');
239
+
240
+ const manifestContent = JSON.stringify(this.manifest, null, 2);
241
+ compilation.emitAsset(manifestPath, new RawSource(manifestContent));
242
+ } // Don't save numeric map - it should only be generated by the external script
243
+
244
+ }
245
+
246
+ parseCustomGroups(jsResourcePath, customGroups) {
247
+ const content = _fs.default.readFileSync(jsResourcePath, 'utf-8');
248
+
249
+ const lines = content.split('\n');
250
+ Object.entries(customGroups).forEach(([groupName, config]) => {
251
+ const {
252
+ bannerStart,
253
+ bannerEnd
254
+ } = config;
255
+ let inGroup = false;
256
+ const groupKeys = [];
257
+ lines.forEach(line => {
258
+ if (line.includes(bannerStart)) {
259
+ inGroup = true;
260
+ } else if (line.includes(bannerEnd)) {
261
+ inGroup = false;
262
+ } else if (inGroup && line.includes('=')) {
263
+ const key = line.split('=')[0].trim();
264
+
265
+ if (key && !key.startsWith('#')) {
266
+ groupKeys.push(key);
267
+ }
268
+ }
269
+ });
270
+ this.customGroups[groupName] = groupKeys;
271
+ });
272
+ }
273
+
274
+ getKeyGroup(key) {
275
+ for (const [groupName, keys] of Object.entries(this.customGroups)) {
276
+ if (keys.includes(key)) {
277
+ return groupName;
278
+ }
279
+ }
280
+
281
+ return null;
282
+ }
283
+
284
+ isDynamicKey(value) {
285
+ // Check if value contains placeholders like {0}, {1}, etc.
286
+ return /\{\d+\}/.test(value);
287
+ }
288
+
289
+ getNumericKey(key) {
290
+ // Return numeric ID if it exists, null otherwise
291
+ return this.numericMap[key] ? String(this.numericMap[key]) : null;
292
+ }
293
+
294
+ generateContentHash(content, compilation) {
295
+ const {
296
+ hashFunction,
297
+ hashDigest,
298
+ hashDigestLength
299
+ } = compilation.outputOptions;
300
+
301
+ const hash = _webpack.util.createHash(hashFunction || 'xxhash64');
302
+
303
+ hash.update(content);
304
+ return hash.digest(hashDigest || 'hex').substring(0, hashDigestLength || 8);
305
+ }
306
+
307
+ constructFilePath(template, locale) {
308
+ const {
309
+ outputFolder,
310
+ singleFile
311
+ } = this.options; // Replace locale placeholder
312
+
313
+ let filePath = template.replace(/\[locale\]/g, locale); // If template already contains outputFolder or starts with a path separator, use as-is
314
+
315
+ if (filePath.includes(outputFolder) || filePath.startsWith('/')) {
316
+ return filePath.replace(/\\/g, '/');
317
+ } // For single-file mode with a simple template like '[locale].js',
318
+ // put it directly in outputFolder without subdirectories
319
+
320
+
321
+ if (singleFile && !filePath.includes('/')) {
322
+ return _path.default.join(outputFolder, filePath).replace(/\\/g, '/');
323
+ } // For other cases, if template contains subdirectories, preserve them
324
+
325
+
326
+ return _path.default.join(outputFolder, filePath).replace(/\\/g, '/');
327
+ }
328
+
329
+ emitChunk(compilation, filenameTemplate, locale, data, jsonpFunc, groupName = null, fileType = null) {
330
+ if (!filenameTemplate || Object.keys(data).length === 0) {
331
+ return null;
332
+ } // Generate content regardless of emitFiles flag (needed for runtime)
333
+
334
+
335
+ const content = this.generateChunkContent(data, jsonpFunc, groupName);
336
+ let outputPath = this.constructFilePath(filenameTemplate, locale); // Handle [contenthash] placeholder in template
337
+
338
+ if (outputPath.includes('[contenthash]')) {
339
+ const contentHash = this.generateContentHash(content, compilation);
340
+ outputPath = outputPath.replace(/\[contenthash\]/g, contentHash);
341
+ } else if (this.options.includeContentHash) {
342
+ // Legacy: Add content hash before .js extension if includeContentHash is true
343
+ const contentHash = this.generateContentHash(content, compilation);
344
+ outputPath = outputPath.replace(/\.js$/, `.${contentHash}.js`);
345
+ } // Track in manifest if enabled
346
+
347
+
348
+ if (this.options.generateManifest) {
349
+ // For single-file mode, use clean locale.js format
350
+ let manifestKey;
351
+
352
+ if (this.options.singleFile) {
353
+ // Just use locale.js without path or type suffix
354
+ manifestKey = `${locale}.js`;
355
+ } else {
356
+ // For multi-file mode, include the full path
357
+ const cleanName = this.constructFilePath(filenameTemplate, locale);
358
+ manifestKey = fileType ? cleanName.replace(/\.js$/, `.${fileType}.js`) : cleanName;
359
+ }
360
+
361
+ this.manifest[manifestKey] = _path.default.basename(outputPath);
362
+ } // Only emit the file if emitFiles is true
363
+
364
+
365
+ if (this.options.emitFiles) {
366
+ compilation.emitAsset(outputPath, new RawSource(content));
367
+ }
368
+
369
+ return outputPath;
370
+ }
371
+
372
+ generateChunkContent(data, jsonpFunc, groupName) {
373
+ // Decode Unicode escapes to convert \uXXXX to actual characters
374
+ const jsonString = (0, _propertiesUtils.decodeUnicodeEscapes)(JSON.stringify(data));
375
+
376
+ if (groupName) {
377
+ // Include group name for lazy loading identification
378
+ return `${jsonpFunc}(${jsonString}, "${groupName}");`;
379
+ }
380
+
381
+ return `${jsonpFunc}(${jsonString});`;
382
+ }
383
+
384
+ normalizeValue(value) {
385
+ if (typeof value !== 'string') return value;
386
+ return value.replace(/\\t/g, '\t').replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\f/g, '\f').replace(/\\:/g, ':').replace(/\\=/g, '=').replace(/\\ /g, ' ');
387
+ }
388
+
389
+ normalizeObjectValues(obj) {
390
+ const out = {};
391
+ Object.keys(obj || {}).forEach(k => {
392
+ out[k] = this.normalizeValue(obj[k]);
393
+ });
394
+ return out;
395
+ }
396
+
397
+ }
398
+
399
+ exports.default = I18nNumericIndexPlugin;