Greasy Fork is available in English.

OpenClaw Docs Language Bar

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
  }
})();