mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-04 00:10:38 +01:00
feat: use decorators
This commit is contained in:
1 parent
44711a58c7
commit
8e3b5de249
5 files changed
+278
-316
No files matched your search
@@ -13,16 +13,23 @@ import type { Tanks } from "../../protos/tanks";
|
|||||||
export abstract class AbstractAPI {
|
export abstract class AbstractAPI {
|
||||||
abstract tankList(): Promise<TankList>;
|
abstract tankList(): Promise<TankList>;
|
||||||
abstract tank(id: string): Promise<Tank>;
|
abstract tank(id: string): Promise<Tank>;
|
||||||
|
abstract tanks(): Promise<Tanks>;
|
||||||
|
abstract popularTanks(): Promise<PopularTanks>;
|
||||||
|
|
||||||
private _tanksCache: Tanks | undefined;
|
abstract strings(locale: string): Promise<Strings>;
|
||||||
protected abstract _tanks(): Promise<Tanks>;
|
abstract gameStrings(locale: string): Promise<Record<string, string>>;
|
||||||
async tanks() {
|
protected abstract _groupedGameStrings(
|
||||||
if (this._tanksCache === undefined) {
|
locale: string,
|
||||||
this._tanksCache = await this._tanks();
|
group: string,
|
||||||
}
|
): Promise<Record<string, string>>;
|
||||||
|
abstract gameStringGroups(): Promise<string[]>;
|
||||||
|
|
||||||
return this._tanksCache;
|
abstract avatar(id: string): Promise<Avatar>;
|
||||||
}
|
abstract avatars(): Promise<Avatars>;
|
||||||
|
abstract avatarList(): Promise<AvatarList>;
|
||||||
|
|
||||||
|
abstract background(id: string): Promise<Background>;
|
||||||
|
abstract backgrounds(): Promise<Backgrounds>;
|
||||||
|
|
||||||
private assertLocale(locale: string) {
|
private assertLocale(locale: string) {
|
||||||
const supported = locales.supported.find(
|
const supported = locales.supported.find(
|
||||||
@@ -36,137 +43,26 @@ export abstract class AbstractAPI {
|
|||||||
return supported;
|
return supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _stringsCache: Record<string, Strings> = {};
|
@Cache<[string, string, boolean]>((...args) => args.join("-"))
|
||||||
protected abstract _strings(locale: string): Promise<Strings>;
|
|
||||||
async strings(locale: string) {
|
|
||||||
this.assertLocale(locale);
|
|
||||||
|
|
||||||
if (this._stringsCache[locale] === undefined) {
|
|
||||||
this._stringsCache[locale] = await this._strings(locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._stringsCache[locale];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _gameStringsCache: Record<string, Record<string, string>> = {};
|
|
||||||
protected abstract _gameStrings(
|
|
||||||
locale: string,
|
|
||||||
): Promise<Record<string, string>>;
|
|
||||||
async gameStrings(locale: string) {
|
|
||||||
this.assertLocale(locale);
|
|
||||||
|
|
||||||
if (this._gameStringsCache[locale] === undefined) {
|
|
||||||
this._gameStringsCache[locale] = await this._gameStrings(locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._gameStringsCache[locale];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _groupedGameStringsCache: Record<
|
|
||||||
string,
|
|
||||||
Record<string, Record<string, string>>
|
|
||||||
> = {};
|
|
||||||
protected abstract _groupedGameStrings(
|
|
||||||
locale: string,
|
|
||||||
group: string,
|
|
||||||
): Promise<Record<string, string>>;
|
|
||||||
async groupedGameStrings(locale: string, group: string, prefix: boolean) {
|
async groupedGameStrings(locale: string, group: string, prefix: boolean) {
|
||||||
this.assertLocale(locale);
|
this.assertLocale(locale);
|
||||||
|
|
||||||
if (this._groupedGameStringsCache[locale] === undefined) {
|
const gameStrings = await this._groupedGameStrings(locale, group);
|
||||||
this._groupedGameStringsCache[locale] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._groupedGameStringsCache[locale][group] === undefined) {
|
if (!prefix) return gameStrings;
|
||||||
this._groupedGameStringsCache[locale][group] =
|
|
||||||
await this._groupedGameStrings(locale, group);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prefix) return this._groupedGameStringsCache[locale][group];
|
|
||||||
|
|
||||||
const strings: Record<string, string> = {};
|
const strings: Record<string, string> = {};
|
||||||
|
|
||||||
for (const key in this._groupedGameStringsCache[locale][group]) {
|
for (const key in gameStrings) {
|
||||||
strings[`${group}__${key}`] =
|
strings[`${group}__${key}`] = gameStrings[key];
|
||||||
this._groupedGameStringsCache[locale][group][key];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings;
|
return strings;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _avatarCache: Record<string, Avatar> = {};
|
|
||||||
protected abstract _avatar(id: string): Promise<Avatar>;
|
|
||||||
async avatar(id: string) {
|
|
||||||
if (this._avatarCache[id] === undefined) {
|
|
||||||
this._avatarCache[id] = await this._avatar(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._avatarCache[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _avatarsCache: Avatars | undefined;
|
|
||||||
protected abstract _avatars(): Promise<Avatars>;
|
|
||||||
async avatars() {
|
|
||||||
if (this._avatarsCache === undefined) {
|
|
||||||
this._avatarsCache = await this._avatars();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._avatarsCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _avatarListCache: AvatarList | undefined;
|
|
||||||
protected abstract _avatarList(): Promise<AvatarList>;
|
|
||||||
async avatarList() {
|
|
||||||
if (this._avatarListCache === undefined) {
|
|
||||||
this._avatarListCache = await this._avatarList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._avatarListCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _backgroundCache: Record<string, Background> = {};
|
|
||||||
protected abstract _background(id: string): Promise<Background>;
|
|
||||||
async background(id: string) {
|
|
||||||
if (this._backgroundCache[id] === undefined) {
|
|
||||||
this._backgroundCache[id] = await this._background(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._backgroundCache[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _backgroundsCache: Backgrounds | undefined;
|
|
||||||
protected abstract _backgrounds(): Promise<Backgrounds>;
|
|
||||||
async backgrounds() {
|
|
||||||
if (this._backgroundsCache === undefined) {
|
|
||||||
this._backgroundsCache = await this._backgrounds();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._backgroundsCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _gameStringGroupsCache: string[] | undefined;
|
|
||||||
protected abstract _gameStringGroups(): Promise<string[]>;
|
|
||||||
async gameStringGroups() {
|
|
||||||
if (this._gameStringGroupsCache === undefined) {
|
|
||||||
this._gameStringGroupsCache = await this._gameStringGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._gameStringGroupsCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _popularTanksCache: PopularTanks | undefined;
|
|
||||||
protected abstract _popularTanks(): Promise<PopularTanks>;
|
|
||||||
async popularTanks() {
|
|
||||||
if (this._popularTanksCache === undefined) {
|
|
||||||
this._popularTanksCache = await this._popularTanks();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._popularTanksCache;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Cache<Arguments extends unknown[]>(
|
export function Cache<Arguments extends unknown[]>(
|
||||||
discriminator: (...args: Arguments) => string = () => "",
|
discriminator: (...args: Arguments) => string = (...args) => args.join("-"),
|
||||||
) {
|
) {
|
||||||
const cache = new WeakMap<object, Map<string, unknown>>();
|
const cache = new WeakMap<object, Map<string, unknown>>();
|
||||||
|
|
||||||
|
|||||||
@@ -3,34 +3,89 @@ import type { Strings } from "@blitzkit/i18n";
|
|||||||
import { Avatars } from "../../protos/avatars";
|
import { Avatars } from "../../protos/avatars";
|
||||||
import { PopularTanks } from "../../protos/popular_tanks";
|
import { PopularTanks } from "../../protos/popular_tanks";
|
||||||
import { Tank } from "../../protos/tank";
|
import { Tank } from "../../protos/tank";
|
||||||
import { AbstractAPI } from "./abstract";
|
import { AbstractAPI, Cache } from "./abstract";
|
||||||
|
|
||||||
|
const rejected = Promise.reject("Not implemented on client");
|
||||||
|
|
||||||
export class ClientAPI extends AbstractAPI {
|
export class ClientAPI extends AbstractAPI {
|
||||||
protected _avatars() {
|
@UnimplementedOnClient
|
||||||
return fetchPB("/api/avatars/all.pb", Avatars);
|
tankList() {
|
||||||
|
return rejected;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _gameStrings(): Promise<Record<string, string>> {
|
@Cache()
|
||||||
throw new Error("Cannot fetch full game strings on client");
|
tank(id: string) {
|
||||||
|
return fetchPB(`/api/tanks/${id}.pb`, Tank);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _groupedGameStrings(locale: string, group: string) {
|
@UnimplementedOnClient
|
||||||
const response = await fetch(`/api/game-strings/${locale}/${group}.json`);
|
tanks() {
|
||||||
const json = (await response.json()) as Record<string, string>;
|
return rejected;
|
||||||
return json;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _strings(locale: string) {
|
@Cache()
|
||||||
const response = await fetch(`/api/strings/${locale}.json`);
|
popularTanks() {
|
||||||
const json = (await response.json()) as Strings;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _popularTanks() {
|
|
||||||
return fetchPB("/api/tanks/popular.pb", PopularTanks);
|
return fetchPB("/api/tanks/popular.pb", PopularTanks);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _tank(id: string) {
|
@Cache()
|
||||||
return fetchPB(`/api/tanks/${id}.pb`, Tank);
|
async strings(locale: string) {
|
||||||
|
const response = await fetch(`/api/strings/${locale}.json`);
|
||||||
|
const json = (await response.json()) as Strings;
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnimplementedOnClient
|
||||||
|
gameStrings() {
|
||||||
|
return rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cache()
|
||||||
|
protected async _groupedGameStrings(locale: string, group: string) {
|
||||||
|
const response = await fetch(`/api/game-strings/${locale}/${group}.json`);
|
||||||
|
const json = (await response.json()) as Record<string, string>;
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnimplementedOnClient
|
||||||
|
gameStringGroups() {
|
||||||
|
return rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnimplementedOnClient
|
||||||
|
avatar() {
|
||||||
|
return rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnimplementedOnClient
|
||||||
|
avatars() {
|
||||||
|
return fetchPB("/api/avatars/all.pb", Avatars);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnimplementedOnClient
|
||||||
|
avatarList() {
|
||||||
|
return rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnimplementedOnClient
|
||||||
|
background() {
|
||||||
|
return rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnimplementedOnClient
|
||||||
|
backgrounds() {
|
||||||
|
return rejected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function UnimplementedOnClient(
|
||||||
|
_target: unknown,
|
||||||
|
propertyKey: string,
|
||||||
|
descriptor: PropertyDescriptor,
|
||||||
|
) {
|
||||||
|
descriptor.value = function () {
|
||||||
|
throw new Error(`${propertyKey} is not implemented on the client`);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -35,104 +35,6 @@ export class ServerAPI extends AbstractAPI {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _gameStrings(locale: string) {
|
|
||||||
const group = this.metadata.group("ClientConfigsEntity");
|
|
||||||
|
|
||||||
if (group.length !== 1) {
|
|
||||||
throw new RangeError(
|
|
||||||
`Don't know how to handle ${group.length} ClientConfigsEntities`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientConfig = this.metadata.item(group[0].id);
|
|
||||||
const localizationResources = clientConfig.LocalizationResources();
|
|
||||||
|
|
||||||
if (!localizationResources.remote_storage) {
|
|
||||||
throw new Error("Localization resources not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
let remoteStorage: RemoteStorageComponent | undefined = undefined;
|
|
||||||
|
|
||||||
for (const candidateId of localizationResources.remote_storage
|
|
||||||
.remote_urls) {
|
|
||||||
const item = this.metadata.item(candidateId);
|
|
||||||
const candidate = item.RemoteStorage();
|
|
||||||
|
|
||||||
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 strings: Record<string, string> = {};
|
|
||||||
const configPath = `${remoteStorage.url}/${remoteStorage.relative_path}/config.yaml`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const config = await fetch(configPath)
|
|
||||||
.then((response) => response.text())
|
|
||||||
.then((text) => parse(text) as LocalizationConfig);
|
|
||||||
|
|
||||||
for (const namespace of config.namespaces) {
|
|
||||||
Object.assign(
|
|
||||||
strings,
|
|
||||||
await fetch(
|
|
||||||
`${remoteStorage.url}${remoteStorage.relative_path}/${namespace}/${locale}.yaml`,
|
|
||||||
)
|
|
||||||
.then((response) => response.text())
|
|
||||||
.then((text) => {
|
|
||||||
const strings: Record<string, string> = {};
|
|
||||||
const parsed = parse(text) as Record<string, string>;
|
|
||||||
|
|
||||||
for (const key in parsed) {
|
|
||||||
strings[key] = parsed[key].replaceAll('\\"', '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
`Failed to fetch config from ${configPath}. Returning empty strings. This will throw an error in production. Error:`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async _groupedGameStrings(locale: string, group: string) {
|
|
||||||
const strings = await this.gameStrings(locale);
|
|
||||||
const filtered: Record<string, string> = {};
|
|
||||||
|
|
||||||
for (const key in strings) {
|
|
||||||
if (key.startsWith(`${group}__`)) {
|
|
||||||
const trimmed = key.substring(group.length + 2);
|
|
||||||
filtered[trimmed] = strings[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async _strings(locale: string) {
|
|
||||||
const localized = (await globbedStrings[
|
|
||||||
`../../../../i18n/strings/${locale}.json`
|
|
||||||
]()) as DeepPartial<Strings>;
|
|
||||||
const defaults = (await globbedStrings[
|
|
||||||
`../../../../i18n/strings/${locales.default}.json`
|
|
||||||
]()) as Strings;
|
|
||||||
const strings = merge({}, defaults, localized);
|
|
||||||
|
|
||||||
return strings;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Cache()
|
@Cache()
|
||||||
async tankList() {
|
async tankList() {
|
||||||
const group = this.metadata.group("TankEntity");
|
const group = this.metadata.group("TankEntity");
|
||||||
@@ -157,18 +59,7 @@ export class ServerAPI extends AbstractAPI {
|
|||||||
return { list };
|
return { list };
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _tanks() {
|
@Cache()
|
||||||
const tankList = await this.tankList();
|
|
||||||
const data: Tanks = { tanks: {} };
|
|
||||||
|
|
||||||
for (const { id } of tankList.list) {
|
|
||||||
data.tanks[id] = await this.tank(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Cache<[string]>((id) => id)
|
|
||||||
async tank(id: string) {
|
async tank(id: string) {
|
||||||
const tankList = await this.tankList();
|
const tankList = await this.tankList();
|
||||||
const tankListEntry = tankList.list.find((tank) => tank.id === id);
|
const tankListEntry = tankList.list.find((tank) => tank.id === id);
|
||||||
@@ -185,70 +76,20 @@ export class ServerAPI extends AbstractAPI {
|
|||||||
return { id, slug, tank, compensation } satisfies Tank;
|
return { id, slug, tank, compensation } satisfies Tank;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _avatars() {
|
@Cache()
|
||||||
const avatars: Avatar[] = [];
|
async tanks() {
|
||||||
|
const tankList = await this.tankList();
|
||||||
|
const data: Tanks = { tanks: {} };
|
||||||
|
|
||||||
for (const item of this.metadata.group("ProfileAvatarEntity")) {
|
for (const { id } of tankList.list) {
|
||||||
avatars.push(await this.avatar(item.name));
|
data.tanks[id] = await this.tank(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { avatars };
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _avatarList() {
|
@Cache()
|
||||||
const avatars = await this.avatars();
|
async popularTanks() {
|
||||||
const list = avatars.avatars.map((avatar) => avatar.name);
|
|
||||||
|
|
||||||
return { list };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async _avatar(id: string) {
|
|
||||||
const avatar = this.metadata.item(`ProfileAvatarEntity.${id}`);
|
|
||||||
const name = avatar.name;
|
|
||||||
const stuff_ui = avatar.StuffUI("UIComponent");
|
|
||||||
const profile_avatar = avatar.ProfileAvatar();
|
|
||||||
const sellable = avatar.components.sellableComponent
|
|
||||||
? avatar.Sellable()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return { name, stuff_ui, profile_avatar, sellable };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async _backgrounds() {
|
|
||||||
const backgrounds: Background[] = [];
|
|
||||||
|
|
||||||
for (const item of this.metadata.group("ProfileBackgroundEntity")) {
|
|
||||||
backgrounds.push(await this.background(item.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
return { backgrounds };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async _background(id: string) {
|
|
||||||
const background = this.metadata.item(`ProfileBackgroundEntity.${id}`);
|
|
||||||
const name = background.name;
|
|
||||||
const stuff_ui = background.StuffUI("UIComponent");
|
|
||||||
const profile_background = background.ProfileBackground();
|
|
||||||
const sellable = background.components.sellableComponent
|
|
||||||
? background.Sellable()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return { name, stuff_ui, profile_background, sellable };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async _gameStringGroups() {
|
|
||||||
const defaultGameStrings = await this.gameStrings(locales.default);
|
|
||||||
const groups = new Set<string>();
|
|
||||||
|
|
||||||
for (const key in defaultGameStrings) {
|
|
||||||
const [group] = key.split("__");
|
|
||||||
groups.add(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(groups.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async _popularTanks() {
|
|
||||||
if (!existsSync("../../temp/popular.json")) {
|
if (!existsSync("../../temp/popular.json")) {
|
||||||
const { tanks } = await this.tanks();
|
const { tanks } = await this.tanks();
|
||||||
const tanksArray = Object.values(tanks);
|
const tanksArray = Object.values(tanks);
|
||||||
@@ -332,4 +173,174 @@ export class ServerAPI extends AbstractAPI {
|
|||||||
const data = await readFile("../../temp/popular.json", "utf-8");
|
const data = await readFile("../../temp/popular.json", "utf-8");
|
||||||
return JSON.parse(data) as PopularTanks;
|
return JSON.parse(data) as PopularTanks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Cache()
|
||||||
|
async strings(locale: string) {
|
||||||
|
const localized = (await globbedStrings[
|
||||||
|
`../../../../i18n/strings/${locale}.json`
|
||||||
|
]()) as DeepPartial<Strings>;
|
||||||
|
const defaults = (await globbedStrings[
|
||||||
|
`../../../../i18n/strings/${locales.default}.json`
|
||||||
|
]()) as Strings;
|
||||||
|
const strings = merge({}, defaults, localized);
|
||||||
|
|
||||||
|
return strings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cache()
|
||||||
|
async gameStrings(locale: string) {
|
||||||
|
const group = this.metadata.group("ClientConfigsEntity");
|
||||||
|
|
||||||
|
if (group.length !== 1) {
|
||||||
|
throw new RangeError(
|
||||||
|
`Don't know how to handle ${group.length} ClientConfigsEntities`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientConfig = this.metadata.item(group[0].id);
|
||||||
|
const localizationResources = clientConfig.LocalizationResources();
|
||||||
|
|
||||||
|
if (!localizationResources.remote_storage) {
|
||||||
|
throw new Error("Localization resources not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let remoteStorage: RemoteStorageComponent | undefined = undefined;
|
||||||
|
|
||||||
|
for (const candidateId of localizationResources.remote_storage
|
||||||
|
.remote_urls) {
|
||||||
|
const item = this.metadata.item(candidateId);
|
||||||
|
const candidate = item.RemoteStorage();
|
||||||
|
|
||||||
|
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 strings: Record<string, string> = {};
|
||||||
|
const configPath = `${remoteStorage.url}/${remoteStorage.relative_path}/config.yaml`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = await fetch(configPath)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((text) => parse(text) as LocalizationConfig);
|
||||||
|
|
||||||
|
for (const namespace of config.namespaces) {
|
||||||
|
Object.assign(
|
||||||
|
strings,
|
||||||
|
await fetch(
|
||||||
|
`${remoteStorage.url}${remoteStorage.relative_path}/${namespace}/${locale}.yaml`,
|
||||||
|
)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((text) => {
|
||||||
|
const strings: Record<string, string> = {};
|
||||||
|
const parsed = parse(text) as Record<string, string>;
|
||||||
|
|
||||||
|
for (const key in parsed) {
|
||||||
|
strings[key] = parsed[key].replaceAll('\\"', '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`Failed to fetch config from ${configPath}. Returning empty strings. This will throw an error in production. Error:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cache()
|
||||||
|
protected async _groupedGameStrings(locale: string, group: string) {
|
||||||
|
const strings = await this.gameStrings(locale);
|
||||||
|
const filtered: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const key in strings) {
|
||||||
|
if (key.startsWith(`${group}__`)) {
|
||||||
|
const trimmed = key.substring(group.length + 2);
|
||||||
|
filtered[trimmed] = strings[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cache()
|
||||||
|
async gameStringGroups() {
|
||||||
|
const defaultGameStrings = await this.gameStrings(locales.default);
|
||||||
|
const groups = new Set<string>();
|
||||||
|
|
||||||
|
for (const key in defaultGameStrings) {
|
||||||
|
const [group] = key.split("__");
|
||||||
|
groups.add(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(groups.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cache()
|
||||||
|
async avatar(id: string) {
|
||||||
|
const avatar = this.metadata.item(`ProfileAvatarEntity.${id}`);
|
||||||
|
const name = avatar.name;
|
||||||
|
const stuff_ui = avatar.StuffUI("UIComponent");
|
||||||
|
const profile_avatar = avatar.ProfileAvatar();
|
||||||
|
const sellable = avatar.components.sellableComponent
|
||||||
|
? avatar.Sellable()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return { name, stuff_ui, profile_avatar, sellable };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cache()
|
||||||
|
async avatars() {
|
||||||
|
const avatars: Avatar[] = [];
|
||||||
|
|
||||||
|
for (const item of this.metadata.group("ProfileAvatarEntity")) {
|
||||||
|
avatars.push(await this.avatar(item.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { avatars };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cache()
|
||||||
|
async avatarList() {
|
||||||
|
const avatars = await this.avatars();
|
||||||
|
const list = avatars.avatars.map((avatar) => avatar.name);
|
||||||
|
|
||||||
|
return { list };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cache()
|
||||||
|
async background(id: string) {
|
||||||
|
const background = this.metadata.item(`ProfileBackgroundEntity.${id}`);
|
||||||
|
const name = background.name;
|
||||||
|
const stuff_ui = background.StuffUI("UIComponent");
|
||||||
|
const profile_background = background.ProfileBackground();
|
||||||
|
const sellable = background.components.sellableComponent
|
||||||
|
? background.Sellable()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return { name, stuff_ui, profile_background, sellable };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cache()
|
||||||
|
async backgrounds() {
|
||||||
|
const backgrounds: Background[] = [];
|
||||||
|
|
||||||
|
for (const item of this.metadata.group("ProfileBackgroundEntity")) {
|
||||||
|
backgrounds.push(await this.background(item.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { backgrounds };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,8 @@ import { useLocale } from "./useLocale";
|
|||||||
export function useGameStrings(group: string) {
|
export function useGameStrings(group: string) {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const strings = useAwait(
|
const strings = useAwait(
|
||||||
() => api.groupedGameStrings(locale, group, true),
|
() => api._groupedGameStrings(locale, group, true),
|
||||||
`game-strings-${locale}-${group}`
|
`game-strings-${locale}-${group}`,
|
||||||
);
|
);
|
||||||
return strings;
|
return strings;
|
||||||
}
|
}
|
||||||
@@ -20,10 +20,10 @@ export async function GET({
|
|||||||
}: {
|
}: {
|
||||||
params: { locale: string; group: string };
|
params: { locale: string; group: string };
|
||||||
}) {
|
}) {
|
||||||
const strings = await api.groupedGameStrings(
|
const strings = await api._groupedGameStrings(
|
||||||
params.locale,
|
params.locale,
|
||||||
params.group,
|
params.group,
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
return Response.json(strings);
|
return Response.json(strings);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user