@babelize/engine 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/dist/BabelizeEngine.d.ts +12 -0
- package/dist/BabelizeEngine.js +47 -0
- package/dist/analysis/analyze.d.ts +106 -0
- package/dist/analysis/analyze.js +323 -0
- package/dist/config/env.d.ts +9 -0
- package/dist/config/env.js +16 -0
- package/dist/config.d.ts +74 -0
- package/dist/config.js +47 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +126 -0
- package/dist/injection/InjectionStrategy.d.ts +62 -0
- package/dist/injection/InjectionStrategy.js +2 -0
- package/dist/injection/StrategyFactory.d.ts +4 -0
- package/dist/injection/StrategyFactory.js +29 -0
- package/dist/injection/django/django-template-injector.d.ts +29 -0
- package/dist/injection/django/django-template-injector.js +216 -0
- package/dist/injection/html/html-injector.d.ts +11 -0
- package/dist/injection/html/html-injector.js +173 -0
- package/dist/injection/strategies/NextIntlStrategy.d.ts +25 -0
- package/dist/injection/strategies/NextIntlStrategy.js +141 -0
- package/dist/injection/strategies/ReactI18nextStrategy.d.ts +19 -0
- package/dist/injection/strategies/ReactI18nextStrategy.js +116 -0
- package/dist/injection/strategies/VanillaStrategy.d.ts +11 -0
- package/dist/injection/strategies/VanillaStrategy.js +73 -0
- package/dist/injection/strategies/VueI18nStrategy.d.ts +11 -0
- package/dist/injection/strategies/VueI18nStrategy.js +84 -0
- package/dist/local/formatter.d.ts +26 -0
- package/dist/local/formatter.js +105 -0
- package/dist/local/git.d.ts +23 -0
- package/dist/local/git.js +136 -0
- package/dist/local/hosted-provider.d.ts +68 -0
- package/dist/local/hosted-provider.js +215 -0
- package/dist/local/index.d.ts +23 -0
- package/dist/local/index.js +75 -0
- package/dist/local/provider.d.ts +56 -0
- package/dist/local/provider.js +119 -0
- package/dist/local/pseudo-provider.d.ts +18 -0
- package/dist/local/pseudo-provider.js +207 -0
- package/dist/local/report.d.ts +177 -0
- package/dist/local/report.js +186 -0
- package/dist/local/translate-core.d.ts +215 -0
- package/dist/local/translate-core.js +461 -0
- package/dist/local/translate-messages.d.ts +49 -0
- package/dist/local/translate-messages.js +91 -0
- package/dist/local/workspace-copy.d.ts +91 -0
- package/dist/local/workspace-copy.js +355 -0
- package/dist/modules/extractor/extract.d.ts +20 -0
- package/dist/modules/extractor/extract.js +701 -0
- package/dist/modules/extractor/index.d.ts +2 -0
- package/dist/modules/extractor/index.js +63 -0
- package/dist/modules/finalization/django-finalize.d.ts +13 -0
- package/dist/modules/finalization/django-finalize.js +275 -0
- package/dist/modules/finalization/finalize.d.ts +36 -0
- package/dist/modules/finalization/finalize.js +717 -0
- package/dist/modules/finalization/index.d.ts +1 -0
- package/dist/modules/finalization/index.js +5 -0
- package/dist/modules/finalization/nextjs-finalize.d.ts +37 -0
- package/dist/modules/finalization/nextjs-finalize.js +329 -0
- package/dist/modules/finalization/static-runtime.d.ts +9 -0
- package/dist/modules/finalization/static-runtime.js +261 -0
- package/dist/modules/format-injection/index.d.ts +19 -0
- package/dist/modules/format-injection/index.js +272 -0
- package/dist/modules/format-injection/inject-formats.d.ts +31 -0
- package/dist/modules/format-injection/inject-formats.js +208 -0
- package/dist/modules/glossary/glossary.d.ts +9 -0
- package/dist/modules/glossary/glossary.js +27 -0
- package/dist/modules/glossary/index.d.ts +3 -0
- package/dist/modules/glossary/index.js +8 -0
- package/dist/modules/glossary/masker.d.ts +23 -0
- package/dist/modules/glossary/masker.js +56 -0
- package/dist/modules/glossary/types.d.ts +22 -0
- package/dist/modules/glossary/types.js +2 -0
- package/dist/modules/injection/index.d.ts +1 -0
- package/dist/modules/injection/index.js +6 -0
- package/dist/modules/injection/inject.d.ts +29 -0
- package/dist/modules/injection/inject.js +1066 -0
- package/dist/modules/keygen/index.d.ts +1 -0
- package/dist/modules/keygen/index.js +11 -0
- package/dist/modules/keygen/keygen.d.ts +4 -0
- package/dist/modules/keygen/keygen.js +116 -0
- package/dist/modules/keygen/locale-data.d.ts +7 -0
- package/dist/modules/keygen/locale-data.js +73 -0
- package/dist/modules/keygen/public.d.ts +2 -0
- package/dist/modules/keygen/public.js +12 -0
- package/dist/modules/locale-builder/index.d.ts +1 -0
- package/dist/modules/locale-builder/index.js +7 -0
- package/dist/modules/locale-builder/locales.d.ts +13 -0
- package/dist/modules/locale-builder/locales.js +107 -0
- package/dist/modules/locale-builder/po.d.ts +33 -0
- package/dist/modules/locale-builder/po.js +131 -0
- package/dist/modules/lockfile/hasher.d.ts +7 -0
- package/dist/modules/lockfile/hasher.js +13 -0
- package/dist/modules/lockfile/index.d.ts +3 -0
- package/dist/modules/lockfile/index.js +21 -0
- package/dist/modules/lockfile/lockfile-manager.d.ts +90 -0
- package/dist/modules/lockfile/lockfile-manager.js +482 -0
- package/dist/modules/lockfile/schema.d.ts +154 -0
- package/dist/modules/lockfile/schema.js +45 -0
- package/dist/modules/parser/index.d.ts +2 -0
- package/dist/modules/parser/index.js +9 -0
- package/dist/modules/summary/index.d.ts +1 -0
- package/dist/modules/summary/index.js +7 -0
- package/dist/modules/summary/summary.d.ts +9 -0
- package/dist/modules/summary/summary.js +195 -0
- package/dist/modules/sync-detector/index.d.ts +2 -0
- package/dist/modules/sync-detector/index.js +7 -0
- package/dist/modules/sync-detector/reporter.d.ts +3 -0
- package/dist/modules/sync-detector/reporter.js +16 -0
- package/dist/modules/sync-detector/sync-detector.d.ts +39 -0
- package/dist/modules/sync-detector/sync-detector.js +374 -0
- package/dist/modules/translator/batch-optimizer.d.ts +12 -0
- package/dist/modules/translator/batch-optimizer.js +30 -0
- package/dist/modules/translator/client/byok.provider.d.ts +64 -0
- package/dist/modules/translator/client/byok.provider.js +319 -0
- package/dist/modules/translator/client/direct-translator.client.d.ts +12 -0
- package/dist/modules/translator/client/direct-translator.client.js +71 -0
- package/dist/modules/translator/client/gemini.provider.d.ts +1 -0
- package/dist/modules/translator/client/gemini.provider.js +110 -0
- package/dist/modules/translator/client/groq.provider.d.ts +1 -0
- package/dist/modules/translator/client/groq.provider.js +111 -0
- package/dist/modules/translator/client/request.types.d.ts +1 -0
- package/dist/modules/translator/client/request.types.js +2 -0
- package/dist/modules/translator/client/retry.d.ts +29 -0
- package/dist/modules/translator/client/retry.js +94 -0
- package/dist/modules/translator/client/test-utils/mock.client.d.ts +31 -0
- package/dist/modules/translator/client/test-utils/mock.client.js +59 -0
- package/dist/modules/translator/client/translator.interface.d.ts +1 -0
- package/dist/modules/translator/client/translator.interface.js +2 -0
- package/dist/modules/translator/language-codes/errors.d.ts +10 -0
- package/dist/modules/translator/language-codes/errors.js +18 -0
- package/dist/modules/translator/language-codes/flores-map.d.ts +1 -0
- package/dist/modules/translator/language-codes/flores-map.js +107 -0
- package/dist/modules/translator/language-codes/index.d.ts +11 -0
- package/dist/modules/translator/language-codes/index.js +24 -0
- package/dist/modules/translator/language-codes/language-mapper.d.ts +25 -0
- package/dist/modules/translator/language-codes/language-mapper.js +132 -0
- package/dist/modules/translator/language-codes/rtl-languages.d.ts +2 -0
- package/dist/modules/translator/language-codes/rtl-languages.js +9 -0
- package/dist/modules/translator/memory/memory-matcher.d.ts +8 -0
- package/dist/modules/translator/memory/memory-matcher.js +218 -0
- package/dist/modules/translator/memory/types.d.ts +22 -0
- package/dist/modules/translator/memory/types.js +2 -0
- package/dist/modules/translator/translate.d.ts +38 -0
- package/dist/modules/translator/translate.js +932 -0
- package/dist/modules/translator/translator.d.ts +5 -0
- package/dist/modules/translator/translator.js +46 -0
- package/dist/modules/vue/migrate.d.ts +13 -0
- package/dist/modules/vue/migrate.js +40 -0
- package/dist/modules/vue/script-inject.d.ts +16 -0
- package/dist/modules/vue/script-inject.js +115 -0
- package/dist/modules/vue/sfc.d.ts +11 -0
- package/dist/modules/vue/sfc.js +51 -0
- package/dist/modules/vue/template-rewrite.d.ts +25 -0
- package/dist/modules/vue/template-rewrite.js +76 -0
- package/dist/modules/vue/template-scan.d.ts +29 -0
- package/dist/modules/vue/template-scan.js +264 -0
- package/dist/modules/workspace/index.d.ts +1 -0
- package/dist/modules/workspace/index.js +5 -0
- package/dist/modules/workspace/validate.d.ts +13 -0
- package/dist/modules/workspace/validate.js +298 -0
- package/dist/orchestrator.d.ts +8 -0
- package/dist/orchestrator.js +616 -0
- package/dist/providers/default-translation-provider.d.ts +20 -0
- package/dist/providers/default-translation-provider.js +30 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +21 -0
- package/dist/providers/types.d.ts +145 -0
- package/dist/providers/types.js +2 -0
- package/dist/qa/check-project.d.ts +49 -0
- package/dist/qa/check-project.js +150 -0
- package/dist/qa/locale-files.d.ts +44 -0
- package/dist/qa/locale-files.js +159 -0
- package/dist/qa/locale-qa.d.ts +61 -0
- package/dist/qa/locale-qa.js +325 -0
- package/dist/qa/review.d.ts +43 -0
- package/dist/qa/review.js +175 -0
- package/dist/runtime/config.d.ts +8 -0
- package/dist/runtime/config.js +36 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/types.d.ts +363 -0
- package/dist/runtime/types.js +38 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.js +12 -0
- package/dist/utils/ast-helpers.d.ts +57 -0
- package/dist/utils/ast-helpers.js +608 -0
- package/dist/utils/cache-keys.d.ts +8 -0
- package/dist/utils/cache-keys.js +18 -0
- package/dist/utils/circuit-breaker.d.ts +43 -0
- package/dist/utils/circuit-breaker.js +119 -0
- package/dist/utils/concurrent-map.d.ts +4 -0
- package/dist/utils/concurrent-map.js +21 -0
- package/dist/utils/detect-format.d.ts +5 -0
- package/dist/utils/detect-format.js +125 -0
- package/dist/utils/dtl-mask.d.ts +25 -0
- package/dist/utils/dtl-mask.js +58 -0
- package/dist/utils/extraction-options.d.ts +23 -0
- package/dist/utils/extraction-options.js +23 -0
- package/dist/utils/file-utils.d.ts +33 -0
- package/dist/utils/file-utils.js +262 -0
- package/dist/utils/glossary-match.d.ts +9 -0
- package/dist/utils/glossary-match.js +25 -0
- package/dist/utils/html-extractor.d.ts +8 -0
- package/dist/utils/html-extractor.js +317 -0
- package/dist/utils/i18next-icu.d.ts +38 -0
- package/dist/utils/i18next-icu.js +159 -0
- package/dist/utils/icu-message.d.ts +65 -0
- package/dist/utils/icu-message.js +346 -0
- package/dist/utils/icu-spans.d.ts +10 -0
- package/dist/utils/icu-spans.js +126 -0
- package/dist/utils/jsx-message.d.ts +93 -0
- package/dist/utils/jsx-message.js +621 -0
- package/dist/utils/key-usage.d.ts +35 -0
- package/dist/utils/key-usage.js +247 -0
- package/dist/utils/logger.d.ts +24 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/metadata.d.ts +80 -0
- package/dist/utils/metadata.js +255 -0
- package/dist/utils/module-constants.d.ts +30 -0
- package/dist/utils/module-constants.js +265 -0
- package/dist/utils/not-injected.d.ts +6 -0
- package/dist/utils/not-injected.js +36 -0
- package/dist/utils/placeholder-protection.d.ts +57 -0
- package/dist/utils/placeholder-protection.js +136 -0
- package/dist/utils/pseudo-markers.d.ts +9 -0
- package/dist/utils/pseudo-markers.js +16 -0
- package/dist/utils/py-extractor.d.ts +14 -0
- package/dist/utils/py-extractor.js +285 -0
- package/dist/utils/string-utils.d.ts +52 -0
- package/dist/utils/string-utils.js +270 -0
- package/dist/utils/translation-quality.d.ts +34 -0
- package/dist/utils/translation-quality.js +156 -0
- package/dist/utils/translation-sanitizer.d.ts +10 -0
- package/dist/utils/translation-sanitizer.js +57 -0
- package/dist/utils/translator-caller.d.ts +32 -0
- package/dist/utils/translator-caller.js +217 -0
- package/dist/utils/translator-load-balancer.d.ts +73 -0
- package/dist/utils/translator-load-balancer.js +293 -0
- package/dist/utils/ui-copy.d.ts +32 -0
- package/dist/utils/ui-copy.js +126 -0
- package/dist/utils/workspace.d.ts +14 -0
- package/dist/utils/workspace.js +89 -0
- package/package.json +64 -4
- package/schemas/translate-report.schema.json +297 -0
- package/README.md +0 -3
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractStringsFromPython = extractStringsFromPython;
|
|
4
|
+
const string_utils_1 = require("./string-utils");
|
|
5
|
+
// Function calls that indicate translatable strings
|
|
6
|
+
const GETTEXT_CALLS = [
|
|
7
|
+
'_',
|
|
8
|
+
'gettext',
|
|
9
|
+
'gettext_lazy',
|
|
10
|
+
'ngettext',
|
|
11
|
+
'ngettext_lazy',
|
|
12
|
+
'ugettext',
|
|
13
|
+
'ugettext_lazy',
|
|
14
|
+
'pgettext',
|
|
15
|
+
'pgettext_lazy',
|
|
16
|
+
'npgettext',
|
|
17
|
+
'npgettext_lazy',
|
|
18
|
+
];
|
|
19
|
+
// Keyword argument names that Django/Python treat as translatable
|
|
20
|
+
const TRANSlatable_KWARGS = [
|
|
21
|
+
'verbose_name',
|
|
22
|
+
'verbose_name_plural',
|
|
23
|
+
'help_text',
|
|
24
|
+
'label',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Extract translatable strings from Python source code.
|
|
28
|
+
*
|
|
29
|
+
* Matches: _(), gettext(), gettext_lazy(), ugettext*(), ngettext()
|
|
30
|
+
* Also opt-in: verbose_name=, help_text=, label= kwargs
|
|
31
|
+
*
|
|
32
|
+
* Uses a character-position lexer approach (not a full AST parser)
|
|
33
|
+
* to handle single/double/triple quotes, raw strings, f-strings, and escape sequences.
|
|
34
|
+
*/
|
|
35
|
+
function extractStringsFromPython(content, filePath) {
|
|
36
|
+
const ctx = {
|
|
37
|
+
filePath,
|
|
38
|
+
content,
|
|
39
|
+
strings: [],
|
|
40
|
+
seenTexts: new Set(),
|
|
41
|
+
warnings: [],
|
|
42
|
+
};
|
|
43
|
+
let i = 0;
|
|
44
|
+
const len = content.length;
|
|
45
|
+
while (i < len) {
|
|
46
|
+
// Skip lines that are comments
|
|
47
|
+
if (content[i] === '#') {
|
|
48
|
+
i = skipToNextLine(content, i);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Skip string literals on their own (not preceded by a function call or kwarg)
|
|
52
|
+
// We only extract strings that are arguments to gettext functions or Django kwargs
|
|
53
|
+
// Check for gettext function calls: _( "..." ), gettext( "..." ), etc.
|
|
54
|
+
for (const callName of GETTEXT_CALLS) {
|
|
55
|
+
const callPattern = new RegExp(`\\b${callName.replace(/_/g, '\\_')}\\s*\\(`);
|
|
56
|
+
const match = callPattern.exec(content.slice(i));
|
|
57
|
+
if (match && match.index === 0) {
|
|
58
|
+
const parenStart = i + match[0].length;
|
|
59
|
+
const args = collectCallArgs(content, parenStart);
|
|
60
|
+
if (args) {
|
|
61
|
+
for (const arg of args) {
|
|
62
|
+
const str = extractStringFromArg(content, arg);
|
|
63
|
+
if (str !== null) {
|
|
64
|
+
addString(ctx, str.text, arg.startLine, 0, str.context);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
i = args ? (args[args.length - 1]?.end || parenStart) : parenStart;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Check for Django keyword arguments: verbose_name="...", help_text="..."
|
|
73
|
+
for (const kwarg of TRANSlatable_KWARGS) {
|
|
74
|
+
const kwargPattern = new RegExp(`\\b${kwarg}\\s*=\\s*`);
|
|
75
|
+
const match = kwargPattern.exec(content.slice(i));
|
|
76
|
+
if (match && match.index === 0) {
|
|
77
|
+
const valueStart = i + match[0].length;
|
|
78
|
+
const str = extractStringLiteral(content, valueStart);
|
|
79
|
+
if (str !== null) {
|
|
80
|
+
addString(ctx, str.text, findLineNumber(content, valueStart), 0, str.context);
|
|
81
|
+
}
|
|
82
|
+
i = valueStart + (str ? str.rawLength : 0);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
return { strings: ctx.strings, warnings: ctx.warnings };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Collect argument ranges for a function call starting after the opening paren.
|
|
92
|
+
* Returns an array of {start, end, startLine} for each argument position.
|
|
93
|
+
*/
|
|
94
|
+
function collectCallArgs(content, start) {
|
|
95
|
+
let depth = 1;
|
|
96
|
+
let i = start;
|
|
97
|
+
const args = [];
|
|
98
|
+
let argStart = -1;
|
|
99
|
+
let parenDepth = 0;
|
|
100
|
+
while (i < content.length && depth > 0) {
|
|
101
|
+
const ch = content[i];
|
|
102
|
+
if (ch === '(') {
|
|
103
|
+
parenDepth++;
|
|
104
|
+
}
|
|
105
|
+
else if (ch === ')') {
|
|
106
|
+
parenDepth--;
|
|
107
|
+
if (parenDepth === 0 && depth === 1) {
|
|
108
|
+
if (argStart !== -1) {
|
|
109
|
+
args.push({
|
|
110
|
+
start: argStart,
|
|
111
|
+
end: i,
|
|
112
|
+
startLine: findLineNumber(content, argStart),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
depth--;
|
|
116
|
+
i++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (ch === ',' && parenDepth === 1) {
|
|
121
|
+
if (argStart !== -1) {
|
|
122
|
+
args.push({
|
|
123
|
+
start: argStart,
|
|
124
|
+
end: i,
|
|
125
|
+
startLine: findLineNumber(content, argStart),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
argStart = -1;
|
|
129
|
+
}
|
|
130
|
+
else if (ch === '"' || ch === "'" || ch === 'f') {
|
|
131
|
+
// String literal — skip over it
|
|
132
|
+
const strEnd = skipString(content, i);
|
|
133
|
+
if (strEnd !== i) {
|
|
134
|
+
if (argStart === -1)
|
|
135
|
+
argStart = i;
|
|
136
|
+
i = strEnd;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Track first non-whitespace as arg start
|
|
141
|
+
if (argStart === -1 && !/\s/.test(ch) && ch !== ',') {
|
|
142
|
+
// Check if this is a keyword arg or string
|
|
143
|
+
if (ch !== ')') {
|
|
144
|
+
argStart = i;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
i++;
|
|
148
|
+
}
|
|
149
|
+
return args.length > 0 ? args : null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Extract the string value from an argument position.
|
|
153
|
+
* Handles named arguments (_("text"), _("text", count)).
|
|
154
|
+
* Only extracts the first string argument (singular form).
|
|
155
|
+
*/
|
|
156
|
+
function extractStringFromArg(content, arg) {
|
|
157
|
+
// Skip keyword name if present (e.g., msgid="text")
|
|
158
|
+
let i = arg.start;
|
|
159
|
+
const end = arg.end;
|
|
160
|
+
while (i < end && content[i] !== '"' && content[i] !== "'" && content[i] !== 'f') {
|
|
161
|
+
i++;
|
|
162
|
+
}
|
|
163
|
+
if (i >= end)
|
|
164
|
+
return null;
|
|
165
|
+
const str = extractStringLiteral(content, i);
|
|
166
|
+
if (!str)
|
|
167
|
+
return null;
|
|
168
|
+
return {
|
|
169
|
+
text: str.text,
|
|
170
|
+
rawLength: str.rawLength,
|
|
171
|
+
context: str.context,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Extract a string literal from content starting at the given position.
|
|
176
|
+
* Handles: "...", '...', """...""", '''...''', f"...", r"...", b"..."
|
|
177
|
+
*/
|
|
178
|
+
function extractStringLiteral(content, start) {
|
|
179
|
+
let i = start;
|
|
180
|
+
const len = content.length;
|
|
181
|
+
// Skip prefix characters: r, f, b, rb, fr, etc.
|
|
182
|
+
while (i < len && /[a-zA-Z]/.test(content[i]) && content[i] !== '"' && content[i] !== "'") {
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
if (i >= len)
|
|
186
|
+
return null;
|
|
187
|
+
const quote = content[i];
|
|
188
|
+
if (quote !== '"' && quote !== "'")
|
|
189
|
+
return null;
|
|
190
|
+
// Check for triple-quote
|
|
191
|
+
const isTriple = content.slice(i, i + 3) === quote.repeat(3);
|
|
192
|
+
const openQuote = isTriple ? quote.repeat(3) : quote;
|
|
193
|
+
const closeQuote = openQuote;
|
|
194
|
+
const valueStart = i + openQuote.length;
|
|
195
|
+
let j = valueStart;
|
|
196
|
+
let text = '';
|
|
197
|
+
let context = 'string-literal';
|
|
198
|
+
// Detect f-string
|
|
199
|
+
const prefixStart = start;
|
|
200
|
+
const prefix = content.slice(prefixStart, i).toLowerCase();
|
|
201
|
+
if (prefix.includes('f')) {
|
|
202
|
+
context = 'template-literal';
|
|
203
|
+
}
|
|
204
|
+
while (j < len) {
|
|
205
|
+
if (content[j] === '\\') {
|
|
206
|
+
// Escape sequence — skip next char
|
|
207
|
+
j += 2;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (content.slice(j, j + closeQuote.length) === closeQuote) {
|
|
211
|
+
// Found closing quote
|
|
212
|
+
const rawLength = j + closeQuote.length - start;
|
|
213
|
+
return { text, rawLength, context };
|
|
214
|
+
}
|
|
215
|
+
text += content[j];
|
|
216
|
+
j++;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Skip over a string literal starting at the given position.
|
|
222
|
+
* Returns the index after the closing quote.
|
|
223
|
+
*/
|
|
224
|
+
function skipString(content, start) {
|
|
225
|
+
let i = start;
|
|
226
|
+
const len = content.length;
|
|
227
|
+
// Skip prefix characters
|
|
228
|
+
while (i < len && /[a-zA-Z]/.test(content[i]) && content[i] !== '"' && content[i] !== "'") {
|
|
229
|
+
i++;
|
|
230
|
+
}
|
|
231
|
+
if (i >= len)
|
|
232
|
+
return start;
|
|
233
|
+
const quote = content[i];
|
|
234
|
+
if (quote !== '"' && quote !== "'")
|
|
235
|
+
return start;
|
|
236
|
+
const isTriple = content.slice(i, i + 3) === quote.repeat(3);
|
|
237
|
+
const closeQuote = isTriple ? quote.repeat(3) : quote;
|
|
238
|
+
i += closeQuote.length;
|
|
239
|
+
while (i < len) {
|
|
240
|
+
if (content[i] === '\\') {
|
|
241
|
+
i += 2;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (content.slice(i, i + closeQuote.length) === closeQuote) {
|
|
245
|
+
return i + closeQuote.length;
|
|
246
|
+
}
|
|
247
|
+
i++;
|
|
248
|
+
}
|
|
249
|
+
return start; // Unterminated string
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Add a string to the extraction context, deduplicating by content hash.
|
|
253
|
+
*/
|
|
254
|
+
function addString(ctx, text, line, column, context) {
|
|
255
|
+
const trimmed = text.trim();
|
|
256
|
+
if (!trimmed || trimmed.length < 2)
|
|
257
|
+
return;
|
|
258
|
+
if (!(0, string_utils_1.isTranslatableText)(trimmed))
|
|
259
|
+
return;
|
|
260
|
+
const contentKey = `${ctx.filePath}:${(0, string_utils_1.hashString)(trimmed)}`;
|
|
261
|
+
if (ctx.seenTexts.has(contentKey))
|
|
262
|
+
return;
|
|
263
|
+
const id = `${ctx.filePath}:${line}:${(0, string_utils_1.hashString)(trimmed)}`;
|
|
264
|
+
ctx.seenTexts.add(contentKey);
|
|
265
|
+
ctx.strings.push({
|
|
266
|
+
text: trimmed,
|
|
267
|
+
file: ctx.filePath,
|
|
268
|
+
line,
|
|
269
|
+
column,
|
|
270
|
+
context,
|
|
271
|
+
id,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function skipToNextLine(content, pos) {
|
|
275
|
+
const newline = content.indexOf('\n', pos);
|
|
276
|
+
return newline === -1 ? content.length : newline + 1;
|
|
277
|
+
}
|
|
278
|
+
function findLineNumber(content, pos) {
|
|
279
|
+
let line = 1;
|
|
280
|
+
for (let i = 0; i < pos && i < content.length; i++) {
|
|
281
|
+
if (content[i] === '\n')
|
|
282
|
+
line++;
|
|
283
|
+
}
|
|
284
|
+
return line;
|
|
285
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine v2 string utilities.
|
|
3
|
+
*
|
|
4
|
+
* Copied from v1 to preserve keygen/normalization behavior.
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizeString(text: string): string;
|
|
7
|
+
export declare function toCamelCase(text: string): string;
|
|
8
|
+
export declare function inferNamespace(filePath: string): string;
|
|
9
|
+
export declare function generateUniqueKey(baseKey: string, existingKeys: Set<string>): string;
|
|
10
|
+
export declare function truncateForKey(text: string, maxWords?: number): string;
|
|
11
|
+
export declare function isTranslatableText(text: string): boolean;
|
|
12
|
+
export declare function extractPlaceholders(text: string): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Protect placeholders by replacing them with AI-safe tokens before translation
|
|
15
|
+
*/
|
|
16
|
+
export declare function protectPlaceholders(text: string): {
|
|
17
|
+
protectedText: string;
|
|
18
|
+
vars: string[];
|
|
19
|
+
};
|
|
20
|
+
export interface RestorationResult {
|
|
21
|
+
restoredText: string;
|
|
22
|
+
restoredCount: number;
|
|
23
|
+
expectedCount: number;
|
|
24
|
+
missingTokens: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Restore placeholders from AI-safe tokens after translation
|
|
28
|
+
* Returns a diagnostic result to detect dropped variables
|
|
29
|
+
*/
|
|
30
|
+
export declare function restorePlaceholders(text: string, vars: string[]): RestorationResult;
|
|
31
|
+
/**
|
|
32
|
+
* Check if text text and associated variables likely represent a pluralized string
|
|
33
|
+
* (e.g. "count files", "Total {length}")
|
|
34
|
+
*/
|
|
35
|
+
export declare function isLikelyPlural(text: string, variables?: string[]): boolean;
|
|
36
|
+
export declare function hasICUSyntax(text: string): boolean;
|
|
37
|
+
export declare function escapeRegex(text: string): string;
|
|
38
|
+
export declare function hashString(text: string): string;
|
|
39
|
+
export declare function matchesCurrencyPattern(text: string): boolean;
|
|
40
|
+
export declare function matchesDatePattern(text: string): boolean;
|
|
41
|
+
export declare function containsPII(text: string): boolean;
|
|
42
|
+
export declare const SKIP_TAGS: Set<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Extract content between balanced opening and closing tags, handling nested tags.
|
|
45
|
+
* Finds the first `<tagName>` and its matching `</tagName>`, correctly counting
|
|
46
|
+
* nested `<tagName ...>` elements.
|
|
47
|
+
*/
|
|
48
|
+
export declare function extractBalancedTag(content: string, tagName: string, allowAttrs?: boolean): {
|
|
49
|
+
fullMatch: string;
|
|
50
|
+
body: string;
|
|
51
|
+
attrs: string;
|
|
52
|
+
} | null;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SKIP_TAGS = void 0;
|
|
4
|
+
exports.normalizeString = normalizeString;
|
|
5
|
+
exports.toCamelCase = toCamelCase;
|
|
6
|
+
exports.inferNamespace = inferNamespace;
|
|
7
|
+
exports.generateUniqueKey = generateUniqueKey;
|
|
8
|
+
exports.truncateForKey = truncateForKey;
|
|
9
|
+
exports.isTranslatableText = isTranslatableText;
|
|
10
|
+
exports.extractPlaceholders = extractPlaceholders;
|
|
11
|
+
exports.protectPlaceholders = protectPlaceholders;
|
|
12
|
+
exports.restorePlaceholders = restorePlaceholders;
|
|
13
|
+
exports.isLikelyPlural = isLikelyPlural;
|
|
14
|
+
exports.hasICUSyntax = hasICUSyntax;
|
|
15
|
+
exports.escapeRegex = escapeRegex;
|
|
16
|
+
exports.hashString = hashString;
|
|
17
|
+
exports.matchesCurrencyPattern = matchesCurrencyPattern;
|
|
18
|
+
exports.matchesDatePattern = matchesDatePattern;
|
|
19
|
+
exports.containsPII = containsPII;
|
|
20
|
+
exports.extractBalancedTag = extractBalancedTag;
|
|
21
|
+
const node_crypto_1 = require("node:crypto");
|
|
22
|
+
/**
|
|
23
|
+
* Engine v2 string utilities.
|
|
24
|
+
*
|
|
25
|
+
* Copied from v1 to preserve keygen/normalization behavior.
|
|
26
|
+
*/
|
|
27
|
+
function normalizeString(text) {
|
|
28
|
+
return text
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^\p{L}\p{N}\s']/gu, ' ')
|
|
31
|
+
.replace(/\s+/g, ' ')
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
function toCamelCase(text) {
|
|
35
|
+
const normalized = normalizeString(text);
|
|
36
|
+
const words = normalized.split(' ').filter(Boolean);
|
|
37
|
+
if (words.length === 0)
|
|
38
|
+
return 'untitled';
|
|
39
|
+
const result = words
|
|
40
|
+
.map((word, index) => {
|
|
41
|
+
word = word.replace(/'/g, '');
|
|
42
|
+
if (index === 0)
|
|
43
|
+
return word.toLowerCase();
|
|
44
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
45
|
+
})
|
|
46
|
+
.join('');
|
|
47
|
+
if (!/[a-zA-Z]/.test(result))
|
|
48
|
+
return 'untitled';
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function inferNamespace(filePath) {
|
|
52
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
53
|
+
const pathWithoutExt = normalizedPath.replace(/\.(ts|tsx|js|jsx)$/, '');
|
|
54
|
+
const parts = pathWithoutExt.split('/').filter(Boolean);
|
|
55
|
+
const prefixesToRemove = ['src', 'app', 'pages', 'components', 'features', 'modules'];
|
|
56
|
+
while (parts.length > 0 && prefixesToRemove.includes(parts[0].toLowerCase())) {
|
|
57
|
+
parts.shift();
|
|
58
|
+
}
|
|
59
|
+
const suffixesToRemove = ['index', 'page', 'component', 'container', 'view'];
|
|
60
|
+
while (parts.length > 1 && suffixesToRemove.includes(parts[parts.length - 1].toLowerCase())) {
|
|
61
|
+
parts.pop();
|
|
62
|
+
}
|
|
63
|
+
if (parts.length === 0)
|
|
64
|
+
return 'common';
|
|
65
|
+
const namespace = parts[parts.length - 1]
|
|
66
|
+
.replace(/([A-Z])/g, (match, p1, offset) => (offset > 0 ? '-' + p1 : p1))
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/^-/, '');
|
|
69
|
+
return namespace.replace(/[^a-z0-9-]/g, '') || 'common';
|
|
70
|
+
}
|
|
71
|
+
function generateUniqueKey(baseKey, existingKeys) {
|
|
72
|
+
if (!existingKeys.has(baseKey))
|
|
73
|
+
return baseKey;
|
|
74
|
+
let counter = 2;
|
|
75
|
+
let uniqueKey = `${baseKey}_${counter}`;
|
|
76
|
+
while (existingKeys.has(uniqueKey)) {
|
|
77
|
+
counter++;
|
|
78
|
+
uniqueKey = `${baseKey}_${counter}`;
|
|
79
|
+
}
|
|
80
|
+
return uniqueKey;
|
|
81
|
+
}
|
|
82
|
+
function truncateForKey(text, maxWords = 5) {
|
|
83
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
84
|
+
if (words.length <= maxWords)
|
|
85
|
+
return text;
|
|
86
|
+
return words.slice(0, maxWords).join(' ');
|
|
87
|
+
}
|
|
88
|
+
function isTranslatableText(text) {
|
|
89
|
+
const trimmed = text.trim();
|
|
90
|
+
if (!trimmed)
|
|
91
|
+
return false;
|
|
92
|
+
if (/^\s*$/.test(trimmed))
|
|
93
|
+
return false;
|
|
94
|
+
if (/^\d+$/.test(trimmed))
|
|
95
|
+
return false;
|
|
96
|
+
if (/^[^\p{L}\p{N}\s]+$/u.test(trimmed))
|
|
97
|
+
return false;
|
|
98
|
+
if (trimmed.length < 2)
|
|
99
|
+
return false;
|
|
100
|
+
if (/^\{.*\}$/.test(trimmed))
|
|
101
|
+
return false;
|
|
102
|
+
if (/^(https?:\/\/|\/|\.\/|\.\.\/)/.test(trimmed))
|
|
103
|
+
return false;
|
|
104
|
+
if (/^[.#][a-zA-Z]/.test(trimmed))
|
|
105
|
+
return false;
|
|
106
|
+
if (/^\p{Ll}[\p{L}\p{N}_]*$/u.test(trimmed) && trimmed.length > 20)
|
|
107
|
+
return false;
|
|
108
|
+
return /[\p{L}]/u.test(trimmed);
|
|
109
|
+
}
|
|
110
|
+
function extractPlaceholders(text) {
|
|
111
|
+
// Use a more specific regex that only matches simple variable names to avoid greedy ICU matching
|
|
112
|
+
// or use non-greedy matching if we expect alphanumeric+underscore names.
|
|
113
|
+
const matches = text.match(/\{(\w+)\}/g);
|
|
114
|
+
if (!matches)
|
|
115
|
+
return [];
|
|
116
|
+
return matches.map((m) => m.slice(1, -1));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Protect placeholders by replacing them with AI-safe tokens before translation
|
|
120
|
+
*/
|
|
121
|
+
function protectPlaceholders(text) {
|
|
122
|
+
// If it's complex ICU syntax, we don't protect simple placeholders as they might be part of the ICU structure
|
|
123
|
+
// This avoids destroying nested braces like {count, plural, one {# item} other {# items}}
|
|
124
|
+
if (hasICUSyntax(text)) {
|
|
125
|
+
return { protectedText: text, vars: [] };
|
|
126
|
+
}
|
|
127
|
+
const vars = [];
|
|
128
|
+
let protectedText = text;
|
|
129
|
+
// Refined regex to only match simple alphanumeric variable names in braces
|
|
130
|
+
const regex = /\{(\w+)\}/g;
|
|
131
|
+
let i = 0;
|
|
132
|
+
protectedText = text.replace(regex, (match, varName) => {
|
|
133
|
+
vars.push(varName);
|
|
134
|
+
const token = `[#VAR_${i}#]`;
|
|
135
|
+
i++;
|
|
136
|
+
return token;
|
|
137
|
+
});
|
|
138
|
+
return { protectedText, vars };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Restore placeholders from AI-safe tokens after translation
|
|
142
|
+
* Returns a diagnostic result to detect dropped variables
|
|
143
|
+
*/
|
|
144
|
+
function restorePlaceholders(text, vars) {
|
|
145
|
+
let restored = text;
|
|
146
|
+
let restoredCount = 0;
|
|
147
|
+
const missingTokens = [];
|
|
148
|
+
for (let i = 0; i < vars.length; i++) {
|
|
149
|
+
const regex = new RegExp(`\\[\\s*#\\s*VAR_${i}\\s*#\\s*\\]`, 'gi');
|
|
150
|
+
const before = restored;
|
|
151
|
+
restored = restored.replace(regex, `{${vars[i]}}`);
|
|
152
|
+
if (restored !== before) {
|
|
153
|
+
restoredCount++;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
missingTokens.push(`[#VAR_${i}#]`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
restoredText: restored,
|
|
161
|
+
restoredCount,
|
|
162
|
+
expectedCount: vars.length,
|
|
163
|
+
missingTokens
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Check if text text and associated variables likely represent a pluralized string
|
|
168
|
+
* (e.g. "count files", "Total {length}")
|
|
169
|
+
*/
|
|
170
|
+
function isLikelyPlural(text, variables = []) {
|
|
171
|
+
const lowerText = text.toLowerCase().trim();
|
|
172
|
+
// Exclude question strings — plural generation produces nonsensical variants
|
|
173
|
+
// for interrogative sentences (e.g. "Do you support tracked shipping?").
|
|
174
|
+
if (/^(do|does|is|are|can|will|would|have|has|did|should|could|was|were)\s/i.test(lowerText))
|
|
175
|
+
return false;
|
|
176
|
+
// Exclude single-brand terms (e.g. "GIFTIFY")
|
|
177
|
+
if (/^[A-Z]{3,}$/.test(text.trim()))
|
|
178
|
+
return false;
|
|
179
|
+
const pluralVars = ['count', 'length', 'total', 'num', 'size'];
|
|
180
|
+
if (variables.some((v) => pluralVars.includes(v.toLowerCase())))
|
|
181
|
+
return true;
|
|
182
|
+
if (/\{\s*(count|length|total|num|size)\s*\}/.test(lowerText))
|
|
183
|
+
return true;
|
|
184
|
+
// Digit + plural noun pattern — exclude longer title-like phrases
|
|
185
|
+
// (5+ words, e.g. "15 reasons naive automation fails") to reduce false positives
|
|
186
|
+
if (/\b\d+\s+[a-z]+s\b/.test(lowerText)) {
|
|
187
|
+
const wordCount = lowerText.split(/\s+/).length;
|
|
188
|
+
if (wordCount >= 5)
|
|
189
|
+
return false;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
function hasICUSyntax(text) {
|
|
195
|
+
return /\{[^}]+,\s*(plural|select|selectordinal)\s*,/.test(text);
|
|
196
|
+
}
|
|
197
|
+
function escapeRegex(text) {
|
|
198
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
199
|
+
}
|
|
200
|
+
function hashString(text) {
|
|
201
|
+
return (0, node_crypto_1.createHash)('sha256').update(text, 'utf8').digest('hex').slice(0, 16);
|
|
202
|
+
}
|
|
203
|
+
function matchesCurrencyPattern(text) {
|
|
204
|
+
return /[€$£¥]/.test(text) || /\d[.,]\d{2}\b/.test(text) || /\b\d{1,3}(,\d{3})+\b/.test(text);
|
|
205
|
+
}
|
|
206
|
+
function matchesDatePattern(text) {
|
|
207
|
+
return (/(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.](19|20)\d\d/.test(text) ||
|
|
208
|
+
/\b(19|20)\d\d[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])\b/.test(text));
|
|
209
|
+
}
|
|
210
|
+
function containsPII(text) {
|
|
211
|
+
const PATTERNS = {
|
|
212
|
+
email: /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/,
|
|
213
|
+
ipv4: /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/,
|
|
214
|
+
creditCard: /\b(?:\d[ -]*?){13,16}\b/,
|
|
215
|
+
secretToken: /\b(?:sk|pk|key|secret|token|auth|password)[-_=]\w{12,}\b/i,
|
|
216
|
+
hexKey: /\b[a-f0-9]{32,64}\b/i,
|
|
217
|
+
phone: /\b(?:\+\d{1,4}[- ]?)?\(?\d{2,5}\)?[- ]?\d{3,5}[- ]?\d{3,5}\b/,
|
|
218
|
+
};
|
|
219
|
+
return Object.values(PATTERNS).some((pattern) => pattern.test(text));
|
|
220
|
+
}
|
|
221
|
+
exports.SKIP_TAGS = new Set(['script', 'style', 'noscript', 'code', 'pre', 'svg']);
|
|
222
|
+
/**
|
|
223
|
+
* Extract content between balanced opening and closing tags, handling nested tags.
|
|
224
|
+
* Finds the first `<tagName>` and its matching `</tagName>`, correctly counting
|
|
225
|
+
* nested `<tagName ...>` elements.
|
|
226
|
+
*/
|
|
227
|
+
function extractBalancedTag(content, tagName, allowAttrs = false) {
|
|
228
|
+
const openPattern = allowAttrs ? `<${tagName}` : `<${tagName}>`;
|
|
229
|
+
const closeTag = `</${tagName}>`;
|
|
230
|
+
const startIdx = content.indexOf(openPattern);
|
|
231
|
+
if (startIdx === -1)
|
|
232
|
+
return null;
|
|
233
|
+
// For allowAttrs=true, find the end of the opening tag
|
|
234
|
+
let afterOpen;
|
|
235
|
+
let attrs = '';
|
|
236
|
+
if (allowAttrs) {
|
|
237
|
+
const gtIdx = content.indexOf('>', startIdx);
|
|
238
|
+
if (gtIdx === -1)
|
|
239
|
+
return null;
|
|
240
|
+
afterOpen = gtIdx + 1;
|
|
241
|
+
attrs = content.slice(startIdx + tagName.length + 1, gtIdx).trim();
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
afterOpen = startIdx + openPattern.length;
|
|
245
|
+
}
|
|
246
|
+
let depth = 1;
|
|
247
|
+
let i = afterOpen;
|
|
248
|
+
while (i < content.length && depth > 0) {
|
|
249
|
+
const nextOpen = content.indexOf(`<${tagName}`, i);
|
|
250
|
+
const nextClose = content.indexOf(closeTag, i);
|
|
251
|
+
if (nextClose === -1)
|
|
252
|
+
return null;
|
|
253
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
254
|
+
depth++;
|
|
255
|
+
i = nextOpen + tagName.length + 1;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
depth--;
|
|
259
|
+
if (depth === 0) {
|
|
260
|
+
return {
|
|
261
|
+
fullMatch: content.slice(startIdx, nextClose + closeTag.length),
|
|
262
|
+
body: content.slice(afterOpen, nextClose),
|
|
263
|
+
attrs,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
i = nextClose + closeTag.length;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Quality Validation
|
|
3
|
+
*
|
|
4
|
+
* Validates translation quality and detects potential issues
|
|
5
|
+
*/
|
|
6
|
+
export interface QualityIssue {
|
|
7
|
+
type: 'missing_placeholder' | 'extra_placeholder' | 'empty_translation' | 'suspicious_length' | 'untranslated_text' | 'html_tag_mismatch' | 'placeholder_order_mismatch';
|
|
8
|
+
severity: 'error' | 'warning';
|
|
9
|
+
message: string;
|
|
10
|
+
original: string;
|
|
11
|
+
translated: string;
|
|
12
|
+
}
|
|
13
|
+
export interface QualityResult {
|
|
14
|
+
isValid: boolean;
|
|
15
|
+
score: number;
|
|
16
|
+
issues: QualityIssue[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Validate translation quality
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateTranslationQuality(original: string, translated: string): QualityResult;
|
|
22
|
+
/**
|
|
23
|
+
* Validate batch of translations
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateBatchQuality(entries: Array<{
|
|
26
|
+
key: string;
|
|
27
|
+
original: string;
|
|
28
|
+
translated: string;
|
|
29
|
+
}>): {
|
|
30
|
+
valid: number;
|
|
31
|
+
invalid: number;
|
|
32
|
+
averageScore: number;
|
|
33
|
+
issues: QualityIssue[];
|
|
34
|
+
};
|