feat: localize performance

This commit is contained in:
tresabhi committed 2025-01-07 12:47:55 -06:00
1 parent 7dfdf4c11b
commit 9de41529d1
15 files changed
+134 -76

No files matched your search

+34
View File
@@ -6,6 +6,16 @@
},
"common": {
"player_count_period": {
"total": "Total",
"d_120": "Past 120 days",
"d_90": "Past 90 days",
"d_60": "Past 60 days",
"d_30": "Past month",
"d_7": "Past week",
"d_1": "Yesterday"
},
"branches": {
"local": "dev",
"dev": "beta",
@@ -738,6 +748,30 @@
"body": "Estimated stats based on %s1 players. Updated %s2 ago.",
"hours": "%s1 hours",
"minutes": "%s1 minutes"
},
"player_count": {
"label": "Player count"
},
"table": {
"tanks": {
"header": "Tank",
"total": "Total"
},
"stats": {
"winrate": "Winrate",
"players": "Players",
"damage": "Damage",
"survival": "Survival",
"xp": "XP",
"kills": "Kills",
"spots": "Spots",
"accuracy": "Accuracy",
"shots": "Shots",
"hits": "Hits",
"damage_ratio": "Dmg. ratio",
"damage_taken": "Dmg. taken",
"capture_points": "Cap points"
}
}
},
"gallery": {
+6 -5
View File
@@ -1,11 +1,8 @@
import { merge } from 'lodash-es';
import type en from '../../lang/en.json';
import { assertSecret, DEFAULT_LOCALE, SUPPORTED_LOCALES } from '../blitzkit';
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from '../blitzkit';
import { TranslationTree } from './translator';
const PUBLIC_DEBUG_MISSING_I18N = assertSecret(
import.meta.env.PUBLIC_DEBUG_MISSING_I18N,
);
const files = import.meta.glob('../../lang/*.json', { eager: true });
export const localizedStrings: Record<string, typeof en> = {};
@@ -25,7 +22,11 @@ SUPPORTED_LOCALES.forEach((locale) => {
const strings = files[`../../lang/${locale}.json`];
const mergedStrings = merge({}, defaultStrings, strings);
if (PUBLIC_DEBUG_MISSING_I18N === 'true') nuke(mergedStrings);
/**
* For some reason, this won't load on the server on the first render so I am
* not asserting this one lol
*/
if (import.meta.env.PUBLIC_DEBUG_MISSING_I18N === 'true') nuke(mergedStrings);
localizedStrings[locale] = mergedStrings as typeof en;
});
@@ -8,6 +8,7 @@ import {
} from '@radix-ui/themes';
import type { ComponentProps } from 'react';
import { radToDeg } from 'three/src/math/MathUtils.js';
import { useLocale } from '../../../hooks/useLocale';
import type {
Shot,
ShotLayer,
@@ -49,6 +50,8 @@ function LayerEntry({
layer: ShotLayer;
angle?: number;
}) {
const { locale } = useLocale();
return (
<Flex align="center" gap="2">
<Text size="1" color="gray" style={{ width: 12 }}>
@@ -61,7 +64,7 @@ function LayerEntry({
{layer.type === null && (
<>
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
{Math.round(layer.distance * 1000).toLocaleString()}
{Math.round(layer.distance * 1000).toLocaleString(locale)}
<Text size="1">mm</Text>
</Text>
@@ -73,7 +76,7 @@ function LayerEntry({
{layer.type === ArmorType.External && (
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
{Math.round(layer.thickness).toLocaleString()}
{Math.round(layer.thickness).toLocaleString(locale)}
<Text size="1">mm</Text>
</Text>
)}
@@ -82,14 +85,14 @@ function LayerEntry({
layer.type === ArmorType.Spaced) && (
<>
<Text size="2" color={shotStatusColor} style={{ width: 80 }}>
{Math.round(layer.thicknessAngled).toLocaleString()}
{Math.round(layer.thicknessAngled).toLocaleString(locale)}
<Text size="1">{`mm${
angle === undefined ? '' : ` ${Math.round(radToDeg(angle))}°`
}`}</Text>
</Text>
<Text size="2" color="gray">
{Math.round(layer.thickness).toLocaleString()}
{Math.round(layer.thickness).toLocaleString(locale)}
<Text size="1">mm</Text> nominal
</Text>
</>
@@ -103,6 +106,8 @@ interface ShotDisplayCardProps extends CardProps {
}
export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
const { locale } = useLocale();
if (!shot) return null;
const outTitleColor = shot.out
@@ -117,7 +122,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
<Text color={inTitleColor} weight="bold">
{shot.in.status !== 'blocked' && shot.in.status !== 'ricochet' && (
<>
{Math.round(shot.damage).toLocaleString()}
{Math.round(shot.damage).toLocaleString(locale)}
<Text size="1">hp</Text>
</>
)}
@@ -182,7 +187,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
{shot.out.status !== 'blocked' &&
shot.out.status !== 'ricochet' && (
<>
{Math.round(shot.damage).toLocaleString()}
{Math.round(shot.damage).toLocaleString(locale)}
<Text size="1">hp</Text>
</>
)}
@@ -16,6 +16,7 @@ import {
} from '@radix-ui/themes';
import { times } from 'lodash-es';
import { useState } from 'react';
import { useLocale } from '../../hooks/useLocale';
import './index.css';
const DAY_TITLES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
@@ -37,6 +38,7 @@ export function DatePicker({
const [yearOffset, setYearOffset] = useState(0);
const [monthOffset, setMonthOffset] = useState(0);
const date = new Date(year + yearOffset, month + monthOffset, day);
const { locale } = useLocale();
const daysInLastMonth = new Date(
year + yearOffset,
@@ -94,11 +96,7 @@ export function DatePicker({
</IconButton>
<Flex gap="1">
<Text>
{date.toLocaleString(undefined, {
month: 'long',
})}
</Text>
<Text>{date.toLocaleString(locale, { month: 'long' })}</Text>
<Popover.Root onOpenChange={() => setYearScroll(0)}>
<Popover.Trigger>
@@ -2,6 +2,7 @@ import { Flex, Text } from '@radix-ui/themes';
import fuzzysort from 'fuzzysort';
import { times } from 'lodash-es';
import { useMemo, useState } from 'react';
import { useLocale } from '../../hooks/useLocale';
import { GalleryEphemeral } from '../../stores/galleryEphemeral';
import { GalleryCard } from './Card';
@@ -20,6 +21,7 @@ const PREVIEW_COUNT = 28;
export function GalleryList({ avatars }: GalleryListProps) {
const search = GalleryEphemeral.use((state) => state.search);
const [loadedCards, setLoadedCards] = useState(DEFAULT_LOADED);
const { locale } = useLocale();
const filtered = useMemo(() => {
setLoadedCards(DEFAULT_LOADED);
@@ -35,7 +37,7 @@ export function GalleryList({ avatars }: GalleryListProps) {
return (
<>
<Text align="center" color="gray">
{filtered.length.toLocaleString()} avatars
{filtered.length.toLocaleString(locale)} avatars
</Text>
<Flex wrap="wrap" gap="4" justify="center">
@@ -4,21 +4,23 @@ import {
CaretUpIcon,
} from '@radix-ui/react-icons';
import { Button, Flex, Table } from '@radix-ui/themes';
import { useLocale } from '../../hooks/useLocale';
import { TankPerformanceSort } from '../../stores/tankPerformanceSort';
import {
TankPerformanceSortTypeNames,
TankPerformanceSortTypeNamesArray,
} from '../../stores/tankPerformanceSort/constants';
import { TankPerformanceSortTypeNamesArray } from '../../stores/tankPerformanceSort/constants';
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
export function Header() {
const sort = TankPerformanceSort.use();
const store = TankPerformanceSort.useStore();
const { strings } = useLocale();
return (
<Table.Header>
<Table.Row align="center">
<StickyColumnHeaderCell>Tank</StickyColumnHeaderCell>
<StickyColumnHeaderCell>
{strings.website.tools.performance.table.tanks.header}
</StickyColumnHeaderCell>
{TankPerformanceSortTypeNamesArray.map((type) => {
const isSelected = sort.type === type;
@@ -42,7 +44,7 @@ export function Header() {
}
}}
>
{TankPerformanceSortTypeNames[type]}
{strings.website.tools.performance.table.stats[type]}
{!isSelected && <CaretSortIcon />}
{isSelected && (
@@ -1,4 +1,5 @@
import { Flex, Select, Text } from '@radix-ui/themes';
import { useLocale } from '../../hooks/useLocale';
import {
TankPerformanceEphemeral,
type PlayerCountPeriod,
@@ -9,10 +10,11 @@ export function PlayerCountControl() {
(state) => state.playerCountPeriod,
);
const mutateTankPerformanceSort = TankPerformanceEphemeral.useMutation();
const { strings } = useLocale();
return (
<Flex justify="center" align="center" gap="2">
<Text>Player count</Text>
<Text>{strings.website.tools.performance.player_count.label}</Text>
<Select.Root
value={playerCountPeriod}
onValueChange={(period) => {
@@ -23,13 +25,27 @@ export function PlayerCountControl() {
>
<Select.Trigger />
<Select.Content>
<Select.Item value="total">Total</Select.Item>
<Select.Item value="d_120">Past 120 days</Select.Item>
<Select.Item value="d_90">Past 90 days</Select.Item>
<Select.Item value="d_60">Past 60 days</Select.Item>
<Select.Item value="d_30">This month</Select.Item>
<Select.Item value="d_7">This week</Select.Item>
<Select.Item value="d_1">Yesterday</Select.Item>
<Select.Item value="total">
{strings.common.player_count_period.total}
</Select.Item>
<Select.Item value="d_120">
{strings.common.player_count_period.d_120}
</Select.Item>
<Select.Item value="d_90">
{strings.common.player_count_period.d_90}
</Select.Item>
<Select.Item value="d_60">
{strings.common.player_count_period.d_60}
</Select.Item>
<Select.Item value="d_30">
{strings.common.player_count_period.d_30}
</Select.Item>
<Select.Item value="d_7">
{strings.common.player_count_period.d_7}
</Select.Item>
<Select.Item value="d_1">
{strings.common.player_count_period.d_1}
</Select.Item>
</Select.Content>
</Select.Root>
</Flex>
@@ -3,6 +3,7 @@ import { Table } from '@radix-ui/themes';
import { memo } from 'react';
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
import { useLocale } from '../../hooks/useLocale';
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
import { TankRowHeaderCell } from '../TankRowHeaderCell';
@@ -19,6 +20,7 @@ export const TankRow = memo<TankRowProps>(
const playerCountPeriod = TankPerformanceEphemeral.use(
(state) => state.playerCountPeriod,
);
const { locale } = useLocale();
return (
<Table.Row>
@@ -29,13 +31,14 @@ export const TankRow = memo<TankRowProps>(
</Table.Cell>
<Table.Cell align="center">
{formatCompact(
locale,
Math.round(ratio * averages.samples[playerCountPeriod]),
)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(
averages.mu.damage_dealt / averages.mu.battles,
).toLocaleString()}
).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(
@@ -44,7 +47,9 @@ export const TankRow = memo<TankRowProps>(
%
</Table.Cell>
<Table.Cell align="center">
{Math.round(averages.mu.xp / averages.mu.battles).toLocaleString()}
{Math.round(averages.mu.xp / averages.mu.battles).toLocaleString(
locale,
)}
</Table.Cell>
<Table.Cell align="center">
{(averages.mu.frags / averages.mu.battles).toFixed(2)}
@@ -67,7 +72,7 @@ export const TankRow = memo<TankRowProps>(
<Table.Cell align="center">
{Math.round(
averages.mu.damage_received / averages.mu.battles,
).toLocaleString()}
).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">
{(averages.mu.capture_points / averages.mu.battles).toFixed(2)}
@@ -7,6 +7,7 @@ import { Table } from '@radix-ui/themes';
import { memo, useCallback } from 'react';
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
import { useLocale } from '../../hooks/useLocale';
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
@@ -25,6 +26,7 @@ export const Total = memo<TotalProps>(
const playerCountPeriod = TankPerformanceEphemeral.use(
(state) => state.playerCountPeriod,
);
const { strings, locale } = useLocale();
const ratio = useAveragesExclusionRatio();
const sum = useCallback(
(slice: (tank: BlitzkitStats) => number) => {
@@ -54,20 +56,23 @@ export const Total = memo<TotalProps>(
return (
<Table.Row>
<StickyRowHeaderCell style={{ overflow: 'hidden', display: 'flex' }}>
Total
{strings.website.tools.performance.table.tanks.total}
</StickyRowHeaderCell>
<Table.Cell align="center">{(winrate * 100).toFixed(1)}%</Table.Cell>
<Table.Cell align="center">
{formatCompact(ratio * averageDefinitions.samples[playerCountPeriod])}
{formatCompact(
locale,
ratio * averageDefinitions.samples[playerCountPeriod],
)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(damage).toLocaleString()}
{Math.round(damage).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">
{Math.round(survival * 100).toFixed(1)}%
</Table.Cell>
<Table.Cell align="center">
{Math.round(xp).toLocaleString()}
{Math.round(xp).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">{kills.toFixed(2)}</Table.Cell>
<Table.Cell align="center">{spots.toFixed(2)}</Table.Cell>
@@ -76,7 +81,7 @@ export const Total = memo<TotalProps>(
<Table.Cell align="center">{hits.toFixed(1)}</Table.Cell>
<Table.Cell align="center">{damageRatio.toFixed(2)}</Table.Cell>
<Table.Cell align="center">
{Math.round(damageReceived).toLocaleString()}
{Math.round(damageReceived).toLocaleString(locale)}
</Table.Cell>
<Table.Cell align="center">{capturePoints.toFixed(2)}</Table.Cell>
</Table.Row>
@@ -187,7 +187,7 @@ export function MetaSection() {
} price`}
>
<Flex align="center" gap="1">
{protagonist.price.value.toLocaleString()}
{protagonist.price.value.toLocaleString(locale)}
<img
style={{ width: '1em', height: '1em' }}
alt={TankPriceType[protagonist.price.type]}
@@ -203,7 +203,7 @@ export function MetaSection() {
</Listing>
<Listing label="Sale price">
<Flex align="center" gap="1">
{(protagonist.price.value / 2).toLocaleString()}
{(protagonist.price.value / 2).toLocaleString(locale)}
<img
style={{ width: '1em', height: '1em' }}
alt={TankPriceType[protagonist.price.type]}
@@ -223,7 +223,7 @@ export function MetaSection() {
{(
protagonist.research_cost.research_cost_type!
.value as number
).toLocaleString()}
).toLocaleString(locale)}
<img
style={{ width: '1em', height: '1em' }}
alt="xp"
@@ -3,6 +3,7 @@ import { CaretLeftIcon, CaretRightIcon, PlusIcon } from '@radix-ui/react-icons';
import { Flex, Heading, IconButton, ScrollArea, Text } from '@radix-ui/themes';
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
import { useLocale } from '../../../hooks/useLocale';
import { Duel } from '../../../stores/duel';
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
@@ -16,6 +17,7 @@ const tankDefinitions = await awaitableTankDefinitions;
export const XP_MULTIPLIERS = [1, 2, 3, 4, 5, 10];
export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
const { locale } = useLocale();
const master = Duel.use((state) => state.protagonist.tank);
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier);
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
@@ -105,7 +107,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
objectPosition: 'center',
}}
/>
{Math.round(totalXp / xpMultiplier).toLocaleString()}
{Math.round(totalXp / xpMultiplier).toLocaleString(locale)}
</Flex>
</Text>
@@ -121,7 +123,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
objectPosition: 'center',
}}
/>
{totalCredits.toLocaleString()}
{totalCredits.toLocaleString(locale)}
</Flex>
</Text>
</Flex>
@@ -39,6 +39,7 @@ import { StickyTableRoot } from '../../../../components/StickyTableRoot';
import { TankRowHeaderCell } from '../../../../components/TankRowHeaderCell';
import { awaitableAverageDefinitions } from '../../../../core/awaitables/averageDefinitions';
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions';
import { useLocale } from '../../../../hooks/useLocale';
import { Session, type SessionTracking } from '../../../../stores/session';
const [tankDefinitions, averageDefinitions] = await Promise.all([
@@ -55,6 +56,7 @@ export function Page() {
}
function Content() {
const { locale } = useLocale();
const [showSearch, setShowSearch] = useState(false);
const [searching, setSearching] = useState(false);
const [searchResults, setSearchResults] = useState<AccountListWithServer>([]);
@@ -265,7 +267,7 @@ function Content() {
<Flex mt="2" mb="2" direction="column" gap="2">
<Heading size="5">Tracking {accountInfo?.nickname}</Heading>
<Text color="gray">
Since {new Date(session.player.since).toLocaleString()}
Since {new Date(session.player.since).toLocaleString(locale)}
</Text>
</Flex>
@@ -1,14 +1,14 @@
---
import {
asset,
createDefaultProvisions,
createDefaultSkills,
fetchEquipmentDefinitions,
fetchModelDefinitions,
fetchProvisionDefinitions,
fetchSkillDefinitions,
fetchTankDefinitions,
TIER_ROMAN_NUMERALS,
asset,
createDefaultProvisions,
createDefaultSkills,
fetchEquipmentDefinitions,
fetchModelDefinitions,
fetchProvisionDefinitions,
fetchSkillDefinitions,
fetchTankDefinitions,
TIER_ROMAN_NUMERALS,
} from '@blitzkit/core';
import strings from '@blitzkit/core/lang/en-US.json';
import type { GetStaticPaths } from 'astro';
@@ -31,8 +31,8 @@ export const getStaticPaths = (async () => {
.flat();
}) satisfies GetStaticPaths;
const { id, locale } = Astro.params;
const {id} = Astro.params;
const locale = Astro.params.locale ?? DEFAULT_LOCALE;
const tankDefinitions = await fetchTankDefinitions();
const skillDefinitions = await fetchSkillDefinitions();
const provisionDefinitions = await fetchProvisionDefinitions();
@@ -77,7 +77,7 @@ const description = `${
name
} armor, statistics, equipment, provisions, and more. ${Math.round(
characteristics.dpm,
).toLocaleString()} DPM, ${characteristics.damage.toFixed(0)}hp alpha, ${characteristics.penetration.toFixed(
).toLocaleString(locale)} DPM, ${characteristics.damage.toFixed(0)}hp alpha, ${characteristics.penetration.toFixed(
0,
)}mm penetration, ${characteristics.speedForwards.toFixed(
0,
@@ -1,21 +1,6 @@
import en from '@blitzkit/core/lang/en.json';
import type { TankPerformanceSortType } from '.';
export const TankPerformanceSortTypeNames = {
winrate: 'Winrate',
players: 'Players',
damage: 'Damage',
survival: 'Survival',
xp: 'XP',
kills: 'Kills',
spots: 'Spots',
accuracy: 'Accuracy',
shots: 'Shots',
hits: 'Hits',
damageRatio: 'Dmg. ratio',
damageTaken: 'Dmg. taken',
capturePoints: 'Cap points',
} as const;
export const TankPerformanceSortTypeNamesArray = Object.keys(
TankPerformanceSortTypeNames,
en.website.tools.performance.table.stats,
) as TankPerformanceSortType[];
@@ -1,8 +1,9 @@
import type en from '@blitzkit/core/lang/en.json';
import { create } from 'zustand';
import { createContextualStore } from '../../core/zustand/createContextualStore';
import { TankPerformanceSortTypeNames } from './constants';
export type TankPerformanceSortType = keyof typeof TankPerformanceSortTypeNames;
export type TankPerformanceSortType =
keyof typeof en.website.tools.performance.table.stats;
export interface TankPerformanceSort {
type: TankPerformanceSortType;