feat: game strings context

This commit is contained in:
TrèsAbhi committed 2025-05-29 20:22:13 -05:00
1 parent 9bf1554c30
commit 1eca71d28c
21 files changed
+208 -137

No files matched your search

BIN
View File
Binary file not shown.
-2
View File
@@ -19,8 +19,6 @@ export * from './idToRegion';
export * from './isOnGoingRatingSeason';
export * from './isValidBlitzId';
export * from './metadata';
export * from './metadata.remote';
export * from './metadata.socket';
export * from './nations';
export * from './normalizeExtra';
export * from './normalizeIds';
-35
View File
@@ -1,35 +0,0 @@
import { BlitzkitAllAvatarsComponent } from '@protos/blitzkit_static_all_avatars_component';
import { CatalogItemAccessor } from './catalogItemAccessor';
export abstract class MetadataAccessor {
abstract get(item: string): Promise<CatalogItemAccessor>;
abstract add(item: CatalogItemAccessor): void;
abstract filter(predicate: (item: string) => boolean): CatalogItemAccessor[];
injectBlitzkit() {
this.injectBlitzkitProfileAvatars();
return this;
}
injectBlitzkitProfileAvatars() {
const allAvatars: BlitzkitAllAvatarsComponent = {
avatars: this.filter((item) =>
item.startsWith('ProfileAvatarEntity.'),
).map((avatar) => avatar.pack()),
};
this.add(
CatalogItemAccessor.fromComponent(
'BlitzkitAllAvatarsEntity.blitzkit_all_avatars',
{
type_url:
'./blitzkit_static_all_avatars_component.BlitzkitAllAvatarsComponent',
value: BlitzkitAllAvatarsComponent.encode(allAvatars).finish(),
},
),
);
}
}
@@ -0,0 +1,113 @@
import { LocalizationResourcesComponent } from '@protos/blitz_static_localization_resources_component';
import { RemoteStorageComponent } from '@protos/blitz_static_remote_storage_component';
import { BlitzkitAllAvatarsComponent } from '@protos/blitzkit_static_all_avatars_component';
import { load } from 'js-yaml';
import { minimatch } from 'minimatch';
import { CatalogItemAccessor } from '../catalogItemAccessor';
interface LocalizationConfig {
namespaces: string[];
}
export abstract class MetadataAccessor {
abstract get(item: string): Promise<CatalogItemAccessor>;
abstract add(item: CatalogItemAccessor): void;
abstract filter(predicate: (item: string) => boolean): CatalogItemAccessor[];
async strings(locale: string) {
const clientConfig = await this.get(
'ClientConfigsEntity.default_client_config',
);
const localizationResources = clientConfig.get(
LocalizationResourcesComponent,
'localizationResourcesComponent',
);
if (!localizationResources.remote_storage) {
throw new Error('Localization resources not found');
}
const { relative_path } = localizationResources.remote_storage;
let remoteStorage: RemoteStorageComponent | undefined = undefined;
for (const candidateId of localizationResources.remote_storage.production) {
const item = await this.get(candidateId);
const candidate = item.get(
RemoteStorageComponent,
'remoteStorageComponent',
);
if (
remoteStorage === undefined ||
candidate.download_speed_weight > remoteStorage.download_speed_weight
) {
remoteStorage = candidate;
}
}
if (remoteStorage === undefined) {
throw new Error('No suitable production remote storage found');
}
const config = await fetch(
`${remoteStorage.url}/${relative_path}/config.yaml`,
)
.then((response) => response.text())
.then((text) => load(text) as LocalizationConfig);
const strings: Record<string, string> = {};
for (const namespace of config.namespaces) {
Object.assign(
strings,
await fetch(
`${remoteStorage.url}/${relative_path}/${namespace}/${locale}.yaml`,
)
.then((response) => response.text())
.then((text) => load(text) as Record<string, string>),
);
}
return strings;
}
async stringsPartial(locale: string, patterns: string[]) {
const strings = await this.strings(locale);
const filtered: Record<string, string> = {};
for (const key in strings) {
if (patterns.some((pattern) => minimatch(key, pattern))) {
filtered[key] = strings[key];
}
}
return filtered;
}
injectBlitzkit() {
this.injectBlitzkitProfileAvatars();
return this;
}
injectBlitzkitProfileAvatars() {
const allAvatars: BlitzkitAllAvatarsComponent = {
avatars: this.filter((item) =>
item.startsWith('ProfileAvatarEntity.'),
).map((avatar) => avatar.pack()),
};
this.add(
CatalogItemAccessor.fromComponent(
'BlitzkitAllAvatarsEntity.blitzkit_all_avatars',
{
type_url:
'./blitzkit_static_all_avatars_component.BlitzkitAllAvatarsComponent',
value: BlitzkitAllAvatarsComponent.encode(allAvatars).finish(),
},
),
);
}
}
@@ -0,0 +1,3 @@
export * from './abstract';
export * from './remote';
export * from './socket';
@@ -1,6 +1,6 @@
import { CatalogItem } from '@protos/blitz_items';
import { CatalogItemAccessor } from './catalogItemAccessor';
import { MetadataAccessor } from './metadata';
import { CatalogItemAccessor } from '../catalogItemAccessor';
import { MetadataAccessor } from './abstract';
export class RemoteMetadataAccessor extends MetadataAccessor {
constructor(private base: string) {
@@ -1,7 +1,7 @@
import { CatalogItem } from '@protos/blitz_items';
import { Metadata } from '@protos/blitz_metadata';
import { CatalogItemAccessor } from './catalogItemAccessor';
import { MetadataAccessor } from './metadata';
import { CatalogItemAccessor } from '../catalogItemAccessor';
import { MetadataAccessor } from './abstract';
export class SocketMetadataAccessor extends MetadataAccessor {
items: Record<string, CatalogItem> = {};
@@ -16,6 +16,7 @@ export class SocketMetadataAccessor extends MetadataAccessor {
async get(item: string) {
if (item in this.items) return new CatalogItemAccessor(this.items[item]);
throw new Error(`Item ${item} does not exist in socket metadata`);
}
+10 -7
View File
@@ -8,19 +8,22 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/react": "^4.3.0",
"@astrolib/seo": "^1.0.0-beta.8",
"@blitzkit/closed": "workspace:*",
"@blitzkit/core": "workspace:*",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/themes": "^3.1.4",
"@astrolib/seo": "^1.0.0-beta.8",
"@blitzkit/core": "workspace:*",
"@blitzkit/closed": "workspace:*",
"@astrojs/react": "^4.3.0",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5",
"astro": "^5.8.0",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"minimatch": "^10.0.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"@types/lodash-es": "^4.17.12",
"lodash-es": "^4.17.21"
"react-dom": "^19.1.0"
}
}
@@ -25,9 +25,9 @@ type Extension =
status: 'error';
};
export function Checks({ locale }: LocaleAcceptorProps) {
export function Checks({ localeData }: LocaleAcceptorProps) {
return (
<LocaleProvider locale={locale}>
<LocaleProvider localeData={localeData}>
<App.Provider>
<Content />
</App.Provider>
@@ -39,7 +39,7 @@ export function Checks({ locale }: LocaleAcceptorProps) {
* Wargaming: 14 (refresh: 7)
*/
function Content() {
const { locale } = useLocale();
const localeData = useLocale();
const logins = App.use((state) => state.logins);
const mutateApp = App.useMutation();
const appStore = App.useStore();
@@ -128,11 +128,17 @@ function Content() {
<Text>
This website utilizes cookies to perform analytics and personalize
your experience. You can learn more through{' '}
<LinkI18n locale={locale} href="/docs/legal/privacy-policy">
<LinkI18n
localeData={localeData}
href="/docs/legal/privacy-policy"
>
our privacy policy
</LinkI18n>
. By using BlitzKit, you also agree to our{' '}
<LinkI18n locale={locale} href="/docs/legal/terms-of-service">
<LinkI18n
localeData={localeData}
href="/docs/legal/terms-of-service"
>
terms of service
</LinkI18n>
.
@@ -1,15 +1,19 @@
---
import { assertSecret, fetchGameDefinitions } from '@blitzkit/core';
import locales from '@blitzkit/i18n/locales.json' with { type: 'json' };
import { Box, Code, Flex, Heading, Link, Text } from '@radix-ui/themes';
import packageJson from '../../../../package.json';
import { getStrings } from '../core/i18n/getStrings';
import { LinkI18n } from './LinkI18n';
import { LocaleSwitcherThemeWrapper } from './LocaleSwitcher';
interface Props {
locale: string;
}
const gameDefinitions = await fetchGameDefinitions();
const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
const locale = Astro.params.locale ?? locales.default;
const { locale } = Astro.props;
const localeData = { locale };
const strings = await getStrings(locale);
---
@@ -44,7 +48,7 @@ const strings = await getStrings(locale);
</Text>
<Box mt="2">
<LocaleSwitcherThemeWrapper {locale} client:load />
<LocaleSwitcherThemeWrapper {localeData} client:load />
</Box>
</Flex>
@@ -79,7 +83,7 @@ const strings = await getStrings(locale);
sm: 'start',
}}
>
<LinkI18n {locale} href="/docs/about/credits" color="gray">
<LinkI18n {localeData} href="/docs/about/credits" color="gray">
{strings.website.footer.about.credits}
</LinkI18n>
<Link
@@ -96,7 +100,7 @@ const strings = await getStrings(locale);
>
{strings.website.footer.about.github}
</Link>
<LinkI18n {locale} href="/docs/patches" color="gray">
<LinkI18n {localeData} href="/docs/patches" color="gray">
{strings.website.footer.about.patches}
</LinkI18n>
</Flex>
@@ -116,10 +120,14 @@ const strings = await getStrings(locale);
>
<Heading size="3">{strings.website.footer.legal.title}</Heading>
<Flex direction="column">
<LinkI18n {locale} href="/docs/legal/privacy-policy" color="gray">
<LinkI18n {localeData} href="/docs/legal/privacy-policy" color="gray">
{strings.website.footer.legal.privacy}
</LinkI18n>
<LinkI18n {locale} href="/docs/legal/terms-of-service" color="gray">
<LinkI18n
{localeData}
href="/docs/legal/terms-of-service"
color="gray"
>
{strings.website.footer.legal.terms}
</LinkI18n>
</Flex>
@@ -3,11 +3,12 @@ import { Link, type LinkProps } from '@radix-ui/themes';
import type { LocaleAcceptorProps } from '../hooks/useLocale';
export function LinkI18n({
locale,
localeData,
href,
...props
}: LinkProps & LocaleAcceptorProps) {
const prefix = locale === locales.default ? '' : `/${locale}`;
const prefix =
localeData.locale === locales.default ? '' : `/${localeData.locale}`;
return (
<Link href={href?.startsWith('/') ? `${prefix}${href}` : href} {...props} />
@@ -4,18 +4,20 @@ import { LOCALE_NAMES, SUPPORTED_LOCALE_FLAGS } from 'packages/i18n/src';
import type { LocaleAcceptorProps } from '../hooks/useLocale';
import { BlitzKitTheme } from './BlitzKitTheme';
export function LocaleSwitcherThemeWrapper({ locale }: LocaleAcceptorProps) {
export function LocaleSwitcherThemeWrapper({
localeData,
}: LocaleAcceptorProps) {
return (
<BlitzKitTheme style={{ background: 'transparent' }}>
<LocaleSwitcher locale={locale} />
<LocaleSwitcher localeData={localeData} />
</BlitzKitTheme>
);
}
export function LocaleSwitcher({ locale }: LocaleAcceptorProps) {
export function LocaleSwitcher({ localeData }: LocaleAcceptorProps) {
return (
<Select.Root
defaultValue={locale}
defaultValue={localeData.locale}
onValueChange={(locale) => {
localStorage.setItem('preferred-locale', locale);
@@ -1,5 +1,4 @@
---
import { assertSecret } from '@blitzkit/core';
import {
Cross1Icon,
DiscordLogoIcon,
@@ -19,8 +18,9 @@ export interface Props {
}
const { integrated, locale } = Astro.props;
const localeData = { locale };
const toolsFiltered = tools.filter((tool) => tool.enabled);
const branchName = resolveBranchName(locale);
const branchName = resolveBranchName();
const isBranchNamed = branchName !== undefined;
const strings = await getStrings(locale);
---
@@ -107,7 +107,7 @@ const strings = await getStrings(locale);
<Flex gap="4" direction="column" maxWidth="80rem" width="100%">
<Flex align="center">
<LinkI18n
{locale}
{localeData}
color="gray"
highContrast
href="/"
@@ -146,7 +146,7 @@ const strings = await getStrings(locale);
return (
<LinkI18n
{locale}
{localeData}
weight={selected ? 'medium' : 'regular'}
color="gray"
highContrast={selected}
@@ -175,7 +175,7 @@ const strings = await getStrings(locale);
</Link>
<LinkI18n
{locale}
{localeData}
style={{ display: 'flex', alignContent: 'center' }}
color="gray"
href="/settings"
@@ -184,8 +184,6 @@ const strings = await getStrings(locale);
<GearIcon width="1em" height="1em" />
</LinkI18n>
<!-- <NavbarAccount client:idle /> -->
<Flex align="center" display={{ initial: 'flex', sm: 'none' }}>
<IconButton variant="ghost" color="gray" className="hamburger">
<Cross1Icon data-expanded />
@@ -1,20 +0,0 @@
import {
LocaleProvider,
useLocale,
type LocaleAcceptorProps,
} from '../hooks/useLocale';
function Content() {
const { locale, strings } = useLocale();
const content = JSON.stringify(strings, null, 2);
return <span>{strings.common.units.deg_s}</span>;
}
export function TestComponent({ locale }: LocaleAcceptorProps) {
return (
<LocaleProvider locale={locale}>
<Content />
</LocaleProvider>
);
}
+24 -10
View File
@@ -1,25 +1,39 @@
import locales from '@blitzkit/i18n/locales.json' with { type: 'json' };
import type { BlitzKitStrings } from 'packages/i18n/src';
import { createContext, use, useContext } from 'react';
const LocaleContext = createContext<string | null>(null);
const LocaleContext = createContext<{
locale: string;
gameStrings: Record<string, string>;
} | null>(null);
export interface LocaleAcceptorProps {
locale: string;
localeData: {
locale: string;
gameStrings?: Record<string, string>;
};
}
interface LocaleProviderProps extends LocaleAcceptorProps {
children: React.ReactNode;
}
export function LocaleProvider({ locale, children }: LocaleProviderProps) {
export function LocaleProvider({ localeData, children }: LocaleProviderProps) {
const isSupported = locales.supported.some(
(supported) => supported.locale === locale,
(supported) => supported.locale === localeData.locale,
);
if (!isSupported) throw new Error(`Unsupported locale: ${locale}`);
if (!isSupported) throw new Error(`Unsupported locale: ${localeData.locale}`);
return (
<LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
<LocaleContext.Provider
value={{
locale: localeData.locale,
gameStrings: localeData.gameStrings ?? {},
}}
>
{children}
</LocaleContext.Provider>
);
}
@@ -41,13 +55,13 @@ function loadLocale(locale: string) {
}
export function useLocale() {
const locale = useContext(LocaleContext);
const localeData = useContext(LocaleContext);
if (locale === null) {
if (localeData === null) {
throw new Error('useLocale must be used within a LocaleProvider');
}
const strings = use(loadLocale(locale));
const strings = use(loadLocale(localeData.locale));
return { locale, strings };
return { ...localeData, strings };
}
@@ -14,6 +14,7 @@ export interface Props {
}
const { noFooter, noNavbar, integrated, locale } = Astro.props;
const localeData = { locale };
---
<html lang={locale}>
@@ -69,11 +70,11 @@ const { noFooter, noNavbar, integrated, locale } = Astro.props;
<Flex direction="column" minHeight="100vh">
<Navbar {locale} {integrated} />
<Checks {locale} client:only="react" />
<Checks {localeData} client:only="react" />
<slot />
{!noFooter && <Footer />}
{!noFooter && <Footer {locale} />}
</Flex>
</BlitzKitTheme>
</body>
+2 -1
View File
@@ -7,6 +7,7 @@ import { PageWrapper } from '../components/PageWrapper';
import BaseLayout from '../layouts/BaseLayout.astro';
const locale = locales.default;
const localeData = { locale };
---
<BaseLayout {locale}>
@@ -15,7 +16,7 @@ const locale = locales.default;
<Text color="gray">Where would you like to go?</Text>
<Flex mt="4" gap="2">
<LinkI18n {locale} href="/">
<LinkI18n {localeData} href="/">
<Button>
<HomeIcon /> Home
</Button>
@@ -5,6 +5,7 @@ import { Flex, Text } from '@radix-ui/themes';
import { CatalogItemAccessor } from 'packages/core/src/blitz/catalogItemAccessor';
import { metadata } from 'packages/website-ue/src/core/blitz/metadata';
import { Var } from 'packages/website-ue/src/core/radix/var';
import type { LocaleAcceptorProps } from 'packages/website-ue/src/hooks/useLocale';
const allAvatars = await metadata
.get('BlitzkitAllAvatarsEntity.blitzkit_all_avatars')
@@ -14,7 +15,7 @@ const allAvatars = await metadata
.avatars,
);
export function Page() {
export function Page({ localeData }: LocaleAcceptorProps) {
return (
<Flex wrap="wrap" gapX="4" gapY="6" justify="center" p="9">
{allAvatars.map((avatar) => {
@@ -1,12 +1,17 @@
---
import { metadata } from 'packages/website-ue/src/core/blitz/metadata';
import BaseLayout from 'packages/website-ue/src/layouts/BaseLayout.astro';
import { Page } from './_index';
export { getStaticPaths } from '../index.astro';
const { locale } = Astro.props;
const gameStrings = await metadata.stringsPartial(locale, [
'ProfileAvatarEntity__*',
]);
---
<BaseLayout {locale}>
<Page client:load />
<Page localeData={{ locale, gameStrings }} client:load />
</BaseLayout>
@@ -1,20 +0,0 @@
import { TankCatalogComponent } from '@protos/blitz_static_tank_component';
import { BlitzkitAllTanksComponent } from '@protos/blitzkit_static_all_tanks_component';
import { SocketCatalogAccessor } from 'packages/core/src';
import { remoteCatalog } from 'packages/website-ue/src/core/blitz/remoteCatalog';
const data = await remoteCatalog
.get('BlitzKitAllTanksEntity.blitzkit_all_tanks')
.then((components) =>
components.get(BlitzkitAllTanksComponent, 'blitzkitAllTanksComponent'),
);
export function Page() {
const tankCatalog = new SocketCatalogAccessor(data.tanks);
const content = tankCatalog
.get('TankEntity.A92_M60')
.get(TankCatalogComponent, 'tankCatalogComponent');
return <pre children={JSON.stringify(content, null, 2)} />;
}
@@ -1,9 +0,0 @@
---
import { Page } from './_index';
export { getStaticPaths } from '../index.astro';
const { locale } = Astro.props;
---
<Page client:load />