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