Queer European MD passionate about IT

languages.py 11 KB

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