feat : make export image be english text ig
This commit is contained in:
@@ -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({
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CardFooter className="flex flex-col gap-2">
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={!ready || exporting}
|
||||
onClick={download}
|
||||
onClick={() => void download()}
|
||||
>
|
||||
{exporting ? (
|
||||
<Spinner data-icon="inline-start" />
|
||||
) : (
|
||||
<DownloadIcon data-icon="inline-start" />
|
||||
)}
|
||||
{exporting ? "กำลังสร้าง PNG…" : "ดาวน์โหลด PNG 3000×3000"}
|
||||
{exporting ? exportText("exporting") : exportText("downloadPng", { width: 3000, height: 3000 })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={!ready || exporting}
|
||||
onClick={() => void download(false)}
|
||||
>
|
||||
<DownloadIcon data-icon="inline-start" />
|
||||
{exportText("withoutNote")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@@ -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({
|
||||
<Dialog>
|
||||
<DialogTrigger render={<Button type="button" variant="outline" size="sm" />}>
|
||||
<DownloadIcon data-icon="inline-start" />
|
||||
ส่งออกภาพ
|
||||
{exportText("exportImage")}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>เลือกข้อมูลสำหรับส่งออก</DialogTitle>
|
||||
<DialogTitle>{exportText("sectionTitle")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
เลือกได้หลายส่วน หรือเลือก Entire Section เพื่อส่งออกทั้งหมด
|
||||
{exportText("sectionDescription")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
@@ -96,7 +97,7 @@ export function ArtifactImageExport({
|
||||
checked ? new Set(sectionOptions.map(([value]) => value)) : new Set(),
|
||||
)}
|
||||
/>
|
||||
<FieldLabel htmlFor="artifact-export-entire">Entire Section</FieldLabel>
|
||||
<FieldLabel htmlFor="artifact-export-entire">{exportText("allSections")}</FieldLabel>
|
||||
</Field>
|
||||
{sectionOptions.map(([value, label]) => (
|
||||
<Field key={value} orientation="horizontal">
|
||||
@@ -112,7 +113,7 @@ export function ArtifactImageExport({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FieldLabel htmlFor={`artifact-export-${value}`}>{label}</FieldLabel>
|
||||
<FieldLabel htmlFor={`artifact-export-${value}`}>{exportText(label)}</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
{hasNote ? (
|
||||
@@ -122,13 +123,13 @@ export function ArtifactImageExport({
|
||||
checked={includeNote}
|
||||
onCheckedChange={(checked) => setIncludeNote(checked === true)}
|
||||
/>
|
||||
<FieldLabel htmlFor="artifact-export-note">Add Note</FieldLabel>
|
||||
<FieldLabel htmlFor="artifact-export-note">{exportText("addNote")}</FieldLabel>
|
||||
</Field>
|
||||
) : null}
|
||||
</FieldGroup>
|
||||
{sections.has("best-stats") && recommendedConditionLabels.length > 1 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium">Recommended Stats condition</p>
|
||||
<p className="text-sm font-medium">{exportText("recommendedCondition")}</p>
|
||||
<ToggleGroup
|
||||
variant="outline"
|
||||
value={[recommendedCondition]}
|
||||
@@ -150,7 +151,7 @@ export function ArtifactImageExport({
|
||||
) : null}
|
||||
{sections.has("target-stats") && targetConditionLabels.length > 1 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium">Target Stats condition</p>
|
||||
<p className="text-sm font-medium">{exportText("targetCondition")}</p>
|
||||
<ToggleGroup
|
||||
variant="outline"
|
||||
value={[targetCondition]}
|
||||
|
||||
@@ -3,10 +3,11 @@ import Image from "next/image";
|
||||
import { GuideExportBranding } from "@/components/public/guide-export-branding";
|
||||
import type { PublicGuide } from "@/lib/guides/queries";
|
||||
import { constellationChartRows } from "@/lib/guides/export-image";
|
||||
import { exportText } from "@/lib/guides/export-text";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function percentage(value: number): string {
|
||||
return `${value.toLocaleString(undefined, { maximumFractionDigits: 2 })}%`;
|
||||
return `${value.toLocaleString("en-US", { maximumFractionDigits: 2 })}%`;
|
||||
}
|
||||
|
||||
export function ConstellationImage({
|
||||
@@ -42,10 +43,10 @@ export function ConstellationImage({
|
||||
: "text-[76px]",
|
||||
)}
|
||||
>
|
||||
{guide.character.name} Constellations
|
||||
{exportText("constellationTitle", { name: guide.character.name })}
|
||||
</h2>
|
||||
<p className="mt-3 line-clamp-2 max-w-[1100px] whitespace-pre-line text-[42px] leading-tight font-bold text-[#f1badd]">
|
||||
Team Damage
|
||||
{exportText("teamDamage")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,7 +83,7 @@ export function ConstellationImage({
|
||||
</div>
|
||||
|
||||
{footer ? (
|
||||
<p className="absolute bottom-10 left-[154px] z-30 max-w-[1000px] truncate text-[28px] font-semibold drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
|
||||
<p data-export-note className="absolute bottom-10 left-[154px] z-30 max-w-[1000px] truncate text-[28px] font-semibold drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
|
||||
{footer}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -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<HTMLDivElement>(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<HTMLElement>("[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<HTMLElement>(".artifact-export-right");
|
||||
if (right) {
|
||||
const sections = right.querySelectorAll<HTMLElement>(
|
||||
"[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 (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(["with-notes", "without-notes"] as const).map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={capturing || disabled}
|
||||
onClick={() => setCapturing(true)}
|
||||
onClick={() => setCaptureMode(mode)}
|
||||
>
|
||||
{capturing ? (
|
||||
{captureMode === mode ? (
|
||||
<Spinner data-icon="inline-start" />
|
||||
) : (
|
||||
<DownloadIcon data-icon="inline-start" />
|
||||
)}
|
||||
{capturing ? "กำลังส่งออก…" : buttonLabel}
|
||||
{captureMode === mode
|
||||
? exportText("exporting")
|
||||
: mode === "without-notes"
|
||||
? exportText("withoutNote")
|
||||
: buttonLabel}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{capturing
|
||||
? createPortal(
|
||||
<div
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
} from "@/lib/catalog/lunaris";
|
||||
import { mergePosterMaterials, type PosterMaterial } from "@/lib/material-poster";
|
||||
import { accentGradient } from "@/lib/overview-image";
|
||||
import { exportText, type Locale } from "@/lib/guides/export-text";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type PosterBlendMode = "normal" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity";
|
||||
@@ -65,7 +66,9 @@ export function MaterialPoster({
|
||||
onArtworkMove,
|
||||
className,
|
||||
bleeding = [],
|
||||
locale = "en",
|
||||
}: {
|
||||
locale?: Locale;
|
||||
character: PosterCharacter;
|
||||
characterArtUrl: string | null;
|
||||
artworkOffsetX: number;
|
||||
@@ -216,8 +219,8 @@ export function MaterialPoster({
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-[4%] top-[49%]" style={{ fontFamily: "Kanit Poster", fontWeight: 800, fontStyle: "italic" }}>
|
||||
<h2 className="text-[7cqw] leading-none" style={{ textShadow: "-6px 14px 0 rgba(0,0,0,0.59)" }}>MATERIALS</h2>
|
||||
<p className="mt-[1%] text-[4.5cqw] leading-none">LV.90</p>
|
||||
<h2 className="text-[7cqw] leading-none" style={{ textShadow: "-6px 14px 0 rgba(0,0,0,0.59)" }}>{exportText("materials", {}, locale)}</h2>
|
||||
<p className="mt-[1%] text-[4.5cqw] leading-none">{exportText("materialLevel", { level: 90 }, locale)}</p>
|
||||
<div className="mt-[5%] grid grid-cols-8 gap-[1.2%]">
|
||||
{materials.map((material, index) => {
|
||||
const materialCard = (
|
||||
|
||||
@@ -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 ? <ExtraSection guide={guide} extra={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) => (
|
||||
<div key={conditionIndex} data-artifact-export-section="best-stats" data-recommended-condition={conditionIndex} className="flex flex-col gap-5">
|
||||
{exporting && recommendedConditions.length > 1 ? <p className="text-sm font-medium">{condition.name || `Condition ${conditionIndex + 1}`}</p> : null}
|
||||
{condition.note ? <p className="whitespace-pre-wrap text-sm text-muted-foreground">{condition.note}</p> : null}
|
||||
{exporting && recommendedConditions.length > 1 ? <p className="text-sm font-medium">{condition.name || exportText("condition", { index: conditionIndex + 1 })}</p> : null}
|
||||
{condition.note ? <p data-export-note className="whitespace-pre-wrap text-sm text-muted-foreground">{condition.note}</p> : null}
|
||||
{profile ? (
|
||||
<Card data-artifact-export-section="best-stats" className="artifact-export-main-stats overflow-hidden pt-0">
|
||||
<CardHeader className="bg-primary py-4 text-center text-primary-foreground">
|
||||
<CardTitle className="text-2xl">ออฟหลักที่ดีที่สุด</CardTitle>
|
||||
<CardTitle className="text-2xl">{exportText("mainStats", {}, exporting ? "en" : "th")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-3 p-0">
|
||||
<ArtifactStat
|
||||
iconSrc="/icon/Icon_Sands_of_Eon.webp"
|
||||
label="นาฬิกา"
|
||||
label={exportText("sands", {}, exporting ? "en" : "th")}
|
||||
values={condition.sands}
|
||||
exporting={exporting}
|
||||
elementIconUrls={guide.elementIconUrls}
|
||||
/>
|
||||
<ArtifactStat
|
||||
iconSrc="/icon/Icon_Goblet_of_Eonothem.webp"
|
||||
label="ถ้วย"
|
||||
label={exportText("goblet", {}, exporting ? "en" : "th")}
|
||||
values={condition.goblet}
|
||||
exporting={exporting}
|
||||
elementIconUrls={guide.elementIconUrls}
|
||||
/>
|
||||
<ArtifactStat
|
||||
iconSrc="/icon/Icon_Circlet_of_Logos.webp"
|
||||
label="หัว"
|
||||
label={exportText("circlet", {}, exporting ? "en" : "th")}
|
||||
values={condition.circlet}
|
||||
exporting={exporting}
|
||||
elementIconUrls={guide.elementIconUrls}
|
||||
@@ -675,7 +675,7 @@ function ArtifactSectionContent({
|
||||
{condition.substats.length ? (
|
||||
<Card data-artifact-export-section="best-stats" className="artifact-export-substats">
|
||||
<CardContent className="flex flex-col items-center gap-2 py-5 text-center text-sm">
|
||||
<strong>ออฟรองที่ดีที่สุด</strong>
|
||||
<strong>{exportText("substats", {}, exporting ? "en" : "th")}</strong>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
{condition.substats.map((stat, index) => (
|
||||
<ArtifactStatValue
|
||||
@@ -710,7 +710,7 @@ function ArtifactSectionContent({
|
||||
<header className="relative z-10 flex items-center justify-center text-center">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<p className="text-sm font-medium text-primary">
|
||||
อาร์ติแฟกต์และสแตทที่แนะนำ
|
||||
{exportText("artifactRecommendation", {}, exporting ? "en" : "th")}
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Image
|
||||
@@ -726,7 +726,7 @@ function ArtifactSectionContent({
|
||||
)}
|
||||
/>
|
||||
<h1 className="whitespace-nowrap font-heading text-4xl font-semibold">
|
||||
Artifact & Stats
|
||||
{exportText("artifactTitle", {}, exporting ? "en" : "th")}
|
||||
</h1>
|
||||
</div>
|
||||
{action}
|
||||
@@ -751,9 +751,9 @@ function ArtifactSectionContent({
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{group.name === "Late Game"
|
||||
? "เลทเกม"
|
||||
? exportText("lateGame", {}, exporting ? "en" : "th")
|
||||
: group.name === "Early Game"
|
||||
? "ต้นเกม"
|
||||
? exportText("earlyGame", {}, exporting ? "en" : "th")
|
||||
: group.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -781,13 +781,13 @@ function ArtifactSectionContent({
|
||||
{row.second ? ` + 2 ${row.second.name}` : ""}
|
||||
</p>
|
||||
{row.note ? (
|
||||
<p className="whitespace-pre-wrap text-sm text-muted-foreground">
|
||||
<p data-export-note className="whitespace-pre-wrap text-sm text-muted-foreground">
|
||||
{row.note}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-2xl font-semibold text-primary">
|
||||
{Number(row.percent) === 0 ? "-%" : metric(row.percent)}
|
||||
{Number(row.percent) === 0 ? "-%" : metric(row.percent, exporting ? "en-US" : undefined)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
@@ -806,8 +806,8 @@ function ArtifactSectionContent({
|
||||
exporting ? (
|
||||
profile.targetConditions.filter((group) => group.rows.length).map((group, groupIndex) => (
|
||||
<div key={groupIndex} data-artifact-export-section="target-stats" data-target-condition={groupIndex}>
|
||||
<p className="mb-2 text-sm font-medium">ค่าสถานะต่ำๆที่ควรจะปั้นไว้</p>
|
||||
{group.note ? <p className="mb-2 whitespace-pre-wrap text-sm text-muted-foreground">{group.note}</p> : null}
|
||||
<p className="mb-2 text-sm font-medium">{exportText("minimumStats", {}, exporting ? "en" : "th")}</p>
|
||||
{group.note ? <p data-export-note className="mb-2 whitespace-pre-wrap text-sm text-muted-foreground">{group.note}</p> : null}
|
||||
<div className="flex flex-col gap-1">
|
||||
{group.rows.map((target, index) => (
|
||||
<div
|
||||
@@ -830,7 +830,7 @@ function ArtifactSectionContent({
|
||||
) : null}
|
||||
<span>
|
||||
{statLabel(
|
||||
normalizeTargetStat(target.stat) ?? target.stat,
|
||||
normalizeTargetStat(target.stat) ?? target.stat, exporting ? "en" : "th",
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
@@ -844,8 +844,8 @@ function ArtifactSectionContent({
|
||||
<ConditionSwitcher label="เลือกเงื่อนไขค่าสถานะเป้าหมาย" labels={profile.targetConditions.filter((group) => group.rows.length).map((group, index) => group.name || `Condition ${index + 1}`)}>
|
||||
{profile.targetConditions.filter((group) => group.rows.length).map((group, groupIndex) => (
|
||||
<div key={groupIndex}>
|
||||
<p className="mb-2 text-sm font-medium">ค่าสถานะต่ำๆที่ควรจะปั้นไว้</p>
|
||||
{group.note ? <p className="mb-2 whitespace-pre-wrap text-sm text-muted-foreground">{group.note}</p> : null}
|
||||
<p className="mb-2 text-sm font-medium">{exportText("minimumStats", {}, exporting ? "en" : "th")}</p>
|
||||
{group.note ? <p data-export-note className="mb-2 whitespace-pre-wrap text-sm text-muted-foreground">{group.note}</p> : null}
|
||||
<div className="flex flex-col gap-1">
|
||||
{group.rows.map((target, index) => (
|
||||
<div key={`${target.stat}-${target.value}`} className={cn("flex items-center justify-between gap-4 rounded-md px-3 py-2 text-sm", index % 2 === 0 ? "bg-muted/60" : "bg-muted/20")}>
|
||||
@@ -865,7 +865,7 @@ function ArtifactSectionContent({
|
||||
{profile.talents.length ? (
|
||||
<div data-artifact-export-section="talents">
|
||||
<p className="mb-2 text-sm font-medium">
|
||||
ลำดับการอัพพรสวรรค์
|
||||
{exportText("talents", {}, exporting ? "en" : "th")}
|
||||
</p>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -915,7 +915,7 @@ function ArtifactSectionContent({
|
||||
</div>
|
||||
<Badge variant="secondary">
|
||||
{talent.level
|
||||
? `เลเวล ${talent.level}+`
|
||||
? exportText("level", { level: talent.level }, exporting ? "en" : "th")
|
||||
: "-"}
|
||||
</Badge>
|
||||
<p
|
||||
@@ -932,7 +932,7 @@ function ArtifactSectionContent({
|
||||
exporting ? "text-xs" : "text-[11px] sm:text-xs",
|
||||
)}
|
||||
>
|
||||
{talentTypeLabel(talent.type)}
|
||||
{exportText(talent.type === "E" ? "skill" : talent.type === "Q" ? "burst" : "normalAttack", {}, exporting ? "en" : "th")}
|
||||
</p>
|
||||
</div>
|
||||
{index < talents.length - 1 ? (
|
||||
@@ -962,8 +962,8 @@ function ArtifactSectionContent({
|
||||
</div>
|
||||
) : null}
|
||||
{profile.note ? (
|
||||
<div data-artifact-export-section="note" className="pt-3">
|
||||
<p className="mb-2 text-sm font-medium">โน็ต</p>
|
||||
<div data-export-note data-artifact-export-section="note" className="pt-3">
|
||||
<p className="mb-2 text-sm font-medium">{exportText("note", {}, exporting ? "en" : "th")}</p>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{profile.note}
|
||||
</p>
|
||||
@@ -978,30 +978,8 @@ function ArtifactSectionContent({
|
||||
);
|
||||
}
|
||||
|
||||
function statLabel(value: string): string {
|
||||
const labels: 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%": "โบนัสการรักษา%",
|
||||
};
|
||||
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}
|
||||
<span className="break-words">{statLabel(value)}</span>
|
||||
<span className="break-words">{statLabel(value, exporting ? "en" : "th")}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1245,7 +1223,7 @@ function ExtraSectionContent({
|
||||
/>
|
||||
) : null}
|
||||
<header className="relative z-10 flex flex-col items-center gap-3 text-center">
|
||||
<p className="text-sm font-medium text-primary">คู่มือเสริม</p>
|
||||
<p className="text-sm font-medium text-primary">{exportText("extraGuide", {}, exporting ? "en" : "th")}</p>
|
||||
<h1 className="font-heading text-4xl font-semibold">{extra.title}</h1>
|
||||
{action}
|
||||
</header>
|
||||
@@ -1296,7 +1274,7 @@ function ExtraSectionContent({
|
||||
) : null}
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium">{item.name}</p>
|
||||
{!item.available ? <Badge variant="secondary">ไม่พบรายการ</Badge> : null}
|
||||
{!item.available ? <Badge variant="secondary">{exportText("missingItem", {}, exporting ? "en" : "th")}</Badge> : null}
|
||||
</div>
|
||||
<ArrowUpRightIcon aria-hidden="true" className="ml-auto shrink-0" />
|
||||
</a>
|
||||
@@ -1308,7 +1286,7 @@ function ExtraSectionContent({
|
||||
<CardDescription>
|
||||
<RichTextContent html={normalizeExtraRichText(row.body, row.highlights)} />
|
||||
</CardDescription>
|
||||
{row.percent ? <Badge>{metric(row.percent)}</Badge> : null}
|
||||
{row.percent ? <Badge>{metric(row.percent, exporting ? "en-US" : undefined)}</Badge> : null}
|
||||
</CardHeader>
|
||||
{row.sheet ? (
|
||||
<CardContent
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type DpsOrder,
|
||||
type TeamSearchTarget,
|
||||
} from "@/lib/guides/team-search";
|
||||
import { exportText } from "@/lib/guides/export-text";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Team = PublicGuide["teams"][number];
|
||||
@@ -168,7 +169,7 @@ function TeamExportImage({ team }: { team: Team }) {
|
||||
<div className="flex min-h-337.5 w-270 flex-col bg-[#05060a] p-[48px_52px] font-heading text-[white]">
|
||||
<header className="flex flex-col items-center text-center">
|
||||
<h2 className="text-[64px] leading-tight font-bold">{team.title}</h2>
|
||||
<p className="text-[32px] text-[#b9b9c2]">ระยะเวลาโรเทชั่น {seconds.toLocaleString(undefined, { maximumFractionDigits: 2 })} วิ</p>
|
||||
<p className="text-[32px] text-[#b9b9c2]">{exportText("rotation", { seconds: seconds.toLocaleString("en-US", { maximumFractionDigits: 2 }) })}</p>
|
||||
</header>
|
||||
<div className="mt-5 flex flex-col gap-4">
|
||||
{team.members.map((member) => {
|
||||
@@ -195,7 +196,7 @@ function TeamExportImage({ team }: { team: Team }) {
|
||||
<span className="truncate">{member.character.name}</span>
|
||||
<span className="shrink-0 text-[28px] text-[#f0c95f]">C{member.constellation}R{member.refinement}</span>
|
||||
</span>
|
||||
<span className="shrink-0 text-[36px]">{damage.toLocaleString()}</span>
|
||||
<span className="shrink-0 text-[36px]">{damage.toLocaleString("en-US")}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-5"><div className="h-11 flex-1 overflow-hidden rounded-xl bg-[#272730]"><div className="h-full rounded-xl bg-linear-to-r from-[#b7791f] to-[#f0b72f]" style={{ width: `${Math.max(2, Math.min(100, percent))}%` }} /></div><span className="w-28 text-right text-[34px] font-bold">{percent.toFixed(1)}%</span></div>
|
||||
</div>
|
||||
@@ -204,8 +205,8 @@ function TeamExportImage({ team }: { team: Team }) {
|
||||
})}
|
||||
</div>
|
||||
<div className="my-3 flex flex-1 items-center justify-center rounded-2xl bg-[#272730] p-4 text-center text-[28px] leading-[1.4]"><p className="whitespace-pre-wrap">{team.combo}</p></div>
|
||||
<div className="flex shrink-0 justify-center border-t-2 border-[#34343d] pt-6 text-[64px] font-bold text-[#f0c95f]">DPS: {seconds > 0 ? (total / seconds).toLocaleString(undefined, { maximumFractionDigits: 0 }) : "0"}</div>
|
||||
<div className="mt-5 flex shrink-0 justify-center text-[40px] text-[#74747f]">By YT เกนชินไม่ใช่เกมมือถือ</div>
|
||||
<div className="flex shrink-0 justify-center border-t-2 border-[#34343d] pt-6 text-[64px] font-bold text-[#f0c95f]">DPS: {seconds > 0 ? (total / seconds).toLocaleString("en-US", { maximumFractionDigits: 0 }) : "0"}</div>
|
||||
<div className="mt-5 flex shrink-0 justify-center text-[40px] text-[#74747f]">{exportText("teamCredit")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
<GuideExportBranding backgroundUrl={guide.character.splashImageUrl ?? guide.coverUrl} />
|
||||
<header className="relative z-10 flex flex-col items-center px-48 text-center drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
|
||||
<h2 className="max-w-full text-5xl leading-tight font-bold text-balance">
|
||||
Weapons {guide.character.name}
|
||||
{exportText("weaponTitle", { name: guide.character.name })}
|
||||
</h2>
|
||||
<p className="mt-1 text-4xl font-bold text-white">
|
||||
{metric === "overallPercent" ? "Overall Damage" : "Personal Damage"}
|
||||
{exportText(metric === "overallPercent" ? "overallDamage" : "personalDamage")}
|
||||
</p>
|
||||
<p className="mt-2 text-2xl font-semibold">{conditionLabel}</p>
|
||||
</header>
|
||||
@@ -84,7 +85,7 @@ function WeaponComparisonImage({
|
||||
</div>
|
||||
<div className="ml-3 flex min-w-0 flex-1 flex-col items-start gap-1 text-white drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
|
||||
<strong className="shrink-0 text-[30px]">{formattedPercent(row[metric])}</strong>
|
||||
{row.note ? <span className="min-w-0 text-xl leading-tight">{row.note}</span> : null}
|
||||
{row.note ? <span data-export-note className="min-w-0 text-xl leading-tight">{row.note}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,7 +94,7 @@ function WeaponComparisonImage({
|
||||
</div>
|
||||
|
||||
{group.note ? (
|
||||
<p className="relative z-10 mt-8 whitespace-pre-wrap text-center text-2xl font-semibold drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
|
||||
<p data-export-note className="relative z-10 mt-8 whitespace-pre-wrap text-center text-2xl font-semibold drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
|
||||
{group.note}
|
||||
</p>
|
||||
) : 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({
|
||||
<Dialog>
|
||||
<DialogTrigger render={<Button type="button" variant="outline" size="sm" />}>
|
||||
<DownloadIcon data-icon="inline-start" />
|
||||
ส่งออกภาพ
|
||||
{exportText("exportImage")}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>เลือกประเภทดาเมจ</DialogTitle>
|
||||
<DialogTitle>{exportText("damageTitle")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
เลือกค่าที่ต้องการใช้สร้างภาพเปรียบเทียบอาวุธ
|
||||
{exportText("damageDescription")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
|
||||
@@ -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