JavaScript app localization with i18next and Localazy
Tags: javascript, i18next, localazy, localization
Published: 2021-02-03 09:12:53

---
title: JavaScript app localization with i18next and Localazy
published: true
description: Localization is hardly the most favourite part of development for anyone. Regardless of whether it is a web, mobile, or desktop app, it is time-consuming and error-prone. This article shows a quick and seamless way of integrating the process into any JavaScript app.
tags: javascript,i18next,Localazy,localization
canonical_url: https://localazy.com/blog/javascript-app-localization-i18next-localazy
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/3jorcdabbc2ev925v3ns.png
---
Localization is hardly the most favourite part of development for anyone. Regardless of whether it is a web, mobile, or desktop app, it is time-consuming and error-prone. This article shows a quick and seamless way of integrating the process into any JavaScript app.

We won’t use any JavaScript framework only to stay focused on the sole purpose of this article. All code examples are supposed to demonstrate the general concept, so feel free to take any part of them and adapt it to your own app’s specific needs. 

## Why Localazy and i18next?
Localazy is a developer-focused localization management platform that supports a wide variety of frameworks and an even more comprehensive range of formats through its CLI. Localazy’s best features are a pro-active review process, highly accurate translation memory supported by community-shared translations, and a simple, crystal clear UI which is not bloated with rarely used options. 

As we’ll show shortly, it works seamlessly with i18next library, which is one of the most popular choices when it comes to internationalization in JavaScript apps. Thanks to that, managing translations in professional teams or even in open-source projects where anybody can contribute is a piece of cake 🍰.

