locai 2.3.0 → 2.5.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/README.md +5 -1
- package/dist/cli/helpers.d.ts +2 -0
- package/dist/cli/helpers.js +344 -0
- package/dist/cli.js +39 -519
- package/dist/commands/translator.d.ts +1 -1
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +26 -0
- package/dist/config/setup.d.ts +10 -0
- package/dist/config/setup.js +79 -0
- package/dist/core/orchestrator.d.ts +3 -0
- package/dist/core/orchestrator.js +27 -0
- package/dist/core/pipeline/steps/ContextEnrichmentStep.d.ts +9 -0
- package/dist/core/pipeline/steps/ContextEnrichmentStep.js +43 -0
- package/dist/services/language-processor.d.ts +22 -0
- package/dist/services/language-processor.js +239 -0
- package/dist/services/source-analyzer.d.ts +14 -0
- package/dist/services/source-analyzer.js +164 -0
- package/dist/services/translation-service.d.ts +1 -49
- package/dist/services/translation-service.js +9 -215
- package/dist/types/context.d.ts +13 -0
- package/dist/types/context.js +1 -0
- package/dist/types/index.d.ts +58 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/ui-manager.js +2 -2
- package/package.json +26 -17
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Project, Node } from "ts-morph";
|
|
2
|
+
import { log } from "../utils/logger.js";
|
|
3
|
+
import path from "path";
|
|
4
|
+
export class SourceCodeAnalyzer {
|
|
5
|
+
project;
|
|
6
|
+
keyUsageMap = new Map();
|
|
7
|
+
rootDir;
|
|
8
|
+
initPromise = null;
|
|
9
|
+
constructor(rootDir = process.cwd()) {
|
|
10
|
+
this.rootDir = rootDir;
|
|
11
|
+
// Initialize with default settings, but don't add files yet for performance
|
|
12
|
+
this.project = new Project({
|
|
13
|
+
skipAddingFilesFromTsConfig: true,
|
|
14
|
+
compilerOptions: {
|
|
15
|
+
allowJs: true,
|
|
16
|
+
jsx: 1, // Preserve
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
initialize(globPatterns = [
|
|
21
|
+
"src/**/*.{ts,tsx}",
|
|
22
|
+
"app/**/*.{ts,tsx}",
|
|
23
|
+
"components/**/*.{ts,tsx}",
|
|
24
|
+
]) {
|
|
25
|
+
if (this.initPromise)
|
|
26
|
+
return this.initPromise;
|
|
27
|
+
this.initPromise = (async () => {
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 0)); // Force async execution for race condition safety
|
|
29
|
+
try {
|
|
30
|
+
// Add source files
|
|
31
|
+
log(`Analyzing source code in ${this.rootDir}...`);
|
|
32
|
+
// Exclude node_modules explicitly just in case
|
|
33
|
+
this.project.addSourceFilesAtPaths(globPatterns);
|
|
34
|
+
const sourceFiles = this.project.getSourceFiles();
|
|
35
|
+
log(`Found ${sourceFiles.length} source files for context analysis.`);
|
|
36
|
+
for (const sourceFile of sourceFiles) {
|
|
37
|
+
this.processFile(sourceFile);
|
|
38
|
+
}
|
|
39
|
+
log(`Source analysis complete. Context extracted for ${this.keyUsageMap.size} keys.`);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.warn(`Error during source analysis: ${error.message}`);
|
|
43
|
+
// Don't throw to prevent blocking the whole app, just log
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
46
|
+
return this.initPromise;
|
|
47
|
+
}
|
|
48
|
+
async ensureInitialized() {
|
|
49
|
+
if (this.initPromise) {
|
|
50
|
+
await this.initPromise;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
processFile(sourceFile) {
|
|
54
|
+
sourceFile.forEachDescendant((node) => {
|
|
55
|
+
if (Node.isCallExpression(node)) {
|
|
56
|
+
this.processCallExpression(node, sourceFile.getFilePath());
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
processCallExpression(node, filePath) {
|
|
61
|
+
// Match t('key') or useTranslations().t('key') pattern
|
|
62
|
+
// This is a heuristic match
|
|
63
|
+
const expression = node.getExpression();
|
|
64
|
+
// Check if expression is accessible
|
|
65
|
+
if (!expression)
|
|
66
|
+
return;
|
|
67
|
+
const expressionText = expression.getText();
|
|
68
|
+
// Simple check for 't' function calls
|
|
69
|
+
if (expressionText.endsWith(".t") || expressionText === "t") {
|
|
70
|
+
const args = node.getArguments();
|
|
71
|
+
if (args.length > 0 && Node.isStringLiteral(args[0])) {
|
|
72
|
+
const key = args[0].getLiteralText();
|
|
73
|
+
this.extractContext(key, node, filePath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
extractContext(key, node, fullFilePath) {
|
|
78
|
+
const relativePath = path.relative(this.rootDir, fullFilePath);
|
|
79
|
+
const line = node.getStartLineNumber();
|
|
80
|
+
// Extract component name
|
|
81
|
+
let componentName = undefined;
|
|
82
|
+
// Use getAncestors() to find the parent safely
|
|
83
|
+
const componentNode = node
|
|
84
|
+
.getAncestors()
|
|
85
|
+
.find((n) => Node.isFunctionDeclaration(n) ||
|
|
86
|
+
Node.isClassDeclaration(n) ||
|
|
87
|
+
Node.isVariableDeclaration(n));
|
|
88
|
+
if (componentNode) {
|
|
89
|
+
if (Node.isFunctionDeclaration(componentNode) ||
|
|
90
|
+
Node.isClassDeclaration(componentNode)) {
|
|
91
|
+
componentName = componentNode.getName();
|
|
92
|
+
}
|
|
93
|
+
else if (Node.isVariableDeclaration(componentNode)) {
|
|
94
|
+
componentName = componentNode.getName();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Extract comments
|
|
98
|
+
// Look for leading comments on the statement
|
|
99
|
+
const statement = node
|
|
100
|
+
.getAncestors()
|
|
101
|
+
.find((n) => Node.isExpressionStatement(n) ||
|
|
102
|
+
Node.isVariableStatement(n) ||
|
|
103
|
+
Node.isJsxElement(n) ||
|
|
104
|
+
Node.isJsxExpression(n));
|
|
105
|
+
const comments = [];
|
|
106
|
+
const targetNode = statement || node;
|
|
107
|
+
const ranges = targetNode.getLeadingCommentRanges();
|
|
108
|
+
for (const range of ranges) {
|
|
109
|
+
comments.push(range
|
|
110
|
+
.getText()
|
|
111
|
+
.replace(/^\/\/\s*/, "")
|
|
112
|
+
.replace(/^\/\*\s*/, "")
|
|
113
|
+
.replace(/\s*\*\/$/, ""));
|
|
114
|
+
}
|
|
115
|
+
// Get props (simple heuristic for JSX)
|
|
116
|
+
const props = {};
|
|
117
|
+
const jsxElement = node
|
|
118
|
+
.getAncestors()
|
|
119
|
+
.find((n) => Node.isJsxOpeningElement(n) || Node.isJsxSelfClosingElement(n));
|
|
120
|
+
if (jsxElement &&
|
|
121
|
+
(Node.isJsxOpeningElement(jsxElement) || Node.isJsxSelfClosingElement(jsxElement))) {
|
|
122
|
+
jsxElement.getAttributes().forEach((attr) => {
|
|
123
|
+
if (Node.isJsxAttribute(attr)) {
|
|
124
|
+
// Safe name extraction
|
|
125
|
+
const nameNode = attr.getNameNode();
|
|
126
|
+
const name = nameNode ? nameNode.getText() : "unknown";
|
|
127
|
+
const initializer = attr.getInitializer();
|
|
128
|
+
let value = initializer ? initializer.getText() : "true";
|
|
129
|
+
// Strip quotes if string literal
|
|
130
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
131
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
132
|
+
value = value.substring(1, value.length - 1);
|
|
133
|
+
}
|
|
134
|
+
props[name] = value;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Code Snippet (surrounding lines)
|
|
139
|
+
// Check for node.getParent() to avoid error if node is root (unlikely for call expression)
|
|
140
|
+
const parent = node.getParent();
|
|
141
|
+
const snippet = parent ? parent.getText() : node.getText();
|
|
142
|
+
const context = {
|
|
143
|
+
filePath: relativePath,
|
|
144
|
+
line,
|
|
145
|
+
component: componentName,
|
|
146
|
+
usageSnippet: snippet.length > 200 ? snippet.substring(0, 200) + "..." : snippet,
|
|
147
|
+
comments: comments.length > 0 ? comments : undefined,
|
|
148
|
+
props: Object.keys(props).length > 0 ? props : undefined,
|
|
149
|
+
};
|
|
150
|
+
if (!this.keyUsageMap.has(key)) {
|
|
151
|
+
this.keyUsageMap.set(key, []);
|
|
152
|
+
}
|
|
153
|
+
this.keyUsageMap.get(key)?.push(context);
|
|
154
|
+
}
|
|
155
|
+
getContext(key) {
|
|
156
|
+
// Return duplicate functionality: prefer the first usage found or merge?
|
|
157
|
+
// For now return the first usage that has comments, or just the first usage.
|
|
158
|
+
const usages = this.keyUsageMap.get(key);
|
|
159
|
+
if (!usages || usages.length === 0)
|
|
160
|
+
return undefined;
|
|
161
|
+
// Prioritize usage with comments
|
|
162
|
+
return usages.find((u) => u.comments && u.comments.length > 0) || usages[0];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -1,60 +1,12 @@
|
|
|
1
1
|
import Orchestrator from "../core/orchestrator.js";
|
|
2
|
-
import { OrchestratorOptions } from "../core/orchestrator.js";
|
|
3
2
|
import StateManager from "../utils/state-manager.js";
|
|
4
|
-
|
|
5
|
-
source: string;
|
|
6
|
-
targets?: string[];
|
|
7
|
-
localesDir?: string;
|
|
8
|
-
debug?: boolean;
|
|
9
|
-
forceUpdate?: boolean;
|
|
10
|
-
syncOptions?: {
|
|
11
|
-
enabled?: boolean;
|
|
12
|
-
removeDeletedKeys?: boolean;
|
|
13
|
-
};
|
|
14
|
-
concurrencyLimit?: number;
|
|
15
|
-
saveReviewQueue?: boolean;
|
|
16
|
-
minConfidence?: number;
|
|
17
|
-
fileExtensions?: string[];
|
|
18
|
-
}
|
|
19
|
-
export interface GlobalStats {
|
|
20
|
-
total: number;
|
|
21
|
-
success: number;
|
|
22
|
-
failed: number;
|
|
23
|
-
skipped: number;
|
|
24
|
-
languages: Record<string, any>;
|
|
25
|
-
byCategory: Record<string, number>;
|
|
26
|
-
details: Record<string, {
|
|
27
|
-
totalConfidence: number;
|
|
28
|
-
samples: number;
|
|
29
|
-
}>;
|
|
30
|
-
error?: {
|
|
31
|
-
message: string;
|
|
32
|
-
time: string;
|
|
33
|
-
stack?: string;
|
|
34
|
-
};
|
|
35
|
-
orchestrators: any[];
|
|
36
|
-
endTime?: string;
|
|
37
|
-
totalDuration?: number;
|
|
38
|
-
totalTime: number;
|
|
39
|
-
startTime: string;
|
|
40
|
-
}
|
|
41
|
-
interface LanguageProcessResult {
|
|
42
|
-
status: any;
|
|
43
|
-
savedMessage?: string;
|
|
44
|
-
error?: string;
|
|
45
|
-
}
|
|
3
|
+
import { TranslationOptions, GlobalStats, LanguageProcessResult } from "../types/index.js";
|
|
46
4
|
/**
|
|
47
5
|
* Service for handling file translation operations.
|
|
48
6
|
* Orchestrates the translation process including state management,
|
|
49
7
|
* verification, and statistics collection.
|
|
50
8
|
*/
|
|
51
9
|
declare class TranslationService {
|
|
52
|
-
/**
|
|
53
|
-
* Check if text only contains placeholders and whitespace.
|
|
54
|
-
* @param {string} text - Text to check.
|
|
55
|
-
* @returns {boolean} - True if text only contains placeholders.
|
|
56
|
-
*/
|
|
57
|
-
isPlaceholderOnlyText(text: string): boolean;
|
|
58
10
|
/**
|
|
59
11
|
* Main translator function to process source file and create translations.
|
|
60
12
|
* @param {string} file - Path to source file.
|
|
@@ -8,26 +8,14 @@ import InputValidator from "../utils/input-validator.js";
|
|
|
8
8
|
import gracefulShutdown from "../utils/graceful-shutdown.js";
|
|
9
9
|
import uiManager from "../utils/ui-manager.js";
|
|
10
10
|
import statisticsManager from "../utils/statistics-manager.js";
|
|
11
|
+
import { LanguageProcessor } from "./language-processor.js";
|
|
12
|
+
import { SourceCodeAnalyzer } from "./source-analyzer.js";
|
|
11
13
|
/**
|
|
12
14
|
* Service for handling file translation operations.
|
|
13
15
|
* Orchestrates the translation process including state management,
|
|
14
16
|
* verification, and statistics collection.
|
|
15
17
|
*/
|
|
16
18
|
class TranslationService {
|
|
17
|
-
/**
|
|
18
|
-
* Check if text only contains placeholders and whitespace.
|
|
19
|
-
* @param {string} text - Text to check.
|
|
20
|
-
* @returns {boolean} - True if text only contains placeholders.
|
|
21
|
-
*/
|
|
22
|
-
isPlaceholderOnlyText(text) {
|
|
23
|
-
if (!text || typeof text !== "string") {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
// Remove all placeholders from the text
|
|
27
|
-
const placeholderRegex = /\{[^}]+\}|\$\{[^}]+\}|%[sd]/g;
|
|
28
|
-
const textWithoutPlaceholders = text.replace(placeholderRegex, "").trim();
|
|
29
|
-
return textWithoutPlaceholders.length === 0;
|
|
30
|
-
}
|
|
31
19
|
/**
|
|
32
20
|
* Main translator function to process source file and create translations.
|
|
33
21
|
* @param {string} file - Path to source file.
|
|
@@ -164,6 +152,10 @@ class TranslationService {
|
|
|
164
152
|
const languageConcurrency = options.concurrencyLimit || 3;
|
|
165
153
|
const targetLanguages = [...(options.targets || [])];
|
|
166
154
|
await uiManager.log(`Processing ${targetLanguages.length} languages with concurrency of ${languageConcurrency}`);
|
|
155
|
+
// Initialize shared SourceCodeAnalyzer
|
|
156
|
+
console.log("Initializing Locai Engine...");
|
|
157
|
+
const sharedAnalyzer = new SourceCodeAnalyzer();
|
|
158
|
+
sharedAnalyzer.initialize();
|
|
167
159
|
// Create shared orchestrators array to collect review queues
|
|
168
160
|
const orchestrators = [];
|
|
169
161
|
for (let i = 0; i < targetLanguages.length; i += languageConcurrency) {
|
|
@@ -180,6 +172,7 @@ class TranslationService {
|
|
|
180
172
|
...options,
|
|
181
173
|
concurrencyLimit: 1,
|
|
182
174
|
progressOptions,
|
|
175
|
+
sourceCodeAnalyzer: sharedAnalyzer,
|
|
183
176
|
});
|
|
184
177
|
orchestrators.push(orchestrator);
|
|
185
178
|
return this.processLanguage(targetLang, resolvedFile, flattenedSource, orchestrator, { ...options, progressOptions }, globalStats, comparison);
|
|
@@ -201,207 +194,7 @@ class TranslationService {
|
|
|
201
194
|
* @returns {Promise<LanguageProcessResult>} - Status and messages for this language.
|
|
202
195
|
*/
|
|
203
196
|
async processLanguage(targetLang, sourceFile, flattenedSource, orchestrator, options, globalStats, comparison) {
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
if (!targetLang || typeof targetLang !== "string") {
|
|
207
|
-
throw new Error("Invalid target language provided");
|
|
208
|
-
}
|
|
209
|
-
if (!sourceFile || typeof sourceFile !== "string") {
|
|
210
|
-
throw new Error("Invalid source file path provided");
|
|
211
|
-
}
|
|
212
|
-
if (!flattenedSource || typeof flattenedSource !== "object") {
|
|
213
|
-
throw new Error("Invalid flattened source data provided");
|
|
214
|
-
}
|
|
215
|
-
if (!orchestrator || typeof orchestrator.processTranslations !== "function") {
|
|
216
|
-
throw new Error("Invalid orchestrator instance provided");
|
|
217
|
-
}
|
|
218
|
-
if (!globalStats || typeof globalStats !== "object") {
|
|
219
|
-
throw new Error("Invalid global stats object provided");
|
|
220
|
-
}
|
|
221
|
-
const safeTargetLang = InputValidator.validateLanguageCode(targetLang, "target language");
|
|
222
|
-
await uiManager.log(`Starting translations for ${safeTargetLang}`);
|
|
223
|
-
let finalStatus = null;
|
|
224
|
-
let savedMessage = undefined;
|
|
225
|
-
statisticsManager.initLanguageStats(safeTargetLang);
|
|
226
|
-
const sourceDir = path.dirname(sourceFile);
|
|
227
|
-
const safeTargetFilename = `${safeTargetLang}.json`;
|
|
228
|
-
const targetPath = InputValidator.createSafeFilePath(sourceDir, safeTargetFilename);
|
|
229
|
-
let targetContent = {};
|
|
230
|
-
try {
|
|
231
|
-
targetContent = await FileManager.readJSON(targetPath);
|
|
232
|
-
if (!targetContent || typeof targetContent !== "object") {
|
|
233
|
-
console.warn(`Invalid content in ${targetPath}, using empty object`);
|
|
234
|
-
targetContent = {};
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
catch (err) {
|
|
238
|
-
if (err.code === "ENOENT") {
|
|
239
|
-
await uiManager.log(`\ud83c\udd95 Creating new translation file for ${safeTargetLang}`);
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
if (options.debug) {
|
|
243
|
-
console.warn(`Error reading ${targetPath}: ${err.message}, using empty object`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
targetContent = {};
|
|
247
|
-
}
|
|
248
|
-
let flattenedTarget = {};
|
|
249
|
-
try {
|
|
250
|
-
flattenedTarget = ObjectTransformer.flatten(targetContent);
|
|
251
|
-
if (!flattenedTarget || typeof flattenedTarget !== "object") {
|
|
252
|
-
flattenedTarget = {};
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
catch (err) {
|
|
256
|
-
console.warn(`Error flattening target content: ${err.message}, using empty object`);
|
|
257
|
-
flattenedTarget = {};
|
|
258
|
-
}
|
|
259
|
-
const missingKeys = [];
|
|
260
|
-
let hasPlaceholderOnlyChanges = false;
|
|
261
|
-
for (const [key, sourceText] of Object.entries(flattenedSource)) {
|
|
262
|
-
try {
|
|
263
|
-
InputValidator.validateKey(key, "translation key");
|
|
264
|
-
InputValidator.validateText(sourceText, "source text");
|
|
265
|
-
}
|
|
266
|
-
catch (keyError) {
|
|
267
|
-
await uiManager.log(`\u26a0\ufe0f Skipping invalid key/text: ${keyError.message}`);
|
|
268
|
-
globalStats.languages[safeTargetLang].failed++;
|
|
269
|
-
globalStats.failed++;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
globalStats.languages[safeTargetLang].processed++;
|
|
273
|
-
if (this.isPlaceholderOnlyText(sourceText)) {
|
|
274
|
-
const existingValue = flattenedTarget[key];
|
|
275
|
-
if (existingValue !== sourceText) {
|
|
276
|
-
flattenedTarget[key] = sourceText;
|
|
277
|
-
hasPlaceholderOnlyChanges = true;
|
|
278
|
-
if (options.debug) {
|
|
279
|
-
await uiManager.log(` Copying placeholder-only text: "${sourceText}"`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
globalStats.languages[safeTargetLang].skipped++;
|
|
283
|
-
globalStats.skipped++;
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
const isNewKey = comparison.newKeys.includes(key);
|
|
287
|
-
const isModifiedKey = comparison.modifiedKeys.includes(key);
|
|
288
|
-
const keyExistsInTarget = key in flattenedTarget;
|
|
289
|
-
if (keyExistsInTarget && !options.forceUpdate && !isModifiedKey) {
|
|
290
|
-
globalStats.languages[safeTargetLang].skipped++;
|
|
291
|
-
globalStats.skipped++;
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
missingKeys.push({
|
|
295
|
-
key,
|
|
296
|
-
text: sourceText,
|
|
297
|
-
targetLang: safeTargetLang,
|
|
298
|
-
existingTranslation: flattenedTarget[key],
|
|
299
|
-
isModified: isModifiedKey,
|
|
300
|
-
isNew: isNewKey,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
if (missingKeys.length === 0 && !hasPlaceholderOnlyChanges) {
|
|
304
|
-
await uiManager.log(`All translations exist for ${safeTargetLang}`);
|
|
305
|
-
globalStats.languages[safeTargetLang].timeMs = Date.now() - langStartTime;
|
|
306
|
-
return { status: { completed: 0, total: 0, language: safeTargetLang } };
|
|
307
|
-
}
|
|
308
|
-
if (missingKeys.length > 0) {
|
|
309
|
-
await uiManager.log(`Found ${missingKeys.length} missing translations for ${safeTargetLang}`);
|
|
310
|
-
}
|
|
311
|
-
let results = [];
|
|
312
|
-
if (missingKeys.length > 0) {
|
|
313
|
-
try {
|
|
314
|
-
results = await orchestrator.processTranslations(missingKeys);
|
|
315
|
-
if (!Array.isArray(results)) {
|
|
316
|
-
console.warn("Invalid results from orchestrator, using empty array");
|
|
317
|
-
results = [];
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
catch (err) {
|
|
321
|
-
console.error(`Error processing translations for ${safeTargetLang}: ${err.message}`);
|
|
322
|
-
results = [];
|
|
323
|
-
if (globalStats.languages[safeTargetLang]) {
|
|
324
|
-
globalStats.languages[safeTargetLang].failed += missingKeys.length;
|
|
325
|
-
globalStats.failed += missingKeys.length;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
if (hasPlaceholderOnlyChanges) {
|
|
331
|
-
await uiManager.log(`Found placeholder-only changes for ${safeTargetLang}`);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
try {
|
|
335
|
-
if (orchestrator?.progress &&
|
|
336
|
-
typeof orchestrator.progress.getStatus === "function") {
|
|
337
|
-
finalStatus = orchestrator.progress.getStatus();
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
catch (err) {
|
|
341
|
-
console.warn(`Error getting orchestrator status: ${err.message}`);
|
|
342
|
-
}
|
|
343
|
-
let validResults = [];
|
|
344
|
-
try {
|
|
345
|
-
validResults = results.filter((result) => result && result.success === true);
|
|
346
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
347
|
-
validResults = validResults.filter((result) => result.key &&
|
|
348
|
-
typeof result.key === "string" &&
|
|
349
|
-
result.translated &&
|
|
350
|
-
typeof result.translated === "string");
|
|
351
|
-
}
|
|
352
|
-
catch (err) {
|
|
353
|
-
console.warn(`Error filtering results: ${err.message}`);
|
|
354
|
-
validResults = [];
|
|
355
|
-
}
|
|
356
|
-
if (validResults.length > 0) {
|
|
357
|
-
validResults.forEach(({ key, translated }) => {
|
|
358
|
-
if (translated) {
|
|
359
|
-
flattenedTarget[key] = translated;
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
const hasChangesToSave = validResults.length > 0 || hasPlaceholderOnlyChanges;
|
|
364
|
-
if (hasChangesToSave) {
|
|
365
|
-
const unflattened = ObjectTransformer.unflatten(flattenedTarget);
|
|
366
|
-
await FileManager.writeJSON(targetPath, unflattened);
|
|
367
|
-
globalStats.total += validResults.length;
|
|
368
|
-
globalStats.success += validResults.length;
|
|
369
|
-
globalStats.failed += results.length - validResults.length;
|
|
370
|
-
globalStats.languages[safeTargetLang].added += validResults.length;
|
|
371
|
-
globalStats.languages[safeTargetLang].failed +=
|
|
372
|
-
results.length - validResults.length;
|
|
373
|
-
validResults.forEach((result) => {
|
|
374
|
-
if (result.context) {
|
|
375
|
-
const category = result.context.category || "general";
|
|
376
|
-
globalStats.byCategory[category] =
|
|
377
|
-
(globalStats.byCategory[category] || 0) + 1;
|
|
378
|
-
if (!globalStats.details[category]) {
|
|
379
|
-
globalStats.details[category] = {
|
|
380
|
-
totalConfidence: 0,
|
|
381
|
-
samples: 0,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
globalStats.details[category].totalConfidence +=
|
|
385
|
-
result.context.confidence || 0;
|
|
386
|
-
globalStats.details[category].samples++;
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
savedMessage = `Translations saved: ${safeTargetLang}.json`;
|
|
390
|
-
}
|
|
391
|
-
globalStats.languages[safeTargetLang].timeMs = Date.now() - langStartTime;
|
|
392
|
-
return { status: finalStatus, savedMessage };
|
|
393
|
-
}
|
|
394
|
-
catch (error) {
|
|
395
|
-
const safeError = error.message.includes("outside working directory")
|
|
396
|
-
? "Invalid file path detected"
|
|
397
|
-
: error.message;
|
|
398
|
-
await uiManager.log(`\n\u274c Error processing ${targetLang}: ${safeError}`);
|
|
399
|
-
if (globalStats.languages[targetLang]) {
|
|
400
|
-
globalStats.languages[targetLang].error = safeError;
|
|
401
|
-
globalStats.languages[targetLang].timeMs = Date.now() - langStartTime;
|
|
402
|
-
}
|
|
403
|
-
return { status: null, error: safeError };
|
|
404
|
-
}
|
|
197
|
+
return await LanguageProcessor.processLanguage(targetLang, sourceFile, flattenedSource, orchestrator, options, globalStats, comparison);
|
|
405
198
|
}
|
|
406
199
|
/**
|
|
407
200
|
* Finalize translation process and save state.
|
|
@@ -415,6 +208,7 @@ class TranslationService {
|
|
|
415
208
|
async finalizeTranslation(stateManager, projectRoot, currentState, globalStats, startTime, options) {
|
|
416
209
|
globalStats.endTime = new Date().toISOString();
|
|
417
210
|
globalStats.totalDuration = (Date.now() - startTime) / 1000;
|
|
211
|
+
globalStats.totalTime = globalStats.totalDuration;
|
|
418
212
|
await uiManager.displayGlobalSummary(globalStats, (options.targets || []).length);
|
|
419
213
|
// Save review queue if confidence scoring was enabled
|
|
420
214
|
if (options.saveReviewQueue || options.minConfidence !== undefined) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CodeContext {
|
|
2
|
+
filePath: string;
|
|
3
|
+
line: number;
|
|
4
|
+
component?: string;
|
|
5
|
+
usageSnippet?: string;
|
|
6
|
+
comments?: string[];
|
|
7
|
+
props?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
export interface SourceAnalyzerConfig {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
include?: string[];
|
|
12
|
+
exclude?: string[];
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { OrchestratorOptions } from "../core/orchestrator.js";
|
|
2
|
+
import { ContextConfig } from "../core/context-processor.js";
|
|
3
|
+
export interface TranslationOptions extends Omit<OrchestratorOptions, "context"> {
|
|
4
|
+
source: string;
|
|
5
|
+
targets?: string[];
|
|
6
|
+
localesDir?: string;
|
|
7
|
+
debug?: boolean;
|
|
8
|
+
forceUpdate?: boolean;
|
|
9
|
+
syncOptions?: {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
removeDeletedKeys?: boolean;
|
|
12
|
+
};
|
|
13
|
+
concurrencyLimit?: number;
|
|
14
|
+
saveReviewQueue?: boolean;
|
|
15
|
+
minConfidence?: number;
|
|
16
|
+
fileExtensions?: string[];
|
|
17
|
+
progressOptions?: any;
|
|
18
|
+
qualityChecks?: any;
|
|
19
|
+
styleGuide?: any;
|
|
20
|
+
context?: ContextConfig;
|
|
21
|
+
lengthControl?: any;
|
|
22
|
+
}
|
|
23
|
+
export interface GlobalStats {
|
|
24
|
+
total: number;
|
|
25
|
+
success: number;
|
|
26
|
+
failed: number;
|
|
27
|
+
skipped: number;
|
|
28
|
+
languages: Record<string, any>;
|
|
29
|
+
byCategory: Record<string, number>;
|
|
30
|
+
details: Record<string, {
|
|
31
|
+
totalConfidence: number;
|
|
32
|
+
samples: number;
|
|
33
|
+
}>;
|
|
34
|
+
error?: {
|
|
35
|
+
message: string;
|
|
36
|
+
time: string;
|
|
37
|
+
stack?: string;
|
|
38
|
+
};
|
|
39
|
+
orchestrators: any[];
|
|
40
|
+
endTime?: string;
|
|
41
|
+
totalDuration?: number;
|
|
42
|
+
totalTime: number;
|
|
43
|
+
startTime: string;
|
|
44
|
+
}
|
|
45
|
+
export interface MissingKey {
|
|
46
|
+
key: string;
|
|
47
|
+
text: string;
|
|
48
|
+
targetLang: string;
|
|
49
|
+
existingTranslation?: string;
|
|
50
|
+
isModified: boolean;
|
|
51
|
+
isNew: boolean;
|
|
52
|
+
issueDetails?: any;
|
|
53
|
+
}
|
|
54
|
+
export interface LanguageProcessResult {
|
|
55
|
+
status: any;
|
|
56
|
+
savedMessage?: string;
|
|
57
|
+
error?: string;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils/ui-manager.js
CHANGED
|
@@ -74,10 +74,10 @@ export class UIManager {
|
|
|
74
74
|
if (details && details.samples > 0) {
|
|
75
75
|
const avgConfidence = details.totalConfidence / details.samples;
|
|
76
76
|
const confidenceStr = `${(avgConfidence * 100).toFixed(1)}%`;
|
|
77
|
-
await this.log(
|
|
77
|
+
await this.log(`${category}: ${count} items (${confidenceStr} avg confidence)`);
|
|
78
78
|
}
|
|
79
79
|
else {
|
|
80
|
-
await this.log(
|
|
80
|
+
await this.log(`${category}: ${count} items`);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "locai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/core/orchestrator.js",
|
|
6
6
|
"types": "dist/core/orchestrator.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"start": "node dist/core/orchestrator.js",
|
|
17
17
|
"dev": "tsx src/core/orchestrator.js",
|
|
18
18
|
"build": "tsc",
|
|
19
|
-
"
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
20
|
"lint": "eslint .",
|
|
21
21
|
"lint:fix": "eslint . --fix",
|
|
22
22
|
"format": "prettier --write \"**/*.{js,json,md,jsx,ts,tsx,css}\"",
|
|
@@ -27,18 +27,26 @@
|
|
|
27
27
|
"prepublishOnly": "pnpm build"
|
|
28
28
|
},
|
|
29
29
|
"keywords": [
|
|
30
|
-
"translation",
|
|
31
|
-
"localization",
|
|
32
30
|
"i18n",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
31
|
+
"localization",
|
|
32
|
+
"translation",
|
|
33
|
+
"l10n",
|
|
34
|
+
"internationalization",
|
|
36
35
|
"automation",
|
|
36
|
+
"cli",
|
|
37
37
|
"llm",
|
|
38
|
+
"ai",
|
|
39
|
+
"artificial-intelligence",
|
|
38
40
|
"openai",
|
|
41
|
+
"anthropic",
|
|
42
|
+
"claude",
|
|
39
43
|
"gemini",
|
|
40
44
|
"deepseek",
|
|
41
|
-
"
|
|
45
|
+
"xai",
|
|
46
|
+
"vector-database",
|
|
47
|
+
"semantic-search",
|
|
48
|
+
"developer-tools",
|
|
49
|
+
"typescript"
|
|
42
50
|
],
|
|
43
51
|
"author": {
|
|
44
52
|
"name": "Ahmet Enes Dur",
|
|
@@ -64,8 +72,8 @@
|
|
|
64
72
|
"./translator": "./dist/commands/translator.js"
|
|
65
73
|
},
|
|
66
74
|
"dependencies": {
|
|
67
|
-
"axios": "1.13.
|
|
68
|
-
"c12": "^3.3.
|
|
75
|
+
"axios": "1.13.2",
|
|
76
|
+
"c12": "^3.3.3",
|
|
69
77
|
"commander": "^14.0.2",
|
|
70
78
|
"dot-properties": "^1.1.0",
|
|
71
79
|
"dotenv": "^17.2.3",
|
|
@@ -73,25 +81,26 @@
|
|
|
73
81
|
"gettext-parser": "^8.0.0",
|
|
74
82
|
"jiti": "^2.6.1",
|
|
75
83
|
"js-yaml": "^4.1.1",
|
|
76
|
-
"lru-cache": "^11.2.
|
|
84
|
+
"lru-cache": "^11.2.4",
|
|
77
85
|
"p-limit": "^7.2.0",
|
|
78
86
|
"prompts": "^2.4.2",
|
|
87
|
+
"ts-morph": "^27.0.2",
|
|
79
88
|
"vectra": "^0.11.1"
|
|
80
89
|
},
|
|
81
90
|
"devDependencies": {
|
|
82
|
-
"@eslint/js": "^9.39.
|
|
91
|
+
"@eslint/js": "^9.39.2",
|
|
83
92
|
"@types/js-yaml": "^4.0.9",
|
|
84
|
-
"@types/node": "^25.0.
|
|
93
|
+
"@types/node": "^25.0.3",
|
|
85
94
|
"@types/prompts": "^2.4.9",
|
|
86
|
-
"@vitest/coverage-v8": "^4.0.
|
|
87
|
-
"eslint": "^9.39.
|
|
95
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
96
|
+
"eslint": "^9.39.2",
|
|
88
97
|
"eslint-config-prettier": "^10.1.8",
|
|
89
98
|
"globals": "^16.5.0",
|
|
90
|
-
"prettier": "^3.
|
|
99
|
+
"prettier": "^3.7.4",
|
|
91
100
|
"std-mocks": "^2.0.0",
|
|
92
101
|
"tsx": "^4.21.0",
|
|
93
102
|
"typescript": "^5.9.3",
|
|
94
|
-
"vitest": "^4.0.
|
|
103
|
+
"vitest": "^4.0.16"
|
|
95
104
|
},
|
|
96
105
|
"files": [
|
|
97
106
|
"dist/"
|