translations.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  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. f"{module.strip('$')}.{key}"
  57. if module.startswith("$")
  58. else f"{prefix}{module}.{key}"
  59. ): value
  60. for module, strings in pack.items()
  61. for key, value in strings.items()
  62. if key != "name"
  63. }}
  64. for language, pack in content.items()
  65. }
  66. return {
  67. (
  68. f"{module.strip('$')}.{key}"
  69. if module.startswith("$")
  70. else f"{prefix}{module}.{key}"
  71. ): value
  72. for module, strings in content.items()
  73. for key, value in strings.items()
  74. if key != "name"
  75. }
  76. def getkey(self, key: str) -> typing.Any:
  77. return self._data.get(key, False)
  78. def gettext(self, text: str) -> typing.Any:
  79. return self.getkey(text) or text
  80. async def load_module_translations(self, pack_url: str) -> typing.Union[bool, dict]:
  81. try:
  82. data = yaml.load((await utils.run_sync(requests.get, pack_url)).text)
  83. except Exception:
  84. logger.exception("Unable to decode %s", pack_url)
  85. return False
  86. if any(len(key) != 2 for key in data):
  87. return data
  88. if lang := self.db.get(__name__, "lang", False):
  89. return next(
  90. (data[language] for language in lang.split() if language in data),
  91. data.get("en", {}),
  92. )
  93. return data.get("en", {})
  94. class Translator(BaseTranslator):
  95. def __init__(self, client: CustomTelegramClient, db: Database):
  96. self._client = client
  97. self.db = db
  98. self._data = {}
  99. self.raw_data = {}
  100. async def init(self) -> bool:
  101. self._data = self._get_pack_content(PACKS / "en.yml")
  102. self.raw_data["en"] = self._data.copy()
  103. any_ = False
  104. if lang := self.db.get(__name__, "lang", False):
  105. for language in lang.split():
  106. if utils.check_url(language):
  107. try:
  108. data = self._get_pack_raw(
  109. (await utils.run_sync(requests.get, language)).text,
  110. language.split(".")[-1],
  111. )
  112. except Exception:
  113. logger.exception("Unable to decode %s", language)
  114. continue
  115. self._data.update(data)
  116. self.raw_data[language] = data
  117. any_ = True
  118. continue
  119. for possible_path in [
  120. PACKS / f"{language}.json",
  121. PACKS / f"{language}.yml",
  122. ]:
  123. if possible_path.exists():
  124. data = self._get_pack_content(possible_path)
  125. self._data.update(data)
  126. self.raw_data[language] = data
  127. any_ = True
  128. for language in SUPPORTED_LANGUAGES:
  129. if language not in self.raw_data and (PACKS / f"{language}.yml").exists():
  130. self.raw_data[language] = self._get_pack_content(
  131. PACKS / f"{language}.yml"
  132. )
  133. return any_
  134. class ExternalTranslator(BaseTranslator):
  135. def __init__(self):
  136. self.data = {}
  137. for lang in SUPPORTED_LANGUAGES:
  138. self.data[lang] = self._get_pack_content(PACKS / f"{lang}.yml", prefix="")
  139. def get(self, key: str, lang: str) -> str:
  140. return self.data[lang].get(key, False) or key
  141. def getdict(self, key: str, **kwargs) -> dict:
  142. return {
  143. lang: fmt(self.data[lang].get(key, False) or key, kwargs)
  144. for lang in self.data
  145. }
  146. class Strings:
  147. def __init__(self, mod: Module, translator: Translator): # skipcq: PYL-W0621
  148. self._mod = mod
  149. self._translator = translator
  150. if not translator:
  151. logger.debug("Module %s got empty translator %s", mod, translator)
  152. self._base_strings = mod.strings # Back 'em up, bc they will get replaced
  153. self.external_strings = {}
  154. def get(self, key: str, lang: typing.Optional[str] = None) -> str:
  155. try:
  156. return self._translator.raw_data[lang][f"{self._mod.__module__}.{key}"]
  157. except KeyError:
  158. return self[key]
  159. def __getitem__(self, key: str) -> str:
  160. return (
  161. self.external_strings.get(key, None)
  162. or (
  163. self._translator.getkey(f"{self._mod.__module__}.{key}")
  164. if self._translator is not None
  165. else False
  166. )
  167. or (
  168. getattr(
  169. self._mod,
  170. next(
  171. (
  172. f"strings_{lang}"
  173. for lang in self._translator.db.get(
  174. __name__,
  175. "lang",
  176. "en",
  177. ).split(" ")
  178. if hasattr(self._mod, f"strings_{lang}")
  179. and isinstance(getattr(self._mod, f"strings_{lang}"), dict)
  180. and key in getattr(self._mod, f"strings_{lang}")
  181. ),
  182. utils.rand(32),
  183. ),
  184. self._base_strings,
  185. )
  186. if self._translator is not None
  187. else self._base_strings
  188. ).get(
  189. key,
  190. self._base_strings.get(key, "Unknown strings"),
  191. )
  192. )
  193. def __call__(
  194. self,
  195. key: str,
  196. _: typing.Optional[typing.Any] = None, # Compatibility tweak for FTG\GeekTG
  197. ) -> str:
  198. return self.__getitem__(key)
  199. def __iter__(self):
  200. return self._base_strings.__iter__()
  201. translator = ExternalTranslator()