ethio_greg_dual_calendar

A Flutter dual-calendar package: render dates and full calendar UIs in either the Gregorian or the Ethiopian (Amete Mihret) calendar, with month and weekday names localized in multiple languages (English, Amharic, Afaan Oromoo — easily extended).

This project is an extraction of the calendar system originally built for a church app, where users can switch both their language and their calendar system at runtime and every rendered date in the app follows along automatically.

Why

Most Flutter calendar widgets assume the Gregorian calendar. In Ethiopia the Ethiopian calendar is the everyday calendar: 13 months — twelve of 30 days plus Pagumē, a short 13th month of 5 days (6 in a leap year) — with the year running ~7–8 years behind the Gregorian year and starting on Meskerem 1 (≈ September 11). Wrapping a Gregorian grid in Ethiopian labels is not enough: the month boundaries, day numbers, year arithmetic, and month count are all different. This package makes the calendar system a first-class, switchable setting.

Features

1. Accurate Gregorian ↔ Ethiopian conversion (EthiopianDate)

  • Pure-Dart conversion through the Julian Day Number (JDN) using the Amete Mihret epoch — no platform channels, no dependencies.
  • EthiopianDate.fromGregorian(DateTime) and EthiopianDate.toGregorian(year, month, day) are exact inverses; verified with zero mismatches across 1900–2100 against the reference abushakir package, including leap-year boundaries.
  • Leap-year rule built in (year % 4 == 3 → Pagumē has 6 days).
  • Sample anchors: 2026-07-12 → 2018/11/05 (Hamle 5), 2024-09-11 → 2017/01/01 (Meskerem 1, Ethiopian New Year), 2000-01-01 → 1992/04/22 (Tahsas 22).

2. Calendar- and locale-aware formatting (AppDate)

One static API the whole app uses for dates. It reads the active calendar system and the active locale, so callers never branch on either:

AppDate.calendar = CalendarSystem.ethiopian; // kept in sync by settings

AppDate.mediumDate(date);        // "Aug 9, 2024"  /  "ነሐሴ 3, 2016"
AppDate.weekdayMonthDay(date);   // "Friday, Aug 9" (weekday is the same physical day)
AppDate.monthYear(date);         // "August 2024"   /  "ነሐሴ 2016"
AppDate.dayOfMonth(date);        // 9 (Gregorian)   /  3 (Ethiopian)
AppDate.relativeOrWeekdayMonthDay(date); // "Today" / "Yesterday" / "Tomorrow" / fallback

Plus calendar-aware month arithmetic, which is where the two systems really diverge (12 vs 13 months per year):

AppDate.monthDays(date);          // all days of the month containing `date`,
                                  // as Gregorian DateTimes — 30 cells for an
                                  // Ethiopian month, 5–6 for Pagumē, 28–31 Gregorian
AppDate.monthStart(date, -1);     // first day of the previous month, crossing
                                  // year boundaries (Pagumē → Meskerem)
AppDate.previousMonthStart(date); // null at the calendar-year edge
AppDate.nextMonthStart(date);     // null in December / Pagumē
AppDate.isSameMonth(a, b);        // same month in the *active* calendar

Key design decision: DateTime (Gregorian) stays the single storage and transport type everywhere — the Ethiopian calendar exists only at the formatting/layout layer. This keeps persistence, APIs, and comparisons simple and makes switching calendars a pure re-render.

