2026-10-18 20:00:31 +02:00
|
|
|
"""What is sent to the translation service (app/translation.py)."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from app import translation
|
2026-10-18 22:04:04 +02:00
|
|
|
from app.translation import detect, languages_of, protect_addresses, translate_html
|
2026-10-18 20:00:31 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-10-18 22:04:04 +02:00
|
|
|
async def test_names_go_with_their_sentence_unless_the_model_changes_them(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
sent: list[list[str]] = []
|
|
|
|
|
model = {
|
|
|
|
|
"I came across Imaginary Pixels today.": "Natknąłem się dziś na Imaginary Pixels.",
|
|
|
|
|
"Dear Lukas,": "Drogi Lukasie,", # an ending: still the name
|
|
|
|
|
"Regards, Milan": "Pozdrawiam, Mediolan", # changed: done again around it
|
|
|
|
|
"Regards,": "Pozdrawiam,",
|
|
|
|
|
}
|
2026-10-18 20:00:31 +02:00
|
|
|
|
|
|
|
|
async def post(path: str, payload: dict[str, object]) -> object:
|
2026-10-18 22:04:04 +02:00
|
|
|
q = payload["q"]
|
|
|
|
|
assert isinstance(q, list)
|
|
|
|
|
sent.append(list(q))
|
|
|
|
|
return {"translatedText": [model.get(x, x) for x in q]}
|
|
|
|
|
|
|
|
|
|
async def installed() -> list[translation.Language]:
|
|
|
|
|
return [translation.Language(c, c) for c in ("en", "pl")]
|
2026-10-18 20:00:31 +02:00
|
|
|
|
|
|
|
|
monkeypatch.setattr(translation, "_post", post)
|
2026-10-18 22:04:04 +02:00
|
|
|
monkeypatch.setattr(translation, "languages", installed)
|
|
|
|
|
html = "<p>I came across Imaginary Pixels today.</p><p>Dear Lukas,</p><p>Regards, Milan</p>"
|
|
|
|
|
out = await translate_html(html, "en", "pl", names=["Imaginary Pixels", "Lukas", "Milan"])
|
|
|
|
|
assert out == (
|
|
|
|
|
"<p>Natknąłem się dziś na Imaginary Pixels.</p><p>Drogi Lukasie,</p>"
|
|
|
|
|
"<p>Pozdrawiam, Milan</p>"
|
2026-10-18 20:00:31 +02:00
|
|
|
)
|
2026-10-18 22:04:04 +02:00
|
|
|
assert sent[1:] == [["Regards,"]] # only the piece that lost a name went again
|
2026-10-18 20:00:31 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fake_detector(monkeypatch: pytest.MonkeyPatch) -> list[str]:
|
|
|
|
|
"""A detector that knows Chinese signs, Polish letters, else English."""
|
|
|
|
|
asked: list[str] = []
|
|
|
|
|
|
2026-10-18 22:04:04 +02:00
|
|
|
def one(text: str) -> str:
|
2026-10-18 20:00:31 +02:00
|
|
|
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"
|
|
|
|
|
|
2026-10-18 22:04:04 +02:00
|
|
|
async def detect_many(texts: list[str]) -> list[str]:
|
|
|
|
|
return [one(t) for t in texts]
|
|
|
|
|
|
|
|
|
|
async def installed() -> list[translation.Language]:
|
|
|
|
|
return [translation.Language(c, c) for c in ("en", "pl", "zh-Hans")]
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(translation, "_detect_many", detect_many)
|
|
|
|
|
monkeypatch.setattr(translation, "languages", installed)
|
2026-10-18 20:00:31 +02:00
|
|
|
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)))
|
2026-10-18 22:04:04 +02:00
|
|
|
return {"translatedText": [f"PL({x.upper()})" for x in q]}
|
2026-10-18 20:00:31 +02:00
|
|
|
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
|
2026-10-18 22:04:04 +02:00
|
|
|
assert "PL(I NOTICED THAT YOU APPLIED TO JOIN THE PLATFORM & MORE.)" in out.replace(
|
2026-10-18 20:00:31 +02:00
|
|
|
"&", "&"
|
|
|
|
|
)
|
|
|
|
|
assert '<span translate="no">+48 780 549 877</span>' in out
|
2026-10-18 22:04:04 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_one_language_needs_no_detector_and_skips_what_is_missing(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
asked = _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") == "en":
|
|
|
|
|
calls.append(("en", list(q)))
|
|
|
|
|
return {"translatedText": [f"PL({x.upper()})" for x in q]}
|
|
|
|
|
return {"translatedText": list(q) if isinstance(q, list) else q}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(translation, "_post", post)
|
|
|
|
|
html = "<p>Hello there, how are you?</p><p>Short one.</p><p><b>1. Thursday 2nd July</b></p>"
|
|
|
|
|
out = await translate_html(html, "en", "pl")
|
|
|
|
|
assert out == (
|
|
|
|
|
"<p>PL(HELLO THERE, HOW ARE YOU?)</p><p>PL(SHORT ONE.)</p>"
|
|
|
|
|
"<p><b>1. PL(THURSDAY 2ND JULY)</b></p>" # the list marker is not sent
|
|
|
|
|
)
|
|
|
|
|
assert asked == [] # the message's one language is known: no detection
|
|
|
|
|
# Hungarian is not installed: left as it is, nothing sent.
|
|
|
|
|
calls.clear()
|
|
|
|
|
assert await translate_html("<p>Jó napot kívánok</p>", "hu", "pl") == "<p>Jó napot kívánok</p>"
|
|
|
|
|
assert calls == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_a_sentence_start_the_model_copied_is_tried_lower_case(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
model = {
|
|
|
|
|
"Enclosed is a summary. Natalia wrote it.": (
|
|
|
|
|
"Enclosed jest podsumowaniem. Natalia to napisała."
|
|
|
|
|
),
|
|
|
|
|
"enclosed is a summary. natalia wrote it.": (
|
|
|
|
|
"załączone jest podsumowanie. Natalia to napisała."
|
|
|
|
|
),
|
|
|
|
|
"Natalia wrote it.": "Natalia to napisała.",
|
|
|
|
|
"natalia wrote it.": "Natalia to napisała.",
|
|
|
|
|
}
|
|
|
|
|
sent: list[str] = []
|
|
|
|
|
|
|
|
|
|
async def post(path: str, payload: dict[str, object]) -> object:
|
|
|
|
|
q = payload["q"]
|
|
|
|
|
assert isinstance(q, list)
|
|
|
|
|
sent.extend(q)
|
|
|
|
|
return {"translatedText": [model[x] for x in q]}
|
|
|
|
|
|
|
|
|
|
async def installed() -> list[translation.Language]:
|
|
|
|
|
return [translation.Language(c, c) for c in ("en", "pl")]
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(translation, "_post", post)
|
|
|
|
|
monkeypatch.setattr(translation, "languages", installed)
|
|
|
|
|
out = await translate_html("<p>Enclosed is a summary. Natalia wrote it.</p>", "en", "pl")
|
|
|
|
|
assert out == "<p>Załączone jest podsumowanie. Natalia to napisała.</p>"
|
|
|
|
|
# A name comes back the same lower-case too: the first try stands.
|
|
|
|
|
sent.clear()
|
|
|
|
|
assert await translate_html("<p>Natalia wrote it.</p>", "en", "pl") == (
|
|
|
|
|
"<p>Natalia to napisała.</p>"
|
|
|
|
|
)
|
|
|
|
|
assert sent == ["Natalia wrote it.", "natalia wrote it."]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_capitalised_again_but_not_after_abbreviations() -> None:
|
|
|
|
|
assert translation._capitalised("załączony na lipiec 2026 r., od 13 lipca. dalej np. tak") == (
|
|
|
|
|
"Załączony na lipiec 2026 r., od 13 lipca. Dalej np. tak"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_comma_word_keeps_the_first_try_case() -> None:
|
|
|
|
|
first = "Enclosed jest podsumowaniem na lipiec 2026, od 13 lipca."
|
|
|
|
|
again = "Załączony jest podsumowanie na lipiec 2026 r., Od 13 lipca, Anna wie."
|
|
|
|
|
assert translation._as_before(again, first) == (
|
|
|
|
|
"Załączony jest podsumowanie na lipiec 2026 r., od 13 lipca, Anna wie."
|
|
|
|
|
)
|