fix : overhual unnatural text
CI / Verify (push) Failing after 1m19s
CI / Build immutable images and deploy (push) Has been skipped

This commit is contained in:
2026-09-14 18:27:20 +07:00 Unverified
parent f214d330f0
commit dc7f61aa6e
12 changed files with 181 additions and 315 deletions
-64
View File
@@ -1,64 +0,0 @@
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);
});
});
-158
View File
@@ -1,158 +0,0 @@
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;
}
+60
View File
@@ -0,0 +1,60 @@
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 { MaterialPoster } from "@/components/public/material-poster";
import { ConstellationImage } from "@/components/public/constellation-image";
import type { PublicGuide } from "@/lib/guides/queries";
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("ส่งออกภาพ");
expect(html).toContain("ส่งออกภาพโดยไม่รวมโน้ต");
expect(html.match(/<button\b/gu)).toHaveLength(2);
});
it("shows only the Thai export button when no-note export is disabled", () => {
const html = renderToStaticMarkup(<GuideImageExport fileName="guide.png" allowWithoutNotes={false}>Content</GuideImageExport>);
expect(html).toContain("ส่งออกภาพ");
expect(html).not.toContain("ส่งออกภาพโดยไม่รวมโน้ต");
expect(html.match(/<button\b/gu)).toHaveLength(1);
});
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);
});
});
describe("material poster text", () => {
const props = {
character: { name: "Raiden", nameCardImageUrl: null, baseStats: [], ascension: [], leveling: [] },
characterArtUrl: null,
artworkOffsetX: 0,
artworkOffsetY: 0,
artworkScale: 100,
topColors: ["#000000", "#000000"] as [string, string],
bottomColors: ["#000000", "#000000"] as [string, string],
};
it("always uses English, including on the website", () => {
const html = renderToStaticMarkup(<MaterialPoster {...props} />);
expect(html).toContain("MATERIALS");
expect(html).toContain("LV.90");
expect(html).not.toContain("วัสดุ");
expect(html).not.toContain("เลเวล 90");
});
});