API Documentation
Nexling translates a website from a single script tag, with no changes to your HTML. If you need to go further, there is also a public REST API, an export endpoint for your own build pipeline, and webhooks.
Quickstart
Add this one line to your page's <head>. Replace
YOUR-PROJECT-ID with the project ID from your project settings.
<script src="https://nexling.app/nl.js"
data-project="YOUR-PROJECT-ID"></script>
That's the whole integration. On page load:
- nl.js resolves the visitor's language — forced language, previously chosen language, browser language, then your default.
- Translations for that language are fetched and held in memory.
- Every text node in the DOM is replaced, along with
placeholder,alt,titleandaria-label. - A
MutationObserverkeeps translating content added later — React, Vue and other SPAs work with no extra configuration.
With the common options
<script src="https://nexling.app/nl.js"
data-project="YOUR-PROJECT-ID"
data-default-language="de-CH"
data-switcher="true"
data-position="bottom-right"></script>
Try it
The public endpoints need no key. Enter your project ID and see the real response — the request runs straight from your browser.
Script attributes
Everything is configured on the <script> tag itself.
| Attribute | Default | Description |
|---|---|---|
data-project | — | Required. The project ID from your project settings. |
data-default-language | en | The language your page is written in — the source language. |
data-current-language | — | Forces a language, overriding the stored choice. Useful when you decide the language server-side. |
data-mode | auto | auto translates the whole page. manual translates only elements carrying data-nl attributes. |
data-switcher | true | Set to false to hide the language switcher. |
data-position | bottom-right | Switcher position: bottom-right, bottom-left, top-right, top-left. |
data-switcher-target | — | CSS selector. Renders the switcher inside your own element instead of floating over the page. |
data-api-url | https://nexling.app | Overrides the API base URL — needed if you serve nl.js from your own CDN. |
data-debug | false | Writes verbose output to the browser console. |
Element attributes
You don't need these in automatic mode. They matter when you want to address a specific string by a stable key, or protect a region from being touched.
<!-- Text content -->
<h1 data-nl="page_title">Willkommen</h1>
<!-- Attributes -->
<input data-nl-placeholder="search_hint" placeholder="Suchen…">
<img data-nl-alt="logo_alt" alt="Firmenlogo">
<a data-nl-title="home_tip" title="Zur Startseite">Home</a>
<button data-nl-label="close_btn" aria-label="Schliessen">×</button>
<!-- Raw HTML (use only with trusted content) -->
<div data-nl-html="rich_intro"><b>Hallo</b> Welt</div>
<!-- Never translate this subtree -->
<pre data-nl-skip>const x = "Speichern";</pre>
| Attribute | Applies to |
|---|---|
data-nl | The element's text content |
data-nl-html | innerHTML — use only with trusted content |
data-nl-placeholder | Input placeholder |
data-nl-alt | Image alt |
data-nl-title | The title attribute |
data-nl-label | aria-label |
data-nl-skip | Excludes the element and everything inside it |
data-nl-skip on code samples, usernames, order numbers — anything
that must stay verbatim. Without it, an AI translation may well rewrite those values.
The older data-lf-* attributes from lf.js still work. New projects should
use data-nl-*.
JavaScript API
Once loaded, window.Nexling is available globally.
window.LocaleFlow remains as an alias.
// Switching
await Nexling.setLanguage('fr-CH');
Nexling.getLanguage(); // → 'fr-CH'
Nexling.getLanguages(); // → [{ code, name, isSource }, …]
// Keyed lookup
Nexling.t('save_button'); // → 'Speichern'
Nexling.t('greeting', { name: 'Ana' }); // → 'Hallo, Ana!'
// Re-applying
Nexling.refresh(); // re-apply current language, no refetch
Nexling.init(); // re-initialise (SPA route changes)
Nexling.restoreOriginal(); // put the original text back
Nexling.restoreToSource('de-CH');
// Events
Nexling.on('ready', () => console.log('translated'));
Nexling.on('langChanged', code => console.log('now', code));
Nexling.on('error', err => console.warn(err));
Nexling.off('ready', handler);
| Method | Description |
|---|---|
setLanguage(code) | Switches language and re-translates the page. The choice is remembered per project. |
getLanguage() | The currently active language code. |
getLanguages() | Every language configured for the project. |
t(key, vars?) | Translation for a key. vars fills placeholders in the string. |
refresh() | Re-applies the current language without refetching. |
init() | Re-initialises — use after route changes in an SPA. |
restoreOriginal() | Puts all original text back. |
restoreToSource(code) | Returns to the source language with no API call. |
on(event, fn) / off(event, fn) | Add or remove an event listener. |
Single-page applications
The MutationObserver already covers most cases. If your router swaps out
large parts of the DOM at once, call init() after navigation.
// React Router / Vue Router — re-scan after each navigation
router.afterEach(() => Nexling.init());
Events
| Event | Fires when |
|---|---|
ready | The first translation pass is done and the page is fully visible. |
langChanged | The language changed. The new code is passed to your handler. |
error | Translations could not be loaded or applied. |
REST API
These are the endpoints nl.js itself calls. They are public, need no key and allow cross-origin requests, so you can call them straight from a browser. They serve translations only — never account data.
Every language configured for the project, with the source language flagged.
curl https://nexling.app/MyApi/languages/YOUR-PROJECT-ID
[
{ "code": "de-CH", "name": "German (Switzerland)", "isSource": true },
{ "code": "fr-CH", "name": "French (Switzerland)", "isSource": false },
{ "code": "it-CH", "name": "Italian (Switzerland)", "isSource": false }
]
A flat key-to-translation map — the right shape when you work with your own keys.
curl https://nexling.app/MyApi/translates/YOUR-PROJECT-ID/fr-CH
{
"save_button": "Enregistrer",
"learn_more": "En savoir plus"
}
The enriched form nl.js uses in automatic mode. byKey behaves as above;
byText maps visible source text to its translation and names the element
type, so the same word can be translated differently depending on context.
curl https://nexling.app/MyApi/map/YOUR-PROJECT-ID/fr-CH
{
"byKey": {
"learn_more": "En savoir plus"
},
"byText": {
"Mehr erfahren": { "value": "En savoir plus", "context": "a" }
}
}
byText also contains the other languages' wording. A French source page
therefore resolves to the same translation as a German one — you don't need a
separate project per source language.
CI/CD export
For build pipelines there is a key-authenticated endpoint that returns translations as a file. Generate the key in your project settings — it is shown once, and we only ever store its hash.
curl -H "X-Api-Key: nxl_your_key_here" \
https://nexling.app/api/export/json/fr-CH
curl -H "X-Api-Key: nxl_your_key_here" \
https://nexling.app/api/export/languages
Example build step
# Pull the latest translations during a build
for LANG in de-CH fr-CH it-CH; do
curl -sf -H "X-Api-Key: $NEXLING_API_KEY" \
"https://nexling.app/api/export/json/$LANG" \
-o "locales/$LANG.json"
done
Webhooks
Nexling can notify your server whenever translations change — to trigger a rebuild or clear a cache, for example. Endpoints are managed in your project settings.
| Event | Fires when |
|---|---|
translation.completed | A bulk AI translation run finished. |
translation.updated | Translations were edited or imported. Rapid single edits are collapsed into one event. |
terms.imported | New source text arrived via the crawler, Autopilot or a file import. |
project.language.added | A language was added to the project. |
export.downloaded | An export was downloaded. |
Request shape
POST /your-endpoint
X-Nexling-Event: translation.completed
X-Nexling-Signature: sha256=9f86d081884c7d65…
User-Agent: Nexling-Webhooks/1.0
{
"event": "translation.completed",
"projectId": "YOUR-PROJECT-ID",
"timestamp": "2026-09-07T14:22:31Z",
"data": {
"languageCode": "fr-CH",
"translatedCount": 128,
"totalTerms": 130,
"engine": "claude-haiku",
"coveragePercent": 98.5
}
}
Verifying the signature
Every request is signed with HMAC-SHA256 over the raw body, keyed with your endpoint's secret. Verify it before acting on the payload — otherwise anyone can post events to your endpoint.
const crypto = require('crypto');
function isFromNexling(rawBody, header, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody) // the raw body, before JSON.parse
.digest('hex');
// Constant-time compare — never use ===
return crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(header)
);
}
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = "sha256=" + Convert.ToHexString(
hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody))).ToLowerInvariant();
var ok = CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signatureHeader));
===.
Export formats
Inside the app you can download translations per language in several formats. Programmatically, JSON is available through the CI/CD endpoint above.
| Format | Typically used by |
|---|---|
| JSON | Web front-ends, i18next, custom pipelines |
| PO | gettext — WordPress, Laravel, Python |
| XLIFF | Translation agencies and CAT tools |
| RESX | .NET applications |
| strings.xml | Android |
| .strings | iOS and macOS |
| CSV | Spreadsheets, manual review |
Limits & errors
| Endpoint | Limit | Counted per |
|---|---|---|
/MyApi/* | 60 requests / minute | IP address |
/api/export/* | 30 requests / minute | API key |
When a limit is exceeded, the API responds like this:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{ "error": "Rate limit exceeded", "retryAfter": 60 }
Retry-After. Visitors never see a
blank page — at worst they see the original text.
Other status codes
| Code | Meaning |
|---|---|
400 | The project ID is not a valid GUID. |
401 | X-Api-Key missing or invalid (CI/CD export only). |
404 | Project or language not found. |
429 | Limit reached — see Retry-After. |
Still stuck?
Email info@nexling.ch — include your project ID and we can look directly.