(upbeat electronic music) - Hi everyone, thanks for coming. My name is David, otherwise
known on the internet as Dabit. You can find me on Twitter and GitHub, Instagram, everywhere as Dabit. And I work for a company named
Michelada, like the drink, you've probably had one of those. And we are a Rails team that you can hire to work from Mexico, more specifically a small town on the west coast that you've probably never
heard of named Colima. But even though we're
headquartered in Mexico, most of our clients are in the U.S. And as such, we've been asked to work with multilingual apps
several times in the past. And even though if we built an
app for a company in Mexico, we probably wouldn't use
Spanish for the actual code, we'd probably use the
internationalization engine in Rails to do that because it will
just make weird looking code if you mixed English and
Spanish, or other language. Like let's say we're building
a hotel reservation system, and you can't have HabitacionesController, or ReservacionesController,
that would just be weird. So we actually, even if
the app is in Spanish we actually use English. So I mean you could do
everything in Spanish, but you probably shouldn't,
or you'll just end up with some multicultural mess, right? (audience laughs) So let's just stick to English, that's what we do even
if the app is in Spanish. So we've done a fair share
of internationalization, which is a pretty weird word to pronounce. So it's often abbreviated as i18n. And the reason it's called that is because someone was having
trouble pronouncing it, like myself, as in-ternat-ional-ization, so I say screw it, it's
an I, 18 characters, and then an N at the very end. And believe it or not,
that's why it's called i18n. So pfft, you can't make
this up. (chuckles) So if we go back to, let's talk a little bit about the basics. You've probably heard the basic stuff regarding internationalization
on your Rails applications, and it goes like this. There's a T function
that's used as the core of translating Rails applications, and to that T function you pass a key. And additionally, if you
want you can pass a scope, which can be either as a parameter, or just prepended to the
actual key, separated by dots. And then you keep yaml files
with this same structure, and in different languages
where you actually store the text that you want translated in the different languages that you have. So you will have something like this. So everywhere you call the T function, and you pass the key and the scope, depending on the locale
that's currently set, it's gonna return the text that you have on your files, right? So like I said, it doesn't
matter if it's scope as an attribute, or just
prepended as part of the key, it works the same. But depending on your purpose
you might need to pass it as an attribute or just like that. So on an actual application you will have something like this. Maybe you have a page that has text that you want to localize
in different languages. So in your view, instead
of using the whole text, or like straight text,
you will replace that with calls to the T function, and then you will pass the
key of the desired text that you want to display, you
just define your Yaml files with the different
translations for English, Spanish, Japanese, whatever you want. And then somewhere on the code you just set the locale, maybe
it comes from the database, because the user set it as a preference. Maybe it comes from a query parameter like I'm doing right here. And that's all you need to set it up, and your application automatically does, once you switch that
locale in the i18n object, then it just works, right? So that's the basics of how
internationalization work. So now I'm going to dive into things that I don't see often, like
I've been included in teams that are already in place, and then there's things that they're doing that could be considered, or
that could be done better, let's say it, right? So maybe you know them, maybe you don't. If you do, it might help
refresh your memory. So let's start with the first one, which is you should split
your Yaml files, right? By default, Rails, it only
reads the locale files from the config locales folder. And then you may assume that you have to just bash everything in
there, separated by locale name. And then as development continues you end up with an ugly,
very, very ugly things, Yaml file with thousands
and thousands of lines that no one can read, no one can maintain. So it's better if you
organize your translations. Maybe separating folders for
a folder for active record and a folder for the admin translations, your folder for device, I
don't know, however you want, but a little bit more
organized by folders. Even in the case of active record, you can for example have
more folders, right, separated by language, and then each model can have its own file. And that way if you are actually working on developing this application, you will know exactly where to
go to change the translation for a specific model, if
I wanna change something on the address model I will know that there's an address.es.yml file. So if you use VI or whatever else you use, you can use Command + T and
you'll know exactly what to type and go there really quick
to where you're looking for. So be nice, and be organized
about your Yaml files. But like I said, this is
not a default in Rails. You have to do something
on the application rb or on initializer to actually tell it to be recursive about reading those files on the locales folder. And to be honest, I don't know
why they haven't changed it, I saw some discussions around it, DHH says that it's already a
very small minority of apps that need localizations, so it
doesn't matter, I don't know. It's just a single line,
so not a big problem, just add that line and
you'll be fine to go. The second thing that you should do when you're translating
or using a Rails app that's in multiple languages
is to use the i18n-debug gem, which is just something
that you can throw in there, add it to your gemfile. And what this gem does is, let's say you have a
form like this, right? What it's gonna do is
when you load that page, you will start seeing all of this. It's gonna show a trace of
how it's querying the backend for the translations, and then it's gonna show a blue screen, I don't know what's going on there. And you will see them in the
order as they're being loaded. Let's say that you're showing that page and there's a lot of things
loading from the backend that you don't even know
because it does it by default, like Rails used some
translation by default. So what I was saying is that even if you're not explicitly
using translations, most of the helpers in Rails
are trying to translate the labels on the forms
and things like that. So by using this gem you will be able to see what's happening in
the order that it's happening. So there's what is called
a default lookup tree, which is what Rails will use
to figure out a translation on your views. So if we take a look at what's
happening here, for example, this is just a regular form with a label and some fields there. If we focus a little bit on the name, we'll see what's happening
here on the background is that you can see how it's querying. Okay, it's querying for a translation for helpers.label.author.name,
and if it can't find that, then it's gonna try
activerecord.attributes.author.name. And if it can't find that it's
gonna try attributes.name. So this means that you can
have a global translation for all the attributes in your application that are called name, because
that's pretty usual, right? Probably name is gonna
be nombre in Spanish for all the models, so you can have that. But then if you have
something more specific, let's say here on this form I
don't want it to show nombre, but something a little bit
larger like nombre de autor. For this particular
model, then I can do that by setting
activerecord.attributes.author.name on my translation file, and
then it will find it first, and that's what it will use. And if that's not enough,
if you need something more fine grain and just
for the labels on the form, you can use the other translation, which is helpers.label.author.name. So this gem helps you in not trying to make up names for translations, there's already something
going on in place. So maybe you just need to
figure out what that is, and then just put it on your Yaml file. And like I said, there's nothing
special that you need to do as you are using the
regular Rails helpers, it's gonna do this by
default, so very helpful. The other thing that you
want to do as much as you can is use lazy lookup. So let's say for example, it's typical that in your applications you have this flash message
that shows at the top when something is created,
or updated or whatnot. And you pass it as notice
through the responder. So if you replace that by a translation because you want it to show
up in different messages, you don't need to come up with a name like authorcreated.flash
notice or something like that, you can just use .flash on the controller when you call the T function. And by starting that with a period, what it's gonna do,
Rails is gonna figure out what the scope of the
translation is automatically, and it's gonna sort of propose something, which is in this case Spanish authors, because it's the authors controller, create because it's the create action, and flash because that's
the name that I've given. So it just sort of calculated that for me, and I don't have to come
up with a weird name. And something interesting that's happened here in the background is that there's also a flash
translation, for example, that I could use for the
authors controller in general, that could be used if
it's the same message for all the actions in the
controller, for example. Or if there's some text that can be used in all of the actions, so it's global. So as you can see there's a
lot of this things happening in the background that we don't know. But that particular gem is
helping figuring that out. And lazy lookup can also be
used on forms, sorry, on views, which this is interesting. You can just, like I said,
prepend for example .page_title, and automatically it's gonna
show as authors.new.page_title as the query scope for that translation. This is interesting because
you can have partials, and they can be used in
different controllers or different views. And by using that lazy lookup,
depending on where it is it's gonna come up with a
whole different query scope for the translation. So you can have the same view used for different controllers,
but if the text changes, like the title on the page or whatnot, you can just change it
using the translations. And speakin' of views, you
can actually localize views. This is the one that I don't
see often in Rails applications and it's the one that I like the most. So sometimes you just have a lot of text that needs to be localized or translated. And I've seen people just adding
a lot of keys on the view, and then adding all of
that text on the Yaml file, and this makes just a lot of text, and text that can be broken and whatnot. So you can actually localize views just by appending a locale
to the extension file. And if you do that it will
just load the whole thing depending on the locale that you're on. So you can have a Japanese view for your terms and conditions page, and instead of having it
in different Yaml files you can just have the whole
view translated, right, and have the same thing in Spanish, and Rails automatically
would try to load the view for the currently set locale, depending on the file extension. And this is also useful because
as you use more languages, maybe the layout itself of the
pages need to change, right? Maybe for the Japanese version you want it to show as a table
instead of just straight, or for Arabic languages
you need to show it from right to left, whatever it is. So you can localize the whole view instead of just the text strings. So you just need to have
different views, and that's it. So that's pretty nice. And it actually works with partials, so even if you don't have
to localize the whole view, if there's just some little piece of text that needs to be localized, you can use it just with a partial. And it works with mailer views of course, so if your newsletter needs
to be on different languages, you just create the views
for each of the actions, and depending on the locale that's the view that's gonna use. So there's a lot of uses for that. Instead of using ifs around in your views, you can actually do this, so. Fallbacks, so even if some
languages are similar, there's always variations
depending on the regions, right? There's some words on the Spanish that is spoken around in Spain versus the one that's spoken in Mexico that's totally different, there's some variation of the words. And even in English, for example, you have the British English, there's some words that
are totally different on the American English, right? Like Soccer, they call it Football, the Football they call
it American Football, the restroom is the
toilet, a bill is a note, and the shower is the bath. So you don't want to keep
translations for both languages for all the words, right, because there's a lot of
things that they share. So what you do is you config in your application the rb file, translation fallbacks in
which you will tell Rails to use one locale, and if it
can't find the key for that then try the next one, right? So in this case I'm telling
it to use the British English, and then if it can't find the
key that it's looking for, then try on the regular English. And as you can see it's an array, so it can be several other languages that can work as fallback. So let's say now you have a page like this where we have the things that they share in common at the top, and three words in common at the bottom. So as you can switch
between the two locales, you will see the differences that are defined on both your Yaml files. But the words in common, for example, you only define them on
the regular English file, and you don't need them anymore
on the British English file. So if we take a look at what's
happening on the background, it's happening exactly
the way we'd expect, which is it's querying
first for all the words on the British English file, because that's our current locale. But then if it can't find those words it will just fall back to
the American English file. So like I say, it's pretty useful because there's many language, I mean British English
and American English, it's just one example, but in Spanish there's a lot of variation
between words in Venezuela, Argentina and Mexico. So if you're trying to
actually be really global, you need to change those,
the little variations, and this is very useful for that so you don't have to
repeat yourself too much. Now let's talk a little
bit about pluralization, because this is another
one that I don't see often. And it was pretty recently,
maybe a couple months ago I saw this being misused
in an application. So sometimes you just need to show numbers and then some text, right? And when it's zero you need
to use the word in plural, when there's just one you
need to use the singular, and then if there's more than one then you need to use the plural
again of the word, right? Or maybe the text says just different. If it's zero you wanna
show there's nothing here, or no points, and I don't know, there's many ways that this can work. And sometimes you'd think
the right way to do this is to just use an if structure like this, and depending on what's the
value on the points variable you just use different translation keys that you can then go and
define on your Yaml files, and that's it, right? And it actually works, it's
not that it doesn't work, but there's actually a
better way to do this which is included in the
whole i18n engine of Rails. So instead of having this, what you can do is actually
just define the keys for points, and then you use the three
key words, which is zero, and that's the way you
tell Rails what to show when there's zero points or
whatever you're counting. What you do when you have one, and what you do when you
have anything else, right? And in the three of them you
can interpolate the count, which will be what you
will pass as a parameter through the T function to determine how many objects we're having right now. So instead of having
this weird if structure, you will have something like this where you call the T function, and then you pass the count variable, and it will do exactly
the same that it does with that weird if structure, but it will automatically
handle the whole thing for you. So as you can see, the
code is much, much cleaner, and way easier to maintain and understand, so works the same. So we've been talking about using Yaml to store your translations, but it's not the only way to store them. There's different strategies,
or what they call backends, to actually store your translations, and then pull them and
display them on your app. And all you have to do is just create an initializer somewhere and define what backend you want to use for this particular application. So let's say for example
that you want to use the ActiveRecord backend. You will need to add a
gem because these backends are not part of the Rails core, but there's a lot of gems to
handle different backends. And when you use this
gem, what it wants you is to have a migration or a table that will look something like this, where you will store the locale, the key that you're looking for, and the value which is exactly
what's being translated on an actual ActiveRecord database. So you set it up as the backend, and let's say that we
have a form like this. Instead of having all those
Yaml query lookups that we had when you used a Simple backend, now you have a bunch of queries
to the database, which is, as you can imagine, not very good if you're not using proper caching. But hey, it still works, right? So it's an option. So the way you actually
create the translations, it's plain ActiveRecord, you
just insert those records in the database, and it
doesn't have to be via console, it can be via an admin tool
that you can build for someone that actually needs to
be actively changing those translations, right? And instead of deploying
a change on the Yaml file, then they can do it on the fly by changing the actual database. So that's one of the use cases
that I've seen for that one. Like I said, there's several backends. The Simple one is the one that
comes by default with Rails, which uses Yaml and some internal caching to not overwhelm your application. There's the ActiveRecord backend that we've talked about a little bit. But there's a Redis backend
if you want to use Redis, and it helps you, then why not use Redis. If you like Mongodb for some
weird reason, I don't know, you can use Mongo for that if it's up, and if it doesn't breaks. Or you can use a thing
that's called Gettext, I don't know if you've
used that in the past. I did for a Rails application and it was not quite a good experience, so I wouldn't recommend that either. But you can use any
already defined backend. And even if there's nothing for you, and maybe you store your
translations using Butterfly, Communication Vibration or whatnot you can draw your own translation backend, and it's pretty simple. So let's say we want to
actually do a Hodor translator, we can just define a class
that's i18n::Backend::Hodor, and there's a helper class from Rails that you can just include
which is the backend base, and then that backend base
will just add everything that will make this class
behave as a backend. And then you just need to
implement the lookup method that will receive the locale, the key, the scope and some options, and then you can do with
that whatever you want. And whatever you wanted stored. So in this case we just want
to return the Hodor string for everything that's
trying to be translated, and then it will just work, right? Everything will be Hodor. So that's pretty flexible, however you want to store your
translations is up to you. And it's pretty handy to know that you can do something with it. And then complementing that, there's a special backend that exists which is called a chain backend, which is part of the Rails toolset. And we talk about having all
of these backend options, but what happens if you want to mix them? You can do that by
using the chain backend. So you declare it as
the translation backend on your initializer, and then you tell it which backends you want it to use, and it's gonna go through
them in order, right? So in this case it's gonna go through, look for a translation
in ActiveRecord first, and then if it doesn't find it there it's gonna go to the Yaml backend. And as you can see, if we go back to this after using the chain backend, if we go back to this
page that we already had, we will see on the log
file that what's happening is that it's creating the query first to try and find it on the database, and if it doesn't find
it then it falls back to the Yaml translation
that we had in the past. So I did mention that you could, one of the use cases for this is that there's maybe some
text on your application that marketing needs to change, and you don't want to be deploying every time marketing
wants to change that text, then you can create real quick
a small interface for them where they can go and change the text directly on the database. It will probably be the other way around, where you have the Yaml backend first and then try to find it on the database in terms of performance,
but you get the idea, right? You can have, maybe if
it's not on the database, it's on Redis for some reason, and then fall back to
Yaml, whatever you want. That's the purpose of the
specific chain backend. And then we need to talk
about how to translate and localize times, dates and numbers. There's an L function that exists that's used to localize
at least your dates, and you can define different date formats on your Yaml files, or on your database, or wherever you're
storing your translations, and just have different ways
to show timestamps, right? Instead of using SDRFTime
and then a pattern, you can use this. If you're not translating the app you can use it to use different
date formats in your app. So let's say that you
have defined a default, and a formal, informal
and whatnot formats. What you will do is go to
your view, use the L function, and then when you pass
through the timestamp. It will obviously use the default one if you don't pass what
format you want to use. But if you pass it one then it will just go to
that translation file and it will format the
date as you define there. So it's pretty useful, like I said, instead of having to use
STRFTime everywhere or whatnot, you can keep your formats in a neat place if you use what's on i18n already. And then there's a bunch
of very useful functions that you don't see a lot used in the wild, like distance_of_time_in_words_to_now, which is a long name for a function, but it will figure out the whole thing about how much time is between here and the timestamp I'm passing. And for example, if I pass three seconds it will return that less than a minute. If I pass three days, it's
gonna just translate three days, three months and so on. And the other thing is that
it's all translated, right? If you define it for different languages, then it's just gonna work
for all of your locales. It's not just some weird Fishbus algorithm that you can come up for English. If you need it in different languages then it's all taken care of for you. So it's pretty useful. Same thing for numbers_with_precision, and this actually includes
a little bit more options. Like for example, maybe in Mexico we use two decimals for a number, but in some other country it's
usual to use five decimals when you round the number,
or different separator, delimiter and all that, those
are different among countries. So you can use this function
to normalize all that and just change it depending
on the locale that you're at with your Yaml files. You can define the separator
they use for the numbers, delimiter, the default precision if you don't want to specify it, and it will just work for
all different languages. So it's pretty useful instead
of using round or whatever. And then there's a number_to_currency too. Instead of having ifs
returning different symbols, you can just use the
number_to_currency function that does the same thing, but also adds, it gives it a more money kind of format to the number that you're passing. And it includes some around the unit, like that dollar symbol,
that can be different depending on the country you're at. So you can use that function
to keep that normalized and just change it depending
on the locale you're on. Last but not least is the
number_to_human function, in case you want to
show the number as text. 3,000 maybe, or 3,000,000, whatever. And you wanna show that
in different languages, then you can just use that helper, and just define all those
words, thousand, milles, or however you want on
your translation files, and just keep your view code clean, and it will just use internals to translate all that text for you. And oh, almost forgot about this one. This is one that's I actually
know it exists for this talk, I didn't know it existed. I'll try to find a good use
of it, number_to_human_size which will just turn the
numbers into kilobytes, megabytes, terabytes, whatever you need. So pretty useful if for some reason it's called different
on a different country. I don't see how that's the
case, but you never know. And that's it, that's all I have for you. I'll give you a little
recap of what we saw here. Remember, be nice, split your yaml files. Use the default lookup tree. Use the gem to figure
out what translations are already being calculated for you. Instead of making up weird names, you just try to go with the convention that Rails already have to
accommodate your localizations. Remember that you can localize views, you can localize partials,
and this is very useful when there's a lot of text on the views that you're trying to show. Use fallbacks for small
language variations instead of creating whole language files for all the other languages. Use pluralization, try not
to use a weird if structure, that code's already in place for you. Remember that you can have
many backends as you want, and you can even roll your own. And use those included methods to localize times, dates and numbers, so you don't have to deal with that, because it's already there for you. Thank you, that's it for me. (audience applause) (gentle acoustic music) (toy squeaks)