Localiser OpenClaw en russe via un proxy sans modifier le code
Les développeurs d'OpenClaw n’ont pas intégré d’infrastructure i18n : les chaînes sont codées en dur dans les composants JSX, sans react-intl ni i18next. Forker avec des traductions manuelles n’est pas viable à cause des mises à jour quotidiennes. La solution ? Un proxy transparent entre nginx et l’application, qui injecte un script de traduction dans les réponses HTML.
Flux du trafic :
- nginx (port 443) → proxy (port 18790) → OpenClaw (port 18789).
Le proxy modifie uniquement le HTML, préservant la logique de l’application. Les connexions WebSocket pour le chat passent sans modification.
Un serveur Node.js (~150 lignes) utilise http-proxy pour intercepter les réponses. Astuce clé : surcharger res.write et res.end pour buffer le corps de la réponse.
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") {
// servir le script de traduction
res.writeHead(200, { "Content-Type": "application/javascript" });
res.end(overlayScript);
return;
}
// intercepter la réponse
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>`
);
}
// supprimer content-length, sinon le navigateur tronque la réponse
// à cause de la longueur modifiée
res.removeHeader("content-length");
_write(body);
_end();
};
proxy.web(req, res);
});
La gestion des WebSocket nécessite d’écouter l’événement upgrade :
server.on("upgrade", (req, socket, head) => {
proxy.ws(req, socket, head);
});
Script côté client avec MutationObserver
OpenClaw est une SPA — le DOM se reconstruit dynamiquement lors des navigations. Une simple traversée des nœuds texte au chargement ne fonctionne pas. MutationObserver suit les nouveaux nœuds et les traduit instantanément.
Dictionnaire de traduction en JSON (~200 lignes) :
const dict = {
"Settings": "Paramètres",
"New conversation": "Nouvelle conversation",
"Send a message": "Envoyer un message",
"Skills": "Compétences",
"Memory": "Mémoire",
"Schedule": "Planning",
"Delete": "Supprimer",
"Cancel": "Annuler",
"Save": "Enregistrer",
// ... autres 200 entrées
};
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]);
}
}
// passe initiale
const walker = document.createTreeWalker(
document.body, NodeFilter.SHOW_TEXT
);
while (walker.nextNode()) translateNode(walker.currentNode);
// surveiller les nouveaux éléments
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 });
Composants clés du script :
- TreeWalker pour parcourir les nœuds texte.
- MutationObserver avec options
{ childList: true, subtree: true }. - Correspondance exacte des clés grâce à trim() pour minimiser les collisions.
Limites de cette approche
- Attributs de placeholder : non capturés par TreeWalker — nécessitent un observateur d’attribut séparé pour les champs input/select.
- Homonymes contextuels : « Run » dans un bouton vs. du texte exige des traductions différentes — le dictionnaire ne peut pas distinguer le contexte.
- Nouvelles chaînes : les éléments non traduits restent en anglais jusqu’à la mise à jour du dictionnaire après une nouvelle version d’OpenClaw.
Performance : délai de traduction ~10–50 ms — imperceptible pour les utilisateurs.
Déploiement
Installation via script :
git clone https://github.com/perfectinn/openclaw-ru-layercd openclaw-ru-layersudo bash scripts/install.sh --patch-nginx
Configure automatiquement le service systemd et nginx. Annulation : sudo bash scripts/uninstall.sh.
Version Docker :
docker build -t openclaw-ru-layer .
docker run --rm -p 18790:18790 \
-e TARGET_ORIGIN=http://host.docker.internal:18789 \
openclaw-ru-layer
Avantages principaux :
- Aucun changement nécessaire dans OpenClaw — fonctionne avec toute version actuelle.
- MutationObserver garantit une traduction en temps réel du contenu dynamique de la SPA.
- Mise à jour du dictionnaire requiert seulement l’ajout d’entrées JSON — pas de redémarrage serveur.
- Les WebSocket sont correctement transférés pour préserver la fonctionnalité du chat.
- Faible surcharge : ~150 lignes de code, 5–6 mises à jour du dictionnaire en deux mois.
Alternatives et évolutivité
Soumettre une PR au dépôt principal n’est pas réaliste — cela demanderait une refonte complète (extraire les chaînes vers JSON, envelopper les composants avec des bibliothèques i18n). La solution actuelle se déploie en quelques minutes.
Pour d’autres SPAs, améliorations possibles :
- Shadow DOM : ajout d’un walker supplémentaire pour explorer les racines shadow.
- Diffusion du Virtual DOM : accrochage à l’API React DevTools (expérimental).
- Service Worker : interception des requêtes fetch au niveau du navigateur.
— Editorial Team
Aucun commentaire pour le moment.