3. Calendar widgets

  • MonthCalendar — a month grid that is genuinely native to the active calendar: an Ethiopian month renders as 30 cells numbered 1–30 (or Pagumē's 5–6), with Ethiopian month/year in the header — not a Gregorian month wearing Ethiopian labels. Weekday columns are shared since a given day falls on the same weekday in both calendars. Supports:
    • full-month and two-week (twoWeeks) formats, as swipeable pages,
    • firstDate/lastDate range clamping of swipes and the ◀ ▶ navigation,
    • per-day event markers (eventCountForDay, capped dots under each cell),
    • today highlight + selected-day outline,
    • table_calendar-style customization: CalendarStyle, HeaderStyle, DaysOfWeekStyle restyle the built-in pieces (per-state text styles and decorations, marker dots, chevrons, title/label formatters), and CalendarBuilders replaces any of them outright (todayBuilder, selectedBuilder, markerBuilder, dowBuilder, headerTitleBuilder, …). Style fields default to null = "derive from the app's ThemeData", so an unstyled calendar keeps the original theme-adaptive look. Plus startingDayOfWeek (any weekday, Sunday default) and weekendDays.
  • WeekLongCalendar / DayButton — a horizontal, scrollable day strip over the active calendar's month, with auto-scroll to the selected day.
  • Month/year selector widgets that step by calendar months (13 in an Ethiopian year).

4. Multi-language month & weekday names

All names live in CalendarL10n, a plain const value object — no codegen, and hosts can supply their own strings by constructing one instance:

  • monthsLong / monthsShort — Gregorian month names per locale
  • ethMonths — the 13 Ethiopian month names per locale (Meskerem, Tikimt, Hidar, Tahsas, Tir, Yekatit, Megabit, Miyazia, Ginbot, Sene, Hamle, Nehase, Pagumē)
  • weekdaysLong / weekdaysShort, plus today / yesterday / tomorrow and the calendar-setting labels used by CalendarSelector

Built-in locales: English (CalendarL10n.en), Amharic (CalendarL10n.am), Afaan Oromoo (CalendarL10n.om), and Tigrinya (CalendarL10n.ti — pending native-speaker review**)**, also reachable by code via CalendarL10n.forLocale('am'). Assign the active instance to AppDate.l10n alongside the host's locale switching. Adding a language is constructing one CalendarL10n.

Day numbers can optionally render in Ge'ez numerals (፩ ፲ ፴) via EthioCalendarConfig(geezDayNumbers: true); GeezNumerals.format(2016) (→ ፳፻፲፮) is also exported on its own.

5. Runtime switching

The calendar system is a user setting (CalendarSystem.gregorian | CalendarSystem.ethiopian), persisted alongside language in the host app's settings and exposed through a simple dropdown (CalendarSelector — a dumb value/onChanged widget, so any state management works). Keep AppDate.calendar / AppDate.l10n in sync with the settings and rebuild the widget tree when they change, and every rendered date in the app re-resolves automatically — no manual refresh plumbing.

6. Injectable configuration (EthioCalendarConfig / EthioCalendarScope)

All formatting and month arithmetic lives on an immutable EthioCalendarConfig (calendar + strings + options); the static AppDate API delegates to a global instance. For subtrees that need a different calendar or language than the rest of the app — e.g. both systems side by side — wrap them in an EthioCalendarScope; this package's widgets resolve the nearest scope and fall back to the global config:

EthioCalendarScope(
  config: const EthioCalendarConfig(
    calendar: CalendarSystem.ethiopian,
    l10n: CalendarL10n.am,
  ),
  child: MonthCalendar(...),
)

Architecture

lib/
├── ethio_calendar.dart        # public exports
└── src/
    ├── ethiopian_date.dart    # pure conversion math (JDN ↔ Ethiopian)
    ├── config.dart            # EthioCalendarConfig: formatting & month arithmetic
    ├── app_date.dart          # static facade over the global config
    ├── scope.dart             # EthioCalendarScope (per-subtree override)
    ├── geez_numerals.dart     # ፩ ፲ ፴ … formatting
    ├── types.dart             # CalendarSystem enum
    ├── l10n/
    │   └── calendar_l10n.dart # months, ethMonths, weekdays, relative words (en/am/om/ti)
    └── widgets/
        ├── month_calendar.dart     # month grid (full / two-week)
        ├── week_long_calendar.dart # horizontal day strip
        ├── day_button.dart         # one day in the strip
        ├── month_selector.dart     # ◀ month ▶ stepper (13 months/yr in Ethiopian)
        └── calendar_selector.dart  # settings dropdown

Layering rule: conversion math → formatting API → widgets, each layer usable on its own. You can use EthiopianDate alone in a backend, AppDate without the widgets, or the widgets as a drop-in UI.

Quick start

// 1. Pick the calendar and language (e.g. from user settings)
AppDate.calendar = CalendarSystem.ethiopian;
AppDate.l10n = CalendarL10n.am;

// 2. Format any DateTime
Text(AppDate.mediumDate(DateTime.now())); // "ሐምሌ 6, 2018"

// 3. Show a month grid
MonthCalendar(
  focusedDate: focused,
  selectedDate: selected,
  firstDate: DateTime(2020),
  lastDate: DateTime(2030),
  onDaySelected: (day) => setState(() => selected = day),
  onFocusedDateChanged: (month) => setState(() => focused = month),
  eventCountForDay: (day) => eventsOn(day).length,
)

License

MIT — see LICENSE.

Libraries

ethio_greg_dual_calendar
A Flutter dual-calendar package: render dates and full calendar UIs in either the Gregorian or the Ethiopian (Amete Mihret) calendar, localized in English, Amharic, and Afaan Oromoo.