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.
@@ -8,26 +8,13 @@ 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";
11
12
  /**
12
13
  * Service for handling file translation operations.
13
14
  * Orchestrates the translation process including state management,
14
15
  * verification, and statistics collection.
15
16
  */
16
17
  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
18
  /**
32
19
  * Main translator function to process source file and create translations.
33
20
  * @param {string} file - Path to source file.
@@ -201,207 +188,7 @@ class TranslationService {
201
188
  * @returns {Promise<LanguageProcessResult>} - Status and messages for this language.
202
189
  */
203
190
  async processLanguage(targetLang, sourceFile, flattenedSource, orchestrator, options, globalStats, comparison) {
204
- const langStartTime = Date.now();
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
- }
191
+ return await LanguageProcessor.processLanguage(targetLang, sourceFile, flattenedSource, orchestrator, options, globalStats, comparison);
405
192
  }
406
193
  /**
407
194
  * Finalize translation process and save state.
@@ -415,6 +202,7 @@ class TranslationService {
415
202
  async finalizeTranslation(stateManager, projectRoot, currentState, globalStats, startTime, options) {
416
203
  globalStats.endTime = new Date().toISOString();
417
204
  globalStats.totalDuration = (Date.now() - startTime) / 1000;
205
+ globalStats.totalTime = globalStats.totalDuration;
418
206
  await uiManager.displayGlobalSummary(globalStats, (options.targets || []).length);
419
207
  // Save review queue if confidence scoring was enabled
420
208
  if (options.saveReviewQueue || options.minConfidence !== undefined) {
@@ -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 {};
@@ -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(` ${category}: ${count} items (${confidenceStr} avg confidence)`);
77
+ await this.log(`${category}: ${count} items (${confidenceStr} avg confidence)`);
78
78
  }
79
79
  else {
80
- await this.log(` ${category}: ${count} items`);
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.0",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "main": "dist/core/orchestrator.js",
6
6
  "types": "dist/core/orchestrator.d.ts",
@@ -27,18 +27,26 @@
27
27
  "prepublishOnly": "pnpm build"
28
28
  },
29
29
  "keywords": [
30
- "translation",
31
- "localization",
32
30
  "i18n",
33
- "nextjs",
34
- "ai-translation",
35
- "cli",
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
- "developer-tools"
45
+ "xai",
46
+ "vector-database",
47
+ "semantic-search",
48
+ "developer-tools",
49
+ "typescript"
42
50
  ],
43
51
  "author": {
44
52
  "name": "Ahmet Enes Dur",