feat: localization
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
export const cardErrorLocs = {
|
||||
en: {
|
||||
private: "The showcase for this UID is private",
|
||||
notInShowcase: "Character not found in showcase",
|
||||
unsupported:
|
||||
"Unsupported character, avatar store doesn't have this character",
|
||||
uidFormat: "Invalid UID format",
|
||||
notFound: "UID not found on enka.network",
|
||||
maintenance: "enka.network is under maintenance",
|
||||
ratelimit: "Too many requests to enka.network, please try again later",
|
||||
internal: "Internal server error",
|
||||
},
|
||||
th: {
|
||||
private: "โปรไฟล์ UID นี้ตั้งค่าเป็นส่วนตัว",
|
||||
notInShowcase: "ไม่พบตัวละครในตั้งโชว์",
|
||||
unsupported: "ไม่รองรับตัวละครนี้",
|
||||
uidFormat: "รูปแบบ UID ไม่ถูกต้อง",
|
||||
notFound: "ไม่พบ UID บน enka.network",
|
||||
maintenance: "enka.network กำลังปิดปรับปรุง",
|
||||
ratelimit: "เรียก API enka.network มากเกินไป กรุณาลองใหม่ภายหลัง",
|
||||
internal: "เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์",
|
||||
},
|
||||
} as const;
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { AvatarElement, ResolvedAvatarInfo } from "enka.ts";
|
||||
import { EnkaLocs } from "@/type/enka";
|
||||
import { EnkaLocs, EnkaLocsLanguage } from "@/type/enka";
|
||||
import { FightPropMapEnum, PercentageFightProp } from "@/type/fightPropMap";
|
||||
|
||||
// Types
|
||||
@@ -141,7 +141,7 @@ function formatValue(
|
||||
*/
|
||||
export function buildDisplayRows(
|
||||
apiPayload: ResolvedAvatarInfo,
|
||||
locs: EnkaLocs,
|
||||
locs: EnkaLocsLanguage,
|
||||
opts: BuildDisplayRowsOptions = {},
|
||||
): DisplayRow[] {
|
||||
const enumMap = invertEnum(FightPropMapEnum);
|
||||
@@ -195,7 +195,7 @@ export function buildDisplayRows(
|
||||
rows.push({
|
||||
id,
|
||||
key: keyName as keyof typeof FightPropMapEnum,
|
||||
name: locs.en[keyName].replace("Max ", ""),
|
||||
name: locs[keyName].replace("Max ", ""),
|
||||
value: Number(value),
|
||||
formatted: formatValue(keyName, value, PercentageFightProp),
|
||||
base: computedBase,
|
||||
|
||||
+20
-3
@@ -103,9 +103,26 @@ export interface RelicStore {
|
||||
Sets: RelicSetMap;
|
||||
}
|
||||
|
||||
export interface EnkaLocs {
|
||||
en: Record<string, string>;
|
||||
}
|
||||
export type EnkaLocsLanguage = Record<string, string>;
|
||||
|
||||
export type EnkaLocs = {
|
||||
[k in
|
||||
| "zh-cn"
|
||||
| "zh-tw"
|
||||
| "de"
|
||||
| "en"
|
||||
| "es"
|
||||
| "fr"
|
||||
| "id"
|
||||
| "ja"
|
||||
| "ko"
|
||||
| "pt"
|
||||
| "ru"
|
||||
| "th"
|
||||
| "vi"
|
||||
| "tr"
|
||||
| "it"]: EnkaLocsLanguage;
|
||||
};
|
||||
|
||||
export const SubstatOrder: Record<string, number> = {
|
||||
FIGHT_PROP_CRITICAL: 1,
|
||||
|
||||
+70
-45
@@ -24,6 +24,7 @@ import { WeaponGlazeSvg } from "@/assets/weapon-glaze";
|
||||
import type { App } from "@/index";
|
||||
import { AssetManager, getEnkaStores } from "@/lib/assets";
|
||||
import { Deduplicator } from "@/lib/dedupe";
|
||||
import { cardErrorLocs } from "@/lib/locs";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
import { mockData } from "@/lib/mock";
|
||||
import { buildDisplayRows } from "@/lib/rows";
|
||||
@@ -102,13 +103,14 @@ export function card(app: App) {
|
||||
async ({
|
||||
params: { uid, char: charId },
|
||||
logger,
|
||||
query: { debug, h: height = 1024, error: errorType },
|
||||
query: { debug, h: heightRaw = 1024, w, error: errorType, lang },
|
||||
}) => {
|
||||
logger`Rendering ${uid}/${charId}`;
|
||||
const error =
|
||||
errorType === "card"
|
||||
? (_: number, message: string) => ErrorPage(message)
|
||||
: status;
|
||||
const errorLoc = cardErrorLocs[lang === "th" ? "th" : "en"];
|
||||
try {
|
||||
// #region Data fetching
|
||||
const [[avatarStore, weaponStore, relicStore, locs], data] =
|
||||
@@ -124,19 +126,18 @@ export function card(app: App) {
|
||||
)
|
||||
: mockData,
|
||||
]);
|
||||
|
||||
const enkaLoc = locs[lang ?? "en"];
|
||||
|
||||
if (typeof data.avatarInfoList === "undefined")
|
||||
return error(400, "The showcase for this UID is private");
|
||||
return error(400, errorLoc.private);
|
||||
const char = data.avatarInfoList.find(
|
||||
(c) => c.avatar.avatarId === Number(charId),
|
||||
);
|
||||
if (!char) return error(400, "Character not found in showcase");
|
||||
if (!char) return error(400, errorLoc.notInShowcase);
|
||||
|
||||
const avatar = avatarStore[char.avatar.avatarId];
|
||||
if (!avatar)
|
||||
return error(
|
||||
400,
|
||||
"Unsupported character, avatar store doesn't have this character",
|
||||
);
|
||||
if (!avatar) return error(400, errorLoc.unsupported);
|
||||
|
||||
if (!char.equipList)
|
||||
return error(501, "INTERNAL: No equipList provided");
|
||||
@@ -147,9 +148,12 @@ export function card(app: App) {
|
||||
store: weaponStore[weaponRaw.itemId ?? "Unknown"],
|
||||
};
|
||||
|
||||
const cacheKey = hash(JSON.stringify({ data, charId })).toString(
|
||||
36,
|
||||
);
|
||||
const height =
|
||||
typeof w === "number" ? w / (1218 / 508) : heightRaw;
|
||||
|
||||
const cacheKey = hash(
|
||||
JSON.stringify({ data, charId, lang, height }),
|
||||
).toString(36);
|
||||
if (feature("API")) {
|
||||
if (!debug) {
|
||||
const cached = cache.get<ImageResponse>(cacheKey);
|
||||
@@ -167,7 +171,7 @@ export function card(app: App) {
|
||||
20 +
|
||||
(charLvl > 90 ? 10 : 0);
|
||||
|
||||
const statRows = buildDisplayRows(char, locs);
|
||||
const statRows = buildDisplayRows(char, enkaLoc);
|
||||
|
||||
const artifacts = (
|
||||
Object.keys(EquipType) as (keyof typeof EquipType)[]
|
||||
@@ -198,7 +202,7 @@ export function card(app: App) {
|
||||
const artifactSets = Object.entries(setCounts)
|
||||
.filter(([, count]) => count >= 2)
|
||||
.map(([set, count]) => ({
|
||||
set: locs.en[relicStore.Sets[set].Name],
|
||||
set: enkaLoc[relicStore.Sets[set].Name],
|
||||
count,
|
||||
}));
|
||||
|
||||
@@ -313,7 +317,7 @@ export function card(app: App) {
|
||||
<img
|
||||
src="/avatar"
|
||||
alt="char"
|
||||
tw="object-cover h-full aspect-[1024/1124] absolute top-0 -left-[2.5em] scale-x-[1.1] scale-y-[1.15]"
|
||||
tw="object-cover h-full aspect-[1024/1124] absolute top-0 -left-[2.0em] scale-x-[1.1] scale-y-[1.15]"
|
||||
/>
|
||||
|
||||
{/* left section */}
|
||||
@@ -482,7 +486,7 @@ export function card(app: App) {
|
||||
"0 .08em .1em rgba(0,0,0,.4),0 .1em .3em rgba(0,0,0,.4)",
|
||||
}}
|
||||
>
|
||||
<span>{locs.en[weapon.store.NameTextMapHash]}</span>
|
||||
<span>{enkaLoc[weapon.store.NameTextMapHash]}</span>
|
||||
</div>
|
||||
{/* stats */}
|
||||
<div tw="pt-[3%] pb-[2%] flex gap-[.5em]">
|
||||
@@ -700,18 +704,15 @@ export function card(app: App) {
|
||||
return res.clone();
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidFormatError)
|
||||
return error(400, "Invalid UID format");
|
||||
return error(400, errorLoc.uidFormat);
|
||||
else if (err instanceof NotFoundError)
|
||||
return error(404, "UID not found on enka.network");
|
||||
return error(404, errorLoc.notFound);
|
||||
else if (err instanceof MaintenanceError)
|
||||
return error(424, "enka.network is under maintenance");
|
||||
return error(424, errorLoc.maintenance);
|
||||
else if (err instanceof RatelimitError)
|
||||
return error(
|
||||
429,
|
||||
"Too many requests to enka.network, please try again later",
|
||||
);
|
||||
return error(429, errorLoc.ratelimit);
|
||||
else if (err instanceof InternalError)
|
||||
return status(502, "Internal server error");
|
||||
return status(502, errorLoc.internal);
|
||||
console.error(err);
|
||||
try {
|
||||
return error(500, inspect(err) as "");
|
||||
@@ -727,28 +728,14 @@ export function card(app: App) {
|
||||
description: "Genshin Impact UID",
|
||||
}),
|
||||
char: t.Number({
|
||||
examples: ["10000120"],
|
||||
examples: [10000120],
|
||||
description: "Character ID",
|
||||
}),
|
||||
}),
|
||||
query: t.Object(
|
||||
{
|
||||
debug: t.Optional(
|
||||
t.Boolean({
|
||||
description: "Return debug information instead of card.",
|
||||
}),
|
||||
),
|
||||
h: t.Optional(
|
||||
t.Number({ default: 1024, description: "Height of card." }),
|
||||
),
|
||||
w: t.Optional(
|
||||
t.Number({
|
||||
description:
|
||||
"Target width of card, used to calculate height. Has higher priority than `h`.",
|
||||
}),
|
||||
),
|
||||
error: t.Optional(
|
||||
t.Enum(
|
||||
query: t.Partial(
|
||||
t.Object(
|
||||
{
|
||||
error: t.Enum(
|
||||
{ http: "http", card: "card" },
|
||||
{
|
||||
default: "http",
|
||||
@@ -756,9 +743,47 @@ export function card(app: App) {
|
||||
"Type of error to return when card fails to render.",
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: true },
|
||||
lang: t.Enum(
|
||||
Object.fromEntries(
|
||||
(
|
||||
[
|
||||
"zh-cn",
|
||||
"zh-tw",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"id",
|
||||
"ja",
|
||||
"ko",
|
||||
"pt",
|
||||
"ru",
|
||||
"th",
|
||||
"vi",
|
||||
"tr",
|
||||
"it",
|
||||
] as const
|
||||
).map((v) => [v, v]),
|
||||
),
|
||||
{
|
||||
default: "en",
|
||||
},
|
||||
),
|
||||
h: t.Number({
|
||||
default: 1024,
|
||||
description: "Height of card.",
|
||||
}),
|
||||
w: t.Number({
|
||||
description:
|
||||
"Target width of card, used to calculate height. Has higher priority than `h`.",
|
||||
}),
|
||||
debug: t.Boolean({
|
||||
description: "Return debug information instead of card.",
|
||||
default: false,
|
||||
}),
|
||||
},
|
||||
{ additionalProperties: true },
|
||||
),
|
||||
),
|
||||
detail: {
|
||||
tags: ["Card"],
|
||||
|
||||
Reference in New Issue
Block a user