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.
@@ -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
+ }