SDK Guides

Bablic server-side translation for sites behind Cloudflare

Set up Bablic server-side translation for WordPress, Drupal and other sites behind Cloudflare using the Node.js SDK and a caching Cloudflare Worker.

11 min readUpdated

This guide describes how to set up Bablic server-side translation for a server-rendered website (Drupal, WordPress or another CMS) whose traffic is served through Cloudflare. Once it is in place, translated pages are delivered to visitors and search engines already translated. The original language is never displayed first, and every language version of a page can be indexed.

The setup runs entirely on your infrastructure and uses two npm packages from Bablic:

  • @bablic-io/sdk-node: Bablic’s translation service, installed on a server running Node.js.
  • @bablic-io/sdk-cloudflare: a Cloudflare Worker that sends your translated pages through that service and caches the results at the edge.

Apart from adding the Bablic script tag, no changes to your site’s code are required.

visitor ──► Cloudflare Worker ──► your origin (original page)
                  │   ▲
                  ▼   │
     Bablic's translation service (@bablic-io/sdk-node)

Summary

Bablic’s side

# Step
B1 Confirm your project is on a Pro plan or free trial. Server-side translation is included only on these plans.
B2 Set your languages to language folders (www.example.com/<language-folder>/… ) or language subdomains (<language>.example.com ) in the Bablic dashboard.
B3 Confirm your site's domains are allowed for the project, and set the link rules so links on translated pages stay in their language.
B4 Provide your project key (P0000… ), the same key used in your Bablic script tag.

Your side

# Step Where
1 Add the Bablic script tag to every page Your CMS theme
2 Allow Bablic in your Content Security Policy Your web server or CMS
3 Run the translation service (@bablic-io/sdk-node ) behind HTTPS A server with Node.js 22
4 Create and deploy the Worker (@bablic-io/sdk-cloudflare ) Any machine with Node.js, plus your Cloudflare account
5 Test Terminal and browser

For assistance at any step, contact support@bablic.com.


Before you start

  • A Bablic account and project (sign up), with steps B1 to B4 done.
  • Your site behind Cloudflare (the DNS record for your hostname is proxied, the orange cloud), and permission to create Workers and routes on the zone. A server for the translation service:
    • Node.js 22.19 or later;
    • reachable from the internet over HTTPS;
    • able to make outbound HTTPS requests to https://edge.bablic.com. It can be the same machine as your website or a separate one.
  • Node.js 22 on the workstation used to deploy the Worker.

Replace the following placeholders in the examples with your own values:

Placeholder Meaning
P0000 Your Bablic project key
www.example.com Your site's hostname
example.com Your Cloudflare zone
translate.example.com The HTTPS address of the translation service
<your-translated-url> A translated page, e.g. https://www.example.com/<language-folder>/about

Step 1: Add the Bablic script tag to every page

The tag must be in the HTML your server sends, on every page. A tag inserted later in the browser, for example by a tag manager, is not visible to the translation service.

Copy the exact tag from the Bablic dashboard (Installation & Deployment). It has this form:

<script src="https://edge.bablic.com/moxie.min.js" data-oljs="P0000"></script>

Drupal: add it inside <head> in your front-end theme’s html.html.twig (if the template is not present, copy it from the base theme into your theme), then rebuild caches:

drush cr

Other CMSs: add it to the shared page template or <head> include.

Then purge Cloudflare’s cache for your HTML pages (Caching → Configuration → Purge Cache), so cached pages pick up the tag.

Check:

curl -s https://www.example.com/ | grep -o '<script[^>]*data-oljs[^>]*>'

Step 2: Allow Bablic in your Content Security Policy

If your site sends a Content-Security-Policy header, the browser part of Bablic must be allowed to load and call home. It translates content that appears after the page loads (menus, AJAX results, pop-ups) and pages for signed-in visitors.

Add https://edge.bablic.com to:

  • script-src (or script-src-elem), unless it already allows all hosts;
  • connect-src.

