You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

locai

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

locai - npm Package Compare versions

Comparing version
2.4.0
to
2.5.0
+9
dist/core/pipeline/steps/ContextEnrichmentStep.d.ts
import PipelineStep from "../PipelineStep.js";
import { TranslationContext } from "../context.js";
import { SourceCodeAnalyzer } from "../../../services/source-analyzer.js";
export default class ContextEnrichmentStep extends PipelineStep {
private analyzer;
private enabled;
constructor(analyzer: SourceCodeAnalyzer, enabled?: boolean);
execute(context: TranslationContext, next: () => Promise<void>): Promise<void>;
}
import PipelineStep from "../PipelineStep.js";
export default class ContextEnrichmentStep extends PipelineStep {
analyzer;
enabled;
constructor(analyzer, enabled = true) {
super();
this.analyzer = analyzer;
this.enabled = enabled;
}
async execute(context, next) {
if (this.enabled) {
try {
// Ensure analysis is complete before checking for context
await this.analyzer.ensureInitialized();
const codeContext = this.analyzer.getContext(context.key);
if (codeContext) {
// Enrich the context meta
context.meta = {
...context.meta,
code: codeContext,
};
// Also append to the AI prompt context if it exists
// (Assuming existing translation prompt uses contextData)
context.contextData = {
...(context.contextData || {}),
visualContext: {
component: codeContext.component,
file: codeContext.filePath,
comments: codeContext.comments,
props: codeContext.props,
snippet: codeContext.usageSnippet,
},
};
}
}
catch (error) {
// Non-blocking error
// console.warn(`Context enrichment failed for ${context.key}`, error);
}
}
await next();
}
}
import { CodeContext } from "../types/context.js";
export declare class SourceCodeAnalyzer {
private project;
private keyUsageMap;
private rootDir;
private initPromise;
constructor(rootDir?: string);
initialize(globPatterns?: string[]): Promise<void>;
ensureInitialized(): Promise<void>;
private processFile;
private processCallExpression;
private extractContext;
getContext(key: string): CodeContext | undefined;
}
import { Project, Node } from "ts-morph";
import { log } from "../utils/logger.js";
import path from "path";
export class SourceCodeAnalyzer {
project;
keyUsageMap = new Map();
rootDir;
initPromise = null;
constructor(rootDir = process.cwd()) {
this.rootDir = rootDir;
// Initialize with default settings, but don't add files yet for performance
this.project = new Project({
skipAddingFilesFromTsConfig: true,
compilerOptions: {
allowJs: true,
jsx: 1, // Preserve
},
});
}
initialize(globPatterns = [
"src/**/*.{ts,tsx}",
"app/**/*.{ts,tsx}",
"components/**/*.{ts,tsx}",
]) {
if (this.initPromise)
return this.initPromise;
this.initPromise = (async () => {
await new Promise((resolve) => setTimeout(resolve, 0)); // Force async execution for race condition safety
try {
// Add source files
log(`Analyzing source code in ${this.rootDir}...`);
// Exclude node_modules explicitly just in case
this.project.addSourceFilesAtPaths(globPatterns);
const sourceFiles = this.project.getSourceFiles();
log(`Found ${sourceFiles.length} source files for context analysis.`);
for (const sourceFile of sourceFiles) {
this.processFile(sourceFile);
}
log(`Source analysis complete. Context extracted for ${this.keyUsageMap.size} keys.`);
}
catch (error) {
console.warn(`Error during source analysis: ${error.message}`);
// Don't throw to prevent blocking the whole app, just log
}
})();
return this.initPromise;
}
async ensureInitialized() {
if (this.initPromise) {
await this.initPromise;
}
}
processFile(sourceFile) {
sourceFile.forEachDescendant((node) => {
if (Node.isCallExpression(node)) {
this.processCallExpression(node, sourceFile.getFilePath());
}
});
}
processCallExpression(node, filePath) {
// Match t('key') or useTranslations().t('key') pattern
// This is a heuristic match
const expression = node.getExpression();
// Check if expression is accessible
if (!expression)
return;
const expressionText = expression.getText();
// Simple check for 't' function calls
if (expressionText.endsWith(".t") || expressionText === "t") {
const args = node.getArguments();
if (args.length > 0 && Node.isStringLiteral(args[0])) {
const key = args[0].getLiteralText();
this.extractContext(key, node, filePath);
}
}
}
extractContext(key, node, fullFilePath) {
const relativePath = path.relative(this.rootDir, fullFilePath);
const line = node.getStartLineNumber();
// Extract component name
let componentName = undefined;
// Use getAncestors() to find the parent safely
const componentNode = node
.getAncestors()
.find((n) => Node.isFunctionDeclaration(n) ||
Node.isClassDeclaration(n) ||
Node.isVariableDeclaration(n));
if (componentNode) {
if (Node.isFunctionDeclaration(componentNode) ||
Node.isClassDeclaration(componentNode)) {
componentName = componentNode.getName();
}
else if (Node.isVariableDeclaration(componentNode)) {
componentName = componentNode.getName();
}
}
// Extract comments
// Look for leading comments on the statement
const statement = node
.getAncestors()
.find((n) => Node.isExpressionStatement(n) ||
Node.isVariableStatement(n) ||
Node.isJsxElement(n) ||
Node.isJsxExpression(n));
const comments = [];
const targetNode = statement || node;
const ranges = targetNode.getLeadingCommentRanges();
for (const range of ranges) {
comments.push(range
.getText()
.replace(/^\/\/\s*/, "")
.replace(/^\/\*\s*/, "")
.replace(/\s*\*\/$/, ""));
}
// Get props (simple heuristic for JSX)
const props = {};
const jsxElement = node
.getAncestors()
.find((n) => Node.isJsxOpeningElement(n) || Node.isJsxSelfClosingElement(n));
if (jsxElement &&
(Node.isJsxOpeningElement(jsxElement) || Node.isJsxSelfClosingElement(jsxElement))) {
jsxElement.getAttributes().forEach((attr) => {
if (Node.isJsxAttribute(attr)) {
// Safe name extraction
const nameNode = attr.getNameNode();
const name = nameNode ? nameNode.getText() : "unknown";
const initializer = attr.getInitializer();
let value = initializer ? initializer.getText() : "true";
// Strip quotes if string literal
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.substring(1, value.length - 1);
}
props[name] = value;
}
});
}
// Code Snippet (surrounding lines)
// Check for node.getParent() to avoid error if node is root (unlikely for call expression)
const parent = node.getParent();
const snippet = parent ? parent.getText() : node.getText();
const context = {
filePath: relativePath,
line,
component: componentName,
usageSnippet: snippet.length > 200 ? snippet.substring(0, 200) + "..." : snippet,
comments: comments.length > 0 ? comments : undefined,
props: Object.keys(props).length > 0 ? props : undefined,
};
if (!this.keyUsageMap.has(key)) {
this.keyUsageMap.set(key, []);
}
this.keyUsageMap.get(key)?.push(context);
}
getContext(key) {
// Return duplicate functionality: prefer the first usage found or merge?
// For now return the first usage that has comments, or just the first usage.
const usages = this.keyUsageMap.get(key);
if (!usages || usages.length === 0)
return undefined;
// Prioritize usage with comments
return usages.find((u) => u.comments && u.comments.length > 0) || usages[0];
}
}
export interface CodeContext {
filePath: string;
line: number;
component?: string;
usageSnippet?: string;
comments?: string[];
props?: Record<string, string>;
}
export interface SourceAnalyzerConfig {
enabled?: boolean;
include?: string[];
exclude?: string[];
}
+1
-0

