translations.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. # ©️ Dan Gazizullin, 2021-2023
  2. # This file is a part of Hikka Userbot
  3. # 🌐 https://github.com/hikariatama/Hikka
  4. # You can redistribute it and/or modify it under the terms of the GNU AGPLv3
  5. # 🔑 https://www.gnu.org/licenses/agpl-3.0.html
  6. import json
  7. import logging
  8. import typing
  9. from pathlib import Path
  10. import requests
  11. from ruamel.yaml import YAML
  12. from . import utils
  13. from .database import Database
  14. from .tl_cache import CustomTelegramClient
  15. from .types import Module
  16. logger = logging.getLogger(__name__)
  17. yaml = YAML(typ="safe")
  18. PACKS = Path(__file__).parent / "langpacks"
  19. SUPPORTED_LANGUAGES = {
  20. "en": "🇬🇧 English",
  21. "ru": "🇷🇺 Русский",
  22. "fr": "🇫🇷 Français",
  23. "it": "🇮🇹 Italiano",
  24. "de": "🇩🇪 Deutsch",
  25. "tr": "🇹🇷 Türkçe",
  26. "uz": "🇺🇿 O'zbekcha",
  27. "es": "🇪🇸 Español",
  28. "kk": "🇰🇿 Қазақша",
  29. "tt": "🥟 Татарча",
  30. }
  31. def fmt(text: str, kwargs: dict) -> str:
  32. for key, value in kwargs.items():
  33. if f"{{{key}}}" in text:
  34. text = text.replace(f"{{{key}}}", str(value))
  35. return text
  36. class BaseTranslator:
  37. def _get_pack_content(
  38. self,
  39. pack: Path,
  40. prefix: str = "hikka.modules.",
  41. ) -> typing.Optional[dict]:
  42. return self._get_pack_raw(pack.read_text(), pack.suffix, prefix)
  43. def _get_pack_raw(
  44. self,
  45. content: str,
  46. suffix: str,
  47. prefix: str = "hikka.modules.",
  48. ) -> typing.Optional[dict]:
  49. if suffix == ".json":
  50. return json.loads(content)
  51. content = yaml.load(content)
  52. if all(len(key) == 2 for key in content):
  53. return {
  54. language: {
  55. {
  56. (
  57. f"{module.strip('$')}.{key}"
  58. if module.startswith("$")
  59. else f"{prefix}{module}.{key}"
  60. ): value
  61. for module, strings in pack.items()
  62. for key, value in strings.items()
  63. if key != "name"
  64. }
  65. }
  66. for language, pack in content.items()
  67. }
  68. return {
  69. (
  70. f"{module.strip('$')}.{key}"
  71. if module.startswith("$")
  72. else f"{prefix}{module}.{key}"
  73. ): value
  74. for module, strings in content.items()
  75. for key, value in strings.items()
  76. if key != "name"
  77. }
  78. def getkey(self, key: str) -> typing.Any:
  79. return self._data.get(key, False)
  80. def gettext(self, text: str) -> typing.Any:
  81. return self.getkey(text) or text
  82. async def load_module_translations(self, pack_url: str) -> typing.Union[bool, dict]:
  83. try:
  84. data = yaml.load((await utils.run_sync(requests.get, pack_url)).text)
  85. except Exception:
  86. logger.exception("Unable to decode %s", pack_url)
  87. return False
  88. if any(len(key) != 2 for key in data):
  89. return data
  90. if lang := self.db.get(__name__, "lang", False):
  91. return next(
  92. (data[language] for language in lang.split() if language in data),
  93. data.get("en", {}),
  94. )
  95. return data.get("en", {})
  96. class Translator(BaseTranslator):
  97. def __init__(self, client: CustomTelegramClient, db: Database):
  98. self._client = client
  99. self.db = db
  100. self._data = {}
  101. self.raw_data = {}
  102. async def init(self) -> bool:
  103. self._data = self._get_pack_content(PACKS / "en.yml")
  104. self.raw_data["en"] = self._data.copy()
  105. any_ = False
  106. if lang := self.db.get(__name__, "lang", False):
  107. for language in lang.split():
  108. if utils.check_url(language):
  109. try:
  110. data = self._get_pack_raw(
  111. (await utils.run_sync(requests.get, language)).text,
  112. language.split(".")[-1],
  113. )
  114. except Exception:
  115. logger.exception("Unable to decode %s", language)
  116. continue
  117. self._data.update(data)
  118. self.raw_data[language] = data
  119. any_ = True
  120. continue
  121. for possible_path in [
  122. PACKS / f"{language}.json",
  123. PACKS / f"{language}.yml",
  124. ]:
  125. if possible_path.exists():
  126. data = self._get_pack_content(possible_path)
  127. self._data.update(data)
  128. self.raw_data[language] = data
  129. any_ = True
  130. for language in SUPPORTED_LANGUAGES:
  131. if language not in self.raw_data and (PACKS / f"{language}.yml").exists():
  132. self.raw_data[language] = self._get_pack_content(
  133. PACKS / f"{language}.yml"
  134. )
  135. return any_
  136. class ExternalTranslator(BaseTranslator):
  137. def __init__(self):
  138. self.data = {}
  139. for lang in SUPPORTED_LANGUAGES:
  140. self.data[lang] = self._get_pack_content(PACKS / f"{lang}.yml", prefix="")
  141. def get(self, key: str, lang: str) -> str:
  142. return self.data[lang].get(key, False) or key
  143. def getdict(self, key: str, **kwargs) -> dict:
  144. return {
  145. lang: fmt(self.data[lang].get(key, False) or key, kwargs)
  146. for lang in self.data
  147. }
  148. class Strings:
  149. def __init__(self, mod: Module, translator: Translator): # skipcq: PYL-W0621
  150. self._mod = mod
  151. self._translator = translator
  152. if not translator:
  153. logger.debug("Module %s got empty translator %s", mod, translator)
  154. self._base_strings = mod.strings # Back 'em up, bc they will get replaced
  155. self.external_strings = {}
  156. def get(self, key: str, lang: typing.Optional[str] = None) -> str:
  157. try:
  158. return self._translator.raw_data[lang][f"{self._mod.__module__}.{key}"]
  159. except KeyError:
  160. return self[key]
  161. def __getitem__(self, key: str) -> str:
  162. return (
  163. self.external_strings.get(key, None)
  164. or (
  165. self._translator.getkey(f"{self._mod.__module__}.{key}")
  166. if self._translator is not None
  167. else False
  168. )
  169. or (
  170. getattr(
  171. self._mod,
  172. next(
  173. (
  174. f"strings_{lang}"
  175. for lang in self._translator.db.get(
  176. __name__,
  177. "lang",
  178. "en",
  179. ).split(" ")
  180. if hasattr(self._mod, f"strings_{lang}")
  181. and isinstance(getattr(self._mod, f"strings_{lang}"), dict)
  182. and key in getattr(self._mod, f"strings_{lang}")
  183. ),
  184. utils.rand(32),
  185. ),
  186. self._base_strings,
  187. )
  188. if self._translator is not None
  189. else self._base_strings
  190. ).get(
  191. key,
  192. self._base_strings.get(key, "Unknown strings"),
  193. )
  194. )
  195. def __call__(
  196. self,
  197. key: str,
  198. _: typing.Optional[typing.Any] = None, # Compatibility tweak for FTG\GeekTG
  199. ) -> str:
  200. return self.__getitem__(key)
  201. def __iter__(self):
  202. return self._base_strings.__iter__()
  203. translator = ExternalTranslator()