WIP: Locale strings

This commit is contained in:
Profitroll 2023-08-29 16:32:37 +02:00
parent 029a965860
commit 9cda8859da
Signed by: profitroll
GPG Key ID: FA35CAB49DACD3B2
14 changed files with 214 additions and 78 deletions

View File

@ -21,5 +21,9 @@
"reports": {
"chat_id": "owner"
},
"disabled_plugins": []
"disabled_plugins": [],
"strings": {
"url_repo": "https://git.end-play.xyz/GarbageReminder/TelegramBot",
"url_contact": "https://git.end-play.xyz/GarbageReminder/TelegramBot/issues"
}
}

View File

@ -33,15 +33,51 @@
"remove_commands": "Unregister all commands"
},
"messages": {
"help": "This bot sends you the notifications about garbage collection according to your local schedule.\n\n**Available commands**\n/help - Show this message\n/setup - Select the location\n/toggle - Disable/enable the reminders\n/set_time - Set the reminders' time\n/set_offset - Set offset between reminders and collection\n/upcoming - Show the upcoming collection\n/language - Select the bot's language\n/checkout - Export or remove your data\n\nYou can also suggest adding your town/district to the bot by contacting the admins using [this link]({url_contact}) and providing your schedule.\n\nWant to host this bot yourself or make some changes? It's open-source, so you can basically fork it. Take a look at [bot's repository]({url_repo}) for details.\n\nHappy using!",
"cancel": "Use /cancel if you want to cancel this operation.",
"cancelled": "Operation has been cancelled.",
"help": "🔔 This bot sends you notifications about garbage collection according to your local schedule.\n\n**Available commands**\n/help - Show this message\n/setup - Select the location\n/toggle - Disable/enable the reminders\n/set_time - Set the reminders' time\n/set_offset - Set offset between reminders and collection\n/upcoming - Show the upcoming collection\n/language - Select the bot's language\n/checkout - Export or remove your data\n\n💭 You can also suggest adding your town/district to the bot by contacting the admins using [this link]({url_contact}) and providing your schedule.\n\n⚙ Want to host this bot yourself or make some changes? It's open-source, so you can basically fork it. Take a look at [bot's repository]({url_repo}) for details.\n\nHappy using! 🤗",
"import_finished": "You have successfully inserted {count} entries.",
"import_invalid_date": "Entries contain invalid date formats. Use **ISO 8601** date format.",
"import_invalid_filetype": "Invalid input. Please, send me a JSON file with entries. {cancel_notice}",
"import_invalid": "This is not a valid garbage collection JSON.",
"import": "Alright. Send me a valid JSON.",
"locale_choice": "Alright. Please choose the language using keyboard below.",
"start_code_invalid": "🚫 You have started the bot by the link containing a location, but it does not seem to be a valid one. Please, use the command /setup to manually configure the location.",
"start_code": " You have started the bot by the link containing a location **{name}**.\n\nPlease, confirm whether you want to use it as your location.",
"start_configure": "📍 Let's configure your location. Press the button on pop-up keyboard to start the process.",
"selection_invalid": "Please, select a valid option using the keyboard provided. {cancel_notice}",
"start_selection_no": "Alright, you're on your own now. Please, use the command /setup to configure your location and start receiving reminders.",
"start_selection_yes": "✅ Finished! Your location is now **{name}**. You will receive reminders about garbage collection {offset} d. in advance at {time}.\n\nPlease, visit /help if you want to know how to change notifications time or disable them.",
"start": "👋 Welcome!\n\nThis small open-source bot is made to simplify your life a bit easier by sending you notifications about upcoming garbage collection in your location.\n\nBy using this bot you accept [Privacy Policy]({privacy_policy}), otherwise please block and remove this bot before further interaction.\n\nNow the official part is over so you can dive into the bot.",
"locale_choice": "Alright. Please choose the language using keyboard below."
"checkout_deleted": "🗑️ Your data has been deleted. If you want to start using this bot again, please use /setup command. Otherwise delete/block the bot and do not interact with it anymore.",
"checkout": "Here's pretty much all the data bot has. Please, use these buttons to choose whether you want to delete your data from the bot.",
"checkout_deletion": "Alright. Please, confirm that you want to delete your data from the bot.\n\nFollowing data will be deleted:\n• Selected location\n• Preferred language of all messages\n• Time of the notifications\n• Offset of the notifications\n\nUse keyboard provided to confirm and continue or /cancel to abort this operation.",
"toggle_disabled": "🔕 Notifications have been disabled.",
"toggle_enabled": "🔔 Notifications have been enabled {offset} d. before garbage collection at {time}. Use /setup to select your location.",
"toggle_enabled_location": "🔔 Notifications have been enabled {offset} d. before garbage collection at {time} at the **{name}**.",
"upcoming_empty_location": "You have no location set. Use /setup to select your location.",
"upcoming_empty": "No garbage collection entries found for the next 30 days at **{name}**",
"upcoming": "Upcoming garbage collection:\n\n{entries}",
"reminder": "**Garbage Collection**\n\nType: {type}\nDate: {date}\n\nDon't forget to prepare your bin for collection!",
"search_nearby_empty": "Could not find any locations nearby. Let's try using the name search.",
"location_select": "Select the location using the keyboard provided.",
"location_name": "Please, send me a location name. It should be the name used in your local authorities' garbage collection schedule. This usually is a name of the district or even the town itself.",
"location_name_invalid": "Please, send the name of the location as a text. {cancel_notice}",
"location_name_empty": "Could not find any locations by this name. Try rephrasing it or make sure you use the same location language and name itself as it in written by your local authorities in garbage collection schedule.\n\n{cancel_notice}"
},
"buttons": {
"configure": "Let's configure the bot"
"start_configure": "⚙️ Let's configure the bot",
"start_code_yes": "✅ Yes, I want to use it",
"start_code_no": "❌ No, I don't want to use it",
"delete_yes": "✅ Yes, I want to delete it",
"delete_no": "❌ No, I don't want to delete it",
"delete_confirm": "I agree and want to proceed"
},
"callbacks": {
"locale_set": "Your language now is: {locale}"
},
"force_replies": {}
"force_replies": {
"import": "JSON with garbage entries",
"location_name": "Location name"
}
}

