feat: random localizations

This commit is contained in:
TrèsAbhi committed 2024-03-07 18:27:05 -06:00
1 parent 796aca21cf
commit 3325536bbc
17 files changed
+423 -166

No files matched your search

+1
View File
@@ -131,6 +131,7 @@
"TYPELESS", "TYPELESS",
"ubjson", "ubjson",
"Unicum", "Unicum",
"UNLOCALIZED",
"UNORM", "UNORM",
"Unregistering", "Unregistering",
"vickers", "vickers",
+6 -2
View File
@@ -20,7 +20,11 @@ import PageWrapper from '../../../components/PageWrapper';
import { Skeleton } from '../../../components/Skeleton'; import { Skeleton } from '../../../components/Skeleton';
import { LEAGUES } from '../../../constants/leagues'; import { LEAGUES } from '../../../constants/leagues';
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../../../constants/ratings'; 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 { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../../../core/blitz/fetchBlitz'; import fetchBlitz from '../../../core/blitz/fetchBlitz';
import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo';
@@ -556,7 +560,7 @@ export default function Page() {
<Select.Content> <Select.Content>
{REGIONS.map((region) => ( {REGIONS.map((region) => (
<Select.Item key={region} value={region}> <Select.Item key={region} value={region}>
{REGION_NAMES[region]} {UNLOCALIZED_REGION_NAMES[region]}
</Select.Item> </Select.Item>
))} ))}
</Select.Content> </Select.Content>
+2 -2
View File
@@ -18,7 +18,7 @@ import {
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { ChangeEvent, useEffect, useRef, useState } from 'react'; import { ChangeEvent, useEffect, useRef, useState } from 'react';
import PageWrapper from '../../../components/PageWrapper'; import PageWrapper from '../../../components/PageWrapper';
import { REGION_NAMES } from '../../../constants/regions'; import { UNLOCALIZED_REGION_NAMES } from '../../../constants/regions';
import searchPlayersAcrossRegions, { import searchPlayersAcrossRegions, {
AccountListWithServer, AccountListWithServer,
} from '../../../core/blitz/searchPlayersAcrossRegions'; } from '../../../core/blitz/searchPlayersAcrossRegions';
@@ -107,7 +107,7 @@ export default function Page() {
setShowSearchResults(false); setShowSearchResults(false);
setSession(region, id, nickname); setSession(region, id, nickname);
}} }}
shortcut={REGION_NAMES[region]} shortcut={UNLOCALIZED_REGION_NAMES[region]}
> >
{nickname} {nickname}
</DropdownMenu.Item> </DropdownMenu.Item>
+2 -2
View File
@@ -89,8 +89,8 @@ async function render(
max: maxX, max: maxX,
precision: 0, precision: 0,
}} }}
xMinLabel={new Date(minTime * 1000).toDateString()} xMinLabel={new Date(minTime * 1000).toLocaleDateString(locale)}
xMaxLabel={new Date(maxTime * 1000).toDateString()} xMaxLabel={new Date(maxTime * 1000).toLocaleDateString(locale)}
> >
{Graph.Line({ {Graph.Line({
color: Graph.LineColor.Red, color: Graph.LineColor.Red,
+3 -1
View File
@@ -63,7 +63,9 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
([name, days]) => ([name, days]) =>
[ [
name, name,
translate('bot.commands.inactive.body.listing', [`${days}`]), translate('bot.commands.inactive.body.listing', [
days.toFixed(0),
]),
] as [string, string], ] as [string, string],
); );
const hasInactiveMembers = inactive.length > 0; const hasInactiveMembers = inactive.length > 0;
+21 -14
View File
@@ -1,18 +1,19 @@
import { teal } from '@radix-ui/colors'; import { teal } from '@radix-ui/colors';
import { GuildMemberRoleManager, SlashCommandBuilder } from 'discord.js'; import { GuildMemberRoleManager } from 'discord.js';
import markdownEscape from 'markdown-escape'; import markdownEscape from 'markdown-escape';
import discord from '../../discord.json' assert { type: 'json' }; import discord from '../../discord.json' assert { type: 'json' };
import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow'; import { Glow } from '../components/AllStatsOverview/components/WN8Display/components/Glow';
import CommandWrapper from '../components/CommandWrapper'; import CommandWrapper from '../components/CommandWrapper';
import { REGION_NAMES } from '../constants/regions';
import { getAccountInfo } from '../core/blitz/getAccountInfo'; import { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz'; import { linkBlitzAndDiscord } from '../core/blitzkrieg/discordBlitz';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import embedNegative from '../core/discord/embedNegative'; import embedNegative from '../core/discord/embedNegative';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config'; import { theme } from '../stitches.config';
@@ -21,14 +22,12 @@ export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
inProduction: true, inProduction: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: createLocalizedCommand('link').addStringOption((option) =>
.setName('link') addUsernameChoices(option).setRequired(true),
.setDescription('Links your Blitz and Discord account') ),
.addStringOption((option) =>
addUsernameChoices(option).setRequired(true),
),
async handler(interaction) { async handler(interaction) {
const { t, translate } = translator(interaction.locale);
const { id, region } = await resolvePlayerFromCommand(interaction); const { id, region } = await resolvePlayerFromCommand(interaction);
const discordId = parseInt(interaction.user.id); const discordId = parseInt(interaction.user.id);
const accountInfo = await getAccountInfo(region, id); const accountInfo = await getAccountInfo(region, id);
@@ -39,8 +38,14 @@ export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
if (interaction.guildId === discord.sklld_guild_id) { if (interaction.guildId === discord.sklld_guild_id) {
if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) { if (!interaction.guild?.members.me?.permissions.has('ManageRoles')) {
return embedNegative( return embedNegative(
`${markdownEscape(interaction.user.username)} failed to verify`, translate(translate('bot.commands.link.body.no_role_permissions'), [
"I don't have the permission to change your manage roles.", markdownEscape(interaction.user.username),
]),
translate(
translate(
'bot.commands.link.body.no_role_permissions.description',
),
),
); );
} }
@@ -140,7 +145,9 @@ export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
height: 16, height: 16,
}} }}
/> />
<span style={{ fontSize: 16 }}>Accounts linked</span> <span
style={{ fontSize: 16 }}
>{t`bot.commands.link.body.accounts_linked`}</span>
</div> </div>
<div <div
@@ -191,7 +198,7 @@ export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
{clanAccountInfo?.clan {clanAccountInfo?.clan
? `[${clanAccountInfo?.clan?.tag}] • ` ? `[${clanAccountInfo?.clan?.tag}] • `
: ''} : ''}
{REGION_NAMES[region]} {translate(`common.regions.normal.${region}`)}
</span> </span>
</div> </div>
</div> </div>
@@ -202,8 +209,8 @@ export const verifyCommand = new Promise<CommandRegistry>((resolve) => {
</CommandWrapper>, </CommandWrapper>,
embedInfo( embedInfo(
'Incorrect account?', t`bot.commands.link.embed.title`,
'Use the command again and select a username from the search results.', t`bot.commands.link.embed.description`,
), ),
]; ];
}, },
+135 -84
View File
@@ -1,6 +1,6 @@
import { import {
APIApplicationCommandOptionChoice, APIApplicationCommandOptionChoice,
SlashCommandBuilder, Locale,
SlashCommandSubcommandBuilder, SlashCommandSubcommandBuilder,
} from 'discord.js'; } from 'discord.js';
import { range } from 'lodash'; import { range } from 'lodash';
@@ -11,7 +11,7 @@ import * as Leaderboard from '../components/Leaderboard';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import { LEAGUES } from '../constants/leagues'; import { LEAGUES } from '../constants/leagues';
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../constants/ratings'; 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 { getAccountInfo } from '../core/blitz/getAccountInfo';
import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo'; import { getClanAccountInfo } from '../core/blitz/getClanAccountInfo';
import getRatingsInfo from '../core/blitz/getRatingsInfo'; import getRatingsInfo from '../core/blitz/getRatingsInfo';
@@ -25,8 +25,10 @@ import getArchivedRatingsMidnightLeaderboard from '../core/blitzkrieg/getArchive
import addRegionChoices from '../core/discord/addRegionChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; 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 resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { UserError } from '../hooks/userError'; import { UserError } from '../hooks/userError';
@@ -117,10 +119,15 @@ export interface RatingsNeighbors {
neighbors: RatingsPlayer[]; neighbors: RatingsPlayer[];
} }
const noOngoingSeason = embedNegative( interface SimplifiedPlayer {
'No ongoing season', id: number;
"Wargaming didn't provide any data for this season.", score: number;
); position: number;
clan: string | undefined;
nickname: string;
}
const DEFAULT_LIMIT = 10;
export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => { export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber(); const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber();
@@ -130,89 +137,131 @@ export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1, latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
); );
function addSubcommands(option: SlashCommandSubcommandBuilder) { function addOptions(option: SlashCommandSubcommandBuilder) {
const { t, translate } = translator(Locale.EnglishUS);
return option return option
.addStringOption(addUsernameChoices)
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('limit') .setName(t`bot.commands.ratings.options.limit`)
.setDescription('How many neighbors to display (default: 10)') .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) .setRequired(false)
.setMinValue(5) .setMinValue(5)
.setMaxValue(30), .setMaxValue(30),
) )
.addStringOption((option) => .addStringOption((option) =>
option option
.setName('season') .setName(t`bot.commands.ratings.options.season`)
.setDescription('Season number') .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) .setRequired(false)
.addChoices( .addChoices(
...seasonNumbers ...seasonNumbers
.map( .map((number, index) => {
(number, index) => const current =
({ index === seasonNumbers.length - 1 && onGoingSeason;
name: `${number}${index === seasonNumbers.length - 1 && onGoingSeason ? ' (current)' : ''}`,
value: `${number}`, return {
}) satisfies APIApplicationCommandOptionChoice<string>, 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<string>;
})
.reverse(), .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<string>,
),
)
.setRequired(true),
),
),
);
resolve({ resolve({
inProduction: true, inProduction: true,
inPublic: 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<string>,
),
)
.setRequired(true);
})
.addStringOption(addRegionChoices);
addOptions(option);
},
},
]),
async handler(interaction) { async handler(interaction) {
interface SimplifiedPlayer { const { t, translate } = translator(interaction.locale);
id: number;
score: number;
position: number;
clan: string | undefined;
nickname: string;
}
const subcommand = interaction.options.getSubcommand(true) as const subcommand = interaction.options.getSubcommand(true) as
| 'league' | 'league'
| 'neighbors'; | 'neighbors';
const limit = interaction.options.getInteger('limit') ?? 10; const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString('season'); const seasonOption = interaction.options.getString('season');
const season = seasonOption const season = seasonOption
? parseInt(seasonOption) ? parseInt(seasonOption)
@@ -223,7 +272,6 @@ export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
let titleName: string; let titleName: string;
let titleImage: string | undefined; let titleImage: string | undefined;
let titleDescription: string; let titleDescription: string;
let playersBefore: number;
let playersAfter: number; let playersAfter: number;
let players: SimplifiedPlayer[] | undefined; let players: SimplifiedPlayer[] | undefined;
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined; let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
@@ -237,7 +285,8 @@ export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
? getRatingsInfo(region) ? getRatingsInfo(region)
: getArchivedRatingsInfo(region, season!)); : 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 const result = isCurrentSeason
? await getRatingsLeague(region, leagueIndex).then(async (result) => ? await getRatingsLeague(region, leagueIndex).then(async (result) =>
@@ -280,7 +329,10 @@ export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
position: firstPlayerIndex + index + 1, position: firstPlayerIndex + index + 1,
clan: clanData[player.id]?.clan?.tag, clan: clanData[player.id]?.clan?.tag,
nickname: nickname:
clanData[index]?.account_name ?? `Deleted ${player.id}`, clanData[index]?.account_name ??
translate('bot.commands.ratings.body.deleted_player', [
`${player.id}`,
]),
}) satisfies SimplifiedPlayer, }) satisfies SimplifiedPlayer,
); );
}, },
@@ -294,23 +346,25 @@ export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
: await getArchivedRatingsMidnightLeaderboard(region, season!); : await getArchivedRatingsMidnightLeaderboard(region, season!);
players = result; players = result;
playersBefore = result ? result[0].position - 1 : 0;
playersAfter = playersAfter =
regionRatingsInfo.count - result[result.length - 1].position; regionRatingsInfo.count - result[result.length - 1].position;
titleName = `${ titleName = `${
leagueInfo ? leagueInfo.title : 'No Ongoing Season' leagueInfo ? leagueInfo.title : ''
} - ${REGION_NAMES_SHORT[region]}`; } - ${translate(`common.regions.short.${region}`)}`;
titleImage = leagueInfo.big_icon.startsWith('http') titleImage = leagueInfo.big_icon.startsWith('http')
? leagueInfo.big_icon ? leagueInfo.big_icon
: `https:${leagueInfo.big_icon}`; : `https:${leagueInfo.big_icon}`;
titleDescription = `Top ${limit} players`; titleDescription = translate('bot.commands.ratings.body.top_players', [
`${limit}`,
]);
} else { } else {
const { region, id } = await resolvePlayerFromCommand(interaction); const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingsInfo = isCurrentSeason regionRatingsInfo = isCurrentSeason
? await getRatingsInfo(region) ? await getRatingsInfo(region)
: await getArchivedRatingsInfo(region, season!); : 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 accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
@@ -336,7 +390,10 @@ export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
if (playerIndex === -1) { if (playerIndex === -1) {
throw new UserError( 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<CommandRegistry>(async (resolve) => {
titleImage = clan titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png` ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined; : undefined;
titleName = neighbors titleName = neighbors ? accountInfo.nickname : '';
? accountInfo.nickname titleDescription = t`bot.commands.ratings.body.neighbors`;
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
titleDescription = 'Ratings neighbors';
playersBefore = neighbors ? neighbors[0].position - 1 : 0;
playersAfter = playersAfter =
regionRatingsInfo.count - neighbors[neighbors.length - 1].position; regionRatingsInfo.count - neighbors[neighbors.length - 1].position;
playerId = id; playerId = id;
@@ -428,15 +482,12 @@ export const ratingsCommand = new Promise<CommandRegistry>(async (resolve) => {
{items && ( {items && (
<Leaderboard.Root> <Leaderboard.Root>
{playersBefore > 0 && (
<Leaderboard.Gap
message={`+ ${playersBefore.toLocaleString()} more`}
/>
)}
{items} {items}
{playersAfter > 0 && ( {playersAfter > 0 && (
<Leaderboard.Gap <Leaderboard.Gap
message={`+ ${playersAfter.toLocaleString()} more`} message={translate('bot.commands.ratings.body.more', [
`${playersAfter.toLocaleString(interaction.locale)}`,
])}
/> />
)} )}
</Leaderboard.Root> </Leaderboard.Root>
+36 -14
View File
@@ -1,46 +1,68 @@
import { SlashCommandBuilder } from 'discord.js'; import { Locale } from 'discord.js';
import markdownEscape from 'markdown-escape'; 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 { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchBlitz'; import fetchBlitz from '../core/blitz/fetchBlitz';
import addClanChoices from '../core/discord/addClanChoices'; import addClanChoices from '../core/discord/addClanChoices';
import addRegionChoices from '../core/discord/addRegionChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import { localizationObject } from '../core/discord/localizationObject';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { ClanList } from '../types/clanList'; import { ClanList } from '../types/clanList';
const DEFAULT_LIMIT = 25;
export const searchClansCommand = new Promise<CommandRegistry>((resolve) => { export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
resolve({ resolve({
inProduction: true, inProduction: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: createLocalizedCommand('search-clans')
.setName('search-clans')
.setDescription('Search clans')
.addStringOption(addRegionChoices) .addStringOption(addRegionChoices)
.addStringOption((option) => .addStringOption((option) =>
addClanChoices(option).setAutocomplete(false), addClanChoices(option).setAutocomplete(false),
) )
.addIntegerOption((option) => .addIntegerOption((option) => {
option const { t, translate } = translator(Locale.EnglishUS);
.setName('limit')
.setDescription('The size of the search result (default: 25)') 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) .setMinValue(1)
.setMaxValue(100), .setMaxValue(100);
), }),
async handler(interaction) { async handler(interaction) {
const { translate } = translator(interaction.locale);
const server = interaction.options.getString('region') as Region; const server = interaction.options.getString('region') as Region;
const clan = interaction.options.getString('clan')!; 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<ClanList>( const clanList = await fetchBlitz<ClanList>(
`https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`, `https://api.wotblitz.${server}/wotb/clans/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${clan}&limit=${limit}`,
); );
return embedInfo( 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 clanList.length === 0
? 'No clans found.' ? translate('bot.commands.search_clans.body.no_results')
: `\`\`\`\n${clanList : `\`\`\`\n${clanList
.map((clan) => `${clan.name} [${clan.tag}]`) .map((clan) => `${clan.name} [${clan.tag}]`)
.join('\n')}\n\`\`\``, .join('\n')}\n\`\`\``,
+30 -9
View File
@@ -1,6 +1,6 @@
import { SlashCommandBuilder } from 'discord.js'; import { Locale } from 'discord.js';
import markdownEscape from 'markdown-escape'; 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 { WARGAMING_APPLICATION_ID } from '../constants/wargamingApplicationID';
import fetchBlitz from '../core/blitz/fetchBlitz'; import fetchBlitz from '../core/blitz/fetchBlitz';
import { import {
@@ -9,25 +9,43 @@ import {
} from '../core/blitz/searchPlayersAcrossRegions'; } from '../core/blitz/searchPlayersAcrossRegions';
import addRegionChoices from '../core/discord/addRegionChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import { localizationObject } from '../core/discord/localizationObject';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
const DEFAULT_LIMIT = 25;
export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => { export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
const { t, translate } = translator(Locale.EnglishUS);
resolve({ resolve({
inProduction: true, inProduction: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: createLocalizedCommand('search-players')
.setName('search-players')
.setDescription('Search players in a Blitz server')
.addStringOption(addRegionChoices) .addStringOption(addRegionChoices)
.addStringOption((option) => .addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true), addUsernameChoices(option).setAutocomplete(false).setRequired(true),
) )
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
.setName('limit') .setName(t`bot.commands.search_players.options.limit`)
.setDescription('The size of the search result (default: 25)') .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) .setMinValue(1)
.setMaxValue(100), .setMaxValue(100),
), ),
@@ -46,10 +64,13 @@ export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
: []; : [];
return embedInfo( 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 players.length === 0
? 'No players found.' ? translate('bot.commands.search_players.body.no_results')
: players.map((player) => player.nickname).join('\n') : players.map((player) => player.nickname).join('\n')
}\`\`\``, }\`\`\``,
); );
+5 -5
View File
@@ -1,7 +1,7 @@
export const LEAGUES = [ export const LEAGUES = [
{ name: 'Diamond', minScore: 5000 }, { name: 'diamond', minScore: 5000 },
{ name: 'Platinum', minScore: 4000 }, { name: 'platinum', minScore: 4000 },
{ name: 'Gold', minScore: 3000 }, { name: 'gold', minScore: 3000 },
{ name: 'Silver', minScore: 2000 }, { name: 'silver', minScore: 2000 },
{ name: 'Bronze', minScore: 0 }, { name: 'bronze', minScore: 0 },
]; ];
+4 -14
View File
@@ -3,21 +3,11 @@ export const REGIONS = ['com', 'eu', 'asia'] as const;
export type Region = (typeof REGIONS)[number]; export type Region = (typeof REGIONS)[number];
export type RegionSubdomain = 'na' | 'eu' | 'asia'; export type RegionSubdomain = 'na' | 'eu' | 'asia';
export const REGION_NAMES: Record<Region, string> = { /**
* @deprecated
*/
export const UNLOCALIZED_REGION_NAMES: Record<Region, string> = {
com: 'North America', com: 'North America',
eu: 'Europe',
asia: 'Asia', asia: 'Asia',
};
// https://planetarynames.wr.usgs.gov/Abbreviations
export const REGION_NAMES_SHORT: Record<Region, string> = {
com: 'NA',
eu: 'EU',
asia: 'ASIA',
};
export const REGION_SUBDOMAIN_NAMES: Record<RegionSubdomain, string> = {
na: 'North America',
eu: 'Europe', eu: 'Europe',
asia: 'Asia',
}; };
+26 -7
View File
@@ -1,14 +1,33 @@
import { SlashCommandStringOption } from 'discord.js'; import {
import { REGION_NAMES } from '../../constants/regions'; 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) { export default function addRegionChoices(option: SlashCommandStringOption) {
const { t, translate } = translator(Locale.EnglishUS);
return option return option
.setName('region') .setName(t`bot.common.options.region`)
.setDescription('The Blitz region you are in') .setNameLocalizations(localizationObject('bot.common.options.region'))
.setDescription(t`bot.common.options.region.description`)
.setDescriptionLocalizations(
localizationObject('bot.common.options.region.description'),
)
.addChoices( .addChoices(
{ name: REGION_NAMES.com, value: 'com' }, ...REGIONS.map(
{ name: REGION_NAMES.eu, value: 'eu' }, (region) =>
{ name: REGION_NAMES.asia, value: 'asia' }, ({
value: region,
name: translate(`common.regions.normal.${region}`),
name_localizations: localizationObject(
`common.regions.normal.${region}`,
),
}) satisfies APIApplicationCommandOptionChoice,
),
) )
.setRequired(true); .setRequired(true);
} }
+8 -3
View File
@@ -3,8 +3,8 @@ import {
AutocompleteInteraction, AutocompleteInteraction,
CacheType, CacheType,
} from 'discord.js'; } from 'discord.js';
import { REGION_NAMES_SHORT } from '../../constants/regions';
import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions'; import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions';
import { translator } from '../localization/translator';
export const DISCORD_CHOICES_MAX_NAME_SIZE = 25; export const DISCORD_CHOICES_MAX_NAME_SIZE = 25;
export const OVERFLOW_SUFFIX = '…'; export const OVERFLOW_SUFFIX = '…';
@@ -12,6 +12,7 @@ export const OVERFLOW_SUFFIX = '…';
export default async function autocompleteClan( export default async function autocompleteClan(
interaction: AutocompleteInteraction<CacheType>, interaction: AutocompleteInteraction<CacheType>,
) { ) {
const { translate } = translator(interaction.locale);
const focusedOption = interaction.options.getFocused(true); const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== 'clan') return; if (focusedOption.name !== 'clan') return;
if (focusedOption.value.length < 2) return interaction.respond([]); if (focusedOption.value.length < 2) return interaction.respond([]);
@@ -20,7 +21,9 @@ export default async function autocompleteClan(
await interaction.respond( await interaction.respond(
clans clans
? clans.map((clan) => { ? 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) { if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) {
const overSize = 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( name = `${clan.name.slice(
0, 0,
clan.name.length - overSize - OVERFLOW_SUFFIX.length, clan.name.length - overSize - OVERFLOW_SUFFIX.length,
)}${OVERFLOW_SUFFIX} (${REGION_NAMES_SHORT[clan.region]})`; )}${OVERFLOW_SUFFIX} (${translate(
`common.regions.short.${clan.region}`,
)})`;
} }
return { return {
+8 -5
View File
@@ -3,8 +3,8 @@ import {
AutocompleteInteraction, AutocompleteInteraction,
CacheType, CacheType,
} from 'discord.js'; } from 'discord.js';
import { REGION_NAMES_SHORT } from '../../constants/regions';
import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions'; import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions';
import { translator } from '../localization/translator';
import { import {
DISCORD_CHOICES_MAX_NAME_SIZE, DISCORD_CHOICES_MAX_NAME_SIZE,
OVERFLOW_SUFFIX, OVERFLOW_SUFFIX,
@@ -13,6 +13,7 @@ import {
export default async function autocompleteUsername( export default async function autocompleteUsername(
interaction: AutocompleteInteraction<CacheType>, interaction: AutocompleteInteraction<CacheType>,
) { ) {
const { translate } = translator(interaction.locale);
const focusedOption = interaction.options.getFocused(true); const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== 'username') return; if (focusedOption.name !== 'username') return;
const players = await searchPlayersAcrossRegions(focusedOption.value); const players = await searchPlayersAcrossRegions(focusedOption.value);
@@ -21,9 +22,9 @@ export default async function autocompleteUsername(
await interaction.respond( await interaction.respond(
players players
? players.map((player) => { ? players.map((player) => {
let name = `${player.nickname} (${ let name = `${player.nickname} (${translate(
REGION_NAMES_SHORT[player.region] `common.regions.short.${player.region}`,
})`; )})`;
if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) { if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) {
const overSize = 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( name = `${player.nickname.slice(
0, 0,
player.nickname.length - overSize - OVERFLOW_SUFFIX.length, player.nickname.length - overSize - OVERFLOW_SUFFIX.length,
)}${OVERFLOW_SUFFIX} (${REGION_NAMES_SHORT[player.region]})`; )}${OVERFLOW_SUFFIX} (${translate(
`common.regions.short.${player.region}`,
)})`;
} }
return { return {
+2 -2
View File
@@ -2,13 +2,13 @@ import { Locale } from 'discord.js';
import { locales } from '../localization/strings'; import { locales } from '../localization/strings';
import { translator } from '../localization/translator'; import { translator } from '../localization/translator';
export function localizationObject(path: string) { export function localizationObject(path: string, literals?: string[]) {
return locales.reduce<Partial<Record<Locale, string>>>( return locales.reduce<Partial<Record<Locale, string>>>(
(localizations, locale) => { (localizations, locale) => {
const { translate } = translator(locale); const { translate } = translator(locale);
return { return {
...localizations, ...localizations,
[locale]: translate(path), [locale]: translate(path, literals),
}; };
}, },
{}, {},
+9 -2
View File
@@ -101,8 +101,15 @@ export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => { export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
return rawCommands.reduce<Record<string, CommandRegistry>>( return rawCommands.reduce<Record<string, CommandRegistry>>(
(commands, registry) => { (commands, registry, index) => {
if (registry.status === 'rejected') return commands; if (registry.status === 'rejected') {
console.warn(
`Command ${index} failed to load; skipping...`,
registry.reason,
);
return commands;
}
if (isDev()) registry.value.command.setDefaultMemberPermissions(0); if (isDev()) registry.value.command.setDefaultMemberPermissions(0);
return { ...commands, [registry.value.command.name]: registry.value }; return { ...commands, [registry.value.command.name]: registry.value };
}, },
+125
View File
@@ -23,6 +23,27 @@
"researchable": "Tech tree", "researchable": "Tech tree",
"premium": "Premium", "premium": "Premium",
"collector": "Collector" "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", "$": "tree-type",
"description": "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"
}
} }
} }
}, },