OpenClaw Docs Language Bar

Replace the docs.openclaw.ai language dropdown with inline buttons.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         OpenClaw Docs Language Bar
// @namespace    http://tampermonkey.net/
// @version      1.3.0
// @description  Replace the docs.openclaw.ai language dropdown with inline buttons.
// @author       Codex
// @match        https://docs.openclaw.ai/*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const PANEL_ID = "oc-language-bar";
  const STYLE_ID = "oc-language-bar-style";
  const HIDDEN_ATTR = "data-oc-lang-hidden";
  const PANEL_LINK_ATTR = "data-oc-language-link";
  const LOCALES = [
    { code: "en", label: "English", icon: "🇺🇸", prefix: "" },
    { code: "zh-CN", label: "简体中文", icon: "🇨🇳", prefix: "/zh-CN" },
    { code: "ja-JP", label: "日本語", icon: "🇯🇵", prefix: "/ja-JP" },
  ];

  let observer = null;
  let renderTimer = 0;
  let linkRewriteTimer = 0;
  const FALLBACK_LAYOUT = {
    top: 16,
    left: 72,
    minHeight: 32,
    fontSize: "14px",
    fontWeight: "500",
  };

  function injectStyle() {
    if (document.getElementById(STYLE_ID)) {
      return;
    }

    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = `
      #${PANEL_ID} {
        position: fixed;
        z-index: 2147483647;
        display: flex;
        align-items: center;
        gap: 6px;
        box-sizing: border-box;
      }

      #${PANEL_ID}[data-hidden="true"] {
        display: none;
      }

      #${PANEL_ID} a {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        padding: 7px 10px;
        border: 1px solid rgba(0, 0, 0, 0.08);
        border-radius: 999px;
        background: rgba(255, 255, 255, 0.96);
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
        color: inherit;
        text-decoration: none;
        line-height: 1;
        white-space: nowrap;
        box-sizing: border-box;
        font-size: 0.92em;
      }

      #${PANEL_ID} a:hover {
        border-color: rgba(255, 93, 54, 0.35);
        color: #ff5d36;
      }

      #${PANEL_ID} a[data-active="true"] {
        border-color: rgba(255, 93, 54, 0.45);
        color: #ff5d36;
        background: rgba(255, 93, 54, 0.08);
      }

      #${PANEL_ID} .oc-language-icon {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        font-size: 1em;
      }

      #${PANEL_ID} .oc-language-label {
        display: inline-block;
      }

      [${HIDDEN_ATTR}="true"] {
        visibility: hidden !important;
        pointer-events: none !important;
      }

      @media (max-width: 768px) {
        #${PANEL_ID} {
          max-width: calc(100vw - 24px);
          overflow-x: auto;
          scrollbar-width: none;
        }

        #${PANEL_ID}::-webkit-scrollbar {
          display: none;
        }
      }
    `;

    document.head.appendChild(style);
  }

  function normalizeText(text) {
    return (text || "").replace(/\s+/g, " ").trim();
  }

  function detectCurrentLocale(pathname) {
    if (pathname === "/zh-CN" || pathname.startsWith("/zh-CN/")) {
      return "zh-CN";
    }

    if (pathname === "/ja-JP" || pathname.startsWith("/ja-JP/")) {
      return "ja-JP";
    }

    return "en";
  }

  function getCurrentLocalePrefix() {
    const locale = detectCurrentLocale(window.location.pathname);
    return LOCALES.find((item) => item.code === locale)?.prefix || "";
  }

  function stripLocalePrefix(pathname) {
    if (pathname === "/zh-CN" || pathname === "/ja-JP") {
      return "/";
    }

    if (pathname.startsWith("/zh-CN/")) {
      return pathname.slice("/zh-CN".length);
    }

    if (pathname.startsWith("/ja-JP/")) {
      return pathname.slice("/ja-JP".length);
    }

    return pathname || "/";
  }

  function buildLocaleUrl(targetLocale) {
    const suffix = stripLocalePrefix(window.location.pathname);
    const path = `${targetLocale.prefix}${suffix === "/" ? "" : suffix}` || "/";
    return `${window.location.origin}${path}${window.location.search}${window.location.hash}`;
  }

  function localizePathname(pathname, prefix) {
    const stripped = stripLocalePrefix(pathname);
    if (!prefix) {
      return stripped || "/";
    }

    return `${prefix}${stripped === "/" ? "" : stripped}` || prefix;
  }

  function localizeUrl(input, base = window.location.href) {
    const prefix = getCurrentLocalePrefix();
    if (!prefix) {
      return null;
    }

    let url;
    try {
      url = new URL(input, base);
    } catch {
      return null;
    }

    if (url.origin !== window.location.origin) {
      return null;
    }

    if (!url.pathname.startsWith("/")) {
      return null;
    }

    url.pathname = localizePathname(url.pathname, prefix);
    return url;
  }

  function ensurePanel() {
    let panel = document.getElementById(PANEL_ID);
    if (!panel) {
      panel = document.createElement("div");
      panel.id = PANEL_ID;
      panel.dataset.hidden = "true";
      document.body.appendChild(panel);
    }
    return panel;
  }

  function getLanguageTrigger() {
    const labels = LOCALES.map((locale) => locale.label.toLowerCase());
    const candidates = Array.from(
      document.querySelectorAll("button, [role='button'], a")
    );

    return candidates.find((node) => {
      const text = normalizeText(node.textContent).toLowerCase();
      if (!labels.some((label) => text.includes(label))) {
        return false;
      }

      const rect = node.getBoundingClientRect();
      return (
        rect.width > 60 &&
        rect.height > 28 &&
        rect.top >= 0 &&
        rect.top < window.innerHeight * 0.35 &&
        rect.left >= 0 &&
        rect.left < window.innerWidth * 0.5
      );
    }) || null;
  }

  function getLogoAnchor() {
    const candidates = Array.from(
      document.querySelectorAll("header img, header svg, nav img, nav svg, img, svg")
    );

    const scored = candidates
      .map((node) => {
        const rect = node.getBoundingClientRect();
        const text = normalizeText(
          node.getAttribute?.("aria-label") ||
          node.getAttribute?.("alt") ||
          node.parentElement?.getAttribute?.("aria-label") ||
          node.parentElement?.textContent
        ).toLowerCase();

        const inTopLeft =
          rect.width >= 16 &&
          rect.height >= 16 &&
          rect.width <= 96 &&
          rect.height <= 96 &&
          rect.top >= 0 &&
          rect.top < 180 &&
          rect.left >= 0 &&
          rect.left < 140;

        if (!inTopLeft) {
          return null;
        }

        let score = 0;
        if (text.includes("openclaw")) {
          score += 5;
        }
        if (text.includes("logo")) {
          score += 4;
        }
        if (node.closest("header, nav")) {
          score += 3;
        }

        score += Math.max(0, 140 - rect.left) / 20;
        score += Math.max(0, 120 - rect.width) / 40;
        score += Math.max(0, 120 - rect.top) / 40;

        return { node, rect, score };
      })
      .filter(Boolean)
      .sort((a, b) => b.score - a.score);

    return scored[0]?.node || null;
  }

  function hideOriginalDropdown(trigger) {
    if (!trigger) {
      return;
    }

    trigger.setAttribute(HIDDEN_ATTR, "true");

    const floatingMenus = Array.from(
      document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper]")
    );

    for (const menu of floatingMenus) {
      const text = normalizeText(menu.textContent).toLowerCase();
      if (!LOCALES.some((locale) => text.includes(locale.label.toLowerCase()))) {
        continue;
      }

      const rect = menu.getBoundingClientRect();
      if (rect.width > 0 && rect.height > 0) {
        menu.setAttribute(HIDDEN_ATTR, "true");
      }
    }
  }

  function applyPanelLayout(panel, anchor) {
    let top = FALLBACK_LAYOUT.top;
    let left = FALLBACK_LAYOUT.left;
    let minHeight = FALLBACK_LAYOUT.minHeight;
    let fontSize = FALLBACK_LAYOUT.fontSize;
    let fontWeight = FALLBACK_LAYOUT.fontWeight;

    if (anchor) {
      const anchorRect = anchor.getBoundingClientRect();
      const anchorStyle = window.getComputedStyle(anchor);
      top = Math.max(Math.round(anchorRect.top + (anchorRect.height - minHeight) / 2), 8);
      left = Math.max(Math.round(anchorRect.right + 12), 8);
      minHeight = Math.max(Math.round(anchorRect.height), 32);
      fontSize = anchorStyle.fontSize || fontSize;
      fontWeight = anchorStyle.fontWeight || fontWeight;
    }

    panel.style.top = `${top}px`;
    panel.style.left = `${left}px`;
    panel.style.minHeight = `${minHeight}px`;
    panel.style.fontSize = fontSize;
    panel.style.fontWeight = fontWeight;
  }

  function renderLinks(panel, currentLocale) {
    panel.replaceChildren();

    for (const locale of LOCALES) {
      const link = document.createElement("a");
      link.href = buildLocaleUrl(locale);
      link.dataset.active = String(locale.code === currentLocale);
      link.setAttribute(PANEL_LINK_ATTR, "true");

      const icon = document.createElement("span");
      icon.className = "oc-language-icon";
      icon.textContent = locale.icon;

      const label = document.createElement("span");
      label.className = "oc-language-label";
      label.textContent = locale.label;

      link.appendChild(icon);
      link.appendChild(label);
      panel.appendChild(link);
    }
  }

  function rewriteInternalLinks() {
    const links = Array.from(document.querySelectorAll("a[href]"));
    for (const link of links) {
      const rawHref = link.getAttribute("href");
      if (!rawHref || rawHref.startsWith("#") || rawHref.startsWith("javascript:")) {
        continue;
      }

      if (link.hasAttribute(PANEL_LINK_ATTR)) {
        continue;
      }

      const url = localizeUrl(rawHref);
      if (!url) {
        continue;
      }

      const nextHref = `${url.origin}${url.pathname}${url.search}${url.hash}`;
      if (link.href !== nextHref) {
        link.href = nextHref;
      }
    }
  }

  function handleDocumentClick(event) {
    if (event.defaultPrevented || event.button !== 0) {
      return;
    }

    if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
      return;
    }

    const target = event.target;
    if (!(target instanceof Element)) {
      return;
    }

    const link = target.closest("a[href]");
    if (!link) {
      return;
    }

    const rawHref = link.getAttribute("href");
    if (!rawHref || rawHref.startsWith("#") || rawHref.startsWith("javascript:")) {
      return;
    }

    if (link.hasAttribute(PANEL_LINK_ATTR)) {
      return;
    }

    if (link.target && link.target !== "_self") {
      return;
    }

    const url = localizeUrl(rawHref);
    if (!url) {
      return;
    }

    const nextHref = `${url.origin}${url.pathname}${url.search}${url.hash}`;
    if (link.href !== nextHref || getCurrentLocalePrefix()) {
      event.preventDefault();
      window.location.assign(nextHref);
    }
  }

  function handleDocumentAuxClick(event) {
    if (event.defaultPrevented || event.button !== 1) {
      return;
    }

    const target = event.target;
    if (!(target instanceof Element)) {
      return;
    }

    const link = target.closest("a[href]");
    if (!link) {
      return;
    }

    const rawHref = link.getAttribute("href");
    if (!rawHref || rawHref.startsWith("#") || rawHref.startsWith("javascript:")) {
      return;
    }

    if (link.hasAttribute(PANEL_LINK_ATTR)) {
      return;
    }

    const url = localizeUrl(rawHref);
    if (!url) {
      return;
    }

    const nextHref = `${url.origin}${url.pathname}${url.search}${url.hash}`;
    if (link.href !== nextHref) {
      link.href = nextHref;
    }
  }

  function alignLocaleWithPath() {
    const prefix = getCurrentLocalePrefix();
    if (!prefix) {
      return;
    }

    const pathWithoutLocale = stripLocalePrefix(window.location.pathname);
    const htmlLang = (document.documentElement.lang || "").toLowerCase();
    const isChinesePath = prefix === "/zh-CN";
    const isJapanesePath = prefix === "/ja-JP";
    const pageStillLooksEnglish =
      (isChinesePath && !htmlLang.startsWith("zh")) ||
      (isJapanesePath && !htmlLang.startsWith("ja"));

    if (pageStillLooksEnglish) {
      const targetUrl = `${window.location.origin}${prefix}${pathWithoutLocale === "/" ? "" : pathWithoutLocale}${window.location.search}${window.location.hash}`;
      if (window.location.href !== targetUrl) {
        window.location.replace(targetUrl);
      }
    }
  }

  function patchLanguageLinks(panel) {
    const links = Array.from(panel.querySelectorAll("a[href]"));
    for (const link of links) {
      link.addEventListener("click", (event) => {
        event.preventDefault();
        window.location.assign(link.href);
      });
    }
  }

  function refresh() {
    render();
    alignLocaleWithPath();
  }

  function handleHistoryLikeNavigation() {
    window.setTimeout(() => {
      refresh();
    }, 0);
  }

  function hookNavigationEvents() {
    const originalPushState = history.pushState.bind(history);
    const originalReplaceState = history.replaceState.bind(history);

    history.pushState = function pushState(state, unused, url) {
      const result = originalPushState(state, unused, url);
      handleHistoryLikeNavigation();
      return result;
    };

    history.replaceState = function replaceState(state, unused, url) {
      const result = originalReplaceState(state, unused, url);
      handleHistoryLikeNavigation();
      return result;
    };
  }

  function render() {
    if (!document.body) {
      return;
    }

    injectStyle();

    const panel = ensurePanel();
    const trigger = getLanguageTrigger();
    if (trigger) {
      hideOriginalDropdown(trigger);
    }

    applyPanelLayout(panel, getLogoAnchor());
    renderLinks(panel, detectCurrentLocale(window.location.pathname));
    patchLanguageLinks(panel);
    rewriteInternalLinks();
    panel.dataset.hidden = "false";
  }

  function scheduleRender() {
    window.clearTimeout(renderTimer);
    renderTimer = window.setTimeout(render, 120);
  }

  function scheduleRewriteLinks() {
    window.clearTimeout(linkRewriteTimer);
    linkRewriteTimer = window.setTimeout(rewriteInternalLinks, 120);
  }

  function init() {
    hookNavigationEvents();
    refresh();
    scheduleRewriteLinks();

    observer = new MutationObserver(() => {
      scheduleRender();
      scheduleRewriteLinks();
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ["class", "style", "aria-expanded"],
    });

    window.addEventListener("resize", scheduleRender);
    window.addEventListener("hashchange", scheduleRender);
    window.addEventListener("popstate", scheduleRender);
    window.addEventListener("hashchange", scheduleRewriteLinks);
    window.addEventListener("popstate", scheduleRewriteLinks);
    document.addEventListener("click", handleDocumentClick, true);
    document.addEventListener("auxclick", handleDocumentAuxClick, true);
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init, { once: true });
  } else {
    init();
  }
})();