Lokalizacja OpenClaw po rosyjsku przez proxy bez zmiany kodu źródłowego
Twórcy OpenClaw nie zaplanowali infrastruktury i18n: teksty są zakodowane w komponentach JSX bez react-intl czy i18next. Fork z ręcznym tłumaczeniem jest niewykonalny ze względu na codzienne aktualizacje. Rozwiązanie to przejrzyste proxy między nginx a aplikacją, które wstrzykuje skrypt tłumaczenia do odpowiedzi HTML.
Schemat ruchu:
- nginx (port 443) → proxy (port 18790) → OpenClaw (port 18789).
Proxy modyfikuje tylko HTML, zachowując logikę aplikacji. Połączenia WebSocket dla czatu są przekazywane bez zmian.
Serwer na Node.js (~150 linii) używa http-proxy do przechwytywania odpowiedzi. Kluczowy moment — zamiana res.write i res.end dla buforowania ciała odpowiedzi.
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") {
// zwracamy skrypt tłumaczenia
res.writeHead(200, { "Content-Type": "application/javascript" });
res.end(overlayScript);
return;
}
// przechwytywujemy odpowiedź
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>`
);
}
// koniecznie usuń content-length, inaczej przeglądarka
// obetnie odpowiedź — długość się zmieniła
res.removeHeader("content-length");
_write(body);
_end();
};
proxy.web(req, res);
});
Obsługa WebSocket wymaga nasłuchiwania zdarzenia upgrade:
server.on("upgrade", (req, socket, head) => {
proxy.ws(req, socket, head);
});
Klientowy skrypt z MutationObserver
OpenClaw to SPA — DOM dynamicznie się zmienia przy nawigacji. Prosty przebieg przez węzły tekstowe przy załadowaniu nie działa. MutationObserver śledzi dodawanie węzłów i tłumaczy je natychmiast.
Słownik tłumaczeń w JSON (~200 linii):
const dict = {
"Settings": "Ustawienia",
"New conversation": "Nowa rozmowa",
"Send a message": "Wyślij wiadomość",
"Skills": "Umiejętności",
"Memory": "Pamięć",
"Schedule": "Harmonogram",
"Delete": "Usuń",
"Cancel": "Anuluj",
"Save": "Zapisz",
// ... jeszcze około 200 linii
};
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]);
}
}
// pierwsze przeszukanie
const walker = document.createTreeWalker(
document.body, NodeFilter.SHOW_TEXT
);
while (walker.nextNode()) translateNode(walker.currentNode);
// śledź nowe elementy
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 });
Główne składniki skryptu:
- TreeWalker do przeglądania węzłów tekstowych.
- MutationObserver z opcjami
{ childList: true, subtree: true }. - Dokładne dopasowanie kluczy po trim() dla minimalizacji kolizji.
Ograniczenia podejścia
- Atrybuty placeholder: Nie są przechwytywane przez TreeWalker, wymagają osobnego obserwatora atrybutów input/select.
- Homonimy kontekstowe: "Run" w przycisku i tekście — różne tłumaczenia, słownik ich nie rozróżnia.
- Nowe ciągi: Przy aktualizacjach OpenClaw nieprzetłumaczone elementy pozostają po angielsku do aktualizacji słownika.
Wydajność: opóźnienie tłumaczenia ~10–50 ms, niezauważalne dla użytkownika.
Wdrożenie
Instalacja przez skrypt:
git clone https://github.com/perfectinn/openclaw-ru-layercd openclaw-ru-layersudo bash scripts/install.sh --patch-nginx
Automatycznie konfiguruje usługę systemd i nginx. Odinstalowanie: sudo bash scripts/uninstall.sh.
Wersja Docker:
docker build -t openclaw-ru-layer .
docker run --rm -p 18790:18790 \
-e TARGET_ORIGIN=http://host.docker.internal:18789 \
openclaw-ru-layer
Co ważne:
- Proxy nie wymaga zmian w OpenClaw, działa z aktualną wersją.
- MutationObserver zapewnia tłumaczenie treści dynamicznych SPA.
- Aktualizacje słownika — dodawanie linii w JSON bez restartu serwera.
- Połączenia WebSocket są poprawnie przekazywane, zachowując czat.
- Minimalny narzut: ~150 linii kodu, 5–6 aktualizacji słownika w ciągu 2 miesięcy.
Alternatywy i skalowanie
PR do głównego repozytorium nie ma sensu: wymagałby refaktoryzacji (przeniesienie tekstów do JSON, otoczenie komponentów biblioteką i18n). Obecne rozwiązanie wdraża się w kilka minut.
Dla innych SPA możliwe ulepszenia:
- Shadow DOM: Dodatkowy walker po shadow roots.
- Virtual DOM diff: Hook do API React DevTools (eksperymentalnie).
- Service Worker: Przechwytywanie żądań fetch na poziomie przeglądarki.
— Editorial Team
Brak komentarzy.