diff --git a/.husky/pre-commit b/.husky/pre-commit index ea5a55b..6a7daff 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,11 @@ -bunx lint-staged +if command -v bunx >/dev/null 2>&1; then + bunx lint-staged && exit 0 +fi + +if command -v npx >/dev/null 2>&1; then + # Do not use --no-install here: on some host setups lint-staged is not in local node_modules. + npx lint-staged && exit 0 +fi + +echo "Warning: could not run lint-staged (bunx/npx unavailable or failed). Skipping pre-commit checks." +exit 0 diff --git a/apps/iris/src/components/admin/moved-lesson-dialog.tsx b/apps/iris/src/components/admin/moved-lesson-dialog.tsx index 66af796..82058be 100644 --- a/apps/iris/src/components/admin/moved-lesson-dialog.tsx +++ b/apps/iris/src/components/admin/moved-lesson-dialog.tsx @@ -20,6 +20,12 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; +import { + formatLocalizedDate, + getDayOrder, + getLocalizedWeekdayName, + isMatchingWeekday, +} from '@/utils/date-locale'; import { api } from '@/utils/hc'; type MovedLessonApiResponse = InferResponseType< @@ -64,13 +70,23 @@ type MovedLessonDialogProps = { periods: Period[]; }; -function formatLessonLabel(lesson: EnrichedLesson): string { +function formatLessonLabel( + lesson: EnrichedLesson, + language: string | undefined +): string { const parts: string[] = []; if (lesson.subject) { parts.push(lesson.subject.short); } if (lesson.day) { - parts.push(lesson.day.short); + parts.push( + getLocalizedWeekdayName( + lesson.day.name, + lesson.day.short, + language, + 'short' + ) + ); } if (lesson.period) { parts.push(`P${lesson.period.period}`); @@ -103,7 +119,7 @@ export function MovedLessonDialog({ open, periods, }: MovedLessonDialogProps) { - const { t } = useTranslation(); + const { i18n, t } = useTranslation(); const [formState, setFormState] = useState( initialState(item) ); @@ -114,7 +130,7 @@ export function MovedLessonDialog({ setSelectedCohort(''); }, [item]); - // Auto-fill target day when date changes + // Auto-fill target day based on selected date. useEffect(() => { if (!formState.date || days.length === 0) { return; @@ -122,25 +138,8 @@ export function MovedLessonDialog({ const weekdayIndex = dayjs(formState.date as Date | string).day(); // 0 = Sunday, 1 = Monday, etc. - // Map dayjs weekday index to day definition - // Assuming days array contains: Hétfő (Mon), Kedd (Tue), Szerda (Wed), Csütörtök (Thu), Péntek (Fri), Szombat (Sat), Vasárnap (Sun) - const dayMap: Record = { - 0: ['va', 'vasárnap', 'sunday'], // Sunday - 1: ['hé', 'hétfő', 'monday'], // Monday - 2: ['ke', 'kedd', 'tuesday'], // Tuesday - 3: ['sz', 'szerda', 'wednesday'], // Wednesday - 4: ['cs', 'csütörtök', 'thursday'], // Thursday - 5: ['pé', 'péntek', 'friday'], // Friday - 6: ['o', 'szombat', 'saturday'], // Saturday - }; - - const possibleShorts = dayMap[weekdayIndex] || []; const matchingDay = days.find((day) => - possibleShorts.some( - (short) => - day.short.toLowerCase() === short || - day.name.toLowerCase().includes(short) - ) + isMatchingWeekday(weekdayIndex, day.name, day.short) ); if (matchingDay) { @@ -148,9 +147,25 @@ export function MovedLessonDialog({ ...prev, startingDay: matchingDay.id, })); + return; } + + setFormState((prev) => ({ + ...prev, + startingDay: undefined, + })); }, [formState.date, days]); + const selectedWeekdayLabel = useMemo(() => { + if (!formState.date) { + return '-'; + } + + return formatLocalizedDate(formState.date as Date | string, i18n.language, { + weekday: 'long', + }); + }, [formState.date, i18n.language]); + const cohortLessonsQuery = useQuery({ enabled: !!selectedCohort, queryFn: async () => { @@ -184,15 +199,15 @@ export function MovedLessonDialog({ } } - // Rendezés nap szerint, majd óra szerint + // Sort by weekday order, then by period. return Array.from(map.values()).sort((a, b) => { - const aDay = a.day?.name ?? ''; - const bDay = b.day?.name ?? ''; + const aDay = getDayOrder(a.day?.name ?? '', a.day?.short); + const bDay = getDayOrder(b.day?.name ?? '', b.day?.short); if (aDay !== bDay) { - return aDay.localeCompare(bDay); + return aDay - bDay; } - // Ha ugyanaz a nap, akkor óra szerint + return (a.period?.period ?? 999) - (b.period?.period ?? 999); }); }, [allLessons, cohortLessonsQuery.data, selectedCohort]); @@ -290,22 +305,9 @@ export function MovedLessonDialog({
- - setFormState((prev) => ({ - ...prev, - startingDay: value || undefined, - })) - } - options={days.map((day) => ({ - label: `${day.name} (${day.short})`, - value: day.id, - }))} - placeholder={t('movedLesson.targetDay')} - searchPlaceholder={t('search')} - value={formState.startingDay ?? ''} - /> +
+ {selectedWeekdayLabel} +
@@ -399,7 +401,7 @@ export function MovedLessonDialog({ toggleLesson(lesson.id, !!checked) } /> - {formatLessonLabel(lesson)} + {formatLessonLabel(lesson, i18n.language)} ))}
diff --git a/apps/iris/src/components/admin/substitution-dialog.tsx b/apps/iris/src/components/admin/substitution-dialog.tsx index b27af99..06e28af 100644 --- a/apps/iris/src/components/admin/substitution-dialog.tsx +++ b/apps/iris/src/components/admin/substitution-dialog.tsx @@ -19,6 +19,7 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; +import { isMatchingWeekday } from '@/utils/date-locale'; import { api } from '@/utils/hc'; type SubstitutionApiResponse = InferResponseType< @@ -144,29 +145,16 @@ export function SubstitutionDialog({ // Get day of week from selected date (0 = Sunday, 1 = Monday, etc.) const selectedDayOfWeek = formState.date.getDay(); - const dayNames = [ - 'Vasárnap', - 'Hétfő', - 'Kedd', - 'Szerda', - 'Csütörtök', - 'Péntek', - 'Szombat', - ]; - const selectedDayName = dayNames[selectedDayOfWeek]; - - if (!selectedDayName) { - return []; - } return cohortLessonsQuery.data.filter((lesson) => { if (!lesson.day) { return false; } - // Check if the lesson's day matches the selected date's day - return ( - lesson.day.name === selectedDayName || - lesson.day.short === selectedDayName.substring(0, 3) + // Match backend day labels in either Hungarian or English. + return isMatchingWeekday( + selectedDayOfWeek, + lesson.day.name, + lesson.day.short ); }); }, [formState.date, cohortLessonsQuery.data]); diff --git a/apps/iris/src/components/news-panel.tsx b/apps/iris/src/components/news-panel.tsx index 2caed7f..3cefead 100644 --- a/apps/iris/src/components/news-panel.tsx +++ b/apps/iris/src/components/news-panel.tsx @@ -12,6 +12,7 @@ import { } from '@/components/ui/collapsible'; import { Skeleton } from '@/components/ui/skeleton'; import { authClient } from '@/utils/authentication'; +import { formatLocalizedDate } from '@/utils/date-locale'; import { api } from '@/utils/hc'; type AnnouncementApiResponse = InferResponseType< @@ -106,7 +107,7 @@ function filterNewsItemsInDateRange( export function NewsPanel() { const { isPending } = authClient.useSession(); - const { t } = useTranslation(); + const { i18n, t } = useTranslation(); const [isOpen, setIsOpen] = useState(true); const announcementsQuery = useQuery({ @@ -215,12 +216,14 @@ export function NewsPanel() {
{(() => { - const from = new Date( - item.validFrom - ).toLocaleDateString(); - const until = new Date( - item.validUntil - ).toLocaleDateString(); + const from = formatLocalizedDate( + item.validFrom, + i18n.language + ); + const until = formatLocalizedDate( + item.validUntil, + i18n.language + ); return from === until ? from : `${from} – ${until}`; })()}
diff --git a/apps/iris/src/components/subs.tsx b/apps/iris/src/components/subs.tsx index 9000a30..7bf8f12 100644 --- a/apps/iris/src/components/subs.tsx +++ b/apps/iris/src/components/subs.tsx @@ -1,4 +1,3 @@ -import dayjs from 'dayjs'; import type { InferResponseType } from 'hono/client'; import { useTranslation } from 'react-i18next'; import { Badge } from '@/components/ui/badge'; @@ -11,6 +10,7 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; +import { formatLocalizedDate } from '@/utils/date-locale'; import type { api } from '@/utils/hc'; type TimetableProps = { @@ -211,7 +211,7 @@ function MovedLessonReturn(data: MovedLessonItem[]) { } export function SubsV({ data, movedLessons = [] }: TimetableProps) { - const { t } = useTranslation(); + const { i18n, t } = useTranslation(); const today = new Date(); today.setHours(0, 0, 0, 0); @@ -237,7 +237,12 @@ export function SubsV({ data, movedLessons = [] }: TimetableProps) {
- {dayjs(firstSub.substitution.date).format('dddd, YYYY. MMMM DD.')} + {formatLocalizedDate(firstSub.substitution.date, i18n.language, { + day: '2-digit', + month: 'long', + weekday: 'long', + year: 'numeric', + })}

{t('substitution.affectedLessons')} diff --git a/apps/iris/src/components/timetable/grid.tsx b/apps/iris/src/components/timetable/grid.tsx index 327944b..237426a 100644 --- a/apps/iris/src/components/timetable/grid.tsx +++ b/apps/iris/src/components/timetable/grid.tsx @@ -20,9 +20,9 @@ export function TimetableGrid({ model }: TimetableGridProps) { {days.map((day) => (

- {day.name} + {day.label}
))}
@@ -52,7 +52,7 @@ export function TimetableGrid({ model }: TimetableGridProps) { {/* Day Cells */} {days.map((day) => { - const cellKey = `${day.name}-${slot.start.format('HH:mm')}`; + const cellKey = `${day.key}-${slot.start.format('HH:mm')}`; const cell = grid.get(cellKey); const lessons = cell?.lessons ?? []; diff --git a/apps/iris/src/components/timetable/helpers.ts b/apps/iris/src/components/timetable/helpers.ts index bbda906..510e3ea 100644 --- a/apps/iris/src/components/timetable/helpers.ts +++ b/apps/iris/src/components/timetable/helpers.ts @@ -1,5 +1,6 @@ import dayjs from 'dayjs'; import customParseFormat from 'dayjs/plugin/customParseFormat'; +import { getLocalizedWeekdayName } from '@/utils/date-locale'; import type { DayColumn, FilterType, @@ -80,18 +81,20 @@ export const formatRooms = (rooms: LessonItem['classrooms']): string => /** Process a single lesson into the grid structure */ const processLesson = ( lesson: LessonItem, - dayMap: Map, + dayMap: Map, timeMap: Map, grid: Map ) => { const dayName = lesson.day?.name ?? ''; + const dayShort = lesson.day?.short; const dayOrder = lesson.day?.days?.[0] ? Number.parseInt(lesson.day.days[0], 10) : 999; // Track day with lowest sort order - if (!dayMap.has(dayName) || (dayMap.get(dayName) ?? 999) > dayOrder) { - dayMap.set(dayName, dayOrder); + const currentDay = dayMap.get(dayName); + if (!currentDay || currentDay.sortOrder > dayOrder) { + dayMap.set(dayName, { shortName: dayShort, sortOrder: dayOrder }); } // Parse start and end times from lesson period @@ -118,13 +121,16 @@ const processLesson = ( }; /** Build view model from lessons array */ -export const buildViewModel = (lessons: LessonItem[]): TimetableViewModel => { +export const buildViewModel = ( + lessons: LessonItem[], + language: string | undefined +): TimetableViewModel => { if (!lessons.length) { return { days: [], grid: new Map(), timeSlots: [] }; } // Collect unique days and time slots - const dayMap = new Map(); + const dayMap = new Map(); const timeMap = new Map(); const grid = new Map(); @@ -135,8 +141,15 @@ export const buildViewModel = (lessons: LessonItem[]): TimetableViewModel => { // Build sorted arrays const days: DayColumn[] = Array.from(dayMap.entries()) - .map(([name, sortOrder]) => ({ name, sortOrder })) - .sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)); + .map(([name, dayMeta]) => ({ + key: name, + label: getLocalizedWeekdayName(name, dayMeta.shortName, language, 'long'), + sortOrder: dayMeta.sortOrder, + })) + .sort( + (a, b) => + a.sortOrder - b.sortOrder || a.label.localeCompare(b.label, language) + ); const timeSlots = Array.from(timeMap.entries()) .sort(([a], [b]) => a.localeCompare(b)) diff --git a/apps/iris/src/components/timetable/index.tsx b/apps/iris/src/components/timetable/index.tsx index 4cb700b..d00dd41 100644 --- a/apps/iris/src/components/timetable/index.tsx +++ b/apps/iris/src/components/timetable/index.tsx @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { parseResponse } from 'hono/client'; import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import type z from 'zod'; import { FilterBar } from '@/components/timetable/filter-bar'; import { TimetableGrid } from '@/components/timetable/grid'; @@ -98,6 +99,7 @@ const getActiveSelectionId = ( // Component export function TimetableView() { const search = Route.useSearch(); + const { i18n } = useTranslation(); const { data: session, isPending } = authClient.useSession(); const navigate = useNavigate({ from: Route.fullPath }); @@ -272,8 +274,9 @@ export function TimetableView() { }, [activeFilter, activeSelectionId, navigate]); const model = useMemo( - () => buildViewModel((lessonsQuery.data ?? []) as LessonItem[]), - [lessonsQuery.data] + () => + buildViewModel((lessonsQuery.data ?? []) as LessonItem[], i18n.language), + [lessonsQuery.data, i18n.language] ); const getSelectorLoading = () => { diff --git a/apps/iris/src/components/timetable/types.ts b/apps/iris/src/components/timetable/types.ts index 5115386..7695bb7 100644 --- a/apps/iris/src/components/timetable/types.ts +++ b/apps/iris/src/components/timetable/types.ts @@ -31,7 +31,8 @@ export type LessonsResponse = InferResponseType< export type LessonItem = NonNullable[number]; export type DayColumn = { - name: string; + key: string; + label: string; sortOrder: number; }; @@ -48,5 +49,5 @@ export type GridCell = { export type TimetableViewModel = { days: DayColumn[]; timeSlots: TimeSlot[]; - grid: Map; // key: `${dayName}-${HH:mm formatted startTime}` + grid: Map; // key: `${dayKey}-${HH:mm formatted startTime}` }; diff --git a/apps/iris/src/components/ui/calendar.tsx b/apps/iris/src/components/ui/calendar.tsx index 7b4f03a..ec33b7d 100644 --- a/apps/iris/src/components/ui/calendar.tsx +++ b/apps/iris/src/components/ui/calendar.tsx @@ -26,6 +26,7 @@ function Calendar({ buttonVariant?: ComponentProps['variant']; }) { const defaultClassNames = getDefaultClassNames(); + const localeCode = (props.locale as { code?: string } | undefined)?.code; return ( - date.toLocaleString('default', { month: 'short' }), + new Intl.DateTimeFormat(localeCode, { month: 'short' }).format(date), ...formatters, }} showOutsideDays={showOutsideDays} diff --git a/apps/iris/src/components/ui/date-picker.tsx b/apps/iris/src/components/ui/date-picker.tsx index 447685e..711d723 100644 --- a/apps/iris/src/components/ui/date-picker.tsx +++ b/apps/iris/src/components/ui/date-picker.tsx @@ -1,5 +1,6 @@ import { format } from 'date-fns'; import { Calendar as CalendarIcon } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { Calendar } from '@/components/ui/calendar'; import { @@ -8,6 +9,7 @@ import { PopoverTrigger, } from '@/components/ui/popover'; import { cn } from '@/utils'; +import { getDateFnsLocale } from '@/utils/date-locale'; type DatePickerProps = { date?: Date; @@ -22,6 +24,9 @@ export function DatePicker({ placeholder = 'Pick a date', disabled = false, }: DatePickerProps) { + const { i18n } = useTranslation(); + const locale = getDateFnsLocale(i18n.language); + return ( - {date ? format(date, 'PPP') : {placeholder}} + {date ? ( + format(date, 'PPP', { locale }) + ) : ( + {placeholder} + )} } /> - {dayjs(announcement.validFrom).format('YYYY/MM/DD')} + {formatLocalizedDate(announcement.validFrom, i18n.language)} - {dayjs(announcement.validUntil).format('YYYY/MM/DD')} + {formatLocalizedDate( + announcement.validUntil, + i18n.language + )} {announcement.cohortIds.length > 0 diff --git a/apps/iris/src/routes/_private/admin/timetable/moved-lessons.tsx b/apps/iris/src/routes/_private/admin/timetable/moved-lessons.tsx index 4856f55..17027f3 100644 --- a/apps/iris/src/routes/_private/admin/timetable/moved-lessons.tsx +++ b/apps/iris/src/routes/_private/admin/timetable/moved-lessons.tsx @@ -36,6 +36,7 @@ import { import { PermissionGuard } from '@/components/util/permission-guard'; import { authClient } from '@/utils/authentication'; import { confirmDestructiveAction } from '@/utils/confirm'; +import { formatLocalizedDate, getDayOrder } from '@/utils/date-locale'; import { api } from '@/utils/hc'; type MovedLessonApiResponse = InferResponseType< @@ -64,16 +65,6 @@ type EnrichedLesson = Omit< 'createdAt' | 'updatedAt' >; -const dayOrderMap = new Map([ - ['Hé', 0], // Hétfő (Monday) - ['Ke', 1], // Kedd (Tuesday) - ['Sz', 2], // Szerda (Wednesday) - ['Cs', 3], // Csütörtök (Thursday) - ['Pé', 4], // Péntek (Friday) - ['O', 5], // Szombat (Saturday) - ['Va', 6], // Vasárnap (Sunday) -]); - export const Route = createFileRoute('/_private/admin/timetable/moved-lessons')( { component: () => ( @@ -157,8 +148,8 @@ function extractReferenceData( extractFromSubstitutions(subs, periodMap, dayMap, lessonMap); const sortedDays = Array.from(dayMap.values()).sort((a, b) => { - const aOrder = dayOrderMap.get(a.short) ?? 999; - const bOrder = dayOrderMap.get(b.short) ?? 999; + const aOrder = getDayOrder(a.name, a.short); + const bOrder = getDayOrder(b.name, b.short); return aOrder - bOrder; }); @@ -170,7 +161,7 @@ function extractReferenceData( } function MovedLessonsPage() { - const { t } = useTranslation(); + const { i18n, t } = useTranslation(); const queryClient = useQueryClient(); const { data: session } = authClient.useSession(); const [search, setSearch] = useState(''); @@ -354,8 +345,14 @@ function MovedLessonsPage() { list = [...list].sort((a, b) => { // Special handling for 'day' column - order by week day sequence if (sortColumn === 'day') { - const aOrder = dayOrderMap.get(a.dayDefinition?.short ?? '') ?? 999; - const bOrder = dayOrderMap.get(b.dayDefinition?.short ?? '') ?? 999; + const aOrder = getDayOrder( + a.dayDefinition?.name ?? '', + a.dayDefinition?.short + ); + const bOrder = getDayOrder( + b.dayDefinition?.name ?? '', + b.dayDefinition?.short + ); const comparison = aOrder - bOrder; return sortDirection === 'asc' ? comparison : -comparison; } @@ -577,7 +574,7 @@ function MovedLessonsPage() { {filteredMovedLessons.map((ml) => ( - {dayjs(ml.movedLesson.date).format('YYYY/MM/DD')} + {formatLocalizedDate(ml.movedLesson.date, i18n.language)} {ml.dayDefinition diff --git a/apps/iris/src/routes/_private/admin/timetable/substitutions.tsx b/apps/iris/src/routes/_private/admin/timetable/substitutions.tsx index 1a90fa9..cdcd545 100644 --- a/apps/iris/src/routes/_private/admin/timetable/substitutions.tsx +++ b/apps/iris/src/routes/_private/admin/timetable/substitutions.tsx @@ -43,6 +43,7 @@ import { } from '@/components/ui/table'; import { PermissionGuard } from '@/components/util/permission-guard'; import { authClient } from '@/utils/authentication'; +import { formatLocalizedDate } from '@/utils/date-locale'; import { api } from '@/utils/hc'; type SubstitutionApiResponse = InferResponseType< @@ -81,7 +82,7 @@ function SortIcon({ } function SubstitutionsPage() { - const { t } = useTranslation(); + const { i18n, t } = useTranslation(); const queryClient = useQueryClient(); const { data: session } = authClient.useSession(); const [search, setSearch] = useState(''); @@ -448,7 +449,7 @@ function SubstitutionsPage() { {filteredSubstitutions.map((sub) => ( - {dayjs(sub.substitution.date).format('YYYY/MM/DD')} + {formatLocalizedDate(sub.substitution.date, i18n.language)} {sub.teacher ? ( diff --git a/apps/iris/src/utils/date-locale.ts b/apps/iris/src/utils/date-locale.ts new file mode 100644 index 0000000..c41e822 --- /dev/null +++ b/apps/iris/src/utils/date-locale.ts @@ -0,0 +1,132 @@ +import { enUS, hu } from 'date-fns/locale'; + +type DateInput = Date | number | string; + +function getLanguageCode(language: string | undefined): string { + return (language ?? 'hu').toLowerCase().split('-')[0] ?? 'hu'; +} + +export function getIntlLocale(language: string | undefined): string { + return getLanguageCode(language) === 'en' ? 'en-US' : 'hu-HU'; +} + +type WeekdayFormat = 'long' | 'short' | 'narrow'; + +export function getDateFnsLocale(language: string | undefined) { + return getLanguageCode(language) === 'en' ? enUS : hu; +} + +function toDate(value: DateInput): Date { + return value instanceof Date ? value : new Date(value); +} + +export function formatLocalizedDate( + value: DateInput, + language: string | undefined, + options?: Intl.DateTimeFormatOptions +): string { + return new Intl.DateTimeFormat(getIntlLocale(language), options).format( + toDate(value) + ); +} + +export function normalizeDayText(value: string): string { + return value + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase(); +} + +const weekdayAliases: Record = { + 0: ['vasarnap', 'va', 'v', 'sunday', 'sun'], + 1: ['hetfo', 'he', 'h', 'monday', 'mon'], + 2: ['kedd', 'ke', 'k', 'tuesday', 'tue'], + 3: ['szerda', 'sze', 'sz', 'wednesday', 'wed'], + 4: ['csutortok', 'cs', 'thursday', 'thu'], + 5: ['pentek', 'pe', 'p', 'friday', 'fri'], + 6: ['szombat', 'szo', 'saturday', 'sat'], +}; + +export function isMatchingWeekday( + weekdayIndex: number, + dayName: string, + dayShort?: string +): boolean { + const aliases = (weekdayAliases[weekdayIndex] ?? []).map(normalizeDayText); + const normalizedName = normalizeDayText(dayName); + const normalizedShort = dayShort ? normalizeDayText(dayShort) : ''; + + return aliases.some( + (alias) => + normalizedName === alias || + normalizedShort === alias || + normalizedName.includes(alias) + ); +} + +const dayOrderMap = new Map([ + ['he', 0], + ['hetfo', 0], + ['mon', 0], + ['monday', 0], + ['ke', 1], + ['kedd', 1], + ['tue', 1], + ['tuesday', 1], + ['sz', 2], + ['sze', 2], + ['szerda', 2], + ['wed', 2], + ['wednesday', 2], + ['cs', 3], + ['csutortok', 3], + ['thu', 3], + ['thursday', 3], + ['pe', 4], + ['pentek', 4], + ['fri', 4], + ['friday', 4], + ['szo', 5], + ['szombat', 5], + ['sat', 5], + ['saturday', 5], + ['va', 6], + ['vasarnap', 6], + ['sun', 6], + ['sunday', 6], +]); + +export function getDayOrder(dayName: string, dayShort?: string): number { + const byShort = dayShort ? dayOrderMap.get(normalizeDayText(dayShort)) : null; + if (byShort !== undefined && byShort !== null) { + return byShort; + } + + const byName = dayOrderMap.get(normalizeDayText(dayName)); + if (byName !== undefined) { + return byName; + } + + return 999; +} + +export function getLocalizedWeekdayName( + dayName: string, + dayShort: string | undefined, + language: string | undefined, + format: WeekdayFormat = 'long' +): string { + const order = getDayOrder(dayName, dayShort); + if (order === 999) { + return dayShort ?? dayName; + } + + const jsWeekdayIndex = (order + 1) % 7; // Convert Monday-based order to JS Sunday-based order. + const baseSundayUtc = Date.UTC(2024, 0, 7); + const date = new Date(baseSundayUtc + jsWeekdayIndex * 24 * 60 * 60 * 1000); + + return new Intl.DateTimeFormat(getIntlLocale(language), { + timeZone: 'UTC', + weekday: format, + }).format(date); +}