Productivity features, UI translations, local email translation
- Snooze (with a Snoozed folder), keyboard shortcuts with a Settings section, mail templates (Compose > More > Templates), a Subscriptions folder with bulk unsubscribe/delete, and tracker blocking with a Privacy toggle (on by default) - Own dialogs instead of browser popups; emoji picker in compose; compose windows survive a visit to Settings - Sentence completion learned from the user's sent mail (no AI) - Interface in English and Polish (one file per language, English as the fallback), picked from the browser on first run, then kept - Email translation on the server via LibreTranslate: Translate button and "Translate to...", messages in several languages translated part by part, names/addresses/emoji/file names/phone numbers kept, an animated line while translating - Antispam "Mark as spam" loads with the message, not when the menu opens Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""Getting a message ready for translation (app/translation_prep.py)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.translation_prep import (
|
||||
find_names,
|
||||
find_names_in_signature,
|
||||
join_wrapped_lines,
|
||||
keep_symbols,
|
||||
keep_terms,
|
||||
split_pieces,
|
||||
tidy,
|
||||
translate_phrases,
|
||||
)
|
||||
|
||||
KEPT = '<span translate="no">'
|
||||
|
||||
|
||||
def test_wrapped_lines_are_joined_but_paragraphs_kept() -> None:
|
||||
assert join_wrapped_lines("<p>ask you about<br>the schedule.</p>") == (
|
||||
"<p>ask you about the schedule.</p>"
|
||||
)
|
||||
assert join_wrapped_lines("<div>ask you about</div><div>the schedule.</div>") == (
|
||||
"<div>ask you about the schedule.</div>"
|
||||
)
|
||||
text = '<div class="wm-plaintext">ask you about\nthe schedule.\n\nThanks,\nAnna</div>'
|
||||
assert join_wrapped_lines(text) == (
|
||||
'<div class="wm-plaintext">ask you about the schedule.\n\nThanks,\nAnna</div>'
|
||||
)
|
||||
# A new sentence, or a capital after the break: left alone.
|
||||
for html in ("<p>Done.<br>then more</p>", "<p>Hi Lukas,<br>Natalia</p>"):
|
||||
assert join_wrapped_lines(html) == html
|
||||
|
||||
|
||||
def test_sign_offs_come_from_the_phrase_table() -> None:
|
||||
assert translate_phrases("<p>Best,<br>Natalia</p>", "en", "pl") == (
|
||||
f"<p>{KEPT}Pozdrawiam,</span><br>Natalia</p>"
|
||||
)
|
||||
assert translate_phrases("<div>Mit freundlichen Grüßen</div>", "de", "en") == (
|
||||
f"<div>{KEPT}Kind regards</span></div>"
|
||||
)
|
||||
assert "Pozdrawiam" not in translate_phrases("<p>Best results so far</p>", "en", "pl")
|
||||
assert translate_phrases("<p>Best,</p>", "fr", "pl") == "<p>Best,</p>" # no table
|
||||
|
||||
|
||||
def test_names_mid_sentence_but_not_headlines_or_german() -> None:
|
||||
html = "<p>I came across Imaginary Pixels and Trusted Partner Network, in Spain.</p>"
|
||||
assert find_names(html, "en") == {"Imaginary Pixels", "Trusted Partner Network"}
|
||||
assert find_names("<p>Why Matchmoving Matters, Editing Digger And More</p>", "en") == set()
|
||||
assert find_names("<p>Wir haben das Neue Angebot gesehen.</p>", "de") == set()
|
||||
# Not across a sentence's end.
|
||||
assert find_names("<p>working mostly in NukeX. My work includes paint.</p>", "en") == set()
|
||||
assert find_names_in_signature("Imaginary Pixels – visual effects studio") == [
|
||||
"Imaginary Pixels"
|
||||
]
|
||||
|
||||
|
||||
def test_kept_terms_and_spacing() -> None:
|
||||
out = keep_terms(
|
||||
"<p>Hi Natalia Raz, see Imaginary Pixels.</p>", ["Natalia Raz", "Imaginary Pixels"]
|
||||
)
|
||||
assert out == f"<p>Hi {KEPT}Natalia Raz</span>, see {KEPT}Imaginary Pixels</span>.</p>"
|
||||
assert keep_terms("<style>.Natalia{}</style>", ["Natalia"]) == "<style>.Natalia{}</style>"
|
||||
assert tidy(f"nad {KEPT}Bruton</span>i dalej") == f"nad {KEPT}Bruton</span> i dalej"
|
||||
assert tidy("<span>a</span>b") == "<span>a</span>b" # the sender's own spans
|
||||
|
||||
|
||||
def test_emoji_kept() -> None:
|
||||
html = (
|
||||
'<p>nice to e-meet you <span style="font-family:Segoe UI Emoji">\n😊</span> </p>'
|
||||
"<p>Great 👍🏽 work ❤️</p>"
|
||||
)
|
||||
out = keep_symbols(html)
|
||||
assert f"{KEPT}😊</span>" in out
|
||||
assert f"{KEPT}👍🏽</span>" in out
|
||||
assert f"{KEPT}❤️</span>" in out
|
||||
assert "nice to e-meet you" in out # the words still go to translation
|
||||
|
||||
|
||||
def test_file_names_kept() -> None:
|
||||
html = (
|
||||
"<li>A_0023C017_260521_130157_p1CAC.mov</li>"
|
||||
"<p>See final_v2 and plate-03.exr, e.g. the U.S cut.</p>"
|
||||
)
|
||||
out = keep_symbols(html)
|
||||
assert f"{KEPT}A_0023C017_260521_130157_p1CAC.mov</span>" in out
|
||||
assert f"{KEPT}final_v2</span>" in out
|
||||
assert f"{KEPT}plate-03.exr</span>" in out
|
||||
assert "e.g. the U.S cut" in out # no file names there
|
||||
|
||||
|
||||
def test_symbols_not_marked_twice_or_in_attributes() -> None:
|
||||
html = (
|
||||
f'<a href="https://x.io/a_b.mov">{KEPT}https://x.io/a_b.mov</span></a>'
|
||||
"<style>.a_b{}</style>"
|
||||
)
|
||||
assert keep_symbols(html) == html
|
||||
|
||||
|
||||
def test_text_without_letters_is_kept() -> None:
|
||||
# A byte-order mark alone between tags came back as "Uzasadnienie".
|
||||
out = keep_symbols(
|
||||
'<div dir="ltr">\ufeff<div>Dear Lukas,</div><div> </div><p> – </p></div>'
|
||||
)
|
||||
assert f"{KEPT}\ufeff</span>" in out
|
||||
assert "<div> </div>" in out # only spaces: nothing to keep
|
||||
assert f"<p> {KEPT}–</span> </p>" in out
|
||||
assert "<div>Dear Lukas,</div>" in out
|
||||
|
||||
|
||||
def test_handles_and_phone_numbers_kept() -> None:
|
||||
out = keep_symbols(
|
||||
"<p>WeChat: Jacquelineeeee_ WhatsApp: +48 780 549 877, for 1300 - 2500 Euro</p>"
|
||||
)
|
||||
assert f"{KEPT}Jacquelineeeee_</span>" in out
|
||||
assert f"{KEPT}+48 780 549 877</span>" in out
|
||||
assert "1300 - 2500 Euro" in out # plain numbers stay in their sentence
|
||||
|
||||
|
||||
def test_paragraphs_split_at_block_tags() -> None:
|
||||
html = (
|
||||
f"<style>p{{}}</style><div>Dear <b>Anna</b>,<br>hello {KEPT}x.mov</span> there</div>"
|
||||
"<p> </p><li>Item</li>"
|
||||
)
|
||||
pieces, paragraphs = split_pieces(html)
|
||||
assert [p.text for p in paragraphs] == ["Dear Anna,", "hello x.mov there", "Item"]
|
||||
# The kept file name counts for the language but is not translated.
|
||||
assert [pieces[i] for i in paragraphs[1].pieces] == ["hello ", " there"]
|
||||
Reference in New Issue
Block a user