i18ntk 3.0.0 → 3.1.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/CHANGELOG.md +57 -16
- package/README.md +226 -98
- package/main/i18ntk-sizing.js +471 -218
- package/main/i18ntk-translate.js +399 -68
- package/main/i18ntk-validate.js +26 -14
- package/main/manage/commands/TranslateCommand.js +273 -52
- package/main/manage/commands/ValidateCommand.js +25 -13
- package/package.json +4 -1
- package/settings/settings-cli.js +75 -29
- package/settings/settings-manager.js +109 -1
- package/ui-locales/de.json +14 -14
- package/ui-locales/en.json +14 -14
- package/ui-locales/es.json +14 -14
- package/ui-locales/fr.json +14 -14
- package/ui-locales/ja.json +14 -14
- package/ui-locales/ru.json +18 -17
- package/ui-locales/zh.json +14 -14
- package/utils/config-manager.js +20 -4
- package/utils/security.js +4 -3
- package/utils/translate/cli.js +20 -16
- package/utils/translate/placeholder.js +60 -0
- package/utils/translate/protection.js +243 -0
- package/utils/translate/report.js +49 -22
- package/utils/validation-risk.js +175 -0
package/utils/config-manager.js
CHANGED
|
@@ -121,7 +121,7 @@ const DEFAULT_CONFIG = {
|
|
|
121
121
|
"complete": null,
|
|
122
122
|
"manage": null
|
|
123
123
|
},
|
|
124
|
-
"processing": {
|
|
124
|
+
"processing": {
|
|
125
125
|
"batchSize": 2000,
|
|
126
126
|
"concurrency": 32,
|
|
127
127
|
"maxFileSize": 524288,
|
|
@@ -150,9 +150,25 @@ const DEFAULT_CONFIG = {
|
|
|
150
150
|
"streaming": true,
|
|
151
151
|
"compression": "brotli",
|
|
152
152
|
"parallelProcessing": true,
|
|
153
|
-
"minimalLogging": true
|
|
154
|
-
},
|
|
155
|
-
"
|
|
153
|
+
"minimalLogging": true
|
|
154
|
+
},
|
|
155
|
+
"autoTranslate": {
|
|
156
|
+
"placeholderMode": "preserve",
|
|
157
|
+
"concurrency": 6,
|
|
158
|
+
"batchSize": 100,
|
|
159
|
+
"progressInterval": 25,
|
|
160
|
+
"retryCount": 3,
|
|
161
|
+
"retryDelay": 1000,
|
|
162
|
+
"timeout": 15000,
|
|
163
|
+
"dryRunFirst": true,
|
|
164
|
+
"reportStdout": true,
|
|
165
|
+
"bom": false,
|
|
166
|
+
"protectionEnabled": true,
|
|
167
|
+
"protectionFile": "./i18ntk-auto-translate.json",
|
|
168
|
+
"promptProtectionSetup": true,
|
|
169
|
+
"promptProtectionUpdate": true
|
|
170
|
+
},
|
|
171
|
+
"reports": {
|
|
156
172
|
"format": "json",
|
|
157
173
|
"includeStats": true,
|
|
158
174
|
"includeMissingKeys": true,
|
package/utils/security.js
CHANGED
|
@@ -687,12 +687,13 @@ static _logging = false;
|
|
|
687
687
|
'projectRoot', 'sourceDir', 'i18nDir', 'outputDir', 'backupDir', 'tempDir', 'cacheDir', 'configDir',
|
|
688
688
|
// Language settings
|
|
689
689
|
'sourceLanguage', 'uiLanguage', 'language', 'defaultLanguages', 'supportedLanguages',
|
|
690
|
-
// Translation markers and content
|
|
691
|
-
'notTranslatedMarker', 'notTranslatedMarkers', 'translatedMarker', 'translatedMarkers',
|
|
690
|
+
// Translation markers and content
|
|
691
|
+
'notTranslatedMarker', 'notTranslatedMarkers', 'translatedMarker', 'translatedMarkers',
|
|
692
|
+
'allowedEnglishTerms', 'englishContentThresholdPercent',
|
|
692
693
|
// File handling
|
|
693
694
|
'supportedExtensions', 'excludeFiles', 'excludeDirs', 'includeFiles', 'includeDirs',
|
|
694
695
|
// Operational settings
|
|
695
|
-
'strictMode', 'debug', 'displayPaths', 'version', 'scriptDirectories',
|
|
696
|
+
'strictMode', 'debug', 'displayPaths', 'version', 'scriptDirectories', 'autoTranslate',
|
|
696
697
|
// Framework and processing
|
|
697
698
|
'framework', 'processing', 'performance', 'advanced',
|
|
698
699
|
// UI and theme settings
|
package/utils/translate/cli.js
CHANGED
|
@@ -6,18 +6,19 @@ const PLACEHOLDER_WARNING = [
|
|
|
6
6
|
' WARNING: DYNAMIC PLACEHOLDER TOKENS DETECTED',
|
|
7
7
|
'============================================================',
|
|
8
8
|
'',
|
|
9
|
-
'
|
|
10
|
-
'
|
|
9
|
+
' Auto Translate can preserve placeholders while translating',
|
|
10
|
+
' only the text around tokens like:',
|
|
11
11
|
'',
|
|
12
12
|
' {name} {{count}} %d %s :param ${var}',
|
|
13
13
|
'',
|
|
14
|
-
'
|
|
15
|
-
'
|
|
14
|
+
' Sending placeholders to a translation provider can corrupt',
|
|
15
|
+
' runtime substitution in your application.',
|
|
16
16
|
'',
|
|
17
|
-
' You have
|
|
17
|
+
' You have three choices for strings containing placeholders:',
|
|
18
18
|
'',
|
|
19
|
-
'
|
|
20
|
-
'
|
|
19
|
+
' PRESERVE - Translate text segments and reinsert placeholders',
|
|
20
|
+
' SKIP - Copy verbatim; manually translate later',
|
|
21
|
+
' SEND - Translate anyway with masking',
|
|
21
22
|
'',
|
|
22
23
|
'============================================================',
|
|
23
24
|
].join('\n');
|
|
@@ -28,18 +29,20 @@ async function confirmGlobalChoice() {
|
|
|
28
29
|
console.log(' What should we do with ALL strings that contain');
|
|
29
30
|
console.log(' dynamic placeholder tokens?');
|
|
30
31
|
console.log('');
|
|
31
|
-
console.log(' [
|
|
32
|
-
console.log(' [
|
|
33
|
-
console.log(' [
|
|
32
|
+
console.log(' [p] PRESERVE all - Translate text around placeholders (recommended)');
|
|
33
|
+
console.log(' [s] SKIP all - Copy verbatim, translate nothing with placeholders');
|
|
34
|
+
console.log(' [t] SEND all - Translate everything with placeholder masking');
|
|
35
|
+
console.log(' [i] ASK each - Decide individually for each key');
|
|
34
36
|
console.log('');
|
|
35
37
|
|
|
36
38
|
while (true) {
|
|
37
|
-
const answer = await ask(' Choice [s/t/i]: ');
|
|
39
|
+
const answer = await ask(' Choice [p/s/t/i]: ');
|
|
38
40
|
const lower = answer.toLowerCase().trim();
|
|
41
|
+
if (lower === '' || lower === 'p' || lower === 'preserve') return { strategy: 'preserve', interactive: false };
|
|
39
42
|
if (lower === 's' || lower === 'skip') return { strategy: 'skip', interactive: false };
|
|
40
43
|
if (lower === 't' || lower === 'send') return { strategy: 'send', interactive: false };
|
|
41
|
-
if (lower === 'i' || lower === 'ask' || lower === 'interactive') return { strategy: '
|
|
42
|
-
console.log(' Please enter s, t, or i.');
|
|
44
|
+
if (lower === 'i' || lower === 'ask' || lower === 'interactive') return { strategy: 'preserve', interactive: true };
|
|
45
|
+
console.log(' Please enter p, s, t, or i.');
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -52,13 +55,14 @@ async function confirmPerKey(keyPath, value, placeholders) {
|
|
|
52
55
|
console.log('');
|
|
53
56
|
|
|
54
57
|
while (true) {
|
|
55
|
-
const answer = await ask(' [s]kip / [t]ranslate
|
|
58
|
+
const answer = await ask(' [p]reserve / [s]kip / [t]ranslate masked / s[k]ip all / [a]ll preserve? ');
|
|
56
59
|
const lower = answer.toLowerCase().trim();
|
|
60
|
+
if (lower === '' || lower === 'p' || lower === 'preserve') return 'preserve';
|
|
57
61
|
if (lower === 's' || lower === 'skip') return 'skip';
|
|
58
62
|
if (lower === 't' || lower === 'translate') return 'send';
|
|
59
63
|
if (lower === 'k' || lower === 'skipall') return 'skip-all';
|
|
60
|
-
if (lower === 'a' || lower === 'all') return '
|
|
61
|
-
console.log(' Please enter s, t, k, or a.');
|
|
64
|
+
if (lower === 'a' || lower === 'all') return 'preserve-all';
|
|
65
|
+
console.log(' Please enter p, s, t, k, or a.');
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -40,6 +40,7 @@ function detectPlaceholders(value, customPatterns) {
|
|
|
40
40
|
const patterns = compilePatterns(customPatterns);
|
|
41
41
|
const found = new Set();
|
|
42
42
|
for (const pattern of patterns) {
|
|
43
|
+
pattern.lastIndex = 0;
|
|
43
44
|
const matches = value.match(pattern);
|
|
44
45
|
if (matches) {
|
|
45
46
|
for (const m of matches) found.add(m);
|
|
@@ -52,11 +53,68 @@ function hasPlaceholders(value, customPatterns) {
|
|
|
52
53
|
if (!value || typeof value !== 'string') return false;
|
|
53
54
|
const patterns = compilePatterns(customPatterns);
|
|
54
55
|
for (const pattern of patterns) {
|
|
56
|
+
pattern.lastIndex = 0;
|
|
55
57
|
if (pattern.test(value)) return true;
|
|
56
58
|
}
|
|
57
59
|
return false;
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
function splitByPlaceholders(value, customPatterns) {
|
|
63
|
+
if (!value || typeof value !== 'string') {
|
|
64
|
+
return [{ type: 'text', value: value || '' }];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const patterns = compilePatterns(customPatterns);
|
|
68
|
+
const matches = [];
|
|
69
|
+
|
|
70
|
+
for (const pattern of patterns) {
|
|
71
|
+
pattern.lastIndex = 0;
|
|
72
|
+
let match;
|
|
73
|
+
while ((match = pattern.exec(value)) !== null) {
|
|
74
|
+
if (!match[0]) {
|
|
75
|
+
pattern.lastIndex++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
matches.push({
|
|
79
|
+
start: match.index,
|
|
80
|
+
end: match.index + match[0].length,
|
|
81
|
+
value: match[0],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (matches.length === 0) {
|
|
87
|
+
return [{ type: 'text', value }];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
matches.sort((a, b) => {
|
|
91
|
+
if (a.start !== b.start) return a.start - b.start;
|
|
92
|
+
return (b.end - b.start) - (a.end - a.start);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const accepted = [];
|
|
96
|
+
for (const match of matches) {
|
|
97
|
+
const overlaps = accepted.some((item) => match.start < item.end && match.end > item.start);
|
|
98
|
+
if (!overlaps) accepted.push(match);
|
|
99
|
+
}
|
|
100
|
+
accepted.sort((a, b) => a.start - b.start);
|
|
101
|
+
|
|
102
|
+
const segments = [];
|
|
103
|
+
let cursor = 0;
|
|
104
|
+
for (const match of accepted) {
|
|
105
|
+
if (match.start > cursor) {
|
|
106
|
+
segments.push({ type: 'text', value: value.slice(cursor, match.start) });
|
|
107
|
+
}
|
|
108
|
+
segments.push({ type: 'placeholder', value: match.value });
|
|
109
|
+
cursor = match.end;
|
|
110
|
+
}
|
|
111
|
+
if (cursor < value.length) {
|
|
112
|
+
segments.push({ type: 'text', value: value.slice(cursor) });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return segments;
|
|
116
|
+
}
|
|
117
|
+
|
|
60
118
|
function maskPlaceholders(value, customPatterns) {
|
|
61
119
|
if (!value || typeof value !== 'string') return { masked: value, map: new Map() };
|
|
62
120
|
const patterns = compilePatterns(customPatterns);
|
|
@@ -64,6 +122,7 @@ function maskPlaceholders(value, customPatterns) {
|
|
|
64
122
|
let idx = 0;
|
|
65
123
|
let masked = value;
|
|
66
124
|
for (const pattern of patterns) {
|
|
125
|
+
pattern.lastIndex = 0;
|
|
67
126
|
masked = masked.replace(pattern, (match) => {
|
|
68
127
|
const ph = `\uE000${idx}\uE001`;
|
|
69
128
|
map.set(ph, match);
|
|
@@ -88,6 +147,7 @@ module.exports = {
|
|
|
88
147
|
compilePatterns,
|
|
89
148
|
detectPlaceholders,
|
|
90
149
|
hasPlaceholders,
|
|
150
|
+
splitByPlaceholders,
|
|
91
151
|
maskPlaceholders,
|
|
92
152
|
unmaskPlaceholders,
|
|
93
153
|
};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const SecurityUtils = require('../security');
|
|
3
|
+
|
|
4
|
+
const DEFAULT_PROTECTION_FILE = 'i18ntk-auto-translate.json';
|
|
5
|
+
const TOKEN_PREFIX = '__I18NTK_KEEP_';
|
|
6
|
+
|
|
7
|
+
function defaultProtectionConfig() {
|
|
8
|
+
return {
|
|
9
|
+
version: 1,
|
|
10
|
+
description: 'Auto Translate protection rules. Terms are masked before translation and restored after translation. Keys and values are copied from the source without translation.',
|
|
11
|
+
terms: [],
|
|
12
|
+
keys: [],
|
|
13
|
+
values: [],
|
|
14
|
+
patterns: []
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveProtectionFile(filePath, cwd = process.cwd()) {
|
|
19
|
+
const requested = filePath || DEFAULT_PROTECTION_FILE;
|
|
20
|
+
const resolved = path.isAbsolute(requested)
|
|
21
|
+
? path.resolve(requested)
|
|
22
|
+
: path.resolve(cwd, requested);
|
|
23
|
+
const root = path.resolve(cwd);
|
|
24
|
+
|
|
25
|
+
if (!resolved.startsWith(root + path.sep) && resolved !== root) {
|
|
26
|
+
throw new Error(`Protection file must be inside the project: ${requested}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return resolved;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeList(value) {
|
|
33
|
+
if (!Array.isArray(value)) return [];
|
|
34
|
+
return value
|
|
35
|
+
.filter(item => typeof item === 'string')
|
|
36
|
+
.map(item => item.trim())
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compilePatterns(patterns) {
|
|
41
|
+
const compiled = [];
|
|
42
|
+
for (const pattern of patterns) {
|
|
43
|
+
try {
|
|
44
|
+
compiled.push(new RegExp(pattern, 'g'));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn(`Invalid Auto Translate protection pattern ignored: ${pattern} (${error.message})`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return compiled;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeProtectionConfig(config = {}, filePath = null) {
|
|
53
|
+
const terms = normalizeList(config.terms);
|
|
54
|
+
const keys = normalizeList(config.keys);
|
|
55
|
+
const values = normalizeList(config.values);
|
|
56
|
+
const patterns = normalizeList(config.patterns);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
enabled: true,
|
|
60
|
+
filePath,
|
|
61
|
+
terms,
|
|
62
|
+
keys,
|
|
63
|
+
values,
|
|
64
|
+
patterns,
|
|
65
|
+
compiledPatterns: compilePatterns(patterns)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createProtectionFile(filePath, options = {}) {
|
|
70
|
+
const resolved = resolveProtectionFile(filePath, options.cwd);
|
|
71
|
+
const dir = path.dirname(resolved);
|
|
72
|
+
if (!SecurityUtils.safeExistsSync(dir, path.dirname(dir))) {
|
|
73
|
+
SecurityUtils.safeMkdirSync(dir, path.dirname(dir), { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!SecurityUtils.safeExistsSync(resolved, dir)) {
|
|
77
|
+
SecurityUtils.safeWriteFileSync(
|
|
78
|
+
resolved,
|
|
79
|
+
JSON.stringify(defaultProtectionConfig(), null, 2) + '\n',
|
|
80
|
+
dir,
|
|
81
|
+
'utf8'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return resolved;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readProtectionFile(filePath, options = {}) {
|
|
89
|
+
const resolved = resolveProtectionFile(filePath, options.cwd);
|
|
90
|
+
const raw = SecurityUtils.safeReadFileSync(resolved, path.dirname(resolved), 'utf8');
|
|
91
|
+
if (!raw) return defaultProtectionConfig();
|
|
92
|
+
return JSON.parse(raw.replace(/^\uFEFF/, ''));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function saveProtectionFile(filePath, config, options = {}) {
|
|
96
|
+
const resolved = resolveProtectionFile(filePath, options.cwd);
|
|
97
|
+
const dir = path.dirname(resolved);
|
|
98
|
+
if (!SecurityUtils.safeExistsSync(dir, path.dirname(dir))) {
|
|
99
|
+
SecurityUtils.safeMkdirSync(dir, path.dirname(dir), { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
const nextConfig = {
|
|
102
|
+
...defaultProtectionConfig(),
|
|
103
|
+
...(config || {}),
|
|
104
|
+
terms: normalizeList(config?.terms),
|
|
105
|
+
keys: normalizeList(config?.keys),
|
|
106
|
+
values: normalizeList(config?.values),
|
|
107
|
+
patterns: normalizeList(config?.patterns)
|
|
108
|
+
};
|
|
109
|
+
SecurityUtils.safeWriteFileSync(resolved, JSON.stringify(nextConfig, null, 2) + '\n', dir, 'utf8');
|
|
110
|
+
return resolved;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function loadProtectionConfig(filePath, options = {}) {
|
|
114
|
+
if (options.enabled === false) {
|
|
115
|
+
return normalizeProtectionConfig({}, null);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const resolved = resolveProtectionFile(filePath, options.cwd);
|
|
119
|
+
const dir = path.dirname(resolved);
|
|
120
|
+
|
|
121
|
+
if (!SecurityUtils.safeExistsSync(resolved, dir)) {
|
|
122
|
+
if (options.create) {
|
|
123
|
+
createProtectionFile(resolved, options);
|
|
124
|
+
} else {
|
|
125
|
+
return normalizeProtectionConfig({}, resolved);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const raw = SecurityUtils.safeReadFileSync(resolved, dir, 'utf8');
|
|
130
|
+
if (!raw) {
|
|
131
|
+
return normalizeProtectionConfig({}, resolved);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(raw.replace(/^\uFEFF/, ''));
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new Error(`Invalid Auto Translate protection JSON at ${resolved}: ${error.message}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return normalizeProtectionConfig(parsed, resolved);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function escapeRegExp(value) {
|
|
145
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function keyMatchesRule(keyPath, rule) {
|
|
149
|
+
if (rule === keyPath) return true;
|
|
150
|
+
if (!rule.includes('*')) return false;
|
|
151
|
+
const regex = new RegExp(`^${rule.split('*').map(escapeRegExp).join('.*')}$`);
|
|
152
|
+
return regex.test(keyPath);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function shouldPreserveWholeValue(keyPath, value, protection) {
|
|
156
|
+
if (!protection || protection.enabled === false) return false;
|
|
157
|
+
if (protection.keys.some(rule => keyMatchesRule(keyPath, rule))) return true;
|
|
158
|
+
const valueText = String(value);
|
|
159
|
+
return protection.values.includes(valueText) || protection.terms.includes(valueText);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function addReplacement(replacements, original) {
|
|
163
|
+
if (!original) return;
|
|
164
|
+
if (replacements.some(item => item.original === original)) return;
|
|
165
|
+
replacements.push({ original });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function collectReplacements(value, protection) {
|
|
169
|
+
const text = String(value);
|
|
170
|
+
const replacements = [];
|
|
171
|
+
|
|
172
|
+
for (const term of protection.terms || []) {
|
|
173
|
+
if (text.includes(term)) {
|
|
174
|
+
addReplacement(replacements, term);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const pattern of protection.compiledPatterns || []) {
|
|
179
|
+
pattern.lastIndex = 0;
|
|
180
|
+
let match;
|
|
181
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
182
|
+
addReplacement(replacements, match[0]);
|
|
183
|
+
if (match[0] === '') pattern.lastIndex++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return replacements.sort((a, b) => b.original.length - a.original.length);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function protectText(value, protection) {
|
|
191
|
+
if (!protection || protection.enabled === false || typeof value !== 'string') {
|
|
192
|
+
return { value, map: new Map(), count: 0 };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const replacements = collectReplacements(value, protection);
|
|
196
|
+
if (replacements.length === 0) {
|
|
197
|
+
return { value, map: new Map(), count: 0 };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let protectedValue = value;
|
|
201
|
+
const map = new Map();
|
|
202
|
+
replacements.forEach((replacement, index) => {
|
|
203
|
+
const token = `${TOKEN_PREFIX}${index}__`;
|
|
204
|
+
map.set(token, replacement.original);
|
|
205
|
+
protectedValue = protectedValue.split(replacement.original).join(token);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return { value: protectedValue, map, count: map.size };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function restoreText(value, map) {
|
|
212
|
+
if (!(map instanceof Map) || map.size === 0 || typeof value !== 'string') return value;
|
|
213
|
+
let restored = value;
|
|
214
|
+
for (const [token, original] of map.entries()) {
|
|
215
|
+
restored = restored.split(token).join(original);
|
|
216
|
+
}
|
|
217
|
+
return restored;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function hasProtectionRules(protection) {
|
|
221
|
+
return Boolean(
|
|
222
|
+
protection &&
|
|
223
|
+
(
|
|
224
|
+
protection.terms.length ||
|
|
225
|
+
protection.keys.length ||
|
|
226
|
+
protection.values.length ||
|
|
227
|
+
protection.patterns.length
|
|
228
|
+
)
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
DEFAULT_PROTECTION_FILE,
|
|
234
|
+
createProtectionFile,
|
|
235
|
+
defaultProtectionConfig,
|
|
236
|
+
hasProtectionRules,
|
|
237
|
+
loadProtectionConfig,
|
|
238
|
+
protectText,
|
|
239
|
+
readProtectionFile,
|
|
240
|
+
restoreText,
|
|
241
|
+
saveProtectionFile,
|
|
242
|
+
shouldPreserveWholeValue
|
|
243
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const SecurityUtils = require('../security');
|
|
2
3
|
|
|
3
4
|
function generateReport(skippedKeys, translatedCount, totalCount, options = {}) {
|
|
4
5
|
const {
|
|
@@ -6,7 +7,11 @@ function generateReport(skippedKeys, translatedCount, totalCount, options = {})
|
|
|
6
7
|
targetLang,
|
|
7
8
|
dryRun = false,
|
|
8
9
|
timestamp = new Date().toISOString(),
|
|
10
|
+
placeholderProtected = 0,
|
|
11
|
+
protectedSkipped = 0,
|
|
9
12
|
} = options;
|
|
13
|
+
const placeholderSkipped = skippedKeys.filter(key => key.skipReason !== 'protected');
|
|
14
|
+
const protectedKeys = skippedKeys.filter(key => key.skipReason === 'protected');
|
|
10
15
|
|
|
11
16
|
const lines = [];
|
|
12
17
|
lines.push('='.repeat(72));
|
|
@@ -20,6 +25,8 @@ function generateReport(skippedKeys, translatedCount, totalCount, options = {})
|
|
|
20
25
|
}
|
|
21
26
|
lines.push(` Total keys: ${totalCount}`);
|
|
22
27
|
lines.push(` Translated: ${translatedCount}`);
|
|
28
|
+
lines.push(` Placeholder-safe: ${String(placeholderProtected).padStart(6)}`);
|
|
29
|
+
lines.push(` Protected: ${String(protectedSkipped).padStart(6)}`);
|
|
23
30
|
lines.push(` Skipped: ${skippedKeys.length}`);
|
|
24
31
|
lines.push('='.repeat(72));
|
|
25
32
|
|
|
@@ -28,13 +35,20 @@ function generateReport(skippedKeys, translatedCount, totalCount, options = {})
|
|
|
28
35
|
lines.push(' All strings were processed. No keys were skipped.');
|
|
29
36
|
lines.push('');
|
|
30
37
|
} else {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
if (placeholderSkipped.length > 0) {
|
|
39
|
+
lines.push('');
|
|
40
|
+
lines.push(' WARNING: The following keys were SKIPPED because they contain');
|
|
41
|
+
lines.push(' dynamic placeholder tokens that should be manually translated');
|
|
42
|
+
lines.push(' to avoid runtime substitution breakage.');
|
|
43
|
+
lines.push('');
|
|
44
|
+
lines.push(' These entries were copied verbatim into the output file.');
|
|
45
|
+
lines.push(' You MUST manually translate them before using the file.');
|
|
46
|
+
}
|
|
47
|
+
if (protectedKeys.length > 0) {
|
|
48
|
+
lines.push('');
|
|
49
|
+
lines.push(' The following keys were copied unchanged because they matched');
|
|
50
|
+
lines.push(' Auto Translate protection rules for keys or exact values.');
|
|
51
|
+
}
|
|
38
52
|
lines.push('');
|
|
39
53
|
lines.push(` ${'-'.repeat(64)}`);
|
|
40
54
|
lines.push(` Key Path Original Value`);
|
|
@@ -51,20 +65,30 @@ function generateReport(skippedKeys, translatedCount, totalCount, options = {})
|
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
lines.push(` ${'-'.repeat(64)}`);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
if (placeholderSkipped.length > 0) {
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push(' REMINDER:');
|
|
71
|
+
lines.push(' 1. Open the target JSON file');
|
|
72
|
+
lines.push(' 2. Search for the placeholder-skipped keys listed above');
|
|
73
|
+
lines.push(' 3. Manually translate each value, preserving all placeholders');
|
|
74
|
+
lines.push(' exactly as they appear in the original');
|
|
75
|
+
lines.push(' 4. Verify placeholder integrity before runtime use');
|
|
76
|
+
lines.push('');
|
|
77
|
+
}
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
lines.push('');
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
if (skippedKeys.length === 0) {
|
|
82
|
+
lines.push(' The generated file can be used immediately after review.');
|
|
83
|
+
lines.push(' Placeholder tokens were preserved automatically where found.');
|
|
84
|
+
} else if (placeholderSkipped.length > 0) {
|
|
85
|
+
lines.push(' The generated file can be used immediately for all');
|
|
86
|
+
lines.push(' translated text. Only the skipped keys need');
|
|
87
|
+
lines.push(' manual attention.');
|
|
88
|
+
} else {
|
|
89
|
+
lines.push(' Protected keys and values were intentionally copied unchanged.');
|
|
90
|
+
lines.push(' Review the output file before using it in production.');
|
|
91
|
+
}
|
|
68
92
|
lines.push('='.repeat(72));
|
|
69
93
|
|
|
70
94
|
return lines.join('\n');
|
|
@@ -73,14 +97,17 @@ function generateReport(skippedKeys, translatedCount, totalCount, options = {})
|
|
|
73
97
|
function writeReport(reportText, filePath) {
|
|
74
98
|
if (!filePath) return;
|
|
75
99
|
try {
|
|
76
|
-
|
|
100
|
+
const resolvedPath = path.resolve(process.cwd(), filePath);
|
|
101
|
+
SecurityUtils.safeWriteFileSync(resolvedPath, reportText + '\n', path.dirname(resolvedPath), 'utf-8');
|
|
77
102
|
} catch (e) {
|
|
78
103
|
console.error('Failed to write report file:', e.message);
|
|
79
104
|
}
|
|
80
105
|
}
|
|
81
106
|
|
|
82
|
-
function formatSummaryLine(skippedCount, translatedCount, totalCount) {
|
|
83
|
-
|
|
107
|
+
function formatSummaryLine(skippedCount, translatedCount, totalCount, placeholderProtected = 0, protectedSkipped = 0) {
|
|
108
|
+
const protectedPart = placeholderProtected > 0 ? `, ${placeholderProtected} placeholder-safe` : '';
|
|
109
|
+
const glossaryPart = protectedSkipped > 0 ? `, ${protectedSkipped} protected` : '';
|
|
110
|
+
return `[translate] ${translatedCount} translated${protectedPart}${glossaryPart}, ${skippedCount} skipped (of ${totalCount} total keys)`;
|
|
84
111
|
}
|
|
85
112
|
|
|
86
113
|
module.exports = {
|