@@ -29,2 +29,3 @@ #!/usr/bin/env node

.option("--localesDir <dir>", "Localization files directory", defaultConfig.localesDir)
.option("--no-deep-context", "Disable deep context analysis (source code scanning)")
.option("--debug", "Enable debug mode with verbose logging", false)

@@ -31,0 +32,0 @@ .option("--verbose", "Enable detailed diagnostic output", false);

@@ -13,2 +13,8 @@ import { ConfigLayer } from "c12";

/**
* Deep Context (Source Code Analysis)
* Scans source code for context (component name, comments, props)
* @default true
*/
deepContext?: boolean;
/**
* Filter by file extensions (e.g. ['.json', '.arb'])

@@ -15,0 +21,0 @@ */

@@ -9,4 +9,6 @@ import ProgressTracker from "../utils/progress-tracker.js";

import { ConfidenceSettings } from "./pipeline/steps/TranslationStep.js";
import { SourceCodeAnalyzer } from "../services/source-analyzer.js";
export interface OrchestratorOptions {
context: ContextConfig;
deepContext?: boolean;
progressOptions?: any;

@@ -82,2 +84,3 @@ styleGuide?: any;

vectorStats: VectorMemoryStats;
sourceAnalyzer: SourceCodeAnalyzer;
confidenceSettings: ConfidenceSettings;

@@ -84,0 +87,0 @@ private concurrencyLimit;

@@ -25,2 +25,4 @@ import rateLimiter from "../utils/rate-limiter.js";

import CacheWriteStep from "./pipeline/steps/CacheWriteStep.js";
import ContextEnrichmentStep from "./pipeline/steps/ContextEnrichmentStep.js";
import { SourceCodeAnalyzer } from "../services/source-analyzer.js";
// Vector Memory Imports

@@ -45,2 +47,4 @@ import { VectorStore } from "../services/vector-store.js";

vectorStats;
// Source Code Analysis
sourceAnalyzer;
confidenceSettings;

@@ -155,2 +159,22 @@ concurrencyLimit;

}
// Initialize Source Analyzer
if (options.sourceCodeAnalyzer) {
this.sourceAnalyzer = options.sourceCodeAnalyzer;
// Assumed initialized or self-initializing
}
else {
this.sourceAnalyzer = new SourceCodeAnalyzer();
// Fire and forget initialization (or await if strict)
// We'll trust it catches up or is fast enough for the first few items,
// or we can await it if we want to block startup.
// For CLI responsiveness, we might want to await it only if deep context is enabled.
const deepContextEnabled = options.deepContext !== false; // Enabled by default
if (deepContextEnabled) {
this.sourceAnalyzer.initialize().catch((err) => {
if (this.advanced.debug) {
console.warn("Source analyzer init failed:", err.message);
}
});
}
}
// Initialize Pipeline

