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
package/dist/config/index.js
CHANGED
|
@@ -50,3 +50,29 @@ export async function loadConfig(cwd = process.cwd()) {
|
|
|
50
50
|
layers,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
+
export const validateEnvironment = (config) => {
|
|
54
|
+
const providerMap = {
|
|
55
|
+
openai: "OPENAI_API_KEY",
|
|
56
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
57
|
+
gemini: "GEMINI_API_KEY",
|
|
58
|
+
deepseek: "DEEPSEEK_API_KEY",
|
|
59
|
+
xai: "XAI_API_KEY",
|
|
60
|
+
dashscope: "DASHSCOPE_API_KEY",
|
|
61
|
+
};
|
|
62
|
+
// If a specific provider is configured, check its key
|
|
63
|
+
if (config?.apiProvider && config.apiProvider !== "auto") {
|
|
64
|
+
const requiredKey = providerMap[config.apiProvider];
|
|
65
|
+
if (requiredKey && !process.env[requiredKey]) {
|
|
66
|
+
console.warn(`\n\u26a0\ufe0f Warning: ${requiredKey} is missing for provider '${config.apiProvider}'`);
|
|
67
|
+
console.warn(` Please add it to your .env file or environment variables.\n`);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// If auto or no provider, check if ANY valid key exists
|
|
72
|
+
const hasAnyKey = Object.values(providerMap).some((key) => process.env[key]);
|
|
73
|
+
if (!hasAnyKey) {
|
|
74
|
+
console.warn("\n\u26a0\ufe0f Warning: No supported API keys found in environment.");
|
|
75
|
+
console.warn(" Supported keys: " + Object.values(providerMap).join(", "));
|
|
76
|
+
console.warn(" Please add at least one to your .env file.\n");
|
|
77
|
+
}
|
|
78
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load environment variables from .env files.
|
|
3
|
+
* Prioritizes .env.local over .env.
|
|
4
|
+
*/
|
|
5
|
+
export declare const loadEnvironmentVariables: () => Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Configure global components (logger, rate limiter, etc.).
|
|
8
|
+
* @param {any} config - Configuration object.
|
|
9
|
+
*/
|
|
10
|
+
export declare const configureComponents: (config: any) => void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { FileManager } from "../utils/file-manager.js";
|
|
5
|
+
import { getLogger } from "../utils/logger.js";
|
|
6
|
+
import rateLimiter from "../utils/rate-limiter.js";
|
|
7
|
+
/**
|
|
8
|
+
* Load environment variables from .env files.
|
|
9
|
+
* Prioritizes .env.local over .env.
|
|
10
|
+
*/
|
|
11
|
+
export const loadEnvironmentVariables = async () => {
|
|
12
|
+
// Load environment files in priority order
|
|
13
|
+
// First load .env (base defaults), then .env.local (local overrides)
|
|
14
|
+
// This ensures .env.local values take precedence over .env values
|
|
15
|
+
const envFiles = [
|
|
16
|
+
{ file: ".env", override: false },
|
|
17
|
+
{ file: ".env.local", override: true },
|
|
18
|
+
];
|
|
19
|
+
let loadedCount = 0;
|
|
20
|
+
for (const { file: envFile, override } of envFiles) {
|
|
21
|
+
const envPath = path.resolve(process.cwd(), envFile);
|
|
22
|
+
try {
|
|
23
|
+
await fs.access(envPath);
|
|
24
|
+
const result = dotenv.config({ path: envPath, override });
|
|
25
|
+
if (!result.error) {
|
|
26
|
+
loadedCount++;
|
|
27
|
+
// Only log in verbose/debug mode to reduce noise
|
|
28
|
+
if (process.env.VERBOSE || process.env.DEBUG) {
|
|
29
|
+
console.log(`Loaded environment variables from ${envFile}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
// ENOENT is expected when files don't exist - silently skip
|
|
35
|
+
if (error.code !== "ENOENT") {
|
|
36
|
+
console.warn(`Warning: Could not load ${envFile}: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Informative message only if verbose/debug and at least one file was loaded
|
|
41
|
+
if ((process.env.VERBOSE || process.env.DEBUG) && loadedCount > 0) {
|
|
42
|
+
console.log(`Environment: ${loadedCount} file(s) loaded`);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Configure global components (logger, rate limiter, etc.).
|
|
47
|
+
* @param {any} config - Configuration object.
|
|
48
|
+
*/
|
|
49
|
+
export const configureComponents = (config) => {
|
|
50
|
+
// Configure FileManager with config settings
|
|
51
|
+
if (config.fileOperations) {
|
|
52
|
+
FileManager.configure(config.fileOperations);
|
|
53
|
+
}
|
|
54
|
+
// Initialize logger with config
|
|
55
|
+
if (config.logging) {
|
|
56
|
+
const logger = getLogger(config.logging);
|
|
57
|
+
if (config.logging.verbose || config.debug) {
|
|
58
|
+
logger.info("Logger initialized", {
|
|
59
|
+
saveErrorLogs: config.logging.saveErrorLogs,
|
|
60
|
+
logDirectory: config.logging.logDirectory,
|
|
61
|
+
rotationEnabled: config.logging.logRotation?.enabled,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (config.rateLimiter) {
|
|
66
|
+
rateLimiter.updateConfig({
|
|
67
|
+
queueStrategy: config.rateLimiter.queueStrategy || "priority",
|
|
68
|
+
queueTimeout: config.rateLimiter.queueTimeout || 30000,
|
|
69
|
+
adaptiveThrottling: config.rateLimiter.adaptiveThrottling !== false,
|
|
70
|
+
providerLimits: config.rateLimiter.providerLimits,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (config.advanced?.debug) {
|
|
74
|
+
process.env.DEBUG = "true";
|
|
75
|
+
}
|
|
76
|
+
if (config.logging?.verbose) {
|
|
77
|
+
process.env.VERBOSE = "true";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
@@ -6,8 +6,10 @@ import GlossaryManager from "../utils/glossary-manager.js";
|
|
|
6
6
|
import { TranslationResult } from "./pipeline/context.js";
|
|
7
7
|
import { CacheStats } from "./pipeline/steps/CacheReadStep.js";
|
|
8
8
|
import { ConfidenceSettings } from "./pipeline/steps/TranslationStep.js";
|
|
9
|
+
import { SourceCodeAnalyzer } from "../services/source-analyzer.js";
|
|
9
10
|
export interface OrchestratorOptions {
|
|
10
11
|
context: ContextConfig;
|
|
12
|
+
deepContext?: boolean;
|
|
11
13
|
progressOptions?: any;
|
|
12
14
|
styleGuide?: any;
|
|
13
15
|
lengthControl?: any;
|
|
@@ -78,6 +80,7 @@ declare class Orchestrator {
|
|
|
78
80
|
vectorStore: VectorStore;
|
|
79
81
|
embeddingProvider: EmbeddingProvider;
|
|
80
82
|
vectorStats: VectorMemoryStats;
|
|
83
|
+
sourceAnalyzer: SourceCodeAnalyzer;
|
|
81
84
|
confidenceSettings: ConfidenceSettings;
|
|
82
85
|
private concurrencyLimit;
|
|
83
86
|
private pipeline;
|
|
@@ -22,6 +22,8 @@ import QualityCheckStep from "./pipeline/steps/QualityCheckStep.js";
|
|
|
22
22
|
import ToneCheckStep from "./pipeline/steps/ToneCheckStep.js";
|
|
23
23
|
import ConfidenceCheckStep from "./pipeline/steps/ConfidenceCheckStep.js";
|
|
24
24
|
import CacheWriteStep from "./pipeline/steps/CacheWriteStep.js";
|
|
25
|
+
import ContextEnrichmentStep from "./pipeline/steps/ContextEnrichmentStep.js";
|
|
26
|
+
import { SourceCodeAnalyzer } from "../services/source-analyzer.js";
|
|
25
27
|
// Vector Memory Imports
|
|
26
28
|
import { VectorStore } from "../services/vector-store.js";
|
|
27
29
|
import { EmbeddingProvider } from "../services/embedding-provider.js";
|
|
@@ -41,6 +43,8 @@ class Orchestrator {
|
|
|
41
43
|
vectorStore;
|
|
42
44
|
embeddingProvider;
|
|
43
45
|
vectorStats;
|
|
46
|
+
// Source Code Analysis
|
|
47
|
+
sourceAnalyzer;
|
|
44
48
|
confidenceSettings;
|
|
45
49
|
concurrencyLimit;
|
|
46
50
|
pipeline;
|
|
@@ -150,6 +154,26 @@ class Orchestrator {
|
|
|
150
154
|
}
|
|
151
155
|
});
|
|
152
156
|
}
|
|
157
|
+
// Initialize Source Analyzer
|
|
158
|
+
if (options.sourceCodeAnalyzer) {
|
|
159
|
+
this.sourceAnalyzer = options.sourceCodeAnalyzer;
|
|
160
|
+
// Assumed initialized or self-initializing
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
this.sourceAnalyzer = new SourceCodeAnalyzer();
|
|
164
|
+
// Fire and forget initialization (or await if strict)
|
|
165
|
+
// We'll trust it catches up or is fast enough for the first few items,
|
|
166
|
+
// or we can await it if we want to block startup.
|
|
167
|
+
// For CLI responsiveness, we might want to await it only if deep context is enabled.
|
|
168
|
+
const deepContextEnabled = options.deepContext !== false; // Enabled by default
|
|
169
|
+
if (deepContextEnabled) {
|
|
170
|
+
this.sourceAnalyzer.initialize().catch((err) => {
|
|
171
|
+
if (this.advanced.debug) {
|
|
172
|
+
console.warn("Source analyzer init failed:", err.message);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
153
177
|
// Initialize Pipeline
|
|
154
178
|
this.pipeline = new Pipeline();
|
|
155
179
|
this._buildPipeline();
|
|
@@ -182,6 +206,9 @@ class Orchestrator {
|
|
|
182
206
|
// 3. Glossary Pre-processing
|
|
183
207
|
this.pipeline.use(new GlossaryPreStep(this.glossaryManager));
|
|
184
208
|
// 4. Translation
|
|
209
|
+
// 3.5 Context Enrichment (Deep Context)
|
|
210
|
+
const deepContextEnabled = this.options.deepContext !== false;
|
|
211
|
+
this.pipeline.use(new ContextEnrichmentStep(this.sourceAnalyzer, deepContextEnabled));
|
|
185
212
|
this.pipeline.use(new TranslationStep(this.options, this.confidenceSettings));
|
|
186
213
|
// 4.5 Vector Cache Write (save new translations)
|
|
187
214
|
if (this.options.vectorMemory?.enabled) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import PipelineStep from "../PipelineStep.js";
|
|
2
|
+
import { TranslationContext } from "../context.js";
|
|
3
|
+
import { SourceCodeAnalyzer } from "../../../services/source-analyzer.js";
|
|
4
|
+
export default class ContextEnrichmentStep extends PipelineStep {
|
|
5
|
+
private analyzer;
|
|
6
|
+
private enabled;
|
|
7
|
+
constructor(analyzer: SourceCodeAnalyzer, enabled?: boolean);
|
|
8
|
+
execute(context: TranslationContext, next: () => Promise<void>): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import PipelineStep from "../PipelineStep.js";
|
|
2
|
+
export default class ContextEnrichmentStep extends PipelineStep {
|
|
3
|
+
analyzer;
|
|
4
|
+
enabled;
|
|
5
|
+
constructor(analyzer, enabled = true) {
|
|
6
|
+
super();
|
|
7
|
+
this.analyzer = analyzer;
|
|
8
|
+
this.enabled = enabled;
|
|
9
|
+
}
|
|
10
|
+
async execute(context, next) {
|
|
11
|
+
if (this.enabled) {
|
|
12
|
+
try {
|
|
13
|
+
// Ensure analysis is complete before checking for context
|
|
14
|
+
await this.analyzer.ensureInitialized();
|
|
15
|
+
const codeContext = this.analyzer.getContext(context.key);
|
|
16
|
+
if (codeContext) {
|
|
17
|
+
// Enrich the context meta
|
|
18
|
+
context.meta = {
|
|
19
|
+
...context.meta,
|
|
20
|
+
code: codeContext,
|
|
21
|
+
};
|
|
22
|
+
// Also append to the AI prompt context if it exists
|
|
23
|
+
// (Assuming existing translation prompt uses contextData)
|
|
24
|
+
context.contextData = {
|
|
25
|
+
...(context.contextData || {}),
|
|
26
|
+
visualContext: {
|
|
27
|
+
component: codeContext.component,
|
|
28
|
+
file: codeContext.filePath,
|
|
29
|
+
comments: codeContext.comments,
|
|
30
|
+
props: codeContext.props,
|
|
31
|
+
snippet: codeContext.usageSnippet,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
// Non-blocking error
|
|
38
|
+
// console.warn(`Context enrichment failed for ${context.key}`, error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await next();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Orchestrator from "../core/orchestrator.js";
|
|
2
|
+
import { TranslationOptions, GlobalStats, LanguageProcessResult } from "../types/index.js";
|
|
3
|
+
export declare class LanguageProcessor {
|
|
4
|
+
/**
|
|
5
|
+
* Process a single language translation.
|
|
6
|
+
* @param {string} targetLang - Target language code.
|
|
7
|
+
* @param {string} sourceFile - Path to source file.
|
|
8
|
+
* @param {Record<string, any>} flattenedSource - Flattened source content.
|
|
9
|
+
* @param {Orchestrator} orchestrator - Orchestrator instance.
|
|
10
|
+
* @param {TranslationOptions} options - Translation options.
|
|
11
|
+
* @param {GlobalStats} globalStats - Global statistics object.
|
|
12
|
+
* @param {any} comparison - State comparison result.
|
|
13
|
+
* @returns {Promise<LanguageProcessResult>} - Status and messages for this language.
|
|
14
|
+
*/
|
|
15
|
+
static processLanguage(targetLang: string, sourceFile: string, flattenedSource: Record<string, any>, orchestrator: Orchestrator, options: TranslationOptions, globalStats: GlobalStats, comparison: any): Promise<LanguageProcessResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Check if text only contains placeholders and whitespace.
|
|
18
|
+
* @param {string} text - Text to check.
|
|
19
|
+
* @returns {boolean} - True if text only contains placeholders.
|
|
20
|
+
*/
|
|
21
|
+
static isPlaceholderOnlyText(text: string): boolean;
|
|
22
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { FileManager } from "../utils/file-manager.js";
|
|
3
|
+
import ObjectTransformer from "../utils/object-transformer.js";
|
|
4
|
+
import InputValidator from "../utils/input-validator.js";
|
|
5
|
+
import uiManager from "../utils/ui-manager.js";
|
|
6
|
+
import statisticsManager from "../utils/statistics-manager.js";
|
|
7
|
+
export class LanguageProcessor {
|
|
8
|
+
/**
|
|
9
|
+
* Process a single language translation.
|
|
10
|
+
* @param {string} targetLang - Target language code.
|
|
11
|
+
* @param {string} sourceFile - Path to source file.
|
|
12
|
+
* @param {Record<string, any>} flattenedSource - Flattened source content.
|
|
13
|
+
* @param {Orchestrator} orchestrator - Orchestrator instance.
|
|
14
|
+
* @param {TranslationOptions} options - Translation options.
|
|
15
|
+
* @param {GlobalStats} globalStats - Global statistics object.
|
|
16
|
+
* @param {any} comparison - State comparison result.
|
|
17
|
+
* @returns {Promise<LanguageProcessResult>} - Status and messages for this language.
|
|
18
|
+
*/
|
|
19
|
+
static async processLanguage(targetLang, sourceFile, flattenedSource, orchestrator, options, globalStats, comparison) {
|
|
20
|
+
if (!targetLang) {
|
|
21
|
+
throw new Error("Invalid target language provided");
|
|
22
|
+
}
|
|
23
|
+
const langStartTime = Date.now();
|
|
24
|
+
try {
|
|
25
|
+
if (!targetLang || typeof targetLang !== "string") {
|
|
26
|
+
throw new Error("Invalid target language provided");
|
|
27
|
+
}
|
|
28
|
+
if (!sourceFile || typeof sourceFile !== "string") {
|
|
29
|
+
throw new Error("Invalid source file path provided");
|
|
30
|
+
}
|
|
31
|
+
if (!flattenedSource || typeof flattenedSource !== "object") {
|
|
32
|
+
throw new Error("Invalid flattened source data provided");
|
|
33
|
+
}
|
|
34
|
+
if (!orchestrator || typeof orchestrator.processTranslations !== "function") {
|
|
35
|
+
throw new Error("Invalid orchestrator instance provided");
|
|
36
|
+
}
|
|
37
|
+
if (!globalStats || typeof globalStats !== "object") {
|
|
38
|
+
throw new Error("Invalid global stats object provided");
|
|
39
|
+
}
|
|
40
|
+
const safeTargetLang = InputValidator.validateLanguageCode(targetLang, "target language");
|
|
41
|
+
await uiManager.log(`Starting translations for ${safeTargetLang}`);
|
|
42
|
+
let finalStatus = null;
|
|
43
|
+
let savedMessage = undefined;
|
|
44
|
+
statisticsManager.initLanguageStats(safeTargetLang);
|
|
45
|
+
const sourceDir = path.dirname(sourceFile);
|
|
46
|
+
const safeTargetFilename = `${safeTargetLang}.json`;
|
|
47
|
+
const targetPath = InputValidator.createSafeFilePath(sourceDir, safeTargetFilename);
|
|
48
|
+
let targetContent = {};
|
|
49
|
+
try {
|
|
50
|
+
targetContent = await FileManager.readJSON(targetPath);
|
|
51
|
+
if (!targetContent || typeof targetContent !== "object") {
|
|
52
|
+
console.warn(`Invalid content in ${targetPath}, using empty object`);
|
|
53
|
+
targetContent = {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err.code === "ENOENT") {
|
|
58
|
+
await uiManager.log(`\ud83c\udd95 Creating new translation file for ${safeTargetLang}`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (options.debug) {
|
|
62
|
+
console.warn(`Error reading ${targetPath}: ${err.message}, using empty object`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
targetContent = {};
|
|
66
|
+
}
|
|
67
|
+
let flattenedTarget = {};
|
|
68
|
+
try {
|
|
69
|
+
flattenedTarget = ObjectTransformer.flatten(targetContent);
|
|
70
|
+
if (!flattenedTarget || typeof flattenedTarget !== "object") {
|
|
71
|
+
flattenedTarget = {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.warn(`Error flattening target content: ${err.message}, using empty object`);
|
|
76
|
+
flattenedTarget = {};
|
|
77
|
+
}
|
|
78
|
+
const missingKeys = [];
|
|
79
|
+
let hasPlaceholderOnlyChanges = false;
|
|
80
|
+
for (const [key, sourceText] of Object.entries(flattenedSource)) {
|
|
81
|
+
try {
|
|
82
|
+
InputValidator.validateKey(key, "translation key");
|
|
83
|
+
InputValidator.validateText(sourceText, "source text");
|
|
84
|
+
}
|
|
85
|
+
catch (keyError) {
|
|
86
|
+
await uiManager.log(`\u26a0\ufe0f Skipping invalid key/text: ${keyError.message}`);
|
|
87
|
+
globalStats.languages[safeTargetLang].failed++;
|
|
88
|
+
globalStats.failed++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
globalStats.languages[safeTargetLang].processed++;
|
|
92
|
+
if (this.isPlaceholderOnlyText(sourceText)) {
|
|
93
|
+
const existingValue = flattenedTarget[key];
|
|
94
|
+
if (existingValue !== sourceText) {
|
|
95
|
+
flattenedTarget[key] = sourceText;
|
|
96
|
+
hasPlaceholderOnlyChanges = true;
|
|
97
|
+
if (options.debug) {
|
|
98
|
+
await uiManager.log(` Copying placeholder-only text: "${sourceText}"`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
globalStats.languages[safeTargetLang].skipped++;
|
|
102
|
+
globalStats.skipped++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const isNewKey = comparison.newKeys.includes(key);
|
|
106
|
+
const isModifiedKey = comparison.modifiedKeys.includes(key);
|
|
107
|
+
const keyExistsInTarget = key in flattenedTarget;
|
|
108
|
+
if (keyExistsInTarget && !options.forceUpdate && !isModifiedKey) {
|
|
109
|
+
globalStats.languages[safeTargetLang].skipped++;
|
|
110
|
+
globalStats.skipped++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
missingKeys.push({
|
|
114
|
+
key,
|
|
115
|
+
text: sourceText,
|
|
116
|
+
targetLang: safeTargetLang,
|
|
117
|
+
existingTranslation: flattenedTarget[key],
|
|
118
|
+
isModified: isModifiedKey,
|
|
119
|
+
isNew: isNewKey,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (missingKeys.length === 0 && !hasPlaceholderOnlyChanges) {
|
|
123
|
+
await uiManager.log(`All translations exist for ${safeTargetLang}`);
|
|
124
|
+
globalStats.languages[safeTargetLang].timeMs = Date.now() - langStartTime;
|
|
125
|
+
return { status: { completed: 0, total: 0, language: safeTargetLang } };
|
|
126
|
+
}
|
|
127
|
+
if (missingKeys.length > 0) {
|
|
128
|
+
await uiManager.log(`Found ${missingKeys.length} missing translations for ${safeTargetLang}`);
|
|
129
|
+
}
|
|
130
|
+
let results = [];
|
|
131
|
+
if (missingKeys.length > 0) {
|
|
132
|
+
try {
|
|
133
|
+
results = await orchestrator.processTranslations(missingKeys);
|
|
134
|
+
if (!Array.isArray(results)) {
|
|
135
|
+
console.warn("Invalid results from orchestrator, using empty array");
|
|
136
|
+
results = [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
console.error(`Error processing translations for ${safeTargetLang}: ${err.message}`);
|
|
141
|
+
results = [];
|
|
142
|
+
if (globalStats.languages[safeTargetLang]) {
|
|
143
|
+
globalStats.languages[safeTargetLang].failed += missingKeys.length;
|
|
144
|
+
globalStats.failed += missingKeys.length;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
if (hasPlaceholderOnlyChanges) {
|
|
150
|
+
await uiManager.log(`Found placeholder-only changes for ${safeTargetLang}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
if (orchestrator?.progress &&
|
|
155
|
+
typeof orchestrator.progress.getStatus === "function") {
|
|
156
|
+
finalStatus = orchestrator.progress.getStatus();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.warn(`Error getting orchestrator status: ${err.message}`);
|
|
161
|
+
}
|
|
162
|
+
let validResults = [];
|
|
163
|
+
try {
|
|
164
|
+
validResults = results.filter((result) => result && result.success === true);
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
|
+
validResults = validResults.filter((result) => result.key &&
|
|
167
|
+
typeof result.key === "string" &&
|
|
168
|
+
result.translated &&
|
|
169
|
+
typeof result.translated === "string");
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
console.warn(`Error filtering results: ${err.message}`);
|
|
173
|
+
validResults = [];
|
|
174
|
+
}
|
|
175
|
+
if (validResults.length > 0) {
|
|
176
|
+
validResults.forEach(({ key, translated }) => {
|
|
177
|
+
if (translated) {
|
|
178
|
+
flattenedTarget[key] = translated;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const hasChangesToSave = validResults.length > 0 || hasPlaceholderOnlyChanges;
|
|
183
|
+
if (hasChangesToSave) {
|
|
184
|
+
const unflattened = ObjectTransformer.unflatten(flattenedTarget);
|
|
185
|
+
await FileManager.writeJSON(targetPath, unflattened);
|
|
186
|
+
globalStats.total += validResults.length;
|
|
187
|
+
globalStats.success += validResults.length;
|
|
188
|
+
globalStats.failed += results.length - validResults.length;
|
|
189
|
+
globalStats.languages[safeTargetLang].added += validResults.length;
|
|
190
|
+
globalStats.languages[safeTargetLang].failed +=
|
|
191
|
+
results.length - validResults.length;
|
|
192
|
+
validResults.forEach((result) => {
|
|
193
|
+
if (result.context) {
|
|
194
|
+
const category = result.context.category || "general";
|
|
195
|
+
globalStats.byCategory[category] =
|
|
196
|
+
(globalStats.byCategory[category] || 0) + 1;
|
|
197
|
+
if (!globalStats.details[category]) {
|
|
198
|
+
globalStats.details[category] = {
|
|
199
|
+
totalConfidence: 0,
|
|
200
|
+
samples: 0,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
globalStats.details[category].totalConfidence +=
|
|
204
|
+
result.context.confidence || 0;
|
|
205
|
+
globalStats.details[category].samples++;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
savedMessage = `Translations saved: ${safeTargetLang}.json`;
|
|
209
|
+
}
|
|
210
|
+
globalStats.languages[safeTargetLang].timeMs = Date.now() - langStartTime;
|
|
211
|
+
return { status: finalStatus, savedMessage };
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const safeError = error.message.includes("outside working directory")
|
|
215
|
+
? "Invalid file path detected"
|
|
216
|
+
: error.message;
|
|
217
|
+
await uiManager.log(`\n\u274c Error processing ${targetLang}: ${safeError}`);
|
|
218
|
+
if (globalStats.languages[targetLang]) {
|
|
219
|
+
globalStats.languages[targetLang].error = safeError;
|
|
220
|
+
globalStats.languages[targetLang].timeMs = Date.now() - langStartTime;
|
|
221
|
+
}
|
|
222
|
+
return { status: null, error: safeError };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Check if text only contains placeholders and whitespace.
|
|
227
|
+
* @param {string} text - Text to check.
|
|
228
|
+
* @returns {boolean} - True if text only contains placeholders.
|
|
229
|
+
*/
|
|
230
|
+
static isPlaceholderOnlyText(text) {
|
|
231
|
+
if (!text || typeof text !== "string") {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
// Remove all placeholders from the text
|
|
235
|
+
const placeholderRegex = /\{[^}]+\}|\$\{[^}]+\}|%[sd]/g;
|
|
236
|
+
const textWithoutPlaceholders = text.replace(placeholderRegex, "").trim();
|
|
237
|
+
return textWithoutPlaceholders.length === 0;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CodeContext } from "../types/context.js";
|
|
2
|
+
export declare class SourceCodeAnalyzer {
|
|
3
|
+
private project;
|
|
4
|
+
private keyUsageMap;
|
|
5
|
+
private rootDir;
|
|
6
|
+
private initPromise;
|
|
7
|
+
constructor(rootDir?: string);
|
|
8
|
+
initialize(globPatterns?: string[]): Promise<void>;
|
|
9
|
+
ensureInitialized(): Promise<void>;
|
|
10
|
+
private processFile;
|
|
11
|
+
private processCallExpression;
|
|
12
|
+
private extractContext;
|
|
13
|
+
getContext(key: string): CodeContext | undefined;
|
|
14
|
+
}
|