We recently added time zone detection for new users by default in Bullet Train, which I mentioned in passing on Twitter:

A few friends asked how we went about implementing it and it seemed like a blog post would be a good place to share.

Methods for Time Zone Detection

JavaScript

One of the easiest and most reliable ways to detect a user's time zone is within JavaScript. There are some subtle limitations as noted in the jsTimezoneDetect README, like reporting that a user is "Europe/Berlin" when they themselves would report that they're in "Europe/Stockholm". However, in practical terms both of these time zones function the same, so the results will still be correct.

GeoIP Look-up

Another method to detect a user's time zone would be to do a geographic look-up on their IP address, but this can produce inaccurate results when a user is browsing through a VPN. This method is also more technically complicated, as it requires having some method to doing IP look-up like a third-party service or database.

At the end of the day, both of these methods are fine, because although we auto-detect this value as a starting point, we explicitly give users the opportunity to select another time zone during onboarding. If we get it wrong, it's of little consequence. Given that, we went with the easier of the two to implement: JavaScript.

Implementing in Rails

We include the jsTimezoneDetect library via Rails Assets:

Gemfile

source 'https://rails-assets.org' do
  gem 'rails-assets-jsTimezoneDetect'
end

You also need to explicitly include this in your JavaScript manifest:

app/assets/javascripts/application.js

//= require jsTimezoneDetect

Detect a User's Time Zone at Registration

We add a hidden field on the registration form like so:

<%= f.hidden_field(:time_zone, value: nil) %>

And populate it on page load with the following JavaScript. (Although you should put this wherever you would normally put your JavaScript for the registration page, I'm representing this as a <script> tag that you could drop right into the form view to keep the example simple.)

<script type="text/javascript">
$(document).ready(function(){
  document.getElementById('user_time_zone').value = jstz.determine().name();
})
</script>

Finally, we need to update the registration controller (in our case, we're using Devise,) to assign this field value to the user object:

private

def set_time_zone(user)
  time_zone = params.dig(:user, :time_zone)
  if time_zone.present?
    user.time_zone = ActiveSupport::TimeZone::MAPPING.key(time_zone)
    user.save
  end
end

And then we reference that in the create method of the registration controller like so:

set_time_zone(current_user)

The secret in the set_time_zone method is that we're taking the time zone as reported by the jsTimezoneDetect library, (e.g. America/Los_Angeles,) and converting it to one of the standard ActiveSupport::TimeZone time zone names (e.g. Pacific Time (US & Canada).) The latter are more user-friendly because there are way fewer options to choose from.

Allowing Users to Update Their Time Zone

Wherever you allow your users to update their account details, add this field:

<%= form.select :time_zone, time_zone_options_for_select(form.object.time_zone, nil, ActiveSupport::TimeZone) %>

Don't forget to add :time_zone to your allowed parameters on the controller, or the value won't pass through to the database.

Anything else?

I think that should be enough information to get you started. If you have any questions or feedback about this, please hit me up on Twitter.