We wanted to shorten the “about 1 month ago” text in an information-rich table that was squeezing created_at data.

We can override distance_in_words with the i18n API (and, by extension, time_ago_in_words).
A quick change to config/locales/en.yml and we’re done:
en:
datetime:
distance_in_words:
less_than_x_seconds:
other: '1 minute'
half_a_minute: '1 minute'
less_than_x_minutes:
one: '1 minute'
x_minutes:
one: '1 minute'
other: '{{count}} minutes'
about_x_hours:
one: '1 hour'
other: '{{count}} hours'
about_x_months:
one: '1 month'
other: '{{count}} months'
about_x_years:
one: '1 year'
other: '{{count}} years'
over_x_years:
one: 'over 1 year'
other: 'over {{count}} years'
The result:

You can see other options in svenfuchs/i18n.
Written by Dan Croak.
This is how I unit tested a view helper to format a date range in a view.
I wrote the interface for the view helper I wanted, but which did not exist yet:
= format_date_range(course.date_range)
The outer integration tests (not shown) failed with NoMethoError: format_date_range.
I generated a new helper file:
script/generate helper date_time
It produced:
# app/helpers/date_time_helper.rb
module DateTimeHelper
end
# test/unit/helpers/date_time_helper_test.rb
require 'test_helper'
class DateTimeHelperTest < ActionView::TestCase
end
I wrote the unit test:
should 'format date range on same day' do
eight_oclock = DateTime.new(2009, 10, 12, 8)
nine_oclock = DateTime.new(2009, 10, 12, 9)
date_range = eight_oclock..nine_oclock
expected = 'October 12, 2009'
assert_equal expected, format_date_range(date_range)
end
This style is flat test structure, intention-revealing temporary variables, respect an 80-character line limit.
I made it pass:
def format_date_range(date_range)
first = date_range.first
last = date_range.last
if same_day?(first, last)
"#{first.to_s(:month_day)}, #{first.year}"
end
end
private
def same_day?(date_one, date_two)
date_one.day == date_two.day
end
Like the test, I used intention-revealing temporary variables and a private method for a more expressive question in the conditional.
I wrote another test case:
should 'format date range on different days of same month' do
monday = DateTime.new(2009, 10, 12)
tuesday = DateTime.new(2009, 10, 13)
date_range = monday..tuesday
expected = 'October 12-13, 2009'
assert_equal expected, format_date_range(date_range)
end
I made it pass:
def format_date_range(date_range)
# ...
elsif same_month?(first, last)
"#{first.to_s(:month_day)}-#{last.day}, #{last.year}"
end
end
private
def same_month?(date_one, date_two)
date_one.month == date_two.month
end
Rails date and time formatting is done in locales:
en:
date:
formats:
long: ! '%B %d, %Y'
month_day: ! '%B %d'
short: ! '%b %d'
I wrote another test case:
should 'format date range on days of different months' do
october = DateTime.new(2009, 10, 31)
november = DateTime.new(2009, 11, 1)
date_range = october..november
expected = 'October 31-November 01, 2009'
assert_equal expected, format_date_range(date_range)
end
I made it pass:
else
"#{first.to_s(:month_day)}-#{last.to_s(:month_day)}, #{last.year}"
end
I wanted to remove the leading zero sucks. We could use `%e instead of %d, which replaces the leading zero with a space:
expected = "October 31-November 1, 2009"
That makes the test look a little ugly but since the output will be HTML, the extra space is fine.
Make it pass:
month_day: ! '%B %e'
Written by Dan Croak.
In this example we’re going to have an XML API on FontsController#index. A GET to /fonts.xml will produce a list of every Font in the database, along with all its information (name, thumbnail, a list of ligatures, price, license, and so on).
This is a long list. Luckily it’s just for the API consumers. The normal HTML people just request /fonts and this gives them a paginated view of the lovely fonts on our system.
So to speed it up we do some simple caching. In FontsController, at the top, we add this:
caches_page :index, :if => Proc.new {|c|
c.request.format.xml?
}
And magically requests to /fonts.xml are cached to public/fonts.xml. Lovely! Thanks, Rails!
So what happens when someone requests /fonts with an Accept: text/xml header? You can try it like this:
curl -H 'Accept: text/xml' http://ihearthelvetica.local/fonts

FontsController#index uses #respond_to, so it sends back the XML as requested. However, #caches_page saves the XML to public/fonts.html! Now when a user requests /fonts from their Web browser, they’re getting back a mess of XML!
No good.
This is a problem deep in the Rails caching code. As a workaround, try this on for size:
class ApplicationController < ActionController::Base
before_filter :fix_caching_extension_for_xml
private
def fix_caching_extension_for_xml
if request.format.xml?
ActionController::Base.page_cache_extension = '.xml'
end
end
end
This manually sets the extension for XML requests to .xml, so that it saves it to the right place.
The other option is to fix this in Ruby on Rails itself. Download the patch attached to the ticket I’ve opened and apply it to an edge version of Rails. The patch has tests and is more generalized, so if the workaround fails to solve your problem the patch might.
Leave a comment on the Lighthouse ticket if the patch works for you, or if you’ve encountered this problem. Together, we can.