@dabalabs/lang
v1.0.0
Published
Embeddable i18n/translation widget for dabalabs projects — auto-detects your site's content, translates it into the languages you configured, and lets you correct a translation in place while developing.
Maintainers
Readme
@dabalabs/lang
Embeddable i18n/translation widget for dabalabs projects. Drop it into any site and it detects the page's own text, translates it into whichever languages you configured from the dashboard, and shows a simple language-switcher pill row for visitors.
While you're developing locally, right-click any translated sentence to correct it in place — the correction is saved immediately and becomes the live translation for every visitor from then on. This editing UI only ever appears on a dev-looking hostname (localhost, 127.0.0.1, *.local, *.test) — there's no way to trigger it once your site is deployed.
Works natively in Plain HTML, React, Next.js, Vue, Nuxt, Svelte, SvelteKit, Astro, and any modern JavaScript stack.
Install
npm install @dabalabs/langQuick start
Script tag (no build step)
<div data-dabalang data-project-id="YOUR_PROJECT_ID" data-api-key="YOUR_API_KEY"></div>
<script src="https://unpkg.com/@dabalabs/lang/dist/dabalang.min.js"></script>The <div> is where the language-switcher pills render; the rest of the page's text is what gets crawled and translated.
npm / bundler
import { DabaLang } from "@dabalabs/lang";
new DabaLang({
container: "#language-switcher",
projectId: "YOUR_PROJECT_ID",
apiKey: "YOUR_API_KEY",
});Get your Project ID and API key
- Go to your dabalabs dashboard → Translate widgets.
- Create a translate project: pick a source language and the target languages you want to offer.
- Create an API key for your domain, with
productset todabalang. - Copy the generated embed snippet.
Options
| Option | Type | Required | Description |
| --- | --- | --- | --- |
| container | string \| HTMLElement | Yes | CSS selector or element where the language pills render. |
| projectId | string | Yes | Your translate project's ID. |
| apiKey | string | Yes | A publishable key with product: "dabalang". Safe to ship in frontend source — the same trust model as Stripe.js: scoped server-side to your allowed origins. |
| gatewayUrl | string | No | Defaults to https://api.dabalabs.com. |
| view | "modal" \| "dropdown" | No | How the switcher presents languages: the flag-grid modal (default) or a compact dropdown anchored to the trigger. Script-tag installs can pass data-view="dropdown". |
| onLanguageChange | (langCode: string) => void | No | Fires after a visitor switches language (the source language's own code when they pick "Original"). |
| onError | (error: DabaLangError) => void | No | Fires on a failed metadata fetch or translate call. Defaults to console.error. |
What gets translated
Every visible text node in <body> is crawled by default — that's what "detect the content inside the website" means in practice; an opt-in scheme would make the widget translate nothing out of the box. To exclude a section (a code sample, a price ticker you don't want touched, anything with its own i18n already), mark it:
<div data-dabalang-ignore>
This text is never crawled or translated.
</div>Password fields, hidden inputs, and credit-card-autocomplete fields are always excluded, with no attribute needed.
Translation is lazy: a language is only translated the first time a visitor selects it, not pre-fetched for every configured language on every page load. The very first visitor to translate a given page into a given language pays the (small) live-translation latency; every subsequent visitor gets an instant cache hit.
Correcting a translation (development only)
On localhost (or 127.0.0.1, a .local/.test hostname), while a non-original language is selected, right-click any translated sentence to open the editor next to it. Save, and:
- The correction applies immediately on your screen.
- It's stored against that exact source sentence for that language.
- Every visitor — in development or production — gets your corrected version from then on, since the point of this workflow is fixing a bad machine translation once, not maintaining a personal dev-only override.
It is a small popover anchored under the phrase you clicked, holding that one string and nothing else. Inline markup splits a sentence into several strings that are translated separately — Everything in <strong>DabaOne</strong>, plus the transcript is three — so when there are others, the popover says + N more in this sentence and expands in place if you want them. Only what you changed is saved. Each field renders in its own text direction, so an Arabic or Hebrew translation reads correctly while the popover's own chrome stays left-to-right.
↵ saves (Shift+↵ for a newline), Esc closes, and a click anywhere outside dismisses it. Right-clicking inside the popover — or on the language switcher — leaves the browser's own menu alone.
It was a double-click before v0.0.9. Double-clicking text is how you select a word, so every correction started by taking the selection away, and translated text that was also a link or a button ran its own handler first.
There is no way to open this editor on a real (non-dev-looking) domain — it's a workflow safety rail for your own QA pass, not a security boundary. Manage or revert past corrections from the project's Corrections tab in the dashboard.
Warming the rest of the site
By default a page is translated when somebody opens it. prewarm: true also covers the page a visitor is on in the languages they are not reading. prewarmSite: true goes one step further and covers the pages nobody has opened yet:
new DabaLang({ container: "#lang", projectId, apiKey, prewarm: true, prewarmSite: true });It reads /sitemap.xml (or a list you pass as prewarmSite: { urls: [...] }), fetches each page, pulls the text out of its HTML, and translates that — painting nothing and changing nothing on screen. What it fills is the two caches: the gateway's, which is keyed by content hash and shared by every visitor, and this browser's own. So one visitor's idle time makes the whole site instant in every language for everyone after them, instead of each page costing its first visitor in each language a wait.
Off by default, and it should stay off unless you want it: it multiplies one visit's translation cost by pages × languages, and that is your bill. What it does to keep that honest:
- Runs only when the browser is idle, one request at a time, after the visitor's own page is correct.
- At most once per tab per project — a route change that remounts the widget does no work at all.
- Never on a connection reporting
saveData. - Capped at
maxPages(default 20). - Same-origin by construction: only the path of each sitemap entry is used, so the canonical host baked into a generated sitemap doesn't stop it working in development or on a preview deployment.
It reads server-rendered HTML. A page whose text only appears after its own JavaScript runs yields nothing here — no cost, and that page is simply translated on arrival as before.
Pre-translated languages
By default the platform translates for you. If the translation already exists, configure the language as pre-translated in the dashboard — it is then never machine-translated. Two shapes:
- Your site already serves that language (a CMS with localized pages, a localized build): give the language a path prefix and enable path routing. Pages under that prefix (
/ar/...) are treated as already translated — the widget doesn't touch their content or spend a single translation call; it only setslang/dir, marks the switcher, and navigates between prefixes. - You have the strings but one set of pages: upload a map of source text → translated text. Uploaded strings are swapped in directly — no machine translation, no per-string cost — and any string your upload doesn't cover falls back to the normal pipeline, so a partial catalogue never breaks a page.
No widget change is needed either way.
URL path routing
Optionally, languages can live under URL path prefixes — example.com/ar/pricing for Arabic, example.com/pricing for the original — configured per language from the dashboard (Languages tab → path prefix, plus the project-level toggle).
With it enabled, the widget:
- Detects on load: visiting a path under a configured prefix (
/ar/...) applies that language immediately — no pill click needed. Un-prefixed paths serve the language marked default (or the original if none is). - Navigates on switch: picking a language that has a prefix navigates to the same page under that prefix (query string and hash preserved); picking the original strips it. Languages without a prefix keep translating in place.
- Never redirects on its own: the widget only reacts to the URL it finds itself on. If you want
/to redirect to/arfor Arabic-locale visitors, do that on your server/CDN — the widget will pick the language up from the path it lands on.
Your server must serve the same page under the prefixed paths (a catch-all/rewrite works fine — the widget re-translates from the path). SPAs and static hosts can map /ar/* to the same bundle.
Limitations (v1)
- Client-side only. There's an unavoidable flash of the original-language content before JavaScript crawls and swaps in a translation — the same tradeoff every client-side-only i18n widget (Weglot, GTranslate's free tier, etc.) makes. A server-side-rendered/SEO-safe tier is not built.
- No browser-language guessing. The widget never infers a visitor's language from browser settings. Without path routing it always starts on "Original"; with path routing the URL decides.
- Server-rendered text only, for
prewarmSite. Warming other pages reads their HTML without running their JavaScript, so client-rendered content is not covered.
License
MIT
Environments
By default the widget talks to Dabalabs production. Point it at staging
with env:
<div data-dabalang
data-project-id="YOUR_PROJECT_ID"
data-api-key="YOUR_API_KEY"
data-dabalang-env="staging"
></div>new DabaLang({
// ...
env: "staging",
});| env | Gateway |
|---|---|
| omitted | https://api.dabalabs.com |
| "production" | https://api.dabalabs.com |
| "staging" | https://api-staging.dabalabs.com |
Production is the default in every direction — an embed that says nothing about its environment is a live site, so staging has to be asked for by name. An unrecognised value warns in the console and falls back to production rather than throwing, so a typo cannot take a customer's page down.
gatewayUrl still overrides env when both are given: it is the only
option that can name a host neither tier covers (a self-hosted
deployment, a local gateway).
