Greasy Fork is available in English.
Replace the docs.openclaw.ai language dropdown with inline buttons.
// ==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();
}
})();