From 8ec2483918d466bc70a90e7b9c863ed05ba14873 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Sun, 1 Nov 2026 19:43:51 +0100 Subject: [PATCH] use norwegian number formatting Add misc_utils.format_number, which separates thousands with a non-breaking space and uses a decimal comma, and use it for the numbers shown in embeds and messages. Co-Authored-By: Claude Opus 5.5 --- src/cogs/anime.py | 76 ++++++++++++++++++++++----------- src/cogs/aurora.py | 2 +- src/cogs/birthday.py | 4 +- src/cogs/botinfo.py | 13 +++--- src/cogs/errors.py | 5 ++- src/cogs/info.py | 28 +++++++----- src/cogs/social_credit.py | 8 +++- src/cogs/streak.py | 6 ++- src/cogs/utils/discord_utils.py | 4 +- src/cogs/utils/misc_utils.py | 23 ++++++++++ src/cogs/word_cloud.py | 2 +- 11 files changed, 120 insertions(+), 51 deletions(-) diff --git a/src/cogs/anime.py b/src/cogs/anime.py index 5526c73..e3dabeb 100644 --- a/src/cogs/anime.py +++ b/src/cogs/anime.py @@ -168,6 +168,22 @@ class Anime(commands.Cog): return entry[stat] return 0 + def format_count(self, count: int | None) -> str: + """ + Formats a count from the Anilist API, which is None when Anilist doesn't know it, + like the episodes of an anime that is still airing + + Parameters + ---------- + count (int | None): The count + + Returns + ---------- + (str): The formatted count. ? if it is unknown + """ + + return "?" if count is None else misc_utils.format_number(count) + def is_nsfw_channel(self, interaction: discord.Interaction) -> bool: """ Checks whether the interaction's channel is age-restricted. DMs are never treated as NSFW @@ -441,10 +457,10 @@ class Anime(commands.Cog): embed = discord.Embed(title=user_name, color=color, url=url) embed.set_author(name="Anilist", icon_url=self.anilist_logo) embed.set_thumbnail(url=profile_pic) - embed.add_field(name="Antall dager sett", value=days_watched) - embed.add_field(name="Antall anime sett", value=anime_completed) - embed.add_field(name="Antall kapitler lest", value=chapters_read) - embed.add_field(name="Antall manga lest", value=manga_completed) + embed.add_field(name="Antall dager sett", value=misc_utils.format_number(days_watched)) + embed.add_field(name="Antall anime sett", value=misc_utils.format_number(anime_completed)) + embed.add_field(name="Antall kapitler lest", value=misc_utils.format_number(chapters_read)) + embed.add_field(name="Antall manga lest", value=misc_utils.format_number(manga_completed)) if favourite_anime: embed.add_field(name="Noen favorittanime", value=favourite_anime, inline=False) if favourite_manga: @@ -528,8 +544,7 @@ class Anime(commands.Cog): dropped = self.get_status_stat(statuses, "DROPPED") anime_mean_score = data["statistics"]["anime"]["meanScore"] - if not anime_mean_score: - anime_mean_score = "**Ingen**" + anime_mean_score = misc_utils.format_number(anime_mean_score) if anime_mean_score else "**Ingen**" most_watched_genres = ", ".join([genre["genre"] for genre in data["statistics"]["anime"]["genres"]]) most_watched_studios = self.construct_favorite_entity_string(data["statistics"]["anime"]["studios"]) @@ -538,12 +553,15 @@ class Anime(commands.Cog): embed.set_author(name="Anilist", icon_url=self.anilist_logo) embed.set_thumbnail(url=profile_pic) embed.add_field(name="Gj.snittsvurdering gitt", value=anime_mean_score) - embed.add_field(name="Antall dager sett", value=days_watched) - embed.add_field(name="Antall episoder sett", value=episodes_watched) - embed.add_field(name="Antall anime sett", value=completed) - embed.add_field(name="Ser på nå", value=watching) - embed.add_field(name="Planlegger å se", value=f"{planning}\n({planning_days} dager)") - embed.add_field(name="Droppet", value=dropped) + embed.add_field(name="Antall dager sett", value=misc_utils.format_number(days_watched)) + embed.add_field(name="Antall episoder sett", value=misc_utils.format_number(episodes_watched)) + embed.add_field(name="Antall anime sett", value=misc_utils.format_number(completed)) + embed.add_field(name="Ser på nå", value=misc_utils.format_number(watching)) + embed.add_field( + name="Planlegger å se", + value=f"{misc_utils.format_number(planning)}\n({misc_utils.format_number(planning_days)} dager)", + ) + embed.add_field(name="Droppet", value=misc_utils.format_number(dropped)) if most_watched_genres: embed.add_field(name="Mest sette sjangere", value=most_watched_genres, inline=False) if most_watched_studios: @@ -623,8 +641,7 @@ class Anime(commands.Cog): dropped = self.get_status_stat(statuses, "DROPPED") manga_mean_score = data["statistics"]["manga"]["meanScore"] - if manga_mean_score == 0: - manga_mean_score = "**Ingen**" + manga_mean_score = misc_utils.format_number(manga_mean_score) if manga_mean_score else "**Ingen**" most_read_genres = ", ".join([genre["genre"] for genre in data["statistics"]["manga"]["genres"]]) most_read_staff = self.construct_favorite_entity_string(data["statistics"]["manga"]["staff"]) @@ -633,12 +650,15 @@ class Anime(commands.Cog): embed.set_author(name="Anilist", icon_url=self.anilist_logo) embed.set_thumbnail(url=profile_pic) embed.add_field(name="Gj.snittsvurdering gitt", value=manga_mean_score) - embed.add_field(name="Antall kapitler lest", value=chapters_read) - embed.add_field(name="Antall volum lest", value=volumes_read) - embed.add_field(name="Antall manga lest", value=completed) - embed.add_field(name="Leser nå", value=reading) - embed.add_field(name="Planlegger å lese", value=f"{planning}\n({planning_chapters} kapitler)") - embed.add_field(name="Droppet", value=dropped) + embed.add_field(name="Antall kapitler lest", value=misc_utils.format_number(chapters_read)) + embed.add_field(name="Antall volum lest", value=misc_utils.format_number(volumes_read)) + embed.add_field(name="Antall manga lest", value=misc_utils.format_number(completed)) + embed.add_field(name="Leser nå", value=misc_utils.format_number(reading)) + embed.add_field( + name="Planlegger å lese", + value=f"{misc_utils.format_number(planning)}\n({misc_utils.format_number(planning_chapters)} kapitler)", + ) + embed.add_field(name="Droppet", value=misc_utils.format_number(dropped)) if most_read_genres: embed.add_field(name="Mest leste sjangere", value=most_read_genres, inline=False) if most_read_staff: @@ -789,11 +809,15 @@ class Anime(commands.Cog): embed.add_field(name="Regissør", value=director_string) embed.add_field(name="Utgivelsesdato", value=date) if media_format == "Film": - embed.add_field(name="Lengde", value=f"{duration} minutter") + embed.add_field(name="Lengde", value=f"{self.format_count(duration)} minutter") + elif duration is None: + embed.add_field(name="Episoder", value=self.format_count(episodes)) else: - embed.add_field(name="Episoder", value=f"{episodes} ({duration} {length_string})") + embed.add_field( + name="Episoder", value=f"{self.format_count(episodes)} ({self.format_count(duration)} {length_string})" + ) if mean_score: - embed.add_field(name="Gj.snittsvurdering", value=f"{mean_score}/100") + embed.add_field(name="Gj.snittsvurdering", value=f"{misc_utils.format_number(mean_score)}/100") embed.add_field(name="Sjangere", value=genres) if len(description) < 1024 and description: embed.add_field(name="Sammendrag", value=description, inline=False) @@ -924,9 +948,11 @@ class Anime(commands.Cog): if staff_string: embed.add_field(name="Laget av", value=staff_string) embed.add_field(name="Utgivelsesdato", value=date) - embed.add_field(name="Lengde", value=f"{chapters} {chapters_string}\n{volumes} volum") + embed.add_field( + name="Lengde", value=f"{self.format_count(chapters)} {chapters_string}\n{self.format_count(volumes)} volum" + ) if mean_score: - embed.add_field(name="Gj.snittsvurdering", value=f"{mean_score}/100") + embed.add_field(name="Gj.snittsvurdering", value=f"{misc_utils.format_number(mean_score)}/100") embed.add_field(name="Sjangere", value=genres) if len(description) < 1024 and description: embed.add_field(name="Sammendrag", value=description, inline=False) diff --git a/src/cogs/aurora.py b/src/cogs/aurora.py index 25935af..222117d 100644 --- a/src/cogs/aurora.py +++ b/src/cogs/aurora.py @@ -118,7 +118,7 @@ class Aurora(commands.Cog): description=f"### Størst sjanse mellom {start} og {end}", color=discord.Color.purple(), ) - embed.add_field(name="Sjanse for Nordlys", value=f"{round(forecast['auroraValue'] * 100)}%", inline=False) + embed.add_field(name="Sjanse for Nordlys", value=f"{round(forecast['auroraValue'] * 100)}\u00a0%", inline=False) embed.add_field(name="KP-Indeks", value=forecast["kpIndex"]) embed.add_field(name="Skydekke", value=f"{forecast['cloudCover']['value']}%") await channel.send( diff --git a/src/cogs/birthday.py b/src/cogs/birthday.py index 2400bd5..fb3ee9e 100644 --- a/src/cogs/birthday.py +++ b/src/cogs/birthday.py @@ -366,7 +366,9 @@ class Birthday(commands.Cog): embed.add_field( name="Bursdag", value=discord.utils.format_dt(misc_utils.localize(birthday, self.bot.timezone), style="D") ) - embed.add_field(name="Hvor gammel?", value=f"{years_old} år\n({days_old} dager)", inline=False) + embed.add_field( + name="Hvor gammel?", value=f"{years_old} år\n({misc_utils.format_number(days_old)} dager)", inline=False + ) embed.add_field(name="Weeb age", value=japanese_era, inline=False) embed.add_field( name="Neste bursdag om", diff --git a/src/cogs/botinfo.py b/src/cogs/botinfo.py index 6c0cfb5..5f09b5e 100644 --- a/src/cogs/botinfo.py +++ b/src/cogs/botinfo.py @@ -70,18 +70,18 @@ class BotInfo(commands.Cog): embed.set_thumbnail(url=self.bot.user.display_avatar) embed.add_field(name="Dev", value=team.name if team else f"{dev.mention}\n{dev.display_name}") embed.add_field(name="Oppetid", value=self.get_uptime()) - embed.add_field(name="Ping", value=f"Websocket ping: {self.get_ping()} ms") - embed.add_field(name="Servere", value=len(self.bot.guilds)) + embed.add_field(name="Ping", value=f"Websocket ping: {misc_utils.format_number(self.get_ping())} ms") + embed.add_field(name="Servere", value=misc_utils.format_number(len(self.bot.guilds))) embed.add_field(name="Discord.py", value=discord.__version__) embed.add_field(name="Python", value=platform.python_version()) - embed.add_field(name="Ressursbruk", value=f"RAM: {memory_usage} MB") + embed.add_field(name="Ressursbruk", value=f"RAM: {misc_utils.format_number(memory_usage)} MB") embed.add_field(name="Kernel", value=f"{platform.system()} {platform.release()}") if exists("/.dockerenv"): embed.add_field(name="Docker", value="✅") if version := self.bot.version: embed.add_field(name="Versjon", value=f"`{version.branch}`\n{self.commit_link(version)}") embed.add_field( - name=f"Brukere ({len(statuses)})", + name=f"Brukere ({misc_utils.format_number(len(statuses))})", value=discord_utils.format_status_counts(status_counts, self.bot.emoji, "\n"), ) embed.add_field( @@ -119,7 +119,10 @@ class BotInfo(commands.Cog): """ embed = discord.Embed(color=interaction.client.user.color) - embed.add_field(name="📶 Ping", value=f"* **Websocket ping:** {self.get_ping()} ms\n* **I hodet:** ∞") + embed.add_field( + name="📶 Ping", + value=f"* **Websocket ping:** {misc_utils.format_number(self.get_ping())} ms\n* **I hodet:** ∞", + ) await interaction.response.send_message(embed=embed, content=None) @app_commands.checks.bot_has_permissions(embed_links=True) diff --git a/src/cogs/errors.py b/src/cogs/errors.py index 216f03a..ad81696 100644 --- a/src/cogs/errors.py +++ b/src/cogs/errors.py @@ -5,6 +5,7 @@ from discord import app_commands from discord.ext import commands from cogs.utils import embed_templates +from cogs.utils import misc_utils class Errors(commands.Cog): @@ -97,7 +98,7 @@ class Errors(commands.Cog): embed = embed_templates.error_warning("Bare boteieren kan gjøre dette") elif isinstance(error, commands.CommandOnCooldown): embed = embed_templates.error_warning( - f"Kommandoen har nettopp blitt brukt\nPrøv igjen om `{error.retry_after:.1f}` sekunder.", + f"Kommandoen har nettopp blitt brukt\nPrøv igjen om `{misc_utils.format_number(error.retry_after, 1)}` sekunder.", ) elif isinstance(error, commands.NoPrivateMessage): embed = embed_templates.error_warning("Denne kommandoen kan bare utføres it servere") @@ -143,7 +144,7 @@ class Errors(commands.Cog): embed = self.error_missing_perms(error.missing_permissions, "Du") elif isinstance(error, app_commands.CommandOnCooldown): embed = embed_templates.error_warning( - f"Kommandoen har nettopp blitt brukt\nPrøv igjen om `{error.retry_after:.1f}` sekunder." + f"Kommandoen har nettopp blitt brukt\nPrøv igjen om `{misc_utils.format_number(error.retry_after, 1)}` sekunder." ) else: embed = embed_templates.error_fatal("En ukjent feil oppstod!") diff --git a/src/cogs/info.py b/src/cogs/info.py index 71dc8e3..8066959 100644 --- a/src/cogs/info.py +++ b/src/cogs/info.py @@ -105,7 +105,7 @@ class Info(commands.Cog): # Days since guild was created creation_date_timestamp = discord.utils.format_dt(interaction.guild.created_at, style="F") since_created_days = (interaction.created_at - interaction.guild.created_at).days - days_ago = f"{since_created_days} dager" if since_created_days != 1 else f"{since_created_days} dag" + days_ago = f"{misc_utils.format_number(since_created_days)} {'dag' if since_created_days == 1 else 'dager'}" # Member count and status counts total_members = interaction.guild.member_count @@ -208,9 +208,9 @@ class Info(commands.Cog): + f"🗃️ Kategorier: **{categories}**", ) embed.add_field( - name=f"Medlemmer ({total_members})", - value=f"👤 Mennesker: **{int(total_members) - int(bot_members)}**\n" - + f"🤖 Båtter: **{bot_members}**\n" + name=f"Medlemmer ({misc_utils.format_number(total_members)})", + value=f"👤 Mennesker: **{misc_utils.format_number(total_members - bot_members)}**\n" + + f"🤖 Båtter: **{misc_utils.format_number(bot_members)}**\n" + discord_utils.format_status_counts(status_counts, self.bot.emoji, " "), ) embed.add_field(name=f"Roller ({len(interaction.guild.roles) - 1})", value=roles, inline=False) @@ -371,7 +371,7 @@ class Info(commands.Cog): embed.add_field(name="Fargekode", value=str(role.color)) embed.add_field( name="Opprettet", - value=f"{created_at_timestamp}\n{since_created_days} " + value=f"{created_at_timestamp}\n{misc_utils.format_number(since_created_days)} " + f'{"dager" if since_created_days != 1 else "dag"} siden', ) embed.add_field(name="Posisjon", value=role.position) @@ -379,7 +379,9 @@ class Info(commands.Cog): embed.add_field(name="Vises separat i medlemsliste", value="Ja" if role.hoist else "Nei") if permissions: embed.add_field(name="Tillatelser", value=permissions, inline=False) - embed.add_field(name=f"Brukere med rollen ({len(role.members)})", value=members, inline=False) + embed.add_field( + name=f"Brukere med rollen ({misc_utils.format_number(len(role.members))})", value=members, inline=False + ) await interaction.response.send_message(embed=embed) @app_commands.checks.bot_has_permissions(embed_links=True) @@ -413,7 +415,7 @@ class Info(commands.Cog): ) if channel.category: embed.add_field(name="Kategori", value=channel.category.name) - embed.add_field(name=f"Antall med tilgang ({len(channel.members)})", value=members) + embed.add_field(name=f"Antall med tilgang ({misc_utils.format_number(len(channel.members))})", value=members) await interaction.response.send_message(embed=embed) @app_commands.checks.bot_has_permissions(embed_links=True) @@ -458,7 +460,9 @@ class Info(commands.Cog): roles = sorted( [r for r in interaction.guild.roles if r.name != "@everyone"], key=lambda x: len(x.members), reverse=True ) - roles_formatted = [f"**#{i + 1}** {r.mention} - {len(r.members)}" for i, r in enumerate(roles)] + roles_formatted = [ + f"**#{i + 1}** {r.mention} - {misc_utils.format_number(len(r.members))}" for i, r in enumerate(roles) + ] paginator = misc_utils.Paginator(roles_formatted) view = discord_utils.Scroller(paginator, interaction.user) @@ -690,16 +694,18 @@ class Info(commands.Cog): embed.set_thumbnail(url=user.display_avatar) embed.add_field( name="Opprettet", - value=f"{created_timestamp}\n{created_days} " + f'{"dag" if created_days == 1 else "dager"} siden', + value=f"{created_timestamp}\n{misc_utils.format_number(created_days)} " + + f'{"dag" if created_days == 1 else "dager"} siden', ) embed.add_field( name="Ble med i serveren", - value=f"{joined_timestamp}\n{joined_days} " + f'{"dag" if joined_days == 1 else "dager"} siden', + value=f"{joined_timestamp}\n{misc_utils.format_number(joined_days)} " + + f'{"dag" if joined_days == 1 else "dager"} siden', ) if user.premium_since: embed.add_field( name="Boost", - value=f"{premium_since}\n{premium_since_days} " + value=f"{premium_since}\n{misc_utils.format_number(premium_since_days)} " + f'{"dag" if premium_since_days == 1 else "dager"} siden\n' + f"Booster #{premium_index} av serveren", inline=False, diff --git a/src/cogs/social_credit.py b/src/cogs/social_credit.py index d84f172..8427f3b 100644 --- a/src/cogs/social_credit.py +++ b/src/cogs/social_credit.py @@ -280,7 +280,9 @@ class SocialCredit(commands.Cog): db_user = CreditUser(*result) - embed = discord.Embed(description=(f"{user.mention} har `{db_user.credit_score}` social credits")) + embed = discord.Embed( + description=(f"{user.mention} har `{misc_utils.format_number(db_user.credit_score)}` social credits") + ) await interaction.response.send_message(embed=embed) @app_commands.checks.bot_has_permissions(embed_links=True) @@ -307,7 +309,9 @@ class SocialCredit(commands.Cog): embed=embed_templates.error_warning("Ingen brukere er registrert i databasen") ) - leaderboard_formatted = [f"**#{s[0]+1}** <@{s[1][0]}> - `{s[1][1]}` poeng" for s in enumerate(result)] + leaderboard_formatted = [ + f"**#{s[0]+1}** <@{s[1][0]}> - `{misc_utils.format_number(s[1][1])}` poeng" for s in enumerate(result) + ] paginator = misc_utils.Paginator(leaderboard_formatted) view = discord_utils.Scroller(paginator, interaction.user) diff --git a/src/cogs/streak.py b/src/cogs/streak.py index 6c95a36..3d9e65a 100644 --- a/src/cogs/streak.py +++ b/src/cogs/streak.py @@ -278,7 +278,7 @@ class Streak(commands.Cog): embed = discord.Embed(title="Streak", description=user.mention) embed.set_author(name=user.name, icon_url=user.avatar) - embed.add_field(name="Antall dager", value=streak_days) + embed.add_field(name="Antall dager", value=misc_utils.format_number(streak_days)) embed.add_field(name="Startet streaken", value=f"{streak_msg_timestamp}\n{streak_msg_link}") await interaction.followup.send(embed=embed) @@ -308,7 +308,9 @@ class Streak(commands.Cog): streaks_formatted = [] for i, (user_id, streak_start_time) in enumerate(streaks, start=1): days = (discord.utils.utcnow() - streak_start_time).days - streaks_formatted.append(f"**#{i}** <@{user_id}> - {days} {'dag' if days == 1 else 'dager'}") + streaks_formatted.append( + f"**#{i}** <@{user_id}> - {misc_utils.format_number(days)} {'dag' if days == 1 else 'dager'}" + ) paginator = misc_utils.Paginator(streaks_formatted) view = discord_utils.Scroller(paginator, interaction.user) diff --git a/src/cogs/utils/discord_utils.py b/src/cogs/utils/discord_utils.py index 7226013..18a8285 100644 --- a/src/cogs/utils/discord_utils.py +++ b/src/cogs/utils/discord_utils.py @@ -31,7 +31,9 @@ def format_status_counts(status_counts: Counter[discord.Status], emoji: dict[str (str): The formatted status counts """ - return separator.join(f"{emoji[status.value]}{status_counts[status]}" for status in SHOWN_STATUSES) + return separator.join( + f"{emoji[status.value]}{misc_utils.format_number(status_counts[status])}" for status in SHOWN_STATUSES + ) async def send_as_txt_file(interaction: discord.Interaction, content: str, filename: str): diff --git a/src/cogs/utils/misc_utils.py b/src/cogs/utils/misc_utils.py index 49883f7..835e570 100644 --- a/src/cogs/utils/misc_utils.py +++ b/src/cogs/utils/misc_utils.py @@ -35,6 +35,29 @@ def localize(dt: datetime.datetime, tz: datetime.tzinfo | None) -> datetime.date return dt.replace(tzinfo=tz) if tz else dt.astimezone() +# Swaps the separators of Python's number formatting for Norwegian ones. The non-breaking space keeps +# Discord from splitting a number across two lines +NORWEGIAN_SEPARATORS = str.maketrans({",": "\u00a0", ".": ","}) + + +def format_number(number: float, decimals: int | None = None) -> str: + """ + Format a number the Norwegian way, with spaces between the thousands and a decimal comma + + Parameters + ---------- + number (float): The number to format + decimals (int | None): How many decimals to round to. None shows the number as is + + Returns + ---------- + (str): The formatted number, like 1 234 567,8 + """ + + formatted = f"{number:,}" if decimals is None else f"{number:,.{decimals}f}" + return formatted.translate(NORWEGIAN_SEPARATORS) + + @dataclass class GitVersion: """The branch and commit the code is checked out at""" diff --git a/src/cogs/word_cloud.py b/src/cogs/word_cloud.py index 696025e..aec6889 100644 --- a/src/cogs/word_cloud.py +++ b/src/cogs/word_cloud.py @@ -576,7 +576,7 @@ class WordCloud(commands.Cog): word_cloud_file = discord.File(word_cloud, filename=f"wordcloud_{interaction.user.id}.png") embed = discord.Embed(title="☁️ Her er ordskyen din! ☁️") - embed.description = f"Basert på de {len(all_messages.split())} mest brukte ordene dine blant de {amount} siste meldingene i {len(channels)} kanaler" + embed.description = f"Basert på de {misc_utils.format_number(len(all_messages.split()))} mest brukte ordene dine blant de {misc_utils.format_number(amount)} siste meldingene i {misc_utils.format_number(len(channels))} kanaler" embed.set_image(url=f"attachment://wordcloud_{interaction.user.id}.png") await interaction.followup.send(embed=embed, file=word_cloud_file)