View File

@ -22,8 +22,6 @@ async def remind(app: PyroClient) -> None:
for user_db in users:
user = PyroUser(**user_db)
logger.info("Processing %s...", user.id)
if not user.enabled or user.location is None:
continue
@ -61,7 +59,7 @@ async def remind(app: PyroClient) -> None:
await app.send_message(
user.id,
"**Garbage Collection**\n\nType: {type}\nDate: {date}\n\nDon't forget to prepare your bin for collection!".format(
app._("reminder", "messages", locale=user.locale).format(
type=garbage_type, date=garbage_date
),
)

View File

@ -10,24 +10,37 @@ from modules.database import col_locations
async def search_name(app: PyroClient, message: Message) -> Union[Location, None]:
user = await app.find_user(message.from_user)
location: Union[Location, None] = None
await message.reply_text(
"Please, send me a location name. It should be the name used in your local authorities' garbage collection schedule. This usually is a name of the district or even the town itself.",
reply_markup=ForceReply(placeholder="Location name"),
app._("location_request_name", "messages", locale=user.locale),
reply_markup=ForceReply(
placeholder=app._("location_name", "force_replies", locale=user.locale)
),
)
while location is None:
answer = await listen_message(app, message.chat.id, 300)
if answer is None or answer.text == "/cancel":
await message.reply_text("Cancelled.", reply_markup=ReplyKeyboardRemove())
await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
if answer.text is None:
await message.reply_text(
"Please, send the name of the location as a text. You can also abort this operation with /cancel command.",
reply_markup=ForceReply(placeholder="Location name"),
app._("location_name_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
),
reply_markup=ForceReply(
placeholder=app._(
"location_name", "force_replies", locale=user.locale
)
),
)
continue
@ -37,8 +50,14 @@ async def search_name(app: PyroClient, message: Message) -> Union[Location, None
if len(locations) == 0:
await message.reply_text(
"Could not find any locations by this name. Try rephrasing it or make sure you use the same location language and name itself as it in written by your local authorities in garbage collection schedule.\n\nYou can also abort this operation with /cancel command.",
reply_markup=ForceReply(placeholder="Location name"),
app._("location_name_empty", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
),
reply_markup=ForceReply(
placeholder=app._(
"location_name", "force_replies", locale=user.locale
)
),
)
continue
@ -46,7 +65,8 @@ async def search_name(app: PyroClient, message: Message) -> Union[Location, None
keyboard.add(*[ReplyButton(db_record["name"]) for db_record in locations])
await message.reply_text(
"Select the location using the keyboard", reply_markup=keyboard
app._("location_select", "messages", locale=user.locale),
reply_markup=keyboard,
)
while True:
@ -54,7 +74,8 @@ async def search_name(app: PyroClient, message: Message) -> Union[Location, None
if answer is None or answer.text == "/cancel":
await message.reply_text(
"Cancelled.", reply_markup=ReplyKeyboardRemove()
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
@ -64,7 +85,9 @@ async def search_name(app: PyroClient, message: Message) -> Union[Location, None
if answer.text is None or location is None:
await answer.reply_text(
"Please, select a valid location using keyboard provided. Use /cancel if you want to cancel this operation.",
app._("selection_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
)
continue

View File

@ -1,6 +1,5 @@
from typing import Union
from bson.son import SON
from convopyro import listen_message
from pykeyboard import ReplyButton, ReplyKeyboard
from pyrogram.types import Message, ReplyKeyboardRemove
@ -12,6 +11,8 @@ from modules.search_name import search_name
async def search_nearby(app: PyroClient, message: Message) -> Union[Location, None]:
user = await app.find_user(message.from_user)
query = {
"location": {
"$within": {
@ -27,7 +28,7 @@ async def search_nearby(app: PyroClient, message: Message) -> Union[Location, No
if len(locations) == 0:
await message.reply_text(
"Could not find any locations nearby. Let's try using the name search."
app._("search_nearby_empty", "messages", locale=user.locale)
)
return await search_name(app, message)
@ -35,7 +36,8 @@ async def search_nearby(app: PyroClient, message: Message) -> Union[Location, No
keyboard.add(*[ReplyButton(db_record["name"]) for db_record in locations])
await message.reply_text(
"Select the location using the keyboard", reply_markup=keyboard
app._("location_select", "messages", locale=user.locale),
reply_markup=keyboard,
)
while True:
@ -43,7 +45,10 @@ async def search_nearby(app: PyroClient, message: Message) -> Union[Location, No
location: Union[Location, None] = None
if answer is None or answer.text == "/cancel":
await message.reply_text("Cancelled.", reply_markup=ReplyKeyboardRemove())
await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
for db_record in locations:
@ -53,7 +58,9 @@ async def search_nearby(app: PyroClient, message: Message) -> Union[Location, No
if answer.text is None or location is None:
await answer.reply_text(
"Please, select a valid location using keyboard provided. Use /cancel if you want to cancel this operation."
app._("selection_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
)
continue

View File

@ -32,12 +32,12 @@ async def command_checkout(app: PyroClient, message: Message):
row_width=1, resize_keyboard=True, one_time_keyboard=True
)
keyboard_delete.add(
ReplyButton("Yes, I want to delete it"),
ReplyButton("No, I don't want to delete it"),
ReplyButton(app._("delete_yes", "buttons", locale=user.locale)),
ReplyButton(app._("delete_no", "buttons", locale=user.locale)),
)
await message.reply_text(
"Here's pretty much all the data bot has. Please, use these buttons to choose whether you want to delete your data from the bot.",
app._("checkout", "messages", locale=user.locale),
reply_markup=keyboard_delete,
)
@ -46,25 +46,25 @@ async def command_checkout(app: PyroClient, message: Message):
if answer_delete is None or answer_delete.text == "/cancel":
await message.reply_text(
"Cancelled.",
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
if answer_delete.text not in [
"Yes, I want to delete it",
"No, I don't want to delete it",
]:
if answer_delete.text not in app._(
"delete_yes", "buttons", locale=user.locale
) + app._("delete_no", "buttons", locale=user.locale):
await answer_delete.reply_text(
"Invalid answer provided. Use /cancel if you want to cancel this operation."
app._("selection_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
)
continue
if answer_delete.text in [
"No, I don't want to delete it",
]:
if answer_delete.text in app._("delete_no", "buttons", locale=user.locale):
await answer_delete.reply_text(
"Alright, cancelled.", reply_markup=ReplyKeyboardRemove()
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
@ -74,10 +74,12 @@ async def command_checkout(app: PyroClient, message: Message):
keyboard_confirm = ReplyKeyboard(
row_width=1, resize_keyboard=True, one_time_keyboard=True
)
keyboard_confirm.add(ReplyButton("I agree and want to proceed"))
keyboard_confirm.add(
ReplyButton(app._("delete_confirm", "buttons", locale=user.locale))
)
await message.reply_text(
"Alright. Please, confirm that you want to delete your data from the bot.\n\nFollowing data will be deleted:\nSelected location, preferred language of the messages, notifications time and your notifications offset.",
app._("checkout_deletion", "messages", locale=user.locale),
reply_markup=keyboard_confirm,
)
@ -86,14 +88,16 @@ async def command_checkout(app: PyroClient, message: Message):
if answer_confirm is None or answer_confirm.text == "/cancel":
await message.reply_text(
"Cancelled.",
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
if answer_confirm.text not in ["I agree and want to proceed"]:
if answer_confirm.text not in app.in_all_locales("delete_confirm", "buttons"):
await answer_confirm.reply_text(
"Invalid answer provided. Use /cancel if you want to cancel this operation."
app._("selection_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
)
continue
@ -101,6 +105,6 @@ async def command_checkout(app: PyroClient, message: Message):
await user.delete()
await answer_confirm.reply_text(
"Your data has been deleted. If you want to start using this bot again, please use /setup command. Otherwise delete/block the bot and do not interact with it anymore.",
app._("checkout_deleted", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)

View File

@ -11,5 +11,9 @@ async def command_help(app: PyroClient, message: Message):
user = await app.find_user(message.from_user)
await message.reply_text(
app._("help", "messages", locale=user.locale), disable_web_page_preview=True
app._("help", "messages", locale=user.locale).format(
url_contact=app.config["strings"]["url_contact"],
url_repo=app.config["strings"]["url_repo"],
),
disable_web_page_preview=True,
)

View File

@ -3,7 +3,7 @@ from typing import List, Mapping, Union
from convopyro import listen_message
from pyrogram import filters
from pyrogram.types import Message
from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
from ujson import loads
from classes.pyroclient import PyroClient
@ -15,18 +15,28 @@ from modules.database import col_entries
~filters.scheduled & filters.private & custom_filters.owner & filters.command(["import"], prefixes=["/"]) # type: ignore
)
async def command_import(app: PyroClient, message: Message):
await message.reply_text("Alright. Send me a valid JSON.")
user = await app.find_user(message.from_user)
await message.reply_text(
app._("import", "messages", locale=user.locale),
reply_markup=ForceReply(placeholder=""),
)
while True:
answer = await listen_message(app, message.chat.id, 300)
if answer is None or answer.text == "/cancel":
await message.reply_text("Cancelled.")
await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
if answer.document is None or answer.document.mime_type != "application/json":
await answer.reply_text(
"Invalid input. Please, send me a JSON file with entries."
app._("import_invalid_filetype", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
)
continue
@ -38,7 +48,10 @@ async def command_import(app: PyroClient, message: Message):
for entry in entries:
if not isinstance(entries, list):
await answer.reply_text("This is not a valid garbage collection JSON.")
await answer.reply_text(
app._("import_invalid", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
for key in ("locations", "garbage_type", "date"):
@ -47,14 +60,18 @@ async def command_import(app: PyroClient, message: Message):
or (key == "garbage_type" and not isinstance(entry[key], int))
or (key == "locations" and not isinstance(entry[key], list))
):
await answer.reply_text("This is not a valid garbage collection JSON.")
await answer.reply_text(
app._("import_invalid", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
if key == "date":
try:
datetime.fromisoformat(str(entry[key]))
except (ValueError, TypeError):
await answer.reply_text(
"Entries contain invalid date formats. Use **ISO 8601** date format."
app._("import_invalid_date", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
@ -70,5 +87,8 @@ async def command_import(app: PyroClient, message: Message):
await col_entries.insert_many(entries_clean)
await answer.reply_text(
f"You have successfully inserted {len(entries_clean)} entries."
app._("import_finished", "messages", locale=user.locale).format(
count=len(entries_clean)
),
reply_markup=ReplyKeyboardRemove(),
)

View File

@ -25,7 +25,10 @@ async def command_set_offset(app: PyroClient, message: Message):
answer = await listen_message(app, message.chat.id, 300)
if answer is None or answer.text == "/cancel":
await message.reply_text("Cancelled.", reply_markup=ReplyKeyboardRemove())
await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
try:

View File

@ -25,7 +25,10 @@ async def command_set_time(app: PyroClient, message: Message):
answer = await listen_message(app, message.chat.id, 300)
if answer is None or answer.text == "/cancel":
await message.reply_text("Cancelled.", reply_markup=ReplyKeyboardRemove())
await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
try:

View File

@ -35,14 +35,19 @@ async def command_setup(app: PyroClient, message: Message):
answer_type = await listen_message(app, message.chat.id, 300)
if answer_type is None or answer_type.text == "/cancel":
await message.reply_text("Cancelled.", reply_markup=ReplyKeyboardRemove())
await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
if answer_type.location is None and answer_type.text not in [
"Search by location name",
]:
await answer_type.reply_text(
"Please, select a valid option using keyboard provided. Use /cancel if you want to cancel this operation."
app._("selection_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
)
continue

View File

@ -27,21 +27,27 @@ async def command_start(app: PyroClient, message: Message):
location = await app.get_location(int(join_code))
except ValueError:
await message.reply_text(
"🚫 You have provided the location but it does not seem to be a valid one. Please, use the command /setup to manually configure the location."
app._("start_code_invalid", "messages", locale=user.locale)
)
return
keyboard = ReplyKeyboardMarkup(
[
[KeyboardButton("Yes, I want to use it")],
[KeyboardButton("No, I don't want to use it")],
[
KeyboardButton(
app._("start_code_yes", "buttons", locale=user.locale)
)
],
[KeyboardButton(app._("start_code_no", "buttons", locale=user.locale))],
],
resize_keyboard=True,
one_time_keyboard=True,
)
await message.reply_text(
f" You have started the bot by the link containing a location **{location.name}**.\n\nPlease, confirm whether you want to use it as your location.",
app._("start_code", "messages", locale=user.locale).format(
name=location.name
),
reply_markup=keyboard,
)
@ -50,24 +56,24 @@ async def command_start(app: PyroClient, message: Message):
if answer is None or answer.text == "/cancel":
await message.reply_text(
"Cancelled.", reply_markup=ReplyKeyboardRemove()
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
if answer.text not in [
"Yes, I want to use it",
"No, I don't want to use it",
]:
if answer.text not in app.in_all_locales(
"start_code_yes", "buttons"
) + app.in_all_locales("start_code_no", "buttons"):
await answer.reply_text(
"Please, select a valid location using keyboard provided. Use /cancel if you want to cancel this operation."
app._("selection_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
)
continue
if answer.text in [
"No, I don't want to use it",
]:
if answer.text in app.in_all_locales("start_code_no", "buttons"):
await answer.reply_text(
"Alright, you're on your own now. Please, use the command /setup to configure your location and start receiving reminders.",
app._("start_selection_no", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
@ -80,16 +86,24 @@ async def command_start(app: PyroClient, message: Message):
app._("time", "formats", locale=user.locale)
)
await answer.reply_text(
f"✅ Finished! Your location is now **{location.name}**. You will receive reminders about garbage collection {user.offset} d. in advance at {user_time}.\n\nPlease, visit /help if you want to know how to change notifications time or disable them.",
app._("start_selection_yes", "messages", locale=user.locale).format(
name=location.name, offset=user.offset, time=user_time
),
reply_markup=ReplyKeyboardRemove(),
)
return
if user.location is None:
await message.reply_text(
"📍 Let's configure your location. Press the button on pop-up keyboard to start the process.",
app._("start_configure", "messages", locale=user.locale),
reply_markup=ReplyKeyboardMarkup(
[[KeyboardButton(app._("configure", "buttons", locale=user.locale))]],
[
[
KeyboardButton(
app._("start_configure", "buttons", locale=user.locale)
)
]
],
resize_keyboard=True,
one_time_keyboard=True,
),

View File

@ -15,15 +15,26 @@ async def command_toggle(app: PyroClient, message: Message):
await user.update_state(not user.enabled)
if user.enabled:
await message.reply_text("Notifications have been disabled.")
await message.reply_text(
app._("toggle_disabled", "messages", locale=user.locale)
)
return
user_time = datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime("%H:%M")
if user.location is None:
await message.reply_text(
f"Notifications have been enabled {user.offset} d. before garbage collection at {datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime('%H:%M')}. Use /setup to select your location."
app._("toggle_enabled", "messages", locale=user.locale).format(
offset=user.offset,
time=user_time,
),
)
return
await message.reply_text(
f"Notifications have been enabled {user.offset} d. before garbage collection at {datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime('%H:%M')} at the **{user.location.name}**."
app._("toggle_enabled_location", "messages", locale=user.locale).format(
offset=user.offset,
time=user_time,
name=user.location.name,
)
)

View File

@ -17,7 +17,7 @@ async def command_upcoming(app: PyroClient, message: Message):
if user.location is None:
await message.reply_text(
"You have no location set. Use /setup to select your location."
app._("upcoming_empty_location", "messages", locale=user.locale)
)
return
@ -52,8 +52,12 @@ async def command_upcoming(app: PyroClient, message: Message):
if not entries:
await message.reply_text(
f"No garbage collection entries found for the next 30 days at **{user.location.name}**"
app._("upcoming_empty", "messages", locale=user.locale).format(
name=user.location.name
)
)
return
await message.reply_text(f"Upcoming garbage collection:\n\n{entries_text}")
await message.reply_text(
app._("upcoming", "messages", locale=user.locale).format(entries=entries_text)
)