@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-madlad

npm · 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.py

Works 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.ts JSDoc, 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-TW maps to MADLAD's zh_Hant, nb to no, es-MX/pt-BR to es/pt — MADLAD has no regional variants). The source language is read from the text; sourceLang is only echoed back as detectedSourceLang.

  • Needs the sidecar running. baseUrl (MADLAD_BASE_URL, default http://127.0.0.1:8765); if the sidecar was started with MADLAD_API_KEY, give the provider the same key (apiKey or the same env var). Keep the sidecar on a private network — without a key anyone who can reach it can translate on your CPU.

  • protect placeholders 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 after timeoutMs (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, preserveFormatting and tagHandling have 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).