@@ -188,2 +212,5 @@ this.pipeline = new Pipeline();

// 4. Translation
// 3.5 Context Enrichment (Deep Context)
const deepContextEnabled = this.options.deepContext !== false;
this.pipeline.use(new ContextEnrichmentStep(this.sourceAnalyzer, deepContextEnabled));
this.pipeline.use(new TranslationStep(this.options, this.confidenceSettings));

@@ -190,0 +217,0 @@ // 4.5 Vector Cache Write (save new translations)

@@ -12,2 +12,3 @@ import path from "path";

import { LanguageProcessor } from "./language-processor.js";
import { SourceCodeAnalyzer } from "./source-analyzer.js";
/**

@@ -155,2 +156,6 @@ * Service for handling file translation operations.

await uiManager.log(`Processing ${targetLanguages.length} languages with concurrency of ${languageConcurrency}`);
// Initialize shared SourceCodeAnalyzer
console.log("Initializing Locai Engine...");
const sharedAnalyzer = new SourceCodeAnalyzer();
sharedAnalyzer.initialize();
// Create shared orchestrators array to collect review queues

@@ -172,2 +177,3 @@ const orchestrators = [];

progressOptions,
sourceCodeAnalyzer: sharedAnalyzer,
});

@@ -174,0 +180,0 @@ orchestrators.push(orchestrator);

+12
-11
{
"name": "locai",
"version": "2.4.0",
"version": "2.5.0",
"type": "module",

@@ -19,3 +19,3 @@ "main": "dist/core/orchestrator.js",

"build": "tsc",
"type-check": "tsc --noEmit",
"typecheck": "tsc --noEmit",
"lint": "eslint .",

@@ -76,4 +76,4 @@ "lint:fix": "eslint . --fix",

"dependencies": {
"axios": "1.13.1",
"c12": "^3.3.2",
"axios": "1.13.2",
"c12": "^3.3.3",
"commander": "^14.0.2",

@@ -86,21 +86,22 @@ "dot-properties": "^1.1.0",

"js-yaml": "^4.1.1",
"lru-cache": "^11.2.2",
"lru-cache": "^11.2.4",
"p-limit": "^7.2.0",
"prompts": "^2.4.2",
"ts-morph": "^27.0.2",
"vectra": "^0.11.1"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@eslint/js": "^9.39.2",
"@types/js-yaml": "^4.0.9",
"@types/node": "^25.0.1",
"@types/node": "^25.0.3",
"@types/prompts": "^2.4.9",
"@vitest/coverage-v8": "^4.0.15",
"eslint": "^9.39.1",
"@vitest/coverage-v8": "^4.0.16",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"globals": "^16.5.0",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"std-mocks": "^2.0.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^4.0.15"
"vitest": "^4.0.16"
},

@@ -107,0 +108,0 @@ "files": [

@@ -16,2 +16,4 @@ # Locai

> **Need a permanent setup?** Check out our [Configuration Guide](./docs/configuration.md) to create a `localize.config.js` file.
## 📖 Documentation

@@ -41,3 +43,5 @@

- **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.
- **🧠 Smart Context Awareness**: Uses AI to understand the context of your strings for better translations.
- **🔬 Deep Context Analysis**: Scans your source code (AST) to extract component names, comments, and props for context-aware translations.
- **📚 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.
- **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.

@@ -44,0 +48,0 @@ - **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).