champollion 0.3.3
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 +133 -0
- package/README.md +387 -0
- package/bin/cli.js +278 -0
- package/index.js +135 -0
- package/lib/api-key.js +127 -0
- package/lib/autofix.js +432 -0
- package/lib/bridge/method_bridge.py +430 -0
- package/lib/card-source-resolution.mjs +284 -0
- package/lib/cards/cache.js +169 -0
- package/lib/cards/env.js +82 -0
- package/lib/cards/fetch-card-child.js +38 -0
- package/lib/cards/reader.js +435 -0
- package/lib/cards/refresh.js +111 -0
- package/lib/cards/remote.js +387 -0
- package/lib/cldf-export.mjs +540 -0
- package/lib/cldf-terms.mjs +62 -0
- package/lib/command-help.js +790 -0
- package/lib/commands/audit.js +49 -0
- package/lib/commands/card.js +454 -0
- package/lib/commands/doctor.js +559 -0
- package/lib/commands/fonts.js +489 -0
- package/lib/commands/help.js +91 -0
- package/lib/commands/init.js +1259 -0
- package/lib/commands/integrity.js +148 -0
- package/lib/commands/leaderboard.js +478 -0
- package/lib/commands/lint.js +30 -0
- package/lib/commands/models.js +177 -0
- package/lib/commands/plugin.js +103 -0
- package/lib/commands/provenance.js +45 -0
- package/lib/commands/recommend.js +75 -0
- package/lib/commands/register-corpus.js +678 -0
- package/lib/commands/repair-script.js +42 -0
- package/lib/commands/seal-corpus.js +355 -0
- package/lib/commands/seo.js +72 -0
- package/lib/commands/serve.js +147 -0
- package/lib/commands/status.js +265 -0
- package/lib/commands/submit.js +332 -0
- package/lib/commands/sync.js +89 -0
- package/lib/commands/tm.js +573 -0
- package/lib/commands/verify.js +39 -0
- package/lib/commands/watch.js +20 -0
- package/lib/commands/wrap.js +138 -0
- package/lib/commands/xliff.js +327 -0
- package/lib/commercial-eligibility.js +235 -0
- package/lib/concurrent.js +87 -0
- package/lib/config.js +523 -0
- package/lib/contamination-lane.js +76 -0
- package/lib/content-sync.js +731 -0
- package/lib/content.js +733 -0
- package/lib/corpus-registration.mjs +608 -0
- package/lib/cost-report.js +346 -0
- package/lib/diff.js +155 -0
- package/lib/docusaurus-sync.js +1256 -0
- package/lib/flatten.js +55 -0
- package/lib/format.js +954 -0
- package/lib/hash.js +159 -0
- package/lib/icu.js +473 -0
- package/lib/integrity.js +689 -0
- package/lib/license-gate.mjs +478 -0
- package/lib/license-identify.mjs +229 -0
- package/lib/lint.js +629 -0
- package/lib/method-manifest.js +60 -0
- package/lib/methods/anthropic.js +140 -0
- package/lib/methods/apertium.js +163 -0
- package/lib/methods/api.js +316 -0
- package/lib/methods/base.js +184 -0
- package/lib/methods/content-separator.js +45 -0
- package/lib/methods/deepl.js +426 -0
- package/lib/methods/direct-llm.js +586 -0
- package/lib/methods/external.js +332 -0
- package/lib/methods/fetch-with-retry.js +124 -0
- package/lib/methods/gemini.js +147 -0
- package/lib/methods/google-translate.js +402 -0
- package/lib/methods/http-utils.js +122 -0
- package/lib/methods/libretranslate.js +314 -0
- package/lib/methods/llm-coached.js +670 -0
- package/lib/methods/llm.js +592 -0
- package/lib/methods/local.js +76 -0
- package/lib/methods/microsoft-translator.js +331 -0
- package/lib/methods/openai.js +131 -0
- package/lib/methods/openrouter-client.js +327 -0
- package/lib/methods/openrouter-pricing.js +156 -0
- package/lib/methods/provider-env.js +115 -0
- package/lib/methods/provider-pricing.js +310 -0
- package/lib/methods/tilde.js +150 -0
- package/lib/methods/translated.js +229 -0
- package/lib/methods/translation-error.js +80 -0
- package/lib/models.js +258 -0
- package/lib/no-translate.js +233 -0
- package/lib/output.js +238 -0
- package/lib/pairs.js +547 -0
- package/lib/plugins.js +447 -0
- package/lib/provenance.js +323 -0
- package/lib/recommend.js +648 -0
- package/lib/registers.js +1185 -0
- package/lib/repair-script.js +266 -0
- package/lib/scripts.js +994 -0
- package/lib/seal.mjs +464 -0
- package/lib/sealed-qualifier.mjs +211 -0
- package/lib/security.js +59 -0
- package/lib/segment.js +369 -0
- package/lib/seo.js +275 -0
- package/lib/serve.js +854 -0
- package/lib/string-classify.js +85 -0
- package/lib/submit.mjs +344 -0
- package/lib/sync.js +969 -0
- package/lib/tags/bcp47.js +202 -0
- package/lib/tags/resolve.js +314 -0
- package/lib/terminology.js +111 -0
- package/lib/tm-seed.js +294 -0
- package/lib/tm.js +515 -0
- package/lib/translate-pair.js +197 -0
- package/lib/translate.js +203 -0
- package/lib/types.js +230 -0
- package/lib/validate.js +510 -0
- package/lib/verify.js +451 -0
- package/lib/watch.js +145 -0
- package/lib/xliff.js +184 -0
- package/package.json +93 -0
- package/shared/ATTRIBUTION.md +145 -0
- package/shared/CORPORA-CARDS.md +288 -0
- package/shared/DATA-SOVEREIGNTY.md +500 -0
- package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
- package/shared/card-lint-baseline.json +3189 -0
- package/shared/cards-fallback.json +1 -0
- package/shared/catalogue/card-config.json +6091 -0
- package/shared/catalogue/external-results.json +3888 -0
- package/shared/catalogue/gender-guidance.json +1038 -0
- package/shared/catalogue/method-coverage.json +1751 -0
- package/shared/catalogue/metric-coverage.json +170 -0
- package/shared/catalogue/metric-reliability.json +1 -0
- package/shared/catalogue/register-presets.json +3180 -0
- package/shared/catalogue/vitality-scales.json +55 -0
- package/shared/cldr-index.json +1115 -0
- package/shared/code-bridge.json +253 -0
- package/shared/corpora-cards-v1-reference.md +281 -0
- package/shared/curated-dictionary-flags.json +35 -0
- package/shared/curated-endonyms.json +35 -0
- package/shared/curated-fsts.json +51 -0
- package/shared/curated-orthography-conventions.json +26 -0
- package/shared/curated-sil-resources.json +374 -0
- package/shared/curated-tools.json +41 -0
- package/shared/docent/corpus.json +11333 -0
- package/shared/docent/faq.en.json +564 -0
- package/shared/docent/register-blocks.json +60 -0
- package/shared/docent/system-prompt.md +144 -0
- package/shared/domain-taxonomy.json +35 -0
- package/shared/explainers/glossary.json +2975 -0
- package/shared/explainers/tc-features.json +20112 -0
- package/shared/explainers/term-watchlist.json +147 -0
- package/shared/human-services.json +59 -0
- package/shared/license-corrections.json +261 -0
- package/shared/license-evidence.json +13452 -0
- package/shared/licenses.json +6781 -0
- package/shared/method-registry.json +236 -0
- package/shared/metric-registry.json +620 -0
- package/shared/model-aliases.json +7 -0
- package/shared/schemas/champollion-plugin.schema.json +206 -0
- package/shared/schemas/corpora-card.schema.json +957 -0
- package/shared/schemas/domain-taxonomy.schema.json +64 -0
- package/shared/schemas/external-results.schema.json +314 -0
- package/shared/schemas/human-services.schema.json +90 -0
- package/shared/schemas/language-card.schema.json +1308 -0
- package/shared/schemas/licenses.schema.json +155 -0
- package/shared/schemas/method-card.schema.json +412 -0
- package/shared/schemas/method-registry.schema.json +85 -0
- package/shared/schemas/metric-registry.schema.json +96 -0
- package/shared/schemas/metric-reliability.schema.json +178 -0
- package/shared/schemas/model-aliases.schema.json +27 -0
- package/shared/schemas/source-snapshot.schema.json +96 -0
package/lib/autofix.js
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autofix — the `wrap` command.
|
|
3
|
+
*
|
|
4
|
+
* Scans source files for hardcoded user-facing strings and wraps them
|
|
5
|
+
* in the project's i18n translation function (e.g., t("key")).
|
|
6
|
+
*
|
|
7
|
+
* SIX SAFETY GATES:
|
|
8
|
+
* 1. Git-clean gate — refuses unless `git status --porcelain` is empty
|
|
9
|
+
* 2. Dry-run first — `wrap --dry` shows diffs without writing
|
|
10
|
+
* 3. Atomic backup — .champollion-backup/ before any write
|
|
11
|
+
* 4. Conservative matching — fix obvious cases, flag ambiguous for human review
|
|
12
|
+
* 5. One-undo — `wrap --undo` restores from backup
|
|
13
|
+
* 6. Diff output — prints git-style diff of every change
|
|
14
|
+
*
|
|
15
|
+
* KEY GENERATION:
|
|
16
|
+
* "Welcome to my portfolio" → t("general.welcome_to_my_portfolio")
|
|
17
|
+
* "Get in Touch" → t("general.get_in_touch")
|
|
18
|
+
* Alt text stays in its context: alt="My photo" → alt={t("general.my_photo")}
|
|
19
|
+
*
|
|
20
|
+
* Zero external dependencies.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { execSync } from 'node:child_process';
|
|
26
|
+
import { isNonTranslatableString } from './string-classify.js';
|
|
27
|
+
|
|
28
|
+
// -----------------------------------------------------------------
|
|
29
|
+
// Key generation
|
|
30
|
+
// -----------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate a translation key from a hardcoded string.
|
|
34
|
+
*
|
|
35
|
+
* Strategy: prefix with "general." namespace, then snake_case the text.
|
|
36
|
+
* Truncates at 5 words to keep keys manageable.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} text - The hardcoded text
|
|
39
|
+
* @param {string} namespace - Key namespace (default: 'general')
|
|
40
|
+
* @returns {string} Generated key like "general.welcome_to_my"
|
|
41
|
+
*/
|
|
42
|
+
function generateKey(text, namespace = 'general') {
|
|
43
|
+
const words = text
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^\w\s]/g, '') // Remove punctuation
|
|
46
|
+
.trim()
|
|
47
|
+
.split(/\s+/) // Split on whitespace
|
|
48
|
+
.filter(w => w.length > 0)
|
|
49
|
+
.slice(0, 5); // Max 5 words
|
|
50
|
+
|
|
51
|
+
if (words.length === 0) return null;
|
|
52
|
+
|
|
53
|
+
const slug = words.join('_');
|
|
54
|
+
return `${namespace}.${slug}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// -----------------------------------------------------------------
|
|
58
|
+
// Replacement logic
|
|
59
|
+
// -----------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generate a replacement for a JSX text node.
|
|
63
|
+
*
|
|
64
|
+
* "Welcome to my portfolio" → {t("general.welcome_to_my_portfolio")}
|
|
65
|
+
*
|
|
66
|
+
* @param {string} original - The original match (e.g., ">Text<")
|
|
67
|
+
* @param {string} text - The extracted text
|
|
68
|
+
* @param {string} key - The generated translation key
|
|
69
|
+
* @param {string} framework - Framework name for t() syntax
|
|
70
|
+
* @returns {string} The replacement string
|
|
71
|
+
*/
|
|
72
|
+
function replaceJsxText(original, text, key, framework) {
|
|
73
|
+
if (framework === 'Hugo') {
|
|
74
|
+
// >Text< → >{{ i18n "key" }}<
|
|
75
|
+
return original.replace(text, `{{ i18n "${key}" }}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// React/Vue: >Text< → >{t("key")}<
|
|
79
|
+
return original.replace(text, `{t("${key}")}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generate a replacement for a translatable attribute.
|
|
84
|
+
*
|
|
85
|
+
* placeholder="Search" → placeholder={t("general.search")}
|
|
86
|
+
*
|
|
87
|
+
* @param {string} attr - The attribute name
|
|
88
|
+
* @param {string} value - The current value
|
|
89
|
+
* @param {string} key - The generated translation key
|
|
90
|
+
* @param {string} framework - Framework name
|
|
91
|
+
* @returns {{ from: string, to: string }} Before/after for the replacement
|
|
92
|
+
*/
|
|
93
|
+
function replaceAttribute(attr, value, key, framework) {
|
|
94
|
+
if (framework === 'Hugo') {
|
|
95
|
+
return {
|
|
96
|
+
from: `${attr}="${value}"`,
|
|
97
|
+
to: `${attr}="{{ i18n "${key}" }}"`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
from: `${attr}="${value}"`,
|
|
103
|
+
to: `${attr}={t("${key}")}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// -----------------------------------------------------------------
|
|
108
|
+
// Safety gates
|
|
109
|
+
// -----------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Gate 1: Check if git working tree is clean.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} cwd - Working directory
|
|
115
|
+
* @returns {{ clean: boolean, status: string }}
|
|
116
|
+
*/
|
|
117
|
+
function checkGitClean(cwd) {
|
|
118
|
+
try {
|
|
119
|
+
const status = execSync('git status --porcelain', {
|
|
120
|
+
cwd,
|
|
121
|
+
encoding: 'utf-8',
|
|
122
|
+
timeout: 5000,
|
|
123
|
+
}).trim();
|
|
124
|
+
|
|
125
|
+
return { clean: status.length === 0, status };
|
|
126
|
+
} catch (_) {
|
|
127
|
+
// Not a git repo — allow but warn
|
|
128
|
+
return { clean: true, status: '(not a git repository)' };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Gate 3: Create a backup of files before modification.
|
|
134
|
+
*
|
|
135
|
+
* @param {string[]} filePaths - Absolute paths to back up
|
|
136
|
+
* @param {string} cwd - Working directory
|
|
137
|
+
* @returns {string} Backup directory path
|
|
138
|
+
*/
|
|
139
|
+
function createBackup(filePaths, cwd) {
|
|
140
|
+
const backupDir = path.join(cwd, '.champollion-backup');
|
|
141
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
for (const filePath of filePaths) {
|
|
144
|
+
const relPath = path.relative(cwd, filePath);
|
|
145
|
+
const backupPath = path.join(backupDir, relPath);
|
|
146
|
+
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
147
|
+
fs.copyFileSync(filePath, backupPath);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return backupDir;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Gate 5: Restore files from backup.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} cwd - Working directory
|
|
157
|
+
* @returns {{ restored: number, errors: string[] }}
|
|
158
|
+
*/
|
|
159
|
+
function restoreFromBackup(cwd) {
|
|
160
|
+
const backupDir = path.join(cwd, '.champollion-backup');
|
|
161
|
+
if (!fs.existsSync(backupDir)) {
|
|
162
|
+
return { restored: 0, errors: ['No backup found at .champollion-backup/'] };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const errors = [];
|
|
166
|
+
let restored = 0;
|
|
167
|
+
|
|
168
|
+
function walkRestore(dir) {
|
|
169
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const fullPath = path.join(dir, entry.name);
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
walkRestore(fullPath);
|
|
174
|
+
} else {
|
|
175
|
+
const relPath = path.relative(backupDir, fullPath);
|
|
176
|
+
const targetPath = path.join(cwd, relPath);
|
|
177
|
+
try {
|
|
178
|
+
fs.copyFileSync(fullPath, targetPath);
|
|
179
|
+
restored++;
|
|
180
|
+
} catch (err) {
|
|
181
|
+
errors.push(`Failed to restore ${relPath}: ${err.message}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
walkRestore(backupDir);
|
|
188
|
+
return { restored, errors };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// -----------------------------------------------------------------
|
|
192
|
+
// Diff generation
|
|
193
|
+
// -----------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Gate 6: Generate a simple text diff between original and modified content.
|
|
197
|
+
*
|
|
198
|
+
* This is a positional diff (compare lines by index), not a true
|
|
199
|
+
* diff (LCS/Myers). Works correctly for autofix because all
|
|
200
|
+
* replacements are in-place string swaps that preserve line count.
|
|
201
|
+
* Would produce misleading output for insertions/deletions — but
|
|
202
|
+
* autofix never inserts or deletes lines.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} original - Original file content
|
|
205
|
+
* @param {string} modified - Modified file content
|
|
206
|
+
* @param {string} filePath - File path for the header
|
|
207
|
+
* @returns {string} Diff output
|
|
208
|
+
*/
|
|
209
|
+
function generateDiff(original, modified, filePath) {
|
|
210
|
+
if (original === modified) return '';
|
|
211
|
+
|
|
212
|
+
const origLines = original.split('\n');
|
|
213
|
+
const modLines = modified.split('\n');
|
|
214
|
+
const lines = [];
|
|
215
|
+
|
|
216
|
+
lines.push(`--- a/${filePath}`);
|
|
217
|
+
lines.push(`+++ b/${filePath}`);
|
|
218
|
+
|
|
219
|
+
for (let i = 0; i < Math.max(origLines.length, modLines.length); i++) {
|
|
220
|
+
const origLine = origLines[i];
|
|
221
|
+
const modLine = modLines[i];
|
|
222
|
+
|
|
223
|
+
if (origLine !== modLine) {
|
|
224
|
+
if (origLine !== undefined) lines.push(`- ${origLine}`);
|
|
225
|
+
if (modLine !== undefined) lines.push(`+ ${modLine}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return lines.join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// -----------------------------------------------------------------
|
|
233
|
+
// Main autofix runner
|
|
234
|
+
// -----------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Process a single source file for hardcoded strings.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} content - File content
|
|
240
|
+
* @param {string} frameworkName - Framework name
|
|
241
|
+
* @param {object} framework - Framework config (from lint.js)
|
|
242
|
+
* @param {number} minLength - Minimum string length
|
|
243
|
+
* @param {object} existingKeys - Already-existing locale keys (for dedup)
|
|
244
|
+
* @returns {{ modified: string, fixes: object[], ambiguous: object[] }}
|
|
245
|
+
*/
|
|
246
|
+
function processFile(content, frameworkName, framework, minLength, existingKeys = {}) {
|
|
247
|
+
const fixes = []; // Applied fixes
|
|
248
|
+
const ambiguous = []; // Flagged for human review
|
|
249
|
+
let modified = content;
|
|
250
|
+
|
|
251
|
+
const lines = content.split('\n');
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < lines.length; i++) {
|
|
254
|
+
const line = lines[i];
|
|
255
|
+
const trimmed = line.trim();
|
|
256
|
+
|
|
257
|
+
// Skip non-content lines (same logic as lint.js)
|
|
258
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;
|
|
259
|
+
if (/^\s*import\s/.test(trimmed)) continue;
|
|
260
|
+
if (/^\s*(?:export\s+)?(?:type|interface|enum)\s/.test(trimmed)) continue;
|
|
261
|
+
if (/^\s*console\.\w+\(/.test(trimmed)) continue;
|
|
262
|
+
|
|
263
|
+
// Check for JSX text nodes: >text here<
|
|
264
|
+
const textPattern = />([^<>{]+)</g;
|
|
265
|
+
let match;
|
|
266
|
+
while ((match = textPattern.exec(line)) !== null) {
|
|
267
|
+
const text = match[1].trim();
|
|
268
|
+
if (text.length < minLength) continue;
|
|
269
|
+
if (!shouldFixText(text)) continue;
|
|
270
|
+
|
|
271
|
+
const key = generateKey(text);
|
|
272
|
+
if (!key) continue;
|
|
273
|
+
|
|
274
|
+
// Gate 4: Conservative — skip ambiguous cases
|
|
275
|
+
if (isAmbiguous(text, line)) {
|
|
276
|
+
ambiguous.push({ line: i + 1, text, reason: 'Complex context — review manually' });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const fullMatch = match[0]; // >Text<
|
|
281
|
+
const replacement = replaceJsxText(fullMatch, text, key, frameworkName);
|
|
282
|
+
modified = modified.replace(fullMatch, replacement);
|
|
283
|
+
fixes.push({ line: i + 1, text, key, type: 'jsx-text' });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check for translatable attributes
|
|
287
|
+
for (const attr of (framework.translatableAttrs || [])) {
|
|
288
|
+
const attrPattern = new RegExp(`${attr}\\s*=\\s*"([^"]*)"`, 'g');
|
|
289
|
+
while ((match = attrPattern.exec(line)) !== null) {
|
|
290
|
+
const value = match[1].trim();
|
|
291
|
+
if (value.length < minLength) continue;
|
|
292
|
+
if (!shouldFixText(value)) continue;
|
|
293
|
+
|
|
294
|
+
const key = generateKey(value);
|
|
295
|
+
if (!key) continue;
|
|
296
|
+
|
|
297
|
+
const { from, to } = replaceAttribute(attr, match[1], key, frameworkName);
|
|
298
|
+
modified = modified.replace(from, to);
|
|
299
|
+
fixes.push({ line: i + 1, text: value, key, type: `attr:${attr}` });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { modified, fixes, ambiguous };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Check if a text string is a good candidate for auto-fix.
|
|
309
|
+
*
|
|
310
|
+
* Conservative: only fix obvious user-facing strings.
|
|
311
|
+
*
|
|
312
|
+
* @param {string} text - The string to evaluate
|
|
313
|
+
* @returns {boolean} true if the string should be wrapped in t()
|
|
314
|
+
*/
|
|
315
|
+
function shouldFixText(text) {
|
|
316
|
+
// Delegate to the shared core — same heuristics used by lint.js's
|
|
317
|
+
// shouldFlagString, but without the lint-specific extensions
|
|
318
|
+
// (file paths, emails, HTML entities, TypeScript types).
|
|
319
|
+
// Autofix is more conservative: it only wraps obvious user-facing text.
|
|
320
|
+
return !isNonTranslatableString(text, 2);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Gate 4: Determine if a string is too ambiguous for auto-fix.
|
|
325
|
+
*
|
|
326
|
+
* Ambiguous cases are flagged for human review rather than auto-fixed.
|
|
327
|
+
*
|
|
328
|
+
* @param {string} text - The extracted string
|
|
329
|
+
* @param {string} lineContext - The full line of source code for context
|
|
330
|
+
* @returns {boolean} true if the string is too ambiguous to auto-fix
|
|
331
|
+
*/
|
|
332
|
+
function isAmbiguous(text, lineContext) {
|
|
333
|
+
// String contains template expressions mixed with text
|
|
334
|
+
if (/\{[^}]+\}/.test(text) && /\w{3,}/.test(text)) return true;
|
|
335
|
+
// String contains HTML entities
|
|
336
|
+
if (/&\w+;/.test(text)) return true;
|
|
337
|
+
// String is inside a ternary or conditional
|
|
338
|
+
if (/\?\s*['"`]/.test(lineContext) || /:\s*['"`]/.test(lineContext)) return true;
|
|
339
|
+
// Very short (1-2 words) — might be an intentional label
|
|
340
|
+
if (text.split(/\s+/).length <= 1 && text.length < 8) return true;
|
|
341
|
+
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Add generated keys to locale files.
|
|
347
|
+
*
|
|
348
|
+
* @param {object[]} fixes - Array of { key, text } from processFile
|
|
349
|
+
* @param {string} localesDir - Path to locale files directory
|
|
350
|
+
* @param {string} sourceLocale - Source locale code (e.g., 'en')
|
|
351
|
+
* @param {string[]} targetLocales - Target locale codes
|
|
352
|
+
*/
|
|
353
|
+
function addKeysToLocales(fixes, localesDir, sourceLocale, targetLocales) {
|
|
354
|
+
if (fixes.length === 0) return;
|
|
355
|
+
|
|
356
|
+
// Build key-value map from fixes
|
|
357
|
+
const newKeys = {};
|
|
358
|
+
for (const fix of fixes) {
|
|
359
|
+
// Expand dot-notation key into nested object
|
|
360
|
+
const parts = fix.key.split('.');
|
|
361
|
+
let current = newKeys;
|
|
362
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
363
|
+
if (!current[parts[i]]) current[parts[i]] = {};
|
|
364
|
+
current = current[parts[i]];
|
|
365
|
+
}
|
|
366
|
+
current[parts[parts.length - 1]] = fix.text;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Update source locale file
|
|
370
|
+
const sourcePath = path.join(localesDir, `${sourceLocale}.json`);
|
|
371
|
+
if (fs.existsSync(sourcePath)) {
|
|
372
|
+
const existing = JSON.parse(fs.readFileSync(sourcePath, 'utf-8'));
|
|
373
|
+
const merged = deepMerge(existing, newKeys);
|
|
374
|
+
fs.writeFileSync(sourcePath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Add placeholder entries to target locale files
|
|
378
|
+
for (const locale of targetLocales) {
|
|
379
|
+
const targetPath = path.join(localesDir, `${locale}.json`);
|
|
380
|
+
if (fs.existsSync(targetPath)) {
|
|
381
|
+
const existing = JSON.parse(fs.readFileSync(targetPath, 'utf-8'));
|
|
382
|
+
// Use [EN] prefix for target locales as untranslated markers
|
|
383
|
+
const placeholders = {};
|
|
384
|
+
for (const fix of fixes) {
|
|
385
|
+
const parts = fix.key.split('.');
|
|
386
|
+
let current = placeholders;
|
|
387
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
388
|
+
if (!current[parts[i]]) current[parts[i]] = {};
|
|
389
|
+
current = current[parts[i]];
|
|
390
|
+
}
|
|
391
|
+
current[parts[parts.length - 1]] = `[EN] ${fix.text}`;
|
|
392
|
+
}
|
|
393
|
+
const merged = deepMerge(existing, placeholders);
|
|
394
|
+
fs.writeFileSync(targetPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Deep merge two objects (source into target), without overwriting existing keys.
|
|
401
|
+
*
|
|
402
|
+
* @param {object} target - Base object
|
|
403
|
+
* @param {object} source - Object to merge in (new keys only)
|
|
404
|
+
* @returns {object} New merged object
|
|
405
|
+
*/
|
|
406
|
+
function deepMerge(target, source) {
|
|
407
|
+
const result = { ...target };
|
|
408
|
+
for (const [key, value] of Object.entries(source)) {
|
|
409
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
410
|
+
result[key] = deepMerge(result[key] || {}, value);
|
|
411
|
+
} else if (!(key in result)) {
|
|
412
|
+
// Only add new keys, don't overwrite existing
|
|
413
|
+
result[key] = value;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export {
|
|
420
|
+
generateKey,
|
|
421
|
+
replaceJsxText,
|
|
422
|
+
replaceAttribute,
|
|
423
|
+
checkGitClean,
|
|
424
|
+
createBackup,
|
|
425
|
+
restoreFromBackup,
|
|
426
|
+
generateDiff,
|
|
427
|
+
processFile,
|
|
428
|
+
shouldFixText,
|
|
429
|
+
isAmbiguous,
|
|
430
|
+
addKeysToLocales,
|
|
431
|
+
deepMerge,
|
|
432
|
+
};
|