30 January 2012

ActiveRecord, I18n, Date, and UX

From a usability perspective, there's a lot to be said for calendar-oriented date input - the kind with a drop-down list for each of day, month, and year, or even a javascripty popup that makes a calendar with pix of lovely ladies) so that your users can simply point to the date they want without needing to think extensively. Also, it's not possible for them to write "LOL" or "thou villainous, clay-brained fustinarian" or any other reddit/4chan internet-meme-insult-joke kind of thing instead of an actual date.

On top of all those advantages, this approach totally avoids the issue of a certain country habitually writing down the month and the date the wrong way around.

From a frustratability perspective though, if your clients are people who use your system day in, day out because their business depends on it, you should probably consider them expert users, consider them willing to learn a particular date format so they can type it in a text box, because that's a helluva lot faster than picking your way through calendar boxes.

Ruby's Date class will parse from a wide variety of formats:

> Date.parse "december 15th, 1965"
 => Wed, 15 Dec 1965
> Date.parse "1st jan"
 => Sun, 01 Jan 2012
> Date.parse "1st jan 2038"
 => Fri, 01 Jan 2038
> Date.parse "1 jul 2000"
 => Sat, 01 Jul 2000
> Date.parse "jul 2nd 2000"
 => Sun, 02 Jul 2000
> Date.parse "27/2/2000"
 => Sun, 27 Feb 2000
> Date.parse "19-09-2009"
 => Sat, 19 Sep 2009

The only problem is when your users happen to be French. (Or German, or Swedish, or any of those crazy places that don't speak Ze English).

> Date.parse "19-fevrier-2013"
 => Thu, 19 Jan 2012
> Date.parse "19-mars-2013"
 => Tue, 19 Mar 2013
> Date.parse "19-avril-2013"
 => Thu, 19 Jan 2012

Something doesn't look right here. March works because (I suppose) Ruby looks at only the first three characters of the month token, so "mars" in French works out the same as "March" in English.

This is all a problem, because your expert users, impatiently bristling to get their hands on your finely-crafted app, are going to enter dates in a non-English language in a text field in a form on a web page that you are going to feed directly to an ActiveRecord object

def update
  @widget.update_attributes params[:widget]

where params[:widget][:expires] is "30 avr 2014" for example. Ouch. Internally, ActiveRecord calls Date._parse (as does Date.parse which we've been looking at above, so you can guess what ActiveRecord will do by looking at what Date.parse does).

So, what you were really looking for was a patch for Date._parse that will automagically convert those foreigner month names so that stuff works as it should.

As it happens, I've written a little gist that you can take home with you and enjoy. I keep it under config/initializers/date.rb. It monkey-patches Date._parse to gsub anything that might be a month, before forwarding to the original Ruby implementation, only if the current locale is not :en. It relies on translation strings that you provide via Rails' I18n library. Specifically, it looks up date.abbr_month_names, and date.month_names to construct a map from $local_month_name to $month_name_in_english that Ruby can deal with.

The code is in a github gist in case you can't see it here in your reader