Compare commits

..
55 Commits
Author SHA1 Message Date
TrèsAbhi 8478930f2e Merge branch 'main' into ue4 2025-08-15 12:40:17 -05:00
TrèsAbhi 6a74451740 feat: some udpates 2025-08-15 12:38:00 -05:00
TrèsAbhi 09f5d4e3f7 feat: copy base64 2025-06-12 20:52:06 -05:00
TrèsAbhi 0365656a0e fix: old proxy 2025-06-12 20:44:39 -05:00
TrèsAbhi 90a46eeffa feat: update protos 2025-06-12 20:43:26 -05:00
TrèsAbhi 1249877a8c feat: log bytes 2025-06-12 20:41:56 -05:00
TrèsAbhi 02df1e59d8 feat: code diff 2025-06-12 20:29:55 -05:00
TrèsAbhi edce64c1bd feat: dev tools 2025-06-12 17:48:43 -05:00
TrèsAbhi 74b821ac13 feat: ini accessor (needs work) 2025-06-10 13:53:30 -05:00
TrèsAbhi a5a72f9536 feat: some more ffi communication 2025-06-10 13:16:14 -05:00
TrèsAbhi fe3d029e74 feat: ffi 2025-06-09 20:19:13 -05:00
TrèsAbhi a69ea38947 feat: internals 2025-06-09 18:17:08 -05:00
TrèsAbhi bc9f7054e1 Merge branch 'main' into ue4 2025-06-08 19:38:46 -05:00
TrèsAbhi 53c2f9605f Merge branch 'main' into ue4 2025-06-08 12:19:04 -05:00
TrèsAbhi 95f67a04d7 Merge branch 'main' into ue4 2025-06-05 14:25:23 -05:00
TrèsAbhi 1e6ee2b80d feat: complete proxy client 2025-06-05 14:20:20 -05:00
TrèsAbhi 6a21c9aaac feat: docs 2025-06-05 12:42:40 -05:00
TrèsAbhi 7e11c1e5ce feat: api 2025-06-03 20:31:53 -05:00
TrèsAbhi 2dc4f12246 feat: download 2025-06-03 14:00:53 -05:00
TrèsAbhi 354aca5057 feat: skeletons 2025-06-02 13:21:49 -05:00
TrèsAbhi c5f156eecc feat: simplify tool card 2025-06-02 12:54:57 -05:00
TrèsAbhi 37b89d1f2d feat: remove debug 2025-06-01 20:53:37 -05:00
TrèsAbhi 74c358dde5 feat: node v22 2025-06-01 20:51:48 -05:00
TrèsAbhi 4362746de3 feat: node 2025-06-01 20:50:28 -05:00
TrèsAbhi d5d97e772a feat: versions 2025-06-01 20:48:59 -05:00
TrèsAbhi 654c87dae7 feat: versions 2025-06-01 20:47:35 -05:00
TrèsAbhi e94f8a7cd2 feat: use bun setup v2 2025-06-01 20:44:19 -05:00
TrèsAbhi d39ffecb3b feat: log env 2025-06-01 20:37:33 -05:00
TrèsAbhi 6f8e12657c fix: quotation escape 2025-05-31 18:41:15 -05:00
TrèsAbhi 4a79f98f3f feat: popup 2025-05-31 18:21:57 -05:00
TrèsAbhi 06de86c5a6 feat: homepage announcement 2025-05-31 18:21:49 -05:00
TrèsAbhi 37ba6d37d4 feat: back to ubuntu 2025-05-31 12:16:33 -05:00
TrèsAbhi 636add3a18 feat: use windows? 2025-05-31 12:13:28 -05:00
TrèsAbhi 2e13bcfee2 feat: remote strings 2025-05-31 12:07:52 -05:00
TrèsAbhi af53713c60 feat: remove prisma usage 2025-05-31 11:56:35 -05:00
TrèsAbhi c37820d2cd fix: github token 2025-05-31 11:54:48 -05:00
TrèsAbhi d51b6bdd18 fix: github token 2025-05-31 11:49:22 -05:00
TrèsAbhi 67ce4815df feat: workflow 2025-05-31 11:47:30 -05:00
TrèsAbhi 8c14c3d093 feat: prep builder 2025-05-31 11:45:00 -05:00
TrèsAbhi 8373fb1b7a feat: dialog 2025-05-30 20:30:16 -05:00
TrèsAbhi 3aafe4d9f1 feat: code split 2025-05-30 15:06:44 -05:00
TrèsAbhi 2c2c3687c7 feat: rename to avatars 2025-05-30 14:50:11 -05:00
TrèsAbhi 9375f134db feat: stacking 2025-05-30 14:09:30 -05:00
TrèsAbhi 1eca71d28c feat: game strings context 2025-05-29 20:22:13 -05:00
TrèsAbhi 9bf1554c30 fix: code split correctly 2025-05-29 19:28:58 -05:00
TrèsAbhi f48c5d246d feat: remote and socket metadata and gallery 2025-05-29 19:20:28 -05:00
TrèsAbhi 5e9aa99755 feat: remote catalog 2025-05-28 18:32:04 -05:00
TrèsAbhi e7386ff766 feat: remove ds 2025-05-28 11:48:40 -05:00
TrèsAbhi 4129a1964f feat: home page 2025-05-28 11:28:22 -05:00
TrèsAbhi 64d12bba8b feat: simplify tools 2025-05-28 10:42:09 -05:00
TrèsAbhi 69b13fd33a feat: custom components 2025-05-27 12:39:04 -05:00
TrèsAbhi bc6bb4f10e feat: 404 page 2025-05-26 20:07:33 -05:00
TrèsAbhi 5adf90bab2 feat: cleanup 2025-05-26 19:04:58 -05:00
TrèsAbhi bc8cfc9897 feat: start new website 2025-05-26 18:39:01 -05:00
TrèsAbhi 9037e2caff feat: update to latest 2025-05-26 18:30:45 -05:00
466 changed files with 15021 additions and 19500 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
{
"indentSize": 2
}
-153
View File
@@ -1,153 +0,0 @@
name: Build Assets
on:
push:
branches: main
workflow_dispatch:
inputs:
cdn_branch:
type: string
required: false
default: main
description: What CDN branch to write to
zip_url:
type: string
required: false
description: ZIP URL to download instead of Steam client
zip_root:
type: string
required: false
description: Root dir of ZIP which houses the executable and Data dir
schedule:
- cron: '0 2 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.cdn_branch }}
cancel-in-progress: true
env:
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
VIDEOS_SHEET: ${{ secrets.VIDEOS_SHEET }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
WOTB_DLC_CDN: ${{ secrets.WOTB_DLC_CDN }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_REPO: blitzkit/assets
PUBLIC_ASSET_BRANCH: main
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: true
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
PUBLIC_PATREON_CLIENT_ID: ${{ secrets.PUBLIC_PATREON_CLIENT_ID }}
PUBLIC_PATREON_REDIRECT_URI: ${{ secrets.PUBLIC_PATREON_REDIRECT_URI }}
PATREON_CLIENT_SECRET: ${{ secrets.PATREON_CLIENT_SECRET }}
PUBLIC_NITROPAY_ID: ${{ secrets.PUBLIC_NITROPAY_ID }}
PUBLIC_DEBUG_MISSING_I18N: false
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_FAST_USER: blitzkit
PUBLIC_ASSET_FAST_REPO: assets
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
jobs:
download_client:
name: Download client
runs-on: ubuntu-latest
steps:
- name: (ZIP) Maximize build space
if: github.event.inputs.zip_url != ''
uses: easimon/maximize-build-space@master
with:
root-reserve-mb: 512
swap-size-mb: 1024
remove-dotnet: 'true'
- uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- name: (Steam) Setup bun
uses: oven-sh/setup-bun@v1
if: github.event.inputs.zip_url == ''
- name: (Steam) Install dependencies
if: github.event.inputs.zip_url == ''
run: bun install
- name: (Steam) Setup DepotDownloader
if: github.event.inputs.zip_url == ''
run: bun run setup:depotdownloader
- name: (Steam) Download client
if: github.event.inputs.zip_url == ''
run: |
chmod +x ./temp/depotDownloader/DepotDownloader
./temp/depotDownloader/DepotDownloader -q -os windows -app 444200 -username ${{ secrets.DEPOT_DOWNLOADER_USERNAME }} -password "${{ secrets.DEPOT_DOWNLOADER_PASSWORD }}"
- name: (Steam) Apply patches
if: github.event.inputs.zip_url == ''
run: bun setup:patches --depot
- name: (ZIP) Download client
if: github.event.inputs.zip_url != ''
env:
ZIP_URL: ${{ github.event.inputs.zip_url }}
ZIP_ROOT: ${{ github.event.inputs.zip_root }}
run: |
wget -q -O client.zip $ZIP_URL
unzip -q client.zip -d client
rm client.zip
mkdir -p depots/444202/0
mv client/$ZIP_ROOT/* depots/444202/0
- name: Archive depots
uses: actions/upload-artifact@v4
with:
name: depots
path: depots
build_assets:
name: Build assets
runs-on: ubuntu-latest
needs: download_client
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- name: Setup bun
uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Lint
run: bun run lint:website
- name: Download depots
uses: actions/download-artifact@v4
with:
name: depots
path: depots
- name: AppArmor hack
run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
- name: Start server and build assets
run: |
bun dev:website --verbosepa &
SERVER_PID=$!
bun run build:assets --all-targets --depot --production
kill $SERVER_PID || true
-49
View File
@@ -1,49 +0,0 @@
name: Averages
on:
workflow_dispatch:
schedule:
- cron: '0 8 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: 'true'
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Build averages
env:
PUBLIC_ASSET_BRANCH: main
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
VIDEOS_SHEET: ${{ secrets.VIDEOS_SHEET }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
run: bun run build:averages --production
-32
View File
@@ -1,32 +0,0 @@
name: Discover IDs
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 1'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_ASSET_BRANCH: ${{ secrets.PUBLIC_ASSET_BRANCH }}
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Discover IDs
run: bun build:ids --production
-13
View File
@@ -22,19 +22,6 @@ env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
SERVICING: ${{ secrets.SERVICING }}
jobs:
host:
-41
View File
@@ -1,41 +0,0 @@
name: Rating Asia
on:
workflow_dispatch:
schedule:
- cron: '0 17 * * *'
jobs:
host:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Generate leaderboard
env:
PUBLIC_ASSET_BRANCH: main
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: 'true'
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
run: bun build:ratingleaderboard --region=asia
-41
View File
@@ -1,41 +0,0 @@
name: Rating Europe
on:
workflow_dispatch:
schedule:
- cron: '0 23 * * *'
jobs:
host:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Generate leaderboard
env:
PUBLIC_ASSET_BRANCH: main
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: 'true'
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
run: bun build:ratingleaderboard --region=eu
-41
View File
@@ -1,41 +0,0 @@
name: Rating North America
on:
workflow_dispatch:
schedule:
- cron: '0 5 * * *'
jobs:
host:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Generate leaderboard
env:
PUBLIC_ASSET_BRANCH: main
PUBLIC_ASSET_REPO: ${{ secrets.PUBLIC_ASSET_REPO }}
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: 'true'
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
run: bun build:ratingleaderboard --region=com
+20 -20
View File
@@ -17,33 +17,34 @@ concurrency:
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
AUTO_PROXY_ORIGIN: ${{ secrets.AUTO_PROXY_ORIGIN }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
SERVICING: ${{ secrets.SERVICING }}
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.GH_TOKEN }}
- uses: oven-sh/setup-bun@v1
- uses: actions/configure-pages@v5
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: latest
- name: Configure pages
uses: actions/configure-pages@v5
- name: Install dependencies
run: bun install
@@ -57,13 +58,12 @@ jobs:
path: dist/website
deploy-github-pages:
if: ${{ github.ref == 'refs/heads/main' }}
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
needs: build
environment:
name: Production
url: ${{ steps.deployment.outputs.page_url }}
if: >
(github.repository == 'tresabhi/blitzkit' && github.ref == 'refs/heads/main') ||
(github.repository == 'blitzkit/host-redux' && github.ref == 'refs/heads/ue4')
steps:
- name: Deploy to GitHub Pages
+224 -26
View File
@@ -22,34 +22,232 @@ depots
public/sitemap*
/firebase.json
target
# packages/core/src/protos/*.ts
/.vercel
.astro
/.astro
.vercel
/src
/packages/bot/src/prisma
dev-dist
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.rsuser
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.user
*.userosscache
*.sln.docstates
*.userprefs
mono_crash.*
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
[Aa][Rr][Mm]64[Ee][Cc]/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
.vs/
Generated\ Files/
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*.VisualState.xml
TestResult.xml
nunit-*.xml
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
BenchmarkDotNet.Artifacts/
project.lock.json
project.fragment.lock.json
artifacts/
ScaffoldingReadMe.txt
StyleCopReport.xml
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.idb
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
_Chutzpah*
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
*.psess
*.vsp
*.vspx
*.sap
*.e2e
$tf/
*.gpState
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
_TeamCity*
*.dotCover
.axoCover/*
!.axoCover/settings.json
coverage*.json
coverage*.xml
coverage*.info
*.coverage
*.coveragexml
_NCrunch_*
.NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
*.mm.*
AutoTest.Net/
.sass-cache/
[Ee]xpress/
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
publish/
*.[Pp]ublish.xml
*.azurePubxml
*.pubxml
*.publishproj
PublishScripts/
*.nupkg
*.snupkg
*.nuget.props
*.nuget.targets
csx/
*.build.csdef
ecf/
rcf/
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
*.[Cc]ache
!?*.[Cc]ache/
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
Generated_Code/
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
*.mdf
*.ldf
*.ndf
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
FakesAssemblies/
*.GhostDoc.xml
.ntvs_analysis.dat
node_modules/
*.plg
*.opt
*.vbw
*.vbp
*.dsw
*.dsp
*.ncb
*.aps
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
.paket/paket.exe
paket-files/
.fake/
.cr/personal
__pycache__/
*.pyc
*.tss
*.jmconfig
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
OpenCover/
ASALocalRun/
*.binlog
.aws-sam
*.nvuser
.mfractor/
.localhistory/
.vshistory/
healthchecksdb
MigrationBackup/
.ionide/
FodyWeavers.xsd
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
.history/
*.cab
*.msi
*.msix
*.msm
*.msp
*.sln.iml
+7
View File
@@ -0,0 +1,7 @@
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 80
}
+12 -9
View File
@@ -21,7 +21,6 @@
"AYUV",
"battleresults",
"bbox",
"beforeinstallprompt",
"bepaint",
"betakrieg",
"BINORMAL",
@@ -52,6 +51,7 @@
"CUBEMAP",
"CUBETEXCOORD",
"customperiod",
"Datacard",
"datasource",
"Dava",
"dbid",
@@ -69,6 +69,7 @@
"DIDS",
"donotremove",
"drei",
"dsapp",
"dvpd",
"dvpl",
"DVPLCRC",
@@ -77,16 +78,13 @@
"echarts",
"Everytime",
"FASTNAME",
"favicons",
"flexdirection",
"fontsource",
"FOURCC",
"frameloop",
"fsformat",
"Fullscreen",
"fullstats",
"fuzzysort",
"gamemode",
"gameplay",
"glsl",
"GLTF",
@@ -133,6 +131,7 @@
"noout",
"normalmap",
"oneof",
"oodle",
"oopsies",
"Oopsy",
"opentest",
@@ -158,7 +157,6 @@
"Pyogenics",
"ratingleaderboard",
"raycaster",
"Redirector",
"researchable",
"Resvg",
"rizz",
@@ -172,7 +170,6 @@
"searchtanks",
"SHAREDEXP",
"SKLLD",
"smoothstep",
"Smri",
"Specularity",
"SSAO",
@@ -192,7 +189,6 @@
"tankopedia",
"tankopedias",
"tankstats",
"tauri",
"texconv",
"TEXCOORD",
"tresabhi",
@@ -200,6 +196,8 @@
"Turretless",
"TYPELESS",
"ubjson",
"Uncompiled",
"undiscriminated",
"Unicum",
"UNLOCALIZED",
"UNORM",
@@ -229,7 +227,12 @@
"yolo",
"youtuber",
"youtubers",
"zoomable"
"zustand"
],
"typescript.tsdk": "node_modules\\typescript\\lib"
"deno.enable": false,
"deno.unstable": false,
"cSpell.enableFiletypes": ["glsl", "proto3"],
"typescript.tsdk": "node_modules\\typescript\\lib",
"liveServer.settings.multiRootWorkspaceName": "blitzkit",
"git.ignoreLimitWarning": true
}
+41
View File
@@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/BlitzKit.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/BlitzKit.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/BlitzKit.sln"
],
"problemMatcher": "$msCompile"
}
]
}
+27
View File
@@ -0,0 +1,27 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F23F76D4-C350-4BBA-9DF1-E67C5CD391B1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlitzKit.FFI", "src\BlitzKit.FFI\BlitzKit.FFI.csproj", "{8E798C66-C863-4A67-87EA-853109EEE037}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8E798C66-C863-4A67-87EA-853109EEE037}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8E798C66-C863-4A67-87EA-853109EEE037}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8E798C66-C863-4A67-87EA-853109EEE037}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8E798C66-C863-4A67-87EA-853109EEE037}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8E798C66-C863-4A67-87EA-853109EEE037} = {F23F76D4-C350-4BBA-9DF1-E67C5CD391B1}
EndGlobalSection
EndGlobal
+14 -41
View File
@@ -1,49 +1,22 @@
# BlitzKit
## BlitzKit
Tools for everything and anything, World of Tanks Blitz.
Your premier source for all things Blitz.
This GitHub repository contains the code for BlitzKit and all its services. If you are here, looking to use BlitzKit, go to https://blitzkit.app/.
### https://blitzkit.app/
If you intend to contribute to the code, note that you may need to check out sensitive, closed-source submodules to locally develop certain aspects of BlitzKit. Communicate with `@tresabhi` on Discord to get started or discuss sensitive content, such as environment variables or submodules.
![BlitzKit website](https://i.imgur.com/KaxUxmt.png)
## Packages
## Invite the bot
| Package | Title | Source | Memo |
| ------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| [packages/app](packages/app) | BlitzKit | [Play Store](https://play.google.com/store/apps/details?id=com.tresabhi.blitzkit), [App Store](https://apps.apple.com/app/4321), [Microsoft Store](https://apps.microsoft.com/detail/zyxw) | The native app of BlitzKit for all platforms. Unavailable from stores at this time. |
| [packages/bot](packages/bot) | BlitzKit Discord Bot | [Discord Application Directory](https://discord.com/discovery/applications/1097673957865443370) | The one-in-all Discord bot for statistics. |
| [packages/core](packages/core) | | | Mutual internal code shared between all other packages. |
| [packages/i18n](packages/i18n) | | [Crowdin](https://crowdin.com/project/blitzkrieg) | Translations served as JSON shared across all other packages, including some utility code. |
| [packages/scripts](packages/scripts) | | | CLI utilities for the development and deployment of BlitzKit services. |
| [packages/varuna](packages/varuna) | Varuna | [NPM](https://www.npmjs.com/package/varuna) | React and Astro-oriented state management library, awaiting separation from BlitzKit. Unavailable from NPM at this time. |
| [packages/website](packages/website) | BlitzKit | https://blitzkit.app | The BlitzKit website provides a wide range of tools, APIs, and interconnects all other services. |
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
## Submodules
![BlitzKit application directory](https://i.imgur.com/YWCtOuJ.png)
| Submodule | Title | Source | Memo |
| ------------------------------------------------------------ | ----------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [submodules/blitzkit-closed](submodules/blitzkit-closed) | ClosedKit | [blitzkit/closed](https://github.com/blitzkit/closed) | Closed source, sensitive code hidden from the public to prevent bad actors from abusing the game. Discuss with `@tresabhi` on Discord for **special permission** to access. |
| [submodules/blitzkit-internal](submodules/blitzkit-internal) | InternalKit | [blitzkit/internal](https://github.com/blitzkit/internal) | Closed source, sensitive internal documentation hidden from the public to prevent bad actors from abusing the game. Access to this submodule is **not negotiable**. |
| [submodules/CUE4Parse](submodules/CUE4Parse) | CUE4Parse | [FabianFG/CUE4Parse](https://github.com/FabianFG/CUE4Parse) | An Unreal Engine archives and packages parsing library in C#. |
## Naming Guide
## Environment Variables
| Name | Source | BlitzKit Default | Memo |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `PUBLIC_WARGAMING_APPLICATION_ID` | [Wargaming Developer's Room](https://developers.wargaming.net/) | | Application ID needed to communicate with Wargaming APIs. |
| `PUBLIC_PROMOTE_OPENTEST` | | `false` | Optional boolean dictating whether the website advertises [the opentest subdomain of BlitzKit](https://opentest.blitzkit.app/). |
| `ALLOW_ROBOTS` | | `true` | Toggles between `Allow: /` and `Disallow: /` in the website's `/robots.txt`. This is usually `false` for all BlitzKit's subdomains. |
| `WOTB_GLOSSARY`, `WOTB_GLOSSARY_DOMAIN`, `WOTB_GLOSSARY_PATH` | | | |
| `PUBLIC_GOOGLE_TAG_MANAGER_ID` | [Google Tag Manager](https://tagmanager.google.com/) | `G-GL2JVHCGPQ` | Google Tag Manager's unique identifier for analytical purposes. |
| `PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID` | [Google Analytics](https://analytics.google.com/) | `430711767` | Google Analytics' unique property ID for [BlitzKit's apex domain](https://blitzkit.app/) and subdomains. |
| `GOOGLE_APPLICATION_CREDENTIALS` | [Google Cloud](https://cloud.google.com/) | | Google Cloud's API key, primarily used for ranking popular tanks and finding relevant tank review videos in Tankopedia. |
| `PUBLIC_DISCORD_SERVER_ID` | [BlitzKit Discord Server](https://discord.gg/nDt7AjGJQH) | `734786591205359697` | BlitzKit Discord Server's id. |
| `PUBLIC_ASSET_BASE` | [blitzkit/assets](https://github.com/blitzkit/assets) | `https://blitzkit.github.io/assets` | The root domain and path that serves the external assets like tank models and icons. |
| `PUBLIC_BRANCH` | | `main` | The branch that the website is operating on. This is completely nominal. |
| `PUBLIC_GUIDE_WEBSITE` | [GuidesBlitz](https://guidesblitz.com/) | `https://guidesblitz.com` | The website that serves the encyclopedia-like content in BlitzKit. |
| `SERVICING` | | `false` | Wether the website is down for servicing. |
| `DISCORD_CLIENT_ID` | [Discord Developer Portal](https://discord.com/developers/applications) | `1097673957865443370` | The BlitzKit Discord Bot's user ID. |
| `GH_TOKEN` | [GitHub Fine-Grained Tokens](https://github.com/settings/personal-access-tokens) | | The GitHub API key used for checking out private submodules and pushing content to the asset repository. |
| `DISCORD_TOKEN` | [Discord Developer Portal](https://discord.com/developers/applications) | | The Discord API Key for the BlitzKit Discord Bot. |
| `DATABASE_URL` | [CockroachDB](https://www.cockroachlabs.com/) | | The CockroachDB-SQL database URL used to link Wargaming and Discord users. |
| `WOTB_DLC_CDN` | | | |
| Type | Style | Example |
| --------- | -------------- | ---------------------------------------------------------------- |
| In-text | CamelCase | BlitzKit |
| Code | lowerCamelCase | `let blitzkit = { ... }`, `function fetchBlitzkitData() { ... }` |
| URLs | kebab-case | `https://example.com/blitzkit/` |
| Constants | SNAKE_CASE | `const BLITZKIT_VERSION = "1.0.0"` |
+96
View File
@@ -0,0 +1,96 @@
{
"name": "blitzkit",
"private": true,
"version": "1.27.11",
"description": "🎉 Tools for everything World of Tanks Blitz",
"type": "module",
"types": "./types.d.ts",
"workspaces": ["packages/*", "submodules/*"],
"scripts": {
"build:assets": "cd packages/scripts && bun run build:assets",
"build:averages": "bun --filter=@blitzkit/scripts run build:averages",
"build:bot": "cd packages/scripts && bun run build:bot",
"build:website": "bun build:prisma --no-hints && cd packages/website && bun run build",
"build:prisma": "prisma generate",
"build:sitemap": "bun scripts/sitemap.ts",
"build:docs": "vitepress build docs",
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard",
"dev:bot": "bun run build:bot --dev && bun start:bot",
"start:bot": "bun dist/bot/main.js",
"dev:docs": "vitepress dev docs",
"dev:website": "cd packages/website-ue && bun run dev",
"dev:api": "bun --filter=@blitzkit/api run dev",
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
"setup:patches": "bun --filter=@blitzkit/scripts run setup:patches",
"start:website": "bun --filter=@blitzkit/website run start",
"host:docs": "vitepress preview docs",
"lint": "tsc",
"lint:website": "cd packages/website && bun run lint",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"update": "yarn upgrade --latest",
"tunnel": "cloudflared tunnel --url http://localhost:3000",
"discoverids": "scripts/discoverIds.ts",
"build:protos": "bun packages/scripts/src/protos.ts",
"preview:website": "cd packages/website && bun run preview",
"build:reviews": "cd packages/scripts && bun build:reviews",
"build:ids": "cd packages/scripts && bun build:ids",
"prunestrings": "cd packages/scripts && bun prunestrings"
},
"license": "MIT",
"dependencies": {
"satori": "^0.10.14",
"@resvg/resvg-js": "^2.6.2",
"discord.js": "^14.16.3",
"bepaint": "^0.1.0-beta.3",
"markdown-escape": "^2.0.0",
"@types/markdown-escape": "^1.1.3",
"@types/adm-zip": "^0.5.5",
"@types/lz4js": "^0.2.1",
"@octokit/plugin-throttling": "^8.2.0",
"@octokit/rest": "^20.1.1",
"@prisma/client": "^6.6.0",
"adm-zip": "^0.5.16",
"crc": "^4.3.2",
"dotenv": "^16.4.5",
"lz4js": "^0.2.0",
"yaml": "^2.5.0",
"ts-deepmerge": "^7.0.2",
"@types/decompress": "^4.2.7",
"@types/progress": "^2.0.7",
"@gltf-transform/core": "^4.0.10",
"@gltf-transform/extensions": "^4.0.10",
"@gltf-transform/functions": "^4.0.10",
"csv-parse": "^5.5.6",
"decompress": "^4.2.1",
"esbuild": "^0.19.12",
"fast-xml-parser": "^4.4.1",
"progress": "^2.0.3",
"puppeteer": "^23.10.4",
"@astrojs/check": "^0.9.4",
"@playform/compress": "^0.1.9",
"@playform/inline": "^0.1.2",
"@types/js-cookie": "^3.0.6",
"astro-compressor": "^1.0.0",
"@fontsource/inter": "^5.2.5",
"@nanostores/persistent": "^0.10.2",
"@nanostores/react": "^0.7.3",
"@nivo/bar": "^0.87.0",
"@nivo/line": "^0.88.0",
"compare-versions": "^6.1.1",
"echarts": "^5.5.1",
"js-cookie": "^3.0.5",
"markdown-to-jsx": "^7.5.0",
"nanostores": "^0.11.3",
"react-device-detect": "^2.2.3",
"react-promise-suspense": "^0.3.4",
"urlon": "^3.1.0",
"@types/bun": "latest",
"@types/node": "^22.14.1",
"prisma": "^6.6.0",
"typescript": "^5.8.3",
"googleapis": "^144.0.0",
"protobufjs": "^7.5.2",
"quicklime": "^1.2.0",
"sharp": "^0.33.5"
}
}
+124
View File
@@ -0,0 +1,124 @@
{
"name": "blitzkit",
"private": true,
"version": "1.27.11",
"description": "🎉 Tools for everything World of Tanks Blitz",
"type": "module",
"types": "./types.d.ts",
"workspaces": [
"packages/*",
"submodules/*"
],
"scripts": {
"build:assets": "cd packages/scripts && bun run build:assets",
"build:averages": "bun --filter=@blitzkit/scripts run build:averages",
"build:bot": "cd packages/scripts && bun run build:bot",
"build:website": "bun build:prisma --no-hints && cd packages/website && bun run build",
"build:prisma": "prisma generate",
"build:sitemap": "bun scripts/sitemap.ts",
"build:docs": "vitepress build docs",
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard",
"dev:bot": "bun run build:bot --dev && bun start:bot",
"start:bot": "bun dist/bot/main.js",
"dev:docs": "vitepress dev docs",
"dev:website": "cd packages/website-ue && bun run dev",
"dev:api": "bun --filter=@blitzkit/api run dev",
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
"setup:patches": "bun --filter=@blitzkit/scripts run setup:patches",
"start:website": "bun --filter=@blitzkit/website run start",
"host:docs": "vitepress preview docs",
"lint": "tsc",
"lint:website": "cd packages/website && bun run lint",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"update": "yarn upgrade --latest",
"tunnel": "cloudflared tunnel --url http://localhost:3000",
"discoverids": "scripts/discoverIds.ts",
"build:protos": "bun packages/scripts/src/protos.ts",
"preview:website": "cd packages/website && bun run preview",
"build:reviews": "cd packages/scripts && bun build:reviews",
"build:ids": "cd packages/scripts && bun build:ids",
"prunestrings": "cd packages/scripts && bun prunestrings"
},
"license": "MIT",
"dependencies": {
"satori": "^0.10.14",
"@resvg/resvg-js": "^2.6.2",
"discord.js": "^14.16.3",
"fuzzysort": "^2.0.4",
"bepaint": "^0.1.0-beta.3",
"markdown-escape": "^2.0.0",
"@types/markdown-escape": "^1.1.3",
"@types/adm-zip": "^0.5.5",
"@types/lz4js": "^0.2.1",
"@octokit/plugin-throttling": "^8.2.0",
"@octokit/rest": "^20.1.1",
"@prisma/client": "^6.6.0",
"adm-zip": "^0.5.16",
"crc": "^4.3.2",
"dotenv": "^16.4.5",
"lz4js": "^0.2.0",
"ts-proto": "^2.2.2",
"yaml": "^2.5.0",
"ts-deepmerge": "^7.0.2",
"@types/decompress": "^4.2.7",
"@types/progress": "^2.0.7",
"@gltf-transform/core": "^4.0.10",
"@gltf-transform/extensions": "^4.0.10",
"@gltf-transform/functions": "^4.0.10",
"csv-parse": "^5.5.6",
"decompress": "^4.2.1",
"esbuild": "^0.19.12",
"fast-xml-parser": "^4.4.1",
"progress": "^2.0.3",
"puppeteer": "^23.10.4",
"@astrojs/check": "^0.9.4",
"@astrojs/sitemap": "^3.2.1",
"@playform/compress": "^0.1.9",
"@playform/inline": "^0.1.2",
"@types/js-cookie": "^3.0.6",
"@types/three": "^0.160.0",
"astro-compressor": "^1.0.0",
"@astrojs/partytown": "^2.1.4",
"@astrolib/seo": "^1.0.0-beta.8",
"@fontsource/inter": "^5.2.5",
"@nanostores/persistent": "^0.10.2",
"@nanostores/react": "^0.7.3",
"@nivo/bar": "^0.87.0",
"@nivo/line": "^0.88.0",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/themes": "^3.1.4",
"@react-three/drei": "^9.115.0",
"@react-three/fiber": "^8.17.10",
"compare-versions": "^6.1.1",
"echarts": "^5.5.1",
"immer": "^10.1.1",
"js-cookie": "^3.0.5",
"markdown-to-jsx": "^7.5.0",
"nanostores": "^0.11.3",
"react-device-detect": "^2.2.3",
"react-promise-suspense": "^0.3.4",
"three": "^0.169.0",
"urlon": "^3.1.0",
"zustand": "^4.5.5",
"@types/bun": "latest",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.14.1",
"prettier": "^3.5.3",
"prisma": "^6.6.0",
"typescript": "^5.8.3",
"googleapis": "^144.0.0",
"lodash-es": "^4.17.21",
"protobufjs": "^7.5.2",
"quicklime": "^1.2.0",
"sharp": "^0.33.5",
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@astrojs/react": "^4.3.0",
"@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5",
"astro": "^5.8.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
}
}
-2984
View File
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.
-25
View File
@@ -1,25 +0,0 @@
# BlitzKit 1.27.16
## Features
- Added links to GuidesBlitz to Tankopedia to aid users understand tanks beyond just numbers and stats
- Added a gun depression and yaw visualizer
## Changes
- Made the colors of the advanced highlighting mode more subtle
- Made the Tankopedia stat lines more condensed
- You can now scroll the page even with your cursor on the Tankopedia sandbox
## Fixes
- Fixed a bug where the range of increased gun depression was incorrectly doubled
- Removed an unintended space before the "x" in the "Gun damaged" stat
- Fixed an issue causing the 3D renderer to render frames even when off screen
- Fixed a differential issue causing excessive rerendering of the stat lines
- Images no longer exceed the width of the device in the docs
- Assault shells no longer scale the module damage with distance
## Technical Changes
- Removed all code relating to the canceled "Shooting Range" mode
-18
View File
@@ -1,18 +0,0 @@
# BlitzKit 1.27.17
This update was released early for the website. Today, it'll be officially announced and released for the bot
## Features
- Added the shell reload visualizer to aid people in getting a feel for how long shell reloads really are
- Added lines of stats for the burst shells
## Changes
- Removed the redundant word "Shell" from "Shell reload" and "Shell reloads"
- Removed the penetration over distance slider
## Fixes
- Fixed the DPM of tanks with burst shells like the ATAC
- The DPM will still be different from the game due to a bug in World of Tanks Blitz; I am trying to get Wargaming to fix it; trust BlitzKit!
-11
View File
@@ -1,11 +0,0 @@
# BlitzKit 1.27.18
## Features
- Added a mini-game to BlitzKit similar to "Guess Who's That Pokemon" but with tanks: http://blitzkit.vercel.app/guess
- Switched to a new renderer for better lighting for the Tankopedia
## Fixes
- The loading bar for the Tankopedia sandbox now shows up again
- Removed a lot of dead code, improving performance
-18
View File
@@ -1,18 +0,0 @@
# BlitzKit 1.27.19
Two updates in a day? I just got a little excited by the new Tankopedia lighting.
## Features
- The camera is now lower down in the Tankopedia, giving the tank a more heroic feel
- Tanks are better framed across all devices, minimizing whitespace
- Added a skip button for the reload visualizer to skip to the next event
## Fixes
- Removed the useless "show grid" setting
- Taking screenshots now no longer includes the scene props like the floor
- The subtler color contrast for the advanced highlighting has now been reverted back to the higher contrast
- The shot display card is now properly aligned as a proper table again
- Tank modules are once again considered in the penetration tester
- Tank filters in the antagonist selector in the Tankopedia sandbox are now properly aligned
-13
View File
@@ -1,13 +0,0 @@
# BlitzKit 1.27.20
It's all technical updates! The HD tanks will be rolled out over the span of few days.
## Technical Changes
- Shadows are disabled if your framerate drops below 30fps
- Pages that redirect to other pages (like the old /tools/tankopedia) now load a few hundred times faster
- Replaced Zustand with a custom state manager called Varuna with the goal to rewrite a significant portion of the website to not require any React for rendering increasing performance
- The transition wireframe in the Tankopedia sandbox now uses a lower quality model for performance reasons
- Tanks with PBR now use the correct maximum resolution textures
- This is a significant update as you can now literally see individual splats of dust on tanks and scratches haha
- Baked the ambient occlusion maps into the textures, hence reducing the overall model load times even with higher resolution textures
-16
View File
@@ -1,16 +0,0 @@
# BlitzKit 1.28.0
BlitzKit, as an app and the new Mixer tool!
## Features
- Created the BlitzKit app that can be downloaded on most devices
- Re-did the BlitzKit logo to be a bit warmer
- Added the Mixer tool which lets you combine tank models into Frankensteins
- Updated all dependencies to the latest version for a decent performance boost
## Fixes
- BlitzKit no longer forgets persistent data if the app crashes without properly closing
- The tools list in the navbar no longer overflows into the neighboring elements on certain devices
- Added keying to a few places, speeding up re-rendering
-5
View File
@@ -1,5 +0,0 @@
# BlitzKit 1.28.1
## Fixes
- The lights no longer crash the Mixer tool
-5
View File
@@ -1,5 +0,0 @@
# BlitzKit 1.28.2
## Fixes
- BlitzKit no longer tries to redirect translated pages to the same page, over and over again
-18
View File
@@ -1,18 +0,0 @@
# BlitzKit 1.28.3
## Features
- Turrets and guns can now be rotated in Mixer without any pitch or yaw limits
- Added nation icons to the search results for Guesser
- The table now occupies the whole screen in Compare
- Added the ability to log in with a Wargaming account in the navbar
- Added owned/unowned filters in the Tankopedia filters
- Revamped Tankopedia quick controls to be more accessible and obvious
## Fixes
- Missing tank icons across the website have been fixed
- Events of starting and stopping of the transform tanks now use an asynchronous event instead of a reactive state, making them faster
- Removed a hook call on root context, speeding up load times
- Tankopedia's pitch input now follows the same sign convention as the pitch and yaw visualizer
- Shadows are no longer disabled when the scene is unloaded when the user scrolls away from the canvas
+11 -25
View File
@@ -1,29 +1,27 @@
{
"name": "blitzkit",
"private": true,
"version": "1.28.3",
"version": "1.27.15",
"description": "🎉 Tools for everything World of Tanks Blitz",
"type": "module",
"types": "./types.d.ts",
"workspaces": [
"packages/*"
"packages/*",
"submodules/*"
],
"scripts": {
"build:assets": "cd packages/scripts && bun run build:assets",
"build:averages": "bun --filter=@blitzkit/scripts run build:averages",
"build:bot": "cd packages/scripts && bun run build:bot",
"build:website": "bun build:prisma --no-hints && cd packages/website && bun run build",
"build:website": "cd packages/website-ue && bun run build",
"build:prisma": "prisma generate",
"build:sitemap": "bun scripts/sitemap.ts",
"build:docs": "vitepress build docs",
"build:proxyclient": "cd packages/scripts && bun run build:proxyclient",
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard",
"build:app": "cd packages/app && bun run build",
"dev:bot": "bun run build:bot --dev && bun start:bot",
"start:bot": "bun dist/bot/main.js",
"dev:docs": "vitepress dev docs",
"dev:website": "cd packages/website && bun run dev",
"dev:app": "cd packages/app && bun run dev",
"dev": "bun run dev:website",
"dev:api": "bun --filter=@blitzkit/api run dev",
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
@@ -33,33 +31,21 @@
"lint": "tsc",
"lint:website": "cd packages/website && bun run lint",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"update": "yarn upgrade --latest",
"update": "yarn upgace --latest",
"tunnel": "cloudflared tunnel --url http://localhost:3000",
"discoverids": "scripts/discoverIds.ts",
"build:protos": "bun packages/scripts/src/protos.ts",
"preview:website": "cd packages/website && bun run preview",
"build:reviews": "cd packages/scripts && bun build:reviews",
"build:ids": "cd packages/scripts && bun build:ids",
"prunestrings": "cd packages/scripts && bun prunestrings"
"prunestrings": "cd packages/scripts && bun prunestrings",
"build:ffi": "bun build:windows:ffi",
"build:windows:ffi": "dotnet publish src/BlitzKit.FFI -r win-x64 /p:PublishAot=true",
"build:linux:ffi": "dotnet publish src/BlitzKit.FFI -r linux-x64 /p:PublishAot=true"
},
"license": "MIT",
"dependencies": {
"@types/xmpp__client": "^0.13.3",
"@xmpp/client": "^0.13.6",
"astro-favicons": "^3.1.5",
"cheerio": "^1.1.2",
"googleapis": "^160.0.0",
"lodash-es": "^4.17.21",
"protobufjs": "^7.5.4",
"quicklime": "^1.2.0",
"sharp": "^0.34.4"
},
"devDependencies": {
"@types/bun": "^1.2.22",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.5.2",
"prettier": "^3.6.2",
"prisma": "^6.16.2",
"typescript": "^5.9.2"
"@types/bun": "^1.2.15",
"prettier": "^3.5.3"
}
}
-18
View File
@@ -1,18 +0,0 @@
{
"name": "@blitzkit/app",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/cli": "^2",
"vite": "^6.0.3",
"typescript": "~5.6.2"
}
}
-7
View File
@@ -1,7 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
-5286
View File
File diff suppressed because it is too large. Load diff
-25
View File
@@ -1,25 +0,0 @@
[package]
name = "blitzkit"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "blitzkit_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
-3
View File
@@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}
@@ -1,10 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}
-14
View File
@@ -1,14 +0,0 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
-27
View File
@@ -1,27 +0,0 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::process::{Command, Stdio};
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let app_handle = app.handle();
let entry = app_handle
.path_resolver()
.resolve_resource("../../dist/website/server/entry.mjs")
.expect("failed to resolve server.ts");
Command::new("bun")
.arg("run")
.arg(entry)
.stderr(Stdio::null())
.spawn()
.expect("failed to start Bun server");
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running Tauri app");
}
-38
View File
@@ -1,38 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "blitzkit",
"version": "0.1.0",
"identifier": "com.tresabhi.blitzkit",
"build": {
"beforeDevCommand": "cd ../website && bun run dev",
"_beforeBuildCommand": "cd ../website && bun run build",
"frontendDist": "../../../dist/website/client",
"devUrl": "http://localhost:4321"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"url": "http://localhost:4321",
"title": "BlitzKit",
"fullscreen": false,
"resizable": true,
"maximized": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/[email protected]",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"extends": ["../../tsconfig.json"],
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
-16
View File
@@ -3,24 +3,8 @@
"private": true,
"type": "module",
"types": "src/types.d.ts",
"dependencies": {
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"satori": "^0.18.3",
"@resvg/resvg-js": "^2.6.2",
"discord.js": "^14.22.1",
"react": "^19.1.1",
"@radix-ui/colors": "^3.0.0",
"fuzzysort": "^3.1.0",
"bepaint": "^0.1.0-beta.3",
"dotenv": "^17.2.2",
"markdown-escape": "^2.0.0"
},
"scripts": {
"dev": "bun --watch src/main.ts",
"start": "bun src/main.ts"
},
"devDependencies": {
"@types/markdown-escape": "^1.1.3"
}
}
+144
View File
@@ -0,0 +1,144 @@
import {
BlitzTankClass,
Region,
regionToRegionSubdomain,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { tankDefinitions } from '../core/blitzkit/nonBlockingPromises';
import { chunkLines } from '../core/discord/chunkLines';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
type Auction = null | {
count: number;
has_next: boolean;
results: Result[];
};
type Result =
| {
id?: number;
type: 'vehicle';
countable?: boolean;
entity?: Entity;
initial_count: number;
current_count: number;
saleable?: boolean;
available_from?: Date;
available_before?: Date;
price?: Price;
next_price: Price | null;
available: true;
display?: boolean;
next_price_datetime: Date;
next_price_timestamp: number;
}
| {
available: false;
type: 'vehicle';
};
interface Entity {
id: number;
name: string;
nation: string;
subnation: string;
use_subnation_flag: boolean;
type_slug: BlitzTankClass;
level: number;
roman_level: string;
user_string: string;
image_url: string;
preview_image_url: string;
is_premium: boolean;
is_collectible: boolean;
}
interface Price {
currency: Currency;
value: number;
}
interface Currency {
name: 'gold';
count: number;
title: 'Gold';
image_url: string;
sizes: unknown;
type: 'currency';
}
const WARNING_COUNT = 500;
export const auctionCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand('auction'),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const awaitedTankDefinitions = await tankDefinitions;
const region: Region = 'com';
const pageSize = 80;
const saleable = true;
const data = await fetch(
`https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/events/items/auction/?page_size=${
pageSize
}&type[]=vehicle&saleable=${saleable}`,
).then((response) => response.json() as Promise<Auction>);
if (data === null) {
return strings.bot.commands.auction.errors.no_auction;
}
const saleableTanks = data.results.filter((result) => result.available);
const nextPricesTimestamp = saleableTanks[0].next_price_timestamp;
const title = `# ${strings.bot.commands.auction.body.title}`;
const subtitle = `${literals(strings.bot.commands.auction.body.subtitle, [
`${saleableTanks.length} / ${data.results.length}`,
`<t:${nextPricesTimestamp}:R>`,
'<https://wotblitz.com/auction/>',
])}`;
const body = saleableTanks.map((data, index) => {
const tank = awaitedTankDefinitions.tanks[data.entity!.id];
if (!tank) return '';
const name = `[${tank.name}](<https://blitzkit.app/tanks/${
tank.slug
}>)`;
const next =
data.next_price === null
? ''
: `\n-# ${literals(strings.bot.commands.auction.body.next, [
`<:gold:1317173197082333244> ${data.next_price.value.toLocaleString(
interaction.locale,
)}`,
])}`;
const available = literals(
strings.bot.commands.auction.body.available,
[
data.current_count.toLocaleString(interaction.locale),
data.initial_count.toLocaleString(interaction.locale),
],
);
const isOut = data.current_count === 0;
const outString = isOut ? '~~' : '';
const isLow = data.current_count <= WARNING_COUNT;
return `${index + 1}. ${
outString
}${name} <:gold:1317173197082333244> ${data.price!.value.toLocaleString(
interaction.locale,
)}${outString}\n-#${isLow ? ' ‼️ ' : ' '}${available}${isOut ? '' : next}`;
});
const header = `${title}\n${subtitle}`;
const lines = [header, ...body];
return chunkLines(lines, '\n\n');
},
});
});
+40 -40
View File
@@ -6,23 +6,23 @@ import {
getTankStats,
getWN8Percentile,
TankDefinition,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { chunk } from "lodash-es";
import * as Breakdown from "../../components/Breakdown";
import { CommandWrapper } from "../../components/CommandWrapper";
import { NoData } from "../../components/NoData";
import { TitleBar } from "../../components/TitleBar";
import { filtersToDescription } from "../../core/blitzkit/filtersToDescription";
import { tankDefinitions } from "../../core/blitzkit/nonBlockingPromises";
import { UserError } from "../../core/blitzkit/userError";
import { filterStats, StatFilters } from "../../core/blitzstars/filterStats";
import { getStatsInPeriod } from "../../core/blitzstars/getStatsInPeriod";
import { blitzStarsTankAverages } from "../../core/blitzstars/tankAverages";
import { ResolvedPeriod } from "../../core/discord/resolvePeriodFromCommand";
import { ResolvedPlayer } from "../../core/discord/resolvePlayerFromCommand";
import { translator } from "../../core/localization/translator";
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { chunk } from 'lodash-es';
import * as Breakdown from '../../components/Breakdown';
import { CommandWrapper } from '../../components/CommandWrapper';
import { NoData } from '../../components/NoData';
import { TitleBar } from '../../components/TitleBar';
import { filtersToDescription } from '../../core/blitzkit/filtersToDescription';
import { tankDefinitions } from '../../core/blitzkit/nonBlockingPromises';
import { UserError } from '../../core/blitzkit/userError';
import { filterStats, StatFilters } from '../../core/blitzstars/filterStats';
import { getStatsInPeriod } from '../../core/blitzstars/getStatsInPeriod';
import { blitzStarsTankAverages } from '../../core/blitzstars/tankAverages';
import { ResolvedPeriod } from '../../core/discord/resolvePeriodFromCommand';
import { ResolvedPlayer } from '../../core/discord/resolvePlayerFromCommand';
import { translator } from '../../core/localization/translator';
const ROWS_PER_PAGE = 8;
const MAX_PAGES = 9;
@@ -31,7 +31,7 @@ export async function renderBreakdown(
{ region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod,
filters: StatFilters,
locale: Locale
locale: Locale,
) {
const { strings, unwrap } = translator(locale);
const awaitedTankDefinitions = await tankDefinitions;
@@ -39,7 +39,7 @@ export async function renderBreakdown(
const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale);
const { filteredOrder } = await filterStats(statsInPeriod, filters);
const accountInfo = await getAccountInfo(region, id);
const clanData = await getClanAccountInfo(region, id, ["clan"]);
const clanData = await getClanAccountInfo(region, id, ['clan']);
const tankStats = await getTankStats(region, id);
const filterDescriptions = await filtersToDescription(filters, locale);
const orderedCurrentStats: BlitzStats[] = [];
@@ -74,63 +74,63 @@ export async function renderBreakdown(
orderedCareerWN8Full.push(
tankAveragesEntry
? calculateWN8(tankAveragesEntry.all, tank.all)
: undefined
: undefined,
);
});
const currentBattles = orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.battles,
0
0,
);
const careerBattles = tankStats.reduce(
(accumulator, current) => accumulator + current.all.battles,
0
0,
);
const currentWinrate =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.wins,
0
0,
) / currentBattles;
const careerWinrate =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.wins,
0
0,
) / careerBattles;
const currentWN8 =
orderedCurrentWN8.reduce<number>(
(accumulator, current, index) =>
accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
0
0,
) /
orderedCurrentStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCurrentWN8[index] === undefined ? 0 : current.battles),
0
0,
);
const careerWN8 =
orderedCareerWN8Full.reduce<number>(
(accumulator, current) => accumulator + (current ?? 0),
0
0,
) /
tankStats.reduce(
(accumulator, current, index) =>
accumulator +
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
0
0,
);
const currentDamage =
orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.damage_dealt,
0
0,
) / currentBattles;
const careerDamage =
tankStats.reduce(
(accumulator, current) => accumulator + current.all.damage_dealt,
0
0,
) / careerBattles;
const children: React.JSX.Element[] = [];
const children: JSX.Element[] = [];
if (filteredOrder.length > 0) {
children.push(
@@ -172,7 +172,7 @@ export async function renderBreakdown(
delta: currentDamage - careerDamage,
},
]}
/>
/>,
);
filteredOrder.forEach((id, index) => {
@@ -193,9 +193,9 @@ export async function renderBreakdown(
title={
tankDefinition
? unwrap(tankDefinition.name)
: literals(strings.bot.commands.breakdown.body.unknown_tank, {
id,
})
: literals(strings.bot.commands.breakdown.body.unknown_tank, [
`${id}`,
])
}
stats={[
{
@@ -206,7 +206,7 @@ export async function renderBreakdown(
{
title: strings.bot.commands.breakdown.body.winrate,
current: `${(100 * (current.wins / current.battles)).toFixed(
2
2,
)}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
delta:
@@ -230,24 +230,24 @@ export async function renderBreakdown(
{
title: strings.bot.commands.breakdown.body.damage,
current: Math.round(
current.damage_dealt / current.battles
current.damage_dealt / current.battles,
).toLocaleString(),
career: Math.round(
career.damage_dealt / career.battles
career.damage_dealt / career.battles,
).toLocaleString(),
delta:
current.damage_dealt / current.battles -
career.damage_dealt / career.battles,
},
]}
/>
/>,
);
});
}
const pages = chunk(
children,
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE)
Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE),
);
if (filteredOrder.length > 0) {
+28 -28
View File
@@ -1,44 +1,44 @@
import { getAccountInfo, getClanInfo } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { GenericStats } from "../components/GenericStats";
import { NoData } from "../components/NoData";
import { TitleBar } from "../components/TitleBar";
import { addClanChoices } from "../core/discord/addClanChoices";
import { autocompleteClan } from "../core/discord/autocompleteClan";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolveClanFromCommand } from "../core/discord/resolveClanFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
import { getAccountInfo, getClanInfo } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { CommandWrapper } from '../components/CommandWrapper';
import { GenericStats } from '../components/GenericStats';
import { NoData } from '../components/NoData';
import { TitleBar } from '../components/TitleBar';
import { addClanChoices } from '../core/discord/addClanChoices';
import { autocompleteClan } from '../core/discord/autocompleteClan';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import { resolveClanFromCommand } from '../core/discord/resolveClanFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
const DEFAULT_THRESHOLD = 7;
export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("inactive")
command: createLocalizedCommand('inactive')
.addStringOption(addClanChoices)
.addNumberOption((option) => {
const { strings } = translator(Locale.EnglishUS);
return option
.setName("threshold")
.setName('threshold')
.setNameLocalizations(
localizationObject(
(strings) => strings.bot.commands.inactive.options.threshold.name,
undefined,
true
)
true,
),
)
.setDescription(
strings.bot.commands.inactive.options.threshold.description
strings.bot.commands.inactive.options.threshold.description,
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.inactive.options.threshold.description
)
strings.bot.commands.inactive.options.threshold.description,
),
)
.setMinValue(0);
}),
@@ -47,7 +47,7 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(interaction.locale);
const { region, id } = await resolveClanFromCommand(interaction);
const threshold =
interaction.options.getNumber("threshold")! ?? DEFAULT_THRESHOLD;
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
const time = new Date().getTime() / 1000;
const clanInfo = await getClanInfo(region, id);
const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
@@ -57,7 +57,7 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
[
account.nickname,
(time - account.last_battle_time) / 60 / 60 / 24,
] as [string, number]
] as [string, number],
)
.filter(([, inactiveDays]) => inactiveDays >= threshold)
.sort((a, b) => b[1] - a[1])
@@ -65,10 +65,10 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
([name, days]) =>
[
name,
literals(strings.bot.commands.inactive.body.listing, {
days: days.toFixed(0),
}),
] as [string, string]
literals(strings.bot.commands.inactive.body.listing, [
days.toFixed(0),
]),
] as [string, string],
);
const hasInactiveMembers = inactive.length > 0;
@@ -79,7 +79,7 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
description={`${literals(
strings.bot.commands.inactive.body.subtitle,
{ days: threshold }
[`${threshold}`],
)} • ${new Date().toLocaleDateString(interaction.locale)}`}
/>
+22 -22
View File
@@ -5,31 +5,31 @@ import {
flags,
getAccountInfo,
getTankStats,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import markdownEscape from "markdown-escape";
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import markdownEscape from 'markdown-escape';
import {
gameDefinitions,
tankDefinitions,
} from "../core/blitzkit/nonBlockingPromises";
import { addTierChoices } from "../core/discord/addTierChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { chunkLines } from "../core/discord/chunkLines";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
} from '../core/blitzkit/nonBlockingPromises';
import { addTierChoices } from '../core/discord/addTierChoices';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { chunkLines } from '../core/discord/chunkLines';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("owned-tanks")
command: createLocalizedCommand('owned-tanks')
.addStringOption(addTierChoices)
.addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings, unwrap } = translator(interaction.locale);
const tier = Number(interaction.options.getString("tier"));
const tier = Number(interaction.options.getString('tier'));
const { id, region } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id);
const tankStats = await getTankStats(region, id);
@@ -43,7 +43,7 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
tankStats.map(async (tankData) => ({
tankDefinitions: (await tankDefinitions).tanks[tankData.tank_id]!,
id: tankData.tank_id,
}))
})),
)
).filter((tank) => tank.tankDefinitions?.tier === tier);
const groupedTanks: Record<string, TankDefinition[]> = {};
@@ -60,25 +60,25 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
const awaitedTankDefinitions = await tankDefinitions;
const awaitedGameDefinitions = await gameDefinitions;
const title = literals(strings.bot.commands.owned_tanks.body.title, {
name: markdownEscape(accountInfo.nickname),
tier: TIER_ROMAN_NUMERALS[tier],
});
const title = literals(strings.bot.commands.owned_tanks.body.title, [
markdownEscape(accountInfo.nickname),
TIER_ROMAN_NUMERALS[tier],
]);
const tankList = tankStats
.map(({ tank_id }) => awaitedTankDefinitions.tanks[tank_id])
.filter((tank) => tank.tier === tier)
.sort(
(a, b) =>
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class)
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
)
.sort(
(a, b) =>
awaitedGameDefinitions.nations.indexOf(a.nation) -
awaitedGameDefinitions.nations.indexOf(b.nation)
awaitedGameDefinitions.nations.indexOf(b.nation),
)
.map(
(tank) =>
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name))}](<https://blitzkit.app/tanks/${tank.slug}>)`
`[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name))}](<https://blitzkit.app/tanks/${tank.slug}>)`,
);
const lines = [title, ...tankList];
+13 -13
View File
@@ -1,11 +1,11 @@
import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
import { literals } from '@blitzkit/i18n';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("permissions"),
command: createLocalizedCommand('permissions'),
async handler(interaction) {
const { strings } = translator(interaction.locale);
@@ -14,33 +14,33 @@ export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
new Date().getDay() === 5 && new Date().getDate() === 13;
const permissions = [
[
interaction.appPermissions?.has("ViewChannel"),
interaction.appPermissions?.has('ViewChannel'),
strings.bot.commands.permissions.body.view_channels,
],
[
interaction.appPermissions?.has("ReadMessageHistory"),
interaction.appPermissions?.has('ReadMessageHistory'),
strings.bot.commands.permissions.body.read_message_history,
],
[
interaction.appPermissions?.has("AttachFiles"),
interaction.appPermissions?.has('AttachFiles'),
strings.bot.commands.permissions.body.attach_files,
],
[
interaction.appPermissions?.has("UseExternalEmojis"),
interaction.appPermissions?.has('UseExternalEmojis'),
strings.bot.commands.permissions.body.external_emojis,
],
];
const body = permissions
.map(([hasPermission, description]) => {
if (hasPermission) {
return `${isFridayThe13th ? "😇" : "✅"} ${description}`;
return `${isFridayThe13th ? '😇' : '✅'} ${description}`;
} else {
return `${isFridayThe13th ? "💀" : "❌"} ${description}`;
return `${isFridayThe13th ? '💀' : '❌'} ${description}`;
}
})
.join("\n");
.join('\n');
return literals(strings.bot.commands.permissions.body.title, { body });
return literals(strings.bot.commands.permissions.body.title, [body]);
},
});
});
+12 -12
View File
@@ -1,15 +1,15 @@
import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
import { literals } from '@blitzkit/i18n';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
export const pingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
handlesInteraction: true,
command: createLocalizedCommand("ping", [
{ subcommand: "blitzkit" },
{ subcommand: "wotb" },
command: createLocalizedCommand('ping', [
{ subcommand: 'blitzkit' },
{ subcommand: 'wotb' },
]),
async handler(interaction) {
@@ -17,17 +17,17 @@ export const pingCommand = new Promise<CommandRegistry>((resolve) => {
const subcommand = interaction.options.getSubcommand();
const executionStart = Date.now();
if (subcommand === "blitzkit") {
if (subcommand === 'blitzkit') {
await interaction.editReply(strings.bot.commands.ping.body.pong);
} else {
await fetch("https://api.wotblitz.com/");
await fetch('https://api.wotblitz.com/');
}
const executionTime = Date.now() - executionStart;
interaction.editReply(
literals(strings.bot.commands.ping.body.pong_with_time, {
time: executionTime.toFixed(0),
})
literals(strings.bot.commands.ping.body.pong_with_time, [
`${Math.round(executionTime)}`,
]),
);
},
});
+18 -18
View File
@@ -1,19 +1,19 @@
import { getAccountInfo } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import markdownEscape from "markdown-escape";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { markdownTable } from "../core/discord/markdownTable";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
import { getAccountInfo } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import markdownEscape from 'markdown-escape';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { embedInfo } from '../core/discord/embedInfo';
import { markdownTable } from '../core/discord/markdownTable';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command:
createLocalizedCommand("player-info").addStringOption(addUsernameChoices),
createLocalizedCommand('player-info').addStringOption(addUsernameChoices),
async handler(interaction) {
const { strings } = translator(interaction.locale);
@@ -22,9 +22,9 @@ export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
const accountInfo = await getAccountInfo(region, id);
return embedInfo(
literals(strings.bot.commands.player_info.body.title, {
name: markdownEscape(accountInfo.nickname),
}),
literals(strings.bot.commands.player_info.body.title, [
markdownEscape(accountInfo.nickname),
]),
markdownTable([
[
@@ -51,16 +51,16 @@ export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
[
strings.bot.commands.player_info.body.created,
new Date(accountInfo.created_at * 1000).toLocaleDateString(
interaction.locale
interaction.locale,
),
],
[
strings.bot.commands.player_info.body.last_battle,
new Date(accountInfo.last_battle_time * 1000).toLocaleDateString(
interaction.locale
interaction.locale,
),
],
])
]),
);
},
+118 -118
View File
@@ -8,42 +8,42 @@ import {
getRatingInfo,
getRatingNeighbors,
imgur,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { SlashCommandSubcommandBuilder } from "discord.js";
import markdownEscape from "markdown-escape";
import { Glow } from "../components/AllStatsOverview/components/HeroStat/components/Glow";
import { CommandWrapper } from "../components/CommandWrapper";
import { DeltaCaret } from "../components/DeltaCaret";
import { TitleBar } from "../components/TitleBar";
import { normalizeLeagueIcon } from "../core/blitz/normalizeLeagueIcon";
import { iconPng } from "../core/blitzkit/iconPng";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { SlashCommandSubcommandBuilder } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow';
import { CommandWrapper } from '../components/CommandWrapper';
import { DeltaCaret } from '../components/DeltaCaret';
import { TitleBar } from '../components/TitleBar';
import { normalizeLeagueIcon } from '../core/blitz/normalizeLeagueIcon';
import { iconPng } from '../core/blitzkit/iconPng';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
const LEAGUE_ACCENT = [
"_purple",
"_sand",
"_amber",
"_mauve",
"_brown",
'_purple',
'_sand',
'_amber',
'_mauve',
'_brown',
] as const;
export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("rating", [
command: createLocalizedCommand('rating', [
{
subcommand: "today",
subcommand: 'today',
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
},
},
{
subcommand: "season",
subcommand: 'season',
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
},
@@ -52,11 +52,11 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
async handler(interaction) {
const subcommand = interaction.options.getSubcommand(true) as
| "season"
| "today";
| 'season'
| 'today';
const { strings } = translator(interaction.locale);
const { id, region } = await resolvePlayerFromCommand(interaction);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const ratingInfo = await getRatingInfo(region);
@@ -67,20 +67,20 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const leaderboard = await getArchivedRatingLeaderboard(
region,
ratingInfo.current_season - (subcommand === "today" ? 0 : 1)
ratingInfo.current_season - (subcommand === 'today' ? 0 : 1),
);
if (leaderboard.version!.$case === "v1") {
if (leaderboard.version!.$case === 'v1') {
throw new Error(
"Encountered bkrl minimal format in latest season. Wait till next cron?"
'Encountered bkrl minimal format in latest season. Wait till next cron?',
);
}
const accountInfo = await getAccountInfo(region, id, [
"statistics.rating",
'statistics.rating',
]);
const statsIndex = leaderboard.version!.value.entries.findIndex(
(entry) => entry.id === id
(entry) => entry.id === id,
);
const statsA = leaderboard
? leaderboard.version!.value.entries[statsIndex!]
@@ -90,9 +90,9 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const statsB2Array = (await getRatingNeighbors(region, id, 0)).neighbors;
if (statsB2Array === undefined) {
return literals(strings.bot.commands.rating.errors.no_participation, {
name: markdownEscape(accountInfo.nickname),
});
return literals(strings.bot.commands.rating.errors.no_participation, [
markdownEscape(accountInfo.nickname),
]);
}
const [statsB2] = statsB2Array;
@@ -102,15 +102,15 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
return strings.bot.commands.rating.errors.unrecorded_stats;
}
if (!statsB1) {
return literals(strings.bot.commands.rating.errors.no_participation, {
name: markdownEscape(accountInfo.nickname),
});
return literals(strings.bot.commands.rating.errors.no_participation, [
markdownEscape(accountInfo.nickname),
]);
}
const reward = ratingInfo.rewards.find(
(reward) =>
statsB2.number >= reward.from_position &&
statsB2.number <= reward.to_position
statsB2.number <= reward.to_position,
);
const positionDelta = leaderboard
? statsB2.number - positionA!
@@ -118,11 +118,11 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const delta = deltaBkrlBlitzStats(
statsA,
accountInfo.statistics.rating!,
statsB2
statsB2,
);
const leagueTheme = LEAGUE_ACCENT[league.index];
let rewardImage = reward
? reward.type === "vehicle"
? reward.type === 'vehicle'
? reward.vehicle.preview_image_url
: reward.stuff.image_url
: undefined;
@@ -131,7 +131,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const { ok } = await fetch(rewardImage);
if (!ok) {
rewardImage = imgur("kP88vqr");
rewardImage = imgur('kP88vqr');
}
}
@@ -147,24 +147,24 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
position: "relative",
display: "flex",
overflow: "hidden",
position: 'relative',
display: 'flex',
overflow: 'hidden',
}}
>
<div
style={{
position: "relative",
position: 'relative',
padding: 16,
backgroundColor: theme.colors[`appBackground2${leagueTheme}`],
borderRadius: 4,
border: `1px solid ${
theme.colors[`componentInteractive${leagueTheme}`]
}`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}}
>
{reward === undefined && (
@@ -172,17 +172,17 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
transform: "translateX(-16px) rotate(90deg)",
transform: 'translateX(-16px) rotate(90deg)',
}}
/>
)}
<div
style={{
display: "flex",
display: 'flex',
gap: 8,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
}}
>
<img
@@ -190,24 +190,24 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
style={{
width: 64,
height: 64,
objectFit: "contain",
objectFit: 'contain',
}}
src={normalizeLeagueIcon(
ratingInfo.leagues[league.index].big_icon
ratingInfo.leagues[league.index].big_icon,
)}
/>
<div
style={{
display: "flex",
flexDirection: "column",
display: 'flex',
flexDirection: 'column',
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
}}
>
<span
@@ -232,7 +232,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
{Math.abs(delta.score).toLocaleString(
interaction.locale
interaction.locale,
)}
</span>
</>
@@ -241,9 +241,9 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
}}
>
<span
@@ -267,7 +267,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
{Math.abs(positionDelta).toLocaleString(
interaction.locale
interaction.locale,
)}
</span>
</>
@@ -279,23 +279,23 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
{reward && (
<div
style={{
display: "flex",
display: 'flex',
gap: 8,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
display: 'flex',
flexDirection: 'column',
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
}}
>
<span
@@ -303,13 +303,13 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors[`textHighContrast${leagueTheme}`],
fontSize: 16,
maxWidth: 136,
textAlign: "right",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
textAlign: 'right',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
}}
>
{reward.type === "vehicle"
{reward.type === 'vehicle'
? reward.vehicle.user_string
: reward.stuff.title}
</span>
@@ -317,9 +317,9 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
}}
>
<span
@@ -329,7 +329,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
}}
>
Reward
{reward.type === "stuff" ? ` x${reward.count}` : ""}
{reward.type === 'stuff' ? ` x${reward.count}` : ''}
</span>
</div>
</div>
@@ -338,7 +338,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
style={{
width: 64,
height: 64,
objectFit: "cover",
objectFit: 'cover',
}}
src={await iconPng(rewardImage!)}
/>
@@ -350,7 +350,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
transform: "translateX(16px) rotate(-90deg)",
transform: 'translateX(16px) rotate(-90deg)',
}}
/>
)}
@@ -362,10 +362,10 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
direction="reverse"
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
position: "absolute",
position: 'absolute',
top: -40,
left: "50%",
transform: "translateX(-50%)",
left: '50%',
transform: 'translateX(-50%)',
}}
/>
)}
@@ -373,32 +373,32 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<Glow
color={theme.colors[`solidBackground${leagueTheme}`]}
style={{
position: "absolute",
position: 'absolute',
bottom: -40,
left: "50%",
transform: "translateX(-50%)",
left: '50%',
transform: 'translateX(-50%)',
}}
/>
)}
</div>
{delta.battles > 0 && (
<div style={{ padding: "8px 0", display: "flex" }}>
<div style={{ padding: '8px 0', display: 'flex' }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
flex: 1,
flexDirection: "column",
flexDirection: 'column',
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
fontWeight: 'bold',
}}
>
{delta.battles.toLocaleString(interaction.locale)}
@@ -414,19 +414,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
flex: 1,
flexDirection: "column",
flexDirection: 'column',
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
fontWeight: 'bold',
}}
>
{(100 * (delta.wins / delta.battles)).toFixed(0)}%
@@ -442,23 +442,23 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
flex: 1,
flexDirection: "column",
flexDirection: 'column',
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
fontWeight: 'bold',
}}
>
{Math.round(delta.damage / delta.battles).toLocaleString(
interaction.locale
interaction.locale,
)}
</span>
<span
@@ -472,19 +472,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
flex: 1,
flexDirection: "column",
flexDirection: 'column',
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
fontWeight: 'bold',
}}
>
{(100 * (delta.survived / delta.battles)).toFixed(0)}%
@@ -500,19 +500,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
flex: 1,
flexDirection: "column",
flexDirection: 'column',
}}
>
<span
style={{
fontSize: 24,
color: theme.colors.textHighContrast,
fontWeight: "bold",
fontWeight: 'bold',
}}
>
{(delta.kills / delta.battles).toFixed(2)}
+80 -79
View File
@@ -13,27 +13,27 @@ import {
RatingInfo,
RatingLeaderboard,
Region,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import {
APIApplicationCommandOptionChoice,
Locale,
SlashCommandSubcommandBuilder,
} from "discord.js";
import { range } from "lodash-es";
import markdownEscape from "markdown-escape";
import { CommandWrapper } from "../components/CommandWrapper";
import * as Leaderboard from "../components/Leaderboard";
import { TitleBar } from "../components/TitleBar";
import { UserError } from "../core/blitzkit/userError";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
} from 'discord.js';
import { range } from 'lodash-es';
import markdownEscape from 'markdown-escape';
import { CommandWrapper } from '../components/CommandWrapper';
import * as Leaderboard from '../components/Leaderboard';
import { TitleBar } from '../components/TitleBar';
import { UserError } from '../core/blitzkit/userError';
import { addRegionChoices } from '../core/discord/addRegionChoices';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteUsername } from '../core/discord/autocompleteUsername';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
interface SimplifiedPlayer {
id: number;
@@ -51,7 +51,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
const onGoingSeason = await isOnGoingRatingSeason();
const seasonNumbers = range(
FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1,
);
function addOptions(option: SlashCommandSubcommandBuilder) {
@@ -66,50 +66,51 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
(strings) =>
strings.bot.commands.rating_leaderboard.options.limit.name,
undefined,
true
)
true,
),
)
.setDescription(
literals(
strings.bot.commands.rating_leaderboard.options.limit
.description,
{ count: DEFAULT_LIMIT }
)
[`${DEFAULT_LIMIT}`],
),
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.limit
.description,
{ count: DEFAULT_LIMIT }
)
[`${DEFAULT_LIMIT}`],
),
)
.setRequired(false)
.setMinValue(5)
.setMaxValue(30)
.setMaxValue(30),
)
.addStringOption((option) =>
option
.setName(
strings.bot.commands.rating_leaderboard.options.season.name
strings.bot.commands.rating_leaderboard.options.season.name,
)
.setNameLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.season.name,
undefined,
true
)
true,
),
)
.setDescription(
strings.bot.commands.rating_leaderboard.options.season.description
strings.bot.commands.rating_leaderboard.options.season
.description,
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.season
.description
)
.description,
),
)
.setRequired(false)
.addChoices(
@@ -123,7 +124,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? literals(
strings.bot.commands.rating_leaderboard.options.season
.choices.current,
{ season: number }
[`${number}`],
)
: `${number}`,
value: `${number}`,
@@ -132,27 +133,27 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
(strings) =>
strings.bot.commands.rating_leaderboard.options
.season.choices.current,
{ season: number }
[`${number}`],
)
: undefined,
} satisfies APIApplicationCommandOptionChoice<string>;
})
.reverse()
)
.reverse(),
),
);
}
resolve({
command: createLocalizedCommand("rating-leaderboard", [
command: createLocalizedCommand('rating-leaderboard', [
{
subcommand: "neighbors",
subcommand: 'neighbors',
modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices);
addOptions(option);
},
},
{
subcommand: "league",
subcommand: 'league',
modify(option: SlashCommandSubcommandBuilder) {
option
.addStringOption((option) => {
@@ -160,7 +161,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
return option
.setName(
strings.bot.commands.rating_leaderboard.options.league.name
strings.bot.commands.rating_leaderboard.options.league.name,
)
.setNameLocalizations(
localizationObject(
@@ -168,19 +169,19 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
strings.bot.commands.rating_leaderboard.options.league
.name,
undefined,
true
)
true,
),
)
.setDescription(
strings.bot.commands.rating_leaderboard.options.league
.description
.description,
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.rating_leaderboard.options.league
.description
)
.description,
),
)
.addChoices(
...LEAGUES.map(
@@ -194,10 +195,10 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
(strings) =>
(
strings.common.leagues as Record<string, string>
)[name]
)[name],
),
}) satisfies APIApplicationCommandOptionChoice<string>
)
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true);
})
@@ -212,10 +213,10 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
async handler(interaction) {
const { strings } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand(true) as
| "league"
| "neighbors";
const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString("season");
| 'league'
| 'neighbors';
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString('season');
const season = seasonOption
? parseInt(seasonOption)
: onGoingSeason
@@ -231,11 +232,11 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
let playerId: number | undefined;
let regionRatingInfo: RatingInfo | null;
if (subcommand === "league") {
if (subcommand === 'league') {
const leagueIndex = parseInt(
interaction.options.getString("league")!
interaction.options.getString('league')!,
);
const region = interaction.options.getString("region") as Region;
const region = interaction.options.getString('region') as Region;
regionRatingInfo = await (isCurrentSeason
? getRatingInfo(region)
: null);
@@ -258,8 +259,8 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer
)
}) satisfies SimplifiedPlayer,
),
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
@@ -268,17 +269,17 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? 0
: players.version!.value.entries.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore
player.score < LEAGUES[leagueIndex - 1].minScore,
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.version!.value.entries.slice(
firstPlayerIndex,
lastPlayerIndex + 1
lastPlayerIndex + 1,
);
const clanData = await getClanAccountInfo(
region,
trimmed.map(({ id }) => id),
["clan"]
['clan'],
);
return trimmed.map(
@@ -293,16 +294,16 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
literals(
strings.bot.commands.rating_leaderboard.body
.deleted_player,
{ id: player.id }
[`${player.id}`],
),
}) satisfies SimplifiedPlayer
}) satisfies SimplifiedPlayer,
);
}
},
);
const leagueInfo = LEAGUES[leagueIndex];
archivedLeaderboard = await getArchivedRatingLeaderboard(
region,
season
season,
);
players = result;
@@ -315,12 +316,12 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? (strings.common.leagues as Record<string, string>)[
LEAGUES[leagueInfo.index].name
]
: ""
: ''
} - ${strings.common.regions.short[region]}`;
titleImage = imgur(leagueInfo.icon);
titleDescription = literals(
strings.bot.commands.rating_leaderboard.body.top_players,
{ count: limit }
[`${limit}`],
);
} else {
const { region, id } = await resolvePlayerFromCommand(interaction);
@@ -337,7 +338,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
}
const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const neighbors = isCurrentSeason
? await getRatingNeighbors(region, id, Math.round(limit / 2)).then(
(players) =>
@@ -349,13 +350,13 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer
)
}) satisfies SimplifiedPlayer,
),
)
: await getArchivedRatingLeaderboard(region, season!).then(
async (players) => {
const playerIndex = players.version!.value.entries.findIndex(
(player) => player.id === id
(player) => player.id === id,
);
if (playerIndex === -1) {
@@ -363,20 +364,20 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
literals(
strings.bot.commands.rating_leaderboard.body
.no_participation,
{ player: markdownEscape(accountInfo.nickname), season }
)
[markdownEscape(accountInfo.nickname), `${season}`],
),
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.version!.value.entries.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1
playerIndex + halfRange + 1,
);
const clanData = await getClanAccountInfo(
region,
trimmed.map((player) => player.id),
["clan"]
['clan'],
);
return trimmed.map((player, index) => ({
@@ -388,18 +389,18 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? clanData[index]!.account_name
: `Deleted ${player.id}`,
}));
}
},
);
archivedLeaderboard = await getArchivedRatingLeaderboard(
region,
season
season,
);
players = neighbors;
titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined;
titleName = neighbors ? accountInfo.nickname : "";
titleName = neighbors ? accountInfo.nickname : '';
titleDescription =
strings.bot.commands.rating_leaderboard.body.neighbors;
playersAfter =
@@ -413,7 +414,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
const items = players.map((player) => {
const midnightIndex =
archivedLeaderboard?.version!.value.entries.findIndex(
(item) => item.id === player.id
(item) => item.id === player.id,
);
const midnightScore =
midnightIndex === undefined
@@ -452,7 +453,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
<Leaderboard.Gap
message={literals(
strings.bot.commands.rating_leaderboard.body.more,
{ count: playersAfter.toLocaleString(interaction.locale) }
[`${playersAfter.toLocaleString(interaction.locale)}`],
)}
/>
)}
@@ -464,5 +465,5 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
autocomplete: autocompleteUsername,
});
}
},
);
+144 -138
View File
@@ -8,31 +8,31 @@ import {
TANK_ICONS,
TankType,
TIER_ROMAN_NUMERALS,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { escapeMarkdown, Locale } from "discord.js";
import { CommandWrapper } from "../components/CommandWrapper";
import { TitleBar } from "../components/TitleBar";
import { resolveTankId } from "../core/blitz/resolveTankId";
import { buildTechTreeLine } from "../core/blitzkit/buildTechTreeLine";
import { iconPng } from "../core/blitzkit/iconPng";
import { tankDefinitions } from "../core/blitzkit/nonBlockingPromises";
import { resolveAncestry } from "../core/blitzkit/resolveAncestry";
import { tankIconPng } from "../core/blitzkit/tankIconPng";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
import { theme } from "../stitches.config";
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { escapeMarkdown, Locale } from 'discord.js';
import { CommandWrapper } from '../components/CommandWrapper';
import { TitleBar } from '../components/TitleBar';
import { resolveTankId } from '../core/blitz/resolveTankId';
import { buildTechTreeLine } from '../core/blitzkit/buildTechTreeLine';
import { iconPng } from '../core/blitzkit/iconPng';
import { tankDefinitions } from '../core/blitzkit/nonBlockingPromises';
import { resolveAncestry } from '../core/blitzkit/resolveAncestry';
import { tankIconPng } from '../core/blitzkit/tankIconPng';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { autocompleteTanks } from '../core/discord/autocompleteTanks';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { localizationObject } from '../core/discord/localizationObject';
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
import { theme } from '../stitches.config';
export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand("research")
command: createLocalizedCommand('research')
.addStringOption((option) =>
option
.setName(strings.bot.commands.research.options.target_tank.name)
@@ -41,20 +41,20 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
(strings) =>
strings.bot.commands.research.options.target_tank.name,
undefined,
true
)
true,
),
)
.setDescription(
strings.bot.commands.research.options.target_tank.description
strings.bot.commands.research.options.target_tank.description,
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.target_tank.description
)
strings.bot.commands.research.options.target_tank.description,
),
)
.setAutocomplete(true)
.setRequired(true)
.setRequired(true),
)
.addStringOption((option) =>
option
@@ -64,20 +64,20 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
(strings) =>
strings.bot.commands.research.options.starting_tank.name,
undefined,
true
)
true,
),
)
.setDescription(
strings.bot.commands.research.options.starting_tank.description
strings.bot.commands.research.options.starting_tank.description,
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.research.options.starting_tank.description
)
strings.bot.commands.research.options.starting_tank.description,
),
)
.setAutocomplete(true)
.setRequired(false)
.setRequired(false),
)
.addStringOption(addUsernameChoices),
@@ -85,11 +85,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { strings, unwrap } = translator(interaction.locale);
const awaitedTankDefinitions = await tankDefinitions;
const targetTankId = await resolveTankId(
interaction.options.getString("target-tank", true),
interaction.options.getString('target-tank', true),
interaction.locale,
true
true,
);
const startingTankRaw = interaction.options.getString("starting-tank");
const startingTankRaw = interaction.options.getString('starting-tank');
const targetTank = awaitedTankDefinitions.tanks[targetTankId];
const { id, region } = await resolvePlayerFromCommand(interaction);
let startingTankId: number;
@@ -99,45 +99,46 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
startingTankId = await resolveTankId(
startingTankRaw,
interaction.locale,
true
true,
);
if (targetTankId === startingTankId) {
return literals(
strings.bot.commands.research.errors.start_end_equal,
{ tank: escapeMarkdown(unwrap(targetTank.name)) }
[escapeMarkdown(unwrap(targetTank.name))],
);
}
const startingTank = awaitedTankDefinitions.tanks[startingTankId];
if (startingTank.tier > targetTank.tier) {
return literals(strings.bot.commands.research.errors.unordered, {
tank_1: escapeMarkdown(unwrap(startingTank.name)),
tank_2: escapeMarkdown(unwrap(targetTank.name)),
});
return literals(strings.bot.commands.research.errors.unordered, [
escapeMarkdown(unwrap(startingTank.name)),
escapeMarkdown(unwrap(targetTank.name)),
escapeMarkdown(unwrap(targetTank.name)),
]);
}
if (!targetTankAncestry.includes(startingTankId)) {
return literals(
strings.bot.commands.research.errors.tanks_not_on_line,
{
tank_1: escapeMarkdown(unwrap(targetTank.name)),
tank_2: escapeMarkdown(unwrap(startingTank.name)),
}
[
escapeMarkdown(unwrap(targetTank.name)),
escapeMarkdown(unwrap(startingTank.name)),
],
);
}
} else {
if (targetTank.type !== TankType.RESEARCHABLE) {
return literals(strings.bot.commands.research.errors.non_tech_tree, {
tank: escapeMarkdown(unwrap(targetTank.name)),
});
return literals(strings.bot.commands.research.errors.non_tech_tree, [
escapeMarkdown(unwrap(targetTank.name)),
]);
}
if (targetTank.tier === 1) {
return literals(strings.bot.commands.research.errors.tier_1, {
tank: escapeMarkdown(unwrap(targetTank.name)),
});
return literals(strings.bot.commands.research.errors.tier_1, [
escapeMarkdown(unwrap(targetTank.name)),
]);
}
const tankStats = await getTankStats(region, id);
@@ -149,18 +150,18 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
return literals(
strings.bot.commands.research.errors.already_researched,
{ tank: escapeMarkdown(unwrap(targetTank.name)) }
[escapeMarkdown(unwrap(targetTank.name))],
);
}
const foundAncestor = targetTankAncestry.find(
(id) =>
awaitedTankDefinitions.tanks[id].tier === 1 ||
tankStats.some(({ tank_id }) => tank_id === id)
tankStats.some(({ tank_id }) => tank_id === id),
);
if (foundAncestor === undefined) {
throw new Error("No first owned tech tree found.");
throw new Error('No first owned tech tree found.');
}
startingTankId = foundAncestor;
@@ -168,12 +169,12 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const techTreeLine = await buildTechTreeLine(
startingTankId,
targetTankId
targetTankId,
);
const line = [...techTreeLine, startingTankId].reverse();
const { nickname } = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan;
const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const costs = await Promise.all(
line.map(async (id) => {
@@ -186,20 +187,25 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
tank.turrets.forEach((turret) => {
if (
turret.research_cost !== undefined &&
typeof turret.research_cost.research_cost_type!.value === "number"
typeof turret.research_cost.research_cost_type!.value === 'number'
) {
turretXps.set(
turret.id,
turret.research_cost.research_cost_type!.value
turret.research_cost.research_cost_type!.value,
);
}
turret.guns.forEach((gun) => {
if (
gun.research_cost !== undefined &&
typeof gun.research_cost.research_cost_type!.value === "number"
gun.gun_type!.value.base.research_cost !== undefined &&
typeof gun.gun_type!.value.base.research_cost
.research_cost_type!.value === 'number'
) {
gunXps.set(gun.id, gun.research_cost.research_cost_type!.value);
gunXps.set(
gun.gun_type!.value.base.id,
gun.gun_type!.value.base.research_cost.research_cost_type!
.value,
);
}
});
});
@@ -207,11 +213,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
tank.engines.forEach((engine) => {
if (
engine.research_cost !== undefined &&
typeof engine.research_cost.research_cost_type!.value === "number"
typeof engine.research_cost.research_cost_type!.value === 'number'
) {
engineXps.set(
engine.id,
engine.research_cost.research_cost_type!.value
engine.research_cost.research_cost_type!.value,
);
}
});
@@ -219,11 +225,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
tank.tracks.forEach((track) => {
if (
track.research_cost !== undefined &&
typeof track.research_cost.research_cost_type!.value === "number"
typeof track.research_cost.research_cost_type!.value === 'number'
) {
trackXps.set(
track.id,
track.research_cost.research_cost_type!.value
track.research_cost.research_cost_type!.value,
);
}
});
@@ -248,10 +254,10 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
purchase: tank.price.value,
equipment: equipmentPriceMatrix[tank.tier].reduce(
(a, b) => a + 3 * b,
0
0,
),
};
})
}),
);
const image = (
@@ -266,14 +272,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: "flex",
flexDirection: "column",
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
<div
style={{
display: "flex",
display: 'flex',
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
@@ -284,10 +290,10 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
right: 24,
}}
>
@@ -303,23 +309,23 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
justifyContent: "center",
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: 8,
padding: "16px 0",
padding: '16px 0',
}}
>
<div
style={{
display: "flex",
display: 'flex',
gap: 4,
alignItems: "center",
alignItems: 'center',
}}
>
<img
alt="XP icon"
src={await iconPng(asset("icons/currencies/xp.webp"))}
src={await iconPng(asset('icons/currencies/xp.webp'))}
width={16}
height={16}
/>
@@ -329,7 +335,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast,
}}
>
Experience:{" "}
Experience:{' '}
{costs
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
.toLocaleString(interaction.locale)}
@@ -338,14 +344,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: "flex",
display: 'flex',
gap: 4,
alignItems: "center",
alignItems: 'center',
}}
>
<img
alt="Silver icon"
src={await iconPng(asset("icons/currencies/silver.webp"))}
src={await iconPng(asset('icons/currencies/silver.webp'))}
width={16}
height={16}
/>
@@ -355,7 +361,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast,
}}
>
Purchase:{" "}
Purchase:{' '}
{costs
.reduce((a, b) => a + b.purchase, 0)
.toLocaleString(interaction.locale)}
@@ -364,14 +370,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: "flex",
display: 'flex',
gap: 4,
alignItems: "center",
alignItems: 'center',
}}
>
<img
alt="Silver icon"
src={await iconPng(asset("icons/currencies/silver.webp"))}
src={await iconPng(asset('icons/currencies/silver.webp'))}
width={16}
height={16}
/>
@@ -381,7 +387,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast,
}}
>
Equipment:{" "}
Equipment:{' '}
{costs
.reduce((a, b) => a + b.equipment, 0)
.toLocaleString(interaction.locale)}
@@ -398,7 +404,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
key={id}
style={{
display: "flex",
display: 'flex',
borderRadius: 4,
backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle,
@@ -407,9 +413,9 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
width: 48,
display: "flex",
alignItems: "center",
justifyContent: "center",
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
@@ -425,9 +431,9 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: "flex",
position: "relative",
right: "5%",
display: 'flex',
position: 'relative',
right: '5%',
}}
>
<img
@@ -436,23 +442,23 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
style={{
height: 100,
width: 100,
objectFit: "contain",
position: "absolute",
left: "25%",
objectFit: 'contain',
position: 'absolute',
left: '25%',
top: 0,
}}
/>
<div
style={{
display: "flex",
display: 'flex',
gap: 4,
alignItems: "center",
justifyContent: "center",
position: "absolute",
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
bottom: 8,
left: "50%",
transform: "translateX(-50%)",
left: '50%',
transform: 'translateX(-50%)',
}}
>
<img
@@ -475,23 +481,23 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
justifyContent: "center",
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: 8,
padding: "16px 0",
padding: '16px 0',
}}
>
<div
style={{
display: "flex",
display: 'flex',
gap: 4,
alignItems: "center",
alignItems: 'center',
}}
>
<img
alt="XP icon"
src={await iconPng(asset("icons/currencies/xp.webp"))}
src={await iconPng(asset('icons/currencies/xp.webp'))}
width={16}
height={16}
/>
@@ -503,11 +509,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
>
{literals(
strings.bot.commands.research.body.research,
{
cost: costs[index].research?.toLocaleString(
interaction.locale
),
}
[
`${costs[index].research?.toLocaleString(
interaction.locale,
)}`,
],
)}
</span>
</div>
@@ -515,15 +521,15 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
{costs[index].upgrades > 0 && (
<div
style={{
display: "flex",
display: 'flex',
gap: 4,
alignItems: "center",
alignItems: 'center',
}}
>
<img
alt="Free XP icon"
src={await iconPng(
asset("icons/currencies/free-xp.webp")
asset('icons/currencies/free-xp.webp'),
)}
width={16}
height={16}
@@ -536,7 +542,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
>
{literals(
strings.bot.commands.research.body.upgrades,
{ cost: costs[index].upgrades.toLocaleString() }
[costs[index].upgrades.toLocaleString()],
)}
</span>
</div>
@@ -544,15 +550,15 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div
style={{
display: "flex",
display: 'flex',
gap: 4,
alignItems: "center",
alignItems: 'center',
}}
>
<img
alt="Silver icon"
src={await iconPng(
asset("icons/currencies/silver.webp")
asset('icons/currencies/silver.webp'),
)}
width={16}
height={16}
@@ -565,26 +571,26 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
>
{literals(
strings.bot.commands.research.body.purchase,
{
cost: costs[index].purchase.toLocaleString(
interaction.locale
[
costs[index].purchase.toLocaleString(
interaction.locale,
),
}
],
)}
</span>
</div>
<div
style={{
display: "flex",
display: 'flex',
gap: 4,
alignItems: "center",
alignItems: 'center',
}}
>
<img
alt="Silver icon"
src={await iconPng(
asset("icons/currencies/silver.webp")
asset('icons/currencies/silver.webp'),
)}
width={16}
height={16}
@@ -597,18 +603,18 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
>
{literals(
strings.bot.commands.research.body.equipment,
{
cost: costs[index].equipment.toLocaleString(
interaction.locale
[
costs[index].equipment.toLocaleString(
interaction.locale,
),
}
],
)}
</span>
</div>
</div>
</div>
);
})
}),
)}
</div>
</CommandWrapper>
@@ -620,7 +626,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
},
autocomplete(interaction) {
autocompleteTanks(interaction, true, ["target-tank", "starting-tank"]);
autocompleteTanks(interaction, true, ['target-tank', 'starting-tank']);
},
});
});
+28 -28
View File
@@ -1,23 +1,23 @@
import { ClanList, fetchBlitz, Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import markdownEscape from "markdown-escape";
import { addClanChoices } from "../core/discord/addClanChoices";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
import { ClanList, fetchBlitz, Region } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { addClanChoices } from '../core/discord/addClanChoices';
import { addRegionChoices } from '../core/discord/addRegionChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { embedInfo } from '../core/discord/embedInfo';
import { localizationObject } from '../core/discord/localizationObject';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
const DEFAULT_LIMIT = 25;
export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
resolve({
command: createLocalizedCommand("search-clans")
command: createLocalizedCommand('search-clans')
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addClanChoices(option).setAutocomplete(false)
addClanChoices(option).setAutocomplete(false),
)
.addIntegerOption((option) => {
const { strings } = translator(Locale.EnglishUS);
@@ -28,21 +28,21 @@ export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
localizationObject(
(strings) => strings.bot.commands.search_clans.options.limit.name,
undefined,
true
)
true,
),
)
.setDescription(
literals(
strings.bot.commands.search_clans.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
[`${DEFAULT_LIMIT}`],
),
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_clans.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
[`${DEFAULT_LIMIT}`],
),
)
.setMinValue(1)
.setMaxValue(100);
@@ -50,24 +50,24 @@ export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
async handler(interaction) {
const { strings } = translator(interaction.locale);
const region = interaction.options.getString("region") as Region;
const clan = interaction.options.getString("clan")!;
const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const clanList = await fetchBlitz<ClanList>(region, "clans/list", {
const region = interaction.options.getString('region') as Region;
const clan = interaction.options.getString('clan')!;
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT;
const clanList = await fetchBlitz<ClanList>(region, 'clans/list', {
search: clan,
limit,
});
return embedInfo(
literals(strings.bot.commands.search_clans.body.title, {
search: markdownEscape(clan),
region: strings.common.regions.normal[region],
}),
literals(strings.bot.commands.search_clans.body.title, [
markdownEscape(clan),
strings.common.regions.normal[region],
]),
clanList.length === 0
? strings.bot.commands.search_clans.body.no_results
: `\`\`\`\n${clanList
.map((clan) => `${clan.name} [${clan.tag}]`)
.join("\n")}\n\`\`\``
.join('\n')}\n\`\`\``,
);
},
});
+30 -30
View File
@@ -3,17 +3,17 @@ import {
fetchBlitz,
Region,
usernamePattern,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import markdownEscape from "markdown-escape";
import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from "../core/discord/localizationObject";
import { translator } from "../core/localization/translator";
import { CommandRegistry } from "../events/interactionCreate";
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { addRegionChoices } from '../core/discord/addRegionChoices';
import { addUsernameChoices } from '../core/discord/addUsernameChoices';
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand';
import { embedInfo } from '../core/discord/embedInfo';
import { localizationObject } from '../core/discord/localizationObject';
import { translator } from '../core/localization/translator';
import { CommandRegistry } from '../events/interactionCreate';
const DEFAULT_LIMIT = 25;
@@ -21,10 +21,10 @@ export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS);
resolve({
command: createLocalizedCommand("search-players")
command: createLocalizedCommand('search-players')
.addStringOption(addRegionChoices)
.addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true)
addUsernameChoices(option).setAutocomplete(false).setRequired(true),
)
.addIntegerOption((option) =>
option
@@ -34,49 +34,49 @@ export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
(strings) =>
strings.bot.commands.search_players.options.limit.name,
undefined,
true
)
true,
),
)
.setDescription(
literals(
strings.bot.commands.search_players.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
[`${DEFAULT_LIMIT}`],
),
)
.setDescriptionLocalizations(
localizationObject(
(strings) =>
strings.bot.commands.search_players.options.limit.description,
{ limit: DEFAULT_LIMIT }
)
[`${DEFAULT_LIMIT}`],
),
)
.setMinValue(1)
.setMaxValue(100)
.setMaxValue(100),
),
async handler(interaction) {
const { strings } = translator(interaction.locale);
const region = interaction.options.getString("region") as Region;
const name = interaction.options.getString("username")!;
const limit = interaction.options.getInteger("limit") ?? 25;
const region = interaction.options.getString('region') as Region;
const name = interaction.options.getString('username')!;
const limit = interaction.options.getInteger('limit') ?? 25;
const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch)
? await fetchBlitz<AccountList>(region, "account/list", {
? await fetchBlitz<AccountList>(region, 'account/list', {
search: trimmedSearch,
limit,
})
: [];
return embedInfo(
literals(strings.bot.commands.search_players.body.title, {
search: markdownEscape(trimmedSearch),
region: strings.common.regions.normal[region],
}),
literals(strings.bot.commands.search_players.body.title, [
markdownEscape(trimmedSearch),
strings.common.regions.normal[region],
]),
`\`\`\`${
players.length === 0
? strings.bot.commands.search_players.body.no_results
: players.map((player) => player.nickname).join("\n")
}\`\`\``
: players.map((player) => player.nickname).join('\n')
}\`\`\``,
);
},
});
+16 -18
View File
@@ -1,33 +1,33 @@
import { SEARCH_KEYS } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { go } from "fuzzysort";
import markdownEscape from "markdown-escape";
import { tankDefinitions, tankNames } from "../blitzkit/nonBlockingPromises";
import { UserError } from "../blitzkit/userError";
import { tankNamesTechTreeOnly } from "../discord/autocompleteTanks";
import { translator } from "../localization/translator";
import { SEARCH_KEYS } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { go } from 'fuzzysort';
import markdownEscape from 'markdown-escape';
import { tankDefinitions, tankNames } from '../blitzkit/nonBlockingPromises';
import { UserError } from '../blitzkit/userError';
import { tankNamesTechTreeOnly } from '../discord/autocompleteTanks';
import { translator } from '../localization/translator';
export async function resolveTankId(
tank: string | number,
locale: Locale,
techTreeOnly = false
techTreeOnly = false,
) {
const { strings } = translator(locale);
const number = typeof tank === "string" ? Number(tank) : tank;
const number = typeof tank === 'string' ? Number(tank) : tank;
if (Number.isNaN(number)) {
const searchResult = go(
`${tank}`,
await (techTreeOnly ? tankNamesTechTreeOnly : tankNames),
{ keys: SEARCH_KEYS, limit: 1 }
{ keys: SEARCH_KEYS, limit: 1 },
);
if (searchResult.length === 0) {
throw new UserError(
literals(strings.bot.common.errors.tank_name_not_found, {
search: markdownEscape(`${tank}`),
})
literals(strings.bot.common.errors.tank_name_not_found, [
markdownEscape(`${tank}`),
]),
);
} else {
return searchResult[0].obj.id;
@@ -39,9 +39,7 @@ export async function resolveTankId(
return number;
} else {
throw new UserError(
literals(strings.bot.common.errors.tank_id_not_found, {
search: number,
})
literals(strings.bot.common.errors.tank_id_not_found, [`${number}`]),
);
}
}
@@ -1,12 +1,12 @@
import { fetchTankDefinitions, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { StatFilters } from "../blitzstars/filterStats";
import { translator } from "../localization/translator";
import { fetchTankDefinitions, TIER_ROMAN_NUMERALS } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { StatFilters } from '../blitzstars/filterStats';
import { translator } from '../localization/translator';
export async function filtersToDescription(
{ nation, tier, type, class: tankClass, tank }: StatFilters,
locale: Locale
locale: Locale,
) {
const { strings, bkLocale } = translator(locale);
const awaitedTankDefinitions = await fetchTankDefinitions();
@@ -18,13 +18,11 @@ export async function filtersToDescription(
info.push((strings.common.nations as Record<string, string>)[nation]);
if (tier) {
info.push(
literals(strings.bot.common.filters.tier, {
tier: TIER_ROMAN_NUMERALS[tier],
})
literals(strings.bot.common.filters.tier, [TIER_ROMAN_NUMERALS[tier]]),
);
}
if (tankClass) info.push(strings.common.tank_class_short[tankClass]);
if (type) info.push(strings.common.tree_type[type]);
return info.length === 0 ? strings.bot.common.filters.none : info.join(", ");
return info.length === 0 ? strings.bot.common.filters.none : info.join(', ');
}
+3 -3
View File
@@ -1,7 +1,7 @@
import { jsxToSvg } from "./jsxToSvg";
import { svgToPng } from "./svgToPng";
import { jsxToSvg } from './jsxToSvg';
import { svgToPng } from './svgToPng';
export async function jsxToPng(jsx: React.JSX.Element) {
export async function jsxToPng(jsx: JSX.Element) {
const svg = await jsxToSvg(jsx);
const png = svgToPng(svg);
@@ -1,10 +1,10 @@
import { jsxToSvg } from "./jsxToSvg";
import { RenderConfiguration } from "./renderConfiguration";
import { svgToPngThreaded } from "./svgToPngThreaded";
import { jsxToSvg } from './jsxToSvg';
import { RenderConfiguration } from './renderConfiguration';
import { svgToPngThreaded } from './svgToPngThreaded';
export async function jsxToPngThreaded(
jsx: React.JSX.Element,
renderConfiguration: RenderConfiguration
jsx: JSX.Element,
renderConfiguration: RenderConfiguration,
) {
const svg = await jsxToSvg(jsx, renderConfiguration);
const png = await svgToPngThreaded(svg);
@@ -1,9 +1,9 @@
import { readFile } from "fs/promises";
import satori from "satori";
import { RenderConfiguration } from "../renderConfiguration";
import robotoBlack from "./Roboto-Black.ttf";
import robotoBold from "./Roboto-Bold.ttf";
import roboto from "./Roboto.ttf";
import { readFile } from 'fs/promises';
import satori from 'satori';
import { RenderConfiguration } from '../renderConfiguration';
import robotoBlack from './Roboto-Black.ttf';
import robotoBold from './Roboto-Bold.ttf';
import roboto from './Roboto.ttf';
const fontsPromise = [
{ file: roboto, weight: 400 as const },
@@ -11,12 +11,12 @@ const fontsPromise = [
{ file: robotoBlack, weight: 900 as const },
].map(async ({ file, weight }) => {
const data = await readFile(`${__dirname}/${file}`);
return { name: "Roboto", weight, data };
return { name: 'Roboto', weight, data };
});
export async function jsxToSvg(
jsx: React.JSX.Element,
renderConfiguration = new RenderConfiguration()
jsx: JSX.Element,
renderConfiguration = new RenderConfiguration(),
) {
const fonts = await Promise.all(fontsPromise);
return await satori(jsx, { width: renderConfiguration.width, fonts });
+17 -17
View File
@@ -1,13 +1,13 @@
import { numberToRomanNumeral } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { numberToRomanNumeral } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import {
APIApplicationCommandOptionChoice,
Locale,
SlashCommandStringOption,
} from "discord.js";
import { range } from "lodash-es";
import { translator } from "../localization/translator";
import { localizationObject } from "./localizationObject";
} from 'discord.js';
import { range } from 'lodash-es';
import { translator } from '../localization/translator';
import { localizationObject } from './localizationObject';
export function addTierChoices(option: SlashCommandStringOption) {
const { strings } = translator(Locale.EnglishUS);
@@ -18,30 +18,30 @@ export function addTierChoices(option: SlashCommandStringOption) {
localizationObject(
(strings) => strings.bot.common.options.tier.name,
undefined,
true
)
true,
),
)
.setDescription(strings.bot.common.options.tier.description)
.setDescriptionLocalizations(
localizationObject(
(strings) => strings.bot.common.options.tier.description
)
(strings) => strings.bot.common.options.tier.description,
),
)
.setChoices(
...range(10, 0).map(
(tier) =>
({
name: literals(strings.bot.common.options.tier.choices.tier, {
tier_1: tier,
tier_2: numberToRomanNumeral(tier),
}),
name: literals(strings.bot.common.options.tier.choices.tier, [
`${tier}`,
numberToRomanNumeral(tier),
]),
value: `${tier}`,
name_localizations: localizationObject(
(strings) => strings.bot.common.options.tier.choices.tier,
{ tier_1: tier, tier_2: numberToRomanNumeral(tier) }
[`${tier}`, numberToRomanNumeral(tier)],
),
}) satisfies APIApplicationCommandOptionChoice
)
}) satisfies APIApplicationCommandOptionChoice,
),
)
.setRequired(true);
}
@@ -1,11 +1,11 @@
import { literals } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { translator } from "../localization/translator";
import { PeriodSize } from "./addPeriodSubCommands";
import { literals } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { translator } from '../localization/translator';
import { PeriodSize } from './addPeriodSubCommands';
export function getPeriodOptionName(period: PeriodSize, locale: Locale) {
const { strings } = translator(locale);
if (period === "career") return strings.bot.common.periods.career;
if (period === "today") return strings.bot.common.periods.today;
return literals(strings.bot.common.periods.days, { days: period });
if (period === 'career') return strings.bot.common.periods.career;
if (period === 'today') return strings.bot.common.periods.today;
return literals(strings.bot.common.periods.days, [period]);
}
@@ -1,15 +1,15 @@
import { BlitzKitStrings, literals as literalsFunc } from "@blitzkit/i18n";
import { Locale } from "discord.js";
import { SUPPORTED_LOCALES_DISCORD } from "../localization/strings/constants";
import { translator } from "../localization/translator";
import { BlitzKitStrings, literals as literalsFunc } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
import { SUPPORTED_LOCALES_DISCORD } from '../localization/strings/constants';
import { translator } from '../localization/translator';
const validNameRegex =
/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u;
export function localizationObject(
path: (strings: BlitzKitStrings) => string,
literals?: Record<string, string | number>,
strict = false
literals: string[] = [],
strict = false,
) {
return SUPPORTED_LOCALES_DISCORD.reduce<Partial<Record<Locale, string>>>(
(localizations, locale) => {
@@ -20,7 +20,7 @@ export function localizationObject(
if (!isValid) {
console.warn(
`Invalid localization for ${locale} (${path}): ${translation}; skipping...`
`Invalid localization for ${locale} (${path}): ${translation}; skipping...`,
);
return localizations;
}
@@ -30,6 +30,6 @@ export function localizationObject(
[locale]: translation,
};
},
{}
{},
);
}
@@ -6,14 +6,14 @@ import {
InteractionEditReplyOptions,
InteractionReplyOptions,
MessageEditOptions,
} from "discord.js";
import { InteractionReturnable } from "../../events/interactionCreate";
import { Writeable } from "../../types/writable";
import { jsxToPngThreaded } from "../blitzkit/jsxToPngThreaded";
import { RenderConfiguration } from "../blitzkit/renderConfiguration";
} from 'discord.js';
import { InteractionReturnable } from '../../events/interactionCreate';
import { Writeable } from '../../types/writable';
import { jsxToPngThreaded } from '../blitzkit/jsxToPngThreaded';
import { RenderConfiguration } from '../blitzkit/renderConfiguration';
export async function normalizeInteractionReturnable(
returnable: InteractionReturnable
returnable: InteractionReturnable,
) {
const images: [number, Buffer][] = [];
const reply: InteractionEditReplyOptions &
@@ -27,7 +27,7 @@ export async function normalizeInteractionReturnable(
await Promise.all(
normalizedReturnable.map(async (item, index) => {
if (typeof item === "string") {
if (typeof item === 'string') {
reply.content = item;
} else if (item instanceof EmbedBuilder) {
if (!reply.embeds) reply.embeds = [];
@@ -36,7 +36,7 @@ export async function normalizeInteractionReturnable(
if (!reply.components)
reply.components = [new ActionRowBuilder<ButtonBuilder>()];
(reply.components[0] as ActionRowBuilder<ButtonBuilder>).addComponents(
item
item,
);
} else if (item instanceof AttachmentBuilder) {
if (!reply.files) reply.files = [];
@@ -49,14 +49,14 @@ export async function normalizeInteractionReturnable(
const image = await jsxToPngThreaded(item, renderConfiguration);
images.push([index, image]);
}
})
}),
);
if (images.length > 0) {
images.sort(([a], [b]) => b - a);
if (!reply.files) reply.files = [];
images.forEach(([, image]) =>
(reply.files as Writeable<typeof reply.files>)?.push(image)
(reply.files as Writeable<typeof reply.files>)?.push(image),
);
}
@@ -1,35 +1,35 @@
import { Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { ChatInputCommandInteraction } from "discord.js";
import markdownEscape from "markdown-escape";
import { searchClansAcrossRegionsBotWrapper } from "../blitz/searchClansAcrossRegionsBotWrapper";
import { UserError } from "../blitzkit/userError";
import { translator } from "../localization/translator";
import { Region } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { ChatInputCommandInteraction } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { searchClansAcrossRegionsBotWrapper } from '../blitz/searchClansAcrossRegionsBotWrapper';
import { UserError } from '../blitzkit/userError';
import { translator } from '../localization/translator';
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
export async function resolveClanFromCommand(
interaction: ChatInputCommandInteraction
interaction: ChatInputCommandInteraction,
) {
const { strings } = translator(interaction.locale);
const clan = interaction.options.getString("clan", true);
const clan = interaction.options.getString('clan', true);
if (serverAndIdPattern.test(clan)) {
const [server, accountId] = clan.split("/");
const [server, accountId] = clan.split('/');
return { region: server as Region, id: Number(accountId) };
} else {
const accounts = await searchClansAcrossRegionsBotWrapper(
clan,
interaction.locale
interaction.locale,
);
if (accounts[0]) {
return { region: accounts[0].region, id: accounts[0].clan_id };
} else {
throw new UserError(
literals(strings.bot.common.errors.clan_not_found, {
search: markdownEscape(clan),
})
literals(strings.bot.common.errors.clan_not_found, [
markdownEscape(clan),
]),
);
}
}
@@ -1,11 +1,11 @@
import { getTimeDaysAgo, Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { CacheType, ChatInputCommandInteraction } from "discord.js";
import { getPeriodNow } from "../blitzkit/getPeriodNow";
import { getPeriodStart } from "../blitzkit/getPeriodStart";
import { translator } from "../localization/translator";
import { PeriodType } from "./addPeriodSubCommands";
import { getPeriodOptionName } from "./getPeriodOptionName";
import { getTimeDaysAgo, Region } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { getPeriodNow } from '../blitzkit/getPeriodNow';
import { getPeriodStart } from '../blitzkit/getPeriodStart';
import { translator } from '../localization/translator';
import { PeriodType } from './addPeriodSubCommands';
import { getPeriodOptionName } from './getPeriodOptionName';
export interface ResolvedPeriod {
name: string;
@@ -16,7 +16,7 @@ export interface ResolvedPeriod {
export function resolvePeriodFromCommand(
region: Region,
interaction: ChatInputCommandInteraction<CacheType>,
forcedPeriod?: PeriodType
forcedPeriod?: PeriodType,
) {
const { strings } = translator(interaction.locale);
let name: string;
@@ -26,16 +26,16 @@ export function resolvePeriodFromCommand(
? forcedPeriod
: (interaction.options.getSubcommand() as PeriodType);
if (periodSubcommand === "custom") {
const startOption = interaction.options.getInteger("start", true);
const endOption = interaction.options.getInteger("end", true);
if (periodSubcommand === 'custom') {
const startOption = interaction.options.getInteger('start', true);
const endOption = interaction.options.getInteger('end', true);
const startDaysAgoMin = Math.min(startOption, endOption);
const endDaysAgoMax = Math.max(startOption, endOption);
name = literals(strings.bot.common.periods.custom, {
day_1: startDaysAgoMin,
day_2: endDaysAgoMax,
});
name = literals(strings.bot.common.periods.custom, [
`${startDaysAgoMin}`,
`${endDaysAgoMax}`,
]);
start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(region, startDaysAgoMin);
} else {
@@ -1,11 +1,11 @@
import { idToRegion, Region } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { CacheType, ChatInputCommandInteraction } from "discord.js";
import markdownEscape from "markdown-escape";
import { searchPlayersAcrossRegionsBotWrapper } from "../../blitz/searchPlayersAcrossRegionsBotWrapper";
import { getBlitzFromDiscord } from "../../blitzkit/getBlitzFromDiscord";
import { UserError } from "../../blitzkit/userError";
import { translator } from "../../localization/translator";
import { idToRegion, Region } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { searchPlayersAcrossRegionsBotWrapper } from '../../blitz/searchPlayersAcrossRegionsBotWrapper';
import { getBlitzFromDiscord } from '../../blitzkit/getBlitzFromDiscord';
import { UserError } from '../../blitzkit/userError';
import { translator } from '../../localization/translator';
export interface ResolvedPlayer {
region: Region;
@@ -13,10 +13,10 @@ export interface ResolvedPlayer {
}
export async function resolvePlayerFromCommand(
interaction: ChatInputCommandInteraction<CacheType>
interaction: ChatInputCommandInteraction<CacheType>,
) {
const { strings } = translator(interaction.locale);
const commandUsername = interaction.options.getString("username");
const commandUsername = interaction.options.getString('username');
if (commandUsername !== null) {
const id = parseInt(commandUsername);
@@ -27,7 +27,7 @@ export async function resolvePlayerFromCommand(
} else {
const accounts = await searchPlayersAcrossRegionsBotWrapper(
commandUsername,
interaction.locale
interaction.locale,
);
if (accounts[0]) {
@@ -37,9 +37,9 @@ export async function resolvePlayerFromCommand(
} satisfies ResolvedPlayer;
} else {
throw new UserError(
literals(strings.bot.common.errors.player_not_found, {
search: markdownEscape(commandUsername),
})
literals(strings.bot.common.errors.player_not_found, [
markdownEscape(commandUsername),
]),
);
}
}
@@ -1,7 +1,7 @@
import locales from "@blitzkit/i18n/locales.json";
import { Locale } from "discord.js";
import { DEFAULT_LOCALE, SupportedLocale } from '@blitzkit/i18n';
import { Locale } from 'discord.js';
const SUPPORTED_LOCALES_DISCORD_MAP: Record<string, Locale> = {
const SUPPORTED_LOCALES_DISCORD_MAP: Record<SupportedLocale, Locale> = {
en: Locale.EnglishUS,
es: Locale.SpanishES,
pt: Locale.PortugueseBR,
@@ -14,12 +14,12 @@ const SUPPORTED_LOCALES_DISCORD_MAP: Record<string, Locale> = {
};
export const SUPPORTED_LOCALES_DISCORD_MAP_INVERSE = Object.fromEntries(
Object.entries(SUPPORTED_LOCALES_DISCORD_MAP).map(([k, v]) => [v, k])
) as Record<Locale, string>;
Object.entries(SUPPORTED_LOCALES_DISCORD_MAP).map(([k, v]) => [v, k]),
) as Record<Locale, SupportedLocale>;
export const SUPPORTED_LOCALES_DISCORD = Object.values(
SUPPORTED_LOCALES_DISCORD_MAP
SUPPORTED_LOCALES_DISCORD_MAP,
);
export const DEFAULT_LOCALE_DISCORD =
SUPPORTED_LOCALES_DISCORD_MAP[locales.default];
SUPPORTED_LOCALES_DISCORD_MAP[DEFAULT_LOCALE];
+14 -12
View File
@@ -1,8 +1,8 @@
import { ReadStream } from "@blitzkit/core";
import AdmZip from "adm-zip";
import { readFile } from "fs/promises";
import { Parser } from "pickleparser";
import protobuf from "protobufjs";
import { ReadStream } from '@blitzkit/core';
import AdmZip from 'adm-zip';
import { readFile, writeFile } from 'fs/promises';
import { Parser } from 'pickleparser';
import protobuf from 'protobufjs';
interface WotbReplayMetaJson {
version: string;
@@ -24,24 +24,26 @@ export class WotbReplayReadStream extends ReadStream {
async wotbReplay() {
const pickleParser = new Parser();
const zip = new AdmZip(Buffer.from(this.buffer));
const metaJsonBuffer = zip.getEntry("meta.json")?.getData();
const battleResultsDat = zip.getEntry("battle_results.dat")?.getData();
const metaJsonBuffer = zip.getEntry('meta.json')?.getData();
const battleResultsDat = zip.getEntry('battle_results.dat')?.getData();
if (!metaJsonBuffer || !battleResultsDat)
throw new SyntaxError("meta.json not found");
throw new SyntaxError('meta.json not found');
const metaJson = JSON.parse(
metaJsonBuffer.toString()
metaJsonBuffer.toString(),
) as WotbReplayMetaJson;
const unpickledBattleResults = pickleParser.parse(battleResultsDat);
const [, battleResultsString] = unpickledBattleResults as [number, string];
const root = await protobuf.load(
`${__dirname}/../../protos/BattleResultsClient.proto`
`${__dirname}/../../protos/BattleResultsClient.proto`,
);
const BattleResults = root.lookupType(
"BattleResultsGenerated.BattleResultsClient"
'BattleResultsGenerated.BattleResultsClient',
);
writeFile('test.dat', Buffer.from(battleResultsString));
const reader = protobuf.Reader.create(Buffer.from(battleResultsString));
// const decodedBattleResults = BattleResults.decodeDelimited(
@@ -51,7 +53,7 @@ export class WotbReplayReadStream extends ReadStream {
}
}
const replay = await readFile("temp/test3.wotbreplay");
const replay = await readFile('temp/test3.wotbreplay');
const stream = new WotbReplayReadStream(replay.buffer as ArrayBuffer);
stream.wotbReplay();
@@ -1,4 +1,4 @@
import { assertSecret } from "@blitzkit/core";
import { assertSecret } from '@blitzkit/core';
import {
AttachmentBuilder,
AutocompleteInteraction,
@@ -13,30 +13,30 @@ import {
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
SlashCommandSubcommandsOnlyBuilder,
} from "discord.js";
import { aboutCommand } from "../../commands/about";
import { breakdownCommand } from "../../commands/breakdown";
import { debugCommand } from "../../commands/debug";
import { evolutionCommand } from "../../commands/evolution";
import { fullStatsCommand } from "../../commands/fullStats";
import { inactiveCommand } from "../../commands/inactive";
import { verifyCommand } from "../../commands/link";
import { ownedTanksCommand } from "../../commands/ownedTanks";
import { permissionsCommand } from "../../commands/permissions";
import { pingCommand } from "../../commands/ping";
import { playerInfoCommand } from "../../commands/playerInfo";
import { ratingCommand } from "../../commands/rating";
import { ratingLeaderboardCommand } from "../../commands/ratingLeaderboard";
import { replayCommand } from "../../commands/replay";
import { researchCommand } from "../../commands/research";
import { searchClansCommand } from "../../commands/searchClans";
import { searchPlayersCommand } from "../../commands/searchPlayers";
import { statsCommand } from "../../commands/stats";
import { todayCommand } from "../../commands/today";
import { RenderConfiguration } from "../../core/blitzkit/renderConfiguration";
import { handleAutocomplete } from "./handlers/autocomplete";
import { handleButton } from "./handlers/button";
import { handleChatInputCommand } from "./handlers/chatInputCommand";
} from 'discord.js';
import { aboutCommand } from '../../commands/about';
import { breakdownCommand } from '../../commands/breakdown';
import { debugCommand } from '../../commands/debug';
import { evolutionCommand } from '../../commands/evolution';
import { fullStatsCommand } from '../../commands/fullStats';
import { inactiveCommand } from '../../commands/inactive';
import { verifyCommand } from '../../commands/link';
import { ownedTanksCommand } from '../../commands/ownedTanks';
import { permissionsCommand } from '../../commands/permissions';
import { pingCommand } from '../../commands/ping';
import { playerInfoCommand } from '../../commands/playerInfo';
import { ratingCommand } from '../../commands/rating';
import { ratingLeaderboardCommand } from '../../commands/ratingLeaderboard';
import { replayCommand } from '../../commands/replay';
import { researchCommand } from '../../commands/research';
import { searchClansCommand } from '../../commands/searchClans';
import { searchPlayersCommand } from '../../commands/searchPlayers';
import { statsCommand } from '../../commands/stats';
import { todayCommand } from '../../commands/today';
import { RenderConfiguration } from '../../core/blitzkit/renderConfiguration';
import { handleAutocomplete } from './handlers/autocomplete';
import { handleButton } from './handlers/button';
import { handleChatInputCommand } from './handlers/chatInputCommand';
export type InteractionRawReturnable =
| string
@@ -44,7 +44,7 @@ export type InteractionRawReturnable =
| ButtonBuilder
| AttachmentBuilder
| RenderConfiguration
| React.JSX.Element
| JSX.Element
| null;
export type InteractionIterableReturnable =
| InteractionRawReturnable
@@ -58,7 +58,7 @@ interface CommandRegistryBase {
| SlashCommandBuilder
| SlashCommandSubcommandsOnlyBuilder
| SlashCommandOptionsOnlyBuilder
| Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">;
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
@@ -72,7 +72,7 @@ interface CommandRegistryImplicit extends CommandRegistryBase {
interface CommandRegistryExplicit extends CommandRegistryBase {
handlesInteraction?: false;
handler: (
interaction: ChatInputCommandInteraction<CacheType>
interaction: ChatInputCommandInteraction<CacheType>,
) => InteractionReturnable;
}
@@ -104,16 +104,16 @@ export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
return rawCommands.reduce<Record<string, CommandRegistry>>(
(commands, registry, index) => {
if (registry.status === "rejected") {
if (registry.status === 'rejected') {
console.warn(
`Command ${index} failed to load; skipping...`,
registry.reason
registry.reason,
);
return commands;
}
return { ...commands, [registry.value.command.name]: registry.value };
},
{}
{},
);
});
@@ -121,12 +121,12 @@ const rest = new REST().setToken(assertSecret(import.meta.env.DISCORD_TOKEN));
commands.then((awaitedCommands) => {
const body = Object.values(awaitedCommands).map((registry) =>
registry.command.toJSON()
registry.command.toJSON(),
);
rest.put(
Routes.applicationCommands(assertSecret(import.meta.env.DISCORD_CLIENT_ID)),
{ body }
{ body },
);
});
+8 -1
View File
@@ -1,4 +1,11 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*", "../core/src/blitzkit/supportedLocales.ts", "../i18n/src/unwrapper.ts"]
"include": [
"src/**/*",
"../core/src/blitzkit/supportedLocales.ts",
"../i18n/src/unwrapper.ts"
],
"compilerOptions": {
"baseUrl": "../../"
}
}
+4 -15
View File
@@ -4,20 +4,9 @@
"type": "module",
"main": "src/index.ts",
"dependencies": {
"@octokit/plugin-throttling": "^11.0.1",
"@octokit/rest": "^22.0.0",
"@prisma/client": "^6.16.2",
"adm-zip": "^0.5.16",
"crc": "^4.3.2",
"dotenv": "^17.2.2",
"lz4js": "^0.2.0",
"pickleparser": "^0.2.1",
"protobufjs": "^7.5.4",
"ts-proto": "^2.7.7",
"yaml": "^2.8.1"
},
"devDependencies": {
"@types/adm-zip": "^0.5.7",
"@types/lz4js": "^0.2.1"
"@types/js-yaml": "^4.0.9",
"js-yaml": "^4.1.0",
"ts-proto": "^2.2.2",
"@blitzkit/closed": "workspace:*"
}
}
@@ -0,0 +1,99 @@
import { CatalogItem } from '@protos/blitz_items';
import { Any } from '@protos/google/protobuf/any';
import { lowerFirst } from 'lodash-es';
import { compareUint8Arrays } from '../../../scripts/src/core/github/compareUint8Arrays';
import { MessageFns } from '../protos';
export class CatalogItemAccessor {
public id: string;
components: Record<string, Any> = {};
count: number;
constructor(private item: CatalogItem) {
this.id = item.catalog_id;
this.count = item.components.length;
for (const component of item.components) {
if (
component.value === undefined ||
component.value.component === undefined
) {
continue;
}
this.components[component.key] = component.value.component;
}
}
undiscriminatedId() {
return this.id.split('.').slice(1).join('.');
}
static fromComponent(id: string, component: Any) {
return this.fromComponents(id, [component]);
}
static fromComponents(id: string, components: Any[]) {
return new CatalogItemAccessor({
catalog_id: id,
components: components.map((component) => ({
key: lowerFirst(
component.type_url.slice(2).split('.').slice(1).join('.'),
),
value: { type_url: component.type_url, component },
})),
});
}
addComponent(key: string, value: Any) {
this.components[key] = value;
return this;
}
getOptional<Type>(Message: MessageFns<Type>, component: string) {
if (component in this.components) {
return Message.decode(this.components[component].value);
}
}
get<Type>(Message: MessageFns<Type>, component: string) {
const value = this.getOptional(Message, component);
if (value === undefined) throw new Error(`Unknown component: ${component}`);
return value;
}
pack() {
const item: CatalogItem = { catalog_id: this.id, components: [] };
for (const componentId in this.components) {
const component = this.components[componentId];
item.components.push({
key: componentId,
value: { type_url: component.type_url, component },
});
}
return item;
}
encode() {
return CatalogItem.encode(this.pack()).finish();
}
equals(other: CatalogItemAccessor) {
return (
this.count === other.count &&
this.item.components.every(
(component) =>
component.value === undefined ||
(component.key in other.components &&
(component.value.component === undefined ||
compareUint8Arrays(
other.components[component.key].value,
component.value.component.value,
))),
)
);
}
}
+1
View File
@@ -18,6 +18,7 @@ export * from './getTankStats';
export * from './idToRegion';
export * from './isOnGoingRatingSeason';
export * from './isValidBlitzId';
export * from './metadata';
export * from './nations';
export * from './normalizeExtra';
export * from './normalizeIds';
@@ -0,0 +1,89 @@
/*
export class SocketMetadataAccessor {
items: Record<string, CatalogItemAccessor> = {};
constructor(catalogItems: CatalogItem[]) {
for (const item of catalogItems) {
if (item.catalog_id in this.items) {
throw new Error(`Duplicate catalog ID: ${item.catalog_id}`);
}
this.items[item.catalog_id] = new CatalogItemAccessor(item);
}
}
get(item: string) {
if (item in this.items) return this.items[item];
throw new Error(`Unknown catalog ID: ${item}`);
}
add(item: CatalogItemAccessor) {
this.items[item.id] = item;
return this;
}
getAllTanks() {
return this.get('CatalogItemsGroupEntity.all_tanks').get(
CatalogItemsComponent,
'catalogItemsGroupComponent',
);
}
injectBlitzkit() {
this.injectBlitzKitTankComponents();
this.injectBlitzKitAllTanksEntity();
return this;
}
injectBlitzKitTankComponents() {
for (const item of this.getAllTanks().catalog_items) {
const tank = this.get(item);
const value = BlitzKitTankCatalogComponent.encode({
id: 'TEST',
set: 'TEST',
}).finish();
tank.addComponent('blitzkitTankCatalogComponent', {
type_url:
'./blitzkit_static_tank_component.BlitzKitTankCatalogComponent',
value,
});
}
}
injectBlitzKitAllTanksEntity() {
const allTanksEntities: BlitzkitAllTanksComponent = { tanks: [] };
for (const item of this.getAllTanks().catalog_items) {
allTanksEntities.tanks.push(this.get(item).pack());
}
this.add(
CatalogItemAccessor.fromComponent(
'BlitzKitAllTanksEntity.blitzkit_all_tanks',
{
type_url:
'./blitzkit_static_all_tanks_component.BlitzkitAllTanksComponent',
value: BlitzkitAllTanksComponent.encode(allTanksEntities).finish(),
},
),
);
}
}
export class RemoteMetadataAccessor {
constructor(private base: string) {}
async get(item: string) {
const response = await fetch(`${this.base}/${item}.pb`);
if (!response.ok) throw new Error(`Unknown catalog ID: ${item}`);
const buffer = await response.arrayBuffer();
const catalogItem = CatalogItem.decode(new Uint8Array(buffer));
return new CatalogItemAccessor(catalogItem);
}
}
*/
+131
View File
@@ -0,0 +1,131 @@
import { CatalogItem } from '@protos/blitz_items';
import { Metadata } from '@protos/blitz_metadata';
import { LocalizationResourcesComponent } from '@protos/blitz_static_localization_resources_component';
import { RemoteStorageComponent } from '@protos/blitz_static_remote_storage_component';
import { load } from 'js-yaml';
import { minimatch } from 'minimatch';
import { CatalogItemAccessor } from 'packages/core/src/blitz/catalogItemAccessor';
interface LocalizationConfig {
namespaces: string[];
}
export class MetadataAccessor {
items: Record<string, CatalogItem> = {};
constructor(metadata: Metadata) {
for (const item of metadata.catalog_items) {
this.items[item.catalog_id] = item;
}
}
get(item: string) {
if (item in this.items) return new CatalogItemAccessor(this.items[item]);
throw new Error(`Item ${item} does not exist in socket metadata`);
}
add(item: CatalogItemAccessor) {
this.items[item.id] = item.pack();
}
filter(predicate: (item: string) => boolean) {
const items: CatalogItemAccessor[] = [];
for (const item in this.items) {
if (predicate(item)) {
items.push(new CatalogItemAccessor(this.items[item]));
}
}
return items;
}
async strings(locale: string) {
const clientConfig = await this.get(
'ClientConfigsEntity.default_client_config',
);
const localizationResources = clientConfig.get(
LocalizationResourcesComponent,
'localizationResourcesComponent',
);
if (!localizationResources.remote_storage) {
throw new Error('Localization resources not found');
}
const { relative_path } = localizationResources.remote_storage;
let remoteStorage: RemoteStorageComponent | undefined = undefined;
for (const candidateId of localizationResources.remote_storage.production) {
const item = await this.get(candidateId);
const candidate = item.get(
RemoteStorageComponent,
'remoteStorageComponent',
);
if (
remoteStorage === undefined ||
candidate.download_speed_weight > remoteStorage.download_speed_weight
) {
remoteStorage = candidate;
}
}
if (remoteStorage === undefined) {
throw new Error('No suitable production remote storage found');
}
const config = await fetch(
`${remoteStorage.url}/${relative_path}/config.yaml`,
)
.then((response) => response.text())
.then((text) => load(text) as LocalizationConfig);
const strings: Record<string, string> = {};
for (const namespace of config.namespaces) {
Object.assign(
strings,
await fetch(
`${remoteStorage.url}/${relative_path}/${namespace}/${locale}.yaml`,
)
.then((response) => response.text())
.then((text) => {
const strings: Record<string, string> = {};
const parsed = load(text) as Record<string, string>;
for (const key in parsed) {
strings[key] = parsed[key].replaceAll('\\"', '"');
}
return strings;
}),
);
}
return strings;
}
async stringsPartial(locale: string, patterns: string[]) {
const strings = await this.strings(locale);
const filtered: Record<string, string> = {};
for (const key in strings) {
if (patterns.some((pattern) => minimatch(key, pattern))) {
filtered[key] = strings[key];
}
}
return filtered;
}
toArray() {
return Object.values(this.items);
}
toSortedArray() {
return this.toArray().sort((a, b) =>
a.catalog_id.localeCompare(b.catalog_id),
);
}
}
@@ -0,0 +1,8 @@
import { BlitzKitAvatars } from '@protos/blitzkit';
import { MetadataAccessor } from '../../blitz/metadata';
export abstract class BlitzkitAPI {
abstract metadata: MetadataAccessor;
abstract avatars(): Promise<BlitzKitAvatars>;
}
+15
View File
@@ -0,0 +1,15 @@
import { BlitzKitAvatars } from '@protos/blitzkit';
import { MetadataAccessor } from '../../blitz/metadata';
import { BlitzkitAPI } from './abstract';
export class BlitzkitClientAPI extends BlitzkitAPI {
get metadata(): MetadataAccessor {
throw new Error('Cannot access metadata from client');
}
async avatars() {
const response = await fetch('/api/internal/avatars.pb');
const buffer = await response.arrayBuffer();
return BlitzKitAvatars.decode(new Uint8Array(buffer));
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from './abstract';
export * from './client';
export * from './server';
+37
View File
@@ -0,0 +1,37 @@
import { ProfileAvatarComponent } from '@protos/blitz_static_profile_avatar_component';
import { SellableComponent } from '@protos/blitz_static_sellable_component';
import { StuffUIComponent } from '@protos/blitz_static_stuff_ui_component';
import { BlitzKitAvatars } from '@protos/blitzkit';
import { BlitzkitAPI } from '.';
import { MetadataAccessor } from '../../blitz/metadata';
export class BlitzkitServerAPI extends BlitzkitAPI {
constructor(public metadata: MetadataAccessor) {
super();
}
async avatars() {
const avatars: BlitzKitAvatars = { avatars: [] };
for (const item of this.metadata.filter((item) =>
item.startsWith('ProfileAvatarEntity.'),
)) {
const stuff = item.get(StuffUIComponent, 'UIComponent');
const avatar = item.get(ProfileAvatarComponent, 'profileAvatarComponent');
const sellable = item.getOptional(SellableComponent, 'sellableComponent');
avatars.avatars.push({
id: item.undiscriminatedId(),
name: stuff.display_name,
obtaining: stuff.obtaining_methods,
description: stuff.description,
grade: stuff.grade,
category: avatar.category,
hidden_if_not_obtained: avatar.hidden_if_not_obtained,
sale: sellable?.reward,
});
}
return avatars;
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
export function assertSecret<Type>(secret?: Type) {
if (secret === undefined) throw new Error("Missing secret");
export function assertSecret(secret?: string) {
if (secret === undefined) throw new Error('Missing secret');
return secret;
}
+1
View File
@@ -41,6 +41,7 @@ export * from './uniqueGuns';
export * from './unwrapBlitzkitResponse';
export * from './votes';
// export * from './withCORSProxy';
export * from './api';
export * from './createDefaultProvisions';
export * from './gallery';
export * from './metaSortTank';
+26 -38
View File
@@ -1,6 +1,6 @@
import {
GunDefinition,
GunDefinitionAutoReloader,
GunDefinitionAutoReloaderProperties,
ShellDefinition,
} from '../protos';
@@ -15,46 +15,34 @@ export function resolveDpm(
let dps: number;
if (gun.gun_type!.$case === 'regular') {
dps = alpha / (reloadCoefficient * gun.gun_type!.value.reload);
dps = alpha / (reloadCoefficient * gun.gun_type!.value.extension.reload);
} else if (gun.gun_type!.$case === 'auto_loader') {
const damage = alpha * gun.gun_type!.value.shell_count;
let time = gun.gun_type!.value.clip_reload * reloadCoefficient;
if (gun.burst) {
time +=
(gun.gun_type!.value.shell_count / gun.burst.count - 1) *
gun.gun_type!.value.intra_clip *
intraClipCoefficient;
time +=
(gun.gun_type!.value.shell_count / gun.burst.count) *
(gun.burst.count - 1) *
gun.burst.interval;
} else {
time +=
(gun.gun_type!.value.shell_count - 1) *
gun.gun_type!.value.intra_clip *
intraClipCoefficient;
}
dps = damage / time;
dps =
(alpha * gun.gun_type!.value.extension.shell_count) /
(gun.gun_type!.value.extension.clip_reload * reloadCoefficient +
(gun.gun_type!.value.extension.shell_count - 1) *
gun.gun_type!.value.extension.intra_clip *
intraClipCoefficient);
} else {
const mostOptimalShell = gun.gun_type!.value.shell_reloads.reduce<null | {
index: number;
reload: number;
}>((current, reloadRaw, index) => {
const reload =
reloadRaw * reloadCoefficient +
(index > 0
? (gun.gun_type!.value as GunDefinitionAutoReloader).intra_clip *
intraClipCoefficient
: 0);
const mostOptimalShell =
gun.gun_type!.value.extension.shell_reloads.reduce<null | {
index: number;
reload: number;
}>((current, reloadRaw, index) => {
const reload =
reloadRaw * reloadCoefficient +
(index > 0
? (
gun.gun_type!.value
.extension as GunDefinitionAutoReloaderProperties
).intra_clip * intraClipCoefficient
: 0);
if (current === null || reload < current.reload) {
return { index, reload };
}
return current;
}, null)!;
if (current === null || reload < current.reload) {
return { index, reload };
}
return current;
}, null)!;
dps = alpha / mostOptimalShell?.reload;
}
+5 -2
View File
@@ -1,7 +1,10 @@
import { asset } from "@blitzkit/core";
import { asset } from '@blitzkit/core';
export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 };
export function tankIcon(id: number, size: "big" | "small" = "big") {
export function tankIcon(
id: number,
size: 'big' | 'small' | 'blitzkit' = 'blitzkit',
) {
return asset(`icons/tanks/${size}/${id}.webp`);
}
+10 -10
View File
@@ -1,8 +1,8 @@
import locales from "@blitzkit/i18n/locales.json";
import { deburr } from "lodash-es";
import { I18nString } from "../protos";
import { fetchCamouflageDefinitions } from "./camouflageDefinitions";
import { fetchTankDefinitions } from "./tankDefinitions";
import locales from '@blitzkit/i18n/locales.json' with { type: 'json' };
import { deburr } from 'lodash-es';
import { I18nString } from '../protos';
import { fetchCamouflageDefinitions } from './camouflageDefinitions';
import { fetchTankDefinitions } from './tankDefinitions';
export async function fetchTankNames() {
const [tankDefinitions, camouflageDefinitions] = await Promise.all([
@@ -28,16 +28,16 @@ export async function fetchTankNames() {
?.map((id) =>
locales.supported.map(
({ locale }) =>
camouflageDefinitions.camouflages[id]?.name.locales[locale]
)
camouflageDefinitions.camouflages[id]?.name.locales[locale],
),
)
.flat()
.filter(Boolean)
.map(deburr)
.join(" "),
.join(' '),
treeType: tank.type,
};
})
}),
);
}
@@ -48,5 +48,5 @@ export const SEARCH_KEYS = [
`searchableNameDeburr.locales.${locale}`,
])
.flat(),
"camouflages",
'camouflages',
];
+2 -2
View File
@@ -7,8 +7,8 @@ export function uniqueGuns(turrets: TurretDefinition[]) {
forEachRight(turrets, (turret) => {
forEachRight(turret.guns, (gun) => {
if (!gunIds.has(gun.id)) {
gunIds.add(gun.id);
if (!gunIds.has(gun.gun_type!.value.base.id)) {
gunIds.add(gun.gun_type!.value.base.id);
guns.push({ turret, gun });
}
});
+33 -22
View File
@@ -108,47 +108,58 @@ message GunDefinition {
GunDefinitionAutoLoader auto_loader = 2;
GunDefinitionAutoReloader auto_reloader = 3;
}
required uint32 id = 4;
required float rotation_speed = 5;
optional ResearchCost research_cost = 6;
required uint32 weight = 7;
required I18nString name = 8;
required uint32 tier = 9;
repeated ShellDefinition shells = 10;
required float camouflage_loss = 11;
required float aim_time = 12;
required float dispersion_base = 13;
required float dispersion_traverse = 14;
required float dispersion_shot = 15;
required float dispersion_damaged = 16;
repeated Unlock unlocks = 17;
required uint32 shell_capacity = 18;
optional AssaultRanges assault_ranges = 19;
optional Burst burst = 20;
}
message GunDefinitionRegular {
required GunDefinitionBase base = 1;
required GunDefinitionRegularProperties extension = 2;
}
message GunDefinitionRegularProperties {
required float reload = 1;
}
message GunDefinitionAutoLoader {
required GunDefinitionBase base = 1;
required GunDefinitionAutoLoaderProperties extension = 2;
}
message GunDefinitionAutoLoaderProperties {
required float clip_reload = 1;
required float intra_clip = 2;
required float shell_count = 3;
}
message Burst {
required uint32 count = 1;
required float interval = 2;
message GunDefinitionAutoReloader {
required GunDefinitionBase base = 1;
required GunDefinitionAutoReloaderProperties extension = 2;
}
message GunDefinitionAutoReloader {
message GunDefinitionAutoReloaderProperties {
repeated float shell_reloads = 1;
required float intra_clip = 2;
required float shell_count = 3;
}
message GunDefinitionBase {
required uint32 id = 1;
required float rotation_speed = 2;
optional ResearchCost research_cost = 3;
required uint32 weight = 4;
required I18nString name = 5;
required uint32 tier = 6;
repeated ShellDefinition shells = 7;
required float camouflage_loss = 8;
required float aim_time = 9;
required float dispersion_base = 10;
required float dispersion_traverse = 11;
required float dispersion_shot = 12;
required float dispersion_damaged = 13;
repeated Unlock unlocks = 14;
required uint32 shell_capacity = 15;
optional AssaultRanges assault_ranges = 16;
}
message AssaultRanges {
repeated AssaultRange ranges = 1;
repeated ShellType types = 2;
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*", "../i18n/src/strings.ts"]
"include": ["src/**/*"],
"compilerOptions": {
"baseUrl": "../../"
}
}
+1 -5
View File
@@ -2,9 +2,5 @@
"name": "@blitzkit/i18n",
"private": true,
"type": "module",
"main": "src/index.ts",
"dependencies": {
"ts-deepmerge": "^7.0.3"
},
"devDependencies": {}
"main": "src/index.ts"
}
+3 -4
View File
@@ -1,4 +1,3 @@
export * from "./literals";
export * from "./strings";
export * from "./unwrapper";
export * from "./wrapper";
export * from './literals';
export * from './strings';
export * from './unwrapper';
+4 -19
View File
@@ -1,20 +1,5 @@
import { assertSecret } from "@blitzkit/core";
const dev = assertSecret(import.meta.env.DEV);
export function literals(
string: string,
literals?: Record<string, string | number | undefined>
) {
for (const key in literals) {
const mutated = string.replaceAll(`{${key}}`, `${literals[key]}`);
if (dev && mutated === string) {
console.warn(`Could not find literal {${key}} in string "${string}"`);
}
string = mutated;
}
return string;
export function literals(string: string, literals: string[]) {
return literals.reduce((accumulator, literal, index) => {
return accumulator.replace(`%s${index + 1}`, literal);
}, string);
}
+21 -62
View File
@@ -1,68 +1,27 @@
import { merge } from "ts-deepmerge";
import locales from "../locales.json";
import en from "../strings/en.json";
import es from "../strings/es.json";
import fr from "../strings/fr.json";
import ja from "../strings/ja.json";
import pl from "../strings/pl.json";
import pt from "../strings/pt.json";
import ru from "../strings/ru.json";
import uk from "../strings/uk.json";
import zh from "../strings/zh.json";
import type en from '../strings/en.json';
export type BlitzKitStrings = typeof en;
const stringsRaw: Record<string, TranslationTree> = {
en,
es,
fr,
ja,
pl,
pt,
ru,
uk,
zh,
};
export const STRINGS: Record<string, BlitzKitStrings> = {};
for (const locale in stringsRaw) {
if (locale === locales.default) {
STRINGS[locales.default] = stringsRaw[locales.default] as BlitzKitStrings;
continue;
}
const localeStrings = stringsRaw[locale];
STRINGS[locale] = merge(
stringsRaw[locales.default],
localeStrings
) as BlitzKitStrings;
}
export const SUPPORTED_LOCALE_BLITZ_MAP: Record<string, string> = {
en: "en",
es: "es",
fr: "fr",
ja: "ja",
pt: "pt",
ru: "ru",
uk: "uk",
zh: "zh-Hans",
pl: "pl",
};
export const SUPPORTED_LOCALE_FLAGS: Record<string, string> = {
en: "🇺🇸",
es: "🇪🇸",
fr: "🇫🇷",
ja: "🇯🇵",
pt: "🇵🇹",
ru: "🇷🇺",
uk: "🇺🇦",
zh: "🇨🇳",
pl: "🇵🇱",
en: '🇺🇸',
es: '🇪🇸',
fr: '🇫🇷',
ja: '🇯🇵',
pt: '🇵🇹',
ru: '🇷🇺',
uk: '🇺🇦',
zh: '🇨🇳',
pl: '🇵🇱',
};
export type TranslationTree = { [key: string]: TranslationNode };
export type TranslationNode = string | TranslationTree;
export const LOCALE_NAMES: Record<string, string> = {
en: 'English',
es: 'Español',
fr: 'Français',
ja: '日本語',
pt: 'Português',
ru: 'Русский',
uk: 'Українська',
zh: '中文',
pl: 'Polski',
};
+3 -3
View File
@@ -1,7 +1,7 @@
import { I18nString } from "@blitzkit/core";
import locales from "@blitzkit/i18n/locales.json";
import { I18nString } from '@blitzkit/core';
import locales from '../locales.json' with { type: 'json' };
export function unwrapper(locale = locales.default) {
export function unwrapper(locale: string) {
return function (i18nString: I18nString) {
if (locale in i18nString.locales) {
return i18nString.locales[locale];
-15
View File
@@ -1,15 +0,0 @@
import locales from "../locales.json";
import { BlitzKitStrings, STRINGS } from "./strings";
export function wrapper(slicer: (strings: BlitzKitStrings) => string) {
const defaultString = slicer(STRINGS[locales.default] as BlitzKitStrings);
const object = { [locales.default]: defaultString };
for (const { locale } of locales.supported) {
const string = slicer(STRINGS[locale] as BlitzKitStrings);
if (string !== defaultString) object[locale] = string;
}
return object;
}
+146 -121
View File
@@ -1,21 +1,28 @@
{
"test": {
"default": "test",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is %s1 because it has the color of %s2. I like it so much that I will say its name 3 times! %s1! %s1! %s1! Once again, it's %s2, not %s3.",
"fox": "The quick brown fox jumps over the lazy dog."
},
"common": {
"name": "BlitzKit",
"grades": {
"0": "Unspecified grade",
"1": "Common",
"2": "Rare",
"3": "Epic",
"4": "Legendary"
},
"units": {
"m": "{value}m",
"mm": "{value}mm",
"deg": "{value}°",
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"m": "%s1m",
"mm": "%s1mm",
"deg": "%s1°",
"hp": "%s1hp",
"x": "x%s1",
"percentage": "%s1%",
"deg_s": "%s1°/s",
"times": "x%s1"
},
"gun_types": {
@@ -206,11 +213,11 @@
"bot": {
"common": {
"errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
"clan_not_found": "# Could not find clan\nCouldn't find \"%s1\". Try selecting a clan from the search result.",
"player_not_found": "# Could not find user\nI couldn't find user \"%s1\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
"player_not_linked": "# Use `/link` to get started\nI don't know who you are but you can tell me today!",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"%s1\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"%s1\". Try typing the name of the tank instead of a number.",
"no_tank_stats": "# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.",
"get_help": "Help Discord server",
"uncaught_error": "# BlitzKit ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.",
@@ -315,7 +322,7 @@
"description": "Tier",
"choices": {
"tier": "Tier {tier_1} - {tier_2}"
"tier": "Tier %s1 - %s2"
}
}
},
@@ -323,13 +330,13 @@
"periods": {
"career": "Career",
"today": "Today",
"days": "{days} days",
"custom": "{day_1} - {day_2} days"
"days": "%s1 days",
"custom": "%s1 - %s2 days"
},
"filters": {
"none": "No filters",
"tier": "Tier {tier}"
"tier": "Tier %s1"
},
"no_data": {
@@ -356,6 +363,24 @@
},
"commands": {
"auction": {
"name": "auction",
"description": "Tanks available right now in the auction",
"body": {
"title": "Blitz Auction",
"subtitle": "%s1 available • next prices %s2\nBid now: %s3",
"next": "Next price: %s1",
"available": "Available: %s1 / %s2"
},
"errors": {
"no_auction": "# No ongoing auction\n\nCurrently, there are no tanks available for purchase in the auction. If you think this is an error, please report this discrepancy in [the official Discord Server](https://discord.gg/nDt7AjGJQH)."
},
"actions": {
"bid": "Bid now"
}
},
"replay": {
"name": "replay",
"description": "Display replay results"
@@ -379,20 +404,20 @@
"body": {
"subtitle": "Research",
"total": "Total",
"research": "Research: {cost}",
"upgrades": "Upgrades: {cost}",
"purchase": "Purchase: {cost}",
"equipment": "Equipment: {cost}",
"research": "Research: %s1",
"upgrades": "Upgrades: %s1",
"purchase": "Purchase: %s1",
"equipment": "Equipment: %s1",
"estimation": "# This is an estimation!\n\nThis list may include already researched tanks because of the inaccurate information Wargaming provides. Try providing a `starting-tank` from the line when using the command if you can."
},
"errors": {
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
"unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank."
"non_tech_tree": "# %s1 is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# %s1 is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# %s1 is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
"start_end_equal": "# %s1 was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# %s1 and %s2 are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
"unordered": "# %s1 higher tiered to %s2\n\nMake sure to provide %s3 as the `starting-tank` instead as it is the lower tiered tank."
}
},
@@ -438,7 +463,7 @@
"wn8": "WN8",
"winrate": "Winrate",
"damage": "Damage",
"unknown_tank": "Unknown tank {id}"
"unknown_tank": "Unknown tank %s1"
}
},
@@ -481,13 +506,13 @@
"options": {
"threshold": {
"name": "threshold",
"description": "The number of days inactive (default: {days} days)"
"description": "The number of days inactive (default: %s1 days)"
}
},
"body": {
"subtitle": "Inactive for {days}+ days",
"listing": "{days} days"
"subtitle": "Inactive for %s1+ days",
"listing": "%s1 days"
}
},
@@ -534,7 +559,7 @@
"body": {
"accounts_linked": "Accounts linked",
"no_role_permissions": {
"title": "{name} failed to verify",
"title": "%s1 failed to verify",
"description": "I don't have the permission to manage roles."
}
},
@@ -552,12 +577,12 @@
"options": {
"limit": {
"name": "limit",
"description": "The max number of clan results (default: {limit})"
"description": "The max number of clan results (default: %s1)"
}
},
"body": {
"title": "Clan search for {search} in region {region}",
"title": "Clan search for %s1 in region %s2",
"no_results": "No clans found"
}
},
@@ -569,12 +594,12 @@
"options": {
"limit": {
"name": "limit",
"description": "The max number of player results (default: {limit})"
"description": "The max number of player results (default: %s1)"
}
},
"body": {
"title": "Player search for {search} in region {region}",
"title": "Player search for %s1 in region %s2",
"no_results": "No players found"
}
},
@@ -598,7 +623,7 @@
"options": {
"limit": {
"name": "limit",
"description": "How many neighbors to display (default: {count})"
"description": "How many neighbors to display (default: %s1)"
},
"season": {
@@ -606,7 +631,7 @@
"description": "Season number (default: current)",
"choices": {
"current": "{season} (current)"
"current": "%s1 (current)"
}
},
@@ -617,12 +642,12 @@
},
"body": {
"top_players": "Top {count} players",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
"top_players": "Top %s1 players",
"no_participation": "# %s1 didn't play rating in season %s2\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 rating season.",
"deleted_player": "Deleted {id}",
"deleted_player": "Deleted %s1",
"neighbors": "Rating neighbors",
"more": "+ {count} more"
"more": "+ %s1 more"
}
},
@@ -641,7 +666,7 @@
"description": "Shows all own tanks in a tier",
"body": {
"title": "# {name}'s owned tier {tier} tanks\n\n"
"title": "# %s1's owned tier %s2 tanks\n\n"
}
},
@@ -650,7 +675,7 @@
"description": "Checks wether the bot has the required permissions to function",
"body": {
"title": "# Permissions\n\n{body}",
"title": "# Permissions\n\n%s1",
"view_channels": "View channels: needed to find the message the refresh button was used on",
"read_message_history": "Read message history: needed to find the message to refresh",
"attach_files": "Attach files: needed to add images to image-based commands",
@@ -664,7 +689,7 @@
"body": {
"pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - {time}ms"
"pong_with_time": "Pong 🏓 - %s1ms"
},
"subcommands": {
@@ -684,7 +709,7 @@
"description": "Basic information about a player",
"body": {
"title": "{name}'s information",
"title": "%s1's information",
"nickname": "Nickname",
"battles": "Battles",
"winrate": "Winrate",
@@ -717,7 +742,7 @@
},
"errors": {
"no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"no_participation": "# %s1 hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"unrecorded_stats": "# Statistics currently unavailable\n\nWargaming may be currently be facing server downtime. If there is an on-going season, statistics should be available withing 24 hours."
}
}
@@ -747,16 +772,13 @@
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Search tanks...",
"recent": "Recently viewed",
"select_all": "Select all",
"count_singular": "1 tank",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"count_plural": "%s1 tanks",
"sorting_by": "Sorting by %s1",
"no_tanks_found": {
"body": "No tanks found.",
"search_again": "Try searching again.",
@@ -820,15 +842,6 @@
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
@@ -836,16 +849,16 @@
},
"hero": {
"credit": "By {name} ❤️"
"credit": "By %s1 ❤️"
},
"hot_tanks": {
"title": "Popular tanks",
"subtitle": "Last {days} days"
"subtitle": "Last %s1 days"
},
"other_projects": {
"title": "Other cool projects"
},
"help_translate": "Please help translate {name} to {language}!",
"help_translate": "Please help translate %s1 to %s2!",
"seo_description": "Tools for everything World of Tanks Blitz"
},
@@ -857,13 +870,11 @@
},
"tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
"screenshot": "Screenshot",
"randomize": "Randomize"
"internal": {
"name": "Internal",
"button": "Internal",
"description": "BlitzKit internal tools",
"seo_description": "BlitzKit internal tools"
},
"guess": {
@@ -878,7 +889,7 @@
"next": "Next",
"no_results": "No results"
},
"stats": "{correct} / {total} guessed correctly"
"stats": "%s1 / %s2 guessed correctly"
},
"tankopedia": {
@@ -888,14 +899,10 @@
"button": "Find tanks",
"description": "Blitz tank encyclopedia",
"seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz",
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.",
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -919,15 +926,9 @@
"view_range": {
"camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
@@ -943,7 +944,9 @@
"green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque",
"dev_wireframe": "DEV: wireframe"
"dev_wireframe": "DEV: wireframe",
"environment": "Environment",
"show_grid": "Show grid"
},
"screenshot": {
@@ -969,6 +972,7 @@
},
"advanced": "Advanced highlighting",
"shooter": "Shooter:",
"shot_card": {
"element": {
@@ -980,15 +984,15 @@
"track": "Tracks"
},
"status": {
"penetration": "{damage}hp penetration",
"penetration": "%s1hp penetration",
"blocked": "Blocked",
"ricochet": "Ricochet",
"splash": "{damage}hp splash"
"splash": "%s1hp splash"
},
"stats": {
"thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "{thickness}mm nominal",
"ricochet_loss": "{loss}% penetration"
"thickness_and_angle": "%s1mm %s2°",
"nominal": "%s1mm nominal",
"ricochet_loss": "%s1% penetration"
}
}
},
@@ -1028,8 +1032,8 @@
"tech_tree": {
"title": "Tech tree",
"battle": "1 battle",
"battles": "{count} battles",
"route": "Route {index} of {count}"
"battles": "%s1 battles",
"route": "Route %s1 of %s2"
},
"game_modes": {
@@ -1038,7 +1042,8 @@
"review": {
"title": "Review videos",
"join": "Apply to {name} YouTuber program"
"disclaimer": "BlitzKit does not endorse any review videos.",
"join": "Apply to %s1 YouTuber program"
},
"configuration": {
@@ -1077,15 +1082,15 @@
"title": "Characteristics",
"compare": {
"label": "Compare",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "Tier %s1 %s2",
"tier": "All of tier %s1",
"all": "All tanks"
},
"values": {
"shell_index": "Shell {index}",
"at_distance": "At {distance}m",
"shell_index": "Shell %s1",
"at_distance": "At %s1m",
"at_distance_slider_label": "Distance",
"dispersion_at_distance": "Dispersion at {distance}m",
"dispersion_at_distance": "Dispersion at %s1m",
"gun_flexibility": "Gun flexibility",
"speed": "Speed",
"terrain_coefficients": "Terrain coefficients",
@@ -1101,8 +1106,8 @@
"dpmEffective": "Effective at 60s",
"shells": "Shells",
"mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Reloads",
"shellReload": "Reload",
"shellReloads": "Shell reloads",
"shellReload": "Shell reload",
"intraClip": "Intra-clip",
"caliber": "Caliber",
"penetration": "Penetration",
@@ -1160,9 +1165,7 @@
"shellRange": "Shell range",
"shellCapacity": "Shell capacity",
"gunType": "Gun type",
"penetrationAt250m": "Penetration at 250m",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
"penetrationAt250m": "Penetration at 250m"
}
},
@@ -1241,9 +1244,9 @@
"description": "Tank performance statistics",
"seo_description": "Best tanks and their performances in World of Tanks Blitz",
"estimation": {
"body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "{count} hours",
"minutes": "{count} minutes"
"body": "Estimated stats based on %s1 players. Updated %s2 ago.",
"hours": "%s1 hours",
"minutes": "%s1 minutes"
},
"player_count": {
"label": "Player count"
@@ -1270,17 +1273,33 @@
}
}
},
"gallery": {
"name": "Gallery",
"avatars": {
"name": "Avatars",
"button": "Browse all",
"description": "Blitz player avatars",
"seo_description": "All World of Tanks Blitz player avatars",
"last_updated": "Last updated on {time}",
"seo_description": "All World of Tanks Blitz player profile avatars",
"description": "Player profile avatars",
"no_description": "This avatar has no description.",
"download": "Download",
"enlarge": "Enlarge",
"hidden": "Hidden",
"unnamed_avatars": "Unnamed avatars",
"search": {
"hint": "Search avatars...",
"results": "{count} avatars"
"results": "%s1 avatars"
},
"categories": {
"0": "Unspecified",
"1": "Premium",
"2": "Special",
"3": "Accumulative",
"4": "Other",
"5": "Temporary"
}
},
"session": {
"name": "Session",
"button": "Start tracking",
@@ -1293,8 +1312,8 @@
"no_results": "No players found"
},
"controls": {
"title": "Tracking {name}",
"subtitle": "Since {time}",
"title": "Tracking %s1",
"subtitle": "Since %s1",
"reset": "Reset",
"change": "Change"
},
@@ -1357,7 +1376,7 @@
"cancel": "Cancel",
"success": {
"title": "Copy URL",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"info": "Generated the URL for %s1. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"back": "Back",
"copy": "Copy URL"
}
@@ -1442,11 +1461,17 @@
"description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit"
},
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Analyze now",
"description": "Rich historic average stats",
"seo_description": "Rich historic average stats in World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Discover today",
"description": "In-game session stats",
"seo_description": "In-game session stats in World of Tanks Blitz"
}
},
+111 -115
View File
@@ -1,19 +1,18 @@
{
"test": {
"default": "test",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
"long": "Esta es una cadena de prueba larga. NO DEBES verla NUNCA en la interfaz de usuario. Mi fruta favorita es %s1 porque tiene el color %s2. ¡Me gusta tanto que diré su nombre 3 veces! ¡ %s1! ¡ %s1! ¡ %s1! Una vez más, es %s2, no %s3.",
"fox": "El rápido zorro marrón salta sobre el perro perezoso."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "{value}m",
"mm": "{value}mm",
"deg": "{value}°",
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"m": "%s1m",
"mm": "%s1mm",
"deg": "%s1°",
"hp": "%s1pv",
"x": "x%s1",
"percentage": "%s1%",
"deg_s": "%s1°/s"
},
"gun_types": {
"auto_loader": "Auto cargador",
@@ -186,11 +185,11 @@
"bot": {
"common": {
"errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
"clan_not_found": "# No se pudo encontrar el clan\nNo se pudo encontrar \"%s\". Prueba seleccionando un clan de los resultados de búsqueda.",
"player_not_found": "# No se pudo encontrar el usuario\nNo pude encontrar al usuario \"%s\". Prueba eligiendo un usuario de los resultados de búsqueda, escribiendo un nombre de usuario aceptable o usando el comando `/vincular`.",
"player_not_linked": "# Usa `/vincular` para empezar\n¡No sé quién eres, pero puedes decírmelo hoy!",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.",
"tank_name_not_found": "# No se pudo encontrar el tanque\nNo se pudo encontrar el tanque con el nombre \"%s\".",
"tank_id_not_found": "# Tanque no encontrado\nNo se pudo encontrar al tanque del ID \"%s\". Prueba escribiendo el nombre del tanque en vez de un número.",
"no_tank_stats": "# No hay estadísticas de tanque disponibles\nEste jugador no tiene estadísticas disponibles. Este podría no ser el jugador que estás buscando.",
"get_help": "Servidor de ayuda de Discord",
"uncaught_error": "# ¡Blitzkrieg sufrió un error!\nLo sentimos mucho. Siéntete libre de [unirte al servidor oficial de Discord](https://discord.gg/nDt7AjGJQH) para obtener ayuda.",
@@ -284,19 +283,19 @@
"name": "tier",
"description": "Nivel",
"choices": {
"tier": "Tier {tier_1} - {tier_2}"
"tier": "Nivel %s - %s"
}
}
},
"periods": {
"career": "Carrera",
"today": "Hoy",
"days": "{days} days",
"custom": "{day_1} - {day_2} days"
"days": "%s días",
"custom": "%s - %s días"
},
"filters": {
"none": "Sin filtros",
"tier": "Tier {tier}"
"tier": "Nivel %s"
},
"no_data": {
"battles_in_period": "No hay batallas jugadas en este período",
@@ -319,6 +318,22 @@
}
},
"commands": {
"auction": {
"name": "subasta",
"description": "Tanques disponibles ahora mismo en la subasta",
"body": {
"title": "Subasta Blitz",
"subtitle": "%s disponible • siguientes precios %s\nPujar ahora: %s",
"next": "Siguiente precio: %s",
"available": "Disponible: %s / %s"
},
"errors": {
"no_auction": "# No hay ninguna subasta\nActualmente no hay tanques disponibles para comprar en la subasta. Si crees que esto es un error, por favor, reporta esta discrepancia en [el servidor oficial de Discord](https://discord.gg/nDt7AjGJQH)."
},
"actions": {
"bid": "Pujar ahora"
}
},
"replay": {
"name": "repetición",
"description": "Mostrar resultados de la repetición"
@@ -339,19 +354,19 @@
"body": {
"subtitle": "Investigar",
"total": "Total",
"research": "Research: {cost}",
"upgrades": "Upgrades: {cost}",
"purchase": "Purchase: {cost}",
"equipment": "Equipment: {cost}",
"research": "Investigar: %s",
"upgrades": "Mejoras: %s",
"purchase": "Comprar: %s",
"equipment": "Equipamiento: %s",
"estimation": "# ¡Esto es una estimación!\n\nEsta lista podría incluir tanques ya investigados debido a la información errónea que proporciona Wargaming. Si puedes, prueba proporcionando un `tanque-inicial` de la línea al usar el comando."
},
"errors": {
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
"unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank."
"non_tech_tree": "# %s no está en el árbol tecnológico\n\nNo puedes investigar un tanque prémium o coleccionista.",
"tier_1": "# %s es un nivel I\n\nLos tanque de nivel I están siempre desbloqueados gratuitamente para todos los jugadores.",
"already_researched": "# %s ya está investigado\n\nPrueba proporcionando un `tanque-inicial` al usar el comando para ver los totales para una línea que ya has investigado.",
"start_end_equal": "# %s fue proporcionado dos veces\n\nAsegúrate de que `tanque-inicial` y `tanque-objetivo` son diferentes.",
"tanks_not_on_line": "# %s y %s no están en la misma línea\n\nAsegúrate de que `tanque-inicial` y `tanque-objetivo` están en la misma línea del árbol tecnológico.",
"unordered": "# %s de nivel superior a %s\n\nAsegúrate de proporcionar %s como el `tanque-inicial` en su lugar ya que es el tanque de nivel inferior."
}
},
"about": {
@@ -394,7 +409,7 @@
"wn8": "WN8",
"winrate": "Tasa de victorias",
"damage": "Daño",
"unknown_tank": "Unknown tank {id}"
"unknown_tank": "Tanque desconocido %s"
}
},
"debug": {
@@ -430,12 +445,12 @@
"options": {
"threshold": {
"name": "umbral",
"description": "The number of days inactive (default: {days} days)"
"description": "El número de días inactivo (predeterminado: %s días)"
}
},
"body": {
"subtitle": "Inactive for {days}+ days",
"listing": "{days} days"
"subtitle": "Inactivo por %s+ días",
"listing": "%s días"
}
},
"full_stats": {
@@ -478,7 +493,7 @@
"body": {
"accounts_linked": "Cuentas vinculadas",
"no_role_permissions": {
"title": "{name} failed to verify",
"title": "%s1 no se pudo verificar",
"description": "No tengo permiso para administrar roles."
}
},
@@ -493,11 +508,11 @@
"options": {
"limit": {
"name": "límite",
"description": "The max number of clan results (default: {limit})"
"description": "El número máximo de resultados de clanes (predeterminado: %s)"
}
},
"body": {
"title": "Clan search for {search} in region {region}",
"title": "Búsqueda del clan %s en la región %s",
"no_results": "No se encontraron clanes"
}
},
@@ -507,11 +522,11 @@
"options": {
"limit": {
"name": "límite",
"description": "The max number of player results (default: {limit})"
"description": "El número máximo para resultados de jugador (predeterminado: %s)"
}
},
"body": {
"title": "Player search for {search} in region {region}",
"title": "Búsqueda del jugador %s en la región %s",
"no_results": "No se encontraron jugadores"
}
},
@@ -531,13 +546,13 @@
"options": {
"limit": {
"name": "limite",
"description": "How many neighbors to display (default: {count})"
"description": "Cuántos vecinos a mostrar (predeterminado: %s)"
},
"season": {
"name": "temporada",
"description": "Número de temporada (predeterminado: actual)",
"choices": {
"current": "{season} (current)"
"current": "%s (actual)"
}
},
"league": {
@@ -546,12 +561,12 @@
}
},
"body": {
"top_players": "Top {count} players",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
"top_players": "Mejores %s jugadores",
"no_participation": "# %s no jugó clasificatoria en la temporada %s\n\nEste jugador no participó en esta temporada o no pasó de la calibración.",
"no_ongoing_season": "# No hay temporada en curso\n\nActualmente no hay ninguna temporada de clasificatoria.",
"deleted_player": "Deleted {id}",
"deleted_player": "%s eliminado",
"neighbors": "Vecinos de clasificatoria",
"more": "+ {count} more"
"more": "+ %s más"
}
},
"today": {
@@ -566,14 +581,14 @@
"name": "tanques en propiedad",
"description": "Muestra todos los tanques propios en un nivel",
"body": {
"title": "# {name}'s owned tier {tier} tanks\n\n"
"title": "# Tanques de %s de nivel %s\n\n"
}
},
"permissions": {
"name": "permisos",
"description": "Comprueba si el bot tiene los permisos requeridos para funcionar",
"body": {
"title": "# Permissions\n\n{body}",
"title": "# Permisos\n\n%s",
"view_channels": "Ver canales: necesario para encontrar el mensaje en el cual el botón \"actualizar\" fue usado",
"read_message_history": "Leer historial de mensajes: necesario para encontrar el mensaje a actualizar",
"attach_files": "Adjuntar archivos: necesario para añadir imágenes a los comandos basados en estas",
@@ -585,7 +600,7 @@
"description": "Comprueba si el bot está vivo.",
"body": {
"pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - {time}ms"
"pong_with_time": "Pong 🏓 - %s1ms"
},
"subcommands": {
"blitzkit": {
@@ -602,7 +617,7 @@
"name": "info del jugador",
"description": "Información básica sobre un jugador",
"body": {
"title": "{name}'s information",
"title": "Información de %s",
"nickname": "Apodo",
"battles": "Batallas",
"winrate": "Tasa de victorias",
@@ -631,7 +646,7 @@
}
},
"errors": {
"no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"no_participation": "# %s no ha jugado clasificatoria\n\nEste jugador no ha participado en el modo clasificatoria esta temporada.",
"unrecorded_stats": "# Estadísticas no disponibles actualmente\n\nWargaming podría estar sufriendo una caída del servidor. Si hay alguna temporada en curso, las estadísticas deberían estar disponibles en 24 horas."
}
}
@@ -656,16 +671,13 @@
"copied": "Copiado!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Buscar tanques...",
"recent": "Visto recientemente",
"select_all": "Seleccionar todo",
"count_singular": "1 tanque",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"count_plural": "%s1 tanques",
"sorting_by": "Ordenar por %s1",
"no_tanks_found": {
"body": "No se encontraron tanques.",
"search_again": "Intenta buscar de nuevo.",
@@ -727,30 +739,22 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
"action": "Discord"
},
"hero": {
"credit": "By {name} ❤️"
"credit": "Por %s1 ❤️"
},
"hot_tanks": {
"title": "Tanques populares",
"subtitle": "Last {days} days"
"subtitle": "Últimos %s1 días"
},
"other_projects": {
"title": "Otros proyectos interesantes"
},
"help_translate": "Please help translate {name} to {language}!",
"help_translate": "¡Por favor ayuda a traducir %s1 a %s2!",
"seo_description": "Herramientas para todo lo que es World of Tanks Blitz"
},
"docs": {
@@ -760,14 +764,6 @@
}
},
"tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -780,7 +776,7 @@
"next": "Next",
"no_results": "Sin resultados"
},
"stats": "{correct} / {total} guessed correctly"
"stats": "%s1 / %s2 guessed correctly"
},
"tankopedia": {
"search": "Search a stat...",
@@ -788,12 +784,9 @@
"button": "Encuentra tanques",
"description": "Enciclopedia de tanques de WOTB",
"seo_description": "Estadísticas, armaduras y más para todos los tanques en World of Tanks Blitz",
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.",
"tank_seo_title": "%s1 - Nivel %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 blindaje, estadisticas, equipamiento, proviciones y mas.\n%s2 DPM, %3hp alfa, %s4mm penetracion, %s5kph velocidad maxima, %s6 hp salud",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -814,14 +807,9 @@
},
"view_range": {
"camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -835,7 +823,9 @@
"green_penetration": "Penetración verde",
"hide_model_under_armor": "Ocultar modelo bajo la armadura",
"opaque": "Opaco",
"dev_wireframe": "DEV: wireframe"
"dev_wireframe": "DEV: wireframe",
"environment": "Entorno",
"show_grid": "Mostrar cuadrícula"
},
"screenshot": {
"download": "Descargar",
@@ -857,6 +847,7 @@
"reset": "Restablecer"
},
"advanced": "Resaltado avanzado",
"shooter": "Tirador:",
"shot_card": {
"element": {
"primary": "Primario",
@@ -867,15 +858,15 @@
"track": "Orugas"
},
"status": {
"penetration": "{damage}hp penetration",
"penetration": "%s1pv penetracion",
"blocked": "Bloqueado",
"ricochet": "Rebote",
"splash": "{damage}hp splash"
"splash": "%s1pv splash"
},
"stats": {
"thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "{thickness}mm nominal",
"ricochet_loss": "{loss}% penetration"
"thickness_and_angle": "%s1mm %s2°",
"nominal": "%s1mm nominal",
"ricochet_loss": "%s1% penetracion"
}
}
},
@@ -913,15 +904,16 @@
"tech_tree": {
"title": "Árbol tecnológico",
"battle": "1 batalla",
"battles": "{count} battles",
"route": "Route {index} of {count}"
"battles": "%s1 batallas",
"route": "Ruta %s1 de %s2"
},
"game_modes": {
"title": "Habilidades del modo de juego"
},
"review": {
"title": "Vídeos de reseña",
"join": "Apply to {name} YouTuber program"
"disclaimer": "BlitzKit no respalda ningún vídeo de reseñas.",
"join": "Aplica para el programa de YouTube de %s1"
},
"configuration": {
"title": "",
@@ -957,15 +949,15 @@
"title": "Características",
"compare": {
"label": "Comparar con",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "Nivel %s1 - %s2",
"tier": "Todos de nivel %s1",
"all": "Todos los tanques"
},
"values": {
"shell_index": "Shell {index}",
"at_distance": "At {distance}m",
"shell_index": "Proyectil %s1",
"at_distance": "A %s1m",
"at_distance_slider_label": "Distancia",
"dispersion_at_distance": "Dispersion at {distance}m",
"dispersion_at_distance": "Dispersión a %s1m",
"gun_flexibility": "Flexibilidad del arma",
"speed": "Velocidad",
"terrain_coefficients": "Coeficientes de terreno",
@@ -981,8 +973,8 @@
"dpmEffective": "Effective at 60s",
"shells": "Proyectiles",
"mostOptimalShellIndex": "Proyectil más óptimo",
"shellReloads": "Reloads",
"shellReload": "Recarga",
"shellReloads": "Tiempos de recarga",
"shellReload": "Tiempo de recarga",
"intraClip": "Intra-clip",
"caliber": "Calibre",
"penetration": "Penetración",
@@ -1040,9 +1032,7 @@
"shellRange": "Rango del proyectil",
"shellCapacity": "Capacidad de proyectiles",
"gunType": "Tipo de cañón",
"penetrationAt250m": "Penetración a 250 m",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
"penetrationAt250m": "Penetración a 250 m"
}
},
"firepower": {
@@ -1114,9 +1104,9 @@
"description": "Estadísticas de rendimiento del tanque",
"seo_description": "Los mejores tanques y su rendimiento en World of Tanks Blitz",
"estimation": {
"body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "{count} hours",
"minutes": "{count} minutes"
"body": "Estadísticas estimadas basadas en %s1 jugadores. Actualizado hace %s2.",
"hours": "%s1 horas",
"minutes": "%s1 minutos"
},
"player_count": {
"label": "Contador de jugadores"
@@ -1148,10 +1138,10 @@
"button": "Explorar todo",
"description": "Avatares para los jugadores de WOTB",
"seo_description": "Todos los avatares del jugador en World of Tanks Blitz",
"last_updated": "Last updated on {time}",
"last_updated": "Última actualización el %s1",
"search": {
"hint": "Buscar avatares...",
"results": "{count} avatars"
"results": "%s1 avatares"
}
},
"session": {
@@ -1166,8 +1156,8 @@
"no_results": "No se encontraron jugadores"
},
"controls": {
"title": "Tracking {name}",
"subtitle": "Since {time}",
"title": "Siguiendo a %s1",
"subtitle": "Desde %s1",
"reset": "Restablecer",
"change": "Change"
},
@@ -1227,7 +1217,7 @@
"cancel": "Cancelar",
"success": {
"title": "Copiar URL",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"info": "Se generó la URL para %s1. El embebido comenzará a rastrear tan pronto como se pegue. Restablece en cualquier momento haciendo clic derecho en PC o manteniendo presionado en móvil y luego seleccionando \"Restablecer\".",
"back": "Volver",
"copy": "Copiar URL"
}
@@ -1310,11 +1300,17 @@
"description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit"
},
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Analizar ahora",
"description": "Rich historic average stats",
"seo_description": "Rich historic average stats in World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Discover today",
"description": "In-game session stats",
"seo_description": "In-game session stats in World of Tanks Blitz"
}
},
"footer": {
+139 -143
View File
@@ -1,19 +1,18 @@
{
"test": {
"default": "teste",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
"long": "Ceci est une longue chaîne de test. Vous ne DEVEZ JAMAIS le voir dans l'interface utilisateur. Mon fruit préféré est %s1, car il a la couleur %s2. Je l'aime tellement que je vais dire son nom 3 fois ! %s1! %s1! %s1 ! Encore une fois, c'est %s2, pas %s3.",
"fox": "Le renard brun et rapide saute par-dessus le chien paresseux."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "{value}m",
"mm": "{value}mm",
"deg": "{value}°",
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"m": "%s1m",
"mm": "%s1mm",
"deg": "%s1°",
"hp": "%s1ps",
"x": "x%s1",
"percentage": "%s1%",
"deg_s": "%s1°/s"
},
"gun_types": {
"auto_loader": "Chargement auto",
@@ -186,11 +185,11 @@
"bot": {
"common": {
"errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
"clan_not_found": "# Impossible de trouver le clan\nImpossible de trouver «%s1 ». Essayez de sélectionner un clan dans le résultat de la recherche.",
"player_not_found": "# Impossible de trouver l'utilisateur\nJe n'ai pas pu trouver l'utilisateur \"%s1\". Essayez de choisir un utilisateur dans le résultat de la recherche, en tapant un nom d'utilisateur valide, ou en utilisant la commande `/link`.",
"player_not_linked": "# Utilisez `/link` pour commencer\nJe ne sais pas qui vous êtes, mais vous pouvez me le dire aujourd'hui !",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.",
"tank_name_not_found": "Impossible de trouver le char sous le nom \"%s1\".",
"tank_id_not_found": "# Réservoir introuvable\nImpossible de trouver le char avec l'ID \"%s1\". Essayez de taper le nom du char au lieu d'un nombre.",
"no_tank_stats": "# Aucune statistique de tank disponible\nCe joueur n'a pas de statistiques disponibles. Ce n'est peut-être pas le joueur que vous cherchez.",
"get_help": "Aidez le serveur Discord",
"uncaught_error": "# Blitzkit a rencontré une erreur !\nNous sommes désolés de cela. N'hésitez pas à [rejoindre le serveur officiel Discord](https://discord.gg/nDt7AjGJQH) pour obtenir de l'aide.",
@@ -284,19 +283,19 @@
"name": "rang",
"description": "Rang",
"choices": {
"tier": "Tier {tier_1} - {tier_2}"
"tier": "Rang %s1 - %s2"
}
}
},
"periods": {
"career": "Carrière",
"today": "Aujourd'hui",
"days": "{days} days",
"custom": "{day_1} - {day_2} days"
"days": "%s1 jour",
"custom": "%s1 - %s2 jours"
},
"filters": {
"none": "Aucun filtre",
"tier": "Tier {tier}"
"tier": "Rang %s1"
},
"no_data": {
"battles_in_period": "",
@@ -319,6 +318,22 @@
}
},
"commands": {
"auction": {
"name": "Char cible",
"description": "Des chars sont disponibles dès maintenant dans la vente aux enchères",
"body": {
"title": "Enchères Blitz",
"subtitle": "%s1 disponible • prochains prix %s2\nOffre : %s3",
"next": "Prochain prix : %s1",
"available": "Disponible : %s1 / %s2"
},
"errors": {
"no_auction": "# Pas de vente aux enchères\n\nActuellement, il n'y a pas de chars disponibles à l'achat dans la vente aux enchères. Si vous pensez qu'il s'agit d'une erreur, veuillez signaler cette anomalie dans [le serveur officiel Discord](https://discord.gg/nDt7AjGJQH)."
},
"actions": {
"bid": "Enchérir maintenant"
}
},
"replay": {
"name": "rejouer",
"description": "Afficher les résultats de la relecture"
@@ -339,19 +354,19 @@
"body": {
"subtitle": "Recherche",
"total": "Total",
"research": "Research: {cost}",
"upgrades": "Upgrades: {cost}",
"purchase": "Purchase: {cost}",
"equipment": "Equipment: {cost}",
"research": "Recherche: %s1",
"upgrades": "Améliorations : %s1",
"purchase": "Achat : %s1",
"equipment": "Equipment: %s1",
"estimation": "# Ceci est une estimation !\n\nCette liste peut inclure des chars déjà recherchés en raison de l'information inexacte fournie par Wargaming. Essayez de fournir un `char de départ` depuis la ligne lorsque vous utilisez la commande si vous le pouvez."
},
"errors": {
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
"unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank."
"non_tech_tree": "# %s1 n'est pas dans l'arbre technologique\n\nVous ne pouvez pas rechercher un char prenium/de collection.",
"tier_1": "# %s1 est un char de tier I\n\nLe char de tier 1 sont déverrouillé gratuitement pour tous les joueurs.",
"already_researched": "# %s1 est déjà recherché\n\nEssayez de fournir un `char de départ` en utilisant la commande pour voir la progression d'une lignée.",
"start_end_equal": "# %s1 was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# %s1 and %s2 are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
"unordered": "# %s1 higher tiered to %s2\n\nMake sure to provide %s3 as the `starting-tank` instead as it is the lower tiered tank."
}
},
"about": {
@@ -365,7 +380,7 @@
},
"embeds": {
"name": "intègre",
"description": "Comment fonctionnent les intégrations"
"description": "How embeds work"
},
"introduction": {
"name": "introduction",
@@ -387,14 +402,14 @@
},
"breakdown": {
"name": "décomposition",
"description": "Répartition d'une période par chars joués",
"description": "A period's breakdown by tanks played",
"body": {
"total": "Total",
"battles": "Batailles",
"wn8": "WN8",
"winrate": "Taux de victoires",
"damage": "Dégâts",
"unknown_tank": "Unknown tank {id}"
"unknown_tank": "Char %s1 inconnu"
}
},
"debug": {
@@ -430,12 +445,12 @@
"options": {
"threshold": {
"name": "seuil",
"description": "The number of days inactive (default: {days} days)"
"description": "Le nombre de jours inactifs (par défaut: %s1 jours)"
}
},
"body": {
"subtitle": "Inactive for {days}+ days",
"listing": "{days} days"
"subtitle": "Inactif depuis %s1+ de jours",
"listing": "%s1 jour"
}
},
"full_stats": {
@@ -456,7 +471,7 @@
"accuracy": "Précision"
},
"lethality": {
"title": "Létalité",
"title": "Lethality",
"average_damage": "Dégâts moyens",
"damage_per_tier": "Dégâts par tier",
"damage_ratio": "Rapport de dégâts",
@@ -478,26 +493,26 @@
"body": {
"accounts_linked": "Comptes liés",
"no_role_permissions": {
"title": "{name} failed to verify",
"title": "La vérification de %s1 a échoué",
"description": "Je n'ai pas l'autorisation de gérer les rôles."
}
},
"embed": {
"title": "Compte invalide ?",
"description": "Utilisez à nouveau la commande et sélectionnez un nom d'utilisateur à partir des résultats de recherche."
"description": "Use the command again and select a username from the search results."
}
},
"search_clans": {
"name": "clans de recherche",
"description": "Rechercher des clans",
"name": "search-clans",
"description": "Search clans",
"options": {
"limit": {
"name": "limite",
"description": "The max number of clan results (default: {limit})"
"description": "The max number of clan results (default: %s1)"
}
},
"body": {
"title": "Clan search for {search} in region {region}",
"title": "Clan search for %s1 in region %s2",
"no_results": "Pas de clan trouvé"
}
},
@@ -507,11 +522,11 @@
"options": {
"limit": {
"name": "limite",
"description": "The max number of player results (default: {limit})"
"description": "Le nombre maximum de résultats de joueur (par défaut: %s1)"
}
},
"body": {
"title": "Player search for {search} in region {region}",
"title": "Recherche de %s1 dans la région %s2",
"no_results": "Pas de joueur de trouvé"
}
},
@@ -520,77 +535,77 @@
"description": "Statistiques des batailles de cotés",
"subcommands": {
"neighbors": {
"name": "voisins",
"description": "Joueurs voisins dans le classement"
"name": "neighbors",
"description": "Neighboring players on the leaderboard"
},
"league": {
"name": "ligue",
"description": "Meilleurs joueurs d'une ligue"
"description": "Top players of a league"
}
},
"options": {
"limit": {
"name": "limite",
"description": "How many neighbors to display (default: {count})"
"description": "How many neighbors to display (default: %s1)"
},
"season": {
"name": "saison",
"description": "Numéro de saison (défaut : courant)",
"name": "season",
"description": "Season number (default: current)",
"choices": {
"current": "{season} (current)"
"current": "%s1 (current)"
}
},
"league": {
"name": "ligue",
"description": "La ligue à afficher"
"description": "The league to display"
}
},
"body": {
"top_players": "Top {count} players",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
"no_ongoing_season": "Pas de saison en cours\n\nIl n'y a pas de saison de notation en cours actuellement.",
"deleted_player": "Deleted {id}",
"neighbors": "évaluer les voisins",
"more": "+ {count} more"
"top_players": "Top %s1 players",
"no_participation": "# %s1 didn't play rating in season %s2\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 rating season.",
"deleted_player": "Deleted %s1",
"neighbors": "Rating neighbors",
"more": "+ %s1 more"
}
},
"today": {
"name": "aujourd'hui",
"description": "Les chars et les statistiques joués aujourd'hui"
"description": "Today's played tanks and statistics"
},
"stats": {
"name": "statistiques",
"description": "Statistiques rapides pour une période"
"description": "Quick statistics for a period"
},
"owned_tanks": {
"name": "chars possédés",
"description": "Affiche tous les chars d'un niveau",
"name": "owned-tanks",
"description": "Shows all own tanks in a tier",
"body": {
"title": "# {name}'s owned tier {tier} tanks\n\n"
"title": "# %s1's owned tier %s2 tanks\n\n"
}
},
"permissions": {
"name": "permissions",
"description": "Vérifie si le bot a les permissions nécessaires pour fonctionner",
"description": "Checks wether the bot has the required permissions to function",
"body": {
"title": "# Permissions\n\n{body}",
"view_channels": "Afficher les canaux : nécessaire pour trouver le message sur lequel le bouton d'actualisation a été utilisé",
"read_message_history": "Lire l'historique des messages : nécessaire pour trouver le message à rafraîchir",
"attach_files": "Joindre des fichiers : nécessaire pour ajouter des images à des commandes basées sur l'image",
"external_emojis": "Émoticônes externes: nécessaire pour ajouter des drapeaux nationaux et des émojis de classe de char"
"title": "# Permissions\n\n%s1",
"view_channels": "View channels: needed to find the message the refresh button was used on",
"read_message_history": "Read message history: needed to find the message to refresh",
"attach_files": "Attach files: needed to add images to image-based commands",
"external_emojis": "External emojis: needed to add national flags and tank class emojis"
}
},
"ping": {
"name": "ping",
"description": "Vérifiez si le bot est en vie.",
"description": "Check wether the bot is alive.",
"body": {
"pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - {time}ms"
"pong_with_time": "Pong 🏓 - %s1ms"
},
"subcommands": {
"blitzkit": {
"name": "blitzkit",
"description": "Ping blitzkit"
"description": "Ping BlitzKit"
},
"wotb": {
"name": "wotb",
@@ -599,10 +614,10 @@
}
},
"player_info": {
"name": "info-joueur",
"description": "Informations de base sur un joueur",
"name": "player-info",
"description": "Basic information about a player",
"body": {
"title": "{name}'s information",
"title": "%s1's information",
"nickname": "Nickname",
"battles": "Batailles",
"winrate": "Taux de victoires",
@@ -620,7 +635,7 @@
"description": "Today's rating statistics"
},
"season": {
"name": "saison",
"name": "season",
"description": "This season's rating statistics"
}
},
@@ -631,7 +646,7 @@
}
},
"errors": {
"no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"no_participation": "# %s1 hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"unrecorded_stats": "# Statistics currently unavailable\n\nWargaming may be currently be facing server downtime. If there is an on-going season, statistics should be available withing 24 hours."
}
}
@@ -656,16 +671,13 @@
"copied": "Copied!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Search tanks...",
"recent": "Recently viewed",
"select_all": "Select all",
"count_singular": "1 tank",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"count_plural": "%s1 tanks",
"sorting_by": "Sorting by %s1",
"no_tanks_found": {
"body": "No tanks found.",
"search_again": "Try searching again.",
@@ -727,30 +739,22 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
"action": "Discord"
},
"hero": {
"credit": "By {name} ❤️"
"credit": "By %s1 ❤️"
},
"hot_tanks": {
"title": "Popular tanks",
"subtitle": "Last {days} days"
"subtitle": "Last %s1 days"
},
"other_projects": {
"title": "Other cool projects"
},
"help_translate": "Please help translate {name} to {language}!",
"help_translate": "Please help translate %s1 to %s2!",
"seo_description": "Tools for everything World of Tanks Blitz"
},
"docs": {
@@ -760,14 +764,6 @@
}
},
"tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -780,7 +776,7 @@
"next": "Next",
"no_results": "No results"
},
"stats": "{correct} / {total} guessed correctly"
"stats": "%s1 / %s2 guessed correctly"
},
"tankopedia": {
"search": "Search a stat...",
@@ -788,12 +784,9 @@
"button": "Find tanks",
"description": "Blitz tank encyclopedia",
"seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz",
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.",
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -814,14 +807,9 @@
},
"view_range": {
"camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -835,7 +823,9 @@
"green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque",
"dev_wireframe": "DEV: wireframe"
"dev_wireframe": "DEV: wireframe",
"environment": "Environment",
"show_grid": "Show grid"
},
"screenshot": {
"download": "Download",
@@ -857,6 +847,7 @@
"reset": "Reset"
},
"advanced": "Advanced highlighting",
"shooter": "Shooter:",
"shot_card": {
"element": {
"primary": "Primary",
@@ -867,15 +858,15 @@
"track": "Tracks"
},
"status": {
"penetration": "{damage}hp penetration",
"penetration": "%s1hp penetration",
"blocked": "Blocked",
"ricochet": "Ricochet",
"splash": "{damage}hp splash"
"splash": "%s1hp splash"
},
"stats": {
"thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "{thickness}mm nominal",
"ricochet_loss": "{loss}% penetration"
"thickness_and_angle": "%s1mm %s2°",
"nominal": "%s1mm nominal",
"ricochet_loss": "%s1% penetration"
}
}
},
@@ -913,15 +904,16 @@
"tech_tree": {
"title": "Arbre technologique",
"battle": "1 battle",
"battles": "{count} battles",
"route": "Route {index} of {count}"
"battles": "%s1 battles",
"route": "Route %s1 of %s2"
},
"game_modes": {
"title": "Game mode abilities"
},
"review": {
"title": "Review videos",
"join": "Apply to {name} YouTuber program"
"disclaimer": "BlitzKit does not endorse any review videos.",
"join": "Apply to %s1 YouTuber program"
},
"configuration": {
"title": "Configuration",
@@ -957,15 +949,15 @@
"title": "Characteristics",
"compare": {
"label": "Compare against",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "Tier %s1 %s2",
"tier": "All of tier %s1",
"all": "All tanks"
},
"values": {
"shell_index": "Shell {index}",
"at_distance": "At {distance}m",
"shell_index": "Shell %s1",
"at_distance": "At %s1m",
"at_distance_slider_label": "Distance",
"dispersion_at_distance": "Dispersion at {distance}m",
"dispersion_at_distance": "Dispersion at %s1m",
"gun_flexibility": "Gun flexibility",
"speed": "Speed",
"terrain_coefficients": "Terrain coefficients",
@@ -981,8 +973,8 @@
"dpmEffective": "Effective at 60s",
"shells": "Shells",
"mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Reloads",
"shellReload": "Reload",
"shellReloads": "Shell reloads",
"shellReload": "Shell reload",
"intraClip": "Intra-clip",
"caliber": "Caliber",
"penetration": "Penetration",
@@ -1040,9 +1032,7 @@
"shellRange": "Shell range",
"shellCapacity": "Shell capacity",
"gunType": "Gun type",
"penetrationAt250m": "Penetration at 250m",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
"penetrationAt250m": "Penetration at 250m"
}
},
"firepower": {
@@ -1114,9 +1104,9 @@
"description": "Tank performance statistics",
"seo_description": "Best tanks and their performances in World of Tanks Blitz",
"estimation": {
"body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "{count} hours",
"minutes": "{count} minutes"
"body": "Estimated stats based on %s1 players. Updated %s2 ago.",
"hours": "%s1 hours",
"minutes": "%s1 minutes"
},
"player_count": {
"label": "Player count"
@@ -1148,10 +1138,10 @@
"button": "Browse all",
"description": "Blitz player avatars",
"seo_description": "All World of Tanks Blitz player avatars",
"last_updated": "Last updated on {time}",
"last_updated": "Last updated on %s1",
"search": {
"hint": "Search avatars...",
"results": "{count} avatars"
"results": "%s1 avatars"
}
},
"session": {
@@ -1166,8 +1156,8 @@
"no_results": "Pas de joueur de trouvé"
},
"controls": {
"title": "Tracking {name}",
"subtitle": "Since {time}",
"title": "Tracking %s1",
"subtitle": "Since %s1",
"reset": "Reset",
"change": "Change"
},
@@ -1227,7 +1217,7 @@
"cancel": "Cancel",
"success": {
"title": "Copy URL",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"info": "Generated the URL for %s1. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"back": "Back",
"copy": "Copy URL"
}
@@ -1310,11 +1300,17 @@
"description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit"
},
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Analyze now",
"description": "Rich historic average stats",
"seo_description": "Rich historic average stats in World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Discover today",
"description": "In-game session stats",
"seo_description": "In-game session stats in World of Tanks Blitz"
}
},
"footer": {
+110 -114
View File
@@ -1,19 +1,18 @@
{
"test": {
"default": "test",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is %s1 because it has the color of %s2. I like it so much that I will say its name 3 times! %s1! %s1! %s1! Once again, it's %s2, not %s3.",
"fox": "すばしっこい茶色のキツネが怠け者の犬を飛び越した。"
},
"common": {
"name": "BlitzKit",
"units": {
"m": "{value}m",
"mm": "{value}mm",
"deg": "{value}°",
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"m": "%s1m",
"mm": "%s1mm",
"deg": "%s1°",
"hp": "%s1hp",
"x": "x%s1",
"percentage": "%s1%",
"deg_s": "%s1°/s"
},
"gun_types": {
"auto_loader": "自動装填砲",
@@ -186,11 +185,11 @@
"bot": {
"common": {
"errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
"clan_not_found": "# クランが見つかりませんでした\n\"%s1\"は見つかりませんでした。検索結果からクランを選択してください。",
"player_not_found": "# ユーザーが見つかりませんでした\nユーザー「\"%s1\"」は見つかりませんでした。検索結果からユーザーを選択するか、有効な名前を入力してください。`/link`コマンドを使用することも可能です。",
"player_not_linked": "# `/link`を使用して始めましょう\nはじめまして!あなたが誰か、私に教えてください。",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.",
"tank_name_not_found": "# 車輌が見つかりませんでした\n\"%s1\"という名前の車輌は見つかりませんでした。",
"tank_id_not_found": "# 車輌が見つかりませんでした\n\"%s1\"というIDの車輌は見つかりませんでした。番号ではなく名前で検索してください。",
"no_tank_stats": "# 利用可能な車輌統計がありません\nこのプレイヤーには利用可能な統計がありません。あなたが探しているプレイヤーではない可能性があります。",
"get_help": "Discordサーバーに参加する",
"uncaught_error": "# BlitzKitがエラーに遭遇しました!\n申し訳ありません。お気軽に[公式Discordサーバー](https://discord.gg/nDt7AjGJQH)にお問い合わせください。",
@@ -284,19 +283,19 @@
"name": "tier",
"description": "Tier",
"choices": {
"tier": "Tier {tier_1} - {tier_2}"
"tier": "Tier %s1 - %s2"
}
}
},
"periods": {
"career": "Career",
"today": "Today",
"days": "{days} days",
"custom": "{day_1} - {day_2} days"
"days": "%s1 days",
"custom": "%s1 - %s2 days"
},
"filters": {
"none": "フィルターなし",
"tier": "Tier {tier}"
"tier": "Tier %s1"
},
"no_data": {
"battles_in_period": "No battles played in this period",
@@ -319,6 +318,22 @@
}
},
"commands": {
"auction": {
"name": "オークション",
"description": "現在オークションで購入可能な車輌",
"body": {
"title": "Blitzオークション",
"subtitle": "%s1 残り• 次の価格 %s2\n今すぐ入札: %s3",
"next": "次の価格: %s1",
"available": "残り: %s1 / %s2"
},
"errors": {
"no_auction": "# 進行中のオークションはありません\n現在、オークションで購入可能な車輌はありません。もしこれが間違いの場合、[公式Discardサーバー](https://discord.gg/nDt7AjGJQH)に報告してください。"
},
"actions": {
"bid": "今すぐ入札"
}
},
"replay": {
"name": "replay",
"description": "Display replay results"
@@ -339,19 +354,19 @@
"body": {
"subtitle": "Research",
"total": "トータル",
"research": "Research: {cost}",
"upgrades": "Upgrades: {cost}",
"purchase": "Purchase: {cost}",
"equipment": "Equipment: {cost}",
"research": "Research: %s1",
"upgrades": "Upgrades: %s1",
"purchase": "Purchase: %s1",
"equipment": "Equipment: %s1",
"estimation": "# This is an estimation!\n\nThis list may include already researched tanks because of the inaccurate information Wargaming provides. Try providing a `starting-tank` from the line when using the command if you can."
},
"errors": {
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
"unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank."
"non_tech_tree": "# %s1 is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# %s1 is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# %s1 is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
"start_end_equal": "# %s1 was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# %s1 and %s2 are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
"unordered": "# %s1 higher tiered to %s2\n\nMake sure to provide %s3 as the `starting-tank` instead as it is the lower tiered tank."
}
},
"about": {
@@ -394,7 +409,7 @@
"wn8": "WN8",
"winrate": "勝率",
"damage": "ダメージ",
"unknown_tank": "Unknown tank {id}"
"unknown_tank": "Unknown tank %s1"
}
},
"debug": {
@@ -430,12 +445,12 @@
"options": {
"threshold": {
"name": "threshold",
"description": "The number of days inactive (default: {days} days)"
"description": "The number of days inactive (default: %s1 days)"
}
},
"body": {
"subtitle": "Inactive for {days}+ days",
"listing": "{days} days"
"subtitle": "Inactive for %s1+ days",
"listing": "%s1 days"
}
},
"full_stats": {
@@ -478,7 +493,7 @@
"body": {
"accounts_linked": "Accounts linked",
"no_role_permissions": {
"title": "{name} failed to verify",
"title": "%s1 failed to verify",
"description": "I don't have the permission to manage roles."
}
},
@@ -493,11 +508,11 @@
"options": {
"limit": {
"name": "limit",
"description": "The max number of clan results (default: {limit})"
"description": "The max number of clan results (default: %s1)"
}
},
"body": {
"title": "Clan search for {search} in region {region}",
"title": "Clan search for %s1 in region %s2",
"no_results": "No clans found"
}
},
@@ -507,11 +522,11 @@
"options": {
"limit": {
"name": "limit",
"description": "The max number of player results (default: {limit})"
"description": "The max number of player results (default: %s1)"
}
},
"body": {
"title": "Player search for {search} in region {region}",
"title": "Player search for %s1 in region %s2",
"no_results": "No players found"
}
},
@@ -531,13 +546,13 @@
"options": {
"limit": {
"name": "limit",
"description": "How many neighbors to display (default: {count})"
"description": "How many neighbors to display (default: %s1)"
},
"season": {
"name": "season",
"description": "Season number (default: current)",
"choices": {
"current": "{season} (current)"
"current": "%s1 (current)"
}
},
"league": {
@@ -546,12 +561,12 @@
}
},
"body": {
"top_players": "Top {count} players",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
"top_players": "Top %s1 players",
"no_participation": "# %s1 didn't play rating in season %s2\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 rating season.",
"deleted_player": "Deleted {id}",
"deleted_player": "Deleted %s1",
"neighbors": "Rating neighbors",
"more": "+ {count} more"
"more": "+ %s1 more"
}
},
"today": {
@@ -566,14 +581,14 @@
"name": "owned-tanks",
"description": "Shows all own tanks in a tier",
"body": {
"title": "# {name}'s owned tier {tier} tanks\n\n"
"title": "# %s1's owned tier %s2 tanks\n\n"
}
},
"permissions": {
"name": "permissions",
"description": "Checks wether the bot has the required permissions to function",
"body": {
"title": "# Permissions\n\n{body}",
"title": "# Permissions\n\n%s1",
"view_channels": "View channels: needed to find the message the refresh button was used on",
"read_message_history": "Read message history: needed to find the message to refresh",
"attach_files": "Attach files: needed to add images to image-based commands",
@@ -585,7 +600,7 @@
"description": "Check wether the bot is alive.",
"body": {
"pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - {time}ms"
"pong_with_time": "Pong 🏓 - %s1ms"
},
"subcommands": {
"blitzkit": {
@@ -602,7 +617,7 @@
"name": "player-info",
"description": "Basic information about a player",
"body": {
"title": "{name}'s information",
"title": "%s1's information",
"nickname": "Nickname",
"battles": "戦闘数",
"winrate": "勝率",
@@ -631,7 +646,7 @@
}
},
"errors": {
"no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"no_participation": "# %s1 hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"unrecorded_stats": "# Statistics currently unavailable\n\nWargaming may be currently be facing server downtime. If there is an on-going season, statistics should be available withing 24 hours."
}
}
@@ -656,16 +671,13 @@
"copied": "コピーしました!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "車輌名を入力…",
"recent": "最近の閲覧",
"select_all": "全て選択",
"count_singular": "1 輌",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"count_plural": "%s1輌",
"sorting_by": ":%s1で並び替え",
"no_tanks_found": {
"body": "該当する車輌が存在しません。",
"search_again": "もう一度検索してください。",
@@ -727,30 +739,22 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
"action": "Discord"
},
"hero": {
"credit": "By {name} ❤️"
"credit": "By %s1 ❤️"
},
"hot_tanks": {
"title": "人気の車輌",
"subtitle": "Last {days} days"
"subtitle": "過去 %s1日間"
},
"other_projects": {
"title": "他のプロジェクト"
},
"help_translate": "Please help translate {name} to {language}!",
"help_translate": "%s1の %s2への翻訳にご協力ください!",
"seo_description": "World of Tanks Blitz全般のツール"
},
"docs": {
@@ -760,14 +764,6 @@
}
},
"tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -780,7 +776,7 @@
"next": "Next",
"no_results": "該当なし"
},
"stats": "{correct} / {total} guessed correctly"
"stats": "%s1 / %s2 guessed correctly"
},
"tankopedia": {
"search": "Search a stat...",
@@ -788,12 +784,9 @@
"button": "戦車を探す",
"description": "Blitz戦車百科",
"seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz",
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.",
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -814,14 +807,9 @@
},
"view_range": {
"camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -835,7 +823,9 @@
"green_penetration": "貫通可能域を緑色表示",
"hide_model_under_armor": "車輌モデルを非表示",
"opaque": "貫通アシストを不透明にする",
"dev_wireframe": "DEV: wireframe"
"dev_wireframe": "DEV: wireframe",
"environment": "環境",
"show_grid": "グリッドを表示"
},
"screenshot": {
"download": "保存する",
@@ -857,6 +847,7 @@
"reset": "リセット"
},
"advanced": "高度なハイライト",
"shooter": "自車両",
"shot_card": {
"element": {
"primary": "主装甲",
@@ -867,15 +858,15 @@
"track": "履帯"
},
"status": {
"penetration": "{damage}hp penetration",
"penetration": "%s1hp 貫通",
"blocked": "非貫通",
"ricochet": "跳弾",
"splash": "{damage}hp splash"
"splash": "%s1hp 爆風ダメージ"
},
"stats": {
"thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "{thickness}mm nominal",
"ricochet_loss": "{loss}% penetration"
"thickness_and_angle": "%s1mm %s2°",
"nominal": "%s1mm 装甲",
"ricochet_loss": "%s1% penetration"
}
}
},
@@ -913,15 +904,16 @@
"tech_tree": {
"title": "技術ツリー",
"battle": "1 戦",
"battles": "{count} battles",
"route": "Route {index} of {count}"
"battles": "%s1 戦",
"route": "ルート %s1 / %s2"
},
"game_modes": {
"title": "ゲームモードアビリティ"
},
"review": {
"title": "Review videos",
"join": "Apply to {name} YouTuber program"
"disclaimer": "BlitzKitはいかなるレビュー動画も後援していません。",
"join": "%s1 YouTuberプログラムに応募する"
},
"configuration": {
"title": "構成",
@@ -957,15 +949,15 @@
"title": "性能値",
"compare": {
"label": "比較対象",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "Tier %s1 %s2",
"tier": "Tier %s1 全車輌",
"all": "全車輌"
},
"values": {
"shell_index": "Shell {index}",
"at_distance": "At {distance}m",
"shell_index": "Shell %s1",
"at_distance": "At %s1m",
"at_distance_slider_label": "距離",
"dispersion_at_distance": "Dispersion at {distance}m",
"dispersion_at_distance": "%s1m での散布界",
"gun_flexibility": "主砲旋回上限",
"speed": "最高速度",
"terrain_coefficients": "地形走破能力",
@@ -981,7 +973,7 @@
"dpmEffective": "60秒での有効値",
"shells": "砲弾",
"mostOptimalShellIndex": "最適な弾数",
"shellReloads": "Reloads",
"shellReloads": "自動装填時間",
"shellReload": "装填時間",
"intraClip": "砲弾再装填時間",
"caliber": "口径",
@@ -1040,9 +1032,7 @@
"shellRange": "射程",
"shellCapacity": "総弾数",
"gunType": "主砲タイプ",
"penetrationAt250m": "250m での貫通力",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
"penetrationAt250m": "250m での貫通力"
}
},
"firepower": {
@@ -1114,9 +1104,9 @@
"description": "車輌の戦闘統計",
"seo_description": "Best tanks and their performances in World of Tanks Blitz",
"estimation": {
"body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "{count} hours",
"minutes": "{count} minutes"
"body": "%s1 人のプレイヤーの推定統計。 %s2前に更新されました。",
"hours": "%s1 時間",
"minutes": "%s1 分"
},
"player_count": {
"label": "統計期間:"
@@ -1148,10 +1138,10 @@
"button": "全て見る",
"description": "Blitzアバター図鑑",
"seo_description": "World of Tanks Blitz の 全ての プレイヤーアバター",
"last_updated": "Last updated on {time}",
"last_updated": "最終更新: %s1",
"search": {
"hint": "アバターを検索...",
"results": "{count} avatars"
"results": "検索結果: %s1 個"
}
},
"session": {
@@ -1166,8 +1156,8 @@
"no_results": "プレイヤーが見つかりませんでした"
},
"controls": {
"title": "Tracking {name}",
"subtitle": "Since {time}",
"title": "%s1 を追跡中",
"subtitle": "開始日時: %s1",
"reset": "リセット",
"change": "変更"
},
@@ -1227,7 +1217,7 @@
"cancel": "Cancel",
"success": {
"title": "Copy URL",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"info": "Generated the URL for %s1. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"back": "戻る",
"copy": "Copy URL"
}
@@ -1310,11 +1300,17 @@
"description": "さあ、BlitzKit を始めよう!",
"seo_description": "さあ、BlitzKit を始めよう!"
},
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Analyze now",
"description": "Rich historic average stats",
"seo_description": "Rich historic average stats in World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Discover today",
"description": "ゲーム内のセッション統計",
"seo_description": "In-game session stats in World of Tanks Blitz"
}
},
"footer": {
+111 -115
View File
@@ -1,19 +1,18 @@
{
"test": {
"default": "",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
"long": "To jest długi ciąg testowy. NIGDY NIE ZOBACZYSZ tego w interfejsie użytkownika. Mój ulubiony owoc to %s1, ponieważ ma kolor %s2. Tak bardzo je lubię, że powiem ich nazwę 3 razy! %s1! %s1! %s1! Po raz kolejny to %s2, a nie %s3.",
"fox": "Szybki brązowy lis przeskoczył nad leniwym psem."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "{value}m",
"mm": "{value}mm",
"deg": "{value}°",
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"m": "%s1m",
"mm": "%s1mm",
"deg": "%s1°",
"hp": "%s1hp",
"x": "x%s1",
"percentage": "%s1%",
"deg_s": "%s1°/s"
},
"gun_types": {
"auto_loader": "Auto loader",
@@ -186,11 +185,11 @@
"bot": {
"common": {
"errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
"clan_not_found": "# Nie można znaleźć klanu\nNie można znaleźć \"%s1\". Spróbuj wybrać klan z wyników wyszukiwania.",
"player_not_found": "# Nie można znaleźć użytkownika\nNie mogłem znaleźć użytkownika \"%s1\". Spróbuj wybrać użytkownika z wyniku wyszukiwania, wpisując poprawną nazwę użytkownika lub używając polecenia `/link`.",
"player_not_linked": "# Użyj `/link` aby zacząć\nNie wiem, kim jesteś, ale możesz mi dziś powiedzieć!",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.",
"tank_name_not_found": "# Czołg nie został znaleziony\nNie można znaleźć czołgu o nazwie \"%s1\".",
"tank_id_not_found": "# Czołg nie został znaleziony\nNie można znaleźć czołgu o ID \"%s1\". Spróbuj wpisać nazwę czołgu zamiast liczby.",
"no_tank_stats": "# Brak dostępnych statystyk czołgów\nTen gracz nie ma żadnych dostępnych statystyk. Może to nie być gracz, którego szukasz.",
"get_help": "Pomóż serwerowi Discord",
"uncaught_error": "# BlitzKit wpadł w błąd!\nPrzepraszamy za to. Możesz [dołączyć do oficjalnego serwera Discord](https://discord.gg/nDt7AjGJQH), aby uzyskać pomoc.",
@@ -284,19 +283,19 @@
"name": "poziom",
"description": "Poziom",
"choices": {
"tier": "Tier {tier_1} - {tier_2}"
"tier": "Poziom %s1 - %s2"
}
}
},
"periods": {
"career": "Kariera",
"today": "Dzisiaj",
"days": "{days} days",
"custom": "{day_1} - {day_2} days"
"days": "%s1 dzień",
"custom": "%s1 - %s2 dni"
},
"filters": {
"none": "Brak filtrów",
"tier": "Tier {tier}"
"tier": "Poziom %s1"
},
"no_data": {
"battles_in_period": "Brak bitew w tym okresie",
@@ -319,6 +318,22 @@
}
},
"commands": {
"auction": {
"name": "aukcja",
"description": "Czołgi dostępne na aukcji",
"body": {
"title": "Aukcja Blitz",
"subtitle": "%s1 dostępne • następne ceny %s2\nOferta teraz: %s3",
"next": "Następna cena: %s1",
"available": "Dostępne: %s1 / %s2"
},
"errors": {
"no_auction": "# Brak bieżącej aukcji\n\nObecnie nie ma dostępnych czołgów do zakupu na aukcji. Jeśli uważasz, że to błąd, zgłoś go na [oficjalnym serwerze Discord](https://discord.gg/nDt7AjGJQH)."
},
"actions": {
"bid": "Oferta teraz"
}
},
"replay": {
"name": "powtórz",
"description": "Wyświetl wyniki powtórki"
@@ -339,19 +354,19 @@
"body": {
"subtitle": "Rozpocznij badania",
"total": "Całkowity",
"research": "Research: {cost}",
"upgrades": "Upgrades: {cost}",
"purchase": "Purchase: {cost}",
"equipment": "Equipment: {cost}",
"research": "Badanie: %s1",
"upgrades": "Ulepszenie: %s1",
"purchase": "Zakup: %s1",
"equipment": "Wyposażenie: %s1",
"estimation": "# To jest szacunek!\n\nTa lista może zawierać już zbadane czołgi z powodu niedokładnych informacji dostarczanych przez Wargaming. Spróbuj podać `starting-tank` z wiersza podczas używania polecenia, jeśli możesz."
},
"errors": {
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
"unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank."
"non_tech_tree": "# %s1 nie jest na drzewie technologii\n\nNie możesz zbadać czołg premium/kolekcjonerski.",
"tier_1": "# %s1 jest czołgami poziomu I\n\nPoziom I są zawsze odblokowane za darmo dla wszystkich graczy.",
"already_researched": "# %s1 jest już zbadany\n\nSpróbuj podać `starting-tank` gdy użyjesz komendy aby zobaczyć sumy dla linii, którą już zbadałeś.",
"start_end_equal": "# %s1 został dostarczony dwa razy\n\nUpewnij się, że `starting-tank` i `target-tank` są inne.",
"tanks_not_on_line": "# %s i %s nie znajdują się w tej samej linii\n\nUpewnij się, że `starting-tank` i `target-tank` znajdują się na tej samej linii drzewa technicznego.",
"unordered": "# %s1 wyższy stopień %s2\n\nUpewnij się, że zamiast tego %s3 jest `starting-tank`, ponieważ jest tierowo niższym czołgiem."
}
},
"about": {
@@ -394,7 +409,7 @@
"wn8": "WN8",
"winrate": "Śr. wygranych",
"damage": "Uszkodzenia",
"unknown_tank": "Unknown tank {id}"
"unknown_tank": "Nieznany czołg %s1"
}
},
"debug": {
@@ -430,12 +445,12 @@
"options": {
"threshold": {
"name": "threshold",
"description": "The number of days inactive (default: {days} days)"
"description": "Liczba dni nieaktywnych (domyślnie: %s dni)"
}
},
"body": {
"subtitle": "Inactive for {days}+ days",
"listing": "{days} days"
"subtitle": "Inactive for %s1+ days",
"listing": "%s1 dzień"
}
},
"full_stats": {
@@ -478,7 +493,7 @@
"body": {
"accounts_linked": "Accounts linked",
"no_role_permissions": {
"title": "{name} failed to verify",
"title": "%s1 failed to verify",
"description": "I don't have the permission to manage roles."
}
},
@@ -493,11 +508,11 @@
"options": {
"limit": {
"name": "limit",
"description": "The max number of clan results (default: {limit})"
"description": "The max number of clan results (default: %s1)"
}
},
"body": {
"title": "Clan search for {search} in region {region}",
"title": "Clan search for %s1 in region %s2",
"no_results": "No clans found"
}
},
@@ -507,11 +522,11 @@
"options": {
"limit": {
"name": "limit",
"description": "The max number of player results (default: {limit})"
"description": "Maksymalna liczba wyników gracza (domyślnie: %s)"
}
},
"body": {
"title": "Player search for {search} in region {region}",
"title": "Player search for %s1 in region %s2",
"no_results": "No players found"
}
},
@@ -531,13 +546,13 @@
"options": {
"limit": {
"name": "limit",
"description": "How many neighbors to display (default: {count})"
"description": "How many neighbors to display (default: %s1)"
},
"season": {
"name": "season",
"description": "Season number (default: current)",
"choices": {
"current": "{season} (current)"
"current": "%s1 (current)"
}
},
"league": {
@@ -546,12 +561,12 @@
}
},
"body": {
"top_players": "Top {count} players",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
"top_players": "Top %s1 players",
"no_participation": "# %s1 didn't play rating in season %s2\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 rating season.",
"deleted_player": "Deleted {id}",
"deleted_player": "Usunięto %s1",
"neighbors": "Rating neighbors",
"more": "+ {count} more"
"more": "+ %s1 więcej"
}
},
"today": {
@@ -566,14 +581,14 @@
"name": "posiadane czołgi",
"description": "Pokazuje wszystkie własne czołgi na poziomie",
"body": {
"title": "# {name}'s owned tier {tier} tanks\n\n"
"title": "# %s1 posiadany poziom %s2 czołgi\n\n"
}
},
"permissions": {
"name": "uprawnienia",
"description": "Sprawdza, czy bot ma wymagane uprawnienia do funkcji",
"body": {
"title": "# Permissions\n\n{body}",
"title": "# Uprawnienia\n\n%s1",
"view_channels": "Wyświetl kanały: potrzebne do znalezienia wiadomości użyty przycisk odświeżania",
"read_message_history": "Przeczytaj historię wiadomości: potrzebne do znalezienia wiadomości do odświeżenia",
"attach_files": "Dołącz pliki: potrzebne do dodawania obrazów do komend opartych na obrazach",
@@ -585,7 +600,7 @@
"description": "Sprawdź, czy bot żyje.",
"body": {
"pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - {time}ms"
"pong_with_time": "Pong 🏓 - %s1ms"
},
"subcommands": {
"blitzkit": {
@@ -602,7 +617,7 @@
"name": "informacje o graczu",
"description": "Podstawowe informacje o graczu",
"body": {
"title": "{name}'s information",
"title": "Informacje o %s1",
"nickname": "Nick",
"battles": "Bitwy",
"winrate": "Śr. wygranych",
@@ -631,7 +646,7 @@
}
},
"errors": {
"no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"no_participation": "# %s1 nie grał w \n\nTen gracz nie uczestniczył w grze oceniającej w tym sezonie.",
"unrecorded_stats": "# Statistics currently unavailable\n\nWargaming may be currently be facing server downtime. If there is an on-going season, statistics should be available withing 24 hours."
}
}
@@ -656,16 +671,13 @@
"copied": "Copied!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Search tanks...",
"recent": "Recently viewed",
"select_all": "Select all",
"count_singular": "1 tank",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"count_plural": "%s1 tanks",
"sorting_by": "Sorting by %s1",
"no_tanks_found": {
"body": "No tanks found.",
"search_again": "Try searching again.",
@@ -727,30 +739,22 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
"action": "Discord"
},
"hero": {
"credit": "By {name} ❤️"
"credit": "By %s1 ❤️"
},
"hot_tanks": {
"title": "Popular tanks",
"subtitle": "Last {days} days"
"subtitle": "Last %s1 days"
},
"other_projects": {
"title": "Other cool projects"
},
"help_translate": "Please help translate {name} to {language}!",
"help_translate": "Please help translate %s1 to %s2!",
"seo_description": "Tools for everything World of Tanks Blitz"
},
"docs": {
@@ -760,14 +764,6 @@
}
},
"tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -780,7 +776,7 @@
"next": "Next",
"no_results": "Brak wyników"
},
"stats": "{correct} / {total} guessed correctly"
"stats": ""
},
"tankopedia": {
"search": "Search a stat...",
@@ -788,12 +784,9 @@
"button": "Find tanks",
"description": "Blitz tank encyclopedia",
"seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz",
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.",
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -814,14 +807,9 @@
},
"view_range": {
"camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -835,7 +823,9 @@
"green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque",
"dev_wireframe": "DEV: wireframe"
"dev_wireframe": "DEV: wireframe",
"environment": "Environment",
"show_grid": "Show grid"
},
"screenshot": {
"download": "Download",
@@ -857,6 +847,7 @@
"reset": "Reset"
},
"advanced": "Advanced highlighting",
"shooter": "Shooter:",
"shot_card": {
"element": {
"primary": "Primary",
@@ -867,15 +858,15 @@
"track": "Tracks"
},
"status": {
"penetration": "{damage}hp penetration",
"penetration": "%s1hp penetration",
"blocked": "Blocked",
"ricochet": "Ricochet",
"splash": "{damage}hp splash"
"splash": "%s1hp splash"
},
"stats": {
"thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "{thickness}mm nominal",
"ricochet_loss": "{loss}% penetration"
"thickness_and_angle": "%s1mm %s2°",
"nominal": "%s1mm nominal",
"ricochet_loss": "%s1% penetration"
}
}
},
@@ -913,15 +904,16 @@
"tech_tree": {
"title": "Tech tree",
"battle": "1 battle",
"battles": "{count} battles",
"route": "Route {index} of {count}"
"battles": "%s1 battles",
"route": "Route %s1 of %s2"
},
"game_modes": {
"title": "Game mode abilities"
},
"review": {
"title": "Review videos",
"join": "Apply to {name} YouTuber program"
"disclaimer": "BlitzKit does not endorse any review videos.",
"join": "Apply to %s1 YouTuber program"
},
"configuration": {
"title": "Configuration",
@@ -957,15 +949,15 @@
"title": "Characteristics",
"compare": {
"label": "Compare against",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "Tier %s1 %s2",
"tier": "All of tier %s1",
"all": "All tanks"
},
"values": {
"shell_index": "Shell {index}",
"at_distance": "At {distance}m",
"shell_index": "Shell %s1",
"at_distance": "At %s1m",
"at_distance_slider_label": "Distance",
"dispersion_at_distance": "Dispersion at {distance}m",
"dispersion_at_distance": "Dispersion at %s1m",
"gun_flexibility": "Gun flexibility",
"speed": "Speed",
"terrain_coefficients": "Terrain coefficients",
@@ -981,8 +973,8 @@
"dpmEffective": "Effective at 60s",
"shells": "Shells",
"mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Reloads",
"shellReload": "Reload",
"shellReloads": "Shell reloads",
"shellReload": "Shell reload",
"intraClip": "Intra-clip",
"caliber": "Caliber",
"penetration": "Penetration",
@@ -1040,9 +1032,7 @@
"shellRange": "Shell range",
"shellCapacity": "Shell capacity",
"gunType": "Gun type",
"penetrationAt250m": "Penetration at 250m",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
"penetrationAt250m": "Penetration at 250m"
}
},
"firepower": {
@@ -1114,9 +1104,9 @@
"description": "Tank performance statistics",
"seo_description": "Best tanks and their performances in World of Tanks Blitz",
"estimation": {
"body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "{count} hours",
"minutes": "{count} minutes"
"body": "Estimated stats based on %s1 players. Updated %s2 ago.",
"hours": "%s1 hours",
"minutes": "%s1 minutes"
},
"player_count": {
"label": "Player count"
@@ -1148,10 +1138,10 @@
"button": "Browse all",
"description": "Blitz player avatars",
"seo_description": "All World of Tanks Blitz player avatars",
"last_updated": "Last updated on {time}",
"last_updated": "Last updated on %s1",
"search": {
"hint": "Search avatars...",
"results": "{count} avatars"
"results": "%s1 avatars"
}
},
"session": {
@@ -1166,8 +1156,8 @@
"no_results": "No players found"
},
"controls": {
"title": "Tracking {name}",
"subtitle": "Since {time}",
"title": "Tracking %s1",
"subtitle": "Since %s1",
"reset": "Reset",
"change": "Change"
},
@@ -1227,7 +1217,7 @@
"cancel": "Cancel",
"success": {
"title": "Copy URL",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"info": "Generated the URL for %s1. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"back": "Back",
"copy": "Copy URL"
}
@@ -1310,11 +1300,17 @@
"description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit"
},
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Analyze now",
"description": "Rich historic average stats",
"seo_description": "Rich historic average stats in World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Discover today",
"description": "In-game session stats",
"seo_description": "In-game session stats in World of Tanks Blitz"
}
},
"footer": {
+111 -115
View File
@@ -1,19 +1,18 @@
{
"test": {
"default": "teste",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
"long": "Esta é uma sequência de testes longa. Você NUNCA DEVE ver isto na UI. Minha fruta favorita é %s1 porque tem a cor de %s2. Eu gosto tanto dela que vou dizer o nome dela 3 vezes! %s1! %s1! %s1! Mais uma vez, é %s2, não %s3.",
"fox": "A ágil raposa castanha salta por cima do cão preguiçoso."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "{value}m",
"mm": "{value}mm",
"deg": "{value}°",
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"m": "%s1m",
"mm": "%s1mm",
"deg": "%s1°",
"hp": "%s1hp",
"x": "x%s1",
"percentage": "%s1%",
"deg_s": "%s1°/s"
},
"gun_types": {
"auto_loader": "Autocarregador",
@@ -186,11 +185,11 @@
"bot": {
"common": {
"errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
"clan_not_found": "# Não foi possível encontrar o clã\nNão foi possível encontrar \"%s1\". Tente selecionar um clã do resultado de pesquisa.",
"player_not_found": "# Não foi possível encontrar o usuário\nEu não consegui encontrar o usuário \"%s1\". Tente escolher um usuário do resultado de pesquisa, digitando um nome de usuário válido, ou usando o comando `/link`.",
"player_not_linked": "Digite \"/link\" para começar.\nEu posso não saber quem é você, mas pode-me dizer hoje sobre isso!",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.",
"tank_name_not_found": "# Tanque não encontrado\nNão foi possível encontrar um tanque pelo nome \"%s1\".",
"tank_id_not_found": "# Tanque não encontrado\nNão foi possível encontrar o tanque pelo ID \"%s1\". Tente digitar o nome do tanque ao invés de um número.",
"no_tank_stats": "# Nenhuma estatística de tanques disponível\nEsse jogador não tem as suas estatísticas disponíveis. Talvez esse pode não ser o jogador que procura.",
"get_help": "Servidor de Ajuda no Discord",
"uncaught_error": "# O BlitzKit apresentou um erro!\nSentimos muito por isso. Sinta-se a vontade para [entrar no servidor oficial no Discord]\n(https://discord.gg/nDt7AjGJQH) para conseguir ajuda.",
@@ -284,19 +283,19 @@
"name": "nível",
"description": "Nível",
"choices": {
"tier": "Tier {tier_1} - {tier_2}"
"tier": "Nível %s1 - %s2"
}
}
},
"periods": {
"career": "Carreira",
"today": "Hoje",
"days": "{days} days",
"custom": "{day_1} - {day_2} days"
"days": "%s1 dias",
"custom": "%s1 - %s2 dias"
},
"filters": {
"none": "Sem filtros",
"tier": "Tier {tier}"
"tier": "Nível %s1"
},
"no_data": {
"battles_in_period": "Não há batalhas jogadas neste período",
@@ -319,6 +318,22 @@
}
},
"commands": {
"auction": {
"name": "leilão",
"description": "Tanques disponíveis agora mesmo no leilão",
"body": {
"title": "Leilão Blitz",
"subtitle": "%s1 disponível • próximos preços %s2 oferta agora: %s3",
"next": "Próximo preço: %s1",
"available": "Disponível: %s1 / %s2"
},
"errors": {
"no_auction": "# Nenhum leilão em curso\n\nNo momento, não há tanques disponíveis para compra no leilão. Se você acha que isso é um erro, relate esta discrepância no [servidor oficial do Discord](https://discord.gg/nDt7AjGJQH)."
},
"actions": {
"bid": "Ofertar agora"
}
},
"replay": {
"name": "replay",
"description": "Exibir resultados de replays"
@@ -339,19 +354,19 @@
"body": {
"subtitle": "Pesquisa",
"total": "Total",
"research": "Research: {cost}",
"upgrades": "Upgrades: {cost}",
"purchase": "Purchase: {cost}",
"equipment": "Equipment: {cost}",
"research": "Pesquisa: %s1",
"upgrades": "Melhorias: %s1",
"purchase": "Compra: %s1",
"equipment": "Equipamento: %s1",
"estimation": "# Esta é uma estimativa!\n\nEsta lista pode incluir tanques já pesquisados devido à informação imprecisa que a Wargaming fornece. Tente fornecer um `tanque-inicial` da linha ao usar o comando, se puder."
},
"errors": {
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
"unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank."
"non_tech_tree": "# %s1 não está na árvore tecnológica\n\nVocê não pode pesquisar um tanque premium/colecionador.",
"tier_1": "# %s1 é um nível I\n\nOs tanques de nível I sempre são desbloqueados gratuitamente para todos os jogadores.",
"already_researched": "# %s1 já foi pesquisado\n\nTente fornecer um `tanque-inicial` ao usar o comando para ver os totais de uma linha que você já pesquisou.",
"start_end_equal": "# %s1 foi fornecido duas vezes\n\nCertifique-se de que `tanque-inicial` e `tanque-alvo` são diferentes.",
"tanks_not_on_line": "# %s1 e %s2 não estão na mesma linha\n\nCertifique-se de que `tanque-inicial` e `tanque-alvo` estão na mesma linha tecnológica.",
"unordered": "# %s1 de nível mais alto para %s2\n\nCertifique-se de fornecer %s3 como o `tanque-inicial` em vez disso, pois é o tanque de nível inferior."
}
},
"about": {
@@ -394,7 +409,7 @@
"wn8": "WN8",
"winrate": "Taxa de vitória",
"damage": "Dano",
"unknown_tank": "Unknown tank {id}"
"unknown_tank": "Tanque desconhecido %s1"
}
},
"debug": {
@@ -430,12 +445,12 @@
"options": {
"threshold": {
"name": "limite",
"description": "The number of days inactive (default: {days} days)"
"description": "O número de dias inativos (padrão: %s1 dias)"
}
},
"body": {
"subtitle": "Inactive for {days}+ days",
"listing": "{days} days"
"subtitle": "Inativo por %s1+ dias",
"listing": "%s1 dias"
}
},
"full_stats": {
@@ -478,7 +493,7 @@
"body": {
"accounts_linked": "Contas vinculadas",
"no_role_permissions": {
"title": "{name} failed to verify",
"title": "%s1 falhou ao verificar",
"description": "Eu não tenho permissão para gerenciar funções."
}
},
@@ -493,11 +508,11 @@
"options": {
"limit": {
"name": "limite",
"description": "The max number of clan results (default: {limit})"
"description": "O número máximo de resultados do clã (padrão: %s1)"
}
},
"body": {
"title": "Clan search for {search} in region {region}",
"title": "Pesquisa do clã para %s1 na região %s2",
"no_results": "Nenhum clã encontrado"
}
},
@@ -507,11 +522,11 @@
"options": {
"limit": {
"name": "limite",
"description": "The max number of player results (default: {limit})"
"description": "O número máximo de resultados do jogador (padrão: %s1)"
}
},
"body": {
"title": "Player search for {search} in region {region}",
"title": "Busca de jogador para %s1 na região %s2",
"no_results": "Nenhum jogador encontrado"
}
},
@@ -531,13 +546,13 @@
"options": {
"limit": {
"name": "limite",
"description": "How many neighbors to display (default: {count})"
"description": "Quantos vizinhos exibir (padrão: %s1)"
},
"season": {
"name": "temporada",
"description": "Número da temporada (padrão: atual)",
"choices": {
"current": "{season} (current)"
"current": "%s1 (atual)"
}
},
"league": {
@@ -546,12 +561,12 @@
}
},
"body": {
"top_players": "Top {count} players",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
"top_players": "Melhores %s1 jogadores",
"no_participation": "# %s1 não jogou classificação na temporada %s2\n\nEste jogador não participou nesta temporada ou não passou da calibração.",
"no_ongoing_season": "# Nenhuma temporada em andamento\n\nNão há uma temporada de classificação em execução no momento.",
"deleted_player": "Deleted {id}",
"deleted_player": "Excluído %s1",
"neighbors": "Classificação de vizinhos",
"more": "+ {count} more"
"more": "+ %s1 mais"
}
},
"today": {
@@ -566,14 +581,14 @@
"name": "tanques-possuídos",
"description": "Mostra todos os tanques possuídos em um nível",
"body": {
"title": "# {name}'s owned tier {tier} tanks\n\n"
"title": "# %s1's possuía nível %s2 tanques\n\n"
}
},
"permissions": {
"name": "Permissões",
"description": "Verifica se o bot tem as permissões necessárias para funcionar",
"body": {
"title": "# Permissions\n\n{body}",
"title": "# Permissões\n\n%s1",
"view_channels": "Ver canais: necessário para encontrar a mensagem em que o botão atualizar foi usado",
"read_message_history": "Histórico de mensagens lidas: necessário para encontrar a mensagem para atualizar",
"attach_files": "Anexar arquivos: necessário para adicionar imagens a comandos baseados em imagens",
@@ -585,7 +600,7 @@
"description": "Verifique se o bot está vivo.",
"body": {
"pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - {time}ms"
"pong_with_time": "Pong 🏓 - %s1ms"
},
"subcommands": {
"blitzkit": {
@@ -602,7 +617,7 @@
"name": "informação do jogador",
"description": "Informações básicas sobre um jogador",
"body": {
"title": "{name}'s information",
"title": "%s1's informações",
"nickname": "Apelido",
"battles": "Batalhas",
"winrate": "Taxa de vitória",
@@ -631,7 +646,7 @@
}
},
"errors": {
"no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"no_participation": "# %s1 não jogou classificação\n\nEste jogador não participou do modo de classificação nesta temporada.",
"unrecorded_stats": "# Estatísticas atualmente indisponível\n\nWargaming pode estar no momento, enfrentando tempo de inatividade do servidor. Se houver uma temporada em curso, as estatísticas deverão ser disponibilizadas dentro de 24 horas."
}
}
@@ -656,16 +671,13 @@
"copied": "Copiado!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Procurar tanques...",
"recent": "Visto recentemente",
"select_all": "Selecionar todos",
"count_singular": "1 tanque",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"count_plural": "%s1 tanques",
"sorting_by": "Ordenando por %s1",
"no_tanks_found": {
"body": "Nenhum tanque encontrado.",
"search_again": "Tente pesquisar novamente.",
@@ -727,30 +739,22 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
"action": "Discord"
},
"hero": {
"credit": "By {name} ❤️"
"credit": "Por %s1 ❤️"
},
"hot_tanks": {
"title": "Tanques populares",
"subtitle": "Last {days} days"
"subtitle": "Últimos %s1 dias"
},
"other_projects": {
"title": "Outros projetos legais"
},
"help_translate": "Please help translate {name} to {language}!",
"help_translate": "Por favor, ajude a traduzir %s1 para %s2!",
"seo_description": "Ferramentas para tudo World of Tanks Blitz"
},
"docs": {
@@ -760,14 +764,6 @@
}
},
"tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -780,7 +776,7 @@
"next": "Next",
"no_results": "Sem resultados"
},
"stats": "{correct} / {total} guessed correctly"
"stats": "%s1 / %s2 guessed correctly"
},
"tankopedia": {
"search": "Search a stat...",
@@ -788,12 +784,9 @@
"button": "Encontrar tanques",
"description": "Blitz enciclopédia de tanques",
"seo_description": "Estatísticas, blindagem, e mais para todos os tanques de Wolrd of Tanks Blitz",
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.",
"tank_seo_title": "%s1 - Nível %s2 %s3 %s4 | %s5 Tanquepedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 blindagem, estatísticas, equipamentos, provisões e mais. %s2 DPM, %s3hp alfa, %s4mm de penetração, %s5km/h velocidade máxima, %s6hp vida.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -814,14 +807,9 @@
},
"view_range": {
"camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -835,7 +823,9 @@
"green_penetration": "Penetração verde",
"hide_model_under_armor": "Ocultar modelo sob blindagem",
"opaque": "Opaco",
"dev_wireframe": "DEV: wireframe"
"dev_wireframe": "DEV: wireframe",
"environment": "Ambiente",
"show_grid": "Exibir grade"
},
"screenshot": {
"download": "Download",
@@ -857,6 +847,7 @@
"reset": "Restaurar"
},
"advanced": "Realce avançado",
"shooter": "Atirador:",
"shot_card": {
"element": {
"primary": "Primário",
@@ -867,15 +858,15 @@
"track": "Esteiras"
},
"status": {
"penetration": "{damage}hp penetration",
"penetration": "%s1HP penetração",
"blocked": "Bloqueado",
"ricochet": "Ricochete",
"splash": "{damage}hp splash"
"splash": "%s1hp respingo"
},
"stats": {
"thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "{thickness}mm nominal",
"ricochet_loss": "{loss}% penetration"
"thickness_and_angle": "%s1mm %s2°",
"nominal": "%s1mm nominal",
"ricochet_loss": "%s1% penetração"
}
}
},
@@ -913,15 +904,16 @@
"tech_tree": {
"title": "Árvore tecnológica",
"battle": "1 batalha",
"battles": "{count} battles",
"route": "Route {index} of {count}"
"battles": "%s1 batalhas",
"route": "Rota %s1 de %s2"
},
"game_modes": {
"title": "Habilidades do modo de jogo"
},
"review": {
"title": "Vídeos de análise",
"join": "Apply to {name} YouTuber program"
"disclaimer": "BlitzKit não aprova nenhum vídeo de análise.",
"join": "Candidate-se para %s1 um programa YouTuber"
},
"configuration": {
"title": "Configuração",
@@ -957,15 +949,15 @@
"title": "Características",
"compare": {
"label": "Comparar com",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "Nível %s1 %s2",
"tier": "Tudo de nível %s1",
"all": "Todos os tanques"
},
"values": {
"shell_index": "Shell {index}",
"at_distance": "At {distance}m",
"shell_index": "Projétil %s1",
"at_distance": "À %s1m",
"at_distance_slider_label": "Distância",
"dispersion_at_distance": "Dispersion at {distance}m",
"dispersion_at_distance": "Dispersão à %s1m",
"gun_flexibility": "Flexibilidade da arma",
"speed": "Velocidade",
"terrain_coefficients": "Coeficientes do terreno",
@@ -981,8 +973,8 @@
"dpmEffective": "Efetivo aos 60s",
"shells": "Projéteis",
"mostOptimalShellIndex": "Projétil mais ideal",
"shellReloads": "Reloads",
"shellReload": "Recarga",
"shellReloads": "Recarga de projétil",
"shellReload": "Recarregamento de projétil",
"intraClip": "Intra-clipe",
"caliber": "Calibre",
"penetration": "Penetração",
@@ -1040,9 +1032,7 @@
"shellRange": "Alcance de projétil",
"shellCapacity": "Capacidade de projéteis",
"gunType": "Tipo de arma",
"penetrationAt250m": "Penetração à 250m",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
"penetrationAt250m": "Penetração à 250m"
}
},
"firepower": {
@@ -1114,9 +1104,9 @@
"description": "Estatísticas de desempenho do tanque",
"seo_description": "Melhores tanques e suas performances no World of Tanks Blitz",
"estimation": {
"body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "{count} hours",
"minutes": "{count} minutes"
"body": "Estatísticas estimadas baseadas em %s1 jogadores. Atualizado %s2 atrás.",
"hours": "%s1 horas",
"minutes": "%s1 minutos"
},
"player_count": {
"label": "Número de jogadores"
@@ -1148,10 +1138,10 @@
"button": "Procurar todos",
"description": "Avatares do jogador Blitz",
"seo_description": "Todos os avatares do jogador World of Tanks Blitz",
"last_updated": "Last updated on {time}",
"last_updated": "Última atualização em %s1",
"search": {
"hint": "Procurar avatares...",
"results": "{count} avatars"
"results": "%s1 avatares"
}
},
"session": {
@@ -1166,8 +1156,8 @@
"no_results": "Nenhum jogador encontrado"
},
"controls": {
"title": "Tracking {name}",
"subtitle": "Since {time}",
"title": "Rastreando %s1",
"subtitle": "Desde %s1",
"reset": "Restaurar",
"change": "Mudança"
},
@@ -1227,7 +1217,7 @@
"cancel": "Cancelar",
"success": {
"title": "Copiar URL",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"info": "Gerou a URL para %s1. A incorporação começará a rastrear assim que a URL for colada. Redefina a qualquer momento clicando com o botão direito no PC ou pressionando longamente no celular e, em seguida, selecionando \"Redefinir\".",
"back": "Voltar",
"copy": "Copiar URL"
}
@@ -1310,11 +1300,17 @@
"description": "Comece com BlitzKit",
"seo_description": "Comece com BlitzKit"
},
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
"blitz_analysis": {
"name": "AnaliseBlitz[]",
"button": "Analise agora",
"description": "Estatísticas médias históricas ricas",
"seo_description": "Estatísticas médias históricas ricas no World of Tanks Blitz"
},
"blitz_insider": {
"name": "Assistente Blitz",
"button": "Descubra hoje",
"description": "Estatísticas da temporada dentro do jogo",
"seo_description": "Estatísticas da sessão no jogo World of Tanks Blitz"
}
},
"footer": {
+120 -124
View File
@@ -1,19 +1,18 @@
{
"test": {
"default": "тест",
"long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт — {fruit}, потому что это цвет {color}. Он мне так нравится, так что я скажу его 3 раза {fruit}! {fruit}! {fruit}! И ещё раз, это {color}, а не {not_color}.",
"fox": "Съешь же ещё этих мягких французских булок, да выпей чаю."
"long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт - %s1, потому что его цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3.",
"fox": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт - %s1, потому что его цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "{value}м",
"mm": "{value}мм",
"deg": "{value}°",
"hp": "{value}оп",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/с"
"m": "%s1м",
"mm": "%s1мм",
"deg": "%s1°",
"hp": "%s1hp",
"x": "x%s1",
"percentage": "%s1%",
"deg_s": "%s1°/с"
},
"gun_types": {
"auto_loader": "Барабан",
@@ -186,11 +185,11 @@
"bot": {
"common": {
"errors": {
"clan_not_found": "# Не удалось найти клан\nНе найден «{search}». Попробуйте выбрать клан из результатов поиска.",
"player_not_found": "# Не удалось найти пользователя\nНе удалось найти пользователя «{search}». Попробуйте выбрать пользователя из результатов поиска, введя правильное имя пользователя или используя команду «/link».",
"clan_not_found": "# Не удалось найти клан\nНе найден «%s1». Попробуйте выбрать клан из результатов поиска.",
"player_not_found": "# Не удалось найти пользователя\nНе удалось найти пользователя «%s1». Попробуйте выбрать пользователя из результатов поиска, введя правильное имя пользователя или используя команду «/link».",
"player_not_linked": "# Используйте «/link», чтобы начать\nЯ не знаю, кто ты, но давай познакомимся сегодня!",
"tank_name_not_found": "# Танк не найден\nНе удалось найти танк с названием «{search}».",
"tank_id_not_found": "# Танк не найден\nНе удалось найти танк с ID «{search}». Попробуйте ввести название танка вместо числа.",
"tank_name_not_found": "# Танк не найден\nНе удалось найти танк по названию «%s1».",
"tank_id_not_found": "# Танк не найден\nНе удалось найти танк по ID «%s1». Попробуйте ввести название танка вместо числа.",
"no_tank_stats": "# Статистика танка недоступна\nУ этого игрока нет доступной статистики. Возможно, это не тот игрок, которого вы ищете.",
"get_help": "Получить помощь",
"uncaught_error": "# BlitzKit столкнулся с ошибкой!\nНам очень жаль. Не стесняйтесь и [присоединяйтесь к официальному серверу Discord](https://discord.gg/nDt7AjGJQH), чтобы получить помощь.",
@@ -284,19 +283,19 @@
"name": "уровень",
"description": "Уровень",
"choices": {
"tier": "Уровень {tier_1} - {tier_2}"
"tier": "Уровень %s - %s"
}
}
},
"periods": {
"career": "Карьера",
"today": "Cегодня",
"days": "{days} дней",
"custom": "{day_1} - {day_2} дней"
"days": "%s дней",
"custom": "%s - %s дней"
},
"filters": {
"none": "Без фильтров",
"tier": " {tier} уровня"
"tier": "Уровень %s1"
},
"no_data": {
"battles_in_period": "За этот период боёв не было",
@@ -319,6 +318,22 @@
}
},
"commands": {
"auction": {
"name": "аукцион",
"description": "Танки доступные на аукционе сейчас",
"body": {
"title": "Blitz аукцион",
"subtitle": "%s доступные • следующие цены %s\nПокупка сейчас: %s",
"next": "Следующая цена: %s",
"available": "Доступно: %s / %s"
},
"errors": {
"no_auction": "# Аукцион сейчас не проходит\n\nНа данный момент нет танков для покупки на аукционе. Если считаете, что это ошибка, пожалуйста о несоответствии на [официальном сервере Discord](https://discord.gg/nDt7AjGJQH)."
},
"actions": {
"bid": "Купить сейчас"
}
},
"replay": {
"name": "повтор",
"description": "Показать результаты повтора"
@@ -339,19 +354,19 @@
"body": {
"subtitle": "Исследование",
"total": "Всего",
"research": "Исследование: {cost}",
"upgrades": "Улучшения: {cost}",
"purchase": "Покупка: {cost}",
"equipment": "Снаряжение: {cost}",
"research": "Исследование: %s",
"upgrades": "Улучшения: %s",
"purchase": "Покупка: %s",
"equipment": "Снаряжение: %s",
"estimation": "# Это приблизительная оценка!\n\nВ этот список могут входить уже исследованные танки из-за неточной информации, предоставляемой Wargaming. Попробуйте указать «стартовый-танк» в строке при использовании команды, если сможете."
},
"errors": {
"non_tech_tree": "# {tank} отсутствует в дереве танков\n\nВы не можете исследовать премиумный / коллекционный танк.",
"tier_1": "# {tank} — танк I уровня\n\nУровень I всегда открыт бесплатно для всех игроков.",
"already_researched": "# {tank} уже исследован\n\nПопробуйте предоставить «starting-tank» при использовании команды для просмотра итогов уже исследованной ветки.",
"start_end_equal": "# {tank} был предоставлен дважды\n\nУбедитесь, что «starting-tank» и «target-tank» различаются.",
"tanks_not_on_line": "# {tank_1} и {tank_2} находятся не в одной ветке\n\nУбедитесь, что «starting-tank» и «target-tank» находятся на той же ветке дерева танков.",
"unordered": "# У {tank_1} более высокий уровень чем у {tank_2}\n\nОбязательно укажите {tank_2} в качестве «starting-tank», так как это танк более низкого уровня."
"non_tech_tree": "# %s отсутствует в дереве танков\n\nВы не можете исследовать премиум / коллекционный танк.",
"tier_1": "# %s - это уровень I\n\nУровень I всегда открыт бесплатно для всех игроков.",
"already_researched": "# %s уже исследован\n\nПопробуйте предоставить «starting-tank» при использовании команды для просмотра итогов уже исследованной ветки.",
"start_end_equal": "# %s был предоставлен дважды\n\nУбедитесь, что «starting-tank» и «target-tank» различаются.",
"tanks_not_on_line": "# %s и %s находятся не в одной ветке\n\nУбедитесь, что «starting-tank» и «target-tank» находятся на той же ветке дерева танков.",
"unordered": "# У %s более высокий уровень чем у %s\n\nОбязательно укажите %s в качестве «starting-tank», так как это танк более низкого уровня."
}
},
"about": {
@@ -394,7 +409,7 @@
"wn8": "WN8",
"winrate": "Коэффициент побед",
"damage": "Урон",
"unknown_tank": "Неизвестный танк {id}"
"unknown_tank": "Неизвестный танк %s"
}
},
"debug": {
@@ -430,12 +445,12 @@
"options": {
"threshold": {
"name": "порог",
"description": "Количество дней не активности (по умолчанию: {days} дней)"
"description": "Количество дней не активности (по умолчанию: %s дней)"
}
},
"body": {
"subtitle": "Неактивен {days}+ дней",
"listing": "{days} дней"
"subtitle": "Неактивен %s+ дней",
"listing": "%s дней"
}
},
"full_stats": {
@@ -478,7 +493,7 @@
"body": {
"accounts_linked": "Аккаунты привязаны",
"no_role_permissions": {
"title": "{name} не удалось проверить",
"title": "%s1 не удалось проверить",
"description": "У меня нет разрешения на управление ролями."
}
},
@@ -493,11 +508,11 @@
"options": {
"limit": {
"name": "лимит",
"description": "Максимальное количество результатов клана (по умолчанию: {limit})"
"description": "Максимальное количество результатов клана (по умолчанию: %s)"
}
},
"body": {
"title": "Поиск клана {search} в регионе {region}",
"title": "Поиск клана %s в регионе %s",
"no_results": "Кланов не найдено"
}
},
@@ -507,11 +522,11 @@
"options": {
"limit": {
"name": "лимит",
"description": "Максимальное число результатов игрока (по умолчанию: {limit})"
"description": "Максимальное число результатов игрока (по умолчанию: %s)"
}
},
"body": {
"title": "Поиск игрока {search} в регионе {region}",
"title": "Поиск игрока %s в регионе %s",
"no_results": "Игроков не найдено"
}
},
@@ -531,13 +546,13 @@
"options": {
"limit": {
"name": "лимит",
"description": "Количество отображаемых ближайших игроков (по умолчанию: {count})"
"description": "Количество отображаемых ближайших игроков (по умолчанию: %s)"
},
"season": {
"name": "сезон",
"description": "Номер сезона (по умолчанию: текущий)",
"choices": {
"current": "{season} (текущий)"
"current": "%s (текущая)"
}
},
"league": {
@@ -546,12 +561,12 @@
}
},
"body": {
"top_players": "Топ {count} игроков",
"no_participation": "# {player} не играл в рейтинг в сезоне {season}\n\nЭтот игрок не участвовал в этом сезоне или не получил прошлую калибровку.",
"top_players": "Топ %s игроков",
"no_participation": "# %s не играл в рейтинг в сезоне %s\n\nЭтот игрок не участвовал в этом сезоне или не получил прошлую калибровку.",
"no_ongoing_season": "# Нет текущего сезона\n\nВ данный момент нет рейтингового сезона.",
"deleted_player": "Удаленный {id}",
"deleted_player": "Удалено %s",
"neighbors": "Рейтинг ближайших игроков",
"more": "+ ещё {count}"
"more": "+ ещё %s"
}
},
"today": {
@@ -566,14 +581,14 @@
"name": "cобственные-танки",
"description": "Показывает все принадлежащие танки на уровне",
"body": {
"title": "# {name} принадлежат танки {tier} уровня\n\n"
"title": "# %s принадлежал уровень %s танков\n\n"
}
},
"permissions": {
"name": "разрешения",
"description": "Проверяет, есть ли у бота необходимые разрешения для работы",
"body": {
"title": "# Разрешения\n\n{body}",
"title": "# Разрешения\n\n%s",
"view_channels": "Просмотр каналов: необходимо найти сообщение, на котором была использована кнопка обновления",
"read_message_history": "Просмотр истории сообщений: необходимо найти сообщение для обновления",
"attach_files": "Прикрепить файлы: необходимо для добавления изображений в команды на основе изображения",
@@ -585,7 +600,7 @@
"description": "Проверьте, что бот жив.",
"body": {
"pong": "Понг 🏓",
"pong_with_time": "Понг 🏓 — {time}мс"
"pong_with_time": "Понг 🏓 - %sмс"
},
"subcommands": {
"blitzkit": {
@@ -602,7 +617,7 @@
"name": "информация-об-игроке",
"description": "Основная информация о игроке",
"body": {
"title": "Информация о {name}",
"title": "Информация о %s",
"nickname": "Никнейм",
"battles": "Боёв",
"winrate": "Коэффициент побед",
@@ -631,7 +646,7 @@
}
},
"errors": {
"no_participation": "# {name} не получил рейтинга\n\nЭтот игрок в этом сезоне не участвовал в рейтинговом режиме.",
"no_participation": "# %s не получил рейтинга\n\nЭтот игрок в этом сезоне не участвовал в рейтинговом режиме.",
"unrecorded_stats": "# Статистика недоступна в настоящее время\n\nВ настоящее время Wargaming, возможно, сталкивается с простоями сервера. Если сезон продолжается, статистика должна быть доступна в течение 24 часов."
}
}
@@ -656,16 +671,13 @@
"copied": "Скопировано!"
},
"tank_search": {
"select_dialog_title": "Поиск танка",
"swap_dialog_title": "Заменить танк",
"clear_filters": "Сбросить фильтры",
"login": "Войти, чтобы воспользоваться",
"search_bar_hint": "Поиск танков...",
"recent": "Недавние",
"select_all": "Выбрать всё",
"count_singular": "1 танк",
"count_plural": "Танков: {count}",
"sorting_by": "Сортировать по {name}",
"count_plural": "%s1 танк",
"sorting_by": "Сортировать по %s1",
"no_tanks_found": {
"body": "Танков не найдено.",
"search_again": "Попробуйте повторить поиск.",
@@ -681,7 +693,7 @@
"survivability.volume": "объём",
"survivability.length": "длина",
"fire.dpm": "базовый ДПМ",
"fire.dpmPremium": "премиумный ДПМ",
"fire.dpmPremium": "улучшенный ДПМ",
"fire.reload": "перезарядка",
"fire.caliber": "калибр",
"fire.standardPenetration": "базовое пробитие",
@@ -727,30 +739,22 @@
}
},
"home": {
"pwa": {
"title": "Установите приложение BlitzKit",
"new": "НОВИНКА",
"checking": "Проверка совместимости...",
"instructions": "Инструкция для устройств Apple.",
"install": "Установка",
"dismiss": "Отклонить"
},
"discord": {
"join_short": "Присоединяйтесь к крутым ребятам!",
"join_long": "Присоединяйся к супер-дупер крутым ребятам!",
"action": "Discord"
},
"hero": {
"credit": "От {name} ❤️"
"credit": "От %s1 ❤️"
},
"hot_tanks": {
"title": "Популярные танки",
"subtitle": "За последнее {days} дней"
"subtitle": "За последнее %s1 дней"
},
"other_projects": {
"title": "Другие классные проекты"
},
"help_translate": "Пожалуйста, помогите перевести {name} на {language}!",
"help_translate": "Пожалуйста, помогите перевести %s1 на %s2!",
"seo_description": "Инструменты для World of Tanks Blitz"
},
"docs": {
@@ -760,16 +764,8 @@
}
},
"tools": {
"mixer": {
"name": "Смешать",
"button": "Смешать и собрать",
"description": "Модули смешанного танка",
"seo_description": "Смешивание и сбор корпусов, башен и пушек танков из World of Tanks Blitz",
"screenshot": "Скриншот",
"randomize": "Случайно"
},
"guess": {
"name": "Угадай танк",
"name": "Угадайте танк",
"button": "Угадать",
"description": "Мини-игра «Силуэт»",
"seo_description": "Играйте угадывая силуэт танка из World of Tanks Blitz",
@@ -780,7 +776,7 @@
"next": "Следующий",
"no_results": "Ничего не найденo"
},
"stats": "{correct} / {total} угадано верно"
"stats": "%s1 / %s2 угадано верно"
},
"tankopedia": {
"search": "Поиск характеристик...",
@@ -788,14 +784,11 @@
"button": "Поиск танков",
"description": "Танковая энциклопедия Blitz",
"seo_description": "Статистика, броня и многое другое по всем танкам World of Tanks Blitz",
"tank_seo_title": "{name} — {class} {nationality} {tier} уровня | Танкопедия {website} | World of Tanks Blitz (WoTB)",
"tank_seo_description": "Броня, статистика, оборудование, снабжение {name} и многое другое. DPM {dpm}, альфа {alpha}оп, пробитие{penetration}мм, лучшая скорость{speed}км/ч, здоровье {health}оп.",
"tank_seo_title": "%s1 - %s4 %s3 %s2 уровня | Танкопедия %s5 | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 броня, статистика, оборудование, снабжение и многое другое. %s2 УВМ, %s3hp альфа, %s4mm пробитие, %s5км/ч лучшая скорость, %s6hp здоровье.",
"visualizers": {
"reload": {
"skip": "Пропуск"
},
"assault": {
"title": "Штурмовые снаряды"
"title": "Assault shells"
},
"rotator": {
"hull": "Корпус",
@@ -813,15 +806,10 @@
"no_ricochet": "Разрывные снаряды не рикошетят"
},
"view_range": {
"camo": "Камуфляж",
"camo": "Камо",
"percentage": "%s1%",
"ghost_factory": "Промзона",
"dead_rail": "Эшелон"
},
"flexibility": {
"yaw": "Поворот",
"pitch": "Наклон",
"min": "Вниз",
"max": "Вверх"
}
},
"sandbox": {
@@ -835,7 +823,9 @@
"green_penetration": "Зелёное пробитие",
"hide_model_under_armor": "Скрыть модель под броней",
"opaque": "Прозрачность",
"dev_wireframe": "DEV: wireframe"
"dev_wireframe": "DEV: wireframe",
"environment": "Окружение",
"show_grid": "Показать сетку"
},
"screenshot": {
"download": "Загрузить",
@@ -857,6 +847,7 @@
"reset": "Сброс"
},
"advanced": "Продвинутая подсветка",
"shooter": "Стреляет:",
"shot_card": {
"element": {
"primary": "Броня",
@@ -867,15 +858,15 @@
"track": "Гусеницы"
},
"status": {
"penetration": "Пробитие на {damage}оп",
"penetration": "Пробитие на %s1оп",
"blocked": "Не пробил",
"ricochet": "Рикошет",
"splash": "Урон осколками на {damage}оп"
"splash": "Урон осколками на %s1оп"
},
"stats": {
"thickness_and_angle": "{thickness}мм {angle}°",
"nominal": "{thickness}1мм номинала",
"ricochet_loss": "{loss}% пробития"
"thickness_and_angle": "%s1мм %s2°",
"nominal": "%s1мм номинала",
"ricochet_loss": "%s1% пробития"
}
}
},
@@ -905,7 +896,7 @@
"tier": "Уровень",
"type": "Тип",
"dev_id": "DEV: ID",
"purchase": "Цена покупки",
"purchase": "Стоимость покупки",
"restoration": "Цена восстановления",
"sale": "Цена продажи",
"research": "Необходимо опыта"
@@ -913,15 +904,16 @@
"tech_tree": {
"title": "Дерево танков",
"battle": "1 бой",
"battles": "{count} боёв",
"route": "Ветка {index} из {count}"
"battles": "%s1 боёв",
"route": "Ветка %s1 из %s2"
},
"game_modes": {
"title": "Способности в игровых режимах"
},
"review": {
"title": "Видеообзоры",
"join": "Предложить ютубера для программы {name}"
"disclaimer": "BlitzKit не несёт ответственности за видеообзоры.",
"join": "Предложить ютубера для программы %s1"
},
"configuration": {
"title": "Конфигурация",
@@ -957,15 +949,15 @@
"title": "Характеристики",
"compare": {
"label": "Сравнивать с",
"class": "{class} {tier} уровня",
"tier": "Все {tier} уровня",
"class": "%s2 %s1 уровня",
"tier": "Все %s1 уровня",
"all": "Все танки"
},
"values": {
"shell_index": "Снаряд {index}",
"at_distance": "На {distance}м",
"shell_index": "Снаряд %s1",
"at_distance": "На %s1м",
"at_distance_slider_label": "Дистанция",
"dispersion_at_distance": "Разброс на {distance}м",
"dispersion_at_distance": "Разброс на %s1м",
"gun_flexibility": "Углы наводки",
"speed": "Скорость",
"terrain_coefficients": "Сопротивление грунтам",
@@ -981,13 +973,13 @@
"dpmEffective": "Эффективность за 60с",
"shells": "Снарядов в барабане",
"mostOptimalShellIndex": "Самый эффективный снаряд",
"shellReloads": "Перезарядка",
"shellReloads": "Перезарядка снарядов",
"shellReload": "Перезарядка",
"intraClip": "Между снарядами",
"caliber": "Калибр",
"penetration": "Пробитие",
"damage": "Урон",
"damageWithoutAssault": "Урон без штурмовых снарядов",
"damageWithoutAssault": "Damage without assault shells",
"clipDamage": "Урон с барабана",
"moduleDamage": "Урон по модулям",
"explosionRadius": "Разлет осколков",
@@ -1040,9 +1032,7 @@
"shellRange": "Дальность снаряда",
"shellCapacity": "Боезапас",
"gunType": "Тип орудия",
"penetrationAt250m": "Пробитие на 250м",
"burstShells": "Кол-во разрывных снарядов",
"burstInterShell": "Взрыв между снарядами"
"penetrationAt250m": "Пробитие на 250м"
}
},
"firepower": {
@@ -1114,9 +1104,9 @@
"description": "Статистика эффективности танков",
"seo_description": "Лучшие танки и их эффективность в World of Tanks Blitz",
"estimation": {
"body": "Примерная статистика на основе {count} игроков. Обновлено {age} назад.",
"hours": "{count} часов",
"minutes": "{count} минут"
"body": "Примерная статистика на основе %s1 игроков. Обновлено %s2 назад.",
"hours": "%s1 часов",
"minutes": "%s1 минут"
},
"player_count": {
"label": "Количество игроков"
@@ -1139,7 +1129,7 @@
"hits": "Уничтожен",
"damage_ratio": "Коэф. урона",
"damage_taken": "Получено урона",
"capture_points": "Точки ограничения"
"capture_points": "Ограничительные точки"
}
}
},
@@ -1148,10 +1138,10 @@
"button": "Просмотреть",
"description": "Аватары игрока Blitz",
"seo_description": "Все аватары игрока World of Tanks Blitz",
"last_updated": "Последнее обновление {time}",
"last_updated": "Последнее обновление %s1",
"search": {
"hint": "Поиск аватаров...",
"results": "Аватаров: {count}"
"results": "%s1 аватар"
}
},
"session": {
@@ -1160,14 +1150,14 @@
"description": "Статистика сеанса игры",
"seo_description": "Отслеживайте свою статистику в реальном времени во время игры World of Tanks Blitz",
"search": {
"lookup": "Введите ник игрока",
"lookup": "Искать игрока, чтобы начать",
"hint": "Поиск игроков...",
"searching": "Поиск...",
"no_results": "Игроков не найдено"
},
"controls": {
"title": "Отслеживание {name}",
"subtitle": "Начиная с {time}",
"title": "Отслеживание %s1",
"subtitle": "Начиная с %s1",
"reset": "Сброс",
"change": "Изменить"
},
@@ -1201,7 +1191,7 @@
"button": "Создать",
"description": "Встраиваемые элементы для стримов",
"seo_description": "Создать вставки для стримов по World of Tanks Blitz с невероятной кастомизации",
"coming_soon": "В скором времени появятся новые встраиваемые элементы.",
"coming_soon": "Скоро появятся новые встраиваемые элементы.",
"suggest": "Предложить идеи.",
"preview": {
"title": "Предпросмотр",
@@ -1227,7 +1217,7 @@
"cancel": "Отмена",
"success": {
"title": "Скопировать URL",
"info": "Сгенерирован URL для {name}. Встройка начнет отслеживание, как только URL будет вставлен. Можно сбросить, щелкнув правой кнопкой мыши на ПК или долгим нажатием на мобильном телефоне и выбором «Сброс».",
"info": "Сгенерирован URL для %s1. Встройка начнет отслеживание, как только URL будет вставлен. Можно сбросить, щелкнув правой кнопкой мыши на ПК или долгим нажатием на мобильном телефоне и выбором «Сброс».",
"back": "Назад",
"copy": "Скопировать URL"
}
@@ -1310,11 +1300,17 @@
"description": "Начните с BlitzKit",
"seo_description": "Начните с BlitzKit"
},
"api": {
"name": "API",
"button": "К документации",
"description": "API сервера BlitzKit",
"seo_description": "Статический API сервера BlitzKit для World of Tanks Blitz"
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Анализируй",
"description": "Богатая статистика",
"seo_description": "Богатые статистика по World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Открой для себя",
"description": "Статистика сеанса игры",
"seo_description": "Статистика сеанса игры в World of Tanks Blitz"
}
},
"footer": {
+111 -115
View File
@@ -1,19 +1,18 @@
{
"test": {
"default": "тест",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is {fruit} because it has the color of {color}. I like it so much that I will say its name 3 times! {fruit}! {fruit}! {fruit}! Once again, it's {color}, not {not_color}.",
"long": "This is a long test string. You MUST NEVER see this in the UI. My favorite fruit is %s1 because it has the color of %s2. I like it so much that I will say its name 3 times! %s1! %s1! %s1! Once again, it's %s2, not %s3.",
"fox": "Швидка бура лисиця перестрибує через лінивого собаку."
},
"common": {
"name": "BlitzKit",
"units": {
"m": "{value}m",
"mm": "{value}mm",
"deg": "{value}°",
"hp": "{value}hp",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}°/s"
"m": "%s1м",
"mm": "%s1мм",
"deg": "%s1°",
"hp": "%s1хп",
"x": "х%s1",
"percentage": "%s1%",
"deg_s": "%s1°/с"
},
"gun_types": {
"auto_loader": "Магазинна система заряджання",
@@ -186,11 +185,11 @@
"bot": {
"common": {
"errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.",
"clan_not_found": "# Не вдалося знайти клан\nНе вдалося знайти клан \"%s1\". Спробуйте вибрати клан з пошукового результату.",
"player_not_found": "# Не вдалося знайти користувача\nЯ не зміг знайти користувача \"%s1\". Спробуйте вибрати користувача з результату пошуку, ввести коректний нікнейм, або використати команду '/link'.",
"player_not_linked": "# Використайте команду \"/link\" щоб почати\nЯ не знаю, хто ви, але ви можете сказати мені сьогодні!",
"tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.",
"tank_name_not_found": "# Танк не знайдено\nНе вдалось знайти танк за ім'ям \"%s1\".",
"tank_id_not_found": "# Танк не знайдено\nНе вдалось знайти танк за ID \"%s1\". Спробуйте ввести назву танка замість числа.",
"no_tank_stats": "# Немає статистики для танка\nЦей гравець не має статистики.\nМожливо, це не той гравець, якого ви шукаєте.",
"get_help": "Допомогти Discord серверу",
"uncaught_error": "# Упс! BlitzKit зіткнувся з помилкою!\nНам дуже шкода. Можете попросити допомоги на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH).",
@@ -284,19 +283,19 @@
"name": "рівень",
"description": "Рівень",
"choices": {
"tier": "Tier {tier_1} - {tier_2}"
"tier": "Рівень %s1 - %s2"
}
}
},
"periods": {
"career": "Кар'єра",
"today": "Сьогодні",
"days": "{days} days",
"custom": "{day_1} - {day_2} days"
"days": "%s1 днів",
"custom": "%s1 - %s2 днів"
},
"filters": {
"none": "Немає фільтрів",
"tier": "Tier {tier}"
"tier": "Рівень %s1"
},
"no_data": {
"battles_in_period": "За цей період боїв не було",
@@ -319,6 +318,22 @@
}
},
"commands": {
"auction": {
"name": "аукціон",
"description": "Танки доступні зараз на аукціоні",
"body": {
"title": "Бліц Аукціон",
"subtitle": "%s1 доступно • наступна ціна %s2\nПоточна ставка: %s3",
"next": "Наступна ціна:%s1",
"available": "Доступно: %s1 / %s2"
},
"errors": {
"no_auction": "# Немає поточного аукціону\n\nНаразі немає танків, доступних для купівлі на аукціоні. Якщо ви вважаєте це помилкою, будь ласка, повідомте про цю невідповідність на [офіційний Дискорд сервер](https://discord.gg/nDt7AjGJQH)."
},
"actions": {
"bid": "Поточна ставка"
}
},
"replay": {
"name": "реплей",
"description": "Показати результати реплею"
@@ -339,19 +354,19 @@
"body": {
"subtitle": "Дослідження",
"total": "Загалом",
"research": "Research: {cost}",
"upgrades": "Upgrades: {cost}",
"purchase": "Purchase: {cost}",
"equipment": "Equipment: {cost}",
"research": "Дослідження: %s1",
"upgrades": "Покращення: %s1",
"purchase": "Придбання: %s1",
"equipment": "Спорядження: %s1",
"estimation": "# Це оцінка!\n\nЦей список може включати вже досліджені танки через неточну інформацію для вантажу. Спробуйте використати \"початковий танк\" з рядка за допомогою команди."
},
"errors": {
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
"unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank."
"non_tech_tree": "# %s1 не в технічному дереві\n\nВи не можете досліджувати преміум/колекційний танк.",
"tier_1": "# %s1 це рівень I\n\nТанки рівню I завжди відкриті для всіх гравців безкоштовно.",
"already_researched": "# %s1 уже досліджено!\n\nСпробуйте забезпечити `starting-tank` коли використовуєте команду щоб подивитись підсумки гілки, яку ви вже дослідили.",
"start_end_equal": "# %s1 було надано двічі\n\nПереконайся, що `starting-tank` та `target-tank` різні.",
"tanks_not_on_line": ".",
"unordered": ""
}
},
"about": {
@@ -394,7 +409,7 @@
"wn8": "WN8",
"winrate": "Відсоток перемог",
"damage": "Шкода",
"unknown_tank": "Unknown tank {id}"
"unknown_tank": "Невідомий танк %s1"
}
},
"debug": {
@@ -430,12 +445,12 @@
"options": {
"threshold": {
"name": "поріг",
"description": "The number of days inactive (default: {days} days)"
"description": "Кількість днів неактивності (за замовчуванням: %s1 днів)"
}
},
"body": {
"subtitle": "Inactive for {days}+ days",
"listing": "{days} days"
"subtitle": "Неактивний %s1+ днів",
"listing": "%s1 днів"
}
},
"full_stats": {
@@ -478,7 +493,7 @@
"body": {
"accounts_linked": "Прив'язані акаунти",
"no_role_permissions": {
"title": "{name} failed to verify",
"title": "%s1 не вдалося перевірити",
"description": "У мене немає дозволу керувати ролями."
}
},
@@ -493,11 +508,11 @@
"options": {
"limit": {
"name": "ліміт",
"description": "The max number of clan results (default: {limit})"
"description": "Максимальна кількість результатів клану (за замовчуванням: %s1)"
}
},
"body": {
"title": "Clan search for {search} in region {region}",
"title": "Clan search for %s1 in region %s2",
"no_results": "Жодного клану не знайдено"
}
},
@@ -507,11 +522,11 @@
"options": {
"limit": {
"name": "ліміт",
"description": "The max number of player results (default: {limit})"
"description": "The max number of player results (default: %s1)"
}
},
"body": {
"title": "Player search for {search} in region {region}",
"title": "Player search for %s1 in region %s2",
"no_results": "Гравця не знайдено"
}
},
@@ -531,13 +546,13 @@
"options": {
"limit": {
"name": "ліміт",
"description": "How many neighbors to display (default: {count})"
"description": ""
},
"season": {
"name": "сезон",
"description": "Базова інформація про гравця",
"choices": {
"current": "{season} (current)"
"current": "%s1 (current)"
}
},
"league": {
@@ -546,12 +561,12 @@
}
},
"body": {
"top_players": "Top {count} players",
"no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.",
"top_players": "Top %s1 players",
"no_participation": "# %s1 didn't play rating in season %s2\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 rating season.",
"deleted_player": "Deleted {id}",
"deleted_player": "Deleted %s1",
"neighbors": "Rating neighbors",
"more": "+ {count} more"
"more": "+ %s1 more"
}
},
"today": {
@@ -566,14 +581,14 @@
"name": "отримані танки",
"description": "Показує всі ваші танки даного рівня",
"body": {
"title": "# {name}'s owned tier {tier} tanks\n\n"
"title": "# %s1's owned tier %s2 tanks\n\n"
}
},
"permissions": {
"name": "дозволи",
"description": "Checks wether the bot has the required permissions to function",
"body": {
"title": "# Permissions\n\n{body}",
"title": "# Permissions\n\n%s1",
"view_channels": "View channels: needed to find the message the refresh button was used on",
"read_message_history": "Read message history: needed to find the message to refresh",
"attach_files": "Attach files: needed to add images to image-based commands",
@@ -585,7 +600,7 @@
"description": "Check wether the bot is alive.",
"body": {
"pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - {time}ms"
"pong_with_time": "Pong 🏓 - %s1ms"
},
"subcommands": {
"blitzkit": {
@@ -602,7 +617,7 @@
"name": "player-info",
"description": "Базова інформація про гравця",
"body": {
"title": "{name}'s information",
"title": "%s1's information",
"nickname": "Нік",
"battles": "Бої",
"winrate": "Відсоток перемог",
@@ -631,7 +646,7 @@
}
},
"errors": {
"no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"no_participation": "# %s1 hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"unrecorded_stats": "# Statistics currently unavailable\n\nWargaming may be currently be facing server downtime. If there is an on-going season, statistics should be available withing 24 hours."
}
}
@@ -656,16 +671,13 @@
"copied": "Скопійовано!"
},
"tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Шукайте техніку...",
"recent": "Недавно переглянуті",
"select_all": "Вибрати все",
"count_singular": "1 танк",
"count_plural": "{count} tanks",
"sorting_by": "Sorting by {name}",
"count_plural": "%s1 танків",
"sorting_by": "Сортувати за %s1",
"no_tanks_found": {
"body": "Нічого не знайдено.",
"search_again": "Спробуйте шукати заново.",
@@ -727,30 +739,22 @@
}
},
"home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": {
"join_short": "Join cool people!",
"join_long": "Join super duper cool people!",
"action": "Discord"
},
"hero": {
"credit": "By {name} ❤️"
"credit": "Зроблено %s1 ❤"
},
"hot_tanks": {
"title": "Популярні танки",
"subtitle": "Last {days} days"
"subtitle": "Останні %s1 дні(в)"
},
"other_projects": {
"title": "Інші класні проєкти"
},
"help_translate": "Please help translate {name} to {language}!",
"help_translate": "Будь ласка, допоможіть перекласти %s1 на %s2!",
"seo_description": "Інструменти для всього, пов'язаного з WoT Blitz"
},
"docs": {
@@ -760,14 +764,6 @@
}
},
"tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -780,7 +776,7 @@
"next": "Next",
"no_results": "Немає результатів"
},
"stats": "{correct} / {total} guessed correctly"
"stats": "%s1 / %s2 guessed correctly"
},
"tankopedia": {
"search": "Search a stat...",
@@ -788,12 +784,9 @@
"button": "Знайти танки",
"description": "Танкова енциклопедія",
"seo_description": "Характеристики, броня та інше для всіх танків WoT Blitz",
"tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "{name} armor, statistics, equipment, provisions, and more. {dpm} DPM, {alpha}hp alpha, {penetration}mm penetration, {speed}kph top speed, {health}hp health.",
"tank_seo_title": "%s1 - Рівень %s2 %s3 %s4 | %s5 Танкопедія | WoT Blitz",
"tank_seo_description": "%s1 броня, характеристики, обладнання, амуніція та більше. %s2 шкода на хвилину, %s3 разова шкода, %s4 мм пробиття, %s5 км/год максимальна швидкість, %s6 міцність.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -814,14 +807,9 @@
},
"view_range": {
"camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -835,7 +823,9 @@
"green_penetration": "Зелене пробиття",
"hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque",
"dev_wireframe": "DEV: wireframe"
"dev_wireframe": "DEV: wireframe",
"environment": "Environment",
"show_grid": "Show grid"
},
"screenshot": {
"download": "Завантажити",
@@ -857,6 +847,7 @@
"reset": "Reset"
},
"advanced": "Advanced highlighting",
"shooter": "Shooter:",
"shot_card": {
"element": {
"primary": "Primary",
@@ -867,15 +858,15 @@
"track": "Tracks"
},
"status": {
"penetration": "{damage}hp penetration",
"penetration": "%s1hp penetration",
"blocked": "Blocked",
"ricochet": "Рикошет",
"splash": "{damage}hp splash"
"splash": "%s1hp splash"
},
"stats": {
"thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "{thickness}mm nominal",
"ricochet_loss": "{loss}% penetration"
"thickness_and_angle": "%s1mm %s2°",
"nominal": "%s1mm nominal",
"ricochet_loss": "%s1% penetration"
}
}
},
@@ -913,15 +904,16 @@
"tech_tree": {
"title": "Звичайний",
"battle": "1 бій",
"battles": "{count} battles",
"route": "Route {index} of {count}"
"battles": "%s1 battles",
"route": "Route %s1 of %s2"
},
"game_modes": {
"title": "Game mode abilities"
},
"review": {
"title": "Review videos",
"join": "Apply to {name} YouTuber program"
"disclaimer": "BlitzKit does not endorse any review videos.",
"join": "Apply to %s1 YouTuber program"
},
"configuration": {
"title": "Configuration",
@@ -957,15 +949,15 @@
"title": "Characteristics",
"compare": {
"label": "Compare against",
"class": "Tier {tier} {class}",
"tier": "All of tier {tier}",
"class": "Tier %s1 %s2",
"tier": "All of tier %s1",
"all": "Усі танки"
},
"values": {
"shell_index": "Shell {index}",
"at_distance": "At {distance}m",
"shell_index": "Снаряд %s1",
"at_distance": "At %s1m",
"at_distance_slider_label": "Відстань",
"dispersion_at_distance": "Dispersion at {distance}m",
"dispersion_at_distance": "Dispersion at %s1m",
"gun_flexibility": "Gun flexibility",
"speed": "Швидкість",
"terrain_coefficients": "Terrain coefficients",
@@ -981,8 +973,8 @@
"dpmEffective": "Effective at 60s",
"shells": "Снаряди",
"mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Reloads",
"shellReload": "Час перезаряджання",
"shellReloads": "Shell reloads",
"shellReload": "Shell reload",
"intraClip": "Intra-clip",
"caliber": "Калібр",
"penetration": "Бронепробиття",
@@ -1040,9 +1032,7 @@
"shellRange": "Shell range",
"shellCapacity": "Shell capacity",
"gunType": "Тип гармати",
"penetrationAt250m": "Penetration at 250m",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
"penetrationAt250m": "Penetration at 250m"
}
},
"firepower": {
@@ -1114,9 +1104,9 @@
"description": "Tank performance statistics",
"seo_description": "Best tanks and their performances in World of Tanks Blitz",
"estimation": {
"body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "{count} hours",
"minutes": "{count} minutes"
"body": "Estimated stats based on %s1 players. Updated %s2 ago.",
"hours": "%s1 hours",
"minutes": "%s1 minutes"
},
"player_count": {
"label": "Player count"
@@ -1148,10 +1138,10 @@
"button": "Browse all",
"description": "Blitz player avatars",
"seo_description": "All World of Tanks Blitz player avatars",
"last_updated": "Last updated on {time}",
"last_updated": "Last updated on %s1",
"search": {
"hint": "Search avatars...",
"results": "{count} avatars"
"results": "%s1 avatars"
}
},
"session": {
@@ -1166,8 +1156,8 @@
"no_results": "Гравця не знайдено"
},
"controls": {
"title": "Tracking {name}",
"subtitle": "Since {time}",
"title": "Відстежуємо %s1",
"subtitle": "Від %s1",
"reset": "Скинути",
"change": "Змінити"
},
@@ -1227,7 +1217,7 @@
"cancel": "Cancel",
"success": {
"title": "Copy URL",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"info": "Generated the URL for %s1. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"back": "Назад",
"copy": "Copy URL"
}
@@ -1310,11 +1300,17 @@
"description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit"
},
"api": {
"name": "API",
"button": "To the docs",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
"blitz_analysis": {
"name": "BlitzAnalysis[]",
"button": "Analyze now",
"description": "Rich historic average stats",
"seo_description": "Rich historic average stats in World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Discover today",
"description": "In-game session stats",
"seo_description": "In-game session stats in World of Tanks Blitz"
}
},
"footer": {
Loaded 100 of 466 files, more files were not shown because too many files have changed in this diff. Show more