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.
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.
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.
Deployment
Install via script:
git clone https://github.com/perfectinn/openclaw-ru-layercd openclaw-ru-layersudo 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
No comments yet.