Architecture
Understand the architecture and workflow of Langnostic's translation system
Langnostic's architecture is built around two core components: the Runner and the Plugin System. The Runner orchestrates the translation workflow, while Plugins handle the specifics of different file formats.
Architecture overview
Langnostic separates concerns into two main components — (1) runner and (2) plugins
Runner
The runner is the orchestration engine that manages the entire translation lifecycle:
- Initialization: Loads configuration and sets up translation groups
- Discovery: Resolves file paths and initializes plugins with source/target files
- Translation: Creates job queues and manages concurrent AI translation requests
- Shutdown: Completes all jobs and reports results
Plugins
Plugins handle format-specific operations for different file types (JSON, MDX, etc.):
- Loading: Reads and parses files in their specific format
- Change Detection: Determines what content needs translation
- Writing: Writes translated content back to files while preserving format
Why this architecture?
This separation of concerns provides several benefits:
- Flexibility: New file formats can be added by creating plugins without modifying the runner
- Optimization: Each plugin can optimize for its format (e.g., hash-based detection for MDX, key comparison for JSON)
- Reusability: The runner handles all the complex orchestration (concurrency, batching, AI calls) once
- Extensibility: Plugin-specific instructions allow format-specific AI guidance
Translation flow
Here's how the runner and plugins work together when you run npx langnostic translate:
Phase 1: Initialization
Runner responsibilities:
- Loads your
langnostic.config.ts - Creates translation groups based on configuration
- Initializes AI provider settings
Example configuration:
The configuration tells the runner which AI model to use, what languages to translate between, and which plugins to use for each file types.
// langnostic.config.ts
export default {
ai: {
model: openai('gpt-4'),
},
localeConfig: {
source: 'en-US',
target: ['zh-CN', 'es-ES'],
},
groups: [
{
name: 'app',
plugin: JsonPlugin(),
paths: [
{
source: './src/locales/en-US.json',
target: './src/locales/{locale}.json',
},
],
},
],
} satisfies ConfigType;Phase 2: Discovery
Runner responsibilities:
- Expands path patterns (e.g.,
./locales/{locale}.json→./locales/zh-CN.json,./locales/es-ES.json) - Resolves source and target file pairs
- Passes file metadata to plugins
Plugin responsibilities:
- Receives file paths and locale information via
trackFiles() - Prepares internal state for tracking these files
Example:
// Runner expands this:
{
source: './src/locales/en-US.json',
target: './src/locales/{locale}.json',
}
// Into actual file pairs:
Source: ./src/locales/en-US.json
Targets: [
{ locale: 'zh-CN', path: './src/locales/zh-CN.json' },
{ locale: 'es-ES', path: './src/locales/es-ES.json' }
]Phase 3: Plugin loading & change detection
Plugin responsibilities:
- Read files: Parse source and target files in format-specific ways
- Detect changes: Determine what content needs translation
- Return translation strings: Provide the runner with strings to translate via
getTranslationStrings()
Each plugin implements its own change detection strategy:
JSON Plugin:
// Compares object keys
Source (en-US.json): { "welcome": "Hello", "new": "New key" }
Target (zh-CN.json): { "welcome": "你好" }
// Returns: "new" needs translation for zh-CNMDX Plugin:
// Uses hash-based detection
Source hash: "abc123" (changed from "abc122")
Registry hash: "abc122"
// Returns: Content needs retranslationThe plugin returns data in a standard format:
{
batchId: './src/locales/en-US.json', // Groups related strings
id: 'welcomeMessage', // Unique identifier
source: {
locale: 'en-US',
string: 'Welcome, {username}!',
description: 'Greeting for logged-in users'
},
targets: ['zh-CN', 'es-ES'] // Which locales need this
}Phase 4: Translation (runner orchestration)
Runner responsibilities:
- Create job queue: Batches strings into jobs based on
stringsPerRequest - Execute jobs concurrently: Runs multiple translation requests in parallel
- Call AI provider: Sends structured prompts with plugin-specific instructions
- Collect results: Gathers translated strings from AI responses
How batching works:
// Plugin says: "Process 50 strings per request"
Total strings: 120
// Runner creates jobs:
Job 1: Strings 1-50
Job 2: Strings 51-100
Job 3: Strings 101-120
// Executes with concurrency limit (default: 5 concurrent jobs)AI Translation request:
// Runner sends to AI:
{
prompt: `Translate these strings to zh-CN and es-ES.
${pluginInstructions} // From plugin.getInstructions()
Strings: [
{ id: "welcome", source: { locale: "en-US", string: "Hello" }, targets: ["zh-CN", "es-ES"] }
]`
}
// AI responds:
{
data: [
{
id: "welcome",
translations: [
{ locale: "zh-CN", string: "你好" },
{ locale: "es-ES", string: "Hola" }
]
}
]
}Phase 5: Generation (plugin writes files)
Runner responsibilities:
- Receives completed translation results from AI
- Calls plugin's
onTranslationBatchComplete()with results - Provides status updates
Plugin responsibilities:
- Receive translated strings: Gets results for a batch of completed translations
- Merge with existing content: Combines new translations with existing files
- Write files: Updates target files while preserving format and structure
Example (JSON Plugin):
// Receives from runner:
{
id: 'greeting',
translations: [
{ locale: 'zh-CN', string: '你好,{name}!' }
]
}
// Plugin merges into existing file:
Existing (zh-CN.json): { "welcome": "欢迎!" }
New translation: { "greeting": "你好,{name}!" }
// Writes:
{
"welcome": "欢迎!",
"greeting": "你好,{name}!"
}Example (MDX Plugin):
// Plugin also updates its registry
.langnostic/registry-abc123.json:
{
"content": {
"source": ["hash1", "hash2"],
"targets": {
"zh-CN": ["hash1", "hash2"] // Tracks what's been translated
}
}
}Phase 6: Shutdown
Runner responsibilities:
- Waits for all jobs to complete
- Reports translation statistics
- Reports any errors
Runner-plugin communication
The runner and plugins communicate through a well-defined interface:
interface LangnosticPlugin {
type: string; // Plugin identifier (e.g., 'json', 'mdx')
stringsPerRequest: number; // How many strings to translate per AI request
// Runner calls these methods in sequence:
trackFiles(files: TranslationFileMetadata[]): Promise<void>;
getTranslationStrings(): Promise<TranslationStringArg[]>;
onTranslationBatchComplete(results: TranslationStringResult[]): Promise<void>;
getInstructions?(): Promise<string>; // Optional AI-specific guidance
}Data flow
Runner → Plugin: trackFiles()
Passes: File paths and locale information
Plugin → Runner: getTranslationStrings()
Returns: Array of strings needing translation
Runner → AI Provider: Translate request
Sends: Batched strings + plugin instructions
AI Provider → Runner: Translation results
Returns: Translated strings
Runner → Plugin: onTranslationBatchComplete()
Passes: Completed translations
Plugin: Writes files
Updates: Target locale files