123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- """Bot support for multiple languages."""
- # Standard library modules
- import asyncio
- from collections import OrderedDict
- import logging
- # Project modules
- from .messages import default_language_messages
- from .utilities import extract, make_button, make_inline_keyboard
- class MultiLanguageObject(object):
- """Make bot inherit from this class to make it support multiple languages.
- Call MultiLanguageObject().get_message(
- field1, field2, ...,
- update, user_record, language,
- format_kwarg1, format_kwarg2, ...
- ) to get the corresponding message in the selected language.
- """
- def __init__(self, *args,
- messages=None,
- default_language='en',
- missing_message="Invalid message!",
- supported_languages=None,
- **kwargs):
- """Instantiate MultiLanguageObject, setting its attributes."""
- if messages is None:
- messages = dict()
- self.messages = messages
- self._default_language = default_language
- self._missing_message = missing_message
- if supported_languages is None:
- supported_languages = OrderedDict(
- {
- self.default_language: OrderedDict(
- name=self.default_language,
- flag=''
- )
- }
- )
- self._supported_languages = supported_languages
- @property
- def default_language(self):
- """Return default language."""
- return self._default_language
- def set_default_language(self, language):
- """Set default language."""
- self._default_language = language
- @property
- def missing_message(self):
- """Return this message when a proper message can not be found."""
- return self._missing_message
- def set_missing_message(self, message):
- """Set message to be returned where a proper one can not be found."""
- self._missing_message = message
- @property
- def supported_languages(self):
- """Return dict of supported languages.
- If it is not set, return default language only without flag.
- """
- return self._supported_languages
- def add_supported_languages(self, languages):
- """Add some `languages` to supported languages.
- Example
- ```python
- languages = {
- 'en': {
- 'flag': '🇬🇧',
- 'name': 'English'
- },
- 'it': {
- 'flag': '🇮🇹',
- 'name': 'Italiano'
- }
- }
- ```
- """
- assert type(languages) is dict, "Supported languages must be in a dict"
- if len(languages) == 0:
- return
- if self._supported_languages is None:
- self._supported_languages = dict()
- self._supported_languages.update(languages)
- def get_language(self, update=None, user_record=None, language=None):
- """Get language.
- Language will be the first non-null value of this list:
- - `language` parameter
- - `user_record['selected_language_code']`: language selected by user
- - `update['language_code']`: language of incoming telegram update
- - Fallback to default language if none of the above fits
- """
- if user_record is None:
- user_record = dict()
- if update is None:
- update = dict()
- if (
- language is None
- and 'selected_language_code' in user_record
- ):
- language = user_record['selected_language_code']
- if (
- language is None
- and 'from' in update
- and 'language_code' in update['from']
- ):
- language = update['from']['language_code']
- return language or self.default_language
- def get_message(self, *fields, update=None, user_record=None,
- default_message=None, language=None, messages=None, **format_kwargs):
- """Given a list of strings (`fields`), return proper message.
- Language will be determined by `get_language` method.
- `format_kwargs` will be passed to format function on the result.
- """
- # Choose language
- if update is None:
- update = dict()
- if user_record is None:
- user_record = dict()
- language = self.get_language(
- update=update,
- user_record=user_record,
- language=language
- )
- # Find result for `language`
- result = messages or self.messages
- for field in fields:
- if field not in result:
- if not default_message:
- logging.debug(
- "Please define self.message{f}".format(
- f=''.join(
- '[\'{field}\']'.format(
- field=field
- )
- for field in fields
- )
- )
- )
- return default_message or self.missing_message
- result = result[field]
- if language not in result:
- # For specific languages, try generic ones
- language = language.partition('-')[0]
- if language not in result:
- language = 'en'
- if language not in result:
- if not default_message:
- logging.debug(
- "Please define self.message{f}['en']".format(
- f=''.join(
- '[\'{field}\']'.format(
- field=field
- )
- for field in fields
- )
- )
- )
- return default_message or self.missing_message
- if type(result) is str:
- return result
- return result[language].format(
- **format_kwargs
- )
- async def _language_command(bot, update, user_record):
- text, reply_markup = get_language_panel(
- bot=bot,
- update=update,
- user_record=user_record
- )
- return dict(
- text=text,
- reply_markup=reply_markup
- )
- def get_language_panel(bot, update, user_record):
- """Get language panel for user.
- Return text and reply_markup of the message about user's language
- preferences.
- """
- text = bot.get_message(
- 'language', 'language_panel', 'text',
- update=update, user_record=user_record,
- )
- text += "\n"
- if 'selected_language_code' in user_record:
- current_code = user_record['selected_language_code']
- else:
- current_code = None
- for code, language in bot.supported_languages.items():
- text += (f"\n{'✅' if code == current_code else '☑️'} "
- f"{language['name']} {language['flag']}")
- reply_markup = make_inline_keyboard(
- [
- make_button(
- text=(
- f"{'✅' if code == current_code else '☑️'} "
- f"{language['name']} {language['flag']}"
- ),
- prefix='lang:///',
- delimiter='|',
- data=['set', code]
- )
- for code, language in bot.supported_languages.items()
- ],
- 3
- )
- return text, reply_markup
- async def _language_button(bot, update, user_record, data):
- result, text, reply_markup = '', '', None
- if len(data) > 1 and data[0] == 'set':
- # If message is already updated, do not update it
- if (
- 'selected_language_code' in user_record
- and data[1] == user_record['selected_language_code']
- and data[1] in bot.supported_languages
- and bot.supported_languages[data[1]]['flag'] in extract(
- update['message']['text'],
- starter='✅',
- ender='\n'
- )
- ):
- return
- # If database-stored information is not updated, update it
- if (
- 'selected_language_code' not in user_record
- or data[1] != user_record['selected_language_code']
- ):
- with bot.db as db:
- db['users'].update(
- dict(
- selected_language_code=data[1],
- id=user_record['id']
- ),
- ['id'],
- ensure=True
- )
- user_record['selected_language_code'] = data[1]
- if 'chat' in update['message'] and update['message']['chat']['id'] > 0:
- asyncio.ensure_future(
- bot.send_message(
- text=bot.get_message(
- 'language', 'language_button', 'language_set',
- update=update['message'], user_record=user_record
- ),
- chat_id=update['message']['chat']['id']
- )
- )
- if len(data) == 0 or data[0] in ('show', 'set'):
- text, reply_markup = get_language_panel(bot=bot, update=update, user_record=user_record)
- if text:
- return dict(
- text=result,
- edit=dict(
- text=text,
- reply_markup=reply_markup
- )
- )
- return result
- def init(telegram_bot,
- language_messages=None,
- show_in_keyboard=True,
- supported_languages=None):
- """Set language support to `bot`."""
- if supported_languages is None:
- supported_languages = {}
- from .bot import Bot
- assert isinstance(telegram_bot, MultiLanguageObject), (
- "Bot must be a MultiLanguageObject subclass in order to support "
- "multiple languages."
- )
- assert isinstance(telegram_bot, Bot), (
- "Bot must be a davtelepot Bot subclass in order to support "
- "command and button decorators."
- )
- if language_messages is None:
- language_messages = default_language_messages
- telegram_bot.messages['language'] = language_messages
- telegram_bot.add_supported_languages(supported_languages)
- aliases = [
- alias
- for alias in language_messages[
- 'language_command']['alias'].values()
- ]
- @telegram_bot.command(
- command='/language',
- aliases=aliases,
- reply_keyboard_button=language_messages['language_command'][
- 'reply_keyboard_button'],
- show_in_keyboard=show_in_keyboard,
- description=language_messages['language_command']['description'],
- authorization_level='everybody'
- )
- async def language_command(bot, update, user_record):
- return await _language_command(bot, update, user_record)
- @telegram_bot.button(
- prefix='lang:///',
- separator='|',
- description=language_messages['language_button']['description'],
- authorization_level='everybody'
- )
- async def language_button(bot, update, user_record, data):
- return await _language_button(bot, update, user_record, data)
|