Compare commits

...
145 Commits
Author SHA1 Message Date
TrèsAbhi c6b006c2ad New translations en.json (Chinese Simplified) 2025-10-11 22:11:51 -05:00
TrèsAbhi 3ec2425597 New translations en.json (Ukrainian) 2025-10-11 22:11:50 -05:00
TrèsAbhi 7785e50783 New translations en.json (Russian) 2025-10-11 22:11:49 -05:00
TrèsAbhi 5a171daa10 New translations en.json (Portuguese) 2025-10-11 22:11:47 -05:00
TrèsAbhi 9843713dad New translations en.json (Polish) 2025-10-11 22:11:46 -05:00
TrèsAbhi ef14d7a28f New translations en.json (Japanese) 2025-10-11 22:11:45 -05:00
TrèsAbhi fcc2d49859 New translations en.json (Spanish) 2025-10-11 22:11:44 -05:00
TrèsAbhi d9e90cdd06 New translations en.json (French) 2025-10-11 22:11:43 -05:00
TrèsAbhi a1e45c2af7 New translations en.json (Russian) 2025-10-05 05:52:38 -05:00
TrèsAbhi 4744eebf3b New translations en.json (Russian) 2025-10-05 04:52:55 -05:00
TrèsAbhi 1e996cb970 New Crowdin updates (#172) 2025-10-04 22:15:25 -05:00
TrèsAbhi 8639b31b8f feat: logs 2025-10-04 22:13:50 -05:00
TrèsAbhi 889a92e98b fix: lighting toggler no longer fires on unload 2025-10-04 22:05:36 -05:00
TrèsAbhi e7fe3a4baf fix: signs of tankiopedia transofmr controls 2025-10-04 22:02:24 -05:00
TrèsAbhi ab10343704 fix: clearer controls of tankoepdia 2025-10-04 21:53:18 -05:00
TrèsAbhi 9412be2d7c feat: filter owned tanks 2025-10-04 21:35:12 -05:00
TrèsAbhi 66cf590f1c fix: hook call outside parent hook 2025-10-04 21:33:28 -05:00
TrèsAbhi 7c18549c00 fix: full screen table 2025-10-02 10:39:36 -05:00
TrèsAbhi 9b97d3e27d feat: guesser flag icons 2025-10-02 10:29:43 -05:00
TrèsAbhi aadc252e47 fix: use new quicklime controls enabled 2025-10-02 10:19:50 -05:00
TrèsAbhi 320a764d45 feat: migrate all 2025-10-02 10:15:38 -05:00
TrèsAbhi 5594c0ec3b feat: some progress 2025-10-01 22:45:21 -05:00
TrèsAbhi 3d69d464f5 feat: start on amending slugs 2025-10-01 22:41:23 -05:00
TrèsAbhi dd0b2bade7 feat: named slugs 2025-10-01 22:41:15 -05:00
TrèsAbhi 99c90cfa9e Merge branch 'dev' of https://github.com/blitzkit/blitzkit into dev 2025-10-01 22:24:10 -05:00
TrèsAbhi dd3ce21508 feat: gun rotation 2025-10-01 22:24:08 -05:00
TrèsAbhi b512cb2292 Update README.md 2025-09-30 20:22:06 -05:00
TrèsAbhi 907e71ce03 feat: turret rotation 2025-09-30 16:48:37 -05:00
TrèsAbhi 10942879a6 feat: start of rotation 2025-09-30 16:34:08 -05:00
TrèsAbhi d60551d30a fix: change blitzkit icons to big icons 2025-09-29 13:41:52 -05:00
TrèsAbhi b8c8528e1c Merge branch 'main' into dev 2025-09-29 13:39:13 -05:00
TrèsAbhi 0c9643016a feat: logs 2025-09-29 13:37:00 -05:00
TrèsAbhi 61d1ebbad6 fix: redirect 2025-09-29 13:35:47 -05:00
TrèsAbhi e9b7059cc0 feat: some linking 2025-09-29 13:30:10 -05:00
TrèsAbhi a359897a2a New Crowdin updates (#171) 2025-09-29 13:29:58 -05:00
TrèsAbhi e5f7d72df3 feat: rearrange 2025-09-28 14:11:33 -05:00
TrèsAbhi 57092d8df0 feat: env vars 2025-09-28 14:10:34 -05:00
TrèsAbhi 86508d5413 feat: submodules and packages 2025-09-28 13:44:15 -05:00
TrèsAbhi 33373c286f feat: logs 2025-09-28 13:13:08 -05:00
TrèsAbhi 7443f75347 fix: less aggressive light manager 2025-09-28 13:12:16 -05:00
TrèsAbhi d09f934b80 feat: start app 2025-09-28 13:09:03 -05:00
TrèsAbhi 7f919ebb87 feat: remove pwa 2025-09-28 13:02:48 -05:00
TrèsAbhi 0929825555 fix: glob and cache 2025-09-27 19:13:29 -05:00
TrèsAbhi 4de0fc5ec1 feat: concurrency 2025-09-27 18:37:51 -05:00
TrèsAbhi e66610f86e fix: side and out 2025-09-27 18:32:18 -05:00
TrèsAbhi 674abc4923 fix: dead code 2025-09-27 18:28:35 -05:00
TrèsAbhi 25cee8a36b fix: icons 2025-09-27 18:26:37 -05:00
TrèsAbhi dcbac5e017 fix: missing package 2025-09-27 18:16:05 -05:00
TrèsAbhi 0c94d42f3f fix: missing package 2025-09-27 18:15:00 -05:00
TrèsAbhi 7ab5e94323 fix: missing package 2025-09-27 18:13:59 -05:00
TrèsAbhi f9ea5ba74a fix: missing package 2025-09-27 18:13:00 -05:00
TrèsAbhi 1e7162d2da fix: missing package 2025-09-27 18:11:49 -05:00
TrèsAbhi cfba53a655 fix: missing package 2025-09-27 18:10:20 -05:00
TrèsAbhi 77a85641f0 fix: missing package 2025-09-27 18:09:10 -05:00
TrèsAbhi 5330a27e96 feat: logs 2025-09-27 18:07:43 -05:00
TrèsAbhi a791aff4a0 Merge branch 'main' into dev 2025-09-27 18:03:37 -05:00
TrèsAbhi df8a5967f0 feat: start of logs 2025-09-27 18:03:20 -05:00
TrèsAbhi 6261dfc9cd fix: more linting 2025-09-27 18:01:20 -05:00
TrèsAbhi 766801826c fix: canvases 2025-09-27 17:51:29 -05:00
TrèsAbhi 7b728596ca feat: storeDeferred 2025-09-27 17:32:20 -05:00
TrèsAbhi 0bf6ac33a5 feat: more promotion 2025-09-27 17:29:12 -05:00
TrèsAbhi 9a33b84b1c feat: install button 2025-09-27 17:05:06 -05:00
TrèsAbhi c14efe86cc fix: update errors 2025-09-27 16:07:45 -05:00
TrèsAbhi 4ff9a2ec7f feat: more pwa entries 2025-09-27 15:51:46 -05:00
TrèsAbhi 75d7b4554b feat: start of pew 2025-09-27 15:38:37 -05:00
TrèsAbhi 27c8b348bb feat: favicons 2025-09-27 13:39:08 -05:00
TrèsAbhi a55ab5e7ee feat: redo base setup 2025-09-27 13:34:11 -05:00
TrèsAbhi 12e90cc6ee feat: update dependencies 2025-09-26 14:51:49 -05:00
TrèsAbhi 01755a2a64 feat: start adding pwa 2025-09-26 14:46:07 -05:00
TrèsAbhi 080f0a0eb7 fix: make tankopedia hero section a bit smaller 2025-09-22 13:52:31 -05:00
TrèsAbhi a646cc1e57 feat: mixer buttons 2025-09-22 13:50:46 -05:00
TrèsAbhi d107813c37 feat: update closed 2025-09-22 13:27:38 -05:00
TrèsAbhi 27d3bbf346 feat: update closed 2025-09-22 13:20:47 -05:00
TrèsAbhi 675a613a0e New translations en.json (Russian) (#170) 2025-09-22 10:06:08 -05:00
TrèsAbhi 50c77e0ae1 fix: seo 2025-09-18 10:43:38 -05:00
TrèsAbhi 6e0429332d feat: mixer tank selector 2025-09-18 10:41:42 -05:00
TrèsAbhi 7afce6cbcb fix: keys 2025-09-18 10:19:41 -05:00
TrèsAbhi 17c7a5c7f5 feat: start of selector 2025-09-13 23:57:10 -05:00
TrèsAbhi f5aa2ff9de feat: reenable mixer 2025-09-13 23:42:26 -05:00
TrèsAbhi 96a8f225fc feat: start on api 2025-09-13 23:07:58 -05:00
TrèsAbhi d15a5aee69 fix: no overflow navbar on certain devices 2025-09-13 22:54:45 -05:00
TrèsAbhi 84dc5e4960 feat: data-driven tools 2025-09-13 22:53:29 -05:00
TrèsAbhi 58b5ee7f83 feat: data-driven tools 2025-09-13 21:54:43 -05:00
TrèsAbhi 8a99d34af9 fix: check intialization this mount 2025-09-13 21:33:16 -05:00
TrèsAbhi 7cd7bd83dc Merge branch 'main' into dev 2025-09-13 21:28:58 -05:00
TrèsAbhi c29d36a4b7 feat: update to main 2025-09-13 10:03:22 -05:00
TrèsAbhi 7d3b6221d7 feat: remake decodeAny 2025-09-13 10:02:46 -05:00
TrèsAbhi fd66f9ab17 feat: compiler based client 2025-09-12 16:49:26 -05:00
TrèsAbhi 6fbb0e81cb simplify error handling 2025-09-12 14:08:53 -05:00
TrèsAbhi 6dc82588c1 feat: cleanup 2025-09-12 13:42:33 -05:00
TrèsAbhi c6eae9ad51 feat: update closed 2025-09-12 13:29:10 -05:00
TrèsAbhi ebdd7629ff update 2025-09-12 13:02:43 -05:00
TrèsAbhi 646bccf370 feat: logs 2025-09-07 16:38:07 -05:00
TrèsAbhi 86206f2d89 Merge branch 'main' into dev 2025-09-07 16:29:28 -05:00
TrèsAbhi bc0dd87e31 Merge branch 'main' into dev 2025-09-07 16:29:09 -05:00
TrèsAbhi e26bb4efe0 feat: some changes to lighting... again 2025-09-06 12:40:35 -05:00
TrèsAbhi 66cf1fd223 feat: baked ao 2025-09-06 00:48:39 -05:00
TrèsAbhi 335cd4d699 feat: rewrite some systems 2025-09-05 23:34:59 -05:00
TrèsAbhi 95fa5bc1a5 fix: use base color map 2025-09-05 20:45:04 -05:00
TrèsAbhi 50eac9eba6 New translations en.json (French) (#169) 2025-09-04 08:04:17 -05:00
TrèsAbhi 1598ae973f feat: auto framerate 2025-09-02 22:44:46 -05:00
TrèsAbhi 69f3a95379 feat: use armor model 2025-09-02 22:16:48 -05:00
TrèsAbhi 3846cbab4a fix: save persistent data 2025-09-02 16:31:11 -05:00
TrèsAbhi 30e87743fc feat: upgrade rest of them 2025-09-02 16:24:13 -05:00
TrèsAbhi fe4afdcbf0 feat: upgrade CompareEphemeral 2025-09-01 23:05:22 -05:00
TrèsAbhi ef2a47e756 feat: upgrade ComparePersistent 2025-09-01 23:02:20 -05:00
TrèsAbhi c2d625e1ed feat: upgrade EmbedBreakdown 2025-09-01 23:00:44 -05:00
TrèsAbhi d33990bdb4 feat: upgrade Gallery 2025-09-01 22:58:26 -05:00
TrèsAbhi 96482d4268 feat: renaming 2025-09-01 22:56:39 -05:00
TrèsAbhi 8b527f2333 fix: last and next checking 2025-09-01 22:55:31 -05:00
TrèsAbhi f56c31fb48 feat: upgrade Guess 2025-09-01 19:22:38 -05:00
TrèsAbhi 7d164e415a feat: upgrade Mixer 2025-09-01 19:19:44 -05:00
TrèsAbhi b3a9f0889e feat: upgrade Session 2025-09-01 19:16:36 -05:00
TrèsAbhi 6d24ed92d0 feat: upgrade TankFilters 2025-09-01 19:13:08 -05:00
TrèsAbhi d287e66265 feat: upgrade tankopediaEphemeral 2025-09-01 19:01:01 -05:00
TrèsAbhi ac29d1ead0 feat: upgrade tankopediaEphemeral 2025-09-01 18:51:04 -05:00
TrèsAbhi b6c3fdc4e9 feat: upgrade performance 2025-09-01 18:16:41 -05:00
TrèsAbhi 4efdaaf7c9 feat: upgrade TankSort 2025-09-01 18:13:41 -05:00
TrèsAbhi 87b6a36520 feat: use custom state manager 2025-09-01 18:03:07 -05:00
TrèsAbhi cf38cb3a6e feat: optimize legacy redirect pages 2025-09-01 17:27:42 -05:00
TrèsAbhi 97fc386bc7 New Crowdin updates (#168)
* New translations en.json (Russian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Russian)

* New translations en.json (Russian)
2025-08-31 09:54:49 -05:00
TrèsAbhi abfb3f8ddb fix: typo 2025-08-29 23:58:47 -05:00
TrèsAbhi 9cb5113f6e feat: haha 2025-08-29 23:58:14 -05:00
TrèsAbhi 7dd1c0c32a feat: logs 2025-08-29 23:57:33 -05:00
TrèsAbhi 03233f3fdc fix: align antagonist selector filters correctly 2025-08-29 23:46:57 -05:00
TrèsAbhi 377159c298 fix: consider tank models 2025-08-29 23:41:35 -05:00
TrèsAbhi d8431fb620 fix: gap alignemnt 2025-08-29 23:28:49 -05:00
TrèsAbhi 0546ddefbd fix: better shot card alignment 2025-08-29 23:24:12 -05:00
TrèsAbhi 26f6889012 feat: revert subtler advanced highlighting coloring 2025-08-29 23:10:20 -05:00
TrèsAbhi 3ad6088565 feat: skip button for reload visualizer 2025-08-29 23:08:44 -05:00
TrèsAbhi b6fd568cc6 fix: screenshot props 2025-08-29 23:00:02 -05:00
TrèsAbhi 3861b07aef fix: remove "show grid" 2025-08-29 22:53:04 -05:00
TrèsAbhi 2bf57e2c72 feat: better framing 2025-08-29 22:51:24 -05:00
TrèsAbhi 744c6654cf feat: cooler initial position 2025-08-29 22:39:34 -05:00
TrèsAbhi b82e905757 fix: dead code 2025-08-29 13:42:44 -05:00
TrèsAbhi c42e5b7dc3 feat: logs 2025-08-29 13:32:46 -05:00
TrèsAbhi b716218ecd fix: loading bar 2025-08-29 13:31:48 -05:00
TrèsAbhi 511346e4bb fix: select 2025-08-29 13:26:34 -05:00
TrèsAbhi 5c2e49453e feat: more lighting 2025-08-29 13:21:41 -05:00
TrèsAbhi 5d397de766 feat: more progress 2025-08-28 18:23:02 -05:00
TrèsAbhi aa763f65d1 feat: more progress 2025-08-28 17:59:31 -05:00
TrèsAbhi 94cbd95509 feat: start of new lighting 2025-08-28 16:52:23 -05:00
TrèsAbhi f3762e356f feat: start of mixer 2025-08-28 16:12:40 -05:00
TrèsAbhi 5747d7549f feat: tweak a few things and enable guess game 2025-08-27 17:43:42 -05:00
TrèsAbhi aea4905d20 fix: vars 2025-08-27 17:22:06 -05:00
319 changed files with 16747 additions and 8204 deletions

No files matched your search

+13
View File
@@ -22,6 +22,19 @@ env:
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }} PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }} PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }} 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: jobs:
host: host:
+27 -2
View File
@@ -24,7 +24,32 @@ public/sitemap*
target target
# packages/core/src/protos/*.ts # packages/core/src/protos/*.ts
/.vercel /.vercel
/.astro .astro
.vercel .vercel
/src /src
/packages/bot/src/prisma /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
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-7
View File
@@ -1,7 +0,0 @@
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 80
}
+9 -7
View File
@@ -21,6 +21,7 @@
"AYUV", "AYUV",
"battleresults", "battleresults",
"bbox", "bbox",
"beforeinstallprompt",
"bepaint", "bepaint",
"betakrieg", "betakrieg",
"BINORMAL", "BINORMAL",
@@ -76,13 +77,16 @@
"echarts", "echarts",
"Everytime", "Everytime",
"FASTNAME", "FASTNAME",
"favicons",
"flexdirection", "flexdirection",
"fontsource",
"FOURCC", "FOURCC",
"frameloop", "frameloop",
"fsformat", "fsformat",
"Fullscreen", "Fullscreen",
"fullstats", "fullstats",
"fuzzysort", "fuzzysort",
"gamemode",
"gameplay", "gameplay",
"glsl", "glsl",
"GLTF", "GLTF",
@@ -154,6 +158,7 @@
"Pyogenics", "Pyogenics",
"ratingleaderboard", "ratingleaderboard",
"raycaster", "raycaster",
"Redirector",
"researchable", "researchable",
"Resvg", "Resvg",
"rizz", "rizz",
@@ -167,6 +172,7 @@
"searchtanks", "searchtanks",
"SHAREDEXP", "SHAREDEXP",
"SKLLD", "SKLLD",
"smoothstep",
"Smri", "Smri",
"Specularity", "Specularity",
"SSAO", "SSAO",
@@ -186,6 +192,7 @@
"tankopedia", "tankopedia",
"tankopedias", "tankopedias",
"tankstats", "tankstats",
"tauri",
"texconv", "texconv",
"TEXCOORD", "TEXCOORD",
"tresabhi", "tresabhi",
@@ -222,12 +229,7 @@
"yolo", "yolo",
"youtuber", "youtuber",
"youtubers", "youtubers",
"zoomable", "zoomable"
"zustand"
], ],
"deno.enable": false, "typescript.tsdk": "node_modules\\typescript\\lib"
"deno.unstable": false,
"cSpell.enableFiletypes": ["glsl", "proto3"],
"typescript.tsdk": "node_modules\\typescript\\lib",
"liveServer.settings.multiRootWorkspaceName": "blitzkit"
} }
+43 -7
View File
@@ -1,13 +1,49 @@
## BlitzKit # BlitzKit
Your premier source for all things Blitz. Tools for everything and anything, World of Tanks Blitz.
### https://blitzkit.app/ 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/.
![BlitzKit website](https://i.imgur.com/KaxUxmt.png) 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.
## Invite the bot ## Packages
Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go. | 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. |
![BlitzKit application directory](https://i.imgur.com/YWCtOuJ.png) ## Submodules
| 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#. |
## 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` | | | |
+2984
View File
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
# 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
@@ -0,0 +1,18 @@
# 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
@@ -0,0 +1,13 @@
# 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
@@ -0,0 +1,16 @@
# 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
@@ -0,0 +1,5 @@
# BlitzKit 1.28.1
## Fixes
- The lights no longer crash the Mixer tool
+5
View File
@@ -0,0 +1,5 @@
# BlitzKit 1.28.2
## Fixes
- BlitzKit no longer tries to redirect translated pages to the same page, over and over again
+18
View File
@@ -0,0 +1,18 @@
# 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
+15 -9
View File
@@ -1,7 +1,7 @@
{ {
"name": "blitzkit", "name": "blitzkit",
"private": true, "private": true,
"version": "1.27.17", "version": "1.28.3",
"description": "🎉 Tools for everything World of Tanks Blitz", "description": "🎉 Tools for everything World of Tanks Blitz",
"type": "module", "type": "module",
"types": "./types.d.ts", "types": "./types.d.ts",
@@ -16,11 +16,14 @@
"build:prisma": "prisma generate", "build:prisma": "prisma generate",
"build:sitemap": "bun scripts/sitemap.ts", "build:sitemap": "bun scripts/sitemap.ts",
"build:docs": "vitepress build docs", "build:docs": "vitepress build docs",
"build:proxyclient": "cd packages/scripts && bun run build:proxyclient",
"build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard", "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", "dev:bot": "bun run build:bot --dev && bun start:bot",
"start:bot": "bun dist/bot/main.js", "start:bot": "bun dist/bot/main.js",
"dev:docs": "vitepress dev docs", "dev:docs": "vitepress dev docs",
"dev:website": "cd packages/website && bun run dev", "dev:website": "cd packages/website && bun run dev",
"dev:app": "cd packages/app && bun run dev",
"dev": "bun run dev:website", "dev": "bun run dev:website",
"dev:api": "bun --filter=@blitzkit/api run dev", "dev:api": "bun --filter=@blitzkit/api run dev",
"setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader", "setup:depotdownloader": "bun --filter=@blitzkit/scripts run setup:depotdownloader",
@@ -41,19 +44,22 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/xmpp__client": "^0.13.3",
"@xmpp/client": "^0.13.6",
"astro-favicons": "^3.1.5",
"cheerio": "^1.1.2", "cheerio": "^1.1.2",
"googleapis": "^144.0.0", "googleapis": "^160.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"protobufjs": "^7.5.2", "protobufjs": "^7.5.4",
"quicklime": "^1.2.0", "quicklime": "^1.2.0",
"sharp": "^0.33.5" "sharp": "^0.34.4"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "^1.2.22",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.14.1", "@types/node": "^24.5.2",
"prettier": "^3.5.3", "prettier": "^3.6.2",
"prisma": "^6.6.0", "prisma": "^6.16.2",
"typescript": "^5.8.3" "typescript": "^5.9.2"
} }
} }
+18
View File
@@ -0,0 +1,18 @@
{
"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
@@ -0,0 +1,7 @@
# 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
@@ -0,0 +1,25 @@
[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
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
@@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}
+14
View File
@@ -0,0 +1,14 @@
// 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
@@ -0,0 +1,27 @@
#![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
@@ -0,0 +1,38 @@
{
"$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
@@ -0,0 +1,19 @@
{
"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
}
}
+5 -5
View File
@@ -6,14 +6,14 @@
"dependencies": { "dependencies": {
"@blitzkit/core": "workspace:*", "@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*", "@blitzkit/i18n": "workspace:*",
"satori": "^0.10.14", "satori": "^0.18.3",
"@resvg/resvg-js": "^2.6.2", "@resvg/resvg-js": "^2.6.2",
"discord.js": "^14.16.3", "discord.js": "^14.22.1",
"react": "^18.3.1", "react": "^19.1.1",
"@radix-ui/colors": "^3.0.0", "@radix-ui/colors": "^3.0.0",
"fuzzysort": "^2.0.4", "fuzzysort": "^3.1.0",
"bepaint": "^0.1.0-beta.3", "bepaint": "^0.1.0-beta.3",
"dotenv": "^16.4.5", "dotenv": "^17.2.2",
"markdown-escape": "^2.0.0" "markdown-escape": "^2.0.0"
}, },
"scripts": { "scripts": {
-144
View File
@@ -1,144 +0,0 @@
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, getTankStats,
getWN8Percentile, getWN8Percentile,
TankDefinition, TankDefinition,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { Locale } from 'discord.js'; import { Locale } from "discord.js";
import { chunk } from 'lodash-es'; import { chunk } from "lodash-es";
import * as Breakdown from '../../components/Breakdown'; import * as Breakdown from "../../components/Breakdown";
import { CommandWrapper } from '../../components/CommandWrapper'; import { CommandWrapper } from "../../components/CommandWrapper";
import { NoData } from '../../components/NoData'; import { NoData } from "../../components/NoData";
import { TitleBar } from '../../components/TitleBar'; import { TitleBar } from "../../components/TitleBar";
import { filtersToDescription } from '../../core/blitzkit/filtersToDescription'; import { filtersToDescription } from "../../core/blitzkit/filtersToDescription";
import { tankDefinitions } from '../../core/blitzkit/nonBlockingPromises'; import { tankDefinitions } from "../../core/blitzkit/nonBlockingPromises";
import { UserError } from '../../core/blitzkit/userError'; import { UserError } from "../../core/blitzkit/userError";
import { filterStats, StatFilters } from '../../core/blitzstars/filterStats'; import { filterStats, StatFilters } from "../../core/blitzstars/filterStats";
import { getStatsInPeriod } from '../../core/blitzstars/getStatsInPeriod'; import { getStatsInPeriod } from "../../core/blitzstars/getStatsInPeriod";
import { blitzStarsTankAverages } from '../../core/blitzstars/tankAverages'; import { blitzStarsTankAverages } from "../../core/blitzstars/tankAverages";
import { ResolvedPeriod } from '../../core/discord/resolvePeriodFromCommand'; import { ResolvedPeriod } from "../../core/discord/resolvePeriodFromCommand";
import { ResolvedPlayer } from '../../core/discord/resolvePlayerFromCommand'; import { ResolvedPlayer } from "../../core/discord/resolvePlayerFromCommand";
import { translator } from '../../core/localization/translator'; import { translator } from "../../core/localization/translator";
const ROWS_PER_PAGE = 8; const ROWS_PER_PAGE = 8;
const MAX_PAGES = 9; const MAX_PAGES = 9;
@@ -31,7 +31,7 @@ export async function renderBreakdown(
{ region, id }: ResolvedPlayer, { region, id }: ResolvedPlayer,
{ start, end, name }: ResolvedPeriod, { start, end, name }: ResolvedPeriod,
filters: StatFilters, filters: StatFilters,
locale: Locale, locale: Locale
) { ) {
const { strings, unwrap } = translator(locale); const { strings, unwrap } = translator(locale);
const awaitedTankDefinitions = await tankDefinitions; const awaitedTankDefinitions = await tankDefinitions;
@@ -39,7 +39,7 @@ export async function renderBreakdown(
const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale); const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale);
const { filteredOrder } = await filterStats(statsInPeriod, filters); const { filteredOrder } = await filterStats(statsInPeriod, filters);
const accountInfo = await getAccountInfo(region, id); 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 tankStats = await getTankStats(region, id);
const filterDescriptions = await filtersToDescription(filters, locale); const filterDescriptions = await filtersToDescription(filters, locale);
const orderedCurrentStats: BlitzStats[] = []; const orderedCurrentStats: BlitzStats[] = [];
@@ -74,63 +74,63 @@ export async function renderBreakdown(
orderedCareerWN8Full.push( orderedCareerWN8Full.push(
tankAveragesEntry tankAveragesEntry
? calculateWN8(tankAveragesEntry.all, tank.all) ? calculateWN8(tankAveragesEntry.all, tank.all)
: undefined, : undefined
); );
}); });
const currentBattles = orderedCurrentStats.reduce( const currentBattles = orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.battles, (accumulator, current) => accumulator + current.battles,
0, 0
); );
const careerBattles = tankStats.reduce( const careerBattles = tankStats.reduce(
(accumulator, current) => accumulator + current.all.battles, (accumulator, current) => accumulator + current.all.battles,
0, 0
); );
const currentWinrate = const currentWinrate =
orderedCurrentStats.reduce( orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.wins, (accumulator, current) => accumulator + current.wins,
0, 0
) / currentBattles; ) / currentBattles;
const careerWinrate = const careerWinrate =
tankStats.reduce( tankStats.reduce(
(accumulator, current) => accumulator + current.all.wins, (accumulator, current) => accumulator + current.all.wins,
0, 0
) / careerBattles; ) / careerBattles;
const currentWN8 = const currentWN8 =
orderedCurrentWN8.reduce<number>( orderedCurrentWN8.reduce<number>(
(accumulator, current, index) => (accumulator, current, index) =>
accumulator + (current ?? 0) * orderedCurrentStats[index].battles, accumulator + (current ?? 0) * orderedCurrentStats[index].battles,
0, 0
) / ) /
orderedCurrentStats.reduce( orderedCurrentStats.reduce(
(accumulator, current, index) => (accumulator, current, index) =>
accumulator + accumulator +
(orderedCurrentWN8[index] === undefined ? 0 : current.battles), (orderedCurrentWN8[index] === undefined ? 0 : current.battles),
0, 0
); );
const careerWN8 = const careerWN8 =
orderedCareerWN8Full.reduce<number>( orderedCareerWN8Full.reduce<number>(
(accumulator, current) => accumulator + (current ?? 0), (accumulator, current) => accumulator + (current ?? 0),
0, 0
) / ) /
tankStats.reduce( tankStats.reduce(
(accumulator, current, index) => (accumulator, current, index) =>
accumulator + accumulator +
(orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles), (orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles),
0, 0
); );
const currentDamage = const currentDamage =
orderedCurrentStats.reduce( orderedCurrentStats.reduce(
(accumulator, current) => accumulator + current.damage_dealt, (accumulator, current) => accumulator + current.damage_dealt,
0, 0
) / currentBattles; ) / currentBattles;
const careerDamage = const careerDamage =
tankStats.reduce( tankStats.reduce(
(accumulator, current) => accumulator + current.all.damage_dealt, (accumulator, current) => accumulator + current.all.damage_dealt,
0, 0
) / careerBattles; ) / careerBattles;
const children: JSX.Element[] = []; const children: React.JSX.Element[] = [];
if (filteredOrder.length > 0) { if (filteredOrder.length > 0) {
children.push( children.push(
@@ -172,7 +172,7 @@ export async function renderBreakdown(
delta: currentDamage - careerDamage, delta: currentDamage - careerDamage,
}, },
]} ]}
/>, />
); );
filteredOrder.forEach((id, index) => { filteredOrder.forEach((id, index) => {
@@ -193,9 +193,9 @@ export async function renderBreakdown(
title={ title={
tankDefinition tankDefinition
? unwrap(tankDefinition.name) ? unwrap(tankDefinition.name)
: literals(strings.bot.commands.breakdown.body.unknown_tank, [ : literals(strings.bot.commands.breakdown.body.unknown_tank, {
`${id}`, id,
]) })
} }
stats={[ stats={[
{ {
@@ -206,7 +206,7 @@ export async function renderBreakdown(
{ {
title: strings.bot.commands.breakdown.body.winrate, title: strings.bot.commands.breakdown.body.winrate,
current: `${(100 * (current.wins / current.battles)).toFixed( current: `${(100 * (current.wins / current.battles)).toFixed(
2, 2
)}%`, )}%`,
career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`, career: `${(100 * (career.wins / career.battles)).toFixed(2)}%`,
delta: delta:
@@ -230,24 +230,24 @@ export async function renderBreakdown(
{ {
title: strings.bot.commands.breakdown.body.damage, title: strings.bot.commands.breakdown.body.damage,
current: Math.round( current: Math.round(
current.damage_dealt / current.battles, current.damage_dealt / current.battles
).toLocaleString(), ).toLocaleString(),
career: Math.round( career: Math.round(
career.damage_dealt / career.battles, career.damage_dealt / career.battles
).toLocaleString(), ).toLocaleString(),
delta: delta:
current.damage_dealt / current.battles - current.damage_dealt / current.battles -
career.damage_dealt / career.battles, career.damage_dealt / career.battles,
}, },
]} ]}
/>, />
); );
}); });
} }
const pages = chunk( const pages = chunk(
children, 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) { if (filteredOrder.length > 0) {
+28 -28
View File
@@ -1,44 +1,44 @@
import { getAccountInfo, getClanInfo } from '@blitzkit/core'; import { getAccountInfo, getClanInfo } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { Locale } from 'discord.js'; import { Locale } from "discord.js";
import { CommandWrapper } from '../components/CommandWrapper'; import { CommandWrapper } from "../components/CommandWrapper";
import { GenericStats } from '../components/GenericStats'; import { GenericStats } from "../components/GenericStats";
import { NoData } from '../components/NoData'; import { NoData } from "../components/NoData";
import { TitleBar } from '../components/TitleBar'; import { TitleBar } from "../components/TitleBar";
import { addClanChoices } from '../core/discord/addClanChoices'; import { addClanChoices } from "../core/discord/addClanChoices";
import { autocompleteClan } from '../core/discord/autocompleteClan'; import { autocompleteClan } from "../core/discord/autocompleteClan";
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from '../core/discord/localizationObject'; import { localizationObject } from "../core/discord/localizationObject";
import { resolveClanFromCommand } from '../core/discord/resolveClanFromCommand'; import { resolveClanFromCommand } from "../core/discord/resolveClanFromCommand";
import { translator } from '../core/localization/translator'; import { translator } from "../core/localization/translator";
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_THRESHOLD = 7; const DEFAULT_THRESHOLD = 7;
export const inactiveCommand = new Promise<CommandRegistry>((resolve) => { export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
resolve({ resolve({
command: createLocalizedCommand('inactive') command: createLocalizedCommand("inactive")
.addStringOption(addClanChoices) .addStringOption(addClanChoices)
.addNumberOption((option) => { .addNumberOption((option) => {
const { strings } = translator(Locale.EnglishUS); const { strings } = translator(Locale.EnglishUS);
return option return option
.setName('threshold') .setName("threshold")
.setNameLocalizations( .setNameLocalizations(
localizationObject( localizationObject(
(strings) => strings.bot.commands.inactive.options.threshold.name, (strings) => strings.bot.commands.inactive.options.threshold.name,
undefined, undefined,
true, true
), )
) )
.setDescription( .setDescription(
strings.bot.commands.inactive.options.threshold.description, strings.bot.commands.inactive.options.threshold.description
) )
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject( localizationObject(
(strings) => (strings) =>
strings.bot.commands.inactive.options.threshold.description, strings.bot.commands.inactive.options.threshold.description
), )
) )
.setMinValue(0); .setMinValue(0);
}), }),
@@ -47,7 +47,7 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(interaction.locale); const { strings } = translator(interaction.locale);
const { region, id } = await resolveClanFromCommand(interaction); const { region, id } = await resolveClanFromCommand(interaction);
const threshold = const threshold =
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD; interaction.options.getNumber("threshold")! ?? DEFAULT_THRESHOLD;
const time = new Date().getTime() / 1000; const time = new Date().getTime() / 1000;
const clanInfo = await getClanInfo(region, id); const clanInfo = await getClanInfo(region, id);
const accountInfo = await getAccountInfo(region, clanInfo.members_ids); const accountInfo = await getAccountInfo(region, clanInfo.members_ids);
@@ -57,7 +57,7 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
[ [
account.nickname, account.nickname,
(time - account.last_battle_time) / 60 / 60 / 24, (time - account.last_battle_time) / 60 / 60 / 24,
] as [string, number], ] as [string, number]
) )
.filter(([, inactiveDays]) => inactiveDays >= threshold) .filter(([, inactiveDays]) => inactiveDays >= threshold)
.sort((a, b) => b[1] - a[1]) .sort((a, b) => b[1] - a[1])
@@ -65,10 +65,10 @@ export const inactiveCommand = new Promise<CommandRegistry>((resolve) => {
([name, days]) => ([name, days]) =>
[ [
name, name,
literals(strings.bot.commands.inactive.body.listing, [ literals(strings.bot.commands.inactive.body.listing, {
days.toFixed(0), days: days.toFixed(0),
]), }),
] as [string, string], ] as [string, string]
); );
const hasInactiveMembers = inactive.length > 0; 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`} image={`https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanInfo.emblem_set_id}.png`}
description={`${literals( description={`${literals(
strings.bot.commands.inactive.body.subtitle, strings.bot.commands.inactive.body.subtitle,
[`${threshold}`], { days: threshold }
)} • ${new Date().toLocaleDateString(interaction.locale)}`} )} • ${new Date().toLocaleDateString(interaction.locale)}`}
/> />
+22 -22
View File
@@ -5,31 +5,31 @@ import {
flags, flags,
getAccountInfo, getAccountInfo,
getTankStats, getTankStats,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import markdownEscape from 'markdown-escape'; import markdownEscape from "markdown-escape";
import { import {
gameDefinitions, gameDefinitions,
tankDefinitions, tankDefinitions,
} from '../core/blitzkit/nonBlockingPromises'; } from "../core/blitzkit/nonBlockingPromises";
import { addTierChoices } from '../core/discord/addTierChoices'; import { addTierChoices } from "../core/discord/addTierChoices";
import { addUsernameChoices } from '../core/discord/addUsernameChoices'; import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from '../core/discord/autocompleteUsername'; import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { chunkLines } from '../core/discord/chunkLines'; import { chunkLines } from "../core/discord/chunkLines";
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand'; import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from '../core/localization/translator'; import { translator } from "../core/localization/translator";
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from "../events/interactionCreate";
export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => { export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
resolve({ resolve({
command: createLocalizedCommand('owned-tanks') command: createLocalizedCommand("owned-tanks")
.addStringOption(addTierChoices) .addStringOption(addTierChoices)
.addStringOption(addUsernameChoices), .addStringOption(addUsernameChoices),
async handler(interaction) { async handler(interaction) {
const { strings, unwrap } = translator(interaction.locale); 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 { id, region } = await resolvePlayerFromCommand(interaction);
const accountInfo = await getAccountInfo(region, id); const accountInfo = await getAccountInfo(region, id);
const tankStats = await getTankStats(region, id); const tankStats = await getTankStats(region, id);
@@ -43,7 +43,7 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
tankStats.map(async (tankData) => ({ tankStats.map(async (tankData) => ({
tankDefinitions: (await tankDefinitions).tanks[tankData.tank_id]!, tankDefinitions: (await tankDefinitions).tanks[tankData.tank_id]!,
id: tankData.tank_id, id: tankData.tank_id,
})), }))
) )
).filter((tank) => tank.tankDefinitions?.tier === tier); ).filter((tank) => tank.tankDefinitions?.tier === tier);
const groupedTanks: Record<string, TankDefinition[]> = {}; const groupedTanks: Record<string, TankDefinition[]> = {};
@@ -60,25 +60,25 @@ export const ownedTanksCommand = new Promise<CommandRegistry>((resolve) => {
const awaitedTankDefinitions = await tankDefinitions; const awaitedTankDefinitions = await tankDefinitions;
const awaitedGameDefinitions = await gameDefinitions; const awaitedGameDefinitions = await gameDefinitions;
const title = literals(strings.bot.commands.owned_tanks.body.title, [ const title = literals(strings.bot.commands.owned_tanks.body.title, {
markdownEscape(accountInfo.nickname), name: markdownEscape(accountInfo.nickname),
TIER_ROMAN_NUMERALS[tier], tier: TIER_ROMAN_NUMERALS[tier],
]); });
const tankList = tankStats const tankList = tankStats
.map(({ tank_id }) => awaitedTankDefinitions.tanks[tank_id]) .map(({ tank_id }) => awaitedTankDefinitions.tanks[tank_id])
.filter((tank) => tank.tier === tier) .filter((tank) => tank.tier === tier)
.sort( .sort(
(a, b) => (a, b) =>
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class), TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class)
) )
.sort( .sort(
(a, b) => (a, b) =>
awaitedGameDefinitions.nations.indexOf(a.nation) - awaitedGameDefinitions.nations.indexOf(a.nation) -
awaitedGameDefinitions.nations.indexOf(b.nation), awaitedGameDefinitions.nations.indexOf(b.nation)
) )
.map( .map(
(tank) => (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]; const lines = [title, ...tankList];
+13 -13
View File
@@ -1,11 +1,11 @@
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from '../core/localization/translator'; import { translator } from "../core/localization/translator";
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from "../events/interactionCreate";
export const permissionsCommand = new Promise<CommandRegistry>((resolve) => { export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
resolve({ resolve({
command: createLocalizedCommand('permissions'), command: createLocalizedCommand("permissions"),
async handler(interaction) { async handler(interaction) {
const { strings } = translator(interaction.locale); const { strings } = translator(interaction.locale);
@@ -14,33 +14,33 @@ export const permissionsCommand = new Promise<CommandRegistry>((resolve) => {
new Date().getDay() === 5 && new Date().getDate() === 13; new Date().getDay() === 5 && new Date().getDate() === 13;
const permissions = [ const permissions = [
[ [
interaction.appPermissions?.has('ViewChannel'), interaction.appPermissions?.has("ViewChannel"),
strings.bot.commands.permissions.body.view_channels, strings.bot.commands.permissions.body.view_channels,
], ],
[ [
interaction.appPermissions?.has('ReadMessageHistory'), interaction.appPermissions?.has("ReadMessageHistory"),
strings.bot.commands.permissions.body.read_message_history, strings.bot.commands.permissions.body.read_message_history,
], ],
[ [
interaction.appPermissions?.has('AttachFiles'), interaction.appPermissions?.has("AttachFiles"),
strings.bot.commands.permissions.body.attach_files, strings.bot.commands.permissions.body.attach_files,
], ],
[ [
interaction.appPermissions?.has('UseExternalEmojis'), interaction.appPermissions?.has("UseExternalEmojis"),
strings.bot.commands.permissions.body.external_emojis, strings.bot.commands.permissions.body.external_emojis,
], ],
]; ];
const body = permissions const body = permissions
.map(([hasPermission, description]) => { .map(([hasPermission, description]) => {
if (hasPermission) { if (hasPermission) {
return `${isFridayThe13th ? '😇' : '✅'} ${description}`; return `${isFridayThe13th ? "😇" : "✅"} ${description}`;
} else { } 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 { literals } from "@blitzkit/i18n";
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { translator } from '../core/localization/translator'; import { translator } from "../core/localization/translator";
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from "../events/interactionCreate";
export const pingCommand = new Promise<CommandRegistry>((resolve) => { export const pingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({ resolve({
handlesInteraction: true, handlesInteraction: true,
command: createLocalizedCommand('ping', [ command: createLocalizedCommand("ping", [
{ subcommand: 'blitzkit' }, { subcommand: "blitzkit" },
{ subcommand: 'wotb' }, { subcommand: "wotb" },
]), ]),
async handler(interaction) { async handler(interaction) {
@@ -17,17 +17,17 @@ export const pingCommand = new Promise<CommandRegistry>((resolve) => {
const subcommand = interaction.options.getSubcommand(); const subcommand = interaction.options.getSubcommand();
const executionStart = Date.now(); const executionStart = Date.now();
if (subcommand === 'blitzkit') { if (subcommand === "blitzkit") {
await interaction.editReply(strings.bot.commands.ping.body.pong); await interaction.editReply(strings.bot.commands.ping.body.pong);
} else { } else {
await fetch('https://api.wotblitz.com/'); await fetch("https://api.wotblitz.com/");
} }
const executionTime = Date.now() - executionStart; const executionTime = Date.now() - executionStart;
interaction.editReply( interaction.editReply(
literals(strings.bot.commands.ping.body.pong_with_time, [ literals(strings.bot.commands.ping.body.pong_with_time, {
`${Math.round(executionTime)}`, time: executionTime.toFixed(0),
]), })
); );
}, },
}); });
+18 -18
View File
@@ -1,19 +1,19 @@
import { getAccountInfo } from '@blitzkit/core'; import { getAccountInfo } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import markdownEscape from 'markdown-escape'; import markdownEscape from "markdown-escape";
import { addUsernameChoices } from '../core/discord/addUsernameChoices'; import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from '../core/discord/autocompleteUsername'; import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from '../core/discord/embedInfo'; import { embedInfo } from "../core/discord/embedInfo";
import { markdownTable } from '../core/discord/markdownTable'; import { markdownTable } from "../core/discord/markdownTable";
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand'; import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from '../core/localization/translator'; import { translator } from "../core/localization/translator";
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from "../events/interactionCreate";
export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => { export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
resolve({ resolve({
command: command:
createLocalizedCommand('player-info').addStringOption(addUsernameChoices), createLocalizedCommand("player-info").addStringOption(addUsernameChoices),
async handler(interaction) { async handler(interaction) {
const { strings } = translator(interaction.locale); const { strings } = translator(interaction.locale);
@@ -22,9 +22,9 @@ export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
const accountInfo = await getAccountInfo(region, id); const accountInfo = await getAccountInfo(region, id);
return embedInfo( return embedInfo(
literals(strings.bot.commands.player_info.body.title, [ literals(strings.bot.commands.player_info.body.title, {
markdownEscape(accountInfo.nickname), name: markdownEscape(accountInfo.nickname),
]), }),
markdownTable([ markdownTable([
[ [
@@ -51,16 +51,16 @@ export const playerInfoCommand = new Promise<CommandRegistry>((resolve) => {
[ [
strings.bot.commands.player_info.body.created, strings.bot.commands.player_info.body.created,
new Date(accountInfo.created_at * 1000).toLocaleDateString( new Date(accountInfo.created_at * 1000).toLocaleDateString(
interaction.locale, interaction.locale
), ),
], ],
[ [
strings.bot.commands.player_info.body.last_battle, strings.bot.commands.player_info.body.last_battle,
new Date(accountInfo.last_battle_time * 1000).toLocaleDateString( new Date(accountInfo.last_battle_time * 1000).toLocaleDateString(
interaction.locale, interaction.locale
), ),
], ],
]), ])
); );
}, },
+118 -118
View File
@@ -8,42 +8,42 @@ import {
getRatingInfo, getRatingInfo,
getRatingNeighbors, getRatingNeighbors,
imgur, imgur,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { SlashCommandSubcommandBuilder } from 'discord.js'; import { SlashCommandSubcommandBuilder } from "discord.js";
import markdownEscape from 'markdown-escape'; import markdownEscape from "markdown-escape";
import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow'; import { Glow } from "../components/AllStatsOverview/components/HeroStat/components/Glow";
import { CommandWrapper } from '../components/CommandWrapper'; import { CommandWrapper } from "../components/CommandWrapper";
import { DeltaCaret } from '../components/DeltaCaret'; import { DeltaCaret } from "../components/DeltaCaret";
import { TitleBar } from '../components/TitleBar'; import { TitleBar } from "../components/TitleBar";
import { normalizeLeagueIcon } from '../core/blitz/normalizeLeagueIcon'; import { normalizeLeagueIcon } from "../core/blitz/normalizeLeagueIcon";
import { iconPng } from '../core/blitzkit/iconPng'; import { iconPng } from "../core/blitzkit/iconPng";
import { addUsernameChoices } from '../core/discord/addUsernameChoices'; import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand'; import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from '../core/localization/translator'; import { translator } from "../core/localization/translator";
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from "../events/interactionCreate";
import { theme } from '../stitches.config'; import { theme } from "../stitches.config";
const LEAGUE_ACCENT = [ const LEAGUE_ACCENT = [
'_purple', "_purple",
'_sand', "_sand",
'_amber', "_amber",
'_mauve', "_mauve",
'_brown', "_brown",
] as const; ] as const;
export const ratingCommand = new Promise<CommandRegistry>((resolve) => { export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
resolve({ resolve({
command: createLocalizedCommand('rating', [ command: createLocalizedCommand("rating", [
{ {
subcommand: 'today', subcommand: "today",
modify(option: SlashCommandSubcommandBuilder) { modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices); option.addStringOption(addUsernameChoices);
}, },
}, },
{ {
subcommand: 'season', subcommand: "season",
modify(option: SlashCommandSubcommandBuilder) { modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices); option.addStringOption(addUsernameChoices);
}, },
@@ -52,11 +52,11 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
async handler(interaction) { async handler(interaction) {
const subcommand = interaction.options.getSubcommand(true) as const subcommand = interaction.options.getSubcommand(true) as
| 'season' | "season"
| 'today'; | "today";
const { strings } = translator(interaction.locale); const { strings } = translator(interaction.locale);
const { id, region } = await resolvePlayerFromCommand(interaction); 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 clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const ratingInfo = await getRatingInfo(region); const ratingInfo = await getRatingInfo(region);
@@ -67,20 +67,20 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const leaderboard = await getArchivedRatingLeaderboard( const leaderboard = await getArchivedRatingLeaderboard(
region, 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( 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, [ const accountInfo = await getAccountInfo(region, id, [
'statistics.rating', "statistics.rating",
]); ]);
const statsIndex = leaderboard.version!.value.entries.findIndex( const statsIndex = leaderboard.version!.value.entries.findIndex(
(entry) => entry.id === id, (entry) => entry.id === id
); );
const statsA = leaderboard const statsA = leaderboard
? leaderboard.version!.value.entries[statsIndex!] ? leaderboard.version!.value.entries[statsIndex!]
@@ -90,9 +90,9 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const statsB2Array = (await getRatingNeighbors(region, id, 0)).neighbors; const statsB2Array = (await getRatingNeighbors(region, id, 0)).neighbors;
if (statsB2Array === undefined) { if (statsB2Array === undefined) {
return literals(strings.bot.commands.rating.errors.no_participation, [ return literals(strings.bot.commands.rating.errors.no_participation, {
markdownEscape(accountInfo.nickname), name: markdownEscape(accountInfo.nickname),
]); });
} }
const [statsB2] = statsB2Array; const [statsB2] = statsB2Array;
@@ -102,15 +102,15 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
return strings.bot.commands.rating.errors.unrecorded_stats; return strings.bot.commands.rating.errors.unrecorded_stats;
} }
if (!statsB1) { if (!statsB1) {
return literals(strings.bot.commands.rating.errors.no_participation, [ return literals(strings.bot.commands.rating.errors.no_participation, {
markdownEscape(accountInfo.nickname), name: markdownEscape(accountInfo.nickname),
]); });
} }
const reward = ratingInfo.rewards.find( const reward = ratingInfo.rewards.find(
(reward) => (reward) =>
statsB2.number >= reward.from_position && statsB2.number >= reward.from_position &&
statsB2.number <= reward.to_position, statsB2.number <= reward.to_position
); );
const positionDelta = leaderboard const positionDelta = leaderboard
? statsB2.number - positionA! ? statsB2.number - positionA!
@@ -118,11 +118,11 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const delta = deltaBkrlBlitzStats( const delta = deltaBkrlBlitzStats(
statsA, statsA,
accountInfo.statistics.rating!, accountInfo.statistics.rating!,
statsB2, statsB2
); );
const leagueTheme = LEAGUE_ACCENT[league.index]; const leagueTheme = LEAGUE_ACCENT[league.index];
let rewardImage = reward let rewardImage = reward
? reward.type === 'vehicle' ? reward.type === "vehicle"
? reward.vehicle.preview_image_url ? reward.vehicle.preview_image_url
: reward.stuff.image_url : reward.stuff.image_url
: undefined; : undefined;
@@ -131,7 +131,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
const { ok } = await fetch(rewardImage); const { ok } = await fetch(rewardImage);
if (!ok) { if (!ok) {
rewardImage = imgur('kP88vqr'); rewardImage = imgur("kP88vqr");
} }
} }
@@ -147,24 +147,24 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
style={{ style={{
position: 'relative', position: "relative",
display: 'flex', display: "flex",
overflow: 'hidden', overflow: "hidden",
}} }}
> >
<div <div
style={{ style={{
position: 'relative', position: "relative",
padding: 16, padding: 16,
backgroundColor: theme.colors[`appBackground2${leagueTheme}`], backgroundColor: theme.colors[`appBackground2${leagueTheme}`],
borderRadius: 4, borderRadius: 4,
border: `1px solid ${ border: `1px solid ${
theme.colors[`componentInteractive${leagueTheme}`] theme.colors[`componentInteractive${leagueTheme}`]
}`, }`,
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'space-between', justifyContent: "space-between",
width: '100%', width: "100%",
}} }}
> >
{reward === undefined && ( {reward === undefined && (
@@ -172,17 +172,17 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
endOpacity={3 / 4} endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]} color={theme.colors[`solidBackground${leagueTheme}`]}
style={{ style={{
transform: 'translateX(-16px) rotate(90deg)', transform: "translateX(-16px) rotate(90deg)",
}} }}
/> />
)} )}
<div <div
style={{ style={{
display: 'flex', display: "flex",
gap: 8, gap: 8,
justifyContent: 'center', justifyContent: "center",
alignItems: 'center', alignItems: "center",
}} }}
> >
<img <img
@@ -190,24 +190,24 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
style={{ style={{
width: 64, width: 64,
height: 64, height: 64,
objectFit: 'contain', objectFit: "contain",
}} }}
src={normalizeLeagueIcon( src={normalizeLeagueIcon(
ratingInfo.leagues[league.index].big_icon, ratingInfo.leagues[league.index].big_icon
)} )}
/> />
<div <div
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'flex-start', justifyContent: "flex-start",
}} }}
> >
<span <span
@@ -232,7 +232,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
}} }}
> >
{Math.abs(delta.score).toLocaleString( {Math.abs(delta.score).toLocaleString(
interaction.locale, interaction.locale
)} )}
</span> </span>
</> </>
@@ -241,9 +241,9 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'flex-start', justifyContent: "flex-start",
}} }}
> >
<span <span
@@ -267,7 +267,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
}} }}
> >
{Math.abs(positionDelta).toLocaleString( {Math.abs(positionDelta).toLocaleString(
interaction.locale, interaction.locale
)} )}
</span> </span>
</> </>
@@ -279,23 +279,23 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
{reward && ( {reward && (
<div <div
style={{ style={{
display: 'flex', display: "flex",
gap: 8, gap: 8,
justifyContent: 'center', justifyContent: "center",
alignItems: 'center', alignItems: "center",
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'flex-end', justifyContent: "flex-end",
}} }}
> >
<span <span
@@ -303,13 +303,13 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors[`textHighContrast${leagueTheme}`], color: theme.colors[`textHighContrast${leagueTheme}`],
fontSize: 16, fontSize: 16,
maxWidth: 136, maxWidth: 136,
textAlign: 'right', textAlign: "right",
whiteSpace: 'nowrap', whiteSpace: "nowrap",
textOverflow: 'ellipsis', textOverflow: "ellipsis",
overflow: 'hidden', overflow: "hidden",
}} }}
> >
{reward.type === 'vehicle' {reward.type === "vehicle"
? reward.vehicle.user_string ? reward.vehicle.user_string
: reward.stuff.title} : reward.stuff.title}
</span> </span>
@@ -317,9 +317,9 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'flex-end', justifyContent: "flex-end",
}} }}
> >
<span <span
@@ -329,7 +329,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
}} }}
> >
Reward Reward
{reward.type === 'stuff' ? ` x${reward.count}` : ''} {reward.type === "stuff" ? ` x${reward.count}` : ""}
</span> </span>
</div> </div>
</div> </div>
@@ -338,7 +338,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
style={{ style={{
width: 64, width: 64,
height: 64, height: 64,
objectFit: 'cover', objectFit: "cover",
}} }}
src={await iconPng(rewardImage!)} src={await iconPng(rewardImage!)}
/> />
@@ -350,7 +350,7 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
endOpacity={3 / 4} endOpacity={3 / 4}
color={theme.colors[`solidBackground${leagueTheme}`]} color={theme.colors[`solidBackground${leagueTheme}`]}
style={{ style={{
transform: 'translateX(16px) rotate(-90deg)', transform: "translateX(16px) rotate(-90deg)",
}} }}
/> />
)} )}
@@ -362,10 +362,10 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
direction="reverse" direction="reverse"
color={theme.colors[`solidBackground${leagueTheme}`]} color={theme.colors[`solidBackground${leagueTheme}`]}
style={{ style={{
position: 'absolute', position: "absolute",
top: -40, top: -40,
left: '50%', left: "50%",
transform: 'translateX(-50%)', transform: "translateX(-50%)",
}} }}
/> />
)} )}
@@ -373,32 +373,32 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
<Glow <Glow
color={theme.colors[`solidBackground${leagueTheme}`]} color={theme.colors[`solidBackground${leagueTheme}`]}
style={{ style={{
position: 'absolute', position: "absolute",
bottom: -40, bottom: -40,
left: '50%', left: "50%",
transform: 'translateX(-50%)', transform: "translateX(-50%)",
}} }}
/> />
)} )}
</div> </div>
{delta.battles > 0 && ( {delta.battles > 0 && (
<div style={{ padding: '8px 0', display: 'flex' }}> <div style={{ padding: "8px 0", display: "flex" }}>
<div <div
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
gap: 4, gap: 4,
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: "column",
}} }}
> >
<span <span
style={{ style={{
fontSize: 24, fontSize: 24,
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
fontWeight: 'bold', fontWeight: "bold",
}} }}
> >
{delta.battles.toLocaleString(interaction.locale)} {delta.battles.toLocaleString(interaction.locale)}
@@ -414,19 +414,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div> </div>
<div <div
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
gap: 4, gap: 4,
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: "column",
}} }}
> >
<span <span
style={{ style={{
fontSize: 24, fontSize: 24,
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
fontWeight: 'bold', fontWeight: "bold",
}} }}
> >
{(100 * (delta.wins / delta.battles)).toFixed(0)}% {(100 * (delta.wins / delta.battles)).toFixed(0)}%
@@ -442,23 +442,23 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div> </div>
<div <div
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
gap: 4, gap: 4,
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: "column",
}} }}
> >
<span <span
style={{ style={{
fontSize: 24, fontSize: 24,
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
fontWeight: 'bold', fontWeight: "bold",
}} }}
> >
{Math.round(delta.damage / delta.battles).toLocaleString( {Math.round(delta.damage / delta.battles).toLocaleString(
interaction.locale, interaction.locale
)} )}
</span> </span>
<span <span
@@ -472,19 +472,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div> </div>
<div <div
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
gap: 4, gap: 4,
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: "column",
}} }}
> >
<span <span
style={{ style={{
fontSize: 24, fontSize: 24,
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
fontWeight: 'bold', fontWeight: "bold",
}} }}
> >
{(100 * (delta.survived / delta.battles)).toFixed(0)}% {(100 * (delta.survived / delta.battles)).toFixed(0)}%
@@ -500,19 +500,19 @@ export const ratingCommand = new Promise<CommandRegistry>((resolve) => {
</div> </div>
<div <div
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
gap: 4, gap: 4,
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: "column",
}} }}
> >
<span <span
style={{ style={{
fontSize: 24, fontSize: 24,
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
fontWeight: 'bold', fontWeight: "bold",
}} }}
> >
{(delta.kills / delta.battles).toFixed(2)} {(delta.kills / delta.battles).toFixed(2)}
+79 -80
View File
@@ -13,27 +13,27 @@ import {
RatingInfo, RatingInfo,
RatingLeaderboard, RatingLeaderboard,
Region, Region,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { import {
APIApplicationCommandOptionChoice, APIApplicationCommandOptionChoice,
Locale, Locale,
SlashCommandSubcommandBuilder, SlashCommandSubcommandBuilder,
} from 'discord.js'; } from "discord.js";
import { range } from 'lodash-es'; import { range } from "lodash-es";
import markdownEscape from 'markdown-escape'; import markdownEscape from "markdown-escape";
import { CommandWrapper } from '../components/CommandWrapper'; import { CommandWrapper } from "../components/CommandWrapper";
import * as Leaderboard from '../components/Leaderboard'; import * as Leaderboard from "../components/Leaderboard";
import { TitleBar } from '../components/TitleBar'; import { TitleBar } from "../components/TitleBar";
import { UserError } from '../core/blitzkit/userError'; import { UserError } from "../core/blitzkit/userError";
import { addRegionChoices } from '../core/discord/addRegionChoices'; import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from '../core/discord/addUsernameChoices'; import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteUsername } from '../core/discord/autocompleteUsername'; import { autocompleteUsername } from "../core/discord/autocompleteUsername";
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from '../core/discord/localizationObject'; import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand'; import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from '../core/localization/translator'; import { translator } from "../core/localization/translator";
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from "../events/interactionCreate";
interface SimplifiedPlayer { interface SimplifiedPlayer {
id: number; id: number;
@@ -51,7 +51,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
const onGoingSeason = await isOnGoingRatingSeason(); const onGoingSeason = await isOnGoingRatingSeason();
const seasonNumbers = range( const seasonNumbers = range(
FIRST_MINIMAL_ARCHIVED_RATING_SEASON, FIRST_MINIMAL_ARCHIVED_RATING_SEASON,
latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1, latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1
); );
function addOptions(option: SlashCommandSubcommandBuilder) { function addOptions(option: SlashCommandSubcommandBuilder) {
@@ -66,51 +66,50 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
(strings) => (strings) =>
strings.bot.commands.rating_leaderboard.options.limit.name, strings.bot.commands.rating_leaderboard.options.limit.name,
undefined, undefined,
true, true
), )
) )
.setDescription( .setDescription(
literals( literals(
strings.bot.commands.rating_leaderboard.options.limit strings.bot.commands.rating_leaderboard.options.limit
.description, .description,
[`${DEFAULT_LIMIT}`], { count: DEFAULT_LIMIT }
), )
) )
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject( localizationObject(
(strings) => (strings) =>
strings.bot.commands.rating_leaderboard.options.limit strings.bot.commands.rating_leaderboard.options.limit
.description, .description,
[`${DEFAULT_LIMIT}`], { count: DEFAULT_LIMIT }
), )
) )
.setRequired(false) .setRequired(false)
.setMinValue(5) .setMinValue(5)
.setMaxValue(30), .setMaxValue(30)
) )
.addStringOption((option) => .addStringOption((option) =>
option option
.setName( .setName(
strings.bot.commands.rating_leaderboard.options.season.name, strings.bot.commands.rating_leaderboard.options.season.name
) )
.setNameLocalizations( .setNameLocalizations(
localizationObject( localizationObject(
(strings) => (strings) =>
strings.bot.commands.rating_leaderboard.options.season.name, strings.bot.commands.rating_leaderboard.options.season.name,
undefined, undefined,
true, true
), )
) )
.setDescription( .setDescription(
strings.bot.commands.rating_leaderboard.options.season strings.bot.commands.rating_leaderboard.options.season.description
.description,
) )
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject( localizationObject(
(strings) => (strings) =>
strings.bot.commands.rating_leaderboard.options.season strings.bot.commands.rating_leaderboard.options.season
.description, .description
), )
) )
.setRequired(false) .setRequired(false)
.addChoices( .addChoices(
@@ -124,7 +123,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? literals( ? literals(
strings.bot.commands.rating_leaderboard.options.season strings.bot.commands.rating_leaderboard.options.season
.choices.current, .choices.current,
[`${number}`], { season: number }
) )
: `${number}`, : `${number}`,
value: `${number}`, value: `${number}`,
@@ -133,27 +132,27 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
(strings) => (strings) =>
strings.bot.commands.rating_leaderboard.options strings.bot.commands.rating_leaderboard.options
.season.choices.current, .season.choices.current,
[`${number}`], { season: number }
) )
: undefined, : undefined,
} satisfies APIApplicationCommandOptionChoice<string>; } satisfies APIApplicationCommandOptionChoice<string>;
}) })
.reverse(), .reverse()
), )
); );
} }
resolve({ resolve({
command: createLocalizedCommand('rating-leaderboard', [ command: createLocalizedCommand("rating-leaderboard", [
{ {
subcommand: 'neighbors', subcommand: "neighbors",
modify(option: SlashCommandSubcommandBuilder) { modify(option: SlashCommandSubcommandBuilder) {
option.addStringOption(addUsernameChoices); option.addStringOption(addUsernameChoices);
addOptions(option); addOptions(option);
}, },
}, },
{ {
subcommand: 'league', subcommand: "league",
modify(option: SlashCommandSubcommandBuilder) { modify(option: SlashCommandSubcommandBuilder) {
option option
.addStringOption((option) => { .addStringOption((option) => {
@@ -161,7 +160,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
return option return option
.setName( .setName(
strings.bot.commands.rating_leaderboard.options.league.name, strings.bot.commands.rating_leaderboard.options.league.name
) )
.setNameLocalizations( .setNameLocalizations(
localizationObject( localizationObject(
@@ -169,19 +168,19 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
strings.bot.commands.rating_leaderboard.options.league strings.bot.commands.rating_leaderboard.options.league
.name, .name,
undefined, undefined,
true, true
), )
) )
.setDescription( .setDescription(
strings.bot.commands.rating_leaderboard.options.league strings.bot.commands.rating_leaderboard.options.league
.description, .description
) )
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject( localizationObject(
(strings) => (strings) =>
strings.bot.commands.rating_leaderboard.options.league strings.bot.commands.rating_leaderboard.options.league
.description, .description
), )
) )
.addChoices( .addChoices(
...LEAGUES.map( ...LEAGUES.map(
@@ -195,10 +194,10 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
(strings) => (strings) =>
( (
strings.common.leagues as Record<string, string> strings.common.leagues as Record<string, string>
)[name], )[name]
), ),
}) satisfies APIApplicationCommandOptionChoice<string>, }) satisfies APIApplicationCommandOptionChoice<string>
), )
) )
.setRequired(true); .setRequired(true);
}) })
@@ -213,10 +212,10 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
async handler(interaction) { async handler(interaction) {
const { strings } = translator(interaction.locale); const { strings } = translator(interaction.locale);
const subcommand = interaction.options.getSubcommand(true) as const subcommand = interaction.options.getSubcommand(true) as
| 'league' | "league"
| 'neighbors'; | "neighbors";
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT; const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const seasonOption = interaction.options.getString('season'); const seasonOption = interaction.options.getString("season");
const season = seasonOption const season = seasonOption
? parseInt(seasonOption) ? parseInt(seasonOption)
: onGoingSeason : onGoingSeason
@@ -232,11 +231,11 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
let playerId: number | undefined; let playerId: number | undefined;
let regionRatingInfo: RatingInfo | null; let regionRatingInfo: RatingInfo | null;
if (subcommand === 'league') { if (subcommand === "league") {
const leagueIndex = parseInt( 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 regionRatingInfo = await (isCurrentSeason
? getRatingInfo(region) ? getRatingInfo(region)
: null); : null);
@@ -259,8 +258,8 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
position: player.number, position: player.number,
clan: player.clan_tag, clan: player.clan_tag,
nickname: player.nickname, nickname: player.nickname,
}) satisfies SimplifiedPlayer, }) satisfies SimplifiedPlayer
), )
) )
: await getArchivedRatingLeaderboard(region, season!).then( : await getArchivedRatingLeaderboard(region, season!).then(
async (players) => { async (players) => {
@@ -269,17 +268,17 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? 0 ? 0
: players.version!.value.entries.findIndex( : players.version!.value.entries.findIndex(
(player) => (player) =>
player.score < LEAGUES[leagueIndex - 1].minScore, player.score < LEAGUES[leagueIndex - 1].minScore
); );
const lastPlayerIndex = firstPlayerIndex + limit - 1; const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.version!.value.entries.slice( const trimmed = players.version!.value.entries.slice(
firstPlayerIndex, firstPlayerIndex,
lastPlayerIndex + 1, lastPlayerIndex + 1
); );
const clanData = await getClanAccountInfo( const clanData = await getClanAccountInfo(
region, region,
trimmed.map(({ id }) => id), trimmed.map(({ id }) => id),
['clan'], ["clan"]
); );
return trimmed.map( return trimmed.map(
@@ -294,16 +293,16 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
literals( literals(
strings.bot.commands.rating_leaderboard.body strings.bot.commands.rating_leaderboard.body
.deleted_player, .deleted_player,
[`${player.id}`], { id: player.id }
), ),
}) satisfies SimplifiedPlayer, }) satisfies SimplifiedPlayer
); );
}, }
); );
const leagueInfo = LEAGUES[leagueIndex]; const leagueInfo = LEAGUES[leagueIndex];
archivedLeaderboard = await getArchivedRatingLeaderboard( archivedLeaderboard = await getArchivedRatingLeaderboard(
region, region,
season, season
); );
players = result; players = result;
@@ -316,12 +315,12 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? (strings.common.leagues as Record<string, string>)[ ? (strings.common.leagues as Record<string, string>)[
LEAGUES[leagueInfo.index].name LEAGUES[leagueInfo.index].name
] ]
: '' : ""
} - ${strings.common.regions.short[region]}`; } - ${strings.common.regions.short[region]}`;
titleImage = imgur(leagueInfo.icon); titleImage = imgur(leagueInfo.icon);
titleDescription = literals( titleDescription = literals(
strings.bot.commands.rating_leaderboard.body.top_players, strings.bot.commands.rating_leaderboard.body.top_players,
[`${limit}`], { count: limit }
); );
} else { } else {
const { region, id } = await resolvePlayerFromCommand(interaction); const { region, id } = await resolvePlayerFromCommand(interaction);
@@ -338,7 +337,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
} }
const accountInfo = await getAccountInfo(region, id); const accountInfo = await getAccountInfo(region, id);
const clan = (await getClanAccountInfo(region, id, ['clan']))?.clan; const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan;
const neighbors = isCurrentSeason const neighbors = isCurrentSeason
? await getRatingNeighbors(region, id, Math.round(limit / 2)).then( ? await getRatingNeighbors(region, id, Math.round(limit / 2)).then(
(players) => (players) =>
@@ -350,13 +349,13 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
score: player.score, score: player.score,
clan: player.clan_tag, clan: player.clan_tag,
nickname: player.nickname, nickname: player.nickname,
}) satisfies SimplifiedPlayer, }) satisfies SimplifiedPlayer
), )
) )
: await getArchivedRatingLeaderboard(region, season!).then( : await getArchivedRatingLeaderboard(region, season!).then(
async (players) => { async (players) => {
const playerIndex = players.version!.value.entries.findIndex( const playerIndex = players.version!.value.entries.findIndex(
(player) => player.id === id, (player) => player.id === id
); );
if (playerIndex === -1) { if (playerIndex === -1) {
@@ -364,20 +363,20 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
literals( literals(
strings.bot.commands.rating_leaderboard.body strings.bot.commands.rating_leaderboard.body
.no_participation, .no_participation,
[markdownEscape(accountInfo.nickname), `${season}`], { player: markdownEscape(accountInfo.nickname), season }
), )
); );
} }
const halfRange = Math.round(limit / 2); const halfRange = Math.round(limit / 2);
const trimmed = players.version!.value.entries.slice( const trimmed = players.version!.value.entries.slice(
playerIndex - halfRange, playerIndex - halfRange,
playerIndex + halfRange + 1, playerIndex + halfRange + 1
); );
const clanData = await getClanAccountInfo( const clanData = await getClanAccountInfo(
region, region,
trimmed.map((player) => player.id), trimmed.map((player) => player.id),
['clan'], ["clan"]
); );
return trimmed.map((player, index) => ({ return trimmed.map((player, index) => ({
@@ -389,18 +388,18 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
? clanData[index]!.account_name ? clanData[index]!.account_name
: `Deleted ${player.id}`, : `Deleted ${player.id}`,
})); }));
}, }
); );
archivedLeaderboard = await getArchivedRatingLeaderboard( archivedLeaderboard = await getArchivedRatingLeaderboard(
region, region,
season, season
); );
players = neighbors; players = neighbors;
titleImage = clan titleImage = clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png` ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined; : undefined;
titleName = neighbors ? accountInfo.nickname : ''; titleName = neighbors ? accountInfo.nickname : "";
titleDescription = titleDescription =
strings.bot.commands.rating_leaderboard.body.neighbors; strings.bot.commands.rating_leaderboard.body.neighbors;
playersAfter = playersAfter =
@@ -414,7 +413,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
const items = players.map((player) => { const items = players.map((player) => {
const midnightIndex = const midnightIndex =
archivedLeaderboard?.version!.value.entries.findIndex( archivedLeaderboard?.version!.value.entries.findIndex(
(item) => item.id === player.id, (item) => item.id === player.id
); );
const midnightScore = const midnightScore =
midnightIndex === undefined midnightIndex === undefined
@@ -453,7 +452,7 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
<Leaderboard.Gap <Leaderboard.Gap
message={literals( message={literals(
strings.bot.commands.rating_leaderboard.body.more, strings.bot.commands.rating_leaderboard.body.more,
[`${playersAfter.toLocaleString(interaction.locale)}`], { count: playersAfter.toLocaleString(interaction.locale) }
)} )}
/> />
)} )}
@@ -465,5 +464,5 @@ export const ratingLeaderboardCommand = new Promise<CommandRegistry>(
autocomplete: autocompleteUsername, autocomplete: autocompleteUsername,
}); });
}, }
); );
+136 -137
View File
@@ -8,31 +8,31 @@ import {
TANK_ICONS, TANK_ICONS,
TankType, TankType,
TIER_ROMAN_NUMERALS, TIER_ROMAN_NUMERALS,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { escapeMarkdown, Locale } from 'discord.js'; import { escapeMarkdown, Locale } from "discord.js";
import { CommandWrapper } from '../components/CommandWrapper'; import { CommandWrapper } from "../components/CommandWrapper";
import { TitleBar } from '../components/TitleBar'; import { TitleBar } from "../components/TitleBar";
import { resolveTankId } from '../core/blitz/resolveTankId'; import { resolveTankId } from "../core/blitz/resolveTankId";
import { buildTechTreeLine } from '../core/blitzkit/buildTechTreeLine'; import { buildTechTreeLine } from "../core/blitzkit/buildTechTreeLine";
import { iconPng } from '../core/blitzkit/iconPng'; import { iconPng } from "../core/blitzkit/iconPng";
import { tankDefinitions } from '../core/blitzkit/nonBlockingPromises'; import { tankDefinitions } from "../core/blitzkit/nonBlockingPromises";
import { resolveAncestry } from '../core/blitzkit/resolveAncestry'; import { resolveAncestry } from "../core/blitzkit/resolveAncestry";
import { tankIconPng } from '../core/blitzkit/tankIconPng'; import { tankIconPng } from "../core/blitzkit/tankIconPng";
import { addUsernameChoices } from '../core/discord/addUsernameChoices'; import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { autocompleteTanks } from '../core/discord/autocompleteTanks'; import { autocompleteTanks } from "../core/discord/autocompleteTanks";
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { localizationObject } from '../core/discord/localizationObject'; import { localizationObject } from "../core/discord/localizationObject";
import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand'; import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand";
import { translator } from '../core/localization/translator'; import { translator } from "../core/localization/translator";
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from "../events/interactionCreate";
import { theme } from '../stitches.config'; import { theme } from "../stitches.config";
export const researchCommand = new Promise<CommandRegistry>((resolve) => { export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS); const { strings } = translator(Locale.EnglishUS);
resolve({ resolve({
command: createLocalizedCommand('research') command: createLocalizedCommand("research")
.addStringOption((option) => .addStringOption((option) =>
option option
.setName(strings.bot.commands.research.options.target_tank.name) .setName(strings.bot.commands.research.options.target_tank.name)
@@ -41,20 +41,20 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
(strings) => (strings) =>
strings.bot.commands.research.options.target_tank.name, strings.bot.commands.research.options.target_tank.name,
undefined, undefined,
true, true
), )
) )
.setDescription( .setDescription(
strings.bot.commands.research.options.target_tank.description, strings.bot.commands.research.options.target_tank.description
) )
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject( localizationObject(
(strings) => (strings) =>
strings.bot.commands.research.options.target_tank.description, strings.bot.commands.research.options.target_tank.description
), )
) )
.setAutocomplete(true) .setAutocomplete(true)
.setRequired(true), .setRequired(true)
) )
.addStringOption((option) => .addStringOption((option) =>
option option
@@ -64,20 +64,20 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
(strings) => (strings) =>
strings.bot.commands.research.options.starting_tank.name, strings.bot.commands.research.options.starting_tank.name,
undefined, undefined,
true, true
), )
) )
.setDescription( .setDescription(
strings.bot.commands.research.options.starting_tank.description, strings.bot.commands.research.options.starting_tank.description
) )
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject( localizationObject(
(strings) => (strings) =>
strings.bot.commands.research.options.starting_tank.description, strings.bot.commands.research.options.starting_tank.description
), )
) )
.setAutocomplete(true) .setAutocomplete(true)
.setRequired(false), .setRequired(false)
) )
.addStringOption(addUsernameChoices), .addStringOption(addUsernameChoices),
@@ -85,11 +85,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const { strings, unwrap } = translator(interaction.locale); const { strings, unwrap } = translator(interaction.locale);
const awaitedTankDefinitions = await tankDefinitions; const awaitedTankDefinitions = await tankDefinitions;
const targetTankId = await resolveTankId( const targetTankId = await resolveTankId(
interaction.options.getString('target-tank', true), interaction.options.getString("target-tank", true),
interaction.locale, interaction.locale,
true, true
); );
const startingTankRaw = interaction.options.getString('starting-tank'); const startingTankRaw = interaction.options.getString("starting-tank");
const targetTank = awaitedTankDefinitions.tanks[targetTankId]; const targetTank = awaitedTankDefinitions.tanks[targetTankId];
const { id, region } = await resolvePlayerFromCommand(interaction); const { id, region } = await resolvePlayerFromCommand(interaction);
let startingTankId: number; let startingTankId: number;
@@ -99,46 +99,45 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
startingTankId = await resolveTankId( startingTankId = await resolveTankId(
startingTankRaw, startingTankRaw,
interaction.locale, interaction.locale,
true, true
); );
if (targetTankId === startingTankId) { if (targetTankId === startingTankId) {
return literals( return literals(
strings.bot.commands.research.errors.start_end_equal, strings.bot.commands.research.errors.start_end_equal,
[escapeMarkdown(unwrap(targetTank.name))], { tank: escapeMarkdown(unwrap(targetTank.name)) }
); );
} }
const startingTank = awaitedTankDefinitions.tanks[startingTankId]; const startingTank = awaitedTankDefinitions.tanks[startingTankId];
if (startingTank.tier > targetTank.tier) { if (startingTank.tier > targetTank.tier) {
return literals(strings.bot.commands.research.errors.unordered, [ return literals(strings.bot.commands.research.errors.unordered, {
escapeMarkdown(unwrap(startingTank.name)), tank_1: escapeMarkdown(unwrap(startingTank.name)),
escapeMarkdown(unwrap(targetTank.name)), tank_2: escapeMarkdown(unwrap(targetTank.name)),
escapeMarkdown(unwrap(targetTank.name)), });
]);
} }
if (!targetTankAncestry.includes(startingTankId)) { if (!targetTankAncestry.includes(startingTankId)) {
return literals( return literals(
strings.bot.commands.research.errors.tanks_not_on_line, strings.bot.commands.research.errors.tanks_not_on_line,
[ {
escapeMarkdown(unwrap(targetTank.name)), tank_1: escapeMarkdown(unwrap(targetTank.name)),
escapeMarkdown(unwrap(startingTank.name)), tank_2: escapeMarkdown(unwrap(startingTank.name)),
], }
); );
} }
} else { } else {
if (targetTank.type !== TankType.RESEARCHABLE) { if (targetTank.type !== TankType.RESEARCHABLE) {
return literals(strings.bot.commands.research.errors.non_tech_tree, [ return literals(strings.bot.commands.research.errors.non_tech_tree, {
escapeMarkdown(unwrap(targetTank.name)), tank: escapeMarkdown(unwrap(targetTank.name)),
]); });
} }
if (targetTank.tier === 1) { if (targetTank.tier === 1) {
return literals(strings.bot.commands.research.errors.tier_1, [ return literals(strings.bot.commands.research.errors.tier_1, {
escapeMarkdown(unwrap(targetTank.name)), tank: escapeMarkdown(unwrap(targetTank.name)),
]); });
} }
const tankStats = await getTankStats(region, id); const tankStats = await getTankStats(region, id);
@@ -150,18 +149,18 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) { if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) {
return literals( return literals(
strings.bot.commands.research.errors.already_researched, strings.bot.commands.research.errors.already_researched,
[escapeMarkdown(unwrap(targetTank.name))], { tank: escapeMarkdown(unwrap(targetTank.name)) }
); );
} }
const foundAncestor = targetTankAncestry.find( const foundAncestor = targetTankAncestry.find(
(id) => (id) =>
awaitedTankDefinitions.tanks[id].tier === 1 || awaitedTankDefinitions.tanks[id].tier === 1 ||
tankStats.some(({ tank_id }) => tank_id === id), tankStats.some(({ tank_id }) => tank_id === id)
); );
if (foundAncestor === undefined) { if (foundAncestor === undefined) {
throw new Error('No first owned tech tree found.'); throw new Error("No first owned tech tree found.");
} }
startingTankId = foundAncestor; startingTankId = foundAncestor;
@@ -169,12 +168,12 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
const techTreeLine = await buildTechTreeLine( const techTreeLine = await buildTechTreeLine(
startingTankId, startingTankId,
targetTankId, targetTankId
); );
const line = [...techTreeLine, startingTankId].reverse(); const line = [...techTreeLine, startingTankId].reverse();
const { nickname } = await getAccountInfo(region, id); 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 clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined;
const costs = await Promise.all( const costs = await Promise.all(
line.map(async (id) => { line.map(async (id) => {
@@ -187,18 +186,18 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
tank.turrets.forEach((turret) => { tank.turrets.forEach((turret) => {
if ( if (
turret.research_cost !== undefined && turret.research_cost !== undefined &&
typeof turret.research_cost.research_cost_type!.value === 'number' typeof turret.research_cost.research_cost_type!.value === "number"
) { ) {
turretXps.set( turretXps.set(
turret.id, turret.id,
turret.research_cost.research_cost_type!.value, turret.research_cost.research_cost_type!.value
); );
} }
turret.guns.forEach((gun) => { turret.guns.forEach((gun) => {
if ( if (
gun.research_cost !== undefined && gun.research_cost !== undefined &&
typeof gun.research_cost.research_cost_type!.value === 'number' typeof gun.research_cost.research_cost_type!.value === "number"
) { ) {
gunXps.set(gun.id, gun.research_cost.research_cost_type!.value); gunXps.set(gun.id, gun.research_cost.research_cost_type!.value);
} }
@@ -208,11 +207,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
tank.engines.forEach((engine) => { tank.engines.forEach((engine) => {
if ( if (
engine.research_cost !== undefined && engine.research_cost !== undefined &&
typeof engine.research_cost.research_cost_type!.value === 'number' typeof engine.research_cost.research_cost_type!.value === "number"
) { ) {
engineXps.set( engineXps.set(
engine.id, engine.id,
engine.research_cost.research_cost_type!.value, engine.research_cost.research_cost_type!.value
); );
} }
}); });
@@ -220,11 +219,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
tank.tracks.forEach((track) => { tank.tracks.forEach((track) => {
if ( if (
track.research_cost !== undefined && track.research_cost !== undefined &&
typeof track.research_cost.research_cost_type!.value === 'number' typeof track.research_cost.research_cost_type!.value === "number"
) { ) {
trackXps.set( trackXps.set(
track.id, track.id,
track.research_cost.research_cost_type!.value, track.research_cost.research_cost_type!.value
); );
} }
}); });
@@ -249,10 +248,10 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
purchase: tank.price.value, purchase: tank.price.value,
equipment: equipmentPriceMatrix[tank.tier].reduce( equipment: equipmentPriceMatrix[tank.tier].reduce(
(a, b) => a + 3 * b, (a, b) => a + 3 * b,
0, 0
), ),
}; };
}), })
); );
const image = ( const image = (
@@ -267,14 +266,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: 8, gap: 8,
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: "flex",
borderRadius: 4, borderRadius: 4,
backgroundColor: theme.colors.appBackground2, backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle, border: theme.borderStyles.subtle,
@@ -285,10 +284,10 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
style={{ style={{
flex: 1, flex: 1,
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
position: 'relative', position: "relative",
right: 24, right: 24,
}} }}
> >
@@ -304,23 +303,23 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
style={{ style={{
flex: 1, flex: 1,
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
justifyContent: 'center', justifyContent: "center",
gap: 8, gap: 8,
padding: '16px 0', padding: "16px 0",
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: "flex",
gap: 4, gap: 4,
alignItems: 'center', alignItems: "center",
}} }}
> >
<img <img
alt="XP icon" alt="XP icon"
src={await iconPng(asset('icons/currencies/xp.webp'))} src={await iconPng(asset("icons/currencies/xp.webp"))}
width={16} width={16}
height={16} height={16}
/> />
@@ -330,7 +329,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
}} }}
> >
Experience:{' '} Experience:{" "}
{costs {costs
.reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0) .reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0)
.toLocaleString(interaction.locale)} .toLocaleString(interaction.locale)}
@@ -339,14 +338,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
style={{ style={{
display: 'flex', display: "flex",
gap: 4, gap: 4,
alignItems: 'center', alignItems: "center",
}} }}
> >
<img <img
alt="Silver icon" alt="Silver icon"
src={await iconPng(asset('icons/currencies/silver.webp'))} src={await iconPng(asset("icons/currencies/silver.webp"))}
width={16} width={16}
height={16} height={16}
/> />
@@ -356,7 +355,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
}} }}
> >
Purchase:{' '} Purchase:{" "}
{costs {costs
.reduce((a, b) => a + b.purchase, 0) .reduce((a, b) => a + b.purchase, 0)
.toLocaleString(interaction.locale)} .toLocaleString(interaction.locale)}
@@ -365,14 +364,14 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
style={{ style={{
display: 'flex', display: "flex",
gap: 4, gap: 4,
alignItems: 'center', alignItems: "center",
}} }}
> >
<img <img
alt="Silver icon" alt="Silver icon"
src={await iconPng(asset('icons/currencies/silver.webp'))} src={await iconPng(asset("icons/currencies/silver.webp"))}
width={16} width={16}
height={16} height={16}
/> />
@@ -382,7 +381,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
color: theme.colors.textHighContrast, color: theme.colors.textHighContrast,
}} }}
> >
Equipment:{' '} Equipment:{" "}
{costs {costs
.reduce((a, b) => a + b.equipment, 0) .reduce((a, b) => a + b.equipment, 0)
.toLocaleString(interaction.locale)} .toLocaleString(interaction.locale)}
@@ -399,7 +398,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
key={id} key={id}
style={{ style={{
display: 'flex', display: "flex",
borderRadius: 4, borderRadius: 4,
backgroundColor: theme.colors.appBackground2, backgroundColor: theme.colors.appBackground2,
border: theme.borderStyles.subtle, border: theme.borderStyles.subtle,
@@ -408,9 +407,9 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
style={{ style={{
width: 48, width: 48,
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
}} }}
> >
<span <span
@@ -426,9 +425,9 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
style={{ style={{
flex: 1, flex: 1,
display: 'flex', display: "flex",
position: 'relative', position: "relative",
right: '5%', right: "5%",
}} }}
> >
<img <img
@@ -437,23 +436,23 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
style={{ style={{
height: 100, height: 100,
width: 100, width: 100,
objectFit: 'contain', objectFit: "contain",
position: 'absolute', position: "absolute",
left: '25%', left: "25%",
top: 0, top: 0,
}} }}
/> />
<div <div
style={{ style={{
display: 'flex', display: "flex",
gap: 4, gap: 4,
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
position: 'absolute', position: "absolute",
bottom: 8, bottom: 8,
left: '50%', left: "50%",
transform: 'translateX(-50%)', transform: "translateX(-50%)",
}} }}
> >
<img <img
@@ -476,23 +475,23 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
style={{ style={{
flex: 1, flex: 1,
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
justifyContent: 'center', justifyContent: "center",
gap: 8, gap: 8,
padding: '16px 0', padding: "16px 0",
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: "flex",
gap: 4, gap: 4,
alignItems: 'center', alignItems: "center",
}} }}
> >
<img <img
alt="XP icon" alt="XP icon"
src={await iconPng(asset('icons/currencies/xp.webp'))} src={await iconPng(asset("icons/currencies/xp.webp"))}
width={16} width={16}
height={16} height={16}
/> />
@@ -504,11 +503,11 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
> >
{literals( {literals(
strings.bot.commands.research.body.research, strings.bot.commands.research.body.research,
[ {
`${costs[index].research?.toLocaleString( cost: costs[index].research?.toLocaleString(
interaction.locale, interaction.locale
)}`, ),
], }
)} )}
</span> </span>
</div> </div>
@@ -516,15 +515,15 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
{costs[index].upgrades > 0 && ( {costs[index].upgrades > 0 && (
<div <div
style={{ style={{
display: 'flex', display: "flex",
gap: 4, gap: 4,
alignItems: 'center', alignItems: "center",
}} }}
> >
<img <img
alt="Free XP icon" alt="Free XP icon"
src={await iconPng( src={await iconPng(
asset('icons/currencies/free-xp.webp'), asset("icons/currencies/free-xp.webp")
)} )}
width={16} width={16}
height={16} height={16}
@@ -537,7 +536,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
> >
{literals( {literals(
strings.bot.commands.research.body.upgrades, strings.bot.commands.research.body.upgrades,
[costs[index].upgrades.toLocaleString()], { cost: costs[index].upgrades.toLocaleString() }
)} )}
</span> </span>
</div> </div>
@@ -545,15 +544,15 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
<div <div
style={{ style={{
display: 'flex', display: "flex",
gap: 4, gap: 4,
alignItems: 'center', alignItems: "center",
}} }}
> >
<img <img
alt="Silver icon" alt="Silver icon"
src={await iconPng( src={await iconPng(
asset('icons/currencies/silver.webp'), asset("icons/currencies/silver.webp")
)} )}
width={16} width={16}
height={16} height={16}
@@ -566,26 +565,26 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
> >
{literals( {literals(
strings.bot.commands.research.body.purchase, strings.bot.commands.research.body.purchase,
[ {
costs[index].purchase.toLocaleString( cost: costs[index].purchase.toLocaleString(
interaction.locale, interaction.locale
), ),
], }
)} )}
</span> </span>
</div> </div>
<div <div
style={{ style={{
display: 'flex', display: "flex",
gap: 4, gap: 4,
alignItems: 'center', alignItems: "center",
}} }}
> >
<img <img
alt="Silver icon" alt="Silver icon"
src={await iconPng( src={await iconPng(
asset('icons/currencies/silver.webp'), asset("icons/currencies/silver.webp")
)} )}
width={16} width={16}
height={16} height={16}
@@ -598,18 +597,18 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
> >
{literals( {literals(
strings.bot.commands.research.body.equipment, strings.bot.commands.research.body.equipment,
[ {
costs[index].equipment.toLocaleString( cost: costs[index].equipment.toLocaleString(
interaction.locale, interaction.locale
), ),
], }
)} )}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
); );
}), })
)} )}
</div> </div>
</CommandWrapper> </CommandWrapper>
@@ -621,7 +620,7 @@ export const researchCommand = new Promise<CommandRegistry>((resolve) => {
}, },
autocomplete(interaction) { 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 { ClanList, fetchBlitz, Region } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { Locale } from 'discord.js'; import { Locale } from "discord.js";
import markdownEscape from 'markdown-escape'; import markdownEscape from "markdown-escape";
import { addClanChoices } from '../core/discord/addClanChoices'; import { addClanChoices } from "../core/discord/addClanChoices";
import { addRegionChoices } from '../core/discord/addRegionChoices'; import { addRegionChoices } from "../core/discord/addRegionChoices";
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from '../core/discord/embedInfo'; import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from '../core/discord/localizationObject'; import { localizationObject } from "../core/discord/localizationObject";
import { translator } from '../core/localization/translator'; import { translator } from "../core/localization/translator";
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_LIMIT = 25; const DEFAULT_LIMIT = 25;
export const searchClansCommand = new Promise<CommandRegistry>((resolve) => { export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
resolve({ resolve({
command: createLocalizedCommand('search-clans') command: createLocalizedCommand("search-clans")
.addStringOption(addRegionChoices) .addStringOption(addRegionChoices)
.addStringOption((option) => .addStringOption((option) =>
addClanChoices(option).setAutocomplete(false), addClanChoices(option).setAutocomplete(false)
) )
.addIntegerOption((option) => { .addIntegerOption((option) => {
const { strings } = translator(Locale.EnglishUS); const { strings } = translator(Locale.EnglishUS);
@@ -28,21 +28,21 @@ export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
localizationObject( localizationObject(
(strings) => strings.bot.commands.search_clans.options.limit.name, (strings) => strings.bot.commands.search_clans.options.limit.name,
undefined, undefined,
true, true
), )
) )
.setDescription( .setDescription(
literals( literals(
strings.bot.commands.search_clans.options.limit.description, strings.bot.commands.search_clans.options.limit.description,
[`${DEFAULT_LIMIT}`], { limit: DEFAULT_LIMIT }
), )
) )
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject( localizationObject(
(strings) => (strings) =>
strings.bot.commands.search_clans.options.limit.description, strings.bot.commands.search_clans.options.limit.description,
[`${DEFAULT_LIMIT}`], { limit: DEFAULT_LIMIT }
), )
) )
.setMinValue(1) .setMinValue(1)
.setMaxValue(100); .setMaxValue(100);
@@ -50,24 +50,24 @@ export const searchClansCommand = new Promise<CommandRegistry>((resolve) => {
async handler(interaction) { async handler(interaction) {
const { strings } = translator(interaction.locale); const { strings } = translator(interaction.locale);
const region = interaction.options.getString('region') as Region; const region = interaction.options.getString("region") as Region;
const clan = interaction.options.getString('clan')!; const clan = interaction.options.getString("clan")!;
const limit = interaction.options.getInteger('limit') ?? DEFAULT_LIMIT; const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT;
const clanList = await fetchBlitz<ClanList>(region, 'clans/list', { const clanList = await fetchBlitz<ClanList>(region, "clans/list", {
search: clan, search: clan,
limit, limit,
}); });
return embedInfo( return embedInfo(
literals(strings.bot.commands.search_clans.body.title, [ literals(strings.bot.commands.search_clans.body.title, {
markdownEscape(clan), search: markdownEscape(clan),
strings.common.regions.normal[region], region: strings.common.regions.normal[region],
]), }),
clanList.length === 0 clanList.length === 0
? strings.bot.commands.search_clans.body.no_results ? strings.bot.commands.search_clans.body.no_results
: `\`\`\`\n${clanList : `\`\`\`\n${clanList
.map((clan) => `${clan.name} [${clan.tag}]`) .map((clan) => `${clan.name} [${clan.tag}]`)
.join('\n')}\n\`\`\``, .join("\n")}\n\`\`\``
); );
}, },
}); });
+30 -30
View File
@@ -3,17 +3,17 @@ import {
fetchBlitz, fetchBlitz,
Region, Region,
usernamePattern, usernamePattern,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { Locale } from 'discord.js'; import { Locale } from "discord.js";
import markdownEscape from 'markdown-escape'; import markdownEscape from "markdown-escape";
import { addRegionChoices } from '../core/discord/addRegionChoices'; import { addRegionChoices } from "../core/discord/addRegionChoices";
import { addUsernameChoices } from '../core/discord/addUsernameChoices'; import { addUsernameChoices } from "../core/discord/addUsernameChoices";
import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; import { createLocalizedCommand } from "../core/discord/createLocalizedCommand";
import { embedInfo } from '../core/discord/embedInfo'; import { embedInfo } from "../core/discord/embedInfo";
import { localizationObject } from '../core/discord/localizationObject'; import { localizationObject } from "../core/discord/localizationObject";
import { translator } from '../core/localization/translator'; import { translator } from "../core/localization/translator";
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from "../events/interactionCreate";
const DEFAULT_LIMIT = 25; const DEFAULT_LIMIT = 25;
@@ -21,10 +21,10 @@ export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
const { strings } = translator(Locale.EnglishUS); const { strings } = translator(Locale.EnglishUS);
resolve({ resolve({
command: createLocalizedCommand('search-players') command: createLocalizedCommand("search-players")
.addStringOption(addRegionChoices) .addStringOption(addRegionChoices)
.addStringOption((option) => .addStringOption((option) =>
addUsernameChoices(option).setAutocomplete(false).setRequired(true), addUsernameChoices(option).setAutocomplete(false).setRequired(true)
) )
.addIntegerOption((option) => .addIntegerOption((option) =>
option option
@@ -34,49 +34,49 @@ export const searchPlayersCommand = new Promise<CommandRegistry>((resolve) => {
(strings) => (strings) =>
strings.bot.commands.search_players.options.limit.name, strings.bot.commands.search_players.options.limit.name,
undefined, undefined,
true, true
), )
) )
.setDescription( .setDescription(
literals( literals(
strings.bot.commands.search_players.options.limit.description, strings.bot.commands.search_players.options.limit.description,
[`${DEFAULT_LIMIT}`], { limit: DEFAULT_LIMIT }
), )
) )
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject( localizationObject(
(strings) => (strings) =>
strings.bot.commands.search_players.options.limit.description, strings.bot.commands.search_players.options.limit.description,
[`${DEFAULT_LIMIT}`], { limit: DEFAULT_LIMIT }
), )
) )
.setMinValue(1) .setMinValue(1)
.setMaxValue(100), .setMaxValue(100)
), ),
async handler(interaction) { async handler(interaction) {
const { strings } = translator(interaction.locale); const { strings } = translator(interaction.locale);
const region = interaction.options.getString('region') as Region; const region = interaction.options.getString("region") as Region;
const name = interaction.options.getString('username')!; const name = interaction.options.getString("username")!;
const limit = interaction.options.getInteger('limit') ?? 25; const limit = interaction.options.getInteger("limit") ?? 25;
const trimmedSearch = name.trim(); const trimmedSearch = name.trim();
const players = usernamePattern.test(trimmedSearch) const players = usernamePattern.test(trimmedSearch)
? await fetchBlitz<AccountList>(region, 'account/list', { ? await fetchBlitz<AccountList>(region, "account/list", {
search: trimmedSearch, search: trimmedSearch,
limit, limit,
}) })
: []; : [];
return embedInfo( return embedInfo(
literals(strings.bot.commands.search_players.body.title, [ literals(strings.bot.commands.search_players.body.title, {
markdownEscape(trimmedSearch), search: markdownEscape(trimmedSearch),
strings.common.regions.normal[region], region: strings.common.regions.normal[region],
]), }),
`\`\`\`${ `\`\`\`${
players.length === 0 players.length === 0
? strings.bot.commands.search_players.body.no_results ? strings.bot.commands.search_players.body.no_results
: players.map((player) => player.nickname).join('\n') : players.map((player) => player.nickname).join("\n")
}\`\`\``, }\`\`\``
); );
}, },
}); });
+18 -16
View File
@@ -1,33 +1,33 @@
import { SEARCH_KEYS } from '@blitzkit/core'; import { SEARCH_KEYS } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { Locale } from 'discord.js'; import { Locale } from "discord.js";
import { go } from 'fuzzysort'; import { go } from "fuzzysort";
import markdownEscape from 'markdown-escape'; import markdownEscape from "markdown-escape";
import { tankDefinitions, tankNames } from '../blitzkit/nonBlockingPromises'; import { tankDefinitions, tankNames } from "../blitzkit/nonBlockingPromises";
import { UserError } from '../blitzkit/userError'; import { UserError } from "../blitzkit/userError";
import { tankNamesTechTreeOnly } from '../discord/autocompleteTanks'; import { tankNamesTechTreeOnly } from "../discord/autocompleteTanks";
import { translator } from '../localization/translator'; import { translator } from "../localization/translator";
export async function resolveTankId( export async function resolveTankId(
tank: string | number, tank: string | number,
locale: Locale, locale: Locale,
techTreeOnly = false, techTreeOnly = false
) { ) {
const { strings } = translator(locale); const { strings } = translator(locale);
const number = typeof tank === 'string' ? Number(tank) : tank; const number = typeof tank === "string" ? Number(tank) : tank;
if (Number.isNaN(number)) { if (Number.isNaN(number)) {
const searchResult = go( const searchResult = go(
`${tank}`, `${tank}`,
await (techTreeOnly ? tankNamesTechTreeOnly : tankNames), await (techTreeOnly ? tankNamesTechTreeOnly : tankNames),
{ keys: SEARCH_KEYS, limit: 1 }, { keys: SEARCH_KEYS, limit: 1 }
); );
if (searchResult.length === 0) { if (searchResult.length === 0) {
throw new UserError( throw new UserError(
literals(strings.bot.common.errors.tank_name_not_found, [ literals(strings.bot.common.errors.tank_name_not_found, {
markdownEscape(`${tank}`), search: markdownEscape(`${tank}`),
]), })
); );
} else { } else {
return searchResult[0].obj.id; return searchResult[0].obj.id;
@@ -39,7 +39,9 @@ export async function resolveTankId(
return number; return number;
} else { } else {
throw new UserError( throw new UserError(
literals(strings.bot.common.errors.tank_id_not_found, [`${number}`]), literals(strings.bot.common.errors.tank_id_not_found, {
search: number,
})
); );
} }
} }
@@ -1,12 +1,12 @@
import { fetchTankDefinitions, TIER_ROMAN_NUMERALS } from '@blitzkit/core'; import { fetchTankDefinitions, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { Locale } from 'discord.js'; import { Locale } from "discord.js";
import { StatFilters } from '../blitzstars/filterStats'; import { StatFilters } from "../blitzstars/filterStats";
import { translator } from '../localization/translator'; import { translator } from "../localization/translator";
export async function filtersToDescription( export async function filtersToDescription(
{ nation, tier, type, class: tankClass, tank }: StatFilters, { nation, tier, type, class: tankClass, tank }: StatFilters,
locale: Locale, locale: Locale
) { ) {
const { strings, bkLocale } = translator(locale); const { strings, bkLocale } = translator(locale);
const awaitedTankDefinitions = await fetchTankDefinitions(); const awaitedTankDefinitions = await fetchTankDefinitions();
@@ -18,11 +18,13 @@ export async function filtersToDescription(
info.push((strings.common.nations as Record<string, string>)[nation]); info.push((strings.common.nations as Record<string, string>)[nation]);
if (tier) { if (tier) {
info.push( info.push(
literals(strings.bot.common.filters.tier, [TIER_ROMAN_NUMERALS[tier]]), literals(strings.bot.common.filters.tier, {
tier: TIER_ROMAN_NUMERALS[tier],
})
); );
} }
if (tankClass) info.push(strings.common.tank_class_short[tankClass]); if (tankClass) info.push(strings.common.tank_class_short[tankClass]);
if (type) info.push(strings.common.tree_type[type]); 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 { jsxToSvg } from "./jsxToSvg";
import { svgToPng } from './svgToPng'; import { svgToPng } from "./svgToPng";
export async function jsxToPng(jsx: JSX.Element) { export async function jsxToPng(jsx: React.JSX.Element) {
const svg = await jsxToSvg(jsx); const svg = await jsxToSvg(jsx);
const png = svgToPng(svg); const png = svgToPng(svg);
@@ -1,10 +1,10 @@
import { jsxToSvg } from './jsxToSvg'; import { jsxToSvg } from "./jsxToSvg";
import { RenderConfiguration } from './renderConfiguration'; import { RenderConfiguration } from "./renderConfiguration";
import { svgToPngThreaded } from './svgToPngThreaded'; import { svgToPngThreaded } from "./svgToPngThreaded";
export async function jsxToPngThreaded( export async function jsxToPngThreaded(
jsx: JSX.Element, jsx: React.JSX.Element,
renderConfiguration: RenderConfiguration, renderConfiguration: RenderConfiguration
) { ) {
const svg = await jsxToSvg(jsx, renderConfiguration); const svg = await jsxToSvg(jsx, renderConfiguration);
const png = await svgToPngThreaded(svg); const png = await svgToPngThreaded(svg);
@@ -1,9 +1,9 @@
import { readFile } from 'fs/promises'; import { readFile } from "fs/promises";
import satori from 'satori'; import satori from "satori";
import { RenderConfiguration } from '../renderConfiguration'; import { RenderConfiguration } from "../renderConfiguration";
import robotoBlack from './Roboto-Black.ttf'; import robotoBlack from "./Roboto-Black.ttf";
import robotoBold from './Roboto-Bold.ttf'; import robotoBold from "./Roboto-Bold.ttf";
import roboto from './Roboto.ttf'; import roboto from "./Roboto.ttf";
const fontsPromise = [ const fontsPromise = [
{ file: roboto, weight: 400 as const }, { file: roboto, weight: 400 as const },
@@ -11,12 +11,12 @@ const fontsPromise = [
{ file: robotoBlack, weight: 900 as const }, { file: robotoBlack, weight: 900 as const },
].map(async ({ file, weight }) => { ].map(async ({ file, weight }) => {
const data = await readFile(`${__dirname}/${file}`); const data = await readFile(`${__dirname}/${file}`);
return { name: 'Roboto', weight, data }; return { name: "Roboto", weight, data };
}); });
export async function jsxToSvg( export async function jsxToSvg(
jsx: JSX.Element, jsx: React.JSX.Element,
renderConfiguration = new RenderConfiguration(), renderConfiguration = new RenderConfiguration()
) { ) {
const fonts = await Promise.all(fontsPromise); const fonts = await Promise.all(fontsPromise);
return await satori(jsx, { width: renderConfiguration.width, fonts }); return await satori(jsx, { width: renderConfiguration.width, fonts });
+17 -17
View File
@@ -1,13 +1,13 @@
import { numberToRomanNumeral } from '@blitzkit/core'; import { numberToRomanNumeral } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { import {
APIApplicationCommandOptionChoice, APIApplicationCommandOptionChoice,
Locale, Locale,
SlashCommandStringOption, SlashCommandStringOption,
} from 'discord.js'; } from "discord.js";
import { range } from 'lodash-es'; import { range } from "lodash-es";
import { translator } from '../localization/translator'; import { translator } from "../localization/translator";
import { localizationObject } from './localizationObject'; import { localizationObject } from "./localizationObject";
export function addTierChoices(option: SlashCommandStringOption) { export function addTierChoices(option: SlashCommandStringOption) {
const { strings } = translator(Locale.EnglishUS); const { strings } = translator(Locale.EnglishUS);
@@ -18,30 +18,30 @@ export function addTierChoices(option: SlashCommandStringOption) {
localizationObject( localizationObject(
(strings) => strings.bot.common.options.tier.name, (strings) => strings.bot.common.options.tier.name,
undefined, undefined,
true, true
), )
) )
.setDescription(strings.bot.common.options.tier.description) .setDescription(strings.bot.common.options.tier.description)
.setDescriptionLocalizations( .setDescriptionLocalizations(
localizationObject( localizationObject(
(strings) => strings.bot.common.options.tier.description, (strings) => strings.bot.common.options.tier.description
), )
) )
.setChoices( .setChoices(
...range(10, 0).map( ...range(10, 0).map(
(tier) => (tier) =>
({ ({
name: literals(strings.bot.common.options.tier.choices.tier, [ name: literals(strings.bot.common.options.tier.choices.tier, {
`${tier}`, tier_1: tier,
numberToRomanNumeral(tier), tier_2: numberToRomanNumeral(tier),
]), }),
value: `${tier}`, value: `${tier}`,
name_localizations: localizationObject( name_localizations: localizationObject(
(strings) => strings.bot.common.options.tier.choices.tier, (strings) => strings.bot.common.options.tier.choices.tier,
[`${tier}`, numberToRomanNumeral(tier)], { tier_1: tier, tier_2: numberToRomanNumeral(tier) }
), ),
}) satisfies APIApplicationCommandOptionChoice, }) satisfies APIApplicationCommandOptionChoice
), )
) )
.setRequired(true); .setRequired(true);
} }
@@ -1,11 +1,11 @@
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { Locale } from 'discord.js'; import { Locale } from "discord.js";
import { translator } from '../localization/translator'; import { translator } from "../localization/translator";
import { PeriodSize } from './addPeriodSubCommands'; import { PeriodSize } from "./addPeriodSubCommands";
export function getPeriodOptionName(period: PeriodSize, locale: Locale) { export function getPeriodOptionName(period: PeriodSize, locale: Locale) {
const { strings } = translator(locale); const { strings } = translator(locale);
if (period === 'career') return strings.bot.common.periods.career; if (period === "career") return strings.bot.common.periods.career;
if (period === 'today') return strings.bot.common.periods.today; if (period === "today") return strings.bot.common.periods.today;
return literals(strings.bot.common.periods.days, [period]); return literals(strings.bot.common.periods.days, { days: period });
} }
@@ -1,15 +1,15 @@
import { BlitzKitStrings, literals as literalsFunc } from '@blitzkit/i18n'; import { BlitzKitStrings, literals as literalsFunc } from "@blitzkit/i18n";
import { Locale } from 'discord.js'; import { Locale } from "discord.js";
import { SUPPORTED_LOCALES_DISCORD } from '../localization/strings/constants'; import { SUPPORTED_LOCALES_DISCORD } from "../localization/strings/constants";
import { translator } from '../localization/translator'; import { translator } from "../localization/translator";
const validNameRegex = const validNameRegex =
/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u; /^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u;
export function localizationObject( export function localizationObject(
path: (strings: BlitzKitStrings) => string, path: (strings: BlitzKitStrings) => string,
literals: string[] = [], literals?: Record<string, string | number>,
strict = false, strict = false
) { ) {
return SUPPORTED_LOCALES_DISCORD.reduce<Partial<Record<Locale, string>>>( return SUPPORTED_LOCALES_DISCORD.reduce<Partial<Record<Locale, string>>>(
(localizations, locale) => { (localizations, locale) => {
@@ -20,7 +20,7 @@ export function localizationObject(
if (!isValid) { if (!isValid) {
console.warn( console.warn(
`Invalid localization for ${locale} (${path}): ${translation}; skipping...`, `Invalid localization for ${locale} (${path}): ${translation}; skipping...`
); );
return localizations; return localizations;
} }
@@ -30,6 +30,6 @@ export function localizationObject(
[locale]: translation, [locale]: translation,
}; };
}, },
{}, {}
); );
} }
@@ -6,14 +6,14 @@ import {
InteractionEditReplyOptions, InteractionEditReplyOptions,
InteractionReplyOptions, InteractionReplyOptions,
MessageEditOptions, MessageEditOptions,
} from 'discord.js'; } from "discord.js";
import { InteractionReturnable } from '../../events/interactionCreate'; import { InteractionReturnable } from "../../events/interactionCreate";
import { Writeable } from '../../types/writable'; import { Writeable } from "../../types/writable";
import { jsxToPngThreaded } from '../blitzkit/jsxToPngThreaded'; import { jsxToPngThreaded } from "../blitzkit/jsxToPngThreaded";
import { RenderConfiguration } from '../blitzkit/renderConfiguration'; import { RenderConfiguration } from "../blitzkit/renderConfiguration";
export async function normalizeInteractionReturnable( export async function normalizeInteractionReturnable(
returnable: InteractionReturnable, returnable: InteractionReturnable
) { ) {
const images: [number, Buffer][] = []; const images: [number, Buffer][] = [];
const reply: InteractionEditReplyOptions & const reply: InteractionEditReplyOptions &
@@ -27,7 +27,7 @@ export async function normalizeInteractionReturnable(
await Promise.all( await Promise.all(
normalizedReturnable.map(async (item, index) => { normalizedReturnable.map(async (item, index) => {
if (typeof item === 'string') { if (typeof item === "string") {
reply.content = item; reply.content = item;
} else if (item instanceof EmbedBuilder) { } else if (item instanceof EmbedBuilder) {
if (!reply.embeds) reply.embeds = []; if (!reply.embeds) reply.embeds = [];
@@ -36,7 +36,7 @@ export async function normalizeInteractionReturnable(
if (!reply.components) if (!reply.components)
reply.components = [new ActionRowBuilder<ButtonBuilder>()]; reply.components = [new ActionRowBuilder<ButtonBuilder>()];
(reply.components[0] as ActionRowBuilder<ButtonBuilder>).addComponents( (reply.components[0] as ActionRowBuilder<ButtonBuilder>).addComponents(
item, item
); );
} else if (item instanceof AttachmentBuilder) { } else if (item instanceof AttachmentBuilder) {
if (!reply.files) reply.files = []; if (!reply.files) reply.files = [];
@@ -49,14 +49,14 @@ export async function normalizeInteractionReturnable(
const image = await jsxToPngThreaded(item, renderConfiguration); const image = await jsxToPngThreaded(item, renderConfiguration);
images.push([index, image]); images.push([index, image]);
} }
}), })
); );
if (images.length > 0) { if (images.length > 0) {
images.sort(([a], [b]) => b - a); images.sort(([a], [b]) => b - a);
if (!reply.files) reply.files = []; if (!reply.files) reply.files = [];
images.forEach(([, image]) => 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 { Region } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { ChatInputCommandInteraction } from 'discord.js'; import { ChatInputCommandInteraction } from "discord.js";
import markdownEscape from 'markdown-escape'; import markdownEscape from "markdown-escape";
import { searchClansAcrossRegionsBotWrapper } from '../blitz/searchClansAcrossRegionsBotWrapper'; import { searchClansAcrossRegionsBotWrapper } from "../blitz/searchClansAcrossRegionsBotWrapper";
import { UserError } from '../blitzkit/userError'; import { UserError } from "../blitzkit/userError";
import { translator } from '../localization/translator'; import { translator } from "../localization/translator";
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
export async function resolveClanFromCommand( export async function resolveClanFromCommand(
interaction: ChatInputCommandInteraction, interaction: ChatInputCommandInteraction
) { ) {
const { strings } = translator(interaction.locale); const { strings } = translator(interaction.locale);
const clan = interaction.options.getString('clan', true); const clan = interaction.options.getString("clan", true);
if (serverAndIdPattern.test(clan)) { if (serverAndIdPattern.test(clan)) {
const [server, accountId] = clan.split('/'); const [server, accountId] = clan.split("/");
return { region: server as Region, id: Number(accountId) }; return { region: server as Region, id: Number(accountId) };
} else { } else {
const accounts = await searchClansAcrossRegionsBotWrapper( const accounts = await searchClansAcrossRegionsBotWrapper(
clan, clan,
interaction.locale, interaction.locale
); );
if (accounts[0]) { if (accounts[0]) {
return { region: accounts[0].region, id: accounts[0].clan_id }; return { region: accounts[0].region, id: accounts[0].clan_id };
} else { } else {
throw new UserError( throw new UserError(
literals(strings.bot.common.errors.clan_not_found, [ literals(strings.bot.common.errors.clan_not_found, {
markdownEscape(clan), search: markdownEscape(clan),
]), })
); );
} }
} }
@@ -1,11 +1,11 @@
import { getTimeDaysAgo, Region } from '@blitzkit/core'; import { getTimeDaysAgo, Region } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { CacheType, ChatInputCommandInteraction } from "discord.js";
import { getPeriodNow } from '../blitzkit/getPeriodNow'; import { getPeriodNow } from "../blitzkit/getPeriodNow";
import { getPeriodStart } from '../blitzkit/getPeriodStart'; import { getPeriodStart } from "../blitzkit/getPeriodStart";
import { translator } from '../localization/translator'; import { translator } from "../localization/translator";
import { PeriodType } from './addPeriodSubCommands'; import { PeriodType } from "./addPeriodSubCommands";
import { getPeriodOptionName } from './getPeriodOptionName'; import { getPeriodOptionName } from "./getPeriodOptionName";
export interface ResolvedPeriod { export interface ResolvedPeriod {
name: string; name: string;
@@ -16,7 +16,7 @@ export interface ResolvedPeriod {
export function resolvePeriodFromCommand( export function resolvePeriodFromCommand(
region: Region, region: Region,
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>,
forcedPeriod?: PeriodType, forcedPeriod?: PeriodType
) { ) {
const { strings } = translator(interaction.locale); const { strings } = translator(interaction.locale);
let name: string; let name: string;
@@ -26,16 +26,16 @@ export function resolvePeriodFromCommand(
? forcedPeriod ? forcedPeriod
: (interaction.options.getSubcommand() as PeriodType); : (interaction.options.getSubcommand() as PeriodType);
if (periodSubcommand === 'custom') { if (periodSubcommand === "custom") {
const startOption = interaction.options.getInteger('start', true); const startOption = interaction.options.getInteger("start", true);
const endOption = interaction.options.getInteger('end', true); const endOption = interaction.options.getInteger("end", true);
const startDaysAgoMin = Math.min(startOption, endOption); const startDaysAgoMin = Math.min(startOption, endOption);
const endDaysAgoMax = Math.max(startOption, endOption); const endDaysAgoMax = Math.max(startOption, endOption);
name = literals(strings.bot.common.periods.custom, [ name = literals(strings.bot.common.periods.custom, {
`${startDaysAgoMin}`, day_1: startDaysAgoMin,
`${endDaysAgoMax}`, day_2: endDaysAgoMax,
]); });
start = getTimeDaysAgo(region, endDaysAgoMax); start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(region, startDaysAgoMin); end = getTimeDaysAgo(region, startDaysAgoMin);
} else { } else {
@@ -1,11 +1,11 @@
import { idToRegion, Region } from '@blitzkit/core'; import { idToRegion, Region } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n'; import { literals } from "@blitzkit/i18n";
import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { CacheType, ChatInputCommandInteraction } from "discord.js";
import markdownEscape from 'markdown-escape'; import markdownEscape from "markdown-escape";
import { searchPlayersAcrossRegionsBotWrapper } from '../../blitz/searchPlayersAcrossRegionsBotWrapper'; import { searchPlayersAcrossRegionsBotWrapper } from "../../blitz/searchPlayersAcrossRegionsBotWrapper";
import { getBlitzFromDiscord } from '../../blitzkit/getBlitzFromDiscord'; import { getBlitzFromDiscord } from "../../blitzkit/getBlitzFromDiscord";
import { UserError } from '../../blitzkit/userError'; import { UserError } from "../../blitzkit/userError";
import { translator } from '../../localization/translator'; import { translator } from "../../localization/translator";
export interface ResolvedPlayer { export interface ResolvedPlayer {
region: Region; region: Region;
@@ -13,10 +13,10 @@ export interface ResolvedPlayer {
} }
export async function resolvePlayerFromCommand( export async function resolvePlayerFromCommand(
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>
) { ) {
const { strings } = translator(interaction.locale); const { strings } = translator(interaction.locale);
const commandUsername = interaction.options.getString('username'); const commandUsername = interaction.options.getString("username");
if (commandUsername !== null) { if (commandUsername !== null) {
const id = parseInt(commandUsername); const id = parseInt(commandUsername);
@@ -27,7 +27,7 @@ export async function resolvePlayerFromCommand(
} else { } else {
const accounts = await searchPlayersAcrossRegionsBotWrapper( const accounts = await searchPlayersAcrossRegionsBotWrapper(
commandUsername, commandUsername,
interaction.locale, interaction.locale
); );
if (accounts[0]) { if (accounts[0]) {
@@ -37,9 +37,9 @@ export async function resolvePlayerFromCommand(
} satisfies ResolvedPlayer; } satisfies ResolvedPlayer;
} else { } else {
throw new UserError( throw new UserError(
literals(strings.bot.common.errors.player_not_found, [ literals(strings.bot.common.errors.player_not_found, {
markdownEscape(commandUsername), search: markdownEscape(commandUsername),
]), })
); );
} }
} }
@@ -1,7 +1,7 @@
import { DEFAULT_LOCALE, SupportedLocale } from '@blitzkit/i18n'; import locales from "@blitzkit/i18n/locales.json";
import { Locale } from 'discord.js'; import { Locale } from "discord.js";
const SUPPORTED_LOCALES_DISCORD_MAP: Record<SupportedLocale, Locale> = { const SUPPORTED_LOCALES_DISCORD_MAP: Record<string, Locale> = {
en: Locale.EnglishUS, en: Locale.EnglishUS,
es: Locale.SpanishES, es: Locale.SpanishES,
pt: Locale.PortugueseBR, pt: Locale.PortugueseBR,
@@ -14,12 +14,12 @@ const SUPPORTED_LOCALES_DISCORD_MAP: Record<SupportedLocale, Locale> = {
}; };
export const SUPPORTED_LOCALES_DISCORD_MAP_INVERSE = Object.fromEntries( export const SUPPORTED_LOCALES_DISCORD_MAP_INVERSE = Object.fromEntries(
Object.entries(SUPPORTED_LOCALES_DISCORD_MAP).map(([k, v]) => [v, k]), Object.entries(SUPPORTED_LOCALES_DISCORD_MAP).map(([k, v]) => [v, k])
) as Record<Locale, SupportedLocale>; ) as Record<Locale, string>;
export const SUPPORTED_LOCALES_DISCORD = Object.values( export const SUPPORTED_LOCALES_DISCORD = Object.values(
SUPPORTED_LOCALES_DISCORD_MAP, SUPPORTED_LOCALES_DISCORD_MAP
); );
export const DEFAULT_LOCALE_DISCORD = export const DEFAULT_LOCALE_DISCORD =
SUPPORTED_LOCALES_DISCORD_MAP[DEFAULT_LOCALE]; SUPPORTED_LOCALES_DISCORD_MAP[locales.default];
+12 -14
View File
@@ -1,8 +1,8 @@
import { ReadStream } from '@blitzkit/core'; import { ReadStream } from "@blitzkit/core";
import AdmZip from 'adm-zip'; import AdmZip from "adm-zip";
import { readFile, writeFile } from 'fs/promises'; import { readFile } from "fs/promises";
import { Parser } from 'pickleparser'; import { Parser } from "pickleparser";
import protobuf from 'protobufjs'; import protobuf from "protobufjs";
interface WotbReplayMetaJson { interface WotbReplayMetaJson {
version: string; version: string;
@@ -24,26 +24,24 @@ export class WotbReplayReadStream extends ReadStream {
async wotbReplay() { async wotbReplay() {
const pickleParser = new Parser(); const pickleParser = new Parser();
const zip = new AdmZip(Buffer.from(this.buffer)); const zip = new AdmZip(Buffer.from(this.buffer));
const metaJsonBuffer = zip.getEntry('meta.json')?.getData(); const metaJsonBuffer = zip.getEntry("meta.json")?.getData();
const battleResultsDat = zip.getEntry('battle_results.dat')?.getData(); const battleResultsDat = zip.getEntry("battle_results.dat")?.getData();
if (!metaJsonBuffer || !battleResultsDat) if (!metaJsonBuffer || !battleResultsDat)
throw new SyntaxError('meta.json not found'); throw new SyntaxError("meta.json not found");
const metaJson = JSON.parse( const metaJson = JSON.parse(
metaJsonBuffer.toString(), metaJsonBuffer.toString()
) as WotbReplayMetaJson; ) as WotbReplayMetaJson;
const unpickledBattleResults = pickleParser.parse(battleResultsDat); const unpickledBattleResults = pickleParser.parse(battleResultsDat);
const [, battleResultsString] = unpickledBattleResults as [number, string]; const [, battleResultsString] = unpickledBattleResults as [number, string];
const root = await protobuf.load( const root = await protobuf.load(
`${__dirname}/../../protos/BattleResultsClient.proto`, `${__dirname}/../../protos/BattleResultsClient.proto`
); );
const BattleResults = root.lookupType( const BattleResults = root.lookupType(
'BattleResultsGenerated.BattleResultsClient', "BattleResultsGenerated.BattleResultsClient"
); );
writeFile('test.dat', Buffer.from(battleResultsString));
const reader = protobuf.Reader.create(Buffer.from(battleResultsString)); const reader = protobuf.Reader.create(Buffer.from(battleResultsString));
// const decodedBattleResults = BattleResults.decodeDelimited( // const decodedBattleResults = BattleResults.decodeDelimited(
@@ -53,7 +51,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); const stream = new WotbReplayReadStream(replay.buffer as ArrayBuffer);
stream.wotbReplay(); stream.wotbReplay();
@@ -1,4 +1,4 @@
import { assertSecret } from '@blitzkit/core'; import { assertSecret } from "@blitzkit/core";
import { import {
AttachmentBuilder, AttachmentBuilder,
AutocompleteInteraction, AutocompleteInteraction,
@@ -13,30 +13,30 @@ import {
SlashCommandBuilder, SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder, SlashCommandOptionsOnlyBuilder,
SlashCommandSubcommandsOnlyBuilder, SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js'; } from "discord.js";
import { aboutCommand } from '../../commands/about'; import { aboutCommand } from "../../commands/about";
import { breakdownCommand } from '../../commands/breakdown'; import { breakdownCommand } from "../../commands/breakdown";
import { debugCommand } from '../../commands/debug'; import { debugCommand } from "../../commands/debug";
import { evolutionCommand } from '../../commands/evolution'; import { evolutionCommand } from "../../commands/evolution";
import { fullStatsCommand } from '../../commands/fullStats'; import { fullStatsCommand } from "../../commands/fullStats";
import { inactiveCommand } from '../../commands/inactive'; import { inactiveCommand } from "../../commands/inactive";
import { verifyCommand } from '../../commands/link'; import { verifyCommand } from "../../commands/link";
import { ownedTanksCommand } from '../../commands/ownedTanks'; import { ownedTanksCommand } from "../../commands/ownedTanks";
import { permissionsCommand } from '../../commands/permissions'; import { permissionsCommand } from "../../commands/permissions";
import { pingCommand } from '../../commands/ping'; import { pingCommand } from "../../commands/ping";
import { playerInfoCommand } from '../../commands/playerInfo'; import { playerInfoCommand } from "../../commands/playerInfo";
import { ratingCommand } from '../../commands/rating'; import { ratingCommand } from "../../commands/rating";
import { ratingLeaderboardCommand } from '../../commands/ratingLeaderboard'; import { ratingLeaderboardCommand } from "../../commands/ratingLeaderboard";
import { replayCommand } from '../../commands/replay'; import { replayCommand } from "../../commands/replay";
import { researchCommand } from '../../commands/research'; import { researchCommand } from "../../commands/research";
import { searchClansCommand } from '../../commands/searchClans'; import { searchClansCommand } from "../../commands/searchClans";
import { searchPlayersCommand } from '../../commands/searchPlayers'; import { searchPlayersCommand } from "../../commands/searchPlayers";
import { statsCommand } from '../../commands/stats'; import { statsCommand } from "../../commands/stats";
import { todayCommand } from '../../commands/today'; import { todayCommand } from "../../commands/today";
import { RenderConfiguration } from '../../core/blitzkit/renderConfiguration'; import { RenderConfiguration } from "../../core/blitzkit/renderConfiguration";
import { handleAutocomplete } from './handlers/autocomplete'; import { handleAutocomplete } from "./handlers/autocomplete";
import { handleButton } from './handlers/button'; import { handleButton } from "./handlers/button";
import { handleChatInputCommand } from './handlers/chatInputCommand'; import { handleChatInputCommand } from "./handlers/chatInputCommand";
export type InteractionRawReturnable = export type InteractionRawReturnable =
| string | string
@@ -44,7 +44,7 @@ export type InteractionRawReturnable =
| ButtonBuilder | ButtonBuilder
| AttachmentBuilder | AttachmentBuilder
| RenderConfiguration | RenderConfiguration
| JSX.Element | React.JSX.Element
| null; | null;
export type InteractionIterableReturnable = export type InteractionIterableReturnable =
| InteractionRawReturnable | InteractionRawReturnable
@@ -58,7 +58,7 @@ interface CommandRegistryBase {
| SlashCommandBuilder | SlashCommandBuilder
| SlashCommandSubcommandsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder
| SlashCommandOptionsOnlyBuilder | SlashCommandOptionsOnlyBuilder
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>; | Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">;
autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void; autocomplete?: (interaction: AutocompleteInteraction<CacheType>) => void;
button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable; button?: (interaction: ButtonInteraction<CacheType>) => InteractionReturnable;
@@ -72,7 +72,7 @@ interface CommandRegistryImplicit extends CommandRegistryBase {
interface CommandRegistryExplicit extends CommandRegistryBase { interface CommandRegistryExplicit extends CommandRegistryBase {
handlesInteraction?: false; handlesInteraction?: false;
handler: ( handler: (
interaction: ChatInputCommandInteraction<CacheType>, interaction: ChatInputCommandInteraction<CacheType>
) => InteractionReturnable; ) => InteractionReturnable;
} }
@@ -104,16 +104,16 @@ export const COMMANDS_RAW: Promise<CommandRegistry>[] = [
export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => { export const commands = Promise.allSettled(COMMANDS_RAW).then((rawCommands) => {
return rawCommands.reduce<Record<string, CommandRegistry>>( return rawCommands.reduce<Record<string, CommandRegistry>>(
(commands, registry, index) => { (commands, registry, index) => {
if (registry.status === 'rejected') { if (registry.status === "rejected") {
console.warn( console.warn(
`Command ${index} failed to load; skipping...`, `Command ${index} failed to load; skipping...`,
registry.reason, registry.reason
); );
return commands; return commands;
} }
return { ...commands, [registry.value.command.name]: registry.value }; 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) => { commands.then((awaitedCommands) => {
const body = Object.values(awaitedCommands).map((registry) => const body = Object.values(awaitedCommands).map((registry) =>
registry.command.toJSON(), registry.command.toJSON()
); );
rest.put( rest.put(
Routes.applicationCommands(assertSecret(import.meta.env.DISCORD_CLIENT_ID)), Routes.applicationCommands(assertSecret(import.meta.env.DISCORD_CLIENT_ID)),
{ body }, { body }
); );
}); });
+8 -8
View File
@@ -4,20 +4,20 @@
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"dependencies": { "dependencies": {
"@octokit/plugin-throttling": "^8.2.0", "@octokit/plugin-throttling": "^11.0.1",
"@octokit/rest": "^20.1.1", "@octokit/rest": "^22.0.0",
"@prisma/client": "^6.6.0", "@prisma/client": "^6.16.2",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"crc": "^4.3.2", "crc": "^4.3.2",
"dotenv": "^16.4.5", "dotenv": "^17.2.2",
"lz4js": "^0.2.0", "lz4js": "^0.2.0",
"pickleparser": "^0.2.1", "pickleparser": "^0.2.1",
"protobufjs": "^7.4.0", "protobufjs": "^7.5.4",
"ts-proto": "^2.2.2", "ts-proto": "^2.7.7",
"yaml": "^2.5.0" "yaml": "^2.8.1"
}, },
"devDependencies": { "devDependencies": {
"@types/adm-zip": "^0.5.5", "@types/adm-zip": "^0.5.7",
"@types/lz4js": "^0.2.1" "@types/lz4js": "^0.2.1"
} }
} }
+2 -2
View File
@@ -1,4 +1,4 @@
export function assertSecret(secret?: string) { export function assertSecret<Type>(secret?: Type) {
if (secret === undefined) throw new Error('Missing secret'); if (secret === undefined) throw new Error("Missing secret");
return secret; return secret;
} }
+2 -5
View File
@@ -1,10 +1,7 @@
import { asset } from '@blitzkit/core'; import { asset } from "@blitzkit/core";
export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 }; export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 };
export function tankIcon( export function tankIcon(id: number, size: "big" | "small" = "big") {
id: number,
size: 'big' | 'small' | 'blitzkit' = 'blitzkit',
) {
return asset(`icons/tanks/${size}/${id}.webp`); return asset(`icons/tanks/${size}/${id}.webp`);
} }
+18 -16
View File
@@ -1,8 +1,8 @@
import { deburr } from 'lodash-es'; import locales from "@blitzkit/i18n/locales.json";
import { SUPPORTED_LOCALES } from '../../../i18n/src/strings'; import { deburr } from "lodash-es";
import { I18nString } from '../protos'; import { I18nString } from "../protos";
import { fetchCamouflageDefinitions } from './camouflageDefinitions'; import { fetchCamouflageDefinitions } from "./camouflageDefinitions";
import { fetchTankDefinitions } from './tankDefinitions'; import { fetchTankDefinitions } from "./tankDefinitions";
export async function fetchTankNames() { export async function fetchTankNames() {
const [tankDefinitions, camouflageDefinitions] = await Promise.all([ const [tankDefinitions, camouflageDefinitions] = await Promise.all([
@@ -26,25 +26,27 @@ export async function fetchTankNames() {
searchableNameDeburr, searchableNameDeburr,
camouflages: tank.camouflages camouflages: tank.camouflages
?.map((id) => ?.map((id) =>
SUPPORTED_LOCALES.map( locales.supported.map(
(locale) => ({ locale }) =>
camouflageDefinitions.camouflages[id]?.name.locales[locale], camouflageDefinitions.camouflages[id]?.name.locales[locale]
), )
) )
.flat() .flat()
.filter(Boolean) .filter(Boolean)
.map(deburr) .map(deburr)
.join(' '), .join(" "),
treeType: tank.type, treeType: tank.type,
}; };
}), })
); );
} }
export const SEARCH_KEYS = [ export const SEARCH_KEYS = [
...SUPPORTED_LOCALES.map((locale) => [ ...locales.supported
`searchableName.locales.${locale}`, .map(({ locale }) => [
`searchableNameDeburr.locales.${locale}`, `searchableName.locales.${locale}`,
]).flat(), `searchableNameDeburr.locales.${locale}`,
'camouflages', ])
.flat(),
"camouflages",
]; ];
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"dependencies": { "dependencies": {
"ts-deepmerge": "^7.0.2" "ts-deepmerge": "^7.0.3"
}, },
"devDependencies": {} "devDependencies": {}
} }
+4 -3
View File
@@ -1,3 +1,4 @@
export * from './literals'; export * from "./literals";
export * from './strings'; export * from "./strings";
export * from './unwrapper'; export * from "./unwrapper";
export * from "./wrapper";
+19 -4
View File
@@ -1,5 +1,20 @@
export function literals(string: string, literals: string[]) { import { assertSecret } from "@blitzkit/core";
return literals.reduce((accumulator, literal, index) => {
return accumulator.replace(`%s${index + 1}`, literal); const dev = assertSecret(import.meta.env.DEV);
}, string);
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;
} }
+50 -82
View File
@@ -1,98 +1,66 @@
import { merge } from 'ts-deepmerge'; import { merge } from "ts-deepmerge";
import en from '../strings/en.json'; import locales from "../locales.json";
import es from '../strings/es.json'; import en from "../strings/en.json";
import fr from '../strings/fr.json'; import es from "../strings/es.json";
import ja from '../strings/ja.json'; import fr from "../strings/fr.json";
import pl from '../strings/pl.json'; import ja from "../strings/ja.json";
import pt from '../strings/pt.json'; import pl from "../strings/pl.json";
import ru from '../strings/ru.json'; import pt from "../strings/pt.json";
import uk from '../strings/uk.json'; import ru from "../strings/ru.json";
import zh from '../strings/zh.json'; import uk from "../strings/uk.json";
import zh from "../strings/zh.json";
export type BlitzKitStrings = typeof en; export type BlitzKitStrings = typeof en;
const stringsRaw = { en, es, fr, ja, pl, pt, ru, uk, zh }; const stringsRaw: Record<string, TranslationTree> = {
en,
es,
fr,
ja,
pl,
pt,
ru,
uk,
zh,
};
export const SUPPORTED_LOCALES = Object.keys(stringsRaw) as SupportedLocale[]; export const STRINGS: Record<string, BlitzKitStrings> = {};
export const DEFAULT_LOCALE = SUPPORTED_LOCALES[0];
function redact(string: string) {
const matches = [...string.matchAll(/%s\d+/g)];
const characters = string
.split('')
.map((char) => (char === ' ' ? ' ' : '█')) as string[];
matches.forEach((match) => {
const start = match.index;
const matchText = match[0];
for (let i = 0; i < matchText.length; i++) {
characters[start + i] = matchText[i];
}
});
return characters.join('');
}
function nuke(object: TranslationTree) {
for (const key in object) {
const typedKey = key as keyof typeof object;
if (typeof object[typedKey] === 'string') {
const value = object[typedKey];
object[typedKey] = redact(value as string);
} else nuke(object[typedKey] as TranslationTree);
}
}
const stringsPartial: Partial<Record<SupportedLocale, TranslationTree>> = {};
for (const locale in stringsRaw) { for (const locale in stringsRaw) {
if (locale === DEFAULT_LOCALE) { if (locale === locales.default) {
stringsPartial[DEFAULT_LOCALE] = stringsRaw[DEFAULT_LOCALE]; STRINGS[locales.default] = stringsRaw[locales.default] as BlitzKitStrings;
continue; continue;
} }
const localeStrings = stringsRaw[locale as SupportedLocale]; const localeStrings = stringsRaw[locale];
stringsPartial[locale as SupportedLocale] = merge( STRINGS[locale] = merge(
stringsRaw[DEFAULT_LOCALE], stringsRaw[locales.default],
localeStrings, localeStrings
); ) as BlitzKitStrings;
if (import.meta.env.PUBLIC_DEBUG_MISSING_I18N === 'true') {
nuke(stringsPartial[locale as SupportedLocale]!);
}
} }
export const STRINGS = stringsPartial as Record< export const SUPPORTED_LOCALE_BLITZ_MAP: Record<string, string> = {
SupportedLocale, en: "en",
BlitzKitStrings es: "es",
>; fr: "fr",
ja: "ja",
export type SupportedLocale = keyof typeof stringsRaw; pt: "pt",
ru: "ru",
export const SUPPORTED_LOCALE_BLITZ_MAP: Record<SupportedLocale, string> = { uk: "uk",
en: 'en', zh: "zh-Hans",
es: 'es', pl: "pl",
fr: 'fr',
ja: 'ja',
pt: 'pt',
ru: 'ru',
uk: 'uk',
zh: 'zh-Hans',
pl: 'pl',
}; };
export const SUPPORTED_LOCALE_FLAGS: Record<SupportedLocale, string> = { export const SUPPORTED_LOCALE_FLAGS: Record<string, string> = {
en: '🇺🇸', en: "🇺🇸",
es: '🇪🇸', es: "🇪🇸",
fr: '🇫🇷', fr: "🇫🇷",
ja: '🇯🇵', ja: "🇯🇵",
pt: '🇵🇹', pt: "🇵🇹",
ru: '🇷🇺', ru: "🇷🇺",
uk: '🇺🇦', uk: "🇺🇦",
zh: '🇨🇳', zh: "🇨🇳",
pl: '🇵🇱', pl: "🇵🇱",
}; };
export type TranslationTree = { [key: string]: TranslationNode }; export type TranslationTree = { [key: string]: TranslationNode };
+4 -4
View File
@@ -1,12 +1,12 @@
import { I18nString } from '@blitzkit/core'; import { I18nString } from "@blitzkit/core";
import { DEFAULT_LOCALE } from './strings'; import locales from "@blitzkit/i18n/locales.json";
export function unwrapper(locale: string) { export function unwrapper(locale = locales.default) {
return function (i18nString: I18nString) { return function (i18nString: I18nString) {
if (locale in i18nString.locales) { if (locale in i18nString.locales) {
return i18nString.locales[locale]; return i18nString.locales[locale];
} }
return i18nString.locales[DEFAULT_LOCALE]; return i18nString.locales[locales.default];
}; };
} }
+15
View File
@@ -0,0 +1,15 @@
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;
}
+108 -109
View File
@@ -1,19 +1,21 @@
{ {
"test": { "test": {
"default": "test", "default": "test",
"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.", "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}.",
"fox": "The quick brown fox jumps over the lazy dog." "fox": "The quick brown fox jumps over the lazy dog."
}, },
"common": { "common": {
"name": "BlitzKit",
"units": { "units": {
"m": "%s1m", "m": "{value}m",
"mm": "%s1mm", "mm": "{value}mm",
"deg": "%s1°", "deg": "{value}°",
"hp": "%s1hp", "hp": "{value}hp",
"x": "x%s1", "x": "x{value}",
"percentage": "%s1%", "percentage": "{value}%",
"deg_s": "%s1°/s" "deg_s": "{value}°/s"
}, },
"gun_types": { "gun_types": {
@@ -204,11 +206,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"clan_not_found": "# Could not find clan\nCouldn't find \"%s1\". Try selecting a clan from the search result.", "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 \"%s1\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.", "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.",
"player_not_linked": "# Use `/link` to get started\nI don't know who you are but you can tell me today!", "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 \"%s1\".", "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 \"%s1\". Try typing the name of the tank instead of a number.", "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.",
"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.", "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", "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.", "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.",
@@ -313,7 +315,7 @@
"description": "Tier", "description": "Tier",
"choices": { "choices": {
"tier": "Tier %s1 - %s2" "tier": "Tier {tier_1} - {tier_2}"
} }
} }
}, },
@@ -321,13 +323,13 @@
"periods": { "periods": {
"career": "Career", "career": "Career",
"today": "Today", "today": "Today",
"days": "%s1 days", "days": "{days} days",
"custom": "%s1 - %s2 days" "custom": "{day_1} - {day_2} days"
}, },
"filters": { "filters": {
"none": "No filters", "none": "No filters",
"tier": "Tier %s1" "tier": "Tier {tier}"
}, },
"no_data": { "no_data": {
@@ -354,24 +356,6 @@
}, },
"commands": { "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": { "replay": {
"name": "replay", "name": "replay",
"description": "Display replay results" "description": "Display replay results"
@@ -395,20 +379,20 @@
"body": { "body": {
"subtitle": "Research", "subtitle": "Research",
"total": "Total", "total": "Total",
"research": "Research: %s1", "research": "Research: {cost}",
"upgrades": "Upgrades: %s1", "upgrades": "Upgrades: {cost}",
"purchase": "Purchase: %s1", "purchase": "Purchase: {cost}",
"equipment": "Equipment: %s1", "equipment": "Equipment: {cost}",
"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." "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": { "errors": {
"non_tech_tree": "# %s1 is not on the tech tree\n\nYou cannot research a premium/collector tank.", "non_tech_tree": "# {tank} 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.", "tier_1": "# {tank} 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.", "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": "# %s1 was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.", "start_end_equal": "# {tank} 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.", "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": "# %s1 higher tiered to %s2\n\nMake sure to provide %s3 as the `starting-tank` instead as it is the lower tiered tank." "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."
} }
}, },
@@ -454,7 +438,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "Winrate", "winrate": "Winrate",
"damage": "Damage", "damage": "Damage",
"unknown_tank": "Unknown tank %s1" "unknown_tank": "Unknown tank {id}"
} }
}, },
@@ -497,13 +481,13 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "threshold", "name": "threshold",
"description": "The number of days inactive (default: %s1 days)" "description": "The number of days inactive (default: {days} days)"
} }
}, },
"body": { "body": {
"subtitle": "Inactive for %s1+ days", "subtitle": "Inactive for {days}+ days",
"listing": "%s1 days" "listing": "{days} days"
} }
}, },
@@ -550,7 +534,7 @@
"body": { "body": {
"accounts_linked": "Accounts linked", "accounts_linked": "Accounts linked",
"no_role_permissions": { "no_role_permissions": {
"title": "%s1 failed to verify", "title": "{name} failed to verify",
"description": "I don't have the permission to manage roles." "description": "I don't have the permission to manage roles."
} }
}, },
@@ -568,12 +552,12 @@
"options": { "options": {
"limit": { "limit": {
"name": "limit", "name": "limit",
"description": "The max number of clan results (default: %s1)" "description": "The max number of clan results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Clan search for %s1 in region %s2", "title": "Clan search for {search} in region {region}",
"no_results": "No clans found" "no_results": "No clans found"
} }
}, },
@@ -585,12 +569,12 @@
"options": { "options": {
"limit": { "limit": {
"name": "limit", "name": "limit",
"description": "The max number of player results (default: %s1)" "description": "The max number of player results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Player search for %s1 in region %s2", "title": "Player search for {search} in region {region}",
"no_results": "No players found" "no_results": "No players found"
} }
}, },
@@ -614,7 +598,7 @@
"options": { "options": {
"limit": { "limit": {
"name": "limit", "name": "limit",
"description": "How many neighbors to display (default: %s1)" "description": "How many neighbors to display (default: {count})"
}, },
"season": { "season": {
@@ -622,7 +606,7 @@
"description": "Season number (default: current)", "description": "Season number (default: current)",
"choices": { "choices": {
"current": "%s1 (current)" "current": "{season} (current)"
} }
}, },
@@ -633,12 +617,12 @@
}, },
"body": { "body": {
"top_players": "Top %s1 players", "top_players": "Top {count} 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_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": "# No ongoing season\n\nThere isn't a currently running rating season.", "no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
"deleted_player": "Deleted %s1", "deleted_player": "Deleted {id}",
"neighbors": "Rating neighbors", "neighbors": "Rating neighbors",
"more": "+ %s1 more" "more": "+ {count} more"
} }
}, },
@@ -657,7 +641,7 @@
"description": "Shows all own tanks in a tier", "description": "Shows all own tanks in a tier",
"body": { "body": {
"title": "# %s1's owned tier %s2 tanks\n\n" "title": "# {name}'s owned tier {tier} tanks\n\n"
} }
}, },
@@ -666,7 +650,7 @@
"description": "Checks wether the bot has the required permissions to function", "description": "Checks wether the bot has the required permissions to function",
"body": { "body": {
"title": "# Permissions\n\n%s1", "title": "# Permissions\n\n{body}",
"view_channels": "View channels: needed to find the message the refresh button was used on", "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", "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", "attach_files": "Attach files: needed to add images to image-based commands",
@@ -680,7 +664,7 @@
"body": { "body": {
"pong": "Pong 🏓", "pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - %s1ms" "pong_with_time": "Pong 🏓 - {time}ms"
}, },
"subcommands": { "subcommands": {
@@ -700,7 +684,7 @@
"description": "Basic information about a player", "description": "Basic information about a player",
"body": { "body": {
"title": "%s1's information", "title": "{name}'s information",
"nickname": "Nickname", "nickname": "Nickname",
"battles": "Battles", "battles": "Battles",
"winrate": "Winrate", "winrate": "Winrate",
@@ -733,7 +717,7 @@
}, },
"errors": { "errors": {
"no_participation": "# %s1 hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.", "no_participation": "# {name} 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." "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."
} }
} }
@@ -763,13 +747,16 @@
}, },
"tank_search": { "tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters", "clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Search tanks...", "search_bar_hint": "Search tanks...",
"recent": "Recently viewed", "recent": "Recently viewed",
"select_all": "Select all", "select_all": "Select all",
"count_singular": "1 tank", "count_singular": "1 tank",
"count_plural": "%s1 tanks", "count_plural": "{count} tanks",
"sorting_by": "Sorting by %s1", "sorting_by": "Sorting by {name}",
"no_tanks_found": { "no_tanks_found": {
"body": "No tanks found.", "body": "No tanks found.",
"search_again": "Try searching again.", "search_again": "Try searching again.",
@@ -833,6 +820,15 @@
}, },
"home": { "home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": { "discord": {
"join_short": "Join cool people!", "join_short": "Join cool people!",
"join_long": "Join super duper cool people!", "join_long": "Join super duper cool people!",
@@ -840,16 +836,16 @@
}, },
"hero": { "hero": {
"credit": "By %s1 ❤️" "credit": "By {name} ❤️"
}, },
"hot_tanks": { "hot_tanks": {
"title": "Popular tanks", "title": "Popular tanks",
"subtitle": "Last %s1 days" "subtitle": "Last {days} days"
}, },
"other_projects": { "other_projects": {
"title": "Other cool projects" "title": "Other cool projects"
}, },
"help_translate": "Please help translate %s1 to %s2!", "help_translate": "Please help translate {name} to {language}!",
"seo_description": "Tools for everything World of Tanks Blitz" "seo_description": "Tools for everything World of Tanks Blitz"
}, },
@@ -861,6 +857,15 @@
}, },
"tools": { "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": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -873,7 +878,7 @@
"next": "Next", "next": "Next",
"no_results": "No results" "no_results": "No results"
}, },
"stats": "%s1 / %s2 guessed correctly" "stats": "{correct} / {total} guessed correctly"
}, },
"tankopedia": { "tankopedia": {
@@ -883,10 +888,14 @@
"button": "Find tanks", "button": "Find tanks",
"description": "Blitz tank encyclopedia", "description": "Blitz tank encyclopedia",
"seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz", "seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz",
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} 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.", "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.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -910,7 +919,6 @@
"view_range": { "view_range": {
"camo": "Camo", "camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory", "ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail" "dead_rail": "Dead Rail"
}, },
@@ -935,9 +943,7 @@
"green_penetration": "Green penetration", "green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor", "hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque", "opaque": "Opaque",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Environment",
"show_grid": "Show grid"
}, },
"screenshot": { "screenshot": {
@@ -963,7 +969,6 @@
}, },
"advanced": "Advanced highlighting", "advanced": "Advanced highlighting",
"shooter": "Shooter:",
"shot_card": { "shot_card": {
"element": { "element": {
@@ -975,15 +980,15 @@
"track": "Tracks" "track": "Tracks"
}, },
"status": { "status": {
"penetration": "%s1hp penetration", "penetration": "{damage}hp penetration",
"blocked": "Blocked", "blocked": "Blocked",
"ricochet": "Ricochet", "ricochet": "Ricochet",
"splash": "%s1hp splash" "splash": "{damage}hp splash"
}, },
"stats": { "stats": {
"thickness_and_angle": "%s1mm %s2°", "thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "%s1mm nominal", "nominal": "{thickness}mm nominal",
"ricochet_loss": "%s1% penetration" "ricochet_loss": "{loss}% penetration"
} }
} }
}, },
@@ -1023,8 +1028,8 @@
"tech_tree": { "tech_tree": {
"title": "Tech tree", "title": "Tech tree",
"battle": "1 battle", "battle": "1 battle",
"battles": "%s1 battles", "battles": "{count} battles",
"route": "Route %s1 of %s2" "route": "Route {index} of {count}"
}, },
"game_modes": { "game_modes": {
@@ -1033,7 +1038,7 @@
"review": { "review": {
"title": "Review videos", "title": "Review videos",
"join": "Apply to %s1 YouTuber program" "join": "Apply to {name} YouTuber program"
}, },
"configuration": { "configuration": {
@@ -1072,15 +1077,15 @@
"title": "Characteristics", "title": "Characteristics",
"compare": { "compare": {
"label": "Compare", "label": "Compare",
"class": "Tier %s1 %s2", "class": "Tier {tier} {class}",
"tier": "All of tier %s1", "tier": "All of tier {tier}",
"all": "All tanks" "all": "All tanks"
}, },
"values": { "values": {
"shell_index": "Shell %s1", "shell_index": "Shell {index}",
"at_distance": "At %s1m", "at_distance": "At {distance}m",
"at_distance_slider_label": "Distance", "at_distance_slider_label": "Distance",
"dispersion_at_distance": "Dispersion at %s1m", "dispersion_at_distance": "Dispersion at {distance}m",
"gun_flexibility": "Gun flexibility", "gun_flexibility": "Gun flexibility",
"speed": "Speed", "speed": "Speed",
"terrain_coefficients": "Terrain coefficients", "terrain_coefficients": "Terrain coefficients",
@@ -1236,9 +1241,9 @@
"description": "Tank performance statistics", "description": "Tank performance statistics",
"seo_description": "Best tanks and their performances in World of Tanks Blitz", "seo_description": "Best tanks and their performances in World of Tanks Blitz",
"estimation": { "estimation": {
"body": "Estimated stats based on %s1 players. Updated %s2 ago.", "body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "%s1 hours", "hours": "{count} hours",
"minutes": "%s1 minutes" "minutes": "{count} minutes"
}, },
"player_count": { "player_count": {
"label": "Player count" "label": "Player count"
@@ -1270,10 +1275,10 @@
"button": "Browse all", "button": "Browse all",
"description": "Blitz player avatars", "description": "Blitz player avatars",
"seo_description": "All World of Tanks Blitz player avatars", "seo_description": "All World of Tanks Blitz player avatars",
"last_updated": "Last updated on %s1", "last_updated": "Last updated on {time}",
"search": { "search": {
"hint": "Search avatars...", "hint": "Search avatars...",
"results": "%s1 avatars" "results": "{count} avatars"
} }
}, },
"session": { "session": {
@@ -1288,8 +1293,8 @@
"no_results": "No players found" "no_results": "No players found"
}, },
"controls": { "controls": {
"title": "Tracking %s1", "title": "Tracking {name}",
"subtitle": "Since %s1", "subtitle": "Since {time}",
"reset": "Reset", "reset": "Reset",
"change": "Change" "change": "Change"
}, },
@@ -1352,7 +1357,7 @@
"cancel": "Cancel", "cancel": "Cancel",
"success": { "success": {
"title": "Copy URL", "title": "Copy URL",
"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\".", "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\".",
"back": "Back", "back": "Back",
"copy": "Copy URL" "copy": "Copy URL"
} }
@@ -1437,17 +1442,11 @@
"description": "Get started with BlitzKit", "description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit" "seo_description": "Get started with BlitzKit"
}, },
"blitz_analysis": { "api": {
"name": "BlitzAnalysis[]", "name": "API",
"button": "Analyze now", "button": "To the docs",
"description": "Rich historic average stats", "description": "BlitzKit hosted API",
"seo_description": "Rich historic average stats in World of Tanks Blitz" "seo_description": "BlitzKit hosted static API for 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"
} }
}, },
+106 -107
View File
@@ -1,18 +1,19 @@
{ {
"test": { "test": {
"default": "test", "default": "test",
"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.", "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}.",
"fox": "El rápido zorro marrón salta sobre el perro perezoso." "fox": "El rápido zorro marrón salta sobre el perro perezoso."
}, },
"common": { "common": {
"name": "BlitzKit",
"units": { "units": {
"m": "%s1m", "m": "{value}m",
"mm": "%s1mm", "mm": "{value}mm",
"deg": "%s1°", "deg": "{value}°",
"hp": "%s1pv", "hp": "{value}hp",
"x": "x%s1", "x": "x{value}",
"percentage": "%s1%", "percentage": "{value}%",
"deg_s": "%s1°/s" "deg_s": "{value}°/s"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Auto cargador", "auto_loader": "Auto cargador",
@@ -185,11 +186,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"clan_not_found": "# No se pudo encontrar el clan\nNo se pudo encontrar \"%s\". Prueba seleccionando un clan de los resultados de búsqueda.", "clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"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_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.",
"player_not_linked": "# Usa `/vincular` para empezar\n¡No sé quién eres, pero puedes decírmelo hoy!", "player_not_linked": "# Usa `/vincular` para empezar\n¡No sé quién eres, pero puedes decírmelo hoy!",
"tank_name_not_found": "# No se pudo encontrar el tanque\nNo se pudo encontrar el tanque con el nombre \"%s\".", "tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"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.", "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.",
"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.", "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", "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.", "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.",
@@ -283,19 +284,19 @@
"name": "tier", "name": "tier",
"description": "Nivel", "description": "Nivel",
"choices": { "choices": {
"tier": "Nivel %s - %s" "tier": "Tier {tier_1} - {tier_2}"
} }
} }
}, },
"periods": { "periods": {
"career": "Carrera", "career": "Carrera",
"today": "Hoy", "today": "Hoy",
"days": "%s días", "days": "{days} days",
"custom": "%s - %s días" "custom": "{day_1} - {day_2} days"
}, },
"filters": { "filters": {
"none": "Sin filtros", "none": "Sin filtros",
"tier": "Nivel %s" "tier": "Tier {tier}"
}, },
"no_data": { "no_data": {
"battles_in_period": "No hay batallas jugadas en este período", "battles_in_period": "No hay batallas jugadas en este período",
@@ -318,22 +319,6 @@
} }
}, },
"commands": { "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": { "replay": {
"name": "repetición", "name": "repetición",
"description": "Mostrar resultados de la repetición" "description": "Mostrar resultados de la repetición"
@@ -354,19 +339,19 @@
"body": { "body": {
"subtitle": "Investigar", "subtitle": "Investigar",
"total": "Total", "total": "Total",
"research": "Investigar: %s", "research": "Research: {cost}",
"upgrades": "Mejoras: %s", "upgrades": "Upgrades: {cost}",
"purchase": "Comprar: %s", "purchase": "Purchase: {cost}",
"equipment": "Equipamiento: %s", "equipment": "Equipment: {cost}",
"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." "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": { "errors": {
"non_tech_tree": "# %s no está en el árbol tecnológico\n\nNo puedes investigar un tanque prémium o coleccionista.", "non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# %s es un nivel I\n\nLos tanque de nivel I están siempre desbloqueados gratuitamente para todos los jugadores.", "tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"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.", "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": "# %s fue proporcionado dos veces\n\nAsegúrate de que `tanque-inicial` y `tanque-objetivo` son diferentes.", "start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"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.", "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": "# %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." "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."
} }
}, },
"about": { "about": {
@@ -409,7 +394,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "Tasa de victorias", "winrate": "Tasa de victorias",
"damage": "Daño", "damage": "Daño",
"unknown_tank": "Tanque desconocido %s" "unknown_tank": "Unknown tank {id}"
} }
}, },
"debug": { "debug": {
@@ -445,12 +430,12 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "umbral", "name": "umbral",
"description": "El número de días inactivo (predeterminado: %s días)" "description": "The number of days inactive (default: {days} days)"
} }
}, },
"body": { "body": {
"subtitle": "Inactivo por %s+ días", "subtitle": "Inactive for {days}+ days",
"listing": "%s días" "listing": "{days} days"
} }
}, },
"full_stats": { "full_stats": {
@@ -493,7 +478,7 @@
"body": { "body": {
"accounts_linked": "Cuentas vinculadas", "accounts_linked": "Cuentas vinculadas",
"no_role_permissions": { "no_role_permissions": {
"title": "%s1 no se pudo verificar", "title": "{name} failed to verify",
"description": "No tengo permiso para administrar roles." "description": "No tengo permiso para administrar roles."
} }
}, },
@@ -508,11 +493,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "límite", "name": "límite",
"description": "El número máximo de resultados de clanes (predeterminado: %s)" "description": "The max number of clan results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Búsqueda del clan %s en la región %s", "title": "Clan search for {search} in region {region}",
"no_results": "No se encontraron clanes" "no_results": "No se encontraron clanes"
} }
}, },
@@ -522,11 +507,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "límite", "name": "límite",
"description": "El número máximo para resultados de jugador (predeterminado: %s)" "description": "The max number of player results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Búsqueda del jugador %s en la región %s", "title": "Player search for {search} in region {region}",
"no_results": "No se encontraron jugadores" "no_results": "No se encontraron jugadores"
} }
}, },
@@ -546,13 +531,13 @@
"options": { "options": {
"limit": { "limit": {
"name": "limite", "name": "limite",
"description": "Cuántos vecinos a mostrar (predeterminado: %s)" "description": "How many neighbors to display (default: {count})"
}, },
"season": { "season": {
"name": "temporada", "name": "temporada",
"description": "Número de temporada (predeterminado: actual)", "description": "Número de temporada (predeterminado: actual)",
"choices": { "choices": {
"current": "%s (actual)" "current": "{season} (current)"
} }
}, },
"league": { "league": {
@@ -561,12 +546,12 @@
} }
}, },
"body": { "body": {
"top_players": "Mejores %s jugadores", "top_players": "Top {count} players",
"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_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": "# No hay temporada en curso\n\nActualmente no hay ninguna temporada de clasificatoria.", "no_ongoing_season": "# No hay temporada en curso\n\nActualmente no hay ninguna temporada de clasificatoria.",
"deleted_player": "%s eliminado", "deleted_player": "Deleted {id}",
"neighbors": "Vecinos de clasificatoria", "neighbors": "Vecinos de clasificatoria",
"more": "+ %s más" "more": "+ {count} more"
} }
}, },
"today": { "today": {
@@ -581,14 +566,14 @@
"name": "tanques en propiedad", "name": "tanques en propiedad",
"description": "Muestra todos los tanques propios en un nivel", "description": "Muestra todos los tanques propios en un nivel",
"body": { "body": {
"title": "# Tanques de %s de nivel %s\n\n" "title": "# {name}'s owned tier {tier} tanks\n\n"
} }
}, },
"permissions": { "permissions": {
"name": "permisos", "name": "permisos",
"description": "Comprueba si el bot tiene los permisos requeridos para funcionar", "description": "Comprueba si el bot tiene los permisos requeridos para funcionar",
"body": { "body": {
"title": "# Permisos\n\n%s", "title": "# Permissions\n\n{body}",
"view_channels": "Ver canales: necesario para encontrar el mensaje en el cual el botón \"actualizar\" fue usado", "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", "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", "attach_files": "Adjuntar archivos: necesario para añadir imágenes a los comandos basados en estas",
@@ -600,7 +585,7 @@
"description": "Comprueba si el bot está vivo.", "description": "Comprueba si el bot está vivo.",
"body": { "body": {
"pong": "Pong 🏓", "pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - %s1ms" "pong_with_time": "Pong 🏓 - {time}ms"
}, },
"subcommands": { "subcommands": {
"blitzkit": { "blitzkit": {
@@ -617,7 +602,7 @@
"name": "info del jugador", "name": "info del jugador",
"description": "Información básica sobre un jugador", "description": "Información básica sobre un jugador",
"body": { "body": {
"title": "Información de %s", "title": "{name}'s information",
"nickname": "Apodo", "nickname": "Apodo",
"battles": "Batallas", "battles": "Batallas",
"winrate": "Tasa de victorias", "winrate": "Tasa de victorias",
@@ -646,7 +631,7 @@
} }
}, },
"errors": { "errors": {
"no_participation": "# %s no ha jugado clasificatoria\n\nEste jugador no ha participado en el modo clasificatoria esta temporada.", "no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"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." "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."
} }
} }
@@ -658,6 +643,8 @@
"body": "This page has moved. Redirecting..." "body": "This page has moved. Redirecting..."
}, },
"settings": { "settings": {
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Modo de desarrollador", "dev_mode": "Modo de desarrollador",
"language": "Idioma" "language": "Idioma"
}, },
@@ -671,13 +658,16 @@
"copied": "Copiado!" "copied": "Copiado!"
}, },
"tank_search": { "tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters", "clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Buscar tanques...", "search_bar_hint": "Buscar tanques...",
"recent": "Visto recientemente", "recent": "Visto recientemente",
"select_all": "Seleccionar todo", "select_all": "Seleccionar todo",
"count_singular": "1 tanque", "count_singular": "1 tanque",
"count_plural": "%s1 tanques", "count_plural": "{count} tanks",
"sorting_by": "Ordenar por %s1", "sorting_by": "Sorting by {name}",
"no_tanks_found": { "no_tanks_found": {
"body": "No se encontraron tanques.", "body": "No se encontraron tanques.",
"search_again": "Intenta buscar de nuevo.", "search_again": "Intenta buscar de nuevo.",
@@ -739,22 +729,30 @@
} }
}, },
"home": { "home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": { "discord": {
"join_short": "Join cool people!", "join_short": "Join cool people!",
"join_long": "Join super duper cool people!", "join_long": "Join super duper cool people!",
"action": "Discord" "action": "Discord"
}, },
"hero": { "hero": {
"credit": "Por %s1 ❤️" "credit": "By {name} ❤️"
}, },
"hot_tanks": { "hot_tanks": {
"title": "Tanques populares", "title": "Tanques populares",
"subtitle": "Últimos %s1 días" "subtitle": "Last {days} days"
}, },
"other_projects": { "other_projects": {
"title": "Otros proyectos interesantes" "title": "Otros proyectos interesantes"
}, },
"help_translate": "¡Por favor ayuda a traducir %s1 a %s2!", "help_translate": "Please help translate {name} to {language}!",
"seo_description": "Herramientas para todo lo que es World of Tanks Blitz" "seo_description": "Herramientas para todo lo que es World of Tanks Blitz"
}, },
"docs": { "docs": {
@@ -764,6 +762,14 @@
} }
}, },
"tools": { "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": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -776,7 +782,7 @@
"next": "Next", "next": "Next",
"no_results": "Sin resultados" "no_results": "Sin resultados"
}, },
"stats": "%s1 / %s2 guessed correctly" "stats": "{correct} / {total} guessed correctly"
}, },
"tankopedia": { "tankopedia": {
"search": "Search a stat...", "search": "Search a stat...",
@@ -784,9 +790,12 @@
"button": "Encuentra tanques", "button": "Encuentra tanques",
"description": "Enciclopedia de tanques de WOTB", "description": "Enciclopedia de tanques de WOTB",
"seo_description": "Estadísticas, armaduras y más para todos los tanques en World of Tanks Blitz", "seo_description": "Estadísticas, armaduras y más para todos los tanques en World of Tanks Blitz",
"tank_seo_title": "%s1 - Nivel %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} 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", "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.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -807,7 +816,6 @@
}, },
"view_range": { "view_range": {
"camo": "Camo", "camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory", "ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail" "dead_rail": "Dead Rail"
}, },
@@ -829,9 +837,7 @@
"green_penetration": "Penetración verde", "green_penetration": "Penetración verde",
"hide_model_under_armor": "Ocultar modelo bajo la armadura", "hide_model_under_armor": "Ocultar modelo bajo la armadura",
"opaque": "Opaco", "opaque": "Opaco",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Entorno",
"show_grid": "Mostrar cuadrícula"
}, },
"screenshot": { "screenshot": {
"download": "Descargar", "download": "Descargar",
@@ -853,7 +859,6 @@
"reset": "Restablecer" "reset": "Restablecer"
}, },
"advanced": "Resaltado avanzado", "advanced": "Resaltado avanzado",
"shooter": "Tirador:",
"shot_card": { "shot_card": {
"element": { "element": {
"primary": "Primario", "primary": "Primario",
@@ -864,15 +869,15 @@
"track": "Orugas" "track": "Orugas"
}, },
"status": { "status": {
"penetration": "%s1pv penetracion", "penetration": "{damage}hp penetration",
"blocked": "Bloqueado", "blocked": "Bloqueado",
"ricochet": "Rebote", "ricochet": "Rebote",
"splash": "%s1pv splash" "splash": "{damage}hp splash"
}, },
"stats": { "stats": {
"thickness_and_angle": "%s1mm %s2°", "thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "%s1mm nominal", "nominal": "{thickness}mm nominal",
"ricochet_loss": "%s1% penetracion" "ricochet_loss": "{loss}% penetration"
} }
} }
}, },
@@ -910,15 +915,15 @@
"tech_tree": { "tech_tree": {
"title": "Árbol tecnológico", "title": "Árbol tecnológico",
"battle": "1 batalla", "battle": "1 batalla",
"battles": "%s1 batallas", "battles": "{count} battles",
"route": "Ruta %s1 de %s2" "route": "Route {index} of {count}"
}, },
"game_modes": { "game_modes": {
"title": "Habilidades del modo de juego" "title": "Habilidades del modo de juego"
}, },
"review": { "review": {
"title": "Vídeos de reseña", "title": "Vídeos de reseña",
"join": "Aplica para el programa de YouTube de %s1" "join": "Apply to {name} YouTuber program"
}, },
"configuration": { "configuration": {
"title": "", "title": "",
@@ -954,15 +959,15 @@
"title": "Características", "title": "Características",
"compare": { "compare": {
"label": "Comparar con", "label": "Comparar con",
"class": "Nivel %s1 - %s2", "class": "Tier {tier} {class}",
"tier": "Todos de nivel %s1", "tier": "All of tier {tier}",
"all": "Todos los tanques" "all": "Todos los tanques"
}, },
"values": { "values": {
"shell_index": "Proyectil %s1", "shell_index": "Shell {index}",
"at_distance": "A %s1m", "at_distance": "At {distance}m",
"at_distance_slider_label": "Distancia", "at_distance_slider_label": "Distancia",
"dispersion_at_distance": "Dispersión a %s1m", "dispersion_at_distance": "Dispersion at {distance}m",
"gun_flexibility": "Flexibilidad del arma", "gun_flexibility": "Flexibilidad del arma",
"speed": "Velocidad", "speed": "Velocidad",
"terrain_coefficients": "Coeficientes de terreno", "terrain_coefficients": "Coeficientes de terreno",
@@ -1111,9 +1116,9 @@
"description": "Estadísticas de rendimiento del tanque", "description": "Estadísticas de rendimiento del tanque",
"seo_description": "Los mejores tanques y su rendimiento en World of Tanks Blitz", "seo_description": "Los mejores tanques y su rendimiento en World of Tanks Blitz",
"estimation": { "estimation": {
"body": "Estadísticas estimadas basadas en %s1 jugadores. Actualizado hace %s2.", "body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "%s1 horas", "hours": "{count} hours",
"minutes": "%s1 minutos" "minutes": "{count} minutes"
}, },
"player_count": { "player_count": {
"label": "Contador de jugadores" "label": "Contador de jugadores"
@@ -1145,10 +1150,10 @@
"button": "Explorar todo", "button": "Explorar todo",
"description": "Avatares para los jugadores de WOTB", "description": "Avatares para los jugadores de WOTB",
"seo_description": "Todos los avatares del jugador en World of Tanks Blitz", "seo_description": "Todos los avatares del jugador en World of Tanks Blitz",
"last_updated": "Última actualización el %s1", "last_updated": "Last updated on {time}",
"search": { "search": {
"hint": "Buscar avatares...", "hint": "Buscar avatares...",
"results": "%s1 avatares" "results": "{count} avatars"
} }
}, },
"session": { "session": {
@@ -1163,8 +1168,8 @@
"no_results": "No se encontraron jugadores" "no_results": "No se encontraron jugadores"
}, },
"controls": { "controls": {
"title": "Siguiendo a %s1", "title": "Tracking {name}",
"subtitle": "Desde %s1", "subtitle": "Since {time}",
"reset": "Restablecer", "reset": "Restablecer",
"change": "Change" "change": "Change"
}, },
@@ -1224,7 +1229,7 @@
"cancel": "Cancelar", "cancel": "Cancelar",
"success": { "success": {
"title": "Copiar URL", "title": "Copiar URL",
"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\".", "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\".",
"back": "Volver", "back": "Volver",
"copy": "Copiar URL" "copy": "Copiar URL"
} }
@@ -1307,17 +1312,11 @@
"description": "Get started with BlitzKit", "description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit" "seo_description": "Get started with BlitzKit"
}, },
"blitz_analysis": { "api": {
"name": "BlitzAnalysis[]", "name": "API",
"button": "Analizar ahora", "button": "To the docs",
"description": "Rich historic average stats", "description": "BlitzKit hosted API",
"seo_description": "Rich historic average stats in World of Tanks Blitz" "seo_description": "BlitzKit hosted static API for 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": { "footer": {
+134 -135
View File
@@ -1,18 +1,19 @@
{ {
"test": { "test": {
"default": "teste", "default": "teste",
"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.", "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}.",
"fox": "Le renard brun et rapide saute par-dessus le chien paresseux." "fox": "Le renard brun et rapide saute par-dessus le chien paresseux."
}, },
"common": { "common": {
"name": "BlitzKit",
"units": { "units": {
"m": "%s1m", "m": "{value}m",
"mm": "%s1mm", "mm": "{value}mm",
"deg": "%s1°", "deg": "{value}°",
"hp": "%s1ps", "hp": "{value}hp",
"x": "x%s1", "x": "x{value}",
"percentage": "%s1%", "percentage": "{value}%",
"deg_s": "%s1°/s" "deg_s": "{value}°/s"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Chargement auto", "auto_loader": "Chargement auto",
@@ -185,11 +186,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"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.", "clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"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_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.",
"player_not_linked": "# Utilisez `/link` pour commencer\nJe ne sais pas qui vous êtes, mais vous pouvez me le dire aujourd'hui !", "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": "Impossible de trouver le char sous le nom \"%s1\".", "tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"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.", "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.",
"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.", "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", "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.", "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.",
@@ -283,19 +284,19 @@
"name": "rang", "name": "rang",
"description": "Rang", "description": "Rang",
"choices": { "choices": {
"tier": "Rang %s1 - %s2" "tier": "Tier {tier_1} - {tier_2}"
} }
} }
}, },
"periods": { "periods": {
"career": "Carrière", "career": "Carrière",
"today": "Aujourd'hui", "today": "Aujourd'hui",
"days": "%s1 jour", "days": "{days} days",
"custom": "%s1 - %s2 jours" "custom": "{day_1} - {day_2} days"
}, },
"filters": { "filters": {
"none": "Aucun filtre", "none": "Aucun filtre",
"tier": "Rang %s1" "tier": "Tier {tier}"
}, },
"no_data": { "no_data": {
"battles_in_period": "", "battles_in_period": "",
@@ -318,22 +319,6 @@
} }
}, },
"commands": { "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": { "replay": {
"name": "rejouer", "name": "rejouer",
"description": "Afficher les résultats de la relecture" "description": "Afficher les résultats de la relecture"
@@ -354,19 +339,19 @@
"body": { "body": {
"subtitle": "Recherche", "subtitle": "Recherche",
"total": "Total", "total": "Total",
"research": "Recherche: %s1", "research": "Research: {cost}",
"upgrades": "Améliorations : %s1", "upgrades": "Upgrades: {cost}",
"purchase": "Achat : %s1", "purchase": "Purchase: {cost}",
"equipment": "Equipment: %s1", "equipment": "Equipment: {cost}",
"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." "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": { "errors": {
"non_tech_tree": "# %s1 n'est pas dans l'arbre technologique\n\nVous ne pouvez pas rechercher un char prenium/de collection.", "non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# %s1 est un char de tier I\n\nLe char de tier 1 sont déverrouillé gratuitement pour tous les joueurs.", "tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"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.", "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": "# %s1 was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.", "start_end_equal": "# {tank} 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.", "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": "# %s1 higher tiered to %s2\n\nMake sure to provide %s3 as the `starting-tank` instead as it is the lower tiered tank." "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."
} }
}, },
"about": { "about": {
@@ -380,7 +365,7 @@
}, },
"embeds": { "embeds": {
"name": "intègre", "name": "intègre",
"description": "How embeds work" "description": "Comment fonctionnent les intégrations"
}, },
"introduction": { "introduction": {
"name": "introduction", "name": "introduction",
@@ -402,14 +387,14 @@
}, },
"breakdown": { "breakdown": {
"name": "décomposition", "name": "décomposition",
"description": "A period's breakdown by tanks played", "description": "Répartition d'une période par chars joués",
"body": { "body": {
"total": "Total", "total": "Total",
"battles": "Batailles", "battles": "Batailles",
"wn8": "WN8", "wn8": "WN8",
"winrate": "Taux de victoires", "winrate": "Taux de victoires",
"damage": "Dégâts", "damage": "Dégâts",
"unknown_tank": "Char %s1 inconnu" "unknown_tank": "Unknown tank {id}"
} }
}, },
"debug": { "debug": {
@@ -445,12 +430,12 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "seuil", "name": "seuil",
"description": "Le nombre de jours inactifs (par défaut: %s1 jours)" "description": "The number of days inactive (default: {days} days)"
} }
}, },
"body": { "body": {
"subtitle": "Inactif depuis %s1+ de jours", "subtitle": "Inactive for {days}+ days",
"listing": "%s1 jour" "listing": "{days} days"
} }
}, },
"full_stats": { "full_stats": {
@@ -471,7 +456,7 @@
"accuracy": "Précision" "accuracy": "Précision"
}, },
"lethality": { "lethality": {
"title": "Lethality", "title": "Létalité",
"average_damage": "Dégâts moyens", "average_damage": "Dégâts moyens",
"damage_per_tier": "Dégâts par tier", "damage_per_tier": "Dégâts par tier",
"damage_ratio": "Rapport de dégâts", "damage_ratio": "Rapport de dégâts",
@@ -493,26 +478,26 @@
"body": { "body": {
"accounts_linked": "Comptes liés", "accounts_linked": "Comptes liés",
"no_role_permissions": { "no_role_permissions": {
"title": "La vérification de %s1 a échoué", "title": "{name} failed to verify",
"description": "Je n'ai pas l'autorisation de gérer les rôles." "description": "Je n'ai pas l'autorisation de gérer les rôles."
} }
}, },
"embed": { "embed": {
"title": "Compte invalide ?", "title": "Compte invalide ?",
"description": "Use the command again and select a username from the search results." "description": "Utilisez à nouveau la commande et sélectionnez un nom d'utilisateur à partir des résultats de recherche."
} }
}, },
"search_clans": { "search_clans": {
"name": "search-clans", "name": "clans de recherche",
"description": "Search clans", "description": "Rechercher des clans",
"options": { "options": {
"limit": { "limit": {
"name": "limite", "name": "limite",
"description": "The max number of clan results (default: %s1)" "description": "The max number of clan results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Clan search for %s1 in region %s2", "title": "Clan search for {search} in region {region}",
"no_results": "Pas de clan trouvé" "no_results": "Pas de clan trouvé"
} }
}, },
@@ -522,11 +507,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "limite", "name": "limite",
"description": "Le nombre maximum de résultats de joueur (par défaut: %s1)" "description": "The max number of player results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Recherche de %s1 dans la région %s2", "title": "Player search for {search} in region {region}",
"no_results": "Pas de joueur de trouvé" "no_results": "Pas de joueur de trouvé"
} }
}, },
@@ -535,77 +520,77 @@
"description": "Statistiques des batailles de cotés", "description": "Statistiques des batailles de cotés",
"subcommands": { "subcommands": {
"neighbors": { "neighbors": {
"name": "neighbors", "name": "voisins",
"description": "Neighboring players on the leaderboard" "description": "Joueurs voisins dans le classement"
}, },
"league": { "league": {
"name": "ligue", "name": "ligue",
"description": "Top players of a league" "description": "Meilleurs joueurs d'une ligue"
} }
}, },
"options": { "options": {
"limit": { "limit": {
"name": "limite", "name": "limite",
"description": "How many neighbors to display (default: %s1)" "description": "How many neighbors to display (default: {count})"
}, },
"season": { "season": {
"name": "season", "name": "saison",
"description": "Season number (default: current)", "description": "Numéro de saison (défaut : courant)",
"choices": { "choices": {
"current": "%s1 (current)" "current": "{season} (current)"
} }
}, },
"league": { "league": {
"name": "ligue", "name": "ligue",
"description": "The league to display" "description": "La ligue à afficher"
} }
}, },
"body": { "body": {
"top_players": "Top %s1 players", "top_players": "Top {count} 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_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": "# No ongoing season\n\nThere isn't a currently running rating season.", "no_ongoing_season": "Pas de saison en cours\n\nIl n'y a pas de saison de notation en cours actuellement.",
"deleted_player": "Deleted %s1", "deleted_player": "Deleted {id}",
"neighbors": "Rating neighbors", "neighbors": "évaluer les voisins",
"more": "+ %s1 more" "more": "+ {count} more"
} }
}, },
"today": { "today": {
"name": "aujourd'hui", "name": "aujourd'hui",
"description": "Today's played tanks and statistics" "description": "Les chars et les statistiques joués aujourd'hui"
}, },
"stats": { "stats": {
"name": "statistiques", "name": "statistiques",
"description": "Quick statistics for a period" "description": "Statistiques rapides pour une période"
}, },
"owned_tanks": { "owned_tanks": {
"name": "owned-tanks", "name": "chars possédés",
"description": "Shows all own tanks in a tier", "description": "Affiche tous les chars d'un niveau",
"body": { "body": {
"title": "# %s1's owned tier %s2 tanks\n\n" "title": "# {name}'s owned tier {tier} tanks\n\n"
} }
}, },
"permissions": { "permissions": {
"name": "permissions", "name": "permissions",
"description": "Checks wether the bot has the required permissions to function", "description": "Vérifie si le bot a les permissions nécessaires pour fonctionner",
"body": { "body": {
"title": "# Permissions\n\n%s1", "title": "# Permissions\n\n{body}",
"view_channels": "View channels: needed to find the message the refresh button was used on", "view_channels": "Afficher les canaux : nécessaire pour trouver le message sur lequel le bouton d'actualisation a été utilisé",
"read_message_history": "Read message history: needed to find the message to refresh", "read_message_history": "Lire l'historique des messages : nécessaire pour trouver le message à rafraîchir",
"attach_files": "Attach files: needed to add images to image-based commands", "attach_files": "Joindre des fichiers : nécessaire pour ajouter des images à des commandes basées sur l'image",
"external_emojis": "External emojis: needed to add national flags and tank class emojis" "external_emojis": "Émoticônes externes: nécessaire pour ajouter des drapeaux nationaux et des émojis de classe de char"
} }
}, },
"ping": { "ping": {
"name": "ping", "name": "ping",
"description": "Check wether the bot is alive.", "description": "Vérifiez si le bot est en vie.",
"body": { "body": {
"pong": "Pong 🏓", "pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - %s1ms" "pong_with_time": "Pong 🏓 - {time}ms"
}, },
"subcommands": { "subcommands": {
"blitzkit": { "blitzkit": {
"name": "blitzkit", "name": "blitzkit",
"description": "Ping BlitzKit" "description": "Ping blitzkit"
}, },
"wotb": { "wotb": {
"name": "wotb", "name": "wotb",
@@ -614,10 +599,10 @@
} }
}, },
"player_info": { "player_info": {
"name": "player-info", "name": "info-joueur",
"description": "Basic information about a player", "description": "Informations de base sur un joueur",
"body": { "body": {
"title": "%s1's information", "title": "{name}'s information",
"nickname": "Nickname", "nickname": "Nickname",
"battles": "Batailles", "battles": "Batailles",
"winrate": "Taux de victoires", "winrate": "Taux de victoires",
@@ -635,7 +620,7 @@
"description": "Today's rating statistics" "description": "Today's rating statistics"
}, },
"season": { "season": {
"name": "season", "name": "saison",
"description": "This season's rating statistics" "description": "This season's rating statistics"
} }
}, },
@@ -646,7 +631,7 @@
} }
}, },
"errors": { "errors": {
"no_participation": "# %s1 hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.", "no_participation": "# {name} 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." "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."
} }
} }
@@ -658,6 +643,8 @@
"body": "This page has moved. Redirecting..." "body": "This page has moved. Redirecting..."
}, },
"settings": { "settings": {
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Development mode", "dev_mode": "Development mode",
"language": "Language" "language": "Language"
}, },
@@ -671,13 +658,16 @@
"copied": "Copied!" "copied": "Copied!"
}, },
"tank_search": { "tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters", "clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Search tanks...", "search_bar_hint": "Search tanks...",
"recent": "Recently viewed", "recent": "Recently viewed",
"select_all": "Select all", "select_all": "Select all",
"count_singular": "1 tank", "count_singular": "1 tank",
"count_plural": "%s1 tanks", "count_plural": "{count} tanks",
"sorting_by": "Sorting by %s1", "sorting_by": "Sorting by {name}",
"no_tanks_found": { "no_tanks_found": {
"body": "No tanks found.", "body": "No tanks found.",
"search_again": "Try searching again.", "search_again": "Try searching again.",
@@ -739,22 +729,30 @@
} }
}, },
"home": { "home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": { "discord": {
"join_short": "Join cool people!", "join_short": "Join cool people!",
"join_long": "Join super duper cool people!", "join_long": "Join super duper cool people!",
"action": "Discord" "action": "Discord"
}, },
"hero": { "hero": {
"credit": "By %s1 ❤️" "credit": "By {name} ❤️"
}, },
"hot_tanks": { "hot_tanks": {
"title": "Popular tanks", "title": "Popular tanks",
"subtitle": "Last %s1 days" "subtitle": "Last {days} days"
}, },
"other_projects": { "other_projects": {
"title": "Other cool projects" "title": "Other cool projects"
}, },
"help_translate": "Please help translate %s1 to %s2!", "help_translate": "Please help translate {name} to {language}!",
"seo_description": "Tools for everything World of Tanks Blitz" "seo_description": "Tools for everything World of Tanks Blitz"
}, },
"docs": { "docs": {
@@ -764,6 +762,14 @@
} }
}, },
"tools": { "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": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -776,7 +782,7 @@
"next": "Next", "next": "Next",
"no_results": "No results" "no_results": "No results"
}, },
"stats": "%s1 / %s2 guessed correctly" "stats": "{correct} / {total} guessed correctly"
}, },
"tankopedia": { "tankopedia": {
"search": "Search a stat...", "search": "Search a stat...",
@@ -784,9 +790,12 @@
"button": "Find tanks", "button": "Find tanks",
"description": "Blitz tank encyclopedia", "description": "Blitz tank encyclopedia",
"seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz", "seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz",
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} 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.", "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.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -807,7 +816,6 @@
}, },
"view_range": { "view_range": {
"camo": "Camo", "camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory", "ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail" "dead_rail": "Dead Rail"
}, },
@@ -829,9 +837,7 @@
"green_penetration": "Green penetration", "green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor", "hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque", "opaque": "Opaque",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Environment",
"show_grid": "Show grid"
}, },
"screenshot": { "screenshot": {
"download": "Download", "download": "Download",
@@ -853,7 +859,6 @@
"reset": "Reset" "reset": "Reset"
}, },
"advanced": "Advanced highlighting", "advanced": "Advanced highlighting",
"shooter": "Shooter:",
"shot_card": { "shot_card": {
"element": { "element": {
"primary": "Primary", "primary": "Primary",
@@ -864,15 +869,15 @@
"track": "Tracks" "track": "Tracks"
}, },
"status": { "status": {
"penetration": "%s1hp penetration", "penetration": "{damage}hp penetration",
"blocked": "Blocked", "blocked": "Blocked",
"ricochet": "Ricochet", "ricochet": "Ricochet",
"splash": "%s1hp splash" "splash": "{damage}hp splash"
}, },
"stats": { "stats": {
"thickness_and_angle": "%s1mm %s2°", "thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "%s1mm nominal", "nominal": "{thickness}mm nominal",
"ricochet_loss": "%s1% penetration" "ricochet_loss": "{loss}% penetration"
} }
} }
}, },
@@ -910,15 +915,15 @@
"tech_tree": { "tech_tree": {
"title": "Arbre technologique", "title": "Arbre technologique",
"battle": "1 battle", "battle": "1 battle",
"battles": "%s1 battles", "battles": "{count} battles",
"route": "Route %s1 of %s2" "route": "Route {index} of {count}"
}, },
"game_modes": { "game_modes": {
"title": "Game mode abilities" "title": "Game mode abilities"
}, },
"review": { "review": {
"title": "Review videos", "title": "Review videos",
"join": "Apply to %s1 YouTuber program" "join": "Apply to {name} YouTuber program"
}, },
"configuration": { "configuration": {
"title": "Configuration", "title": "Configuration",
@@ -954,15 +959,15 @@
"title": "Characteristics", "title": "Characteristics",
"compare": { "compare": {
"label": "Compare against", "label": "Compare against",
"class": "Tier %s1 %s2", "class": "Tier {tier} {class}",
"tier": "All of tier %s1", "tier": "All of tier {tier}",
"all": "All tanks" "all": "All tanks"
}, },
"values": { "values": {
"shell_index": "Shell %s1", "shell_index": "Shell {index}",
"at_distance": "At %s1m", "at_distance": "At {distance}m",
"at_distance_slider_label": "Distance", "at_distance_slider_label": "Distance",
"dispersion_at_distance": "Dispersion at %s1m", "dispersion_at_distance": "Dispersion at {distance}m",
"gun_flexibility": "Gun flexibility", "gun_flexibility": "Gun flexibility",
"speed": "Speed", "speed": "Speed",
"terrain_coefficients": "Terrain coefficients", "terrain_coefficients": "Terrain coefficients",
@@ -1111,9 +1116,9 @@
"description": "Tank performance statistics", "description": "Tank performance statistics",
"seo_description": "Best tanks and their performances in World of Tanks Blitz", "seo_description": "Best tanks and their performances in World of Tanks Blitz",
"estimation": { "estimation": {
"body": "Estimated stats based on %s1 players. Updated %s2 ago.", "body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "%s1 hours", "hours": "{count} hours",
"minutes": "%s1 minutes" "minutes": "{count} minutes"
}, },
"player_count": { "player_count": {
"label": "Player count" "label": "Player count"
@@ -1145,10 +1150,10 @@
"button": "Browse all", "button": "Browse all",
"description": "Blitz player avatars", "description": "Blitz player avatars",
"seo_description": "All World of Tanks Blitz player avatars", "seo_description": "All World of Tanks Blitz player avatars",
"last_updated": "Last updated on %s1", "last_updated": "Last updated on {time}",
"search": { "search": {
"hint": "Search avatars...", "hint": "Search avatars...",
"results": "%s1 avatars" "results": "{count} avatars"
} }
}, },
"session": { "session": {
@@ -1163,8 +1168,8 @@
"no_results": "Pas de joueur de trouvé" "no_results": "Pas de joueur de trouvé"
}, },
"controls": { "controls": {
"title": "Tracking %s1", "title": "Tracking {name}",
"subtitle": "Since %s1", "subtitle": "Since {time}",
"reset": "Reset", "reset": "Reset",
"change": "Change" "change": "Change"
}, },
@@ -1224,7 +1229,7 @@
"cancel": "Cancel", "cancel": "Cancel",
"success": { "success": {
"title": "Copy URL", "title": "Copy URL",
"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\".", "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\".",
"back": "Back", "back": "Back",
"copy": "Copy URL" "copy": "Copy URL"
} }
@@ -1307,17 +1312,11 @@
"description": "Get started with BlitzKit", "description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit" "seo_description": "Get started with BlitzKit"
}, },
"blitz_analysis": { "api": {
"name": "BlitzAnalysis[]", "name": "API",
"button": "Analyze now", "button": "To the docs",
"description": "Rich historic average stats", "description": "BlitzKit hosted API",
"seo_description": "Rich historic average stats in World of Tanks Blitz" "seo_description": "BlitzKit hosted static API for 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": { "footer": {
+106 -107
View File
@@ -1,18 +1,19 @@
{ {
"test": { "test": {
"default": "test", "default": "test",
"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.", "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}.",
"fox": "すばしっこい茶色のキツネが怠け者の犬を飛び越した。" "fox": "すばしっこい茶色のキツネが怠け者の犬を飛び越した。"
}, },
"common": { "common": {
"name": "BlitzKit",
"units": { "units": {
"m": "%s1m", "m": "{value}m",
"mm": "%s1mm", "mm": "{value}mm",
"deg": "%s1°", "deg": "{value}°",
"hp": "%s1hp", "hp": "{value}hp",
"x": "x%s1", "x": "x{value}",
"percentage": "%s1%", "percentage": "{value}%",
"deg_s": "%s1°/s" "deg_s": "{value}°/s"
}, },
"gun_types": { "gun_types": {
"auto_loader": "自動装填砲", "auto_loader": "自動装填砲",
@@ -185,11 +186,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"clan_not_found": "# クランが見つかりませんでした\n\"%s1\"は見つかりませんでした。検索結果からクランを選択してください。", "clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"player_not_found": "# ユーザーが見つかりませんでした\nユーザー「\"%s1\"」は見つかりませんでした。検索結果からユーザーを選択するか、有効な名前を入力してください。`/link`コマンドを使用することも可能です。", "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.",
"player_not_linked": "# `/link`を使用して始めましょう\nはじめまして!あなたが誰か、私に教えてください。", "player_not_linked": "# `/link`を使用して始めましょう\nはじめまして!あなたが誰か、私に教えてください。",
"tank_name_not_found": "# 車輌が見つかりませんでした\n\"%s1\"という名前の車輌は見つかりませんでした。", "tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# 車輌が見つかりませんでした\n\"%s1\"というIDの車輌は見つかりませんでした。番号ではなく名前で検索してください。", "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.",
"no_tank_stats": "# 利用可能な車輌統計がありません\nこのプレイヤーには利用可能な統計がありません。あなたが探しているプレイヤーではない可能性があります。", "no_tank_stats": "# 利用可能な車輌統計がありません\nこのプレイヤーには利用可能な統計がありません。あなたが探しているプレイヤーではない可能性があります。",
"get_help": "Discordサーバーに参加する", "get_help": "Discordサーバーに参加する",
"uncaught_error": "# BlitzKitがエラーに遭遇しました!\n申し訳ありません。お気軽に[公式Discordサーバー](https://discord.gg/nDt7AjGJQH)にお問い合わせください。", "uncaught_error": "# BlitzKitがエラーに遭遇しました!\n申し訳ありません。お気軽に[公式Discordサーバー](https://discord.gg/nDt7AjGJQH)にお問い合わせください。",
@@ -283,19 +284,19 @@
"name": "tier", "name": "tier",
"description": "Tier", "description": "Tier",
"choices": { "choices": {
"tier": "Tier %s1 - %s2" "tier": "Tier {tier_1} - {tier_2}"
} }
} }
}, },
"periods": { "periods": {
"career": "Career", "career": "Career",
"today": "Today", "today": "Today",
"days": "%s1 days", "days": "{days} days",
"custom": "%s1 - %s2 days" "custom": "{day_1} - {day_2} days"
}, },
"filters": { "filters": {
"none": "フィルターなし", "none": "フィルターなし",
"tier": "Tier %s1" "tier": "Tier {tier}"
}, },
"no_data": { "no_data": {
"battles_in_period": "No battles played in this period", "battles_in_period": "No battles played in this period",
@@ -318,22 +319,6 @@
} }
}, },
"commands": { "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": { "replay": {
"name": "replay", "name": "replay",
"description": "Display replay results" "description": "Display replay results"
@@ -354,19 +339,19 @@
"body": { "body": {
"subtitle": "Research", "subtitle": "Research",
"total": "トータル", "total": "トータル",
"research": "Research: %s1", "research": "Research: {cost}",
"upgrades": "Upgrades: %s1", "upgrades": "Upgrades: {cost}",
"purchase": "Purchase: %s1", "purchase": "Purchase: {cost}",
"equipment": "Equipment: %s1", "equipment": "Equipment: {cost}",
"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." "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": { "errors": {
"non_tech_tree": "# %s1 is not on the tech tree\n\nYou cannot research a premium/collector tank.", "non_tech_tree": "# {tank} 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.", "tier_1": "# {tank} 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.", "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": "# %s1 was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.", "start_end_equal": "# {tank} 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.", "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": "# %s1 higher tiered to %s2\n\nMake sure to provide %s3 as the `starting-tank` instead as it is the lower tiered tank." "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."
} }
}, },
"about": { "about": {
@@ -409,7 +394,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "勝率", "winrate": "勝率",
"damage": "ダメージ", "damage": "ダメージ",
"unknown_tank": "Unknown tank %s1" "unknown_tank": "Unknown tank {id}"
} }
}, },
"debug": { "debug": {
@@ -445,12 +430,12 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "threshold", "name": "threshold",
"description": "The number of days inactive (default: %s1 days)" "description": "The number of days inactive (default: {days} days)"
} }
}, },
"body": { "body": {
"subtitle": "Inactive for %s1+ days", "subtitle": "Inactive for {days}+ days",
"listing": "%s1 days" "listing": "{days} days"
} }
}, },
"full_stats": { "full_stats": {
@@ -493,7 +478,7 @@
"body": { "body": {
"accounts_linked": "Accounts linked", "accounts_linked": "Accounts linked",
"no_role_permissions": { "no_role_permissions": {
"title": "%s1 failed to verify", "title": "{name} failed to verify",
"description": "I don't have the permission to manage roles." "description": "I don't have the permission to manage roles."
} }
}, },
@@ -508,11 +493,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "limit", "name": "limit",
"description": "The max number of clan results (default: %s1)" "description": "The max number of clan results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Clan search for %s1 in region %s2", "title": "Clan search for {search} in region {region}",
"no_results": "No clans found" "no_results": "No clans found"
} }
}, },
@@ -522,11 +507,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "limit", "name": "limit",
"description": "The max number of player results (default: %s1)" "description": "The max number of player results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Player search for %s1 in region %s2", "title": "Player search for {search} in region {region}",
"no_results": "No players found" "no_results": "No players found"
} }
}, },
@@ -546,13 +531,13 @@
"options": { "options": {
"limit": { "limit": {
"name": "limit", "name": "limit",
"description": "How many neighbors to display (default: %s1)" "description": "How many neighbors to display (default: {count})"
}, },
"season": { "season": {
"name": "season", "name": "season",
"description": "Season number (default: current)", "description": "Season number (default: current)",
"choices": { "choices": {
"current": "%s1 (current)" "current": "{season} (current)"
} }
}, },
"league": { "league": {
@@ -561,12 +546,12 @@
} }
}, },
"body": { "body": {
"top_players": "Top %s1 players", "top_players": "Top {count} 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_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": "# No ongoing season\n\nThere isn't a currently running rating season.", "no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
"deleted_player": "Deleted %s1", "deleted_player": "Deleted {id}",
"neighbors": "Rating neighbors", "neighbors": "Rating neighbors",
"more": "+ %s1 more" "more": "+ {count} more"
} }
}, },
"today": { "today": {
@@ -581,14 +566,14 @@
"name": "owned-tanks", "name": "owned-tanks",
"description": "Shows all own tanks in a tier", "description": "Shows all own tanks in a tier",
"body": { "body": {
"title": "# %s1's owned tier %s2 tanks\n\n" "title": "# {name}'s owned tier {tier} tanks\n\n"
} }
}, },
"permissions": { "permissions": {
"name": "permissions", "name": "permissions",
"description": "Checks wether the bot has the required permissions to function", "description": "Checks wether the bot has the required permissions to function",
"body": { "body": {
"title": "# Permissions\n\n%s1", "title": "# Permissions\n\n{body}",
"view_channels": "View channels: needed to find the message the refresh button was used on", "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", "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", "attach_files": "Attach files: needed to add images to image-based commands",
@@ -600,7 +585,7 @@
"description": "Check wether the bot is alive.", "description": "Check wether the bot is alive.",
"body": { "body": {
"pong": "Pong 🏓", "pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - %s1ms" "pong_with_time": "Pong 🏓 - {time}ms"
}, },
"subcommands": { "subcommands": {
"blitzkit": { "blitzkit": {
@@ -617,7 +602,7 @@
"name": "player-info", "name": "player-info",
"description": "Basic information about a player", "description": "Basic information about a player",
"body": { "body": {
"title": "%s1's information", "title": "{name}'s information",
"nickname": "Nickname", "nickname": "Nickname",
"battles": "戦闘数", "battles": "戦闘数",
"winrate": "勝率", "winrate": "勝率",
@@ -646,7 +631,7 @@
} }
}, },
"errors": { "errors": {
"no_participation": "# %s1 hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.", "no_participation": "# {name} 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." "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."
} }
} }
@@ -658,6 +643,8 @@
"body": "This page has moved. Redirecting..." "body": "This page has moved. Redirecting..."
}, },
"settings": { "settings": {
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "開発者モード", "dev_mode": "開発者モード",
"language": "言語 / Language" "language": "言語 / Language"
}, },
@@ -671,13 +658,16 @@
"copied": "コピーしました!" "copied": "コピーしました!"
}, },
"tank_search": { "tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters", "clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "車輌名を入力…", "search_bar_hint": "車輌名を入力…",
"recent": "最近の閲覧", "recent": "最近の閲覧",
"select_all": "全て選択", "select_all": "全て選択",
"count_singular": "1 輌", "count_singular": "1 輌",
"count_plural": "%s1輌", "count_plural": "{count} tanks",
"sorting_by": ":%s1で並び替え", "sorting_by": "Sorting by {name}",
"no_tanks_found": { "no_tanks_found": {
"body": "該当する車輌が存在しません。", "body": "該当する車輌が存在しません。",
"search_again": "もう一度検索してください。", "search_again": "もう一度検索してください。",
@@ -739,22 +729,30 @@
} }
}, },
"home": { "home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": { "discord": {
"join_short": "Join cool people!", "join_short": "Join cool people!",
"join_long": "Join super duper cool people!", "join_long": "Join super duper cool people!",
"action": "Discord" "action": "Discord"
}, },
"hero": { "hero": {
"credit": "By %s1 ❤️" "credit": "By {name} ❤️"
}, },
"hot_tanks": { "hot_tanks": {
"title": "人気の車輌", "title": "人気の車輌",
"subtitle": "過去 %s1日間" "subtitle": "Last {days} days"
}, },
"other_projects": { "other_projects": {
"title": "他のプロジェクト" "title": "他のプロジェクト"
}, },
"help_translate": "%s1の %s2への翻訳にご協力ください!", "help_translate": "Please help translate {name} to {language}!",
"seo_description": "World of Tanks Blitz全般のツール" "seo_description": "World of Tanks Blitz全般のツール"
}, },
"docs": { "docs": {
@@ -764,6 +762,14 @@
} }
}, },
"tools": { "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": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -776,7 +782,7 @@
"next": "Next", "next": "Next",
"no_results": "該当なし" "no_results": "該当なし"
}, },
"stats": "%s1 / %s2 guessed correctly" "stats": "{correct} / {total} guessed correctly"
}, },
"tankopedia": { "tankopedia": {
"search": "Search a stat...", "search": "Search a stat...",
@@ -784,9 +790,12 @@
"button": "戦車を探す", "button": "戦車を探す",
"description": "Blitz戦車百科", "description": "Blitz戦車百科",
"seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz", "seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz",
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} 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.", "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.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -807,7 +816,6 @@
}, },
"view_range": { "view_range": {
"camo": "Camo", "camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory", "ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail" "dead_rail": "Dead Rail"
}, },
@@ -829,9 +837,7 @@
"green_penetration": "貫通可能域を緑色表示", "green_penetration": "貫通可能域を緑色表示",
"hide_model_under_armor": "車輌モデルを非表示", "hide_model_under_armor": "車輌モデルを非表示",
"opaque": "貫通アシストを不透明にする", "opaque": "貫通アシストを不透明にする",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "環境",
"show_grid": "グリッドを表示"
}, },
"screenshot": { "screenshot": {
"download": "保存する", "download": "保存する",
@@ -853,7 +859,6 @@
"reset": "リセット" "reset": "リセット"
}, },
"advanced": "高度なハイライト", "advanced": "高度なハイライト",
"shooter": "自車両",
"shot_card": { "shot_card": {
"element": { "element": {
"primary": "主装甲", "primary": "主装甲",
@@ -864,15 +869,15 @@
"track": "履帯" "track": "履帯"
}, },
"status": { "status": {
"penetration": "%s1hp 貫通", "penetration": "{damage}hp penetration",
"blocked": "非貫通", "blocked": "非貫通",
"ricochet": "跳弾", "ricochet": "跳弾",
"splash": "%s1hp 爆風ダメージ" "splash": "{damage}hp splash"
}, },
"stats": { "stats": {
"thickness_and_angle": "%s1mm %s2°", "thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "%s1mm 装甲", "nominal": "{thickness}mm nominal",
"ricochet_loss": "%s1% penetration" "ricochet_loss": "{loss}% penetration"
} }
} }
}, },
@@ -910,15 +915,15 @@
"tech_tree": { "tech_tree": {
"title": "技術ツリー", "title": "技術ツリー",
"battle": "1 戦", "battle": "1 戦",
"battles": "%s1 戦", "battles": "{count} battles",
"route": "ルート %s1 / %s2" "route": "Route {index} of {count}"
}, },
"game_modes": { "game_modes": {
"title": "ゲームモードアビリティ" "title": "ゲームモードアビリティ"
}, },
"review": { "review": {
"title": "Review videos", "title": "Review videos",
"join": "%s1 YouTuberプログラムに応募する" "join": "Apply to {name} YouTuber program"
}, },
"configuration": { "configuration": {
"title": "構成", "title": "構成",
@@ -954,15 +959,15 @@
"title": "性能値", "title": "性能値",
"compare": { "compare": {
"label": "比較対象", "label": "比較対象",
"class": "Tier %s1 %s2", "class": "Tier {tier} {class}",
"tier": "Tier %s1 全車輌", "tier": "All of tier {tier}",
"all": "全車輌" "all": "全車輌"
}, },
"values": { "values": {
"shell_index": "Shell %s1", "shell_index": "Shell {index}",
"at_distance": "At %s1m", "at_distance": "At {distance}m",
"at_distance_slider_label": "距離", "at_distance_slider_label": "距離",
"dispersion_at_distance": "%s1m での散布界", "dispersion_at_distance": "Dispersion at {distance}m",
"gun_flexibility": "主砲旋回上限", "gun_flexibility": "主砲旋回上限",
"speed": "最高速度", "speed": "最高速度",
"terrain_coefficients": "地形走破能力", "terrain_coefficients": "地形走破能力",
@@ -1111,9 +1116,9 @@
"description": "車輌の戦闘統計", "description": "車輌の戦闘統計",
"seo_description": "Best tanks and their performances in World of Tanks Blitz", "seo_description": "Best tanks and their performances in World of Tanks Blitz",
"estimation": { "estimation": {
"body": "%s1 人のプレイヤーの推定統計。 %s2前に更新されました。", "body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "%s1 時間", "hours": "{count} hours",
"minutes": "%s1 分" "minutes": "{count} minutes"
}, },
"player_count": { "player_count": {
"label": "統計期間:" "label": "統計期間:"
@@ -1145,10 +1150,10 @@
"button": "全て見る", "button": "全て見る",
"description": "Blitzアバター図鑑", "description": "Blitzアバター図鑑",
"seo_description": "World of Tanks Blitz の 全ての プレイヤーアバター", "seo_description": "World of Tanks Blitz の 全ての プレイヤーアバター",
"last_updated": "最終更新: %s1", "last_updated": "Last updated on {time}",
"search": { "search": {
"hint": "アバターを検索...", "hint": "アバターを検索...",
"results": "検索結果: %s1 個" "results": "{count} avatars"
} }
}, },
"session": { "session": {
@@ -1163,8 +1168,8 @@
"no_results": "プレイヤーが見つかりませんでした" "no_results": "プレイヤーが見つかりませんでした"
}, },
"controls": { "controls": {
"title": "%s1 を追跡中", "title": "Tracking {name}",
"subtitle": "開始日時: %s1", "subtitle": "Since {time}",
"reset": "リセット", "reset": "リセット",
"change": "変更" "change": "変更"
}, },
@@ -1224,7 +1229,7 @@
"cancel": "Cancel", "cancel": "Cancel",
"success": { "success": {
"title": "Copy URL", "title": "Copy URL",
"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\".", "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\".",
"back": "戻る", "back": "戻る",
"copy": "Copy URL" "copy": "Copy URL"
} }
@@ -1307,17 +1312,11 @@
"description": "さあ、BlitzKit を始めよう!", "description": "さあ、BlitzKit を始めよう!",
"seo_description": "さあ、BlitzKit を始めよう!" "seo_description": "さあ、BlitzKit を始めよう!"
}, },
"blitz_analysis": { "api": {
"name": "BlitzAnalysis[]", "name": "API",
"button": "Analyze now", "button": "To the docs",
"description": "Rich historic average stats", "description": "BlitzKit hosted API",
"seo_description": "Rich historic average stats in World of Tanks Blitz" "seo_description": "BlitzKit hosted static API for 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": { "footer": {
+106 -107
View File
@@ -1,18 +1,19 @@
{ {
"test": { "test": {
"default": "", "default": "",
"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.", "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}.",
"fox": "Szybki brązowy lis przeskoczył nad leniwym psem." "fox": "Szybki brązowy lis przeskoczył nad leniwym psem."
}, },
"common": { "common": {
"name": "BlitzKit",
"units": { "units": {
"m": "%s1m", "m": "{value}m",
"mm": "%s1mm", "mm": "{value}mm",
"deg": "%s1°", "deg": "{value}°",
"hp": "%s1hp", "hp": "{value}hp",
"x": "x%s1", "x": "x{value}",
"percentage": "%s1%", "percentage": "{value}%",
"deg_s": "%s1°/s" "deg_s": "{value}°/s"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Auto loader", "auto_loader": "Auto loader",
@@ -185,11 +186,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"clan_not_found": "# Nie można znaleźć klanu\nNie można znaleźć \"%s1\". Spróbuj wybrać klan z wyników wyszukiwania.", "clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"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_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.",
"player_not_linked": "# Użyj `/link` aby zacząć\nNie wiem, kim jesteś, ale możesz mi dziś powiedzieć!", "player_not_linked": "# Użyj `/link` aby zacząć\nNie wiem, kim jesteś, ale możesz mi dziś powiedzieć!",
"tank_name_not_found": "# Czołg nie został znaleziony\nNie można znaleźć czołgu o nazwie \"%s1\".", "tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"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.", "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.",
"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.", "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", "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.", "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.",
@@ -283,19 +284,19 @@
"name": "poziom", "name": "poziom",
"description": "Poziom", "description": "Poziom",
"choices": { "choices": {
"tier": "Poziom %s1 - %s2" "tier": "Tier {tier_1} - {tier_2}"
} }
} }
}, },
"periods": { "periods": {
"career": "Kariera", "career": "Kariera",
"today": "Dzisiaj", "today": "Dzisiaj",
"days": "%s1 dzień", "days": "{days} days",
"custom": "%s1 - %s2 dni" "custom": "{day_1} - {day_2} days"
}, },
"filters": { "filters": {
"none": "Brak filtrów", "none": "Brak filtrów",
"tier": "Poziom %s1" "tier": "Tier {tier}"
}, },
"no_data": { "no_data": {
"battles_in_period": "Brak bitew w tym okresie", "battles_in_period": "Brak bitew w tym okresie",
@@ -318,22 +319,6 @@
} }
}, },
"commands": { "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": { "replay": {
"name": "powtórz", "name": "powtórz",
"description": "Wyświetl wyniki powtórki" "description": "Wyświetl wyniki powtórki"
@@ -354,19 +339,19 @@
"body": { "body": {
"subtitle": "Rozpocznij badania", "subtitle": "Rozpocznij badania",
"total": "Całkowity", "total": "Całkowity",
"research": "Badanie: %s1", "research": "Research: {cost}",
"upgrades": "Ulepszenie: %s1", "upgrades": "Upgrades: {cost}",
"purchase": "Zakup: %s1", "purchase": "Purchase: {cost}",
"equipment": "Wyposażenie: %s1", "equipment": "Equipment: {cost}",
"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." "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": { "errors": {
"non_tech_tree": "# %s1 nie jest na drzewie technologii\n\nNie możesz zbadać czołg premium/kolekcjonerski.", "non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# %s1 jest czołgami poziomu I\n\nPoziom I są zawsze odblokowane za darmo dla wszystkich graczy.", "tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"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ś.", "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": "# %s1 został dostarczony dwa razy\n\nUpewnij się, że `starting-tank` i `target-tank` są inne.", "start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"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.", "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": "# %s1 wyższy stopień %s2\n\nUpewnij się, że zamiast tego %s3 jest `starting-tank`, ponieważ jest tierowo niższym czołgiem." "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."
} }
}, },
"about": { "about": {
@@ -409,7 +394,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "Śr. wygranych", "winrate": "Śr. wygranych",
"damage": "Uszkodzenia", "damage": "Uszkodzenia",
"unknown_tank": "Nieznany czołg %s1" "unknown_tank": "Unknown tank {id}"
} }
}, },
"debug": { "debug": {
@@ -445,12 +430,12 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "threshold", "name": "threshold",
"description": "Liczba dni nieaktywnych (domyślnie: %s dni)" "description": "The number of days inactive (default: {days} days)"
} }
}, },
"body": { "body": {
"subtitle": "Inactive for %s1+ days", "subtitle": "Inactive for {days}+ days",
"listing": "%s1 dzień" "listing": "{days} days"
} }
}, },
"full_stats": { "full_stats": {
@@ -493,7 +478,7 @@
"body": { "body": {
"accounts_linked": "Accounts linked", "accounts_linked": "Accounts linked",
"no_role_permissions": { "no_role_permissions": {
"title": "%s1 failed to verify", "title": "{name} failed to verify",
"description": "I don't have the permission to manage roles." "description": "I don't have the permission to manage roles."
} }
}, },
@@ -508,11 +493,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "limit", "name": "limit",
"description": "The max number of clan results (default: %s1)" "description": "The max number of clan results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Clan search for %s1 in region %s2", "title": "Clan search for {search} in region {region}",
"no_results": "No clans found" "no_results": "No clans found"
} }
}, },
@@ -522,11 +507,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "limit", "name": "limit",
"description": "Maksymalna liczba wyników gracza (domyślnie: %s)" "description": "The max number of player results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Player search for %s1 in region %s2", "title": "Player search for {search} in region {region}",
"no_results": "No players found" "no_results": "No players found"
} }
}, },
@@ -546,13 +531,13 @@
"options": { "options": {
"limit": { "limit": {
"name": "limit", "name": "limit",
"description": "How many neighbors to display (default: %s1)" "description": "How many neighbors to display (default: {count})"
}, },
"season": { "season": {
"name": "season", "name": "season",
"description": "Season number (default: current)", "description": "Season number (default: current)",
"choices": { "choices": {
"current": "%s1 (current)" "current": "{season} (current)"
} }
}, },
"league": { "league": {
@@ -561,12 +546,12 @@
} }
}, },
"body": { "body": {
"top_players": "Top %s1 players", "top_players": "Top {count} 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_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": "# No ongoing season\n\nThere isn't a currently running rating season.", "no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
"deleted_player": "Usunięto %s1", "deleted_player": "Deleted {id}",
"neighbors": "Rating neighbors", "neighbors": "Rating neighbors",
"more": "+ %s1 więcej" "more": "+ {count} more"
} }
}, },
"today": { "today": {
@@ -581,14 +566,14 @@
"name": "posiadane czołgi", "name": "posiadane czołgi",
"description": "Pokazuje wszystkie własne czołgi na poziomie", "description": "Pokazuje wszystkie własne czołgi na poziomie",
"body": { "body": {
"title": "# %s1 posiadany poziom %s2 czołgi\n\n" "title": "# {name}'s owned tier {tier} tanks\n\n"
} }
}, },
"permissions": { "permissions": {
"name": "uprawnienia", "name": "uprawnienia",
"description": "Sprawdza, czy bot ma wymagane uprawnienia do funkcji", "description": "Sprawdza, czy bot ma wymagane uprawnienia do funkcji",
"body": { "body": {
"title": "# Uprawnienia\n\n%s1", "title": "# Permissions\n\n{body}",
"view_channels": "Wyświetl kanały: potrzebne do znalezienia wiadomości użyty przycisk odświeżania", "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", "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", "attach_files": "Dołącz pliki: potrzebne do dodawania obrazów do komend opartych na obrazach",
@@ -600,7 +585,7 @@
"description": "Sprawdź, czy bot żyje.", "description": "Sprawdź, czy bot żyje.",
"body": { "body": {
"pong": "Pong 🏓", "pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - %s1ms" "pong_with_time": "Pong 🏓 - {time}ms"
}, },
"subcommands": { "subcommands": {
"blitzkit": { "blitzkit": {
@@ -617,7 +602,7 @@
"name": "informacje o graczu", "name": "informacje o graczu",
"description": "Podstawowe informacje o graczu", "description": "Podstawowe informacje o graczu",
"body": { "body": {
"title": "Informacje o %s1", "title": "{name}'s information",
"nickname": "Nick", "nickname": "Nick",
"battles": "Bitwy", "battles": "Bitwy",
"winrate": "Śr. wygranych", "winrate": "Śr. wygranych",
@@ -646,7 +631,7 @@
} }
}, },
"errors": { "errors": {
"no_participation": "# %s1 nie grał w \n\nTen gracz nie uczestniczył w grze oceniającej w tym sezonie.", "no_participation": "# {name} 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." "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."
} }
} }
@@ -658,6 +643,8 @@
"body": "This page has moved. Redirecting..." "body": "This page has moved. Redirecting..."
}, },
"settings": { "settings": {
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Development mode", "dev_mode": "Development mode",
"language": "Language" "language": "Language"
}, },
@@ -671,13 +658,16 @@
"copied": "Copied!" "copied": "Copied!"
}, },
"tank_search": { "tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters", "clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Search tanks...", "search_bar_hint": "Search tanks...",
"recent": "Recently viewed", "recent": "Recently viewed",
"select_all": "Select all", "select_all": "Select all",
"count_singular": "1 tank", "count_singular": "1 tank",
"count_plural": "%s1 tanks", "count_plural": "{count} tanks",
"sorting_by": "Sorting by %s1", "sorting_by": "Sorting by {name}",
"no_tanks_found": { "no_tanks_found": {
"body": "No tanks found.", "body": "No tanks found.",
"search_again": "Try searching again.", "search_again": "Try searching again.",
@@ -739,22 +729,30 @@
} }
}, },
"home": { "home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": { "discord": {
"join_short": "Join cool people!", "join_short": "Join cool people!",
"join_long": "Join super duper cool people!", "join_long": "Join super duper cool people!",
"action": "Discord" "action": "Discord"
}, },
"hero": { "hero": {
"credit": "By %s1 ❤️" "credit": "By {name} ❤️"
}, },
"hot_tanks": { "hot_tanks": {
"title": "Popular tanks", "title": "Popular tanks",
"subtitle": "Last %s1 days" "subtitle": "Last {days} days"
}, },
"other_projects": { "other_projects": {
"title": "Other cool projects" "title": "Other cool projects"
}, },
"help_translate": "Please help translate %s1 to %s2!", "help_translate": "Please help translate {name} to {language}!",
"seo_description": "Tools for everything World of Tanks Blitz" "seo_description": "Tools for everything World of Tanks Blitz"
}, },
"docs": { "docs": {
@@ -764,6 +762,14 @@
} }
}, },
"tools": { "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": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -776,7 +782,7 @@
"next": "Next", "next": "Next",
"no_results": "Brak wyników" "no_results": "Brak wyników"
}, },
"stats": "" "stats": "{correct} / {total} guessed correctly"
}, },
"tankopedia": { "tankopedia": {
"search": "Search a stat...", "search": "Search a stat...",
@@ -784,9 +790,12 @@
"button": "Find tanks", "button": "Find tanks",
"description": "Blitz tank encyclopedia", "description": "Blitz tank encyclopedia",
"seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz", "seo_description": "Statistics, armor, and more for all tanks in World of Tanks Blitz",
"tank_seo_title": "%s1 - Tier %s2 %s3 %s4 | %s5 Tankopedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} 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.", "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.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -807,7 +816,6 @@
}, },
"view_range": { "view_range": {
"camo": "Camo", "camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory", "ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail" "dead_rail": "Dead Rail"
}, },
@@ -829,9 +837,7 @@
"green_penetration": "Green penetration", "green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor", "hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque", "opaque": "Opaque",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Environment",
"show_grid": "Show grid"
}, },
"screenshot": { "screenshot": {
"download": "Download", "download": "Download",
@@ -853,7 +859,6 @@
"reset": "Reset" "reset": "Reset"
}, },
"advanced": "Advanced highlighting", "advanced": "Advanced highlighting",
"shooter": "Shooter:",
"shot_card": { "shot_card": {
"element": { "element": {
"primary": "Primary", "primary": "Primary",
@@ -864,15 +869,15 @@
"track": "Tracks" "track": "Tracks"
}, },
"status": { "status": {
"penetration": "%s1hp penetration", "penetration": "{damage}hp penetration",
"blocked": "Blocked", "blocked": "Blocked",
"ricochet": "Ricochet", "ricochet": "Ricochet",
"splash": "%s1hp splash" "splash": "{damage}hp splash"
}, },
"stats": { "stats": {
"thickness_and_angle": "%s1mm %s2°", "thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "%s1mm nominal", "nominal": "{thickness}mm nominal",
"ricochet_loss": "%s1% penetration" "ricochet_loss": "{loss}% penetration"
} }
} }
}, },
@@ -910,15 +915,15 @@
"tech_tree": { "tech_tree": {
"title": "Tech tree", "title": "Tech tree",
"battle": "1 battle", "battle": "1 battle",
"battles": "%s1 battles", "battles": "{count} battles",
"route": "Route %s1 of %s2" "route": "Route {index} of {count}"
}, },
"game_modes": { "game_modes": {
"title": "Game mode abilities" "title": "Game mode abilities"
}, },
"review": { "review": {
"title": "Review videos", "title": "Review videos",
"join": "Apply to %s1 YouTuber program" "join": "Apply to {name} YouTuber program"
}, },
"configuration": { "configuration": {
"title": "Configuration", "title": "Configuration",
@@ -954,15 +959,15 @@
"title": "Characteristics", "title": "Characteristics",
"compare": { "compare": {
"label": "Compare against", "label": "Compare against",
"class": "Tier %s1 %s2", "class": "Tier {tier} {class}",
"tier": "All of tier %s1", "tier": "All of tier {tier}",
"all": "All tanks" "all": "All tanks"
}, },
"values": { "values": {
"shell_index": "Shell %s1", "shell_index": "Shell {index}",
"at_distance": "At %s1m", "at_distance": "At {distance}m",
"at_distance_slider_label": "Distance", "at_distance_slider_label": "Distance",
"dispersion_at_distance": "Dispersion at %s1m", "dispersion_at_distance": "Dispersion at {distance}m",
"gun_flexibility": "Gun flexibility", "gun_flexibility": "Gun flexibility",
"speed": "Speed", "speed": "Speed",
"terrain_coefficients": "Terrain coefficients", "terrain_coefficients": "Terrain coefficients",
@@ -1111,9 +1116,9 @@
"description": "Tank performance statistics", "description": "Tank performance statistics",
"seo_description": "Best tanks and their performances in World of Tanks Blitz", "seo_description": "Best tanks and their performances in World of Tanks Blitz",
"estimation": { "estimation": {
"body": "Estimated stats based on %s1 players. Updated %s2 ago.", "body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "%s1 hours", "hours": "{count} hours",
"minutes": "%s1 minutes" "minutes": "{count} minutes"
}, },
"player_count": { "player_count": {
"label": "Player count" "label": "Player count"
@@ -1145,10 +1150,10 @@
"button": "Browse all", "button": "Browse all",
"description": "Blitz player avatars", "description": "Blitz player avatars",
"seo_description": "All World of Tanks Blitz player avatars", "seo_description": "All World of Tanks Blitz player avatars",
"last_updated": "Last updated on %s1", "last_updated": "Last updated on {time}",
"search": { "search": {
"hint": "Search avatars...", "hint": "Search avatars...",
"results": "%s1 avatars" "results": "{count} avatars"
} }
}, },
"session": { "session": {
@@ -1163,8 +1168,8 @@
"no_results": "No players found" "no_results": "No players found"
}, },
"controls": { "controls": {
"title": "Tracking %s1", "title": "Tracking {name}",
"subtitle": "Since %s1", "subtitle": "Since {time}",
"reset": "Reset", "reset": "Reset",
"change": "Change" "change": "Change"
}, },
@@ -1224,7 +1229,7 @@
"cancel": "Cancel", "cancel": "Cancel",
"success": { "success": {
"title": "Copy URL", "title": "Copy URL",
"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\".", "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\".",
"back": "Back", "back": "Back",
"copy": "Copy URL" "copy": "Copy URL"
} }
@@ -1307,17 +1312,11 @@
"description": "Get started with BlitzKit", "description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit" "seo_description": "Get started with BlitzKit"
}, },
"blitz_analysis": { "api": {
"name": "BlitzAnalysis[]", "name": "API",
"button": "Analyze now", "button": "To the docs",
"description": "Rich historic average stats", "description": "BlitzKit hosted API",
"seo_description": "Rich historic average stats in World of Tanks Blitz" "seo_description": "BlitzKit hosted static API for 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": { "footer": {
+106 -107
View File
@@ -1,18 +1,19 @@
{ {
"test": { "test": {
"default": "teste", "default": "teste",
"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.", "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}.",
"fox": "A ágil raposa castanha salta por cima do cão preguiçoso." "fox": "A ágil raposa castanha salta por cima do cão preguiçoso."
}, },
"common": { "common": {
"name": "BlitzKit",
"units": { "units": {
"m": "%s1m", "m": "{value}m",
"mm": "%s1mm", "mm": "{value}mm",
"deg": "%s1°", "deg": "{value}°",
"hp": "%s1hp", "hp": "{value}hp",
"x": "x%s1", "x": "x{value}",
"percentage": "%s1%", "percentage": "{value}%",
"deg_s": "%s1°/s" "deg_s": "{value}°/s"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Autocarregador", "auto_loader": "Autocarregador",
@@ -185,11 +186,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"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.", "clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"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_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.",
"player_not_linked": "Digite \"/link\" para começar.\nEu posso não saber quem é você, mas pode-me dizer hoje sobre isso!", "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": "# Tanque não encontrado\nNão foi possível encontrar um tanque pelo nome \"%s1\".", "tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"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.", "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.",
"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.", "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", "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.", "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.",
@@ -283,19 +284,19 @@
"name": "nível", "name": "nível",
"description": "Nível", "description": "Nível",
"choices": { "choices": {
"tier": "Nível %s1 - %s2" "tier": "Tier {tier_1} - {tier_2}"
} }
} }
}, },
"periods": { "periods": {
"career": "Carreira", "career": "Carreira",
"today": "Hoje", "today": "Hoje",
"days": "%s1 dias", "days": "{days} days",
"custom": "%s1 - %s2 dias" "custom": "{day_1} - {day_2} days"
}, },
"filters": { "filters": {
"none": "Sem filtros", "none": "Sem filtros",
"tier": "Nível %s1" "tier": "Tier {tier}"
}, },
"no_data": { "no_data": {
"battles_in_period": "Não há batalhas jogadas neste período", "battles_in_period": "Não há batalhas jogadas neste período",
@@ -318,22 +319,6 @@
} }
}, },
"commands": { "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": { "replay": {
"name": "replay", "name": "replay",
"description": "Exibir resultados de replays" "description": "Exibir resultados de replays"
@@ -354,19 +339,19 @@
"body": { "body": {
"subtitle": "Pesquisa", "subtitle": "Pesquisa",
"total": "Total", "total": "Total",
"research": "Pesquisa: %s1", "research": "Research: {cost}",
"upgrades": "Melhorias: %s1", "upgrades": "Upgrades: {cost}",
"purchase": "Compra: %s1", "purchase": "Purchase: {cost}",
"equipment": "Equipamento: %s1", "equipment": "Equipment: {cost}",
"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." "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": { "errors": {
"non_tech_tree": "# %s1 não está na árvore tecnológica\n\nVocê não pode pesquisar um tanque premium/colecionador.", "non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# %s1 é um nível I\n\nOs tanques de nível I sempre são desbloqueados gratuitamente para todos os jogadores.", "tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"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.", "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": "# %s1 foi fornecido duas vezes\n\nCertifique-se de que `tanque-inicial` e `tanque-alvo` são diferentes.", "start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"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.", "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": "# %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." "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."
} }
}, },
"about": { "about": {
@@ -409,7 +394,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "Taxa de vitória", "winrate": "Taxa de vitória",
"damage": "Dano", "damage": "Dano",
"unknown_tank": "Tanque desconhecido %s1" "unknown_tank": "Unknown tank {id}"
} }
}, },
"debug": { "debug": {
@@ -445,12 +430,12 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "limite", "name": "limite",
"description": "O número de dias inativos (padrão: %s1 dias)" "description": "The number of days inactive (default: {days} days)"
} }
}, },
"body": { "body": {
"subtitle": "Inativo por %s1+ dias", "subtitle": "Inactive for {days}+ days",
"listing": "%s1 dias" "listing": "{days} days"
} }
}, },
"full_stats": { "full_stats": {
@@ -493,7 +478,7 @@
"body": { "body": {
"accounts_linked": "Contas vinculadas", "accounts_linked": "Contas vinculadas",
"no_role_permissions": { "no_role_permissions": {
"title": "%s1 falhou ao verificar", "title": "{name} failed to verify",
"description": "Eu não tenho permissão para gerenciar funções." "description": "Eu não tenho permissão para gerenciar funções."
} }
}, },
@@ -508,11 +493,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "limite", "name": "limite",
"description": "O número máximo de resultados do clã (padrão: %s1)" "description": "The max number of clan results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Pesquisa do clã para %s1 na região %s2", "title": "Clan search for {search} in region {region}",
"no_results": "Nenhum clã encontrado" "no_results": "Nenhum clã encontrado"
} }
}, },
@@ -522,11 +507,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "limite", "name": "limite",
"description": "O número máximo de resultados do jogador (padrão: %s1)" "description": "The max number of player results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Busca de jogador para %s1 na região %s2", "title": "Player search for {search} in region {region}",
"no_results": "Nenhum jogador encontrado" "no_results": "Nenhum jogador encontrado"
} }
}, },
@@ -546,13 +531,13 @@
"options": { "options": {
"limit": { "limit": {
"name": "limite", "name": "limite",
"description": "Quantos vizinhos exibir (padrão: %s1)" "description": "How many neighbors to display (default: {count})"
}, },
"season": { "season": {
"name": "temporada", "name": "temporada",
"description": "Número da temporada (padrão: atual)", "description": "Número da temporada (padrão: atual)",
"choices": { "choices": {
"current": "%s1 (atual)" "current": "{season} (current)"
} }
}, },
"league": { "league": {
@@ -561,12 +546,12 @@
} }
}, },
"body": { "body": {
"top_players": "Melhores %s1 jogadores", "top_players": "Top {count} players",
"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_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": "# Nenhuma temporada em andamento\n\nNão há uma temporada de classificação em execução no momento.", "no_ongoing_season": "# Nenhuma temporada em andamento\n\nNão há uma temporada de classificação em execução no momento.",
"deleted_player": "Excluído %s1", "deleted_player": "Deleted {id}",
"neighbors": "Classificação de vizinhos", "neighbors": "Classificação de vizinhos",
"more": "+ %s1 mais" "more": "+ {count} more"
} }
}, },
"today": { "today": {
@@ -581,14 +566,14 @@
"name": "tanques-possuídos", "name": "tanques-possuídos",
"description": "Mostra todos os tanques possuídos em um nível", "description": "Mostra todos os tanques possuídos em um nível",
"body": { "body": {
"title": "# %s1's possuía nível %s2 tanques\n\n" "title": "# {name}'s owned tier {tier} tanks\n\n"
} }
}, },
"permissions": { "permissions": {
"name": "Permissões", "name": "Permissões",
"description": "Verifica se o bot tem as permissões necessárias para funcionar", "description": "Verifica se o bot tem as permissões necessárias para funcionar",
"body": { "body": {
"title": "# Permissões\n\n%s1", "title": "# Permissions\n\n{body}",
"view_channels": "Ver canais: necessário para encontrar a mensagem em que o botão atualizar foi usado", "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", "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", "attach_files": "Anexar arquivos: necessário para adicionar imagens a comandos baseados em imagens",
@@ -600,7 +585,7 @@
"description": "Verifique se o bot está vivo.", "description": "Verifique se o bot está vivo.",
"body": { "body": {
"pong": "Pong 🏓", "pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - %s1ms" "pong_with_time": "Pong 🏓 - {time}ms"
}, },
"subcommands": { "subcommands": {
"blitzkit": { "blitzkit": {
@@ -617,7 +602,7 @@
"name": "informação do jogador", "name": "informação do jogador",
"description": "Informações básicas sobre um jogador", "description": "Informações básicas sobre um jogador",
"body": { "body": {
"title": "%s1's informações", "title": "{name}'s information",
"nickname": "Apelido", "nickname": "Apelido",
"battles": "Batalhas", "battles": "Batalhas",
"winrate": "Taxa de vitória", "winrate": "Taxa de vitória",
@@ -646,7 +631,7 @@
} }
}, },
"errors": { "errors": {
"no_participation": "# %s1 não jogou classificação\n\nEste jogador não participou do modo de classificação nesta temporada.", "no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"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." "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."
} }
} }
@@ -658,6 +643,8 @@
"body": "This page has moved. Redirecting..." "body": "This page has moved. Redirecting..."
}, },
"settings": { "settings": {
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Modo de desenvolvimento", "dev_mode": "Modo de desenvolvimento",
"language": "Idioma" "language": "Idioma"
}, },
@@ -671,13 +658,16 @@
"copied": "Copiado!" "copied": "Copiado!"
}, },
"tank_search": { "tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters", "clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Procurar tanques...", "search_bar_hint": "Procurar tanques...",
"recent": "Visto recentemente", "recent": "Visto recentemente",
"select_all": "Selecionar todos", "select_all": "Selecionar todos",
"count_singular": "1 tanque", "count_singular": "1 tanque",
"count_plural": "%s1 tanques", "count_plural": "{count} tanks",
"sorting_by": "Ordenando por %s1", "sorting_by": "Sorting by {name}",
"no_tanks_found": { "no_tanks_found": {
"body": "Nenhum tanque encontrado.", "body": "Nenhum tanque encontrado.",
"search_again": "Tente pesquisar novamente.", "search_again": "Tente pesquisar novamente.",
@@ -739,22 +729,30 @@
} }
}, },
"home": { "home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": { "discord": {
"join_short": "Join cool people!", "join_short": "Join cool people!",
"join_long": "Join super duper cool people!", "join_long": "Join super duper cool people!",
"action": "Discord" "action": "Discord"
}, },
"hero": { "hero": {
"credit": "Por %s1 ❤️" "credit": "By {name} ❤️"
}, },
"hot_tanks": { "hot_tanks": {
"title": "Tanques populares", "title": "Tanques populares",
"subtitle": "Últimos %s1 dias" "subtitle": "Last {days} days"
}, },
"other_projects": { "other_projects": {
"title": "Outros projetos legais" "title": "Outros projetos legais"
}, },
"help_translate": "Por favor, ajude a traduzir %s1 para %s2!", "help_translate": "Please help translate {name} to {language}!",
"seo_description": "Ferramentas para tudo World of Tanks Blitz" "seo_description": "Ferramentas para tudo World of Tanks Blitz"
}, },
"docs": { "docs": {
@@ -764,6 +762,14 @@
} }
}, },
"tools": { "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": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -776,7 +782,7 @@
"next": "Next", "next": "Next",
"no_results": "Sem resultados" "no_results": "Sem resultados"
}, },
"stats": "%s1 / %s2 guessed correctly" "stats": "{correct} / {total} guessed correctly"
}, },
"tankopedia": { "tankopedia": {
"search": "Search a stat...", "search": "Search a stat...",
@@ -784,9 +790,12 @@
"button": "Encontrar tanques", "button": "Encontrar tanques",
"description": "Blitz enciclopédia de tanques", "description": "Blitz enciclopédia de tanques",
"seo_description": "Estatísticas, blindagem, e mais para todos os tanques de Wolrd of Tanks Blitz", "seo_description": "Estatísticas, blindagem, e mais para todos os tanques de Wolrd of Tanks Blitz",
"tank_seo_title": "%s1 - Nível %s2 %s3 %s4 | %s5 Tanquepedia | World of Tanks Blitz (WoTB)", "tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | 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.", "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.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -807,7 +816,6 @@
}, },
"view_range": { "view_range": {
"camo": "Camo", "camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory", "ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail" "dead_rail": "Dead Rail"
}, },
@@ -829,9 +837,7 @@
"green_penetration": "Penetração verde", "green_penetration": "Penetração verde",
"hide_model_under_armor": "Ocultar modelo sob blindagem", "hide_model_under_armor": "Ocultar modelo sob blindagem",
"opaque": "Opaco", "opaque": "Opaco",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Ambiente",
"show_grid": "Exibir grade"
}, },
"screenshot": { "screenshot": {
"download": "Download", "download": "Download",
@@ -853,7 +859,6 @@
"reset": "Restaurar" "reset": "Restaurar"
}, },
"advanced": "Realce avançado", "advanced": "Realce avançado",
"shooter": "Atirador:",
"shot_card": { "shot_card": {
"element": { "element": {
"primary": "Primário", "primary": "Primário",
@@ -864,15 +869,15 @@
"track": "Esteiras" "track": "Esteiras"
}, },
"status": { "status": {
"penetration": "%s1HP penetração", "penetration": "{damage}hp penetration",
"blocked": "Bloqueado", "blocked": "Bloqueado",
"ricochet": "Ricochete", "ricochet": "Ricochete",
"splash": "%s1hp respingo" "splash": "{damage}hp splash"
}, },
"stats": { "stats": {
"thickness_and_angle": "%s1mm %s2°", "thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "%s1mm nominal", "nominal": "{thickness}mm nominal",
"ricochet_loss": "%s1% penetração" "ricochet_loss": "{loss}% penetration"
} }
} }
}, },
@@ -910,15 +915,15 @@
"tech_tree": { "tech_tree": {
"title": "Árvore tecnológica", "title": "Árvore tecnológica",
"battle": "1 batalha", "battle": "1 batalha",
"battles": "%s1 batalhas", "battles": "{count} battles",
"route": "Rota %s1 de %s2" "route": "Route {index} of {count}"
}, },
"game_modes": { "game_modes": {
"title": "Habilidades do modo de jogo" "title": "Habilidades do modo de jogo"
}, },
"review": { "review": {
"title": "Vídeos de análise", "title": "Vídeos de análise",
"join": "Candidate-se para %s1 um programa YouTuber" "join": "Apply to {name} YouTuber program"
}, },
"configuration": { "configuration": {
"title": "Configuração", "title": "Configuração",
@@ -954,15 +959,15 @@
"title": "Características", "title": "Características",
"compare": { "compare": {
"label": "Comparar com", "label": "Comparar com",
"class": "Nível %s1 %s2", "class": "Tier {tier} {class}",
"tier": "Tudo de nível %s1", "tier": "All of tier {tier}",
"all": "Todos os tanques" "all": "Todos os tanques"
}, },
"values": { "values": {
"shell_index": "Projétil %s1", "shell_index": "Shell {index}",
"at_distance": "À %s1m", "at_distance": "At {distance}m",
"at_distance_slider_label": "Distância", "at_distance_slider_label": "Distância",
"dispersion_at_distance": "Dispersão à %s1m", "dispersion_at_distance": "Dispersion at {distance}m",
"gun_flexibility": "Flexibilidade da arma", "gun_flexibility": "Flexibilidade da arma",
"speed": "Velocidade", "speed": "Velocidade",
"terrain_coefficients": "Coeficientes do terreno", "terrain_coefficients": "Coeficientes do terreno",
@@ -1111,9 +1116,9 @@
"description": "Estatísticas de desempenho do tanque", "description": "Estatísticas de desempenho do tanque",
"seo_description": "Melhores tanques e suas performances no World of Tanks Blitz", "seo_description": "Melhores tanques e suas performances no World of Tanks Blitz",
"estimation": { "estimation": {
"body": "Estatísticas estimadas baseadas em %s1 jogadores. Atualizado %s2 atrás.", "body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "%s1 horas", "hours": "{count} hours",
"minutes": "%s1 minutos" "minutes": "{count} minutes"
}, },
"player_count": { "player_count": {
"label": "Número de jogadores" "label": "Número de jogadores"
@@ -1145,10 +1150,10 @@
"button": "Procurar todos", "button": "Procurar todos",
"description": "Avatares do jogador Blitz", "description": "Avatares do jogador Blitz",
"seo_description": "Todos os avatares do jogador World of Tanks Blitz", "seo_description": "Todos os avatares do jogador World of Tanks Blitz",
"last_updated": "Última atualização em %s1", "last_updated": "Last updated on {time}",
"search": { "search": {
"hint": "Procurar avatares...", "hint": "Procurar avatares...",
"results": "%s1 avatares" "results": "{count} avatars"
} }
}, },
"session": { "session": {
@@ -1163,8 +1168,8 @@
"no_results": "Nenhum jogador encontrado" "no_results": "Nenhum jogador encontrado"
}, },
"controls": { "controls": {
"title": "Rastreando %s1", "title": "Tracking {name}",
"subtitle": "Desde %s1", "subtitle": "Since {time}",
"reset": "Restaurar", "reset": "Restaurar",
"change": "Mudança" "change": "Mudança"
}, },
@@ -1224,7 +1229,7 @@
"cancel": "Cancelar", "cancel": "Cancelar",
"success": { "success": {
"title": "Copiar URL", "title": "Copiar URL",
"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\".", "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\".",
"back": "Voltar", "back": "Voltar",
"copy": "Copiar URL" "copy": "Copiar URL"
} }
@@ -1307,17 +1312,11 @@
"description": "Comece com BlitzKit", "description": "Comece com BlitzKit",
"seo_description": "Comece com BlitzKit" "seo_description": "Comece com BlitzKit"
}, },
"blitz_analysis": { "api": {
"name": "AnaliseBlitz[]", "name": "API",
"button": "Analise agora", "button": "To the docs",
"description": "Estatísticas médias históricas ricas", "description": "BlitzKit hosted API",
"seo_description": "Estatísticas médias históricas ricas no World of Tanks Blitz" "seo_description": "BlitzKit hosted static API for 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": { "footer": {
+118 -119
View File
@@ -1,18 +1,19 @@
{ {
"test": { "test": {
"default": "тест", "default": "тест",
"long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт - %s1, потому что его цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3.", "long": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт — {fruit}, потому что это цвет {color}. Он мне так нравится, так что я скажу его 3 раза {fruit}! {fruit}! {fruit}! И ещё раз, это {color}, а не {not_color}.",
"fox": "Это длинная тестовая строка. Вы НИКОГДА не должны видеть её в пользовательском интерфейсе. Мой любимый фрукт - %s1, потому что его цвет %s2. Он мне так нравится, что я произнесу его имя 3 раза! %s1! %s1! %s1! И ещё раз, это %s2, а не %s3." "fox": "Съешь же ещё этих мягких французских булок, да выпей чаю."
}, },
"common": { "common": {
"name": "BlitzKit",
"units": { "units": {
"m": "%s1м", "m": "{value}м",
"mm": "%s1мм", "mm": "{value}мм",
"deg": "%s1°", "deg": "{value}°",
"hp": "%s1hp", "hp": "{value}оп",
"x": "x%s1", "x": "x{value}",
"percentage": "%s1%", "percentage": "{value}%",
"deg_s": "%s1°/с" "deg_s": "{value}°/с"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Барабан", "auto_loader": "Барабан",
@@ -185,11 +186,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"clan_not_found": "# Не удалось найти клан\nНе найден «%s1». Попробуйте выбрать клан из результатов поиска.", "clan_not_found": "# Не удалось найти клан\nНе найден «{search}». Попробуйте выбрать клан из результатов поиска.",
"player_not_found": "# Не удалось найти пользователя\nНе удалось найти пользователя «%s1». Попробуйте выбрать пользователя из результатов поиска, введя правильное имя пользователя или используя команду «/link».", "player_not_found": "# Не удалось найти пользователя\nНе удалось найти пользователя «{search}». Попробуйте выбрать пользователя из результатов поиска, введя правильное имя пользователя или используя команду «/link».",
"player_not_linked": "# Используйте «/link», чтобы начать\nЯ не знаю, кто ты, но давай познакомимся сегодня!", "player_not_linked": "# Используйте «/link», чтобы начать\nЯ не знаю, кто ты, но давай познакомимся сегодня!",
"tank_name_not_found": "# Танк не найден\nНе удалось найти танк по названию «%s1».", "tank_name_not_found": "# Танк не найден\nНе удалось найти танк с названием «{search}».",
"tank_id_not_found": "# Танк не найден\nНе удалось найти танк по ID «%s1». Попробуйте ввести название танка вместо числа.", "tank_id_not_found": "# Танк не найден\nНе удалось найти танк с ID «{search}». Попробуйте ввести название танка вместо числа.",
"no_tank_stats": "# Статистика танка недоступна\nУ этого игрока нет доступной статистики. Возможно, это не тот игрок, которого вы ищете.", "no_tank_stats": "# Статистика танка недоступна\nУ этого игрока нет доступной статистики. Возможно, это не тот игрок, которого вы ищете.",
"get_help": "Получить помощь", "get_help": "Получить помощь",
"uncaught_error": "# BlitzKit столкнулся с ошибкой!\nНам очень жаль. Не стесняйтесь и [присоединяйтесь к официальному серверу Discord](https://discord.gg/nDt7AjGJQH), чтобы получить помощь.", "uncaught_error": "# BlitzKit столкнулся с ошибкой!\nНам очень жаль. Не стесняйтесь и [присоединяйтесь к официальному серверу Discord](https://discord.gg/nDt7AjGJQH), чтобы получить помощь.",
@@ -283,19 +284,19 @@
"name": "уровень", "name": "уровень",
"description": "Уровень", "description": "Уровень",
"choices": { "choices": {
"tier": "Уровень %s - %s" "tier": "Уровень {tier_1} - {tier_2}"
} }
} }
}, },
"periods": { "periods": {
"career": "Карьера", "career": "Карьера",
"today": "Cегодня", "today": "Cегодня",
"days": "%s дней", "days": "{days} дней",
"custom": "%s - %s дней" "custom": "{day_1} - {day_2} дней"
}, },
"filters": { "filters": {
"none": "Без фильтров", "none": "Без фильтров",
"tier": "Уровень %s1" "tier": " {tier} уровня"
}, },
"no_data": { "no_data": {
"battles_in_period": "За этот период боёв не было", "battles_in_period": "За этот период боёв не было",
@@ -318,22 +319,6 @@
} }
}, },
"commands": { "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": { "replay": {
"name": "повтор", "name": "повтор",
"description": "Показать результаты повтора" "description": "Показать результаты повтора"
@@ -354,19 +339,19 @@
"body": { "body": {
"subtitle": "Исследование", "subtitle": "Исследование",
"total": "Всего", "total": "Всего",
"research": "Исследование: %s", "research": "Исследование: {cost}",
"upgrades": "Улучшения: %s", "upgrades": "Улучшения: {cost}",
"purchase": "Покупка: %s", "purchase": "Покупка: {cost}",
"equipment": "Снаряжение: %s", "equipment": "Снаряжение: {cost}",
"estimation": "# Это приблизительная оценка!\n\nВ этот список могут входить уже исследованные танки из-за неточной информации, предоставляемой Wargaming. Попробуйте указать «стартовый-танк» в строке при использовании команды, если сможете." "estimation": "# Это приблизительная оценка!\n\nВ этот список могут входить уже исследованные танки из-за неточной информации, предоставляемой Wargaming. Попробуйте указать «стартовый-танк» в строке при использовании команды, если сможете."
}, },
"errors": { "errors": {
"non_tech_tree": "# %s отсутствует в дереве танков\n\nВы не можете исследовать премиум / коллекционный танк.", "non_tech_tree": "# {tank} отсутствует в дереве танков\n\nВы не можете исследовать премиумный / коллекционный танк.",
"tier_1": "# %s - это уровень I\n\nУровень I всегда открыт бесплатно для всех игроков.", "tier_1": "# {tank} — танк I уровня\n\nУровень I всегда открыт бесплатно для всех игроков.",
"already_researched": "# %s уже исследован\n\nПопробуйте предоставить «starting-tank» при использовании команды для просмотра итогов уже исследованной ветки.", "already_researched": "# {tank} уже исследован\n\nПопробуйте предоставить «starting-tank» при использовании команды для просмотра итогов уже исследованной ветки.",
"start_end_equal": "# %s был предоставлен дважды\n\nУбедитесь, что «starting-tank» и «target-tank» различаются.", "start_end_equal": "# {tank} был предоставлен дважды\n\nУбедитесь, что «starting-tank» и «target-tank» различаются.",
"tanks_not_on_line": "# %s и %s находятся не в одной ветке\n\nУбедитесь, что «starting-tank» и «target-tank» находятся на той же ветке дерева танков.", "tanks_not_on_line": "# {tank_1} и {tank_2} находятся не в одной ветке\n\nУбедитесь, что «starting-tank» и «target-tank» находятся на той же ветке дерева танков.",
"unordered": "# У %s более высокий уровень чем у %s\n\nОбязательно укажите %s в качестве «starting-tank», так как это танк более низкого уровня." "unordered": "# У {tank_1} более высокий уровень чем у {tank_2}\n\nОбязательно укажите {tank_2} в качестве «starting-tank», так как это танк более низкого уровня."
} }
}, },
"about": { "about": {
@@ -409,7 +394,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "Коэффициент побед", "winrate": "Коэффициент побед",
"damage": "Урон", "damage": "Урон",
"unknown_tank": "Неизвестный танк %s" "unknown_tank": "Неизвестный танк {id}"
} }
}, },
"debug": { "debug": {
@@ -445,12 +430,12 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "порог", "name": "порог",
"description": "Количество дней не активности (по умолчанию: %s дней)" "description": "Количество дней не активности (по умолчанию: {days} дней)"
} }
}, },
"body": { "body": {
"subtitle": "Неактивен %s+ дней", "subtitle": "Неактивен {days}+ дней",
"listing": "%s дней" "listing": "{days} дней"
} }
}, },
"full_stats": { "full_stats": {
@@ -493,7 +478,7 @@
"body": { "body": {
"accounts_linked": "Аккаунты привязаны", "accounts_linked": "Аккаунты привязаны",
"no_role_permissions": { "no_role_permissions": {
"title": "%s1 не удалось проверить", "title": "{name} не удалось проверить",
"description": "У меня нет разрешения на управление ролями." "description": "У меня нет разрешения на управление ролями."
} }
}, },
@@ -508,11 +493,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "лимит", "name": "лимит",
"description": "Максимальное количество результатов клана (по умолчанию: %s)" "description": "Максимальное количество результатов клана (по умолчанию: {limit})"
} }
}, },
"body": { "body": {
"title": "Поиск клана %s в регионе %s", "title": "Поиск клана {search} в регионе {region}",
"no_results": "Кланов не найдено" "no_results": "Кланов не найдено"
} }
}, },
@@ -522,11 +507,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "лимит", "name": "лимит",
"description": "Максимальное число результатов игрока (по умолчанию: %s)" "description": "Максимальное число результатов игрока (по умолчанию: {limit})"
} }
}, },
"body": { "body": {
"title": "Поиск игрока %s в регионе %s", "title": "Поиск игрока {search} в регионе {region}",
"no_results": "Игроков не найдено" "no_results": "Игроков не найдено"
} }
}, },
@@ -546,13 +531,13 @@
"options": { "options": {
"limit": { "limit": {
"name": "лимит", "name": "лимит",
"description": "Количество отображаемых ближайших игроков (по умолчанию: %s)" "description": "Количество отображаемых ближайших игроков (по умолчанию: {count})"
}, },
"season": { "season": {
"name": "сезон", "name": "сезон",
"description": "Номер сезона (по умолчанию: текущий)", "description": "Номер сезона (по умолчанию: текущий)",
"choices": { "choices": {
"current": "%s (текущая)" "current": "{season} (текущий)"
} }
}, },
"league": { "league": {
@@ -561,12 +546,12 @@
} }
}, },
"body": { "body": {
"top_players": "Топ %s игроков", "top_players": "Топ {count} игроков",
"no_participation": "# %s не играл в рейтинг в сезоне %s\n\nЭтот игрок не участвовал в этом сезоне или не получил прошлую калибровку.", "no_participation": "# {player} не играл в рейтинг в сезоне {season}\n\nЭтот игрок не участвовал в этом сезоне или не получил прошлую калибровку.",
"no_ongoing_season": "# Нет текущего сезона\n\nВ данный момент нет рейтингового сезона.", "no_ongoing_season": "# Нет текущего сезона\n\nВ данный момент нет рейтингового сезона.",
"deleted_player": "Удалено %s", "deleted_player": "Удаленный {id}",
"neighbors": "Рейтинг ближайших игроков", "neighbors": "Рейтинг ближайших игроков",
"more": "+ ещё %s" "more": "+ ещё {count}"
} }
}, },
"today": { "today": {
@@ -581,14 +566,14 @@
"name": "cобственные-танки", "name": "cобственные-танки",
"description": "Показывает все принадлежащие танки на уровне", "description": "Показывает все принадлежащие танки на уровне",
"body": { "body": {
"title": "# %s принадлежал уровень %s танков\n\n" "title": "# {name} принадлежат танки {tier} уровня\n\n"
} }
}, },
"permissions": { "permissions": {
"name": "разрешения", "name": "разрешения",
"description": "Проверяет, есть ли у бота необходимые разрешения для работы", "description": "Проверяет, есть ли у бота необходимые разрешения для работы",
"body": { "body": {
"title": "# Разрешения\n\n%s", "title": "# Разрешения\n\n{body}",
"view_channels": "Просмотр каналов: необходимо найти сообщение, на котором была использована кнопка обновления", "view_channels": "Просмотр каналов: необходимо найти сообщение, на котором была использована кнопка обновления",
"read_message_history": "Просмотр истории сообщений: необходимо найти сообщение для обновления", "read_message_history": "Просмотр истории сообщений: необходимо найти сообщение для обновления",
"attach_files": "Прикрепить файлы: необходимо для добавления изображений в команды на основе изображения", "attach_files": "Прикрепить файлы: необходимо для добавления изображений в команды на основе изображения",
@@ -600,7 +585,7 @@
"description": "Проверьте, что бот жив.", "description": "Проверьте, что бот жив.",
"body": { "body": {
"pong": "Понг 🏓", "pong": "Понг 🏓",
"pong_with_time": "Понг 🏓 - %sмс" "pong_with_time": "Понг 🏓 — {time}мс"
}, },
"subcommands": { "subcommands": {
"blitzkit": { "blitzkit": {
@@ -617,7 +602,7 @@
"name": "информация-об-игроке", "name": "информация-об-игроке",
"description": "Основная информация о игроке", "description": "Основная информация о игроке",
"body": { "body": {
"title": "Информация о %s", "title": "Информация о {name}",
"nickname": "Никнейм", "nickname": "Никнейм",
"battles": "Боёв", "battles": "Боёв",
"winrate": "Коэффициент побед", "winrate": "Коэффициент побед",
@@ -646,7 +631,7 @@
} }
}, },
"errors": { "errors": {
"no_participation": "# %s не получил рейтинга\n\nЭтот игрок в этом сезоне не участвовал в рейтинговом режиме.", "no_participation": "# {name} не получил рейтинга\n\nЭтот игрок в этом сезоне не участвовал в рейтинговом режиме.",
"unrecorded_stats": "# Статистика недоступна в настоящее время\n\nВ настоящее время Wargaming, возможно, сталкивается с простоями сервера. Если сезон продолжается, статистика должна быть доступна в течение 24 часов." "unrecorded_stats": "# Статистика недоступна в настоящее время\n\nВ настоящее время Wargaming, возможно, сталкивается с простоями сервера. Если сезон продолжается, статистика должна быть доступна в течение 24 часов."
} }
} }
@@ -658,6 +643,8 @@
"body": "Эта страница перемещена. Перенаправление..." "body": "Эта страница перемещена. Перенаправление..."
}, },
"settings": { "settings": {
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Режим разработчика", "dev_mode": "Режим разработчика",
"language": "Язык" "language": "Язык"
}, },
@@ -671,13 +658,16 @@
"copied": "Скопировано!" "copied": "Скопировано!"
}, },
"tank_search": { "tank_search": {
"select_dialog_title": "Поиск танка",
"swap_dialog_title": "Заменить танк",
"clear_filters": "Сбросить фильтры", "clear_filters": "Сбросить фильтры",
"login": "Войти, чтобы воспользоваться",
"search_bar_hint": "Поиск танков...", "search_bar_hint": "Поиск танков...",
"recent": "Недавние", "recent": "Недавние",
"select_all": "Выбрать всё", "select_all": "Выбрать всё",
"count_singular": "1 танк", "count_singular": "1 танк",
"count_plural": "%s1 танк", "count_plural": "Танков: {count}",
"sorting_by": "Сортировать по %s1", "sorting_by": "Сортировать по {name}",
"no_tanks_found": { "no_tanks_found": {
"body": "Танков не найдено.", "body": "Танков не найдено.",
"search_again": "Попробуйте повторить поиск.", "search_again": "Попробуйте повторить поиск.",
@@ -693,7 +683,7 @@
"survivability.volume": "объём", "survivability.volume": "объём",
"survivability.length": "длина", "survivability.length": "длина",
"fire.dpm": "базовый ДПМ", "fire.dpm": "базовый ДПМ",
"fire.dpmPremium": "улучшенный ДПМ", "fire.dpmPremium": "премиумный ДПМ",
"fire.reload": "перезарядка", "fire.reload": "перезарядка",
"fire.caliber": "калибр", "fire.caliber": "калибр",
"fire.standardPenetration": "базовое пробитие", "fire.standardPenetration": "базовое пробитие",
@@ -739,22 +729,30 @@
} }
}, },
"home": { "home": {
"pwa": {
"title": "Установите приложение BlitzKit",
"new": "НОВИНКА",
"checking": "Проверка совместимости...",
"instructions": "Инструкция для устройств Apple.",
"install": "Установка",
"dismiss": "Отклонить"
},
"discord": { "discord": {
"join_short": "Присоединяйтесь к крутым ребятам!", "join_short": "Присоединяйтесь к крутым ребятам!",
"join_long": "Присоединяйся к супер-дупер крутым ребятам!", "join_long": "Присоединяйся к супер-дупер крутым ребятам!",
"action": "Discord" "action": "Discord"
}, },
"hero": { "hero": {
"credit": "От %s1 ❤️" "credit": "От {name} ❤️"
}, },
"hot_tanks": { "hot_tanks": {
"title": "Популярные танки", "title": "Популярные танки",
"subtitle": "За последнее %s1 дней" "subtitle": "За последнее {days} дней"
}, },
"other_projects": { "other_projects": {
"title": "Другие классные проекты" "title": "Другие классные проекты"
}, },
"help_translate": "Пожалуйста, помогите перевести %s1 на %s2!", "help_translate": "Пожалуйста, помогите перевести {name} на {language}!",
"seo_description": "Инструменты для World of Tanks Blitz" "seo_description": "Инструменты для World of Tanks Blitz"
}, },
"docs": { "docs": {
@@ -764,8 +762,16 @@
} }
}, },
"tools": { "tools": {
"mixer": {
"name": "Смешать",
"button": "Смешать и собрать",
"description": "Модули смешанного танка",
"seo_description": "Смешивание и сбор корпусов, башен и пушек танков из World of Tanks Blitz",
"screenshot": "Скриншот",
"randomize": "Случайно"
},
"guess": { "guess": {
"name": "Угадайте танк", "name": "Угадай танк",
"button": "Угадать", "button": "Угадать",
"description": "Мини-игра «Силуэт»", "description": "Мини-игра «Силуэт»",
"seo_description": "Играйте угадывая силуэт танка из World of Tanks Blitz", "seo_description": "Играйте угадывая силуэт танка из World of Tanks Blitz",
@@ -776,7 +782,7 @@
"next": "Следующий", "next": "Следующий",
"no_results": "Ничего не найденo" "no_results": "Ничего не найденo"
}, },
"stats": "%s1 / %s2 угадано верно" "stats": "{correct} / {total} угадано верно"
}, },
"tankopedia": { "tankopedia": {
"search": "Поиск характеристик...", "search": "Поиск характеристик...",
@@ -784,9 +790,12 @@
"button": "Поиск танков", "button": "Поиск танков",
"description": "Танковая энциклопедия Blitz", "description": "Танковая энциклопедия Blitz",
"seo_description": "Статистика, броня и многое другое по всем танкам World of Tanks Blitz", "seo_description": "Статистика, броня и многое другое по всем танкам World of Tanks Blitz",
"tank_seo_title": "%s1 - %s4 %s3 %s2 уровня | Танкопедия %s5 | World of Tanks Blitz (WoTB)", "tank_seo_title": "{name} — {class} {nationality} {tier} уровня | Танкопедия {website} | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 броня, статистика, оборудование, снабжение и многое другое. %s2 УВМ, %s3hp альфа, %s4mm пробитие, %s5км/ч лучшая скорость, %s6hp здоровье.", "tank_seo_description": "Броня, статистика, оборудование, снабжение {name} и многое другое. DPM {dpm}, альфа {alpha}оп, пробитие{penetration}мм, лучшая скорость{speed}км/ч, здоровье {health}оп.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Пропуск"
},
"assault": { "assault": {
"title": "Штурмовые снаряды" "title": "Штурмовые снаряды"
}, },
@@ -806,13 +815,12 @@
"no_ricochet": "Разрывные снаряды не рикошетят" "no_ricochet": "Разрывные снаряды не рикошетят"
}, },
"view_range": { "view_range": {
"camo": "Камо", "camo": "Камуфляж",
"percentage": "%s1%",
"ghost_factory": "Промзона", "ghost_factory": "Промзона",
"dead_rail": "Эшелон" "dead_rail": "Эшелон"
}, },
"flexibility": { "flexibility": {
"yaw": "Отклонение", "yaw": "Поворот",
"pitch": "Наклон", "pitch": "Наклон",
"min": "Вниз", "min": "Вниз",
"max": "Вверх" "max": "Вверх"
@@ -829,9 +837,7 @@
"green_penetration": "Зелёное пробитие", "green_penetration": "Зелёное пробитие",
"hide_model_under_armor": "Скрыть модель под броней", "hide_model_under_armor": "Скрыть модель под броней",
"opaque": "Прозрачность", "opaque": "Прозрачность",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Окружение",
"show_grid": "Показать сетку"
}, },
"screenshot": { "screenshot": {
"download": "Загрузить", "download": "Загрузить",
@@ -853,7 +859,6 @@
"reset": "Сброс" "reset": "Сброс"
}, },
"advanced": "Продвинутая подсветка", "advanced": "Продвинутая подсветка",
"shooter": "Стреляет:",
"shot_card": { "shot_card": {
"element": { "element": {
"primary": "Броня", "primary": "Броня",
@@ -864,15 +869,15 @@
"track": "Гусеницы" "track": "Гусеницы"
}, },
"status": { "status": {
"penetration": "Пробитие на %s1оп", "penetration": "Пробитие на {damage}оп",
"blocked": "Не пробил", "blocked": "Не пробил",
"ricochet": "Рикошет", "ricochet": "Рикошет",
"splash": "Урон осколками на %s1оп" "splash": "Урон осколками на {damage}оп"
}, },
"stats": { "stats": {
"thickness_and_angle": "%s1мм %s2°", "thickness_and_angle": "{thickness}мм {angle}°",
"nominal": "%s1мм номинала", "nominal": "{thickness}1мм номинала",
"ricochet_loss": "%s1% пробития" "ricochet_loss": "{loss}% пробития"
} }
} }
}, },
@@ -902,7 +907,7 @@
"tier": "Уровень", "tier": "Уровень",
"type": "Тип", "type": "Тип",
"dev_id": "DEV: ID", "dev_id": "DEV: ID",
"purchase": "Стоимость покупки", "purchase": "Цена покупки",
"restoration": "Цена восстановления", "restoration": "Цена восстановления",
"sale": "Цена продажи", "sale": "Цена продажи",
"research": "Необходимо опыта" "research": "Необходимо опыта"
@@ -910,15 +915,15 @@
"tech_tree": { "tech_tree": {
"title": "Дерево танков", "title": "Дерево танков",
"battle": "1 бой", "battle": "1 бой",
"battles": "%s1 боёв", "battles": "{count} боёв",
"route": "Ветка %s1 из %s2" "route": "Ветка {index} из {count}"
}, },
"game_modes": { "game_modes": {
"title": "Способности в игровых режимах" "title": "Способности в игровых режимах"
}, },
"review": { "review": {
"title": "Видеообзоры", "title": "Видеообзоры",
"join": "Предложить ютубера для программы %s1" "join": "Предложить ютубера для программы {name}"
}, },
"configuration": { "configuration": {
"title": "Конфигурация", "title": "Конфигурация",
@@ -954,15 +959,15 @@
"title": "Характеристики", "title": "Характеристики",
"compare": { "compare": {
"label": "Сравнивать с", "label": "Сравнивать с",
"class": "%s2 %s1 уровня", "class": "{class} {tier} уровня",
"tier": "Все %s1 уровня", "tier": "Все {tier} уровня",
"all": "Все танки" "all": "Все танки"
}, },
"values": { "values": {
"shell_index": "Снаряд %s1", "shell_index": "Снаряд {index}",
"at_distance": "На %s1м", "at_distance": "На {distance}м",
"at_distance_slider_label": "Дистанция", "at_distance_slider_label": "Дистанция",
"dispersion_at_distance": "Разброс на %s1м", "dispersion_at_distance": "Разброс на {distance}м",
"gun_flexibility": "Углы наводки", "gun_flexibility": "Углы наводки",
"speed": "Скорость", "speed": "Скорость",
"terrain_coefficients": "Сопротивление грунтам", "terrain_coefficients": "Сопротивление грунтам",
@@ -978,7 +983,7 @@
"dpmEffective": "Эффективность за 60с", "dpmEffective": "Эффективность за 60с",
"shells": "Снарядов в барабане", "shells": "Снарядов в барабане",
"mostOptimalShellIndex": "Самый эффективный снаряд", "mostOptimalShellIndex": "Самый эффективный снаряд",
"shellReloads": "Reloads", "shellReloads": "Перезарядка",
"shellReload": "Перезарядка", "shellReload": "Перезарядка",
"intraClip": "Между снарядами", "intraClip": "Между снарядами",
"caliber": "Калибр", "caliber": "Калибр",
@@ -1038,8 +1043,8 @@
"shellCapacity": "Боезапас", "shellCapacity": "Боезапас",
"gunType": "Тип орудия", "gunType": "Тип орудия",
"penetrationAt250m": "Пробитие на 250м", "penetrationAt250m": "Пробитие на 250м",
"burstShells": "Burst shell count", "burstShells": "Кол-во разрывных снарядов",
"burstInterShell": "Burst inter-shell" "burstInterShell": "Взрыв между снарядами"
} }
}, },
"firepower": { "firepower": {
@@ -1111,9 +1116,9 @@
"description": "Статистика эффективности танков", "description": "Статистика эффективности танков",
"seo_description": "Лучшие танки и их эффективность в World of Tanks Blitz", "seo_description": "Лучшие танки и их эффективность в World of Tanks Blitz",
"estimation": { "estimation": {
"body": "Примерная статистика на основе %s1 игроков. Обновлено %s2 назад.", "body": "Примерная статистика на основе {count} игроков. Обновлено {age} назад.",
"hours": "%s1 часов", "hours": "{count} часов",
"minutes": "%s1 минут" "minutes": "{count} минут"
}, },
"player_count": { "player_count": {
"label": "Количество игроков" "label": "Количество игроков"
@@ -1136,7 +1141,7 @@
"hits": "Уничтожен", "hits": "Уничтожен",
"damage_ratio": "Коэф. урона", "damage_ratio": "Коэф. урона",
"damage_taken": "Получено урона", "damage_taken": "Получено урона",
"capture_points": "Ограничительные точки" "capture_points": "Точки ограничения"
} }
} }
}, },
@@ -1145,10 +1150,10 @@
"button": "Просмотреть", "button": "Просмотреть",
"description": "Аватары игрока Blitz", "description": "Аватары игрока Blitz",
"seo_description": "Все аватары игрока World of Tanks Blitz", "seo_description": "Все аватары игрока World of Tanks Blitz",
"last_updated": "Последнее обновление %s1", "last_updated": "Последнее обновление {time}",
"search": { "search": {
"hint": "Поиск аватаров...", "hint": "Поиск аватаров...",
"results": "%s1 аватар" "results": "Аватаров: {count}"
} }
}, },
"session": { "session": {
@@ -1157,14 +1162,14 @@
"description": "Статистика сеанса игры", "description": "Статистика сеанса игры",
"seo_description": "Отслеживайте свою статистику в реальном времени во время игры World of Tanks Blitz", "seo_description": "Отслеживайте свою статистику в реальном времени во время игры World of Tanks Blitz",
"search": { "search": {
"lookup": "Искать игрока, чтобы начать", "lookup": "Введите ник игрока",
"hint": "Поиск игроков...", "hint": "Поиск игроков...",
"searching": "Поиск...", "searching": "Поиск...",
"no_results": "Игроков не найдено" "no_results": "Игроков не найдено"
}, },
"controls": { "controls": {
"title": "Отслеживание %s1", "title": "Отслеживание {name}",
"subtitle": "Начиная с %s1", "subtitle": "Начиная с {time}",
"reset": "Сброс", "reset": "Сброс",
"change": "Изменить" "change": "Изменить"
}, },
@@ -1198,7 +1203,7 @@
"button": "Создать", "button": "Создать",
"description": "Встраиваемые элементы для стримов", "description": "Встраиваемые элементы для стримов",
"seo_description": "Создать вставки для стримов по World of Tanks Blitz с невероятной кастомизации", "seo_description": "Создать вставки для стримов по World of Tanks Blitz с невероятной кастомизации",
"coming_soon": "Скоро появятся новые встраиваемые элементы.", "coming_soon": "В скором времени появятся новые встраиваемые элементы.",
"suggest": "Предложить идеи.", "suggest": "Предложить идеи.",
"preview": { "preview": {
"title": "Предпросмотр", "title": "Предпросмотр",
@@ -1224,7 +1229,7 @@
"cancel": "Отмена", "cancel": "Отмена",
"success": { "success": {
"title": "Скопировать URL", "title": "Скопировать URL",
"info": "Сгенерирован URL для %s1. Встройка начнет отслеживание, как только URL будет вставлен. Можно сбросить, щелкнув правой кнопкой мыши на ПК или долгим нажатием на мобильном телефоне и выбором «Сброс».", "info": "Сгенерирован URL для {name}. Встройка начнет отслеживание, как только URL будет вставлен. Можно сбросить, щелкнув правой кнопкой мыши на ПК или долгим нажатием на мобильном телефоне и выбором «Сброс».",
"back": "Назад", "back": "Назад",
"copy": "Скопировать URL" "copy": "Скопировать URL"
} }
@@ -1307,17 +1312,11 @@
"description": "Начните с BlitzKit", "description": "Начните с BlitzKit",
"seo_description": "Начните с BlitzKit" "seo_description": "Начните с BlitzKit"
}, },
"blitz_analysis": { "api": {
"name": "BlitzAnalysis[]", "name": "API",
"button": "Анализируй", "button": "К документации",
"description": "Богатая статистика", "description": "API сервера BlitzKit",
"seo_description": "Богатые статистика по World of Tanks Blitz" "seo_description": "Статический API сервера BlitzKit для World of Tanks Blitz"
},
"blitz_insider": {
"name": "Blitz Insider",
"button": "Открой для себя",
"description": "Статистика сеанса игры",
"seo_description": "Статистика сеанса игры в World of Tanks Blitz"
} }
}, },
"footer": { "footer": {
+106 -107
View File
@@ -1,18 +1,19 @@
{ {
"test": { "test": {
"default": "тест", "default": "тест",
"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.", "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}.",
"fox": "Швидка бура лисиця перестрибує через лінивого собаку." "fox": "Швидка бура лисиця перестрибує через лінивого собаку."
}, },
"common": { "common": {
"name": "BlitzKit",
"units": { "units": {
"m": "%s1м", "m": "{value}m",
"mm": "%s1мм", "mm": "{value}mm",
"deg": "%s1°", "deg": "{value}°",
"hp": "%s1хп", "hp": "{value}hp",
"x": "х%s1", "x": "x{value}",
"percentage": "%s1%", "percentage": "{value}%",
"deg_s": "%s1°/с" "deg_s": "{value}°/s"
}, },
"gun_types": { "gun_types": {
"auto_loader": "Магазинна система заряджання", "auto_loader": "Магазинна система заряджання",
@@ -185,11 +186,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"clan_not_found": "# Не вдалося знайти клан\nНе вдалося знайти клан \"%s1\". Спробуйте вибрати клан з пошукового результату.", "clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"player_not_found": "# Не вдалося знайти користувача\nЯ не зміг знайти користувача \"%s1\". Спробуйте вибрати користувача з результату пошуку, ввести коректний нікнейм, або використати команду '/link'.", "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.",
"player_not_linked": "# Використайте команду \"/link\" щоб почати\nЯ не знаю, хто ви, але ви можете сказати мені сьогодні!", "player_not_linked": "# Використайте команду \"/link\" щоб почати\nЯ не знаю, хто ви, але ви можете сказати мені сьогодні!",
"tank_name_not_found": "# Танк не знайдено\nНе вдалось знайти танк за ім'ям \"%s1\".", "tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# Танк не знайдено\nНе вдалось знайти танк за ID \"%s1\". Спробуйте ввести назву танка замість числа.", "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.",
"no_tank_stats": "# Немає статистики для танка\nЦей гравець не має статистики.\nМожливо, це не той гравець, якого ви шукаєте.", "no_tank_stats": "# Немає статистики для танка\nЦей гравець не має статистики.\nМожливо, це не той гравець, якого ви шукаєте.",
"get_help": "Допомогти Discord серверу", "get_help": "Допомогти Discord серверу",
"uncaught_error": "# Упс! BlitzKit зіткнувся з помилкою!\nНам дуже шкода. Можете попросити допомоги на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH).", "uncaught_error": "# Упс! BlitzKit зіткнувся з помилкою!\nНам дуже шкода. Можете попросити допомоги на [офіційному сервері Discord](https://discord.gg/nDt7AjGJQH).",
@@ -283,19 +284,19 @@
"name": "рівень", "name": "рівень",
"description": "Рівень", "description": "Рівень",
"choices": { "choices": {
"tier": "Рівень %s1 - %s2" "tier": "Tier {tier_1} - {tier_2}"
} }
} }
}, },
"periods": { "periods": {
"career": "Кар'єра", "career": "Кар'єра",
"today": "Сьогодні", "today": "Сьогодні",
"days": "%s1 днів", "days": "{days} days",
"custom": "%s1 - %s2 днів" "custom": "{day_1} - {day_2} days"
}, },
"filters": { "filters": {
"none": "Немає фільтрів", "none": "Немає фільтрів",
"tier": "Рівень %s1" "tier": "Tier {tier}"
}, },
"no_data": { "no_data": {
"battles_in_period": "За цей період боїв не було", "battles_in_period": "За цей період боїв не було",
@@ -318,22 +319,6 @@
} }
}, },
"commands": { "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": { "replay": {
"name": "реплей", "name": "реплей",
"description": "Показати результати реплею" "description": "Показати результати реплею"
@@ -354,19 +339,19 @@
"body": { "body": {
"subtitle": "Дослідження", "subtitle": "Дослідження",
"total": "Загалом", "total": "Загалом",
"research": "Дослідження: %s1", "research": "Research: {cost}",
"upgrades": "Покращення: %s1", "upgrades": "Upgrades: {cost}",
"purchase": "Придбання: %s1", "purchase": "Purchase: {cost}",
"equipment": "Спорядження: %s1", "equipment": "Equipment: {cost}",
"estimation": "# Це оцінка!\n\nЦей список може включати вже досліджені танки через неточну інформацію для вантажу. Спробуйте використати \"початковий танк\" з рядка за допомогою команди." "estimation": "# Це оцінка!\n\nЦей список може включати вже досліджені танки через неточну інформацію для вантажу. Спробуйте використати \"початковий танк\" з рядка за допомогою команди."
}, },
"errors": { "errors": {
"non_tech_tree": "# %s1 не в технічному дереві\n\nВи не можете досліджувати преміум/колекційний танк.", "non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# %s1 це рівень I\n\nТанки рівню I завжди відкриті для всіх гравців безкоштовно.", "tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# %s1 уже досліджено!\n\nСпробуйте забезпечити `starting-tank` коли використовуєте команду щоб подивитись підсумки гілки, яку ви вже дослідили.", "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": "# %s1 було надано двічі\n\nПереконайся, що `starting-tank` та `target-tank` різні.", "start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": ".", "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": "" "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."
} }
}, },
"about": { "about": {
@@ -409,7 +394,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "Відсоток перемог", "winrate": "Відсоток перемог",
"damage": "Шкода", "damage": "Шкода",
"unknown_tank": "Невідомий танк %s1" "unknown_tank": "Unknown tank {id}"
} }
}, },
"debug": { "debug": {
@@ -445,12 +430,12 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "поріг", "name": "поріг",
"description": "Кількість днів неактивності (за замовчуванням: %s1 днів)" "description": "The number of days inactive (default: {days} days)"
} }
}, },
"body": { "body": {
"subtitle": "Неактивний %s1+ днів", "subtitle": "Inactive for {days}+ days",
"listing": "%s1 днів" "listing": "{days} days"
} }
}, },
"full_stats": { "full_stats": {
@@ -493,7 +478,7 @@
"body": { "body": {
"accounts_linked": "Прив'язані акаунти", "accounts_linked": "Прив'язані акаунти",
"no_role_permissions": { "no_role_permissions": {
"title": "%s1 не вдалося перевірити", "title": "{name} failed to verify",
"description": "У мене немає дозволу керувати ролями." "description": "У мене немає дозволу керувати ролями."
} }
}, },
@@ -508,11 +493,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "ліміт", "name": "ліміт",
"description": "Максимальна кількість результатів клану (за замовчуванням: %s1)" "description": "The max number of clan results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Clan search for %s1 in region %s2", "title": "Clan search for {search} in region {region}",
"no_results": "Жодного клану не знайдено" "no_results": "Жодного клану не знайдено"
} }
}, },
@@ -522,11 +507,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "ліміт", "name": "ліміт",
"description": "The max number of player results (default: %s1)" "description": "The max number of player results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "Player search for %s1 in region %s2", "title": "Player search for {search} in region {region}",
"no_results": "Гравця не знайдено" "no_results": "Гравця не знайдено"
} }
}, },
@@ -546,13 +531,13 @@
"options": { "options": {
"limit": { "limit": {
"name": "ліміт", "name": "ліміт",
"description": "" "description": "How many neighbors to display (default: {count})"
}, },
"season": { "season": {
"name": "сезон", "name": "сезон",
"description": "Базова інформація про гравця", "description": "Базова інформація про гравця",
"choices": { "choices": {
"current": "%s1 (current)" "current": "{season} (current)"
} }
}, },
"league": { "league": {
@@ -561,12 +546,12 @@
} }
}, },
"body": { "body": {
"top_players": "Top %s1 players", "top_players": "Top {count} 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_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": "# No ongoing season\n\nThere isn't a currently running rating season.", "no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.",
"deleted_player": "Deleted %s1", "deleted_player": "Deleted {id}",
"neighbors": "Rating neighbors", "neighbors": "Rating neighbors",
"more": "+ %s1 more" "more": "+ {count} more"
} }
}, },
"today": { "today": {
@@ -581,14 +566,14 @@
"name": "отримані танки", "name": "отримані танки",
"description": "Показує всі ваші танки даного рівня", "description": "Показує всі ваші танки даного рівня",
"body": { "body": {
"title": "# %s1's owned tier %s2 tanks\n\n" "title": "# {name}'s owned tier {tier} tanks\n\n"
} }
}, },
"permissions": { "permissions": {
"name": "дозволи", "name": "дозволи",
"description": "Checks wether the bot has the required permissions to function", "description": "Checks wether the bot has the required permissions to function",
"body": { "body": {
"title": "# Permissions\n\n%s1", "title": "# Permissions\n\n{body}",
"view_channels": "View channels: needed to find the message the refresh button was used on", "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", "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", "attach_files": "Attach files: needed to add images to image-based commands",
@@ -600,7 +585,7 @@
"description": "Check wether the bot is alive.", "description": "Check wether the bot is alive.",
"body": { "body": {
"pong": "Pong 🏓", "pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - %s1ms" "pong_with_time": "Pong 🏓 - {time}ms"
}, },
"subcommands": { "subcommands": {
"blitzkit": { "blitzkit": {
@@ -617,7 +602,7 @@
"name": "player-info", "name": "player-info",
"description": "Базова інформація про гравця", "description": "Базова інформація про гравця",
"body": { "body": {
"title": "%s1's information", "title": "{name}'s information",
"nickname": "Нік", "nickname": "Нік",
"battles": "Бої", "battles": "Бої",
"winrate": "Відсоток перемог", "winrate": "Відсоток перемог",
@@ -646,7 +631,7 @@
} }
}, },
"errors": { "errors": {
"no_participation": "# %s1 hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.", "no_participation": "# {name} 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." "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."
} }
} }
@@ -658,6 +643,8 @@
"body": "This page has moved. Redirecting..." "body": "This page has moved. Redirecting..."
}, },
"settings": { "settings": {
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "Development mode", "dev_mode": "Development mode",
"language": "Мова" "language": "Мова"
}, },
@@ -671,13 +658,16 @@
"copied": "Скопійовано!" "copied": "Скопійовано!"
}, },
"tank_search": { "tank_search": {
"select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters", "clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Шукайте техніку...", "search_bar_hint": "Шукайте техніку...",
"recent": "Недавно переглянуті", "recent": "Недавно переглянуті",
"select_all": "Вибрати все", "select_all": "Вибрати все",
"count_singular": "1 танк", "count_singular": "1 танк",
"count_plural": "%s1 танків", "count_plural": "{count} tanks",
"sorting_by": "Сортувати за %s1", "sorting_by": "Sorting by {name}",
"no_tanks_found": { "no_tanks_found": {
"body": "Нічого не знайдено.", "body": "Нічого не знайдено.",
"search_again": "Спробуйте шукати заново.", "search_again": "Спробуйте шукати заново.",
@@ -739,22 +729,30 @@
} }
}, },
"home": { "home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": { "discord": {
"join_short": "Join cool people!", "join_short": "Join cool people!",
"join_long": "Join super duper cool people!", "join_long": "Join super duper cool people!",
"action": "Discord" "action": "Discord"
}, },
"hero": { "hero": {
"credit": "Зроблено %s1 ❤" "credit": "By {name} ❤️"
}, },
"hot_tanks": { "hot_tanks": {
"title": "Популярні танки", "title": "Популярні танки",
"subtitle": "Останні %s1 дні(в)" "subtitle": "Last {days} days"
}, },
"other_projects": { "other_projects": {
"title": "Інші класні проєкти" "title": "Інші класні проєкти"
}, },
"help_translate": "Будь ласка, допоможіть перекласти %s1 на %s2!", "help_translate": "Please help translate {name} to {language}!",
"seo_description": "Інструменти для всього, пов'язаного з WoT Blitz" "seo_description": "Інструменти для всього, пов'язаного з WoT Blitz"
}, },
"docs": { "docs": {
@@ -764,6 +762,14 @@
} }
}, },
"tools": { "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": { "guess": {
"name": "Guess the tank", "name": "Guess the tank",
"button": "Guess away", "button": "Guess away",
@@ -776,7 +782,7 @@
"next": "Next", "next": "Next",
"no_results": "Немає результатів" "no_results": "Немає результатів"
}, },
"stats": "%s1 / %s2 guessed correctly" "stats": "{correct} / {total} guessed correctly"
}, },
"tankopedia": { "tankopedia": {
"search": "Search a stat...", "search": "Search a stat...",
@@ -784,9 +790,12 @@
"button": "Знайти танки", "button": "Знайти танки",
"description": "Танкова енциклопедія", "description": "Танкова енциклопедія",
"seo_description": "Характеристики, броня та інше для всіх танків WoT Blitz", "seo_description": "Характеристики, броня та інше для всіх танків WoT Blitz",
"tank_seo_title": "%s1 - Рівень %s2 %s3 %s4 | %s5 Танкопедія | WoT Blitz", "tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 броня, характеристики, обладнання, амуніція та більше. %s2 шкода на хвилину, %s3 разова шкода, %s4 мм пробиття, %s5 км/год максимальна швидкість, %s6 міцність.", "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.",
"visualizers": { "visualizers": {
"reload": {
"skip": "Skip"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -807,7 +816,6 @@
}, },
"view_range": { "view_range": {
"camo": "Camo", "camo": "Camo",
"percentage": "%s1%",
"ghost_factory": "Ghost Factory", "ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail" "dead_rail": "Dead Rail"
}, },
@@ -829,9 +837,7 @@
"green_penetration": "Зелене пробиття", "green_penetration": "Зелене пробиття",
"hide_model_under_armor": "Hide model under armor", "hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque", "opaque": "Opaque",
"dev_wireframe": "DEV: wireframe", "dev_wireframe": "DEV: wireframe"
"environment": "Environment",
"show_grid": "Show grid"
}, },
"screenshot": { "screenshot": {
"download": "Завантажити", "download": "Завантажити",
@@ -853,7 +859,6 @@
"reset": "Reset" "reset": "Reset"
}, },
"advanced": "Advanced highlighting", "advanced": "Advanced highlighting",
"shooter": "Shooter:",
"shot_card": { "shot_card": {
"element": { "element": {
"primary": "Primary", "primary": "Primary",
@@ -864,15 +869,15 @@
"track": "Tracks" "track": "Tracks"
}, },
"status": { "status": {
"penetration": "%s1hp penetration", "penetration": "{damage}hp penetration",
"blocked": "Blocked", "blocked": "Blocked",
"ricochet": "Рикошет", "ricochet": "Рикошет",
"splash": "%s1hp splash" "splash": "{damage}hp splash"
}, },
"stats": { "stats": {
"thickness_and_angle": "%s1mm %s2°", "thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "%s1mm nominal", "nominal": "{thickness}mm nominal",
"ricochet_loss": "%s1% penetration" "ricochet_loss": "{loss}% penetration"
} }
} }
}, },
@@ -910,15 +915,15 @@
"tech_tree": { "tech_tree": {
"title": "Звичайний", "title": "Звичайний",
"battle": "1 бій", "battle": "1 бій",
"battles": "%s1 battles", "battles": "{count} battles",
"route": "Route %s1 of %s2" "route": "Route {index} of {count}"
}, },
"game_modes": { "game_modes": {
"title": "Game mode abilities" "title": "Game mode abilities"
}, },
"review": { "review": {
"title": "Review videos", "title": "Review videos",
"join": "Apply to %s1 YouTuber program" "join": "Apply to {name} YouTuber program"
}, },
"configuration": { "configuration": {
"title": "Configuration", "title": "Configuration",
@@ -954,15 +959,15 @@
"title": "Characteristics", "title": "Characteristics",
"compare": { "compare": {
"label": "Compare against", "label": "Compare against",
"class": "Tier %s1 %s2", "class": "Tier {tier} {class}",
"tier": "All of tier %s1", "tier": "All of tier {tier}",
"all": "Усі танки" "all": "Усі танки"
}, },
"values": { "values": {
"shell_index": "Снаряд %s1", "shell_index": "Shell {index}",
"at_distance": "At %s1m", "at_distance": "At {distance}m",
"at_distance_slider_label": "Відстань", "at_distance_slider_label": "Відстань",
"dispersion_at_distance": "Dispersion at %s1m", "dispersion_at_distance": "Dispersion at {distance}m",
"gun_flexibility": "Gun flexibility", "gun_flexibility": "Gun flexibility",
"speed": "Швидкість", "speed": "Швидкість",
"terrain_coefficients": "Terrain coefficients", "terrain_coefficients": "Terrain coefficients",
@@ -1111,9 +1116,9 @@
"description": "Tank performance statistics", "description": "Tank performance statistics",
"seo_description": "Best tanks and their performances in World of Tanks Blitz", "seo_description": "Best tanks and their performances in World of Tanks Blitz",
"estimation": { "estimation": {
"body": "Estimated stats based on %s1 players. Updated %s2 ago.", "body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "%s1 hours", "hours": "{count} hours",
"minutes": "%s1 minutes" "minutes": "{count} minutes"
}, },
"player_count": { "player_count": {
"label": "Player count" "label": "Player count"
@@ -1145,10 +1150,10 @@
"button": "Browse all", "button": "Browse all",
"description": "Blitz player avatars", "description": "Blitz player avatars",
"seo_description": "All World of Tanks Blitz player avatars", "seo_description": "All World of Tanks Blitz player avatars",
"last_updated": "Last updated on %s1", "last_updated": "Last updated on {time}",
"search": { "search": {
"hint": "Search avatars...", "hint": "Search avatars...",
"results": "%s1 avatars" "results": "{count} avatars"
} }
}, },
"session": { "session": {
@@ -1163,8 +1168,8 @@
"no_results": "Гравця не знайдено" "no_results": "Гравця не знайдено"
}, },
"controls": { "controls": {
"title": "Відстежуємо %s1", "title": "Tracking {name}",
"subtitle": "Від %s1", "subtitle": "Since {time}",
"reset": "Скинути", "reset": "Скинути",
"change": "Змінити" "change": "Змінити"
}, },
@@ -1224,7 +1229,7 @@
"cancel": "Cancel", "cancel": "Cancel",
"success": { "success": {
"title": "Copy URL", "title": "Copy URL",
"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\".", "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\".",
"back": "Назад", "back": "Назад",
"copy": "Copy URL" "copy": "Copy URL"
} }
@@ -1307,17 +1312,11 @@
"description": "Get started with BlitzKit", "description": "Get started with BlitzKit",
"seo_description": "Get started with BlitzKit" "seo_description": "Get started with BlitzKit"
}, },
"blitz_analysis": { "api": {
"name": "BlitzAnalysis[]", "name": "API",
"button": "Analyze now", "button": "To the docs",
"description": "Rich historic average stats", "description": "BlitzKit hosted API",
"seo_description": "Rich historic average stats in World of Tanks Blitz" "seo_description": "BlitzKit hosted static API for 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": { "footer": {
+106 -107
View File
@@ -1,18 +1,19 @@
{ {
"test": { "test": {
"default": "测试", "default": "测试",
"long": "这是一个测试用长字符串。你不会在用户界面中看见这些。我最喜欢的水果是%s1因为它的颜色是%s2。我如此喜欢它以至于我会把它的名字念3次!%s1!%s1!%s1!重申一次,它是%s2而不是%s3。", "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}.",
"fox": "褐色的狐狸迅速地跳过那只懒狗" "fox": "褐色的狐狸迅速地跳过那只懒狗"
}, },
"common": { "common": {
"name": "BlitzKit",
"units": { "units": {
"m": "%s1公尺", "m": "{value}m",
"mm": "%s1毫米", "mm": "{value}mm",
"deg": "%s1度", "deg": "{value}°",
"hp": "%s1生命值", "hp": "{value}hp",
"x": "x%s1", "x": "x{value}",
"percentage": "%s1%", "percentage": "{value}%",
"deg_s": "%s1度/秒" "deg_s": "{value}°/s"
}, },
"gun_types": { "gun_types": {
"auto_loader": "弹夹型战车", "auto_loader": "弹夹型战车",
@@ -185,11 +186,11 @@
"bot": { "bot": {
"common": { "common": {
"errors": { "errors": {
"clan_not_found": "# 找不到公会\n找不到\"%s1\"。请尝试从搜索结果中选择一个公会。", "clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.",
"player_not_found": "# 找不到玩家\n无法找到玩家 “%s1”。 尝试从搜索结果中选择玩家,或输入一个有效的玩家名称, 或使用“/link”命令。", "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.",
"player_not_linked": "# 使用 `/link` 来开始\n我不知道你是谁,但你今天可以告诉我!", "player_not_linked": "# 使用 `/link` 来开始\n我不知道你是谁,但你今天可以告诉我!",
"tank_name_not_found": "# 找不到战车\n找不到名为 “%s1 ”的战车。", "tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".",
"tank_id_not_found": "# 找不到战车\n无法找到ID为 “%s1”的战车。 请尝试输入战车的名称而不是数字。", "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.",
"no_tank_stats": "# 没有这辆战车的数据\n这个玩家没有可用资料。 这可能不是你要找的玩家。", "no_tank_stats": "# 没有这辆战车的数据\n这个玩家没有可用资料。 这可能不是你要找的玩家。",
"get_help": "Discord的协助服务器", "get_help": "Discord的协助服务器",
"uncaught_error": "# BlitzKit 发生错误!\n我们对此感到抱歉。 欢迎加入[官方Discord](https://discord.gg/nDt7AjGJQH),以方便帮助你解决问题。", "uncaught_error": "# BlitzKit 发生错误!\n我们对此感到抱歉。 欢迎加入[官方Discord](https://discord.gg/nDt7AjGJQH),以方便帮助你解决问题。",
@@ -283,19 +284,19 @@
"name": "阶级", "name": "阶级",
"description": "阶级", "description": "阶级",
"choices": { "choices": {
"tier": "%s1阶 - %s2" "tier": "Tier {tier_1} - {tier_2}"
} }
} }
}, },
"periods": { "periods": {
"career": "玩家资料", "career": "玩家资料",
"today": "今日:", "today": "今日:",
"days": "%s1 天", "days": "{days} days",
"custom": "%s1 - %s2 天" "custom": "{day_1} - {day_2} days"
}, },
"filters": { "filters": {
"none": "无筛选器", "none": "无筛选器",
"tier": "%s1 阶级" "tier": "Tier {tier}"
}, },
"no_data": { "no_data": {
"battles_in_period": "在这段时间里没有参与任何战斗", "battles_in_period": "在这段时间里没有参与任何战斗",
@@ -318,22 +319,6 @@
} }
}, },
"commands": { "commands": {
"auction": {
"name": "拍卖",
"description": "目前拍卖中的战车",
"body": {
"title": "Blitz 拍卖会",
"subtitle": "%s1 可用 • 下一个价格 %s2\n现在出价: %s3",
"next": "下一个价格: %s1",
"available": "可用: %s1 / %s2"
},
"errors": {
"no_auction": "# 目前没有正在进行中的拍卖会\n\n拍卖中没有可供购买的战车。 如果您认为这是一个错误,请在[官方的 Discord服务器](https://discord.gg/nDt7AjGJQH)中报告这个问题。"
},
"actions": {
"bid": "立刻购买"
}
},
"replay": { "replay": {
"name": "回放", "name": "回放",
"description": "显示重播结果" "description": "显示重播结果"
@@ -354,19 +339,19 @@
"body": { "body": {
"subtitle": "研发", "subtitle": "研发",
"total": "全部", "total": "全部",
"research": "研发:%s1", "research": "Research: {cost}",
"upgrades": "升级:%s1", "upgrades": "Upgrades: {cost}",
"purchase": "购买:%s1", "purchase": "Purchase: {cost}",
"equipment": "装备:%s1", "equipment": "Equipment: {cost}",
"estimation": "# 这是一个估计值!\n\n这个列表可能包含已经研发过的战车,因为 Wargaming 提供了不正确的资料。 如果可以,请尝试在同一研发线路中提供一个 “起始战车” 。" "estimation": "# 这是一个估计值!\n\n这个列表可能包含已经研发过的战车,因为 Wargaming 提供了不正确的资料。 如果可以,请尝试在同一研发线路中提供一个 “起始战车” 。"
}, },
"errors": { "errors": {
"non_tech_tree": "# \"%s1\" 不在科技树里\n\n你不能研究一个 加值/收藏家 战车。", "non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
"tier_1": "# “%s1”是一阶战车\n\n一阶战车是可免费解锁的。", "tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
"already_researched": "# \"%s1\" 已完成研发\n\n请尝试在同一研发线路上提供“起始战车”, 以查看全部您已研发过的战车。", "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": "# \"%s1\" 重复提供了\n\n请确认 \"初始战车\" 和 \"目标战车\" 是不同的。", "start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
"tanks_not_on_line": "# “%s1” 和 “%s2” 不在同一个研发线路上\n\n请确认“起始战车” 和 “目标战车” 在同一研发线路中。", "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": "# “%s1” 阶级比 “%s2” 高\n\n请以 %s3 作为起始战车,因为它的阶级更低。" "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."
} }
}, },
"about": { "about": {
@@ -409,7 +394,7 @@
"wn8": "WN8", "wn8": "WN8",
"winrate": "胜率", "winrate": "胜率",
"damage": "伤害", "damage": "伤害",
"unknown_tank": "未知战车 %s1" "unknown_tank": "Unknown tank {id}"
} }
}, },
"debug": { "debug": {
@@ -445,12 +430,12 @@
"options": { "options": {
"threshold": { "threshold": {
"name": "阈值", "name": "阈值",
"description": "未上线天数( 默认:%s1 天)" "description": "The number of days inactive (default: {days} days)"
} }
}, },
"body": { "body": {
"subtitle": "%s1+ 天以上未上线", "subtitle": "Inactive for {days}+ days",
"listing": "%s1 天" "listing": "{days} days"
} }
}, },
"full_stats": { "full_stats": {
@@ -493,7 +478,7 @@
"body": { "body": {
"accounts_linked": "帐号已绑定", "accounts_linked": "帐号已绑定",
"no_role_permissions": { "no_role_permissions": {
"title": "%s1 验证失败", "title": "{name} failed to verify",
"description": "我没有管理身份的权限。" "description": "我没有管理身份的权限。"
} }
}, },
@@ -508,11 +493,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "限制", "name": "限制",
"description": "公会结果的最大数量 (默认:%s1)" "description": "The max number of clan results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "在 %s2 地区搜索 %s1 公会的结果", "title": "Clan search for {search} in region {region}",
"no_results": "没有找到该公会" "no_results": "没有找到该公会"
} }
}, },
@@ -522,11 +507,11 @@
"options": { "options": {
"limit": { "limit": {
"name": "限制", "name": "限制",
"description": "最大玩家结果数 (默认:%s1)" "description": "The max number of player results (default: {limit})"
} }
}, },
"body": { "body": {
"title": "在 %s2 地区搜索 %s1 玩家的结果", "title": "Player search for {search} in region {region}",
"no_results": "未找到玩家" "no_results": "未找到玩家"
} }
}, },
@@ -546,13 +531,13 @@
"options": { "options": {
"limit": { "limit": {
"name": "限制", "name": "限制",
"description": "显示附近有多少玩家(默认:%s1)" "description": "How many neighbors to display (default: {count})"
}, },
"season": { "season": {
"name": "赛季", "name": "赛季",
"description": "赛季数 (默认:当前)", "description": "赛季数 (默认:当前)",
"choices": { "choices": {
"current": "%s1 (当前)" "current": "{season} (current)"
} }
}, },
"league": { "league": {
@@ -561,12 +546,12 @@
} }
}, },
"body": { "body": {
"top_players": "前 %s1 玩家", "top_players": "Top {count} players",
"no_participation": "# %s1 在赛季 %s2 没有进行过评价战斗\n\n该玩家没有参加该赛季或没有完成校准。", "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": "# 没有进行中的赛季\n\n目前没有赛季。", "no_ongoing_season": "# 没有进行中的赛季\n\n目前没有赛季。",
"deleted_player": "已删除玩家 %s1", "deleted_player": "Deleted {id}",
"neighbors": "对邻居进行评分", "neighbors": "对邻居进行评分",
"more": "+ %s1 更多" "more": "+ {count} more"
} }
}, },
"today": { "today": {
@@ -581,14 +566,14 @@
"name": "已拥有的战车", "name": "已拥有的战车",
"description": "显示该阶级中所有您已拥有的战车", "description": "显示该阶级中所有您已拥有的战车",
"body": { "body": {
"title": "# %s1 所拥有的 %s2 阶战车\n\n" "title": "# {name}'s owned tier {tier} tanks\n\n"
} }
}, },
"permissions": { "permissions": {
"name": "权限", "name": "权限",
"description": "检查机器人是否拥有运行所需的权限", "description": "检查机器人是否拥有运行所需的权限",
"body": { "body": {
"title": "# 权限\n\n%s1", "title": "# Permissions\n\n{body}",
"view_channels": "查看频道:需要找到重新整理按钮使用的讯息", "view_channels": "查看频道:需要找到重新整理按钮使用的讯息",
"read_message_history": "读取消息历史: 需要找到消息以刷新", "read_message_history": "读取消息历史: 需要找到消息以刷新",
"attach_files": "附加文件:需要将图像添加到基于图像的命令", "attach_files": "附加文件:需要将图像添加到基于图像的命令",
@@ -600,7 +585,7 @@
"description": "检查机器人是否处于活动状态。", "description": "检查机器人是否处于活动状态。",
"body": { "body": {
"pong": "Pong 🏓", "pong": "Pong 🏓",
"pong_with_time": "Pong 🏓 - %s1ms" "pong_with_time": "Pong 🏓 - {time}ms"
}, },
"subcommands": { "subcommands": {
"blitzkit": { "blitzkit": {
@@ -617,7 +602,7 @@
"name": "玩家信息", "name": "玩家信息",
"description": "玩家基本信息", "description": "玩家基本信息",
"body": { "body": {
"title": "%s1的信息", "title": "{name}'s information",
"nickname": "昵称", "nickname": "昵称",
"battles": "战斗场次", "battles": "战斗场次",
"winrate": "胜率", "winrate": "胜率",
@@ -646,7 +631,7 @@
} }
}, },
"errors": { "errors": {
"no_participation": "# %s1 未完成评价\n\n该玩家未参与本赛季的评价战斗。", "no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.",
"unrecorded_stats": "# 统计数据目前无法使用\n\nWargaming 可能暂停了服务器。 如果有连续的赛季,统计数据可能在24小时内提供。" "unrecorded_stats": "# 统计数据目前无法使用\n\nWargaming 可能暂停了服务器。 如果有连续的赛季,统计数据可能在24小时内提供。"
} }
} }
@@ -658,6 +643,8 @@
"body": "此页面已被移动。正在重定向……" "body": "此页面已被移动。正在重定向……"
}, },
"settings": { "settings": {
"title": "Settings",
"description": "Change BlitzKit settings",
"dev_mode": "开发者模式", "dev_mode": "开发者模式",
"language": "语言" "language": "语言"
}, },
@@ -671,13 +658,16 @@
"copied": "已拷贝!" "copied": "已拷贝!"
}, },
"tank_search": { "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_bar_hint": "搜寻战车……",
"recent": "最近查看的", "recent": "最近查看的",
"select_all": "全选", "select_all": "全选",
"count_singular": "1辆战车", "count_singular": "1辆战车",
"count_plural": "%s1辆战车", "count_plural": "{count} tanks",
"sorting_by": "按 %s1 排序", "sorting_by": "Sorting by {name}",
"no_tanks_found": { "no_tanks_found": {
"body": "找不到战车。", "body": "找不到战车。",
"search_again": "请尝试重新搜索。", "search_again": "请尝试重新搜索。",
@@ -739,22 +729,30 @@
} }
}, },
"home": { "home": {
"pwa": {
"title": "Install the BlitzKit App",
"new": "NEW",
"checking": "Checking compatibility...",
"instructions": "Instructions for Apple devices.",
"install": "Install",
"dismiss": "Dismiss"
},
"discord": { "discord": {
"join_short": "加入超級酷的人群!", "join_short": "加入超級酷的人群!",
"join_long": "加入我們的Discord社群,和超酷的人一起聊天!", "join_long": "加入我們的Discord社群,和超酷的人一起聊天!",
"action": "Discord" "action": "Discord"
}, },
"hero": { "hero": {
"credit": "由 %s1 制作 ❤️" "credit": "By {name} ❤️"
}, },
"hot_tanks": { "hot_tanks": {
"title": "受欢迎的坦克", "title": "受欢迎的坦克",
"subtitle": "过去 %s1 天" "subtitle": "Last {days} days"
}, },
"other_projects": { "other_projects": {
"title": "其他酷炫的项目" "title": "其他酷炫的项目"
}, },
"help_translate": "请帮忙将 %s1 翻译成 %s2!", "help_translate": "Please help translate {name} to {language}!",
"seo_description": "战车世界闪击战 全方位工具" "seo_description": "战车世界闪击战 全方位工具"
}, },
"docs": { "docs": {
@@ -764,6 +762,14 @@
} }
}, },
"tools": { "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": { "guess": {
"name": "猜坦克", "name": "猜坦克",
"button": "猜猜看", "button": "猜猜看",
@@ -776,7 +782,7 @@
"next": "下一个", "next": "下一个",
"no_results": "沒有結果" "no_results": "沒有結果"
}, },
"stats": "猜对了 %s1 / %s2" "stats": "{correct} / {total} guessed correctly"
}, },
"tankopedia": { "tankopedia": {
"search": "搜索属性……", "search": "搜索属性……",
@@ -784,9 +790,12 @@
"button": "搜索战车", "button": "搜索战车",
"description": "闪击战战车百科全书", "description": "闪击战战车百科全书",
"seo_description": "战车世界闪击战的所有坦克参数, 装甲, 等等...", "seo_description": "战车世界闪击战的所有坦克参数, 装甲, 等等...",
"tank_seo_title": "%s1 - %s2 阶 %s3 %s4 | %s5 战车百科 | 战车世界闪击战 (WoTB)", "tank_seo_title": "{name} - Tier {tier} {nationality} {class} | {website} Tankopedia | World of Tanks Blitz (WoTB)",
"tank_seo_description": "%s1 装甲,统计设备,装备,供给等。 %s2 每分钟伤害, %s3生命值, %s4毫米穿透, %s5kph 最高速度, %s6hp 生命值。", "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.",
"visualizers": { "visualizers": {
"reload": {
"skip": "跳过"
},
"assault": { "assault": {
"title": "Assault shells" "title": "Assault shells"
}, },
@@ -807,7 +816,6 @@
}, },
"view_range": { "view_range": {
"camo": "迷彩", "camo": "迷彩",
"percentage": "%s1%",
"ghost_factory": "幽灵工厂", "ghost_factory": "幽灵工厂",
"dead_rail": "死亡铁道" "dead_rail": "死亡铁道"
}, },
@@ -829,9 +837,7 @@
"green_penetration": "可击穿区域", "green_penetration": "可击穿区域",
"hide_model_under_armor": "隐藏装甲下的模型", "hide_model_under_armor": "隐藏装甲下的模型",
"opaque": "不透明", "opaque": "不透明",
"dev_wireframe": "开发选项:网格", "dev_wireframe": "开发选项:网格"
"environment": "环境",
"show_grid": "显示网格"
}, },
"screenshot": { "screenshot": {
"download": "下载", "download": "下载",
@@ -853,7 +859,6 @@
"reset": "重置" "reset": "重置"
}, },
"advanced": "进阶高亮显示", "advanced": "进阶高亮显示",
"shooter": "主视角射击者",
"shot_card": { "shot_card": {
"element": { "element": {
"primary": "主装甲", "primary": "主装甲",
@@ -864,15 +869,15 @@
"track": "履带" "track": "履带"
}, },
"status": { "status": {
"penetration": "%s1生命值 击穿", "penetration": "{damage}hp penetration",
"blocked": "抵挡伤害", "blocked": "抵挡伤害",
"ricochet": "跳弹", "ricochet": "跳弹",
"splash": "%s1生命值 溅射" "splash": "{damage}hp splash"
}, },
"stats": { "stats": {
"thickness_and_angle": "等效%s1毫米 %s2度", "thickness_and_angle": "{thickness}mm {angle}°",
"nominal": "%s1毫米 装甲厚度", "nominal": "{thickness}mm nominal",
"ricochet_loss": "%s1%概率击穿" "ricochet_loss": "{loss}% penetration"
} }
} }
}, },
@@ -910,15 +915,15 @@
"tech_tree": { "tech_tree": {
"title": "科技树", "title": "科技树",
"battle": "一场比赛", "battle": "一场比赛",
"battles": "%s1场比赛", "battles": "{count} battles",
"route": "路线 %s1 / %s2" "route": "Route {index} of {count}"
}, },
"game_modes": { "game_modes": {
"title": "游戏模式技能" "title": "游戏模式技能"
}, },
"review": { "review": {
"title": "回顾影片", "title": "回顾影片",
"join": "应用到 %s1 YouTuber程序" "join": "Apply to {name} YouTuber program"
}, },
"configuration": { "configuration": {
"title": "配置设定", "title": "配置设定",
@@ -954,15 +959,15 @@
"title": "属性", "title": "属性",
"compare": { "compare": {
"label": "比较", "label": "比较",
"class": "%s1阶 %s2", "class": "Tier {tier} {class}",
"tier": "所有 %s1 阶", "tier": "All of tier {tier}",
"all": "所有战车" "all": "所有战车"
}, },
"values": { "values": {
"shell_index": "炮弹 %s1", "shell_index": "Shell {index}",
"at_distance": "在 %s1 公尺", "at_distance": "At {distance}m",
"at_distance_slider_label": "距离", "at_distance_slider_label": "距离",
"dispersion_at_distance": "%s1 公尺扩散度", "dispersion_at_distance": "Dispersion at {distance}m",
"gun_flexibility": "炮管转动界限", "gun_flexibility": "炮管转动界限",
"speed": "速度", "speed": "速度",
"terrain_coefficients": "地形系数", "terrain_coefficients": "地形系数",
@@ -1111,9 +1116,9 @@
"description": "战车性能统计", "description": "战车性能统计",
"seo_description": "最好的战车及其在战车世界闪击战中的表现", "seo_description": "最好的战车及其在战车世界闪击战中的表现",
"estimation": { "estimation": {
"body": "基于 %s1 个玩家的估计数据。更新于 %s2 之前。", "body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "%s1 小时", "hours": "{count} hours",
"minutes": "%s1 分钟" "minutes": "{count} minutes"
}, },
"player_count": { "player_count": {
"label": "玩家数量" "label": "玩家数量"
@@ -1145,10 +1150,10 @@
"button": "浏览全部", "button": "浏览全部",
"description": "Blitz玩家头像", "description": "Blitz玩家头像",
"seo_description": "所有战车世界闪击战玩家头像", "seo_description": "所有战车世界闪击战玩家头像",
"last_updated": "最后更新于 %s1", "last_updated": "Last updated on {time}",
"search": { "search": {
"hint": "搜索頭像……", "hint": "搜索頭像……",
"results": "%s1 头像" "results": "{count} avatars"
} }
}, },
"session": { "session": {
@@ -1163,8 +1168,8 @@
"no_results": "找不到玩家" "no_results": "找不到玩家"
}, },
"controls": { "controls": {
"title": "追踪 %s1", "title": "Tracking {name}",
"subtitle": "起始于 %s1", "subtitle": "Since {time}",
"reset": "重置", "reset": "重置",
"change": "变更" "change": "变更"
}, },
@@ -1224,7 +1229,7 @@
"cancel": "取消", "cancel": "取消",
"success": { "success": {
"title": "拷贝 URL", "title": "拷贝 URL",
"info": "已为 %s1 生成 URL。 当 URL 粘贴后,嵌入功能将开始追踪。 您可以随时在 PC 上点击右键或手机上长按并选择「重置」来重置。", "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\".",
"back": "返回", "back": "返回",
"copy": "拷贝 URL" "copy": "拷贝 URL"
} }
@@ -1307,17 +1312,11 @@
"description": "开始使用 BlitzKit", "description": "开始使用 BlitzKit",
"seo_description": "开始使用 BlitzKit" "seo_description": "开始使用 BlitzKit"
}, },
"blitz_analysis": { "api": {
"name": "闪击战分析", "name": "API",
"button": "立即分析", "button": "To the docs",
"description": "丰富的历史平均统计", "description": "BlitzKit hosted API",
"seo_description": "战车世界闪击战中丰富的历史平均统计" "seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
},
"blitz_insider": {
"name": "闪击战内嵌插件",
"button": "开始探索",
"description": "游戏内场次统计",
"seo_description": "战车世界闪击战内的游戏内场次统计"
} }
}, },
"footer": { "footer": {
+8 -7
View File
@@ -11,20 +11,21 @@
"build:ids": "bun src/discoverIds.ts", "build:ids": "bun src/discoverIds.ts",
"build:bot": "bun src/buildBot.ts", "build:bot": "bun src/buildBot.ts",
"build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts", "build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts",
"build:proxyclient": "bun src/buildProxyClient.ts",
"prunestrings": "bun src/pruneStrings.ts" "prunestrings": "bun src/pruneStrings.ts"
}, },
"dependencies": { "dependencies": {
"@blitzkit/core": "workspace:*", "@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*", "@blitzkit/i18n": "workspace:*",
"@gltf-transform/core": "^4.0.10", "@gltf-transform/core": "^4.2.1",
"@gltf-transform/extensions": "^4.0.10", "@gltf-transform/extensions": "^4.2.1",
"@gltf-transform/functions": "^4.0.10", "@gltf-transform/functions": "^4.2.1",
"csv-parse": "^5.5.6", "csv-parse": "^6.1.0",
"decompress": "^4.2.1", "decompress": "^4.2.1",
"esbuild": "^0.19.12", "esbuild": "^0.25.10",
"fast-xml-parser": "^4.4.1", "fast-xml-parser": "^5.2.5",
"progress": "^2.0.3", "progress": "^2.0.3",
"puppeteer": "^23.10.4" "puppeteer": "^24.22.3"
}, },
"devDependencies": { "devDependencies": {
"@types/decompress": "^4.2.7", "@types/decompress": "^4.2.7",
@@ -1,11 +1,11 @@
import { BLITZKIT_TANK_ICON_SIZE, fetchTankDefinitions } from '@blitzkit/core'; import { BLITZKIT_TANK_ICON_SIZE, fetchTankDefinitions } from "@blitzkit/core";
import { launch } from 'puppeteer'; import { launch } from "puppeteer";
import sharp from 'sharp'; import sharp from "sharp";
import { commitAssets } from '../core/github/commitAssets'; import { commitAssets } from "../core/github/commitAssets";
import { FileChange } from '../core/github/commitMultipleFiles'; import { FileChange } from "../core/github/commitMultipleFiles";
export async function blitzkitTankIcons() { export async function blitzkitTankIcons() {
console.log('Building blitzkit tank icons...'); console.log("Building blitzkit tank icons...");
const browser = await launch(); const browser = await launch();
const page = await browser.newPage(); const page = await browser.newPage();
@@ -21,7 +21,7 @@ export async function blitzkitTankIcons() {
try { try {
await page.goto(`http://localhost:4321/api/tankopedia/tank-icon/${id}/`, { await page.goto(`http://localhost:4321/api/tankopedia/tank-icon/${id}/`, {
waitUntil: 'networkidle0', waitUntil: "networkidle0",
}); });
} catch (error) { } catch (error) {
console.error(`${id} ${name} time out`, error); console.error(`${id} ${name} time out`, error);
@@ -29,21 +29,21 @@ export async function blitzkitTankIcons() {
} }
const screenshot = await page.screenshot({ const screenshot = await page.screenshot({
type: 'webp', type: "webp",
omitBackground: true, omitBackground: true,
}); });
const content = await sharp(screenshot).trim({ threshold: 100 }).toBuffer(); const content = await sharp(screenshot).trim({ threshold: 100 }).toBuffer();
index++; index++;
console.log( console.log(
`Rendered ${id} (${name}) ${((index / tanks.length) * 100).toFixed(2)}%`, `Rendered ${id} (${name}) ${((index / tanks.length) * 100).toFixed(2)}%`
); );
files.push({ path: `icons/tanks/blitzkit/${id}.webp`, content }); files.push({ path: `icons/tanks/big/${id}.webp`, content });
} }
await page.close(); await page.close();
await browser.close(); await browser.close();
await commitAssets('tank icons', files); await commitAssets("tank icons", files);
} }
File diff suppressed because it is too large. Load diff
+25 -23
View File
@@ -1,50 +1,52 @@
import { toUniqueId } from '@blitzkit/core'; import { toUniqueId } from "@blitzkit/core";
import { NodeIO } from '@gltf-transform/core'; import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from '@gltf-transform/extensions'; import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { readdir } from 'fs/promises'; import { readdir } from "fs/promises";
import { extractModel } from '../core/blitz/extractModel'; import { extractModel } from "../core/blitz/extractModel";
import { readXMLDVPL } from '../core/blitz/readXMLDVPL'; import { readXMLDVPL } from "../core/blitz/readXMLDVPL";
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL'; import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL";
import { commitAssets } from '../core/github/commitAssets'; import { commitAssets } from "../core/github/commitAssets";
import { FileChange } from '../core/github/commitMultipleFiles'; import { FileChange } from "../core/github/commitMultipleFiles";
import { DATA } from './constants'; import { DATA } from "./constants";
import { VehicleDefinitionList } from './definitions'; import { VehicleDefinitionList } from "./definitions";
import { TankParameters } from './tankIcons'; import { TankParameters } from "./tankIcons";
export async function tankModels() { export async function tankModels() {
console.log('Building tank models...'); console.log("Building tank models...");
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS); const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then( const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
(nations) => nations.filter((nation) => nation !== 'common'), (nations) => nations.filter((nation) => nation !== "common")
); );
for (const nationIndex in nations) { for (const nationIndex in nations) {
const nation = nations[nationIndex]; const nation = nations[nationIndex];
const changes: FileChange[] = []; const changes: FileChange[] = [];
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>( const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`, `${DATA}/XML/item_defs/vehicles/${nation}/list.xml`
); );
const entries = Object.entries(tanks.root);
console.log(`Building models for ${nation}`); console.log(`Building models for ${nation}`);
for (const [tankKey, tank] of Object.entries(tanks.root)) { let index = 0;
if (tankKey.includes('tutorial_bot')) continue; for (const [tankKey, tank] of entries) {
console.log(` ${++index} / ${entries.length} : ${tankKey}`);
if (tankKey.includes("tutorial_bot")) continue;
const id = toUniqueId(nation, tank.id); const id = toUniqueId(nation, tank.id);
const parameters = await readYAMLDVPL<TankParameters>( const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`, `${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
); );
const model = await extractModel( const model = await extractModel(
DATA, DATA,
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''), parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, "")
); );
const content = Buffer.from(await nodeIO.writeBinary(model));
changes.push({ changes.push({ path: `3d/tanks/models/${id}.glb`, content });
path: `3d/tanks/models/${id}.glb`,
content: Buffer.from(await nodeIO.writeBinary(model)),
});
} }
await commitAssets(`tank models ${nation}`, changes); await commitAssets(`tank models ${nation}`, changes);
+45
View File
@@ -0,0 +1,45 @@
import { readFile, writeFile } from "fs/promises";
const IMPORT = "@protos/auto_legacy_proxy_client";
const SOURCE =
"../../submodules/blitzkit-closed/protos/auto_legacy_proxy_client.proto";
const TARGET =
"../../submodules/blitzkit-closed/src/unreal/methodPopulatedProxyClient.ts";
const requestPattern = /message (\w+)Request {/g;
const source = await readFile(SOURCE).then((buffer) => buffer.toString());
const requests = source.matchAll(requestPattern);
const imports: string[] = [];
const methods: { name: string; method: string }[] = [];
for (const match of requests) {
const name = match[1];
// const method =
// name.charAt(0).toLowerCase() + name.slice(1).replace(/Async$/, "");
const method = name;
imports.push(`${name}Request`, `${name}Response`);
methods.push({ name, method });
}
let content = "";
content += "import {\n";
content += imports.map((i) => ` ${i},`).join("\n");
content += `\n} from "${IMPORT}";\n`;
content += 'import { BaseProxyClient } from "./baseProxyClient";\n\n';
content +=
"export abstract class MethodPopulatedProxyClient extends BaseProxyClient {\n";
content += methods
.map(({ name, method }) => {
return ` ${method}(data: ${name}Request) {
return this.request("${name}Request", ${name}Request, data, ${name}Response);
}\n`;
})
.join("");
content += "}\n";
await writeFile(TARGET, content);
@@ -1,13 +1,13 @@
import { VertexAttribute, vertexAttributeVectorSizes } from '@blitzkit/core'; import { VertexAttribute, vertexAttributeVectorSizes } from "@blitzkit/core";
export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> = export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> =
{ {
[VertexAttribute.VERTEX]: 'POSITION', [VertexAttribute.VERTEX]: "POSITION",
[VertexAttribute.NORMAL]: 'NORMAL', [VertexAttribute.NORMAL]: "NORMAL",
[VertexAttribute.TEXCOORD0]: 'TEXCOORD_0', [VertexAttribute.TEXCOORD0]: "TEXCOORD_0",
[VertexAttribute.TEXCOORD1]: 'TEXCOORD_0', [VertexAttribute.TEXCOORD1]: "TEXCOORD_1",
[VertexAttribute.TEXCOORD2]: 'TEXCOORD_0', [VertexAttribute.TEXCOORD2]: "TEXCOORD_2",
[VertexAttribute.TEXCOORD3]: 'TEXCOORD_0', [VertexAttribute.TEXCOORD3]: "TEXCOORD_3",
}; };
export const vertexAttributeGltfVectorSizes = { export const vertexAttributeGltfVectorSizes = {
...vertexAttributeVectorSizes, ...vertexAttributeVectorSizes,
@@ -4,35 +4,35 @@ import {
ScgReadStream, ScgReadStream,
Textures, Textures,
VertexAttribute, VertexAttribute,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { Document, Material, Node, Scene } from '@gltf-transform/core'; import { Document, Material, Node, Scene } from "@gltf-transform/core";
import { dedup, prune } from '@gltf-transform/functions'; import { dedup, prune } from "@gltf-transform/functions";
import { times } from 'lodash-es'; import { times } from "lodash-es";
import { dirname } from 'path'; import { dirname } from "path";
import { Matrix4, Quaternion, Vector3, Vector4Tuple } from 'three'; import { readBaseColor } from "../readBaseColor";
import { readDVPLFile } from '../readDVPLFile'; import { readDVPLFile } from "../readDVPLFile";
import { readTexture } from '../readTexture'; import { readNormal } from "../readNormal";
import { TextureMutation } from '../readTexture/constants'; import { readRoughnessMetallic } from "../readRoughnessMetallic";
import { import {
vertexAttributeGLTFName, vertexAttributeGLTFName,
vertexAttributeGltfVectorSizes, vertexAttributeGltfVectorSizes,
} from './constants'; } from "./constants";
const ERROR_ON_UNKNOWN_COMPONENT = false; const ERROR_ON_UNKNOWN_COMPONENT = false;
const omitMeshNames = { const omitMeshNames = {
start: ['chassis_chassis_', 'chassis_track_crash_', 'HP_'], start: ["chassis_chassis_", "chassis_track_crash_", "HP_"],
end: ['_POINT'], end: ["_POINT"],
}; };
export async function extractModel(data: string, path: string) { export async function extractModel(data: string, path: string) {
const sc2Path = `${data}/3d/${path}.sc2`; const sc2Path = `${data}/3d/${path}.sc2`;
const scgPath = `${data}/3d/${path}.scg`; const scgPath = `${data}/3d/${path}.scg`;
const sc2 = new Sc2ReadStream( const sc2 = new Sc2ReadStream(
(await readDVPLFile(sc2Path)).buffer as ArrayBuffer, (await readDVPLFile(sc2Path)).buffer as ArrayBuffer
).sc2(); ).sc2();
const scg = new ScgReadStream( const scg = new ScgReadStream(
(await readDVPLFile(scgPath)).buffer as ArrayBuffer, (await readDVPLFile(scgPath)).buffer as ArrayBuffer
).scg(); ).scg();
const document = new Document(); const document = new Document();
const scene = document.createScene(); const scene = document.createScene();
@@ -41,8 +41,8 @@ export async function extractModel(data: string, path: string) {
// create materials // create materials
await Promise.all( await Promise.all(
sc2['#dataNodes'].map(async (node) => { sc2["#dataNodes"].map(async (node) => {
const id = new DataView(node['#id']).getBigUint64(0, true); const id = new DataView(node["#id"]).getBigUint64(0, true);
if (node.parentMaterialKey !== undefined) { if (node.parentMaterialKey !== undefined) {
/** /**
@@ -58,7 +58,7 @@ export async function extractModel(data: string, path: string) {
if (node.textures) { if (node.textures) {
textures = node.textures; textures = node.textures;
} else if (typeof node.configCount === 'number') { } else if (typeof node.configCount === "number") {
textures = node.configArchive_0.textures; textures = node.configArchive_0.textures;
} }
@@ -66,26 +66,27 @@ export async function extractModel(data: string, path: string) {
material.setBaseColorTexture( material.setBaseColorTexture(
document document
.createTexture(node.materialName) .createTexture(node.materialName)
.setMimeType('image/jpeg') .setMimeType("image/jpeg")
.setImage( .setImage(
await readTexture( await readBaseColor(
`${data}/3d/${dirname(path)}/${textures.albedo}`, `${data}/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
TextureMutation.Albedo, textures.miscMap
), ? `${data}/3d/${dirname(path)}/${textures.miscMap}`
), : undefined
)
)
); );
if (textures.baseRMMap) { if (textures.baseRMMap) {
material.setMetallicRoughnessTexture( material.setMetallicRoughnessTexture(
document document
.createTexture(node.materialName) .createTexture(node.materialName)
.setMimeType('image/jpeg') .setMimeType("image/jpeg")
.setImage( .setImage(
await readTexture( await readRoughnessMetallic(
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`, `${data}/3d/${dirname(path)}/${textures.baseRMMap}`
TextureMutation.RoughnessMetallicness, )
), )
),
); );
} }
@@ -95,47 +96,33 @@ export async function extractModel(data: string, path: string) {
material.setNormalTexture( material.setNormalTexture(
document document
.createTexture(node.materialName) .createTexture(node.materialName)
.setMimeType('image/jpeg') .setMimeType("image/jpeg")
.setImage( .setImage(
await readTexture( await readNormal(
`${data}/3d/${dirname(path)}/${ `${data}/3d/${dirname(path)}/${
textures.baseNormalMap ?? textures.normalmap textures.baseNormalMap ?? textures.normalmap
}`, }`,
isBase ? TextureMutation.Normal : undefined, isBase
), )
), )
);
}
if (textures.miscMap) {
material.setOcclusionTexture(
document
.createTexture(node.materialName)
.setMimeType('image/jpeg')
.setImage(
await readTexture(
`${data}/3d/${dirname(path)}/${textures.miscMap}`,
TextureMutation.Miscellaneous,
),
),
); );
} }
materials.set( materials.set(
new DataView(node['#id']).getBigUint64(0, true), new DataView(node["#id"]).getBigUint64(0, true),
material, material
); );
} }
}), })
); );
// replace children materials with parents // replace children materials with parents
materials.forEach((material, id) => { materials.forEach((material, id) => {
if (typeof material !== 'bigint') return; if (typeof material !== "bigint") return;
let resolvedMaterial: Material | bigint | undefined = material; let resolvedMaterial: Material | bigint | undefined = material;
while (typeof resolvedMaterial === 'bigint') { while (typeof resolvedMaterial === "bigint") {
const linkedParentMaterial = materials.get(resolvedMaterial); const linkedParentMaterial = materials.get(resolvedMaterial);
resolvedMaterial = linkedParentMaterial; resolvedMaterial = linkedParentMaterial;
@@ -155,36 +142,36 @@ export async function extractModel(data: string, path: string) {
const node = document.createNode(hierarchy.name); const node = document.createNode(hierarchy.name);
const components = times( const components = times(
hierarchy.components.count, hierarchy.components.count,
(index) => hierarchy.components[index.toString().padStart(4, '0')], (index) => hierarchy.components[index.toString().padStart(4, "0")]
); );
components.forEach((component) => { components.forEach((component) => {
switch (component['comp.typename']) { switch (component["comp.typename"]) {
case 'LodComponent': case "LodComponent":
// found and used later by transform component // found and used later by transform component
// TODO: lod component always shows up before transform component; cache it before parsing transform // TODO: lod component always shows up before transform component; cache it before parsing transform
break; break;
case 'TransformComponent': { case "TransformComponent": {
node.setTranslation(component['tc.localTranslation']); node.setTranslation(component["tc.localTranslation"]);
node.setRotation(component['tc.localRotation']); node.setRotation(component["tc.localRotation"]);
node.setScale(component['tc.localScale']); node.setScale(component["tc.localScale"]);
break; break;
} }
case 'RenderComponent': { case "RenderComponent": {
const renderObject = component['rc.renderObj']; const renderObject = component["rc.renderObj"];
times(renderObject['ro.batchCount'], (batchIndex): void => { times(renderObject["ro.batchCount"], (batchIndex): void => {
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`]; const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
if (lodIndex !== 0) return; if (lodIndex !== 0) return;
const batchKey = batchIndex.toString().padStart(4, '0'); const batchKey = batchIndex.toString().padStart(4, "0");
const batch = renderObject['ro.batches'][batchKey]; const batch = renderObject["ro.batches"][batchKey];
const material = materials.get(batch['rb.nmatname']); const material = materials.get(batch["rb.nmatname"]);
const polygonGroup = scg.get(batch['rb.datasource']); const polygonGroup = scg.get(batch["rb.datasource"]);
if (!(material instanceof Material)) { if (!(material instanceof Material)) {
// probably shadow material // probably shadow material
@@ -192,14 +179,14 @@ export async function extractModel(data: string, path: string) {
} }
if (polygonGroup === undefined) { if (polygonGroup === undefined) {
throw new Error( throw new Error(
`Missing polygon group ${batch['rb.datasource']}`, `Missing polygon group ${batch["rb.datasource"]}`
); );
} }
const lodNode = document.createNode(batchKey); const lodNode = document.createNode(batchKey);
const indicesAccessor = document const indicesAccessor = document
.createAccessor() .createAccessor()
.setType('SCALAR') .setType("SCALAR")
.setArray(new Uint16Array(polygonGroup.indices)) .setArray(new Uint16Array(polygonGroup.indices))
.setBuffer(buffer); .setBuffer(buffer);
const primitive = document const primitive = document
@@ -233,7 +220,7 @@ export async function extractModel(data: string, path: string) {
const vertexSize = vertexAttributeGltfVectorSizes[attribute]; const vertexSize = vertexAttributeGltfVectorSizes[attribute];
const attributeAccessor = document const attributeAccessor = document
.createAccessor(name) .createAccessor(name)
.setType(vertexSize === 1 ? 'SCALAR' : `VEC${vertexSize}`) .setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
.setArray(new Float32Array(value.flat())) .setArray(new Float32Array(value.flat()))
.setBuffer(buffer); .setBuffer(buffer);
@@ -241,7 +228,7 @@ export async function extractModel(data: string, path: string) {
}); });
const mesh = document const mesh = document
.createMesh(batch['##name']) .createMesh(batch["##name"])
.addPrimitive(primitive); .addPrimitive(primitive);
lodNode.setMesh(mesh); lodNode.setMesh(mesh);
node.addChild(lodNode); node.addChild(lodNode);
@@ -253,24 +240,24 @@ export async function extractModel(data: string, path: string) {
default: { default: {
if (ERROR_ON_UNKNOWN_COMPONENT) { if (ERROR_ON_UNKNOWN_COMPONENT) {
throw new Error( throw new Error(
`Unhandled component type: ${component['comp.typename']}`, `Unhandled component type: ${component["comp.typename"]}`
); );
} }
} }
} }
}); });
if (hierarchy['#hierarchy']) { if (hierarchy["#hierarchy"]) {
parseHierarchies(hierarchy['#hierarchy'], node); parseHierarchies(hierarchy["#hierarchy"], node);
} }
parent.addChild(node); parent.addChild(node);
}); });
} }
parseHierarchies(sc2['#hierarchy'], scene); parseHierarchies(sc2["#hierarchy"], scene);
scene.addChild(document.createNode('test')); scene.addChild(document.createNode("test"));
await document.transform(prune({ keepAttributes: true }), dedup()); await document.transform(prune({ keepAttributes: true }), dedup());
@@ -0,0 +1,48 @@
import sharp from "sharp";
import { readTexture } from "./readTexture";
export async function readBaseColor(path: string, occlusionPath?: string) {
const baseRaw = await readTexture(path);
const occlusionRaw = occlusionPath
? await readTexture(occlusionPath)
: undefined;
const base = await sharp(baseRaw.data, { raw: baseRaw })
.removeAlpha()
.raw()
.toBuffer();
const occlusion = occlusionRaw
? await sharp(occlusionRaw.data, { raw: occlusionRaw })
.extractChannel(3)
.raw()
.toBuffer()
: undefined;
const combined = Buffer.alloc(baseRaw.width * baseRaw.height * 3);
for (let i = 0; i < baseRaw.width * baseRaw.height; i++) {
let c = 1;
if (occlusionRaw) {
const x = i % baseRaw.width;
const y = Math.floor(i / baseRaw.width);
const u = Math.floor(x * (occlusionRaw.width / baseRaw.width));
const v = Math.floor(y * (occlusionRaw.height / baseRaw.height));
const occlusionI = u + v * occlusionRaw.width;
c = occlusion![occlusionI] / 255;
}
combined[i * 3 + 0] = Math.round(base[i * 3 + 0] * c);
combined[i * 3 + 1] = Math.round(base[i * 3 + 1] * c);
combined[i * 3 + 2] = Math.round(base[i * 3 + 2] * c);
}
const image = await sharp(combined, {
raw: { width: baseRaw.width, height: baseRaw.height, channels: 3 },
})
.jpeg()
.toBuffer();
return image;
}
@@ -0,0 +1,29 @@
import sharp from "sharp";
import { readTexture } from "./readTexture";
export async function readNormal(path: string, isBase: boolean) {
const raw = await readTexture(path);
if (!isBase) {
return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
}
const bytes = 4 * raw.width * raw.height;
for (let index = 0; index < bytes; index += 4) {
/**
* Red is always 255 and blue is always 0. Only alpha and green contain any
* sort of information.
*/
let x = raw.data[index + 3] * (2 / 255) - 1;
let y = raw.data[index + 1] * (2 / 255) - 1;
let z = Math.sqrt(Math.max(0, 1 - x ** 2 - y ** 2));
raw.data[index] = Math.round((x + 1) * (255 / 2));
raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
raw.data[index + 3] = 255;
}
return await sharp(raw.data, { raw }).jpeg().toBuffer();
}
@@ -0,0 +1,24 @@
import sharp from "sharp";
import { readTexture } from "./readTexture";
export async function readRoughnessMetallic(path: string) {
const raw = await readTexture(path);
const blitz = sharp(raw.data, { raw: raw });
const metallicness = await blitz?.extractChannel(1).raw().toBuffer();
const roughness = await blitz?.extractChannel(3).raw().toBuffer();
const combined = Buffer.alloc(raw.width * raw.height * 3);
for (let i = 0; i < raw.width * raw.height; i++) {
combined[i * 3 + 1] = roughness[i];
combined[i * 3 + 2] = metallicness[i];
}
const image = await sharp(combined, {
raw: { width: raw.width, height: raw.height, channels: 3 },
})
.jpeg()
.toBuffer();
return image;
}
@@ -1,81 +1,83 @@
import { DdsReadStream, PvrReadStream } from '@blitzkit/core'; import { DdsReadStream, PvrReadStream } from "@blitzkit/core";
import { existsSync } from 'fs'; import { existsSync } from "fs";
import sharp from 'sharp'; import sharp from "sharp";
import { DATA_COMPRESSED } from '../../../buildAssets/constants'; import { DATA_COMPRESSED } from "../../../buildAssets/constants";
import { readDVPLFile } from '../readDVPLFile'; import { readDVPLFile } from "../readDVPLFile";
import { TextureMutation } from './constants'; import { TextureMutation } from "./constants";
export async function readTexture(path: string, mutation?: TextureMutation) { export async function readTexture(path: string) {
const ddsTexturePath = path.replace('.tex', '.dx11.dds'); const ddsTexturePath = path.replace(".tex", ".dx11.dds");
const isDds = existsSync( const isDds = existsSync(
`${ddsTexturePath}${DATA_COMPRESSED ? '.dvpl' : ''}`, `${ddsTexturePath}${DATA_COMPRESSED ? ".dvpl" : ""}`
); );
const resolvedTexturePath = isDds const resolvedTexturePath = isDds
? ddsTexturePath ? ddsTexturePath
: ddsTexturePath.replace('.dds', '.pvr'); : ddsTexturePath.replace(".dds", ".pvr");
const decompressedDvpl = await readDVPLFile(`${resolvedTexturePath}.dvpl`); const decompressedDvpl = await readDVPLFile(`${resolvedTexturePath}.dvpl`);
const raw = isDds const raw = isDds
? await new DdsReadStream(decompressedDvpl.buffer as ArrayBuffer).dds() ? await new DdsReadStream(decompressedDvpl.buffer as ArrayBuffer).dds()
: new PvrReadStream(decompressedDvpl.buffer as ArrayBuffer).pvr(); : new PvrReadStream(decompressedDvpl.buffer as ArrayBuffer).pvr();
switch (mutation) { return raw;
case TextureMutation.Normal: {
const bytes = 4 * raw.width * raw.height;
for (let index = 0; index < bytes; index += 4) {
/**
* Red is always 255 and blue is always 0. Only alpha and green contain any
* sort of information.
*/
let x = raw.data[index + 3] * (2 / 255) - 1;
let y = raw.data[index + 1] * (2 / 255) - 1;
const underSqrt = 1 - x ** 2 - y ** 2;
let z = underSqrt > 0 ? Math.sqrt(underSqrt) : -Math.sqrt(-underSqrt);
raw.data[index] = Math.round((x + 1) * (255 / 2)); // switch (mutation) {
raw.data[index + 1] = Math.round((y + 1) * (255 / 2)); // case TextureMutation.Normal: {
raw.data[index + 2] = Math.round((z + 1) * (255 / 2)); // const bytes = 4 * raw.width * raw.height;
raw.data[index + 3] = 255; // for (let index = 0; index < bytes; index += 4) {
} // /**
// * Red is always 255 and blue is always 0. Only alpha and green contain any
// * sort of information.
// */
// let x = raw.data[index + 3] * (2 / 255) - 1;
// let y = raw.data[index + 1] * (2 / 255) - 1;
// const underSqrt = 1 - x ** 2 - y ** 2;
// let z = underSqrt > 0 ? Math.sqrt(underSqrt) : -Math.sqrt(-underSqrt);
return await sharp(raw.data, { raw }).jpeg().toBuffer(); // raw.data[index] = Math.round((x + 1) * (255 / 2));
} // raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
// raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
// raw.data[index + 3] = 255;
// }
case TextureMutation.RoughnessMetallicness: { // return await sharp(raw.data, { raw }).jpeg().toBuffer();
const newImage = sharp(Buffer.alloc(raw.width * raw.height), { // }
raw: { ...raw, channels: 1 },
});
const metallicness = sharp(raw.data, { raw })
.extractChannel('green')
.jpeg()
.toBuffer();
const roughness = sharp(raw.data, { raw })
.extractChannel('alpha')
.jpeg()
.toBuffer();
return await newImage // case TextureMutation.RoughnessMetallicness: {
.joinChannel(await Promise.all([roughness, metallicness])) // const newImage = sharp(Buffer.alloc(raw.width * raw.height), {
.jpeg() // raw: { ...raw, channels: 1 },
.toBuffer(); // });
} // const metallicness = sharp(raw.data, { raw })
// .extractChannel("green")
// .jpeg()
// .toBuffer();
// const roughness = sharp(raw.data, { raw })
// .extractChannel("alpha")
// .jpeg()
// .toBuffer();
case TextureMutation.Miscellaneous: { // return await newImage
/** // .joinChannel(await Promise.all([roughness, metallicness]))
* Alpha is ambient occlusion and green is emissive. But very few tanks // .jpeg()
* use emissive so I am ignoring it for now. // .toBuffer();
*/ // }
return await sharp(raw.data, { raw })
.extractChannel('alpha')
.jpeg()
.toBuffer();
}
case TextureMutation.Albedo: { // case TextureMutation.Miscellaneous: {
return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer(); // /**
} // * Alpha is ambient occlusion and green is emissive. But very few tanks
// * use emissive so I am ignoring it for now.
// */
// return await sharp(raw.data, { raw })
// .extractChannel("alpha")
// .jpeg()
// .toBuffer();
// }
default: // case TextureMutation.Albedo: {
return await sharp(raw.data, { raw }).jpeg().toBuffer(); // return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
} // }
// default:
// return await sharp(raw.data, { raw }).jpeg().toBuffer();
// }
} }
+50 -36
View File
@@ -1,47 +1,61 @@
import { exec as execSync } from 'child_process'; import { exec as execSync } from "child_process";
import { existsSync } from 'fs'; import { existsSync } from "fs";
import { readdir, rm, writeFile } from 'fs/promises'; import { readdir, rm } from "fs/promises";
import { promisify } from 'util'; import { promisify } from "util";
const ROOT = 'packages/core/src/protos'; const MAX_COMMAND_LENGTH = 2 ** 11;
const roots = [
// "packages/core/src/protos",
"submodules/blitzkit-closed/protos",
];
const exec = promisify(execSync); const exec = promisify(execSync);
let executableFileExtension: string; let executableFileExtension: string;
if (existsSync('node_modules/.bin/protoc-gen-ts_proto.exe')) { if (existsSync("node_modules/.bin/protoc-gen-ts_proto.exe")) {
executableFileExtension = '.exe'; executableFileExtension = ".exe";
} else { } else {
executableFileExtension = ''; executableFileExtension = "";
} }
const filesRaw = await readdir(`${ROOT}`); for (const root of roots) {
await Promise.all( const filesRaw = await readdir(`${root}`);
filesRaw.map(async (file) => { const files: string[] = [];
if (!file.endsWith('.proto')) {
await rm(`${ROOT}/${file}`); for (const file of filesRaw) {
if (file.endsWith(".proto")) files.push(file);
if (file.endsWith(".ts")) await rm(`${root}/${file}`);
}
while (files.length > 0) {
let command = [
"protoc",
`--plugin=./node_modules/.bin/protoc-gen-ts_proto${executableFileExtension}`,
"--ts_proto_opt=esModuleInterop=true",
"--ts_proto_opt=oneof=unions-value",
// "--ts_proto_opt=removeEnumPrefix=true",
"--ts_proto_opt=unrecognizedEnum=false",
"--ts_proto_opt=snakeToCamel=false",
`--ts_proto_out=${root}`,
`-I=${root}`,
].join(" ");
while (true) {
if (files.length === 0) break;
const file = files.at(-1);
const newLine = ` ${root}/${file}`;
if (command.length + newLine.length < MAX_COMMAND_LENGTH) {
command += newLine;
files.pop();
} else {
break;
}
} }
}),
);
const files = filesRaw.filter((file) => file.endsWith('.proto')); await exec(command);
const args = [ }
`--plugin=./node_modules/.bin/protoc-gen-ts_proto${executableFileExtension}`, }
'--ts_proto_opt=esModuleInterop=true',
'--ts_proto_opt=oneof=unions-value',
'--ts_proto_opt=removeEnumPrefix=true',
'--ts_proto_opt=unrecognizedEnum=false',
'--ts_proto_opt=snakeToCamel=false',
'--ts_proto_out=.',
...files.map((file) => `${ROOT}/${file}`),
];
await exec(`protoc ${args.join(' ')}`);
await writeFile(
`${ROOT}/index.ts`,
'// @ts-nocheck\n' +
files
.map((file) => file.replace('.proto', ''))
.map((file) => `export * from './${file}.ts';`)
.join('\n'),
);
+12 -15
View File
@@ -1,29 +1,26 @@
import { import { type TranslationTree } from "@blitzkit/i18n";
DEFAULT_LOCALE, import locales from "@blitzkit/i18n/locales.json";
SUPPORTED_LOCALES, import { writeFile } from "fs/promises";
TranslationTree,
} from '@blitzkit/i18n';
import { writeFile } from 'fs/promises';
const ROOT = '@blitzkit/i18n/strings'; const ROOT = "@blitzkit/i18n/strings";
const WORKING_ROOT = '../../packages/i18n/strings'; const WORKING_ROOT = "../../packages/i18n/strings";
const defaultStrings = require(`${ROOT}/${DEFAULT_LOCALE}.json`).default; const defaultStrings = require(`${ROOT}/${locales.default}.json`).default;
const nonDefaultLocales = SUPPORTED_LOCALES.filter( const nonDefaultLocales = locales.supported.filter(
(locale) => locale !== DEFAULT_LOCALE, ({ locale }) => locale !== locales.default
); );
function prune(defaultStrings: TranslationTree, strings: TranslationTree) { function prune(defaultStrings: TranslationTree, strings: TranslationTree) {
for (const key in strings) { for (const key in strings) {
if (key in defaultStrings) { if (key in defaultStrings) {
if (typeof defaultStrings[key] === typeof strings[key]) { if (typeof defaultStrings[key] === typeof strings[key]) {
if (typeof strings[key] === 'string') { if (typeof strings[key] === "string") {
if ( if (
// are equivalent regardless of %s flavor // are equivalent regardless of %s flavor
(defaultStrings[key] as string).replaceAll(/%s\d/g, '%s') === (defaultStrings[key] as string).replaceAll(/%s\d/g, "%s") ===
(strings[key] as string).replaceAll(/%s\d/g, '%s') || (strings[key] as string).replaceAll(/%s\d/g, "%s") ||
// string is empty // string is empty
strings[key] === '' || strings[key] === "" ||
// string has old flavor of %s // string has old flavor of %s
strings[key].match(/%s([^\d]|$)/g) !== null strings[key].match(/%s([^\d]|$)/g) !== null
) { ) {
+8
View File
@@ -0,0 +1,8 @@
{
"name": "varuna",
"type": "module",
"main": "src/index.ts",
"dependencies": {
"react": "^19.1.1"
}
}
+122
View File
@@ -0,0 +1,122 @@
import { produce } from "immer";
import { debounce, merge } from "lodash-es";
import { useEffect, useRef, useState } from "react";
export class Varuna<Type, Arguments = void> {
private listeners = new Set<(state: Type) => void>();
private initialized = false;
private _initial?: Type;
private _state?: Type;
constructor(
private creator: Type | ((...args: Arguments[]) => Type),
private persistence?: string
) {
if (typeof creator !== "function") this.initialize(creator);
}
private initialize(initial: Type) {
let data = initial;
if (this.persistence && typeof localStorage !== "undefined") {
const dehydrated = localStorage.getItem(this.persistence);
if (dehydrated) {
const rehydrated = JSON.parse(dehydrated) as unknown;
data = merge(data, rehydrated);
}
window.addEventListener("beforeunload", this.store.bind(this));
}
this.initialized = true;
this._initial = data;
this._state = data;
}
useInitialization(...args: Arguments[]) {
const initializedThisMount = useRef(false);
if (initializedThisMount.current) return;
if (typeof this.creator !== "function") {
throw new Error("Provider must be used with a function creator");
}
this.initialize((this.creator as (...args: Arguments[]) => Type)(...args));
initializedThisMount.current = true;
}
private dispatch() {
const state = this.state;
this.listeners.forEach((callback) => callback(state));
}
on<Slice>(slicer: (state: Type) => Slice, callback: (state: Slice) => void) {
let last = slicer(this.state);
function listener(state: Type) {
const next = slicer(state);
if (next !== last) {
callback(next);
last = next;
}
}
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
mutate(mutator: (state: Type) => void) {
this.set(produce(this.state, mutator));
}
private assertInitialized() {
if (!this.initialized) {
throw new Error("Varuna store used before initialization");
}
}
get state() {
this.assertInitialized();
return this._state as Type;
}
set(state: Type) {
this.assertInitialized();
this._state = state;
this.dispatch();
this.storeDeferred();
}
get initial() {
this.assertInitialized();
return this._initial as Type;
}
use<Slice = Type>(
slicer: (state: Type) => Slice = (state) => state as unknown as Slice
) {
return this.useDeferred(slicer, slicer(this.state));
}
useDeferred<Slice = Type>(slicer: (state: Type) => Slice, initial: Slice) {
const instance = this;
const [slice, setSlice] = useState(initial);
useEffect(() => instance.on(slicer, setSlice), []);
return slice;
}
store() {
localStorage.setItem(this.persistence!, JSON.stringify(this.state));
}
storeDeferred = debounce(this.store.bind(this), 500);
}
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}
-24
View File
@@ -1,24 +0,0 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
.vercel/
-35
View File
@@ -1,35 +0,0 @@
// @ts-check
import react from '@astrojs/react';
import sitemap from '@astrojs/sitemap';
import { SUPPORTED_LOCALES } from '@blitzkit/i18n';
import { defineConfig } from 'astro/config';
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: [...SUPPORTED_LOCALES],
routing: {
prefixDefaultLocale: false,
},
},
devToolbar: { enabled: false },
output: 'static',
site: 'https://blitzkit.app',
outDir: '../../dist/website',
prefetch: true,
integrations: [react(), sitemap()],
vite: {
resolve: {
alias: {
'.prisma/client/index-browser':
'../../node_modules/.prisma/client/index-browser.js',
},
},
ssr: {
noExternal: [/^d3.*$/, /^@nivo.*$/],
},
},
});
+114
View File
@@ -0,0 +1,114 @@
import node from "@astrojs/node";
import react from "@astrojs/react";
import { wrapper } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import strings from "@blitzkit/i18n/strings/en.json";
import { mauve, mauveDark } from "@radix-ui/colors";
import AstroPWA from "@vite-pwa/astro";
import favicons from "astro-favicons";
import { defineConfig } from "astro/config";
import packageJSON from "../../package.json";
import { tools } from "./src/constants/tools";
const POSSIBLE_TARGETS = ["app", "web"];
const target = process.env.PUBLIC_TARGET;
if (!target || !POSSIBLE_TARGETS.includes(target)) {
throw new Error("Invalid target");
}
export default defineConfig({
devToolbar: { enabled: false },
vite: { server: { allowedHosts: [] } },
output: target === "web" ? "static" : "server",
site: "https://blitzkit.app",
outDir: "../../dist/website",
prefetch: { defaultStrategy: "hover", prefetchAll: true },
build: { concurrency: 4 },
adapter: target === "app" ? node({ mode: "standalone" }) : undefined,
integrations: [
favicons({
name: strings.common.name,
short_name: strings.common.name,
background: mauveDark.mauve1,
themes: [mauve.mauve1, mauveDark.mauve1],
appleStatusBarStyle: "black-translucent",
version: packageJSON.version,
shortcuts: Object.values(tools)
.filter((tool) => !tool.disabled && !tool.href)
.map((tool) => {
const stringId = (tool.strings ??
tool.id) as keyof typeof strings.website.tools;
return {
name: strings.website.tools[stringId].name,
name_localized: wrapper(
(strings) => strings.website.tools[stringId].name
),
description: strings.website.tools[stringId].description,
description_localized: wrapper(
(strings) => strings.website.tools[stringId].description
),
short_name: strings.website.tools[stringId].name,
short_name_localized: wrapper(
(strings) => strings.website.tools[stringId].name
),
url: `/${tool.id}`,
icon: `public/assets/images/tools/${tool.id}.webp`,
};
}),
manifest: {
description: strings.website.home.seo_description,
description_localized: wrapper(
(strings) => strings.website.home.seo_description
),
dir: "ltr",
categories: ["utilities", "games", "developer", "developer tools"],
id: "com.tresabhi.blitzkit",
start_url: "/",
launch_handler: { client_mode: "navigate-new" },
display: "fullscreen",
display_override: ["fullscreen", "standalone", "minimal-ui"],
lang: locales.default,
orientation: "any",
},
icons: {
windows: true,
android: true,
appleIcon: true,
appleStartup: true,
favicons: true,
yandex: true,
},
}),
AstroPWA({
manifest: false,
devOptions: { enabled: true },
workbox: {
globPatterns: ["**/*.{html,css,js,png,webp,svg,xml,json}"],
runtimeCaching: [
{
urlPattern: ({ url }) =>
url.href.startsWith(import.meta.env.PUBLIC_ASSET_BASE),
handler: "NetworkFirst",
},
],
},
}),
react(),
],
});
+30 -43
View File
@@ -2,53 +2,40 @@
"name": "@blitzkit/website", "name": "@blitzkit/website",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "PUBLIC_TARGET=web astro dev",
"start": "astro dev", "build": "PUBLIC_TARGET=web astro build",
"preview": "PUBLIC_TARGET=web astro preview",
"lint": "astro check", "lint": "astro check",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrolib/seo": "^1.0.0-beta.8",
"@blitzkit/core": "workspace:*",
"@blitzkit/i18n": "workspace:*",
"@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",
"quicklime": "^1.2.0",
"react": "^18.3.1",
"react-device-detect": "^2.2.3",
"react-dom": "^18.3.1",
"react-promise-suspense": "^0.3.4",
"three": "^0.169.0",
"urlon": "^3.1.0",
"zustand": "^4.5.5"
},
"devDependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
"@astrojs/react": "^3.6.2", "@astrojs/node": "^9.4.4",
"@astrojs/sitemap": "^3.2.1", "@astrojs/react": "^4.4.0",
"@playform/compress": "^0.1.9", "@astrolib/seo": "^1.0.0-beta.8",
"@playform/inline": "^0.1.2", "@fontsource/inter": "^5.2.8",
"@types/js-cookie": "^3.0.6", "@radix-ui/colors": "^3.0.0",
"@types/react": "^18.3.12", "@radix-ui/react-icons": "^1.3.2",
"@types/react-dom": "^18.3.1", "@radix-ui/themes": "^3.2.1",
"@types/three": "^0.160.0", "@react-three/drei": "^10.7.6",
"astro": "^4.16.7", "@react-three/fiber": "^9.3.0",
"astro-compressor": "^1.0.0" "@types/react": "^19.1.14",
"@types/react-dom": "^19.1.9",
"@types/three": "^0.180.0",
"@vite-pwa/astro": "^1.1.0",
"astro": "^5.14.1",
"astro-favicons": "^3.1.5",
"compare-versions": "^6.1.1",
"fuzzysort": "^3.1.0",
"immer": "^10.1.3",
"markdown-to-jsx": "^7.7.13",
"pwa-install-handler": "^2.6.3",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-promise-suspense": "^0.3.4",
"three": "^0.180.0",
"three-stdlib": "^2.36.0",
"typescript": "^5.9.2",
"urlon": "^3.1.0"
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 490 KiB

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 B

Loaded 100 of 319 files, more files were not shown because too many files have changed in this diff. Show more