diff --git a/README.md b/README.md index 9fe2775..a5e0e69 100644 --- a/README.md +++ b/README.md @@ -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 ## 许可证 -[添加许可证类型] \ No newline at end of file +MIT License \ No newline at end of file diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..ae871ab --- /dev/null +++ b/backend/app.py @@ -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) \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..2411c28 --- /dev/null +++ b/backend/requirements.txt @@ -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 \ No newline at end of file diff --git a/backend/utils/__pycache__/docx_translator.cpython-310.pyc b/backend/utils/__pycache__/docx_translator.cpython-310.pyc new file mode 100644 index 0000000..de637e2 Binary files /dev/null and b/backend/utils/__pycache__/docx_translator.cpython-310.pyc differ diff --git a/backend/utils/docx_translator.py b/backend/utils/docx_translator.py new file mode 100644 index 0000000..4d1e699 --- /dev/null +++ b/backend/utils/docx_translator.py @@ -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 \ No newline at end of file diff --git a/manifest.json b/manifest.json index a3a10fb..202078a 100644 --- a/manifest.json +++ b/manifest.json @@ -11,5 +11,10 @@ "action": { "default_popup": "src/html/popup.html" }, - "permissions": ["activeTab"] + "permissions": [ + "activeTab" + ], + "host_permissions": [ + "http://localhost:5000/*" + ] } \ No newline at end of file diff --git a/src/css/styles.css b/src/css/styles.css index 28de6b8..a03e92d 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -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翻译"; } \ No newline at end of file diff --git a/src/html/popup.html b/src/html/popup.html index eb767b9..1aa9db1 100644 --- a/src/html/popup.html +++ b/src/html/popup.html @@ -12,11 +12,12 @@