npm package: @jwrunge/transmut
Description: A client-side translation helper that watches the DOM, normalizes dynamic content, and populates localized text or attributes as translations become available. It is designed for single-page applications that need to translate asynchronously fetched conte
Latest version: 1.0.2

# transmut Translation Observer

A client-side translation helper that watches the DOM, normalizes dynamic content, and populates localized text or attributes as translations become available. It is designed for single-page applications that need to translate asynchronously fetched content without re-rendering the entire view.

## Installation

This package currently ships as source. Add it to your project as a workspace package or copy the `src/observer` folder into your bundle. Ensure your build pipeline includes the files in `src/observer`.

```bash
# via npm workspaces or a relative dependency
npm install @jwrunge/transmut
```

Vitest is configured for local testing (`npm test`).

## Using in a TypeScript Project

You can import the sources directly without a publish step. Add a path mapping so the compiler resolves the `.ts` files shipped with this package:

```jsonc
// tsconfig.json
{
	"compilerOptions": {
		"paths": {
			"@jwrunge/transmut": ["./node_modules/@jwrunge/transmut/src/index.ts"],
			"@jwrunge/transmut/*": ["./node_modules/@jwrunge/transmut/src/*"]
		}
	}
}
```

Then import the observer or the SQLite helpers straight from your code:

```ts
import { TranslationObserver, upsertTranslations } from "@jwrunge/transmut";

const observer = new TranslationObserver(/* ... */);
await upsertTranslations({
	databasePath: "./translations.sqlite",
	locale: "es-MX",
	translations: { Cart: "Carrito" },
});
```

Bundlers such as Vite/Esbuild can compile the `.ts` sources during your build. If you run Node directly, build the project first (`tsc -p .`) before executing the output.

## Quick Start

```ts
import { TranslationObserver } from "@jwrunge/transmut";

const observer = new TranslationObserver(
	"en-US", // default/source locale
	"es-MX", // initial target locale (optional)
	async ({ langCode, region }, keys, currentUrl) => {
		// Fetch translations from your API
		const response = await fetch(
			`/api/translations?lang=${langCode}&region=${region}`
		);
		return (await response.json()) as Record<string, string>;
	},
	24, // hours before cached entries are considered stale (optional)
	async () => ["session.banner"], // invalidate cache keys (optional)
	{
		requireExplicitOptIn: true,
		skipEditable: true,
	}
);

await observer.changeLocale("es", "MX");
```

Place `data-transmut="include"` (or other directives) on elements that provide opt-in. The observer will automatically translate matching text nodes and opted-in attributes.

## SQLite Backend (Optional)

The module in `src/backend/sqlite-translations.ts` persists translations in a local SQLite database using `sql.js`. It exposes helpers that can run in a Node environment (or be bundled to a standalone binary with tools such as `npx pkg`, `bun build --compile`, or `deno compile --unstable` when the accompanying `sql-wasm.wasm` file is copied next to the executable).

```ts
import {
	createSqliteTranslationProvider,
	upsertTranslations,
} from "./src/backend/sqlite-translations";

// Persist or update translation pairs
await upsertTranslations({
	databasePath: "/var/app/data/translations.sqlite",
	locale: { langCode: "es", region: "MX" },
	translations: {
		"Hello, world!": "¡Hola, mundo!",
		Checkout: "Pagar",
	},
});

// Hook the database up to TranslationObserver on the server
const sqliteProvider = createSqliteTranslationProvider(
	"/var/app/data/translations.sqlite"
);

// Example inside an HTTP handler
app.get("/api/translations", async (req, res) => {
	const { lang, region, keys } = req.query;
	const map = await sqliteProvider(
		{ langCode: String(lang), region: String(region ?? "") },
		Array.isArray(keys) ? keys : String(keys).split(",")
	);
	res.json(map);
});
```

Additional utilities are available:

-   `loadTranslations` — fetch a subset of keys directly from disk.
-   `listTranslations` — inspect all entries for a locale (useful for admin tools or exporting).
-   `SqliteTranslationProviderOptions` — control locale fallback behaviour.

### CLI / Binary Workflow

The `src/backend/cli.ts` entry provides a small command-line tool for managing translation databases. You can execute it with Node after compiling:

```bash
npm run build:pkg:prepare
node dist/backend/cli.js list --db ./translations.sqlite --locale es
```

