locai 2.1.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.
Files changed (72) hide show
  1. package/README.md +928 -0
  2. package/dist/cli.js +718 -0
  3. package/dist/commands/review.js +357 -0
  4. package/dist/commands/translator.js +29 -0
  5. package/dist/config/index.js +52 -0
  6. package/dist/core/adapters/base-adapter.js +1 -0
  7. package/dist/core/adapters/factory.js +44 -0
  8. package/dist/core/adapters/json-adapter.js +20 -0
  9. package/dist/core/adapters/po-adapter.js +50 -0
  10. package/dist/core/adapters/properties-adapter.js +31 -0
  11. package/dist/core/adapters/yaml-adapter.js +24 -0
  12. package/dist/core/context-processor.js +247 -0
  13. package/dist/core/detection/framework-detector.js +56 -0
  14. package/dist/core/detection/structure-detector.js +30 -0
  15. package/dist/core/fallback-provider.js +442 -0
  16. package/dist/core/orchestrator.js +438 -0
  17. package/dist/core/pipeline/Pipeline.js +48 -0
  18. package/dist/core/pipeline/PipelineStep.js +13 -0
  19. package/dist/core/pipeline/context.js +21 -0
  20. package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
  21. package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
  22. package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
  23. package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
  24. package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
  25. package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
  26. package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
  27. package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
  28. package/dist/core/pipeline/steps/TranslationStep.js +65 -0
  29. package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
  30. package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
  31. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
  32. package/dist/core/provider-factory.js +139 -0
  33. package/dist/providers/base-provider.js +230 -0
  34. package/dist/providers/dashscope.js +128 -0
  35. package/dist/providers/deepseek.js +111 -0
  36. package/dist/providers/gemini.js +135 -0
  37. package/dist/providers/openai.js +203 -0
  38. package/dist/providers/xai.js +111 -0
  39. package/dist/services/embedding-provider.js +50 -0
  40. package/dist/services/translation-service.js +609 -0
  41. package/dist/services/vector-store.js +79 -0
  42. package/dist/utils/ai-context-analyzer.js +460 -0
  43. package/dist/utils/confidence-scorer.js +277 -0
  44. package/dist/utils/error-helper.js +386 -0
  45. package/dist/utils/file-manager.js +357 -0
  46. package/dist/utils/glossary-manager.js +194 -0
  47. package/dist/utils/graceful-shutdown.js +118 -0
  48. package/dist/utils/input-validator.js +663 -0
  49. package/dist/utils/logger.js +374 -0
  50. package/dist/utils/object-transformer.js +177 -0
  51. package/dist/utils/progress-tracker.js +277 -0
  52. package/dist/utils/prompt-templates.js +415 -0
  53. package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
  54. package/dist/utils/quality/base-checker.js +73 -0
  55. package/dist/utils/quality/code-block-checker.js +214 -0
  56. package/dist/utils/quality/html-tag-checker.js +64 -0
  57. package/dist/utils/quality/index.js +220 -0
  58. package/dist/utils/quality/length-checker.js +140 -0
  59. package/dist/utils/quality/markdown-checker.js +220 -0
  60. package/dist/utils/quality/placeholder-checker.js +142 -0
  61. package/dist/utils/quality/punctuation-checker.js +50 -0
  62. package/dist/utils/quality/quote-balance-checker.js +143 -0
  63. package/dist/utils/quality/special-characters-checker.js +229 -0
  64. package/dist/utils/quality/style-guide-checker.js +192 -0
  65. package/dist/utils/quality/text-sanitizer.js +120 -0
  66. package/dist/utils/quality/tone-verifier.js +91 -0
  67. package/dist/utils/rate-limiter.js +398 -0
  68. package/dist/utils/retry-helper.js +166 -0
  69. package/dist/utils/state-manager.js +162 -0
  70. package/dist/utils/statistics-manager.js +51 -0
  71. package/dist/utils/ui-manager.js +98 -0
  72. package/package.json +80 -0
