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