locai 2.3.0 → 2.4.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
+ };
@@ -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
+ }
@@ -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
- export interface TranslationOptions extends OrchestratorOptions {
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.