@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,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.analyzeModuleConstants = analyzeModuleConstants;
|
|
37
|
+
const t = __importStar(require("@babel/types"));
|
|
38
|
+
const types_1 = require("../runtime/types");
|
|
39
|
+
const ast_helpers_1 = require("./ast-helpers");
|
|
40
|
+
const string_utils_1 = require("./string-utils");
|
|
41
|
+
const jsx_message_1 = require("./jsx-message");
|
|
42
|
+
const ui_copy_1 = require("./ui-copy");
|
|
43
|
+
const attr = (el, name) => el.openingElement.attributes.some((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name }));
|
|
44
|
+
function positionOf(expr) {
|
|
45
|
+
const container = expr.parentPath;
|
|
46
|
+
if (!container?.isJSXExpressionContainer() || container.node.expression !== expr.node)
|
|
47
|
+
return 'other';
|
|
48
|
+
const holder = container.parentPath;
|
|
49
|
+
if (holder?.isJSXElement()) {
|
|
50
|
+
const tag = holder.node.openingElement.name;
|
|
51
|
+
// Form data, not display text: <option>{x}</option> without a value is
|
|
52
|
+
// the submitted value; <textarea>{x}</textarea> is the field's content.
|
|
53
|
+
if (t.isJSXIdentifier(tag, { name: 'option' }) && !attr(holder.node, 'value'))
|
|
54
|
+
return 'form';
|
|
55
|
+
if (t.isJSXIdentifier(tag, { name: 'textarea' }))
|
|
56
|
+
return 'form';
|
|
57
|
+
return 'render';
|
|
58
|
+
}
|
|
59
|
+
if (holder?.isJSXFragment())
|
|
60
|
+
return 'render';
|
|
61
|
+
if (holder?.isJSXAttribute() && t.isJSXIdentifier(holder.node.name)) {
|
|
62
|
+
if (holder.node.name.name === 'key')
|
|
63
|
+
return 'identity';
|
|
64
|
+
if (types_1.JSX_TRANSLATABLE_ATTRS.includes(holder.node.name.name))
|
|
65
|
+
return 'render';
|
|
66
|
+
}
|
|
67
|
+
return 'other';
|
|
68
|
+
}
|
|
69
|
+
const inComponent = (p) => (0, ast_helpers_1.isInsideComponent)(p) && !!(0, ast_helpers_1.findComponentBody)(p);
|
|
70
|
+
/** The render site is part of a JSX sentence (one message): the sentence pass owns it. */
|
|
71
|
+
function inSentence(p) {
|
|
72
|
+
let el = p.parentPath;
|
|
73
|
+
while (el && (el.isJSXExpressionContainer() || el.isJSXElement() || el.isJSXFragment())) {
|
|
74
|
+
if ((el.isJSXElement() || el.isJSXFragment()) && (0, jsx_message_1.buildJsxMessage)(el.node).kind === 'message')
|
|
75
|
+
return true;
|
|
76
|
+
el = el.parentPath;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function memberName(p) {
|
|
81
|
+
if (!p.isMemberExpression() || p.node.computed || !t.isIdentifier(p.node.property))
|
|
82
|
+
return null;
|
|
83
|
+
return p.node.property.name;
|
|
84
|
+
}
|
|
85
|
+
/** Plans for a file's module-level UI constants that are rendered in components. */
|
|
86
|
+
function analyzeModuleConstants(ast) {
|
|
87
|
+
const plans = [];
|
|
88
|
+
(0, ast_helpers_1.traverse)(ast, {
|
|
89
|
+
Program(program) {
|
|
90
|
+
for (const stmt of program.get('body')) {
|
|
91
|
+
const exported = stmt.isExportNamedDeclaration();
|
|
92
|
+
const decl = exported ? stmt.get('declaration') : stmt;
|
|
93
|
+
if (!decl?.isVariableDeclaration() || decl.node.kind !== 'const')
|
|
94
|
+
continue;
|
|
95
|
+
for (const d of decl.get('declarations')) {
|
|
96
|
+
const plan = planFor(d, exported);
|
|
97
|
+
if (plan)
|
|
98
|
+
plans.push(plan);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
program.stop();
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return plans;
|
|
105
|
+
}
|
|
106
|
+
function planFor(d, exported) {
|
|
107
|
+
if (!t.isIdentifier(d.node.id))
|
|
108
|
+
return null;
|
|
109
|
+
const name = d.node.id.name;
|
|
110
|
+
const init = d.get('init');
|
|
111
|
+
const isCopy = (s) => (0, string_utils_1.isTranslatableText)(s.trim());
|
|
112
|
+
// Per "field" ('' for an array of strings): the UI-copy literals that would
|
|
113
|
+
// become keys, every translatable literal (reported if not provable), and
|
|
114
|
+
// whether every value of the field is such a literal. A render site may
|
|
115
|
+
// only be wrapped in t() if all its values become keys — `["Overview",
|
|
116
|
+
// "FAQ"]` with only "Overview" keyed would render a missing key for FAQ.
|
|
117
|
+
const byField = new Map();
|
|
118
|
+
const translatable = new Map();
|
|
119
|
+
const incomplete = new Set();
|
|
120
|
+
const push = (map, field, lit) => map.set(field, [...(map.get(field) ?? []), lit]);
|
|
121
|
+
const consider = (field, value, uiCopy) => {
|
|
122
|
+
if (!value.isStringLiteral()) {
|
|
123
|
+
incomplete.add(field);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (isCopy(value.node.value))
|
|
127
|
+
push(translatable, field, value);
|
|
128
|
+
if (isCopy(value.node.value) && uiCopy(value.node.value))
|
|
129
|
+
push(byField, field, value);
|
|
130
|
+
else
|
|
131
|
+
incomplete.add(field);
|
|
132
|
+
};
|
|
133
|
+
let kind;
|
|
134
|
+
const objectFields = (objects) => {
|
|
135
|
+
const fields = new Set();
|
|
136
|
+
for (const obj of objects) {
|
|
137
|
+
for (const prop of obj.get('properties')) {
|
|
138
|
+
if (prop.isObjectProperty() && !prop.node.computed && t.isIdentifier(prop.node.key))
|
|
139
|
+
fields.add(prop.node.key.name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const obj of objects) {
|
|
143
|
+
const props = new Map();
|
|
144
|
+
let opaque = false;
|
|
145
|
+
for (const prop of obj.get('properties')) {
|
|
146
|
+
if (prop.isObjectProperty() && !prop.node.computed && t.isIdentifier(prop.node.key))
|
|
147
|
+
props.set(prop.node.key.name, prop.get('value'));
|
|
148
|
+
else
|
|
149
|
+
opaque = true; // spread / computed keys: any field may come from there
|
|
150
|
+
}
|
|
151
|
+
for (const field of fields) {
|
|
152
|
+
const value = props.get(field);
|
|
153
|
+
if (!value || opaque)
|
|
154
|
+
incomplete.add(field);
|
|
155
|
+
if (value)
|
|
156
|
+
consider(field, value, (text) => (0, ui_copy_1.isUiCopyForName)(field, text));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
if (init.isArrayExpression()) {
|
|
161
|
+
const elements = init.get('elements');
|
|
162
|
+
if (elements.length > 0 && elements.every((e) => e.isStringLiteral())) {
|
|
163
|
+
kind = 'strings';
|
|
164
|
+
for (const e of elements)
|
|
165
|
+
consider('', e, ui_copy_1.looksLikeCopy);
|
|
166
|
+
}
|
|
167
|
+
else if (elements.length > 0 && elements.every((e) => e.isObjectExpression())) {
|
|
168
|
+
kind = 'objects';
|
|
169
|
+
objectFields(elements);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (init.isObjectExpression()) {
|
|
176
|
+
kind = 'object';
|
|
177
|
+
objectFields([init]);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (translatable.size === 0)
|
|
183
|
+
return null;
|
|
184
|
+
const binding = d.scope.getBinding(name);
|
|
185
|
+
if (!binding)
|
|
186
|
+
return null;
|
|
187
|
+
const sites = new Map();
|
|
188
|
+
const fail = [];
|
|
189
|
+
const site = (field, expr) => {
|
|
190
|
+
if (!translatable.has(field))
|
|
191
|
+
return; // nothing translatable rendered from it
|
|
192
|
+
const pos = positionOf(expr);
|
|
193
|
+
if (pos === 'render') {
|
|
194
|
+
if (!inComponent(expr))
|
|
195
|
+
fail.push('rendered outside a component');
|
|
196
|
+
else if (inSentence(expr))
|
|
197
|
+
fail.push('rendered inside a sentence');
|
|
198
|
+
else if (incomplete.has(field))
|
|
199
|
+
fail.push('not every value is a translatable string');
|
|
200
|
+
sites.set(field, [...(sites.get(field) ?? []), expr]);
|
|
201
|
+
}
|
|
202
|
+
else if (pos === 'form') {
|
|
203
|
+
// Shown, but also the submitted value: report rather than rewrite.
|
|
204
|
+
fail.push(`${expr.toString()} is form data (<option> without a value, or <textarea>)`);
|
|
205
|
+
sites.set(field, [...(sites.get(field) ?? []), expr]);
|
|
206
|
+
}
|
|
207
|
+
else if (pos === 'other') {
|
|
208
|
+
fail.push(`${expr.toString()} is used as data`);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
if (exported)
|
|
212
|
+
fail.push('exported');
|
|
213
|
+
if (binding.constantViolations.length)
|
|
214
|
+
fail.push('reassigned');
|
|
215
|
+
for (const ref of binding.referencePaths) {
|
|
216
|
+
const parent = ref.parentPath;
|
|
217
|
+
const prop = memberName(parent);
|
|
218
|
+
if ((kind === 'strings' || kind === 'objects') && prop === 'map' && parent.parentPath?.isCallExpression() && parent.parentPath.node.callee === parent.node) {
|
|
219
|
+
const cb = parent.parentPath.get('arguments')[0];
|
|
220
|
+
const param = cb && (cb.isArrowFunctionExpression() || cb.isFunctionExpression()) ? cb.node.params[0] : undefined;
|
|
221
|
+
if (!cb || !t.isIdentifier(param)) {
|
|
222
|
+
fail.push(`${name}.map callback doesn't take a plain parameter`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const item = cb.scope.getBinding(param.name);
|
|
226
|
+
if (!item || item.constantViolations.length) {
|
|
227
|
+
fail.push(`${name}.map item is reassigned`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
for (const use of item.referencePaths) {
|
|
231
|
+
if (kind === 'strings') {
|
|
232
|
+
site('', use);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const member = use.parentPath;
|
|
236
|
+
const field = memberName(member);
|
|
237
|
+
if (field === null || !member.isMemberExpression() || member.node.object !== use.node) {
|
|
238
|
+
fail.push(`${param.name} is used as a whole`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
site(field, member);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (kind === 'object' && prop !== null) {
|
|
247
|
+
site(prop, parent);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (kind === 'strings' && parent.isMemberExpression() && parent.node.computed && t.isNumericLiteral(parent.node.property)) {
|
|
251
|
+
// TABS[0]: only that element is rendered; keep it simple and treat as one field.
|
|
252
|
+
site('', parent);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
fail.push(`${name} is used as data`);
|
|
256
|
+
}
|
|
257
|
+
if (sites.size === 0)
|
|
258
|
+
return null; // never rendered: it's data
|
|
259
|
+
const provable = fail.length === 0;
|
|
260
|
+
// Provable: the keyed literals (all values of each rendered field).
|
|
261
|
+
// Otherwise: every translatable literal that is rendered, to report.
|
|
262
|
+
const literals = [...sites.keys()].flatMap((f) => (provable ? byField : translatable).get(f) ?? []);
|
|
263
|
+
const renderSites = [...sites.values()].flat();
|
|
264
|
+
return { name, provable, literals, renderSites, reason: fail[0] };
|
|
265
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { InjectionResult, KeyGenerationResult, NotInjectedString } from '../runtime/types';
|
|
2
|
+
/**
|
|
3
|
+
* Keys generated this run whose string the injector didn't replace in its
|
|
4
|
+
* file. Their translations exist, but the app keeps showing the literal.
|
|
5
|
+
*/
|
|
6
|
+
export declare function findNotInjected(keyGeneration: KeyGenerationResult, injection: InjectionResult): NotInjectedString[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findNotInjected = findNotInjected;
|
|
4
|
+
/**
|
|
5
|
+
* Keys generated this run whose string the injector didn't replace in its
|
|
6
|
+
* file. Their translations exist, but the app keeps showing the literal.
|
|
7
|
+
*/
|
|
8
|
+
function findNotInjected(keyGeneration, injection) {
|
|
9
|
+
const injected = new Set();
|
|
10
|
+
for (const file of injection.modifiedFiles) {
|
|
11
|
+
for (const i of file.injections)
|
|
12
|
+
injected.add(`${i.file ?? file.file}\u0000${i.key}`);
|
|
13
|
+
}
|
|
14
|
+
const skipped = new Map(injection.skippedFiles.map((f) => [f.file, f]));
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const k of keyGeneration.keys) {
|
|
18
|
+
if (k.isPlural)
|
|
19
|
+
continue; // the base key stands for its plural forms
|
|
20
|
+
const file = k.extractionId.split(':')[0];
|
|
21
|
+
const id = `${file}\u0000${k.key}`;
|
|
22
|
+
if (injected.has(id) || seen.has(id))
|
|
23
|
+
continue;
|
|
24
|
+
seen.add(id);
|
|
25
|
+
const skip = skipped.get(file);
|
|
26
|
+
out.push({
|
|
27
|
+
file,
|
|
28
|
+
key: k.key,
|
|
29
|
+
text: k.value,
|
|
30
|
+
reason: skip
|
|
31
|
+
? `file skipped (${skip.reason}${skip.details ? `: ${skip.details}` : ''})`
|
|
32
|
+
: 'no place to add the translation hook (e.g. outside a component, or a pattern the injector does not handle)',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder protection for machine translation.
|
|
3
|
+
*
|
|
4
|
+
* Everything in a UI string that must reach the translated output byte-for-byte
|
|
5
|
+
* — interpolation variables, markup tags, URLs, never-translate terms — is
|
|
6
|
+
* swapped for an opaque `[#VAR_n#]` token before translation, then validated
|
|
7
|
+
* and swapped back. The token format is shared with the Rust translator
|
|
8
|
+
* (apps/translator/src/placeholders.rs), so the same text works for every
|
|
9
|
+
* provider.
|
|
10
|
+
*
|
|
11
|
+
* ICU messages (`{count, plural, ...}`) are not tokenized: their structure has
|
|
12
|
+
* to change per locale (plural categories), so they are validated by
|
|
13
|
+
* ./icu-message instead.
|
|
14
|
+
*/
|
|
15
|
+
export interface ProtectedText {
|
|
16
|
+
/** Text to send to the translator, with protected spans replaced by tokens. */
|
|
17
|
+
text: string;
|
|
18
|
+
/** Original text of each token, by index: tokens[i] is what `[#VAR_i#]` stands for. */
|
|
19
|
+
tokens: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface TokenCheck {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
/** Indices whose token is absent from the translation. */
|
|
24
|
+
missing: number[];
|
|
25
|
+
/** Indices whose token appears more than once. */
|
|
26
|
+
duplicated: number[];
|
|
27
|
+
/** Token indices in the translation that were never issued. */
|
|
28
|
+
unexpected: number[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Replace every protected span (and every never-translate term) with a token.
|
|
32
|
+
* With `icu: true`, braces are left alone (they are ICU structure) and only
|
|
33
|
+
* URLs, emails, code, tags and terms are tokenized.
|
|
34
|
+
* Literal `[#VAR_n#]` text already present in the source is protected too, so
|
|
35
|
+
* it can never be confused with an issued token.
|
|
36
|
+
*/
|
|
37
|
+
export declare function protectText(text: string, neverTranslateTerms?: string[], options?: {
|
|
38
|
+
icu?: boolean;
|
|
39
|
+
}): ProtectedText;
|
|
40
|
+
/** Every issued token must appear exactly once, and no other token may appear. */
|
|
41
|
+
export declare function checkTokens(translated: string, tokenCount: number): TokenCheck;
|
|
42
|
+
/** Swap tokens back for their original text. Call only after checkTokens passed. */
|
|
43
|
+
export declare function restoreText(translated: string, tokens: string[]): string;
|
|
44
|
+
/**
|
|
45
|
+
* True when nothing translatable is left once protected spans are removed —
|
|
46
|
+
* e.g. "{count}", "GitHub", "https://…", "%s / %s". Such strings are their
|
|
47
|
+
* own translation and should not be sent to a translator.
|
|
48
|
+
*/
|
|
49
|
+
export declare function hasNothingToTranslate(protectedText: string): boolean;
|
|
50
|
+
/** Human-readable description of a failed token check, for retry prompts and reports. */
|
|
51
|
+
export declare function describeTokenProblem(check: TokenCheck, tokens: string[]): string;
|
|
52
|
+
/**
|
|
53
|
+
* Whether two strings contain the same protected spans (variables, tags,
|
|
54
|
+
* URLs, terms), ignoring order. Used to vet rewrites of an already-validated
|
|
55
|
+
* translation, e.g. a brand-voice polish pass.
|
|
56
|
+
*/
|
|
57
|
+
export declare function sameProtectedSpans(a: string, b: string, neverTranslateTerms?: string[]): boolean;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Placeholder protection for machine translation.
|
|
4
|
+
*
|
|
5
|
+
* Everything in a UI string that must reach the translated output byte-for-byte
|
|
6
|
+
* — interpolation variables, markup tags, URLs, never-translate terms — is
|
|
7
|
+
* swapped for an opaque `[#VAR_n#]` token before translation, then validated
|
|
8
|
+
* and swapped back. The token format is shared with the Rust translator
|
|
9
|
+
* (apps/translator/src/placeholders.rs), so the same text works for every
|
|
10
|
+
* provider.
|
|
11
|
+
*
|
|
12
|
+
* ICU messages (`{count, plural, ...}`) are not tokenized: their structure has
|
|
13
|
+
* to change per locale (plural categories), so they are validated by
|
|
14
|
+
* ./icu-message instead.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.protectText = protectText;
|
|
18
|
+
exports.checkTokens = checkTokens;
|
|
19
|
+
exports.restoreText = restoreText;
|
|
20
|
+
exports.hasNothingToTranslate = hasNothingToTranslate;
|
|
21
|
+
exports.describeTokenProblem = describeTokenProblem;
|
|
22
|
+
exports.sameProtectedSpans = sameProtectedSpans;
|
|
23
|
+
const tokenFor = (index) => `[#VAR_${index}#]`;
|
|
24
|
+
/** Matches a token, tolerating the spacing/casing drift LLMs introduce. */
|
|
25
|
+
const TOKEN_PATTERN = /\[\s*#\s*VAR_(\d+)\s*#\s*\]/gi;
|
|
26
|
+
/**
|
|
27
|
+
* What gets protected, in priority order (earlier alternatives win where
|
|
28
|
+
* matches overlap — e.g. a `{var}` inside a URL stays part of the URL).
|
|
29
|
+
*/
|
|
30
|
+
const PROTECTED_PATTERNS = [
|
|
31
|
+
{ kind: 'url', source: String.raw `\b(?:https?|ftp|mailto):[^\s<>"'\])]+[^\s<>"'\]).,;:!?]` },
|
|
32
|
+
{ kind: 'email', source: String.raw `\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b` },
|
|
33
|
+
{ kind: 'code', source: String.raw `\x60[^\x60\n]+\x60` },
|
|
34
|
+
// i18next / Handlebars / Mustache: {{name}}, {{- name}}, {{count, number}}
|
|
35
|
+
{ kind: 'mustache', source: String.raw `\{\{[^{}]*\}\}` },
|
|
36
|
+
// JS template literal leftovers: ${name}
|
|
37
|
+
{ kind: 'template', source: String.raw `\$\{[^{}]*\}` },
|
|
38
|
+
// i18next nesting: $t(key) / $t(key, {...})
|
|
39
|
+
{ kind: 'nesting', source: String.raw `\$t\([^()]*\)` },
|
|
40
|
+
// Python named printf: %(name)s
|
|
41
|
+
{ kind: 'printf', source: String.raw `%\(\w+\)[sdifr]` },
|
|
42
|
+
// printf: %s %d %1$s %.2f %@ — not "%" followed by a word ("50%done")
|
|
43
|
+
{ kind: 'printf', source: String.raw `%(?:\d+\$)?[-+0#]*\d*(?:\.\d+)?[sdifu@xXc](?![A-Za-z])` },
|
|
44
|
+
// ICU simple / formatted args and positional args: {name} {0} {price, number} {d, date, short}
|
|
45
|
+
{ kind: 'argument', source: String.raw `\{\s*[\w.]+\s*(?:,\s*(?:number|date|time)\s*(?:,\s*[^{}]+)?)?\}` },
|
|
46
|
+
// Markup / react-i18next Trans tags: <0>, </0>, <1/>, <b>, </strong>, <br />, <Link href="...">
|
|
47
|
+
{ kind: 'tag', source: String.raw `<\/?\d+\s*\/?>|<\/?[A-Za-z][\w.:-]*(?:\s+[^<>]*?)?\s*\/?>` },
|
|
48
|
+
];
|
|
49
|
+
function escapeRegex(s) {
|
|
50
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
51
|
+
}
|
|
52
|
+
/** In ICU messages braces are structure, so only these kinds are tokenized. */
|
|
53
|
+
const ICU_SAFE_KINDS = new Set(['url', 'email', 'code', 'tag']);
|
|
54
|
+
function buildPattern(neverTranslateTerms, icu) {
|
|
55
|
+
const sources = PROTECTED_PATTERNS.filter((p) => !icu || ICU_SAFE_KINDS.has(p.kind)).map((p) => p.source);
|
|
56
|
+
const terms = [...new Set(neverTranslateTerms.filter((t) => t.trim().length > 0))]
|
|
57
|
+
.sort((a, b) => b.length - a.length)
|
|
58
|
+
.map((t) => `(?<![\\w])${escapeRegex(t)}(?![\\w])`);
|
|
59
|
+
if (terms.length > 0)
|
|
60
|
+
sources.push(terms.join('|'));
|
|
61
|
+
return new RegExp(sources.map((s) => `(?:${s})`).join('|'), 'gi');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Replace every protected span (and every never-translate term) with a token.
|
|
65
|
+
* With `icu: true`, braces are left alone (they are ICU structure) and only
|
|
66
|
+
* URLs, emails, code, tags and terms are tokenized.
|
|
67
|
+
* Literal `[#VAR_n#]` text already present in the source is protected too, so
|
|
68
|
+
* it can never be confused with an issued token.
|
|
69
|
+
*/
|
|
70
|
+
function protectText(text, neverTranslateTerms = [], options = {}) {
|
|
71
|
+
const tokens = [];
|
|
72
|
+
const pattern = buildPattern(neverTranslateTerms, options.icu ?? false);
|
|
73
|
+
// Guard pre-existing token-shaped text first.
|
|
74
|
+
const guarded = text.replace(TOKEN_PATTERN, (m) => {
|
|
75
|
+
tokens.push(m);
|
|
76
|
+
return `\uE000${tokens.length - 1}\uE000`;
|
|
77
|
+
});
|
|
78
|
+
const protectedText = guarded
|
|
79
|
+
.replace(pattern, (match) => {
|
|
80
|
+
tokens.push(match);
|
|
81
|
+
return tokenFor(tokens.length - 1);
|
|
82
|
+
})
|
|
83
|
+
.replace(/\uE000(\d+)\uE000/g, (_, i) => tokenFor(Number(i)));
|
|
84
|
+
return { text: protectedText, tokens };
|
|
85
|
+
}
|
|
86
|
+
/** Every issued token must appear exactly once, and no other token may appear. */
|
|
87
|
+
function checkTokens(translated, tokenCount) {
|
|
88
|
+
const counts = new Map();
|
|
89
|
+
for (const m of translated.matchAll(TOKEN_PATTERN)) {
|
|
90
|
+
const i = Number(m[1]);
|
|
91
|
+
counts.set(i, (counts.get(i) ?? 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
const missing = [];
|
|
94
|
+
const duplicated = [];
|
|
95
|
+
for (let i = 0; i < tokenCount; i++) {
|
|
96
|
+
const c = counts.get(i) ?? 0;
|
|
97
|
+
if (c === 0)
|
|
98
|
+
missing.push(i);
|
|
99
|
+
else if (c > 1)
|
|
100
|
+
duplicated.push(i);
|
|
101
|
+
}
|
|
102
|
+
const unexpected = [...counts.keys()].filter((i) => i >= tokenCount);
|
|
103
|
+
return { ok: missing.length === 0 && duplicated.length === 0 && unexpected.length === 0, missing, duplicated, unexpected };
|
|
104
|
+
}
|
|
105
|
+
/** Swap tokens back for their original text. Call only after checkTokens passed. */
|
|
106
|
+
function restoreText(translated, tokens) {
|
|
107
|
+
return translated.replace(TOKEN_PATTERN, (m, i) => tokens[Number(i)] ?? m);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* True when nothing translatable is left once protected spans are removed —
|
|
111
|
+
* e.g. "{count}", "GitHub", "https://…", "%s / %s". Such strings are their
|
|
112
|
+
* own translation and should not be sent to a translator.
|
|
113
|
+
*/
|
|
114
|
+
function hasNothingToTranslate(protectedText) {
|
|
115
|
+
return !/\p{L}/u.test(protectedText.replace(TOKEN_PATTERN, ''));
|
|
116
|
+
}
|
|
117
|
+
/** Human-readable description of a failed token check, for retry prompts and reports. */
|
|
118
|
+
function describeTokenProblem(check, tokens) {
|
|
119
|
+
const parts = [];
|
|
120
|
+
if (check.missing.length)
|
|
121
|
+
parts.push(`dropped ${check.missing.map((i) => `${tokenFor(i)} (${tokens[i]})`).join(', ')}`);
|
|
122
|
+
if (check.duplicated.length)
|
|
123
|
+
parts.push(`repeated ${check.duplicated.map(tokenFor).join(', ')}`);
|
|
124
|
+
if (check.unexpected.length)
|
|
125
|
+
parts.push(`invented ${check.unexpected.map(tokenFor).join(', ')}`);
|
|
126
|
+
return parts.join('; ');
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Whether two strings contain the same protected spans (variables, tags,
|
|
130
|
+
* URLs, terms), ignoring order. Used to vet rewrites of an already-validated
|
|
131
|
+
* translation, e.g. a brand-voice polish pass.
|
|
132
|
+
*/
|
|
133
|
+
function sameProtectedSpans(a, b, neverTranslateTerms = []) {
|
|
134
|
+
const spans = (t) => protectText(t, neverTranslateTerms).tokens.slice().sort().join('\u0001');
|
|
135
|
+
return spans(a) === spans(b);
|
|
136
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Marks pseudo-localized text (`babelize translate --provider pseudo`); never part of a real translation. */
|
|
2
|
+
export declare const PSEUDO_OPEN = "\u27E6";
|
|
3
|
+
export declare const PSEUDO_CLOSE = "\u27E7";
|
|
4
|
+
/**
|
|
5
|
+
* True for pseudo-localized output: text wrapped in ⟦…⟧ (whole value, or
|
|
6
|
+
* each branch of a plural the pseudo provider built). Accented letters alone
|
|
7
|
+
* are not a signal — "Sávé" could be real text in many languages.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isPseudoLocalized(value: string): boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PSEUDO_CLOSE = exports.PSEUDO_OPEN = void 0;
|
|
4
|
+
exports.isPseudoLocalized = isPseudoLocalized;
|
|
5
|
+
/** Marks pseudo-localized text (`babelize translate --provider pseudo`); never part of a real translation. */
|
|
6
|
+
exports.PSEUDO_OPEN = '⟦';
|
|
7
|
+
exports.PSEUDO_CLOSE = '⟧';
|
|
8
|
+
/**
|
|
9
|
+
* True for pseudo-localized output: text wrapped in ⟦…⟧ (whole value, or
|
|
10
|
+
* each branch of a plural the pseudo provider built). Accented letters alone
|
|
11
|
+
* are not a signal — "Sávé" could be real text in many languages.
|
|
12
|
+
*/
|
|
13
|
+
function isPseudoLocalized(value) {
|
|
14
|
+
const open = value.indexOf(exports.PSEUDO_OPEN);
|
|
15
|
+
return open !== -1 && value.indexOf(exports.PSEUDO_CLOSE, open + 1) !== -1;
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExtractedString, Warning } from '../runtime/types';
|
|
2
|
+
/**
|
|
3
|
+
* Extract translatable strings from Python source code.
|
|
4
|
+
*
|
|
5
|
+
* Matches: _(), gettext(), gettext_lazy(), ugettext*(), ngettext()
|
|
6
|
+
* Also opt-in: verbose_name=, help_text=, label= kwargs
|
|
7
|
+
*
|
|
8
|
+
* Uses a character-position lexer approach (not a full AST parser)
|
|
9
|
+
* to handle single/double/triple quotes, raw strings, f-strings, and escape sequences.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractStringsFromPython(content: string, filePath: string): {
|
|
12
|
+
strings: ExtractedString[];
|
|
13
|
+
warnings: Warning[];
|
|
14
|
+
};
|