Today's Date
Gregorian Calendar
Ethiopian Calendar
Implementation Details: How the Main Calendar Works
This section documents the user-interface logic used by the Ethiopian Calendar app: how state is stored, how month views are rendered, how holidays are mapped, and how navigation/keyboard controls work.
Author: Anit Kumar Tarafdar
Last reviewed:
1) Overview Flow
On DOMContentLoaded the app calls init(), which:
- Computes
todayJdnfrom the device date viagregorianToJdn(). - Displays “today” in both GC and ET panels (
#gregorian-today,#ethiopian-today). - Initializes calendar state (
viewState.etYear,viewState.etMonth) and populates the month/year dropdowns. - Calls
renderCalendar()to build the month grid. - Initializes the converter (
setupConverter()) and the holiday table (populateHolidayTable()), if those elements exist. - Wires mobile menu open/close and keyboard handlers.
2) State & Constants
ETHIOPIC_EPOCH_JDN = 1723856is the Ethiopic epoch constant used by all conversions.viewStateholds the current Ethiopian year and month being viewed.holidayInfoMapcaches holidays for the visible Ethiopic year keyed by JDN.- Name tables:
GREGORIAN_MONTHS_EN,GREGORIAN_WEEKDAYS_EN,ETHIOPIC_MONTHS,ETHIOPIC_WEEKDAYS. HOLIDAYSis an array with entries of typeethiopian,gregorian, orgregorian-leap(which adds a leap-day shift).
3) Rendering Pipeline (renderCalendar())
-
Sync UI controls: Clears the grid, updates the month/year selects to the current
viewState. -
Determine GC year span for this ET year:
Compute the GC date of Meskerem 1 via
jdnToGregorian(ethiopianToJdn(etYear, 1, 1)).year→gregYearStart. -
Populate holidays for this ET year window:
For each
hinHOLIDAYS:- If
h.type === 'ethiopian', map withethiopianToJdn(etYear, h.month, h.day). - Else (Gregorian-based): use
holidayGregYear = (h.month < 9) ? gregYearStart + 1 : gregYearStart. - For
gregorian-leap, adjust day toh.leap_daywhenisGregorianLeap(holidayGregYear). - Get JDN via
gregorianToJdn(holidayGregYear, h.month, holidayGregDay)and store inholidayInfoMap.
- If
-
Header & month length:
Title uses ET month names (Amharic + English).
Days in month = 30 (months 1–12); for month 13 (Pagume) it’s 5 or 6 depending on
(etYear % 4) === 3. -
GC range text:
Compute
firstDayJdnandlastDayJdnand show the GC month name(s) spanned (#gregorian-span). -
Monday-first grid offset:
const firstDayOfWeek = (firstDayJdn + 1) % 7; // Sunday=0 indexing const startOffset = (firstDayOfWeek === 0) ? 6 : firstDayOfWeek - 1; const startJdn = firstDayJdn - startOffset; // backfill previous month cells -
Build 42 cells: for
i = 0…41, compute:
Then mount DOM:const cellJdn = startJdn + i; const cellEtDate = jdnToEthiopian(cellJdn); const cellGregDate = jdnToGregorian(cellJdn); const isCurrentMonth = (cellEtDate.month === viewState.etMonth); const isToday = (cellJdn === todayJdn); const isHoliday = holidayInfoMap.has(cellJdn);- Container
.calendar-cell→.calendar-cell-content(rounded/bordered). - Style by state:
holiday-cell(if holiday + current month),bg-white border-slate-200(current month),bg-slate-50 border-transparent(adjacent months), andtoday-cellfor today highlight. - Date block shows ET day in Ge’ez numerals via
toGeez()and GC day as a sub-label. - If holiday + current month, append
.holiday-names(Amharic + English) and bind click →showToast().
- Container
4) Navigation & Static Pages
-
Prev/Next buttons: Change
viewState, wrap across 1↔13, and re-render. In static mode (<body data-static="1">) it redirects to month files usingMONTH_SLUGSviagoToStatic(y,m). - Dropdowns: On change, either re-render (dynamic) or redirect (static).
-
Keyboard: Left/Right arrows trigger prev/next when focus is inside
#calendar-container.
5) Converter (setupConverter())
- GC → ET: Validates Gregorian input with a Date round-trip, then
gregorianToJdn()→jdnToEthiopian(). - ET → GC: Validates ET ranges (months 1–12: 30 days; month 13: 5 or 6 by ET leap year). Then
ethiopianToJdn()→jdnToGregorian(). Weekday via(jdn + 1) % 7. - UX: Result color toggles for valid/invalid; reset buttons clear fields.
6) Holiday Table (populateHolidayTable())
Builds a dual-date table for the current ET year (derived from today’s JDN). It uses the same
GC start year rule (Sep–Dec → start year, Jan–Aug → start year + 1) and leap-aware shifts for
gregorian-leap entries, outputting ET and GC labels side by side.
7) Toast & Mobile Menu
- Toast:
#holiday-toastusesaria-live="polite".showToast()sets text, toggles a CSS.showclass, and auto-hides after 2.2s. - Mobile menu: Toggles classes and
aria-expanded; Escape key closes it. Body scroll is locked while open.
8) Accessibility & UX Notes
- Week start: Monday-first grid is explicit (independent of browser locale).
- Focus & labels: Buttons include accessible labels; consider adding
aria-labelon calendar cells and a rovingtabindexfor keyboard focus. - Color contrast: Holiday/today classes use high-contrast backgrounds and borders.
9) Performance Considerations
- All date math is integer-only; 42 cells keep DOM work bounded.
- Optional micro-optimizations: build cells in a
DocumentFragment, then append once; batch class changes to avoid layout thrash. - Use
deferfor script loading (you already run after DOMContentLoaded).
10) Known Limitations
- Date-only scope: No time zones or times; near midnight transitions, GC labels can differ by ±1 day vs UTC.
- Holiday list: Fixed set; movable feasts (e.g., Fasika) not included yet.
- Static mode: Month navigation redirects to prebuilt pages; dynamic features (e.g., dropdowns) are hidden.
11) Suggested Tests (Manual QA)
- Meskerem 1 renders on correct GC day (Sep 11, or Sep 12 in GC leap alignment).
- Pagume length = 5 (non-leap ET) or 6 (when
etYear % 4 === 3). - Meskel shifts to Sep 28 in GC leap years; Timket shifts to Jan 20 in GC leap years.
- “Today” highlight matches
todayJdnregardless of month view. - Left/Right keys move months; static mode redirects correctly using slugs.