From 972e10876ba084d9c8972211eb365dea326ff718 Mon Sep 17 00:00:00 2001 From: Lukas Remis Date: Sun, 18 Oct 2026 22:04:04 +0200 Subject: [PATCH] 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 --- .env.example | 2 + Makefile | 2 +- backend/app/api/admin.py | 62 ++- backend/app/api/translation.py | 2 +- backend/app/config.py | 6 +- backend/app/identity/assets/ultimo-logo.png | Bin 5024 -> 6091 bytes backend/app/translation.py | 298 ++++++++---- backend/app/translation_prep.py | 30 +- backend/tests/api/test_admin_translation.py | 81 ++++ backend/tests/test_translation_text.py | 139 ++++-- deploy/build-push.sh | 12 +- deploy/webmail.sh | 24 +- docker-compose.yml | 38 +- docker/Dockerfile.translator | 38 ++ docker/docker-compose.yml | 24 +- docs/nas-install.md | 17 + frontend/icons/ultimo-logo-signet.svg | 124 ++++- frontend/icons/ultimo-logo.svg | 129 +++++- frontend/public/apple-touch-icon.png | Bin 3827 -> 6646 bytes frontend/public/badge-96.png | Bin 2430 -> 1363 bytes frontend/public/bimi/ultimo-logo.svg | 11 +- frontend/public/favicon.ico | Bin 15086 -> 15086 bytes frontend/public/icon-192.png | Bin 9074 -> 7416 bytes frontend/public/icon-512.png | Bin 25482 -> 28144 bytes frontend/public/icon-maskable-192.png | Bin 5322 -> 6944 bytes frontend/public/icon-maskable-512.png | Bin 15222 -> 23035 bytes frontend/public/manifest.webmanifest | 2 +- frontend/public/ultimo-logo-signet.svg | 90 +++- frontend/src/admin/AdminApp.tsx | 137 +++++- frontend/src/components/thread/ThreadView.tsx | 2 +- frontend/src/i18n/en.ts | 14 + frontend/src/i18n/index.tsx | 11 +- frontend/src/i18n/pl.ts | 14 + frontend/src/lib/api.ts | 6 + frontend/src/lib/types.ts | 12 + .../ui/ultimo-logo-signet.svg | 90 +++- translator/requirements.txt | 6 + translator/translator.py | 427 ++++++++++++++++++ 38 files changed, 1627 insertions(+), 223 deletions(-) create mode 100644 backend/tests/api/test_admin_translation.py create mode 100644 docker/Dockerfile.translator create mode 100644 translator/requirements.txt create mode 100644 translator/translator.py diff --git a/.env.example b/.env.example index 86bb653..1640352 100644 --- a/.env.example +++ b/.env.example @@ -21,3 +21,5 @@ RSPAMD_CONTROLLER_PASSWORD=devpass # Spelling/grammar check in the compose window (LanguageTool). Empty = off. LANGUAGETOOL_URL=http://127.0.0.1:8010 +# Email translation (the translator service, translator/). Empty = off. +TRANSLATOR_URL=http://127.0.0.1:5005 diff --git a/Makefile b/Makefile index cdca34c..2c4cc3a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ up: ## start services - docker compose -f docker/docker-compose.yml up -d postgres redis meilisearch languagetool libretranslate + docker compose -f docker/docker-compose.yml up -d postgres redis meilisearch languagetool translator down: ## stop services (data kept) docker compose -f docker/docker-compose.yml down reset-db: ## DESTRUCTIVE: wipe db + search + blobs, re-migrate diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 07e1e12..4606a82 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -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) diff --git a/backend/app/api/translation.py b/backend/app/api/translation.py index e6807c3..829eef6 100644 --- a/backend/app/api/translation.py +++ b/backend/app/api/translation.py @@ -25,6 +25,6 @@ class TranslationInfoOut(BaseModel): async def translation_languages(user: CurrentUser) -> TranslationInfoOut: found = await translation.languages() return TranslationInfoOut( - available=bool(found), + available=len(found) > 1, # English alone translates nothing languages=[LanguageOut(code=x.code, name=x.name) for x in found], ) diff --git a/backend/app/config.py b/backend/app/config.py index 5ed920e..c48723e 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -71,9 +71,9 @@ class Settings(BaseSettings): # Spelling and grammar checking in the compose window (LanguageTool server, # e.g. http://languagetool:8010). Empty: the check is off for everyone. languagetool_url: str = Field(default="", alias="LANGUAGETOOL_URL") - # Email translation: a LibreTranslate server (app/translation.py), e.g. - # http://libretranslate:5000. Empty: translation is off. - libretranslate_url: str = Field(default="", alias="LIBRETRANSLATE_URL") + # Email translation: the translator service (translator/, app/translation.py), + # e.g. http://translator:5000. Empty: translation is off. + translator_url: str = Field(default="", alias="TRANSLATOR_URL") # New passwords are checked against known data breaches (Have I Been Pwned's # k-anonymity range API: only 5 hex digits of a SHA-1 leave the server). password_breach_check: bool = Field(default=True, alias="PASSWORD_BREACH_CHECK") diff --git a/backend/app/identity/assets/ultimo-logo.png b/backend/app/identity/assets/ultimo-logo.png index 0965a0e659f7fe62e120c23518ea9ed9e52adb7b..2a914590af22bc19caca6d808350b5fb92020bca 100644 GIT binary patch literal 6091 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 literal 5024 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 b=~~Gsik+)ee*OEMx(3iz*N0ZBB3}I$J{)b@ diff --git a/backend/app/translation.py b/backend/app/translation.py index 14384aa..6897e14 100644 --- a/backend/app/translation.py +++ b/backend/app/translation.py @@ -1,19 +1,22 @@ -"""Email translation, on this server: LibreTranslate (Argos models) in its own -container, reached only by the API. A message's text never leaves the server. +"""Email translation, on this server: the translator service (translator/: +Mozilla's Firefox Translations models and fastText language identification) +in its own container, reached only by the API. A message's text never leaves +the server. - A message's language is detected the first time it is shown (from its own text: quoted replies and signatures left out) and kept on the message, so the reading pane can offer "Translate" when it differs from the interface language. - A translation is of the message HTML the user sees (already sanitized; the result is sanitized again), cached per message, target language and whether - remote images were shown. + remote images were shown. It is made paragraph by paragraph, as plain text + (translation_prep.split_pieces), each paragraph from its own language. +- The languages on offer are those installed on the service (admin panel). -Off when ``LIBRETRANSLATE_URL`` is empty. +Off when ``TRANSLATOR_URL`` is empty. """ from __future__ import annotations -import asyncio import html as html_lib import logging import re @@ -30,10 +33,10 @@ from app.config import get_settings logger = logging.getLogger(__name__) DETECT_CHARS = 1500 # of the message's own text: plenty to tell the language -MIN_CONFIDENCE = 50.0 # below this, LibreTranslate is guessing +MIN_CONFIDENCE = 50.0 # below this, the detector is guessing MAX_HTML_CHARS = 150_000 # a translation request bigger than this is refused _TIMEOUT = httpx.Timeout(120.0, connect=5.0) # long messages take a while on a CPU -_LANG_CACHE_S = 600 +_LANG_CACHE_S = 60 class TranslationError(Exception): @@ -47,65 +50,108 @@ class Language: def enabled() -> bool: - return bool(get_settings().libretranslate_url.strip()) + return bool(get_settings().translator_url.strip()) def _url(path: str) -> str: - return get_settings().libretranslate_url.rstrip("/") + path + return get_settings().translator_url.rstrip("/") + path -async def _post(path: str, payload: dict[str, object]) -> object: +async def _call(method: str, path: str, payload: dict[str, object] | None = None) -> object: if not enabled(): raise TranslationError("translation is not set up on this server") try: async with httpx.AsyncClient(timeout=_TIMEOUT) as client: - resp = await client.post(_url(path), json=payload) + resp = await client.request(method, _url(path), json=payload) except httpx.HTTPError as exc: raise TranslationError(f"the translation service is unreachable: {exc}") from exc - if resp.status_code != 200: + if resp.status_code not in (200, 202): detail = resp.text[:200] raise TranslationError(f"the translation service refused: {resp.status_code} {detail}") return resp.json() +async def _post(path: str, payload: dict[str, object]) -> object: + return await _call("POST", path, payload) + + _languages: tuple[float, list[Language]] = (0.0, []) async def languages() -> list[Language]: - """What the service can translate between (cached for a few minutes).""" + """The installed languages, English among them (cached for a minute). The + browser names them in the interface language; ``name`` is the code.""" global _languages + if not enabled(): + return [] stamp, cached = _languages if cached and time.monotonic() - stamp < _LANG_CACHE_S: return cached - if not enabled(): - return [] try: async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=3.0)) as client: resp = await client.get(_url("/languages")) resp.raise_for_status() - rows = resp.json() - except (httpx.HTTPError, ValueError) as exc: + codes = resp.json()["installed"] + except (httpx.HTTPError, ValueError, KeyError, TypeError) as exc: logger.info("translation languages unavailable: %s", exc) return [] - found = [Language(code=str(r["code"]), name=str(r["name"])) for r in rows if "code" in r] + found = [Language(code=str(c), name=str(c)) for c in codes] _languages = (time.monotonic(), found) return found +def forget_languages() -> None: + """After an install or removal: ask the service again.""" + global _languages + _languages = (0.0, []) + + +async def status() -> dict[str, object]: + """The service's own account (admin panel): installed, installing (bytes + done of total), available from Mozilla, and why an install failed.""" + async with httpx.AsyncClient(timeout=httpx.Timeout(20.0, connect=3.0)) as client: + try: + resp = await client.get(_url("/languages")) + resp.raise_for_status() + data = resp.json() + except (httpx.HTTPError, ValueError) as exc: + raise TranslationError(f"the translation service is unreachable: {exc}") from exc + return data if isinstance(data, dict) else {} + + +async def install(code: str) -> None: + await _call("POST", f"/languages/{code}") + forget_languages() + + +async def remove(code: str) -> None: + await _call("DELETE", f"/languages/{code}") + forget_languages() + + _CJK = re.compile(r"[\u3040-\u30ff\u3400-\u9fff\uac00-\ud7af\uf900-\ufaff]") MIN_LETTERS = 25 # fewer, and the detector guesses ("Jacqueline Lian" → Esperanto) MAX_DETECT = 60 # pieces of one message asked about MIN_SHARE = 0.15 # of a message's letters, for its language to count +async def _detect_many(texts: list[str]) -> list[str]: + """The language of each of ``texts`` ('' where unsure), in one request.""" + if not texts: + return [] + result = await _post("/detect", {"q": [t[:DETECT_CHARS] for t in texts]}) + if not isinstance(result, list) or len(result) != len(texts): + return [""] * len(texts) + return [ + str(r.get("language", "")) + if isinstance(r, dict) and float(r.get("confidence", 0)) >= MIN_CONFIDENCE + else "" + for r in result + ] + + async def _detect_one(text: str) -> str: - result = await _post("/detect", {"q": text[:DETECT_CHARS]}) - if not isinstance(result, list) or not result: - return "" - best = result[0] - if not isinstance(best, dict) or float(best.get("confidence", 0)) < MIN_CONFIDENCE: - return "" - return str(best.get("language", "")) + return (await _detect_many([text]))[0] def _weight(text: str) -> int: @@ -126,13 +172,8 @@ async def languages_of(parts: list[str]) -> list[str]: for i, part in enumerate(parts) if _CJK.search(part) or sum(c.isalpha() for c in part) >= MIN_LETTERS ][:MAX_DETECT] - gate = asyncio.Semaphore(4) - - async def one(i: int) -> None: - async with gate: - told[i] = await _detect_one(parts[i]) - - await asyncio.gather(*(one(i) for i in asked)) + for i, language in zip(asked, await _detect_many([parts[i] for i in asked]), strict=True): + told[i] = language out = list(told) following = "" for i in reversed(range(len(parts))): @@ -178,47 +219,163 @@ def _base(code: str) -> str: return code.lower().split("-")[0] -async def _translate_paragraphs(html: str, target: str) -> str: - """``html`` in more than one language, in ``target``: each paragraph from - its own language; paragraphs already in ``target`` are left as they are.""" +async def _texts(texts: list[str], source: str, target: str) -> list[str]: + """``texts`` translated, in batches.""" + out: list[str] = [] + for start in range(0, len(texts), 100): + batch = texts[start : start + 100] + result = await _post("/translate", {"q": batch, "source": source, "target": target}) + got = result.get("translatedText") if isinstance(result, dict) else None + if not isinstance(got, list) or len(got) != len(batch): + raise TranslationError("the translation service sent no translation") + out += [str(x) for x in got] + return out + + +def _kept(name: str, translated: str) -> bool: + """Whether ``name`` came through a translation: each of its words still + there, give or take an ending ("Lukas" → "Lukasie": yes; "Milan" → + "Mediolan": no).""" + return all( + word[: max(3, len(word) - 2)] in translated for word in name.split() if len(word) >= 2 + ) + + +# A capitalised word that starts a sentence ("Enclosed is a summary…"). +_SENTENCE_START = re.compile(r"(?:^|(?<=[.!?…]\s))(?P[^\W\d_][^\W\d_'’-]{2,})\b") + + +def _copied_starts(core: str, text: str, names: list[str]) -> list[str]: + """Words that start a sentence of ``core`` and came through ``text`` + untranslated, and are no known name: the models take a capitalised + sentence start for a name ("Enclosed is…" → "Enclosed jest…").""" + found: list[str] = [] + for m in _SENTENCE_START.finditer(core): + word = m.group("word") + if not word[0].isupper() or word.isupper() or any(word in n for n in names): + continue + if re.search(rf"(? str: + """``core`` with ``words`` lower-cased where they start a sentence.""" + return _SENTENCE_START.sub( + lambda m: m.group(0).lower() if m.group("word") in words else m.group(0), core + ) + + +def _capitalised(text: str) -> str: + """Sentence starts capitalised again (after ``_lowered``): the first + letter, and one after a sentence's end — a full stop after a word of four + letters or more, so not after "r.", "np.", "tzw." (abbreviations).""" + text = re.sub(r"^(\W*)([^\W\d_])", lambda m: m.group(1) + m.group(2).upper(), text) + return re.sub( + r"((?:[^\W\d_]{4,}[.…]|[!?])\s+)([^\W\d_])", + lambda m: m.group(1) + m.group(2).upper(), + text, + ) + + +def _as_before(text: str, first: str) -> str: + """A word after a comma capitalised in ``text`` but not in ``first`` (the + first try) is lower-cased: lower-cased input makes the models capitalise + elsewhere ("…2026, od 13 lipca" → "…2026 r., Od 13 lipca").""" + + def repl(m: re.Match[str]) -> str: + word = m.group(2) + lower = word[0].lower() + word[1:] + return m.group(1) + (lower if f", {lower}" in first else word) + + return re.sub(r"(,\s+)([^\W\d_][\w'’-]*)", repl, text) + + +async def _with_names_kept(core: str, names: list[str], source: str, target: str) -> str: + """``core`` translated around ``names``, which are left as they are.""" + pattern = re.compile("(" + "|".join(re.escape(n) for n in names) + ")") + parts = pattern.split(core) + todo = [ + (i, m.groups()) + for i, part in enumerate(parts) + if i % 2 == 0 and (m := _EDGE_SPACE.match(part)) and any(c.isalnum() for c in part) + ] + done = await _texts([groups[1] for _, groups in todo], source, target) + for (i, (lead, _, trail)), text in zip(todo, done, strict=True): + parts[i] = f"{lead}{text}{trail}" + return "".join(parts) + + +async def _translate_paragraphs( + html: str, target: str, sources: list[str], names: Iterable[str] = () +) -> str: + """``html`` in ``target``, as plain text paragraph by paragraph: from the + one language in ``sources``, or (several, or none known) each paragraph + from its own. Paragraphs already in ``target``, or in a language that is + not installed, are left as they are. + + ``names`` go along with their sentence, which then translates as a whole + (kept apart, "I came across " lost its verb: "Przyszedłem + Imaginary Pixels"). Only a piece where one came out changed ("Milan" → + "Mediolan") is translated again with it left as it is.""" + watch = sorted({n.strip() for n in names if len(n.strip()) >= 2}, key=len, reverse=True) pieces, paragraphs = prep.split_pieces(html) - found = await languages_of([p.text for p in paragraphs]) + if len(sources) == 1: + found = [sources[0]] * len(paragraphs) + else: + found = await languages_of([p.text for p in paragraphs]) + installed = {x.code for x in await languages()} # Chinese and Japanese go without spaces: two pieces of one sentence # ("…奎琳,我从…") need one between them once translated. followed = {i for p in paragraphs for i in p.pieces[:-1]} by_language: dict[str, list[int]] = {} for paragraph, language in zip(paragraphs, found, strict=True): - if language and _base(language) != _base(target): + if language in installed and _base(language) != _base(target): by_language.setdefault(language, []).extend(paragraph.pieces) for language, where in by_language.items(): todo: list[tuple[int, str, str, str]] = [] for i in where: m = _EDGE_SPACE.match(html_lib.unescape(pieces[i])) lead, core, trail = m.groups() if m else ("", "", "") + # A list marker stays out: the models repeat it ("1. Thursday" → + # "1. 1. Czwartek"). + marker = _LIST_MARKER.match(core) + if marker: + lead, core = lead + marker.group(0), core[marker.end() :] if not trail and i in followed and _base(language) in _NO_SPACES: trail = " " if any(c.isalnum() for c in core): todo.append((i, lead, core, trail)) - for start in range(0, len(todo), 100): - batch = todo[start : start + 100] - result = await _post( - "/translate", - {"q": [core for _, _, core, _ in batch], "source": language, "target": target}, + done = await _texts([core for _, _, core, _ in todo], language, target) + # Sentence starts that came through untranslated: once more, lower-case. + again = [ + (k, words) + for k, ((_, _, core, _), text) in enumerate(zip(todo, done, strict=True)) + if (words := _copied_starts(core, text, watch)) + ] + if again: + retried = await _texts( + [_lowered(todo[k][2], words) for k, words in again], language, target ) - out = result.get("translatedText") if isinstance(result, dict) else None - if not isinstance(out, list) or len(out) != len(batch): - raise TranslationError("the translation service sent no translation") - for (i, lead, _, trail), text in zip(batch, out, strict=True): - pieces[i] = html_lib.escape(f"{lead}{text}{trail}", quote=False) + for (k, words), text in zip(again, retried, strict=True): + # Still there: a name after all, and the first try stands. + if len(_copied_starts(todo[k][2], text, watch)) < len(words): + done[k] = _as_before(_capitalised(text), done[k]) + for (i, lead, core, trail), text in zip(todo, done, strict=True): + lost = [n for n in watch if n in core and not _kept(n, text)] + if lost: + text = await _with_names_kept(core, lost, language, target) + pieces[i] = html_lib.escape(f"{lead}{text}{trail}", quote=False) return "".join(pieces) _NO_SPACES = {"zh", "ja", "th"} +_LIST_MARKER = re.compile(r"(?:\d{1,3}[.)]|[a-zA-Z][.)]|[-•*–])\s+(?=\S)") _EDGE_SPACE = re.compile(r"^(\s*)(.*?)(\s*)$", re.DOTALL) # Web and mail addresses in the text: the models break them up ("https: / / -# vimeo.com"), so they are marked translate="no", which LibreTranslate keeps. +# vimeo.com"), so they are marked translate="no" and not sent. _ADDRESS = re.compile(r"(?:https?://|www\.)[^\s<>\"']+|[\w.+-]+@[\w-]+(?:\.[\w-]+)+", re.IGNORECASE) _TAG = re.compile(r"(<[^>]*>)") @@ -243,36 +400,9 @@ def protect_addresses(html: str) -> str: return "".join(out) -_PUNCT = re.compile(r"[\W_]+") -MAX_NAMES = 200 # names checked for one message - - -async def names_to_keep(names: Iterable[str], source: str, target: str) -> set[str]: - """Of ``names``, those the model changes when it translates them alone - ("Milan" → "Mediolan", "Imaginary Pixels" → "Imaginanal Pixels"); one - request for them all. If the check fails, all of them.""" - wanted = sorted({n.strip() for n in names if len(n.strip()) >= 2})[:MAX_NAMES] - if not wanted: - return set() - try: - result = await _post( - "/translate", {"q": wanted, "source": source or "auto", "target": target} - ) - except TranslationError: - return set(wanted) - out = result.get("translatedText") if isinstance(result, dict) else None - if not isinstance(out, list) or len(out) != len(wanted): - return set(wanted) - - def same(a: str, b: str) -> bool: - return _PUNCT.sub("", a).casefold() == _PUNCT.sub("", b).casefold() - - return {name for name, got in zip(wanted, out, strict=True) if not same(name, str(got))} - - # Bumped when what goes to the service, or what is done with the result, # changes: translations kept from before are made again. -PIPELINE_VERSION = 5 +PIPELINE_VERSION = 8 async def translate_html( @@ -288,16 +418,6 @@ async def translate_html( prepared = prep.join_wrapped_lines(html) for language in sources: prepared = prep.translate_phrases(prepared, language, target) - single = sources[0] if len(sources) == 1 else "auto" - candidates = {*names, *prep.find_names(prepared, single if sources else "")} - terms = {*keep, *await names_to_keep(candidates, single, target)} - prepared = prep.keep_symbols(protect_addresses(prep.keep_terms(prepared, terms))) - if len(sources) > 1: - return prep.tidy(await _translate_paragraphs(prepared, target)) - result = await _post( - "/translate", - {"q": prepared, "source": source or "auto", "target": target, "format": "html"}, - ) - if not isinstance(result, dict) or "translatedText" not in result: - raise TranslationError("the translation service sent no translation") - return prep.tidy(str(result["translatedText"])) + candidates = {*names, *prep.find_names(prepared, sources[0] if sources else "")} + prepared = prep.keep_symbols(protect_addresses(prep.keep_terms(prepared, keep))) + return prep.tidy(await _translate_paragraphs(prepared, target, sources, candidates)) diff --git a/backend/app/translation_prep.py b/backend/app/translation_prep.py index aff7cf1..256e7d3 100644 --- a/backend/app/translation_prep.py +++ b/backend/app/translation_prep.py @@ -1,25 +1,25 @@ """Getting a message ready for translation, and tidying what comes back. -LibreTranslate translates each piece of text between tags on its own, and its -small models stumble on things a reader would not: a sentence wrapped onto two -lines becomes two sentences ("ask you about" / "Plan for next month"), -"Best," becomes "Najlepsze," and a company name gets translated word by word. -So before a message goes out: +A message is translated as plain text, piece by piece (the text between +tags), and the models stumble on things a reader would not: a sentence +wrapped onto two lines becomes two sentences ("ask you about" / "Plan for next +month"), "Best," becomes "Najlepsze," and a company name gets translated word +by word. So before a message goes out: - lines that carry on a sentence (no end punctuation, next starts lowercase) are joined, in HTML (
, one
per line) and in wrapped plain text; - greetings and sign-offs that make a whole line are put in the target language from a small phrase table (English, Polish, German); -- names are marked translate="no", which LibreTranslate keeps — the people - on the message, the user's own names and signature names, and capitalised - multi-word names mid-sentence — but only the names the model would change - ("Milan" → "Mediolan"): a kept name splits its sentence, and LibreTranslate - translates each piece alone ("Dear Lukas Remis," → - "Kochanie."), so a name the model leaves alone stays in the sentence; +- names are marked translate="no" and not sent — the people on the message, + the user's own names and signature names, and capitalised multi-word names + mid-sentence — but only the names the model would change ("Milan" → + "Mediolan"): a kept name splits its sentence into pieces translated alone + ("Dear Lukas Remis," → "Kochanie."), so a name the model + leaves alone stays in the sentence; - so are emoji, file names ("A_0023C017_p1CAC.mov") and text without a letter; -- a message in more than one language is translated paragraph by paragraph, - each from its own language (split_pieces); those in the target stay; -- and afterwards, the space LibreTranslate drops after a kept word comes back. +- the message is translated paragraph by paragraph (split_pieces), each from + its own language when there are several; those in the target stay; +- and afterwards, a kept word gets back the spaces around it. """ from __future__ import annotations @@ -315,7 +315,7 @@ def split_pieces(html: str) -> tuple[list[str], list[Paragraph]]: # --- after translation ------------------------------------------------------------- -# LibreTranslate drops the space after a kept span ("Brutoni twoje"). +# A kept span can lose the space beside it ("Brutoni twoje"). # Only kept spans: they still carry translate="no" here (the sanitizer that # runs next takes it off); other spans are the sender's and stay as they are. _KEPT = re.compile(r'([^<]*)') diff --git a/backend/tests/api/test_admin_translation.py b/backend/tests/api/test_admin_translation.py new file mode 100644 index 0000000..9dd1b0c --- /dev/null +++ b/backend/tests/api/test_admin_translation.py @@ -0,0 +1,81 @@ +"""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": {}, + } diff --git a/backend/tests/test_translation_text.py b/backend/tests/test_translation_text.py index 28d6b96..e500d7d 100644 --- a/backend/tests/test_translation_text.py +++ b/backend/tests/test_translation_text.py @@ -5,7 +5,7 @@ from __future__ import annotations import pytest from app import translation -from app.translation import detect, languages_of, names_to_keep, protect_addresses, translate_html +from app.translation import detect, languages_of, protect_addresses, translate_html def test_addresses_in_text_are_kept_from_translation() -> None: @@ -25,45 +25,55 @@ def test_style_blocks_are_left_alone() -> None: assert protect_addresses(html) == html -async def test_only_names_the_model_changes_are_kept(monkeypatch: pytest.MonkeyPatch) -> None: - sent: list[object] = [] +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,", + } 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] + 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")] monkeypatch.setattr(translation, "_post", post) - kept = await names_to_keep( - ["Milan", "Lukas Remis", "Imaginary Pixels", "Milan", "x"], "en", "pl" + monkeypatch.setattr(translation, "languages", installed) + html = "

