- Email translation moves from LibreTranslate to our own translator
service (translator/): Mozilla's Firefox Translations models on the
Bergamot engine, fastText language detection (176 languages). Polish,
English, German, French, Spanish and Italian on first start (~350 MB
instead of ~10 GB); Admin panel > Email translation installs and
removes languages. Built and pushed as the `translator` image.
- Every message is translated paragraph by paragraph as plain text;
names travel with their sentence and are kept only where the model
changed them; a sentence start the model copied ("Enclosed") is tried
again in lower case; list markers are not sent
- The Translate button shows only for installed languages
- New logo: favicon, app/maskable/iPhone icons, notification badge,
email logo and calendar copy regenerated; BIMI logo rebuilt in SVG
Tiny PS with its gradients; masks render in browsers
- Launch screen background is the app's dark grey
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
2.9 KiB
Python
82 lines
2.9 KiB
Python
"""Email translation's languages in the admin panel (app/api/admin.py), with
|
|
the translator service replaced by a stand-in."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app import translation
|
|
from tests.api.test_admin_users import World, world # noqa: F401 — the fixture
|
|
|
|
|
|
@pytest.fixture
|
|
def service(monkeypatch: pytest.MonkeyPatch) -> dict[str, list[str]]:
|
|
state: dict[str, list[str]] = {"installed": ["en", "pl"], "calls": []}
|
|
|
|
async def status() -> dict[str, object]:
|
|
return {
|
|
"installed": list(state["installed"]),
|
|
"installing": {"de": {"done": 10, "total": 40}} if "de" in state["calls"] else {},
|
|
"available": ["en", "pl", "de", "hu"],
|
|
"errors": {},
|
|
}
|
|
|
|
async def install(code: str) -> None:
|
|
state["calls"].append(code)
|
|
|
|
async def remove(code: str) -> None:
|
|
state["installed"].remove(code)
|
|
|
|
monkeypatch.setattr(translation, "enabled", lambda: True)
|
|
monkeypatch.setattr(translation, "status", status)
|
|
monkeypatch.setattr(translation, "install", install)
|
|
monkeypatch.setattr(translation, "remove", remove)
|
|
return state
|
|
|
|
|
|
async def test_admin_installs_and_removes_languages(
|
|
world: World, # noqa: F811
|
|
service: dict[str, list[str]],
|
|
) -> None:
|
|
async with await world.client(await world.user(admin=True)) as c:
|
|
got = (await c.get("/api/admin/translation")).json()
|
|
assert got["reachable"] and got["installed"] == ["en", "pl"]
|
|
assert got["available"] == ["en", "pl", "de", "hu"]
|
|
|
|
got = (await c.post("/api/admin/translation/languages/de")).json()
|
|
assert service["calls"] == ["de"]
|
|
assert got["installing"] == {"de": {"done": 10, "total": 40}}
|
|
|
|
got = (await c.delete("/api/admin/translation/languages/pl")).json()
|
|
assert got["installed"] == ["en"]
|
|
|
|
assert (await c.post("/api/admin/translation/languages/x;rm")).status_code == 422
|
|
|
|
|
|
async def test_only_admins(world: World, service: dict[str, list[str]]) -> None: # noqa: F811
|
|
async with await world.client(await world.user()) as c:
|
|
assert (await c.get("/api/admin/translation")).status_code == 403
|
|
assert (await c.post("/api/admin/translation/languages/de")).status_code == 403
|
|
assert service["calls"] == []
|
|
|
|
|
|
async def test_unreachable_service_is_reported(
|
|
world: World, # noqa: F811
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
async def down() -> dict[str, object]:
|
|
raise translation.TranslationError("down")
|
|
|
|
monkeypatch.setattr(translation, "enabled", lambda: True)
|
|
monkeypatch.setattr(translation, "status", down)
|
|
async with await world.client(await world.user(admin=True)) as c:
|
|
got = (await c.get("/api/admin/translation")).json()
|
|
assert got == {
|
|
"enabled": True,
|
|
"reachable": False,
|
|
"installed": [],
|
|
"installing": {},
|
|
"available": [],
|
|
"errors": {},
|
|
}
|