timeago.dart icon indicating copy to clipboard operation
timeago.dart copied to clipboard

Make API more customizable

Open MarcelGarus opened this issue 5 years ago • 2 comments

I have a proposal on how to make the package more customizable, including having different rules per locale: It's basically a declarative way of formulating the business logic currently hardcoded in the package.

Proposed architecture

I propose an architecture of formatters, matchers, and rules.

Formatters just take a locale, a DateTime to format and another DateTime relative to which to format the first one and turn it into a String and a Duration after which the string becomes invalid:

// TimeAgoInput contains a Locale, a DateTime value and a DateTime relativeTo.
// FormattingResult contains a String and a Duration.
typedef Formatter = FormattingResult Function(TimeAgoInput);

A Formatters class could contain some predefined formatters, while developers are welcome to create their own. Here are some examples of formatters:

  • Some that always return a constant value, for example, "just now", "a moment ago", or "yesterday".
  • Some that return the date difference in minutes, hours, weeks, months, years as in "21 minutes ago", "1 week ago", "3 months ago", "2 years ago".
  • Some that return the weekday: "Monday", "Thursday".

Matchers take a locale, date and current date and return whether it matches and a duration after which the matching status will change:

// MatchingResult contains a bool and a Duration.
typedef Matcher = MatchingResult Function(TimeAgoInput);

Matchers could have some common matcher types:

  • difference(from, to), checking if the Duration between both dates is between from and to (if from < difference < to).
  • Checking if the date is today or yesterday or the dayBeforeYesterday.

Finally, rules combine matchers and formatters. Users could then customize the formatting by providing a list of rules.

final matchers = [
  Rule(matcher: Matchers.difference(to: 20.seconds), formatter: Formatters.justNow), // just now
  Rule(matcher: Matchers.difference(to: 40.seconds), formatter: Formatters.aMomentAgo), // a moment ago
  Rule(matcher: Matchers.difference(to: 45.minutes), formatter: Formatters.minutesAgo), // 12 minutes ago
  Rule(matcher: Matchers.difference(to: 90.minutes), formatter: Formatters.aboutAnHour), // about an hour ago
  Rule(matcher: Matchers.today, formatter: Formatters.hoursAgo), // 4 hours ago
  Rule(matcher: Matchers.yesterday, formatter: Formatters.yesterday), // yesterday
  Rule(matcher: Matchers.withinLastWeek, formatter: Formatters.weekday), // Monday
  …
]
timeago.format(rules, …);

This would give users of this package much more flexibility. Additionally, because formatters could return the localized String as well as the Duration after which it gets invalid, it could make the Flutter part of the package much more efficient.

Showcases of the new architecture

Several issues could get resolved:

  • #47: Use a rule like Rule(matcher: Matcher.difference(to: 1.days), formatter: Formatters.exact).
  • #80: Create a formatter that respects the Locale.
  • #89: Just use fewer rules than the default.
  • #100: Just use different formatters that don't use "ago" and "from now".

Additional goodies include being able to customize the matching by Locale. In Germany, for example, there's "vorgestern", a word which describes the day before yesterday. One could easily imagine including a Matcher.dayBeforeYesterday which only matches in the German locale.

There are even cases where providing a whole set of new formatters makes sense. For example, to create Telegram-style ambiguous-by-design "last seen recently" formats. Or in our Schul-Cloud app, where teachers will be able to see when students submitted their work for an assignment, formats like "just in time" or "5 minutes before the deadline" would become possible:

final rules = [
  Rule(
    matcher: Matcher.difference(to: 1.minutes),
    formatter: (input) => 'justInTime',
  ),
  Rule(
    matcher: Matcher.difference(to: 1.hours),
    formatter: (input) => '${input.difference.minutes} minutes before the deadline',
  ),
  …
];

Evaluation

I know this is a stark divergence from the current architecture of the package and will result in a new breaking version. But localization and time formatting are both areas where a lot of variation is going on. Most developers want to have more control over the rules than just tweaking a few settings in a pre-defined function. This approach is modular by design and that's why I believe implementing this change could make the package drastically more powerful and adaptable to developers' needs.

What still needs to be resolved

Locales won't be able to be specified in a single file. Rather than having locales in one file and the different formats inside the locale, I believe it would have to go the other way around: There are a number of formatters and in each formatter, all the applicable translations are contained.

What do you think about this proposed change? I'm looking forward to hearing your opinion. I'd be happy to help implement the changes.

MarcelGarus avatar Mar 24 '20 13:03 MarcelGarus

Any updates on this? I'd be happy to implement a prototype

MarcelGarus avatar Apr 15 '20 10:04 MarcelGarus

You can create your custom LookupMessages and apply your custom rules by the moment.

I like the the idea of rules and the format function can have a new optional input rules with a default value with the current rules.

kranfix avatar Oct 11 '20 15:10 kranfix