[Utils] Add humanize_number() function to chat formatting (#2836)

This adds babel as a dependency, and also includes `redbot.core.i18n.get_babel_locale()`
This commit is contained in:
Draper 2019-08-27 23:44:52 +01:00 committed by Toby Harradine
parent 6c3a3fea66
commit 3c1b6ae4cf
15 changed files with 238 additions and 80 deletions

View File

@ -0,0 +1 @@
New :func:`humanize_number` in :module:`redbot.core.utils.chat_formatting` function to convert numbers into text which respect locale.

View File

@ -0,0 +1 @@
New :func:`humanize_number` is used throughout the bot.

View File

@ -19,7 +19,7 @@ import redbot.core
from redbot.core import Config, commands, checks, bank from redbot.core import Config, commands, checks, bank
from redbot.core.data_manager import cog_data_path from redbot.core.data_manager import cog_data_path
from redbot.core.i18n import Translator, cog_i18n 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 ( from redbot.core.utils.menus import (
menu, menu,
DEFAULT_CONTROLS, DEFAULT_CONTROLS,
@ -442,7 +442,7 @@ class Audio(commands.Cog):
await self._embed_msg( await self._embed_msg(
ctx, ctx,
_("Track queueing command price set to {price} {currency}.").format( _("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) msg += _("DJ Role: [{role.name}]\n").format(role=dj_role_obj)
if jukebox: if jukebox:
msg += _("Jukebox: [{jukebox_name}]\n").format(jukebox_name=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: if maxlength > 0:
msg += _("Max track length: [{tracklength}]\n").format( msg += _("Max track length: [{tracklength}]\n").format(
tracklength=self._dynamic_time(maxlength) tracklength=self._dynamic_time(maxlength)
@ -762,11 +764,15 @@ class Audio(commands.Cog):
em = discord.Embed( em = discord.Embed(
colour=await ctx.embed_colour(), colour=await ctx.embed_colour(),
title=_("Playing in {num}/{total} servers:").format( 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, 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 pages += 1
servers_embed.append(em) servers_embed.append(em)
@ -933,7 +939,9 @@ class Audio(commands.Cog):
embed = discord.Embed( embed = discord.Embed(
colour=await ctx.embed_colour(), description=(f"{header}\n{formatted_page}") 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) page_list.append(embed)
if len(page_list) == 1: if len(page_list) == 1:
return await ctx.send(embed=page_list[0]) 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"]) song_info = "{} {}".format(i["name"], i["artists"][0]["name"])
else: else:
song_info = "{} {}".format(i["track"]["name"], i["track"]["artists"][0]["name"]) song_info = "{} {}".format(i["track"]["name"], i["track"]["artists"][0]["name"])
try: try:
track_url = await self._youtube_api_search(yt_key, song_info) track_url = await self._youtube_api_search(yt_key, song_info)
except (RuntimeError, aiohttp.client_exceptions.ServerDisconnectedError): except (RuntimeError, aiohttp.client_exceptions.ServerDisconnectedError):
@ -1878,7 +1887,6 @@ class Audio(commands.Cog):
) )
await playlist_msg.edit(embed=error_embed) await playlist_msg.edit(embed=error_embed)
return None return None
pass
try: try:
yt_track = await player.get_tracks(track_url) yt_track = await player.get_tracks(track_url)
except (RuntimeError, aiohttp.client_exceptions.ServerDisconnectedError): except (RuntimeError, aiohttp.client_exceptions.ServerDisconnectedError):
@ -3416,8 +3424,8 @@ class Audio(commands.Cog):
" Votes: {num_votes}/{num_members}" " Votes: {num_votes}/{num_members}"
" ({cur_percent}% out of {required_percent}% needed)" " ({cur_percent}% out of {required_percent}% needed)"
).format( ).format(
num_votes=num_votes, num_votes=humanize_number(num_votes),
num_members=num_members, num_members=humanize_number(num_members),
cur_percent=vote, cur_percent=vote,
required_percent=percent, required_percent=percent,
) )
@ -3869,7 +3877,7 @@ class Audio(commands.Cog):
await self._embed_msg( await self._embed_msg(
ctx, ctx,
_("Not enough {currency} ({required_credits} required).").format( _("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 return False

View File

@ -1,5 +1,5 @@
import discord 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 import checks, bank, commands
from redbot.core.i18n import Translator, cog_i18n 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" "Bank settings:\n\nBank name: {bank_name}\nCurrency: {currency_name}\n"
"Default balance: {default_balance}" "Default balance: {default_balance}"
).format( ).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)) await ctx.send(box(settings))

View File

@ -8,6 +8,7 @@ import discord
from redbot.core import checks, commands from redbot.core import checks, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n 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.mod import slow_deletion, mass_purge
from redbot.core.utils.predicates import MessagePredicate from redbot.core.utils.predicates import MessagePredicate
from .converters import RawMessageIds from .converters import RawMessageIds
@ -39,7 +40,9 @@ class Cleanup(commands.Cog):
return True return True
prompt = await ctx.send( 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)) 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) to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format( 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) log.info(reason)
@ -208,7 +215,14 @@ class Cleanup(commands.Cog):
reason = ( reason = (
"{}({}) deleted {} messages " "{}({}) deleted {} messages "
" made by {}({}) in channel {}." " 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) log.info(reason)
@ -240,7 +254,10 @@ class Cleanup(commands.Cog):
) )
reason = "{}({}) deleted {} messages in channel {}.".format( 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) log.info(reason)
@ -277,7 +294,10 @@ class Cleanup(commands.Cog):
to_delete.append(ctx.message) to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages in channel {}.".format( 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) log.info(reason)
@ -319,7 +339,10 @@ class Cleanup(commands.Cog):
) )
to_delete.append(ctx.message) to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages in channel {}.".format( 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) log.info(reason)
@ -420,7 +443,12 @@ class Cleanup(commands.Cog):
reason = ( reason = (
"{}({}) deleted {} " "{}({}) deleted {} "
" command messages in channel {}." " 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) log.info(reason)
@ -500,7 +528,12 @@ class Cleanup(commands.Cog):
reason = ( reason = (
"{}({}) deleted {} messages " "{}({}) deleted {} messages "
"sent by the bot in {}." "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) log.info(reason)

View File

@ -10,7 +10,7 @@ import discord
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
from redbot.core import Config, bank, commands, errors from redbot.core import Config, bank, commands, errors
from redbot.core.i18n import Translator, cog_i18n 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.utils.menus import menu, DEFAULT_CONTROLS
from redbot.core.bot import Red from redbot.core.bot import Red
@ -162,7 +162,7 @@ class Economy(commands.Cog):
await ctx.send( await ctx.send(
_("{user}'s balance is {num} {currency}").format( _("{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( await ctx.send(
_("{user} transferred {num} {currency} to {other_user}").format( _("{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) await bank.deposit_credits(to, creds.sum)
msg = _("{author} added {num} {currency} to {user}'s account.").format( msg = _("{author} added {num} {currency} to {user}'s account.").format(
author=author.display_name, author=author.display_name,
num=creds.sum, num=humanize_number(creds.sum),
currency=currency, currency=currency,
user=to.display_name, user=to.display_name,
) )
@ -211,7 +214,7 @@ class Economy(commands.Cog):
await bank.withdraw_credits(to, creds.sum) await bank.withdraw_credits(to, creds.sum)
msg = _("{author} removed {num} {currency} from {user}'s account.").format( msg = _("{author} removed {num} {currency} from {user}'s account.").format(
author=author.display_name, author=author.display_name,
num=creds.sum, num=humanize_number(creds.sum),
currency=currency, currency=currency,
user=to.display_name, user=to.display_name,
) )
@ -219,7 +222,7 @@ class Economy(commands.Cog):
await bank.set_balance(to, creds.sum) await bank.set_balance(to, creds.sum)
msg = _("{author} set {user}'s account balance to {num} {currency}.").format( msg = _("{author} set {user}'s account balance to {num} {currency}.").format(
author=author.display_name, author=author.display_name,
num=creds.sum, num=humanize_number(creds.sum),
currency=currency, currency=currency,
user=to.display_name, user=to.display_name,
) )
@ -271,7 +274,9 @@ class Economy(commands.Cog):
"You've reached the maximum amount of {currency}!" "You've reached the maximum amount of {currency}!"
"Please spend some more \N{GRIMACING FACE}\n\n" "Please spend some more \N{GRIMACING FACE}\n\n"
"You currently have {new_balance} {currency}." "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 return
next_payday = cur_time + await self.config.PAYDAY_TIME() next_payday = cur_time + await self.config.PAYDAY_TIME()
@ -287,9 +292,9 @@ class Economy(commands.Cog):
).format( ).format(
author=author, author=author,
currency=credits_name, currency=credits_name,
amount=await self.config.PAYDAY_CREDITS(), amount=humanize_number(await self.config.PAYDAY_CREDITS()),
new_balance=await bank.get_balance(author), new_balance=humanize_number(await bank.get_balance(author)),
pos=pos, pos=humanize_number(pos) if pos else pos,
) )
) )
@ -319,7 +324,9 @@ class Economy(commands.Cog):
"You've reached the maximum amount of {currency}! " "You've reached the maximum amount of {currency}! "
"Please spend some more \N{GRIMACING FACE}\n\n" "Please spend some more \N{GRIMACING FACE}\n\n"
"You currently have {new_balance} {currency}." "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 return
next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME() next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
@ -334,9 +341,9 @@ class Economy(commands.Cog):
).format( ).format(
author=author, author=author,
currency=credits_name, currency=credits_name,
amount=credit_amount, amount=humanize_number(credit_amount),
new_balance=await bank.get_balance(author), new_balance=humanize_number(await bank.get_balance(author)),
pos=pos, pos=humanize_number(pos) if pos else pos,
) )
) )
else: else:
@ -364,7 +371,7 @@ class Economy(commands.Cog):
else: else:
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild) bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
try: 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 # first user is the largest we'll see
except IndexError: except IndexError:
return await ctx.send(_("There are no accounts in the bank.")) 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): if await ctx.bot.is_owner(ctx.author):
user_id = f"({str(acc[0])})" user_id = f"({str(acc[0])})"
name = f"{acc[1]['name']} {user_id}" name = f"{acc[1]['name']} {user_id}"
balance = acc[1]["balance"] balance = humanize_number(acc[1]["balance"])
if acc[0] != author.id: 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: else:
temp_msg += ( temp_msg += (
f"{f'{pos}.': <{pound_len+2}} " f"{f'{humanize_number(pos)}.': <{pound_len+2}} "
f"{balance: <{bal_len + 5}} " f"{balance: <{bal_len + 5}} "
f"<<{author.display_name}>>\n" 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}!" "Please spend some more \N{GRIMACING FACE}\n{old_balance} -> {new_balance}!"
).format( ).format(
currency=await bank.get_currency_name(getattr(channel, "guild", None)), currency=await bank.get_currency_name(getattr(channel, "guild", None)),
old_balance=then, old_balance=humanize_number(then),
new_balance=exc.max_balance, new_balance=humanize_number(exc.max_balance),
) )
) )
return return
@ -523,10 +533,10 @@ class Economy(commands.Cog):
slot=slot, slot=slot,
author=author, author=author,
phrase=phrase, phrase=phrase,
bid=bid, bid=humanize_number(bid),
old_balance=then, old_balance=humanize_number(then),
new_balance=now, new_balance=humanize_number(now),
pay=pay, pay=humanize_number(pay),
) )
) )
@ -552,12 +562,12 @@ class Economy(commands.Cog):
"Payday cooldown: {payday_time}\n" "Payday cooldown: {payday_time}\n"
"Amount given at account registration: {register_amount}" "Amount given at account registration: {register_amount}"
).format( ).format(
slot_min=await conf.SLOT_MIN(), slot_min=humanize_number(await conf.SLOT_MIN()),
slot_max=await conf.SLOT_MAX(), slot_max=humanize_number(await conf.SLOT_MAX()),
slot_time=await conf.SLOT_TIME(), slot_time=humanize_number(await conf.SLOT_TIME()),
payday_time=await conf.PAYDAY_TIME(), payday_time=humanize_number(await conf.PAYDAY_TIME()),
payday_amount=await conf.PAYDAY_CREDITS(), payday_amount=humanize_number(await conf.PAYDAY_CREDITS()),
register_amount=await bank.get_default_balance(guild), 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) await self.config.guild(guild).SLOT_MIN.set(bid)
credits_name = await bank.get_currency_name(guild) credits_name = await bank.get_currency_name(guild)
await ctx.send( 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() @economyset.command()
@ -594,7 +606,9 @@ class Economy(commands.Cog):
else: else:
await self.config.guild(guild).SLOT_MAX.set(bid) await self.config.guild(guild).SLOT_MAX.set(bid)
await ctx.send( 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() @economyset.command()
@ -635,7 +649,7 @@ class Economy(commands.Cog):
await self.config.guild(guild).PAYDAY_CREDITS.set(creds) await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
await ctx.send( await ctx.send(
_("Every payday will now give {num} {currency}.").format( _("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} " "Every payday will now give {num} {currency} "
"to people with the role {role_name}." "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() @economyset.command()
@ -668,7 +682,7 @@ class Economy(commands.Cog):
await bank.set_default_balance(creds, guild) await bank.set_default_balance(creds, guild)
await ctx.send( await ctx.send(
_("Registering an account will now give {num} {currency}.").format( _("Registering an account will now give {num} {currency}.").format(
num=creds, currency=credits_name num=humanize_number(creds), currency=credits_name
) )
) )

View File

@ -7,7 +7,7 @@ import discord
from redbot.core import commands from redbot.core import commands
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS 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__) _ = T_ = Translator("General", __file__)
@ -89,7 +89,11 @@ class General(commands.Cog):
author = ctx.author author = ctx.author
if number > 1: if number > 1:
n = randint(1, number) 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: else:
await ctx.send(_("{author.mention} Maybe higher than 1? ;P").format(author=author)) 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): async def serverinfo(self, ctx):
"""Show server information.""" """Show server information."""
guild = ctx.guild guild = ctx.guild
online = len([m.status for m in guild.members if m.status != discord.Status.offline]) online = humanize_number(
total_users = len(guild.members) len([m.status for m in guild.members if m.status != discord.Status.offline])
text_channels = len(guild.text_channels) )
voice_channels = len(guild.voice_channels) 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 passed = (ctx.message.created_at - guild.created_at).days
created_at = _("Since {date}. That's over {num} days ago!").format( created_at = _("Since {date}. That's over {num} days ago!").format(
date=guild.created_at.strftime("%d %b %Y %H:%M"), num=passed 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 = discord.Embed(description=created_at, colour=(await ctx.embed_colour()))
data.add_field(name=_("Region"), value=str(guild.region)) data.add_field(name=_("Region"), value=str(guild.region))
data.add_field(name=_("Users"), value=f"{online}/{total_users}") data.add_field(name=_("Users"), value=f"{online}/{total_users}")
data.add_field(name=_("Text Channels"), value=str(text_channels)) data.add_field(name=_("Text Channels"), value=text_channels)
data.add_field(name=_("Voice Channels"), value=str(voice_channels)) data.add_field(name=_("Voice Channels"), value=voice_channels)
data.add_field(name=_("Roles"), value=str(len(guild.roles))) data.add_field(name=_("Roles"), value=humanize_number(len(guild.roles)))
data.add_field(name=_("Owner"), value=str(guild.owner)) data.add_field(name=_("Owner"), value=str(guild.owner))
data.set_footer(text=_("Server ID: ") + str(guild.id)) data.set_footer(text=_("Server ID: ") + str(guild.id))

View File

@ -7,7 +7,7 @@ from typing import cast, Optional, Union
import discord import discord
from redbot.core import commands, i18n, checks, modlog 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 redbot.core.utils.mod import is_allowed_by_hierarchy, get_audit_reason
from .abc import MixinMeta from .abc import MixinMeta
from .converters import RawUserIds from .converters import RawUserIds
@ -244,7 +244,9 @@ class KickBanMixin(MixinMeta):
errors = {} errors = {}
async def show_results(): 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: if errors:
text += _("\nErrors:\n") text += _("\nErrors:\n")
text += "\n".join(errors.values()) text += "\n".join(errors.values())

View File

@ -6,7 +6,7 @@ from collections import Counter
import discord import discord
from redbot.core import bank from redbot.core import bank
from redbot.core.i18n import Translator 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 redbot.core.utils.common_filters import normalize_smartquotes
from .log import LOG from .log import LOG
@ -292,7 +292,7 @@ class TriviaSession:
" for coming first." " for coming first."
).format( ).format(
user=winner.display_name, user=winner.display_name,
num=amount, num=humanize_number(amount),
currency=await bank.get_currency_name(self.ctx.guild), currency=await bank.get_currency_name(self.ctx.guild),
) )
) )

View File

@ -5,6 +5,7 @@ from functools import wraps
import discord import discord
from redbot.core.utils.chat_formatting import humanize_number
from . import Config, errors, commands from . import Config, errors, commands
from .i18n import Translator from .i18n import Translator
@ -237,11 +238,20 @@ async def withdraw_credits(member: discord.Member, amount: int) -> int:
if not isinstance(amount, int): if not isinstance(amount, int):
raise TypeError("Withdrawal amount must be of type int, not {}.".format(type(amount))) raise TypeError("Withdrawal amount must be of type int, not {}.".format(type(amount)))
if _invalid_amount(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) bal = await get_balance(member)
if amount > bal: 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) 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): if not isinstance(amount, int):
raise TypeError("Deposit amount must be of type int, not {}.".format(type(amount))) raise TypeError("Deposit amount must be of type int, not {}.".format(type(amount)))
if _invalid_amount(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) bal = await get_balance(member)
return await set_balance(member, amount + bal) 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): if not isinstance(amount, int):
raise TypeError("Transfer amount must be of type int, not {}.".format(type(amount))) raise TypeError("Transfer amount must be of type int, not {}.".format(type(amount)))
if _invalid_amount(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: if await get_balance(to) + amount > MAX_BALANCE:
currency = await get_currency_name(to.guild) currency = await get_currency_name(to.guild)
@ -727,7 +745,7 @@ def cost(amount: int):
credits_name = await get_currency_name(context.guild) credits_name = await get_currency_name(context.guild)
raise commands.UserFeedbackCheckFailure( raise commands.UserFeedbackCheckFailure(
_("You need at least {cost} {currency} to use this command.").format( _("You need at least {cost} {currency} to use this command.").format(
cost=amount, currency=credits_name cost=humanize_number(amount), currency=credits_name
) )
) )
else: else:

View File

@ -2,6 +2,7 @@ import importlib.machinery
import discord import discord
from redbot.core.utils.chat_formatting import humanize_number
from .i18n import Translator from .i18n import Translator
_ = Translator(__name__, __file__) _ = Translator(__name__, __file__)
@ -45,8 +46,8 @@ class BalanceTooHigh(BankError, OverflowError):
self.currency_name = currency_name self.currency_name = currency_name
def __str__(self) -> str: def __str__(self) -> str:
return _("{user}'s balance cannot rise above {max:,} {currency}.").format( return _("{user}'s balance cannot rise above max {currency}.").format(
user=self.user, max=self.max_balance, currency=self.currency_name user=self.user, max=humanize_number(self.max_balance), currency=self.currency_name
) )

View File

@ -1,10 +1,21 @@
import contextlib import contextlib
import functools
import io import io
import os import os
from pathlib import Path 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" _current_locale = "en-US"
@ -160,6 +171,44 @@ class Translator(Callable[[str], str]):
self.translations[untranslated] = translated 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 import to be down here to avoid circular import issues.
# This will be cleaned up at a later date # This will be cleaned up at a later date
# noinspection PyPep8 # noinspection PyPep8

View File

@ -1,11 +1,13 @@
import itertools import itertools
import datetime import datetime
from typing import Sequence, Iterator, List, Optional from typing import Sequence, Iterator, List, Optional, Union
from io import BytesIO 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__) _ = Translator("UtilsChatFormatting", __file__)
@ -432,6 +434,25 @@ def humanize_timedelta(
return ", ".join(strings) 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( def text_to_file(
text: str, filename: str = "file.txt", *, spoiler: bool = False, encoding: str = "utf-8" text: str, filename: str = "file.txt", *, spoiler: bool = False, encoding: str = "utf-8"
): ):

View File

@ -31,6 +31,7 @@ install_requires =
appdirs==1.4.3 appdirs==1.4.3
async-timeout==3.0.1 async-timeout==3.0.1
attrs==19.1.0 attrs==19.1.0
babel==2.7.0
chardet==3.0.4 chardet==3.0.4
Click==7.0 Click==7.0
colorama==0.4.1 colorama==0.4.1

View File

@ -9,6 +9,7 @@ install_requires =
aiohttp aiohttp
aiohttp-json-rpc aiohttp-json-rpc
appdirs appdirs
babel
click click
colorama colorama
discord.py discord.py