Software localization

Localizing Meteor Applications Powered by React

Learn how to add internationalization to Meteor applications with the help of meteor-universe-i18n. Learn how to set the library up and perform localization.
Software localization blog category featured image | Phrase

Meteor is a quite popular full-stack JavaScript platform to create powerful web and mobile applications. It allows you to develop both client-side and server-side in JavaScript, and also can be used in conjunction with Angular or React. Many developers are fond of Meteor because with it you can create applications blazingly fast, which is especailly important in prototyping, for example.

In this article we are going to talk about adding internationalization to Meteor applications with the help of meteor-universe-i18n. You are going to learn how to setup this library, prepare translations, perform localization, switch between locales, infer locale based on the user's preferences, and work with currencies. Our demo application will be powered by Meteor combined with React so we'll also see how to perform translations using React components.

Sounds good? So let's get started!

The source code for this article can be found on GitHub.

🔗 Resource » Make sure to stop by our Ultimate Guide on JavaScript Localization for a deep dive into frameworks like React and Angular and learn everything you need to make your JS apps accessible to international users.

Hello, Meteor I18n!

Preparing the App

To start, create a new Meteor application called ShopFu:

meteor create ShopFu

This is going to be an online shop with some random goods. Of course, we are not aiming at something like Shopify at the moment, but who knows what the future might bring...

At the time of writing this article, meteor-universe-i18n had some compatibility issues with Meteor 1.7, so you might need to downgrade to version 1.6:

meteor create ShopFu --release 1.6.1.3

The next thing to do is introduce React support into our application. UniverseI18n is mainly focused on localizing Meteor+React applications, but there is an accompanying library that provides some helpers for the Blaze templates.

meteor npm install --save react react-dom

meteor remove blaze-html-templates

meteor add static-html

So, we are installing all React goodies and are getting rid of the Blaze templates. All templating will now be done by React itself.

Also install the UniverseI18n:

meteor add universe:i18n

Nice! Before proceeding to the next section, let's render a very simple welcoming message at the main page of the site. To do that, replace all contents inside the client/main.html file with the following:

< body>

  < div id="welcome"></div >

</ body>

Replace everything inside the client/main.js with:

import React from 'react';

import { Meteor } from 'meteor/meteor';

import Header from '../imports/ui/Header.js';

Meteor.startup(() => {

  render(<Header />, document.getElementById('welcome'));

});

Effectively, we are waiting for Meteor to start up and then render the Header component.

This component will live in the imports/ui/Header.js file:

import React, { Component } from 'react';

export default class Header extends React.Component {

  render() {

    return (

      <h1>Welcome to the Shop Fu!</h1>

    );

  }

}

That's it! You may now boot your app by running

meteor

Navigate to http://localhost:3000 and make sure that the header is displayed. The next step is to translate this header, so let's proceed to the next section.

Setting Up UniverseI18n

Getting started with UniverseI18n is quite simple really. All you need to do is import it, provide basic configuration, add load translations. Let's do that inside the client/main.js file:

import React from 'react';

import { Meteor } from 'meteor/meteor';

import { render } from 'react-dom';

import i18n from 'meteor/universe:i18n'; // <--- 1

import Header from '../imports/ui/Header.js';

Meteor.startup(() => {

  i18n.addTranslations('en-US', {  // <--- 2

    Common: { // <--- 3

      welcome: 'Welcome to the Shop Fu!' // <--- 4

    }

  });

  i18n.addTranslations('ru-RU', {  // <--- 5

    Common: {

      welcome: 'Добро пожаловать в Shop Fu!'

    }

  });

  i18n.setOptions({  // <--- 6

    defaultLocale: 'en-US'

  });

  render(<Header />, document.getElementById('welcome'));

});

