We are preparing to open source Langnostic for public use. Follow GreatFrontEnd on LinkedIn for updates or shoot us an email.

Langnostic

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-CN

MDX Plugin:

// Uses hash-based detection
Source hash: "abc123" (changed from "abc122")
Registry hash: "abc122"

// Returns: Content needs retranslation

The 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