Queer European MD passionate about IT

languages.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. """Bot support for multiple languages."""
  2. # Standard library modules
  3. from collections import OrderedDict
  4. import logging
  5. # Third party modules
  6. from .utilities import extract, make_button, make_inline_keyboard
  7. default_language_messages = {
  8. 'language_command': {
  9. 'name': {
  10. 'en': "/language",
  11. 'it': "/lingua"
  12. },
  13. 'alias': {
  14. 'en': "Language 🗣",
  15. 'it': "Lingua 🗣"
  16. },
  17. 'description': {
  18. 'en': "Change language settings",
  19. 'it': "Cambia le impostazioni della lingua"
  20. }
  21. },
  22. 'language_button': {
  23. 'description': {
  24. 'en': "Change language settings",
  25. 'it': "Cambia le impostazioni della lingua"
  26. }
  27. },
  28. 'language_panel': {
  29. 'text': {
  30. 'en': "<b>Choose a language</b>",
  31. 'it': "<b>Seleziona una lingua</b>"
  32. }
  33. }
  34. }
  35. class MultiLanguageObject(object):
  36. """Make bot inherit from this class to make it support multiple languages.
  37. Call MultiLanguageObject().get_message(
  38. field1, field2, ...,
  39. update, user_record, language,
  40. format_kwarg1, format_kwarg2, ...
  41. ) to get the corresponding message in the selected language.
  42. """
  43. def __init__(self, *args,
  44. messages=dict(),
  45. default_language='en',
  46. missing_message="Invalid message!",
  47. supported_languages=None,
  48. **kwargs):
  49. """Instantiate MultiLanguageObject, setting its attributes."""
  50. self.messages = messages
  51. self._default_language = default_language
  52. self._missing_message = missing_message
  53. if supported_languages is None:
  54. supported_languages = OrderedDict(
  55. {
  56. self.default_language: OrderedDict(
  57. name=self.default_language,
  58. flag=''
  59. )
  60. }
  61. )
  62. self._supported_languages = supported_languages
  63. @property
  64. def default_language(self):
  65. """Return default language."""
  66. return self._default_language
  67. def set_default_language(self, language):
  68. """Set default language."""
  69. self._default_language = language
  70. @property
  71. def missing_message(self):
  72. """Return this message when a proper message can not be found."""
  73. return self._missing_message
  74. def set_missing_message(self, message):
  75. """Set message to be returned where a proper one can not be found."""
  76. self._missing_message = message
  77. @property
  78. def supported_languages(self):
  79. """Return dict of supported languages.
  80. If it is not set, return default language only without flag.
  81. """
  82. return self._supported_languages
  83. def add_supported_languages(self, languages):
  84. """Add some `languages` to supported languages.
  85. Example
  86. ```python
  87. languages = {
  88. 'en': {
  89. 'flag': '🇬🇧',
  90. 'name': 'English'
  91. },
  92. 'it': {
  93. 'flag': '🇮🇹',
  94. 'name': 'Italiano'
  95. }
  96. }
  97. ```
  98. """
  99. assert type(languages) is dict, "Supported languages must be in a dict"
  100. if len(languages) == 0:
  101. return
  102. if self._supported_languages is None:
  103. self._supported_languages = dict()
  104. self._supported_languages.update(languages)
  105. def get_language(self, update=dict(), user_record=dict(), language=None):
  106. """Get language.
  107. Language will be the first non-null value of this list:
  108. - `language` parameter
  109. - `user_record['selected_language_code']`: language selected by user
  110. - `update['language_code']`: language of incoming telegram update
  111. - Fallback to default language if none of the above fits
  112. """
  113. if (
  114. language is None
  115. and 'selected_language_code' in user_record
  116. ):
  117. language = user_record['selected_language_code']
  118. if (
  119. language is None
  120. and 'from' in update
  121. and 'language_code' in update['from']
  122. ):
  123. language = update['from']['language_code']
  124. return language or self.default_language
  125. def get_message(self, *fields, update=dict(), user_record=dict(),
  126. default_message=None, language=None, **format_kwargs):
  127. """Given a list of strings (`fields`), return proper message.
  128. Language will be determined by `get_language` method.
  129. `format_kwargs` will be passed to format function on the result.
  130. """
  131. # Choose language
  132. language = self.get_language(
  133. update=update,
  134. user_record=user_record,
  135. language=language
  136. )
  137. # Find result for `language`
  138. result = self.messages
  139. for field in fields:
  140. if field not in result:
  141. logging.error(
  142. "Please define self.message{f}".format(
  143. f=''.join(
  144. '[\'{field}\']'.format(
  145. field=field
  146. )
  147. for field in fields
  148. )
  149. )
  150. )
  151. return default_message or self.missing_message
  152. result = result[field]
  153. if language not in result:
  154. # For specific languages, try generic ones
  155. language = language.partition('-')[0]
  156. if language not in result:
  157. language = 'en'
  158. if language not in result:
  159. logging.error(
  160. "Please define self.message{f}['en']".format(
  161. f=''.join(
  162. '[\'{field}\']'.format(
  163. field=field
  164. )
  165. for field in fields
  166. )
  167. )
  168. )
  169. return default_message or self.missing_message
  170. return result[language].format(
  171. **format_kwargs
  172. )
  173. async def _language_command(bot, update, user_record):
  174. text, reply_markup = get_language_panel(bot, user_record)
  175. return dict(
  176. text=text,
  177. reply_markup=reply_markup
  178. )
  179. def get_language_panel(bot, user_record):
  180. """Get language panel for user.
  181. Return text and reply_markup of the message about user's language
  182. preferences.
  183. """
  184. text = bot.get_message(
  185. 'language', 'language_panel', 'text',
  186. user_record=user_record,
  187. )
  188. text += "\n"
  189. if 'selected_language_code' in user_record:
  190. current_code = user_record['selected_language_code']
  191. else:
  192. current_code = None
  193. for code, language in bot.supported_languages.items():
  194. text += (f"\n{'✅' if code == current_code else '☑️'} "
  195. f"{language['name']} {language['flag']}")
  196. reply_markup = make_inline_keyboard(
  197. [
  198. make_button(
  199. text=(
  200. f"{'✅' if code == current_code else '☑️'} "
  201. f"{language['name']} {language['flag']}"
  202. ),
  203. prefix='lang:///',
  204. delimiter='|',
  205. data=['set', code]
  206. )
  207. for code, language in bot.supported_languages.items()
  208. ],
  209. 3
  210. )
  211. return text, reply_markup
  212. async def _language_button(bot, update, user_record, data):
  213. result, text, reply_markup = '', '', None
  214. if len(data) > 1 and data[0] == 'set':
  215. # If message is already updated, do not update it
  216. if (
  217. 'selected_language_code' in user_record
  218. and data[1] == user_record['selected_language_code']
  219. and data[1] in bot.supported_languages
  220. and bot.supported_languages[data[1]]['flag'] in extract(
  221. update['message']['text'],
  222. starter='✅',
  223. ender='\n'
  224. )
  225. ):
  226. return
  227. # If database-stored information is not updated, update it
  228. if (
  229. 'selected_language_code' not in user_record
  230. or data[1] != user_record['selected_language_code']
  231. ):
  232. with bot.db as db:
  233. db['users'].update(
  234. dict(
  235. selected_language_code=data[1],
  236. id=user_record['id']
  237. ),
  238. ['id'],
  239. ensure=True
  240. )
  241. user_record['selected_language_code'] = data[1]
  242. if len(data) == 0 or data[0] in ('show', 'set'):
  243. text, reply_markup = get_language_panel(bot, user_record)
  244. if text:
  245. return dict(
  246. text=result,
  247. edit=dict(
  248. text=text,
  249. reply_markup=reply_markup
  250. )
  251. )
  252. return result
  253. def init(
  254. bot, language_messages=None, show_in_keyboard=True,
  255. supported_languages={}
  256. ):
  257. """Set language support to `bot`."""
  258. assert isinstance(bot, MultiLanguageObject), (
  259. "Bot must be a MultiLanguageObject subclass in order to support "
  260. "multiple languages."
  261. )
  262. if language_messages is None:
  263. language_messages = default_language_messages
  264. bot.messages['language'] = language_messages
  265. bot.add_supported_languages(supported_languages)
  266. language_command_alias = bot.get_message(
  267. 'language', 'language_command', 'alias',
  268. language='en', default_message=None
  269. )
  270. if language_command_alias is None:
  271. aliases = []
  272. else:
  273. aliases = [language_command_alias]
  274. @bot.command(
  275. command='/language',
  276. aliases=aliases,
  277. show_in_keyboard=show_in_keyboard,
  278. description=language_messages['language_command']['description'],
  279. authorization_level='everybody'
  280. )
  281. async def language_command(bot, update, user_record):
  282. return await _language_command(bot, update, user_record)
  283. @bot.button(
  284. prefix='lang:///',
  285. separator='|',
  286. description=language_messages['language_button']['description'],
  287. authorization_level='everybody'
  288. )
  289. async def language_button(bot, update, user_record, data):
  290. return await _language_button(bot, update, user_record, data)