코드 수정 없이 OpenClaw을 러시아어로 로컬라이즈하는 프록시 방법
OpenClaw 개발팀은 i18n 인프라를 포함하지 않았습니다. 문자열은 react-intl나 i18next 없이 JSX 컴포넌트에 하드코딩되어 있습니다. 매일 업데이트되는 상황에서 수동 번역으로 포크하는 것은 현실적이지 않습니다. 해결책은 무엇일까요? 정답은 nginx와 앱 사이에 위치한 투명한 프록시입니다. 이 프록시는 HTML 응답에 번역 스크립트를 삽입하여 작동합니다.
트래픽 흐름:
- nginx (포트 443) → 프록시 (포트 18790) → OpenClaw (포트 18789)
프록시는 오직 HTML만 수정하며, 앱의 로직은 그대로 유지합니다. 채팅용 WebSocket 연결은 원본 상태로 통과됩니다.
Node.js 서버 (~150줄)는 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 처리는 upgrade 이벤트를 듣는 것으로 구현됩니다:
server.on("upgrade", (req, socket, head) => {
proxy.ws(req, socket, head);
});
클라이언트 측 스크립트: MutationObserver 활용
OpenClaw은 SPA이므로, 탐색 시 DOM이 동적으로 재구성됩니다. 로드 시 단순한 텍스트 노드 순회는 효과가 없습니다. 대신 MutationObserver를 사용해 새로운 노드를 실시간으로 감지하고 즉시 번역할 수 있습니다.
JSON 형식의 번역 사전 (~200줄):
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",
// ... 다른 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 });
핵심 구성 요소:
- 텍스트 노드 탐색을 위한
TreeWalker { childList: true, subtree: true }옵션을 가진MutationObservertrim()을 사용한 정확한 키 매칭으로 충돌 최소화
이 접근 방식의 한계
- 플레이스홀더 속성:
TreeWalker가 감지하지 못함 → 입력/선택 필드용 별도의 속성 관찰자 필요 - 맥락적 동음이의어: 버튼의 "Run"과 텍스트의 "Run"은 서로 다른 번역이 필요 → 사전은 맥락을 구분할 수 없음
- 새로운 문자열: OpenClaw 출시 후 사전이 업데이트되기 전까지 미번역 요소는 영어로 남음
성능: 번역 지연 시간은 ~10–50ms로 사용자에게 거의 느껴지지 않음.
배포 방법
스크립트로 설치:
git clone https://github.com/perfectinn/openclaw-ru-layercd openclaw-ru-layersudo bash scripts/install.sh --patch-nginx
자동으로 systemd 서비스와 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
주요 장점:
- OpenClaw 코드 변경 없이 모든 현재 버전과 호환
MutationObserver로 동적 SPA 콘텐츠 실시간 번역 보장- 사전 업데이트는 JSON에 항목 추가만으로 가능, 서버 재시작 필요 없음
- WebSocket은 정상적으로 전달되어 채팅 기능 유지
- 최소한의 부하: ~150줄 코드, 2개월간 5–6회 사전 업데이트만 필요
대안 및 확장성
메인 리포지토리에 PR을 제출하는 것은 불가능합니다. 전체 리팩터링(문자열을 JSON으로 추출, i18n 라이브러리로 컴포넌트 감싸기)이 필요하기 때문입니다. 현재 솔루션은 분단위 내 배포 가능합니다.
다른 SPA에도 적용 가능한 개선 방향:
- Shadow DOM: 추가적인 워커로 색다른 루트 탐색 가능
- 가상 DOM 차이 비교: React DevTools API에 연결 (실험적)
- 서비스 워커: 브라우저 레벨에서
fetch요청 가로채기
— Editorial Team
아직 댓글이 없습니다.