fix(pdf): le texte persan s'affichait en points d'interrogation
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m51s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m51s
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
This commit is contained in:
@@ -346,7 +346,16 @@ export default function ReviewPage() {
|
|||||||
if (f) importXliff(f);
|
if (f) importXliff(f);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" onClick={rebuild} disabled={isRebuilding || counts.approved + counts.edited === 0}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={rebuild}
|
||||||
|
disabled={isRebuilding || counts.approved + counts.edited === 0}
|
||||||
|
title={
|
||||||
|
counts.approved + counts.edited === 0
|
||||||
|
? t('reviews.rebuildDisabledHint')
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
{isRebuilding ? <Loader2 className="size-3.5 animate-spin" /> : <Download className="size-3.5" />}
|
{isRebuilding ? <Loader2 className="size-3.5 animate-spin" /> : <Download className="size-3.5" />}
|
||||||
{t('reviews.rebuild')}
|
{t('reviews.rebuild')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "No segments match your filter.",
|
"reviews.noMatchingSegments": "No segments match your filter.",
|
||||||
"reviews.empty.title": "لا توجد أجزاء لمراجعتها في هذه المهمة",
|
"reviews.empty.title": "لا توجد أجزاء لمراجعتها في هذه المهمة",
|
||||||
"reviews.empty.desc": "لا تحتوي هذه الترجمة على أجزاء محفوظة: إما أنها فشلت قبل ترجمة النص، أو لم يعد الخادم يحتفظ بها. يمكنك إزالتها من قائمة الترجمات الأخيرة."
|
"reviews.empty.desc": "لا تحتوي هذه الترجمة على أجزاء محفوظة: إما أنها فشلت قبل ترجمة النص، أو لم يعد الخادم يحتفظ بها. يمكنك إزالتها من قائمة الترجمات الأخيرة.",
|
||||||
|
"reviews.rebuildDisabledHint": "وافق على جزء واحد على الأقل أو عدّله أولاً — تعيد إعادة البناء تطبيق مراجعتك."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "No segments match your filter.",
|
"reviews.noMatchingSegments": "No segments match your filter.",
|
||||||
"reviews.empty.title": "Für diesen Auftrag gibt es nichts zu prüfen",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "No segments match your filter.",
|
"reviews.noMatchingSegments": "No segments match your filter.",
|
||||||
"reviews.empty.title": "Nothing to review for this job",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "No segments match your filter.",
|
"reviews.noMatchingSegments": "No segments match your filter.",
|
||||||
"reviews.empty.title": "Nada que revisar en este trabajo",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "No segments match your filter.",
|
"reviews.noMatchingSegments": "No segments match your filter.",
|
||||||
"reviews.empty.title": "بخشی برای بازبینی در این کار وجود ندارد",
|
"reviews.empty.title": "بخشی برای بازبینی در این کار وجود ندارد",
|
||||||
"reviews.empty.desc": "این ترجمه بخش ذخیرهشدهای ندارد: یا پیش از ترجمهٔ متن ناموفق بوده، یا سرور دیگر آن را نگه نمیدارد. میتوانید آن را از فهرست ترجمههای اخیر حذف کنید."
|
"reviews.empty.desc": "این ترجمه بخش ذخیرهشدهای ندارد: یا پیش از ترجمهٔ متن ناموفق بوده، یا سرور دیگر آن را نگه نمیدارد. میتوانید آن را از فهرست ترجمههای اخیر حذف کنید.",
|
||||||
|
"reviews.rebuildDisabledHint": "ابتدا حداقل یک بخش را تأیید یا ویرایش کنید — بازسازی، بازبینی شما را اعمال میکند."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approbation ({current}/{total})…",
|
"reviews.approvingProgress": "Approbation ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "Aucun segment ne correspond à votre filtre.",
|
"reviews.noMatchingSegments": "Aucun segment ne correspond à votre filtre.",
|
||||||
"reviews.empty.title": "Rien à relire pour ce travail",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "No segments match your filter.",
|
"reviews.noMatchingSegments": "No segments match your filter.",
|
||||||
"reviews.empty.title": "Niente da rivedere per questo lavoro",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "No segments match your filter.",
|
"reviews.noMatchingSegments": "No segments match your filter.",
|
||||||
"reviews.empty.title": "このジョブに校正するセグメントはありません",
|
"reviews.empty.title": "このジョブに校正するセグメントはありません",
|
||||||
"reviews.empty.desc": "この翻訳には保存されたセグメントがありません。テキストの翻訳前に失敗したか、サーバーに保持されていません。最近の翻訳リストから削除できます。"
|
"reviews.empty.desc": "この翻訳には保存されたセグメントがありません。テキストの翻訳前に失敗したか、サーバーに保持されていません。最近の翻訳リストから削除できます。",
|
||||||
|
"reviews.rebuildDisabledHint": "まず少なくとも1つのセグメントを承認または編集してください。再構築には校正内容が必要です。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "No segments match your filter.",
|
"reviews.noMatchingSegments": "No segments match your filter.",
|
||||||
"reviews.empty.title": "이 작업에 검토할 세그먼트가 없습니다",
|
"reviews.empty.title": "이 작업에 검토할 세그먼트가 없습니다",
|
||||||
"reviews.empty.desc": "이 번역에는 저장된 세그먼트가 없습니다. 텍스트 번역 전에 실패했거나 서버에 더 이상 보관되지 않습니다. 최근 번역 목록에서 제거할 수 있습니다."
|
"reviews.empty.desc": "이 번역에는 저장된 세그먼트가 없습니다. 텍스트 번역 전에 실패했거나 서버에 더 이상 보관되지 않습니다. 최근 번역 목록에서 제거할 수 있습니다.",
|
||||||
|
"reviews.rebuildDisabledHint": "먼저 세그먼트를 하나 이상 승인하거나 수정하세요 — 재구성에는 검토 내용이 필요합니다."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "No segments match your filter.",
|
"reviews.noMatchingSegments": "No segments match your filter.",
|
||||||
"reviews.empty.title": "Niets te controleren voor deze opdracht",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "No segments match your filter.",
|
"reviews.noMatchingSegments": "No segments match your filter.",
|
||||||
"reviews.empty.title": "Nada a revisar neste trabalho",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "No segments match your filter.",
|
"reviews.noMatchingSegments": "No segments match your filter.",
|
||||||
"reviews.empty.title": "Для этого задания нет сегментов для проверки",
|
"reviews.empty.title": "Для этого задания нет сегментов для проверки",
|
||||||
"reviews.empty.desc": "У этого перевода нет сохранённых сегментов: либо он завершился ошибкой до перевода текста, либо сервер его больше не хранит. Вы можете удалить его из списка недавних переводов."
|
"reviews.empty.desc": "У этого перевода нет сохранённых сегментов: либо он завершился ошибкой до перевода текста, либо сервер его больше не хранит. Вы можете удалить его из списка недавних переводов.",
|
||||||
|
"reviews.rebuildDisabledHint": "Сначала утвердите или измените хотя бы один сегмент — пересборка применит вашу правку."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,6 @@
|
|||||||
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
"reviews.approvingProgress": "Approving ({current}/{total})…",
|
||||||
"reviews.noMatchingSegments": "No segments match your filter.",
|
"reviews.noMatchingSegments": "No segments match your filter.",
|
||||||
"reviews.empty.title": "此任务没有可校对的段落",
|
"reviews.empty.title": "此任务没有可校对的段落",
|
||||||
"reviews.empty.desc": "此翻译没有已存储的段落:可能是在翻译文本之前失败,或服务器已不再保留。您可以将其从最近翻译列表中移除。"
|
"reviews.empty.desc": "此翻译没有已存储的段落:可能是在翻译文本之前失败,或服务器已不再保留。您可以将其从最近翻译列表中移除。",
|
||||||
|
"reviews.rebuildDisabledHint": "请先批准或修改至少一个段落——重建会应用您的校对内容。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1151,3 +1151,35 @@ class TestLtrNormalization:
|
|||||||
assert out_para is not None
|
assert out_para is not None
|
||||||
out_pPr = out_para._p.find(f"{{{A_NS}}}pPr")
|
out_pPr = out_para._p.find(f"{{{A_NS}}}pPr")
|
||||||
assert out_pPr is None or out_pPr.get("rtl") != "1"
|
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)
|
||||||
|
|||||||
@@ -1065,15 +1065,15 @@ class PDFTranslator:
|
|||||||
color = self._int_to_rgb(block["color"])
|
color = self._int_to_rgb(block["color"])
|
||||||
align = fitz.TEXT_ALIGN_RIGHT if rtl_target else fitz.TEXT_ALIGN_LEFT
|
align = fitz.TEXT_ALIGN_RIGHT if rtl_target else fitz.TEXT_ALIGN_LEFT
|
||||||
|
|
||||||
# PyMuPDF bug: fontname=None raises AttributeError. Default to 'helv'.
|
# PyMuPDF: fontname must be a CUSTOM name when a fontfile is given —
|
||||||
# If a custom font file is available, use it via fontfile (fontname ignored).
|
# a base-14 name ("helv"…) makes the fontfile SILENTLY IGNORED and
|
||||||
# Base-14 font variant honouring the block's bold/italic flags —
|
# every non-Latin glyph renders as "?". This is exactly how Persian
|
||||||
# previously every block (headings included) rendered in regular.
|
# PDFs used to come out full of question marks.
|
||||||
# When a custom Unicode fontfile is used we keep it: we have no
|
# Without a font file, pick the base-14 variant honouring the
|
||||||
# bold/italic variant of that file, and glyph coverage matters more
|
# block's bold/italic flags. When a custom Unicode fontfile is used
|
||||||
# than weight.
|
# we keep one regular face: glyph coverage matters more than weight.
|
||||||
if font_path:
|
if font_path:
|
||||||
fontname = "helv"
|
fontname = "wordly-unicode"
|
||||||
fontfile = font_path
|
fontfile = font_path
|
||||||
else:
|
else:
|
||||||
fontname = (
|
fontname = (
|
||||||
|
|||||||
Reference in New Issue
Block a user