i18ntk 3.2.0 → 4.0.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 +62 -2
- package/README.md +177 -22
- package/SECURITY.md +28 -8
- package/main/i18ntk-backup.js +305 -62
- package/main/i18ntk-complete.js +120 -49
- package/main/i18ntk-scanner.js +188 -49
- package/main/i18ntk-sizing.js +223 -29
- package/main/i18ntk-translate.js +25 -1
- package/main/i18ntk-usage.js +203 -3
- package/main/i18ntk-validate.js +107 -3
- package/main/manage/commands/FixerCommand.js +23 -21
- package/main/manage/index.js +13 -7
- package/main/manage/services/FileManagementService.js +12 -6
- package/package.json +3 -3
- package/runtime/i18ntk.d.ts +22 -16
- package/runtime/index.d.ts +9 -7
- package/runtime/index.js +240 -50
- package/ui-locales/de.json +1389 -1359
- package/ui-locales/en.json +1 -1
- package/ui-locales/es.json +1503 -1473
- package/ui-locales/fr.json +1626 -1596
- package/ui-locales/ja.json +1595 -1565
- package/ui-locales/ru.json +1638 -1608
- package/ui-locales/zh.json +1613 -1583
- package/utils/translate/api.js +164 -41
- package/utils/translate/protection.js +147 -6
- package/utils/translate/safe-network.js +280 -0
- package/utils/watch-locales.js +183 -36
package/utils/translate/api.js
CHANGED
|
@@ -1,39 +1,11 @@
|
|
|
1
|
-
const https = require('https');
|
|
2
1
|
const { URL } = require('url');
|
|
2
|
+
const { safeHttpGet, safeHttpPost, buildGoogleTranslateUrl } = require('./safe-network');
|
|
3
3
|
|
|
4
4
|
const DEFAULT_CONCURRENCY = 3;
|
|
5
5
|
const DEFAULT_RETRY_COUNT = 3;
|
|
6
6
|
const DEFAULT_RETRY_DELAY = 1000;
|
|
7
7
|
const MAX_BACKOFF_DELAY = 30000;
|
|
8
8
|
|
|
9
|
-
function httpGet(urlString, timeout = 15000) {
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
const url = new URL(urlString);
|
|
12
|
-
if (url.protocol !== 'https:') {
|
|
13
|
-
resolve({ ok: false, error: 'ProtocolError', message: 'Only HTTPS requests are supported' });
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
const req = https.get(urlString, { headers: { 'User-Agent': 'i18ntk/3.2.0' } }, (res) => {
|
|
17
|
-
let data = '';
|
|
18
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
19
|
-
res.on('end', () => {
|
|
20
|
-
try {
|
|
21
|
-
const parsed = JSON.parse(data);
|
|
22
|
-
resolve({ ok: true, data: parsed, status: res.statusCode });
|
|
23
|
-
} catch (e) {
|
|
24
|
-
resolve({ ok: false, error: 'ParseError', raw: data.substring(0, 500), status: res.statusCode });
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
req.setTimeout(timeout, () => {
|
|
29
|
-
req.destroy();
|
|
30
|
-
resolve({ ok: false, error: 'TimeoutError', message: 'Request timed out' });
|
|
31
|
-
});
|
|
32
|
-
req.on('error', (e) => {
|
|
33
|
-
resolve({ ok: false, error: e.code || 'NetworkError', message: e.message });
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
9
|
|
|
38
10
|
function extractTranslation(result) {
|
|
39
11
|
if (result && Array.isArray(result) && result[0]) {
|
|
@@ -46,6 +18,154 @@ function extractTranslation(result) {
|
|
|
46
18
|
return null;
|
|
47
19
|
}
|
|
48
20
|
|
|
21
|
+
function normalizeProvider(provider) {
|
|
22
|
+
const value = String(provider || process.env.I18NTK_TRANSLATE_PROVIDER || 'google').trim().toLowerCase();
|
|
23
|
+
if (value === 'deepl-free' || value === 'deepl-pro') return 'deepl';
|
|
24
|
+
if (value === 'libre' || value === 'libretranslate') return 'libretranslate';
|
|
25
|
+
if (value === 'google' || value === 'gtx') return 'google';
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeDeepLLanguage(code) {
|
|
30
|
+
return String(code || '').replace('-', '_').toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getDeepLApiUrl(options = {}) {
|
|
34
|
+
if (options.deeplApiUrl) return options.deeplApiUrl;
|
|
35
|
+
if (process.env.DEEPL_API_URL) return process.env.DEEPL_API_URL;
|
|
36
|
+
return 'https://api-free.deepl.com/v2/translate';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isEnabled(value) {
|
|
40
|
+
return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getDeepLAllowedHosts(url, options = {}) {
|
|
44
|
+
const hosts = ['api-free.deepl.com', 'api.deepl.com'];
|
|
45
|
+
if (options.allowCustomTranslateHosts || isEnabled(process.env.I18NTK_ALLOW_CUSTOM_TRANSLATE_HOSTS)) {
|
|
46
|
+
hosts.push(new URL(url).hostname);
|
|
47
|
+
}
|
|
48
|
+
return [...new Set(hosts)];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseProviderUrl(url, provider) {
|
|
52
|
+
try {
|
|
53
|
+
return { ok: true, parsed: new URL(url) };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: 'InvalidProviderUrl',
|
|
58
|
+
message: `${provider} provider URL is invalid: ${error.message}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractDeepLTranslation(data) {
|
|
64
|
+
const value = data && Array.isArray(data.translations) && data.translations[0] && data.translations[0].text;
|
|
65
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getLibreTranslateUrl(options = {}) {
|
|
69
|
+
if (options.libreTranslateUrl) return options.libreTranslateUrl;
|
|
70
|
+
if (process.env.LIBRETRANSLATE_URL) return process.env.LIBRETRANSLATE_URL;
|
|
71
|
+
return 'https://libretranslate.com/translate';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getLibreTranslateAllowedHosts(url) {
|
|
75
|
+
return [...new Set(['libretranslate.com', new URL(url).hostname])];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function extractLibreTranslateTranslation(data) {
|
|
79
|
+
const value = data && data.translatedText;
|
|
80
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildProviderRequest(text, targetLang, options = {}) {
|
|
84
|
+
const provider = normalizeProvider(options.provider);
|
|
85
|
+
const sourceLang = options.sourceLang || 'en';
|
|
86
|
+
|
|
87
|
+
if (provider === 'google') {
|
|
88
|
+
return {
|
|
89
|
+
provider,
|
|
90
|
+
method: 'GET',
|
|
91
|
+
url: buildGoogleTranslateUrl(text, sourceLang, targetLang),
|
|
92
|
+
extract: extractTranslation,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (provider === 'deepl') {
|
|
97
|
+
const apiKey = options.deeplApiKey || process.env.DEEPL_API_KEY;
|
|
98
|
+
if (!apiKey) {
|
|
99
|
+
return {
|
|
100
|
+
provider,
|
|
101
|
+
error: 'MissingApiKey',
|
|
102
|
+
message: 'DeepL provider requires DEEPL_API_KEY in the environment or deeplApiKey in options.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const url = getDeepLApiUrl(options);
|
|
107
|
+
const parsedUrl = parseProviderUrl(url, 'DeepL');
|
|
108
|
+
if (!parsedUrl.ok) return { provider, error: parsedUrl.error, message: parsedUrl.message };
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
provider,
|
|
112
|
+
method: 'POST',
|
|
113
|
+
url,
|
|
114
|
+
body: {
|
|
115
|
+
text: [text],
|
|
116
|
+
target_lang: normalizeDeepLLanguage(targetLang),
|
|
117
|
+
source_lang: normalizeDeepLLanguage(sourceLang),
|
|
118
|
+
},
|
|
119
|
+
requestOptions: {
|
|
120
|
+
provider,
|
|
121
|
+
allowedHosts: getDeepLAllowedHosts(url, options),
|
|
122
|
+
allowedPaths: ['/v2/translate'],
|
|
123
|
+
headers: {
|
|
124
|
+
Authorization: `DeepL-Auth-Key ${apiKey}`,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
extract: extractDeepLTranslation,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (provider === 'libretranslate') {
|
|
132
|
+
const apiKey = options.libreTranslateApiKey || process.env.LIBRETRANSLATE_API_KEY || '';
|
|
133
|
+
const url = getLibreTranslateUrl(options);
|
|
134
|
+
const parsedUrl = parseProviderUrl(url, 'LibreTranslate');
|
|
135
|
+
if (!parsedUrl.ok) return { provider, error: parsedUrl.error, message: parsedUrl.message };
|
|
136
|
+
|
|
137
|
+
const params = new URLSearchParams({
|
|
138
|
+
q: text,
|
|
139
|
+
source: sourceLang,
|
|
140
|
+
target: targetLang,
|
|
141
|
+
format: 'text',
|
|
142
|
+
});
|
|
143
|
+
if (apiKey) params.set('api_key', apiKey);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
provider,
|
|
147
|
+
method: 'POST',
|
|
148
|
+
url,
|
|
149
|
+
body: params.toString(),
|
|
150
|
+
requestOptions: {
|
|
151
|
+
provider,
|
|
152
|
+
allowedHosts: getLibreTranslateAllowedHosts(url),
|
|
153
|
+
allowedPaths: ['/translate'],
|
|
154
|
+
headers: {
|
|
155
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
extract: extractLibreTranslateTranslation,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
provider,
|
|
164
|
+
error: 'UnsupportedProvider',
|
|
165
|
+
message: `Unsupported translation provider "${provider}". Use google, deepl, or libretranslate.`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
49
169
|
function detectRateLimitError(result) {
|
|
50
170
|
if (!result.ok && result.status === 429) return true;
|
|
51
171
|
if (result.ok && result.status === 429) return true;
|
|
@@ -61,6 +181,8 @@ async function translateText(text, targetLang, options = {}) {
|
|
|
61
181
|
retryDelay = DEFAULT_RETRY_DELAY,
|
|
62
182
|
customFn,
|
|
63
183
|
timeout = 15000,
|
|
184
|
+
httpGet = safeHttpGet,
|
|
185
|
+
httpPost = safeHttpPost,
|
|
64
186
|
} = options;
|
|
65
187
|
|
|
66
188
|
if (!text || text.trim().length === 0) return { ok: true, translated: text };
|
|
@@ -74,14 +196,10 @@ async function translateText(text, targetLang, options = {}) {
|
|
|
74
196
|
}
|
|
75
197
|
}
|
|
76
198
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
dt: 't',
|
|
82
|
-
q: text,
|
|
83
|
-
});
|
|
84
|
-
const url = `https://translate.googleapis.com/translate_a/single?${params.toString()}`;
|
|
199
|
+
const request = buildProviderRequest(text, targetLang, { ...options, sourceLang });
|
|
200
|
+
if (request.error) {
|
|
201
|
+
return { ok: false, translated: null, error: request.error, message: request.message };
|
|
202
|
+
}
|
|
85
203
|
|
|
86
204
|
let lastError = null;
|
|
87
205
|
for (let attempt = 0; attempt < retryCount; attempt++) {
|
|
@@ -90,10 +208,12 @@ async function translateText(text, targetLang, options = {}) {
|
|
|
90
208
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
91
209
|
}
|
|
92
210
|
|
|
93
|
-
const result =
|
|
211
|
+
const result = request.method === 'POST'
|
|
212
|
+
? await httpPost(request.url, request.body, { ...(request.requestOptions || {}), timeout })
|
|
213
|
+
: await httpGet(request.url, timeout);
|
|
94
214
|
|
|
95
215
|
if (result.ok) {
|
|
96
|
-
const translated =
|
|
216
|
+
const translated = request.extract(result.data);
|
|
97
217
|
if (translated !== null && translated !== text) {
|
|
98
218
|
return { ok: true, translated };
|
|
99
219
|
}
|
|
@@ -101,7 +221,7 @@ async function translateText(text, targetLang, options = {}) {
|
|
|
101
221
|
return { ok: true, translated: text };
|
|
102
222
|
}
|
|
103
223
|
if (result.status === 429 || (translated === null && result.status >= 400)) {
|
|
104
|
-
lastError = { error: 'RateLimited', message:
|
|
224
|
+
lastError = { error: 'RateLimited', message: `${request.provider} rate limit hit` };
|
|
105
225
|
continue;
|
|
106
226
|
}
|
|
107
227
|
}
|
|
@@ -165,8 +285,11 @@ async function translateBatch(batch, targetLang, options = {}) {
|
|
|
165
285
|
}
|
|
166
286
|
|
|
167
287
|
module.exports = {
|
|
168
|
-
httpGet,
|
|
169
288
|
extractTranslation,
|
|
289
|
+
extractDeepLTranslation,
|
|
290
|
+
extractLibreTranslateTranslation,
|
|
291
|
+
buildProviderRequest,
|
|
292
|
+
normalizeProvider,
|
|
170
293
|
detectRateLimitError,
|
|
171
294
|
translateText,
|
|
172
295
|
translateBatch,
|
|
@@ -3,6 +3,8 @@ const SecurityUtils = require('../security');
|
|
|
3
3
|
|
|
4
4
|
const DEFAULT_PROTECTION_FILE = 'i18ntk-auto-translate.json';
|
|
5
5
|
const TOKEN_PREFIX = '__I18NTK_KEEP_';
|
|
6
|
+
const MAX_CONTEXT_RULES = 100;
|
|
7
|
+
const MAX_CONTEXT_INPUT_LENGTH = 200;
|
|
6
8
|
|
|
7
9
|
function defaultProtectionConfig() {
|
|
8
10
|
return {
|
|
@@ -49,12 +51,85 @@ function compilePatterns(patterns) {
|
|
|
49
51
|
return compiled;
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function parseContextString(context) {
|
|
55
|
+
if (typeof context !== 'string') return null;
|
|
56
|
+
if (context.length > MAX_CONTEXT_INPUT_LENGTH) return null;
|
|
57
|
+
|
|
58
|
+
const trimmed = context.trim();
|
|
59
|
+
|
|
60
|
+
if (trimmed === 'standalone') {
|
|
61
|
+
return { mode: 'standalone' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const afterMatch = trimmed.match(/^after:(.+)$/i);
|
|
65
|
+
if (afterMatch) {
|
|
66
|
+
const words = afterMatch[1].split('|').map(w => w.trim()).filter(Boolean);
|
|
67
|
+
if (words.length === 0) return null;
|
|
68
|
+
return { mode: 'after', words };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const beforeMatch = trimmed.match(/^before:(.+)$/i);
|
|
72
|
+
if (beforeMatch) {
|
|
73
|
+
const words = beforeMatch[1].split('|').map(w => w.trim()).filter(Boolean);
|
|
74
|
+
if (words.length === 0) return null;
|
|
75
|
+
return { mode: 'before', words };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const surroundedMatch = trimmed.match(/^surrounded:(.+),(.+)$/i);
|
|
79
|
+
if (surroundedMatch) {
|
|
80
|
+
const leftWords = surroundedMatch[1].split('|').map(w => w.trim()).filter(Boolean);
|
|
81
|
+
const rightWords = surroundedMatch[2].split('|').map(w => w.trim()).filter(Boolean);
|
|
82
|
+
if (leftWords.length === 0 || rightWords.length === 0) return null;
|
|
83
|
+
return { mode: 'surrounded', left: leftWords, right: rightWords };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseContextRule(rule) {
|
|
90
|
+
if (typeof rule === 'string') {
|
|
91
|
+
const trimmed = rule.trim();
|
|
92
|
+
if (!trimmed || trimmed.length > MAX_CONTEXT_INPUT_LENGTH) return null;
|
|
93
|
+
return { value: trimmed, type: 'global' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
typeof rule === 'object' &&
|
|
98
|
+
rule !== null &&
|
|
99
|
+
typeof rule.value === 'string' &&
|
|
100
|
+
typeof rule.context === 'string'
|
|
101
|
+
) {
|
|
102
|
+
const value = rule.value.trim();
|
|
103
|
+
const rawContext = rule.context;
|
|
104
|
+
|
|
105
|
+
if (!value || value.length > MAX_CONTEXT_INPUT_LENGTH) return null;
|
|
106
|
+
if (!rawContext || rawContext.length > MAX_CONTEXT_INPUT_LENGTH) return null;
|
|
107
|
+
|
|
108
|
+
const parsed = parseContextString(rawContext);
|
|
109
|
+
if (!parsed) return null;
|
|
110
|
+
|
|
111
|
+
return { value, type: 'context', context: parsed };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
52
117
|
function normalizeProtectionConfig(config = {}, filePath = null) {
|
|
53
118
|
const terms = normalizeList(config.terms);
|
|
54
119
|
const keys = normalizeList(config.keys);
|
|
55
120
|
const values = normalizeList(config.values);
|
|
56
121
|
const patterns = normalizeList(config.patterns);
|
|
57
122
|
|
|
123
|
+
const rawTerms = Array.isArray(config.terms) ? config.terms : [];
|
|
124
|
+
const normalizedTerms = [];
|
|
125
|
+
for (const term of rawTerms) {
|
|
126
|
+
const normalized = parseContextRule(term);
|
|
127
|
+
if (normalized) {
|
|
128
|
+
normalizedTerms.push(normalized);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const cappedTerms = normalizedTerms.slice(0, MAX_CONTEXT_RULES);
|
|
132
|
+
|
|
58
133
|
return {
|
|
59
134
|
enabled: true,
|
|
60
135
|
filePath,
|
|
@@ -62,7 +137,8 @@ function normalizeProtectionConfig(config = {}, filePath = null) {
|
|
|
62
137
|
keys,
|
|
63
138
|
values,
|
|
64
139
|
patterns,
|
|
65
|
-
compiledPatterns: compilePatterns(patterns)
|
|
140
|
+
compiledPatterns: compilePatterns(patterns),
|
|
141
|
+
normalizedTerms: cappedTerms
|
|
66
142
|
};
|
|
67
143
|
}
|
|
68
144
|
|
|
@@ -98,10 +174,20 @@ function saveProtectionFile(filePath, config, options = {}) {
|
|
|
98
174
|
if (!SecurityUtils.safeExistsSync(dir, path.dirname(dir))) {
|
|
99
175
|
SecurityUtils.safeMkdirSync(dir, path.dirname(dir), { recursive: true });
|
|
100
176
|
}
|
|
177
|
+
|
|
178
|
+
const rawTerms = Array.isArray(config?.terms) ? config.terms : [];
|
|
179
|
+
const sanitizedTerms = [];
|
|
180
|
+
for (const term of rawTerms) {
|
|
181
|
+
const normalized = parseContextRule(term);
|
|
182
|
+
if (normalized) {
|
|
183
|
+
sanitizedTerms.push(term);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
101
187
|
const nextConfig = {
|
|
102
188
|
...defaultProtectionConfig(),
|
|
103
189
|
...(config || {}),
|
|
104
|
-
terms:
|
|
190
|
+
terms: sanitizedTerms.slice(0, MAX_CONTEXT_RULES),
|
|
105
191
|
keys: normalizeList(config?.keys),
|
|
106
192
|
values: normalizeList(config?.values),
|
|
107
193
|
patterns: normalizeList(config?.patterns)
|
|
@@ -156,7 +242,8 @@ function shouldPreserveWholeValue(keyPath, value, protection) {
|
|
|
156
242
|
if (!protection || protection.enabled === false) return false;
|
|
157
243
|
if (protection.keys.some(rule => keyMatchesRule(keyPath, rule))) return true;
|
|
158
244
|
const valueText = String(value);
|
|
159
|
-
return protection.values.includes(valueText) ||
|
|
245
|
+
return protection.values.includes(valueText) ||
|
|
246
|
+
(protection.normalizedTerms || []).some(rule => rule.value === valueText);
|
|
160
247
|
}
|
|
161
248
|
|
|
162
249
|
function addReplacement(replacements, original) {
|
|
@@ -165,13 +252,65 @@ function addReplacement(replacements, original) {
|
|
|
165
252
|
replacements.push({ original });
|
|
166
253
|
}
|
|
167
254
|
|
|
255
|
+
function shouldProtectInContext(value, rule, index, fullText) {
|
|
256
|
+
if (rule.type === 'global') return true;
|
|
257
|
+
|
|
258
|
+
const { context } = rule;
|
|
259
|
+
const before = fullText.substring(0, index);
|
|
260
|
+
const after = fullText.substring(index + value.length);
|
|
261
|
+
|
|
262
|
+
if (context.mode === 'after') {
|
|
263
|
+
const alternation = context.words.map(escapeRegExp).join('|');
|
|
264
|
+
const regex = new RegExp(`\\b(${alternation})\\s+$`, 'i');
|
|
265
|
+
return regex.test(before);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (context.mode === 'before') {
|
|
269
|
+
const alternation = context.words.map(escapeRegExp).join('|');
|
|
270
|
+
const regex = new RegExp(`^\\s+(${alternation})\\b`, 'i');
|
|
271
|
+
return regex.test(after);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (context.mode === 'standalone') {
|
|
275
|
+
const isBoundaryBefore = before === '' || /\s$/.test(before);
|
|
276
|
+
const isBoundaryAfter = after === '' || /^[\s.,!?;:]/.test(after);
|
|
277
|
+
return isBoundaryBefore && isBoundaryAfter;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (context.mode === 'surrounded') {
|
|
281
|
+
const leftAlternation = context.left.map(escapeRegExp).join('|');
|
|
282
|
+
const rightAlternation = context.right.map(escapeRegExp).join('|');
|
|
283
|
+
const leftRegex = new RegExp(`\\b(${leftAlternation})\\s+$`, 'i');
|
|
284
|
+
const rightRegex = new RegExp(`^\\s+(${rightAlternation})\\b`, 'i');
|
|
285
|
+
return leftRegex.test(before) && rightRegex.test(after);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
168
291
|
function collectReplacements(value, protection) {
|
|
169
292
|
const text = String(value);
|
|
170
293
|
const replacements = [];
|
|
171
294
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
295
|
+
const rules = protection.normalizedTerms || (protection.terms || [])
|
|
296
|
+
.map(parseContextRule)
|
|
297
|
+
.filter(Boolean);
|
|
298
|
+
for (const rule of rules) {
|
|
299
|
+
if (rule.type === 'global') {
|
|
300
|
+
if (text.includes(rule.value)) {
|
|
301
|
+
addReplacement(replacements, rule.value);
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
if (!rule.value) continue;
|
|
305
|
+
const escaped = escapeRegExp(rule.value);
|
|
306
|
+
const regex = new RegExp(escaped, 'gi');
|
|
307
|
+
let match;
|
|
308
|
+
while ((match = regex.exec(text)) !== null) {
|
|
309
|
+
if (shouldProtectInContext(rule.value, rule, match.index, text)) {
|
|
310
|
+
addReplacement(replacements, match[0]);
|
|
311
|
+
}
|
|
312
|
+
if (match[0] === '') regex.lastIndex++;
|
|
313
|
+
}
|
|
175
314
|
}
|
|
176
315
|
}
|
|
177
316
|
|
|
@@ -221,6 +360,7 @@ function hasProtectionRules(protection) {
|
|
|
221
360
|
return Boolean(
|
|
222
361
|
protection &&
|
|
223
362
|
(
|
|
363
|
+
(protection.normalizedTerms && protection.normalizedTerms.length) ||
|
|
224
364
|
protection.terms.length ||
|
|
225
365
|
protection.keys.length ||
|
|
226
366
|
protection.values.length ||
|
|
@@ -235,6 +375,7 @@ module.exports = {
|
|
|
235
375
|
defaultProtectionConfig,
|
|
236
376
|
hasProtectionRules,
|
|
237
377
|
loadProtectionConfig,
|
|
378
|
+
parseContextRule,
|
|
238
379
|
protectText,
|
|
239
380
|
readProtectionFile,
|
|
240
381
|
restoreText,
|