From 3c1b6ae4cf56c516fa8d2b1ea778a7242db33aa6 Mon Sep 17 00:00:00 2001 From: Draper <27962761+Drapersniper@users.noreply.github.com> Date: Tue, 27 Aug 2019 23:44:52 +0100 Subject: [PATCH] [Utils] Add humanize_number() function to chat formatting (#2836) This adds babel as a dependency, and also includes `redbot.core.i18n.get_babel_locale()` --- changelog.d/2836.feature.rst | 1 + changelog.d/2836.misc.rst | 1 + redbot/cogs/audio/audio.py | 28 ++++++---- redbot/cogs/bank/bank.py | 6 +- redbot/cogs/cleanup/cleanup.py | 49 +++++++++++++--- redbot/cogs/economy/economy.py | 84 ++++++++++++++++------------ redbot/cogs/general/general.py | 24 +++++--- redbot/cogs/mod/kickban.py | 6 +- redbot/cogs/trivia/session.py | 4 +- redbot/core/bank.py | 28 ++++++++-- redbot/core/errors.py | 5 +- redbot/core/i18n.py | 53 +++++++++++++++++- redbot/core/utils/chat_formatting.py | 27 ++++++++- setup.cfg | 1 + tools/primary_deps.ini | 1 + 15 files changed, 238 insertions(+), 80 deletions(-) create mode 100644 changelog.d/2836.feature.rst create mode 100644 changelog.d/2836.misc.rst diff --git a/changelog.d/2836.feature.rst b/changelog.d/2836.feature.rst new file mode 100644 index 000000000..2ab5446d1 --- /dev/null +++ b/changelog.d/2836.feature.rst @@ -0,0 +1 @@ +New :func:`humanize_number` in :module:`redbot.core.utils.chat_formatting` function to convert numbers into text which respect locale. \ No newline at end of file diff --git a/changelog.d/2836.misc.rst b/changelog.d/2836.misc.rst new file mode 100644 index 000000000..d203c9ec7 --- /dev/null +++ b/changelog.d/2836.misc.rst @@ -0,0 +1 @@ +New :func:`humanize_number` is used throughout the bot. diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py index bbebdbe62..4dde73984 100644 --- a/redbot/cogs/audio/audio.py +++ b/redbot/cogs/audio/audio.py @@ -19,7 +19,7 @@ import redbot.core from redbot.core import Config, commands, checks, bank from redbot.core.data_manager import cog_data_path from redbot.core.i18n import Translator, cog_i18n -from redbot.core.utils.chat_formatting import bold, box, pagify +from redbot.core.utils.chat_formatting import bold, box, pagify, humanize_number from redbot.core.utils.menus import ( menu, DEFAULT_CONTROLS, @@ -442,7 +442,7 @@ class Audio(commands.Cog): await self._embed_msg( ctx, _("Track queueing command price set to {price} {currency}.").format( - price=price, currency=await bank.get_currency_name(ctx.guild) + price=humanize_number(price), currency=await bank.get_currency_name(ctx.guild) ), ) @@ -613,7 +613,9 @@ class Audio(commands.Cog): msg += _("DJ Role: [{role.name}]\n").format(role=dj_role_obj) if jukebox: msg += _("Jukebox: [{jukebox_name}]\n").format(jukebox_name=jukebox) - msg += _("Command price: [{jukebox_price}]\n").format(jukebox_price=jukebox_price) + msg += _("Command price: [{jukebox_price}]\n").format( + jukebox_price=humanize_number(jukebox_price) + ) if maxlength > 0: msg += _("Max track length: [{tracklength}]\n").format( tracklength=self._dynamic_time(maxlength) @@ -762,11 +764,15 @@ class Audio(commands.Cog): em = discord.Embed( colour=await ctx.embed_colour(), title=_("Playing in {num}/{total} servers:").format( - num=server_num, total=total_num + num=humanize_number(server_num), total=humanize_number(total_num) ), description=page, ) - em.set_footer(text="Page {}/{}".format(pages, (math.ceil(len(msg) / 1500)))) + em.set_footer( + text="Page {}/{}".format( + humanize_number(pages), humanize_number((math.ceil(len(msg) / 1500))) + ) + ) pages += 1 servers_embed.append(em) @@ -933,7 +939,9 @@ class Audio(commands.Cog): embed = discord.Embed( colour=await ctx.embed_colour(), description=(f"{header}\n{formatted_page}") ) - embed.set_footer(text=_("{num} preset(s)").format(num=len(list(eq_presets.keys())))) + embed.set_footer( + text=_("{num} preset(s)").format(num=humanize_number(len(list(eq_presets.keys())))) + ) page_list.append(embed) if len(page_list) == 1: return await ctx.send(embed=page_list[0]) @@ -1869,6 +1877,7 @@ class Audio(commands.Cog): song_info = "{} {}".format(i["name"], i["artists"][0]["name"]) else: song_info = "{} {}".format(i["track"]["name"], i["track"]["artists"][0]["name"]) + try: track_url = await self._youtube_api_search(yt_key, song_info) except (RuntimeError, aiohttp.client_exceptions.ServerDisconnectedError): @@ -1878,7 +1887,6 @@ class Audio(commands.Cog): ) await playlist_msg.edit(embed=error_embed) return None - pass try: yt_track = await player.get_tracks(track_url) except (RuntimeError, aiohttp.client_exceptions.ServerDisconnectedError): @@ -3416,8 +3424,8 @@ class Audio(commands.Cog): " Votes: {num_votes}/{num_members}" " ({cur_percent}% out of {required_percent}% needed)" ).format( - num_votes=num_votes, - num_members=num_members, + num_votes=humanize_number(num_votes), + num_members=humanize_number(num_members), cur_percent=vote, required_percent=percent, ) @@ -3869,7 +3877,7 @@ class Audio(commands.Cog): await self._embed_msg( ctx, _("Not enough {currency} ({required_credits} required).").format( - currency=credits_name, required_credits=jukebox_price + currency=credits_name, required_credits=humanize_number(jukebox_price) ), ) return False diff --git a/redbot/cogs/bank/bank.py b/redbot/cogs/bank/bank.py index 6d186ee6b..70f06a344 100644 --- a/redbot/cogs/bank/bank.py +++ b/redbot/cogs/bank/bank.py @@ -1,5 +1,5 @@ import discord -from redbot.core.utils.chat_formatting import box +from redbot.core.utils.chat_formatting import box, humanize_number from redbot.core import checks, bank, commands from redbot.core.i18n import Translator, cog_i18n @@ -88,7 +88,9 @@ class Bank(commands.Cog): "Bank settings:\n\nBank name: {bank_name}\nCurrency: {currency_name}\n" "Default balance: {default_balance}" ).format( - bank_name=bank_name, currency_name=currency_name, default_balance=default_balance + bank_name=bank_name, + currency_name=currency_name, + default_balance=humanize_number(default_balance), ) await ctx.send(box(settings)) diff --git a/redbot/cogs/cleanup/cleanup.py b/redbot/cogs/cleanup/cleanup.py index f54d075ee..74ff35790 100644 --- a/redbot/cogs/cleanup/cleanup.py +++ b/redbot/cogs/cleanup/cleanup.py @@ -8,6 +8,7 @@ import discord from redbot.core import checks, commands from redbot.core.bot import Red from redbot.core.i18n import Translator, cog_i18n +from redbot.core.utils.chat_formatting import humanize_number from redbot.core.utils.mod import slow_deletion, mass_purge from redbot.core.utils.predicates import MessagePredicate from .converters import RawMessageIds @@ -39,7 +40,9 @@ class Cleanup(commands.Cog): return True prompt = await ctx.send( - _("Are you sure you want to delete {number} messages? (y/n)").format(number=number) + _("Are you sure you want to delete {number} messages? (y/n)").format( + number=humanize_number(number) + ) ) response = await ctx.bot.wait_for("message", check=MessagePredicate.same_context(ctx)) @@ -152,7 +155,11 @@ class Cleanup(commands.Cog): to_delete.append(ctx.message) reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format( - author.name, author.id, len(to_delete), text, channel.id + author.name, + author.id, + humanize_number(len(to_delete), override_locale="en_us"), + text, + channel.id, ) log.info(reason) @@ -208,7 +215,14 @@ class Cleanup(commands.Cog): reason = ( "{}({}) deleted {} messages " " made by {}({}) in channel {}." - "".format(author.name, author.id, len(to_delete), member or "???", _id, channel.name) + "".format( + author.name, + author.id, + humanize_number(len(to_delete), override_locale="en_US"), + member or "???", + _id, + channel.name, + ) ) log.info(reason) @@ -240,7 +254,10 @@ class Cleanup(commands.Cog): ) reason = "{}({}) deleted {} messages in channel {}.".format( - author.name, author.id, len(to_delete), channel.name + author.name, + author.id, + humanize_number(len(to_delete), override_locale="en_US"), + channel.name, ) log.info(reason) @@ -277,7 +294,10 @@ class Cleanup(commands.Cog): to_delete.append(ctx.message) reason = "{}({}) deleted {} messages in channel {}.".format( - author.name, author.id, len(to_delete), channel.name + author.name, + author.id, + humanize_number(len(to_delete), override_locale="en_US"), + channel.name, ) log.info(reason) @@ -319,7 +339,10 @@ class Cleanup(commands.Cog): ) to_delete.append(ctx.message) reason = "{}({}) deleted {} messages in channel {}.".format( - author.name, author.id, len(to_delete), channel.name + author.name, + author.id, + humanize_number(len(to_delete), override_locale="en_US"), + channel.name, ) log.info(reason) @@ -420,7 +443,12 @@ class Cleanup(commands.Cog): reason = ( "{}({}) deleted {} " " command messages in channel {}." - "".format(author.name, author.id, len(to_delete), channel.name) + "".format( + author.name, + author.id, + humanize_number(len(to_delete), override_locale="en_US"), + channel.name, + ) ) log.info(reason) @@ -500,7 +528,12 @@ class Cleanup(commands.Cog): reason = ( "{}({}) deleted {} messages " "sent by the bot in {}." - "".format(author.name, author.id, len(to_delete), channel_name) + "".format( + author.name, + author.id, + humanize_number(len(to_delete), override_locale="en_US"), + channel_name, + ) ) log.info(reason) diff --git a/redbot/cogs/economy/economy.py b/redbot/cogs/economy/economy.py index 468935deb..1f952140d 100644 --- a/redbot/cogs/economy/economy.py +++ b/redbot/cogs/economy/economy.py @@ -10,7 +10,7 @@ import discord from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin from redbot.core import Config, bank, commands, errors from redbot.core.i18n import Translator, cog_i18n -from redbot.core.utils.chat_formatting import box +from redbot.core.utils.chat_formatting import box, humanize_number from redbot.core.utils.menus import menu, DEFAULT_CONTROLS from redbot.core.bot import Red @@ -162,7 +162,7 @@ class Economy(commands.Cog): await ctx.send( _("{user}'s balance is {num} {currency}").format( - user=user.display_name, num=bal, currency=currency + user=user.display_name, num=humanize_number(bal), currency=currency ) ) @@ -179,7 +179,10 @@ class Economy(commands.Cog): await ctx.send( _("{user} transferred {num} {currency} to {other_user}").format( - user=from_.display_name, num=amount, currency=currency, other_user=to.display_name + user=from_.display_name, + num=humanize_number(amount), + currency=currency, + other_user=to.display_name, ) ) @@ -203,7 +206,7 @@ class Economy(commands.Cog): await bank.deposit_credits(to, creds.sum) msg = _("{author} added {num} {currency} to {user}'s account.").format( author=author.display_name, - num=creds.sum, + num=humanize_number(creds.sum), currency=currency, user=to.display_name, ) @@ -211,7 +214,7 @@ class Economy(commands.Cog): await bank.withdraw_credits(to, creds.sum) msg = _("{author} removed {num} {currency} from {user}'s account.").format( author=author.display_name, - num=creds.sum, + num=humanize_number(creds.sum), currency=currency, user=to.display_name, ) @@ -219,7 +222,7 @@ class Economy(commands.Cog): await bank.set_balance(to, creds.sum) msg = _("{author} set {user}'s account balance to {num} {currency}.").format( author=author.display_name, - num=creds.sum, + num=humanize_number(creds.sum), currency=currency, user=to.display_name, ) @@ -271,7 +274,9 @@ class Economy(commands.Cog): "You've reached the maximum amount of {currency}!" "Please spend some more \N{GRIMACING FACE}\n\n" "You currently have {new_balance} {currency}." - ).format(currency=credits_name, new_balance=exc.max_balance) + ).format( + currency=credits_name, new_balance=humanize_number(exc.max_balance) + ) ) return next_payday = cur_time + await self.config.PAYDAY_TIME() @@ -287,9 +292,9 @@ class Economy(commands.Cog): ).format( author=author, currency=credits_name, - amount=await self.config.PAYDAY_CREDITS(), - new_balance=await bank.get_balance(author), - pos=pos, + amount=humanize_number(await self.config.PAYDAY_CREDITS()), + new_balance=humanize_number(await bank.get_balance(author)), + pos=humanize_number(pos) if pos else pos, ) ) @@ -319,7 +324,9 @@ class Economy(commands.Cog): "You've reached the maximum amount of {currency}! " "Please spend some more \N{GRIMACING FACE}\n\n" "You currently have {new_balance} {currency}." - ).format(currency=credits_name, new_balance=exc.max_balance) + ).format( + currency=credits_name, new_balance=humanize_number(exc.max_balance) + ) ) return next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME() @@ -334,9 +341,9 @@ class Economy(commands.Cog): ).format( author=author, currency=credits_name, - amount=credit_amount, - new_balance=await bank.get_balance(author), - pos=pos, + amount=humanize_number(credit_amount), + new_balance=humanize_number(await bank.get_balance(author)), + pos=humanize_number(pos) if pos else pos, ) ) else: @@ -364,7 +371,7 @@ class Economy(commands.Cog): else: bank_sorted = await bank.get_leaderboard(positions=top, guild=guild) try: - bal_len = len(str(bank_sorted[0][1]["balance"])) + bal_len = len(humanize_number(bank_sorted[0][1]["balance"])) # first user is the largest we'll see except IndexError: return await ctx.send(_("There are no accounts in the bank.")) @@ -387,14 +394,17 @@ class Economy(commands.Cog): if await ctx.bot.is_owner(ctx.author): user_id = f"({str(acc[0])})" name = f"{acc[1]['name']} {user_id}" - balance = acc[1]["balance"] + balance = humanize_number(acc[1]["balance"]) if acc[0] != author.id: - temp_msg += f"{f'{pos}.': <{pound_len+2}} {balance: <{bal_len + 5}} {name}\n" + temp_msg += ( + f"{f'{humanize_number(pos)}.': <{pound_len+2}} " + f"{balance: <{bal_len + 5}} {name}\n" + ) else: temp_msg += ( - f"{f'{pos}.': <{pound_len+2}} " + f"{f'{humanize_number(pos)}.': <{pound_len+2}} " f"{balance: <{bal_len + 5}} " f"<<{author.display_name}>>\n" ) @@ -503,8 +513,8 @@ class Economy(commands.Cog): "Please spend some more \N{GRIMACING FACE}\n{old_balance} -> {new_balance}!" ).format( currency=await bank.get_currency_name(getattr(channel, "guild", None)), - old_balance=then, - new_balance=exc.max_balance, + old_balance=humanize_number(then), + new_balance=humanize_number(exc.max_balance), ) ) return @@ -523,10 +533,10 @@ class Economy(commands.Cog): slot=slot, author=author, phrase=phrase, - bid=bid, - old_balance=then, - new_balance=now, - pay=pay, + bid=humanize_number(bid), + old_balance=humanize_number(then), + new_balance=humanize_number(now), + pay=humanize_number(pay), ) ) @@ -552,12 +562,12 @@ class Economy(commands.Cog): "Payday cooldown: {payday_time}\n" "Amount given at account registration: {register_amount}" ).format( - slot_min=await conf.SLOT_MIN(), - slot_max=await conf.SLOT_MAX(), - slot_time=await conf.SLOT_TIME(), - payday_time=await conf.PAYDAY_TIME(), - payday_amount=await conf.PAYDAY_CREDITS(), - register_amount=await bank.get_default_balance(guild), + slot_min=humanize_number(await conf.SLOT_MIN()), + slot_max=humanize_number(await conf.SLOT_MAX()), + slot_time=humanize_number(await conf.SLOT_TIME()), + payday_time=humanize_number(await conf.PAYDAY_TIME()), + payday_amount=humanize_number(await conf.PAYDAY_CREDITS()), + register_amount=humanize_number(await bank.get_default_balance(guild)), ) ) ) @@ -575,7 +585,9 @@ class Economy(commands.Cog): await self.config.guild(guild).SLOT_MIN.set(bid) credits_name = await bank.get_currency_name(guild) await ctx.send( - _("Minimum bid is now {bid} {currency}.").format(bid=bid, currency=credits_name) + _("Minimum bid is now {bid} {currency}.").format( + bid=humanize_number(bid), currency=credits_name + ) ) @economyset.command() @@ -594,7 +606,9 @@ class Economy(commands.Cog): else: await self.config.guild(guild).SLOT_MAX.set(bid) await ctx.send( - _("Maximum bid is now {bid} {currency}.").format(bid=bid, currency=credits_name) + _("Maximum bid is now {bid} {currency}.").format( + bid=humanize_number(bid), currency=credits_name + ) ) @economyset.command() @@ -635,7 +649,7 @@ class Economy(commands.Cog): await self.config.guild(guild).PAYDAY_CREDITS.set(creds) await ctx.send( _("Every payday will now give {num} {currency}.").format( - num=creds, currency=credits_name + num=humanize_number(creds), currency=credits_name ) ) @@ -655,7 +669,7 @@ class Economy(commands.Cog): _( "Every payday will now give {num} {currency} " "to people with the role {role_name}." - ).format(num=creds, currency=credits_name, role_name=role.name) + ).format(num=humanize_number(creds), currency=credits_name, role_name=role.name) ) @economyset.command() @@ -668,7 +682,7 @@ class Economy(commands.Cog): await bank.set_default_balance(creds, guild) await ctx.send( _("Registering an account will now give {num} {currency}.").format( - num=creds, currency=credits_name + num=humanize_number(creds), currency=credits_name ) ) diff --git a/redbot/cogs/general/general.py b/redbot/cogs/general/general.py index 5d6d0e519..0814525a9 100644 --- a/redbot/cogs/general/general.py +++ b/redbot/cogs/general/general.py @@ -7,7 +7,7 @@ import discord from redbot.core import commands from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.menus import menu, DEFAULT_CONTROLS -from redbot.core.utils.chat_formatting import escape, italics +from redbot.core.utils.chat_formatting import escape, italics, humanize_number _ = T_ = Translator("General", __file__) @@ -89,7 +89,11 @@ class General(commands.Cog): author = ctx.author if number > 1: n = randint(1, number) - await ctx.send("{author.mention} :game_die: {n} :game_die:".format(author=author, n=n)) + await ctx.send( + "{author.mention} :game_die: {n} :game_die:".format( + author=author, n=humanize_number(n) + ) + ) else: await ctx.send(_("{author.mention} Maybe higher than 1? ;P").format(author=author)) @@ -223,10 +227,12 @@ class General(commands.Cog): async def serverinfo(self, ctx): """Show server information.""" guild = ctx.guild - online = len([m.status for m in guild.members if m.status != discord.Status.offline]) - total_users = len(guild.members) - text_channels = len(guild.text_channels) - voice_channels = len(guild.voice_channels) + online = humanize_number( + len([m.status for m in guild.members if m.status != discord.Status.offline]) + ) + total_users = humanize_number(len(guild.members)) + text_channels = humanize_number(len(guild.text_channels)) + voice_channels = humanize_number(len(guild.voice_channels)) passed = (ctx.message.created_at - guild.created_at).days created_at = _("Since {date}. That's over {num} days ago!").format( date=guild.created_at.strftime("%d %b %Y %H:%M"), num=passed @@ -234,9 +240,9 @@ class General(commands.Cog): data = discord.Embed(description=created_at, colour=(await ctx.embed_colour())) data.add_field(name=_("Region"), value=str(guild.region)) data.add_field(name=_("Users"), value=f"{online}/{total_users}") - data.add_field(name=_("Text Channels"), value=str(text_channels)) - data.add_field(name=_("Voice Channels"), value=str(voice_channels)) - data.add_field(name=_("Roles"), value=str(len(guild.roles))) + data.add_field(name=_("Text Channels"), value=text_channels) + data.add_field(name=_("Voice Channels"), value=voice_channels) + data.add_field(name=_("Roles"), value=humanize_number(len(guild.roles))) data.add_field(name=_("Owner"), value=str(guild.owner)) data.set_footer(text=_("Server ID: ") + str(guild.id)) diff --git a/redbot/cogs/mod/kickban.py b/redbot/cogs/mod/kickban.py index 31e64e09e..beabc1f05 100644 --- a/redbot/cogs/mod/kickban.py +++ b/redbot/cogs/mod/kickban.py @@ -7,7 +7,7 @@ from typing import cast, Optional, Union import discord from redbot.core import commands, i18n, checks, modlog -from redbot.core.utils.chat_formatting import pagify +from redbot.core.utils.chat_formatting import pagify, humanize_number from redbot.core.utils.mod import is_allowed_by_hierarchy, get_audit_reason from .abc import MixinMeta from .converters import RawUserIds @@ -244,7 +244,9 @@ class KickBanMixin(MixinMeta): errors = {} async def show_results(): - text = _("Banned {num} users from the server.").format(num=len(banned)) + text = _("Banned {num} users from the server.").format( + num=humanize_number(len(banned)) + ) if errors: text += _("\nErrors:\n") text += "\n".join(errors.values()) diff --git a/redbot/cogs/trivia/session.py b/redbot/cogs/trivia/session.py index 46274007f..d7ca7aac7 100644 --- a/redbot/cogs/trivia/session.py +++ b/redbot/cogs/trivia/session.py @@ -6,7 +6,7 @@ from collections import Counter import discord from redbot.core import bank from redbot.core.i18n import Translator -from redbot.core.utils.chat_formatting import box, bold, humanize_list +from redbot.core.utils.chat_formatting import box, bold, humanize_list, humanize_number from redbot.core.utils.common_filters import normalize_smartquotes from .log import LOG @@ -292,7 +292,7 @@ class TriviaSession: " for coming first." ).format( user=winner.display_name, - num=amount, + num=humanize_number(amount), currency=await bank.get_currency_name(self.ctx.guild), ) ) diff --git a/redbot/core/bank.py b/redbot/core/bank.py index ee5bb8948..de7060ea8 100644 --- a/redbot/core/bank.py +++ b/redbot/core/bank.py @@ -5,6 +5,7 @@ from functools import wraps import discord +from redbot.core.utils.chat_formatting import humanize_number from . import Config, errors, commands from .i18n import Translator @@ -237,11 +238,20 @@ async def withdraw_credits(member: discord.Member, amount: int) -> int: if not isinstance(amount, int): raise TypeError("Withdrawal amount must be of type int, not {}.".format(type(amount))) if _invalid_amount(amount): - raise ValueError("Invalid withdrawal amount {} < 0".format(amount)) + raise ValueError( + "Invalid withdrawal amount {} < 0".format( + humanize_number(amount, override_locale="en_US") + ) + ) bal = await get_balance(member) if amount > bal: - raise ValueError("Insufficient funds {} > {}".format(amount, bal)) + raise ValueError( + "Insufficient funds {} > {}".format( + humanize_number(amount, override_locale="en_US"), + humanize_number(bal, override_locale="en_US"), + ) + ) return await set_balance(member, bal - amount) @@ -272,7 +282,11 @@ async def deposit_credits(member: discord.Member, amount: int) -> int: if not isinstance(amount, int): raise TypeError("Deposit amount must be of type int, not {}.".format(type(amount))) if _invalid_amount(amount): - raise ValueError("Invalid deposit amount {} <= 0".format(amount)) + raise ValueError( + "Invalid deposit amount {} <= 0".format( + humanize_number(amount, override_locale="en_US") + ) + ) bal = await get_balance(member) return await set_balance(member, amount + bal) @@ -309,7 +323,11 @@ async def transfer_credits(from_: discord.Member, to: discord.Member, amount: in if not isinstance(amount, int): raise TypeError("Transfer amount must be of type int, not {}.".format(type(amount))) if _invalid_amount(amount): - raise ValueError("Invalid transfer amount {} <= 0".format(amount)) + raise ValueError( + "Invalid transfer amount {} <= 0".format( + humanize_number(amount, override_locale="en_US") + ) + ) if await get_balance(to) + amount > MAX_BALANCE: currency = await get_currency_name(to.guild) @@ -727,7 +745,7 @@ def cost(amount: int): credits_name = await get_currency_name(context.guild) raise commands.UserFeedbackCheckFailure( _("You need at least {cost} {currency} to use this command.").format( - cost=amount, currency=credits_name + cost=humanize_number(amount), currency=credits_name ) ) else: diff --git a/redbot/core/errors.py b/redbot/core/errors.py index 5bd7f4fa2..66f5f347b 100644 --- a/redbot/core/errors.py +++ b/redbot/core/errors.py @@ -2,6 +2,7 @@ import importlib.machinery import discord +from redbot.core.utils.chat_formatting import humanize_number from .i18n import Translator _ = Translator(__name__, __file__) @@ -45,8 +46,8 @@ class BalanceTooHigh(BankError, OverflowError): self.currency_name = currency_name def __str__(self) -> str: - return _("{user}'s balance cannot rise above {max:,} {currency}.").format( - user=self.user, max=self.max_balance, currency=self.currency_name + return _("{user}'s balance cannot rise above max {currency}.").format( + user=self.user, max=humanize_number(self.max_balance), currency=self.currency_name ) diff --git a/redbot/core/i18n.py b/redbot/core/i18n.py index 2f56cd6c0..7790e0ee5 100644 --- a/redbot/core/i18n.py +++ b/redbot/core/i18n.py @@ -1,10 +1,21 @@ import contextlib +import functools import io import os from pathlib import Path -from typing import Callable, Union, Dict +from typing import Callable, Union, Dict, Optional -__all__ = ["get_locale", "set_locale", "reload_locales", "cog_i18n", "Translator"] +import babel.localedata +from babel.core import Locale + +__all__ = [ + "get_locale", + "set_locale", + "reload_locales", + "cog_i18n", + "Translator", + "get_babel_locale", +] _current_locale = "en-US" @@ -160,6 +171,44 @@ class Translator(Callable[[str], str]): self.translations[untranslated] = translated +@functools.lru_cache() +def _get_babel_locale(red_locale: str) -> babel.core.Locale: + supported_locales = babel.localedata.locale_identifiers() + try: # Handles cases where red_locale is already Babel supported + babel_locale = Locale(*babel.parse_locale(red_locale)) + except (ValueError, babel.core.UnknownLocaleError): + try: + babel_locale = Locale(*babel.parse_locale(red_locale, sep="-")) + except (ValueError, babel.core.UnknownLocaleError): + # ValueError is Raised by `parse_locale` when an invalid Locale is given to it + # Lets handle it silently and default to "en_US" + try: + # Try to find a babel locale that's close to the one used by red + babel_locale = Locale(Locale.negotiate([red_locale], supported_locales, sep="-")) + except (ValueError, TypeError, babel.core.UnknownLocaleError): + # If we fail to get a close match we will then default to "en_US" + babel_locale = Locale("en", "US") + return babel_locale + + +def get_babel_locale(locale: Optional[str] = None) -> babel.core.Locale: + """Function to convert a locale to a ``babel.core.Locale``. + + Parameters + ---------- + locale : Optional[str] + The locale to convert, if not specified it defaults to the bot's locale. + + Returns + ------- + babel.core.Locale + The babel locale object. + """ + if locale is None: + locale = get_locale() + return _get_babel_locale(locale) + + # This import to be down here to avoid circular import issues. # This will be cleaned up at a later date # noinspection PyPep8 diff --git a/redbot/core/utils/chat_formatting.py b/redbot/core/utils/chat_formatting.py index a73abf78c..92d9261ee 100644 --- a/redbot/core/utils/chat_formatting.py +++ b/redbot/core/utils/chat_formatting.py @@ -1,11 +1,13 @@ import itertools import datetime -from typing import Sequence, Iterator, List, Optional +from typing import Sequence, Iterator, List, Optional, Union from io import BytesIO -import discord -from redbot.core.i18n import Translator +import discord +from babel.numbers import format_decimal + +from redbot.core.i18n import Translator, get_babel_locale _ = Translator("UtilsChatFormatting", __file__) @@ -432,6 +434,25 @@ def humanize_timedelta( return ", ".join(strings) +def humanize_number(val: Union[int, float], override_locale=None) -> str: + """ + Convert an int or float to a str with digit separators based on bot locale + + Parameters + ---------- + val : Union[int, float] + The int/float to be formatted. + override_locale: Optional[str] + A value to override the bots locale. + + Returns + ------- + str + locale aware formatted number. + """ + return format_decimal(val, locale=get_babel_locale(override_locale)) + + def text_to_file( text: str, filename: str = "file.txt", *, spoiler: bool = False, encoding: str = "utf-8" ): diff --git a/setup.cfg b/setup.cfg index 76acee512..3579342c5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ install_requires = appdirs==1.4.3 async-timeout==3.0.1 attrs==19.1.0 + babel==2.7.0 chardet==3.0.4 Click==7.0 colorama==0.4.1 diff --git a/tools/primary_deps.ini b/tools/primary_deps.ini index bd6e8819a..02cb62010 100644 --- a/tools/primary_deps.ini +++ b/tools/primary_deps.ini @@ -9,6 +9,7 @@ install_requires = aiohttp aiohttp-json-rpc appdirs + babel click colorama discord.py