ethio_greg_dual_calendar 0.1.1
ethio_greg_dual_calendar: ^0.1.1 copied to clipboard
Dual-calendar dates and calendar UIs for Flutter in both the Gregorian and Ethiopian (Amete Mihret) calendars, localized in English, Amharic, and Afaan Oromoo.
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.