Check: open a translated page in your browser and look in the developer-tools console. There should be no “Refused to connect” or “Refused to load” errors that mention a Bablic host. If another Bablic host appears there, add it the same way.


Step 3: Run Bablic’s translation service (@bablic-io/sdk-node)

3a. Install and start it

On your server:

mkdir -p /opt/bablic && cd /opt/bablic
npm init -y
npm install @bablic-io/sdk-node
BABLIC_LISTEN=0.0.0.0 ./node_modules/.bin/bablic-node serve
# bablic-node listening on 0.0.0.0:8790

The translation service stores no project settings. Each request carries your project key; all other settings are read from your Bablic project.

3b. Keep it running (systemd example)

/etc/systemd/system/bablic-node.service:

[Unit]
Description=Bablic translation service
After=network-online.target

[Service]
WorkingDirectory=/opt/bablic
ExecStart=/usr/bin/env node /opt/bablic/node_modules/@bablic-io/sdk-node/dist/cli.js serve
Environment=BABLIC_LISTEN=127.0.0.1
Environment=BABLIC_PORT=8790
Restart=always
User=nobody

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now bablic-node

Docker: a Dockerfile is provided in the @bablic-io/sdk-node README.

3c. Put HTTPS in front of it

Cloudflare reaches the service over HTTPS. Example with nginx on the same server (the service listens on 127.0.0.1:8790):

server {
    listen 443 ssl;
    server_name translate.example.com;
    # ssl_certificate / ssl_certificate_key: your certificate

    client_max_body_size 6m;          # pages up to 5 MB (BABLIC_MAX_BODY_MB)
    location / {
        proxy_pass http://127.0.0.1:8790;
        proxy_read_timeout 30s;
    }
}

The hostname can sit on Cloudflare too (proxied DNS record), or be served directly from the server.

3d. Check it

curl -s https://translate.example.com/healthz
# {"ok":true,"edge":"ok","projects":0}

Then translate one page by hand. Fetch an original page and ask for its translated URL:

curl -s https://www.example.com/about > /tmp/page.html
curl -s -D - -o /tmp/translated.html https://translate.example.com/translate \
  -H 'x-bablic-project: P0000' \
  -H 'x-bablic-url: https://www.example.com/<language-folder>/about' \
  -H 'content-type: text/html' \
  --data-binary @/tmp/page.html | grep -i -E '^HTTP|x-moxie'

Expect x-moxie: MISS the first time and HIT on a repeat. /tmp/translated.html holds the translated page.

Main settings

Variable Default
BABLIC_PORT 8790
BABLIC_LISTEN 127.0.0.1 Interface to listen on.
BABLIC_TIMEOUT_MS 3000 Translation budget per page. Past it the visitor gets the original page, and the translation finishes in the background for the next visitor.
BABLIC_CACHE_MB 256 In-memory cache of translated pages.
BABLIC_REDIS_URL Share the cache between several instances (npm install ioredis next to the service).
BABLIC_CONCURRENCY CPUs Pages translated at once.
BABLIC_METRICS_PORT 8791 Prometheus /metrics , loopback only. Do not expose this port.

Full list: the @bablic-io/sdk-node README.


Step 4: Create and deploy the Cloudflare Worker (@bablic-io/sdk-cloudflare)

4a. Generate the Worker project

On any machine with Node.js 22:

npx @bablic-io/sdk-cloudflare init bablic-worker \
  --project P0000 \
  --engine https://translate.example.com \
  --bypass-cookies 'SESS,SSESS' \
  --route 'www.example.com/*' --zone example.com
cd bablic-worker
npm install

This writes three files:

  • wrangler.toml: the Worker’s settings, route and the nodejs_compat flag;
  • package.json;
  • src/index.js: the Worker’s entry point.

Any option can be omitted and set in wrangler.toml afterwards.

