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,123 @@
|
||||
"""What is sent to the translation service (app/translation.py)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app import translation
|
||||
from app.translation import detect, languages_of, names_to_keep, protect_addresses, translate_html
|
||||
|
||||
|
||||
def test_addresses_in_text_are_kept_from_translation() -> None:
|
||||
html = (
|
||||
'<p>My reel: <a href="https://vimeo.com/1">https://vimeo.com/1</a>, '
|
||||
"write to anna@x.example or see www.example.com today.</p>"
|
||||
)
|
||||
out = protect_addresses(html)
|
||||
assert '<span translate="no">https://vimeo.com/1</span>' in out
|
||||
assert '<span translate="no">anna@x.example</span>' in out
|
||||
assert '<span translate="no">www.example.com</span>' in out
|
||||
assert '<a href="https://vimeo.com/1">' in out # attributes untouched
|
||||
|
||||
|
||||
def test_style_blocks_are_left_alone() -> None:
|
||||
html = "<style>.a{background:url(https://x.example/a.png)}</style><p>Hi</p>"
|
||||
assert protect_addresses(html) == html
|
||||
|
||||
|
||||
async def test_only_names_the_model_changes_are_kept(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
sent: list[object] = []
|
||||
|
||||
async def post(path: str, payload: dict[str, object]) -> object:
|
||||
sent.append(payload["q"])
|
||||
alone = {
|
||||
"Lukas Remis": "Lukas Remis.",
|
||||
"Milan": "Mediolan",
|
||||
"Imaginary Pixels": "Imaginanal",
|
||||
}
|
||||
return {"translatedText": [alone[q] for q in payload["q"]]} # type: ignore[attr-defined]
|
||||
|
||||
monkeypatch.setattr(translation, "_post", post)
|
||||
kept = await names_to_keep(
|
||||
["Milan", "Lukas Remis", "Imaginary Pixels", "Milan", "x"], "en", "pl"
|
||||
)
|
||||
assert kept == {"Milan", "Imaginary Pixels"} # "Lukas Remis" stays in its sentence
|
||||
assert len(sent) == 1 # one request for all
|
||||
|
||||
|
||||
async def test_all_names_kept_when_the_check_fails(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
async def post(path: str, payload: dict[str, object]) -> object:
|
||||
raise translation.TranslationError("down")
|
||||
|
||||
monkeypatch.setattr(translation, "_post", post)
|
||||
assert await names_to_keep(["Milan", "Lukas Remis"], "en", "pl") == {"Milan", "Lukas Remis"}
|
||||
|
||||
|
||||
def _fake_detector(monkeypatch: pytest.MonkeyPatch) -> list[str]:
|
||||
"""A detector that knows Chinese signs, Polish letters, else English."""
|
||||
asked: list[str] = []
|
||||
|
||||
async def detect_one(text: str) -> str:
|
||||
asked.append(text)
|
||||
if any("\u4e00" <= c <= "\u9fff" for c in text):
|
||||
return "zh-Hans"
|
||||
return "pl" if any(c in "ąćęłńóśźż" for c in text.lower()) else "en"
|
||||
|
||||
monkeypatch.setattr(translation, "_detect_one", detect_one)
|
||||
return asked
|
||||
|
||||
|
||||
async def test_short_parts_take_the_next_language(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
asked = _fake_detector(monkeypatch)
|
||||
parts = [
|
||||
"您好!",
|
||||
"Szanowny Kliencie,", # short: goes with the Polish that follows
|
||||
"Widzę, że złożyłeś wniosek o otwarcie sklepu na platformie.",
|
||||
"-----",
|
||||
"Hello, my name is Jacqueline, the manager for Europe.",
|
||||
"Jacqueline Lian", # short, last: goes with the one before
|
||||
]
|
||||
assert await languages_of(parts) == ["zh-Hans", "pl", "pl", "", "en", "en"]
|
||||
assert "Jacqueline Lian" not in asked # too short to ask about
|
||||
|
||||
|
||||
async def test_a_message_in_three_languages(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_fake_detector(monkeypatch)
|
||||
text = (
|
||||
"尊敬的客户,我是速卖通的官方客户经理,我从后台看到您申请入驻速卖通平台开店。\n"
|
||||
"Szanowny Kliencie,\n"
|
||||
"Widzę, że złożyłeś wniosek o otwarcie sklepu na platformie AliExpress.\n"
|
||||
"Dears,\nI noticed from the backend that you have applied to join the platform.\n"
|
||||
)
|
||||
assert set((await detect(text)).split(",")) == {"zh-Hans", "pl", "en"}
|
||||
# One language: one code.
|
||||
assert await detect("I noticed from the backend that you applied, thanks a lot.") == "en"
|
||||
|
||||
|
||||
async def test_each_paragraph_from_its_own_language(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_fake_detector(monkeypatch)
|
||||
calls: list[tuple[str, list[str]]] = []
|
||||
|
||||
async def post(path: str, payload: dict[str, object]) -> object:
|
||||
q = payload["q"]
|
||||
if path == "/translate" and isinstance(q, list) and payload.get("source") != "auto":
|
||||
calls.append((str(payload["source"]), list(q)))
|
||||
return {"translatedText": [f"PL({x})" for x in q]}
|
||||
return {"translatedText": list(q) if isinstance(q, list) else q} # names: unchanged
|
||||
|
||||
monkeypatch.setattr(translation, "_post", post)
|
||||
html = (
|
||||
"<div><span>我是速卖通的官方客户经理奎琳,</span>我从后台看到您申请入驻速卖通平台开店。</div>"
|
||||
"<div>Widzę, że złożyłeś wniosek o otwarcie sklepu & więcej.</div>"
|
||||
"<div>I noticed that you applied to join the platform & more.</div>"
|
||||
"<div>WhatsApp: +48 780 549 877</div>"
|
||||
)
|
||||
out = await translate_html(html, "zh-Hans,pl,en", "pl")
|
||||
assert [c[0] for c in calls] == ["zh-Hans", "en"] # the Polish is left alone
|
||||
assert "<div>Widzę, że złożyłeś wniosek o otwarcie sklepu & więcej.</div>" in out
|
||||
# Two pieces of one Chinese sentence get a space between them.
|
||||
assert "PL(我是速卖通的官方客户经理奎琳,) </span>PL(" in out
|
||||
assert "PL(I noticed that you applied to join the platform & more.)" in out.replace(
|
||||
"&", "&"
|
||||
)
|
||||
assert '<span translate="no">+48 780 549 877</span>' in out
|
||||
Reference in New Issue
Block a user