Translation on Firefox Translations models; new logo

- 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>
This commit is contained in:
2026-10-18 22:04:04 +02:00
co-authored by Claude Opus 5.5
parent 8a1b9979e9
commit 972e10876b
38 changed files with 1627 additions and 223 deletions
+61 -1
View File
@@ -15,10 +15,12 @@ import re
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, Path, Query
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app import translation
from app.api.errors import raise_api_error
from app.config import get_settings
from app.db import get_session
@@ -332,3 +334,61 @@ async def remove_user(user_id: uuid.UUID, admin: CurrentAdmin, db: DbSession) ->
except MailServerError as exc:
raise_api_error(502, "mail_server_unavailable", f"the mail server did not answer: {exc}")
return {"status": "deleted"}
# --- email translation languages (translator service) ------------------------------
class InstallProgressOut(BaseModel):
done: int
total: int
class AdminTranslationOut(BaseModel):
enabled: bool # TRANSLATOR_URL is set
reachable: bool
installed: list[str] = []
installing: dict[str, InstallProgressOut] = {}
available: list[str] = [] # what Mozilla has models for (empty: offline)
errors: dict[str, str] = {} # why a language's last install failed
_LANG = Path(pattern=r"^[a-z]{2,3}(-[A-Za-z]{2,4})?$")
@router.get("/translation", response_model=AdminTranslationOut)
async def translation_status(admin: CurrentAdmin) -> AdminTranslationOut:
if not translation.enabled():
return AdminTranslationOut(enabled=False, reachable=False)
try:
data = await translation.status()
except translation.TranslationError:
return AdminTranslationOut(enabled=True, reachable=False)
return AdminTranslationOut(enabled=True, reachable=True, **data)
@router.post(
"/translation/languages/{code}",
response_model=AdminTranslationOut,
dependencies=[Depends(require_csrf)],
)
async def install_language(admin: CurrentAdmin, code: str = _LANG) -> AdminTranslationOut:
"""Download a language's models (to and from English) in the background."""
try:
await translation.install(code)
except translation.TranslationError as exc:
raise_api_error(502, "translator_failed", str(exc))
return await translation_status(admin)
@router.delete(
"/translation/languages/{code}",
response_model=AdminTranslationOut,
dependencies=[Depends(require_csrf)],
)
async def remove_language(admin: CurrentAdmin, code: str = _LANG) -> AdminTranslationOut:
try:
await translation.remove(code)
except translation.TranslationError as exc:
raise_api_error(502, "translator_failed", str(exc))
return await translation_status(admin)