## Create a new project
Let’s start with an empty project. We will use `npm` to manage dependencies, so make sure you have [Node.js](https://nodejs.org/en/) installed. You can verify the installation by running `npm -v`. 

```bash
mkdir localize-js-app && cd localize-js-app && npm init
```

After running the above command and answering the initial questions, a `package.json` file should be created. Let’s complete our empty project structure to look like the tree below. These should be all the files we’ll need initially.

```bash
.
├── index.html
├── package.json
├── src
│   ├── i18n.js
│   └── index.js
└── style.css
```

Paste the following content into your `index.html` file. This will be our app index, and we’re not going to touch it anymore, since we’re going to create all content dynamically with JavaScript.

```html
<!DOCTYPE html>
<html>
<head>
  <title>Localize Javascript App with Localazy</title>
  <link rel="stylesheet" href="style.css"/>
  <meta charset="UTF-8" />
</head>
<body>
  <div id="app"></div>
  <script src="src/index.js">
  </script>
</body>
</html>
```

To work with the project more conveniently, let’s install `parcel-bundler`  first.
If you don’t know [Parcel](https://parceljs.org/getting_started.html) yet, it’s a web application bundler, with a great developer experience. It offers blazing-fast performance and requires zero configuration. So basically with one package installed, you’ll get a development server with hot-reloading and production builds. Excellent for quick projects and rapid prototyping.

```bash
npm install -D parcel-bundler
```

You can then simplify running the Parcel bundler by adding a *run* and *build* task to the scripts section in your `package.json`. Once done, try to run the development server with  `npm start`.

```json
{
  "scripts": {
    "start": "parcel index.html --open",
    "build": "parcel build index.html"
  }
}
```

## Add source language
Create new `src/locales` folder next to our `index.js` file and put an `en.json` file inside it. This will be our base language for the app. 

```bash
.
├── index.html
├── package.json
├── src
│   ├── i18n.js
│   ├── index.js
│   └── locales
│       └── en.json
└── style.css
```

Let’s add a couple of simple translatable strings in English.

```json
{
  "hello_localazy": "Hello Localazy!",
  "calendar": "Calendar",
  "field": "Field",
  "event": "Event",
  "title": "Title",
  "color": "Color"
}
```

As a next step, we’re going to install the `i18next` library.  It will help us to quickly load all the locales we have,   get our content translated by some convinient methods it provides and eventually to implement a custom language switcher. We will get to it shortly. 

```bash
npm install i18next
```

To test that the library works for us, we need to import the base strings and initialize the `i18next` object.
Add the following content to the `i18n.js` file. 

```js
// i18n.js
import i18next from "i18next";
import enJson from "./locales/en.json";

export const i18n = i18next;

export const initI18n = (callback) => {
  i18next.init({
    lng: "en",
    debug: true,
    resources: {
      en: {
        translation: enJson,
      },
    },
  }, function (err, t) {
    // Call this function once i18next is initialized
    callback()
  });
}
```

And run the initializer in `index.js`.  We pass the `createPageContent` function as a callback when the initialization finishes. To test that the translations are loaded correctly, we simply set some text to the `#app`  content. Note that this is not an optimal approach and might get unmaintainable in bigger projects, but it should serve well for our demonstrating purposes. 
```js
// index.js
import { i18n, initI18n } from "./i18n";

const createPageContent = () => {
  document.querySelector("#app").innerHTML = i18n.t("hello_localazy");
}
initI18n(createPageContent);
```

Going back to the browser while having the server running, you should see the `Hello Localazy!` greeting to appear. Awesome, we have the basic setup working, but let’s get the phrases translated now. 
Now, that’s where the fun begins. In real-life projects, we don’t want to handle translations in JSON manually, it’s usually very tedious and error-prone. Also, we need some sort of a quality control mechanism, to ensure that the translations meet the required standards. With Localazy you get all of this plus automated translations as a bonus.

## Integrate with Localazy
Go to [Localazy signup](https://localazy.com/register) and create an account, then create a new app. I’m leaving it public, this way other people can freely contribute with their translations. Source language is English, and community translations enabled. They will help us to translate some parts of the app automatically.

![](https://directus.localazy.com/_/assets/create_app.png)

Now in your app, from the available integrations select JavaScript.  The setup steps you see there is a general-purpose quick start, but I’ll give you more precise instructions here, so read on. The only thing needed is to note down the `readKey` and `writeKey`, we will need them in a moment.

![](https://directus.localazy.com/_/assets/select_integration.png)

Now we need to configure our app to synchronize strings with Localazy. Go back to your app’s terminal and install [Localazy CLI](https://testing.localazy.com/docs/cli/the-basics).

```bash
npm install -D @localazy/cli
```

Then create a new file called `localazy.json` in your project root, paste the following content. This is a configuration for the CLI we’ve just installed.  Don’t forget to fill in the keys, you’ve noted earlier. If you would like to better understand the used configuration, go to the [Localazy CLI](https://localazy.com/docs/cli/the-basics) documentation, where you’ll find all the necessary details.

```json
// localazy.json
{
  "writeKey": "<your-write-key>",
  "readKey": "<your-read-key>", 
  "upload": {  
    "type": "json",
    "files": "src/locales/en.json"         
  },
  "download": {
    "files": "src/locales/${lang}.json"
  }
}
```

In `package.json`  add  `upload` and `download` tasks to the `"scripts"` section, right after the parcel commands. You can also add a `list`  task, to check what files are available on the platform, but we won’t need it in this article specifically. 

```json
{
  "scripts": {
    "start": "parcel index.html --open",
    "build": "parcel build index.html",
    "upload": "localazy upload",
    "download": "localazy download",
    "list": "localazy list"
  }
}
```

If set up correctly, you should be able to run `npm run upload` to upload strings. Note that you can always run the tasks with the `-s` option to simulate the action and verify that everything is configured correctly. No files will be uploaded or written then.

Now go back to your app at Localazy, you should see our base language (English) to appear.  If successful, try to add some new language. Some of the languages might be already translated by ShareTM technology.  Adding one of these languages will show you a review button, where you can approve the suggested phrases. If you add language without suggestions, you can start translating immediately with the help of classic machine translations. 

![](https://directus.localazy.com/_/assets/share_tm.png)

 After adding some languages and translating or approving a few phrases, go back to the app and run the download task as `npm run download`. You should see the new languages to appear immediately in the `locales` folder. Note that only approved phrases or phrases translated directly by you – the owner will be downloaded. 

As an example, I’ve added French and Czech and translated them fully.

![](https://directus.localazy.com/_/assets/Screenshot%202020-12-08%20at%2010.39.23.png)

Next step to use the downloaded translations. Open the `i18n.js` file again and import the new files like the following. I’m importing the JSON directly, and then I’m adding it to the resources object. To try out your app in a different language, simply set  `lng`  property to  `"fr"` for example. You should see the translated message to appear in your app.

```js
// i18n.js
import i18next from "i18next";
import enJson from "./locales/en.json";
import frJson from "./locales/fr.json";
import csJson from "./locales/cs.json";

export const i18n = i18next;

export const initI18n = (callback) => {
  i18next.init({
    lng: "en",
    debug: true,
    resources: {
      en: {
        translation: enJson,
      },
      fr: {
        translation: frJson,
      },
      cs: {
        translation: csJson,
      },
    },
  }, function (err, t) {
	// Call this function once i18next is initialized
    callback()
  });
}
```


We’ve just implemented the basic i18next functionality, and we’re able to translate our content. However, we don’t have an option to change the current language because it’s hardcoded. Let’s implement the language switcher as a next step. 

## Implement language switcher
Localazy CLI has an additional option called [metadata file](https://localazy.com/docs/cli/metadata-file), which lets you retrieve current languages metadata directly from Localazy. This enables you to quickly implement language switcher, by knowing exactly which languages will be available and what are their full localized names and other properties. The language metadata file is available in JavaScript, Typescript or JSON file format. We will use the JavaScript version since it also contains plurals resolver, which we will use later.  Let’s configure it in the `"download"` section in `localazy.json` file.

```json
{
  "download": {
    "files": "src/locales/${lang}.json",
    "metadataFileJs": "src/localazy-meta.js"
  }
}
```

Now run the `npm run download` command again, you should see a new `localazy-meta.js` file appear.  Let’s use the metadata file directly and add a few convenience methods to our `i18n.js` file for it.

```js
import i18next from "i18next";
import LocalazyMeta from './localazy-meta';
import enJson from "./locales/en.json";
import frJson from "./locales/fr.json";
import csJson from "./locales/cs.json";

export const i18n = i18next;

export const getSupportedLangCodes = () => {
  return LocalazyMeta.languages.map(l => l.language)
}

export const getBaseLanguage = () => {
  return LocalazyMeta.baseLocale;
}

export const getCurrentLanguage = () => {
  return window.localStorage.i18nextLng || 'en';
}

export const getLanguages = ()=>{
  return LocalazyMeta.languages;
}

export const initI18n = (callback) => {
  i18next.init({
    lng: getBaseLanguage(),
    fallbackLng: getBaseLanguage(),
    debug: true,
    supportedLngs: getSupportedLangCodes(),
    resources: {
      en: {
        translation: enJson,
      },
      fr: {
        translation: frJson,
      },
      cs: {
        translation: csJson,
      },
    },
  }, function (err, t) {
    callback()
  });
}
```

We will now create a really basic language switcher directly in our `index.js` file. 

```js
import { i18n, initI18n, getLanguages, getCurrentLanguage } from "./i18n";

const createLanguageSelector = () => {
  let template = '<select id="selector">';
  getLanguages().forEach(l => {
    template += `
    <option ${l.language === getCurrentLanguage() ? "selected" : ""} value="${l.language}">
      ${l.localizedName}
    </option>`
  })
  template += '</select>';
  return template;
}

const getTranslatedContent = () => {
  return i18n.t("hello_localazy");
}

const updateTranslatedContent = () => {
  document.querySelector("#content").innerHTML = getTranslatedContent();
}

const initPageContent = () => {
  document.querySelector("#app").innerHTML = `  
  ${createLanguageSelector()}   
  <div id="content">  
    ${getTranslatedContent()}
  </div>`;

  document.querySelector("#selector").addEventListener("change", (e) => {
    i18n.changeLanguage(e.target.value);
    updateTranslatedContent();
  })
}

initI18n(initPageContent);
```

I'm also adding a few lines of CSS to the `styles.css` file. Just enough to make the page okay to look at, but you can embrace your creativity here.

```css
#selector {
  width: 150px;
}
#content {
  margin-top: 1rem;
}
```

That’s all needed for a language switcher. Your app should now look like the following. 

![](https://directus.localazy.com/_/assets/Screenshot%202020-12-08%20at%2011.48.23.png)

## Add plurals support
Different languages come with various pluralization rules, and it’s one of the most challenging tasks to get right when localizing apps. Languages like French or English have more straightforward rules, a word can have either singular or plural form.  (also called as “one” and “other” ). This is not the case for Czech for example which has rules for “one” ,“few” and “other” quantities.  All possible forms for all common languages are well defined in the CLDR [Language Plural Rules](https://unicode-org.github.io/cldr-staging/charts/37/supplemental/language_plural_rules.html).

However, i18next supports plural rules for some languages, but unfortunately, it’s not the entire set. Moreover, i18next calls “one” form as “singular” which, in my opinion, creates confusion as it deviates from the CLDR standard.
Luckily, Localazy covers the entire specification automatically in the language metadata file, where it returns correct plural rules for languages which are part of the specification.

Let’s change some of our base phrases into plural forms in the `en.json` file. There are multiple ways of defining plurals in JSON. We will use an object notation, but you can read about all other options here: [File Format - JSON](https://localazy.com/docs/cli/json-format#plurals). 

```json
{
  "hello_localazy": "Hello Localazy!",
  "calendar": {
    "one": "Calendar",
    "other": "Calendars"
  },
  "field": {
    "one": "Field",
    "other": "Fields"
  },
  "title": {
    "one": "Title",
    "other": "Titles"
  },
  "event": {
    "one": "Event",
    "other": "Events"
  },
  "color": {
    "one": "Color",
    "other": "Colors"
  }
}
```

To enable that feature in the CLI, we need to also add  `plural_object` in the `"upload"` section of our `localazy.json` file.

```json
{
  "upload": {  
    "type": "json",
    "features": ["plural_object"],
    "files": "src/locales/en.json"    
  },
  "download": {
    "files": "src/locales/${lang}.json",
    "metadataFileJs": "src/localazy-meta.js"
  }
}
```

Now, try to run the `npm run upload` task again and go to the Localazy web interface. If you now try to translate  
some of the new phrases, you should see that all plurals are displayed in a slightly different interface, which accommodates according to each language plural rules as per the CLDR specification. 

![](https://directus.localazy.com/_/assets/plurals.png)

Try to add some example translations for the phrases and run `npm run download` task again. 
We will now add a plural helper into `i18n.js` file, which will resolve correct plural form for each language as defined in Localazy.

```js
// i18n.js
//...
export const getKeyPlural = (key, count) => {    
  const currentLanguage = LocalazyMeta.languages.find(l => l.language === i18next.language);
  const pluralType = currentLanguage.pluralType(+count);
  return `${key}.${pluralType}`;
}
//...
``` 

Then we will update the `getTranslatedContent` function in  `index.js` file to resolve our pluralized content. You can tweak the count variable to display different plural forms.

```js
// index.js
import { i18n, initI18n, getLanguages, getCurrentLanguage, getKeyPlural as p } from "./i18n";
let count = 0;
//...
const getTranslatedContent = () => {
  return `${i18n.t("hello_localazy")}<br><br>
          ${count}  ${i18n.t(p("calendar", count))}<br>
          ${count}  ${i18n.t(p("field", count))}<br>
          ${count}  ${i18n.t(p("event", count))}<br>
          ${count}  ${i18n.t(p("title", count))}<br>
          ${count}  ${i18n.t(p("color", count))}`;
}
//..
```
 
To improve the example a little bit, we will add a number input and update the count variable according to the input value, so we can test the functionality based on our input.  Here’s the final `index.js` file. 
```js
import { i18n, initI18n, getLanguages, getCurrentLanguage, getKeyPlural as p } from "./i18n";
let count = 0;

const createLanguageSelector = () => {
  let template = '<select id="selector">';
  getLanguages().forEach(l => {
    template += `
    <option ${l.language === getCurrentLanguage() ? "selected" : ""} value="${l.language}">
      ${l.localizedName}
    </option>`
  })
  template += '</select>';
  return template;
}

const getTranslatedContent = () => {
  return `${i18n.t("hello_localazy")}<br><br>
          ${count}  ${i18n.t(p("calendar", count))}<br>
          ${count}  ${i18n.t(p("field", count))}<br>
          ${count}  ${i18n.t(p("event", count))}<br>
          ${count}  ${i18n.t(p("title", count))}<br>
          ${count}  ${i18n.t(p("color", count))}`;
}

const updateTranslatedContent = () => {
  document.querySelector("#content").innerHTML = getTranslatedContent();
}

const initPageContent = () => {
  document.querySelector("#app").innerHTML = `  
  ${createLanguageSelector()}   
  <input id="count" type="number" placeholder="count" value="${count}" min="0"/>
  <div id="content">  
    ${getTranslatedContent()}
  </div>`;

  document.querySelector("#selector").addEventListener("change", (e) => {
    i18n.changeLanguage(e.target.value);
    updateTranslatedContent();
  })

  document.querySelector("#count").addEventListener("input", (e) => {
    if (e.target.value) {
      count = e.target.value;
      updateTranslatedContent();
    }
  })
}

initI18n(initPageContent);

```

And the final example will look like following. Changing the number should correctly update the plural form. 

![](https://directus.localazy.com/_/assets/Screenshot%202020-12-08%20at%2012.59.28.png)

## Closing words
I hope you’ve enjoyed this sneak peek on what Localazy can do for you and understood how easy it is to start localizing any JavaScript app with it.  If you would like to play around with the prototype, you can check it out on [CodeSandbox](https://codesandbox.io/s/localazy-javascript-i18n-example-imskh?file=/src/i18n.js). The whole git repository is available on [GitHub](https://github.com/localazy/javascript-i18next-example). If you have any questions, just join us on our [Discord](https://discord.gg/sQucDznuMp) for a chat.