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)andEthiopianDate.toGregorian(year, month, day)are exact inverses; verified with zero mismatches across 1900–2100 against the referenceabushakirpackage, 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/lastDaterange 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,DaysOfWeekStylerestyle the built-in pieces (per-state text styles and decorations, marker dots, chevrons, title/label formatters), andCalendarBuildersreplaces any of them outright (todayBuilder,selectedBuilder,markerBuilder,dowBuilder,headerTitleBuilder, …). Style fields default to null = "derive from the app'sThemeData", so an unstyled calendar keeps the original theme-adaptive look. PlusstartingDayOfWeek(any weekday, Sunday default) andweekendDays.
- full-month and two-week (
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 localeethMonths— the 13 Ethiopian month names per locale (Meskerem, Tikimt, Hidar, Tahsas, Tir, Yekatit, Megabit, Miyazia, Ginbot, Sene, Hamle, Nehase, Pagumē)weekdaysLong/weekdaysShort, plustoday/yesterday/tomorrowand the calendar-setting labels used byCalendarSelector
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.