132 lines
3.0 KiB
Python
132 lines
3.0 KiB
Python
import json
|
||
import re
|
||
from pathlib import Path
|
||
|
||
_LATIN_RUN_RE = re.compile(r"[A-Za-z]+")
|
||
|
||
# Фонетические правила en->ru. Применяются слева направо, длиннейшее
|
||
# совпадение выигрывает (триграфы раньше диграфов, диграфы раньше букв).
|
||
_RULES: dict[str, str] = {
|
||
"tion": "шн",
|
||
"sure": "шур",
|
||
"ture": "тур",
|
||
"ough": "оф",
|
||
"tch": "ч",
|
||
"sch": "ш",
|
||
"igh": "ай",
|
||
"ng": "нг",
|
||
"nk": "нк",
|
||
"qu": "кв",
|
||
"th": "т",
|
||
"sh": "ш",
|
||
"ch": "ч",
|
||
"ck": "к",
|
||
"ph": "ф",
|
||
"wh": "у",
|
||
"wr": "р",
|
||
"kn": "н",
|
||
"ee": "и",
|
||
"ea": "и",
|
||
"oo": "у",
|
||
"ou": "ау",
|
||
"ow": "ау",
|
||
"ai": "ай",
|
||
"ay": "эй",
|
||
"oi": "ой",
|
||
"oy": "ой",
|
||
"au": "о",
|
||
"aw": "о",
|
||
"ir": "ер",
|
||
"ur": "ер",
|
||
"er": "ер",
|
||
"ar": "ар",
|
||
"or": "ор",
|
||
"a": "а",
|
||
"b": "б",
|
||
"c": "к",
|
||
"d": "д",
|
||
"e": "е",
|
||
"f": "ф",
|
||
"g": "г",
|
||
"h": "х",
|
||
"i": "и",
|
||
"j": "дж",
|
||
"k": "к",
|
||
"l": "л",
|
||
"m": "м",
|
||
"n": "н",
|
||
"o": "о",
|
||
"p": "п",
|
||
"q": "к",
|
||
"r": "р",
|
||
"s": "с",
|
||
"t": "т",
|
||
"u": "у",
|
||
"v": "в",
|
||
"w": "у",
|
||
"x": "кс",
|
||
"y": "и",
|
||
"z": "з",
|
||
}
|
||
_RULE_KEYS = sorted(_RULES, key=len, reverse=True)
|
||
|
||
_dict_path = Path("translit_dict.json")
|
||
_cache: tuple[dict[str, str], dict[str, str]] | None = None
|
||
|
||
|
||
def set_dict_path(path: str | Path) -> None:
|
||
global _dict_path, _cache
|
||
_dict_path = Path(path)
|
||
_cache = None
|
||
|
||
|
||
def _load_dict() -> tuple[dict[str, str], dict[str, str]]:
|
||
global _cache
|
||
if _cache is None:
|
||
data = json.loads(_dict_path.read_text(encoding="utf-8"))
|
||
exact = {k.lower(): v for k, v in data.get("exact", {}).items()}
|
||
abbrev = {k.upper(): v for k, v in data.get("abbreviations", {}).items()}
|
||
_cache = (exact, abbrev)
|
||
return _cache
|
||
|
||
|
||
def _apply_rules(word: str) -> str:
|
||
out: list[str] = []
|
||
i = 0
|
||
n = len(word)
|
||
while i < n:
|
||
for key in _RULE_KEYS:
|
||
if word.startswith(key, i):
|
||
out.append(_RULES[key])
|
||
i += len(key)
|
||
break
|
||
else:
|
||
out.append(word[i])
|
||
i += 1
|
||
result = "".join(out)
|
||
if word.endswith("e") and len(result) > 2 and result.endswith("е"):
|
||
result = result[:-1]
|
||
return result
|
||
|
||
|
||
def _match_case(token: str, lower_out: str) -> str:
|
||
if token.isupper():
|
||
return lower_out.upper()
|
||
if token[:1].isupper():
|
||
return lower_out.capitalize()
|
||
return lower_out
|
||
|
||
|
||
def transliterate_token(token: str) -> str:
|
||
exact, abbrev = _load_dict()
|
||
if token.isupper() and token in abbrev:
|
||
return abbrev[token]
|
||
key = token.lower()
|
||
if key in exact:
|
||
return _match_case(token, exact[key])
|
||
return _match_case(token, _apply_rules(key))
|
||
|
||
|
||
def transliterate_latin(text: str) -> str:
|
||
return _LATIN_RUN_RE.sub(lambda m: transliterate_token(m.group()), text)
|