From 0917d678a3063f4ad12e24659e196df856d39a76 Mon Sep 17 00:00:00 2001 From: JOYCEQL <1449239013@qq.com> Date: Wed, 11 Feb 2026 22:00:18 +0800 Subject: [PATCH] feat: enhance date formatting utility with locale support and integrate it across preview components. --- src/app/providers.tsx | 25 ++++---- src/components/preview/BaseInfo.tsx | 2 +- src/components/preview/CustomSection.tsx | 5 +- src/components/preview/EducationSection.tsx | 5 +- src/components/preview/ExperienceSection.tsx | 5 +- src/components/preview/ProjectSection.tsx | 5 +- src/lib/utils.ts | 61 ++++++++++++++++---- 7 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/app/providers.tsx b/src/app/providers.tsx index a381328..206fb46 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -3,20 +3,23 @@ import { ThemeProvider } from "next-themes"; import { Analytics } from "@vercel/analytics/next"; import { HeroUIProvider } from "@heroui/react"; +import { useLocale } from "next-intl"; export function Providers({ children }: { children: React.ReactNode }) { + const locale = useLocale(); + return ( - - - {children} - - + + + {children} + + ); } diff --git a/src/components/preview/BaseInfo.tsx b/src/components/preview/BaseInfo.tsx index f497c06..9215d65 100644 --- a/src/components/preview/BaseInfo.tsx +++ b/src/components/preview/BaseInfo.tsx @@ -65,7 +65,7 @@ const BaseInfo = ({ key: field.key, value: field.key === "birthDate" && basic[field.key] - ? formatDateString(basic[field.key] as string) + ? formatDateString(basic[field.key] as string, locale) : (basic[field.key] as string), icon: basic.icons?.[field.key] || "User", label: field.label, diff --git a/src/components/preview/CustomSection.tsx b/src/components/preview/CustomSection.tsx index 9abb862..3fbf41f 100644 --- a/src/components/preview/CustomSection.tsx +++ b/src/components/preview/CustomSection.tsx @@ -4,6 +4,8 @@ import SectionTitle from "./SectionTitle"; import { GlobalSettings, CustomItem } from "@/types/resume"; import { useResumeStore } from "@/store/useResumeStore"; import { normalizeRichTextContent } from "@/lib/richText"; +import { formatDateString } from "@/lib/utils"; +import { useLocale } from "next-intl"; interface CustomSectionProps { sectionId: string; @@ -21,6 +23,7 @@ const CustomSection = ({ showTitle = true, }: CustomSectionProps) => { const { setActiveSection } = useResumeStore(); + const locale = useLocale(); const visibleItems = items?.filter((item) => { return item.visible && (item.title || item.description); }); @@ -86,7 +89,7 @@ const CustomSection = ({ fontSize: `${globalSettings?.subheaderSize || 16}px`, }} > - {item.dateRange} + {formatDateString(item.dateRange, locale)} diff --git a/src/components/preview/EducationSection.tsx b/src/components/preview/EducationSection.tsx index 689da26..baddb3e 100644 --- a/src/components/preview/EducationSection.tsx +++ b/src/components/preview/EducationSection.tsx @@ -85,8 +85,9 @@ const EducationSection = ({ fontSize: `${globalSettings?.subheaderSize || 16}px`, }} > - {`${formatDateString(edu.startDate)} - ${formatDateString( - edu.endDate + {`${formatDateString(edu.startDate, locale)} - ${formatDateString( + edu.endDate, + locale )}`} diff --git a/src/components/preview/ExperienceSection.tsx b/src/components/preview/ExperienceSection.tsx index 1cbba75..3e7a235 100644 --- a/src/components/preview/ExperienceSection.tsx +++ b/src/components/preview/ExperienceSection.tsx @@ -5,6 +5,8 @@ import { Experience, GlobalSettings } from "@/types/resume"; import SectionTitle from "./SectionTitle"; import { useResumeStore } from "@/store/useResumeStore"; import { normalizeRichTextContent } from "@/lib/richText"; +import { formatDateString } from "@/lib/utils"; +import { useLocale } from "next-intl"; interface ExperienceSectionProps { experiences?: Experience[]; @@ -22,6 +24,7 @@ const ExperienceItem = React.forwardRef( const centerSubtitle = globalSettings?.centerSubtitle; const flexLayout = globalSettings?.flexibleHeaderLayout; const gridColumns = centerSubtitle ? 3 : 2; + const locale = useLocale(); return ( ( fontSize: `${globalSettings?.subheaderSize || 16}px`, }} > - {experience.date} + {formatDateString(experience.date, locale)} {experience.position && !centerSubtitle && ( diff --git a/src/components/preview/ProjectSection.tsx b/src/components/preview/ProjectSection.tsx index 1b3b64b..9772738 100644 --- a/src/components/preview/ProjectSection.tsx +++ b/src/components/preview/ProjectSection.tsx @@ -6,6 +6,8 @@ import SectionTitle from "./SectionTitle"; import { Project, GlobalSettings } from "@/types/resume"; import { useResumeStore } from "@/store/useResumeStore"; import { normalizeRichTextContent } from "@/lib/richText"; +import { formatDateString } from "@/lib/utils"; +import { useLocale } from "next-intl"; interface ProjectItemProps { project: Project; @@ -15,6 +17,7 @@ interface ProjectItemProps { const ProjectItem = React.forwardRef( ({ project, globalSettings }, ref) => { const centerSubtitle = globalSettings?.centerSubtitle; + const locale = useLocale(); const flexLayout = globalSettings?.flexibleHeaderLayout; @@ -84,7 +87,7 @@ const ProjectItem = React.forwardRef( fontSize: `${globalSettings?.subheaderSize || 16}px`, }} > - {project.date} + {formatDateString(project.date, locale)} {project.role && !centerSubtitle && ( diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a61c106..84f8c3f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -5,19 +5,56 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } -export function formatDateString(dateStr: string | undefined): string { - if (!dateStr) return ""; - // Check if it matches YYYY-MM - // If so, replace - with . - +function parseToDate(dateStr: string): Date | null { + let year: number | null = null; + let month: number | null = null; + if (dateStr.match(/^\d{4}-\d{2}$/)) { - return dateStr.replace("-", "."); + const parts = dateStr.split("-"); + year = parseInt(parts[0], 10); + month = parseInt(parts[1], 10); + } else if (dateStr.match(/^\d{4}-\d{2}-\d{2}$/)) { + const parts = dateStr.split("-"); + year = parseInt(parts[0], 10); + month = parseInt(parts[1], 10); + } else if (dateStr.match(/^\d{4}\.\d{2}$/)) { + const parts = dateStr.split("."); + year = parseInt(parts[0], 10); + month = parseInt(parts[1], 10); + } else if (dateStr.match(/^\d{4}\/\d{2}$/)) { + const parts = dateStr.split("/"); + year = parseInt(parts[0], 10); + month = parseInt(parts[1], 10); } - - if (dateStr.match(/^\d{4}-\d{2}-\d{2}$/)) { - // Cut off day if present? User wants "month" precision. - return dateStr.substring(0, 7).replace("-", "."); + + if (year !== null && month !== null) { + return new Date(Date.UTC(year, month - 1, 1)); + } + return null; +} + +export function formatDateString(dateStr: string | undefined, locale: string = "zh"): string { + if (!dateStr) return ""; + + if (dateStr.includes(" - ")) { + const [start, end] = dateStr.split(" - "); + return `${formatDateString(start, locale)} - ${formatDateString(end, locale)}`; + } + + const date = parseToDate(dateStr); + if (!date) return dateStr; + + try { + if (locale === "zh" || locale === "zh-CN") { + return `${date.getUTCFullYear()}.${String(date.getUTCMonth() + 1).padStart(2, "0")}`; + } + const formatter = new Intl.DateTimeFormat(locale, { + year: 'numeric', + month: '2-digit', + timeZone: 'UTC' + }); + return formatter.format(date); + } catch (e) { + return dateStr; } - - return dateStr; }