@dxv-systems/dynamic-translations
v0.1.2
Published
DXV translation orchestrator — batched LLM translation of admin-configured DB content via OpenRouter, with a pluggable persistence layer and lock-aware rebuild modes.
Maintainers
Readme
@dxv-systems/dynamic-translations
Batched LLM translation of admin-configured DB content via OpenRouter (default model: anthropic/claude-haiku-4.5), with a pluggable persistence layer and lock-aware rebuild modes.
Built for the case where the source language is Dutch and the target language set is small (DE / EN). The package does not know or care about your database; you provide a TranslationStore implementation that reads from and writes to wherever you keep translations.
Why
Hardcoded Dutch text → translated text maps couple translations to seed data and silently fall back to the source the moment the admin edits anything. This package lets an admin run two operations on demand:
- Fill gaps — translate sources that don't have a translation yet.
- Full rebuild — re-translate every source, except rows the admin has locked.
Install
npm install @dxv-systems/dynamic-translationsUsage
import {
makeTranslator,
rebuildTranslations,
type TranslationStore,
} from "@dxv-systems/dynamic-translations";
const translator = makeTranslator({
apiKey: process.env.OPENROUTER_API_KEY!,
appName: "Arcabo Checklist",
appUrl: "https://arcabo.example.com",
});
const store: TranslationStore = /* your DB-backed implementation */;
await rebuildTranslations({
sources: [
{ source: "Slaapkamers", refs: [{ entity: "ChecklistField", field: "bedrooms" }] },
{ source: "Bouwnorm", refs: [{ entity: "ChecklistField", field: "bouwnorm" }] },
// ...
],
targetLangs: ["de", "en"],
mode: "gaps", // or "full"
store,
translator,
onProgress: (done, total, lang) => console.log(`${lang}: ${done}/${total}`),
});Lock semantics
store.lockedSources(...) returns the set of sources the admin has manually edited. mode: "full" excludes those rows from the translator call and never writes over them. mode: "gaps" doesn't touch them either (they aren't missing).
TranslationStore
You implement this. Methods are batch-oriented because every wizard page touches dozens of sources at once.
interface TranslationStore {
getMany(sources: string[], lang: TargetLang): Promise<Map<string, string>>;
getAll(lang: TargetLang): Promise<TranslationRow[]>;
setMany(entries: Array<{ source; lang; translated; refs }>): Promise<void>;
setOne(args: { source; lang; translated; locked }): Promise<void>;
missing(sources: string[], lang: TargetLang): Promise<string[]>;
lockedSources(sources: string[], lang: TargetLang): Promise<Set<string>>;
}Costs
Claude Haiku 4.5 via OpenRouter is ~$0.25 / $1.25 per 1M input/output tokens. A label round-trips in ~10 tokens; ~200 labels × 2 languages costs ~$0.004 to translate the whole catalogue once. Locked rows and the gaps mode keep recurring costs near zero.