--bypass-cookies names the session cookies of signed-in visitors. Their pages are personalized, so the Worker neither translates nor caches them; Bablic translates them in the browser instead.

Platform Session cookies
Drupal SESS,SSESS
WordPress wordpress_logged_in_
Other The cookie your CMS sets at sign-in (check in the browser's developer tools)

4b. Deploy

npx wrangler login      # first time only; opens Cloudflare in your browser
npx wrangler deploy

4c. Routes

  • Language folders: the route www.example.com/* covers everything. The Worker forwards requests for untranslated pages directly to your origin. To have the Worker run only on translated pages, use one route per language folder instead, e.g. www.example.com/<language-folder>/*.
  • Language subdomains: add a route for each language hostname, e.g. <language>.example.com/*, and make sure each subdomain has a proxied DNS record pointing at your origin.

Routes can also be managed in the Cloudflare dashboard: Workers & Pages → bablic-translate → Settings → Domains & Routes.

4d. Settings

These are [vars] in wrangler.toml, or Settings → Variables in the Cloudflare dashboard. Run npx wrangler deploy again after changing wrangler.toml.

Variable Default
BABLIC_PROJECT Your project key.
BABLIC_ENGINE_URL The HTTPS address of the translation service, e.g. https://translate.example.com .
BABLIC_BYPASS_COOKIES Session cookies of signed-in visitors.
BABLIC_EDGE_TTL 300 Seconds a translated page stays in Cloudflare's edge cache. 0 turns it off.
BABLIC_ENGINE_TIMEOUT_MS 8000 How long the Worker waits for the translation service.

Step 5: Test

5a. Response headers

curl -sI <your-translated-url> | grep -i -E 'x-bablic-edge|x-moxie'
curl -sI <your-translated-url> | grep -i -E 'x-bablic-edge|x-moxie'
You see Meaning
x-bablic-edge: MISS , x-moxie: MISS Translated on this request.
x-bablic-edge: HIT (second request) Served from Cloudflare's edge cache.
x-moxie: HIT or STALE Served from the translation service's cache.

Untranslated pages carry no x-bablic-edge header, because the Worker forwards them directly to your origin.

5b. The page itself

curl -s <your-translated-url> | grep -o -E '<html[^>]*>|<link[^>]*hreflang[^>]*>' | head
  • <html lang="…"> has the page’s language, and data-moxie-mode="ssr".
  • hreflang links list every language.
  • In the browser’s View Source, the page text is already translated. This is the version search engines index.
  • Links on the page stay inside the language folder or subdomain.

5c. Signed-in visitors and Bablic Studio

Sign in to your site, then open a translated page:

curl -sI -H 'cookie: SESSexample=1' <your-translated-url> | grep -i -E 'x-bablic-edge|cache-control'
# x-bablic-edge: bypass
# cache-control: private, no-store

In the browser, the page is still displayed translated: Bablic translates it on the page. Bablic Studio works on translated pages as before.

5d. Dynamic content

On a translated page, use a pager, a filter, search suggestions or a pop-up. Content that appears after the page loads is translated by Bablic in the browser.


Debugging

Start from the response headers of a translated URL:

curl -sI <your-translated-url> | grep -i -E 'x-bablic-edge|x-moxie'

Whenever a page is not translated, the x-moxie-reason header gives a one-line explanation.

Symptom Likely cause Fix
No x-bablic-edge header on a translated URL The Worker's route does not match the URL (www versus the bare domain, or a missing language subdomain), or the DNS record is not proxied Check Domains & Routes and the orange cloud on the DNS record
The Worker fails as soon as it is deployed The nodejs_compat compatibility flag is missing Keep compatibility_flags = ["nodejs_compat"] in wrangler.toml and redeploy
x-bablic-edge: pass with "needs BABLIC_PROJECT and BABLIC_ENGINE_URL" Worker settings empty Set both and redeploy
x-bablic-edge: engine-unavailable Cloudflare cannot reach the translation service: a firewall, the certificate, or the service is not running Run curl https://translate.example.com/healthz from outside your network; check the certificate and systemctl status bablic-node
/healthz returns 503 The server cannot reach https://edge.bablic.com Allow outbound HTTPS to that host
x-moxie: skipped , reason please subscribe to pro tier The project's plan does not include server-side translation Upgrade the plan, or contact support@bablic.com. Allow about 15 minutes for a plan change to take effect
x-moxie: pass , reason no language folder matches … The URL is not one of the project's translated URLs. Folders must match exactly: /fr-fr/ is not /fr/ Compare the URL with the folders or subdomains in your Bablic dashboard
x-moxie: pass , reason … is not a language domain … As above, for language subdomains: the hostname is not one of the project's subdomains Compare the hostname with your Bablic dashboard
x-moxie: skipped , reason no <script data-oljs> tag The tag is not in the server HTML: it is added by a tag manager, or rewritten by Cloudflare Rocket Loader Place the tag in the theme (Step 1). If Rocket Loader is enabled, add data-cfasync="false" to the tag
x-moxie: skipped , reason Bablic script has no server-side mode The script tag loads an old copy of the Bablic script Use the tag from your Bablic dashboard, from https://edge.bablic.com/moxie.min.js
x-moxie: failed , reason timeout… on the first visit to a page The first translation of a page took longer than BABLIC_TIMEOUT_MS This is expected for new pages: the translation completes in the background and later requests are translated. If it persists, increase BABLIC_TIMEOUT_MS
x-moxie: failed with a 403 in the reason Bablic refused the page's domain for this project Ask support@bablic.com to add the domain to your project
Translated page, but links lead back to the original language Link rules not set for this hostname Bablic's side: B3
A form or AJAX request from a translated page returns 404 The request was sent to a language-folder URL that your origin does not recognize Report the URL to support@bablic.com, so the link rules leave that form or AJAX address untouched
Signed-in visitors see the original language briefly This is expected: pages for signed-in visitors are translated in the browser, not on the server No action is required. Confirm that their session cookie is listed in BABLIC_BYPASS_COOKIES , so their pages are never cached
A signed-in visitor's name appears on a page for other visitors Their session cookie is missing from BABLIC_BYPASS_COOKIES Add it, redeploy, and set BABLIC_EDGE_TTL to 0 until the old copies expire
Browser console shows CSP errors for a Bablic host Content Security Policy Step 2
Dashboard changes (languages, folders) are not reflected yet Bablic's settings are cached for up to about 10 minutes, in addition to the translation service cache and the edge cache Allow time for the caches to expire, or restart the translation service and set BABLIC_EDGE_TTL=0 while testing
A content change is not reflected on the translated page The edge cache keeps a page for BABLIC_EDGE_TTL seconds; the translation service rechecks pages within the hour Lower BABLIC_EDGE_TTL , or allow the cache to expire
First requests after a restart are slow The translation service's in-memory cache is empty after a restart This is expected. Set BABLIC_REDIS_URL to keep the cache across restarts and instances
CMS cache still serves pages without the tag Drupal page cache, a CDN cache, or both drush cr (or your CMS's cache clear), then purge Cloudflare

Useful commands

# Translation service logs (systemd)
journalctl -u bablic-node -f

# Worker logs, live
npx wrangler tail

# Translation service metrics, on the server itself
curl -s http://127.0.0.1:8791/metrics | grep bablic_pages_total

Keeping it up to date

Update both packages when Bablic releases a new version:

# Translation service
cd /opt/bablic && npm install @bablic-io/sdk-node@latest && sudo systemctl restart bablic-node

# Worker
cd bablic-worker && npm install @bablic-io/sdk-cloudflare@latest && npx wrangler deploy

Contact

Bablic support: support@bablic.com. Please include:

  • your project key;
  • a translated URL;
  • the output of curl -sI <your-translated-url>.