Back to Home

OpenClaw Translation to Russian Without Source Code

Article describes the implementation of runtime localization of OpenClaw to Russian via transparent proxy server. Node.js service injects translation script into HTML, MutationObserver handles dynamic DOM of SPA application. Solution does not require forking sources and works with daily updates.

Russian OpenClaw Interface: Proxy Without Fork
Advertisement 728x90

Localizing OpenClaw in Russian via Proxy Without Code Changes

OpenClaw developers didn’t include i18n infrastructure: strings are hardcoded in JSX components without react-intl or i18next. Forking with manual translations isn’t practical due to daily updates. The solution? A transparent proxy between nginx and the app that injects a translation script into HTML responses.

Traffic flow:

  • nginx (port 443) → proxy (port 18790) → OpenClaw (port 18789).

The proxy modifies only HTML, preserving app logic. WebSocket connections for chat are passed through unchanged.

Google AdInline article slot

A Node.js server (~150 lines) uses http-proxy to intercept responses. Key trick: overriding res.write and res.end to buffer the response body.

import http from "node:http";
import httpProxy from "http-proxy";

const TARGET = process.env.TARGET_ORIGIN || "http://127.0.0.1:18789";
const proxy = httpProxy.createProxyServer({ target: TARGET, ws: true });

const server = http.createServer((req, res) => {
  if (req.url === "/ru-overlay.js") {
    // serve the translation script
    res.writeHead(200, { "Content-Type": "application/javascript" });
    res.end(overlayScript);
    return;
  }

  // intercept response
  const _write = res.write.bind(res);
  const _end = res.end.bind(res);
  let chunks = [];

  res.write = (chunk) => { chunks.push(chunk); };
  res.end = (chunk) => {
    if (chunk) chunks.push(chunk);
    let body = Buffer.concat(chunks).toString("utf-8");

    if (res.getHeader("content-type")?.includes("text/html")) {
      body = body.replace(
        "</body>",
        `<script src="/ru-overlay.js"></script></body>`
      );
    }
    // remove content-length, otherwise browser truncates response
    // due to changed length
    res.removeHeader("content-length");
    _write(body);
    _end();
  };

  proxy.web(req, res);
});

WebSocket handling requires listening to the upgrade event:

server.on("upgrade", (req, socket, head) => {
  proxy.ws(req, socket, head);
});

Client-Side Script with MutationObserver

OpenClaw is a SPA—DOM dynamically rebuilds on navigation. A simple text node traversal on load won’t work. MutationObserver tracks new nodes and translates them instantly.

Google AdInline article slot

Translation dictionary in JSON (~200 lines):

const dict = {
  "Settings": "Settings",
  "New conversation": "New conversation",
  "Send a message": "Send message",
  "Skills": "Skills",
  "Memory": "Memory",
  "Schedule": "Schedule",
  "Delete": "Delete",
  "Cancel": "Cancel",
  "Save": "Save",
  // ... another 200 entries
};

function translateNode(node) {
  if (node.nodeType !== Node.TEXT_NODE) return;
  const key = node.textContent.trim();
  if (dict[key]) {
    node.textContent = node.textContent.replace(key, dict[key]);
  }
}

// initial pass
const walker = document.createTreeWalker(
  document.body, NodeFilter.SHOW_TEXT
);
while (walker.nextNode()) translateNode(walker.currentNode);

// watch for new elements
new MutationObserver((mutations) => {
  for (const m of mutations) {
    for (const node of m.addedNodes) {
      if (node.nodeType === Node.ELEMENT_NODE) {
        const w = document.createTreeWalker(
          node, NodeFilter.SHOW_TEXT
        );
        while (w.nextNode()) translateNode(w.currentNode);
      } else {
        translateNode(node);
      }
    }
  }
}).observe(document.body, { childList: true, subtree: true });

Key script components:

  • TreeWalker for traversing text nodes.
  • MutationObserver with { childList: true, subtree: true } options.
  • Exact key matching using trim() to minimize collisions.

Limitations of This Approach

  • Placeholder attributes: Not captured by TreeWalker—require a separate attribute observer for input/select fields.
  • Contextual homonyms: "Run" in a button vs. text needs different translations—dictionary can’t distinguish context.
  • New strings: Untranslated elements remain in English until the dictionary is updated after OpenClaw releases.

Performance: Translation delay is ~10–50ms—unnoticeable to users.

Google AdInline article slot

Deployment

Install via script:

  • git clone https://github.com/perfectinn/openclaw-ru-layer
  • cd openclaw-ru-layer
  • sudo bash scripts/install.sh --patch-nginx

Automatically configures systemd service and nginx. Rollback: sudo bash scripts/uninstall.sh.

Docker version:

docker build -t openclaw-ru-layer .
docker run --rm -p 18790:18790 \
  -e TARGET_ORIGIN=http://host.docker.internal:18789 \
  openclaw-ru-layer

Key advantages:

  • No changes needed in OpenClaw—works with any current version.
  • MutationObserver ensures real-time translation of dynamic SPA content.
  • Dictionary updates require only adding entries to JSON—no server restart.
  • WebSockets are properly forwarded to preserve chat functionality.
  • Minimal overhead: ~150 lines of code, 5–6 dictionary updates over two months.

Alternatives and Scaling

Submitting a PR to the main repo isn’t feasible—it would require full refactoring (extracting strings to JSON, wrapping components with i18n libraries). Current solution deploys in minutes.

For other SPAs, possible enhancements:

  • Shadow DOM: Additional walker to traverse shadow roots.
  • Virtual DOM diff: Hook into React DevTools API (experimental).
  • Service Worker: Intercept fetch requests at the browser level.

— Editorial Team

Advertisement 728x90

Read Next