From e7218ccb4e5bc1662f1a9c28a6547ef81732a3be Mon Sep 17 00:00:00 2001 From: Kerri Salciccioli Date: Fri, 10 Jul 2020 23:00:59 -0400 Subject: [PATCH 1/6] i18n init --- MrMouse.py | 5 +++ cogs/role.py | 100 ++++++++++++++++++++------------------------ i18n/general.en.yml | 9 ++++ i18n/role.en.yml | 46 ++++++++++++++++++++ 4 files changed, 105 insertions(+), 55 deletions(-) create mode 100644 i18n/general.en.yml create mode 100644 i18n/role.en.yml diff --git a/MrMouse.py b/MrMouse.py index 67d37b1..8b6cf80 100644 --- a/MrMouse.py +++ b/MrMouse.py @@ -2,6 +2,11 @@ from settings.config import TOKEN, PREFIX +import i18n + +i18n.load_path.append("i18n/") +i18n.config.set('fallback', 'en') + bot = Bot(command_prefix=PREFIX, fetch_offline_members=True) extensions = [ diff --git a/cogs/role.py b/cogs/role.py index 2ef4519..81d208b 100644 --- a/cogs/role.py +++ b/cogs/role.py @@ -1,5 +1,4 @@ import operator -from typing import Union import discord from discord.ext import commands @@ -7,9 +6,10 @@ from settings.config import settings from settings.constants import ASSIGNABLE_ROLE_COLORS, NATIVE_COLOR, YES_EMOJI, INFO_COLOR, MAIN_COLOR, NO_EMOJI, \ ADMIN_ROLES, LEARNING_COLOR, FLUENT_COLOR -from settings.lines import text_lines from utils.utils import send_error_embed, chunks +import i18n + # Basically, this lets us specify that a command requires a *valid role* as an argument. # We inherit RoleConverter, which makes sure that the passed argument is a *valid role*. @@ -78,21 +78,21 @@ async def role_command(self, ctx: commands.Context, *, role: LinglotLanguageRole # noinspection PyTypeChecker if not self.is_assignable_role(role): # Role isn't self-assignable - return await send_error_embed(ctx, text_lines['roles']['assign']['not_allowed']) + return await send_error_embed(ctx, i18n.t('role.assign.not_allowed')) if role in ctx.author.roles: if self.native_role_count(ctx.author) == 1 and role.color.value == NATIVE_COLOR: # User is trying to remove their only native role - return await send_error_embed(ctx, text_lines['roles']['assign']['cant_remove_native']) + return await send_error_embed(ctx, i18n.t('role.assign.native.cant_remove')) else: return await self.yes_no_dialogue(ctx, role) else: if self.native_role_count(ctx.author) == 0 and role.color.value != NATIVE_COLOR: # User does not have a native role, they should add one first! - return await send_error_embed(ctx, text_lines['roles']['assign']['native_first']) + return await send_error_embed(ctx, i18n.t('role.assign.native.native_first')) # add the user to the role, then let them know it was successful await ctx.author.add_roles(role, reason='self-added') - embed = discord.Embed(title=text_lines['roles']['assign']['added'].format(role.name), + embed = discord.Embed(title=i18n.t('role.assign.added', role=role), colour=discord.Colour(MAIN_COLOR)) await ctx.send(embed=embed) @@ -102,18 +102,18 @@ async def role_remove_command(self, ctx: commands.Context, *, role: LinglotRole) # noinspection PyTypeChecker if not self.is_assignable_role(role): # Role isn't self-assignable - return await send_error_embed(ctx, text_lines['roles']['assign']['not_allowed']) + return await send_error_embed(ctx, i18n.t('role.assign.not_allowed')) if role not in ctx.author.roles: # User does not have the role - return await send_error_embed(ctx, text_lines['roles']['assign']['dont_have']) + return await send_error_embed(ctx, i18n.t('role.assign.dont_have')) if self.native_role_count(ctx.author) == 1 and role.color.value == NATIVE_COLOR: # User is trying to remove their only native role - return await send_error_embed(ctx, text_lines['roles']['assign']['cant_remove_native']) + return await send_error_embed(ctx, i18n.t('role.assign.native.cant_remove')) await ctx.author.remove_roles(role, reason='self-removed') - embed = discord.Embed(title=text_lines['roles']['assign']['removed'].format(role.name), + embed = discord.Embed(title=i18n.t('role.assign.removed', role=role.name), color=discord.Color(MAIN_COLOR)) await ctx.send(embed=embed) @@ -127,22 +127,21 @@ async def role_error(self, ctx: commands.Context, error): # User didn't provide any roles elif isinstance(error, commands.MissingRequiredArgument): - await send_error_embed(ctx, text_lines['roles']['assign']['empty']) + await send_error_embed(ctx, i18n.t('general.commands.roles_not_provided')) # User provided a non-existent role to ;role or ;fluent or ;native elif isinstance(error, commands.BadUnionArgument): role: str = ctx.message.content.replace(ctx.prefix + ctx.invoked_with + ' ', '') if ctx.invoked_with in ['fluent', 'native']: role = ctx.invoked_with + ' ' + role - await send_error_embed(ctx, text_lines['roles']['search']['no_role'].format(role.title())) + await send_error_embed(ctx, i18n.t('general.commands.roles_dont_exist', count=1)) @commands.command(name='count') @commands.guild_only() async def count_command(self, ctx: commands.Context, *, roles: LinglotRoleList): # noinspection PyTypeChecker if len(roles) > settings['roles']['search']['limit']: - return await send_error_embed(ctx, text_lines['roles']['search']['limit'] - .format(settings['roles']['search']['limit'])) + return await send_error_embed(ctx, i18n.t('role.search.limit', max=settings['roles']['search']['limit'])) # Find total users who have *all* of the provided roles total = sum(roles.issubset(user.roles) for user in ctx.guild.members) @@ -153,16 +152,13 @@ async def count_command(self, ctx: commands.Context, *, roles: LinglotRoleList): # Generate a list of how many users have *each* role for role in roles: users_in_role = sum(role in user.roles for user in ctx.guild.members) - embed_body += text_lines['roles']['count']['total'].format(role.name, users_in_role) + # embed_body += text_lines['roles']['count']['total'].format(role.name, users_in_role) + embed_body += i18n.t('role.count.total', count=users_in_role, role=role.name) + '\n' # noinspection PyTypeChecker role_names = ', '.join(role.name for role in roles) - if total <= 0: - embed_title = text_lines['roles']['count']['no_users'].format(role_names) - elif total == 1: - embed_title = text_lines['roles']['count']['one_user'].format(role_names) - else: - embed_title = text_lines['roles']['count']['x_users'].format(total, role_names) + + embed_title = i18n.t('role.count.users', count=total, role_list=role_names) embed = discord.Embed(title=embed_title, color=discord.Color(MAIN_COLOR)) @@ -184,31 +180,24 @@ async def search_command(self, ctx: commands.Context, *, roles: LinglotRoleList) title_line = ", ".join(role.name for role in roles) if not len(found): - no_results = discord.Embed(title=text_lines['roles']['search']['no_users_title'], - description=text_lines['roles']['search']['try_again'], + no_results = discord.Embed(title=i18n.t('role.search.no_users'), + description=i18n.t('role.search.try_again'), colour=discord.Colour(MAIN_COLOR)) return await ctx.send(embed=no_results) - elif len(found) == 1: - title = text_lines['roles']['search']['one_user'].format(title_line) - else: - title = text_lines['roles']['search']['x_users'].format(len(found), title_line) + title = i18n.t('role.search.users.body', count=found, role_list=title_line) embed = discord.Embed(title=title, color=discord.Colour(MAIN_COLOR)) if len(found) < 6: - if len(found) == 1: - header = text_lines['roles']['search']['one_user_header'] - else: - header = text_lines['roles']['search']['many_users_header'].format(1, len(found)) - + header = i18n.t('role.search.users.header', count=len(found), start=1, end=len(found)) embed.add_field(name=header, value="\n".join(member.display_name for member in found), inline=True) else: if len(found) > 30: - embed.set_footer(text=text_lines['roles']['search']['and_more'].format(len(found) - 30)) + embed.set_footer(text=i18n.t('role.search.users.more', count=len(found) - 30)) chunked_list = list(chunks(found[:30], 10)) ranges = [[found.index(chunk[0]) + 1, found.index(chunk[-1]) + 1] for chunk in chunked_list] for i in range(0, len(chunked_list)): - header = text_lines['roles']['search']['many_users_header'].format(*ranges[i]) + header = i18n.t('role.search.users.header', count=2, start=ranges[i][0], end=ranges[i][1]) members = "\n".join( member.display_name[:settings['roles']['search']['max_length']] for member in chunked_list[i] ) @@ -221,11 +210,11 @@ async def search_command(self, ctx: commands.Context, *, roles: LinglotRoleList) async def role_search_error(self, ctx: commands.Context, error): # user provided one or more non-existent roles if isinstance(error, commands.BadArgument): - await send_error_embed(ctx, 'Sorry, one or more of those roles does not exist!') + await send_error_embed(ctx, i18n.t('general.commands.roles_dont_exist', count=1)) # user provided no roles elif isinstance(error, commands.MissingRequiredArgument): - await send_error_embed(ctx, 'You must provide one or more roles for this command!') + await send_error_embed(ctx, i18n.t('general.commands.roles_not_provided')) # we specify cooldown_after_parsing=True so that ping commands that fail due to non-existent roles, or no arguments, # don't trigger a cooldown, @@ -237,20 +226,21 @@ async def ping_command(self, ctx: commands.Context, *, roles: LinglotRoleList): # Make sure the user is not trying to ping any blacklisted roles # noinspection PyTypeChecker if any([role.name in settings['roles']['ping']['blacklist'] for role in roles]): - return await send_error_embed(ctx, 'Sorry, one or more of those roles is not ping-able!') + return await send_error_embed(ctx, i18n.t('role.ping.not_allowed')) + role_list = ', '.join(role.mention for role in roles) # noinspection PyTypeChecker - await ctx.send(f"PING! {ctx.author.mention} is pinging {', '.join(role.mention for role in roles)}") + await ctx.send(i18n.t('role.ping.body', author=ctx.author.mention, role_list=role_list)) @ping_command.error async def ping_error(self, ctx: commands.Context, error): # User did not provide a valid list of roles if isinstance(error, commands.BadArgument): - await send_error_embed(ctx, 'Sorry, one or more of those roles does not exist!') + await send_error_embed(ctx, i18n.t('general.commands.roles_dont_exist', count=2)) # user provided no roles elif isinstance(error, commands.MissingRequiredArgument): - await send_error_embed(ctx, 'Sorry, you need to provide at least one role to ping') + await send_error_embed(ctx, i18n.t('role.ping.missing_roles')) # User is hitting cooldown for this command elif isinstance(error, commands.CommandOnCooldown): @@ -259,7 +249,7 @@ async def ping_error(self, ctx: commands.Context, error): # ... then bypass the cooldown and let them use it! await ctx.reinvoke() else: - await send_error_embed(ctx, 'Sorry, this command is on cooldown!') + await send_error_embed(ctx, i18n.t('general.commands.on_cooldown')) @commands.command(name='top10', aliases=['top']) @commands.guild_only() @@ -273,11 +263,11 @@ async def top_roles_command(self, ctx: commands.Context): embed = discord.Embed(colour=discord.Colour(MAIN_COLOR)) embed.set_author(name=server.name, icon_url=server.icon_url) - embed.add_field(name="Natives", value=native, inline=True) - embed.add_field(name="Fluent", value=fluent, inline=True) - embed.add_field(name="Learning", value=learning, inline=True) + embed.add_field(name=i18n.t('role.top10.native'), value=native, inline=True) + embed.add_field(name=i18n.t('role.top10.fluent'), value=fluent, inline=True) + embed.add_field(name=i18n.t('role.top10.learning'), value=learning, inline=True) - embed.set_footer(text="Out of {} roles and {} members".format(len(roles), len(server.members)), + embed.set_footer(text=i18n.t('role.top10.footer', role_count=len(roles), member_count=len(server.members)), icon_url=self.bot.user.avatar_url) await ctx.send(embed=embed) @@ -289,7 +279,7 @@ async def less_than_command(self, ctx: commands.Context, target_size: int): # make sure the target role size is within the right limits if target_size <= 0 or target_size > settings['roles']['less_than']['limit']: - return await send_error_embed(ctx, 'Number out of range') + return await send_error_embed(ctx, i18n.t('general.commands.out_of_range')) # generate the list, greatest to least, of the roles with less than `target_size` members output = {role.name: len(role.members) for role in ctx.guild.roles if len(role.members) < target_size} @@ -303,7 +293,7 @@ async def less_than_command(self, ctx: commands.Context, target_size: int): line = line[:1996] + '...' embed = discord.Embed(colour=discord.Colour(MAIN_COLOR), - title=text_lines['roles']['less_than']['title'].format(target_size), + title=i18n.t('role.less.title', count=target_size), description=line) await ctx.send(embed=embed) @@ -311,13 +301,13 @@ async def less_than_command(self, ctx: commands.Context, target_size: int): @less_than_command.error async def lessthan_error(self, ctx, error): if isinstance(error, commands.BadArgument): - return await send_error_embed(ctx, 'You must pass a number to this command') + return await send_error_embed(ctx, i18n.t('general.commands.must_pass_number')) @commands.command(name='list') @commands.guild_only() async def list_command(self, ctx: commands.Context, role_type: str): if not role_type.lower() in ['native', 'fluent', 'learning']: - return await send_error_embed(ctx, "You must specify 'native', 'fluent', or 'learning'.") + return await send_error_embed(ctx, i18n.t('role.list.must_specify_type')) role_map = { 'native': NATIVE_COLOR, @@ -335,16 +325,16 @@ async def list_command(self, ctx: commands.Context, role_type: str): role.color.value == NATIVE_COLOR and not role.name.startswith('Native')] roles += combined_roles roles.sort() - return await ctx.send(content=f"**{role_type} roles: **" + ", ".join(roles)) + return await ctx.send(content=i18n.t('role.list.body', role_type=role_type, role_list=", ".join(roles))) @list_command.error async def list_error(self, ctx: commands.Context, error): - await send_error_embed(ctx, "You must specify 'native', 'fluent', or 'learning'.") + return await send_error_embed(ctx, i18n.t('role.list.must_specify_type')) async def yes_no_dialogue(self, ctx, role): # TODO: fix this trash fire - embed = discord.Embed(title=text_lines['roles']['assign']['already_have_title'], - description=text_lines['roles']['assign']['already_have_msg'], + embed = discord.Embed(title=i18n.t('role.assign.already_have.title'), + description=i18n.t('role.assign.already_have.body'), colour=discord.Colour(INFO_COLOR)) msg = await ctx.send(embed=embed) await msg.add_reaction(YES_EMOJI) @@ -359,10 +349,10 @@ async def yes_no_dialogue(self, ctx, role): else: if reaction.emoji == YES_EMOJI: await user.remove_roles(role, reason='self-removed') - embed = discord.Embed(title=text_lines['roles']['assign']['removed'].format(role.name), + embed = discord.Embed(title=i18n.t('role.assign.removed', role=role.name), colour=discord.Colour(MAIN_COLOR)) else: - embed = discord.Embed(title=text_lines['roles']['assign']['keep'].format(role.name), + embed = discord.Embed(title=i18n.t('role.assign.keep', role=role.name), colour=discord.Colour(MAIN_COLOR)) await msg.edit(embed=embed) diff --git a/i18n/general.en.yml b/i18n/general.en.yml new file mode 100644 index 0000000..4bdbec8 --- /dev/null +++ b/i18n/general.en.yml @@ -0,0 +1,9 @@ +en: + commands: + on_cooldown: Sorry, this command is on cooldown! + roles_dont_exist: + one: Sorry, that role does not exist! + many: Sorry, one or more of those roles does not exist! + roles_not_provided: You must provide one or more roles for this command! + out_of_range: Number out of range + must_pass_number: You must pass a number to this command \ No newline at end of file diff --git a/i18n/role.en.yml b/i18n/role.en.yml new file mode 100644 index 0000000..397d86b --- /dev/null +++ b/i18n/role.en.yml @@ -0,0 +1,46 @@ +en: + list: + must_specify_type: You must specify 'native', 'fluent', or 'learning'. + body: "**%{role_type} roles**: %{role_list}" + assign: + added: The role **%{role}** has been added + removed: The role **%{role}** has been removed + keep: You already had **%{role}** and opted to keep it + already_have: + title: You already have this role + body: React with ✅ to remove it or react with ❌ if you don't want to do that. _Ignoring this message is also an option if you don't want to remove it_ + not_allowed: You can't self-assign this role + dont_have: You don't have that role + native: + cant_remove: Did you add the wrong native tag? Add your actual native language first, then remove the incorrect tag using ;not + need_role: "**You need to tag your native language first**. Once you have a native tag, you can add learning and fluent tags." + search: + limit: You have to search at least 1 role, up to a maximum of %{max} + role_doesnt_exist: The role **%{role}** does not exist on the server. + no_users: No users were found + try_again: Please try again + users: + body: + one: 1 user matching %{role_list} + many: "%{count} users matching %{role_list}" + header: + one: The one and only + many: "%{start} - %{end}" + more: And %{count} more... + count: + users: + zero: No users match the combination %{role_list} + one: 1 user matches the combination %{role_list} + many: "%{count} users match the combination %{role_list}" + total: Total in %{role} - **%{count}** + ping: + not_allowed: Sorry, you cannot ping 1 or more of those roles! + missing_roles: Sorry, you need to provide at least one role to ping! + body: PING! %{author} is pinging %{role_list} + top10: + native: Native + fluent: Fluent + learning: Learning + footer: Out of %{role_count} roles and %{member_count} members + less: + title: Roles with fewer than %{count} members From 9f968c0b2260271cce6041dcc06e7f30769c1b7c Mon Sep 17 00:00:00 2001 From: Kerri Salciccioli Date: Sat, 11 Jul 2020 00:17:34 -0400 Subject: [PATCH 2/6] add crowdin configuration --- crowdin.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 crowdin.yml diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000..703a2d5 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,5 @@ +files: + - source: /i18n/*.en.yml + translation: '/i18n/%file_name%%two_letters_code%.yml' + translation_replace: + '.en': '.' \ No newline at end of file From 055236e8f88e8b7c265d1a24d0f74649f5d6b342 Mon Sep 17 00:00:00 2001 From: Kerri Salciccioli Date: Sun, 12 Jul 2020 16:14:40 -0400 Subject: [PATCH 3/6] i18n updates --- cogs/role.py | 124 +++++++++++++++-------- crowdin.yml | 4 +- events/on_error.py | 8 +- i18n/{general.en.yml => general.eng.yml} | 0 i18n/{role.en.yml => role.eng.yml} | 2 +- settings/constants.py | 86 ++++++++++++++++ 6 files changed, 179 insertions(+), 45 deletions(-) rename i18n/{general.en.yml => general.eng.yml} (100%) rename i18n/{role.en.yml => role.eng.yml} (95%) diff --git a/cogs/role.py b/cogs/role.py index 81d208b..66be8dd 100644 --- a/cogs/role.py +++ b/cogs/role.py @@ -5,7 +5,7 @@ from settings.config import settings from settings.constants import ASSIGNABLE_ROLE_COLORS, NATIVE_COLOR, YES_EMOJI, INFO_COLOR, MAIN_COLOR, NO_EMOJI, \ - ADMIN_ROLES, LEARNING_COLOR, FLUENT_COLOR + ADMIN_ROLES, LEARNING_COLOR, FLUENT_COLOR, LANGUAGE_CODES from utils.utils import send_error_embed, chunks import i18n @@ -75,45 +75,47 @@ def __init__(self, bot): @commands.command(name='role', aliases=['native', 'fluent', 'learning']) @commands.guild_only() async def role_command(self, ctx: commands.Context, *, role: LinglotLanguageRole2): + lang = self.get_user_locale(ctx.author) # noinspection PyTypeChecker if not self.is_assignable_role(role): # Role isn't self-assignable - return await send_error_embed(ctx, i18n.t('role.assign.not_allowed')) + return await send_error_embed(ctx, i18n.t('role.assign.not_allowed', locale=lang)) if role in ctx.author.roles: if self.native_role_count(ctx.author) == 1 and role.color.value == NATIVE_COLOR: # User is trying to remove their only native role - return await send_error_embed(ctx, i18n.t('role.assign.native.cant_remove')) + return await send_error_embed(ctx, i18n.t('role.assign.native.cant_remove', locale=lang)) else: return await self.yes_no_dialogue(ctx, role) else: if self.native_role_count(ctx.author) == 0 and role.color.value != NATIVE_COLOR: # User does not have a native role, they should add one first! - return await send_error_embed(ctx, i18n.t('role.assign.native.native_first')) + return await send_error_embed(ctx, i18n.t('role.assign.native.native_first', locale=lang)) # add the user to the role, then let them know it was successful await ctx.author.add_roles(role, reason='self-added') - embed = discord.Embed(title=i18n.t('role.assign.added', role=role), + embed = discord.Embed(title=i18n.t('role.assign.added', role=role, locale=lang), colour=discord.Colour(MAIN_COLOR)) await ctx.send(embed=embed) @commands.command(name='not') @commands.guild_only() async def role_remove_command(self, ctx: commands.Context, *, role: LinglotRole): + lang = self.get_user_locale(ctx.author) # noinspection PyTypeChecker if not self.is_assignable_role(role): # Role isn't self-assignable - return await send_error_embed(ctx, i18n.t('role.assign.not_allowed')) + return await send_error_embed(ctx, i18n.t('role.assign.not_allowed', locale=lang)) if role not in ctx.author.roles: # User does not have the role - return await send_error_embed(ctx, i18n.t('role.assign.dont_have')) + return await send_error_embed(ctx, i18n.t('role.assign.dont_have', locale=lang)) if self.native_role_count(ctx.author) == 1 and role.color.value == NATIVE_COLOR: # User is trying to remove their only native role - return await send_error_embed(ctx, i18n.t('role.assign.native.cant_remove')) + return await send_error_embed(ctx, i18n.t('role.assign.native.cant_remove', locale=lang)) await ctx.author.remove_roles(role, reason='self-removed') - embed = discord.Embed(title=i18n.t('role.assign.removed', role=role.name), + embed = discord.Embed(title=i18n.t('role.assign.removed', role=role.name, locale=lang), color=discord.Color(MAIN_COLOR)) await ctx.send(embed=embed) @@ -121,27 +123,32 @@ async def role_remove_command(self, ctx: commands.Context, *, role: LinglotRole) @role_command.error @role_remove_command.error async def role_error(self, ctx: commands.Context, error): + lang = self.get_user_locale(ctx.author) + # User provided a non-existent role to ;not if isinstance(error, commands.BadArgument): await send_error_embed(ctx, error.args[0]) # User didn't provide any roles elif isinstance(error, commands.MissingRequiredArgument): - await send_error_embed(ctx, i18n.t('general.commands.roles_not_provided')) + await send_error_embed(ctx, i18n.t('general.commands.roles_not_provided', locale=lang)) # User provided a non-existent role to ;role or ;fluent or ;native elif isinstance(error, commands.BadUnionArgument): role: str = ctx.message.content.replace(ctx.prefix + ctx.invoked_with + ' ', '') if ctx.invoked_with in ['fluent', 'native']: role = ctx.invoked_with + ' ' + role - await send_error_embed(ctx, i18n.t('general.commands.roles_dont_exist', count=1)) + await send_error_embed(ctx, i18n.t('general.commands.roles_dont_exist', count=1, locale=lang)) @commands.command(name='count') @commands.guild_only() async def count_command(self, ctx: commands.Context, *, roles: LinglotRoleList): + lang = self.get_user_locale(ctx.author) + # noinspection PyTypeChecker if len(roles) > settings['roles']['search']['limit']: - return await send_error_embed(ctx, i18n.t('role.search.limit', max=settings['roles']['search']['limit'])) + return await send_error_embed(ctx, i18n.t('role.search.limit', max=settings['roles']['search']['limit'], + locale=lang)) # Find total users who have *all* of the provided roles total = sum(roles.issubset(user.roles) for user in ctx.guild.members) @@ -153,12 +160,12 @@ async def count_command(self, ctx: commands.Context, *, roles: LinglotRoleList): for role in roles: users_in_role = sum(role in user.roles for user in ctx.guild.members) # embed_body += text_lines['roles']['count']['total'].format(role.name, users_in_role) - embed_body += i18n.t('role.count.total', count=users_in_role, role=role.name) + '\n' + embed_body += i18n.t('role.count.total', count=users_in_role, role=role.name, locale=lang) + '\n' # noinspection PyTypeChecker role_names = ', '.join(role.name for role in roles) - embed_title = i18n.t('role.count.users', count=total, role_list=role_names) + embed_title = i18n.t('role.count.users', count=total, role_list=role_names, locale=lang) embed = discord.Embed(title=embed_title, color=discord.Color(MAIN_COLOR)) @@ -172,6 +179,8 @@ async def count_command(self, ctx: commands.Context, *, roles: LinglotRoleList): @commands.command(name='search', aliases=['who', 'inrole', 'inroles']) @commands.guild_only() async def search_command(self, ctx: commands.Context, *, roles: LinglotRoleList): + lang = self.get_user_locale(ctx.author) + found = [] for member in ctx.guild.members: if roles.issubset(member.roles): @@ -180,24 +189,24 @@ async def search_command(self, ctx: commands.Context, *, roles: LinglotRoleList) title_line = ", ".join(role.name for role in roles) if not len(found): - no_results = discord.Embed(title=i18n.t('role.search.no_users'), - description=i18n.t('role.search.try_again'), + no_results = discord.Embed(title=i18n.t('role.search.no_users', locale=lang), + description=i18n.t('role.search.try_again', locale=lang), colour=discord.Colour(MAIN_COLOR)) return await ctx.send(embed=no_results) - title = i18n.t('role.search.users.body', count=found, role_list=title_line) + title = i18n.t('role.search.users.body', count=found, role_list=title_line, locale=lang) embed = discord.Embed(title=title, color=discord.Colour(MAIN_COLOR)) if len(found) < 6: - header = i18n.t('role.search.users.header', count=len(found), start=1, end=len(found)) + header = i18n.t('role.search.users.header', count=len(found), start=1, end=len(found), locale=lang) embed.add_field(name=header, value="\n".join(member.display_name for member in found), inline=True) else: if len(found) > 30: - embed.set_footer(text=i18n.t('role.search.users.more', count=len(found) - 30)) + embed.set_footer(text=i18n.t('role.search.users.more', count=len(found) - 30, locale=lang)) chunked_list = list(chunks(found[:30], 10)) ranges = [[found.index(chunk[0]) + 1, found.index(chunk[-1]) + 1] for chunk in chunked_list] for i in range(0, len(chunked_list)): - header = i18n.t('role.search.users.header', count=2, start=ranges[i][0], end=ranges[i][1]) + header = i18n.t('role.search.users.header', count=2, start=ranges[i][0], end=ranges[i][1], locale=lang) members = "\n".join( member.display_name[:settings['roles']['search']['max_length']] for member in chunked_list[i] ) @@ -208,13 +217,15 @@ async def search_command(self, ctx: commands.Context, *, roles: LinglotRoleList) @count_command.error @search_command.error async def role_search_error(self, ctx: commands.Context, error): + lang = self.get_user_locale(ctx.author) + # user provided one or more non-existent roles if isinstance(error, commands.BadArgument): - await send_error_embed(ctx, i18n.t('general.commands.roles_dont_exist', count=1)) + await send_error_embed(ctx, i18n.t('general.commands.roles_dont_exist', count=1, locale=lang)) # user provided no roles elif isinstance(error, commands.MissingRequiredArgument): - await send_error_embed(ctx, i18n.t('general.commands.roles_not_provided')) + await send_error_embed(ctx, i18n.t('general.commands.roles_not_provided', locale=lang)) # we specify cooldown_after_parsing=True so that ping commands that fail due to non-existent roles, or no arguments, # don't trigger a cooldown, @@ -223,10 +234,12 @@ async def role_search_error(self, ctx: commands.Context, error): @commands.cooldown(rate=1, per=settings['roles']['ping']['cooldown'], type=commands.BucketType.user) @commands.guild_only() async def ping_command(self, ctx: commands.Context, *, roles: LinglotRoleList): + lang = self.get_user_locale(ctx.author) + # Make sure the user is not trying to ping any blacklisted roles # noinspection PyTypeChecker if any([role.name in settings['roles']['ping']['blacklist'] for role in roles]): - return await send_error_embed(ctx, i18n.t('role.ping.not_allowed')) + return await send_error_embed(ctx, i18n.t('role.ping.not_allowed', locale=lang)) role_list = ', '.join(role.mention for role in roles) # noinspection PyTypeChecker @@ -234,13 +247,15 @@ async def ping_command(self, ctx: commands.Context, *, roles: LinglotRoleList): @ping_command.error async def ping_error(self, ctx: commands.Context, error): + lang = self.get_user_locale(ctx.author) + # User did not provide a valid list of roles if isinstance(error, commands.BadArgument): - await send_error_embed(ctx, i18n.t('general.commands.roles_dont_exist', count=2)) + await send_error_embed(ctx, i18n.t('general.commands.roles_dont_exist', count=2, locale=lang)) # user provided no roles elif isinstance(error, commands.MissingRequiredArgument): - await send_error_embed(ctx, i18n.t('role.ping.missing_roles')) + await send_error_embed(ctx, i18n.t('role.ping.missing_roles', locale=lang)) # User is hitting cooldown for this command elif isinstance(error, commands.CommandOnCooldown): @@ -249,11 +264,13 @@ async def ping_error(self, ctx: commands.Context, error): # ... then bypass the cooldown and let them use it! await ctx.reinvoke() else: - await send_error_embed(ctx, i18n.t('general.commands.on_cooldown')) + await send_error_embed(ctx, i18n.t('general.commands.on_cooldown', locale=lang)) @commands.command(name='top10', aliases=['top']) @commands.guild_only() async def top_roles_command(self, ctx: commands.Context): + lang = self.get_user_locale(ctx.author) + server = ctx.message.guild roles = {role.name: len(role.members) for role in server.roles} @@ -263,12 +280,14 @@ async def top_roles_command(self, ctx: commands.Context): embed = discord.Embed(colour=discord.Colour(MAIN_COLOR)) embed.set_author(name=server.name, icon_url=server.icon_url) - embed.add_field(name=i18n.t('role.top10.native'), value=native, inline=True) - embed.add_field(name=i18n.t('role.top10.fluent'), value=fluent, inline=True) - embed.add_field(name=i18n.t('role.top10.learning'), value=learning, inline=True) + embed.add_field(name=i18n.t('role.top10.native', locale=lang), value=native, inline=True) + embed.add_field(name=i18n.t('role.top10.fluent', locale=lang), value=fluent, inline=True) + embed.add_field(name=i18n.t('role.top10.learning', locale=lang), value=learning, inline=True) - embed.set_footer(text=i18n.t('role.top10.footer', role_count=len(roles), member_count=len(server.members)), - icon_url=self.bot.user.avatar_url) + embed.set_footer( + text=i18n.t('role.top10.footer', role_count=len(roles), member_count=len(server.members), locale=lang), + icon_url=self.bot.user.avatar_url + ) await ctx.send(embed=embed) @@ -276,10 +295,11 @@ async def top_roles_command(self, ctx: commands.Context): @commands.has_any_role(*ADMIN_ROLES) @commands.guild_only() async def less_than_command(self, ctx: commands.Context, target_size: int): + lang = self.get_user_locale(ctx.author) # make sure the target role size is within the right limits if target_size <= 0 or target_size > settings['roles']['less_than']['limit']: - return await send_error_embed(ctx, i18n.t('general.commands.out_of_range')) + return await send_error_embed(ctx, i18n.t('general.commands.out_of_range', locale=lang)) # generate the list, greatest to least, of the roles with less than `target_size` members output = {role.name: len(role.members) for role in ctx.guild.roles if len(role.members) < target_size} @@ -293,21 +313,26 @@ async def less_than_command(self, ctx: commands.Context, target_size: int): line = line[:1996] + '...' embed = discord.Embed(colour=discord.Colour(MAIN_COLOR), - title=i18n.t('role.less.title', count=target_size), + title=i18n.t('role.less.title', count=target_size, locale=lang), description=line) await ctx.send(embed=embed) @less_than_command.error async def lessthan_error(self, ctx, error): + lang = self.get_user_locale(ctx.author) + if isinstance(error, commands.BadArgument): - return await send_error_embed(ctx, i18n.t('general.commands.must_pass_number')) + return await send_error_embed(ctx, i18n.t('general.commands.must_pass_number', locale=lang)) @commands.command(name='list') @commands.guild_only() async def list_command(self, ctx: commands.Context, role_type: str): + lang = self.get_user_locale(ctx.author) + if not role_type.lower() in ['native', 'fluent', 'learning']: - return await send_error_embed(ctx, i18n.t('role.list.must_specify_type')) + return await send_error_embed(ctx, i18n.t('role.list.must_specify_type', + locale=lang) + ' native, fluent, learning') role_map = { 'native': NATIVE_COLOR, @@ -325,16 +350,22 @@ async def list_command(self, ctx: commands.Context, role_type: str): role.color.value == NATIVE_COLOR and not role.name.startswith('Native')] roles += combined_roles roles.sort() - return await ctx.send(content=i18n.t('role.list.body', role_type=role_type, role_list=", ".join(roles))) + return await ctx.send( + content=i18n.t('role.list.body', role_type=role_type, role_list=", ".join(roles), locale=lang)) @list_command.error async def list_error(self, ctx: commands.Context, error): - return await send_error_embed(ctx, i18n.t('role.list.must_specify_type')) + lang = self.get_user_locale(ctx.author) + + return await send_error_embed(ctx, i18n.t('role.list.must_specify_type', + locale=lang) + ' native, fluent, learning') async def yes_no_dialogue(self, ctx, role): + lang = self.get_user_locale(ctx.author) + # TODO: fix this trash fire - embed = discord.Embed(title=i18n.t('role.assign.already_have.title'), - description=i18n.t('role.assign.already_have.body'), + embed = discord.Embed(title=i18n.t('role.assign.already_have.title', locale=lang), + description=i18n.t('role.assign.already_have.body', locale=lang), colour=discord.Colour(INFO_COLOR)) msg = await ctx.send(embed=embed) await msg.add_reaction(YES_EMOJI) @@ -349,10 +380,10 @@ async def yes_no_dialogue(self, ctx, role): else: if reaction.emoji == YES_EMOJI: await user.remove_roles(role, reason='self-removed') - embed = discord.Embed(title=i18n.t('role.assign.removed', role=role.name), + embed = discord.Embed(title=i18n.t('role.assign.removed', role=role.name, locale=lang), colour=discord.Colour(MAIN_COLOR)) else: - embed = discord.Embed(title=i18n.t('role.assign.keep', role=role.name), + embed = discord.Embed(title=i18n.t('role.assign.keep', role=role.name, locale=lang), colour=discord.Colour(MAIN_COLOR)) await msg.edit(embed=embed) @@ -382,6 +413,17 @@ def native_role_count(user: discord.Member): """ return len([role for role in user.roles if role.color.value == NATIVE_COLOR]) + @staticmethod + def get_user_locale(user: discord.Member): + native_roles = [role.name.replace('Native ', '') for role in user.roles if role.color.value == NATIVE_COLOR] + if len(native_roles) == 0: + return 'en' + native_roles.sort() + try: + return LANGUAGE_CODES[native_roles[0]] + except KeyError: + return 'en' + @staticmethod def __make_top10_lines(roles): sorted_roles = sorted(roles.items(), reverse=True, key=operator.itemgetter(1)) diff --git a/crowdin.yml b/crowdin.yml index 703a2d5..9f63b74 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,5 +1,5 @@ files: - source: /i18n/*.en.yml - translation: '/i18n/%file_name%%two_letters_code%.yml' + translation: '/i18n/%file_name%%three_letters_code%.yml' translation_replace: - '.en': '.' \ No newline at end of file + '.eng': '.' \ No newline at end of file diff --git a/events/on_error.py b/events/on_error.py index 2ef289b..482734d 100644 --- a/events/on_error.py +++ b/events/on_error.py @@ -1,5 +1,6 @@ from discord import Forbidden -from discord.ext.commands import CheckFailure, NoPrivateMessage, CommandNotFound, BadArgument, Cog +from discord.ext.commands import CheckFailure, NoPrivateMessage, CommandNotFound, BadArgument, Cog, \ + MissingRequiredArgument from settings.lines import text_lines from utils.utils import send_error_embed @@ -13,6 +14,9 @@ def __init__(self, bot): async def on_command_error(self, ctx, exception): channel = ctx.channel + if hasattr(ctx.command, 'on_error'): + return + if isinstance(exception, NoPrivateMessage): await send_error_embed(ctx, text_lines['technical']['cant_do_in_pm']) elif isinstance(exception, BadArgument): @@ -21,6 +25,8 @@ async def on_command_error(self, ctx, exception): await send_error_embed(ctx, text_lines['roles']['ping']['no_access']) elif isinstance(exception, CommandNotFound): return + elif isinstance(exception, MissingRequiredArgument): + return elif isinstance(exception, Forbidden): await send_error_embed(ctx, text_lines['technical']['forbidden'].format(channel.name, ctx.guild), dm=True) else: diff --git a/i18n/general.en.yml b/i18n/general.eng.yml similarity index 100% rename from i18n/general.en.yml rename to i18n/general.eng.yml diff --git a/i18n/role.en.yml b/i18n/role.eng.yml similarity index 95% rename from i18n/role.en.yml rename to i18n/role.eng.yml index 397d86b..7dd4fae 100644 --- a/i18n/role.en.yml +++ b/i18n/role.eng.yml @@ -1,6 +1,6 @@ en: list: - must_specify_type: You must specify 'native', 'fluent', or 'learning'. + must_specify_type: "Which list are you looking for? You can select one of the following:" body: "**%{role_type} roles**: %{role_list}" assign: added: The role **%{role}** has been added diff --git a/settings/constants.py b/settings/constants.py index 924bfb7..83f8f1d 100644 --- a/settings/constants.py +++ b/settings/constants.py @@ -26,3 +26,89 @@ GITHUB_LINK = 'https://github.com/Linglot/mouse' INVITE_LINK = 'https://discord.gg/uFWNUBQ' + +LANGUAGE_CODES = { + 'Afrikaans': 'afr', + 'Albanian': 'sqi', + 'Arabic': 'ara', + 'Armenian': 'hye', + 'Azerbaijani': 'aze', + 'Belarusian': 'bel', + 'Bengali': 'bn', + 'Berber': 'ber', + 'Bisaya': 'ceb', + 'Bosnian': 'bos', + 'Bulgarian': 'bul', + 'Burmese': 'mya', + 'Cantonese': 'yue', + 'Cherokee': 'chr', + 'Croatian': 'hrv', + 'Czech': 'ces', + 'Danish': 'dan', + 'Dutch': 'nld', + 'English': 'eng', + 'Estonian': 'est', + 'Faroese': 'fao', + 'Finnish': 'fin', + 'French': 'fra', + 'Fula': 'ful', + 'Georgian': 'kat', + 'German': 'deu', + 'Greek': 'ell', + 'Greenlandic': 'kal', + 'Gujatari': 'guj', + 'Hakka': 'hak', + 'Hebrew': 'heb', + 'Hindi': 'hin', + 'Hungarian': 'hun', + 'Icelandic': 'isl', + 'Indonesian': 'ind', + 'Irish': 'gle', + 'Italian': 'ita', + 'Japanese': 'jpn', + 'Kannada': 'kan', + 'Kazakh': 'kaz', + 'Khmer': 'khm', + 'Korean': 'kor', + 'Kurdish': 'kur', + 'Kyrgyz': 'kir', + 'Latvian': 'lav', + 'Lithuanian': 'lit', + 'Luxembourgish': 'ltz', + 'Macedonian': 'mkd', + 'Malagasy': 'mlg', + 'Malayalam': 'mal', + 'Malaysian': 'msa', + 'Mandarin': 'cmn', + 'Marathi': 'mar', + 'Mongolian': 'mon', + 'Montenegrin': 'cnr', + 'Nepali': 'nep', + 'Norwegian': 'nor', + 'Pashto': 'pus', + 'Persian': 'fas', + 'Polish': 'pol', + 'Portuguese': 'por', + 'Punjabi': 'pan', + 'Romanian': 'ron', + 'Russian': 'rus', + 'Serbian': 'srp', + 'Sinhalese': 'sin', + 'Slovak': 'slk', + 'Slovene': 'slv', + 'Spanish': 'spa', + 'Swahili': 'swa', + 'Sewdish': 'swe', + 'Tagalog': 'tgl', + 'Tamil': 'tam', + 'Tatar': 'tat', + 'Telugu': 'tel', + 'Thai': 'tha', + 'Turkish': 'tur', + 'Ukrainian': 'ukr', + 'Urdu': 'urd', + 'Uzbek': 'uzb', + 'Vietnamese': 'vie', + 'Welsh': 'cym', + 'Wolof': 'wol' +} From 87ec95ae336820139d407bf7885976438bdc21a0 Mon Sep 17 00:00:00 2001 From: Kerri Salciccioli Date: Sun, 12 Jul 2020 16:37:44 -0400 Subject: [PATCH 4/6] oops --- cogs/role.py | 4 ++-- settings/constants.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/role.py b/cogs/role.py index 66be8dd..a2c1326 100644 --- a/cogs/role.py +++ b/cogs/role.py @@ -417,12 +417,12 @@ def native_role_count(user: discord.Member): def get_user_locale(user: discord.Member): native_roles = [role.name.replace('Native ', '') for role in user.roles if role.color.value == NATIVE_COLOR] if len(native_roles) == 0: - return 'en' + return 'eng' native_roles.sort() try: return LANGUAGE_CODES[native_roles[0]] except KeyError: - return 'en' + return 'eng' @staticmethod def __make_top10_lines(roles): diff --git a/settings/constants.py b/settings/constants.py index 83f8f1d..98b48c5 100644 --- a/settings/constants.py +++ b/settings/constants.py @@ -98,7 +98,7 @@ 'Slovene': 'slv', 'Spanish': 'spa', 'Swahili': 'swa', - 'Sewdish': 'swe', + 'Swedish': 'swe', 'Tagalog': 'tgl', 'Tamil': 'tam', 'Tatar': 'tat', From 6cf1b253610d94a3da3d0c29fd246222253dc6b4 Mon Sep 17 00:00:00 2001 From: Kerri Salciccioli Date: Sun, 12 Jul 2020 16:42:51 -0400 Subject: [PATCH 5/6] whoops --- MrMouse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MrMouse.py b/MrMouse.py index 8b6cf80..41a2532 100644 --- a/MrMouse.py +++ b/MrMouse.py @@ -5,7 +5,7 @@ import i18n i18n.load_path.append("i18n/") -i18n.config.set('fallback', 'en') +i18n.config.set('fallback', 'eng') bot = Bot(command_prefix=PREFIX, fetch_offline_members=True) From dee9bf10f7ebf3027b36c3d151f2c2748f6d820d Mon Sep 17 00:00:00 2001 From: Kerri Salciccioli Date: Sun, 12 Jul 2020 16:45:32 -0400 Subject: [PATCH 6/6] whoops --- crowdin.yml | 2 +- i18n/general.eng.yml | 2 +- i18n/role.eng.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crowdin.yml b/crowdin.yml index 9f63b74..dddf3a5 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,5 +1,5 @@ files: - - source: /i18n/*.en.yml + - source: /i18n/*.eng.yml translation: '/i18n/%file_name%%three_letters_code%.yml' translation_replace: '.eng': '.' \ No newline at end of file diff --git a/i18n/general.eng.yml b/i18n/general.eng.yml index 4bdbec8..69af5b5 100644 --- a/i18n/general.eng.yml +++ b/i18n/general.eng.yml @@ -1,4 +1,4 @@ -en: +eng: commands: on_cooldown: Sorry, this command is on cooldown! roles_dont_exist: diff --git a/i18n/role.eng.yml b/i18n/role.eng.yml index 7dd4fae..e456e4c 100644 --- a/i18n/role.eng.yml +++ b/i18n/role.eng.yml @@ -1,4 +1,4 @@ -en: +eng: list: must_specify_type: "Which list are you looking for? You can select one of the following:" body: "**%{role_type} roles**: %{role_list}"