From e10fced28a6c40a3d19cb5175c57618e4482923f Mon Sep 17 00:00:00 2001 From: Hamed Fallah <45401212+hamedf62@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:12:42 +0330 Subject: [PATCH] feat: add Jalali (Persian/Shamsi) calendar support with Saturday as week start (#490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add Jalali (Persian/Shamsi) calendar support with Saturday as week start - Add jalaali-js library for Gregorian ↔ Jalali date conversion - Create lib/jalali-utils.ts with Jalali calendar utilities - Create hooks/use-calendar-locale.ts for unified calendar locale handling - Expand FirstDayOfWeek type to include 6 (Saturday) - Update all calendar views (month, week, day, mini, toolbar) to support Jalali calendar display and Saturday-first week ordering - Add Jalali month names (Farvardin … Esfand) to all locale files - Add Persian (fa) locale with full translations - Update settings UI to include Saturday as first day of week option - Update useFormatEventDate to show Jalali dates when locale is fa - Auto-detect Jalali calendar when fa locale is active The calendar system automatically switches to Jalali when the locale is set to Persian (fa). All internal date handling remains Gregorian (ISO 8601) for JMAP protocol compatibility; Jalali conversion is purely at the display layer. * Add PR template for Jalali calendar feature * chore: remove accidentally added PR template * fix: add image_too_large key to fa locale for PR #462 compatibility --- components/calendar/calendar-month-view.tsx | 53 ++--- components/calendar/calendar-toolbar.tsx | 40 ++-- components/calendar/calendar-week-view.tsx | 2 +- components/calendar/mini-calendar.tsx | 62 +++-- components/settings/language-settings.tsx | 1 + hooks/use-calendar-locale.ts | 251 ++++++++++++++++++++ hooks/use-format-event-date.ts | 23 +- lib/jalali-utils.ts | 169 +++++++++++++ locales/cs/common.json | 14 +- locales/da/common.json | 14 +- locales/de/common.json | 14 +- locales/en/common.json | 14 +- locales/es/common.json | 14 +- locales/fa/common.json | 24 +- locales/fr/common.json | 14 +- locales/hu/common.json | 14 +- locales/it/common.json | 14 +- locales/ja/common.json | 14 +- locales/ko/common.json | 14 +- locales/lv/common.json | 14 +- locales/nl/common.json | 14 +- locales/pl/common.json | 14 +- locales/pt/common.json | 14 +- locales/ro/common.json | 14 +- locales/ru/common.json | 14 +- locales/tr/common.json | 14 +- locales/uk/common.json | 14 +- locales/zh/common.json | 14 +- package-lock.json | 22 +- package.json | 1 + stores/settings-store.ts | 2 +- 31 files changed, 798 insertions(+), 118 deletions(-) create mode 100644 hooks/use-calendar-locale.ts create mode 100644 lib/jalali-utils.ts diff --git a/components/calendar/calendar-month-view.tsx b/components/calendar/calendar-month-view.tsx index 0b2305127..c91a71bd9 100644 --- a/components/calendar/calendar-month-view.tsx +++ b/components/calendar/calendar-month-view.tsx @@ -1,11 +1,8 @@ "use client"; import { useMemo, useState, useCallback, type DragEvent } from "react"; -import { useTranslations, useFormatter } from "next-intl"; -import { - startOfMonth, endOfMonth, startOfWeek, endOfWeek, - eachDayOfInterval, isSameDay, isSameMonth, isToday, format, parseISO, -} from "date-fns"; +import { useTranslations } from "next-intl"; +import { format, parseISO } from "date-fns"; import { cn } from "@/lib/utils"; import { EventCard } from "./event-card"; import { buildWeekSegments, getEventDayBounds, getPrimaryCalendarId } from "@/lib/calendar-utils"; @@ -14,6 +11,7 @@ import { useAuthStore } from "@/stores/auth-store"; import { useCalendarStore } from "@/stores/calendar-store"; import type { PendingEventPreview } from "./event-modal"; import { toast } from "@/stores/toast-store"; +import { useCalendarLocale } from "@/hooks/use-calendar-locale"; interface CalendarMonthViewProps { selectedDate: Date; @@ -47,16 +45,21 @@ export function CalendarMonthView({ pendingPreview, }: CalendarMonthViewProps) { const t = useTranslations("calendar"); - const intlFormatter = useFormatter(); - const weekStart = (firstDayOfWeek === 0 ? 0 : 1) as 0 | 1; + const { + weekStartsOn, + dayHeaderKeys, + getMonthGridDays, + checkIsToday, + checkIsSameMonth, + checkIsSameDay, + formatDayNumber, + formatFullDate, + } = useCalendarLocale(); - const days = useMemo(() => { - const monthStart = startOfMonth(selectedDate); - const monthEnd = endOfMonth(selectedDate); - const gridStart = startOfWeek(monthStart, { weekStartsOn: weekStart }); - const gridEnd = endOfWeek(monthEnd, { weekStartsOn: weekStart }); - return eachDayOfInterval({ start: gridStart, end: gridEnd }); - }, [selectedDate, weekStart]); + const days = useMemo( + () => getMonthGridDays(selectedDate), + [selectedDate, getMonthGridDays], + ); const calendarMap = useMemo(() => { const map = new Map(); @@ -83,10 +86,6 @@ export function CalendarMonthView({ return map; }, [events]); - const dayHeaders = firstDayOfWeek === 0 - ? ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const - : ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const; - const weeks = useMemo(() => { const result: Date[][] = []; for (let i = 0; i < days.length; i += 7) { @@ -141,9 +140,9 @@ export function CalendarMonthView({ }, [t]); return ( -
+
- {dayHeaders.map((d) => ( + {dayHeaderKeys.map((d) => (
{week.map((day) => { - const inMonth = isSameMonth(day, selectedDate); - const selected = isSameDay(day, selectedDate); - const today = isToday(day); + const inMonth = checkIsSameMonth(day, selectedDate); + const selected = checkIsSameDay(day, selectedDate); + const today = checkIsToday(day); const key = format(day, "yyyy-MM-dd"); const dayEvents = eventsByDate.get(key) || []; - const fullDateLabel = intlFormatter.dateTime(day, { weekday: "long", month: "long", day: "numeric", year: "numeric" }); + const fullDateLabel = formatFullDate(day); return (
- {format(day, "d")} + {formatDayNumber(day)}
{isMobile ? ( @@ -219,7 +218,7 @@ export function CalendarMonthView({ {dayEvents.length > 3 && ( )} - {pendingPreview && isSameDay(pendingPreview.start, day) && ( + {pendingPreview && checkIsSameDay(pendingPreview.start, day) && ( {!isMobile && pendingPreview && (() => { - const previewDayIdx = week.findIndex(d => isSameDay(d, pendingPreview.start)); + const previewDayIdx = week.findIndex(d => checkIsSameDay(d, pendingPreview.start)); if (previewDayIdx === -1) return null; const previewRow = rowCount; const cal = calendarMap.get(pendingPreview.calendarId); diff --git a/components/calendar/calendar-toolbar.tsx b/components/calendar/calendar-toolbar.tsx index ec9480dd6..0a1296634 100644 --- a/components/calendar/calendar-toolbar.tsx +++ b/components/calendar/calendar-toolbar.tsx @@ -1,13 +1,14 @@ "use client"; import { useState, useRef, useEffect } from "react"; -import { useTranslations, useFormatter } from "next-intl"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { ChevronLeft, ChevronRight, Plus, Upload, CalendarDays, Globe, ChevronDown, ArrowLeft, Menu } from "lucide-react"; -import { addDays, startOfWeek } from "date-fns"; +import { startOfWeek } from "date-fns"; import { cn } from "@/lib/utils"; import type { CalendarViewMode } from "@/stores/calendar-store"; import type { Calendar } from "@/lib/jmap/types"; +import { useCalendarLocale } from "@/hooks/use-calendar-locale"; interface CalendarToolbarProps { selectedDate: Date; @@ -50,7 +51,14 @@ export function CalendarToolbar({ onMenuClick, }: CalendarToolbarProps) { const t = useTranslations("calendar"); - const formatter = useFormatter(); + const { + weekStartsOn, + formatMonthYear, + formatMonthYearShort, + formatWeekRange, + formatWeekRangeShort, + formatFullDate, + } = useCalendarLocale(); const views: CalendarViewMode[] = enableCalendarTasks ? ["month", "week", "day", "agenda", "tasks"] : ["month", "week", "day", "agenda"]; @@ -72,28 +80,22 @@ export function CalendarToolbar({ switch (viewMode) { case "month": return isMobile - ? formatter.dateTime(selectedDate, { month: "short", year: "numeric" }) - : formatter.dateTime(selectedDate, { month: "long", year: "numeric" }); + ? formatMonthYearShort(selectedDate) + : formatMonthYear(selectedDate); case "week": { - const ws = startOfWeek(selectedDate, { weekStartsOn: firstDayOfWeek as 0 | 1 }); - const we = addDays(ws, 6); - if (isMobile) { - return `${formatter.dateTime(ws, { month: "short", day: "numeric" })} – ${formatter.dateTime(we, { day: "numeric" })}`; - } - const sameMonth = ws.getMonth() === we.getMonth(); - if (sameMonth) { - return `${formatter.dateTime(ws, { month: "short", day: "numeric" })} – ${formatter.dateTime(we, { day: "numeric" })}, ${we.getFullYear()}`; - } - return `${formatter.dateTime(ws, { month: "short", day: "numeric" })} – ${formatter.dateTime(we, { month: "short", day: "numeric" })}, ${we.getFullYear()}`; + const ws = startOfWeek(selectedDate, { weekStartsOn }); + return isMobile + ? formatWeekRangeShort(ws) + : formatWeekRange(ws); } case "day": return isMobile - ? formatter.dateTime(selectedDate, { weekday: "short", month: "short", day: "numeric" }) - : formatter.dateTime(selectedDate, { weekday: "long", month: "long", day: "numeric", year: "numeric" }); + ? formatFullDate(selectedDate) + : formatFullDate(selectedDate); case "agenda": return isMobile - ? formatter.dateTime(selectedDate, { month: "short", year: "numeric" }) - : formatter.dateTime(selectedDate, { month: "long", year: "numeric" }); + ? formatMonthYearShort(selectedDate) + : formatMonthYear(selectedDate); case "tasks": return t("views.tasks"); } diff --git a/components/calendar/calendar-week-view.tsx b/components/calendar/calendar-week-view.tsx index 4d4a40520..dbfe08cee 100644 --- a/components/calendar/calendar-week-view.tsx +++ b/components/calendar/calendar-week-view.tsx @@ -58,7 +58,7 @@ export function CalendarWeekView({ const intlFormatter = useFormatter(); const scrollRef = useRef(null); const rootRef = useRef(null); - const weekStart = (firstDayOfWeek === 0 ? 0 : 1) as 0 | 1; + const weekStart = (firstDayOfWeek === 0 ? 0 : firstDayOfWeek === 6 ? 6 : 1) as 0 | 1 | 6; const weekDays = useMemo(() => { const start = startOfWeek(selectedDate, { weekStartsOn: weekStart }); diff --git a/components/calendar/mini-calendar.tsx b/components/calendar/mini-calendar.tsx index fa602afad..a51f74208 100644 --- a/components/calendar/mini-calendar.tsx +++ b/components/calendar/mini-calendar.tsx @@ -1,25 +1,19 @@ "use client"; import { useState, useMemo, Fragment } from "react"; -import { useTranslations, useFormatter } from "next-intl"; +import { useTranslations } from "next-intl"; import { ChevronLeft, ChevronRight, ChevronDown } from "lucide-react"; import { - startOfMonth, endOfMonth, startOfWeek, endOfWeek, addMonths, subMonths, addYears, subYears, setMonth, setYear, - eachDayOfInterval, getMonth, getYear, getISOWeek, getWeek, - isSameDay, isSameMonth, isToday, format, + getISOWeek, getWeek, format, } from "date-fns"; import { cn } from "@/lib/utils"; import { getEventDayBounds } from "@/lib/calendar-utils"; import type { CalendarEvent } from "@/lib/jmap/types"; +import { useCalendarLocale } from "@/hooks/use-calendar-locale"; type PickerView = "days" | "months" | "years"; -const MONTH_LABELS = [ - "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", -]; - interface MiniCalendarProps { selectedDate: Date; displayMonth: Date; @@ -40,17 +34,25 @@ export function MiniCalendar({ showWeekNumbers = false, }: MiniCalendarProps) { const t = useTranslations("calendar"); - const intlFormatter = useFormatter(); - const weekStart = (firstDayOfWeek === 0 ? 0 : 1) as 0 | 1; + const { + weekStartsOn, + dayHeaderKeys, + getMonthGridDays, + checkIsToday, + checkIsSameMonth, + checkIsSameDay, + formatDayNumber, + formatMonthYear, + getMonth, + getYear, + monthLabelKeys, + } = useCalendarLocale(); const [pickerView, setPickerView] = useState("days"); - const days = useMemo(() => { - const monthStart = startOfMonth(displayMonth); - const monthEnd = endOfMonth(displayMonth); - const gridStart = startOfWeek(monthStart, { weekStartsOn: weekStart }); - const gridEnd = endOfWeek(monthEnd, { weekStartsOn: weekStart }); - return eachDayOfInterval({ start: gridStart, end: gridEnd }); - }, [displayMonth, weekStart]); + const days = useMemo( + () => getMonthGridDays(displayMonth), + [displayMonth, getMonthGridDays], + ); const eventDates = useMemo(() => { const set = new Set(); @@ -67,20 +69,16 @@ export function MiniCalendar({ return set; }, [events]); - const dayHeaders = firstDayOfWeek === 0 - ? ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const - : ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const; - // Compute week numbers for each row (one per 7-day chunk) const weekNumbers = useMemo(() => { if (!showWeekNumbers) return []; const nums: number[] = []; for (let i = 0; i < days.length; i += 7) { // Use the first day of each row to determine the week number - nums.push(weekStart === 1 ? getISOWeek(days[i]) : getWeek(days[i], { weekStartsOn: 0 })); + nums.push(weekStartsOn === 1 ? getISOWeek(days[i]) : getWeek(days[i], { weekStartsOn: 0 })); } return nums; - }, [days, showWeekNumbers, weekStart]); + }, [days, showWeekNumbers, weekStartsOn]); const currentYear = getYear(displayMonth); const currentMonth = getMonth(displayMonth); @@ -116,7 +114,7 @@ export function MiniCalendar({ const headerLabel = pickerView === "days" - ? intlFormatter.dateTime(displayMonth, { month: "long", year: "numeric" }) + ? formatMonthYear(displayMonth) : pickerView === "months" ? String(currentYear) : `${decadeStart}\u2013${decadeStart + 9}`; @@ -160,15 +158,15 @@ export function MiniCalendar({ {showWeekNumbers && (
)} - {dayHeaders.map((d) => ( + {dayHeaderKeys.map((d) => (
{t(`days.${d}`)}
))} {days.map((day, index) => { - const inMonth = isSameMonth(day, displayMonth); - const selected = isSameDay(day, selectedDate); - const today = isToday(day); + const inMonth = checkIsSameMonth(day, displayMonth); + const selected = checkIsSameDay(day, selectedDate); + const today = checkIsToday(day); const hasEvent = eventDates.has(format(day, "yyyy-MM-dd")); const isFirstDayOfRow = index % 7 === 0; @@ -193,7 +191,7 @@ export function MiniCalendar({ selected && "bg-primary text-primary-foreground" )} > - {format(day, "d")} + {formatDayNumber(day)} {hasEvent && !selected && ( )} @@ -206,7 +204,7 @@ export function MiniCalendar({ {pickerView === "months" && (
- {MONTH_LABELS.map((label, i) => { + {monthLabelKeys.map((labelKey, i) => { const isCurrentMonth = i === currentMonth && currentYear === getYear(new Date()); const isSelected = i === getMonth(selectedDate) && currentYear === getYear(selectedDate); return ( @@ -220,7 +218,7 @@ export function MiniCalendar({ !isSelected && !isCurrentMonth && "hover:bg-muted" )} > - {label} + {t(`months.${labelKey}`)} ); })} diff --git a/components/settings/language-settings.tsx b/components/settings/language-settings.tsx index ab678c664..fc46e523d 100644 --- a/components/settings/language-settings.tsx +++ b/components/settings/language-settings.tsx @@ -104,6 +104,7 @@ export function LanguageSettings() { onChange={(value) => updateSetting('firstDayOfWeek', parseInt(value) as FirstDayOfWeek)} options={[ { value: '1', label: tDays('monday') }, + { value: '6', label: tDays('saturday') }, { value: '0', label: tDays('sunday') }, ]} /> diff --git a/hooks/use-calendar-locale.ts b/hooks/use-calendar-locale.ts new file mode 100644 index 000000000..d0ed10022 --- /dev/null +++ b/hooks/use-calendar-locale.ts @@ -0,0 +1,251 @@ +"use client"; + +import { useMemo } from "react"; +import { useLocale } from "next-intl"; +import { useSettingsStore } from "@/stores/settings-store"; +import { + toJalali, + jalaliMonthLength, + startOfJalaliMonth, + endOfJalaliMonth, + eachDayOfJalaliMonth, + getDayHeaderKeys, + shouldUseJalaliCalendar, + JALALI_MONTHS, + type JalaliDate, +} from "@/lib/jalali-utils"; +import { + startOfMonth, + endOfMonth, + startOfWeek, + endOfWeek, + eachDayOfInterval, + isSameDay, + isSameMonth, + isToday, +} from "date-fns"; + +/** + * Unified calendar-locale hook. + * + * Abstracts away the differences between Gregorian and Jalali calendars so + * view components can render dates correctly without calendar-specific + * branching. + */ +export function useCalendarLocale() { + const locale = useLocale(); + const firstDayOfWeek = useSettingsStore((s) => s.firstDayOfWeek); + const isJalali = shouldUseJalaliCalendar(locale); + + // Normalize weekStart for date-fns (0 | 1 | 2 | 3 | 4 | 5 | 6) + const weekStartsOn = useMemo(() => { + if (firstDayOfWeek === 0) return 0 as const; + if (firstDayOfWeek === 6) return 6 as const; + return 1 as const; + }, [firstDayOfWeek]); + + // Ordered day-header translation keys + const dayHeaderKeys = useMemo( + () => getDayHeaderKeys(weekStartsOn), + [weekStartsOn], + ); + + // ------------------------------------------------------------------ + // Month-grid construction + // ------------------------------------------------------------------ + + /** Build the flat array of Dates that populate a full month grid. */ + const getMonthGridDays = (referenceDate: Date): Date[] => { + if (isJalali) { + const { jy, jm } = toJalali(referenceDate); + return eachDayOfJalaliMonth(jy, jm, weekStartsOn); + } + const monthStart = startOfMonth(referenceDate); + const monthEnd = endOfMonth(referenceDate); + const gridStart = startOfWeek(monthStart, { weekStartsOn }); + const gridEnd = endOfWeek(monthEnd, { weekStartsOn }); + return eachDayOfInterval({ start: gridStart, end: gridEnd }); + }; + + // ------------------------------------------------------------------ + // Day-level queries + // ------------------------------------------------------------------ + + /** Is the given date "today" in the active calendar system? */ + const checkIsToday = (date: Date): boolean => { + if (isJalali) { + const now = toJalali(new Date()); + const target = toJalali(date); + return now.jy === target.jy && now.jm === target.jm && now.jd === target.jd; + } + return isToday(date); + }; + + /** Does the date belong to the same month as the reference date? */ + const checkIsSameMonth = (date: Date, referenceDate: Date): boolean => { + if (isJalali) { + const a = toJalali(date); + const b = toJalali(referenceDate); + return a.jy === b.jy && a.jm === b.jm; + } + return isSameMonth(date, referenceDate); + }; + + /** Are two dates the same calendar day? */ + const checkIsSameDay = (date1: Date, date2: Date): boolean => { + if (isJalali) { + const a = toJalali(date1); + const b = toJalali(date2); + return a.jy === b.jy && a.jm === b.jm && a.jd === b.jd; + } + return isSameDay(date1, date2); + }; + + // ------------------------------------------------------------------ + // Display formatting + // ------------------------------------------------------------------ + + /** Day-of-month number for a calendar cell (string). */ + const formatDayNumber = (date: Date): string => { + if (isJalali) { + return String(toJalali(date).jd); + } + return String(date.getDate()); + }; + + /** Full month + year label for the toolbar / mini-calendar header. */ + const formatMonthYear = (date: Date): string => { + if (isJalali) { + const { jy, jm } = toJalali(date); + return `${JALALI_MONTHS[jm - 1]} ${jy}`; + } + const month = date.toLocaleString(locale === "en" ? "en-US" : locale, { + month: "long", + }); + return `${month} ${date.getFullYear()}`; + }; + + /** Short month + year for mobile. */ + const formatMonthYearShort = (date: Date): string => { + if (isJalali) { + const { jy, jm } = toJalali(date); + const short = JALALI_MONTHS[jm - 1].slice(0, 3); + return `${short} ${jy}`; + } + const month = date.toLocaleString(locale === "en" ? "en-US" : locale, { + month: "short", + }); + return `${month} ${date.getFullYear()}`; + }; + + /** Week range label (e.g. "6 – 12 Farvardin 1404"). */ + const formatWeekRange = (weekStart: Date): string => { + const weekEnd = new Date(weekStart); + weekEnd.setDate(weekEnd.getDate() + 6); + if (isJalali) { + const start = toJalali(weekStart); + const end = toJalali(weekEnd); + if (start.jm === end.jm) { + return `${start.jd} – ${end.jd} ${JALALI_MONTHS[start.jm - 1]} ${start.jy}`; + } + return `${start.jd} ${JALALI_MONTHS[start.jm - 1]} – ${end.jd} ${JALALI_MONTHS[end.jm - 1]} ${end.jy}`; + } + const sameMonth = weekStart.getMonth() === weekEnd.getMonth(); + const s = weekStart.toLocaleString(locale === "en" ? "en-US" : locale, { + month: "short", + day: "numeric", + }); + const e = weekEnd.toLocaleString(locale === "en" ? "en-US" : locale, { + month: sameMonth ? undefined : "short", + day: "numeric", + }); + return `${s} – ${e}, ${weekEnd.getFullYear()}`; + }; + + /** Short week range for mobile. */ + const formatWeekRangeShort = (weekStart: Date): string => { + const weekEnd = new Date(weekStart); + weekEnd.setDate(weekEnd.getDate() + 6); + if (isJalali) { + const start = toJalali(weekStart); + const end = toJalali(weekEnd); + return `${start.jd}/${start.jm} – ${end.jd}/${end.jm}`; + } + const s = weekStart.toLocaleString(locale === "en" ? "en-US" : locale, { + month: "short", + day: "numeric", + }); + const e = weekEnd.toLocaleString(locale === "en" ? "en-US" : locale, { + day: "numeric", + }); + return `${s} – ${e}`; + }; + + /** Full date label for accessibility / tooltips. */ + const formatFullDate = (date: Date): string => { + if (isJalali) { + const { jy, jm, jd } = toJalali(date); + const dayOfWeek = date.getDay(); + const dayNames = getDayHeaderKeys(weekStartsOn); + // Map from Gregorian day index to the correct label from the reordered list + const dayIdx = (dayOfWeek - weekStartsOn + 7) % 7; + const dayKey = dayNames[dayIdx]; + return `${dayKey} ${jd} ${JALALI_MONTHS[jm - 1]} ${jy}`; + } + return date.toLocaleString(locale === "en" ? "en-US" : locale, { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + }); + }; + + // ------------------------------------------------------------------ + // Calendar-system-aware month/year getters (for navigation, etc.) + // All return values use **0-based** months to stay compatible with + // date-fns functions like `setMonth`. + // ------------------------------------------------------------------ + + const getMonth = (date: Date): number => { + if (isJalali) return toJalali(date).jm - 1; // 0-11 + return date.getMonth(); // 0-11 + }; + + const getYear = (date: Date): number => { + if (isJalali) return toJalali(date).jy; + return date.getFullYear(); + }; + + /** Keys for the month selector dropdown (used by MiniCalendar). */ + const monthLabelKeys = useMemo(() => { + if (isJalali) { + return [ + "far", "ord", "kho", "tir", "mor", "sha", + "meh", "aba", "aza", "dey", "bah", "esf", + ]; + } + return [ + "jan", "feb", "mar", "apr", "may", "jun", + "jul", "aug", "sep", "oct", "nov", "dec", + ]; + }, [isJalali]); + + return { + isJalali, + weekStartsOn, + dayHeaderKeys, + getMonthGridDays, + checkIsToday, + checkIsSameMonth, + checkIsSameDay, + formatDayNumber, + formatMonthYear, + formatMonthYearShort, + formatWeekRange, + formatWeekRangeShort, + formatFullDate, + getMonth, + getYear, + monthLabelKeys, + } as const; +} diff --git a/hooks/use-format-event-date.ts b/hooks/use-format-event-date.ts index 137f1b396..9e09b3ee7 100644 --- a/hooks/use-format-event-date.ts +++ b/hooks/use-format-event-date.ts @@ -1,27 +1,42 @@ import { useCallback } from "react"; -import { useTranslations } from "next-intl"; +import { useTranslations, useLocale } from "next-intl"; import { format } from "date-fns"; +import { toJalali, shouldUseJalaliCalendar, JALALI_MONTHS } from "@/lib/jalali-utils"; /** * Returns a memoized function that formats a calendar event date * using the current locale for day and month names. - * + * * The string will be in the format: "EEE, MMM d, yyyy" - * + * * For example: "Wed, Apr 29, 2026" (en) * "Qua, Abr 29, 2026" (pt) + * + * When the Jalali calendar is active (fa locale), the format uses + * Persian day/month names with the Jalali year, e.g.: + * "چهارشنبه, ۹ اردیبهشت ۱۴۰۵" */ export function useFormatEventDate(): (date: Date) => string { const t = useTranslations("calendar"); + const locale = useLocale(); + const isJalali = shouldUseJalaliCalendar(locale); return useCallback( (date: Date): string => { + if (isJalali) { + const { jy, jm, jd } = toJalali(date); + // Use Gregorian day-of-week for the translation key (date-fns format) + const dayOfWeek = format(date, "EEE").toLowerCase(); + const monthName = JALALI_MONTHS[jm - 1]; + return `${t(`days.${dayOfWeek}`)}, ${jd} ${monthName} ${jy}`; + } + const dayOfWeek = format(date, "EEE").toLowerCase(); const month = format(date, "MMM").toLowerCase(); const day = format(date, "d"); const year = format(date, "yyyy"); return `${t(`days.${dayOfWeek}`)}, ${t(`months.${month}`)} ${day}, ${year}`; }, - [t] + [t, isJalali] ); } diff --git a/lib/jalali-utils.ts b/lib/jalali-utils.ts new file mode 100644 index 000000000..1670e37e9 --- /dev/null +++ b/lib/jalali-utils.ts @@ -0,0 +1,169 @@ +/** + * Jalali (Persian/Shamsi) calendar utilities. + * + * All internal date handling remains Gregorian (ISO 8601). The functions + * in this module convert Gregorian ↔ Jalali at the display layer only. + * + * Uses `jalaali-js` for the underlying calendar math. + */ +import * as jalaali from 'jalaali-js'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +/** A Jalali date represented as year, month (1-12), day (1-31). */ +export interface JalaliDate { + /** Jalali year (e.g. 1405) */ + jy: number; + /** Jalali month (1 = Farvardin, 12 = Esfand) */ + jm: number; + /** Jalali day of month (1-31) */ + jd: number; +} + +// --------------------------------------------------------------------------- +// Gregorian ↔ Jalali conversion +// --------------------------------------------------------------------------- + +/** Convert a Gregorian Date to its Jalali equivalent. */ +export function toJalali(date: Date): JalaliDate { + const { jy, jm, jd } = jalaali.toJalaali(date); + return { jy, jm, jd }; +} + +/** Convert a Jalali date to a Gregorian Date. */ +export function toGregorian(jy: number, jm: number, jd: number): Date { + const { gy, gm, gd } = jalaali.toGregorian(jy, jm, jd); + return new Date(gy, gm - 1, gd); +} + +// --------------------------------------------------------------------------- +// Jalali month / day info +// --------------------------------------------------------------------------- + +/** Full Persian month names (Farvardin … Esfand). */ +export const JALALI_MONTHS: readonly string[] = [ + 'فروردین', + 'اردیبهشت', + 'خرداد', + 'تیر', + 'مرداد', + 'شهریور', + 'مهر', + 'آبان', + 'آذر', + 'دی', + 'بهمن', + 'اسفند', +]; + +/** Number of days in a Jalali month (handles leap years). */ +export function jalaliMonthLength(jy: number, jm: number): number { + return jalaali.jalaaliMonthLength(jy, jm); +} + +/** Is the given Jalali year a leap year? */ +export function isJalaliLeapYear(jy: number): boolean { + return jalaali.isLeapJalaaliYear(jy); +} + +// --------------------------------------------------------------------------- +// Calendar grid helpers (analogous to date-fns startOfWeek / eachDayOfInterval) +// --------------------------------------------------------------------------- + +/** + * Return the first day of the Jalali month (Gregorian Date) aligned to the + * week grid so the month view can be rendered. `weekStartsOn` follows the + * same convention as `date-fns`: 0=Sun, 1=Mon, …, 6=Sat. + */ +export function startOfJalaliMonth( + jy: number, + jm: number, + weekStartsOn: number = 6, +): Date { + const firstDay = toGregorian(jy, jm, 1); + const dayOfWeek = firstDay.getDay(); // 0=Sun … 6=Sat + const offset = (dayOfWeek - weekStartsOn + 7) % 7; + const result = new Date(firstDay); + result.setDate(result.getDate() - offset); + return result; +} + +/** + * Return the last day of the Jalali month (Gregorian Date) aligned to the + * week grid. + */ +export function endOfJalaliMonth( + jy: number, + jm: number, + weekStartsOn: number = 6, +): Date { + const lastDay = toGregorian(jy, jm, jalaliMonthLength(jy, jm)); + const dayOfWeek = lastDay.getDay(); + const offset = (weekStartsOn - dayOfWeek + 6) % 7; + const result = new Date(lastDay); + result.setDate(result.getDate() + offset); + return result; +} + +/** + * Build a flat array of Gregorian Dates covering the entire calendar grid + * for a Jalali month (from the week-aligned start to the week-aligned end). + */ +export function eachDayOfJalaliMonth( + jy: number, + jm: number, + weekStartsOn: number = 6, +): Date[] { + const start = startOfJalaliMonth(jy, jm, weekStartsOn); + const end = endOfJalaliMonth(jy, jm, weekStartsOn); + const days: Date[] = []; + const cursor = new Date(start); + while (cursor <= end) { + days.push(new Date(cursor)); + cursor.setDate(cursor.getDate() + 1); + } + return days; +} + +// --------------------------------------------------------------------------- +// Locale-aware day header order +// --------------------------------------------------------------------------- + +/** + * Return the array of day-abbreviation translation keys in the correct order + * for the given `firstDayOfWeek` (0=Sun … 6=Sat). + * + * Usage: + * const dayHeaders = getDayHeaderKeys(firstDayOfWeek); + * dayHeaders.map((key) => t(`calendar.days.${key}`)) + */ +export function getDayHeaderKeys( + firstDayOfWeek: number, +): readonly string[] { + const ALL: readonly string[] = [ + 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', + ] as const; + return [...ALL.slice(firstDayOfWeek), ...ALL.slice(0, firstDayOfWeek)]; +} + +// --------------------------------------------------------------------------- +// Locale detection helper +// --------------------------------------------------------------------------- + +/** + * Should the UI render dates using the Jalali calendar? + * + * Currently this is keyed off the `fa` locale. Administrators who want a + * different locale with Jalali dates can extend this logic later. + */ +export function shouldUseJalaliCalendar(locale: string): boolean { + return locale === 'fa'; +} + +/** Default `firstDayOfWeek` for a given locale. */ +export function defaultFirstDayOfWeek(locale: string): number { + if (locale === 'fa') return 6; // Saturday + return 1; // Monday (ISO convention) +} diff --git a/locales/cs/common.json b/locales/cs/common.json index f9eb7d1ac..86eb234a9 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -2904,7 +2904,19 @@ "sep": "zář", "oct": "říj", "nov": "lis", - "dec": "pro" + "dec": "pro", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "nav_open_menu": "Otevřít nabídku" }, diff --git a/locales/da/common.json b/locales/da/common.json index 0a7379f0e..334b635ce 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -2723,7 +2723,19 @@ "sep": "Sep", "oct": "Okt", "nov": "Nov", - "dec": "Dec" + "dec": "Dec", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "notifications": { "event_created": "Begivenhed oprettet", diff --git a/locales/de/common.json b/locales/de/common.json index 62dccaf53..02b42581f 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -2904,7 +2904,19 @@ "sep": "Sep", "oct": "Okt", "nov": "Nov", - "dec": "Dez" + "dec": "Dez", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "nav_open_menu": "Menü öffnen" }, diff --git a/locales/en/common.json b/locales/en/common.json index 5518786d7..9c27f008f 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -2723,7 +2723,19 @@ "sep": "Sep", "oct": "Oct", "nov": "Nov", - "dec": "Dec" + "dec": "Dec", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "notifications": { "event_created": "Event created", diff --git a/locales/es/common.json b/locales/es/common.json index ce98a1d32..7110b9bee 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -2904,7 +2904,19 @@ "sep": "Sep", "oct": "Oct", "nov": "Nov", - "dec": "Dic" + "dec": "Dic", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "nav_open_menu": "Abrir menú" }, diff --git a/locales/fa/common.json b/locales/fa/common.json index c14293e17..b04243952 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -681,7 +681,12 @@ "recipient_email_placeholder": "آدرس ایمیل", "recipient_name_placeholder": "نام نمایشی", "autocomplete_search_server": "جستجو در سرور", - "autocomplete_searching": "در حال جستجو..." + "autocomplete_searching": "در حال جستجو...", + "text_direction": { + "toggle": "تغییر جهت متن (چپ‌به‌راست / راست‌به‌چپ)", + "ltr": "چپ‌به‌راست", + "rtl": "راست‌به‌چپ" + } }, "confirm_dialog": { "confirm": "تأیید", @@ -1880,7 +1885,8 @@ "validation": { "empty": "نام قالب الزامی است", "too_long": "نام قالب باید ۲۰۰ کاراکتر یا کمتر باشد" - } + }, + "image_too_large": "حجم تصویر بیش از حد مجاز (حداکثر ۱ مگابایت)" }, "files": { "display": { @@ -2723,7 +2729,19 @@ "sep": "سپتامبر", "oct": "اکتبر", "nov": "نوامبر", - "dec": "دسامبر" + "dec": "دسامبر", + "far": "فروردین", + "ord": "اردیبهشت", + "kho": "خرداد", + "tir": "تیر", + "mor": "مرداد", + "sha": "شهریور", + "meh": "مهر", + "aba": "آبان", + "aza": "آذر", + "dey": "دی", + "bah": "بهمن", + "esf": "اسفند" }, "notifications": { "event_created": "رویداد ایجاد شد", diff --git a/locales/fr/common.json b/locales/fr/common.json index 3ab055de3..9066c9d51 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -2723,7 +2723,19 @@ "sep": "sept.", "oct": "oct.", "nov": "nov.", - "dec": "déc." + "dec": "déc.", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "notifications": { "event_created": "Événement créé", diff --git a/locales/hu/common.json b/locales/hu/common.json index 7caee6575..261eeb3fd 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -2723,7 +2723,19 @@ "sep": "Szep", "oct": "Okt", "nov": "Nov", - "dec": "Dec" + "dec": "Dec", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "notifications": { "event_created": "Esemény létrehozva", diff --git a/locales/it/common.json b/locales/it/common.json index 4e285a140..d288518ba 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -2904,7 +2904,19 @@ "sep": "set", "oct": "ott", "nov": "nov", - "dec": "dic" + "dec": "dic", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "nav_open_menu": "Apri menu" }, diff --git a/locales/ja/common.json b/locales/ja/common.json index 28c6af0e6..97bcec1fa 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -2904,7 +2904,19 @@ "sep": "9月", "oct": "10月", "nov": "11月", - "dec": "12月" + "dec": "12月", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "nav_open_menu": "メニューを開く" }, diff --git a/locales/ko/common.json b/locales/ko/common.json index 1e9f20b91..55d2852c6 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -2904,7 +2904,19 @@ "sep": "9월", "oct": "10월", "nov": "11월", - "dec": "12월" + "dec": "12월", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "nav_open_menu": "메뉴 열기" }, diff --git a/locales/lv/common.json b/locales/lv/common.json index f61bb9006..f9fc75b70 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -2904,7 +2904,19 @@ "sep": "sept.", "oct": "okt.", "nov": "nov.", - "dec": "dec." + "dec": "dec.", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "nav_open_menu": "Atvērt izvēlni" }, diff --git a/locales/nl/common.json b/locales/nl/common.json index f47da90a2..52624f8b9 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -2904,7 +2904,19 @@ "sep": "sep", "oct": "okt", "nov": "nov", - "dec": "dec" + "dec": "dec", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "nav_open_menu": "Menu openen" }, diff --git a/locales/pl/common.json b/locales/pl/common.json index 5f28dd906..c139a7c61 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -2904,7 +2904,19 @@ "sep": "wrz", "oct": "paź", "nov": "lis", - "dec": "gru" + "dec": "gru", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "nav_open_menu": "Otwórz menu" }, diff --git a/locales/pt/common.json b/locales/pt/common.json index 2cfc68696..c11eced38 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -2723,7 +2723,19 @@ "sep": "set", "oct": "out", "nov": "nov", - "dec": "dez" + "dec": "dez", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "notifications": { "event_created": "Evento criado", diff --git a/locales/ro/common.json b/locales/ro/common.json index 2fafc8df1..ef5fed7dc 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -2723,7 +2723,19 @@ "sep": "Sep", "oct": "Oct", "nov": "Nov", - "dec": "Dec" + "dec": "Dec", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "notifications": { "event_created": "Eveniment creat", diff --git a/locales/ru/common.json b/locales/ru/common.json index 9aa419f88..02d6ce9b0 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -2904,7 +2904,19 @@ "sep": "сент.", "oct": "окт.", "nov": "нояб.", - "dec": "дек." + "dec": "дек.", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "nav_open_menu": "Открыть меню" }, diff --git a/locales/tr/common.json b/locales/tr/common.json index bdd156b95..79f886a8f 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -2723,7 +2723,19 @@ "sep": "Eyl", "oct": "Eki", "nov": "Kas", - "dec": "Ara" + "dec": "Ara", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "notifications": { "event_created": "Etkinlik oluşturuldu", diff --git a/locales/uk/common.json b/locales/uk/common.json index 3e984acaf..a4abe245c 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -2904,7 +2904,19 @@ "sep": "верес.", "oct": "жовт.", "nov": "лист.", - "dec": "груд." + "dec": "груд.", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "nav_open_menu": "Відкрити меню" }, diff --git a/locales/zh/common.json b/locales/zh/common.json index e54cae254..5e151addb 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -2904,7 +2904,19 @@ "sep": "9月", "oct": "10月", "nov": "11月", - "dec": "12月" + "dec": "12月", + "far": "Farvardin", + "ord": "Ordibehesht", + "kho": "Khordad", + "tir": "Tir", + "mor": "Mordad", + "sha": "Shahrivar", + "meh": "Mehr", + "aba": "Aban", + "aza": "Azar", + "dey": "Dey", + "bah": "Bahman", + "esf": "Esfand" }, "nav_open_menu": "打开菜单" }, diff --git a/package-lock.json b/package-lock.json index ed51ec03e..0763801ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "dompurify": "^3.4.1", + "jalaali-js": "^2.0.0", "jszip": "^3.10.1", "lucide-react": "^1.8.0", "next": "^16.2.6", @@ -6222,7 +6223,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -7139,6 +7139,15 @@ "node": ">= 0.4" } }, + "node_modules/jalaali-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-2.0.0.tgz", + "integrity": "sha512-HkWlwO3KxuYwERP1jsn+5M+QA+EKpIJ+zGesLee5VJNn2d2Melue0uQIvd9C/0QYFR7XVWvfF/uiNEz9Jbr9Hw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -7894,17 +7903,6 @@ } } }, - "node_modules/next-intl/node_modules/@swc/helpers": { - "version": "0.5.23", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", - "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index dc954f4c5..baf46cf03 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "dompurify": "^3.4.1", + "jalaali-js": "^2.0.0", "jszip": "^3.10.1", "lucide-react": "^1.8.0", "next": "^16.2.6", diff --git a/stores/settings-store.ts b/stores/settings-store.ts index e958f17b0..abb4b3242 100644 --- a/stores/settings-store.ts +++ b/stores/settings-store.ts @@ -48,7 +48,7 @@ export type DateFormat = 'smart' | 'relative' | 'full'; */ export type DateLocale = 'auto' | 'iso' | 'en-GB' | 'en-US'; export type TimeFormat = '12h' | '24h'; -export type FirstDayOfWeek = 0 | 1; // 0 = Sunday, 1 = Monday +export type FirstDayOfWeek = 0 | 1 | 6; // 0 = Sunday, 1 = Monday, 6 = Saturday export type ExternalContentPolicy = 'ask' | 'block' | 'allow'; export type MailAttachmentAction = 'preview' | 'download'; export type AttachmentPosition = 'beside-sender' | 'below-header';