locai 2.4.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 CHANGED
@@ -13,6 +13,8 @@ Translate your project in seconds without installing anything:
13
13
  npx locai translate --source en --targets tr,es,de
14
14
  ```
15
15
 
16
+ > **Need a permanent setup?** Check out our [Configuration Guide](./docs/configuration.md) to create a `localize.config.js` file.
17
+
16
18
  ## 📖 Documentation
17
19
 
18
20
  Everything you need to know to master Locai.
@@ -37,7 +39,9 @@ Choose the right brain for your localization. Compare OpenAI, Gemini, DeepSeek,
37
39
 
38
40
  Deep dive into Locai's intelligent features. We didn't just wrap an API; we built a localization engine.
39
41
 
40
- - **Infinite Memory**: Vector-based caching. We "remember" every translation. If you translate the same (or similar) sentence again, we fetch it from local cache instantly. Zero cost, 100% consistency.
42
+ - **🧠 Smart Context Awareness**: Uses AI to understand the context of your strings for better translations.
43
+ - **🔬 Deep Context Analysis**: Scans your source code (AST) to extract component names, comments, and props for context-aware translations.
44
+ - **📚 Infinite Memory**: Uses vector embeddings to remember previous translations and maintain consistency. If you translate the same (or similar) sentence again, we fetch it from local cache instantly. Zero cost, 100% consistency.
41
45
  - **Smart Sync**: We hash every key. Locai only translates **new** or **modified** keys. If you have 1000 keys and change 5, we only send 5 to the AI.
42
46
  - **Context Awareness**: Locai analyzes your content. It knows if a string is for a **Button** (keep it short), **Marketing** (make it punchy), or **Legal** (be precise).
43
47
  - **Glossary & Brand Safety**: Define immutable terms (like "Locai", "API", "SaaS"). We guarantee they are never translated or corrupted.
package/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ const main = async () => {
26
26
  .option("-s, --source <lang>", "Source language", defaultConfig.source)
27
27
  .option("-t, --targets <langs>", "Target languages (comma separated)", (val) => val.split(","), defaultConfig.targets)
28
28
  .option("--localesDir <dir>", "Localization files directory", defaultConfig.localesDir)
29
+ .option("--no-deep-context", "Disable deep context analysis (source code scanning)")
29
30
  .option("--debug", "Enable debug mode with verbose logging", false)
30
31
  .option("--verbose", "Enable detailed diagnostic output", false);
31
32
  program.on("option:debug", function () {
@@ -9,6 +9,12 @@ export interface LocalizeConfig extends Omit<Partial<OrchestratorOptions>, "cont
9
9
  * Target language codes (e.g. ['tr', 'es'])
10
10
  */
11
11
  targets?: string[];
12
+ /**
13
+ * Deep Context (Source Code Analysis)
14
+ * Scans source code for context (component name, comments, props)
15
+ * @default true
16
+ */
17
+ deepContext?: boolean;
12
18
  /**
13
19
  * Filter by file extensions (e.g. ['.json', '.arb'])
14
20
  */
@@ -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,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
+ }
@@ -0,0 +1,164 @@
1
+ import { Project, Node } from "ts-morph";
2
+ import { log } from "../utils/logger.js";
3
+ import path from "path";
4
+ export class SourceCodeAnalyzer {
5
+ project;
6
+ keyUsageMap = new Map();
7
+ rootDir;
8
+ initPromise = null;
9
+ constructor(rootDir = process.cwd()) {
10
+ this.rootDir = rootDir;
11
+ // Initialize with default settings, but don't add files yet for performance
12
+ this.project = new Project({
13
+ skipAddingFilesFromTsConfig: true,
14
+ compilerOptions: {
15
+ allowJs: true,
16
+ jsx: 1, // Preserve
17
+ },
18
+ });
19
+ }
20
+ initialize(globPatterns = [
21
+ "src/**/*.{ts,tsx}",
22
+ "app/**/*.{ts,tsx}",
23
+ "components/**/*.{ts,tsx}",
24
+ ]) {
25
+ if (this.initPromise)
26
+ return this.initPromise;
27
+ this.initPromise = (async () => {
28
+ await new Promise((resolve) => setTimeout(resolve, 0)); // Force async execution for race condition safety
29
+ try {
30
+ // Add source files
31
+ log(`Analyzing source code in ${this.rootDir}...`);
32
+ // Exclude node_modules explicitly just in case
33
+ this.project.addSourceFilesAtPaths(globPatterns);
34
+ const sourceFiles = this.project.getSourceFiles();
35
+ log(`Found ${sourceFiles.length} source files for context analysis.`);
36
+ for (const sourceFile of sourceFiles) {
37
+ this.processFile(sourceFile);
38
+ }
39
+ log(`Source analysis complete. Context extracted for ${this.keyUsageMap.size} keys.`);
40
+ }
41
+ catch (error) {
42
+ console.warn(`Error during source analysis: ${error.message}`);
43
+ // Don't throw to prevent blocking the whole app, just log
44
+ }
45
+ })();
46
+ return this.initPromise;
47
+ }
48
+ async ensureInitialized() {
49
+ if (this.initPromise) {
50
+ await this.initPromise;
51
+ }
52
+ }
53
+ processFile(sourceFile) {
54
+ sourceFile.forEachDescendant((node) => {
55
+ if (Node.isCallExpression(node)) {
56
+ this.processCallExpression(node, sourceFile.getFilePath());
57
+ }
58
+ });
59
+ }
60
+ processCallExpression(node, filePath) {
61
+ // Match t('key') or useTranslations().t('key') pattern
62
+ // This is a heuristic match
63
+ const expression = node.getExpression();
64
+ // Check if expression is accessible
65
+ if (!expression)
66
+ return;
67
+ const expressionText = expression.getText();
68
+ // Simple check for 't' function calls
69
+ if (expressionText.endsWith(".t") || expressionText === "t") {
70
+ const args = node.getArguments();
71
+ if (args.length > 0 && Node.isStringLiteral(args[0])) {
72
+ const key = args[0].getLiteralText();
73
+ this.extractContext(key, node, filePath);
74
+ }
75
+ }
76
+ }
77
+ extractContext(key, node, fullFilePath) {
78
+ const relativePath = path.relative(this.rootDir, fullFilePath);
79
+ const line = node.getStartLineNumber();
80
+ // Extract component name
81
+ let componentName = undefined;
82
+ // Use getAncestors() to find the parent safely
83
+ const componentNode = node
84
+ .getAncestors()
85
+ .find((n) => Node.isFunctionDeclaration(n) ||
86
+ Node.isClassDeclaration(n) ||
87
+ Node.isVariableDeclaration(n));
88
+ if (componentNode) {
89
+ if (Node.isFunctionDeclaration(componentNode) ||
90
+ Node.isClassDeclaration(componentNode)) {
91
+ componentName = componentNode.getName();
92
+ }
93
+ else if (Node.isVariableDeclaration(componentNode)) {
94
+ componentName = componentNode.getName();
95
+ }
96
+ }
97
+ // Extract comments
98
+ // Look for leading comments on the statement
99
+ const statement = node
100
+ .getAncestors()
101
+ .find((n) => Node.isExpressionStatement(n) ||
102
+ Node.isVariableStatement(n) ||
103
+ Node.isJsxElement(n) ||
104
+ Node.isJsxExpression(n));
105
+ const comments = [];
106
+ const targetNode = statement || node;
107
+ const ranges = targetNode.getLeadingCommentRanges();
108
+ for (const range of ranges) {
109
+ comments.push(range
110
+ .getText()
111
+ .replace(/^\/\/\s*/, "")
112
+ .replace(/^\/\*\s*/, "")
113
+ .replace(/\s*\*\/$/, ""));
114
+ }
115
+ // Get props (simple heuristic for JSX)
116
+ const props = {};
117
+ const jsxElement = node
118
+ .getAncestors()
119
+ .find((n) => Node.isJsxOpeningElement(n) || Node.isJsxSelfClosingElement(n));
120
+ if (jsxElement &&
121
+ (Node.isJsxOpeningElement(jsxElement) || Node.isJsxSelfClosingElement(jsxElement))) {
122
+ jsxElement.getAttributes().forEach((attr) => {
123
+ if (Node.isJsxAttribute(attr)) {
124
+ // Safe name extraction
125
+ const nameNode = attr.getNameNode();
126
+ const name = nameNode ? nameNode.getText() : "unknown";
127
+ const initializer = attr.getInitializer();
128
+ let value = initializer ? initializer.getText() : "true";
129
+ // Strip quotes if string literal
130
+ if ((value.startsWith('"') && value.endsWith('"')) ||
131
+ (value.startsWith("'") && value.endsWith("'"))) {
132
+ value = value.substring(1, value.length - 1);
133
+ }
134
+ props[name] = value;
135
+ }
136
+ });
137
+ }
138
+ // Code Snippet (surrounding lines)
139
+ // Check for node.getParent() to avoid error if node is root (unlikely for call expression)
140
+ const parent = node.getParent();
141
+ const snippet = parent ? parent.getText() : node.getText();
142
+ const context = {
143
+ filePath: relativePath,
144
+ line,
145
+ component: componentName,
146
+ usageSnippet: snippet.length > 200 ? snippet.substring(0, 200) + "..." : snippet,
147
+ comments: comments.length > 0 ? comments : undefined,
148
+ props: Object.keys(props).length > 0 ? props : undefined,
149
+ };
150
+ if (!this.keyUsageMap.has(key)) {
151
+ this.keyUsageMap.set(key, []);
152
+ }
153
+ this.keyUsageMap.get(key)?.push(context);
154
+ }
155
+ getContext(key) {
156
+ // Return duplicate functionality: prefer the first usage found or merge?
157
+ // For now return the first usage that has comments, or just the first usage.
158
+ const usages = this.keyUsageMap.get(key);
159
+ if (!usages || usages.length === 0)
160
+ return undefined;
161
+ // Prioritize usage with comments
162
+ return usages.find((u) => u.comments && u.comments.length > 0) || usages[0];
163
+ }
164
+ }
@@ -9,6 +9,7 @@ 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
11
  import { LanguageProcessor } from "./language-processor.js";
12
+ import { SourceCodeAnalyzer } from "./source-analyzer.js";
12
13
  /**
13
14
  * Service for handling file translation operations.
14
15
  * Orchestrates the translation process including state management,
@@ -151,6 +152,10 @@ class TranslationService {
151
152
  const languageConcurrency = options.concurrencyLimit || 3;
152
153
  const targetLanguages = [...(options.targets || [])];
153
154
  await uiManager.log(`Processing ${targetLanguages.length} languages with concurrency of ${languageConcurrency}`);
155
+ // Initialize shared SourceCodeAnalyzer
156
+ console.log("Initializing Locai Engine...");
157
+ const sharedAnalyzer = new SourceCodeAnalyzer();
158
+ sharedAnalyzer.initialize();
154
159
  // Create shared orchestrators array to collect review queues
155
160
  const orchestrators = [];
156
161
  for (let i = 0; i < targetLanguages.length; i += languageConcurrency) {
@@ -167,6 +172,7 @@ class TranslationService {
167
172
  ...options,
168
173
  concurrencyLimit: 1,
169
174
  progressOptions,
175
+ sourceCodeAnalyzer: sharedAnalyzer,
170
176
  });
171
177
  orchestrators.push(orchestrator);
172
178
  return this.processLanguage(targetLang, resolvedFile, flattenedSource, orchestrator, { ...options, progressOptions }, globalStats, comparison);
@@ -0,0 +1,13 @@
1
+ export interface CodeContext {
2
+ filePath: string;
3
+ line: number;
4
+ component?: string;
5
+ usageSnippet?: string;
6
+ comments?: string[];
7
+ props?: Record<string, string>;
8
+ }
9
+ export interface SourceAnalyzerConfig {
10
+ enabled?: boolean;
11
+ include?: string[];
12
+ exclude?: string[];
13
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "locai",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "type": "module",
5
5
  "main": "dist/core/orchestrator.js",
6
6
  "types": "dist/core/orchestrator.d.ts",
@@ -16,7 +16,7 @@
16
16
  "start": "node dist/core/orchestrator.js",
17
17
  "dev": "tsx src/core/orchestrator.js",
18
18
  "build": "tsc",
19
- "type-check": "tsc --noEmit",
19
+ "typecheck": "tsc --noEmit",
20
20
  "lint": "eslint .",
21
21
  "lint:fix": "eslint . --fix",
22
22
  "format": "prettier --write \"**/*.{js,json,md,jsx,ts,tsx,css}\"",
@@ -72,8 +72,8 @@
72
72
  "./translator": "./dist/commands/translator.js"
73
73
  },
74
74
  "dependencies": {
75
- "axios": "1.13.1",
76
- "c12": "^3.3.2",
75
+ "axios": "1.13.2",
76
+ "c12": "^3.3.3",
77
77
  "commander": "^14.0.2",
78
78
  "dot-properties": "^1.1.0",
79
79
  "dotenv": "^17.2.3",
@@ -81,25 +81,26 @@
81
81
  "gettext-parser": "^8.0.0",
82
82
  "jiti": "^2.6.1",
83
83
  "js-yaml": "^4.1.1",
84
- "lru-cache": "^11.2.2",
84
+ "lru-cache": "^11.2.4",
85
85
  "p-limit": "^7.2.0",
86
86
  "prompts": "^2.4.2",
87
+ "ts-morph": "^27.0.2",
87
88
  "vectra": "^0.11.1"
88
89
  },
89
90
  "devDependencies": {
90
- "@eslint/js": "^9.39.1",
91
+ "@eslint/js": "^9.39.2",
91
92
  "@types/js-yaml": "^4.0.9",
92
- "@types/node": "^25.0.1",
93
+ "@types/node": "^25.0.3",
93
94
  "@types/prompts": "^2.4.9",
94
- "@vitest/coverage-v8": "^4.0.15",
95
- "eslint": "^9.39.1",
95
+ "@vitest/coverage-v8": "^4.0.16",
96
+ "eslint": "^9.39.2",
96
97
  "eslint-config-prettier": "^10.1.8",
97
98
  "globals": "^16.5.0",
98
- "prettier": "^3.6.2",
99
+ "prettier": "^3.7.4",
99
100
  "std-mocks": "^2.0.0",
100
101
  "tsx": "^4.21.0",
101
102
  "typescript": "^5.9.3",
102
- "vitest": "^4.0.15"
103
+ "vitest": "^4.0.16"
103
104
  },
104
105
  "files": [
105
106
  "dist/"