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:
2026-10-18 20:00:31 +02:00
co-authored by Claude Opus 5.5
parent 463f96ad57
commit 8a1b9979e9
83 changed files with 8677 additions and 293 deletions
+128
View File
@@ -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> &nbsp;</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> &nbsp; </div><p> – </p></div>'
)
assert f"{KEPT}\ufeff</span>" in out
assert "<div> &nbsp; </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> &nbsp; </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"]