Powrót do strony głównej

Tłumaczenie OpenClaw na rosyjski bez kodu źródłowego

Artykuł opisuje implementację runtime-lokalizacji OpenClaw na język rosyjski za pomocą przezroczystego serwera proxy. Serwis Node.js wstrzykuje skrypt tłumaczenia do HTML, MutationObserver obsługuje dynamiczny DOM aplikacji SPA. Rozwiązanie nie wymaga forka kodu źródłowego i działa z codziennymi aktualizacjami.

Rosyjski interfejs OpenClaw: proxy bez forka
Advertisement 728x90

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.

Google AdInline article slot

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.

Google AdInline article slot

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.

Google AdInline article slot

Wdrożenie

Instalacja przez skrypt:

  • git clone https://github.com/perfectinn/openclaw-ru-layer
  • cd openclaw-ru-layer
  • sudo 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

Advertisement 728x90

Czytaj dalej