@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,701 @@
|
|
|
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.extractStrings = extractStrings;
|
|
37
|
+
exports.extractStringsFromFile = extractStringsFromFile;
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const types_1 = require("../../runtime/types");
|
|
40
|
+
const html_extractor_1 = require("../../utils/html-extractor");
|
|
41
|
+
const ast_helpers_1 = require("../../utils/ast-helpers");
|
|
42
|
+
const file_utils_1 = require("../../utils/file-utils");
|
|
43
|
+
const string_utils_1 = require("../../utils/string-utils");
|
|
44
|
+
const detect_format_1 = require("../../utils/detect-format");
|
|
45
|
+
const dtl_mask_1 = require("../../utils/dtl-mask");
|
|
46
|
+
const py_extractor_1 = require("../../utils/py-extractor");
|
|
47
|
+
const jsx_message_1 = require("../../utils/jsx-message");
|
|
48
|
+
const ui_copy_1 = require("../../utils/ui-copy");
|
|
49
|
+
const module_constants_1 = require("../../utils/module-constants");
|
|
50
|
+
/** Frameworks whose injection strategies can render a JSX sentence as one message. */
|
|
51
|
+
const SENTENCE_FRAMEWORKS = new Set(['react', 'nextjs']);
|
|
52
|
+
async function extractStrings(workspace, filesToProcess, extractOptions = {}) {
|
|
53
|
+
const detectFormats = !!workspace.formatConfig?.enabled;
|
|
54
|
+
const strings = [];
|
|
55
|
+
const filesAnalyzed = [];
|
|
56
|
+
const skippedFiles = [];
|
|
57
|
+
const warnings = [];
|
|
58
|
+
const notInjected = [];
|
|
59
|
+
const sourceFiles = [];
|
|
60
|
+
if (filesToProcess && filesToProcess.length > 0) {
|
|
61
|
+
// If specific files are provided, only scan those
|
|
62
|
+
for (const relPath of filesToProcess) {
|
|
63
|
+
sourceFiles.push(path.join(workspace.path, relPath));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Otherwise scan all source directories
|
|
68
|
+
for (const sourceDir of workspace.sourceDirs) {
|
|
69
|
+
const dirPath = path.join(workspace.path, sourceDir);
|
|
70
|
+
const files = await (0, file_utils_1.findFiles)(dirPath, types_1.SUPPORTED_EXTENSIONS, types_1.EXCLUDED_PATTERNS);
|
|
71
|
+
sourceFiles.push(...files);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const raw = process.env.BABELIZE_EXTRACT_CONCURRENCY;
|
|
75
|
+
const EXTRACTION_CONCURRENCY = raw ? Math.max(1, parseInt(raw, 10) || 1) : 10;
|
|
76
|
+
const { results: extractedResults, errors: extractionErrors } = await (0, file_utils_1.concurrentMap)(sourceFiles, async (filePath) => {
|
|
77
|
+
const relativePath = (0, file_utils_1.getRelativePath)(workspace.path, filePath);
|
|
78
|
+
try {
|
|
79
|
+
const content = await (0, file_utils_1.readFile)(filePath);
|
|
80
|
+
const extracted = await extractStringsFromFile(content, relativePath, detectFormats, { framework: workspace.framework, ...extractOptions });
|
|
81
|
+
return { relativePath, extracted };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
throw Object.assign(error instanceof Error ? error : new Error(String(error)), { relativePath });
|
|
85
|
+
}
|
|
86
|
+
}, EXTRACTION_CONCURRENCY);
|
|
87
|
+
for (const result of extractedResults) {
|
|
88
|
+
if (!result)
|
|
89
|
+
continue;
|
|
90
|
+
const { relativePath, extracted } = result;
|
|
91
|
+
strings.push(...extracted.strings);
|
|
92
|
+
warnings.push(...extracted.warnings);
|
|
93
|
+
if (extracted.notInjected)
|
|
94
|
+
notInjected.push(...extracted.notInjected);
|
|
95
|
+
filesAnalyzed.push(relativePath);
|
|
96
|
+
}
|
|
97
|
+
for (const err of extractionErrors) {
|
|
98
|
+
skippedFiles.push({
|
|
99
|
+
file: err.relativePath || 'unknown',
|
|
100
|
+
reason: 'parse-error',
|
|
101
|
+
details: err.message,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const extraction = { strings, filesAnalyzed, skippedFiles, warnings, notInjected };
|
|
105
|
+
return { extraction };
|
|
106
|
+
}
|
|
107
|
+
async function extractStringsFromFile(content, filePath, detectFormats, options) {
|
|
108
|
+
// Django template handling
|
|
109
|
+
if (options?.framework === 'django' && (filePath.endsWith('.html') || filePath.endsWith('.htm'))) {
|
|
110
|
+
const { masked } = (0, dtl_mask_1.maskTemplateTags)(content);
|
|
111
|
+
const result = await (0, html_extractor_1.extractStringsFromHtml)(masked, filePath, detectFormats);
|
|
112
|
+
// Don't restore — we extract from the masked version, the restore happens at inject time
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
// Python file handling
|
|
116
|
+
if (filePath.endsWith('.py')) {
|
|
117
|
+
return (0, py_extractor_1.extractStringsFromPython)(content, filePath);
|
|
118
|
+
}
|
|
119
|
+
if (filePath.endsWith('.html') || filePath.endsWith('.htm')) {
|
|
120
|
+
return (0, html_extractor_1.extractStringsFromHtml)(content, filePath, detectFormats);
|
|
121
|
+
}
|
|
122
|
+
if (filePath.endsWith('.vue')) {
|
|
123
|
+
return extractStringsFromVueSfc(content, filePath, detectFormats);
|
|
124
|
+
}
|
|
125
|
+
const strings = [];
|
|
126
|
+
const warnings = [];
|
|
127
|
+
const seenTexts = new Set();
|
|
128
|
+
// Every node each extraction id stands for (identical text on one line is
|
|
129
|
+
// one id): injection replaces exactly these nodes, never others that
|
|
130
|
+
// merely have the same text, like `{ value: 'Active', label: 'Active' }`.
|
|
131
|
+
const offsetsById = new Map();
|
|
132
|
+
const noteOffset = (id, start) => {
|
|
133
|
+
if (typeof start !== 'number')
|
|
134
|
+
return;
|
|
135
|
+
const list = offsetsById.get(id) ?? [];
|
|
136
|
+
if (!list.includes(start))
|
|
137
|
+
list.push(start);
|
|
138
|
+
offsetsById.set(id, list);
|
|
139
|
+
};
|
|
140
|
+
// Group JSX sentences into one message (React / Next.js, and direct calls
|
|
141
|
+
// that don't name a framework). Other frameworks keep per-text extraction.
|
|
142
|
+
const groupSentences = options?.sentences !== false && (!options?.framework || SENTENCE_FRAMEWORKS.has(options.framework));
|
|
143
|
+
// Text nodes, literal `{" "}` children and inline elements already folded
|
|
144
|
+
// into a sentence message — not extracted again on their own.
|
|
145
|
+
const consumed = new WeakSet();
|
|
146
|
+
const unwired = [];
|
|
147
|
+
const isTypeScript = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
|
|
148
|
+
try {
|
|
149
|
+
const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript });
|
|
150
|
+
// UI copy in module-level constants rendered by components (#248): keys
|
|
151
|
+
// when every use is a render, otherwise reported — never silently skipped.
|
|
152
|
+
if (!options?.framework || SENTENCE_FRAMEWORKS.has(options.framework)) {
|
|
153
|
+
for (const plan of (0, module_constants_1.analyzeModuleConstants)(ast)) {
|
|
154
|
+
for (const lit of plan.literals) {
|
|
155
|
+
const text = lit.node.value.trim();
|
|
156
|
+
const line = lit.node.loc?.start.line || 0;
|
|
157
|
+
if (!plan.provable) {
|
|
158
|
+
unwired.push({
|
|
159
|
+
file: filePath,
|
|
160
|
+
key: '',
|
|
161
|
+
text,
|
|
162
|
+
reason: `module-level constant ${plan.name} (${plan.reason}): move it inside the component or translate where it's rendered`,
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
consumed.add(lit.node);
|
|
167
|
+
const id = `${filePath}:${line}:${(0, string_utils_1.hashString)(text)}`;
|
|
168
|
+
noteOffset(id, lit.node.start);
|
|
169
|
+
if (seenTexts.has(id))
|
|
170
|
+
continue;
|
|
171
|
+
seenTexts.add(id);
|
|
172
|
+
strings.push({ text, file: filePath, line, column: lit.node.loc?.start.column || 0, context: 'string-literal', id });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const processExtractedText = (text, path, variables = []) => {
|
|
177
|
+
if (typeof text !== 'string' || !text) {
|
|
178
|
+
if (text !== null && text !== undefined && typeof text !== 'string') {
|
|
179
|
+
warnings.push({
|
|
180
|
+
type: 'malformed-ast',
|
|
181
|
+
file: filePath,
|
|
182
|
+
line: path.node?.loc?.start?.line,
|
|
183
|
+
message: `Extraction produced non-string value: ${String(text)}. Skipping.`,
|
|
184
|
+
severity: 'warning',
|
|
185
|
+
dismissible: true,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
text = text.trim();
|
|
191
|
+
if (consumed.has(path.node))
|
|
192
|
+
return;
|
|
193
|
+
if (!options?.relaxComponentGuard && !isInJSXContext(path) && !(0, ast_helpers_1.isInsideComponent)(path))
|
|
194
|
+
return;
|
|
195
|
+
if ((0, ast_helpers_1.isNonTranslatableContext)(path))
|
|
196
|
+
return;
|
|
197
|
+
if (path.parentPath && path.parentPath.isCallExpression() && path.parentPath.node.arguments.includes(path.node)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// Program data in initializers (`{ name: "Ada" }`, `href: "/"`) is not UI copy.
|
|
201
|
+
if (!(0, string_utils_1.matchesCurrencyPattern)(text) && !(0, string_utils_1.matchesDatePattern)(text) && !(0, ui_copy_1.isUiCopyLiteral)(path.node, path.parent, text))
|
|
202
|
+
return;
|
|
203
|
+
const format = detectFormats ? (0, detect_format_1.detectFormat)(text) : null;
|
|
204
|
+
if (format) {
|
|
205
|
+
warnings.push({
|
|
206
|
+
type: 'ambiguous-format',
|
|
207
|
+
file: filePath,
|
|
208
|
+
line: path.node.loc?.start.line,
|
|
209
|
+
message: `Detected ${format.class} format: "${text}". Will be replaced with Intl.* API call.`,
|
|
210
|
+
severity: 'warning',
|
|
211
|
+
dismissible: true,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
if (/[€$£¥]/.test(text) || /\d[.,]\d{2}\b/.test(text) || /\b\d{1,3}(,\d{3})+\b/.test(text)) {
|
|
216
|
+
warnings.push({
|
|
217
|
+
type: 'hardcoded-currency',
|
|
218
|
+
file: filePath,
|
|
219
|
+
line: path.node.loc?.start.line,
|
|
220
|
+
message: 'Hardcoded currency/number format detected. Use Intl.NumberFormat.',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (/(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.](19|20)\d\d/.test(text) ||
|
|
224
|
+
/\b(19|20)\d\d[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])\b/.test(text)) {
|
|
225
|
+
warnings.push({
|
|
226
|
+
type: 'hardcoded-date',
|
|
227
|
+
file: filePath,
|
|
228
|
+
line: path.node.loc?.start.line,
|
|
229
|
+
message: 'Hardcoded date format detected. Use Intl.DateTimeFormat.',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if ((0, string_utils_1.isLikelyPlural)(text, variables)) {
|
|
234
|
+
warnings.push({
|
|
235
|
+
type: 'hardcoded-plural',
|
|
236
|
+
file: filePath,
|
|
237
|
+
line: path.node.loc?.start.line,
|
|
238
|
+
message: 'Hardcoded plural structure detected. Consider using structured plural keys (key_one, key_other).',
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
let parent = path.parentPath;
|
|
242
|
+
while (parent) {
|
|
243
|
+
if (parent.isJSXAttribute()) {
|
|
244
|
+
const nameNode = parent.node.name;
|
|
245
|
+
if (ast_helpers_1.t.isJSXIdentifier(nameNode) && !types_1.JSX_TRANSLATABLE_ATTRS.includes(nameNode.name)) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
if (parent.isJSXElement() || parent.isJSXFragment())
|
|
251
|
+
break;
|
|
252
|
+
parent = parent.parentPath;
|
|
253
|
+
}
|
|
254
|
+
if ((0, ast_helpers_1.isAlreadyTranslated)(path.node, path))
|
|
255
|
+
return;
|
|
256
|
+
const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(text);
|
|
257
|
+
const hasDate = (0, string_utils_1.matchesDatePattern)(text);
|
|
258
|
+
const isDateOrCurrencyValue = (hasCurrency || hasDate) && text.length >= 3 && !(0, string_utils_1.containsPII)(text);
|
|
259
|
+
if (!(0, string_utils_1.isTranslatableText)(text) || (0, string_utils_1.containsPII)(text) || text.length < 3) {
|
|
260
|
+
if (!isDateOrCurrencyValue && !format)
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const id = `${filePath}:${path.node.loc?.start.line}:${(0, string_utils_1.hashString)(text)}`;
|
|
264
|
+
let context = 'string-literal';
|
|
265
|
+
if (format) {
|
|
266
|
+
context = 'format-string';
|
|
267
|
+
}
|
|
268
|
+
else if (isDateOrCurrencyValue && !(0, string_utils_1.isTranslatableText)(text)) {
|
|
269
|
+
context = hasCurrency ? 'hardcoded-currency' : 'hardcoded-date';
|
|
270
|
+
}
|
|
271
|
+
noteOffset(id, path.node.start);
|
|
272
|
+
if (!seenTexts.has(id)) {
|
|
273
|
+
seenTexts.add(id);
|
|
274
|
+
strings.push({
|
|
275
|
+
text,
|
|
276
|
+
file: filePath,
|
|
277
|
+
line: path.node.loc?.start.line || 0,
|
|
278
|
+
column: path.node.loc?.start.column || 0,
|
|
279
|
+
context,
|
|
280
|
+
id,
|
|
281
|
+
detectedFormat: format ?? undefined,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
(0, ast_helpers_1.traverse)(ast, {
|
|
286
|
+
'JSXElement|JSXFragment'(path) {
|
|
287
|
+
if (!groupSentences || consumed.has(path.node))
|
|
288
|
+
return;
|
|
289
|
+
if ((0, ast_helpers_1.isAlreadyTranslated)(path.node, path))
|
|
290
|
+
return;
|
|
291
|
+
const result = (0, jsx_message_1.buildJsxMessage)(path.node, { plurals: options?.plurals !== false });
|
|
292
|
+
if (result.kind === 'none')
|
|
293
|
+
return;
|
|
294
|
+
// Rich text needs a component (t.rich / <Trans>); module-level JSX only
|
|
295
|
+
// gets the plain t() of the vanilla strategy, so keep its fragments.
|
|
296
|
+
if (result.kind === 'message' && (0, jsx_message_1.hasTags)(result.message) && !(0, ast_helpers_1.isInsideComponent)(path))
|
|
297
|
+
return;
|
|
298
|
+
const line = path.node.loc?.start.line || 0;
|
|
299
|
+
if (result.kind === 'unsafe') {
|
|
300
|
+
warnings.push({
|
|
301
|
+
type: 'complex-expression',
|
|
302
|
+
file: filePath,
|
|
303
|
+
line,
|
|
304
|
+
message: `Sentence kept as separate fragments: ${result.reason}. Translations can't reorder words around it.`,
|
|
305
|
+
severity: 'warning',
|
|
306
|
+
dismissible: true,
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const { message } = result;
|
|
311
|
+
for (const node of message.consumed)
|
|
312
|
+
consumed.add(node);
|
|
313
|
+
const id = `${filePath}:${line}:${(0, string_utils_1.hashString)(message.text)}`;
|
|
314
|
+
noteOffset(id, path.node.start);
|
|
315
|
+
if (seenTexts.has(id))
|
|
316
|
+
return;
|
|
317
|
+
seenTexts.add(id);
|
|
318
|
+
strings.push({
|
|
319
|
+
text: message.text,
|
|
320
|
+
file: filePath,
|
|
321
|
+
line,
|
|
322
|
+
column: path.node.loc?.start.column || 0,
|
|
323
|
+
context: 'jsx-text',
|
|
324
|
+
id,
|
|
325
|
+
variables: message.entries.filter((e) => e.kind === 'placeholder').map((e) => e.name),
|
|
326
|
+
icuMessage: true,
|
|
327
|
+
});
|
|
328
|
+
},
|
|
329
|
+
JSXText(path) {
|
|
330
|
+
if (consumed.has(path.node))
|
|
331
|
+
return;
|
|
332
|
+
const text = (0, ast_helpers_1.extractJSXTextContent)(path.node);
|
|
333
|
+
if (!text || (0, string_utils_1.containsPII)(text))
|
|
334
|
+
return;
|
|
335
|
+
const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(text);
|
|
336
|
+
const hasDate = (0, string_utils_1.matchesDatePattern)(text);
|
|
337
|
+
const isDateOrCurrencyValue = (hasCurrency || hasDate) && text.length >= 3;
|
|
338
|
+
const format = detectFormats ? (0, detect_format_1.detectFormat)(text) : null;
|
|
339
|
+
if (!(0, string_utils_1.isTranslatableText)(text) && !isDateOrCurrencyValue && !format)
|
|
340
|
+
return;
|
|
341
|
+
if (format) {
|
|
342
|
+
warnings.push({
|
|
343
|
+
type: 'ambiguous-format',
|
|
344
|
+
file: filePath,
|
|
345
|
+
line: path.node.loc?.start.line,
|
|
346
|
+
message: `Detected ${format.class} format: "${text}". Will be replaced with Intl.* API call.`,
|
|
347
|
+
severity: 'warning',
|
|
348
|
+
dismissible: true,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
if (hasCurrency) {
|
|
353
|
+
warnings.push({
|
|
354
|
+
type: 'hardcoded-currency',
|
|
355
|
+
file: filePath,
|
|
356
|
+
line: path.node.loc?.start.line,
|
|
357
|
+
message: 'Hardcoded currency/number format detected. Use Intl.NumberFormat.',
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (hasDate) {
|
|
361
|
+
warnings.push({
|
|
362
|
+
type: 'hardcoded-date',
|
|
363
|
+
file: filePath,
|
|
364
|
+
line: path.node.loc?.start.line,
|
|
365
|
+
message: 'Hardcoded date format detected. Use Intl.DateTimeFormat.',
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if ((0, ast_helpers_1.isAlreadyTranslated)(path.node, path))
|
|
370
|
+
return;
|
|
371
|
+
const isDateOrCurrencyOnly = isDateOrCurrencyValue && !(0, string_utils_1.isTranslatableText)(text);
|
|
372
|
+
let context = 'jsx-text';
|
|
373
|
+
if (format) {
|
|
374
|
+
context = 'format-string';
|
|
375
|
+
}
|
|
376
|
+
else if (isDateOrCurrencyOnly) {
|
|
377
|
+
context = hasCurrency ? 'hardcoded-currency' : 'hardcoded-date';
|
|
378
|
+
}
|
|
379
|
+
const ctx = (0, ast_helpers_1.getParentContext)(path);
|
|
380
|
+
const id = `${filePath}:${path.node.loc?.start.line}:${(0, string_utils_1.hashString)(text)}`;
|
|
381
|
+
// Dedup by location, not by text: identical labels rendered at
|
|
382
|
+
// different lines must all be extracted so every occurrence is
|
|
383
|
+
// injected, not just the first.
|
|
384
|
+
noteOffset(id, path.node.start);
|
|
385
|
+
if (!seenTexts.has(id)) {
|
|
386
|
+
seenTexts.add(id);
|
|
387
|
+
strings.push({
|
|
388
|
+
text,
|
|
389
|
+
file: filePath,
|
|
390
|
+
line: path.node.loc?.start.line || 0,
|
|
391
|
+
column: path.node.loc?.start.column || 0,
|
|
392
|
+
context,
|
|
393
|
+
id,
|
|
394
|
+
parentContext: ctx?.parentContext,
|
|
395
|
+
siblingGroupId: ctx?.siblingGroupId,
|
|
396
|
+
detectedFormat: format ?? undefined,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
JSXAttribute(path) {
|
|
401
|
+
const name = path.node.name;
|
|
402
|
+
if (!ast_helpers_1.t.isJSXIdentifier(name))
|
|
403
|
+
return;
|
|
404
|
+
if (!types_1.JSX_TRANSLATABLE_ATTRS.includes(name.name))
|
|
405
|
+
return;
|
|
406
|
+
const value = path.node.value;
|
|
407
|
+
if (!ast_helpers_1.t.isStringLiteral(value))
|
|
408
|
+
return;
|
|
409
|
+
const text = value.value.trim();
|
|
410
|
+
if (!text || !(0, string_utils_1.isTranslatableText)(text) || (0, string_utils_1.containsPII)(text))
|
|
411
|
+
return;
|
|
412
|
+
if ((0, ast_helpers_1.isAlreadyTranslated)(path.node, path))
|
|
413
|
+
return;
|
|
414
|
+
const id = `${filePath}:${path.node.loc?.start.line}:${(0, string_utils_1.hashString)(text)}`;
|
|
415
|
+
noteOffset(id, path.node.start);
|
|
416
|
+
if (!seenTexts.has(id)) {
|
|
417
|
+
seenTexts.add(id);
|
|
418
|
+
strings.push({
|
|
419
|
+
text,
|
|
420
|
+
file: filePath,
|
|
421
|
+
line: path.node.loc?.start.line || 0,
|
|
422
|
+
column: path.node.loc?.start.column || 0,
|
|
423
|
+
context: 'jsx-attr',
|
|
424
|
+
id,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
ObjectProperty(path) {
|
|
429
|
+
if (filePath.endsWith('.jsx') || filePath.endsWith('.tsx'))
|
|
430
|
+
return;
|
|
431
|
+
if (!ast_helpers_1.t.isStringLiteral(path.node.value))
|
|
432
|
+
return;
|
|
433
|
+
if (isInJSXContext(path))
|
|
434
|
+
return;
|
|
435
|
+
if ((0, ast_helpers_1.isNonTranslatableContext)(path))
|
|
436
|
+
return;
|
|
437
|
+
const text = path.node.value.value.trim();
|
|
438
|
+
if (!(0, string_utils_1.matchesCurrencyPattern)(text) && !(0, string_utils_1.matchesDatePattern)(text) && !(0, ui_copy_1.isUiCopyLiteral)(path.node.value, path.node, text))
|
|
439
|
+
return;
|
|
440
|
+
const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(text);
|
|
441
|
+
const hasDate = (0, string_utils_1.matchesDatePattern)(text);
|
|
442
|
+
const isDateOrCurrencyValue = (hasCurrency || hasDate) && text.length >= 3 && !(0, string_utils_1.containsPII)(text);
|
|
443
|
+
const format = detectFormats ? (0, detect_format_1.detectFormat)(text) : null;
|
|
444
|
+
if (!(0, string_utils_1.isTranslatableText)(text) || (0, string_utils_1.containsPII)(text) || text.length < 3) {
|
|
445
|
+
if (!isDateOrCurrencyValue && !format)
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (format) {
|
|
449
|
+
warnings.push({
|
|
450
|
+
type: 'ambiguous-format',
|
|
451
|
+
file: filePath,
|
|
452
|
+
line: path.node.loc?.start.line,
|
|
453
|
+
message: `Detected ${format.class} format: "${text}". Will be replaced with Intl.* API call.`,
|
|
454
|
+
severity: 'warning',
|
|
455
|
+
dismissible: true,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
if (hasCurrency) {
|
|
460
|
+
warnings.push({
|
|
461
|
+
type: 'hardcoded-currency',
|
|
462
|
+
file: filePath,
|
|
463
|
+
line: path.node.loc?.start.line,
|
|
464
|
+
message: 'Hardcoded currency/number format detected. Use Intl.NumberFormat.',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (hasDate) {
|
|
468
|
+
warnings.push({
|
|
469
|
+
type: 'hardcoded-date',
|
|
470
|
+
file: filePath,
|
|
471
|
+
line: path.node.loc?.start.line,
|
|
472
|
+
message: 'Hardcoded date format detected. Use Intl.DateTimeFormat.',
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if ((0, string_utils_1.isLikelyPlural)(text, [])) {
|
|
477
|
+
warnings.push({
|
|
478
|
+
type: 'hardcoded-plural',
|
|
479
|
+
file: filePath,
|
|
480
|
+
line: path.node.loc?.start.line,
|
|
481
|
+
message: 'Hardcoded plural structure detected. Consider using structured plural keys (key_one, key_other).',
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
if ((0, ast_helpers_1.isAlreadyTranslated)(path.node, path))
|
|
485
|
+
return;
|
|
486
|
+
const id = `${filePath}:${path.node.loc?.start.line}:${(0, string_utils_1.hashString)(text)}`;
|
|
487
|
+
let context = 'string-literal';
|
|
488
|
+
if (format) {
|
|
489
|
+
context = 'format-string';
|
|
490
|
+
}
|
|
491
|
+
else if (isDateOrCurrencyValue && !(0, string_utils_1.isTranslatableText)(text)) {
|
|
492
|
+
context = hasCurrency ? 'hardcoded-currency' : 'hardcoded-date';
|
|
493
|
+
}
|
|
494
|
+
noteOffset(id, path.node.value.start);
|
|
495
|
+
if (!seenTexts.has(id)) {
|
|
496
|
+
seenTexts.add(id);
|
|
497
|
+
strings.push({
|
|
498
|
+
text,
|
|
499
|
+
file: filePath,
|
|
500
|
+
line: path.node.loc?.start.line || 0,
|
|
501
|
+
column: path.node.loc?.start.column || 0,
|
|
502
|
+
context,
|
|
503
|
+
id,
|
|
504
|
+
detectedFormat: format ?? undefined,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
StringLiteral(path) {
|
|
509
|
+
processExtractedText(path.node.value, path);
|
|
510
|
+
},
|
|
511
|
+
TemplateLiteral(path) {
|
|
512
|
+
const variables = [];
|
|
513
|
+
const text = (0, ast_helpers_1.extractExpressionContent)(path.node, variables);
|
|
514
|
+
processExtractedText(text, path, variables);
|
|
515
|
+
},
|
|
516
|
+
BinaryExpression(path) {
|
|
517
|
+
if (path.node.operator === '+') {
|
|
518
|
+
const variables = [];
|
|
519
|
+
const text = (0, ast_helpers_1.extractExpressionContent)(path.node, variables);
|
|
520
|
+
if (text && text.length > 2)
|
|
521
|
+
processExtractedText(text, path, variables);
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
ConditionalExpression(path) {
|
|
525
|
+
const content = (0, ast_helpers_1.extractConditionalContent)(path.node);
|
|
526
|
+
if (content.consequent)
|
|
527
|
+
processExtractedText(content.consequent, path.get('consequent'));
|
|
528
|
+
if (content.alternate)
|
|
529
|
+
processExtractedText(content.alternate, path.get('alternate'));
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
throw new Error(`Failed to parse ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
535
|
+
}
|
|
536
|
+
for (const s of strings) {
|
|
537
|
+
const offsets = offsetsById.get(s.id);
|
|
538
|
+
if (offsets)
|
|
539
|
+
s.offsets = offsets;
|
|
540
|
+
}
|
|
541
|
+
return { strings, warnings, notInjected: unwired };
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Extract strings from a Vue Single File Component (SFC).
|
|
545
|
+
* Splits the SFC into template and script sections, extracts strings
|
|
546
|
+
* from each using the appropriate extractor, and merges results.
|
|
547
|
+
*/
|
|
548
|
+
async function extractStringsFromVueSfc(content, filePath, detectFormats) {
|
|
549
|
+
const allStrings = [];
|
|
550
|
+
const allWarnings = [];
|
|
551
|
+
const seenSources = new Set();
|
|
552
|
+
const templateMatch = (0, string_utils_1.extractBalancedTag)(content, 'template', true);
|
|
553
|
+
const scriptMatch = content.match(/<script([^>]*)>([\s\S]*?)<\/script>/);
|
|
554
|
+
// Extract from template section
|
|
555
|
+
if (templateMatch) {
|
|
556
|
+
const templateContent = templateMatch.body;
|
|
557
|
+
const lines = templateContent.split('\n');
|
|
558
|
+
const templateResult = extractVueTemplateStrings(lines, filePath, seenSources, detectFormats);
|
|
559
|
+
allStrings.push(...templateResult.strings);
|
|
560
|
+
allWarnings.push(...templateResult.warnings);
|
|
561
|
+
}
|
|
562
|
+
// Extract from script section
|
|
563
|
+
if (scriptMatch) {
|
|
564
|
+
const scriptAttrs = scriptMatch[1];
|
|
565
|
+
const scriptContent = scriptMatch[2];
|
|
566
|
+
const hasTs = /\blang\s*=\s*["'](?:ts|typescript)["']/.test(scriptAttrs);
|
|
567
|
+
const virtualPath = filePath.replace(/\.vue$/, hasTs ? '.ts' : '.js');
|
|
568
|
+
// Wrap script content in a component function so the component guard
|
|
569
|
+
// correctly identifies top-level declarations as component-scoped.
|
|
570
|
+
const result = await extractStringsFromFile(scriptContent, virtualPath, detectFormats, { relaxComponentGuard: true });
|
|
571
|
+
for (const s of result.strings) {
|
|
572
|
+
const vueId = s.id.replace(virtualPath, filePath);
|
|
573
|
+
if (!seenSources.has(vueId)) {
|
|
574
|
+
seenSources.add(vueId);
|
|
575
|
+
allStrings.push({ ...s, file: filePath, id: vueId });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
allWarnings.push(...result.warnings);
|
|
579
|
+
}
|
|
580
|
+
return { strings: allStrings, warnings: allWarnings };
|
|
581
|
+
}
|
|
582
|
+
const HTML_TEXT_RE = /<([a-zA-Z][a-zA-Z0-9]*)(?:\s[^>]*)?>([^<]+?)<\/\1>/gs;
|
|
583
|
+
const HTML_ATTR_RE = /\b(alt|title|placeholder|aria-label|aria-description)\s*=\s*"([^"]*?)"/g;
|
|
584
|
+
const VUE_INTERPOLATION_SQ_RE = /{{\s*'([^']+)'\s*}}/g;
|
|
585
|
+
const VUE_INTERPOLATION_DQ_RE = /{{\s*"([^"]+)"\s*}}/g;
|
|
586
|
+
const VUE_MUSTACHE_RE = /{{\s*([^}]+?)\s*}}/g;
|
|
587
|
+
const SKIP_TAGS = new Set(['script', 'style', 'noscript', 'code', 'pre', 'svg']);
|
|
588
|
+
function extractVueTemplateStrings(lines, filePath, seenSources, detectFormats) {
|
|
589
|
+
const strings = [];
|
|
590
|
+
const warnings = [];
|
|
591
|
+
const addString = (text, lineOffset, context) => {
|
|
592
|
+
const trimmed = text.trim();
|
|
593
|
+
if (!trimmed || trimmed.length < 2)
|
|
594
|
+
return;
|
|
595
|
+
if (!(0, string_utils_1.isTranslatableText)(trimmed))
|
|
596
|
+
return;
|
|
597
|
+
if ((0, string_utils_1.containsPII)(trimmed))
|
|
598
|
+
return;
|
|
599
|
+
const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(trimmed);
|
|
600
|
+
const hasDate = (0, string_utils_1.matchesDatePattern)(trimmed);
|
|
601
|
+
const isDateOrCurrencyValue = (hasCurrency || hasDate) && trimmed.length >= 3;
|
|
602
|
+
const format = detectFormats ? (0, detect_format_1.detectFormat)(trimmed) : null;
|
|
603
|
+
if (!(0, string_utils_1.isTranslatableText)(trimmed) && !isDateOrCurrencyValue && !format)
|
|
604
|
+
return;
|
|
605
|
+
if (format) {
|
|
606
|
+
warnings.push({
|
|
607
|
+
type: 'ambiguous-format',
|
|
608
|
+
file: filePath,
|
|
609
|
+
line: lineOffset,
|
|
610
|
+
message: `Detected ${format.class} format: "${trimmed}". Will be replaced with Intl.* API call.`,
|
|
611
|
+
severity: 'warning',
|
|
612
|
+
dismissible: true,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
if (hasCurrency) {
|
|
617
|
+
warnings.push({
|
|
618
|
+
type: 'hardcoded-currency',
|
|
619
|
+
file: filePath,
|
|
620
|
+
line: lineOffset,
|
|
621
|
+
message: 'Hardcoded currency/number format detected. Use Intl.NumberFormat.',
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
if (hasDate) {
|
|
625
|
+
warnings.push({
|
|
626
|
+
type: 'hardcoded-date',
|
|
627
|
+
file: filePath,
|
|
628
|
+
line: lineOffset,
|
|
629
|
+
message: 'Hardcoded date format detected. Use Intl.DateTimeFormat.',
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const contentKey = `${filePath}:${(0, string_utils_1.hashString)(trimmed)}`;
|
|
634
|
+
if (seenSources.has(contentKey))
|
|
635
|
+
return;
|
|
636
|
+
seenSources.add(contentKey);
|
|
637
|
+
const id = `${filePath}:${lineOffset}:${(0, string_utils_1.hashString)(trimmed)}`;
|
|
638
|
+
let effectiveContext = context;
|
|
639
|
+
if (format) {
|
|
640
|
+
effectiveContext = 'format-string';
|
|
641
|
+
}
|
|
642
|
+
else if (isDateOrCurrencyValue && !(0, string_utils_1.isTranslatableText)(trimmed)) {
|
|
643
|
+
effectiveContext = hasCurrency ? 'hardcoded-currency' : 'hardcoded-date';
|
|
644
|
+
}
|
|
645
|
+
strings.push({ text: trimmed, file: filePath, line: lineOffset, column: 0, context: effectiveContext, id, detectedFormat: format ?? undefined });
|
|
646
|
+
};
|
|
647
|
+
const content = lines.join('\n');
|
|
648
|
+
let match;
|
|
649
|
+
HTML_TEXT_RE.lastIndex = 0;
|
|
650
|
+
while ((match = HTML_TEXT_RE.exec(content)) !== null) {
|
|
651
|
+
const tagName = match[1].toLowerCase();
|
|
652
|
+
if (SKIP_TAGS.has(tagName))
|
|
653
|
+
continue;
|
|
654
|
+
// Normalize {{ var }} interpolations to {var} so translators see a
|
|
655
|
+
// placeholder instead of raw Mustache syntax.
|
|
656
|
+
const normalized = match[2].replace(VUE_MUSTACHE_RE, (_, name) => `{${name.trim()}}`);
|
|
657
|
+
const text = normalized.trim();
|
|
658
|
+
if (text) {
|
|
659
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
660
|
+
addString(text, line, 'jsx-text');
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
HTML_ATTR_RE.lastIndex = 0;
|
|
664
|
+
while ((match = HTML_ATTR_RE.exec(content)) !== null) {
|
|
665
|
+
const text = match[2].trim();
|
|
666
|
+
if (text) {
|
|
667
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
668
|
+
addString(text, line, 'jsx-attr');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
VUE_INTERPOLATION_SQ_RE.lastIndex = 0;
|
|
672
|
+
while ((match = VUE_INTERPOLATION_SQ_RE.exec(content)) !== null) {
|
|
673
|
+
const text = match[1].trim();
|
|
674
|
+
if (text) {
|
|
675
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
676
|
+
addString(text, line, 'jsx-text');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
VUE_INTERPOLATION_DQ_RE.lastIndex = 0;
|
|
680
|
+
while ((match = VUE_INTERPOLATION_DQ_RE.exec(content)) !== null) {
|
|
681
|
+
const text = match[1].trim();
|
|
682
|
+
if (text) {
|
|
683
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
684
|
+
addString(text, line, 'jsx-text');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return { strings, warnings };
|
|
688
|
+
}
|
|
689
|
+
function isInJSXContext(path) {
|
|
690
|
+
let current = path.parentPath;
|
|
691
|
+
while (current) {
|
|
692
|
+
if (current.isJSXElement() ||
|
|
693
|
+
current.isJSXFragment() ||
|
|
694
|
+
current.isJSXExpressionContainer() ||
|
|
695
|
+
current.isJSXAttribute()) {
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
current = current.parentPath;
|
|
699
|
+
}
|
|
700
|
+
return false;
|
|
701
|
+
}
|