Using Rails localization outside of your views
When I went to Europe for the first time, first to England and then to France, I was deeply obsessed with track cycling. Since I was (kind of) writing my Masters Thesis while my wife was (actually) doing serious research for hers, I had plenty of spare time to hunt down every concrete or wood-banked oval in all of France. My process started out by simply looking for the famous ones like Roubaix and Vélodrome d’Hiver. I quickly realized that every town in France has a great website and they nearly always listed the address of their velodrome if they had one! With that in hand, I would then search for it in Google Earth and put a push-pin there. This was a strange and silly obsession but it taught me a few things. It taught me to read a bit of French, to provide correct accents to Google France’s search site, and to do my best to search like I was actually a French speaker. I’m not sure if I succeeded, but this is the exact opposite of what we want our users to have to do with any of the sites we create. Luckily Ruby on Rails provides extensive support for Internationalization (i18n for short) via mechanisms such as Translation and Localization. The prior simply creates mappings between variables (and parameters) and some copy in the language to be displayed. The latter does things like format numbers, times and dates, and currencies correctly according to the language to which we are localizing. This alone should make us ecstatic but we can leverage the tools provided in even more exciting ways.
More after the break.
Recently I have worked with a customer on accomplishing a very simple task: Create a CSV template for download by their customers. The content was mostly static headers with a little bit of dynamic content to boot:
Since we like to do a little something, show it to the customer, and see what they think, this was a completely sane first step. The static headers were provided by them via a story in Pivotal Tracker, so we simply copied them into the code and added a little business logic in the model.
The client was happy and accepted the story but immediately created a second story because they wanted a few more headers that weren’t there. Clearly, I can fix this very easily: I just get the new headers, fire up the editor, add them, and push:
Great. Done. Next project.
Actually there are much cooler ways to do this, but first a short aside. Early on at Highgroove, during our weekly code-reviews, one of the team came across a short and easy commit I had made. The only change was to update some copy in the view that was provided by the copy-writer at the client shop. I had created some generic text explaining what I thought things did just as a place-holder, knowing full well (and in fact creating a story asking for) new text which would replace it later. Actually this text was in the i18n file (
en.yml) file and looked something like:
And my commit actually updated
config/locales/en.yml rather than the view itself. My reviewer said “Have you let the client know they can simply edit the translation file themselves when they want the text changed?” To be honest, I had no idea. I got back a quick skitch that summed it up nicely:
Github provides so much great functionality it can be hard to describe it to someone without running out of breath in the process. One of those features is the ability to edit some files in Github without checking out the repository. This is great for people who aren’t developers but are actively involved in the development process in some way, like say, copy-writers!
Great, so now we can work on models and edit files in the browser, thanks to the folks at Github, but how does this work with CSVs? Well, what if we can store the headers themselves somewhere that the client can edit them without asking us? Can we access the internationalization file from within Models and not just views? It turns out we can with the
#human_attribute_name method provided by ActiveModel::Translation.
Note that to utilize the “human attributes” we define a new section of our
en.yml file per the Translations for Active Record Models section of RailsGuides.
This provides many advantages over the previous implementation: * We separate model logic and data storage. This is the same natural data abstraction that MVC is intended to provide. * Multiple methods can now access the static headers and do whatever they may need to do with them (such as validate them later). To do this in the model we would require a constant, a class variable, or some other mechanism to bind it to the model directly.
Since the localization files are written in YAML we can leverage its flexibility to embed simple objects within our files which leads to another application of i18n functionality in our code. If a client has a complicated external collection of test-cases that is updated often, perhaps based on their own agile or iterative process, then we can leverage i18n in our testing regime.
Consider a test-suite like the following:
In this case the list of the three expected countries (line 6) are provided by the customer, as is the country file. If they have a new and better test fixture that handles some complicated edge case, then they have to provide us with the new file. We can then analyze it, do the right TDD thing by updating the test-suite, etc. Or, the customer could update
en.yml with the new test data and the necessary fixture, and we can keep the test-suite exactly as it was. It could potentially break at this point, which is a great place to start our red-red-green TDD process:
Leveraging i18n within all aspects of a rails app, including testing and models, rather than restricting ourself to our views empowers developers and clients in our Agile process. We can work on delivering high-quality code which is also extremely flexible to the needs of the customer. This in turn allows the customer to make necessary changes without a back-and-forth via a medium such as Tracker or a chat-room. Instead ‘text-needed’ or ‘data-needed’ stories can be created and delivered without developers being involved unless they are necessary, making the entire team more productive.