@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.
- package/CHANGELOG.md +28 -0
- package/README.md +130 -0
- package/README_backup.md +102 -0
- package/docs/DYNAMIC_TEMPLATE_EXAMPLE.md +129 -0
- package/docs/I18N_NUMERIC_INDEXING_PLUGIN.md +225 -0
- package/docs/I18N_SINGLE_FILE_MODE.md +126 -0
- package/docs/client_build_tool_source_doc.md +390 -0
- package/lib/schemas/defaultConfigValues.js +46 -1
- package/lib/schemas/defaultConfigValuesOnly.js +31 -4
- package/lib/shared/babel/getBabelPlugin.js +3 -4
- package/lib/shared/babel/runBabelForTsFile.js +1 -1
- package/lib/shared/bundler/webpack/custom_plugins/I18nNumericIndexPlugin/I18nGroupRuntimeModule.js +139 -0
- package/lib/shared/bundler/webpack/custom_plugins/I18nNumericIndexPlugin/I18nNumericIndexHtmlInjectorPlugin.js +98 -0
- package/lib/shared/bundler/webpack/custom_plugins/I18nNumericIndexPlugin/I18nNumericIndexPlugin.js +399 -0
- package/lib/shared/bundler/webpack/custom_plugins/I18nNumericIndexPlugin/utils/i18nDataLoader.js +141 -0
- package/lib/shared/bundler/webpack/custom_plugins/I18nSplitPlugin/utils/propertiesUtils.js +19 -1
- package/lib/shared/bundler/webpack/jsLoaders.js +24 -1
- package/lib/shared/bundler/webpack/loaderConfigs/i18nIdReplaceLoaderConfig.js +90 -0
- package/lib/shared/bundler/webpack/loaders/i18nIdReplaceLoader.js +146 -0
- package/lib/shared/bundler/webpack/pluginConfigs/configI18nIndexingPlugin.js +42 -0
- package/lib/shared/bundler/webpack/pluginConfigs/configI18nNumericHtmlInjector.js +92 -0
- package/lib/shared/bundler/webpack/pluginConfigs/configI18nNumericIndexPlugin.js +112 -0
- package/lib/shared/bundler/webpack/plugins.js +3 -1
- package/lib/shared/postcss/custom_postcss_plugins/VariableModificationPlugin/index.js +1 -1
- package/lib/shared/server/mockApiHandler.js +7 -0
- package/lib/shared/server/urlConcat.js +4 -3
- package/npm-shrinkwrap.json +8225 -32
- package/package.json +6 -5
package/lib/shared/bundler/webpack/custom_plugins/I18nNumericIndexPlugin/I18nGroupRuntimeModule.js
ADDED
|
@@ -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
|
+
};
|
package/lib/shared/bundler/webpack/custom_plugins/I18nNumericIndexPlugin/I18nNumericIndexPlugin.js
ADDED
|
@@ -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;
|