"""Getting a message ready for translation, and tidying what comes back.
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
]*>)\s*(?=[{_LOWER_START}])" ) _TEXT_SOFT_BREAK = re.compile(rf"([^\s.!?:;])[ \t]*\n[ \t]*(?=[{_LOWER_START}])") def join_wrapped_lines(html: str) -> str: html = _HTML_SOFT_BREAK.sub(r"\1 ", html) # Plain text (wm-plaintext): newlines inside text, outside tags. return "".join( p if p.startswith("<") else _TEXT_SOFT_BREAK.sub(r"\1 ", p) for p in _TAG.split(html) ) # --- greetings and sign-offs ----------------------------------------------------- # source language → the line as written (lower case) → target language → line. _PHRASES: dict[str, dict[str, dict[str, str]]] = { "en": { "best": {"pl": "Pozdrawiam", "de": "Viele Grüße"}, "best regards": {"pl": "Pozdrawiam serdecznie", "de": "Beste Grüße"}, "best wishes": {"pl": "Najlepsze życzenia", "de": "Beste Grüße"}, "kind regards": {"pl": "Z pozdrowieniami", "de": "Freundliche Grüße"}, "warm regards": {"pl": "Serdecznie pozdrawiam", "de": "Herzliche Grüße"}, "regards": {"pl": "Pozdrawiam", "de": "Grüße"}, "cheers": {"pl": "Pozdrawiam", "de": "Viele Grüße"}, "sincerely": {"pl": "Z poważaniem", "de": "Mit freundlichen Grüßen"}, "yours sincerely": {"pl": "Z poważaniem", "de": "Mit freundlichen Grüßen"}, "thanks": {"pl": "Dzięki", "de": "Danke"}, "many thanks": {"pl": "Wielkie dzięki", "de": "Vielen Dank"}, "thank you": {"pl": "Dziękuję", "de": "Vielen Dank"}, "thanks in advance": {"pl": "Z góry dziękuję", "de": "Vielen Dank im Voraus"}, "all the best": {"pl": "Wszystkiego dobrego", "de": "Alles Gute"}, "talk soon": {"pl": "Do usłyszenia", "de": "Bis bald"}, "have a great day": {"pl": "Miłego dnia", "de": "Einen schönen Tag noch"}, "have a nice day": {"pl": "Miłego dnia", "de": "Einen schönen Tag noch"}, }, "pl": { "pozdrawiam": {"en": "Best regards", "de": "Viele Grüße"}, "pozdrawiam serdecznie": {"en": "Warm regards", "de": "Herzliche Grüße"}, "serdecznie pozdrawiam": {"en": "Warm regards", "de": "Herzliche Grüße"}, "z poważaniem": {"en": "Kind regards", "de": "Mit freundlichen Grüßen"}, "z pozdrowieniami": {"en": "Kind regards", "de": "Freundliche Grüße"}, "dziękuję": {"en": "Thank you", "de": "Vielen Dank"}, "dzięki": {"en": "Thanks", "de": "Danke"}, "z góry dziękuję": {"en": "Thanks in advance", "de": "Vielen Dank im Voraus"}, "miłego dnia": {"en": "Have a nice day", "de": "Einen schönen Tag noch"}, "do usłyszenia": {"en": "Talk soon", "de": "Bis bald"}, }, "de": { "mit freundlichen grüßen": {"en": "Kind regards", "pl": "Z poważaniem"}, "freundliche grüße": {"en": "Kind regards", "pl": "Z pozdrowieniami"}, "viele grüße": {"en": "Best regards", "pl": "Pozdrawiam"}, "beste grüße": {"en": "Best regards", "pl": "Pozdrawiam"}, "liebe grüße": {"en": "Warm regards", "pl": "Serdecznie pozdrawiam"}, "herzliche grüße": {"en": "Warm regards", "pl": "Serdecznie pozdrawiam"}, "danke": {"en": "Thanks", "pl": "Dzięki"}, "vielen dank": {"en": "Many thanks", "pl": "Bardzo dziękuję"}, "bis bald": {"en": "Talk soon", "pl": "Do usłyszenia"}, }, } # A whole line: after a tag or line start, before a tag or line end. _LINE = re.compile(r"(^|>|\n)([ \t]*)([^<>\n]{2,40}?)([ \t]*[,.!]?)([ \t]*)(?=<|\n|$)") def _base(code: str) -> str: return code.lower().split("-")[0] def translate_phrases(html: str, source: str, target: str) -> str: table = _PHRASES.get(_base(source), {}) if not table: return html tgt = _base(target) def repl(m: re.Match[str]) -> str: found = table.get(" ".join(m.group(3).lower().split()), {}).get(tgt) if found is None: return m.group(0) return f"{m.group(1)}{m.group(2)}{KEEP_OPEN}{found}{m.group(4).strip()}{m.group(5)}" # The pattern never crosses a tag: it stops at "<" and ">". return _LINE.sub(repl, html) # --- names ------------------------------------------------------------------------ # A capitalised word; dots only inside it ("U.S"), never at its end — a name # does not run on over a sentence's end ("NukeX. My work…"). _WORD = rf"[{_UPPER}][\w'’&-]*(?:\.[\w'’&-]+)*" # Two or more capitalised words in a row ("Imaginary Pixels", "Trusted Partner # Network"), after a lowercase word: a name, not the start of a sentence. _MULTI_NAME = re.compile( rf"(?<=[{_LOWER_START},] )({_WORD}(?: (?:&|of|the|de|von|van|{_WORD}))* {_WORD})" ) # Languages that capitalise every noun: capitals say nothing about names there. _CAPITALISE_NOUNS = {"de", "lb"} def _title_case(text: str) -> bool: """A Headline Like This: nearly every longer word capitalised (short ones — of, and, in — often are not). A sentence full of names is not one.""" words = [w for w in re.findall(r"[^\W\d_][\w'’-]*", text) if len(w) >= 4] return len(words) >= 3 and sum(w[0].isupper() for w in words) / len(words) > 0.8 def find_names(html: str, source: str) -> set[str]: """Capitalised multi-word names in the middle of sentences (not in headings or Title Case lines, and not for languages that capitalise all nouns).""" if _base(source) in _CAPITALISE_NOUNS: return set() found: set[str] = set() for piece in _TAG.split(html): if piece.startswith("<") or _title_case(piece): continue found.update(m.group(1).rstrip(".") for m in _MULTI_NAME.finditer(piece)) return found _SIG_NAME = re.compile(rf"{_WORD}(?: (?:&|of|the|{_WORD}))* {_WORD}") def find_names_in_signature(text: str) -> list[str]: """Capitalised multi-word names in a signature ("Imaginary Pixels"): the user's company or title, which a translation should leave alone.""" return [m.group(0).rstrip(".") for m in _SIG_NAME.finditer(text)] def keep_terms(html: str, terms: Iterable[str]) -> str: """Mark each of ``terms`` in ``html``'s text translate="no" (longest first; whole words; style blocks and tags left alone).""" words = sorted({t.strip() for t in terms if len(t.strip()) >= 2}, key=len, reverse=True) if not words: return html pattern = re.compile(r"(?])(" + "|".join(re.escape(w) for w in words) + r")(?![\w<])") return _mark(html, pattern) # Emoji and other pictographs (with their skin tones, joiners and variation # selectors): on its own, an emoji comes back as a made-up word ("😊" → # "Uzasadnienie"). _SYMBOLS = re.compile( "[\u2190-\u21ff\u2300-\u23ff\u2460-\u27bf\u2900-\u297f\u2b00-\u2bff" "\U0001f000-\U0001faff]" "[\u2190-\u21ff\u2300-\u23ff\u2460-\u27bf\u2900-\u297f\u2b00-\u2bff" "\U0001f000-\U0001faff\ufe0f\u200d\u20e3]*" ) # File names and codes with underscores ("A_0023C017_260521_p1CAC.mov", # "final_v2"): the models split them at every "_" and dot. _FILE_NAME = re.compile( r"(? str: """Mark emoji, file names, handles ("Jacqueline_"), phone numbers and text without a letter in ``html`` translate="no".""" html = _mark(_mark(html, _SYMBOLS), _PHONE) def is_file(m: re.Match[str]) -> bool: word = m.group(0) if "_" in word and word.strip("_"): return True return bool(_FILE_EXT.search(word)) and any(c.isdigit() or c in "_-" for c in word) return _map_text(_mark(html, _FILE_NAME, keep=is_file), _keep_letterless) _ENTITY = re.compile(r"?\w+;") _EDGES = re.compile(r"^(\s*)(.*?)(\s*)$", re.DOTALL) def _keep_letterless(text: str) -> str: """A piece of text with no letter or digit — an invisible byte-order mark, a lone symbol — kept as it is: the models make up a word for it ("\ufeff" → "Uzasadnienie").""" if any(c.isalnum() for c in _ENTITY.sub("", text)): return text lead, core, trail = _EDGES.match(text).groups() # type: ignore[union-attr] if not _ENTITY.sub("", core).strip(): return text # only spaces and return f"{lead}{KEEP_OPEN}{core}{trail}" def _mark( html: str, pattern: re.Pattern[str], keep: Callable[[re.Match[str]], bool] | None = None ) -> str: """Wrap ``pattern``'s matches in ``html``'s text in translate="no" spans (where ``keep(match)`` says so).""" def repl(m: re.Match[str]) -> str: if keep is not None and not keep(m): return m.group(0) return f"{KEEP_OPEN}{m.group(0)}" return _map_text(html, lambda text: pattern.sub(repl, text)) def _map_text(html: str, fn: Callable[[str], str]) -> str: """``fn`` applied to each piece of ``html``'s text; style blocks, tags and text already kept are left alone.""" out: list[str] = [] in_style = in_keep = False for piece in _TAG.split(html): if piece.startswith("<"): low = piece.lower() if low.startswith("i 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'([^<]*)') def tidy(html: str) -> str: def repl(m: re.Match[str]) -> str: before = html[m.start() - 1] if m.start() > 0 else "" after = html[m.end()] if m.end() < len(html) else "" lead = " " if re.match(r"[\w,;:)]", before) else "" trail = " " if re.match(r"[\w(„\"“]", after) else "" return f"{lead}{m.group(1)}{trail}" return _KEPT.sub(repl, html)