🟩 1. 初衷和问题背景

在博客中引入翻译功能,常见做法有两种:

  • 嵌入 Google Translate 插件 ✅ 快捷,但影响样式、不可控;
  • 自己调用 Google API 实现 ✅ 灵活,但有缓存/性能/兼容性问题。

于是我决定打造一个完全自主可控的翻译系统,适配我博客使用的模版 Hexo+Butterfly。

image-20250502211434488


🟩 2. 实现亮点

  • ✅ 支持中文 ↔ 英文切换;
  • ⚙️ 使用 translate.googleapis.com API;
  • 🔄 分批翻译文本(30条一批),解决长度限制;
  • 🧠 缓存翻译结果,切换语言无需重复调用;
  • 💡 支持标题、正文、段落、代码块翻译;
  • 🌐 提供”整页翻译”功能;
  • 🎯 自动识别当前语言,保留用户偏好;
  • 🧼 翻译状态进度提示,用户体验更友好;
  • 🧱 代码模块清晰,易于拓展到法语、日语等。

🟩 3. 小Bug & 已知问题

  • 🔁 当前版本中,语言切换时可能需要点两次(因为加载+缓存尚未写入);
  • 🧪 后续考虑使用 MutationObserver 监控 PJAX 页面刷新内容自动绑定;
  • 📦 正在测试是否适配移动端(如按钮定位)。

🟩 4. 核心代码解析

下面我将展示并讲解一些关键代码部分,而不是全部代码(全代码有700+行)。

4.1 系统初始化与状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 封装在立即执行函数中,避免全局变量污染
(function () {
console.log('DEBUG: 初始化翻译系统 - 版本 1.8.0 (纯整页翻译版)');

// 核心状态管理
let currentLang = localStorage.getItem('blog_language') || 'zh'; // 从本地存储读取语言偏好
let isTranslating = false; // 翻译进行中标记
let isInitialized = false; // 初始化状态标记

// 使用Map结构存储原始内容和翻译内容
const originalContent = new Map();
const translatedContent = new Map();
// 翻译缓存
const translateCache = new Map();

// 防止重复初始化
if (window.autoTranslateInitialized) {
console.log('DEBUG: 翻译系统已经初始化,跳过重复初始化');
return;
}
window.autoTranslateInitialized = true;

// 初始化入口函数
function initTranslation() {
// 初始化逻辑和UI更新...
}

// 在DOM加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initTranslation);
} else {
initTranslation();
}
})();

解析: 系统初始化与状态管理部分,使用立即执行函数避免全局变量污染。Map数据结构用于缓存原始内容和翻译内容,确保快速切换语言时无需重复调用API。

4.2 智能语言检测

1
2
3
4
5
6
// 语言检测函数
function detectLanguage(text) {
// 简单检测,超过50%的字符是中文则认为是中文
const chineseChars = text.match(/[\u4e00-\u9fa5]/g) || [];
return chineseChars.length / text.length > 0.5 ? 'zh' : 'en';
}

解析: 通过Unicode字符范围匹配中文字符,计算文本中中文字符占比,实现简单高效的语言检测。当中文字符占比超过50%时,判定为中文文本。

4.3 整页翻译按钮功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 添加独立翻译按钮
function addStandaloneTranslateButton() {
if (document.getElementById('standalone-translate-btn')) return;
const btn = document.createElement('button');
btn.id = 'standalone-translate-btn';
btn.style.position = 'fixed';
btn.style.bottom = '32px';
btn.style.right = '32px';
btn.style.zIndex = '99999';
btn.style.background = '#4a7dbe';
btn.style.color = '#fff';
btn.style.border = 'none';
btn.style.borderRadius = '50%';
btn.style.width = '48px';
btn.style.height = '48px';
btn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
btn.style.cursor = 'pointer';
btn.style.fontSize = '16px';
btn.style.transition = 'background 0.2s, opacity 0.3s';
btn.style.opacity = '0.9';
btn.title = currentLang === 'zh' ? 'Translate to English' : '切换为中文';
btn.textContent = currentLang === 'zh' ? 'EN' : '中';
btn.addEventListener('click', () => {
if (isTranslating) return;
if (currentLang === 'zh') {
translate('en');
} else {
restoreOriginalContent();
}
});
document.body.appendChild(btn);
}

解析: 在页面右下角添加一个固定位置的翻译按钮,点击时可以在中英文之间切换。按钮根据当前语言状态显示不同的文本和提示。

4.4 分批翻译处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
async function translate(targetLang) {
// 前置检查与准备...

// 查找可翻译元素
const elements = findTranslatableElements(container);

// 首次翻译时保存原始内容
elements.forEach(el => {
if (!originalContent.has(el)) {
originalContent.set(el, el.textContent);
}
});

// 分批处理翻译
const batchSize = 30; // 每批30个元素
let completedBatches = 0;
const totalBatches = Math.ceil(elements.length / batchSize);

for (let i = 0; i < elements.length; i += batchSize) {
const batch = elements.slice(i, i + batchSize);
const texts = batch.map(el => el.textContent.trim()).filter(Boolean);
completedBatches++;

// 进度显示
const progress = Math.floor((completedBatches / totalBatches) * 100);
showTranslationProgressBar(progress, `翻译进度: ${completedBatches}/${totalBatches}`);

// 检查缓存中是否已有翻译结果
const cacheKey = `${combined}_zh-CN_en`;
let result;

if (translateCache.has(cacheKey)) {
result = translateCache.get(cacheKey);
console.log('DEBUG: 使用缓存的翻译结果');
} else {
// 拼接文本并用分隔符标记
const combined = texts.join('\n[SPLIT]\n');
result = await translateText(combined, 'zh-CN', 'en');

// 缓存翻译结果
if (result) {
translateCache.set(cacheKey, result);
}
}

// 处理翻译结果并应用...
}
}

解析: 这段代码展示了如何解决API长度限制问题——将所有需要翻译的文本分成多批(每批30个元素),使用特殊分隔符[SPLIT]连接发送,翻译后再拆分回来。增加了缓存机制,避免重复请求相同内容,大大提高效率同时避免频率限制。

4.5 Google翻译API调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function translateText(text, from, to) {
try {
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${from}&tl=${to}&dt=t&q=${encodeURIComponent(text)}`;
const response = await fetch(url);

if (!response.ok) throw new Error('翻译API请求失败');

const data = await response.json();
return data[0].map(item => item[0]).join('');
} catch (error) {
console.error('翻译失败:', error);
return null;
}
}

解析: 直接调用Google翻译API,无需认证密钥。使用client=gtx参数和合适的请求格式可免费使用。返回数据需要特殊处理:取第一层数组的每个元素的第一个值,并拼接起来。


🟩 5. 后续计划

  • ✅ 兼容夜间模式;
  • 🧪 添加本地词汇表(Glossary);
  • 🪙 引入我自己的 API Token(避免免费限流);
  • 🔀 添加更多语言的支持(法语、日语等);

🟩 6. 结尾

1
2
3
如果你觉得这个功能不错,欢迎点个赞 ⭐,或者留言告诉我你想翻译的语言 😄

🆕 最新版本更加简洁高效,仅保留了整页翻译功能,通过右下角固定的翻译按钮一键切换语言!

🟩 7. 开源声明

本翻译系统已开源,欢迎大家学习、使用和二次开发!

项目地址:Github开源仓库地址