@@ -0,0 +1,162 @@
1
+ import crypto from "crypto";
2
+ import { FileManager } from "./file-manager.js"; // Note: Changed named import to default import based on file-manager migration
3
+ import path from "path";
4
+ import { readFileSync } from "fs";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname } from "path";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ /**
10
+ * StateManager - Tracks changes in source locale files using hash-based state management
11
+ * This enables detection of deleted/modified keys to synchronize target locale files
12
+ */
13
+ class StateManager {
14
+ options;
15
+ constructor(options = {}) {
16
+ // Support both direct options and nested stateTracking config
17
+ const stateTracking = "stateTracking" in options ? options.stateTracking : options;
18
+ this.options = {
19
+ enabled: stateTracking.enabled !== false,
20
+ stateFileName: stateTracking.stateFileName || "localization.state.json",
21
+ stateDir: stateTracking.stateDir || ".localize-cache",
22
+ ...stateTracking,
23
+ };
24
+ }
25
+ /**
26
+ * Generate a SHA-256 hash for given text content
27
+ */
28
+ _generateHash(text) {
29
+ if (typeof text !== "string") {
30
+ text = JSON.stringify(text);
31
+ }
32
+ return crypto.createHash("sha256").update(text, "utf8").digest("hex");
33
+ }
34
+ /**
35
+ * Get the full path to the state file
36
+ */
37
+ _getStateFilePath(projectRoot) {
38
+ const stateDir = path.join(projectRoot, this.options.stateDir);
39
+ return path.join(stateDir, this.options.stateFileName);
40
+ }
41
+ /**
42
+ * Load previous state from state file
43
+ */
44
+ async loadState(projectRoot) {
45
+ try {
46
+ const stateFilePath = this._getStateFilePath(projectRoot);
47
+ const state = await FileManager.readJSON(stateFilePath);
48
+ return state || {};
49
+ }
50
+ catch (error) {
51
+ // State file doesn't exist or is corrupted - return empty state
52
+ if (error.message.includes("File read error") || error.code === "ENOENT") {
53
+ return {};
54
+ }
55
+ throw new Error(`Failed to load state: ${error.message}`);
56
+ }
57
+ }
58
+ /**
59
+ * Save current state to state file
60
+ */
61
+ async saveState(projectRoot, state) {
62
+ try {
63
+ const stateFilePath = this._getStateFilePath(projectRoot);
64
+ // Ensure state directory exists
65
+ const stateDir = path.dirname(stateFilePath);
66
+ await FileManager.ensureDir(stateDir);
67
+ // Save state with metadata
68
+ const stateWithMetadata = {
69
+ ...state,
70
+ _metadata: {
71
+ lastUpdated: new Date().toISOString(),
72
+ version: "1.0.0",
73
+ toolVersion: JSON.parse(readFileSync(path.join(__dirname, "../../package.json"), "utf8")).version,
74
+ },
75
+ };
76
+ await FileManager.writeJSON(stateFilePath, stateWithMetadata);
77
+ return true;
78
+ }
79
+ catch (error) {
80
+ throw new Error(`Failed to save state: ${error.message}`);
81
+ }
82
+ }
83
+ /**
84
+ * Generate state object from source content by creating hashes for each key
85
+ */
86
+ generateStateFromSource(sourceContent) {
87
+ const state = {};
88
+ for (const [key, value] of Object.entries(sourceContent)) {
89
+ // Generate hash for the value content
90
+ state[key] = this._generateHash(value);
91
+ }
92
+ return state;
93
+ }
94
+ /**
95
+ * Compare two states and identify changes
96
+ */
97
+ compareStates(previousState, currentState) {
98
+ const deletedKeys = [];
99
+ const modifiedKeys = [];
100
+ const newKeys = [];
101
+ // Remove metadata from comparison if it exists
102
+ const cleanPreviousState = { ...previousState };
103
+ const cleanCurrentState = { ...currentState };
104
+ delete cleanPreviousState["_metadata"];
105
+ delete cleanCurrentState["_metadata"];
106
+ // Find deleted keys (exist in previous but not in current)
107
+ for (const key of Object.keys(cleanPreviousState)) {
108
+ if (!(key in cleanCurrentState)) {
109
+ deletedKeys.push(key);
110
+ }
111
+ }
112
+ // Find modified and new keys
113
+ for (const [key, currentHash] of Object.entries(cleanCurrentState)) {
114
+ if (!(key in cleanPreviousState)) {
115
+ // New key
116
+ newKeys.push(key);
117
+ }
118
+ else if (cleanPreviousState[key] !== currentHash) {
119
+ // Modified key (hash changed)
120
+ modifiedKeys.push(key);
121
+ }
122
+ }
123
+ return {
124
+ deletedKeys,
125
+ modifiedKeys,
126
+ newKeys,
127
+ hasChanges: deletedKeys.length > 0 || modifiedKeys.length > 0 || newKeys.length > 0,
128
+ };
129
+ }
130
+ /**
131
+ * Get statistics about the state comparison
132
+ */
133
+ getComparisonStats(comparison) {
134
+ return {
135
+ totalChanges: comparison.deletedKeys.length +
136
+ comparison.modifiedKeys.length +
137
+ comparison.newKeys.length,
138
+ deletedCount: comparison.deletedKeys.length,
139
+ modifiedCount: comparison.modifiedKeys.length,
140
+ newCount: comparison.newKeys.length,
141
+ hasChanges: comparison.hasChanges,
142
+ };
143
+ }
144
+ /**
145
+ * Clean up old state files (maintenance utility)
146
+ */
147
+ async cleanupState(projectRoot) {
148
+ try {
149
+ const stateFilePath = this._getStateFilePath(projectRoot);
150
+ await FileManager.deleteFile(stateFilePath);
151
+ return true;
152
+ }
153
+ catch (error) {
154
+ // File doesn't exist - that's fine
155
+ if (error.message.includes("ENOENT")) {
156
+ return true;
157
+ }
158
+ throw new Error(`Failed to cleanup state: ${error.message}`);
159
+ }
160
+ }
161
+ }
162
+ export default StateManager;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Statistics manager to track translation progress and metrics
3
+ */
4
+ export class StatisticsManager {
5
+ stats;
6
+ constructor() {
7
+ this.stats = this.initializeGlobalStats();
8
+ }
9
+ /**
10
+ * Initialize global statistics structure
11
+ */
12
+ initializeGlobalStats() {
13
+ return {
14
+ total: 0,
15
+ byCategory: {},
16
+ details: {},
17
+ totalTime: 0,
18
+ success: 0,
19
+ failed: 0,
20
+ skipped: 0,
21
+ languages: {},
22
+ startTime: new Date().toISOString(),
23
+ orchestrators: [],
24
+ };
25
+ }
26
+ /**
27
+ * Reset statistics
28
+ */
29
+ reset() {
30
+ this.stats = this.initializeGlobalStats();
31
+ }
32
+ /**
33
+ * Get current stats
34
+ */
35
+ getStats() {
36
+ return this.stats;
37
+ }
38
+ /**
39
+ * Initialize language stats
40
+ */
41
+ initLanguageStats(langCode) {
42
+ this.stats.languages[langCode] = {
43
+ processed: 0,
44
+ added: 0,
45
+ skipped: 0,
46
+ failed: 0,
47
+ timeMs: 0,
48
+ };
49
+ }
50
+ }
51
+ export default new StatisticsManager();
@@ -0,0 +1,98 @@
1
+ /**
2
+ * UI Manager for handling CLI output and user interaction
3
+ */
4
+ export class UIManager {
5
+ consoleLock;
6
+ constructor() {
7
+ this.consoleLock = {
8
+ queue: [],
9
+ isLocked: false,
10
+ async log(message) {
11
+ return new Promise((resolve) => {
12
+ const executeLog = () => {
13
+ console.log(message);
14
+ this.isLocked = false;
15
+ resolve();
16
+ this._processQueue();
17
+ };
18
+ if (this.isLocked) {
19
+ this.queue.push(executeLog);
20
+ }
21
+ else {
22
+ this.isLocked = true;
23
+ executeLog();
24
+ }
25
+ });
26
+ },
27
+ _processQueue() {
28
+ if (this.queue.length > 0 && !this.isLocked) {
29
+ this.isLocked = true;
30
+ const nextLog = this.queue.shift();
31
+ if (nextLog)
32
+ nextLog();
33
+ }
34
+ },
35
+ };
36
+ }
37
+ /**
38
+ * Log a message safely preventing overlap
39
+ */
40
+ async log(message) {
41
+ await this.consoleLock.log(message);
42
+ }
43
+ /**
44
+ * Display global summary of translation
45
+ */
46
+ async displayGlobalSummary(stats, totalLanguages) {
47
+ await this.log("\n" + "=".repeat(60));
48
+ await this.log("Global Translation Summary");
49
+ await this.log("=".repeat(60));
50
+ await this.log(`\nLanguages Processed: ${totalLanguages}`);
51
+ await this.log(`Total Translations: ${stats.total}`);
52
+ await this.log(`Success: ${stats.success}`);
53
+ await this.log(`Failed: ${stats.failed}`);
54
+ await this.log(`Skipped: ${stats.skipped}`);
55
+ await this.log(`Total Time: ${stats.totalTime?.toFixed(1) || 0}s`);
56
+ await this.log(`Average per language: ${(stats.totalTime / (totalLanguages || 1)).toFixed(1)}s`);
57
+ // Display detailed language stats
58
+ if (Object.keys(stats.languages).length > 0) {
59
+ await this.log("\n" + "-".repeat(60));
60
+ await this.log("Per-language Performance:");
61
+ await this.log("-".repeat(60));
62
+ for (const [lang, langStats] of Object.entries(stats.languages)) {
63
+ const timeSeconds = langStats.timeMs / 1000;
64
+ await this.log(` ${lang.padEnd(4)} | ${String(langStats.added).padStart(3)} added | ${String(langStats.skipped).padStart(3)} skipped | ${String(langStats.failed).padStart(2)} failed | ${timeSeconds.toFixed(1)}s`);
65
+ }
66
+ }
67
+ // Only show categories if we have them
68
+ if (Object.keys(stats.byCategory).length > 0) {
69
+ await this.log("\n" + "-".repeat(60));
70
+ await this.log("Context Analysis by Category:");
71
+ await this.log("-".repeat(60));
72
+ for (const [category, count] of Object.entries(stats.byCategory)) {
73
+ const details = stats.details[category];
74
+ if (details && details.samples > 0) {
75
+ const avgConfidence = details.totalConfidence / details.samples;
76
+ const confidenceStr = `${(avgConfidence * 100).toFixed(1)}%`;
77
+ await this.log(` ${category}: ${count} items (${confidenceStr} avg confidence)`);
78
+ }
79
+ else {
80
+ await this.log(` ${category}: ${count} items`);
81
+ }
82
+ }
83
+ }
84
+ // Clear completion message - only once
85
+ await this.log("\n" + "=".repeat(60));
86
+ const duration = stats.totalDuration || 0;
87
+ await this.log(`All operations completed successfully in ${duration.toFixed(1)}s`);
88
+ await this.log("=".repeat(60) + "\n");
89
+ }
90
+ async logBatchResults(batchResults) {
91
+ for (const result of batchResults) {
92
+ if (result && result.savedMessage) {
93
+ await this.log(result.savedMessage);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ export default new UIManager();
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "locai",
3
+ "version": "2.1.0",
4
+ "type": "module",
5
+ "main": "dist/core/orchestrator.js",
6
+ "directories": {
7
+ "src": "src",
8
+ "dist": "dist"
9
+ },
10
+ "bin": {
11
+ "locai": "./dist/cli.js",
12
+ "localize": "./dist/cli.js"
13
+ },
14
+ "scripts": {
15
+ "start": "node dist/core/orchestrator.js",
16
+ "dev": "tsx src/core/orchestrator.js",
17
+ "build": "tsc",
18
+ "type-check": "tsc --noEmit",
19
+ "lint": "eslint .",
20
+ "lint:fix": "eslint . --fix",
21
+ "format": "prettier --write \"**/*.{js,json,md,jsx,ts,tsx,css}\"",
22
+ "format:check": "prettier --check \"**/*.{js,json,md,jsx,ts,tsx,css}\"",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "test:coverage": "vitest run --coverage",
26
+ "prepublishOnly": "pnpm build"
27
+ },
28
+ "keywords": [
29
+ "translation",
30
+ "localization",
31
+ "i18n",
32
+ "nextjs",
33
+ "ai-translation"
34
+ ],
35
+ "author": {
36
+ "name": "Ahmet Enes Dur",
37
+ "email": "ahmetenesdur@gmail.com",
38
+ "url": "https://github.com/ahmetenesdur"
39
+ },
40
+ "license": "ISC",
41
+ "description": "CLI tool for translating Next.js locale files with advanced settings and multiple AI providers",
42
+ "engines": {
43
+ "node": ">=14.13.0"
44
+ },
45
+ "exports": {
46
+ ".": "./dist/core/orchestrator.js",
47
+ "./orchestrator": "./dist/core/orchestrator.js",
48
+ "./translator": "./dist/commands/translator.js"
49
+ },
50
+ "dependencies": {
51
+ "axios": "1.13.1",
52
+ "c12": "^3.3.2",
53
+ "commander": "^14.0.2",
54
+ "dot-properties": "^1.1.0",
55
+ "dotenv": "^17.2.3",
56
+ "gettext-parser": "^8.0.0",
57
+ "jiti": "^2.6.1",
58
+ "js-yaml": "^4.1.1",
59
+ "lru-cache": "^11.2.2",
60
+ "p-limit": "^7.2.0",
61
+ "vectra": "^0.11.1"
62
+ },
63
+ "devDependencies": {
64
+ "@eslint/js": "^9.39.1",
65
+ "@types/js-yaml": "^4.0.9",
66
+ "@types/node": "^25.0.1",
67
+ "@vitest/coverage-v8": "^4.0.15",
68
+ "eslint": "^9.39.1",
69
+ "eslint-config-prettier": "^10.1.8",
70
+ "globals": "^16.5.0",
71
+ "prettier": "^3.6.2",
72
+ "std-mocks": "^2.0.0",
73
+ "tsx": "^4.21.0",
74
+ "typescript": "^5.9.3",
75
+ "vitest": "^4.0.15"
76
+ },
77
+ "files": [
78
+ "dist/"
79
+ ]
80
+ }