I came across Imaginary Pixels today.

Dear Lukas,

Regards, Milan

" + out = await translate_html(html, "en", "pl", names=["Imaginary Pixels", "Lukas", "Milan"]) + assert out == ( + "

Natknąłem się dziś na Imaginary Pixels.

Drogi Lukasie,

" + "

Pozdrawiam, Milan

" ) - 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"} + assert sent[1:] == [["Regards,"]] # only the piece that lost a name went again 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: + def 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) + 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) return asked @@ -102,7 +112,7 @@ async def test_each_paragraph_from_its_own_language(monkeypatch: pytest.MonkeyPa 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": [f"PL({x.upper()})" for x in q]} return {"translatedText": list(q) if isinstance(q, list) else q} # names: unchanged monkeypatch.setattr(translation, "_post", post) @@ -117,7 +127,84 @@ async def test_each_paragraph_from_its_own_language(monkeypatch: pytest.MonkeyPa assert "
Widzę, że złożyłeś wniosek o otwarcie sklepu & więcej.
" in out # Two pieces of one Chinese sentence get a space between them. assert "PL(我是速卖通的官方客户经理奎琳,) PL(" in out - assert "PL(I noticed that you applied to join the platform & more.)" in out.replace( + assert "PL(I NOTICED THAT YOU APPLIED TO JOIN THE PLATFORM & MORE.)" in out.replace( "&", "&" ) assert '+48 780 549 877' in out + + +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 = "

Hello there, how are you?

Short one.

1. Thursday 2nd July

" + out = await translate_html(html, "en", "pl") + assert out == ( + "

PL(HELLO THERE, HOW ARE YOU?)

PL(SHORT ONE.)

" + "

1. PL(THURSDAY 2ND JULY)

" # 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("

Jó napot kívánok

", "hu", "pl") == "

Jó napot kívánok

" + 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("

Enclosed is a summary. Natalia wrote it.

", "en", "pl") + assert out == "

Załączone jest podsumowanie. Natalia to napisała.

" + # A name comes back the same lower-case too: the first try stands. + sent.clear() + assert await translate_html("

Natalia wrote it.

", "en", "pl") == ( + "

Natalia to napisała.

" + ) + 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." + ) diff --git a/deploy/build-push.sh b/deploy/build-push.sh index 6f64278..568bafe 100755 --- a/deploy/build-push.sh +++ b/deploy/build-push.sh @@ -43,15 +43,17 @@ build() { "$ROOT_DIR" } -build backend Dockerfile.backend -build web Dockerfile.frontend +build backend Dockerfile.backend +build web Dockerfile.frontend +build translator Dockerfile.translator if [ "$MODE" = "--save" ]; then out="${ROOT_DIR}/webmail-${TAG}.tar" - echo "== saving both images to ${out}" + echo "== saving the images to ${out}" docker save -o "$out" \ "${REGISTRY_PREFIX}/backend:${TAG}" \ - "${REGISTRY_PREFIX}/web:${TAG}" + "${REGISTRY_PREFIX}/web:${TAG}" \ + "${REGISTRY_PREFIX}/translator:${TAG}" echo "done. Copy it to the NAS and run: docker load -i $(basename "$out")" exit 0 fi @@ -72,6 +74,8 @@ fi echo "== pushing" docker push "${REGISTRY_PREFIX}/backend:${TAG}" docker push "${REGISTRY_PREFIX}/web:${TAG}" +docker push "${REGISTRY_PREFIX}/translator:${TAG}" docker push "${REGISTRY_PREFIX}/backend:latest" docker push "${REGISTRY_PREFIX}/web:latest" +docker push "${REGISTRY_PREFIX}/translator:latest" echo "pushed ${TAG}. On the NAS: ./deploy/webmail.sh update ${TAG}" diff --git a/deploy/webmail.sh b/deploy/webmail.sh index 0aefc16..3cb407b 100755 --- a/deploy/webmail.sh +++ b/deploy/webmail.sh @@ -436,6 +436,24 @@ print('OFF' if not url else urllib.request.urlopen(url + '/v2/languages', timeou printf ' %s\n' "$lt" ;; esac + # Email translation: is the translator answering, with which languages? + tr="$(compose exec -T api python -c " +import json, os, urllib.request +url = os.environ.get('TRANSLATOR_URL', '') +if not url: + print('OFF') +else: + d = json.load(urllib.request.urlopen(url + '/languages', timeout=5)) + busy = ', '.join(d.get('installing') or {}) + print('OK ' + ' '.join(d['installed']) + (' (installing: ' + busy + ')' if busy else '')) +" 2>&1 | tail -1)" || true + case "$tr" in + OK*) grn " translation answering; languages: ${tr#OK }" ;; + OFF) ylw " translation is off (TRANSLATOR_URL is empty)" ;; + *) ylw " translation not answering — the Translate button stays hidden" + printf ' %s\n' "$tr" ;; + esac + if command -v ss >/dev/null 2>&1; then local p for p in "WEB_PORT:${WEB_PORT:-8088}" "ADMIN_PORT:${ADMIN_PORT:-8089}"; do @@ -503,7 +521,7 @@ cmd_pull() { Set REGISTRY_PREFIX and IMAGE_TAG in ${ENV_FILE}, or use: webmail.sh up --build" detect_compose IMAGE_TAG="$tag" compose pull || die "pull failed — is the registry reachable and logged in?" - grn "pulled ${REGISTRY_PREFIX}/{backend,web}:${tag}. Next: ./deploy/webmail.sh up" + grn "pulled ${REGISTRY_PREFIX}/{backend,web,translator}:${tag}. Next: ./deploy/webmail.sh up" } cmd_down() { require_env; compose --profile meili down; grn "stopped (data kept in ${DATA_ROOT})"; } @@ -587,6 +605,10 @@ cmd_update() { # then the API, then the web front end. IMAGE_TAG="$target" compose up -d worker-sync worker-jobs IMAGE_TAG="$target" compose up -d api web + # The translator is ours too, so it follows the tag. Named here (not left + # to --no-recreate below) so it is replaced on every update, and so init, + # which it depends on, runs again when a release adds a data directory. + IMAGE_TAG="$target" compose up -d translator # Converge anything the ordered roll does not name — a release that adds a # service (the antispam engine did) would otherwise never start it, and the # stack would look updated while missing a container. --no-recreate so this diff --git a/docker-compose.yml b/docker-compose.yml index 2147137..1e0f706 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -72,7 +72,7 @@ x-backend-env: &backend-env # Spelling and grammar check in the compose window. Empty turns it off. LANGUAGETOOL_URL: ${LANGUAGETOOL_URL-http://languagetool:8010} # Email translation; empty turns it off. - LIBRETRANSLATE_URL: ${LIBRETRANSLATE_URL-http://libretranslate:5000} + TRANSLATOR_URL: ${TRANSLATOR_URL-http://translator:5000} LOG_LEVEL: ${LOG_LEVEL:-INFO} UNDO_SECONDS: ${UNDO_SECONDS:-10} SESSION_TTL_DAYS: ${SESSION_TTL_DAYS:-30} @@ -111,13 +111,13 @@ services: - | set -e mkdir -p /mnt/postgres /mnt/blobs /mnt/backups /mnt/redis /mnt/meili /mnt/state \ - /mnt/languagetool-ngrams /mnt/libretranslate + /mnt/languagetool-ngrams /mnt/translator # Only recurse when ownership is actually wrong: `chown -R` over a large # blob store on every start would be minutes of pointless disk churn. [ "$$(stat -c %u /mnt/blobs)" = "10001" ] || chown -R 10001:10001 /mnt/blobs [ "$$(stat -c %u /mnt/backups)" = "999" ] || chown -R 999:999 /mnt/backups [ "$$(stat -c %u /mnt/redis)" = "999" ] || chown -R 999:999 /mnt/redis - [ "$$(stat -c %u /mnt/libretranslate)" = "1032" ] || chown -R 1032:65534 /mnt/libretranslate + [ "$$(stat -c %u /mnt/translator)" = "10001" ] || chown -R 10001:10001 /mnt/translator chmod 700 /mnt/backups echo "[init] data directories ready" volumes: @@ -316,30 +316,26 @@ services: cpus: ${LANGUAGETOOL_CPUS:-1.0} logging: *logging - # Email translation (app/translation.py), all on this server. The first - # start downloads every language model (a few GB) into DATA_ROOT; later - # starts reuse them. A model is loaded into memory the first time its - # language is used. - libretranslate: - image: libretranslate/libretranslate:v1.9.6 + # Email translation (translator/, app/translation.py), all on this server: + # Mozilla's Firefox Translations models and fastText language detection. + # The first start downloads the default languages (about 350 MB) into + # DATA_ROOT; more are installed from the admin panel. + translator: + image: ${REGISTRY_PREFIX:-webmail}/translator:${IMAGE_TAG:-local} + build: + context: . + dockerfile: docker/Dockerfile.translator environment: - LT_DISABLE_WEB_UI: "true" - LT_UPDATE_MODELS: "false" - LT_THREADS: ${LIBRETRANSLATE_THREADS:-2} + TRANSLATOR_LANGUAGES: ${TRANSLATOR_LANGUAGES:-pl,de,fr,es,it} + TRANSLATOR_THREADS: ${TRANSLATOR_THREADS:-2} TZ: ${TZ:-UTC} volumes: - - ${DATA_ROOT:-/NO-.env-FOUND-RUN-deploy_webmail.sh-init-FIRST}/libretranslate:/home/libretranslate/.local - healthcheck: - test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:5000/languages', timeout=4)\""] - interval: 30s - timeout: 6s - retries: 5 - start_period: 30m # the first start downloads the models + - ${DATA_ROOT:-/NO-.env-FOUND-RUN-deploy_webmail.sh-init-FIRST}/translator:/data depends_on: init: { condition: service_completed_successfully } restart: unless-stopped - mem_limit: ${LIBRETRANSLATE_MEM_LIMIT:-6g} - cpus: ${LIBRETRANSLATE_CPUS:-2.0} + mem_limit: ${TRANSLATOR_MEM_LIMIT:-2g} + cpus: ${TRANSLATOR_CPUS:-2.0} logging: *logging # Bayes tokens and fuzzy hashes. Separate from the app's redis on purpose diff --git a/docker/Dockerfile.translator b/docker/Dockerfile.translator new file mode 100644 index 0000000..5fccf76 --- /dev/null +++ b/docker/Dockerfile.translator @@ -0,0 +1,38 @@ +# Translation service (translator/translator.py): Mozilla's Firefox +# Translations models on the Bergamot engine, and fastText language +# identification. Python 3.10: the newest the bergamot package is built for. +# +# docker build -f docker/Dockerfile.translator -t webmail/translator . +# +# The models are not in the image: they are downloaded into /data (a bind +# mount) on the first start and from the admin panel. + +FROM python:3.10-slim + +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_NO_CACHE_DIR=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + DATA_DIR=/data \ + HOME=/tmp + +COPY translator/requirements.txt /app/requirements.txt +RUN pip install -r /app/requirements.txt + +# fastText's language identifier (lid.176, compressed; CC BY-SA 3.0). +ADD --checksum=sha256:8f3472cfe8738a7b6099e8e999c3cbfae0dcd15696aac7d7738a8039db603e83 \ + https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.ftz /opt/lid.176.ftz + +COPY translator/translator.py /app/translator.py + +# The same uid as the backend: DATA_ROOT/translator is owned by it (init). +RUN useradd --uid 10001 --no-create-home --shell /usr/sbin/nologin translator \ + && mkdir -p /data && chown translator /data /opt/lid.176.ftz +USER translator +WORKDIR /app +EXPOSE 5000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=5 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:5000/health', timeout=4)" + +CMD ["uvicorn", "translator:app", "--host", "0.0.0.0", "--port", "5000", "--no-access-log"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1593809..67ae976 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -158,22 +158,18 @@ services: retries: 12 restart: unless-stopped - # Email translation (app/translation.py). The first start downloads every - # language model (several GB) into the volume; later starts reuse them. - libretranslate: - image: libretranslate/libretranslate:v1.9.6 - environment: - LT_DISABLE_WEB_UI: "true" - LT_UPDATE_MODELS: "false" + # Email translation (translator/, app/translation.py): Firefox Translations + # models and fastText. The first start downloads the default languages + # (about 350 MB) into the volume; the admin panel adds more. + translator: + build: + context: .. + dockerfile: docker/Dockerfile.translator + image: webmail/translator:dev ports: - "127.0.0.1:5005:5000" volumes: - - ltmodels:/home/libretranslate/.local - healthcheck: - test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:5000/languages', timeout=4)\""] - interval: 15s - timeout: 6s - retries: 240 + - translatordata:/data restart: unless-stopped greenmail: @@ -188,7 +184,7 @@ services: - "127.0.0.1:3993:3993" # IMAPS volumes: - ltmodels: + translatordata: pgdata: meilidata: blobs: diff --git a/docs/nas-install.md b/docs/nas-install.md index 944a1c9..ee6f37a 100644 --- a/docs/nas-install.md +++ b/docs/nas-install.md @@ -701,6 +701,23 @@ priority, so even the first checks after a restart are served from RAM. On mechanical disks that warm-up takes several minutes; an NVMe cache in front of the volume makes it — and any later cache misses — fast. +### Email translation (translator) + +`translator` translates email in the reading pane, entirely on this server: +Mozilla's Firefox Translations models (the ones Firefox uses for page +translation) and fastText for telling which language a message is in. + +- The first start downloads the languages in `TRANSLATOR_LANGUAGES` (default + `pl,de,fr,es,it`; English is always there) into `$DATA_ROOT/translator`, + about 35 MB per language and direction (~350 MB for the default set). +- More languages are installed, and unused ones removed, from the **admin + panel → Translation**. That list is kept in `$DATA_ROOT/translator`, so a + restart does not undo it. +- Models translate to and from English; other pairs go through English. +- Capped at 2 GB of RAM and 2 CPUs (`TRANSLATOR_MEM_LIMIT`, `TRANSLATOR_CPUS`). +- `./deploy/webmail.sh status` shows whether it answers and which languages + are installed. `TRANSLATOR_URL=` (empty) in `.env` turns translation off. + Two NAS-specific realities worth knowing: - **Disk hibernation won't happen.** Active accounts hold IMAP IDLE connections diff --git a/frontend/icons/ultimo-logo-signet.svg b/frontend/icons/ultimo-logo-signet.svg index f5ff865..ae125ed 100644 --- a/frontend/icons/ultimo-logo-signet.svg +++ b/frontend/icons/ultimo-logo-signet.svg @@ -8,9 +8,31 @@ version="1.1" id="svg1" xml:space="preserve" + sodipodi:docname="ultimo-logo-signet.svg" + inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"> + gradientTransform="matrix(0.75684298,0,0,0.80151105,3.0076311,94.135157)" /> diff --git a/frontend/icons/ultimo-logo.svg b/frontend/icons/ultimo-logo.svg index 4d24abe..a62679f 100644 --- a/frontend/icons/ultimo-logo.svg +++ b/frontend/icons/ultimo-logo.svg @@ -8,10 +8,48 @@ version="1.1" id="svg1" xml:space="preserve" + sodipodi:docname="ultimo-logo.svg" + inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"> + gradientTransform="matrix(0.75684298,0,0,0.80151105,3.0076311,94.135157)" />ltimo diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png index 648f6a88f9213021d2521125b85381b8147dd8f0..fb785d5ca3f69be2458676f2b928b4dd958c34b0 100644 GIT binary patch literal 6646 zcmds6^;;9r*Pn_4A}~ZLK^%&JbUIqPTbeOKN@B!D$5)tylp>vylNul;qXg-mba%(3 zbL_?MZ|@K9Kk(k?x%b>U_niAYpL5QQ2yIOz^7{<;0RRBGin6@!4Xyf@fwynUC25V9 zH-yMeMneVwsE8%Ivb=lKerBz#s{sJ`-i-YR2>@K&bp2-o06_2o09zISfLJmBK0 z(UG_rxM!uNBv1HnA~ialx#=N>t7s??Z{2@L4d(hAHSr4opu13!mwD?wwL9zM{Yatp z-ru6F&6Mty7jGWMb!7lR&+e@xngY!%si=q`1`16!l4FQ=#Q31K^dLgN0dlttLfP=f zSZ5MpS*OEeBYOKbODN}u&yk<|;D!M~LK=fFZq>)p``(+IKb+7?N2V0*1$mpcJ_E$Tu9`*(o(iO= zMOt1WP?lY5Zv5gEYwhkg%cLJn=W-}`Zqo~x=ci%TShxL7P73DX;@9}q(TIzN*;-yS z4O`JXFUNC5?RcOpNx$AnJ>^ipanf;0E@L+=fZT@O*@UvLXEUc$3syNbXjPnYJ|~ip*xZ=Mfy-T~ zVG??a!1ERL3O*sR?E5IPptZ1f+|H{>`WAh;aSls8kUFxSALZ5?p;|7QOIAxmRXHTB z!~3mB(e8h|TSNPK17?o8_%_5(d7unKCnYap8&=`{IZS1f_+#43+tYw!u#`K#|*Ufzg339O8i#q+}Oe-p1(0| z->MLE((dtYYsu$d|ldOAOKDz4y(or4G=W^r%z5#>{xn2viyVH$zv`aQ7(K99-z zFP9Ja%k6)UbCZf^C+&G*ny?Dc>h(q5vWS_>$opS-JGKdv$78hi`;);7N*^NqWaH`(efr*r-n_7!2%7eRM8E8kRH?9*bpNbq* z%reH2y$a#L_|Wr83c~<{hKWrEC2CL?xA3xBr@~c8j6NlI)9=lxfBH!lrW*L=WkWZ7 z%@{%HoT?L2#b4)oa2}XetQ-*3`Wh|I0g8X68sgThCS4%83lFfq6P$~ULf>CUYMYp* zB`X&ew6_Z%LqNK8)=@xx{YRiE0DtW>$`&jULaUyd?IAU3A=+iEBzrzs&x}U_)V8ei z$oD=7sr{)iHu~d&{fq6DN=@lp`km9xPCXOz%r7$Gz+~mU2f6x-$3?F|9U^DSGJ1} zi9zHUp|V7`U0&xa#jLCVC5KvJJU6dI5?|yXc5LNUc_y;KVQqxPBfT$kHXc zfHz;!JDC6_6xl|H0(DKMYPj>2VryVp5=-N%mb0gA2DZICpS0AxHFZpC)YV_e>jW#s z_gTMtN>%*T>SN&SDN#L;Q6z!|g9Hw62-l90+O;Wdc!xb^mBE47ZK8mxoSehMqfhTN z2e?YrO$&2x?hSc$oT!_owt1CPj;2fPP2=+YhV9^Vww~&JlCT{|kMgOg2g`Ju7xeQ% zw;-!WDKt-jj5S!NL!O8_;gH{$0^>3xxqL*~yAH?n%0^l^&Lib=Gvb~3Ej8t)9Q=0- zF4lP?F-AYJ-LA+jw=9ck@Z>l>aQNNQ8rkG_Hh1{KUy|t9g+ylSF&(mt_-m(z;dSGG zh*?d0O>)i#4`0a?`m`^TdV*-Ht= zR*5$POKt?xOf7vInPD&#Dkgt86G`C=Hbw0fh8}k-6@4E}`kY#5E-H1HPla}=%k7Y) zA~!s zo-?4&?ShKc_a@v{%*K~UUpKg?+wbqua5I3pNb|yHfiq2lN=bdE{LqTvWpz`2GHb?@ z12AHU#62inXhgY5G%hG zGDF5@*mUsZ0X}!||To?1}ncbBR*!Be4_1;&wZp zM(1dFvZ>=QF}OwjK#eqH_ek^~21Oz)5wOnGehXPy;<;cbsSh#LRRa>(r$qUAxkjpi zg(Od7>$Asnco{Z;C`y2>cC*RuGzSo$kyli@Y;GQ4b2^iR%TDW|v zD1?l$Pws4`qY;*Z=rC}Na$O*Y*Ny(+{#}e4vIZPXtkmzADjXJaIOLNzwh;!D)mE%@ z5Im1#e~aJ{FZ*_?rF;3{`n+H3&kLRQ%;?aXkhWjzXEViNgtCjxw*=14LN*R5$$U4Z zk1n8UC-=V)&Pp|_r)1k38yG}GRMr#r=?r_fiI9cQ?wDf~#@81(foeSy6WV$-3Po)NGl!xR;2*0)Qt$M6$f7mCn*Jejb|j7bM3csFWH zkxFG(vh@RN*T#_pd!>Gd`XrUJI?e4LNG(pDft}ueZgeZM61P`*CvW7O2y z9uh{M*zF<~xmgixaWouz5yLc+(jBARK|MBF!f)3qD{S-x_HghBv5Qb8b90qtK~vA} zPhFPATK=kWqTu&4`ut*C7#9gM*Z{R~V8I9-%_u^_;rj}appvt!>jWH=l1nZe7jmsX zr0t;1)=u&joXQY7<{J`4|9x5fsv>F1&byvam->>F)^B46u0DbCaZ1h!gg1I$OKquW zYD6yF9`OGNeiOfdht0%GLXut_)gISg1a~DI&jhFB)))^Kjo1vZRA;t6$XGM;y2nw= zf6U3nS`hyO#*-qlYCD>QVXFu8d0wx^l0PED8;&CVQon{t$tPh#DXv@&?%8bSGZjGP zNhF{23bRGfK7I{q2>Sbg-ccl47-QM^P^|NHJl%gv$~RFaj0ka#?AOp}!s{@# z=wTzqTytH7r>a(QeGPMb!8N|XW^i7kS#2xn3q#F^!3s2??bsEiesOvS$DyX=*--fH zIz6$=GB(ahM94cKdch^KLO3>j^b1ISm_aJl+n}9?D9rhIe3>KzVCzg;5vnGabV!=v z;5+8{Iq}PwluSSVI6UdPW36`6)6z}PrR)!DL{c99dmw+n+_0d7@wjD@+3uT!w>lb0 zrd~g{ycDDCbhl#FGi+}g1*roniCZj(Q%o4ggt^aZpnt!Xk2UBNE6C2S@I{}UW{)Y& z?(eYv|dB2M~OBb5ME8McE`l4do2zJQvnDbH8SlDCs zwWEg6EL)1F(XDbWCl;16dv4qsx$6zDZ-kuIW!TH3p32ULzuK0ix3r6~`ZcXK*?<{wZ7G zjF+pDhC0CIq5zan+b#N{9%~57ws|)$>d;9Tz-Vs_=*Es<+)jtEU=;G>z*>06-E#+% z;ww)kh(oDzeGD7aBxoyXTnvI4rsI9Mk+f1?@P$Y=nD)L9EwRL`HH^G@wz~yOzG8*q zq}d(Pujn`FT0fc&wezH7bNj2hk*@dZ6|yzmWvdfoV^x3fAb;)IcmBFa{u?)V<tvqKW2E*sAS@06*zAqP%(FarM+cP1B?(nF>6Dwcao51R$EpJkdV5N zUXPS-6t?^~Ho8t!#@3BI_sqodX$}qP;4cOpx5Pm5^X}&ut2&vnIY6~E_ zY<8%!rj2J@G8^uzXIm3FI)7wM9+M^UzM53ECq&bbLu8??`eQqaCJ(}Oiu~?#VFD#s z54;46)>rgqXI#!BFt+@?db?#KZAaFTJ((h=^tS%&_w!Q=*k$cgMa|xCN6aqX1@8ghNUmsIabfh zFa7c3FdYrKiNMO=IHyA`2eUZokvz2t+#@<(mo2U}J2Zh%7=6jd?fE_#C;7F70 zwc&-px|>mP6~1lvka9|59_uX8ybQ-%-MN_^H@o;lvo_2tLQ)bjT9q*WY3oz!CkE6u zEgrmX#*Cjd&62XYu?`vS<(lHri}ER^8d@>2Nop)ZoMc%XTUXX|_ihmR!5GJNoNj3qm3kh3HOE;M%3%6H{q>HqP|h*vfU_ zjVv`Fqwb7LZQ2}`wfRzT(b?;7a1g7%Pwn45?m5#PI8*MIzlMnhiSNnQ3UTCor)gBw zAO3UWtfTBF8qa;*;nsHU28YGQCev4*0qJO)fA?LF=&jY}GulNw!>tO#X0;h~bh7xn zJ)rDWVDtQ@i6W&85w>7}w2w4geV97+)Te# zd-A7tb6!G;xy+|3R0Ou_@KIFw;n0#jQEczBuE+_#iNN{gtVn*gHpfXr)#Cg~{amWA z)ZoET)8*0rprJhn84scJukwyv`{;RH0}MzX9ddJxTb+Miz1W=1Va51_F#^Vh_2X^w zicsp!sNaFjKYTb;b(nXUmHAeTZMGH>(| zt@@S7K>BioQ}8HQ9~!{aqAx!=vNM_@II2+V?`O?vGR#x0Q80W=A*};3^MB{H@QZA` z;X*AFGO<*z`Q?)9L?&#}IjOG9@2tSj(+ZyJ24vJ=DWcdDHQU8`D-8trwVMar&SI(? z%*jY_jF+!*vlyIPHk+$J>)^lRg8R5?hcCbFM>V2oxzel8cxG$;7gJlToGlto29<-* zmtBpOxa9U9^)`rZgfDa3DogR8{u+lcr$GSY^@W zGtWE=<=P4%&~VzGhi_22;!HNPn(C4Dlld!LkWU$TsD=+g@ZdLe&9#3-q>+vV)Fz*J-6 zS#R5iKPRNYpH~Mv0)7FyGpbW$0hy3`$5&=e#jgpT67n zx?!_wcyB(tX3~aP=mNO7Du{RxvVkEv6N-XHQkPw$J|=q`h@L4G z!BCVaB_3GOWc==Og(BH$! zR8~SAfiYTVB~p3|EUb^&oBw7rP`7%hb#%NP>1Bn-0#(b*9qP=-(VNXrBvZy#j(S>6 zRw(3L<(a;e`wX5wx#KZkY;=M?Tb(*+71DWm{UQboL05LECH?7~Y-maon=jPNiWfR# zFd1zOsK1Shyi0QiR;S0H(}F!M(uUqHQDc zPl8L5#+Go5Nb|F1oAlw;;7}0QD*RdS-(M%N6XzwolpBcP@3q+C)n=Zt zPiM4{Rih8y?hCEXn%sKgJKb(vmsFXFr^Zyhd!O{Ql(+S2!05vQn0RF48Zdr8h%0C5 zZ10r9|K*JZ|M0GuFc4box`jmvq1?fnaJa#$+*RQ#T!h0HPGWp1j&hE8V~wU+ZAU9H z-{9qik<3Xcci7Nl=i3ee-~U*?FK*$=)|Q3hH#;ilN$zrih`y* JTGk@?e*inZ^hf{z literal 3827 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 diff --git a/frontend/public/badge-96.png b/frontend/public/badge-96.png index 638eb2467c0b2dec4997762632287883c8e08b9f..29a5ec4c0ab7aaeabb02ca6a41c239929ab23160 100644 GIT binary patch delta 1355 zcmZ{kdpHw%9L9fZv2uCBaT}73yD=McJql&HgxqqkTozVGZWCLMO%oc1iX4|CQVOdy zmr+b%w(wYU_oP#ED^9soNG@l8o%5VO&il{%yx;HhdH;NOrQ)GK1 z?cMAFXvmY?3KbJ&jc_L~HvmXF08r8a*c6>89{@;10k9APfORndO0i{ao;IS#p0JaS z4q!Jkub(YSFoLri7PcTE_midJa?W_Cs5{8n!QLl%@>7XhRPbrFou3)AFO ztstmI>*z!aav1JH9KF=sRd?wvG2zel7%$SGLF@X|7hCp^xsI65zJIhxDomfqLNrL1 zdgL-Ue32~aZdE>+u2bK=ucL@*F69?z+e^fLNy)SLMe@MVr{ zAS^2%Hu)Wizs5<_UBI*-G>1G9v%r(qiy|`()I>uBo|&BlWH% zAEwvxh)6lj(L6NW*rQgs0`&Op+RovxOC^+`<1We#(hSbn6_ev=+hKVYT3p)8iw&}RhRHZobRv>SSUa$Pp`R%ou zSYv}&i6bVe@&3HXut-4Z#}_eOw3pIm*L=cS>(gc-`O=r}uW07W)Jp$KE;ofbAtqxI z_r(T$)OPc4>)bVdv=8h=*&e3J=HHq!bepMlOk$-T3z^HlMoM=qI!xnR4<9SbG$6z#O> zFL<-m6V#}J?9n%@v>JE~hlM6B)XADF1jL$~h3|Tafu*@vS0=m1J_Uq8f0Xs^UdyEm zp`eafY!&%FAtiIJN!Fq-wEx@cf?8?;4PVI@#4BD%F(YbGY66k!A#}w-Tl%m zS%n8@R^MEK;Bn~daZpSGBZ&V)h=452+L{Oe&RBPcN5?|0 F{ta9#Xx0D# literal 2430 zcmV-^34!*BP)7Z9Xv7!9Xkud2G)j!c z2ZV$~Nl0kqA*iV3p;8jV)ADF5tyn;yP@uJ9OYhA2{A1Rpo5S4RIp@sGJ!d>$GMTwI zGiTOX-`?xnd!4=aK#dwTYSgGvqehJyHEPtTQ6mLOjp+5!k}i?7M$%?UTO{2rX{Drh z_WH~i_}`3PpCjo;Nl#0fl=NRo8zlWi(z^%lf4GpO{Um)`(l$+vU6QVnblA9%5P~o1 zt&(ojzOh@0*Dt&$^~Jrg$Dg-InkT7IMH1X@le8cT{-mZy@c(gdxP^Pc<$l3e z)IWj|H&nG3L$|3FnHK^r ze{EU)@W-ceetsqqZ2d&!M_#NbLcnPSg4giRog~SCPK5~QxXtf!9~wFR2Ut{ufW!NI z-@aPW8-dwoCU>gr>Br1W!2ZD80u3Ui*&j)i zD%2BZEAydT)?8q@sU)^%zgx%3B2SEwo4lP7O)98#mugE z+dIQdpFbfGNyJ_P?ZV3CC{MPs%F;l6%uA< zUj#lM*)d9S4r03P*jZmCJzTW^WHnW?9*bZx? zQz`;_i1Qy@7uF3~oMW-Q6*$?<9v+q0mWqHL;=Iwz7dLr;20ipkBC%Zse8kLN7^T>j zl7N&r?=Z8ofgkYt5^zzm4Yi$%%Yr0*p{Av9^!ll@IK(ajN&ZsH7(#~ zAL(qEX|b&Y0X@X|A9U;RtNRq^kXD+&56$c>Guu%Hv8@yV5eW@5`;VEO3w+lD+iRU^ z7-9y^_^g?IB`h&7gVU&zHm3)CGW@4UA>(ETg>dAbca!;&G(fi zAet{U%q;QQ^Tt<2z_&q_Ip0^FfS?Oez{9$x(oC$zo`5tFBgVirWf&rCj4x&lnzZQ% z*pq|#Im2o#CBTt6KJ#)B0=DPqO>a)kO2+{~*pJJQIYI;6R)m0OGPLH9dge=-Ta`_D z(?-!Z?MZr5M1W-=U`r7KHf1Cr0S*S^qLm7E&vW;?^64IGr{A9QjnQF?C7g zbV$_>ZHkVoInmGl$#Pszc;9x{j$+-=gZE&MW4Zc0FXuYH)j(!m&6f0N)S&vs zq{r&on9RJ!wEX$v9H&Ob@Bdb~UYSYoBihU#zs`ABQy}ZKK@boQt()iPrX@K}&I%-a zE$UUx5(d8&_0;QfHn@5DoJPf|Yn7x~QEO|?WRLL1(2D*(M$(^h`Wg3@ z^I}p5l@pKlLrOb2dO>X5eUd(qZhcNhW0-_Qn$}7k#yTPCbJ3Zod4r!wdf2bm7-(+v z=amHvOy|>EB%LW~|0x31^gs>b(X*#{YF!~|-4r(FNwsHW$^F)E%klRf1Uy3bW_Xr~ zNqUkJpCz#w_#<#T-Cg!inAz?j^?-wc`M`UC57J%qb!b#_Zse?Ab$tGFlbN0E3j;a> zBOoy9ld6vbH?nu_WnRQvgaX>Y)4-F!Q^50dcUJBKCNmboW_k5;E-;Vo&6J~f{TXh( z?8sAq+z!0m?YDk!nOfFVc0i8DSOn z1aOT9UwMO{n}8GvkI~&=x7H0C>e&i2h%j1e0V~bylflm-i+~Hw)*=#~@t$x6d)M%{ z$E?+dQ&i43v#&>MQD?rfrG8`PJW0<^@yilbLXw*C-y!LQ!WGUT*I7cz;s{As_bA9E z6%71VNf)K7rseIguPt&dmUL^6nn|Z*WP}D1_tt+)x=hmHaUri7Ni$XRm$XFEYX6m` zG#Tv~ASC?o&}5Gk`J|-pNjj!iqZ%}91ieAgn*IeobXyLbI7L}PDcfuSA{syUDnIN5 z{sLSJ++t?iBm0wK3ayPWExl$Fg`~FuOMp{=g>)D7PK>CN?)16KJwB1V%gp|@FMBIZ zKx(wqNY_gw9RM5)oB$l}<&7hO!|1LxJ;2A`{o0%Or0`{62i@J1PXK=h9tGBiJ!NUz w@{|*5)TmLTMvWRZYSgGvqehJyH7bJt0q>u@kbQmViU0rr07*qoM6N<$f=OSW@&Et; diff --git a/frontend/public/bimi/ultimo-logo.svg b/frontend/public/bimi/ultimo-logo.svg index 808c5cd..9077174 100644 --- a/frontend/public/bimi/ultimo-logo.svg +++ b/frontend/public/bimi/ultimo-logo.svg @@ -1,14 +1,7 @@ Ultimo Email Ultimo Email logo, for BIMI (SVG Tiny Portable/Secure) - - - - - - + - - - + diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index 2293c69639f13ff274afb9c56abc24240d884bce..30c35a4150638147735ccfdce399d2941944702f 100644 GIT binary patch literal 15086 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 literal 15086 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 Rd*D3*psA{-f>J_6{U4O{s22bL literal 9074 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

i1 literal 25482 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 BYI6Vp diff --git a/frontend/public/icon-maskable-192.png b/frontend/public/icon-maskable-192.png index facbea5f4711b0511b06350d1f9a81f85ed65342..395f73cff7781c767a16f0a73ef7f040dfab47b7 100644 GIT binary patch literal 6944 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 U=++t|$!Gq_uI+wsN!08jhGF#rGn diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest index 0434241..6f0755e 100644 --- a/frontend/public/manifest.webmanifest +++ b/frontend/public/manifest.webmanifest @@ -7,7 +7,7 @@ "scope": "/", "display": "standalone", "orientation": "any", - "background_color": "#ffffff", + "background_color": "#1b1e20", "theme_color": "#f3f4f6", "categories": ["productivity", "business"], "icons": [ diff --git a/frontend/public/ultimo-logo-signet.svg b/frontend/public/ultimo-logo-signet.svg index f5ff865..567fffc 100644 --- a/frontend/public/ultimo-logo-signet.svg +++ b/frontend/public/ultimo-logo-signet.svg @@ -26,9 +26,87 @@ x2="54.340946" y2="162.66415" gradientUnits="userSpaceOnUse" - gradientTransform="matrix(0.75684298,0,0,0.80151105,3.0076311,94.135157)" /> + gradientTransform="matrix(0.75684298,0,0,0.80151105,3.0076311,94.135157)" /> diff --git a/frontend/src/admin/AdminApp.tsx b/frontend/src/admin/AdminApp.tsx index ac254e6..7386034 100644 --- a/frontend/src/admin/AdminApp.tsx +++ b/frontend/src/admin/AdminApp.tsx @@ -2,10 +2,10 @@ import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tansta import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; -import { useT } from "../i18n"; +import { languageName, useLocale, useT } from "../i18n"; import { api, ApiError } from "../lib/api"; import { useMe } from "../lib/auth"; -import type { AdminMail, AdminUser } from "../lib/types"; +import type { AdminMail, AdminTranslation, AdminUser } from "../lib/types"; import { useConfirm } from "../components/AppDialog"; const GIB = 1024 ** 3; @@ -44,8 +44,9 @@ export function AdminApp() {

{/* The divider runs the full height of the taller column. */} -
+
+
)} @@ -287,6 +288,136 @@ function Panel({ handle }: { handle: string }) { ); } +/** Email translation's languages: installed ones (removable, but English), + * downloads in progress, and any language Mozilla has models for to add. */ +function Translation() { + const t = useT(); + const { locale } = useLocale(); + const queryClient = useQueryClient(); + const [confirm, confirmDialog] = useConfirm(); + const [adding, setAdding] = useState(""); + const info = useQuery({ + queryKey: ["adminTranslation"], + queryFn: api.adminTranslation, + // While something downloads, follow its progress. + refetchInterval: (q) => + Object.keys((q.state.data as AdminTranslation | undefined)?.installing ?? {}).length ? 1500 : false, + }); + const done = (data: AdminTranslation) => { + queryClient.setQueryData(["adminTranslation"], data); + void queryClient.invalidateQueries({ queryKey: ["translation-languages"] }); + }; + const install = useMutation({ + mutationFn: api.installTranslationLanguage, + onSuccess: (data) => { + done(data); + setAdding(""); + }, + }); + const remove = useMutation({ mutationFn: api.removeTranslationLanguage, onSuccess: done }); + + const name = (code: string) => languageName(code, locale); + const data = info.data; + const busy = install.isPending || remove.isPending; + const installing = Object.entries(data?.installing ?? {}); + const addable = (data?.available ?? []) + .filter((c) => !data?.installed.includes(c) && !(c in (data?.installing ?? {}))) + .sort((a, b) => name(a).localeCompare(name(b), locale)); + const error = (install.error ?? remove.error) as unknown; + + return ( +
+

{t("admin.translation")}

+

{t("admin.translationHelp")}

+ {info.error instanceof ApiError &&

{info.error.message}

} + {data && !data.enabled &&

{t("admin.translationOff")}

} + {data?.enabled && !data.reachable && ( +

{t("admin.translationUnreachable")}

+ )} + {data?.reachable && ( + <> +
    + {[...data.installed] + .sort((a, b) => name(a).localeCompare(name(b), locale)) + .map((code) => ( +
  • + + {name(code)} {code} + + {code === "en" ? ( + {t("admin.translationAlways")} + ) : ( + + )} +
  • + ))} + {installing.map(([code, p]) => ( +
  • + {name(code)} + + {t("admin.translationInstalling", { + percent: p.total ? Math.floor((p.done / p.total) * 100) : 0, + })} + +
  • + ))} +
+ {Object.entries(data.errors).map(([code, why]) => ( +

+ {t("admin.translationFailed", { language: name(code) })} {why} +

+ ))} + {addable.length > 0 ? ( +
+ + +
+ ) : ( + data.available.length === 0 && ( +

{t("admin.translationCatalogueOffline")}

+ ) + )} + {error instanceof ApiError &&

{error.message}

} + + )} + {confirmDialog} +
+ ); +} + type MailDraft = Omit; /** The SMTP account the server sends password-reset links from. */ diff --git a/frontend/src/components/thread/ThreadView.tsx b/frontend/src/components/thread/ThreadView.tsx index 387dd93..6ef64a7 100644 --- a/frontend/src/components/thread/ThreadView.tsx +++ b/frontend/src/components/thread/ThreadView.tsx @@ -478,7 +478,7 @@ function MessageItem({ : []; const offerTranslate = translateCodes.length > 0 && - (translateTo !== null || needsTranslation(language, locale)); + (translateTo !== null || needsTranslation(language, locale, translateCodes)); const target = translateCodes.find((c) => sameLanguage(c, locale)) ?? null; const replyBtn = (kind: string, label: string, icon: string) => (