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:

  1. nl.js resolves the visitor's language — forced language, previously chosen language, browser language, then your default.
  2. Translations for that language are fetched and held in memory.
  3. Every text node in the DOM is replaced, along with placeholder, alt, title and aria-label.
  4. A MutationObserver keeps translating content added later — React, Vue and other SPAs work with no extra configuration.
Original text is kept. Switching languages always translates from the original, never from an already-translated string, so nothing degrades through repeated translation.

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.

AttributeDefaultDescription
data-projectRequired. The project ID from your project settings.
data-default-languageenThe language your page is written in — the source language.
data-current-languageForces a language, overriding the stored choice. Useful when you decide the language server-side.
data-modeautoauto translates the whole page. manual translates only elements carrying data-nl attributes.
data-switchertrueSet to false to hide the language switcher.
data-positionbottom-rightSwitcher position: bottom-right, bottom-left, top-right, top-left.
data-switcher-targetCSS selector. Renders the switcher inside your own element instead of floating over the page.
data-api-urlhttps://nexling.appOverrides the API base URL — needed if you serve nl.js from your own CDN.
data-debugfalseWrites verbose output to the browser console.
The switcher renders inside a Shadow DOM. Your page CSS cannot accidentally restyle it, and it will not restyle your page.

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>
AttributeApplies to
data-nlThe element's text content
data-nl-htmlinnerHTML — use only with trusted content
data-nl-placeholderInput placeholder
data-nl-altImage alt
data-nl-titleThe title attribute
data-nl-labelaria-label
data-nl-skipExcludes the element and everything inside it
Put 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);
MethodDescription
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

EventFires when
readyThe first translation pass is done and the page is fully visible.
langChangedThe language changed. The new code is passed to your handler.
errorTranslations 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.

GET /MyApi/languages/{projectId}

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 }
]
GET /MyApi/translates/{projectId}/{lang}

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"
}
GET /MyApi/map/{projectId}/{lang}

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.

GET /api/export/json/{langCode}
curl -H "X-Api-Key: nxl_your_key_here" \
     https://nexling.app/api/export/json/fr-CH
GET /api/export/languages
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
Keep the key in your pipeline's secrets, not in the repository. The public project ID is deliberately not accepted here — this endpoint always requires a real key.

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.

EventFires when
translation.completedA bulk AI translation run finished.
translation.updatedTranslations were edited or imported. Rapid single edits are collapsed into one event.
terms.importedNew source text arrived via the crawler, Autopilot or a file import.
project.language.addedA language was added to the project.
export.downloadedAn 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));
Sign the body exactly as it arrived, before parsing. Re-serialising changes whitespace and the signature will no longer match. Compare in constant time, not with ===.
Delivery is non-blocking with a 5-second timeout. After 10 consecutive failures an endpoint is disabled automatically; re-enabling it resets the counter. Every attempt is visible in that endpoint's delivery history.

Export formats

Inside the app you can download translations per language in several formats. Programmatically, JSON is available through the CI/CD endpoint above.

FormatTypically used by
JSONWeb front-ends, i18next, custom pipelines
POgettext — WordPress, Laravel, Python
XLIFFTranslation agencies and CAT tools
RESX.NET applications
strings.xmlAndroid
.stringsiOS and macOS
CSVSpreadsheets, manual review

Limits & errors

EndpointLimitCounted per
/MyApi/*60 requests / minuteIP address
/api/export/*30 requests / minuteAPI 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 }
nl.js handles this itself: the page is shown immediately in the source language and retried once after the delay named in Retry-After. Visitors never see a blank page — at worst they see the original text.

Other status codes

CodeMeaning
400The project ID is not a valid GUID.
401X-Api-Key missing or invalid (CI/CD export only).
404Project or language not found.
429Limit reached — see Retry-After.

Still stuck?

Email info@nexling.ch — include your project ID and we can look directly.