@molecule/api-ai-translation-madlad
Provider bond · ai-translation · API (Node) · v1.1.1 · Apache-2.0
Self-hosted translation provider for molecule.dev — Google MADLAD-400 (400+ languages) through a small CTranslate2 sidecar server, shipped with the package
npm install @molecule/api-ai-translation-madladnpm · Source on GitHub · Implements @molecule/api-ai-translation
How it works
@molecule/api-ai-translation-madlad 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.
# Start the sidecar (downloads the 2.96 GB model into the volume on first start):
docker build -t madlad-sidecar node_modules/@molecule/api-ai-translation-madlad/sidecar
docker run -d -p 8765:8765 -v madlad-model:/model madlad-sidecar
# …or without Docker (Python 3.10+):
pip install -r node_modules/@molecule/api-ai-translation-madlad/sidecar/requirements.txt
python node_modules/@molecule/api-ai-translation-madlad/sidecar/server.pyWorks with: @molecule/api-ai-translation
Reference
Self-hosted translation provider for molecule.dev, built on Google's MADLAD-400 model.
Implements the @molecule/api-ai-translation contract against a small HTTP
server that runs MADLAD-400 3B (int8) in CTranslate2. CTranslate2 has no
Node binding, so the model runs in that server — which ships inside this
package (sidecar/: server.py, requirements.txt, Dockerfile) — and the
provider only needs its URL.
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.tsJSDoc, not this file.
Quick Start
# Start the sidecar (downloads the 2.96 GB model into the volume on first start):
docker build -t madlad-sidecar node_modules/@molecule/api-ai-translation-madlad/sidecar
docker run -d -p 8765:8765 -v madlad-model:/model madlad-sidecar
# …or without Docker (Python 3.10+):
pip install -r node_modules/@molecule/api-ai-translation-madlad/sidecar/requirements.txt
python node_modules/@molecule/api-ai-translation-madlad/sidecar/server.py
import { setProvider, requireProvider } from '@molecule/api-ai-translation'
import { createProvider } from '@molecule/api-ai-translation-madlad'
setProvider(createProvider({ baseUrl: 'http://madlad:8765' })) // or MADLAD_BASE_URL
const { translations } = await requireProvider().translate({
text: ['{{count}} items in your cart', 'Checkout'],
targetLang: 'kk',
protect: ['{{count}}'],
})
Type
provider
Installation
npm install @molecule/api-ai-translation-madlad @molecule/api-ai-translation
API
Interfaces
MarkerForm
One way of writing an indexed placeholder.
interface MarkerForm {
/** Short name, for diagnostics. */
name: string
/** Writes marker N. */
write: (index: number) => string
/** Finds markers in a translation; group 1 is the index. */
find: RegExp
}
MadladConfig
Configuration for the MADLAD-400 translation provider.
interface MadladConfig {
/**
* URL of a running MADLAD sidecar (`sidecar/server.py` in this package). Defaults to
* `MADLAD_BASE_URL`, then `http://127.0.0.1:8765`.
*/
baseUrl?: string
/** Bearer token, if the sidecar was started with `MADLAD_API_KEY`. Defaults to that env var. */
apiKey?: string
/** Texts per HTTP request. Defaults to 32. */
batchSize?: number
/** Beam width: 1 is fastest; 2–4 is slower and sometimes better. Defaults to 1. */
beamSize?: number
/** Longest translation, in tokens. Defaults to 256. */
maxLength?: number
/** Request timeout in milliseconds. Defaults to 10 minutes (CPU translation is slow). */
timeoutMs?: number
/** Replaces `fetch` — for tests or a proxy-aware client. */
fetch?: typeof fetch
}
Functions
toMadladLanguage(code, known)
Reduces a caller's language code to MADLAD's target token (without <2 >).
A code whose exact form is a MADLAD token (e.g. ms_Arab) is kept as is;
otherwise the region is dropped (pt-BR → pt, es-MX → es).
function toMadladLanguage(code: string, known?: ReadonlySet<string>): string
code— Any BCP-47-ish code (de,DE,pt-BR,zh-TW,nb).known— MADLAD's token list, when known, to keep exact matches.
Returns: The token code (de, pt, zh_Hant, no).
languageName(code)
English name of a MADLAD language, for getSupportedLanguages.
function languageName(code: string): string
code— A MADLAD token code (de,zh_Hant,ms_Arab).
Returns: The English name, or the code when the runtime has no name for it.
splitProtected(text, protect)
Splits a text at its protected substrings.
function splitProtected(text: string, protect: string[]): { pieces: string[]; originals: string[] }
text— The caller's text.protect— Literal substrings to protect.
Returns: pieces (length n+1) around the n originals.
maskWith(pieces, form)
Masks a split text with one marker form.
function maskWith(pieces: string[], form: MarkerForm): string
pieces— Text around the placeholders.form— Marker shape.
Returns: The masked text.
markersIntact(translated, count, form)
Whether a translation carries every marker exactly once and no others.
function markersIntact(translated: string, count: number, form: MarkerForm): boolean
translated— The model's output.count— How many placeholders the source had.form— Marker shape.
Returns: True when markers 0…count-1 each appear once.
unmaskWith(translated, originals, form)
Restores markers by index.
function unmaskWith(translated: string, originals: string[], form: MarkerForm): string
translated— The model's output.originals— The protected substrings.form— Marker shape.
Returns: The restored text.
isTranslatable(piece)
Whether a piece needs translating (has a letter or number, not just spacing/punctuation).
function isTranslatable(piece: string): boolean
piece— Text between placeholders.
Returns: True when the model should see it.
assemblePieces(pieces, translatedPieces, originals)
Re-assembles a text from separately translated pieces, keeping each piece's original leading/trailing spacing so placeholders stay separated as before.
function assemblePieces(pieces: string[], translatedPieces: string[], originals: string[]): string
pieces— Source pieces.translatedPieces— Translation of each piece (untranslatable ones as-is).originals— The protected substrings.
Returns: The assembled text.
createProvider(config)
Creates a MADLAD-400 translation provider.
function createProvider(config?: MadladConfig): AITranslationProvider
config— Sidecar URL, key, batching and decoding options.
Returns: An AITranslationProvider backed by a MADLAD sidecar.
Constants
MARKER_FORMS
The marker shapes tried in order.
const MARKER_FORMS: MarkerForm[]
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-madlad'
export function setupAiTranslationMadlad(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-ai-translation>=1.1.0
Runtime Dependencies
-
@molecule/api-ai-translation -
Languages: over 400 targets, including all of molecule's 79 UI languages (
zh-TWmaps to MADLAD'szh_Hant,nbtono,es-MX/pt-BRtoes/pt— MADLAD has no regional variants). The source language is read from the text;sourceLangis only echoed back asdetectedSourceLang. -
Needs the sidecar running.
baseUrl(MADLAD_BASE_URL, defaulthttp://127.0.0.1:8765); if the sidecar was started withMADLAD_API_KEY, give the provider the same key (apiKeyor the same env var). Keep the sidecar on a private network — without a key anyone who can reach it can translate on your CPU. -
protectplaceholders are the weak spot. MADLAD has no markup mode and drops or rewrites placeholders more often than DeepL/Google. Each placeholder is sent as an indexed marker ([0], then<x0/>, then⟦0⟧for texts that lost one); a text that still loses one is translated in pieces around its placeholders, which always keeps them but can read like English word order. Check every result before storing it. -
Speed: CPU-bound and slow for its size. Expect a few short strings per second on a many-core CPU (the sidecar uses every core per batch by default; see
MADLAD_INTRA_THREADS/MADLAD_INTER_THREADS). For bulk jobs run the sidecar on a GPU (MADLAD_DEVICE=cuda,MADLAD_COMPUTE_TYPE=int8_float16). Requests time out aftertimeoutMs(default 10 minutes). -
Sentence-level model: translate UI strings or single paragraphs; split long documents first (
maxLength, default 256 tokens, caps the output). -
formality,glossaryId,context,modelType,preserveFormattingandtagHandlinghave no MADLAD equivalent and are ignored. -
Licences: MADLAD-400 weights Apache-2.0 (Google); the int8 conversion used by default (
Nextcloud-AI/madlad400-3b-mt-ct2-int8, pinned by commit and SHA-256) Apache-2.0; CTranslate2 MIT; SentencePiece Apache-2.0 — all fine for a commercial service.
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).