Deep dive into the code of Local Time

Liroy Leshed
Oct 30 · 5 min read

Time Localization is hard. How do we display the same time to users in different locations around the world with different time zones? Even more so, what time zone do we save in the database? The time in Sydney, the time in New York, or the time in Los Angeles?

Luckily, this problem was solved long ago with the convention to save a “neutral” form and accepted standard of time value: UTC . So to solve this problem we save the time in UTC and then convert it to the user’s local time zone through JavaScript. This keeps things simple and bugs-free.

In this post, we’ll see how the Local Time gem actually solves this problem as we dive deep into the code and observe how the program runs step-by-step, function-by-function and how it actually works, focusing on the client-side JavaScript which is written in CoffeeScript. Let’s get started.

Local Time is mostly CoffeeScript with Ruby Sprinkles

The clarity and architecture of this library make understanding its flow much easier. It’s composed mostly of CoffeeScript and also of a little bit of Ruby that’s responsible for creating the <time> tag server-side. On the client-side there are no dependencies whatsoever because it’s using the browser’s API directly in every case.

Its mission is to convert UTC <time> tags rendered from the server to the browser’s local time using JavaScript.

The Ruby helper method local_time will produce the <time> tag containing the data-format and data-local data attributes as well as the datetime attribute. The JavaScript will then look for <time> tags checking for these attributes.

Let’s explain each one of the attributes: The data-format data attribute will determine the time format the user will see in the browser. The data-local data attribute will determine whether the value is “time”, “date”, or “time-ago” among others. And the datetime attribute will contain the time in UTC. In addition, it will render the timestamp in a format JavaScript understands (2013–11–27T23:43:22Z):

<time data-format=”%B %e, %Y %l:%M%P”
datetime=”2013–11–27T23:43:22Z”>November 27, 2013 11:43pm</time>

Then, most of the work happens client-side with JavaScript in the browser as the library uses the built-in Date object to check for the user’s specific local time zone. It will then convert what the user sees in the browser into their own local time zone and mark the element as “localized” by adding an additional data attribute data-localized=”true”. It will also add the localized time to the title attribute for the tag:

<time data-format=”%B %e, %Y %l:%M%P”
title=”November 27, 2013 6:43pm EDT”
data-localized=”true”>November 27, 2013 6:43pm</time>

It all starts with LocalTime.start

The LocalTime.start class method will start the library. This first thing it does is to check whether or not the library has been started using the started local variable. Then it does another check to make sure the dom has been loaded properly.

It does so by checking whether the browser supports the Mutation Observer API, and if not it will call domReady, and check for the dom loading state “manually”. If neither of these methods work it will call nextFrame which will use the Request Animation Frame API to load the JavaScript in the next frame in order to make sure the dom is loaded:

LocalTime.start = ->
unless started
started = true
if MutationObserver? or domReady() # Wait for the dom to load


The domReady method will use the readyState on the document to check whether or not loading has been completed properly:

domReady = ->
if document.attachEvent
document.readyState is "complete"
document.readyState isnt "loading"


The Request Animation Frame browser’s API will run the function in the next frame. In case the browser doesn’t support it, it will simply call the setTimeout function and run the function it gets (starting the controller) 17 milliseconds later to make sure the dom is ready:

nextFrame = (fn) ->
requestAnimationFrame?(fn) ? setTimeout(fn, 17)


Next it will call the startController function which will in turn get the controller using the class method getController and then call controller.start() to actually start the controller (which we’ll look at shortly):

startController = ->
controller = LocalTime.getController()


Next it will call the getController method which will create a new instance of the controller LocalTime.Controller and then save it in the controller instance variable in case it doesn’t already exist:

@LocalTime =
getController: ->
@controller ?= new LocalTime.Controller


Once the controller has been created the contructor will create a new instance of the LocalTime.PageObserver class for the page observer. Passing it the selector which will just look for <time> tags that haven’t been localized yet. The mutation observer will run the processElements method every time there’s any change to the dom:

class LocalTime.Controller
SELECTOR = "time[data-local]:not([data-localized])"
constructor: ->
@pageObserver = new LocalTime.PageObserver SELECTOR,

controller.start( )

If you recall the startController method got an instance of the controller inside the getController method and then called start on the controller. start will check whether the controller has already been started with the started instance variable. Then it will process the elements with the processElements method. It will also start the timer with startTimer. The timer will run the processElements method every 60 seconds (similar, but different from the page observer). In addtition, it will start the page observer by calling pageObserver.start. And finally, the instance variable flag started will be changed to true:

class LocalTime.Controller  start: ->
unless @started
@started = true


The first thing this function does is process the elements (<time> tags) that haven’t been localized yet. It will loop through them one-by-one and then in turn it will call processElement for each element in the loop.

Pay attention to the fat arrow => which will save the scope of the class to be able to later call this.processElement inside this function:

processElements: (elements = document.querySelectorAll(SELECTOR)) =>
@processElement(element) for element in elements


This method grabs all of the attributes of the tag and processes them first by saving each one to a local variable with its corresponding name: datetime, format, local, time, and title.

The actual localization happens when the parseDate function is called with datetime as the argument. We’ll see exactly how shortly.

Next it will look for the title, and if it doesn’t exist it will create a brand new localized time title for the tag:

processElement: (element) ->
datetime = element.getAttribute("datetime")
format = element.getAttribute("data-format")
local = element.getAttribute("data-local")
time = parseDate(datetime)
return if isNaN time
unless element.hasAttribute("title")
title = strftime(time,
element.setAttribute("title", title)

parseDate — where the localization happens

This is where the magic happens. The dateString that was passed will be used to create a new Date in order to get the user’s specific local time zone:

LocalTime.parseDate = (dateString) ->
dateString = dateString.toString()
new Date Date.parse(dateString)

Lastly, presenting the local time zone

Lastly, the textContent will be deterimed by the local variable that was passed. The textContent is the text value inside the tag which is what the user sees. It will mark the tag as localized, by setting data-localized=”true” on the tag and present the actual local time of the user in the right format by calling strftime(time, format):

processElement: (element) ->
# ...
element.textContent = content =
switch local
when "time"
strftime(time, format)
# More options...

Ruby Inside

Liroy Leshed

Written by

Founder & CEO @

Ruby Inside

Ruby articles and posts

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade