Zero-dependency, framework-agnostic translation library using MutationObserver for automatic DOM translation
npm install auto-dom-i18n


auto-dom-i18n is a zero-dependency, framework-agnostic translation library that uses MutationObserver to automatically translate text content in your application.
It features Smart Masking, Inline Tag Support, and Context-Aware Resolution. Unlike traditional libraries that map static keys to strings, this library observes the DOM, detects natural language, and resolves the correct translation based on the user's gender or formalityβall without manual plumbing.
- Features
- Installation
- Quick Start
- Configuration
- Backend Requirements
- Programmatic API
- How It Works
- Browser Support
- Performance
- Security
- Debugging
- Framework Integration
- Contributors Guide
- License
* Automatic Detection: Uses MutationObserver to watch for new DOM elements or text changes.
* Natural Pluralization: Handles plurals automatically. "1 item" and "5 items" generate distinct translation keys, eliminating the need for complex client-side pluralization logic.
* Polymorphic Translation: Supports complex variants (gender, formality). The backend can return an object of variants, and the library automatically selects the most specific match based on the user's current context (e.g., female_formal).
* Smart Masking: Automatically identifies dynamic content (numbers and symbols, including date formats like 01/15/2024) and replaces them with placeholders. Proper nouns and other terms can be masked via the ignoreWords config. All-uppercase text is normalized to share a cache key with its lowercase equivalent. Casing is restored after translation.
* Rich Context Hook: The translation callback receives the original text, masked text, and extracted variables, giving your backend (or LLM) full context.
* Attribute Preservation: Automatically strips attributes (like href, class) from tags before translation and re-injects them.
* Inline Tag Support: Intelligently handles HTML tags like , , or as part of the sentence structure.
* Hybrid Caching: Instantly replaces text if the translation is known; queues async requests for unknown text.
---
``bash`
npm install auto-dom-i18nor
yarn add auto-dom-i18nor
pnpm add auto-dom-i18n
Initialize the library at the root of your application.
`javascript
import { I18nObserver } from 'auto-dom-i18n';
// 1. Define your configuration
const i18n = new I18nObserver({
locale: 'es', // Target language
// GLOBAL CONTEXT: Defines the current user state for variant resolution.
// These values are used to generate keys (e.g., "female", "female_formal")
context: {
gender: 'female',
formality: 'formal'
},
// FALLBACK CONTEXT: Defines the default values to use if the current context
// doesn't match any keys in the translation object.
fallbackContext: {
gender: 'neutral',
formality: 'neutral'
},
// ORDER MATTERS: Defines how compound keys are generated (e.g. "gender_formality")
contextOrder: ['gender', 'formality'],
// Words to treat as variables (never translated)
ignoreWords: ['Google', 'John Doe'],
// THE CALLBACK: Called when text is not in cache
onMissingTranslation: async (items, locale) => {
// 'items' contains: [{ masked: "Hello {{0}}", original: "Hello Mary", ... }]
const response = await fetch('/api/translate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items, target: locale })
});
// Return map/object. Can return null to only log/report.
return await response.json();
}
});
// 2. Start observing
i18n.start();
`
The I18nObserver constructor accepts a config object with the following properties:
| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| locale | string | Required | The target language code (e.g., 'en', 'fr', 'ja'). |onMissingTranslation
| | function | Required | Async function called when text is not in cache. Receives (items[], locale). Return a Map/Object to apply translations, or null to take no action. If the callback throws, the affected elements remain in their pending state and the error is logged to the console. |context
| | object | {} | Global state used to resolve polymorphic translations. Values must be strings. |fallbackContext
| | object | { gender: 'neutral', formality: 'neutral' } | The specific context values to use as a fallback if the current context fails to find a match in the translation variants. |contextOrder
| | string[] | ['gender', 'formality'] | The priority order used to construct compound keys for variants (e.g., gender_formality). |allowedInlineTags
| | string[] | ['a', 'b', 'i', 'u', 'strong', 'em', 'span', 'small', 'mark', 'del'] | HTML tags that are considered part of the sentence structure. |translatableAttributes
| | string[] | ['title', 'placeholder', 'alt', 'aria-label'] | HTML attributes to translate alongside text nodes. |ignoreSelectors
| | string[] | ['script', 'style', 'code'] | CSS selectors to ignore. Content inside these elements will never be observed or translated. |ignoreWords
| | string[] | [] | Proper nouns or terms to treat as variables. |initialCache
| | object | {} | A dictionary of pre-loaded translations keyed by masked string. Values can be a translated string or a variant object (e.g., { "Hello {{0}}": "Hola {{0}}" }). |rootElement
| | HTMLElement | document.body | The DOM element to observe. Use this to scope translation to a specific subtree. |debounceTime
| | number | 200 | Time in ms to wait before batching requests. |maxBatchSize
| | number | 50 | Maximum number of strings per request. |originalAttribute
| | string | 'data-i18n-original' | The attribute name used to store the original text on translated elements. |pendingAttribute
| | string | 'data-i18n-pending' | The attribute name added to elements while a translation is in-flight. |keyAttribute
| | string | 'data-i18n-key' | If this attribute is present on an element, its value is used as the cache key instead of the computed masked string. |ignoreAttribute
| | string | 'data-i18n-ignore' | The attribute name that marks an element (and its entire subtree) to be completely skipped by the observer. |debug
| | boolean | false | When enabled, each item in onMissingTranslation includes a debug field with DOM context for bug reporting. See Debugging. |
The items array passed to your callback contains objects with this structure:
` 'typescriptattribute:${string}
{
masked: string; // "Hello
original: string; // "Hello Mary" (For LLM Context)
variables: string[]; // ["Mary"]
debug?: { // Only present when debug: true
elementOpenTag: string; // '
childElements: Array<{ tag: string; classes: string }>;
source: 'text' | ;`
};
}
---
Since auto-dom-i18n is client-side only, you must provide a backend endpoint to perform the actual translation (e.g., database lookup, translation API, and/or an LLM).
`json`
{
"target": "es",
"items": [
{
"masked": "Welcome back, {{0}}",
"original": "Welcome back, John",
"variables": ["John"]
}
]
}
strings sent in the request, and the values are either a string or a variant object.Simple Response:
`json
{
"Welcome back, {{0}}": "Bienvenido de nuevo, {{0}}"
}
`Polymorphic Response (recommended for LLMs):
`json
{
"Welcome back, {{0}}": {
"male": "Bienvenido de nuevo, {{0}}",
"female": "Bienvenida de nuevo, {{0}}",
"formal": "Le damos la bienvenida, {{0}}"
}
}
`Important Implementation Notes:
1. Preserve Variables: Your translation logic must preserve the
{{0}}, {{1}} placeholders in the output string.
2. Preserve Tags: If the input contains , the output must also contain wrapping the corresponding translated text.
3. Context Hints: Pass the original string to your LLM prompt to help it understand context (e.g., that "Save" is a button, or "John" is a name), but always key your response by the masked string.> Privacy Note: All visible text content is sent to your
onMissingTranslation endpoint. If your application displays sensitive data, consider excluding those DOM regions using ignoreSelectors or the data-i18n-ignore attribute.---
π Programmatic API
While the library primarily works by observing the DOM, you can also interact with the internal cache programmatically. This is useful for SSR hydration, pre-fetching, or imperative usage.
$3
Connects the
MutationObserver to the configured rootElement and begins observing for text changes. Any existing text content is processed immediately.`javascript
i18n.start();
`$3
Manually load translations into the cache. This bypasses the network queue and marks these keys as 'resolved'.
`javascript
i18n.setTranslation('es', {
"Welcome {{0}}": "Bienvenido {{0}}",
"Save": { "male": "Guardar", "formal": "Almacenar" }
});
`$3
Retrieves the raw translation entry (string or variant object) from the cache. This is useful for debugging or inspecting what variants are available for a key.
`javascript
// Returns { "male": "Guardar", "formal": "Almacenar" }
const variants = i18n.getTranslation("Save", "es");
`$3
Imperatively translate a string using the current configuration and context. Returns the original text (with variables substituted) if no translation is found.
*
text: The string to translate (e.g. "Hello {{0}}")
variables: (Optional)* Array of strings to replace placeholders (e.g. ["World"])`javascript
// Returns "Bienvenido John" based on current context
const text = i18n.translate("Welcome {{0}}", ["John"]);
`$3
Updates the target locale and re-translates all observed nodes using the new locale's cache. Any uncached keys in the new locale will trigger
onMissingTranslation. The original text is preserved in the configured originalAttribute (default data-i18n-original) on each translated element, so switching locales does not require a page reload.`javascript
i18n.setLocale('fr');
`$3
Replaces the entire global context and re-resolves all visible translations using the new values. This is a full replacement, not a merge β any keys omitted from the new context will be removed. No network requests are made; only the variant resolution changes.
`javascript
i18n.setContext({ gender: 'male', formality: 'formal' });
`$3
Returns a copy of the current ignore words list.
`javascript
const words = i18n.getIgnoreWords(); // ['Google', 'John Doe']
`$3
Adds one or more words to the ignore list and re-translates all observed nodes. Duplicates and empty strings are silently skipped.
`javascript
i18n.addIgnoreWords('Acme', 'Jane Doe');
`$3
Removes one or more words from the ignore list and re-translates all observed nodes. Words not in the list are silently ignored.
`javascript
i18n.removeIgnoreWords('Google');
`$3
Replaces the entire ignore words list and re-translates all observed nodes.
`javascript
i18n.setIgnoreWords(['NewBrand', 'Jane']);
`$3
Disconnects the
MutationObserver and clears any pending translation queues. Does not clear the cache. Call start() to resume observation.`javascript
i18n.stop();
`$3
Returns a snapshot of the current translation cache for the given locale (or the current locale if omitted). Useful for persisting translations to
localStorage or IndexedDB and restoring them via initialCache.`javascript
const cache = i18n.getCache('es');
localStorage.setItem('i18n-cache-es', JSON.stringify(cache));
`$3
Flushes the translation cache for the given locale, or all locales if omitted. Subsequent DOM observations will re-trigger
onMissingTranslation for cleared keys.`javascript
i18n.clearCache('es');
`---
π How It Works
$3
The library watches the DOM for text changes. It intelligently masks variables and handles inline HTML to create a normalized "Cache Key".
The library uses three data attributes during translation (all configurable via options):
1.
data-i18n-original: Stores the original text when a translation is applied. The observer checks this attribute to skip nodes it has already translated, preventing infinite loops. It also enables setLocale() and setContext() to re-translate from the original source.
2. data-i18n-pending: Added to elements while a translation request is in-flight. Removed once the translation is applied. Use this for CSS-based FOUC mitigation (e.g., [data-i18n-pending] { visibility: hidden; }).
3. data-i18n-key: (Optional, user-provided) If present on an element, its value is used as the cache key instead of the computed masked string. This is useful when automatic masking produces an ambiguous key, or when you want to share a translation across elements with different source text.
4. data-i18n-ignore: (Optional, user-provided) If present on an element, the observer will completely skip that element and its entire subtree β no text, attributes, or mutations will be processed. Useful for excluding regions that contain sensitive data, code snippets, or content that should never be translated.Example:
* Original DOM:
Please click here to login.
* Masked Key: Please click
* Variable Map: maps to { href: "/login" }Note: The actual attributes (href, class) are stripped for the translation key but re-applied during restoration.
$3
Your backend can return a simple string, or a Variant Object if the translation depends on context (gender, formality, plurality).
#### Variant Data Format
The variant object uses keys that correspond to valid context combinations. Compound keys are joined by an underscore
_. Specific keys are preferred over generic fallbacks.Scenario:
* Current Context:
{ gender: 'female', formality: 'formal' }
* Fallback Context: { gender: 'male' }
* Context Order: ['gender', 'formality']
* Source Text: Welcome Backend Response:
`json
{
"Welcome {{0}} ": {
"male": "Bienvenido {{0}} ",
"female": "Bienvenida {{0}} ",
"female_formal": "Le damos la bienvenida a {{0}} ",
"formal": "Le damos la bienvenida {{0}} "
}
}
`#### Resolution Logic
The library attempts to find the most specific match using the Current Context. If no match is found, it attempts to resolve using the Fallback Context.
With the context above (
female + formal), the lookup order is:1. Exact Compound Match:
female_formal (Found! Uses this one)
2. Partial Match (Current): female
3. Partial Match (Current): formal
4. Fallback Match: male (Derived from fallbackContext.gender)$3
The library applies the chosen translation variant and re-injects all original variables and attributes.
* Result:
Le damos la bienvenida a Mary$3
Since the library observes the rendered DOM, pluralization is handled naturally by the masking process. You do not need to set a global
plural context.* Singular:
You have 1 apple becomes You have {{0}} apple.
* Plural: You have 5 apples becomes You have {{0}} apples.These result in two different cache keys, allowing your backend to provide distinct translations for each form without complex client-side logic.
> Note: This approach captures the plural forms present in the source language. For target languages with additional plural forms (e.g., Russian, Arabic, Polish), you can use additional context variables (e.g.,
{ plural: 'few' }) alongside your backend's CLDR plural rules to return the correct variant for each source key.---
π Browser Support
auto-dom-i18n relies on
MutationObserver, which is supported in all modern browsers:- Chrome 26+
- Firefox 14+
- Safari 6.1+
- Edge 12+
---
β‘ Performance
- Debounced Updates: Mutations are batched and debounced (default 200ms) to prevent excessive network requests and DOM re-renders.
- Hybrid Caching: Instant synchronous replacement for known strings; asynchronous queuing for new content.
- Selector Filtering: Use
ignoreSelectors to prevent the library from observing high-frequency or sensitive areas (like real-time charts or password fields).
- Cache Persistence: The translation cache is in-memory by default. To persist across page loads, use getCache() to export it to localStorage or IndexedDB, then pass it back as initialCache on the next initialization. This eliminates redundant network requests and reduces FOUC on repeat visits.$3
On first load, users may briefly see source-language text before translations arrive. To minimize this:
- Pre-load translations using
initialCache or setTranslation() before calling start().
- Hide pending elements with CSS: [data-i18n-pending] { visibility: hidden; }. The library adds this attribute while a translation is in-flight and removes it once applied.
- SSR hydration: Call setTranslation() with server-provided translations before start() to populate the cache immediately.---
π Security
The library reconstructs translated HTML by re-injecting inline tags and attributes into the DOM. Because these translations come from your backend, it is important to ensure the response is trustworthy.
- Tag allowlist: Only tags listed in
allowedInlineTags are permitted in restored output. Any tags not in the allowlist are escaped as plain text.
- Attribute stripping: Event handler attributes (e.g., onclick, onerror) are always stripped from restored tags, even if they appear in the translation response.
- Recommendation: Ensure your translation backend is authenticated and returns sanitized content. If using an LLM, validate responses before returning them to the client.---
π Debugging
When something looks wrong with how text is being captured or translated, enable
debug: true to get DOM context on every translation item. This makes it easy to understand what's happening and to file reproducible bug reports.`javascript
const i18n = new I18nObserver({
locale: 'es',
debug: true, // Enable debug mode
onMissingTranslation: async (items, locale) => {
for (const item of items) {
if (item.debug) {
console.log('Translation item:', {
masked: item.masked,
original: item.original,
variables: item.variables,
debug: item.debug,
});
}
}
// ... your translation logic
}
});
`Each item's
debug field contains:| Field | Type | Description |
| :--- | :--- | :--- |
|
elementOpenTag | string | The opening HTML tag of the element, including all attributes. E.g., '