OpenClaw на русском — как я перевёл интерфейс, не трогая исходники
- понедельник, 30 марта 2026 г. в 00:00:04
У меня OpenClaw крутится на VPS уже два месяца. Штука крутая, но есть один момент, который бесит: весь интерфейс личного кабинета на английском. Я-то ладно, привык, но когда показываешь коллегам или знакомым — сразу «а что тут нажимать» и «а это что за Settings».
Казалось бы, ну загугли «openclaw на русском» — и найдёшь готовую локализацию. Нет. Нету. В roadmap проекта тоже ничего про i18n. Строки захардкожены прямо в JSX, никакого react-intl или i18next там и близко нет. То есть даже если отправить PR — его некуда класть, фреймворка для переводов просто не существует в проекте.
Форкать OpenClaw и вручную менять строки — вариант для мазохистов. Проект обновляется чуть ли не каждый день, мержить свои правки в каждый апдейт я точно не буду.
В итоге я сделал проще.
Идея тупая до безобразия: поставить между nginx и OpenClaw прокси, которая будет на лету подсовывать в HTML скрипт с переводом. OpenClaw ничего не знает про прокси, прокси ничего не меняет в логике OpenClaw. Просто берёт HTML, дописывает <script> перед </body>, и всё.
nginx (:443) → раньше шло на OpenClaw (:18789) → теперь идёт на мою прослойку (:18790) → та проксирует всё на OpenClaw (:18789) → но в HTML-ответы втыкает скрипт с переводом
Прослойка — это ~150 строк на Node.js. Назвал openclaw-ru-layer, потому что фантазия кончилась.
Обычный http-proxy. Ничего хитрого, кроме одного нюанса — нужно перехватывать res.write и res.end, чтобы модифицировать тело ответа до того, как оно уйдёт клиенту.
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") { // отдаём скрипт перевода res.writeHead(200, { "Content-Type": "application/javascript" }); res.end(overlayScript); return; } // перехватываем ответ 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>` ); } // обязательно убрать content-length, иначе браузер // обрежет ответ — длина-то изменилась res.removeHeader("content-length"); _write(body); _end(); }; proxy.web(req, res); });
Один момент, на котором я потерял час: WebSocket. OpenClaw использует WS для чата, и если не пробросить upgrade — интерфейс загружается, но чат молчит. Лечится одной строчкой:
server.on("upgrade", (req, socket, head) => { proxy.ws(req, socket, head); });
Вот тут было интереснее. Первая версия была примитивная: при загрузке страницы пробежаться по всем текстовым нодам и заменить английские строки на русские из словаря.
Работало ровно до первого клика. OpenClaw — SPA, весь контент рендерится динамически. Переключаешь вкладку — DOM перестраивается, и мой перевод пропадает.
Пришлось вешать MutationObserver:
const dict = { "Settings": "Настройки", "New conversation": "Новый диалог", "Send a message": "Отправить сообщение", "Skills": "Навыки", "Memory": "Память", "Schedule": "Расписание", "Delete": "Удалить", "Cancel": "Отмена", "Save": "Сохранить", // ... ещё строк 200 }; 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]); } } // первичный проход const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT ); while (walker.nextNode()) translateNode(walker.currentNode); // следим за новыми элементами 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 });
Да, это грубо. Да, на долю секунды виден английский текст до того, как скрипт отработает. Но на практике это незаметно — MutationObserver срабатывает почти мгновенно.
Не буду делать вид, что это идеальное решение. Проблемы есть:
Placeholder’ы. Текст внутри <input placeholder="Search..."> — это атрибут, а не текстовая нода. TreeWalker его не видит. Нужно отдельно обрабатывать через тот же MutationObserver, но на атрибутах. Я пока забил — руки не дошли.
Контекст. Слово “Run” в кнопке и “Run” в тексте диалога — это разные вещи. Словарь не различает контекст. Пока не мешает, потому что интерфейсные строки обычно уникальны, но теоретически может стрельнуть.
Обновления OpenClaw. Если в новой версии появится кнопка с текстом, которого нет в словаре — она останется на английском. Не сломается, просто не переведётся. Добавляешь строку в словарь, перезапускаешь сервис — готово. За два месяца пришлось дописывать раз пять-шесть.
Я серьёзно думал об этом. Но посмотрел на кодовую базу OpenClaw и передумал. Там нет никакой инфраструктуры для локализации — строки лежат прямо в компонентах, вот так:
<Button>Save changes</Button> <h2>Conversation history</h2>
Чтобы сделать нормальный PR с i18n, нужно:
Выбрать библиотеку (react-intl? i18next?).
Вынести все строки в JSON-файлы.
Обернуть каждый компонент.
Добавить переключатель языка.
Это рефакторинг на несколько дней. И непонятно, примут ли его — у core-команды другие приоритеты. А моя прослойка решает проблему прямо сейчас, за полчаса установки, без зависимости от чьих-то решений.
Если кому-то нужен перевод OpenClaw на русский — вот:
git clone https://github.com/perfectinn/openclaw-ru-layer cd openclaw-ru-layer sudo bash scripts/install.sh --patch-nginx
Скрипт ставит npm-зависимости, создаёт systemd-сервис и (с флагом --patch-nginx) переключает nginx на прослойку. Удаление — sudo bash scripts/uninstall.sh, откатывает всё назад.
Для Docker:
docker build -t openclaw-ru-layer . docker run --rm -p 18790:18790 \ -e TARGET_ORIGIN=http://host.docker.internal:18789 \ openclaw-ru-layer
Репозиторий: github.com/perfectinn/openclaw-ru-layer
Если пользуетесь — кидайте PR с новыми строками для словаря. Там реально нечего делать, одна строчка в JSON.
У кого-нибудь был опыт подобного runtime-перевода для других приложений? Интересно, есть ли более элегантный способ, чем MutationObserver по всему DOM.