From 8a5f62342a1e414b715adcbf3919a63c36fe2fda Mon Sep 17 00:00:00 2001 From: Leon Date: Mon, 8 Jun 2026 14:07:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20MT=E6=A8=A1=E5=BC=8F=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E6=AE=8B=E7=95=99=E3=80=81docx=E6=A0=BC=E5=BC=8F=E4=B8=A2?= =?UTF-8?q?=E5=A4=B1=E3=80=81=E8=AF=AD=E8=A8=80=E5=88=87=E6=8D=A2=E5=99=A8?= =?UTF-8?q?=E5=8F=8Aprovider=E5=9F=9F=E5=90=8D=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - provider.py: 域名匹配改为包含匹配,覆盖dashscope-intl国际站 - segments_agent.py: MT模式改用<<>>纯文本标记替代JSON,避免qwen-mt模型原文残留 - docx_translator.py: _apply_translation改为按字符比例分配译文到各Run,保留原始格式 - i18nData.json: vi(越南语)替换为id(印尼语),含完整175键翻译 - index.html: 语言切换器移至顶部标题栏,新增浏览器语言自动检测 Co-Authored-By: Claude Opus 4.7 --- docutranslate/agents/provider/provider.py | 2 +- docutranslate/agents/segments_agent.py | 210 ++++++----- docutranslate/static/i18nData.json | 346 +++++++++--------- docutranslate/static/index.html | 83 ++--- .../ai_translator/docx_translator.py | 67 ++-- 5 files changed, 377 insertions(+), 331 deletions(-) diff --git a/docutranslate/agents/provider/provider.py b/docutranslate/agents/provider/provider.py index 827d73b..0fe4f15 100644 --- a/docutranslate/agents/provider/provider.py +++ b/docutranslate/agents/provider/provider.py @@ -5,7 +5,7 @@ ProviderType: TypeAlias = Literal["ollama", "bigmodel", "aliyuncs", "volces", "g def get_provider_by_domain(domain:str)->ProviderType: if domain == "open.bigmodel.cn": return "bigmodel" - elif domain == "dashscope.aliyuncs.com": + elif "dashscope.aliyuncs.com" in domain: return "aliyuncs" elif domain == "ark.cn-beijing.volces.com": return "volces" diff --git a/docutranslate/agents/segments_agent.py b/docutranslate/agents/segments_agent.py index a684f02..48afac8 100644 --- a/docutranslate/agents/segments_agent.py +++ b/docutranslate/agents/segments_agent.py @@ -15,10 +15,13 @@ from docutranslate.agents.agent import PartialAgentResultError, AgentResultError from docutranslate.glossary.glossary import Glossary from docutranslate.utils.json_utils import segments2json_chunks, fix_json_string +# MT mode plain-text segment marker — designed to survive machine translation unchanged +MT_SEG_MARKER_RE = re.compile(r'<<>>\s*\n(.*?)(?=<<>>|\Z)', re.DOTALL) + def generate_prompt(json_segments: str, to_lang: str): return f""" -You will receive a sequence of original text segments to be translated, represented in JSON format. The keys are segment IDs, and the values are the text content to be translated. +You will receive a sequence of original text segments to be translated, represented in JSON format. The keys are segment IDs, and the values are the text content to be translated. Here is the input: @@ -58,8 +61,8 @@ Below is an example of how merging should be done when necessary: input: ```json {{ -"EXAMPLE_KEY_1":"汤姆说:“杰克你", -"EXAMPLE_KEY_2":"好”。" +"EXAMPLE_KEY_1":"汤姆说:\"杰克你", +"EXAMPLE_KEY_2":"好\"。" }} ``` output: @@ -92,6 +95,44 @@ def get_target_segments(result: str): return result +def _chunk_to_mt_prompt(chunk: dict) -> str: + """Convert a JSON chunk like {'0': 'text1', '1': 'text2'} to MT-friendly plain text.""" + parts = [] + for key in sorted(chunk.keys(), key=int): + parts.append(f"<<>>\n{chunk[key]}") + return "\n".join(parts) + + +def _parse_mt_prompt_to_dict(mt_prompt: str) -> dict: + """Parse an MT prompt string back to the original segment dict.""" + result = {} + for match in MT_SEG_MARKER_RE.finditer(mt_prompt): + key = match.group(1) + value = match.group(2).strip() + result[key] = value + if not result: + # MT format parsing failed — wrap entire prompt as single segment + result = {"0": mt_prompt} + return result + + +def _parse_mt_response(text: str, original_chunk: dict) -> dict: + """Parse MT plain-text response using <<>> markers back to dict.""" + result = {} + for match in MT_SEG_MARKER_RE.finditer(text): + key = match.group(1) + value = match.group(2).strip() + if key in original_chunk: + result[key] = value + + # Fill missing keys from original + for key in original_chunk: + if key not in result: + result[key] = "" + + return result + + @dataclass(kw_only=True) class SegmentsTranslateAgentConfig(AgentConfig): to_lang: str @@ -123,20 +164,16 @@ class SegmentsTranslateAgent(Agent): def _result_handler(self, result: str, origin_prompt: str, logger: Logger): """ 处理成功的API响应。 - - 如果键完全匹配,返回翻译结果。 - - 如果键不匹配,构造一个部分成功的结果,并通过 PartialTranslationError 异常抛出,以触发重试。 - - 其他错误(如JSON解析失败、模型偷懒)则抛出普通 ValueError 触发重试。 - - MT模式下,如果返回的是纯文本而非JSON,将其按行分割并映射到原始键。 + MT模式下使用 <<>> 标记解析纯文本响应,避免JSON格式不兼容问题。 """ - # MT模式下直接解析origin_prompt为JSON(纯净JSON,没有包装) if self.is_mt_mode: - original_segments = origin_prompt - else: - original_segments = get_original_segments(origin_prompt) + return self._result_handler_mt(result, origin_prompt, logger) + + # --- Non-MT mode (JSON-based) --- + original_segments = get_original_segments(origin_prompt) result = get_target_segments(result) if result == "": if original_segments.strip() != "": - # print(f"【测试】origin_prompt:\n{origin_prompt}\nresult:\n{result}") raise AgentResultError("result为空值但原文不为空") return {} try: @@ -144,37 +181,6 @@ class SegmentsTranslateAgent(Agent): original_chunk = json_repair.loads(original_segments) repaired_result = json_repair.loads(result) - # MT模式兼容:处理各种非标准返回格式 - if self.is_mt_mode: - # 如果是列表,尝试合并所有字典 - if isinstance(repaired_result, list): - logger.debug(f"[MT模式] 返回结果是列表,包含 {len(repaired_result)} 个元素") - merged_result = {} - for item in repaired_result: - if isinstance(item, dict): - merged_result.update(item) - repaired_result = merged_result - - # 如果返回的是纯文本(字符串),尝试将其映射到原始键 - if isinstance(repaired_result, str): - original_keys = list(original_chunk.keys()) - # 按行分割结果,去除空行 - result_lines = [line.strip() for line in repaired_result.split('\n') if line.strip()] - - # 如果只有一行结果但多个键,将整个结果分配给第一个键,其余为空 - if len(result_lines) == 1 and len(original_keys) > 1: - repaired_result = {original_keys[0]: result_lines[0]} - for key in original_keys[1:]: - repaired_result[key] = "" - # 如果结果行数与键数匹配,逐行对应 - elif len(result_lines) == len(original_keys): - repaired_result = {original_keys[i]: result_lines[i] for i in range(len(original_keys))} - # 如果结果行数不匹配,将所有结果合并给第一个键 - else: - repaired_result = {original_keys[0]: repaired_result} - for key in original_keys[1:]: - repaired_result[key] = "" - if not isinstance(repaired_result, dict): raise AgentResultError(f"Agent返回结果不是dict的json形式, result: {result}") @@ -184,9 +190,7 @@ class SegmentsTranslateAgent(Agent): original_keys = set(original_chunk.keys()) result_keys = set(repaired_result.keys()) - # 如果键不完全匹配 if original_keys != result_keys: - # 仍然先构造一个最完整的“部分结果” final_chunk = {} common_keys = original_keys.intersection(result_keys) missing_keys = original_keys - result_keys @@ -201,74 +205,104 @@ class SegmentsTranslateAgent(Agent): for key in missing_keys: final_chunk[key] = str(original_chunk[key]) + raise PartialAgentResultError("键不匹配,触发重试", partial_result=final_chunk, + append_prompt=f"\nBe careful not to omit any keys from the input; do not combine sentences when translating.\n") - # 抛出自定义异常,将部分结果和错误信息一起传递出去 - raise PartialAgentResultError("键不匹配,触发重试", partial_result=final_chunk,append_prompt=f"\nBe careful not to omit any keys from the input; do not combine sentences when translating.\n") - - # 如果键完全匹配(理想情况),正常返回 for key, value in repaired_result.items(): repaired_result[key] = str(value) return repaired_result except (RuntimeError, JSONDecodeError) as e: - # MT模式兼容:如果JSON解析失败,尝试将结果作为纯文本处理 - if self.is_mt_mode: - try: - original_chunk = json_repair.loads(original_segments) - original_keys = list(original_chunk.keys()) - result_lines = [line.strip() for line in result.split('\n') if line.strip()] - - if len(result_lines) == 1 and len(original_keys) > 1: - repaired_result = {original_keys[0]: result_lines[0]} - for key in original_keys[1:]: - repaired_result[key] = "" - elif len(result_lines) == len(original_keys): - repaired_result = {original_keys[i]: result_lines[i] for i in range(len(original_keys))} - else: - repaired_result = {original_keys[0]: result} - for key in original_keys[1:]: - repaired_result[key] = "" - - # 验证结果 - if set(repaired_result.keys()) != set(original_chunk.keys()): - raise AgentResultError(f"MT模式解析后键不匹配") - - return repaired_result - except Exception as mt_e: - raise AgentResultError(f"MT模式纯文本处理失败: {mt_e.__repr__()}") - - # 对于JSON解析等硬性错误,继续抛出普通ValueError raise AgentResultError(f"结果处理失败: {e.__repr__()}") + def _result_handler_mt(self, result: str, origin_prompt: str, logger: Logger) -> dict: + """MT模式专用结果处理器:解析 <<>> 标记格式的纯文本响应。""" + result_clean = result.strip() + if result_clean == "": + if origin_prompt.strip() != "": + raise AgentResultError("result为空值但原文不为空") + return {} + + original_chunk = _parse_mt_prompt_to_dict(origin_prompt) + original_keys = set(original_chunk.keys()) + + # Try parsing with <<>> markers + parsed = _parse_mt_response(result_clean, original_chunk) + + if parsed and any(v.strip() for v in parsed.values()): + result_keys = set(parsed.keys()) + if result_keys == original_keys: + # Check if result is identical to original (no translation happened) + all_same = all( + parsed.get(k, "").strip() == str(original_chunk.get(k, "")).strip() + for k in original_keys + ) + if all_same: + raise AgentResultError("翻译结果与原文完全相同,疑似翻译失败,将进行重试。") + return parsed + + # If key mismatch, try as Partial result + if result_keys and result_keys != original_keys: + final_chunk = {} + for key in original_keys: + final_chunk[key] = parsed.get(key, str(original_chunk.get(key, ""))) + raise PartialAgentResultError( + "MT模式键不匹配,触发重试", + partial_result=final_chunk, + append_prompt="\nPreserve all <<>> markers exactly as they appear.\n" + ) + + # Fallback: Try line-by-line mapping (MT model might have removed markers) + result_lines = [line.strip() for line in result_clean.split('\n') if line.strip()] + original_seg_list = [str(original_chunk.get(str(i), "")) for i in range(len(original_chunk))] + + non_empty_lines = [l for l in result_lines if l] + if len(non_empty_lines) == len(original_chunk): + repaired = {str(i): non_empty_lines[i] for i in range(len(non_empty_lines))} + all_same = all( + repaired.get(k, "").strip() == str(original_chunk.get(k, "")).strip() + for k in original_keys + ) + if all_same: + raise AgentResultError("翻译结果与原文完全相同(逐行),疑似翻译失败,将进行重试。") + return repaired + + # Last fallback: assign all result text to first key + if non_empty_lines: + repaired = {str(i): "" for i in range(len(original_chunk))} + repaired["0"] = "\n".join(non_empty_lines) + return repaired + + raise AgentResultError("MT模式无法解析响应") + def _error_result_handler(self, origin_prompt: str, logger: Logger): """ 处理在所有重试后仍然失败的请求。 - 作为备用方案,返回原文内容,并将所有值转换为字符串。 + 作为备用方案,返回原文内容。 """ - # MT模式下直接解析origin_prompt为JSON(纯净JSON,没有包装) if self.is_mt_mode: - original_segments = origin_prompt - else: - original_segments = get_original_segments(origin_prompt) + original_chunk = _parse_mt_prompt_to_dict(origin_prompt) + for key in list(original_chunk.keys()): + original_chunk[key] = f"{original_chunk[key]}" + return original_chunk + + original_segments = get_original_segments(origin_prompt) if original_segments == "": return {} try: original_chunk = json_repair.loads(original_segments) - # 此处逻辑保留,作为最终的兜底方案 for key, value in original_chunk.items(): original_chunk[key] = f"{value}" return original_chunk except (RuntimeError, JSONDecodeError): logger.error(f"原始prompt也不是有效的json格式: {original_segments}") - # 如果原始prompt本身也无效,返回一个清晰的错误对象 return {"error": f"{original_segments}"} def send_segments(self, segments: list[str], chunk_size: int) -> list[str]: indexed_originals, chunks, merged_indices_list = segments2json_chunks(segments, chunk_size) - # MT模式下直接发送纯净JSON,不添加额外提示词 if self.is_mt_mode: - prompts = [json.dumps(chunk, ensure_ascii=False, indent=0) for chunk in chunks] + prompts = [_chunk_to_mt_prompt(chunk) for chunk in chunks] else: prompts = [generate_prompt(json.dumps(chunk, ensure_ascii=False, indent=0), self.to_lang) for chunk in chunks] translated_chunks = super().send_prompts(prompts=prompts, json_format=self.force_json, @@ -292,7 +326,6 @@ class SegmentsTranslateAgent(Agent): except Exception as e: self.logger.error(f"处理chunk时发生未知错误: {e.__repr__()}") - # 重建最终列表 result = [] last_end = 0 ls = list(indexed_translated.values()) @@ -308,9 +341,8 @@ class SegmentsTranslateAgent(Agent): async def send_segments_async(self, segments: list[str], chunk_size: int) -> list[str]: indexed_originals, chunks, merged_indices_list = await asyncio.to_thread(segments2json_chunks, segments, chunk_size) - # MT模式下直接发送纯净JSON,不添加额外提示词 if self.is_mt_mode: - prompts = [json.dumps(chunk, ensure_ascii=False, indent=0) for chunk in chunks] + prompts = [_chunk_to_mt_prompt(chunk) for chunk in chunks] else: prompts = [generate_prompt(json.dumps(chunk, ensure_ascii=False, indent=0), self.to_lang) for chunk in chunks] @@ -326,7 +358,6 @@ class SegmentsTranslateAgent(Agent): continue for key, val in chunk.items(): if key in indexed_translated: - # 此处不再需要 str(val),因为 _result_handler 已经处理好了 indexed_translated[key] = val else: self.logger.warning(f"在结果chunk中发现未知键 '{key}',已忽略。") @@ -335,7 +366,6 @@ class SegmentsTranslateAgent(Agent): except Exception as e: self.logger.error(f"处理chunk时发生未知错误: {e.__repr__()}") - # 重建最终列表 result = [] last_end = 0 ls = list(indexed_translated.values()) diff --git a/docutranslate/static/i18nData.json b/docutranslate/static/i18nData.json index c660636..ecf6516 100644 --- a/docutranslate/static/i18nData.json +++ b/docutranslate/static/i18nData.json @@ -353,181 +353,181 @@ "configImportError": "Failed to parse config file, please check the file format.", "providerLabel": "Provider" }, - "vi": { - "pageTitle": "DocuTranslate - Dịch tài liệu tương tác", - "tutorialBtn": "Hướng dẫn", - "projectContributeBtn": "Hợp tác dự án", - "workflowTitle": "Chọn quy trình làm việc", - "workflowOptionMarkdown": "Chuyển sang Markdown rồi dịch (.pdf/.md/.png, v.v.)", - "workflowOptionTxt": "Dịch văn bản thuần (.txt)", - "workflowOptionEpub": "Dịch EPUB (.epub)", - "workflowOptionDocx": "Dịch DOCX (.docx)", - "workflowOptionXlsx": "Dịch XLSX (.xlsx/.csv)", - "workflowOptionSrt": "Dịch phụ đề SRT (.srt)", - "workflowOptionAss": "Dịch phụ đề ASS (.ass)", - "workflowOptionJson": "Dịch JSON (.json)", - "workflowOptionHtml": "Dịch HTML (.html)", - "workflowOptionPptx": "Thuyết trình PPTX (.pptx)", - "autoWorkflowLabel": "Tự động chọn quy trình", - "txtSettingsTitleText": "Tùy chọn dịch TXT", - "insertModeLabel": "Chế độ chèn", - "insertModeReplace": "Thay thế bản gốc (Replace)", - "insertModeAppend": "Thêm vào sau bản gốc (Append)", - "insertModePrepend": "Thêm vào trước bản gốc (Prepend)", - "insertModeHelpTxt": "Chọn cách chèn văn bản đã dịch.", - "separatorLabel": "Dấu phân cách", - "separatorPlaceholderSimple": "Ví dụ: \\n---\\n", - "separatorHelp": "Ký tự dùng để phân cách văn bản gốc và văn bản dịch trong chế độ Append hoặc Prepend. \\n đại diện cho xuống dòng.", - "segmentModeLabel": "Chế độ phân đoạn", - "segmentModeLine": "Theo dòng (Mỗi dòng là một đoạn)", - "segmentModeParagraph": "Theo đoạn văn (Gộp các dòng không trống liên tiếp)", - "segmentModeNone": "Không phân đoạn (Toàn bộ văn bản là một đoạn)", - "segmentModeHelp": "Chọn cách phân chia văn bản để dịch.", - "docxSettingsTitleText": "Tùy chọn dịch DOCX", - "insertModeHelpDocx": "Chọn cách chèn văn bản đã dịch.", - "xlsxSettingsTitleText": "Tùy chọn dịch XLSX", - "insertModeHelpXlsx": "Chọn cách chèn văn bản đã dịch vào các ô.", - "xlsxTranslateRegionsLabel": "Vùng dịch (Tùy chọn)", - "xlsxTranslateRegionsPlaceholder": "Mỗi dòng một vùng, ví dụ: Sheet1!A1:B10 (áp dụng cho tất cả các sheet nếu bỏ qua tên sheet).", - "srtSettingsTitleText": "Tùy chọn dịch SRT", - "insertModeHelpSrt": "Chọn cách chèn văn bản đã dịch.", - "epubSettingsTitleText": "Tùy chọn dịch EPUB", - "insertModeHelpEpub": "Chọn cách chèn văn bản đã dịch.", - "htmlSettingsTitleText": "Tùy chọn dịch HTML", - "insertModeHelpHtml": "Chọn cách chèn văn bản đã dịch.", - "assSettingsTitleText": "Tùy chọn dịch ASS", - "insertModeHelpAss": "Chọn cách chèn văn bản đã dịch.", - "separatorPlaceholderAss": "Ví dụ: \\N (ký tự xuống dòng)", - "separatorHelpAss": "Ký tự dùng để phân cách văn bản gốc và văn bản dịch trong chế độ Append hoặc Prepend. \\N là ký tự xuống dòng cho định dạng ASS.", - "pptxSettingsTitleText": "Tùy chọn dịch PPTX", - "insertModeHelpPptx": "Chọn cách chèn văn bản đã dịch vào hộp văn bản.", - "jsonSettingsTitleText": "Cấu hình đường dẫn JSON", - "jsonPathLabel": "Đường dẫn JSON cần dịch", - "jsonPathPlaceholder": "Mỗi dòng một đường dẫn, ví dụ:\n$.name\n$.*", - "jsonPathHelp": "Sử dụng cú pháp jsonpath-ng. Mỗi dòng đại diện cho một đường dẫn JSON. Tất cả các chuỗi trong các đối tượng khớp sẽ được dịch.", - "parsingSettingsTitleText": "Cấu hình phân tích", - "parsingEngineLabel": "Công cụ phân tích", - "parsingEngineHelp": "Nếu tệp tải lên đã ở định dạng .md, mục này có thể bỏ qua.", - "mineruTokenPlaceholder": "Bắt buộc khi sử dụng công cụ Mineru", - "modelVersionLabel": "Phiên bản mô hình Mineru", + "id": { + "pageTitle": "DocuTranslate - Terjemahan Dokumen Interaktif", + "tutorialBtn": "Tutorial", + "projectContributeBtn": "Kolaborasi Proyek", + "workflowTitle": "Pilih Alur Kerja", + "workflowOptionMarkdown": "Konversi ke Markdown lalu Terjemahkan (.pdf/.md/.png, dll.)", + "workflowOptionTxt": "Terjemahan Teks Biasa (.txt)", + "workflowOptionEpub": "Terjemahan EPUB (.epub)", + "workflowOptionDocx": "Terjemahan DOCX (.docx)", + "workflowOptionXlsx": "Terjemahan XLSX (.xlsx/.csv)", + "workflowOptionSrt": "Terjemahan Subtitle SRT (.srt)", + "workflowOptionAss": "Terjemahan Subtitle ASS (.ass)", + "workflowOptionJson": "Terjemahan JSON (.json)", + "workflowOptionHtml": "Terjemahan HTML (.html)", + "workflowOptionPptx": "Presentasi PPTX (.pptx)", + "autoWorkflowLabel": "Pilih Alur Kerja Otomatis", + "txtSettingsTitleText": "Opsi Terjemahan TXT", + "insertModeLabel": "Mode Sisip", + "insertModeReplace": "Ganti Asli (Replace)", + "insertModeAppend": "Tambahkan ke Asli (Append)", + "insertModePrepend": "Sisipkan ke Depan (Prepend)", + "insertModeHelpTxt": "Pilih cara menyisipkan teks terjemahan.", + "separatorLabel": "Pemisah", + "separatorPlaceholderSimple": "contoh: \\n---\\n", + "separatorHelp": "Karakter pemisah antara teks asli dan terjemahan. \\n untuk baris baru.", + "segmentModeLabel": "Mode Segmen", + "segmentModeLine": "Per Baris", + "segmentModeParagraph": "Per Paragraf", + "segmentModeNone": "Tanpa Segmentasi", + "segmentModeHelp": "Pilih cara membagi teks untuk terjemahan.", + "docxSettingsTitleText": "Opsi Terjemahan DOCX", + "insertModeHelpDocx": "Pilih cara menyisipkan teks terjemahan.", + "xlsxSettingsTitleText": "Opsi Terjemahan XLSX", + "insertModeHelpXlsx": "Pilih cara menyisipkan teks terjemahan ke dalam sel.", + "xlsxTranslateRegionsLabel": "Area Terjemahan (Opsional)", + "xlsxTranslateRegionsPlaceholder": "Satu area per baris, contoh: Sheet1!A1:B10", + "srtSettingsTitleText": "Opsi Terjemahan SRT", + "insertModeHelpSrt": "Pilih cara menyisipkan teks terjemahan.", + "epubSettingsTitleText": "Opsi Terjemahan EPUB", + "insertModeHelpEpub": "Pilih cara menyisipkan teks terjemahan.", + "htmlSettingsTitleText": "Opsi Terjemahan HTML", + "insertModeHelpHtml": "Pilih cara menyisipkan teks terjemahan.", + "assSettingsTitleText": "Opsi Terjemahan ASS", + "insertModeHelpAss": "Pilih cara menyisipkan teks terjemahan.", + "separatorPlaceholderAss": "contoh: \\N (karakter baris baru)", + "separatorHelpAss": "Karakter pemisah. \\N untuk format ASS.", + "pptxSettingsTitleText": "Opsi Terjemahan PPTX", + "insertModeHelpPptx": "Pilih cara menyisipkan teks terjemahan ke kotak teks.", + "jsonSettingsTitleText": "Konfigurasi JSON Path", + "jsonPathLabel": "JSON Path untuk Diterjemahkan", + "jsonPathPlaceholder": "Satu path per baris, contoh:\n$.name\n$.*", + "jsonPathHelp": "Menggunakan sintaks jsonpath-ng. Setiap baris adalah path JSON.", + "parsingSettingsTitleText": "Konfigurasi Parsing", + "parsingEngineLabel": "Mesin Parsing", + "parsingEngineHelp": "Jika file sudah dalam format .md, ini bisa dilewati.", + "mineruTokenPlaceholder": "Diperlukan saat menggunakan mesin Mineru", + "modelVersionLabel": "Versi Model Mineru", "modelVersionVlm": "VLM", "modelVersionPipline": "Pipeline", - "modelVersionHelp": "Mineru VLM là mô hình thử nghiệm nội bộ mới hơn.", - "mineruDeployBaseUrlLabel": "URL dịch vụ (Base URL)", - "mineruDeployBaseUrlPlaceholder": "Ví dụ: http://127.0.0.1:8000", - "mineruDeployBackendLabel": "Loại Backend", - "mineruDeployLangListLabel": "Danh sách ngôn ngữ (Chế độ Pipeline)", - "mineruDeployServerUrlLabel": "URL máy chủ", - "mineruDeployServerUrlPlaceholder": "Ví dụ: http://127.0.0.1:30000", - "mineruDeployParseMethodLabel": "Phương pháp phân tích", - "mineruDeployTableEnableLabel": "Nhận dạng bảng", - "mineruDeployStartPageLabel": "Trang bắt đầu", - "mineruDeployEndPageLabel": "Trang kết thúc", - "mineruDeployFormulaEnableLabel": "Bật phân tích công thức", - "formulaOcrLabel": "Nhận dạng công thức", - "codeOcrLabel": "Nhận dạng mã (Code)", - "aiSettingsTitleText": "Mô hình dịch", - "skipTranslationLabel": "Bỏ qua dịch thuật", - "modelPresetLabel": "Mẫu mô hình", - "modelPresetPlaceholder": "Chọn mẫu mô hình", - "modelPresetEmpty": "Hãy cấu hình sẵn mẫu mô hình trong biến môi trường phía máy chủ", - "modelPresetRuntimeHint": "Nhà cung cấp, endpoint và API Key sẽ được đọc từ biến môi trường phía máy chủ khi chạy.", - "platformLabel": "Chọn nền tảng", - "platformCustom": "API tùy chỉnh", - "baseUrlLabel": "Địa chỉ API (Base URL)", - "baseUrlPlaceholder": "Địa chỉ tương thích OpenAI", - "apiKeyPlaceholder": "Vui lòng nhập API Key của bạn", + "modelVersionHelp": "Mineru VLM adalah model uji internal yang lebih baru.", + "mineruDeployBaseUrlLabel": "URL Layanan (Base URL)", + "mineruDeployBaseUrlPlaceholder": "contoh: http://127.0.0.1:8000", + "mineruDeployBackendLabel": "Tipe Backend", + "mineruDeployLangListLabel": "Daftar Bahasa (Mode Pipeline)", + "mineruDeployServerUrlLabel": "URL Server", + "mineruDeployServerUrlPlaceholder": "contoh: http://127.0.0.1:30000", + "mineruDeployParseMethodLabel": "Metode Parse", + "mineruDeployTableEnableLabel": "Pengenalan Tabel", + "mineruDeployStartPageLabel": "Halaman Awal", + "mineruDeployEndPageLabel": "Halaman Akhir", + "mineruDeployFormulaEnableLabel": "Aktifkan Parsing Formula", + "formulaOcrLabel": "Pengenalan Formula", + "codeOcrLabel": "Pengenalan Kode", + "aiSettingsTitleText": "Model Terjemahan", + "skipTranslationLabel": "Lewati Terjemahan", + "modelPresetLabel": "Preset Model", + "modelPresetPlaceholder": "Pilih preset model", + "modelPresetEmpty": "Konfigurasikan preset model di server terlebih dahulu", + "modelPresetRuntimeHint": "Provider, endpoint, dan API Key akan dibaca dari environment server.", + "platformLabel": "Pilih Platform", + "platformCustom": "API Kustom", + "baseUrlLabel": "Alamat API (Base URL)", + "baseUrlPlaceholder": "Alamat kompatibel OpenAI", + "apiKeyPlaceholder": "Masukkan API Key Anda", "modelIdLabel": "Model ID", - "modelIdPlaceholder": "Ví dụ: gpt-4o, glm-4", - "systemProxyLabel": "Bật proxy hệ thống", - "forceJson": "Bắt buộc xuất JSON", - "forceJsonTooltip": "Bắt buộc mô hình xuất định dạng JSON khi được yêu cầu. Có thể làm giảm chất lượng dịch; khuyên dùng nên tắt đối với các mô hình tuân thủ hướng dẫn tốt.", - "translationSettingsTitleText": "Cấu hình dịch thuật", - "targetLanguageLabel": "Ngôn ngữ đích", - "targetLanguageCustom": "Khác (Tùy chỉnh)", - "customLangPlaceholder": "Vui lòng nhập ngôn ngữ đích, ví dụ: Vietnamese", - "thinkingModeLabel": "Chế độ tư duy (Thinking Mode)", - "thinkingModeTooltip": "Thiết lập xem mô hình suy luận hỗn hợp có thực hiện tư duy hay không. Hiện được hỗ trợ bởi dòng glm4.5 của Zhipu, dòng seed1.6 của Volcengine, SiliconFlow, dòng Gemini của Google và 302AI (một phần). Khuyên dùng tùy chọn 'Tắt'.", - "thinkingModeEnable": "Bật", - "thinkingModeDisable": "Tắt (Khuyên dùng)", - "thinkingModeDefault": "Mặc định", - "customPromptLabel": "Prompt tùy chỉnh", - "customPromptPlaceholder": "Tùy chọn, ví dụ: \"Không dịch tên người, giữ nguyên ngôn ngữ gốc\"", - "chunkSizeLabel": "Kích thước phân khối (Chunk Size)", - "resetBtn": "Đặt lại", - "concurrentLabel": "Số lượng đồng thời", - "retryLabel": "Thử lại", - "rpmLabel": "Số yêu cầu mỗi phút (RPM)", - "tpmLabel": "Số token mỗi phút (TPM - Ước tính)", - "unlimitedPlaceholder": "Để trống nếu không giới hạn", - "glossaryGenTitle": "Thuật ngữ", - "glossaryLabel": "Bảng thuật ngữ (Tùy chọn)", - "glossaryHelp": "Chọn một hoặc nhiều tệp CSV. Các tệp phải chứa tiêu đề 'src' và 'dst', đại diện cho thuật ngữ nguồn và đích.", - "viewGlossaryBtn": "Xem bảng thuật ngữ", - "clearGlossaryBtn": "Xóa", - "glossaryGenEnableLabel": "Tự động tạo bảng thuật ngữ", - "glossaryCustomPromptLabel": "Prompt tùy chỉnh", - "glossaryCustomPromptPlaceholder": "Gợi ý tạo bảng thuật ngữ", - "glossaryGenConfigLabel": "Cấu hình tạo thuật ngữ", - "glossaryGenConfigSame": "Giống cấu hình dịch", - "glossaryGenConfigCustom": "Tùy chỉnh", - "importConfigBtn": "Nhập cấu hình", - "exportConfigBtn": "Xuất cấu hình", - "taskListTitle": "Danh sách tác vụ", - "newTaskBtn": "Tác vụ mới", - "noTaskPlaceholder": "Chưa có tác vụ nào. Nhấn \"Tác vụ mới\" để bắt đầu!", - "taskCardIdLabel": "ID Tác vụ", - "taskCardIdPlaceholder": "Đang chờ gửi...", - "taskCardFileDrop": "Nhấp hoặc kéo tệp vào đây", - "taskCardFileSelected": "Đã chọn tệp", - "taskCardFilenameLabel": "Tên tệp: ", - "taskCardLogLabel": "Nhật ký (Logs)", - "copyLogsTooltip": "Sao chép nhật ký", - "taskCardStatusWaiting": "Đang chờ tải tệp lên...", - "taskCardPreviewBtn": "Xem trước", - "taskCardDownloadBtn": "Tải xuống", - "taskCardAttachmentBtn": "Tệp đính kèm", - "taskCardStartBtn": "Bắt đầu dịch", - "downloadMdEmbedded": "Markdown (Nhúng ảnh)", - "downloadMdZip": "Markdown nén (Zip)", - "previewBilingualBtn": "Song ngữ", - "previewTranslatedOnlyBtn": "Chỉ bản dịch", - "syncScrollTooltip": "Cuộn đồng bộ", - "previewOriginal": "Bản gốc", - "previewTranslated": "Bản dịch", - "closeBtn": "Đóng", - "tutorialModalTitle": "Hướng dẫn sử dụng", - "tutorialModalBody": "

Video hướng dẫn có sẵn trên Bilibili bằng cách tìm kiếm docutranslate.

Chào mừng bạn đến với DocuTranslate! Vui lòng làm theo các bước sau để dịch tài liệu của bạn:

  1. Bước 1: Chọn quy trình làm việc

    Ở đầu bảng cấu hình bên trái, trước tiên hãy chọn quy trình xử lý phù hợp nhất với loại tệp của bạn.

    Mẹo: \"Tự động chọn quy trình\" được bật theo mặc định. Chỉ cần tải tệp lên, hệ thống sẽ tự động khớp với quy trình phù hợp để đơn giản hóa thao tác.
    • Chuyển sang Markdown rồi Dịch: Thích hợp để dịch PDF, Markdown, hình ảnh, v.v. Đây là chế độ đa năng và mạnh mẽ nhất.
    • Dịch văn bản thuần: Dùng để dịch các tệp văn bản thuần .txt.
    • Dịch EPUB: Dùng để dịch các tệp sách điện tử .epub.
    • Dịch DOCX: Dùng để dịch các tài liệu Word .docx.
    • Dịch XLSX: Dùng để dịch các tệp bảng tính .xlsx hoặc .csv.
    • Dịch PPTX: Dùng để dịch các tệp trình chiếu .pptx.
    • Dịch phụ đề SRT: Dùng để dịch các tệp phụ đề .srt.
    • Dịch phụ đề ASS: Dùng để dịch các tệp phụ đề nâng cao .ass.
    • Dịch JSON: Dùng để dịch các trường cụ thể trong tệp .json.
    • Dịch HTML: Dùng để dịch các tệp trang web .html.
  2. Bước 2: Cấu hình tham số

    Sau khi chọn quy trình, các tùy chọn cấu hình liên quan sẽ xuất hiện bên dưới. Vui lòng hoàn tất cài đặt theo thứ tự (tất cả cấu hình được tự động lưu trong trình duyệt của bạn):

    A. Tùy chọn riêng theo quy trình (Xuất hiện dựa trên lựa chọn ở Bước 1):

    • Nếu chọn \"Chuyển sang Markdown rồi Dịch\", hãy cấu hình Cấu hình phân tích:
      • Công cụ phân tích: Chọn công cụ để chuyển đổi tệp của bạn (như PDF) sang định dạng Markdown phù hợp để dịch. Không cần chọn nếu tệp đã ở định dạng Markdown.
      • Mineru Token: Nếu chọn công cụ minerU, bạn cần nhập token vào đây.
    • Nếu chọn \"Văn bản thuần/DOCX/XLSX/PPTX/SRT/ASS/EPUB/HTML\", hãy cấu hình Tùy chọn dịch tương ứng:
      • Chế độ chèn: Xác định cách đặt kết quả dịch vào tài liệu. Bạn có thể chọn \"Thay thế\" bản gốc, \"Thêm vào sau\" (Append), hoặc \"Thêm vào trước\" (Prepend).
      • Dấu phân cách: Khi chọn chế độ \"Thêm vào sau\" hoặc \"Thêm vào trước\", ký tự này được dùng để ngăn cách giữa bản gốc và bản dịch (ví dụ: \\\\N thường dùng cho định dạng ASS, <br /> cho định dạng EPUB để xuống dòng).
    • Nếu chọn \"Dịch JSON\", hãy cấu hình Đường dẫn JSON:
      • Đường dẫn JSON cần dịch: Nhập mỗi dòng một biểu thức JSONPath để dịch tất cả các chuỗi trong các đối tượng khớp. Ví dụ: $.* (dịch tất cả chuỗi), $..description (dịch tất cả giá trị có khóa là description).

    B. Tùy chọn chung (Áp dụng cho mọi quy trình):

    • Mô hình dịch:
      • Chọn nền tảng/Địa chỉ API/API Key/Model ID: Cấu hình dịch vụ dịch thuật AI bạn muốn sử dụng. Mô hình tuân thủ hướng dẫn càng tốt thì xác suất lỗibỏ sót càng thấp.
      • Bỏ qua dịch thuật: Nếu chọn, hệ thống chỉ thực hiện phân tích tài liệu và chuyển đổi định dạng mà không gọi AI để dịch.
    • Cấu hình dịch thuật:
      • Ngôn ngữ đích: Chỉ định ngôn ngữ cần dịch sang.
      • Prompt tùy chỉnh: Tùy chọn, thêm hướng dẫn bổ sung, ví dụ \"Không dịch tên riêng\".
      • Chế độ tư duy: Cài đặt cho một số mô hình hỗ trợ suy luận hỗn hợp; khuyên dùng \"Tắt (Khuyên dùng)\".
      • Kích thước phân khối/Số lượng đồng thời, v.v.: Các tham số nâng cao để điều chỉnh hiệu suất và hành vi yêu cầu API; thông thường nên giữ mặc định.
    • Thuật ngữ:
      • Tải lên bảng thuật ngữ (Tùy chọn): Tải lên tệp CSV (phải chứa cột 'src' và 'dst') để đảm bảo tính nhất quán và chính xác cho các thuật ngữ cụ thể.
      • Tự động tạo bảng thuật ngữ: Khi bật, chương trình sẽ trích xuất thuật ngữ từ văn bản gốc để tạo bảng thuật ngữ trước khi tiến hành dịch.
  3. Bước 3: Tải tệp lên

    Trong danh sách tác vụ bên phải, nhấp hoặc kéo tài liệu của bạn vào khu vực tải lên.

  4. Bước 4: Bắt đầu dịch

    Sau khi chọn tệp thành công, nhấp nút Bắt đầu dịch ở góc dưới bên phải thẻ tác vụ. Hệ thống sẽ bắt đầu xử lý và bạn có thể xem tiến độ thời gian thực trong khu vực nhật ký.

  5. Bước 5: Xem và Tải xuống

    Sau khi dịch xong, các nút thao tác sẽ xuất hiện trên thẻ tác vụ:

    • Xem trước: So sánh văn bản gốc và bản dịch song song trong bảng trượt ra.
    • Tải xuống: Tải xuống bản dịch ở nhiều định dạng, bao gồm PDF, DOCX, Markdown, v.v.
    • Tệp đính kèm: Nếu có tệp bổ sung nào được tạo trong quá trình (như bảng thuật ngữ tự động), bạn có thể tải xuống tại đây.
Lưu ý quan trọng: Tất cả cấu hình được tự động lưu cục bộ trong trình duyệt để sử dụng lần sau. Bạn cũng có thể dùng nút \"Xuất cấu hình\" và \"Nhập cấu hình\" mới để sao lưu và khôi phục cài đặt.
", - "tutorialUnderstandBtn": "Tôi đã hiểu", - "contributorsModalTitle": "Cảm ơn vì đã đóng góp", - "contributorsPara1": "DocuTranslate là một dự án mã nguồn mở! Nhu cầu và việc sử dụng của cộng đồng là động lực thúc đẩy sự tiến bộ của dự án.", - "contributorsPara2": "Cảm ơn tất cả những người đã tài trợ, gửi mã, đóng góp ý kiến quý báu và gắn sao (star) cho dự án!", - "contributorsWelcome": "Hoan nghênh bạn đóng góp qua các cách sau:", - "contributorsGithub": "Trang GitHub", - "contributorsPR": "Gửi Pull Request", - "contributorsIssue": "Báo cáo vấn đề (Issue)", - "contributorsQQ": "Hoặc liên hệ tác giả qua nhóm QQ: 1047781902", - "glossaryModalTitle": "Bảng thuật ngữ hiện tại", - "glossaryTableSource": "Nguồn (src)", - "glossaryTableDestination": "Đích (dst)", - "engineOptionIdentity": "Đã là định dạng Markdown", - "engineOptionMineru": "Mineru (Khuyên dùng)", + "modelIdPlaceholder": "contoh: gpt-4o, glm-4", + "systemProxyLabel": "Aktifkan Proxy Sistem", + "forceJson": "Paksa output JSON", + "forceJsonTooltip": "Paksa model untuk output format JSON.", + "translationSettingsTitleText": "Konfigurasi Terjemahan", + "targetLanguageLabel": "Bahasa Target", + "targetLanguageCustom": "Lainnya (Kustom)", + "customLangPlaceholder": "Masukkan bahasa target, contoh: Italian", + "thinkingModeLabel": "Mode Berpikir", + "thinkingModeTooltip": "Disarankan: Nonaktifkan.", + "thinkingModeEnable": "Aktifkan", + "thinkingModeDisable": "Nonaktifkan (Disarankan)", + "thinkingModeDefault": "Default", + "customPromptLabel": "Prompt Kustom", + "customPromptPlaceholder": "Opsional, contoh: \"Jangan terjemahkan nama orang\"", + "chunkSizeLabel": "Ukuran Potongan", + "resetBtn": "Reset", + "concurrentLabel": "Konkurensi", + "retryLabel": "Percobaan Ulang", + "rpmLabel": "Permintaan per Menit (RPM)", + "tpmLabel": "Token per Menit (TPM - Estimasi)", + "unlimitedPlaceholder": "Kosongkan untuk tanpa batas", + "glossaryGenTitle": "Glosarium", + "glossaryLabel": "Glosarium (Opsional)", + "glossaryHelp": "Pilih file CSV dengan kolom src dan dst.", + "viewGlossaryBtn": "Lihat Glosarium", + "clearGlossaryBtn": "Hapus", + "glossaryGenEnableLabel": "Buat Glosarium Otomatis", + "glossaryCustomPromptLabel": "Prompt Kustom", + "glossaryCustomPromptPlaceholder": "Prompt pembuatan glosarium", + "glossaryGenConfigLabel": "Konfigurasi Pembuatan Glosarium", + "glossaryGenConfigSame": "Sama dengan Konfigurasi Terjemahan", + "glossaryGenConfigCustom": "Kustom", + "importConfigBtn": "Impor Konfigurasi", + "exportConfigBtn": "Ekspor Konfigurasi", + "taskListTitle": "Daftar Tugas", + "newTaskBtn": "Tugas Baru", + "noTaskPlaceholder": "Belum ada tugas. Klik \"Tugas Baru\" untuk memulai!", + "taskCardIdLabel": "ID Tugas", + "taskCardIdPlaceholder": "Menunggu...", + "taskCardFileDrop": "Klik atau seret file ke sini", + "taskCardFileSelected": "File Dipilih", + "taskCardFilenameLabel": "Nama File: ", + "taskCardLogLabel": "Log", + "copyLogsTooltip": "Salin log", + "taskCardStatusWaiting": "Menunggu unggahan file...", + "taskCardPreviewBtn": "Pratinjau", + "taskCardDownloadBtn": "Unduh", + "taskCardAttachmentBtn": "Lampiran", + "taskCardStartBtn": "Mulai Terjemahan", + "downloadMdEmbedded": "Markdown (Gambar Disematkan)", + "downloadMdZip": "Markdown Zip", + "previewBilingualBtn": "Dwi Bahasa", + "previewTranslatedOnlyBtn": "Hanya Terjemahan", + "syncScrollTooltip": "Sinkronisasi Gulir", + "previewOriginal": "Asli", + "previewTranslated": "Terjemahan", + "closeBtn": "Tutup", + "tutorialModalTitle": "Tutorial", + "tutorialModalBody": "

Video tutorials are available on Bilibili by searching for docutranslate.

Welcome to DocuTranslate! Please follow these steps to translate your documents:

  1. Step 1: Select Workflow

    At the top of the left-side configuration panel, first choose the processing flow that best suits your file type.

    Tip: \"Auto-select Workflow\" is enabled by default. Simply upload your file, and the system will automatically match it with the appropriate workflow to simplify the process.
    • Convert to Markdown then Translate: Suitable for translating PDF, Markdown, images, etc. This is the most versatile and powerful mode.
    • Plain Text Translation: For translating .txt plain text files.
    • EPUB Translation: For translating .epub e-book files.
    • DOCX Translation: For translating .docx Word documents.
    • XLSX Translation: For translating .xlsx or .csv spreadsheet files.
    • PPTX Translation: For translating .pptx slide files.
    • SRT Subtitle Translation: For translating .srt subtitle files.
    • ASS Subtitle Translation: For translating .ass advanced subtitle files.
    • JSON Translation: For translating specific fields within .json files.
    • HTML Translation: For translating .html web page files.
  2. Step 2: Configure Parameters

    After selecting a workflow, the relevant configuration options will appear below. Please complete the settings in order (all configurations are automatically saved in your browser):

    A. Workflow-Specific Options (Appears based on your choice in Step 1):

    • If \"Convert to Markdown then Translate\" is selected, configure Parsing Configuration:
      • Parsing Engine: Choose an engine to convert your file (like a PDF) into a translation-friendly Markdown format. No selection is needed if your file is already in Markdown format.
      • Mineru Token: If you choose the minerU engine, you need to enter your token here.
    • If \"Plain Text/DOCX/XLSX/PPTX/SRT/ASS/EPUB/HTML\" is selected, configure its Translation Options:
      • Insert Mode: Defines how the translation result is placed in the document. You can choose to \"Replace\" the original, \"Append\" it after the original, or \"Prepend\" it before the original.
      • Separator: When \"Append\" or \"Prepend\" mode is selected, this is used to insert a separator between the original and translated text (e.g., \\\\N is common for ASS format, <br /> for EPUB format as a line break).
    • If \"JSON Translation\" is selected, configure JSON Paths:
      • JSON Paths to Translate: Enter one JSONPath expression per line to translate all strings within the matched objects. For example: $.* (translate all strings), $..description (translate all values with the key description).

    B. General Options (Applicable to all workflows):

    • Translation Model:
      • Select Platform/API Address/API Key/Model ID: Configure the AI translation service you wish to use. The better the model follows instructions, the lower the probability of errors and missed translations.
      • Skip Translation: If checked, only document parsing and format conversion will be performed, without calling the AI for translation.
    • Translation Configuration:
      • Target Language: Specify the target language for the translation.
      • Custom Prompt: Optional, add extra instructions, like \"Do not translate personal names.\"
      • Thinking Mode: A setting for some models that support mixed inference; \"Disable (Recommended)\" is the suggested choice.
      • Chunk Size/Concurrency, etc.: Advanced parameters for adjusting performance and API request behavior; usually, the defaults are fine.
    • Glossary:
      • Upload Glossary (Optional): Upload a CSV file (must contain 'src' and 'dst' columns) to ensure consistency and accuracy for specific terms.
      • Auto-generate Glossary: When enabled, the program will first extract terms from the original text to create a glossary before proceeding with the translation.
  3. Step 3: Upload File

    In the task list on the right, click or drag your document into the file upload area.

  4. Step 4: Start Translation

    Once the file is successfully selected, click the Start Translation button on the bottom right of the task card. The system will begin processing the task, and you can view the real-time progress in the log area.

  5. Step 5: View and Download

    After the translation is complete, action buttons will appear on the task card:

    • Preview: Compare the original and translated text side-by-side in a slide-out panel.
    • Download: Download the translation in various formats, including PDF, DOCX, Markdown, etc.
    • Attachments: If any additional files were generated during the process (like an auto-generated glossary), they can be downloaded here.
Important Note: All configurations are automatically saved locally in your browser for future use. You can also use the new \"Export Config\" and \"Import Config\" buttons to back up and restore your settings.
", + "tutorialUnderstandBtn": "Saya Mengerti", + "contributorsModalTitle": "Terima Kasih Kontribusi", + "contributorsPara1": "DocuTranslate adalah proyek open-source!", + "contributorsPara2": "Terima kasih kepada semua sponsor dan kontributor!", + "contributorsWelcome": "Anda dapat berkontribusi melalui:", + "contributorsGithub": "Halaman GitHub", + "contributorsPR": "Kirim Pull Request", + "contributorsIssue": "Laporkan Issue", + "contributorsQQ": "Atau hubungi penulis melalui grup QQ: 1047781902", + "glossaryModalTitle": "Glosarium Saat Ini", + "glossaryTableSource": "Sumber (src)", + "glossaryTableDestination": "Tujuan (dst)", + "engineOptionIdentity": "Sudah Format Markdown", + "engineOptionMineru": "Mineru (Disarankan)", "engineOptionDocling": "Docling", - "engineOptionMineru_deploy": "Dịch vụ Mineru Deploy", - "apiHrefInfo302ai": "👈 Đăng ký qua liên kết này để nhận $1 tín dụng miễn phí", - "glossaryEmpty": "Bảng thuật ngữ trống", - "status_fillRequired": "Vui lòng điền tất cả các trường bắt buộc!", - "btn_initializing": "Đang khởi tạo...", - "btn_cancelTranslation": "Hủy dịch", - "status_cancelling": "Đang hủy...", - "btn_reTranslate": "Dịch lại", - "preview_cantPreviewType": "Không thể xem trước loại tệp này", - "preview_noOriginalCache": "Không có tệp gốc được lưu trong bộ nhớ đệm để xem trước.", - "pdf_preparing": "Đang chuẩn bị PDF...", - "preview_bilingual": "Xem trước song ngữ", - "preview_translatedOnly": "Xem trước chỉ bản dịch", - "configImportSuccess": "Nhập cấu hình thành công!", - "configImportError": "Không thể phân tích tệp cấu hình, vui lòng kiểm tra định dạng tệp.", - "providerLabel": "Nhà cung cấp" + "engineOptionMineru_deploy": "Layanan Mineru Deploy", + "apiHrefInfo302ai": "👈 Daftar melalui tautan ini untuk kredit gratis $1", + "glossaryEmpty": "Glosarium kosong", + "status_fillRequired": "Harap isi semua kolom yang diperlukan!", + "btn_initializing": "Menginisialisasi...", + "btn_cancelTranslation": "Batalkan Terjemahan", + "status_cancelling": "Membatalkan...", + "btn_reTranslate": "Terjemahkan Ulang", + "preview_cantPreviewType": "Tidak dapat melihat pratinjau tipe file ini", + "preview_noOriginalCache": "Tidak ada file asli yang di-cache.", + "pdf_preparing": "Menyiapkan PDF...", + "preview_bilingual": "Pratinjau Dwi Bahasa", + "preview_translatedOnly": "Pratinjau Hanya Terjemahan", + "configImportSuccess": "Konfigurasi berhasil diimpor!", + "configImportError": "Gagal memproses file konfigurasi.", + "providerLabel": "Provider" } -} +} \ No newline at end of file diff --git a/docutranslate/static/index.html b/docutranslate/static/index.html index 0be3e2a..d476799 100644 --- a/docutranslate/static/index.html +++ b/docutranslate/static/index.html @@ -1,5 +1,5 @@ - + @@ -159,15 +159,6 @@ white-space: pre; } - .bottom-left-controls { - position: fixed; - bottom: 1rem; - left: 1rem; - z-index: 1050; - display: flex; - gap: 0.5rem; - } - .step-number { margin-right: 0.25rem; } @@ -226,6 +217,31 @@

DocuTranslate

+ +
+ + +
@@ -923,40 +939,7 @@ - -
- - -
+ @@ -1048,7 +1031,14 @@ components: {SliderControl, ModelPresetSelector}, setup() { const version = ref(""); - const currentLang = ref(localStorage.getItem('ui_language') || 'zh'); + function detectBrowserLang() { + const nav = navigator.language || navigator.userLanguage || ''; + const lang = nav.split('-')[0].toLowerCase(); + if (['zh', 'en', 'id'].includes(lang)) return lang; + if (lang === 'zh') return 'zh'; + return 'en'; // default to English for unrecognized languages + } + const currentLang = ref(localStorage.getItem('ui_language') || detectBrowserLang()); const i18nData = ref({}); const glossaryData = ref({}); const tasks = ref([]); @@ -1868,7 +1858,8 @@ const setLang = (l) => { currentLang.value = l; localStorage.setItem('ui_language', l); - document.documentElement.lang = l === 'zh' ? 'zh-CN' : 'en'; + const langMap = {zh: 'zh-CN', en: 'en', id: 'id'}; + document.documentElement.lang = langMap[l] || 'en'; }; const setTheme = (t) => { localStorage.setItem('theme', t); diff --git a/docutranslate/translator/ai_translator/docx_translator.py b/docutranslate/translator/ai_translator/docx_translator.py index fc84d03..6d736a0 100644 --- a/docutranslate/translator/ai_translator/docx_translator.py +++ b/docutranslate/translator/ai_translator/docx_translator.py @@ -326,33 +326,58 @@ class DocxTranslator(AiTranslator): runs = element_info["runs"] if not runs: return - first_real_run_index = -1 - # 找到第一个可以写入文本的run - for i, run in enumerate(runs): + # Filter to runs that are still attached to the document + valid_runs = [] + for run in runs: if run.element.getparent() is not None: - # 如果 run 是副本的一部分,其 _parent 可能仍然指向原始文档的段落 - # 但我们需要确保它与 element_info["paragraph"] 同步 run._parent = element_info["paragraph"] - run.text = final_text - first_real_run_index = i - break + valid_runs.append(run) - # 如果没有找到有效的run(例如,它们都已被删除),则记录警告 - if first_real_run_index == -1: + if not valid_runs: self.logger.warning(f"无法应用翻译 '{final_text}',因为找不到有效的run。") return - # 删除所有后续的run,因为它们的文本已经被合并到第一个run中了 - for i in range(first_real_run_index + 1, len(runs)): - run = runs[i] - parent_element = run.element.getparent() - if parent_element is not None: - try: - parent_element.remove(run.element) - except ValueError: - # 在某些复杂情况下,一个run可能已经被其父元素隐式删除 - self.logger.debug(f"尝试删除一个不存在的run元素。这通常是安全的。") - pass + if len(valid_runs) == 1: + # Single run: just write the translation + valid_runs[0].text = final_text + return + + # Multiple runs: proportionally distribute translated text to preserve formatting + orig_lengths = [len(r.text) for r in valid_runs] + total_orig = sum(orig_lengths) + final_len = len(final_text) + + if total_orig == 0: + valid_runs[0].text = final_text + for run in valid_runs[1:]: + self._remove_run_element(run) + return + + # Distribute characters proportionally + char_pos = 0 + for i, run in enumerate(valid_runs): + if i == len(valid_runs) - 1: + # Last run gets all remaining text + run.text = final_text[char_pos:] + else: + ratio = orig_lengths[i] / total_orig + run_char_count = max(1, round(final_len * ratio)) + run_char_count = min(run_char_count, final_len - char_pos - (len(valid_runs) - i - 1)) + if run_char_count <= 0: + # Remove runs that would get zero characters + self._remove_run_element(run) + continue + run.text = final_text[char_pos:char_pos + run_char_count] + char_pos += run_char_count + + def _remove_run_element(self, run) -> None: + """Safely remove a run element from its parent.""" + parent_element = run.element.getparent() + if parent_element is not None: + try: + parent_element.remove(run.element) + except ValueError: + self.logger.debug(f"尝试删除一个不存在的run元素。这通常是安全的。") # ---------- FIX START: 新增用于清理副本段落的辅助方法 ---------- def _prune_unwanted_elements_from_copy(self, p_element: OxmlElement):