add word translate

This commit is contained in:
scout 2024-11-13 18:40:15 +08:00
parent d33e96e662
commit 3eeb9f66b9
9 changed files with 676 additions and 52 deletions

View File

@ -1,51 +1,88 @@
# 丰链工具箱 Chrome 扩展
一个实用的 Chrome 扩展工具集合,目前包含二维码生成器和 Word 转 HTML 功能。
一个集成多种实用工具的 Chrome 扩展包括二维码生成、Word 转 HTML、Word 文档翻译等功能。
## 功能特
## 功能特
### 二维码生成器 🔲
### 1. 二维码生成
- 自动获取当前标签页 URL 并生成二维码
- 支持手动输入文本/链接生成二维码
- 支持手动输入文本链接生成二维码
- 高清晰度二维码输出
### Word 转 HTML 转换器 📄
### 2. Word 转 HTML
- 支持 .docx 文件转换为 HTML
- 保持原文档格式和样式
- 实时预览转换结果
- 保留文档格式和样式
- 支持下载转换后的 HTML 文件
- 一键下载 HTML 文件
## 安装方法
### 3. Word 文档翻译
- 支持中文文档翻译为英文、日文、繁体中文
- 完整保留原文档格式(包括字体、颜色、加粗等样式)
- 支持表格内容翻译
- 实时显示翻译进度
- 保持文档的换行格式
- 自动重试机制,提高翻译稳定性
## 安装说明
### 扩展安装
1. 下载本项目代码
2. 打开 Chrome 浏览器,进入扩展程序页面 (chrome://extensions/)
2. 打开 Chrome 浏览器,进入扩展管理页面 (chrome://extensions/)
3. 开启"开发者模式"
4. 点击"加载已解压的扩展程序"
5. 选择项目文件夹即可完成安装
4. 点击"加载已解压的扩展程序",选择项目目录
## 使用方法
### 后端服务安装
1. 确保已安装 Python 3.8+
2. 进入 backend 目录
3. 创建虚拟环境(可选):
```bash
python -m venv venv
# Windows
venv\Scripts\activate
# Mac/Linux
source venv/bin/activate
```
4. 安装依赖:
```bash
pip install -r requirements.txt
```
5. 启动后端服务:
```bash
python app.py
```
1. 点击 Chrome 工具栏中的扩展图标打开工具箱
2. 选择需要使用的功能标签页
3. 根据需要使用相应功能
## 使用说明
### 二维码生成
- 自动显示当前页面的二维码
- 可以在输入框中输入其他文本/链接并点击"生成二维码"
1. 点击扩展图标打开工具箱
2. 默认显示当前页面 URL 的二维码
3. 可在输入框中修改文本,点击"生成二维码"更新
### Word 转 HTML
- 点击选择文件上传 Word 文档
- 等待转换完成后预览效果
- 点击"下载 HTML"保存转换结果
1. 选择"Word转HTML"选项卡
2. 点击上传区域选择 .docx 文件
3. 等待转换完成后预览结果
4. 点击"下载HTML"保存文件
### Word 文档翻译
1. 选择"Word翻译"选项卡
2. 点击上传区域选择要翻译的 Word 文档
3. 从下拉菜单选择目标语言(英文/日文/繁体中文)
4. 点击"开始翻译"按钮
5. 等待翻译完成,自动下载翻译后的文档
## 注意事项
1. 使用翻译功能时需要确保后端服务正在运行
2. 翻译功能使用百度翻译 API请确保网络连接正常
3. 大文件翻译可能需要较长时间,请耐心等待
4. 建议定期清理 temp 目录下的临时文件
## 技术栈
- HTML5
- CSS3
- JavaScript
- Chrome Extension API
- QRCode.js
- Mammoth.js
- 前端HTML, CSS, JavaScript
- 后端Python, Flask
- 依赖库python-docx, mammoth.js, qrcode.js
## 开发者
@ -53,4 +90,4 @@ Made by Scout
## 许可证
[添加许可证类型]
MIT License

85
backend/app.py Normal file
View File

@ -0,0 +1,85 @@
from flask import Flask, request, send_file
from flask_cors import CORS
import os
from utils.docx_translator import DocxTranslator
import shutil
import time
app = Flask(__name__)
CORS(app, resources={
r"/*": {
"origins": ["chrome-extension://*"],
"methods": ["POST", "OPTIONS", "GET"],
"allow_headers": ["Content-Type"]
}
})
# 全局变量存储翻译进度
translation_progress = 0
UPLOAD_FOLDER = 'temp'
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
def update_progress(progress):
global translation_progress
translation_progress = progress
@app.route('/progress', methods=['GET'])
def get_progress():
return {'progress': translation_progress}
@app.route('/translate', methods=['POST'])
def translate_docx():
global translation_progress
translation_progress = 0
input_path = None
output_path = None
if 'file' not in request.files:
return {'error': 'No file provided'}, 400
file = request.files['file']
target_lang = request.form.get('target_lang', 'en')
if file.filename == '':
return {'error': 'No file selected'}, 400
try:
timestamp = str(int(time.time()))
safe_filename = f"{timestamp}_{file.filename}"
input_path = os.path.join(UPLOAD_FOLDER, safe_filename)
file.save(input_path)
translator = DocxTranslator()
output_path = translator.translate_document(
input_path,
target_lang=target_lang,
progress_callback=update_progress
)
temp_output = output_path + '.tmp'
shutil.copy2(output_path, temp_output)
return send_file(
temp_output,
as_attachment=True,
download_name=f"translated_{file.filename}"
)
except Exception as e:
return {'error': str(e)}, 500
finally:
try:
if input_path and os.path.exists(input_path):
os.remove(input_path)
if output_path and os.path.exists(output_path):
os.remove(output_path)
if 'temp_output' in locals() and os.path.exists(temp_output):
os.remove(temp_output)
except Exception as e:
print(f"Error cleaning up files: {e}")
if __name__ == '__main__':
app.run(debug=True, port=5000)

5
backend/requirements.txt Normal file
View File

@ -0,0 +1,5 @@
flask==2.3.3
werkzeug==2.3.7
flask-cors==4.0.0
python-docx==0.8.11
requests==2.31.0

View File

@ -0,0 +1,244 @@
from docx import Document
import hashlib
import time
import requests
import os
class DocxTranslator:
def __init__(self):
self.appid = "20241112002200806"
self.secret_key = "preM0becByYCdotRTP_a"
self.api_url = "https://api.fanyi.baidu.com/api/trans/vip/translate"
self.max_length = 2000
self.lang_map = {
'en': 'en', # English
'jp': 'jp', # Japanese
'cht': 'cht' # Traditional Chinese
}
def _get_sign(self, text, salt):
sign_str = f"{self.appid}{text}{salt}{self.secret_key}"
return hashlib.md5(sign_str.encode()).hexdigest()
def translate_text(self, text, to_lang):
target_lang = self.lang_map.get(to_lang, to_lang)
if len(text) > self.max_length:
segments = text.split('')
current_segment = ''
translated_segments = []
for segment in segments:
if len(current_segment) + len(segment) < self.max_length:
current_segment += segment + ''
else:
if current_segment:
translated_segments.append(self._translate_segment(current_segment, target_lang))
current_segment = segment + ''
if current_segment:
translated_segments.append(self._translate_segment(current_segment, target_lang))
return ''.join(translated_segments)
else:
return self._translate_segment(text, target_lang)
def _translate_segment(self, text, to_lang):
if not text.strip():
return text
salt = str(int(time.time()))
sign = self._get_sign(text, salt)
params = {
'q': text,
'from': 'zh',
'to': to_lang,
'appid': self.appid,
'salt': salt,
'sign': sign
}
max_retries = 3
for attempt in range(max_retries):
try:
response = requests.get(self.api_url, params=params)
result = response.json()
if 'error_code' in result:
if attempt < max_retries - 1:
time.sleep(1)
continue
raise Exception(f"Translation error: {result['error_msg']}")
return result['trans_result'][0]['dst']
except Exception as e:
if attempt < max_retries - 1:
time.sleep(1)
continue
raise e
def translate_document(self, input_path, target_lang='en', progress_callback=None):
doc = Document(input_path)
output_path = input_path.replace('.docx', f'_translated.docx')
try:
# 计算总翻译项
total_items = len(doc.paragraphs)
for table in doc.tables:
total_items += sum(len(row.cells) for row in table.rows)
current_item = 0
# 翻译段落
for paragraph in doc.paragraphs:
if paragraph.text.strip():
try:
# 保存原始的段落格式和换行
runs = paragraph.runs
original_runs = []
# 收集每个run的文本和格式信息保留原始换行符
for run in runs:
text = run.text
if text: # 不去除空白字符,保留原始格式
original_runs.append({
'text': text,
'bold': run.bold,
'italic': run.italic,
'underline': run.underline,
'font_name': run.font.name,
'font_size': run.font.size,
'color_rgb': run.font.color.rgb if run.font.color else None,
'break_type': 'break' if any(text.endswith(x) for x in ['\n', '\v', '\r']) else None
})
# 清除原有内容但保持段落格式
paragraph.clear()
# 分别翻译和添加每个run的文本
for i, orig_run in enumerate(original_runs):
if orig_run['text']:
# 保留换行符
has_break = orig_run['break_type'] == 'break'
text_to_translate = orig_run['text'].rstrip('\n\r\v')
# 翻译非空文本
if text_to_translate.strip():
translated_text = self.translate_text(text_to_translate, target_lang)
else:
translated_text = text_to_translate
# 创建新的run并应用格式
new_run = paragraph.add_run()
new_run.bold = orig_run['bold']
new_run.italic = orig_run['italic']
new_run.underline = orig_run['underline']
new_run.font.name = orig_run['font_name']
if orig_run['font_size']:
new_run.font.size = orig_run['font_size']
if orig_run['color_rgb']:
new_run.font.color.rgb = orig_run['color_rgb']
# 添加翻译后的文本
new_run.text = translated_text
# 如果原文有换行,添加换行符
if has_break:
new_run.add_break() # 不指定类型,使用默认换行
except Exception as e:
print(f"Error translating paragraph: {e}")
# 保留原文和格式
paragraph.clear()
for run in runs:
new_run = paragraph.add_run(run.text)
new_run.bold = run.bold
new_run.italic = run.italic
new_run.underline = run.underline
new_run.font.name = run.font.name
if run.font.size:
new_run.font.size = run.font.size
if run.font.color and run.font.color.rgb:
new_run.font.color.rgb = run.font.color.rgb
current_item += 1
if progress_callback:
progress_callback(int((current_item / total_items) * 100))
# 翻译表格 (使用相同的逻辑处理表格单元格)
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
if cell.text.strip():
try:
for paragraph in cell.paragraphs:
runs = paragraph.runs
original_runs = []
for run in runs:
text = run.text
if text:
original_runs.append({
'text': text,
'bold': run.bold,
'italic': run.italic,
'underline': run.underline,
'font_name': run.font.name,
'font_size': run.font.size,
'color_rgb': run.font.color.rgb if run.font.color else None,
'break_type': 'break' if any(text.endswith(x) for x in ['\n', '\v', '\r']) else None
})
paragraph.clear()
for i, orig_run in enumerate(original_runs):
if orig_run['text']:
has_break = orig_run['break_type'] == 'break'
text_to_translate = orig_run['text'].rstrip('\n\r\v')
if text_to_translate.strip():
translated_text = self.translate_text(text_to_translate, target_lang)
else:
translated_text = text_to_translate
new_run = paragraph.add_run()
new_run.bold = orig_run['bold']
new_run.italic = orig_run['italic']
new_run.underline = orig_run['underline']
new_run.font.name = orig_run['font_name']
if orig_run['font_size']:
new_run.font.size = orig_run['font_size']
if orig_run['color_rgb']:
new_run.font.color.rgb = orig_run['color_rgb']
new_run.text = translated_text
if has_break:
new_run.add_break()
except Exception as e:
print(f"Error translating cell: {e}")
# 保留原文和格式
for paragraph in cell.paragraphs:
paragraph.clear()
for run in runs:
new_run = paragraph.add_run(run.text)
new_run.bold = run.bold
new_run.italic = run.italic
new_run.underline = run.underline
new_run.font.name = run.font.name
if run.font.size:
new_run.font.size = run.font.size
if run.font.color and run.font.color.rgb:
new_run.font.color.rgb = run.font.color.rgb
current_item += 1
if progress_callback:
progress_callback(int((current_item / total_items) * 100))
doc.save(output_path)
return output_path
finally:
del doc

View File

@ -11,5 +11,10 @@
"action": {
"default_popup": "src/html/popup.html"
},
"permissions": ["activeTab"]
"permissions": [
"activeTab"
],
"host_permissions": [
"http://localhost:5000/*"
]
}

View File

@ -25,8 +25,9 @@ h1 {
}
.tab {
display: flex;
gap: 10px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
margin-bottom: 20px;
background: #f5f5f5;
padding: 5px;
@ -34,19 +35,24 @@ h1 {
}
.tab button {
flex: 1;
padding: 12px;
padding: 8px 4px;
font-size: 12px;
white-space: nowrap;
min-width: 0;
border: none;
background: transparent;
color: #666;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s ease;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
gap: 4px;
}
.tab button .icon {
font-size: 14px;
}
.tab button:hover {
@ -203,11 +209,10 @@ button:hover {
.status-message {
margin: 10px 0;
padding: 10px;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
display: none;
animation: fadeIn 0.3s ease;
font-size: 13px;
text-align: center;
}
.status-message.success {
@ -241,4 +246,82 @@ button:hover {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 添加新的样式 */
.language-select {
margin: 10px 0;
}
.language-select select {
width: 100%;
padding: 8px;
border-radius: 4px;
border: 1px solid #ccc;
}
.translate-btn {
width: 100%;
padding: 10px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
}
.translate-btn:hover {
background: #45a049;
}
/* 添加进度条样式 */
.progress-container {
margin: 15px 0;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
height: 4px;
display: none;
}
.progress-bar {
height: 100%;
background: #1a73e8;
width: 0;
transition: width 0.3s ease;
}
/* 添加进度文本样式 */
.progress-text {
font-size: 12px;
color: #666;
text-align: center;
margin-top: 5px;
display: none;
}
/* 修改选项卡文字样式 */
.tab button[data-tab="qr"] {
font-size: 12px;
}
.tab button[data-tab="qr"]::after {
content: "二维码生成";
}
.tab button[data-tab="word"] {
font-size: 12px;
}
.tab button[data-tab="word"]::after {
content: "Word转HTML";
}
.tab button[data-tab="translate"] {
font-size: 12px;
}
.tab button[data-tab="translate"]::after {
content: "Word翻译";
}

View File

@ -12,11 +12,12 @@
<div class="tab">
<button class="tablinks active" data-tab="qr">
<span class="icon">🔲</span>
二维码生成
</button>
<button class="tablinks" data-tab="word">
<span class="icon">📄</span>
Word转HTML
</button>
<button class="tablinks" data-tab="translate">
<span class="icon">🔄</span>
</button>
</div>
@ -43,6 +44,32 @@
下载HTML
</button>
</div>
<div id="translate" class="tabcontent" style="display:none;">
<div class="file-upload">
<label for="wordTranslateFile" class="file-label">
<span class="icon">📎</span>
<span id="translateFileNameDisplay">选择Word文件</span>
</label>
<input type="file" id="wordTranslateFile" accept=".docx" hidden>
</div>
<div class="language-select">
<select id="targetLang">
<option value="en">英文</option>
<option value="jp">日文</option>
<option value="cht">繁体中文</option>
</select>
</div>
<div id="translateProgress" class="progress-container">
<div class="progress-bar"></div>
</div>
<div id="progressText" class="progress-text"></div>
<div id="translateStatus" class="status-message"></div>
<button id="translateBtn" class="translate-btn" style="display:none;">
<span class="icon">🔄</span>
开始翻译
</button>
</div>
</div>
<script src="../js/popup.js"></script>
</body>

View File

@ -13,24 +13,41 @@ document.addEventListener('DOMContentLoaded', function() {
const tabs = document.querySelectorAll('.tablinks');
const tabContents = document.querySelectorAll('.tabcontent');
function switchTab(tabName) {
// 移除所有标签的激活状态
tabs.forEach(tab => tab.classList.remove('active'));
// 隐藏所有内容
tabContents.forEach(content => {
content.style.display = 'none';
});
// 激活选中的标签
const selectedTab = document.querySelector(`[data-tab="${tabName}"]`);
if (selectedTab) {
selectedTab.classList.add('active');
}
// 显示对应内容
const selectedContent = document.getElementById(tabName);
if (selectedContent) {
selectedContent.style.display = 'block';
}
}
// 为每个标签添加点击事件
tabs.forEach(tab => {
tab.addEventListener('click', (e) => {
const tabName = e.target.getAttribute('data-tab');
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
tabContents.forEach(content => {
content.style.display = 'none';
});
const selectedContent = document.getElementById(tabName);
if (selectedContent) {
selectedContent.style.display = 'block';
const tabName = e.currentTarget.getAttribute('data-tab');
if (tabName) {
switchTab(tabName);
}
});
});
// 初始化显示第一个标签页
switchTab('qr');
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
if (tabs[0] && tabs[0].url) {
urlInput.value = tabs[0].url;
@ -131,4 +148,125 @@ document.addEventListener('DOMContentLoaded', function() {
saveAs: true
});
});
// 添加百度翻译API配置
const BAIDU_APPID = '20241112002200806';
const BAIDU_KEY = 'preM0becByYCdotRTP_a';
let wordTranslateFile = null;
document.getElementById('wordTranslateFile').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (file) {
wordTranslateFile = file;
document.getElementById('translateFileNameDisplay').textContent = file.name;
document.getElementById('translateBtn').style.display = 'block';
}
});
document.getElementById('translateBtn').addEventListener('click', async () => {
if (!wordTranslateFile) return;
const translateBtn = document.getElementById('translateBtn');
const translateStatus = document.getElementById('translateStatus');
const progressBar = document.querySelector('.progress-bar');
const progressContainer = document.querySelector('.progress-container');
const progressText = document.getElementById('progressText');
// 禁用翻译按钮
translateBtn.disabled = true;
translateBtn.style.opacity = '0.6';
translateBtn.textContent = '翻译中...';
translateStatus.textContent = '正在处理文档...';
progressContainer.style.display = 'block';
progressText.style.display = 'block';
progressBar.style.width = '0%';
// 进度检查变量
let lastProgress = 0;
let noProgressCount = 0;
try {
const formData = new FormData();
formData.append('file', wordTranslateFile);
formData.append('target_lang', document.getElementById('targetLang').value);
// 开始翻译请求
const response = await fetch('http://localhost:5000/translate', {
method: 'POST',
body: formData
});
// 定时检查进度
const progressCheck = setInterval(async () => {
try {
const progressResponse = await fetch('http://localhost:5000/progress');
const progressData = await progressResponse.json();
// 检查进度是否停滞
if (progressData.progress === lastProgress) {
noProgressCount++;
if (noProgressCount > 10) { // 5秒无进度更新
clearInterval(progressCheck);
throw new Error('翻译进度停滞,请重试');
}
} else {
noProgressCount = 0;
lastProgress = progressData.progress;
}
progressBar.style.width = `${progressData.progress}%`;
progressText.textContent = `翻译进度: ${progressData.progress}%`;
} catch (error) {
clearInterval(progressCheck);
throw error;
}
}, 500);
if (!response.ok) {
clearInterval(progressCheck);
const error = await response.json();
throw new Error(error.error || '翻译失败');
}
// 下载翻译后的文件
const blob = await response.blob();
clearInterval(progressCheck);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `translated_${wordTranslateFile.name}`;
a.click();
translateStatus.textContent = '翻译完成!';
progressBar.style.width = '100%';
// 重置按钮状态
setTimeout(() => {
translateBtn.disabled = false;
translateBtn.style.opacity = '1';
translateBtn.innerHTML = '<span class="icon">🔄</span>开始翻译';
progressContainer.style.display = 'none';
progressText.style.display = 'none';
}, 2000);
} catch (error) {
translateStatus.textContent = '翻译失败:' + error.message;
progressContainer.style.display = 'none';
progressText.style.display = 'none';
// 重置按钮状态
translateBtn.disabled = false;
translateBtn.style.opacity = '1';
translateBtn.innerHTML = '<span class="icon">🔄</span>开始翻译';
}
});
// MD5函数实现
function MD5(string) {
// 实现MD5加密
// 可以使用第三方库如crypto-js
}
});