RailsConf 2019 - Localize your Rails application like a pro by David Padilla

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
(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)
Info
Channel: Confreaks
Views: 1,566
Rating: 5 out of 5
Keywords:
Id: YqgLQ70K7uU
Channel Id: undefined
Length: 33min 51sec (2031 seconds)
Published: Sun May 26 2019
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.