@xtatistix/autotranslate
v0.2.10
Published
Attribute-based DOM auto-translation for web apps
Keywords
Readme
auto-translate
DOM-driven auto-translation for web apps. Pairs input fields via data attributes, detects empty targets on blur, and batch-translates through pluggable providers (DeepL, Google, LLM). Includes built-in free fallback providers (MyMemory -> Glosbe, no API keys) for resilient translation. Framework-agnostic — works with Angular, React, Vue, or vanilla JS.
How it works
Annotate your inputs with data-at-key and data-at-locale. When a user fills in one language and leaves the field, auto-translate detects the empty pair and fills it.
<label>Turkish</label>
<input data-at-key="edu" data-at-locale="tr" value="İlkokul" />
<label>English</label>
<input data-at-key="edu" data-at-locale="en" value="" />
<!-- ↑ auto-filled with "Primary School" on blur -->No wrapper components. No framework bindings. Just attributes.
Install
npm install @istabot/autotranslateQuick start
import { AutoTranslate } from '@istabot/autotranslate';
AutoTranslate.init({
providers: {
short: [{ type: 'deepl', apiKey: 'YOUR_KEY' }],
long: [{ type: 'llm', apiKey: 'YOUR_KEY', model: 'gpt-4o-mini' }]
},
sourceLocale: 'tr',
targetLocales: ['en'],
});That's it. Every data-at-key pair in the DOM is now live.
You can also omit providers completely. In that case, auto-translate uses built-in free fallback providers (mymemory -> glosbe).
Production setup (no API keys in the browser)
AutoTranslate.init({
providers: {
short: [{ type: 'proxy', endpoint: '/api/translate' }],
long: [{ type: 'proxy', endpoint: '/api/translate' }]
},
sourceLocale: 'tr',
targetLocales: ['en'],
});Your backend receives { texts, from, to, context } and returns { translations: string[] }.
Data attributes
| Attribute | Required | Description |
|---|---|---|
| data-at-key | yes | Groups inputs that represent the same translatable content |
| data-at-locale | yes | BCP 47 locale code (tr, en, de, ar, etc.) |
| data-at-context | no | Domain hint for better translations (e.g., "medical", "statistics") |
| data-at-maxlength | no | Truncate translation to N characters |
Configuration
AutoTranslate.init({
// --- Providers (optional) ---
providers: {
short: [{ type: 'deepl', apiKey: '...' }],
long: [{ type: 'llm', apiKey: '...', model: 'gpt-4o-mini' }]
},
// If omitted, built-in free fallback providers are used.
// --- Routing ---
threshold: 50, // characters — above this → long provider
// --- Locales ---
sourceLocale: 'tr',
targetLocales: ['en'],
// --- Trigger ---
trigger: 'blur', // 'blur' | 'manual'
enableGuard: () => true, // return false → package does nothing
// --- Batching ---
batch: {
debounceMs: 300,
maxSize: 50,
},
// --- Behavior ---
overwrite: false, // true → re-translate even when target has value
root: document, // narrow the scope to a specific container
context: 'statistics', // global domain hint for LLM translations
});Events
const at = AutoTranslate.init({ ... });
at.on('translated', (results) => {
console.log(results);
// [{ key: 'edu', locale: 'en', value: 'Primary School' }]
});
at.on('error', (err) => {
console.error(err.key, err.message);
});
at.on('before-send', (batch) => { /* inspect or log */ });
at.on('skipped', (reason) => { /* enable guard off, target not empty, etc. */ });Manual control
at.translateAll(); // translate all empty targets now
at.scan(); // re-scan DOM after dynamic content changes
at.update({ // patch one or multiple config fields without re-init
sourceLocale: 'de',
targetLocales: ['en', 'fr'],
});
at.destroy(); // remove all listeners and observersTrigger direction
- Translation is one-way from
sourceLocaleto target locales. - Blurring a target-locale field never triggers translation.
- In blur mode, only fields that match
sourceLocalecan trigger translation.
Dynamic DOM
auto-translate detects new inputs added after init (Angular *ngFor, React lists, vanilla JS) via MutationObserver. Enable it explicitly:
AutoTranslate.init({
observe: true,
root: document.querySelector('.editor-panel'),
// ...
});Providers
| Provider | Best for | Batch support | API key required |
|---|---|---|---|
| deepl | Short labels, UI text | yes (array) | yes |
| google | Short labels, UI text | yes (array) | yes |
| llm | Long text, domain-specific terms | yes (JSON array prompt) | yes |
| proxy | Production (keys stay on server) | yes | no (server-side) |
| mymemory | Free fallback translation | no (single request per text) | no |
| glosbe | Free fallback when MyMemory fails | no (single request per text) | no |
Silent fallback chain
You can pass a provider chain for automatic failover. The package retries with bounded backoff and silently moves to the next provider when one fails.
Built-in fallback behavior:
- Your configured provider chain is tried first.
mymemoryis automatically appended as free fallback.glosbeis automatically appended aftermymemory.- No API key is required for these built-in fallbacks.
AutoTranslate.init({
providers: {
short: [
{ type: 'google', apiKey: 'GOOGLE_KEY' },
{ type: 'deepl', apiKey: 'DEEPL_KEY' },
{ type: 'proxy', endpoint: '/api/translate' }
],
long: [
{ type: 'llm', apiKey: 'OPENAI_KEY', model: 'gpt-4o-mini' },
{ type: 'proxy', endpoint: '/api/translate' }
]
}
});error is emitted only when all configured providers and built-in free fallbacks (mymemory, glosbe) fail.
Custom provider
import type { TranslationProvider } from '@istabot/autotranslate';
const myProvider: TranslationProvider = {
translate: async (texts, from, to, context?) => {
const res = await fetch('/my-endpoint', {
method: 'POST',
body: JSON.stringify({ texts, from, to, context }),
});
const data = await res.json();
return data.translations; // string[], same order as input
}
};
AutoTranslate.init({
providers: {
short: myProvider,
long: myProvider,
},
// ...
});Framework examples
Angular
// app.component.ts
import { AutoTranslate } from '@istabot/autotranslate';
ngOnInit() {
this.at = AutoTranslate.init({
root: this.editorRef.nativeElement,
observe: true,
providers: {
short: [{ type: 'proxy', endpoint: '/api/translate' }],
long: [{ type: 'proxy', endpoint: '/api/translate' }],
},
sourceLocale: 'tr',
targetLocales: ['en'],
enableGuard: () => this.syncLabels,
});
}
onSyncLabelsChange(value: boolean) {
this.syncLabels = value;
this.at.update({
enableGuard: () => this.syncLabels,
});
}
ngOnDestroy() {
this.at.destroy();
}React
import { AutoTranslate } from '@istabot/autotranslate';
function VariableEditor() {
const containerRef = useRef(null);
const atRef = useRef(null);
const [sync, setSync] = useState(false);
useEffect(() => {
const at = AutoTranslate.init({
root: containerRef.current,
observe: true,
providers: {
short: [{ type: 'proxy', endpoint: '/api/translate' }],
long: [{ type: 'proxy', endpoint: '/api/translate' }],
},
sourceLocale: 'tr',
targetLocales: ['en'],
enableGuard: () => sync,
});
atRef.current = at;
return () => at.destroy();
}, []);
useEffect(() => {
atRef.current?.update({
enableGuard: () => sync,
});
}, [sync]);
return <div ref={containerRef}>...</div>;
}Design decisions
Why data attributes instead of a component API? Framework-agnostic by design. A React hook or Angular directive can wrap auto-translate, but the core doesn't depend on any framework. This means it works in server-rendered HTML, CMS editors, legacy jQuery apps — anything with a DOM.
Why hybrid providers? Short labels like "İlkokul" → "Primary School" don't need an LLM. A translation API is faster, cheaper, and more deterministic. LLMs are reserved for longer text where context and domain knowledge matter.
Why blur, not keystroke? Translating on every keystroke wastes API calls on incomplete words. Blur means the user finished typing. The debounce window batches multiple fields changed in quick succession.
Why no overwrite by default? If a user manually typed an English label, auto-translate should not silently replace it. Overwrite is opt-in.
Bundle
- Zero runtime dependencies
- ESM + CJS + TypeScript declarations
- Tree-shakeable — unused providers are excluded
- Target: < 5KB gzipped (core only)
Current verification (local): dist/index.js gzipped size is below 5KB.
Release checklist
npm test
npm run test:coverage
npm run build
npm run size
npm run publish:github
npm run publish:npmRequirements
- Node 18+
- Modern browsers (Chrome, Firefox, Safari, Edge)
- TypeScript 5+ (for type-checking only — JavaScript works fine)
License
MIT
Credits
Built by istabot.
Inspired by the real-world pain of manually translating hundreds of variable labels in statistical analysis software.