To distribute a standalone binary, install [`pkg`](https://github.com/vercel/pkg) globally (`npm install -g pkg`) or run it via `npx`, then build:

```bash
npm run build:pkg
```

This produces `dist/transmut` (Linux x64 by default) together with `dist/sql-wasm.wasm`. Ship both files and ensure `SQLJS_WASM_PATH` points to the WASM if you relocate it.

The CLI supports three commands:

-   `upsert --db <file> --locale <lang[-REGION]> --input <json or ->` — merge JSON translations into the database (STDIN with `-`).
-   `list --db <file> --locale <lang[-REGION]>` — dump stored entries for inspection/export.
-   `load --db <file> --locale <lang[-REGION]> --keys key1,key2` — fetch a subset, honouring base-locale fallback unless `--no-fallback` is passed.

## Constructor Signature

```ts
new TranslationObserver(
	defaultLangCode?: string,
	initialLocale?: string,
	getTranslations: GetTransMapFn,
	expiryHours?: number,
	invalidateFn?: InvalidateFn,
	options?: TranslationObserverOptions
);
```

-   **defaultLangCode**: BCP 47 tag representing the source language (defaults to `en`).
-   **initialLocale**: Target locale to translate into. If omitted, the browser `navigator.language` is used.
-   **getTranslations**: Required async (or sync) function that returns a map of translation strings keyed by normalized source phrases. Returning a JSON string is also supported.
-   **expiryHours**: Number of hours before cached translations are considered stale. Set to `0` or omit to disable staleness checks.
-   **invalidateFn**: Optional callback invoked on startup to return translation keys that should be removed from IndexedDB before use.
-   **options**: Optional configuration (detailed below).

### Lifecycle Helpers

-   `changeLocale(langCode?: string, region?: string): Promise<void>` — updates the target locale, reapplies language metadata, and re-translates tracked nodes/attributes.
-   `observeShadowRoot(root: ShadowRoot): void` — explicitly opt a shadow root into observation.
-   `disconnect(): void` — stop observing mutations and close caches. Call this when tearing down your app.

## HTML Integration

The observer uses `data-transmut-*` directives to decide what to translate.

| Attribute                                | Purpose                                                                                                 | Notes                                               |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `data-transmut="include"`                | Opt-in a node (and descendants) for translation.                                                        | Required when `requireExplicitOptIn` is `true`.     |
| `data-transmut="skip"`                   | Prevent translation for the node and its subtree.                                                       | Equivalent to adding `data-transmut-skip`.          |
| `data-transmut-skip`                     | When present (any truthy value), skips translation for the subtree.                                     | Empty string counts as `true`.                      |
| `data-transmut-attrs="title,aria-label"` | Comma-separated list of attribute names to translate.                                                   | Attributes must exist on the element.               |
| `data-transmut-locale`                   | Override locale for a section. Values such as `inherit`, `auto`, or empty fall back to observer locale. | When set to another locale the subtree is skipped.  |
| `data-transmut-dir`                      | Force text direction (`ltr` or `rtl`).                                                                  | Applied alongside locale metadata.                  |
| `data-transmut-{variable}`               | Supply values for dynamic placeholders (see below).                                                     | The `{variable}` name is derived from placeholders. |

### Dynamic Content Placeholders

Source strings that contain placeholders are normalized before being sent to `getTranslations`. By default, `${variable}` tokens and numbers are replaced with `{}` in the translation key.

By default, protected token patterns are also normalized to `{}` so unique values are preserved during reconstruction:

-   Email addresses (e.g. `person@example.com`)
-   UUIDs (e.g. `550e8400-e29b-41d4-a716-446655440000`)
-   HTTP(S) URLs (e.g. `https://example.com/help`)

Example:

```html
<p data-transmut="include" data-transmut-count="5">
	You have ${count} unread messages.
</p>
```

-   The observer requests a translation for `"You have {} unread messages."`.
-   After receiving the translation (e.g., `"Tienes {} mensajes sin leer."`), the observer reconstructs the sentence and replaces the placeholder with the value from `data-transmut-count`.

You can customise placeholder detection via the `variablePattern` and `variableNameGroup` options if your templates use different syntax.

### Attribute Translation

Attributes listed in `data-transmut-attrs` or matched by the default list (`title`, `aria-label`, `aria-description`, `placeholder`, `alt`) are translated alongside text. Opt in using directives or selectors when `requireExplicitOptIn` is enabled.

### Svelte Patterns

In Svelte, `{value}` expressions are resolved before the observer sees the DOM. For predictable translation keys and safe reconstruction, prefer literal placeholders in text plus `data-transmut-*` value attributes.

```svelte
<p
	data-transmut="include"
	data-transmut-myfavoritefood={myFavoriteFood}
>
	I love ${myFavoriteFood}!
</p>
```

This yields the translation key `I love {}!` and reinserts the runtime value from `data-transmut-myfavoritefood`.

For sensitive/dynamic values such as emails, UUIDs, or URLs:

-   They are protected by default and normalized to `{}` in lookup keys.
-   Keep `requireExplicitOptIn: true` so only marked content is translated.
-   Use `data-transmut-skip` on subtrees that should never be translated.

Example with mixed translated/static-sensitive content:

```svelte
<p data-transmut="include">
	My email address is <span data-transmut-skip>{myEmail}</span>.
</p>
```

## Options Reference

`TranslationObserverOptions` control how the observer targets nodes and handles directionality.

| Option                  | Type                             | Default                                           | Description                                                                                      |
| ----------------------- | -------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `requireExplicitOptIn`  | `boolean`                        | `false`                                           | When `true`, only nodes matching `textSelector` or with `data-transmut="include"` are processed. |
| `textSelector`          | `string \| null`                 | `requireExplicitOptIn ? "[data-transmut]" : null` | Additional CSS selector that opts elements in for text translation.                              |
| `attributeSelector`     | `string \| null`                 | `"[data-transmut-attrs]"`                         | CSS selector used to detect attribute translation candidates.                                    |
| `attributeNames`        | `string[]`                       | See defaults above                                | Attribute names automatically considered for translation.                                        |
| `skipEditable`          | `boolean`                        | `true`                                            | When `true`, editable controls (`input`, `textarea`, contentEditable) are ignored.               |
| `setLanguageAttributes` | `boolean`                        | `true`                                            | Apply `lang`, `dir`, and `data-transmut-*` metadata to observed roots and the document element.  |
| `direction`             | `'ltr' \| 'rtl' \| 'auto'`       | `'auto'`                                          | `auto` infers direction from locale using defaults plus overrides.                               |
| `directionOverrides`    | `Record<string, 'ltr' \| 'rtl'>` | `DEFAULT_DIRECTION_OVERRIDES`                     | Extend or override the built-in map of locale → direction. Keys should be lowercase BCP 47 tags. |
| `variablePattern`       | `RegExp`                         | `/\${\s*([^}]+?)\s*}/g`                           | Pattern used to detect variable placeholders. Must be global (`g`).                              |
| `variableNameGroup`     | `number`                         | `1`                                               | Capture group index that contains the placeholder name.                                          |
| `protectedPatterns`     | `RegExp[]`                       | email, UUID, and URL defaults                     | Additional token patterns replaced with `{}` during lookup and restored after translation.       |

## Translation Cache

An IndexedDB-backed cache stores translations per locale.

-   Database names follow `transmut.<lang>.<region>` (`region` defaults to `default`).
-   Cached entries include `updatedAt` timestamps. When `expiryHours` is provided, stale keys are re-fetched.
-   `invalidateFn` runs once on startup. Returning an array of keys deletes them across all known locales before translation begins.

If IndexedDB is unavailable (e.g., SSR or non-browser contexts), the cache gracefully no-ops.

## Environment Requirements

-   Runs in browsers with `MutationObserver` and (optionally) `indexedDB`.
-   For unit testing, the project uses Vitest with a `jsdom` environment (`npm test`).
-   Ensure your bundler supports modern ES modules (`target` is `ES2022`).

## Development Workflow

-   `npm install`
-   `npm test`

## Tips and Patterns

-   Wrap `getTranslations` with your own batching logic or memoization to reduce network chatter.
-   Use `observer.observeShadowRoot(shadowRoot)` for web components.
-   Apply `data-transmut-skip` to sections that should stay in the source language (e.g., brand names).
-   Consider emitting analytics or logs inside `getTranslations` to monitor missing keys.

## License

MIT

```

```