In this series I'll demonstrate one way of localizing a real world Vue.js app using these excellent Mozilla projects:
Fluent: "Fluent is a family of localization specifications, implementations and good practices developed by Mozilla. With Fluent, translators can create expressive translations that sound great in their language."
Pontoon: "Pontoon is a translation management system used and developed by the Mozilla localization community. It specializes in open source localization that is driven by the community and uses version-control systems for storing translations."
My aim is to establish a workflow that scales well with an application's growing size and number of locales and translators. This series will chronicle my journey to realize that goal.
For this first part I'll focus on adapting the app code. The second part will focus on using Pontoon
to improve the collaboration process with the team of translators.
The idea is to have one or more catalogs based on your app structure. At a minimum we'll have a base catalog, we'll call it global
, that has the basic text needed for the app to work initially, possibly being the only catalog if your app is small. You can then create other catalogs with varying levels of granularity. If your app is big and you use dynamic components to load only the part the user is in, you could for instance have a profile
catalog that would be loaded for the profile or any other related page. At the finest granularity we can even have component-specific catalogs.
The catalog need to be loaded in the code at some point, and for that there are some options, like:
- A wrapper component that handle the loading, possibly displaying some loading indication.
- Manually using a functional API
I'll focus on using a functional API for the sake of simplicity.
I want the catalogs to be able to be treated as part of a whole, in which case the keys are global and each catalog adds its keys to the global pool. It would be nice if they could also be bound to a context, inheriting or not from the global pool, in which case overwritten keys would only affect components under that context.
The app
I want to demonstrate the process of localizing an existing app, and it would be nice to use a real world application as an example. For that we'll use the Vue RealWorld example app as a starting point.
Starting up
To make it easier for you to follow, I've set up a GitHub repository with all the code at https://github.com/davidrios/vue-realworld-example-app. I'll refer to specific commits so you can see the changes along the way.
This is the state of the code with minor changes made after forking:
https://github.com/davidrios/vue-realworld-example-app/tree/f621d819
We'll use the excellent fluent-vue project, that already implements fluent for Vue.js. First install the packages:
yarn add fluent-vue @fluent/bundle intl-pluralrules
We're using Vue 2 in this example so, per fluent-vue requirement, we need to also install:
yarn add @vue/composition-api
Loading the locale files
We'll start simple and use the raw-loader
to easily load ftl files through webpack by adding this configuration:
https://github.com/davidrios/vue-realworld-example-app/commit/e5038262
Now we need to load the catalogs. It would be nice if we chose the catalog based on the user's language as detected by the browser. For that I added some code to detect the language, load catalogs and setup fluent-vue
:
https://github.com/davidrios/vue-realworld-example-app/commit/cff8b43f
This code will be improved on later.
From 015c35dc
to 307bf3ca
I just extracted strings for translation:
https://github.com/davidrios/vue-realworld-example-app/compare/015c35dc...307bf3ca
Here I have improved the catalog loading and added the option for the user to change the locale at runtime:
https://github.com/davidrios/vue-realworld-example-app/commit/0585b5a1
Live reload of locale files
As I was making more translations, I started to dislike the fact that the whole page was reloaded every time I changed any catalog, which I think is unecessary. I know webpack has a way to reload only the parts that changed with the right configuration, so I searched around but could not find anything that suited my needs.
I ended up writing my own loader to help with that:
# https://www.npmjs.com/package/@davidrios/hot-reloader
yarn add -D @davidrios/hot-reloader
And then I refactored all the catalog loading code to be more generic and make use of webpack's HMR, so now changed catalogs update the page instantly without a reload:
https://github.com/davidrios/vue-realworld-example-app/commit/fbc238ee
Separating catalogs
Separating the app in more than one catalog will be quite easy thanks to the last update to the loading code:
https://github.com/davidrios/vue-realworld-example-app/commit/45c2ea72
Some localization examples
Using fluent's own date formatting:
https://github.com/davidrios/vue-realworld-example-app/commit/ccc4da77
Localizing content with tags
One very common problem of localizing web apps arise when you need HTML tags / components in the middle of some text. Consider the example:
<p><a href='x'>Sign up</a> or <a href='y'>sign in</a> to add comments on this article.</p>
Or even worse, using components:
<p>
<router-link :to="{ name: 'login' }">Sign in</router-link>
or
<router-link :to="{ name: 'register' }">sign up</router-link>
to add comments on this article.
</p>
Best practices of localization (actually the only sensible thing to do!), say that you should translate the sentence as a whole, so how do we do that without risking translators messing up the code or worse, introducing security issues? Luckly vue
is powerful enough to provide the tools necessary to tackle that problem, and the fluent-vue
project do a perfect job of realizing that with the help of fluent
's powerful syntax.
The fluent code would look like this:
# The two parameters will be replaced with links and each link
# will use the .sign-*-label as its text
sign-in-up-to-add-comments =
{$signInLink} or {$signUpLink} to add comments on this article.
.sign-in-label = Sign in
.sign-up-label = sign up
I personally think the result is great. We have comments explaining what is happening, it's very flexible to the translator, the pieces needed are in-context and there's no HTML in sight!
For the vue part, fluent-vue
provides a nice component named i18n
with everything we need. The vue code would look like this:
<i18n path="sign-in-up-to-add-comments" tag="p">
<template #signInLink="{ signInLabel }">
<router-link :to="{ name: 'login' }">{{ signInLabel }}</router-link>
</template>
<template #signUpLink="{ signUpLabel }">
<router-link :to="{ name: 'register' }">{{ signUpLabel }}</router-link>
</template>
</i18n>
Notes:
- The
path
property takes the name of the translation key. - Each variable in the text, like
$signInLink
can be used either as a direct value by passing it as args to thei18n
component, for instance:args="{ signInLink: 'The link' }"
, or like in the previous example as a named slot. - Each named slot receives the other translation attributes as slot props with their keys camelized. In the previous example they would receive the object:
{ signInLabel: 'Sign in', signUpLabel: 'sign up' }
, so you can use object destructuring to make the code cleaner, like#signInLink="{ signInLabel }"
, which will receive the value of the translation attribute.sign-in-label
.
The fluent syntax is very powerful, yet relatively simple, I highly recommend you take the time to read the full guide here.
Managing fluent catalogs
The idea is to manage the localization files using Pontoon but, since that will be discussed later in part 2 of this series, for the sake of completeness in this article I added a simple script that updates a catalog based on the base locale one:
https://github.com/davidrios/vue-realworld-example-app/commit/1a8f7767
Thanks to the good folks at the fluent
project that provided an API for dealing with catalogs programatically with the subproject @fluent/syntax.
You can run the script executing:
yarn update-catalog LOCALE CATALOG_NAME [FROM_LOCALE]
FROM_LOCALE
is an optional parameter that if not provided will default to 'en-US'. To update the pt-BR
global
catalog for instance you would execute:
yarn update-catalog pt-BR global
This will merge the contents of the FROM_LOCALE
catalog with the chosen one, preserving comments from both and moving keys that doens't exit in the base catalog to the end of the file with a comment noting that. The resulting file will be saved with a new name if it already exists or with the final name if creating a new one.
I've used the script to merge the catalogs, translated the rest of the keys and published a build here:
https://davidrios.github.io/vue-realworld-example-app/
And that's all for now. With all this in place I hope you already have the basic framework to start localizing your apps the "right way", while being convenient for the developers and easy on the translators.
Thanks for reading!
Top comments (0)