From f93b13b53f39ea17afdcc6bfc679cbdeca0c3a0d Mon Sep 17 00:00:00 2001 From: sepehr Date: Tue, 1 Sep 2026 21:32:40 +0200 Subject: [PATCH] fix(pdf): le texte persan s'affichait en points d'interrogation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cause racine (reproduite et verrouillee par test) : l'insertion PDF demandait la police integree Helvetica tout en fournissant un fichier de police — PyMuPDF ignore alors le fichier, et Helvetica n'a aucun glyphe arabe : chaque lettre persane devenait un '?', et ces '?' debordaient les boites d'origine (les nombreux « [translation overflow] »). Defaut present dans toutes les versions deployees. - le nom de police est desormais Personnalise quand un fichier de police est fourni : le fichier est reellement integre - reproduction complete avant/apres : 0 '?', 0 debordement, persan rendu avec la police resolue - test de non-regression : la page traduite ne contient PAS Helvetica et le texte extrait n'a aucun '?' - bouton « Reconstruire » de la relecture : une infobulle explique desormais pourquoi il est grise (aucun segment approuve ni modifie), dans les 13 langues --- .../app/dashboard/reviews/[jobId]/page.tsx | 11 ++++++- .../src/lib/i18n/messages/ar/reviews.json | 3 +- .../src/lib/i18n/messages/de/reviews.json | 3 +- .../src/lib/i18n/messages/en/reviews.json | 3 +- .../src/lib/i18n/messages/es/reviews.json | 3 +- .../src/lib/i18n/messages/fa/reviews.json | 3 +- .../src/lib/i18n/messages/fr/reviews.json | 3 +- .../src/lib/i18n/messages/it/reviews.json | 3 +- .../src/lib/i18n/messages/ja/reviews.json | 3 +- .../src/lib/i18n/messages/ko/reviews.json | 3 +- .../src/lib/i18n/messages/nl/reviews.json | 3 +- .../src/lib/i18n/messages/pt/reviews.json | 3 +- .../src/lib/i18n/messages/ru/reviews.json | 3 +- .../src/lib/i18n/messages/zh/reviews.json | 3 +- tests/test_translators/test_rtl_layout.py | 32 +++++++++++++++++++ translators/pdf_translator.py | 16 +++++----- 16 files changed, 76 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/dashboard/reviews/[jobId]/page.tsx b/frontend/src/app/dashboard/reviews/[jobId]/page.tsx index bd78754..ddc776e 100644 --- a/frontend/src/app/dashboard/reviews/[jobId]/page.tsx +++ b/frontend/src/app/dashboard/reviews/[jobId]/page.tsx @@ -346,7 +346,16 @@ export default function ReviewPage() { if (f) importXliff(f); }} /> - diff --git a/frontend/src/lib/i18n/messages/ar/reviews.json b/frontend/src/lib/i18n/messages/ar/reviews.json index cb427fc..a3450bb 100644 --- a/frontend/src/lib/i18n/messages/ar/reviews.json +++ b/frontend/src/lib/i18n/messages/ar/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approving ({current}/{total})…", "reviews.noMatchingSegments": "No segments match your filter.", "reviews.empty.title": "لا توجد أجزاء لمراجعتها في هذه المهمة", - "reviews.empty.desc": "لا تحتوي هذه الترجمة على أجزاء محفوظة: إما أنها فشلت قبل ترجمة النص، أو لم يعد الخادم يحتفظ بها. يمكنك إزالتها من قائمة الترجمات الأخيرة." + "reviews.empty.desc": "لا تحتوي هذه الترجمة على أجزاء محفوظة: إما أنها فشلت قبل ترجمة النص، أو لم يعد الخادم يحتفظ بها. يمكنك إزالتها من قائمة الترجمات الأخيرة.", + "reviews.rebuildDisabledHint": "وافق على جزء واحد على الأقل أو عدّله أولاً — تعيد إعادة البناء تطبيق مراجعتك." } diff --git a/frontend/src/lib/i18n/messages/de/reviews.json b/frontend/src/lib/i18n/messages/de/reviews.json index 7bc519d..3d52e17 100644 --- a/frontend/src/lib/i18n/messages/de/reviews.json +++ b/frontend/src/lib/i18n/messages/de/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approving ({current}/{total})…", "reviews.noMatchingSegments": "No segments match your filter.", "reviews.empty.title": "Für diesen Auftrag gibt es nichts zu prüfen", - "reviews.empty.desc": "Diese Übersetzung enthält keine gespeicherten Segmente: Entweder ist sie vor der Textübersetzung fehlgeschlagen, oder der Server bewahrt sie nicht mehr auf. Sie können sie aus der Liste der letzten Übersetzungen entfernen." + "reviews.empty.desc": "Diese Übersetzung enthält keine gespeicherten Segmente: Entweder ist sie vor der Textübersetzung fehlgeschlagen, oder der Server bewahrt sie nicht mehr auf. Sie können sie aus der Liste der letzten Übersetzungen entfernen.", + "reviews.rebuildDisabledHint": "Genehmigen oder bearbeiten Sie zuerst mindestens ein Segment — der Neuaufbau übernimmt Ihre Prüfung." } diff --git a/frontend/src/lib/i18n/messages/en/reviews.json b/frontend/src/lib/i18n/messages/en/reviews.json index df23b68..b09523f 100644 --- a/frontend/src/lib/i18n/messages/en/reviews.json +++ b/frontend/src/lib/i18n/messages/en/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approving ({current}/{total})…", "reviews.noMatchingSegments": "No segments match your filter.", "reviews.empty.title": "Nothing to review for this job", - "reviews.empty.desc": "This translation has no stored segments: either it failed before any text was translated, or it is no longer retained by the server. You can remove it from your recent translations list." + "reviews.empty.desc": "This translation has no stored segments: either it failed before any text was translated, or it is no longer retained by the server. You can remove it from your recent translations list.", + "reviews.rebuildDisabledHint": "Approve or edit at least one segment first — the rebuild applies your reviewed wording." } diff --git a/frontend/src/lib/i18n/messages/es/reviews.json b/frontend/src/lib/i18n/messages/es/reviews.json index 09d4b40..12c659a 100644 --- a/frontend/src/lib/i18n/messages/es/reviews.json +++ b/frontend/src/lib/i18n/messages/es/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approving ({current}/{total})…", "reviews.noMatchingSegments": "No segments match your filter.", "reviews.empty.title": "Nada que revisar en este trabajo", - "reviews.empty.desc": "Esta traducción no tiene segmentos guardados: o falló antes de traducir el texto, o el servidor ya no la conserva. Puede quitarla de la lista de traducciones recientes." + "reviews.empty.desc": "Esta traducción no tiene segmentos guardados: o falló antes de traducir el texto, o el servidor ya no la conserva. Puede quitarla de la lista de traducciones recientes.", + "reviews.rebuildDisabledHint": "Primero aprueba o edita al menos un segmento: la reconstrucción aplica tu revisión." } diff --git a/frontend/src/lib/i18n/messages/fa/reviews.json b/frontend/src/lib/i18n/messages/fa/reviews.json index 0b3f4ff..53e1e90 100644 --- a/frontend/src/lib/i18n/messages/fa/reviews.json +++ b/frontend/src/lib/i18n/messages/fa/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approving ({current}/{total})…", "reviews.noMatchingSegments": "No segments match your filter.", "reviews.empty.title": "بخشی برای بازبینی در این کار وجود ندارد", - "reviews.empty.desc": "این ترجمه بخش ذخیره‌شده‌ای ندارد: یا پیش از ترجمهٔ متن ناموفق بوده، یا سرور دیگر آن را نگه نمی‌دارد. می‌توانید آن را از فهرست ترجمه‌های اخیر حذف کنید." + "reviews.empty.desc": "این ترجمه بخش ذخیره‌شده‌ای ندارد: یا پیش از ترجمهٔ متن ناموفق بوده، یا سرور دیگر آن را نگه نمی‌دارد. می‌توانید آن را از فهرست ترجمه‌های اخیر حذف کنید.", + "reviews.rebuildDisabledHint": "ابتدا حداقل یک بخش را تأیید یا ویرایش کنید — بازسازی، بازبینی شما را اعمال می‌کند." } diff --git a/frontend/src/lib/i18n/messages/fr/reviews.json b/frontend/src/lib/i18n/messages/fr/reviews.json index 38b3788..9a00f3d 100644 --- a/frontend/src/lib/i18n/messages/fr/reviews.json +++ b/frontend/src/lib/i18n/messages/fr/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approbation ({current}/{total})…", "reviews.noMatchingSegments": "Aucun segment ne correspond à votre filtre.", "reviews.empty.title": "Rien à relire pour ce travail", - "reviews.empty.desc": "Cette traduction n'a aucun segment enregistré : soit elle a échoué avant la traduction du texte, soit elle n'est plus conservée par le serveur. Vous pouvez la retirer de la liste des traductions récentes." + "reviews.empty.desc": "Cette traduction n'a aucun segment enregistré : soit elle a échoué avant la traduction du texte, soit elle n'est plus conservée par le serveur. Vous pouvez la retirer de la liste des traductions récentes.", + "reviews.rebuildDisabledHint": "Approuvez ou modifiez d'abord au moins un segment — la reconstruction applique votre relecture." } diff --git a/frontend/src/lib/i18n/messages/it/reviews.json b/frontend/src/lib/i18n/messages/it/reviews.json index 786ab8a..b844860 100644 --- a/frontend/src/lib/i18n/messages/it/reviews.json +++ b/frontend/src/lib/i18n/messages/it/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approving ({current}/{total})…", "reviews.noMatchingSegments": "No segments match your filter.", "reviews.empty.title": "Niente da rivedere per questo lavoro", - "reviews.empty.desc": "Questa traduzione non ha segmenti salvati: o è fallita prima di tradurre il testo, o il server non la conserva più. Puoi rimuoverla dall'elenco delle traduzioni recenti." + "reviews.empty.desc": "Questa traduzione non ha segmenti salvati: o è fallita prima di tradurre il testo, o il server non la conserva più. Puoi rimuoverla dall'elenco delle traduzioni recenti.", + "reviews.rebuildDisabledHint": "Prima approva o modifica almeno un segmento: la ricostruzione applica la tua revisione." } diff --git a/frontend/src/lib/i18n/messages/ja/reviews.json b/frontend/src/lib/i18n/messages/ja/reviews.json index 7dae8de..3b0899f 100644 --- a/frontend/src/lib/i18n/messages/ja/reviews.json +++ b/frontend/src/lib/i18n/messages/ja/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approving ({current}/{total})…", "reviews.noMatchingSegments": "No segments match your filter.", "reviews.empty.title": "このジョブに校正するセグメントはありません", - "reviews.empty.desc": "この翻訳には保存されたセグメントがありません。テキストの翻訳前に失敗したか、サーバーに保持されていません。最近の翻訳リストから削除できます。" + "reviews.empty.desc": "この翻訳には保存されたセグメントがありません。テキストの翻訳前に失敗したか、サーバーに保持されていません。最近の翻訳リストから削除できます。", + "reviews.rebuildDisabledHint": "まず少なくとも1つのセグメントを承認または編集してください。再構築には校正内容が必要です。" } diff --git a/frontend/src/lib/i18n/messages/ko/reviews.json b/frontend/src/lib/i18n/messages/ko/reviews.json index 303eb61..d74a978 100644 --- a/frontend/src/lib/i18n/messages/ko/reviews.json +++ b/frontend/src/lib/i18n/messages/ko/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approving ({current}/{total})…", "reviews.noMatchingSegments": "No segments match your filter.", "reviews.empty.title": "이 작업에 검토할 세그먼트가 없습니다", - "reviews.empty.desc": "이 번역에는 저장된 세그먼트가 없습니다. 텍스트 번역 전에 실패했거나 서버에 더 이상 보관되지 않습니다. 최근 번역 목록에서 제거할 수 있습니다." + "reviews.empty.desc": "이 번역에는 저장된 세그먼트가 없습니다. 텍스트 번역 전에 실패했거나 서버에 더 이상 보관되지 않습니다. 최근 번역 목록에서 제거할 수 있습니다.", + "reviews.rebuildDisabledHint": "먼저 세그먼트를 하나 이상 승인하거나 수정하세요 — 재구성에는 검토 내용이 필요합니다." } diff --git a/frontend/src/lib/i18n/messages/nl/reviews.json b/frontend/src/lib/i18n/messages/nl/reviews.json index 56ce3b0..c8e7d89 100644 --- a/frontend/src/lib/i18n/messages/nl/reviews.json +++ b/frontend/src/lib/i18n/messages/nl/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approving ({current}/{total})…", "reviews.noMatchingSegments": "No segments match your filter.", "reviews.empty.title": "Niets te controleren voor deze opdracht", - "reviews.empty.desc": "Deze vertaling heeft geen opgeslagen segmenten: of hij mislukte voordat er tekst werd vertaald, of de server bewaart hem niet meer. U kunt hem verwijderen uit de lijst met recente vertalingen." + "reviews.empty.desc": "Deze vertaling heeft geen opgeslagen segmenten: of hij mislukte voordat er tekst werd vertaald, of de server bewaart hem niet meer. U kunt hem verwijderen uit de lijst met recente vertalingen.", + "reviews.rebuildDisabledHint": "Keur eerst minstens één segment goed of bewerk het — de herbouw past uw controle toe." } diff --git a/frontend/src/lib/i18n/messages/pt/reviews.json b/frontend/src/lib/i18n/messages/pt/reviews.json index 5861bcf..46b0b2b 100644 --- a/frontend/src/lib/i18n/messages/pt/reviews.json +++ b/frontend/src/lib/i18n/messages/pt/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approving ({current}/{total})…", "reviews.noMatchingSegments": "No segments match your filter.", "reviews.empty.title": "Nada a revisar neste trabalho", - "reviews.empty.desc": "Esta tradução não tem segmentos armazenados: ou falhou antes de qualquer texto ser traduzido, ou o servidor já não a conserva. Pode removê-la da lista de traduções recentes." + "reviews.empty.desc": "Esta tradução não tem segmentos armazenados: ou falhou antes de qualquer texto ser traduzido, ou o servidor já não a conserva. Pode removê-la da lista de traduções recentes.", + "reviews.rebuildDisabledHint": "Aprove ou edite primeiro pelo menos um segmento — a reconstrução aplica a sua revisão." } diff --git a/frontend/src/lib/i18n/messages/ru/reviews.json b/frontend/src/lib/i18n/messages/ru/reviews.json index 1520a34..f8d5468 100644 --- a/frontend/src/lib/i18n/messages/ru/reviews.json +++ b/frontend/src/lib/i18n/messages/ru/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approving ({current}/{total})…", "reviews.noMatchingSegments": "No segments match your filter.", "reviews.empty.title": "Для этого задания нет сегментов для проверки", - "reviews.empty.desc": "У этого перевода нет сохранённых сегментов: либо он завершился ошибкой до перевода текста, либо сервер его больше не хранит. Вы можете удалить его из списка недавних переводов." + "reviews.empty.desc": "У этого перевода нет сохранённых сегментов: либо он завершился ошибкой до перевода текста, либо сервер его больше не хранит. Вы можете удалить его из списка недавних переводов.", + "reviews.rebuildDisabledHint": "Сначала утвердите или измените хотя бы один сегмент — пересборка применит вашу правку." } diff --git a/frontend/src/lib/i18n/messages/zh/reviews.json b/frontend/src/lib/i18n/messages/zh/reviews.json index a0ba782..cf929c9 100644 --- a/frontend/src/lib/i18n/messages/zh/reviews.json +++ b/frontend/src/lib/i18n/messages/zh/reviews.json @@ -49,5 +49,6 @@ "reviews.approvingProgress": "Approving ({current}/{total})…", "reviews.noMatchingSegments": "No segments match your filter.", "reviews.empty.title": "此任务没有可校对的段落", - "reviews.empty.desc": "此翻译没有已存储的段落:可能是在翻译文本之前失败,或服务器已不再保留。您可以将其从最近翻译列表中移除。" + "reviews.empty.desc": "此翻译没有已存储的段落:可能是在翻译文本之前失败,或服务器已不再保留。您可以将其从最近翻译列表中移除。", + "reviews.rebuildDisabledHint": "请先批准或修改至少一个段落——重建会应用您的校对内容。" } diff --git a/tests/test_translators/test_rtl_layout.py b/tests/test_translators/test_rtl_layout.py index 83e7753..8afd809 100644 --- a/tests/test_translators/test_rtl_layout.py +++ b/tests/test_translators/test_rtl_layout.py @@ -1151,3 +1151,35 @@ class TestLtrNormalization: assert out_para is not None out_pPr = out_para._p.find(f"{{{A_NS}}}pPr") assert out_pPr is None or out_pPr.get("rtl") != "1" + + @pytest.mark.skipif(_host_rtl_font() is None, reason="no Arabic-capable font on this host") + def test_layout_mode_uses_the_resolved_font_file(self, tmp_path): + """Regression guard: PyMuPDF ignores the fontfile when fontname is a + base-14 name ("helv") — every Persian/Arabic glyph used to render as + '?' in Helvetica, with mass "[translation overflow]" markers.""" + import fitz + + src = tmp_path / "src.pdf" + doc = fitz.open() + page = doc.new_page() + page.insert_textbox( + fitz.Rect(72, 72, 400, 96), "Bonjour le monde", + fontsize=11, fontname="helv", + ) + doc.save(str(src)) + doc.close() + + translator = PDFTranslator( + provider=MockTranslationProvider({"Bonjour le monde": "سلام دنیا"}) + ) + out = tmp_path / "out.pdf" + translator.translate_file(src, out, "fa") + + doc2 = fitz.open(str(out)) + fonts = [f[3] for f in doc2[0].get_fonts()] + text = doc2[0].get_text() + doc2.close() + + assert "Helvetica" not in fonts, fonts + assert "?" not in text + assert any(0xFB50 <= ord(c) <= 0xFEFF for c in text) diff --git a/translators/pdf_translator.py b/translators/pdf_translator.py index f9e761e..3114048 100644 --- a/translators/pdf_translator.py +++ b/translators/pdf_translator.py @@ -1065,15 +1065,15 @@ class PDFTranslator: color = self._int_to_rgb(block["color"]) align = fitz.TEXT_ALIGN_RIGHT if rtl_target else fitz.TEXT_ALIGN_LEFT - # PyMuPDF bug: fontname=None raises AttributeError. Default to 'helv'. - # If a custom font file is available, use it via fontfile (fontname ignored). - # Base-14 font variant honouring the block's bold/italic flags — - # previously every block (headings included) rendered in regular. - # When a custom Unicode fontfile is used we keep it: we have no - # bold/italic variant of that file, and glyph coverage matters more - # than weight. + # PyMuPDF: fontname must be a CUSTOM name when a fontfile is given — + # a base-14 name ("helv"…) makes the fontfile SILENTLY IGNORED and + # every non-Latin glyph renders as "?". This is exactly how Persian + # PDFs used to come out full of question marks. + # Without a font file, pick the base-14 variant honouring the + # block's bold/italic flags. When a custom Unicode fontfile is used + # we keep one regular face: glyph coverage matters more than weight. if font_path: - fontname = "helv" + fontname = "wordly-unicode" fontfile = font_path else: fontname = (