davtelepot/davtelepot/languages.py

340 lines
11 KiB
Python

"""Bot support for multiple languages."""
# Standard library modules
import asyncio
from collections import OrderedDict
import logging
# Third party modules
from .utilities import extract, make_button, make_inline_keyboard
default_language_messages = {
'language_command': {
'name': {
'en': "/language",
'it': "/lingua"
},
'alias': {
'en': "Language 🗣",
'it': "Lingua 🗣"
},
'reply_keyboard_button': {
'en': "Language 🗣",
'it': "Lingua 🗣"
},
'description': {
'en': "Change language settings",
'it': "Cambia le impostazioni della lingua"
}
},
'language_button': {
'description': {
'en': "Change language settings",
'it': "Cambia le impostazioni della lingua"
},
'language_set': {
'en': "Selected language: English 🇬🇧",
'it': "Lingua selezionata: Italiano 🇮🇹"
}
},
'language_panel': {
'text': {
'en': "<b>Choose a language</b>",
'it': "<b>Seleziona una lingua</b>"
}
}
}
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=dict(),
default_language='en',
missing_message="Invalid message!",
supported_languages=None,
**kwargs):
"""Instantiate MultiLanguageObject, setting its attributes."""
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=dict(), user_record=dict(), 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 (
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=dict(), user_record=dict(),
default_message=None, language=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
language = self.get_language(
update=update,
user_record=user_record,
language=language
)
# Find result for `language`
result = self.messages
for field in fields:
if field not in result:
logging.error(
"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:
logging.error(
"Please define self.message{f}['en']".format(
f=''.join(
'[\'{field}\']'.format(
field=field
)
for field in fields
)
)
)
return default_message or self.missing_message
return result[language].format(
**format_kwargs
)
async def _language_command(bot, update, user_record):
text, reply_markup = get_language_panel(bot, user_record)
return dict(
text=text,
reply_markup=reply_markup
)
def get_language_panel(bot, 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',
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, user_record)
if text:
return dict(
text=result,
edit=dict(
text=text,
reply_markup=reply_markup
)
)
return result
def init(
bot, language_messages=None, show_in_keyboard=True,
supported_languages={}
):
"""Set language support to `bot`."""
assert isinstance(bot, MultiLanguageObject), (
"Bot must be a MultiLanguageObject subclass in order to support "
"multiple languages."
)
if language_messages is None:
language_messages = default_language_messages
bot.messages['language'] = language_messages
bot.add_supported_languages(supported_languages)
aliases = [
alias
for alias in language_messages[
'language_command']['alias'].values()
]
@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)
@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)