diff --git a/application/bot/main.py b/application/bot/main.py index aedf9f97..48a92746 100644 --- a/application/bot/main.py +++ b/application/bot/main.py @@ -14,6 +14,7 @@ from src.broker.amqp_connection import AmqpConnection from src.broker.amqp_connection_pool import AmqpConnectionPool from src.broker.handlers import on_message +from src.i18n import Translator def setup_logger(): @@ -58,6 +59,10 @@ def main(): except: logger.warning("Missing locale nb_NO.utf8 on server") + # set up translator + translator = Translator(language_folder="./src/lang") + injector.binder.bind(Translator, to=translator, scope=singleton) + # Set up rabbitmq setup_connection_pool() setup_consumption_queue_listener() diff --git a/application/bot/src/api/bot_api.py b/application/bot/src/api/bot_api.py index a93c231d..248559bd 100644 --- a/application/bot/src/api/bot_api.py +++ b/application/bot/src/api/bot_api.py @@ -10,6 +10,7 @@ from src.broker.broker_client import BrokerClient from src.injector import injector import logging +from src.i18n import Translator class BotApiConfiguration: def __init__(self, timezone): @@ -22,6 +23,7 @@ def __init__(self, config: BotApiConfiguration, logger: logging.Logger): self.HOURS_BETWEEN_REMINDERS = int(os.environ["HOURS_BETWEEN_REMINDERS"]) self.timezone = config.timezone self.logger = logger + self.translator = injector.get(Translator) def __enter__(self): self.client = injector.get(BrokerClient) @@ -34,7 +36,7 @@ def welcome(self, slack_client, team_id): channel_id = self.join_channel(slack_client=slack_client, team_id=team_id) self.send_slack_message( channel_id=channel_id, - text="Hei! Jeg er pizzabot. Hvis dere vil endre hvilke kanal jeg bruker så kan dere gå inn i riktig kanal og bruke kommandoen '/set-pizza-channel'. Hvis kanalen er privat må dere legge meg til først.", + text=self.translator.translate("botWelcome"), slack_client=slack_client ) @@ -130,7 +132,7 @@ def send_reminders(self): if invitation['reminded_at'] < remind_timestamp: slack_client.send_slack_message( channel_id=invitation['slack_id'], - text="Hei du! Jeg hørte ikke noe mer? Er du gira?" + text=self.translator.translate("eventReminder") ) was_updated = self.client.update_invitation( slack_id=invitation['slack_id'], @@ -158,7 +160,7 @@ def send_event_finalized(self, timestamp, restaurant_name, slack_ids, channel_id # Send the finalization Slack message slack_client.send_slack_message( channel_id=channel_id, - text="Halloi! %s! Dere skal spise 🍕 på %s, %s. %s booker bord, og %s legger ut for maten. Blank betaler!" % (ids_string, restaurant_name, timestamp.strftime("%A %d. %B kl %H:%M"), booker, payer) + text=self.translator.translate("eventFinalized", user_ids=ids_string, restaurant_name=restaurant_name, time_stamp=timestamp.strftime("%A %d. %B kl %H:%M"), booker=booker, payer=payer) ) def send_event_unfinalized(self, timestamp, restaurant_name, slack_ids, channel_id, slack_client): @@ -171,7 +173,7 @@ def send_event_unfinalized(self, timestamp, restaurant_name, slack_ids, channel_ # Send message that the event unfinalized slack_client.send_slack_message( channel_id=channel_id, - text="Halloi! %s! Hvis den som meldte seg av besøket til %s %s skulle betale eller booke så må nesten en av dere andre sørge for det. I mellomtiden letes det etter en erstatter." % (ids_string, restaurant_name, timestamp.strftime("%A %d. %B kl %H:%M")) + text=self.translator.translate("eventUnfinalized", user_ids=ids_string, restaurant_name=restaurant_name, time_stamp=timestamp.strftime("%A %d. %B kl %H:%M")) ) # Invite more users for the event self.invite_multiple_if_needed() @@ -181,7 +183,7 @@ def send_user_withdrew_after_finalization(self, user_id, timestamp, restaurant_n # Send message that the user withdrew slack_client.send_slack_message( channel_id=channel_id, - text="Halloi! <@%s> meldte seg nettopp av besøket til %s %s." % (user_id, restaurant_name, timestamp.strftime("%A %d. %B kl %H:%M")) + text=self.translator.translate("userWithdrawAfterFinalization", user_id=user, restaurant_name=restaurant_name, time_stamp=timestamp.strftime("%A %d. %B kl %H:%M")) ) # Invite more users for the event self.invite_multiple_if_needed() @@ -219,7 +221,7 @@ def auto_reply(self): # Send the user a message that the invite expired slack_client.send_slack_message( channel_id=invitation['slack_id'], - text="Neivel, da antar jeg du ikke kan/gidder. Håper du blir med neste gang! 🤞" + text=self.translator.translate("autoReplyNoAttending") ) self.logger.info("%s didn't answer. Setting RSVP to not attending." % invitation['slack_id']) else: @@ -310,7 +312,7 @@ def inform_users_unfinalized_event_got_cancelled(self, time, restaurant_name, sl # Send the user a message that the event has been cancelled slack_client.send_slack_message( channel_id=slack_id, - text="Halloi! Besøket til %s, %s har blitt kansellert. Sorry!" % (restaurant_name, time.strftime("%A %d. %B kl %H:%M")) + text=self.translator.translate("unfinalizedEventCancelled", restaurant_name=restaurant_name, time_stamp=time.strftime("%A %d. %B kl %H:%M")) ) self.logger.info("Informed user: %s" % slack_id) @@ -322,7 +324,7 @@ def inform_users_finalized_event_got_cancelled(self, time, restaurant_name, slac self.logger.info("finalized event got cancelled for users %s" % ", ".join(slack_user_ids)) slack_client.send_slack_message( channel_id=channel_id, - text="Halloi! %s! Besøket til %s, %s har blitt kansellert. Sorry!" % (ids_string, restaurant_name, time.strftime("%A %d. %B kl %H:%M"),) + text=self.translator.translate("finalizedEventCancelled", user_ids=ids_string, restaurant_name=restaurant_name, time_stamp=time.strftime("%A %d. %B kl %H:%M")) ) # Update invitation message - remove buttons and tell user it has been cancelled for slack_user_data in slack_data: @@ -342,7 +344,7 @@ def inform_users_unfinalized_event_got_updated(self, old_time, time, old_restaur for slack_id in slack_ids: slack_client.send_slack_message( channel_id=slack_id, - text="Halloi! Besøket til %s, %s har blit endret til %s, %s." % (old_restaurant_name, old_time.strftime("%A %d. %B kl %H:%M"), restaurant_name, time.strftime("%A %d. %B kl %H:%M")) + text=self.translator.translate("unfinalizedEventUpdate", old_restaurant_name=old_restaurant_name, old_time_stamp=old_time.strftime("%A %d. %B kl %H:%M"),restaurant_name=restaurant_name, time_stamp=time.strftime("%A %d. %B kl %H:%M")) ) self.logger.info("Informed user: %s" % slack_id) @@ -352,7 +354,7 @@ def inform_users_finalized_event_got_updated(self, old_time, time, old_restauran self.logger.info("finalized event got updated for users %s" % ", ".join(slack_ids)) slack_client.send_slack_message( channel_id=channel_id, - text="Halloi! %s! Besøket til %s, %s har blit endret til %s, %s." % (ids_string, old_restaurant_name, old_time.strftime("%A %d. %B kl %H:%M"), restaurant_name, time.strftime("%A %d. %B kl %H:%M")) + text=self.translator.translate("finalizedEventUpdate", user_ids=ids_string, old_restaurant_name=old_restaurant_name, old_time_stamp=old_time.strftime("%A %d. %B kl %H:%M"),restaurant_name=restaurant_name, time_stamp=time.strftime("%A %d. %B kl %H:%M")) ) def send_slack_message(self, channel_id, text, slack_client, blocks=None, thread_ts=None): @@ -362,20 +364,20 @@ def update_slack_message(self, channel_id, ts, slack_client, text=None, blocks=N return slack_client.update_slack_message(channel_id, ts, text, blocks) def send_pizza_invite(self, channel_id, event_id, place, datetime, deadline, slack_client): - top_level_title_text = f"Pizzainvitasjon: {place}, {datetime}" + top_level_title_text = self.translator.translate("topLevelPizzaInvitation", restaurant_name=place, time_stamp=datetime) blocks = [ { "type": "header", "text": { "type": "plain_text", - "text": "Pizzainvitasjon" + "text": self.translator.translate("pizzaInvitationHeader") } }, { "type": "section", "text": { "type": "plain_text", - "text": f"Du er invitert til :pizza: på {place}, {datetime}. Pls svar innen {deadline} timer :pray:. Kan du?" + "text": self.translator.translate("pizzaInvitationBody", restaurant_name=place, time_stamp=datetime, deadline=deadline) } }, { @@ -388,7 +390,7 @@ def send_pizza_invite(self, channel_id, event_id, place, datetime, deadline, sla "type": "button", "text": { "type": "plain_text", - "text": "Hells yesss!!! 🍕🍕🍕" + "text": self.translator.translate("pizzaInvitationAttendButton") }, "value": event_id, "action_id": "rsvp_yes", @@ -397,7 +399,7 @@ def send_pizza_invite(self, channel_id, event_id, place, datetime, deadline, sla "type": "button", "text": { "type": "plain_text", - "text": "Nah ☹️" + "text": self.translator.translate("pizzaInvitationNoAttendButton") }, "value": event_id, "action_id": "rsvp_no", @@ -421,7 +423,7 @@ def send_pizza_invite_loading(self, channel_id, ts, old_blocks, event_id, slack_ "type": "section", "text": { "type": "mrkdwn", - "text": ":hourglass_flowing_sand: Behandler forespørselen din...", + "text": self.translator.translate("inviteLoading") } } ] @@ -435,7 +437,7 @@ def send_pizza_invite_not_among_invited_users(self, channel_id, ts, old_blocks, "type": "section", "text": { "type": "plain_text", - "text": "Kunne ikke oppdatere invitasjonen. Du var ikke blant de inviterte.", + "text": self.translator.translate("inviteNotAmongUsers") } } ] @@ -480,7 +482,7 @@ def send_update_pizza_invite_unanswered(self, channel_id, ts, event_id, slack_cl "type": "button", "text": { "type": "plain_text", - "text": "Hells yesss!!! 🍕🍕🍕" + "text": self.translator.translate("pizzaInvitationAttendButton") }, "value": str(event_id), "action_id": "rsvp_yes", @@ -489,7 +491,7 @@ def send_update_pizza_invite_unanswered(self, channel_id, ts, event_id, slack_cl "type": "button", "text": { "type": "plain_text", - "text": "Nah ☹️" + "text": self.translator.translate("pizzaInvitationNoAttendButton") }, "value": str(event_id), "action_id": "rsvp_no", @@ -506,7 +508,7 @@ def send_pizza_invite_answered(self, channel_id, ts, event_id, old_blocks, atten "type": "section", "text": { "type": "plain_text", - "text": f"Du har takket {'ja. Sweet! 🤙' if attending else 'nei. Ok 😕'}", + "text": self.translator.translate("pizzaInviteAnswerAttend") if attending else self.translator.translate("pizzaInviteAnswerNoAttend") , } } ] @@ -518,13 +520,13 @@ def send_pizza_invite_answered(self, channel_id, ts, event_id, old_blocks, atten "type": "section", "text": { "type": "mrkdwn", - "text": "Hvis noe skulle skje så kan du melde deg av ved å klikke på knappen!" + "text": self.translator.translate("unsubscribeBody") }, "accessory": { "type": "button", "text": { "type": "plain_text", - "text": "Meld meg av" + "text": self.translator.translate("unsubscribeButton") }, "value": str(event_id), "action_id": "rsvp_withdraw" @@ -543,7 +545,7 @@ def send_invitation_invalidated_event_cancelled(self, channel_id, ts, old_blocks "type": "section", "text": { "type": "plain_text", - "text": "Arrangementet har blitt avlyst.", + "text": self.translator.translate("invalidatedEventCancelled") } } ] @@ -557,7 +559,7 @@ def send_invitation_expired(self, channel_id, ts, old_blocks, slack_client): "type": "section", "text": { "type": "plain_text", - "text": "Invitasjonen er utløpt.", + "text": self.translator.translate("invitationExpired") } } ] @@ -571,7 +573,7 @@ def send_pizza_invite_withdraw(self, channel_id, ts, old_blocks, slack_client): "type": "section", "text": { "type": "plain_text", - "text": "Du har meldt deg av. Ok 😕", + "text": self.translator.translate("inviteWithdrawn") } } ] @@ -585,7 +587,7 @@ def send_pizza_invite_withdraw_failure(self, channel_id, ts, old_blocks, slack_c "type": "section", "text": { "type": "plain_text", - "text": "Pizza arrangementet er over. Avmelding er ikke mulig.", + "text": self.translator.translate("inviteWithdrawnFailure") } } ] diff --git a/application/bot/src/i18n.py b/application/bot/src/i18n.py new file mode 100644 index 00000000..97492539 --- /dev/null +++ b/application/bot/src/i18n.py @@ -0,0 +1,38 @@ +import json +import os +import glob +from src.injector import injector +import logging +from string import Template + + + +supported_format = ["json"] + +class Translator: + def __init__(self, language_folder="./lang", default_locale="en") -> None: + self.data= {} + self.locale = default_locale + self.logger = injector.get(logging.Logger) + + + for filename in glob.glob(os.path.join(language_folder, '*.json')): + loc = os.path.splitext(os.path.basename(filename))[0] + with open(filename, encoding="utf-8", mode="r") as f: + self.data[loc] = json.load(f) + + def set_locale(self, locale): + if locale in self.data: + self.locale = locale + else: + self.logger.warn(f"Unvalid locale: {locale}, fallback to default locale: {self.locale}") + + def translate(self, key, **kwargs): + if key in self.data[self.locale]: + text = self.data[self.locale][key] + return Template(text).safe_substitute(**kwargs) + else: + self.logger.warn(f"The key '{key}' does not match any text. Defaults text to key") + return key + + diff --git a/application/bot/src/lang/en.json b/application/bot/src/lang/en.json new file mode 100644 index 00000000..6e4f162f --- /dev/null +++ b/application/bot/src/lang/en.json @@ -0,0 +1,31 @@ +{ + "thanksForFile": "Thanks for the file! 🤙", + "pizzaChannelError": "Something went wrong. Couldn't set Pizza channel.", + "pizzaChannelConfirm": "Pizza channel has now been set to <#$channel_id>", + "botWelcome": "Hello! I'm the pizza bot. If you want to change the channel I use, you can go to the appropriate channel and use the command '/set-pizza-channel'. If the channel is private, you need to add me first.", + "eventReminder": "Hey there! I didn't hear any more from you. Are you comming?", + "eventFinalized": "Hello there! $user_ids! You're going to have 🍕 at $restaurant_name, $time_stamp. $booker is booking the table!", + "eventUnfinalized": "Hello there! $user_ids! If the one who withdrew from the visit to $restaurant_name $time_stamp was supposed to book, then one of you others will have to take care of that. Meanwhile, a replacement is being sought.", + "userWithdrawAfterFinalization": "Hello there! <@$user_id> just withdrew from the visit to $restaurant_name $time_stamp.", + "autoReplyNoAttending": "Alright, I guess you can't or don't want to. Hope you'll join next time! 🤞", + "unfinalizedEventCancelled": "Hello there! The visit to $restaurant_name, $time_stamp has been canceled. Sorry!", + "finalizedEventCancelled": "Hello there! $user_ids! The visit to $restaurant_name, $time_stamp has been canceled. Sorry!", + "unfinalizedEventUpdate": "Hello there! The visit to $old_restaurant_name, $old_time_stamp has been updated to $restaurant_name, $time_stamp.", + "finalizedEventUpdate": "Hello there! $user_ids! The visit to $old_restaurant_name, $old_time_stamp has been updated to $restaurant_name, $time_stamp.", + "topLevelPizzaInvitation": "Pizza Invitation: $restaurant_name, $time_stamp", + "pizzaInvitationHeader": "Pizza Invitation", + "pizzaInvitationBody": "You're invited to have :pizza: at $restaurant_name, $time_stamp. Please respond within $deadline hours :pray:. Will you join in?", + "inviteLoading": ":hourglass_flowing_sand: Processing your request...", + "inviteNotAmongUsers": "Couldn't update the invitation. You were not among the invited.", + "pizzaInvitationAttendButton": "Hells yesss!!! 🍕🍕🍕", + "pizzaInvitationNoAttendButton": "Nah ☹️", + "pizzaInviteAnswerAttend": "You've accepted. Sweet! 🤙", + "pizzaInviteAnswerNoAttend": "You've declined. Ok 😕", + "unsubscribeBody": "If something comes up, you can unsubscribe by clicking the button!", + "unsubscribeButton": "Unsubscribe me", + "invalidatedEventCancelled": "The event has been canceled.", + "invitationExpired": "The invitation has expired.", + "inviteWithdrawn": "You've withdrawn. Ok 😕", + "inviteWithdrawnFailure": "The pizza event is over. Withdrawal is not possible." +} + diff --git a/application/bot/src/lang/no.json b/application/bot/src/lang/no.json new file mode 100644 index 00000000..8cbe6fb7 --- /dev/null +++ b/application/bot/src/lang/no.json @@ -0,0 +1,30 @@ +{ + "thanksForFile": "Takk for fil! 🤙", + "pizzaChannelError": "Noe gikk galt. Klarte ikke å sette Pizzakanal", + "pizzaChannelConfirm": "Pizzakanal er nå satt til <#$channel_id>", + "botWelcome": "Hei! Jeg er pizzabot. Hvis dere vil endre hvilke kanal jeg bruker så kan dere gå inn i riktig kanal og bruke kommandoen '/set-pizza-channel'. Hvis kanalen er privat må dere legge meg til først.", + "eventReminder": "Hei du! Jeg hørte ikke noe mer? Er du gira?", + "eventFinalized": "Halloi! $user_ids! Dere skal spise 🍕 på $restaurant_name, $time_stamp. $booker booker bord!", + "eventUnfinalized":"Halloi! $user_ids! Hvis den som meldte seg av besøket til $restaurant_name $time_stamp skulle betale eller booke så må nesten en av dere andre sørge for det. I mellomtiden letes det etter en erstatter.", + "userWithdrawAfterFinalization": "Halloi! <@$user_id> meldte seg nettopp av besøket til $restaurant_name $time_stamp.", + "autoReplyNoAttending":"Neivel, da antar jeg du ikke kan/gidder. Håper du blir med neste gang! 🤞", + "unfinalizedEventCancelled": "Halloi! Besøket til $restaurant_name, $time_stamp har blitt kansellert. Sorry!", + "finalizedEventCnacelled": "Halloi! $user_ids! Besøket til $restaurant_name, $time_stamp har blitt kansellert. Sorry!", + "unfinalizedEventUpdate": "Halloi! Besøket til $old_restaurant_name, $old_time_stamp har blit endret til $restaurant_name, $time_stamp.", + "finalizedEvnetUpdate": "Halloi! $user_ids! Besøket til $old_restaurant_name, $old_time_stamp har blit endret til $restaurant_name, $time_stamp.", + "topLevelPizzaInvitation": "Pizzainvitasjon: $restaurant_name$, $time_stamp", + "pizzaInvitationHeader":"Pizzainvitasjon", + "pizzaInvitationBody": "Du er invitert til :pizza: på $restaurant_name, $time_stamp. Pls svar innen $deadline timer :pray:. Kan du?", + "inviteLoading": ":hourglass_flowing_sand: Behandler forespørselen din...", + "inviteNotAmongUsers":"Kunne ikke oppdatere invitasjonen. Du var ikke blant de inviterte.", + "pizzaInvitationAttendButton": "Hells yesss!!! 🍕🍕🍕", + "pizzaInvitationNoAttendButton":"Nah ☹️", + "pizzaInviteAnswerAttend":"Du har takket ja. Sweet! 🤙", + "pizzaInviteAnswerNoAttend":"Du har takket nei. Ok 😕", + "unsubscribeBody": "Hvis noe skulle skje så kan du melde deg av ved å klikke på knappen!", + "unsubscribeButton":"Meld meg av", + "invalidatedEventCacelled": "Arrangementet har blitt avlyst.", + "invitationExpired":"Invitasjonen er utløpt.", + "inviteWithdrawn": "Du har meldt deg av. Ok 😕", + "inviteWithdrawnFailure":"Arrangementet er over. Avmelding er ikke mulig." +} \ No newline at end of file diff --git a/application/bot/src/slack/__init__.py b/application/bot/src/slack/__init__.py index 29ec4257..3898e483 100644 --- a/application/bot/src/slack/__init__.py +++ b/application/bot/src/slack/__init__.py @@ -13,12 +13,14 @@ from src.injector import injector from src.slack.installation_store import BrokerInstallationStore from src.api.slack_api import SlackApi +from src.i18n import Translator slack_signing_secret = os.environ["SLACK_SIGNING_SECRET"] client_id = os.environ["SLACK_CLIENT_ID"], client_secret = os.environ["SLACK_CLIENT_SECRET"], slack_app_token = os.environ["SLACK_APP_TOKEN"] + slack_app = App( signing_secret=slack_signing_secret, installation_store=BrokerInstallationStore(), @@ -138,11 +140,12 @@ def handle_rsvp_withdraw(ack, body, context): def handle_file_share(event, say, token, client): + translator = injector.get(Translator) channel = event["channel"] if 'files' in event and 'thread_ts' not in event: files = event['files'] with injector.get(BotApi) as ba: - ba.send_slack_message(channel_id=channel, text=u'Takk for fil! 🤙', slack_client=client) + ba.send_slack_message(channel_id=channel, text=translator.translate("thanksForFile"), slack_client=client) headers = {u'Authorization': u'Bearer %s' % token} for file in files: r = requests.get( @@ -163,6 +166,7 @@ def handle_file_share(event, say, token, client): @slack_app.command("/set-pizza-channel") def handle_some_command(ack, body, say, context): + translator = injector.get(Translator) ack() with injector.get(BotApi) as ba: team_id = body["team_id"] @@ -172,13 +176,13 @@ def handle_some_command(ack, body, say, context): if channel_id is None: ba.send_slack_message( channel_id=message_channel_id, - text='Noe gikk galt. Klarte ikke å sette Pizza kanal', + text=translator.translate("pizzaChannelError"), slack_client=client ) else: ba.send_slack_message( channel_id=channel_id, - text='Pizza kanal er nå satt til <#%s>' % channel_id, + text=translator.translate("pizzaChannelConfirm", channel_id=channel_id), slack_client=client )