diff --git a/bun.lockb b/bun.lockb index 78ce50ff0..80bdc4b51 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/core/src/blitz/index.ts b/packages/core/src/blitz/index.ts index 8f137f83e..4b55b4492 100644 --- a/packages/core/src/blitz/index.ts +++ b/packages/core/src/blitz/index.ts @@ -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'; diff --git a/packages/core/src/blitz/metadata.ts b/packages/core/src/blitz/metadata.ts deleted file mode 100644 index 1de35e133..000000000 --- a/packages/core/src/blitz/metadata.ts +++ /dev/null @@ -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; - - 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(), - }, - ), - ); - } -} diff --git a/packages/core/src/blitz/metadata/abstract.ts b/packages/core/src/blitz/metadata/abstract.ts new file mode 100644 index 000000000..f81faec3d --- /dev/null +++ b/packages/core/src/blitz/metadata/abstract.ts @@ -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; + + 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 = {}; + + 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), + ); + } + + return strings; + } + + async stringsPartial(locale: string, patterns: string[]) { + const strings = await this.strings(locale); + const filtered: Record = {}; + + 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(), + }, + ), + ); + } +} diff --git a/packages/core/src/blitz/metadata/index.ts b/packages/core/src/blitz/metadata/index.ts new file mode 100644 index 000000000..495f63f5f --- /dev/null +++ b/packages/core/src/blitz/metadata/index.ts @@ -0,0 +1,3 @@ +export * from './abstract'; +export * from './remote'; +export * from './socket'; diff --git a/packages/core/src/blitz/metadata.remote.ts b/packages/core/src/blitz/metadata/remote.ts similarity index 86% rename from packages/core/src/blitz/metadata.remote.ts rename to packages/core/src/blitz/metadata/remote.ts index df9d543cf..b92a18968 100644 --- a/packages/core/src/blitz/metadata.remote.ts +++ b/packages/core/src/blitz/metadata/remote.ts @@ -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) { diff --git a/packages/core/src/blitz/metadata.socket.ts b/packages/core/src/blitz/metadata/socket.ts similarity index 88% rename from packages/core/src/blitz/metadata.socket.ts rename to packages/core/src/blitz/metadata/socket.ts index b3e8ed1dc..432ddd8d3 100644 --- a/packages/core/src/blitz/metadata.socket.ts +++ b/packages/core/src/blitz/metadata/socket.ts @@ -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 = {}; @@ -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`); } diff --git a/packages/website-ue/package.json b/packages/website-ue/package.json index 6108771c1..cf10d08e9 100644 --- a/packages/website-ue/package.json +++ b/packages/website-ue/package.json @@ -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" } } diff --git a/packages/website-ue/src/components/Checks/index.tsx b/packages/website-ue/src/components/Checks/index.tsx index 8d6f543e2..292590133 100644 --- a/packages/website-ue/src/components/Checks/index.tsx +++ b/packages/website-ue/src/components/Checks/index.tsx @@ -25,9 +25,9 @@ type Extension = status: 'error'; }; -export function Checks({ locale }: LocaleAcceptorProps) { +export function Checks({ localeData }: LocaleAcceptorProps) { return ( - + @@ -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() { This website utilizes cookies to perform analytics and personalize your experience. You can learn more through{' '} - + our privacy policy . By using BlitzKit, you also agree to our{' '} - + terms of service . diff --git a/packages/website-ue/src/components/Footer.astro b/packages/website-ue/src/components/Footer.astro index 70aed754b..190f461db 100644 --- a/packages/website-ue/src/components/Footer.astro +++ b/packages/website-ue/src/components/Footer.astro @@ -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); - + @@ -79,7 +83,7 @@ const strings = await getStrings(locale); sm: 'start', }} > - + {strings.website.footer.about.credits} {strings.website.footer.about.github} - + {strings.website.footer.about.patches} @@ -116,10 +120,14 @@ const strings = await getStrings(locale); > {strings.website.footer.legal.title} - + {strings.website.footer.legal.privacy} - + {strings.website.footer.legal.terms} diff --git a/packages/website-ue/src/components/LinkI18n.tsx b/packages/website-ue/src/components/LinkI18n.tsx index 9c38e3c34..3683be164 100644 --- a/packages/website-ue/src/components/LinkI18n.tsx +++ b/packages/website-ue/src/components/LinkI18n.tsx @@ -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 ( diff --git a/packages/website-ue/src/components/LocaleSwitcher.tsx b/packages/website-ue/src/components/LocaleSwitcher.tsx index 80850c4fd..1025ac782 100644 --- a/packages/website-ue/src/components/LocaleSwitcher.tsx +++ b/packages/website-ue/src/components/LocaleSwitcher.tsx @@ -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 ( - + ); } -export function LocaleSwitcher({ locale }: LocaleAcceptorProps) { +export function LocaleSwitcher({ localeData }: LocaleAcceptorProps) { return ( { localStorage.setItem('preferred-locale', locale); diff --git a/packages/website-ue/src/components/Navbar.astro b/packages/website-ue/src/components/Navbar.astro index db324b894..e5f25a1b6 100644 --- a/packages/website-ue/src/components/Navbar.astro +++ b/packages/website-ue/src/components/Navbar.astro @@ -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); - - diff --git a/packages/website-ue/src/components/TestComponent.tsx b/packages/website-ue/src/components/TestComponent.tsx deleted file mode 100644 index 6db40081c..000000000 --- a/packages/website-ue/src/components/TestComponent.tsx +++ /dev/null @@ -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 {strings.common.units.deg_s}; -} - -export function TestComponent({ locale }: LocaleAcceptorProps) { - return ( - - - - ); -} diff --git a/packages/website-ue/src/hooks/useLocale.tsx b/packages/website-ue/src/hooks/useLocale.tsx index fe7528d97..3a0d5b098 100644 --- a/packages/website-ue/src/hooks/useLocale.tsx +++ b/packages/website-ue/src/hooks/useLocale.tsx @@ -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(null); + +const LocaleContext = createContext<{ + locale: string; + gameStrings: Record; +} | null>(null); export interface LocaleAcceptorProps { - locale: string; + localeData: { + locale: string; + gameStrings?: Record; + }; } 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 ( - {children} + + {children} + ); } @@ -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 }; } diff --git a/packages/website-ue/src/layouts/BaseLayout.astro b/packages/website-ue/src/layouts/BaseLayout.astro index aaebf04f9..c3bfc2279 100644 --- a/packages/website-ue/src/layouts/BaseLayout.astro +++ b/packages/website-ue/src/layouts/BaseLayout.astro @@ -14,6 +14,7 @@ export interface Props { } const { noFooter, noNavbar, integrated, locale } = Astro.props; +const localeData = { locale }; --- @@ -69,11 +70,11 @@ const { noFooter, noNavbar, integrated, locale } = Astro.props; - + - {!noFooter &&