diff --git a/components/admin/overview-image-generator.tsx b/components/admin/overview-image-generator.tsx index 6c5ac77..40a68da 100644 --- a/components/admin/overview-image-generator.tsx +++ b/components/admin/overview-image-generator.tsx @@ -98,6 +98,7 @@ import { weaponTypeIconPath, type OverviewRole, } from "@/lib/overview-image"; +import { exportText, withoutNotesFileName } from "@/lib/guides/export-text"; import { cn } from "@/lib/utils"; type Character = { @@ -139,8 +140,7 @@ const blendModeItems: Array<{ value: OverviewBlendMode; label: string }> = [ ...overviewBlendModes, ]; -const DISCLAIMER = - "ไม่สามารถเอาไปเปรียบเทียบกับการคำนวณของ TC ท่านอื่นๆได้\nเพราะแต่ละคนใช้สแตทในการคำนวณที่ไม่เหมือนกัน\nและตารางที่เห็นอาจมีการเปลี่ยนแปลงได้ในอนาคต"; +const DISCLAIMER = exportText("disclaimer"); function DraggablePosterLayer({ id, @@ -999,7 +999,7 @@ export function OverviewImageGenerator({ } } - async function download() { + async function download(includeNotes = true) { const node = captureRef.current; if ( !node || @@ -1029,14 +1029,15 @@ export function OverviewImageGenerator({ const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; - link.download = overviewFileName(character.key, displayRole); + const fileName = overviewFileName(character.key, displayRole); + link.download = includeNotes ? fileName : withoutNotesFileName(fileName); document.body.append(link); link.click(); link.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 0); - toast.success("ดาวน์โหลดภาพ Overview แล้ว"); + toast.success(exportText("overviewDownloaded")); } catch { - toast.error("สร้างภาพไม่สำเร็จ กรุณาลองอีกครั้ง"); + toast.error(exportText("exportError")); } finally { setExporting(false); } @@ -1335,18 +1336,27 @@ export function OverviewImageGenerator({ - + + diff --git a/components/public/artifact-image-export.tsx b/components/public/artifact-image-export.tsx index 8f1892f..b59a206 100644 --- a/components/public/artifact-image-export.tsx +++ b/components/public/artifact-image-export.tsx @@ -3,6 +3,7 @@ import { useState, type ReactNode } from "react"; import { DownloadIcon } from "lucide-react"; +import { exportText } from "@/lib/guides/export-text"; import { GuideImageExport } from "@/components/public/guide-image-export"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -18,10 +19,10 @@ import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; const sectionOptions = [ - ["tierlist", "Artifact Tierlist"], - ["best-stats", "Best Main Stats & Substats"], - ["target-stats", "Target Stats"], - ["talents", "Talent Priority"], + ["tierlist", "tierlist"], + ["best-stats", "bestStats"], + ["target-stats", "targetStats"], + ["talents", "talents"], ] as const; type ArtifactExportSection = (typeof sectionOptions)[number][0]; @@ -78,13 +79,13 @@ export function ArtifactImageExport({ }> - ส่งออกภาพ + {exportText("exportImage")} - เลือกข้อมูลสำหรับส่งออก + {exportText("sectionTitle")} - เลือกได้หลายส่วน หรือเลือก Entire Section เพื่อส่งออกทั้งหมด + {exportText("sectionDescription")} @@ -96,7 +97,7 @@ export function ArtifactImageExport({ checked ? new Set(sectionOptions.map(([value]) => value)) : new Set(), )} /> - Entire Section + {exportText("allSections")} {sectionOptions.map(([value, label]) => ( @@ -112,7 +113,7 @@ export function ArtifactImageExport({ }); }} /> - {label} + {exportText(label)} ))} {hasNote ? ( @@ -122,13 +123,13 @@ export function ArtifactImageExport({ checked={includeNote} onCheckedChange={(checked) => setIncludeNote(checked === true)} /> - Add Note + {exportText("addNote")} ) : null} {sections.has("best-stats") && recommendedConditionLabels.length > 1 ? (
-

Recommended Stats condition

+

{exportText("recommendedCondition")}

1 ? (
-

Target Stats condition

+

{exportText("targetCondition")}

- {guide.character.name} Constellations + {exportText("constellationTitle", { name: guide.character.name })}

- Team Damage + {exportText("teamDamage")}

@@ -82,7 +83,7 @@ export function ConstellationImage({ {footer ? ( -

+

{footer}

) : null} diff --git a/components/public/guide-image-export.tsx b/components/public/guide-image-export.tsx index 42ad4da..82a4b62 100644 --- a/components/public/guide-image-export.tsx +++ b/components/public/guide-image-export.tsx @@ -7,6 +7,7 @@ import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; +import { exportText, withoutNotesFileName } from "@/lib/guides/export-text"; import { cn } from "@/lib/utils"; const EXPORT_WIDTH = 1200; @@ -23,7 +24,7 @@ export function GuideImageExport({ format = "png", disabled = false, captureClassName, - buttonLabel = "ส่งออกภาพ", + buttonLabel = exportText("exportImage"), }: { children: ReactNode; fileName: string; @@ -35,7 +36,10 @@ export function GuideImageExport({ buttonLabel?: string; }) { const captureRef = useRef(null); - const [capturing, setCapturing] = useState(false); + const [captureMode, setCaptureMode] = useState< + "with-notes" | "without-notes" | null + >(null); + const capturing = captureMode !== null; useEffect(() => { if (!capturing) return; @@ -47,6 +51,23 @@ export function GuideImageExport({ const node = captureRef.current; if (!node) throw new Error("Capture target is unavailable"); + if (captureMode === "without-notes") { + node.querySelectorAll("[data-export-note]").forEach((note) => { + note.style.display = "none"; + }); + // A note can be the only remaining content in the artifact right column. + const right = node.querySelector(".artifact-export-right"); + if (right) { + const sections = right.querySelectorAll( + "[data-artifact-export-section]", + ); + const hasVisibleSection = Array.from(sections).some( + (section) => getComputedStyle(section).display !== "none", + ); + if (!hasVisibleSection) right.style.display = "none"; + } + } + const { domToBlob, waitUntilLoad } = await import("modern-screenshot"); await Promise.all([document.fonts.ready, waitUntilLoad(node)]); const blob = await domToBlob(node, { @@ -60,16 +81,20 @@ export function GuideImageExport({ const url = URL.createObjectURL(blob); const link = document.createElement("a"); - link.download = downloadFileName(fileName); + link.download = downloadFileName( + captureMode === "without-notes" + ? withoutNotesFileName(fileName) + : fileName, + ); link.href = url; document.body.append(link); link.click(); link.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 0); } catch { - if (!cancelled) toast.error("ส่งออกภาพไม่สำเร็จ กรุณาลองอีกครั้ง"); + if (!cancelled) toast.error(exportText("exportError")); } finally { - if (!cancelled) setCapturing(false); + if (!cancelled) setCaptureMode(null); } } @@ -77,24 +102,33 @@ export function GuideImageExport({ return () => { cancelled = true; }; - }, [capturing, fileName, format, height, width]); + }, [capturing, captureMode, fileName, format, height, width]); return ( <> - +
+ {(["with-notes", "without-notes"] as const).map((mode) => ( + + ))} +
{capturing ? createPortal(
-

MATERIALS

-

LV.90

+

{exportText("materials", {}, locale)}

+

{exportText("materialLevel", { level: 90 }, locale)}

{materials.map((material, index) => { const materialCard = ( diff --git a/components/public/structured-guide.tsx b/components/public/structured-guide.tsx index 4a17e49..be8ffbb 100644 --- a/components/public/structured-guide.tsx +++ b/components/public/structured-guide.tsx @@ -51,10 +51,10 @@ import { import { normalizeTalentPriorities, talentRelationSymbol, - talentTypeLabel, } from "@/lib/guides/talents"; import { parseYouTubeUrl } from "@/lib/guides/youtube"; import { normalizeExtraRichText } from "@/lib/rich-text"; +import { exportText, exportStatLabel, type Locale } from "@/lib/guides/export-text"; import { cn } from "@/lib/utils"; function hasSection(guide: PublicGuide, slug: string): boolean { @@ -352,10 +352,10 @@ function GuideSection({ return extra ? : null; } -function metric(value: string | null) { +function metric(value: string | null, locale?: string) { return value === null ? null - : `${Number(value).toLocaleString(undefined, { maximumFractionDigits: 2 })}%`; + : `${Number(value).toLocaleString(locale, { maximumFractionDigits: 2 })}%`; } function lunarisItemUrl( @@ -640,31 +640,31 @@ function ArtifactSectionContent({ const recommendedConditions = profile?.recommendedConditions ?? []; const recommendedCards = recommendedConditions.map((condition, conditionIndex) => (
- {exporting && recommendedConditions.length > 1 ?

{condition.name || `Condition ${conditionIndex + 1}`}

: null} - {condition.note ?

{condition.note}

: null} + {exporting && recommendedConditions.length > 1 ?

{condition.name || exportText("condition", { index: conditionIndex + 1 })}

: null} + {condition.note ?

{condition.note}

: null} {profile ? ( - ออฟหลักที่ดีที่สุด + {exportText("mainStats", {}, exporting ? "en" : "th")} - ออฟรองที่ดีที่สุด + {exportText("substats", {}, exporting ? "en" : "th")}
{condition.substats.map((stat, index) => (

- อาร์ติแฟกต์และสแตทที่แนะนำ + {exportText("artifactRecommendation", {}, exporting ? "en" : "th")}

- Artifact & Stats + {exportText("artifactTitle", {}, exporting ? "en" : "th")}

{action} @@ -751,9 +751,9 @@ function ArtifactSectionContent({ {group.name === "Late Game" - ? "เลทเกม" + ? exportText("lateGame", {}, exporting ? "en" : "th") : group.name === "Early Game" - ? "ต้นเกม" + ? exportText("earlyGame", {}, exporting ? "en" : "th") : group.name} @@ -781,13 +781,13 @@ function ArtifactSectionContent({ {row.second ? ` + 2 ${row.second.name}` : ""}

{row.note ? ( -

+

{row.note}

) : null}

- {Number(row.percent) === 0 ? "-%" : metric(row.percent)} + {Number(row.percent) === 0 ? "-%" : metric(row.percent, exporting ? "en-US" : undefined)}

))} @@ -806,8 +806,8 @@ function ArtifactSectionContent({ exporting ? ( profile.targetConditions.filter((group) => group.rows.length).map((group, groupIndex) => (
-

ค่าสถานะต่ำๆที่ควรจะปั้นไว้

- {group.note ?

{group.note}

: null} +

{exportText("minimumStats", {}, exporting ? "en" : "th")}

+ {group.note ?

{group.note}

: null}
{group.rows.map((target, index) => (
{statLabel( - normalizeTargetStat(target.stat) ?? target.stat, + normalizeTargetStat(target.stat) ?? target.stat, exporting ? "en" : "th", )} @@ -844,8 +844,8 @@ function ArtifactSectionContent({ group.rows.length).map((group, index) => group.name || `Condition ${index + 1}`)}> {profile.targetConditions.filter((group) => group.rows.length).map((group, groupIndex) => (
-

ค่าสถานะต่ำๆที่ควรจะปั้นไว้

- {group.note ?

{group.note}

: null} +

{exportText("minimumStats", {}, exporting ? "en" : "th")}

+ {group.note ?

{group.note}

: null}
{group.rows.map((target, index) => (
@@ -865,7 +865,7 @@ function ArtifactSectionContent({ {profile.talents.length ? (

- ลำดับการอัพพรสวรรค์ + {exportText("talents", {}, exporting ? "en" : "th")}

{talent.level - ? `เลเวล ${talent.level}+` + ? exportText("level", { level: talent.level }, exporting ? "en" : "th") : "-"}

- {talentTypeLabel(talent.type)} + {exportText(talent.type === "E" ? "skill" : talent.type === "Q" ? "burst" : "normalAttack", {}, exporting ? "en" : "th")}

{index < talents.length - 1 ? ( @@ -962,8 +962,8 @@ function ArtifactSectionContent({
) : null} {profile.note ? ( -
-

โน็ต

+
+

{exportText("note", {}, exporting ? "en" : "th")}

{profile.note}

@@ -978,30 +978,8 @@ function ArtifactSectionContent({ ); } -function statLabel(value: string): string { - const labels: Record = { - 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%": "โบนัสการรักษา%", - }; - return labels[value] ?? value; +function statLabel(value: string, locale: Locale = "th"): string { + return exportStatLabel(value, locale); } function ArtifactStat({ @@ -1102,7 +1080,7 @@ function ArtifactStatValue({ className="size-5 shrink-0 object-contain brightness-0 invert" /> ) : null} - {statLabel(value)} + {statLabel(value, exporting ? "en" : "th")} ); } @@ -1245,7 +1223,7 @@ function ExtraSectionContent({ /> ) : null}
-

คู่มือเสริม

+

{exportText("extraGuide", {}, exporting ? "en" : "th")}

{extra.title}

{action}
@@ -1296,7 +1274,7 @@ function ExtraSectionContent({ ) : null}

{item.name}

- {!item.available ? ไม่พบรายการ : null} + {!item.available ? {exportText("missingItem", {}, exporting ? "en" : "th")} : null}

{team.combo}

-
DPS: {seconds > 0 ? (total / seconds).toLocaleString(undefined, { maximumFractionDigits: 0 }) : "0"}
-
By YT เกนชินไม่ใช่เกมมือถือ
+
DPS: {seconds > 0 ? (total / seconds).toLocaleString("en-US", { maximumFractionDigits: 0 }) : "0"}
+
{exportText("teamCredit")}
); } diff --git a/components/public/weapon-image-export.tsx b/components/public/weapon-image-export.tsx index 17f7c2d..efb389f 100644 --- a/components/public/weapon-image-export.tsx +++ b/components/public/weapon-image-export.tsx @@ -14,6 +14,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { exportText } from "@/lib/guides/export-text"; import type { PublicGuide } from "@/lib/guides/queries"; type WeaponGroup = PublicGuide["weaponGroups"][number]; @@ -23,7 +24,7 @@ function formattedPercent(value: string | null): string { if (value === null) return "-%"; const numeric = Number(value); if (numeric === 0) return "-%"; - return `${numeric.toLocaleString(undefined, { maximumFractionDigits: 2 })}%`; + return `${numeric.toLocaleString("en-US", { maximumFractionDigits: 2 })}%`; } function WeaponComparisonImage({ @@ -45,10 +46,10 @@ function WeaponComparisonImage({

- Weapons {guide.character.name} + {exportText("weaponTitle", { name: guide.character.name })}

- {metric === "overallPercent" ? "Overall Damage" : "Personal Damage"} + {exportText(metric === "overallPercent" ? "overallDamage" : "personalDamage")}

{conditionLabel}

@@ -84,7 +85,7 @@ function WeaponComparisonImage({
{formattedPercent(row[metric])} - {row.note ? {row.note} : null} + {row.note ? {row.note} : null}
@@ -93,7 +94,7 @@ function WeaponComparisonImage({
{group.note ? ( -

+

{group.note}

) : null} @@ -110,11 +111,11 @@ export function WeaponImageExport({ group: WeaponGroup; conditionIndex: number; }) { - const conditionLabel = group.name || `Condition ${conditionIndex + 1}`; + const conditionLabel = group.name || exportText("condition", { index: conditionIndex + 1 }); const baseName = `${guide.name}-Weapon-${conditionLabel}`; const metricOptions = ([ - ["overallPercent", "Overall Damage"], - ["personalPercent", "Personal Damage"], + ["overallPercent", exportText("overallDamage")], + ["personalPercent", exportText("personalDamage")], ] as const).filter(([metric]) => group.rows.some((row) => row[metric] !== null), ); @@ -123,13 +124,13 @@ export function WeaponImageExport({ }> - ส่งออกภาพ + {exportText("exportImage")} - เลือกประเภทดาเมจ + {exportText("damageTitle")} - เลือกค่าที่ต้องการใช้สร้างภาพเปรียบเทียบอาวุธ + {exportText("damageDescription")}
diff --git a/lib/guides/export-text.test.tsx b/lib/guides/export-text.test.tsx new file mode 100644 index 0000000..3f5870a --- /dev/null +++ b/lib/guides/export-text.test.tsx @@ -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(No notes); + expect(html).toContain("Export image"); + expect(html).toContain("Export without note"); + expect(html.match(/ { + const html = renderToStaticMarkup(No sections); + 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); + }); +}); diff --git a/lib/guides/export-text.ts b/lib/guides/export-text.ts new file mode 100644 index 0000000..b3fb092 --- /dev/null +++ b/lib/guides/export-text.ts @@ -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 = { + 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 = {}, + 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 = { + 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 = { + 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; +}