feat : make export image be english text ig
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { GuideImageExport } from "@/components/public/guide-image-export";
|
||||
import { ConstellationImage } from "@/components/public/constellation-image";
|
||||
import type { PublicGuide } from "@/lib/guides/queries";
|
||||
import { exportMessages, exportStatLabel, exportText, withoutNotesFileName, type ExportMessageKey } from "@/lib/guides/export-text";
|
||||
|
||||
describe("export translations", () => {
|
||||
it("keeps matching keys and named placeholders in both languages", () => {
|
||||
expect(Object.keys(exportMessages.th).sort()).toEqual(Object.keys(exportMessages.en).sort());
|
||||
for (const key of Object.keys(exportMessages.en) as ExportMessageKey[]) {
|
||||
const placeholders = (text: string) => [...text.matchAll(/\{(\w+)\}/gu)].map((match) => match[1]).sort();
|
||||
expect(placeholders(exportMessages.th[key]), key).toEqual(placeholders(exportMessages.en[key]));
|
||||
expect(exportMessages.en[key], key).not.toMatch(/[\u0e00-\u0e7f]/u);
|
||||
}
|
||||
});
|
||||
|
||||
it("defaults to English and interpolates names without interpreting their contents", () => {
|
||||
expect(exportText("weaponTitle", { name: "Raiden" })).toBe("Raiden’s Weapon Comparison");
|
||||
expect(exportText("rotation", { seconds: "20.5" })).toBe("Rotation duration: 20.5 seconds");
|
||||
expect(exportText("weaponTitle", { name: "{seconds}" })).toBe("{seconds}’s Weapon Comparison");
|
||||
expect(exportText("condition", { index: 2 }, "th")).toBe("เงื่อนไข 2");
|
||||
});
|
||||
|
||||
it("uses English stat names for exports while retaining Thai page labels and custom values", () => {
|
||||
expect(exportStatLabel("EM")).toBe("Elemental Mastery");
|
||||
expect(exportStatLabel("CRIT DAMAGE")).toBe("CRIT DMG");
|
||||
expect(exportStatLabel("EM", "th")).toBe("ความชำนาญธาตุ");
|
||||
expect(exportStatLabel("Pyro DMG%")).toBe("Pyro DMG%");
|
||||
expect(exportStatLabel("Custom value")).toBe("Custom value");
|
||||
});
|
||||
|
||||
it("distinguishes no-note downloads while preserving the image extension", () => {
|
||||
expect(withoutNotesFileName("guide.png")).toBe("guide-without-notes.png");
|
||||
expect(withoutNotesFileName("guide.v2.webp")).toBe("guide.v2-without-notes.webp");
|
||||
expect(withoutNotesFileName("guide")).toBe("guide-without-notes");
|
||||
});
|
||||
});
|
||||
|
||||
describe("image export controls", () => {
|
||||
it("always offers both export modes even without notes", () => {
|
||||
const html = renderToStaticMarkup(<GuideImageExport fileName="guide.png">No notes</GuideImageExport>);
|
||||
expect(html).toContain("Export image");
|
||||
expect(html).toContain("Export without note");
|
||||
expect(html.match(/<button\b/gu)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("disables both modes when an export selection is empty", () => {
|
||||
const html = renderToStaticMarkup(<GuideImageExport fileName="guide.png" disabled>No sections</GuideImageExport>);
|
||||
expect(html.match(/\sdisabled=""/gu)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it.each(["English note", "โน้ตภาษาไทย"])("marks constellation notes for omission regardless of language: %s", (note) => {
|
||||
const guide = { character: { name: "Raiden", splashImageUrl: "/splash.png" }, coverUrl: "/cover.png" } as PublicGuide;
|
||||
const group = { rows: [], note } as unknown as PublicGuide["constellationGroups"][number];
|
||||
const html = renderToStaticMarkup(createElement(ConstellationImage, { guide, group }));
|
||||
expect(html).toContain("Raiden’s Constellations");
|
||||
expect(html).toContain("Team Damage");
|
||||
expect(html).toContain('data-export-note="true"');
|
||||
expect(html).toContain(note);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
export type Locale = "th" | "en";
|
||||
|
||||
const english = {
|
||||
exportImage: "Export image",
|
||||
withoutNote: "Export without note",
|
||||
exporting: "Exporting…",
|
||||
exportError: "Could not export the image. Please try again.",
|
||||
damageTitle: "Choose a damage metric",
|
||||
damageDescription: "Choose the damage metric for your weapon comparison image.",
|
||||
overallDamage: "Overall Damage",
|
||||
personalDamage: "Personal Damage",
|
||||
weaponTitle: "{name}’s Weapon Comparison",
|
||||
condition: "Condition {index}",
|
||||
sectionTitle: "Choose what to export",
|
||||
sectionDescription: "Select individual sections or choose All sections to export everything.",
|
||||
allSections: "All sections",
|
||||
tierlist: "Artifact Tier List",
|
||||
bestStats: "Best Main Stats & Substats",
|
||||
targetStats: "Target Stats",
|
||||
talents: "Talent Priority",
|
||||
addNote: "Include note",
|
||||
recommendedCondition: "Recommended stats condition",
|
||||
targetCondition: "Target stats condition",
|
||||
mainStats: "Best Main Stats",
|
||||
substats: "Best Substats",
|
||||
sands: "Sands",
|
||||
goblet: "Goblet",
|
||||
circlet: "Circlet",
|
||||
artifactRecommendation: "Recommended Artifacts & Stats",
|
||||
artifactTitle: "Artifacts & Stats",
|
||||
lateGame: "Late Game",
|
||||
earlyGame: "Early Game",
|
||||
minimumStats: "Recommended Minimum Stats",
|
||||
level: "Level {level}+",
|
||||
normalAttack: "Normal Attack",
|
||||
skill: "Elemental Skill",
|
||||
burst: "Elemental Burst",
|
||||
note: "Notes",
|
||||
extraGuide: "Additional Guide",
|
||||
missingItem: "Item unavailable",
|
||||
constellationTitle: "{name}’s Constellations",
|
||||
teamDamage: "Team Damage",
|
||||
rotation: "Rotation duration: {seconds} seconds",
|
||||
teamCredit: "By YT Genshin Is Not a Mobile Game",
|
||||
disclaimer: "Results cannot be directly compared with other theorycrafters’ calculations,\nas each uses different stats.\nThe figures shown may change in future updates.",
|
||||
overviewDownloaded: "Overview image downloaded.",
|
||||
downloadPng: "Download PNG {width}×{height}",
|
||||
materials: "MATERIALS",
|
||||
materialLevel: "LV.{level}"
|
||||
} as const;
|
||||
|
||||
export type ExportMessageKey = keyof typeof english;
|
||||
|
||||
const thai: Record<ExportMessageKey, string> = {
|
||||
exportImage: "ส่งออกภาพ",
|
||||
withoutNote: "ส่งออกภาพโดยไม่รวมโน้ต",
|
||||
exporting: "กำลังส่งออก…",
|
||||
exportError: "ส่งออกภาพไม่สำเร็จ กรุณาลองอีกครั้ง",
|
||||
damageTitle: "เลือกประเภทดาเมจ",
|
||||
damageDescription: "เลือกค่าที่ต้องการใช้สร้างภาพเปรียบเทียบอาวุธ",
|
||||
overallDamage: "ดาเมจทั้งทีม",
|
||||
personalDamage: "ดาเมจส่วนตัว",
|
||||
weaponTitle: "เปรียบเทียบอาวุธของ {name}",
|
||||
condition: "เงื่อนไข {index}",
|
||||
sectionTitle: "เลือกข้อมูลสำหรับส่งออก",
|
||||
sectionDescription: "เลือกได้หลายส่วน หรือเลือกทั้งหมดเพื่อส่งออกทุกส่วน",
|
||||
allSections: "ทั้งหมด",
|
||||
tierlist: "อันดับอาร์ติแฟกต์",
|
||||
bestStats: "ออฟหลักและออฟรองที่ดีที่สุด",
|
||||
targetStats: "ค่าสถานะเป้าหมาย",
|
||||
talents: "ลำดับการอัพพรสวรรค์",
|
||||
addNote: "รวมโน้ต",
|
||||
recommendedCondition: "เงื่อนไขค่าสถานะแนะนำ",
|
||||
targetCondition: "เงื่อนไขค่าสถานะเป้าหมาย",
|
||||
mainStats: "ออฟหลักที่ดีที่สุด",
|
||||
substats: "ออฟรองที่ดีที่สุด",
|
||||
sands: "นาฬิกา",
|
||||
goblet: "ถ้วย",
|
||||
circlet: "หัว",
|
||||
artifactRecommendation: "อาร์ติแฟกต์และสแตทที่แนะนำ",
|
||||
artifactTitle: "Artifact & Stats",
|
||||
lateGame: "เลทเกม",
|
||||
earlyGame: "ต้นเกม",
|
||||
minimumStats: "ค่าสถานะต่ำๆที่ควรจะปั้นไว้",
|
||||
level: "เลเวล {level}+",
|
||||
normalAttack: "ตีปกติ",
|
||||
skill: "สกิลธาตุ",
|
||||
burst: "ท่าไม้ตาย",
|
||||
note: "โน็ต",
|
||||
extraGuide: "คู่มือเสริม",
|
||||
missingItem: "ไม่พบรายการ",
|
||||
constellationTitle: "กลุ่มดาวของ {name}",
|
||||
teamDamage: "ดาเมจทั้งทีม",
|
||||
rotation: "ระยะเวลาโรเทชั่น {seconds} วิ",
|
||||
teamCredit: "By YT เกนชินไม่ใช่เกมมือถือ",
|
||||
disclaimer: "ไม่สามารถเอาไปเปรียบเทียบกับการคำนวณของ TC ท่านอื่นๆได้\nเพราะแต่ละคนใช้สแตทในการคำนวณที่ไม่เหมือนกัน\nและตารางที่เห็นอาจมีการเปลี่ยนแปลงได้ในอนาคต",
|
||||
overviewDownloaded: "ดาวน์โหลดภาพ Overview แล้ว",
|
||||
downloadPng: "ดาวน์โหลด PNG {width}×{height}",
|
||||
materials: "วัสดุ",
|
||||
materialLevel: "เลเวล {level}"
|
||||
};
|
||||
|
||||
export const exportMessages = { en: english, th: thai };
|
||||
|
||||
export function exportText(
|
||||
key: ExportMessageKey,
|
||||
params: Record<string, string | number> = {},
|
||||
locale: Locale = "en",
|
||||
): string {
|
||||
return exportMessages[locale][key].replace(
|
||||
/\{(\w+)\}/gu,
|
||||
(placeholder, name: string) => String(params[name] ?? placeholder),
|
||||
);
|
||||
}
|
||||
|
||||
export function withoutNotesFileName(fileName: string): string {
|
||||
return /\.[^.]+$/u.test(fileName)
|
||||
? fileName.replace(/(\.[^.]+)$/u, "-without-notes$1")
|
||||
: `${fileName}-without-notes`;
|
||||
}
|
||||
|
||||
const thaiStats: Record<string, string> = {
|
||||
HP: "พลังชีวิต",
|
||||
ATK: "พลังโจมตี",
|
||||
DEF: "พลังป้องกัน",
|
||||
EM: "ความชำนาญธาตุ",
|
||||
ER: "ฟื้นฟูพลังงาน",
|
||||
"CRIT RATE": "อัตราคริ",
|
||||
"CRIT DAMAGE": "ความแรงคริ",
|
||||
"ATK%": "พลังโจมตี%",
|
||||
"HP%": "พลังชีวิต%",
|
||||
"DEF%": "พลังป้องกัน%",
|
||||
"CRIT Rate%/CRIT DMG%": "อัตราคริ/ความแรงคริ",
|
||||
"Pyro DMG%": "ความเสียหายธาตุไฟ%",
|
||||
"Hydro DMG%": "ความเสียหายธาตุน้ำ%",
|
||||
"Cryo DMG%": "ความเสียหายธาตุน้ำแข็ง%",
|
||||
"Electro DMG%": "ความเสียหายธาตุไฟฟ้า%",
|
||||
"Anemo DMG%": "ความเสียหายธาตุลม%",
|
||||
"Geo DMG%": "ความเสียหายธาตุดิน%",
|
||||
"Dendro DMG%": "ความเสียหายธาตุไม้%",
|
||||
"Physical DMG%": "ความเสียหายกายภาพ%",
|
||||
"Healing Bonus%": "โบนัสการรักษา%",
|
||||
};
|
||||
|
||||
const englishStats: Record<string, string> = {
|
||||
HP: "HP",
|
||||
ATK: "ATK",
|
||||
DEF: "DEF",
|
||||
EM: "Elemental Mastery",
|
||||
ER: "Energy Recharge",
|
||||
"CRIT RATE": "CRIT Rate",
|
||||
"CRIT DAMAGE": "CRIT DMG",
|
||||
"CRIT Rate%/CRIT DMG%": "CRIT Rate / CRIT DMG",
|
||||
};
|
||||
|
||||
export function exportStatLabel(value: string, locale: Locale = "en"): string {
|
||||
return (locale === "th" ? thaiStats : englishStats)[value] ?? value;
|
||||
}
|
||||
Reference in New Issue
Block a user