@molecule/api-ai-translation-llm

Provider bond · ai-translation · API (Node) · v1.1.1 · Apache-2.0

Language-model translation provider for molecule.dev — translates through any @molecule/api-ai model, hosted or self-hosted

npm install @molecule/api-ai-translation-llm

npm · Source on GitHub · Implements @molecule/api-ai-translation

How it works

@molecule/api-ai-translation-llm is a provider bond on the API (Node) side: it implements the ai-translation core interface (@molecule/api-ai-translation) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

import { setProvider as setAi } from '@molecule/api-ai'
import { provider as deepseek } from '@molecule/api-ai-deepseek'
import { setProvider, requireProvider } from '@molecule/api-ai-translation'
import { createProvider } from '@molecule/api-ai-translation-llm'

setAi(deepseek) // the app's AI provider
setProvider(createProvider()) // translate with it — or createProvider({ ai, model }) for another

const { translations } = await requireProvider().translate({
  text: ['{{count}} items in your cart', 'Checkout'],
  targetLang: 'de',
  protect: ['{{count}}'],
  context: 'Labels in an online store',
})

Works with: @molecule/api-ai, @molecule/api-ai-translation

Reference

Language-model translation provider for molecule.dev.

Implements the @molecule/api-ai-translation contract with any @molecule/api-ai provider, so translation can run on whichever model is cheapest or best for the job — a hosted model (DeepSeek, Qwen, GPT, Claude, …) or a self-hosted one through @molecule/api-ai-local (vLLM, Ollama, llama.cpp), with no translation-vendor account at all.

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { setProvider as setAi } from '@molecule/api-ai'
import { provider as deepseek } from '@molecule/api-ai-deepseek'
import { setProvider, requireProvider } from '@molecule/api-ai-translation'
import { createProvider } from '@molecule/api-ai-translation-llm'

setAi(deepseek) // the app's AI provider
setProvider(createProvider()) // translate with it — or createProvider({ ai, model }) for another

const { translations } = await requireProvider().translate({
  text: ['{{count}} items in your cart', 'Checkout'],
  targetLang: 'de',
  protect: ['{{count}}'],
  context: 'Labels in an online store',
})

Type

provider

Installation

npm install @molecule/api-ai-translation-llm @molecule/api-ai @molecule/api-ai-translation

API

Interfaces

MaskedText

A text prepared for the model, plus what is needed to restore it.

interface MaskedText {
  /** The text with each protected substring replaced by `⟦N⟧`. */
  masked: string
  /** The protected substrings, in order of occurrence. */
  originals: string[]
}

LlmTranslationConfig

Configuration for the language-model translation provider.

interface LlmTranslationConfig {
  /**
   * The `@molecule/api-ai` provider to translate with. Defaults to the provider
   * bonded as `ai` (`requireProvider()` from `@molecule/api-ai`) at first use —
   * pass one explicitly to translate with a different model than the app chats
   * with (e.g. a cheap model, or a self-hosted one via `@molecule/api-ai-local`).
   */
  ai?: AIProvider
  /** Model id passed to the AI provider. Defaults to the provider's own default model. */
  model?: string
  /** Texts per model request. Defaults to 40 — larger batches are cheaper but fail more often. */
  batchSize?: number
  /** Sampling temperature. Defaults to 0 (translation should be deterministic). */
  temperature?: number
  /**
   * Extra instructions appended to the system prompt — a style guide, a glossary
   * ("translate 'workspace' as 'Arbeitsbereich'"), or product context.
   */
  instructions?: string
}

Functions

languageName(code)

English name of a language code, falling back to the code itself. Accepts provider-flavored codes in any case (de, DE, PT-BR, zh-TW, ZH-HANT).

function languageName(code: string): string
  • code — A BCP-47-style language code.

Returns: The language's English name, e.g. 'German' or 'Chinese (Taiwan)'.

systemPrompt(targetLang, sourceLang, context, instructions)

Builds the system prompt for one target language.

function systemPrompt(
  targetLang: string,
  sourceLang: string | undefined,
  context: string | undefined,
  instructions: string | undefined,
): string
  • targetLang — Target language code.
  • sourceLang — Source language code, if known.
  • context — Caller-supplied context about the texts (not translated).
  • instructions — Extra standing instructions from the provider config.

