From 3325536bbc7d7d7afc4c09245b01d5f5e11a37e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 7 Mar 2024 18:27:05 -0600 Subject: [PATCH] feat: random localizations --- .vscode/settings.json | 1 + src/app/tools/ratings/page.tsx | 8 +- src/app/tools/session/page.tsx | 4 +- src/commands/evolution.tsx | 4 +- src/commands/inactive.tsx | 4 +- src/commands/link.tsx | 35 ++-- src/commands/ratings.tsx | 219 ++++++++++++++--------- src/commands/searchClans.ts | 50 ++++-- src/commands/searchPlayers.ts | 39 +++- src/constants/leagues.ts | 10 +- src/constants/regions.ts | 18 +- src/core/discord/addRegionChoices.ts | 33 +++- src/core/discord/autocompleteClan.ts | 11 +- src/core/discord/autocompleteUsername.ts | 13 +- src/core/discord/localizationObject.ts | 4 +- src/events/interactionCreate/index.ts | 11 +- src/lang/en-US.json | 125 +++++++++++++ 17 files changed, 423 insertions(+), 166 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index dca4e6fa9..57b505836 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -131,6 +131,7 @@ "TYPELESS", "ubjson", "Unicum", + "UNLOCALIZED", "UNORM", "Unregistering", "vickers", diff --git a/src/app/tools/ratings/page.tsx b/src/app/tools/ratings/page.tsx index 979e4c1c3..555bb9f8d 100644 --- a/src/app/tools/ratings/page.tsx +++ b/src/app/tools/ratings/page.tsx @@ -20,7 +20,11 @@ import PageWrapper from '../../../components/PageWrapper'; import { Skeleton } from '../../../components/Skeleton'; import { LEAGUES } from '../../../constants/leagues'; import { FIRST_ARCHIVED_RATINGS_SEASON } from '../../../constants/ratings'; -import { REGIONS, REGION_NAMES, Region } from '../../../constants/regions'; +import { + REGIONS, + Region, + UNLOCALIZED_REGION_NAMES, +} from '../../../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID'; import fetchBlitz from '../../../core/blitz/fetchBlitz'; import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo'; @@ -556,7 +560,7 @@ export default function Page() { {REGIONS.map((region) => ( - {REGION_NAMES[region]} + {UNLOCALIZED_REGION_NAMES[region]} ))} diff --git a/src/app/tools/session/page.tsx b/src/app/tools/session/page.tsx index eecd2e180..a092b4f8a 100644 --- a/src/app/tools/session/page.tsx +++ b/src/app/tools/session/page.tsx @@ -18,7 +18,7 @@ import { import { debounce } from 'lodash'; import { ChangeEvent, useEffect, useRef, useState } from 'react'; import PageWrapper from '../../../components/PageWrapper'; -import { REGION_NAMES } from '../../../constants/regions'; +import { UNLOCALIZED_REGION_NAMES } from '../../../constants/regions'; import searchPlayersAcrossRegions, { AccountListWithServer, } from '../../../core/blitz/searchPlayersAcrossRegions'; @@ -107,7 +107,7 @@ export default function Page() { setShowSearchResults(false); setSession(region, id, nickname); }} - shortcut={REGION_NAMES[region]} + shortcut={UNLOCALIZED_REGION_NAMES[region]} > {nickname} diff --git a/src/commands/evolution.tsx b/src/commands/evolution.tsx index fd6a61cdd..fce791045 100644 --- a/src/commands/evolution.tsx +++ b/src/commands/evolution.tsx @@ -89,8 +89,8 @@ async function render( max: maxX, precision: 0, }} - xMinLabel={new Date(minTime * 1000).toDateString()} - xMaxLabel={new Date(maxTime * 1000).toDateString()} + xMinLabel={new Date(minTime * 1000).toLocaleDateString(locale)} + xMaxLabel={new Date(maxTime * 1000).toLocaleDateString(locale)} > {Graph.Line({ color: Graph.LineColor.Red, diff --git a/src/commands/inactive.tsx b/src/commands/inactive.tsx index d5f3d78ac..0c74df574 100644 --- a/src/commands/inactive.tsx +++ b/src/commands/inactive.tsx @@ -63,7 +63,9 @@ export const inactiveCommand = new Promise((resolve) => { ([name, days]) => [ name, - translate('bot.commands.inactive.body.listing', [`${days}`]), + translate('bot.commands.inactive.body.listing', [ + days.toFixed(0), + ]), ] as [string, string], ); const hasInactiveMembers = inactive.length > 0; diff --git a/src/commands/link.tsx b/src/commands/link.tsx index 5f81fa346..55c2cc463 100644 --- a/src/commands/link.tsx +++ b/src/commands/link.tsx @@ -1,18 +1,19 @@ import { teal } from '@radix-ui/colors'; -import { GuildMemberRoleManager, SlashCommandBuilder } from 'discord.js'; +import { GuildMemberRoleManager } from 'discord.js'; import markdownEscape from 'markdown-escape'; import discord from '../../discord.json' assert { type: 'json' }; import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow'; import CommandWrapper from '../components/CommandWrapper'; -import { REGION_NAMES } from '../constants/regions'; import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz'; import addUsernameChoices from '../core/discord/addUsernameChoices'; import autocompleteUsername from '../core/discord/autocompleteUsername'; +import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import embedInfo from '../core/discord/embedInfo'; import embedNegative from '../core/discord/embedNegative'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; +import { translator } from '../core/localization/translator'; import { CommandRegistry } from '../events/interactionCreate'; import { theme } from '../stitches.config'; @@ -21,14 +22,12 @@ export const verifyCommand = new Promise((resolve) => { inProduction: true, inPublic: true, - command: new SlashCommandBuilder() - .setName('link') - .setDescription('Links your Blitz and Discord account') - .addStringOption((option) => - addUsernameChoices(option).setRequired(true), - ), + command: createLocalizedCommand('link').addStringOption((option) => + addUsernameChoices(option).setRequired(true), + ), async handler(interaction) { + const { t, translate } = translator(interaction.locale); const { id, region } = await resolvePlayerFromCommand(interaction); const discordId = parseInt(interaction.user.id); const accountInfo = await getAccountInfo(region, id); @@ -39,8 +38,14 @@ export const verifyCommand = new Promise((resolve) => { if (interaction.guildId === discord.sklld_guild_id) { if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) { return embedNegative( - `${markdownEscape(interaction.user.username)} failed to verify`, - "I don't have the permission to change your manage roles.", + translate(translate('bot.commands.link.body.no_role_permissions'), [ + markdownEscape(interaction.user.username), + ]), + translate( + translate( + 'bot.commands.link.body.no_role_permissions.description', + ), + ), ); } @@ -140,7 +145,9 @@ export const verifyCommand = new Promise((resolve) => { height: 16, }} /> - Accounts linked + {t`bot.commands.link.body.accounts_linked`}
((resolve) => { {clanAccountInfo?.clan ? `[${clanAccountInfo?.clan?.tag}] • ` : ''} - {REGION_NAMES[region]} + {translate(`common.regions.normal.${region}`)}
@@ -202,8 +209,8 @@ export const verifyCommand = new Promise((resolve) => { , embedInfo( - 'Incorrect account?', - 'Use the command again and select a username from the search results.', + t`bot.commands.link.embed.title`, + t`bot.commands.link.embed.description`, ), ]; }, diff --git a/src/commands/ratings.tsx b/src/commands/ratings.tsx index 5fbe47c79..fcfbf4ac2 100644 --- a/src/commands/ratings.tsx +++ b/src/commands/ratings.tsx @@ -1,6 +1,6 @@ import { APIApplicationCommandOptionChoice, - SlashCommandBuilder, + Locale, SlashCommandSubcommandBuilder, } from 'discord.js'; import { range } from 'lodash'; @@ -11,7 +11,7 @@ import * as Leaderboard from '../components/Leaderboard'; import TitleBar from '../components/TitleBar'; import { LEAGUES } from '../constants/leagues'; import { FIRST_ARCHIVED_RATINGS_SEASON } from '../constants/ratings'; -import { REGION_NAMES_SHORT, Region } from '../constants/regions'; +import { Region } from '../constants/regions'; import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import getRatingsInfo from '../core/blitz/getRatingsInfo'; @@ -25,8 +25,10 @@ import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchive import addRegionChoices from '../core/discord/addRegionChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices'; import autocompleteUsername from '../core/discord/autocompleteUsername'; -import embedNegative from '../core/discord/embedNegative'; +import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; +import { localizationObject } from '../core/discord/localizationObject'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; +import { translator } from '../core/localization/translator'; import { CommandRegistry } from '../events/interactionCreate'; import { UserError } from '../hooks/userError'; @@ -117,10 +119,15 @@ export interface RatingsNeighbors { neighbors: RatingsPlayer[]; } -const noOngoingSeason = embedNegative( - 'No ongoing season', - "Wargaming didn't provide any data for this season.", -); +interface SimplifiedPlayer { + id: number; + score: number; + position: number; + clan: string | undefined; + nickname: string; +} + +const DEFAULT_LIMIT = 10; export const ratingsCommand = new Promise(async (resolve) => { const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber(); @@ -130,89 +137,131 @@ export const ratingsCommand = new Promise(async (resolve) => { latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1, ); - function addSubcommands(option: SlashCommandSubcommandBuilder) { + function addOptions(option: SlashCommandSubcommandBuilder) { + const { t, translate } = translator(Locale.EnglishUS); + return option - .addStringOption(addUsernameChoices) .addIntegerOption((option) => option - .setName('limit') - .setDescription('How many neighbors to display (default: 10)') + .setName(t`bot.commands.ratings.options.limit`) + .setNameLocalizations( + localizationObject('bot.commands.ratings.options.limit'), + ) + .setDescription( + translate('bot.commands.ratings.options.limit.description', [ + `${DEFAULT_LIMIT}`, + ]), + ) + .setDescriptionLocalizations( + localizationObject( + 'bot.commands.ratings.options.limit.description', + [`${DEFAULT_LIMIT}`], + ), + ) .setRequired(false) .setMinValue(5) .setMaxValue(30), ) .addStringOption((option) => option - .setName('season') - .setDescription('Season number') + .setName(t`bot.commands.ratings.options.season`) + .setNameLocalizations( + localizationObject('bot.commands.ratings.options.season'), + ) + .setDescription( + translate('bot.commands.ratings.options.season.description'), + ) + .setDescriptionLocalizations( + localizationObject( + 'bot.commands.ratings.options.season.description', + ), + ) .setRequired(false) .addChoices( ...seasonNumbers - .map( - (number, index) => - ({ - name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`, - value: `${number}`, - }) satisfies APIApplicationCommandOptionChoice, - ) + .map((number, index) => { + const current = + index === seasonNumbers.length - 1 && onGoingSeason; + + return { + name: current + ? translate( + 'bot.commands.ratings.options.season.choices.current', + [`${number}`], + ) + : `${number}`, + value: `${number}`, + name_localizations: current + ? localizationObject( + 'bot.commands.ratings.options.season.choices.current', + [`${number}`], + ) + : undefined, + } satisfies APIApplicationCommandOptionChoice; + }) .reverse(), ), ); } - const command = new SlashCommandBuilder() - .setName('ratings') - .setDescription('Ratings battles statistics') - .addSubcommand((option) => - addSubcommands( - option - .setName('neighbors') - .setDescription('Shows neighboring players on the leaderboard'), - ), - ) - .addSubcommand((option) => - addSubcommands( - option - .setName('league') - .setDescription('Shows top players from a league') - .addStringOption(addRegionChoices) - .addStringOption((option) => - option - .setName('league') - .setDescription('The league to display') - .addChoices( - ...LEAGUES.map( - ({ name }, value) => - ({ - name, - value: `${value}`, - }) satisfies APIApplicationCommandOptionChoice, - ), - ) - .setRequired(true), - ), - ), - ); - resolve({ inProduction: true, inPublic: true, - command, + command: createLocalizedCommand('ratings', [ + { + subcommand: 'neighbors', + modify(option: SlashCommandSubcommandBuilder) { + option.addStringOption(addUsernameChoices); + addOptions(option); + }, + }, + { + subcommand: 'league', + modify(option: SlashCommandSubcommandBuilder) { + option + .addStringOption((option) => { + const { t, translate } = translator(Locale.EnglishUS); + + return option + .setName(t`bot.commands.ratings.options.league`) + .setNameLocalizations( + localizationObject('bot.commands.ratings.options.league'), + ) + .setDescription( + translate('bot.commands.ratings.options.league.description'), + ) + .setDescriptionLocalizations( + localizationObject( + 'bot.commands.ratings.options.league.description', + ), + ) + .addChoices( + ...LEAGUES.map( + ({ name }, value) => + ({ + name: translate(`common.leagues.${name}`), + value: `${value}`, + name_localizations: localizationObject( + `common.leagues.${name}`, + ), + }) satisfies APIApplicationCommandOptionChoice, + ), + ) + .setRequired(true); + }) + .addStringOption(addRegionChoices); + addOptions(option); + }, + }, + ]), async handler(interaction) { - interface SimplifiedPlayer { - id: number; - score: number; - position: number; - clan: string | undefined; - nickname: string; - } - + const { t, translate } = translator(interaction.locale); const subcommand = interaction.options.getSubcommand(true) as | 'league' | 'neighbors'; - const limit = interaction.options.getInteger('limit') ?? 10; + const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT; const seasonOption = interaction.options.getString('season'); const season = seasonOption ? parseInt(seasonOption) @@ -223,7 +272,6 @@ export const ratingsCommand = new Promise(async (resolve) => { let titleName: string; let titleImage: string | undefined; let titleDescription: string; - let playersBefore: number; let playersAfter: number; let players: SimplifiedPlayer[] | undefined; let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined; @@ -237,7 +285,8 @@ export const ratingsCommand = new Promise(async (resolve) => { ? getRatingsInfo(region) : getArchivedRatingsInfo(region, season!)); - if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; + if (regionRatingsInfo.detail !== undefined) + return t`bot.commands.ratings.body.no_ongoing_season`; const result = isCurrentSeason ? await getRatingsLeague(region, leagueIndex).then(async (result) => @@ -280,7 +329,10 @@ export const ratingsCommand = new Promise(async (resolve) => { position: firstPlayerIndex + index + 1, clan: clanData[player.id]?.clan?.tag, nickname: - clanData[index]?.account_name ?? `Deleted ${player.id}`, + clanData[index]?.account_name ?? + translate('bot.commands.ratings.body.deleted_player', [ + `${player.id}`, + ]), }) satisfies SimplifiedPlayer, ); }, @@ -294,23 +346,25 @@ export const ratingsCommand = new Promise(async (resolve) => { : await getArchivedRatingsMidnightLeaderboard(region, season!); players = result; - playersBefore = result ? result[0].position - 1 : 0; playersAfter = regionRatingsInfo.count - result[result.length - 1].position; titleName = `${ - leagueInfo ? leagueInfo.title : 'No Ongoing Season' - } - ${REGION_NAMES_SHORT[region]}`; + leagueInfo ? leagueInfo.title : '' + } - ${translate(`common.regions.short.${region}`)}`; titleImage = leagueInfo.big_icon.startsWith('http') ? leagueInfo.big_icon : `https:${leagueInfo.big_icon}`; - titleDescription = `Top ${limit} players`; + titleDescription = translate('bot.commands.ratings.body.top_players', [ + `${limit}`, + ]); } else { const { region, id } = await resolvePlayerFromCommand(interaction); regionRatingsInfo = isCurrentSeason ? await getRatingsInfo(region) : await getArchivedRatingsInfo(region, season!); - if (regionRatingsInfo.detail !== undefined) return noOngoingSeason; + if (regionRatingsInfo.detail !== undefined) + return t`bot.commands.ratings.body.no_ongoing_season`; const accountInfo = await getAccountInfo(region, id); const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; @@ -336,7 +390,10 @@ export const ratingsCommand = new Promise(async (resolve) => { if (playerIndex === -1) { throw new UserError( - `# ${markdownEscape(accountInfo.nickname)} didn't play ratings in season ${season}\nThis player did not participate in this season or did not get past calibration.`, + translate('bot.commands.ratings.body.no_participation', [ + markdownEscape(accountInfo.nickname), + `${season}`, + ]), ); } @@ -373,11 +430,8 @@ export const ratingsCommand = new Promise(async (resolve) => { titleImage = clan ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png` : undefined; - titleName = neighbors - ? accountInfo.nickname - : `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`; - titleDescription = 'Ratings neighbors'; - playersBefore = neighbors ? neighbors[0].position - 1 : 0; + titleName = neighbors ? accountInfo.nickname : ''; + titleDescription = t`bot.commands.ratings.body.neighbors`; playersAfter = regionRatingsInfo.count - neighbors[neighbors.length - 1].position; playerId = id; @@ -428,15 +482,12 @@ export const ratingsCommand = new Promise(async (resolve) => { {items && ( - {playersBefore > 0 && ( - - )} {items} {playersAfter > 0 && ( )} diff --git a/src/commands/searchClans.ts b/src/commands/searchClans.ts index 4b70afae5..3fe09224a 100644 --- a/src/commands/searchClans.ts +++ b/src/commands/searchClans.ts @@ -1,46 +1,68 @@ -import { SlashCommandBuilder } from 'discord.js'; +import { Locale } from 'discord.js'; import markdownEscape from 'markdown-escape'; -import { REGION_NAMES, Region } from '../constants/regions'; +import { Region } from '../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; import fetchBlitz from '../core/blitz/fetchBlitz'; import addClanChoices from '../core/discord/addClanChoices'; import addRegionChoices from '../core/discord/addRegionChoices'; +import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import embedInfo from '../core/discord/embedInfo'; +import { localizationObject } from '../core/discord/localizationObject'; +import { translator } from '../core/localization/translator'; import { CommandRegistry } from '../events/interactionCreate'; import { ClanList } from '../types/clanList'; +const DEFAULT_LIMIT = 25; + export const searchClansCommand = new Promise((resolve) => { resolve({ inProduction: true, inPublic: true, - command: new SlashCommandBuilder() - .setName('search-clans') - .setDescription('Search clans') + command: createLocalizedCommand('search-clans') .addStringOption(addRegionChoices) .addStringOption((option) => addClanChoices(option).setAutocomplete(false), ) - .addIntegerOption((option) => - option - .setName('limit') - .setDescription('The size of the search result (default: 25)') + .addIntegerOption((option) => { + const { t, translate } = translator(Locale.EnglishUS); + + return option + .setName(t`bot.commands.search_clans.options.limit`) + .setNameLocalizations( + localizationObject('bot.commands.search_clans.options.limit'), + ) + .setDescription( + translate('bot.commands.search_clans.options.limit.description', [ + `${DEFAULT_LIMIT}`, + ]), + ) + .setDescriptionLocalizations( + localizationObject( + 'bot.commands.search_clans.options.limit.description', + [`${DEFAULT_LIMIT}`], + ), + ) .setMinValue(1) - .setMaxValue(100), - ), + .setMaxValue(100); + }), async handler(interaction) { + const { translate } = translator(interaction.locale); const server = interaction.options.getString('region') as Region; const clan = interaction.options.getString('clan')!; - const limit = interaction.options.getInteger('limit') ?? 25; + const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT; const clanList = await fetchBlitz( `https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`, ); return embedInfo( - `Clan search for "${markdownEscape(clan)}" in ${REGION_NAMES[server]}`, + translate('bot.commands.search_clans.body.title', [ + markdownEscape(clan), + translate(`common.regions.normal.${server}`), + ]), clanList.length === 0 - ? 'No clans found.' + ? translate('bot.commands.search_clans.body.no_results') : `\`\`\`\n${clanList .map((clan) => `${clan.name} [${clan.tag}]`) .join('\n')}\n\`\`\``, diff --git a/src/commands/searchPlayers.ts b/src/commands/searchPlayers.ts index 11f642735..aa93d115b 100644 --- a/src/commands/searchPlayers.ts +++ b/src/commands/searchPlayers.ts @@ -1,6 +1,6 @@ -import { SlashCommandBuilder } from 'discord.js'; +import { Locale } from 'discord.js'; import markdownEscape from 'markdown-escape'; -import { REGION_NAMES, Region } from '../constants/regions'; +import { Region } from '../constants/regions'; import { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID'; import fetchBlitz from '../core/blitz/fetchBlitz'; import { @@ -9,25 +9,43 @@ import { } from '../core/blitz/searchPlayersAcrossRegions'; import addRegionChoices from '../core/discord/addRegionChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices'; +import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import embedInfo from '../core/discord/embedInfo'; +import { localizationObject } from '../core/discord/localizationObject'; +import { translator } from '../core/localization/translator'; import { CommandRegistry } from '../events/interactionCreate'; +const DEFAULT_LIMIT = 25; + export const searchPlayersCommand = new Promise((resolve) => { + const { t, translate } = translator(Locale.EnglishUS); + resolve({ inProduction: true, inPublic: true, - command: new SlashCommandBuilder() - .setName('search-players') - .setDescription('Search players in a Blitz server') + command: createLocalizedCommand('search-players') .addStringOption(addRegionChoices) .addStringOption((option) => addUsernameChoices(option).setAutocomplete(false).setRequired(true), ) .addIntegerOption((option) => option - .setName('limit') - .setDescription('The size of the search result (default: 25)') + .setName(t`bot.commands.search_players.options.limit`) + .setNameLocalizations( + localizationObject('bot.commands.search_players.options.limit'), + ) + .setDescription( + translate('bot.commands.search_players.options.limit.description', [ + `${DEFAULT_LIMIT}`, + ]), + ) + .setDescriptionLocalizations( + localizationObject( + 'bot.commands.search_players.options.limit.description', + [`${DEFAULT_LIMIT}`], + ), + ) .setMinValue(1) .setMaxValue(100), ), @@ -46,10 +64,13 @@ export const searchPlayersCommand = new Promise((resolve) => { : []; return embedInfo( - `Player search for "${markdownEscape(name)}" in ${REGION_NAMES[server]}`, + translate('bot.commands.search_players.body.title', [ + markdownEscape(trimmedSearch), + translate(`common.regions.normal.${server}`), + ]), `\`\`\`${ players.length === 0 - ? 'No players found.' + ? translate('bot.commands.search_players.body.no_results') : players.map((player) => player.nickname).join('\n') }\`\`\``, ); diff --git a/src/constants/leagues.ts b/src/constants/leagues.ts index 082886490..ee77accb4 100644 --- a/src/constants/leagues.ts +++ b/src/constants/leagues.ts @@ -1,7 +1,7 @@ export const LEAGUES = [ - { name: 'Diamond', minScore: 5000 }, - { name: 'Platinum', minScore: 4000 }, - { name: 'Gold', minScore: 3000 }, - { name: 'Silver', minScore: 2000 }, - { name: 'Bronze', minScore: 0 }, + { name: 'diamond', minScore: 5000 }, + { name: 'platinum', minScore: 4000 }, + { name: 'gold', minScore: 3000 }, + { name: 'silver', minScore: 2000 }, + { name: 'bronze', minScore: 0 }, ]; diff --git a/src/constants/regions.ts b/src/constants/regions.ts index d6e088235..85e84a7d0 100644 --- a/src/constants/regions.ts +++ b/src/constants/regions.ts @@ -3,21 +3,11 @@ export const REGIONS = ['com', 'eu', 'asia'] as const; export type Region = (typeof REGIONS)[number]; export type RegionSubdomain = 'na' | 'eu' | 'asia'; -export const REGION_NAMES: Record = { +/** + * @deprecated + */ +export const UNLOCALIZED_REGION_NAMES: Record = { com: 'North America', - eu: 'Europe', asia: 'Asia', -}; - -// https://planetarynames.wr.usgs.gov/Abbreviations -export const REGION_NAMES_SHORT: Record = { - com: 'NA', - eu: 'EU', - asia: 'ASIA', -}; - -export const REGION_SUBDOMAIN_NAMES: Record = { - na: 'North America', eu: 'Europe', - asia: 'Asia', }; diff --git a/src/core/discord/addRegionChoices.ts b/src/core/discord/addRegionChoices.ts index eb0042a5d..9e53ca323 100644 --- a/src/core/discord/addRegionChoices.ts +++ b/src/core/discord/addRegionChoices.ts @@ -1,14 +1,33 @@ -import { SlashCommandStringOption } from 'discord.js'; -import { REGION_NAMES } from '../../constants/regions'; +import { + APIApplicationCommandOptionChoice, + Locale, + SlashCommandStringOption, +} from 'discord.js'; +import { REGIONS } from '../../constants/regions'; +import { translator } from '../localization/translator'; +import { localizationObject } from './localizationObject'; export default function addRegionChoices(option: SlashCommandStringOption) { + const { t, translate } = translator(Locale.EnglishUS); + return option - .setName('region') - .setDescription('The Blitz region you are in') + .setName(t`bot.common.options.region`) + .setNameLocalizations(localizationObject('bot.common.options.region')) + .setDescription(t`bot.common.options.region.description`) + .setDescriptionLocalizations( + localizationObject('bot.common.options.region.description'), + ) .addChoices( - { name: REGION_NAMES.com, value: 'com' }, - { name: REGION_NAMES.eu, value: 'eu' }, - { name: REGION_NAMES.asia, value: 'asia' }, + ...REGIONS.map( + (region) => + ({ + value: region, + name: translate(`common.regions.normal.${region}`), + name_localizations: localizationObject( + `common.regions.normal.${region}`, + ), + }) satisfies APIApplicationCommandOptionChoice, + ), ) .setRequired(true); } diff --git a/src/core/discord/autocompleteClan.ts b/src/core/discord/autocompleteClan.ts index bbbd9a2de..142282ad2 100644 --- a/src/core/discord/autocompleteClan.ts +++ b/src/core/discord/autocompleteClan.ts @@ -3,8 +3,8 @@ import { AutocompleteInteraction, CacheType, } from 'discord.js'; -import { REGION_NAMES_SHORT } from '../../constants/regions'; import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions'; +import { translator } from '../localization/translator'; export const DISCORD_CHOICES_MAX_NAME_SIZE = 25; export const OVERFLOW_SUFFIX = '…'; @@ -12,6 +12,7 @@ export const OVERFLOW_SUFFIX = '…'; export default async function autocompleteClan( interaction: AutocompleteInteraction, ) { + const { translate } = translator(interaction.locale); const focusedOption = interaction.options.getFocused(true); if (focusedOption.name !== 'clan') return; if (focusedOption.value.length < 2) return interaction.respond([]); @@ -20,7 +21,9 @@ export default async function autocompleteClan( await interaction.respond( clans ? clans.map((clan) => { - let name = `${clan.name} (${REGION_NAMES_SHORT[clan.region]})`; + let name = `${clan.name} (${translate( + `common.regions.short.${clan.region}`, + )})`; if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) { const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE; @@ -28,7 +31,9 @@ export default async function autocompleteClan( name = `${clan.name.slice( 0, clan.name.length - overSize - OVERFLOW_SUFFIX.length, - )}${OVERFLOW_SUFFIX} (${REGION_NAMES_SHORT[clan.region]})`; + )}${OVERFLOW_SUFFIX} (${translate( + `common.regions.short.${clan.region}`, + )})`; } return { diff --git a/src/core/discord/autocompleteUsername.ts b/src/core/discord/autocompleteUsername.ts index 333b51624..a41b3860c 100644 --- a/src/core/discord/autocompleteUsername.ts +++ b/src/core/discord/autocompleteUsername.ts @@ -3,8 +3,8 @@ import { AutocompleteInteraction, CacheType, } from 'discord.js'; -import { REGION_NAMES_SHORT } from '../../constants/regions'; import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions'; +import { translator } from '../localization/translator'; import { DISCORD_CHOICES_MAX_NAME_SIZE, OVERFLOW_SUFFIX, @@ -13,6 +13,7 @@ import { export default async function autocompleteUsername( interaction: AutocompleteInteraction, ) { + const { translate } = translator(interaction.locale); const focusedOption = interaction.options.getFocused(true); if (focusedOption.name !== 'username') return; const players = await searchPlayersAcrossRegions(focusedOption.value); @@ -21,9 +22,9 @@ export default async function autocompleteUsername( await interaction.respond( players ? players.map((player) => { - let name = `${player.nickname} (${ - REGION_NAMES_SHORT[player.region] - })`; + let name = `${player.nickname} (${translate( + `common.regions.short.${player.region}`, + )})`; if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) { const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE; @@ -31,7 +32,9 @@ export default async function autocompleteUsername( name = `${player.nickname.slice( 0, player.nickname.length - overSize - OVERFLOW_SUFFIX.length, - )}${OVERFLOW_SUFFIX} (${REGION_NAMES_SHORT[player.region]})`; + )}${OVERFLOW_SUFFIX} (${translate( + `common.regions.short.${player.region}`, + )})`; } return { diff --git a/src/core/discord/localizationObject.ts b/src/core/discord/localizationObject.ts index 28f2f0e90..2a41e7963 100644 --- a/src/core/discord/localizationObject.ts +++ b/src/core/discord/localizationObject.ts @@ -2,13 +2,13 @@ import { Locale } from 'discord.js'; import { locales } from '../localization/strings'; import { translator } from '../localization/translator'; -export function localizationObject(path: string) { +export function localizationObject(path: string, literals?: string[]) { return locales.reduce>>( (localizations, locale) => { const { translate } = translator(locale); return { ...localizations, - [locale]: translate(path), + [locale]: translate(path, literals), }; }, {}, diff --git a/src/events/interactionCreate/index.ts b/src/events/interactionCreate/index.ts index 3008bf026..3f21ef942 100644 --- a/src/events/interactionCreate/index.ts +++ b/src/events/interactionCreate/index.ts @@ -101,8 +101,15 @@ export const COMMANDS_RAW: Promise[] = [ export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => { return rawCommands.reduce>( - (commands, registry) => { - if (registry.status === 'rejected') return commands; + (commands, registry, index) => { + if (registry.status === 'rejected') { + console.warn( + `Command ${index} failed to load; skipping...`, + registry.reason, + ); + + return commands; + } if (isDev()) registry.value.command.setDefaultMemberPermissions(0); return { ...commands, [registry.value.command.name]: registry.value }; }, diff --git a/src/lang/en-US.json b/src/lang/en-US.json index 9f5868b84..fcc60b649 100644 --- a/src/lang/en-US.json +++ b/src/lang/en-US.json @@ -23,6 +23,27 @@ "researchable": "Tech tree", "premium": "Premium", "collector": "Collector" + }, + + "regions": { + "normal": { + "com": "North America", + "eu": "Europe", + "asia": "Asia" + }, + "short": { + "com": "NA", + "eu": "EU", + "asia": "ASIA" + } + }, + + "leagues": { + "diamond": "Diamond", + "platinum": "Platinum", + "gold": "Gold", + "silver": "Silver", + "bronze": "Bronze" } }, @@ -109,6 +130,11 @@ "$": "tree-type", "description": "Tree type" } + }, + + "region": { + "$": "region", + "description": "Region" } }, @@ -261,6 +287,105 @@ } } } + }, + + "link": { + "$": "link", + "description": "Links your Blitz and Discord account", + + "body": { + "accounts_linked": "Accounts linked", + "no_role_permissions": { + "$": "%s failed to verify", + "description": "I don't have the permission to change your manage roles." + } + }, + + "embed": { + "title": "Incorrect account?", + "description": "Use the command again and select a username from the search results." + } + }, + + "search_clans": { + "$": "search-clans", + "description": "Search clans", + + "options": { + "limit": { + "$": "limit", + "description": "The max number of clan results (default: %s)" + } + }, + + "body": { + "title": "Clan search for %s in region %s", + "no_results": "No clans found" + } + }, + + "search_players": { + "$": "search-players", + "description": "Search players", + + "options": { + "limit": { + "$": "limit", + "description": "The max number of player results (default: %s)" + } + }, + + "body": { + "title": "Player search for %s in region %s", + "no_results": "No players found" + } + }, + + "ratings": { + "$": "ratings", + "description": "Ratings battles statistics", + + "subcommands": { + "neighbors": { + "$": "neighbors", + "description": "Neighboring players on the leaderboard" + }, + + "league": { + "$": "league", + "description": "Top players of a league" + } + }, + + "options": { + "limit": { + "$": "limit", + "description": "How many neighbors to display (default: %s)" + }, + + "season": { + "$": "season", + "description": "Season number (default: current)", + + "choices": { + "current": "%s (current)" + } + }, + + "league": { + "$": "league", + "description": "The league to display" + } + }, + + "body": { + "top_players": "Top %s players", + "no_participation": "# %s didn't play ratings in season %s\n\nThis player did not participate in this season or did not get past calibration.", + "no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running ratings season.", + "deleted_player": "Deleted %s", + "neighbors": "Ratings neighbors", + "more": "+%s more" + } } } },