I've pinpointed the main parts of this code, so let's talk about them a bit:

  1. Here we are importing the UniverseI18n — nothing special.
  2. This is how you can load translations for the English locale. Out of the box UniverseI18n supports dozens of languages (including information about currency, locale's native name etc), and in this demo we'll also add support for the Russian locale. Also, note that it is possible to store translations in a separate JSON or YAML file as explained here.
  3. This Common key is called a "namespace". You may have as many namespaces as you like, which is especially important for large applications with hundreds of translations. In this demo, we'll have only one namespace.
  4. This is the actual translation. welcome here is key, whereas the 'Welcome to the Shop Fu!' string is the value to be displayed.
  5. In the same way we are loading translations for the Russian language.
  6. Here you may set options for the UniverseI18n. It has sane defaults, so all I'd like to do here is provide the default language to use.

That's it! We may now translate our first message!

Performing Translations

You have two options to perform translations in UniverseI18n:

  • By using i18n.getTranslation() or i18n.__() methods
  • By creating a React component

In this demo, we'll mostly deal with React components as they're much more convenient to work with but let me briefly show you how to perform translations explicitly.  Add the following line of code to client/main.js:

// imports...

Meteor.startup(() => {

  // UniverseI18n config...

  console.log(i18n.__("Common" ,"welcome"));

  // render...

});

After reloading the page, you should see 'Welcome to the Shop Fu!' in the browser console. Basically, the i18n.__() accepts a namespace, a translation key, and parameters (which are optional). It is also possible to create a special "translator" object, provide options there and perform translations in the following way:

var translator = i18n.createTranslator('Common', 'ru'); // Russian is explicitly set

console.log(translator("welcome"));

Translating with React Components

Now let's see how to translate using React components. Modify the Header.js file by importing UniverseI18n and creating a special T constant:

import React, { Component } from 'react';

import i18n from 'meteor/universe:i18n';

const T = i18n.createComponent();

// our class...

Now modify the render() method:

// imports...

export default class Header extends React.Component {

  render() {

    return (

      <T>Common.welcome</T>

    );

  }

}

<T> is a special tag that is going to perform the actual translation. Common, as you remember, is our namespace, whereas welcome is the key. As you see, nothing complex about this approach.

However, after you reload the page, you'll note that the welcoming message is now displayed in a <span> tag, rather than <h1>. To fix that, override the tag name inside the render() method:

// imports...

export default class Header extends React.Component {

  render() {

    return (

      <T _tagType='h1'>Common.welcome</T>

    );

  }

}

Switching Between Locales

Our translation works, however, there is no way to switch to a different language. Let's introduce this feature now by creating a new Locales.js file under the imports/ui:

import React, { Component } from 'react';

import i18n from 'meteor/universe:i18n';

export default class Locales extends React.Component {

  render() {

    return (

      <ul>

        {this.renderLanguages()}

      </ul>

    );

  }

}

This is going to render all supported languages in an unordered list. Upon clicking on one of the languages, the locale should be switched.

Code the renderLanguages() method:

// ...

export default class Locales extends React.Component {

  renderLanguages() {

    return this.getLanguages().map((lang) => (

      <Language key={lang.code} lang={lang} />

    ));

  }

  // ...

}

Here we are loading the supported languages and then use the Language component to display them. This component is going to be created in a moment, but before that let's also add the getLanguages():

// ...

export default class Locales extends React.Component {

  getLanguages() {

    return i18n.getLanguages().map(code => ({

      code,

      name: i18n.getLanguageNativeName(code)

    })

    );

  }

  // ...

}

This method relies on UniverseI18n's getLanguages() that loads all available locales. Then we construct an object with the language's code and its native name (fetched with the help of getLanguageNativeName()).

Now add a Language.js file inside the ui/imports:

import React, { Component } from 'react';

import ReactDOM from 'react-dom';

import i18n from 'meteor/universe:i18n';

export default class Language extends Component {

  switchLocale(event) {

    i18n.setLocale(this.props.lang.code);

  }

  render() {

    return (

      <li onClick={this.switchLocale.bind(this)}>{this.props.lang.name}</li>

    );

  }

}

Inside this file, we render a list item with the language's name. List item also has a click event handler that calls the setLocale() method and switches the language to the chosen one.

Lastly, don't forget to import this newly created file:

import Language from './Language.js';

This feature is done, so we may render the locale switcher inside the main.js:

// ...

import Locales from '../imports/ui/Locales.js';

Meteor.startup(() => {

  // ...

  render(<Locales />, document.getElementById('locales'));

});

Add the div#locales to the main.html:

< div id="locales"></ div>

Now reload the page and observe the result! One cool thing to note is that after you switch locale, the welcoming message is instantly translated thanks to our React component.

Inferring User's Locale

Another common thing to do is trying to "guess" which locale the user prefers. In the simplest case it can be done by introducing a new function to the main.js:

Meteor.startup(() => {

  // all your configs, renders, and other stuff...

  function getLang () {

    return (

        navigator.languages && navigator.languages[0] ||

        navigator.language ||

        navigator.browserLanguage ||

        navigator.userLanguage ||

        'en-US'

    );

  }

  i18n.setLocale(getLang());

}

After everything is rendered on the screen, we set locale to an inferred one. Feel free to tweak this code to make sure that the inferred locale is actually supported by our app.

Displaying Product List

As long as we are creating a pseudo-shop, there should be some products, right? Let's render them in a separate component, just like we did with locales. Create a Product.js file inside the ui/imports folder:

import React, { Component } from 'react';

import Product from './Product.js';

export default class Products extends React.Component {

  getProducts() {

    return [

      {

        id: 1,

        name: "bag",

        price: 20

      },

      {

        id: 2,

        name: "sunglasses",

        price: 30

      }

    ]

  }

  renderProducts() {

    return this.getProducts().map((product) => (

      <Product key={product.id} product={product} />

    ));

  }

  render() {

    return (

      <ul>

        {this.renderProducts()}

      </ul>

    );

  }

}

We render an array of products that are returned by the getProducts method. In a real world you would probably fetch them from a Mongo database, but for the purposes of this demo it is not required.

Now code the Product.js file inside the ui/imports:

import React, { Component } from 'react';

import i18n from 'meteor/universe:i18n';

const T = i18n.createComponent('Common');

export default class Product extends Component {

  render() {

    return (

      <T _tagType='li'>

        {this.props.product.name}

      </T>

    );

  }

}

Note that here I am passing the Common namespace right to the createComponent() method, because I would like to dynamically perform translation based on the product's name. Alternatively, you may specify namespace as an attribute:

< T _namespace='Common' >

Now we need to provide translations for each product, and render our products:

import Products from '../imports/ui/Products.js';

Meteor.startup(() => {

  i18n.addTranslations('en', {

    Common: {

      welcome: 'Welcome to the Shop Fu!',

      bag: 'Stylish bag',

      sunglasses: 'Cool sunglasses'

    }

  });

  i18n.addTranslations('ru', {

    Common: {

      welcome: 'Добро пожаловать в Shop Fu!',

      bag: 'Модная сумка',

      sunglasses: 'Крутые очки'

    }

  });

  // ...

  render(<Products />, document.getElementById('products'));

  // ...

}

Don't forget to tweak main.html by adding:

< div id="products"></div >

Good! Now the products are translated properly, however, there is still one thing left: they don't have any price. Yeah, actually it is quite a major problem because the users are supposed to pay for the products, rather than receive them for free. Therefore, let's solve this task as well!

Updating Prices

Working with prices is a bit more complex than with products' names. Why? Well, because ideally we should also convert the prices to the local currency and display the proper currency symbol. For English locale that'll be dollars, but for Russian it should be roubles. Therefore we can't simply render the price inside the <T> tag — some other approach should be used. For example, we can add a data- attribute to the tag with price, and store the initial value (in dollars) there. When the locale is switched, we recalculate the price and display the correct currency symbol.

Start by modifying the Product.js file and adding a new <span> tag inside the <T>:

export default class Product extends Component {

  render() {

    return (

      <T _tagType='li'>

        {this.props.product.name}

        <span className="price" data-price={this.props.product.price}></span>

      </T>

    );

  }

}

And now what we can do is listen for the "locale changed" event and recalculate the prices. Here is the event listener inside the main.js:

Meteor.startup(() => {

  // ...

  i18n.onChangeLocale(function(newLocale){

    updatePrices(newLocale);

  });

  // ...

}

And here is the implementation of the updatePrice() function:

  function updatePrices(locale) {

    ratios = {

      "ru-RU": 63,

      "en-US": 1

    }

    $('.price').each(function() {

      let $this = $(this);

      let price = parseFloat($this.data('price')) * ratios[locale];

      $this.text(i18n.getCurrencySymbol(locale) + ' ' + i18n.parseNumber(price));

    });

  }

ratios is the object that we are going to use to convert price in dollars to price in another currency. 1 dollar currently equals about 63 roubles, that's why I've provided 63 for the ru-RU key.

Then we just fetch each .price element, get the initial value from the data-price attribute, parse it, and multiply by the ratio. After that use getCurrencySymbol() to display either $ or Р, and also parse the number with the help of parseNumber(). Pretty cool, eh?

Here I am taking advantage of jQuery, so hook it up inside the main.html file:

< head >

< script src="https://code.jquery.com/jquery-3.3.1.min.js"></script >

</ head>

This is it! Try switching between locales and make sure that both the product's name and its price are being updated properly.

Phrase and Translation Files

Working with translation files can be challenging, especially when your app is of bigger scope and supports many languages. You might easily miss some translations for a specific language, which can lead to confusion among users.

And so Phrase can make your life easier: Grab your 14-day trial today. Phrase supports many different languages and frameworks, including JavaScript of course. It allows you to easily import and export translation data. What’s even greater, you can quickly understand which translation keys are missing because it’s easy to lose track when working with many languages in big applications.

On top of that, you can collaborate with translators as it’s much better to have professionally done localization for your website.

Conclusion

In this article, we have discussed how to introduce internationalization support for Meteor applications powered by React. We have seen a meteor-universe-i18n library in action, added support for two languages, performed translations in various ways, added locale switcher, and also worked with events a bit. UniverseI18n has a bunch of other features available so be sure to browser its docs and read the official Meteor guide about I18n.

Hopefully, this article was interesting and useful for you! As always, thanks for staying with me and until the next time.