2025-12-17
计算机
00

目录

1. 使用开源(https://github.com/xnx3/translate) 网页翻译插件初始不加载
2. 使用开源(https://github.com/xnx3/translate) 网页翻译插件初始加载
3. 使用Google网页翻译插件

本文记录了在 VanBlog(或类似带导航栏的博客主题)中实现“中 / En”一键切换的三种方案:两种基于开源项目 xnx3/translate(分别是「初始不加载(懒加载)」与「初始加载」),以及一种基于 Google 网页翻译。三种方案主要差异在于首屏加载速度与切换语言时的响应速度;你可以根据站点性能需求与可用性环境选择其一。文中同时给出配套的 CSS / Script / HTML,用于隐藏翻译顶部横幅、避免按钮文字被翻译、并将切换按钮稳定插入到导航栏指定位置。

1. 使用开源(https://github.com/xnx3/translate) 网页翻译插件初始不加载

优点:初始加载网页速度快

缺点:点击语言切换时速度慢

  • 自定义CSS
css
.footer-powered-by-vanblog{ display: none} /* 包裹容器:用于插入导航栏 */ #vb-lang-wrap{ display: inline-flex; align-items: center; } /* 间距微调:让它更贴近左侧“亮暗切换”,并与右侧 RSS 保持适中距离 不满意就调这两个值: - margin-left: -8 ~ 0 - margin-right: 8 ~ 16 */ .nav-action #vb-lang-wrap{ margin-left: -6px; margin-right: 12px; } /* label 本体:保持原来“一个正方形区域”,用于绝对定位两行字 */ .lang-circle{ --s: 28px; /* 图标占位大小(之前的版本就是这个量级);想更大可改 30/32 */ position: relative; width: var(--s); height: var(--s); cursor: pointer; user-select: none; /* 颜色:亮色黑,暗色白(使用 currentColor 思路) */ color: #111; background: transparent; border: 0; padding: 0; line-height: 1; transform: translateZ(0); transition: transform .15s ease; } .lang-circle:hover{ transform: scale(1.15); } /* 暗色:白(兼容 tailwind dark / data-theme) */ .dark .lang-circle, [data-theme="dark"] .lang-circle{ color: #fff; } /* 两段文字:保持原来的绝对定位方式与字号 */ .lang-circle span{ position: absolute; font-family: system-ui, -apple-system, "Segoe UI", Arial, "PingFang SC", "Microsoft YaHei", sans-serif; font-weight: 700; font-size: 10px; /* 保持原来小图标的视觉比例 */ line-height: 1; /* 默认弱化 */ opacity: .45; transition: opacity .2s ease; /* 用 currentColor 上色 */ color: currentColor; } /* 中:左上(保持你之前那组位置) */ .lang-circle .zh{ top: 5px; left: 6px; } /* En:右下(保持你之前那组位置) */ .lang-circle .en{ bottom: 5px; right: 5px; } /* 当前语言高亮(沿用 checkbox 状态) */ #lang-toggle:not(:checked) + .lang-circle .zh{ opacity: 1; } #lang-toggle:checked + .lang-circle .en{ opacity: 1; } /* 关键:彻底去掉“圆形/弧线”(如果之前版本存在伪元素) */ .lang-circle::before, .lang-circle::after{ content: none !important; } /* ===== 旧版/常见选择器 ===== */ iframe.goog-te-banner-frame, .goog-te-banner-frame, .goog-te-banner-frame.skiptranslate, .goog-te-banner, .goog-te-balloon-frame, #goog-gt-tt, .goog-tooltip, .goog-tooltip:hover { display: none !important; } /* Google 往 body/html 上加的顶端偏移,强制清掉 */ html, body { top: 0 !important; margin-top: 0 !important; } /* ===== 新版(常见是 VIpgJd- 前缀)顶部条/弹层 ===== */ .VIpgJd-ZVi9od-ORHb, /* 顶部横幅容器(常见) */ .VIpgJd-ZVi9od-ORHb-OEVmcd, /* 顶部横幅 */ .VIpgJd-ZVi9od-aZ2wEe-wOHMyf, /* 相关包裹层(不同版本会出现) */ .VIpgJd-ZVi9od-l4eHX-hSRGPd, /* 可能的提示/浮层 */ .VIpgJd-ZVi9od-SmfZ-OEVmcd { /* 可能的另一类顶条 */ display: none !important; }
  • 自定义Script
js
(function () { const LS_KEY = "vb_lang"; // localStorage 里仍用 zh-CN / en,兼容你历史数据 const SOURCE_LANG = "zh-CN"; const TARGET_EN = "en"; // xnx3/translate 的语言标识 const T_ZH = "chinese_simplified"; const T_EN = "english"; function getSavedLang() { return localStorage.getItem(LS_KEY) || SOURCE_LANG; } function setSavedLang(lang) { localStorage.setItem(LS_KEY, lang); document.documentElement.setAttribute("data-vb-lang", lang); } function setToggleCheckedByLang(lang) { const toggle = document.getElementById("lang-toggle"); if (toggle) toggle.checked = lang === TARGET_EN; } // —— 懒加载并初始化 translate.js(只做一次)—— function initTranslateOnce() { if (window.__vb_translate_init_promise) return window.__vb_translate_init_promise; window.__vb_translate_init_promise = new Promise((resolve, reject) => { function doInit() { try { // 设置当前网页原始语言(中文简体) translate.language.setLocal(T_ZH); // 设置翻译服务通道 translate.service.use("client.edge"); // 忽略你的语言切换按钮区域,避免 “中” 被翻成 middle if (translate.ignore) { if (Array.isArray(translate.ignore.class) && !translate.ignore.class.includes("notranslate")) { translate.ignore.class.push("notranslate"); } if (Array.isArray(translate.ignore.id)) { ["vb-lang-wrap", "lang-toggle"].forEach((id) => { if (!translate.ignore.id.includes(id)) translate.ignore.id.push(id); }); } } // 动态监控(如果你想进一步提速,也可以在翻译成功后再 start) translate.listener.start(); translate.execute(); resolve(); } catch (e) { reject(e); } } // 若已存在则直接 init if (window.translate && typeof window.translate.execute === "function") { doInit(); return; } // 否则:此时才加载脚本(关键:只在需要时才发生) const s = document.createElement("script"); s.src = "https://cdn.staticfile.net/translate.js/3.18.66/translate.js"; s.async = true; s.onload = doInit; s.onerror = () => reject(new Error("Failed to load translate.js")); document.head.appendChild(s); }); return window.__vb_translate_init_promise; } function switchTranslateLang(target) { const t = window.translate; if (!t) return; if (typeof t.changeLanguage === "function") { t.changeLanguage(target); return; } if (t.language && typeof t.language.setCurrent === "function") { t.language.setCurrent(target); if (typeof t.execute === "function") t.execute(); return; } if (typeof t.execute === "function") t.execute(); } async function applyLang(lang) { setSavedLang(lang); setToggleCheckedByLang(lang); // 默认中文:不加载脚本、不做任何事(保持原文,最快) if (lang === SOURCE_LANG) { // 如果之前已经加载并翻译过,为了彻底还原最稳妥的是刷新 // 如果你不想刷新,可以改成:if (window.translate) switchTranslateLang(T_ZH); if (window.translate) location.reload(); return; } // 切到英文:此时才加载 + 初始化 + 翻译 await initTranslateOnce(); switchTranslateLang(T_EN); } // 插入到“亮暗切换”和“RSS订阅”之间(RSS 前) function ensureMounted() { const wrap = document.getElementById("vb-lang-wrap"); const nav = document.querySelector(".nav-action"); if (!wrap) return false; if (!nav) { wrap.style.display = "none"; return false; } const rss = nav.querySelector('[title="RSS 订阅"]'); if (rss) { if (wrap.parentElement !== nav || wrap.nextElementSibling !== rss) { nav.insertBefore(wrap, rss); } } else { if (!nav.contains(wrap)) nav.appendChild(wrap); } wrap.style.display = "inline-flex"; return true; } // 监听 checkbox 切换 document.addEventListener("change", (e) => { if (e.target && e.target.id === "lang-toggle") { const lang = e.target.checked ? TARGET_EN : SOURCE_LANG; applyLang(lang).catch(console.error); } }); window.addEventListener("load", () => { ensureMounted(); const saved = getSavedLang(); setSavedLang(saved); setToggleCheckedByLang(saved); // 只有“上次保存是英文”才自动加载并翻译 if (saved === TARGET_EN) { setTimeout(() => { applyLang(TARGET_EN).catch(console.error); }, 300); } }); // 导航会重渲染:持续确保挂载 const obs = new MutationObserver(() => ensureMounted()); obs.observe(document.documentElement, { childList: true, subtree: true }); ensureMounted(); })();
  • 自定义HTML(Body)
js
<!-- 语言切换(不参与翻译) --> <div id="vb-lang-wrap" style="display:none;" class="notranslate" translate="no" lang="zh-CN"> <input type="checkbox" id="lang-toggle" hidden> <label for="lang-toggle" class="lang-circle notranslate" translate="no" lang="zh-CN" title="切换语言" aria-label="切换中英文"> <span class="zh notranslate" translate="no" lang="zh-CN"></span> <span class="en notranslate" translate="no" lang="en">En</span> </label> </div>

2. 使用开源(https://github.com/xnx3/translate) 网页翻译插件初始加载

优点:点击语言切换时速度快

缺点:初始加载网页速度慢

  • 自定义CSS
css
.footer-powered-by-vanblog{ display: none} /* 包裹容器:用于插入导航栏 */ #vb-lang-wrap{ display: inline-flex; align-items: center; } /* 间距微调:让它更贴近左侧“亮暗切换”,并与右侧 RSS 保持适中距离 不满意就调这两个值: - margin-left: -8 ~ 0 - margin-right: 8 ~ 16 */ .nav-action #vb-lang-wrap{ margin-left: -6px; margin-right: 12px; } /* label 本体:保持原来“一个正方形区域”,用于绝对定位两行字 */ .lang-circle{ --s: 28px; /* 图标占位大小(之前的版本就是这个量级);想更大可改 30/32 */ position: relative; width: var(--s); height: var(--s); cursor: pointer; user-select: none; /* 颜色:亮色黑,暗色白(使用 currentColor 思路) */ color: #111; background: transparent; border: 0; padding: 0; line-height: 1; transform: translateZ(0); transition: transform .15s ease; } .lang-circle:hover{ transform: scale(1.15); } /* 暗色:白(兼容 tailwind dark / data-theme) */ .dark .lang-circle, [data-theme="dark"] .lang-circle{ color: #fff; } /* 两段文字:保持原来的绝对定位方式与字号 */ .lang-circle span{ position: absolute; font-family: system-ui, -apple-system, "Segoe UI", Arial, "PingFang SC", "Microsoft YaHei", sans-serif; font-weight: 700; font-size: 10px; /* 保持原来小图标的视觉比例 */ line-height: 1; /* 默认弱化 */ opacity: .45; transition: opacity .2s ease; /* 用 currentColor 上色 */ color: currentColor; } /* 中:左上(保持你之前那组位置) */ .lang-circle .zh{ top: 5px; left: 6px; } /* En:右下(保持你之前那组位置) */ .lang-circle .en{ bottom: 5px; right: 5px; } /* 当前语言高亮(沿用 checkbox 状态) */ #lang-toggle:not(:checked) + .lang-circle .zh{ opacity: 1; } #lang-toggle:checked + .lang-circle .en{ opacity: 1; } /* 关键:彻底去掉“圆形/弧线”(如果之前版本存在伪元素) */ .lang-circle::before, .lang-circle::after{ content: none !important; } /* ===== 旧版/常见选择器 ===== */ iframe.goog-te-banner-frame, .goog-te-banner-frame, .goog-te-banner-frame.skiptranslate, .goog-te-banner, .goog-te-balloon-frame, #goog-gt-tt, .goog-tooltip, .goog-tooltip:hover { display: none !important; } /* Google 往 body/html 上加的顶端偏移,强制清掉 */ html, body { top: 0 !important; margin-top: 0 !important; } /* ===== 新版(常见是 VIpgJd- 前缀)顶部条/弹层 ===== */ .VIpgJd-ZVi9od-ORHb, /* 顶部横幅容器(常见) */ .VIpgJd-ZVi9od-ORHb-OEVmcd, /* 顶部横幅 */ .VIpgJd-ZVi9od-aZ2wEe-wOHMyf, /* 相关包裹层(不同版本会出现) */ .VIpgJd-ZVi9od-l4eHX-hSRGPd, /* 可能的提示/浮层 */ .VIpgJd-ZVi9od-SmfZ-OEVmcd { /* 可能的另一类顶条 */ display: none !important; }
  • 自定义Script
js
(function () { const LS_KEY = "vb_lang"; // localStorage 里仍用 zh-CN / en,兼容你历史数据 const SOURCE_LANG = "zh-CN"; const TARGET_EN = "en"; // xnx3/translate 的语言标识 const T_ZH = "chinese_simplified"; const T_EN = "english"; function getSavedLang() { return localStorage.getItem(LS_KEY) || SOURCE_LANG; } function setSavedLang(lang) { localStorage.setItem(LS_KEY, lang); document.documentElement.setAttribute("data-vb-lang", lang); } function setToggleCheckedByLang(lang) { const toggle = document.getElementById("lang-toggle"); if (toggle) toggle.checked = lang === TARGET_EN; } // —— 初始化 translate.js(只做一次)—— function initTranslateOnce() { if (window.__vb_translate_init_promise) return window.__vb_translate_init_promise; window.__vb_translate_init_promise = new Promise((resolve, reject) => { function doInit() { try { // 设置当前网页原始语言(中文简体) translate.language.setLocal(T_ZH); // 设置翻译服务通道 translate.service.use("client.edge"); // ===== 方案A:新增忽略翻译配置(关键)===== // 目标:不要翻译你的语言切换图标里的“中/En” // 注意:不同版本 translate.js 的 ignore 结构可能略有差异,这里做了容错。 if (window.translate && translate.ignore) { // 忽略 class:notranslate if (Array.isArray(translate.ignore.class)) { if (!translate.ignore.class.includes("notranslate")) { translate.ignore.class.push("notranslate"); } } else if (typeof translate.ignore.class === "string") { // 少数实现可能是字符串:转成数组或拼接 if (!translate.ignore.class.includes("notranslate")) { translate.ignore.class += ",notranslate"; } } // 忽略 id:vb-lang-wrap / lang-toggle(以及 label 上的 lang-circle 如有需要也可加) if (Array.isArray(translate.ignore.id)) { ["vb-lang-wrap", "lang-toggle"].forEach((id) => { if (!translate.ignore.id.includes(id)) translate.ignore.id.push(id); }); } else if (typeof translate.ignore.id === "string") { ["vb-lang-wrap", "lang-toggle"].forEach((id) => { if (!translate.ignore.id.includes(id)) translate.ignore.id += `,${id}`; }); } } // ===== 方案A结束 ===== // 开启页面元素动态监控 translate.listener.start(); // 完成翻译初始化,进行翻译 translate.execute(); resolve(); } catch (e) { reject(e); } } // 如果你没在 Head 引入 translate.js,这里动态加载兜底 if (window.translate && typeof window.translate.execute === "function") { doInit(); } else { const s = document.createElement("script"); s.src = "https://cdn.staticfile.net/translate.js/3.18.66/translate.js"; s.async = true; s.onload = doInit; s.onerror = () => reject(new Error("Failed to load translate.js")); document.head.appendChild(s); } }); return window.__vb_translate_init_promise; } // 切换语言:优先调用 changeLanguage,做一些兜底 function switchTranslateLang(target) { const t = window.translate; if (!t) return; if (typeof t.changeLanguage === "function") { t.changeLanguage(target); return; } if (t.language && typeof t.language.setCurrent === "function") { t.language.setCurrent(target); if (typeof t.execute === "function") t.execute(); return; } if (typeof t.execute === "function") t.execute(); } async function applyLang(lang) { setSavedLang(lang); setToggleCheckedByLang(lang); await initTranslateOnce(); if (lang === TARGET_EN) { switchTranslateLang(T_EN); } else { switchTranslateLang(T_ZH); } } // 插入到“亮暗切换”和“RSS订阅”之间(RSS 前) function ensureMounted() { const wrap = document.getElementById("vb-lang-wrap"); const nav = document.querySelector(".nav-action"); if (!wrap) return false; if (!nav) { wrap.style.display = "none"; return false; } const rss = nav.querySelector('[title="RSS 订阅"]'); if (rss) { if (wrap.parentElement !== nav || wrap.nextElementSibling !== rss) { nav.insertBefore(wrap, rss); } } else { if (!nav.contains(wrap)) nav.appendChild(wrap); } wrap.style.display = "inline-flex"; return true; } // 监听 checkbox 切换 document.addEventListener("change", (e) => { if (e.target && e.target.id === "lang-toggle") { const lang = e.target.checked ? TARGET_EN : SOURCE_LANG; applyLang(lang).catch(console.error); } }); window.addEventListener("load", () => { ensureMounted(); const saved = getSavedLang(); setSavedLang(saved); setToggleCheckedByLang(saved); initTranslateOnce() .then(() => { setTimeout(() => { applyLang(saved).catch(console.error); }, 300); }) .catch(console.error); }); // 导航会重渲染:持续确保挂载 const obs = new MutationObserver(() => ensureMounted()); obs.observe(document.documentElement, { childList: true, subtree: true }); ensureMounted(); })();
  • 自定义HTML(Body)
js
<!-- 语言切换(不参与翻译) --> <div id="vb-lang-wrap" style="display:none;" class="notranslate" translate="no" lang="zh-CN"> <input type="checkbox" id="lang-toggle" hidden> <label for="lang-toggle" class="lang-circle notranslate" translate="no" lang="zh-CN" title="切换语言" aria-label="切换中英文"> <span class="zh notranslate" translate="no" lang="zh-CN"></span> <span class="en notranslate" translate="no" lang="en">En</span> </label> </div>
  • 自定义HTML(Head)
js
<script src="https://cdn.staticfile.net/translate.js/3.18.66/translate.js"></script>

3. 使用Google网页翻译插件

  • 自定义CSS
css
.footer-powered-by-vanblog{ display: none} /* ====== 防止 Google Translate 顶部横幅把页面顶下来 ====== */ .goog-te-banner-frame.skiptranslate { display: none !important; } body { top: 0 !important; } #goog-gt-tt, .goog-te-balloon-frame { display: none !important; } /* ====== 语言切换:保留“中/En”原相对位置,去掉圆形外圈 ====== */ /* 包裹容器:用于插入导航栏 */ #vb-lang-wrap{ display: inline-flex; align-items: center; } /* 间距微调:让它更贴近左侧“亮暗切换”,并与右侧 RSS 保持适中距离 不满意就调这两个值: - margin-left: -8 ~ 0 - margin-right: 8 ~ 16 */ .nav-action #vb-lang-wrap{ margin-left: -6px; margin-right: 12px; } /* label 本体:保持原来“一个正方形区域”,用于绝对定位两行字 */ .lang-circle{ --s: 28px; /* 图标占位大小(之前的版本就是这个量级);想更大可改 30/32 */ position: relative; width: var(--s); height: var(--s); cursor: pointer; user-select: none; /* 颜色:亮色黑,暗色白(使用 currentColor 思路) */ color: #111; background: transparent; border: 0; padding: 0; line-height: 1; transform: translateZ(0); transition: transform .15s ease; } .lang-circle:hover{ transform: scale(1.15); } /* 暗色:白(兼容 tailwind dark / data-theme) */ .dark .lang-circle, [data-theme="dark"] .lang-circle{ color: #fff; } /* 两段文字:保持原来的绝对定位方式与字号 */ .lang-circle span{ position: absolute; font-family: system-ui, -apple-system, "Segoe UI", Arial, "PingFang SC", "Microsoft YaHei", sans-serif; font-weight: 700; font-size: 10px; /* 保持原来小图标的视觉比例 */ line-height: 1; /* 默认弱化 */ opacity: .45; transition: opacity .2s ease; /* 用 currentColor 上色 */ color: currentColor; } /* 中:左上(保持你之前那组位置) */ .lang-circle .zh{ top: 5px; left: 6px; } /* En:右下(保持你之前那组位置) */ .lang-circle .en{ bottom: 5px; right: 5px; } /* 当前语言高亮(沿用 checkbox 状态) */ #lang-toggle:not(:checked) + .lang-circle .zh{ opacity: 1; } #lang-toggle:checked + .lang-circle .en{ opacity: 1; } /* 关键:彻底去掉“圆形/弧线”(如果之前版本存在伪元素) */ .lang-circle::before, .lang-circle::after{ content: none !important; } /* ===== 旧版/常见选择器 ===== */ iframe.goog-te-banner-frame, .goog-te-banner-frame, .goog-te-banner-frame.skiptranslate, .goog-te-banner, .goog-te-balloon-frame, #goog-gt-tt, .goog-tooltip, .goog-tooltip:hover { display: none !important; } /* Google 往 body/html 上加的顶端偏移,强制清掉 */ html, body { top: 0 !important; margin-top: 0 !important; } /* ===== 新版(常见是 VIpgJd- 前缀)顶部条/弹层 ===== */ .VIpgJd-ZVi9od-ORHb, /* 顶部横幅容器(常见) */ .VIpgJd-ZVi9od-ORHb-OEVmcd, /* 顶部横幅 */ .VIpgJd-ZVi9od-aZ2wEe-wOHMyf, /* 相关包裹层(不同版本会出现) */ .VIpgJd-ZVi9od-l4eHX-hSRGPd, /* 可能的提示/浮层 */ .VIpgJd-ZVi9od-SmfZ-OEVmcd { /* 可能的另一类顶条 */ display: none !important; }
  • 自定义Script
js
(function () { const LS_KEY = "vb_lang"; const SOURCE_LANG = "zh-CN"; const TARGET_EN = "en"; function getSavedLang() { return localStorage.getItem(LS_KEY) || SOURCE_LANG; } function setSavedLang(lang) { localStorage.setItem(LS_KEY, lang); document.documentElement.setAttribute("data-vb-lang", lang); } function setToggleCheckedByLang(lang) { const toggle = document.getElementById("lang-toggle"); if (toggle) toggle.checked = (lang === TARGET_EN); } function setGoogTransCookie(targetLang) { const v = `/${SOURCE_LANG}/${targetLang}`; document.cookie = `googtrans=${v};path=/;max-age=31536000`; document.cookie = `googtrans=${v};path=/;domain=${location.hostname};max-age=31536000`; } function ensureGTMount() { let mount = document.getElementById("google_translate_element"); if (!mount) { mount = document.createElement("div"); mount.id = "google_translate_element"; mount.style.cssText = "position:fixed;left:-9999px;top:0;"; // 关键:挂到 body 末尾,尽量避开 React/Next 管理的节点 document.body.appendChild(mount); } return mount; } function waitForCombo(timeout = 8000) { const start = Date.now(); return new Promise((resolve, reject) => { (function check() { const combo = document.querySelector(".goog-te-combo"); if (combo) return resolve(combo); if (Date.now() - start > timeout) return reject(new Error("goog-te-combo not found")); setTimeout(check, 100); })(); }); } // 按需加载/初始化 Google Translate(避免 Next.js hydration 阶段改 DOM) function loadGoogleTranslate() { if (window.google?.translate?.TranslateElement) return Promise.resolve(); if (window.__vb_gt_loading) return window.__vb_gt_loading; window.__vb_gt_loading = new Promise((resolve, reject) => { window.googleTranslateElementInit = function () { try { ensureGTMount(); new google.translate.TranslateElement( { pageLanguage: SOURCE_LANG, includedLanguages: `${SOURCE_LANG},${TARGET_EN}`, autoDisplay: false, }, "google_translate_element" ); resolve(); } catch (e) { reject(e); } }; const s = document.createElement("script"); s.src = "https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"; s.async = true; s.onerror = () => reject(new Error("Failed to load Google Translate script")); document.head.appendChild(s); }); return window.__vb_gt_loading; } async function applyLang(lang) { setSavedLang(lang); setToggleCheckedByLang(lang); setGoogTransCookie(lang); // 切回中文:最稳的是刷新还原(否则有时翻译残留) if (lang === SOURCE_LANG) { location.reload(); return; } // 切到英文:按需加载并触发 await loadGoogleTranslate(); const combo = await waitForCombo(); // 兼容 value = en / zh-CN const wanted = lang === TARGET_EN ? ["en", "en-US", "en-GB"] : ["zh-CN", "zh"]; const opt = Array.from(combo.options).find(o => wanted.includes(o.value)); if (opt) { combo.value = opt.value; combo.dispatchEvent(new Event("change", { bubbles: true })); } } // 插入到“亮暗切换”和“RSS订阅”之间(即 RSS 前) function ensureMounted() { const wrap = document.getElementById("vb-lang-wrap"); const nav = document.querySelector(".nav-action"); if (!wrap) return false; if (!nav) { wrap.style.display = "none"; return false; } const rss = nav.querySelector('[title="RSS 订阅"]'); if (rss) { if (wrap.parentElement !== nav || wrap.nextElementSibling !== rss) { nav.insertBefore(wrap, rss); } } else { if (!nav.contains(wrap)) nav.appendChild(wrap); } wrap.style.display = "inline-flex"; return true; } // 监听 checkbox 切换 document.addEventListener("change", (e) => { if (e.target && e.target.id === "lang-toggle") { const lang = e.target.checked ? TARGET_EN : SOURCE_LANG; applyLang(lang).catch(console.error); } }); window.addEventListener("load", () => { ensureMounted(); // 同步 UI 状态 const saved = getSavedLang(); setSavedLang(saved); setToggleCheckedByLang(saved); setGoogTransCookie(saved); // 如果上次是英文:延迟一点再翻译,避开 Next.js 水合阶段 if (saved === TARGET_EN) { setTimeout(() => { applyLang(TARGET_EN).catch(console.error); }, 1200); } }); // 导航会重渲染:持续确保挂载 const obs = new MutationObserver(() => ensureMounted()); obs.observe(document.documentElement, { childList: true, subtree: true }); ensureMounted(); })();
  • 自定义HTML(Body)
js
<!-- 语言切换(不参与翻译) --> <div id="vb-lang-wrap" style="display:none;" class="notranslate" translate="no" lang="zh-CN"> <input type="checkbox" id="lang-toggle" hidden> <label for="lang-toggle" class="lang-circle notranslate" translate="no" lang="zh-CN" title="切换语言" aria-label="切换中英文"> <span class="zh notranslate" translate="no" lang="zh-CN"></span> <span class="en notranslate" translate="no" lang="en">En</span> </label> </div>