Returns: The system prompt.

parseTranslations(answer, expected)

Parses the model's answer into an array of strings.

function parseTranslations(answer: string, expected: number): string[] | null
  • answer — The model's full text output.
  • expected — How many translations the request carried.

Returns: The translations, or null when the answer is not a same-length string array.

maskText(text, protect)

Replaces each protected substring (longest first) with a numbered token.

function maskText(text: string, protect: string[]): MaskedText
  • text — The caller's text.
  • protect — Literal substrings to protect.

Returns: The masked text and its protected substrings.

tokensIntact(translated, originals)

Whether a translation carries every token of its source exactly once and no others.

function tokensIntact(translated: string, originals: string[]): boolean
  • translated — The model's output for one text.
  • originals — The substrings maskText protected for that text.

Returns: True when every ⟦N⟧ is present once and none is extra.

unmaskText(translated, originals)

Restores protected substrings by index.

function unmaskText(translated: string, originals: string[]): string
  • translated — The model's output for one text.
  • originals — The substrings maskText protected for that text.

Returns: The restored text.

createProvider(config)

Creates a language-model translation provider.

function createProvider(config?: LlmTranslationConfig): AITranslationProvider
  • config — Which AI provider/model to use and how to batch.

Returns: An AITranslationProvider backed by a language model.

Constants

provider

The provider implementation.

const provider: AITranslationProvider

Core Interface

Implements @molecule/api-ai-translation interface.

Bond Wiring

Setup function to register this provider with the core interface:

import { setProvider } from '@molecule/api-ai-translation'
import { provider } from '@molecule/api-ai-translation-llm'

export function setupAiTranslationLlm(): void {
  setProvider(provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-ai >=1.0.0
  • @molecule/api-ai-translation >=1.1.0

Runtime Dependencies

  • @molecule/api-ai

  • @molecule/api-ai-translation

  • Uses the AI provider bonded as ai unless you pass one. To translate with a cheaper model than the app chats with, pass createProvider({ ai, model }). Spend is the AI provider's token spend — getUsage() only counts characters in this process (characterLimit is Infinity).

  • protect placeholders travel as numbered tokens (⟦0⟧) and are restored by index; the model may move them for word order. A batch whose answer is not a same-length JSON array, or that loses a token, is retried in halves down to one text. A text that still fails comes back WITHOUT its placeholders intact — so check every result holds its placeholders before storing it; never store the English source as a "translation" instead.

  • Language codes are free-form (de, DE, PT-BR, zh-TW): they are turned into English language names for the model. getSupportedLanguages() lists ~80 common languages for pickers; the model can do more.

  • context (e.g. "button labels in a banking app") and the config's instructions (a glossary or style guide) go into the prompt — use them: one-word UI strings are ambiguous ("Close" as a verb or an adjective), and context is the only fix.

  • formality, glossaryId, modelType, preserveFormatting and tagHandling have no model equivalent and are ignored.

  • Output quality depends on the model. Small models are weaker on low-resource languages (Yoruba, Lao, Zulu…); spot-check those.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip. The sandbox has a live AI provider, so translations run for real; output is non-deterministic, so assert on the resulting LANGUAGE/meaning, never an exact string:

  • Translating real text to a target language through the UI returns text ACTUALLY in that language — English→Spanish produces recognizably Spanish, not the original echoed back or left in English.
  • Switching the target language (from the picker populated by getSupportedLanguages('target')) changes the output language for the same input — the same source re-translates into the newly chosen language.
  • With sourceLang omitted the provider auto-detects: a known-language input comes back with the correct detectedSourceLang, and if the UI shows a detected-language label it names the right one.
  • Text already in the target language is left sensible — unchanged or a valid paraphrase, never mangled, doubled, or emptied.
  • Empty or untranslatable input (whitespace, emoji, a bare code snippet) is handled gracefully — a clear UI state, nothing crashes.
  • A provider failure (bad key, quota exhausted, network drop) surfaces a visible error in the UI, not an unhandled 500 or a silently blank result.
  • The translate call runs server-side only — the provider key never reaches the browser (check the network panel: no key in any request).