Blog
The progressive guide to jQuery internationalization (i18n) using i18next

The progressive guide to jQuery internationalization (i18n) using i18next

February 2, 2022
Blog Hero Image

Every web developer may have met the perennial Methuselah jQuery. Created back in January 2006 at BarCamp NYC by John Resig and currently maintained by a team of developers led by Timmy Willison.

You may think:

Why a blog post about the venerable but aged JavaScript library, that made things like HTML document traversal and manipulation, etc. easier?

Because with a combination of versatility and extensibility, jQuery has changed the way that millions of people write JavaScript!

And you can see this by the huge usage of jQuery:

Based on w3Techs web technology surveys, jQuery is used by 95.4% of all the websites whose JavaScript library they know. And 78.3% of all websites.

Checking the npm download trends of the jquery module it is approaching the 5 million downloads per week.

So you see, jQuery is not only still relevant, it takes up the majority of all websites.

Therefore, in this article, we will be using the i18next framework to internationalize a jQuery website.

TOC

So first of all: "Why i18next?"

When it comes to jQuery localization, one of the most popular is i18next with it's jQuery extension jquery-i18next, and for good reasons:

i18next was created in late 2011. It's older than most of the libraries you will use nowadays, including your main frontend technology (React, Angular, Vue, ...). Only jQuery is older 😉➡️ sustainable

Based on how long i18next already is available open source, there is no real i18n case that could not be solved with i18next.

➡️ mature

i18next can be used in any javascript (and a few non-javascript - .net, elm, iOS, android, ruby, ...) environment, with any UI framework, with any i18n format, ... the possibilities are endless.

➡️ extensible

There is a plenty of features and possibilities you'll get with i18next compared to other regular i18n frameworks.

➡️ rich

Here you can find more information about why i18next is special and how it works.

Let's get into it...

Prerequisites

Make sure you have a jQuery based website or web app. It's best, if you have some experience with simple HTML, JavaScript and basic jQuery, before jumping to jquery-i18next. This jQuery i18n example is not intended to be a jQuery beginner tutorial.

Getting started

Take your own jQuery project or create a new one.

I have here an awesome landing page 😉

We are going to adapt the website to detect the language according to the user’s preference. And we will create a language switcher to make the content change between different languages.

Let's install some i18next dependencies:

1<script src="https://cdn.jsdelivr.net/npm/i18next@21.6.10/i18next.min.js"></script>
2<script src="https://cdn.jsdelivr.net/npm/jquery-i18next@1.2.1/jquery-i18next.min.js"></script>
3<script src="https://cdn.jsdelivr.net/npm/i18next-browser-languagedetector@6.1.3/i18nextBrowserLanguageDetector.min.js"></script>
4

Let's prepare an i18n.js file:

1$(function () {
2  // use plugins and options as needed, for options, detail see
3  // https://www.i18next.com
4  i18next
5    // detect user language
6    // learn more: https://github.com/i18next/i18next-browser-languageDetector
7    .use(i18nextBrowserLanguageDetector)
8    // init i18next
9    // for all options read: https://www.i18next.com/overview/configuration-options
10    .init({
11      debug: true,
12      fallbackLng: 'en',
13      resources: {
14        en: {
15          translation: {
16            // here we will place our translations...
17          }
18        }
19      }
20    }, (err, t) => {
21      if (err) return console.error(err);
22
23      // for options see
24      // https://github.com/i18next/jquery-i18next#initialize-the-plugin
25      jqueryI18next.init(i18next, $, { useOptionsAttr: true });
26
27      // start localizing, details:
28      // https://github.com/i18next/jquery-i18next#usage-of-selector-function
29      $('body').localize();
30    });
31});

Let's load that file:

1<script src="https://cdn.jsdelivr.net/npm/i18next@21.6.10/i18next.min.js"></script>
2<script src="https://cdn.jsdelivr.net/npm/jquery-i18next@1.2.1/jquery-i18next.min.js"></script>
3<script src="https://cdn.jsdelivr.net/npm/i18next-browser-languagedetector@6.1.3/i18nextBrowserLanguageDetector.min.js"></script>
4
5<script src="js/i18n.js"></script>

Now let's try to move some hard coded text out to the translations.

1<!-- ... -->
2<h1 data-i18n="intro.title">Landing Page</h1>
3<h3 data-i18n="intro.subTitle">Some subtitle</h3>
4<!-- ... -->

Since the texts will be part of our translation resources, they could also be removed:

1<!-- ... -->
2<h1 data-i18n="intro.title"></h1>
3<h3 data-i18n="intro.subTitle"></h3>
4<!-- ... -->

The texts are now part of the translation resources:

1$(function () {
2  // use plugins and options as needed, for options, detail see
3  // https://www.i18next.com
4  i18next
5    // detect user language
6    // learn more: https://github.com/i18next/i18next-browser-languageDetector
7    .use(i18nextBrowserLanguageDetector)
8    // init i18next
9    // for all options read: https://www.i18next.com/overview/configuration-options
10    .init({
11      debug: true,
12      fallbackLng: 'en',
13      resources: {
14        en: {
15          translation: {
16            intro: {
17              title: 'Landing Page',
18              subTitle: 'Some subtitle'
19            }
20          }
21        }
22      }
23    }, (err, t) => {
24      if (err) return console.error(err);
25
26      // for options see
27      // https://github.com/i18next/jquery-i18next#initialize-the-plugin
28      jqueryI18next.init(i18next, $, { useOptionsAttr: true });
29
30      // start localizing, details:
31      // https://github.com/i18next/jquery-i18next#usage-of-selector-function
32      $('body').localize();
33    });
34});

Language Switcher

Now let's define a language switcher:

1<!-- ... -->
2<select name="language" id="languageSwitcher"></select>
3<!-- ... -->

And also add some translations for the new language:

1const lngs = {
2  en: { nativeName: 'English' },
3  de: { nativeName: 'Deutsch' }
4};
5
6const rerender = () => {
7  // start localizing, details:
8  // https://github.com/i18next/jquery-i18next#usage-of-selector-function
9  $('body').localize();
10}
11
12$(function () {
13  // use plugins and options as needed, for options, detail see
14  // https://www.i18next.com
15  i18next
16    // detect user language
17    // learn more: https://github.com/i18next/i18next-browser-languageDetector
18    .use(i18nextBrowserLanguageDetector)
19    // init i18next
20    // for all options read: https://www.i18next.com/overview/configuration-options
21    .init({
22      debug: true,
23      fallbackLng: 'en',
24      resources: {
25        en: {
26          translation: {
27            intro: {
28              title: 'Landing Page',
29              subTitle: 'Some subtitle'
30            }
31          }
32        },
33        de: {
34          translation: {
35            intro: {
36              title: 'Webseite',
37              subTitle: 'Ein Untertitel'
38            }
39          }
40        }
41      }
42    }, (err, t) => {
43      if (err) return console.error(err);
44
45      // for options see
46      // https://github.com/i18next/jquery-i18next#initialize-the-plugin
47      jqueryI18next.init(i18next, $, { useOptionsAttr: true });
48
49      // fill language switcher
50      Object.keys(lngs).map((lng) => {
51        const opt = new Option(lngs[lng].nativeName, lng);
52        if (lng === i18next.resolvedLanguage) {
53          opt.setAttribute("selected", "selected");
54        }
55        $('#languageSwitcher').append(opt);
56      });
57      $('#languageSwitcher').change((a, b, c) => {
58        const chosenLng = $(this).find("option:selected").attr('value');
59        i18next.changeLanguage(chosenLng, () => {
60          rerender();
61        });
62      });
63
64      rerender();
65    });
66});

jquery language switcher
jquery language switcher

🥳 Awesome, you've just created your first language switcher!

Thanks to i18next-browser-languagedetector now it tries to detect the browser language and automatically use that language if you've provided the translations for it. The manually selected language in the language switcher is persisted in the localStorage, next time you visit the page, that language is used as preferred language.

Translate head information

Let's translate also the title and description of the website. We do this by extending our rerender function, and adding the additional translation resources:

1const rerender = () => {
2  // start localizing, details:
3  // https://github.com/i18next/jquery-i18next#usage-of-selector-function
4  $('body').localize();
5
6  $('title').text($.t('head.title'))
7  $('meta[name=description]').attr('content', $.t('head.description'))
8}
9
10// ...
11
12resources: {
13  en: {
14    translation: {
15      head: {
16        title: 'My Awesome Landing-Page',
17        description: 'The description of this awesome landing page.'
18      },
19      intro: {
20        title: 'Landing Page',
21        subTitle: 'Some subtitle'
22      }
23    }
24  },
25  de: {
26    translation: {
27      head: {
28        title: 'Meine grossartige Webseite',
29        description: 'Die Beschreibung dieser grossartigen Webseite.'
30      },
31      intro: {
32        title: 'Webseite',
33        subTitle: 'Ein Untertitel'
34      }
35    }
36  }
37}

So you see, this can be also done with the $.t() helper function.

Let's check the DOM:

Nice 👍

Interpolation and Pluralization

i18next goes beyond just providing the standard i18n features. But for sure it's able to handle plurals and interpolation.

Let's count each time the language gets changed:

1<!-- ... -->
2<li>
3  <select name="language" id="languageSwitcher"></select>
4</li>
5<li id="languageChangedNotification" style="display: none;">
6  <i data-i18n="footer.counter" data-i18n-options='{ "count": 0 }'></i>
7</li>
8<!-- ... -->

Let's remember the count in the languageChangedCounter variable and increment it on each language change....and extending the translation resources:

1const lngs = {
2  en: { nativeName: 'English' },
3  de: { nativeName: 'Deutsch' }
4};
5
6const rerender = () => {
7  // start localizing, details:
8  // https://github.com/i18next/jquery-i18next#usage-of-selector-function
9  $('body').localize();
10
11  $('title').text($.t('head.title'))
12  $('meta[name=description]').attr('content', $.t('head.description'))
13}
14
15$(function () {
16  // use plugins and options as needed, for options, detail see
17  // https://www.i18next.com
18  i18next
19    // detect user language
20    // learn more: https://github.com/i18next/i18next-browser-languageDetector
21    .use(i18nextBrowserLanguageDetector)
22    // init i18next
23    // for all options read: https://www.i18next.com/overview/configuration-options
24    .init({
25      debug: true,
26      fallbackLng: 'en',
27      resources: {
28        en: {
29          translation: {
30            head: {
31              title: 'My Awesome Landing-Page',
32              description: 'The description of this awesome landing page.'
33            },
34            intro: {
35              title: 'Landing Page',
36              subTitle: 'Some subtitle'
37            },
38            footer: {
39              counter_one: 'Changed language just once',
40              counter_other: 'Changed language already {{count}} times'
41            }
42          }
43        },
44        de: {
45          translation: {
46            head: {
47              title: 'Meine grossartige Webseite',
48              description: 'Die Beschreibung dieser grossartigen Webseite.'
49            },
50            intro: {
51              title: 'Webseite',
52              subTitle: 'Ein Untertitel'
53            },
54            footer: {
55              counter_one: 'Die Sprache wurde erst ein mal gewechselt',
56              counter_other: 'Die Sprache wurde {{count}} mal gewechselt'
57            }
58          }
59        }
60      }
61    }, (err, t) => {
62      if (err) return console.error(err);
63
64      // for options see
65      // https://github.com/i18next/jquery-i18next#initialize-the-plugin
66      jqueryI18next.init(i18next, $, { useOptionsAttr: true });
67
68      // fill language switcher
69      Object.keys(lngs).map((lng) => {
70        const opt = new Option(lngs[lng].nativeName, lng);
71        if (lng === i18next.resolvedLanguage) {
72          opt.setAttribute("selected", "selected");
73        }
74        $('#languageSwitcher').append(opt);
75      });
76      let languageChangedCounter = 0;
77      $('#languageSwitcher').change((a, b, c) => {
78        const chosenLng = $(this).find("option:selected").attr('value');
79        i18next.changeLanguage(chosenLng, () => {
80          rerender();
81            
82          // language changed message
83          languageChangedCounter++;
84          $('#languageChangedNotification').localize({ count: languageChangedCounter })
85          if (languageChangedCounter === 1) {
86            $('#languageChangedNotification').show();
87          }
88        });
89      });
90
91      rerender();
92    });
93});

Based on the count value i18next will choose the correct plural form. Read more about pluralization and interpolation in the official i18next documentation.

jQuery pluralization
jQuery pluralization

💡 i18next is also able to handle languages with multiple plural forms, like arabic:

1// translation resources:
2{
3  "key_zero": "zero",
4  "key_one": "singular",
5  "key_two": "two",
6  "key_few": "few",
7  "key_many": "many",
8  "key_other": "other"
9}
10
11// usage:
12t('key', {count: 0}); // -> "zero"
13t('key', {count: 1}); // -> "singular"
14t('key', {count: 2}); // -> "two"
15t('key', {count: 3}); // -> "few"
16t('key', {count: 4}); // -> "few"
17t('key', {count: 5}); // -> "few"
18t('key', {count: 11}); // -> "many"
19t('key', {count: 99}); // -> "many"
20t('key', {count: 100}); // -> "other"

Formatting

Now, let’s check out how we can use different date formats with the help of i18next and moment.js to handle date and time.

<!-- ... -->
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/min/moment-with-locales.min.js"></script>
<!-- ... -->

We like to have the footer displaying the current date:

<!-- ... -->
<p id="footerMessage" class="text-muted small" data-i18n="footer.date"></p>
<!-- ... -->

Define a format function, like documented in the documentation and add the new translation key:

1const lngs = {
2  en: { nativeName: 'English' },
3  de: { nativeName: 'Deutsch' }
4};
5
6const rerender = () => {
7  // start localizing, details:
8  // https://github.com/i18next/jquery-i18next#usage-of-selector-function
9  $('body').localize();
10
11  $('title').text($.t('head.title'))
12  $('meta[name=description]').attr('content', $.t('head.description'))
13}
14
15$(function () {
16  // use plugins and options as needed, for options, detail see
17  // https://www.i18next.com
18  i18next
19    // detect user language
20    // learn more: https://github.com/i18next/i18next-browser-languageDetector
21    .use(i18nextBrowserLanguageDetector)
22    // init i18next
23    // for all options read: https://www.i18next.com/overview/configuration-options
24    .init({
25      debug: true,
26      fallbackLng: 'en',
27      resources: {
28        en: {
29          translation: {
30            head: {
31              title: 'My Awesome Landing-Page',
32              description: 'The description of this awesome landing page.'
33            },
34            intro: {
35              title: 'Landing Page',
36              subTitle: 'Some subtitle'
37            },
38            footer: {
39              counter_one: 'Changed language just once',
40              counter_other: 'Changed language already {{count}} times',
41              date: 'It\'s {{date, LLLL}}'
42            }
43          }
44        },
45        de: {
46          translation: {
47            head: {
48              title: 'Meine grossartige Webseite',
49              description: 'Die Beschreibung dieser grossartigen Webseite.'
50            },
51            intro: {
52              title: 'Webseite',
53              subTitle: 'Ein Untertitel'
54            },
55            footer: {
56              counter_one: 'Die Sprache wurde erst ein mal gewechselt',
57              counter_other: 'Die Sprache wurde {{count}} mal gewechselt',
58              date: 'Es ist {{date, LLLL}}'
59            }
60          }
61        }
62      }
63    }, (err, t) => {
64      if (err) return console.error(err);
65
66      // define the formatter function
67      i18next.services.formatter.add('LLLL', (value, lng, options) => {
68        return moment(value).locale(lng).format('LLLL');
69      });
70
71      // for options see
72      // https://github.com/i18next/jquery-i18next#initialize-the-plugin
73      jqueryI18next.init(i18next, $, { useOptionsAttr: true });
74
75      // fill language switcher
76      Object.keys(lngs).map((lng) => {
77        const opt = new Option(lngs[lng].nativeName, lng);
78        if (lng === i18next.resolvedLanguage) {
79          opt.setAttribute("selected", "selected");
80        }
81        $('#languageSwitcher').append(opt);
82      });
83      let languageChangedCounter = 0;
84      $('#languageSwitcher').change((a, b, c) => {
85        const chosenLng = $(this).find("option:selected").attr('value');
86        i18next.changeLanguage(chosenLng, () => {
87          rerender();
88            
89          // language changed message
90          languageChangedCounter++;
91          $('#languageChangedNotification').localize({ count: languageChangedCounter })
92          if (languageChangedCounter === 1) {
93            $('#languageChangedNotification').show();
94          }
95        });
96      });
97
98      rerender();
99    });
100});

😎 Cool, now we have a language specific date formatting!

English:

jQuery english
jQuery english

German:

jQuery german
jQuery german

Context

What about a specific greeting message based on the current day time? i.e. morning, evening, etc. This is possible thanks to the context feature of i18next.

Let's create a getGreetingTime function and use the result as context information for our footer translation.And add some context specific translations keys:

1// ...
2
3const getGreetingTime = () => {
4  const split_afternoon = 12; // 24hr time to split the afternoon
5  const split_evening = 17; // 24hr time to split the evening
6  const currentHour = moment().hour();
7
8  if (currentHour >= split_afternoon && currentHour <= split_evening) {
9    return 'afternoon';
10  } else if (currentHour >= split_evening) {
11    return 'evening';
12  }
13  return 'morning';
14}
15
16const rerender = () => {
17  // start localizing, details:
18  // https://github.com/i18next/jquery-i18next#usage-of-selector-function
19  $('body').localize();
20  $('#footerMessage').localize({ context: getGreetingTime() });
21  $('title').text($.t('head.title'))
22  $('meta[name=description]').attr('content', $.t('head.description'))
23}
24
25// ...
26
27resources: {
28  en: {
29    translation: {
30      // ...
31      footer: {
32        counter_one: 'Changed language just once',
33        counter_other: 'Changed language already {{count}} times',
34        date: 'It\'s {{date, LLLL}}',
35        date_afternoon: 'Good afternoon! It\'s {{date, LLLL}}',
36        date_evening: 'Good evening! Today was the {{date, LLLL}}',
37        date_morning: 'Good morning! Today is {{date, LLLL}} | Have a nice day!'
38      }
39    }
40  },
41  de: {
42    translation: {
43      // ...
44      footer: {
45        counter_one: 'Die Sprache wurde erst ein mal gewechselt',
46        counter_other: 'Die Sprache wurde {{count}} mal gewechselt',
47        date: 'Es ist {{date, LLLL}}',
48        date_afternoon: 'Guten Tag! Es ist {{date, LLLL}}',
49        date_evening: 'Guten Abend! Heute war {{date, LLLL}}',
50        date_morning: 'Guten Morgen! Heute ist {{date, LLLL}} | Wünsche einen schönen Tag!'
51      }
52    }
53  }
54}

😁 Yeah, It works!

jQuery translations
jQuery translations

Separate translations from code

Having the translations in our i18n.js file works, but is not that suitable to work with, for translators. Let's separate the translations from the code and pleace them in dedicated json files.

Because this is a web application, i18next-http-backend will help us to do so.

<script src="https://cdn.jsdelivr.net/npm/i18next-http-backend@1.3.2/i18nextHttpBackend.min.js"></script>

Create a locales folder and move the translations there:

public locales
public locales

Adapt the i18n.js file to use the i18next-http-backend:

1// ...
2
3$(function () {
4  // use plugins and options as needed, for options, detail see
5  // https://www.i18next.com
6  i18next
7    // i18next-http-backend
8    // loads translations from your server
9    // https://github.com/i18next/i18next-http-backend
10    .use(i18nextHttpBackend)
11    // detect user language
12    // learn more: https://github.com/i18next/i18next-browser-languageDetector
13    .use(i18nextBrowserLanguageDetector)
14    // init i18next
15    // for all options read: https://www.i18next.com/overview/configuration-options
16    .init({
17      debug: true,
18      fallbackLng: 'en'
19      // i.e. if you want to customize a different translation path,
20      // use the loadPath option:
21      // backend: {
22      //   loadPath: '/assets/locales/{{lng}}/{{ns}}.json'
23      // }
24    }, (err, t) => {
25      if (err) return console.error(err);
26
27      // ...
28    });
29});

Now the translations are loaded asynchronously, so it may be the UI will refresh a bit later, as soon as the translations are loaded. To optimize this behaviour, you can show some sort of loading indicator until the i18next is initialized.

Something like:

1<div id="loader">Loading...</div>
2<div id="content" style="display: none;">
3  <!-- your real content -->
4</div>

$('#loader').hide();
$('#content').show();

Now your app looks still the same, but your translations are separated.

If you want to support a new language, you just create a new folder and a new translation json file. This gives you the possibility to send the translations to some translators. Or if you're working with a translation management system you can just synchronize the files with a cli.

Better translation management

By sending the translations to some translators or translator agency you have more control and a direct contact with them. But this also means more work for you. This is a traditional way. But be aware sending files around creates always an overhead.

Does a better option exist?

For sure!

i18next helps to get the application translated, and this is great - but there is more to it.

  • How do you integrate any translation services / agency
  • How do you keep track of new or removed content?
  • How do you handle proper versioning?
  • How do you deploy translation changes without deploying your complete application?
  • and a lot more...

Looking for something like this❓

transform the localization process
transform the localization process

How does this look like?

First you need to signup at locize and login. Then create a new project in locize and add your translations. You can add your translations either by using the cli or by importing the individual json files or via API.

Done so, we're going to replace i18next-http-backend with i18next-locize-backend.

<!-- ... -->
<script src="https://cdn.jsdelivr.net/npm/i18next-locize-backend@4.2.8/i18nextLocizeBackend.min.js"></script>
<!-- ... -->

After having imported the translations to locize, delete the locales folder and adapt the i18n.js file to use the i18next-locize-backend and make sure you copy the project-id and api-key from within your locize project:

1// ...
2
3const locizeOptions = {
4  projectId: '8d751621-323e-4bda-94c8-7d2368102e62',
5  apiKey: '302aca54-2ea8-4b9f-b5f0-df1369c59427' // YOU should not expose your apps API key to production!!!
6};
7
8$(function () {
9  // use plugins and options as needed, for options, detail see
10  // https://www.i18next.com
11  i18next
12    // i18next-locize-backend
13    // loads translations from your project, saves new keys to it (saveMissing: true)
14    // https://github.com/locize/i18next-locize-backend
15    .use(i18nextLocizeBackend)
16    // detect user language
17    // learn more: https://github.com/i18next/i18next-browser-languageDetector
18    .use(i18nextBrowserLanguageDetector)
19    // init i18next
20    // for all options read: https://www.i18next.com/overview/configuration-options
21    .init({
22      debug: true,
23      fallbackLng: 'en',
24      backend: locizeOptions
25    }, (err, t) => {
26      if (err) return console.error(err);
27
28      // ...
29    });
30});

i18next-locize-backend offers a functionality to retrieve the available languages directly from locize, let's use it:

1// ...
2
3const locizeOptions = {
4  projectId: '8d751621-323e-4bda-94c8-7d2368102e62',
5  apiKey: '302aca54-2ea8-4b9f-b5f0-df1369c59427' // YOU should not expose your apps API key to production!!!
6};
7
8$(function () {
9  const locizeBackend = new i18nextLocizeBackend(locizeOptions, (err, opts, lngs) => {
10    if (err) return console.error(err);
11
12    // use plugins and options as needed, for options, detail see
13    // https://www.i18next.com
14    i18next
15      // i18next-locize-backend
16      // loads translations from your project, saves new keys to it (saveMissing: true)
17      // https://github.com/locize/i18next-locize-backend
18      .use(locizeBackend)
19      // detect user language
20      // learn more: https://github.com/i18next/i18next-browser-languageDetector
21      .use(i18nextBrowserLanguageDetector)
22      // init i18next
23      // for all options read: https://www.i18next.com/overview/configuration-options
24      .init({
25        debug: true,
26        fallbackLng: 'en',
27        backend: locizeOptions
28      }, (err, t) => {
29        if (err) return console.error(err);
30
31        // new usage
32        i18next.services.formatter.add('LLLL', (value, lng, options) => {
33          return moment(value).locale(lng).format('LLLL');
34        });
35
36        // for options see
37        // https://github.com/i18next/jquery-i18next#initialize-the-plugin
38        jqueryI18next.init(i18next, $, { useOptionsAttr: true });
39
40        // fill language switcher
41        // with the lngs retrieved directly from locize...
42        Object.keys(lngs).map((lng) => {
43          const opt = new Option(lngs[lng].nativeName, lng);
44          if (lng === i18next.resolvedLanguage) {
45            opt.setAttribute("selected", "selected");
46          }
47          $('#languageSwitcher').append(opt);
48        });
49        let languageChangedCounter = 0;
50        $('#languageSwitcher').change((a, b, c) => {
51          const chosenLng = $(this).find("option:selected").attr('value');
52          i18next.changeLanguage(chosenLng, () => {
53            rerender();
54            
55            // language changed message
56            languageChangedCounter++;
57            $('#languageChangedNotification').localize({ count: languageChangedCounter })
58            if (languageChangedCounter === 1) {
59              $('#languageChangedNotification').show();
60            }
61          });
62        });
63
64        rerender();
65
66        $('#loader').hide();
67        $('#content').show();
68      });
69  });
70});

Now the translations are served directly from the locize CDN. The jQuery i18n has now CDN superpower 😁.

save missing translations

Thanks to the use of the saveMissing functionality, new keys gets added to locize automatically, while developing the app.

Just pass saveMissing: true in the i18next options:

1// ...
2.init({
3  debug: true,
4  fallbackLng: 'en',
5  backend: locizeOptions,
6  saveMissing: true // do not enable it on production
7}, (err, t) => {
8// ...

Each time you'll use a new key, it will be sent to locize, i.e.:

<div data-i18n="new.key">this will be added automatically</div>

will result in locize like this:

missing key
missing ke

👀 but there's more...

Thanks to the locize-lastused plugin, you'll be able to find and filter in locize which keys are used or not used anymore.

With the help of the locize plugin, you'll be able to use your app within the locize InContext Editor..

Lastly, with the help of the auto-machinetranslation workflow and the use of the saveMissing functionality, new keys not only gets added to locize automatically, while developing the app, but are also automatically translated into the target languages using machine translation.

Check out this video to see how the automatic machine translation workflow looks like!

1<!-- ... -->
2<script src="https://cdn.jsdelivr.net/npm/locize-lastused@3.0.13/locizeLastUsed.min.js"></script>
3<script src="https://cdn.jsdelivr.net/npm/locize@2.2.4/locize.min.js"></script>
4<!-- ... -->

use them in i18n.js:

1const getGreetingTime = () => {
2  const split_afternoon = 12; // 24hr time to split the afternoon
3  const split_evening = 17; // 24hr time to split the evening
4  const currentHour = moment().hour();
5
6  if (currentHour >= split_afternoon && currentHour <= split_evening) {
7    return 'afternoon';
8  } else if (currentHour >= split_evening) {
9    return 'evening';
10  }
11  return 'morning';
12}
13
14const rerender = () => {
15  // start localizing, details:
16  // https://github.com/i18next/jquery-i18next#usage-of-selector-function
17  $('body').localize();
18  $('#footerMessage').localize({ context: getGreetingTime() });
19  $('title').text($.t('head.title'))
20  $('meta[name=description]').attr('content', $.t('head.description'))
21}
22
23const locizeOptions = {
24  projectId: '8d751621-323e-4bda-94c8-7d2368102e62',
25  apiKey: '302aca54-2ea8-4b9f-b5f0-df1369c59427' // YOU should not expose your apps API key to production!!!
26};
27
28i18next.on('editorSaved', rerender); // used for the inContext editor
29
30$(function () {
31  const locizeBackend = new i18nextLocizeBackend(locizeOptions, (err, opts, lngs) => {
32    if (err) return console.error(err);
33
34    // use plugins and options as needed, for options, detail see
35    // https://www.i18next.com
36    i18next
37      // locize-editor
38      // InContext Editor of locize
39      .use(locize.locizePlugin)
40      // locize-lastused (do not use this in production)
41      // sets a timestamp of last access on every translation segment on locize
42      // -> safely remove the ones not being touched for weeks/months
43      // https://github.com/locize/locize-lastused
44      .use(locizeLastUsed)
45      // i18next-locize-backend
46      // loads translations from your project, saves new keys to it (saveMissing: true)
47      // https://github.com/locize/i18next-locize-backend
48      .use(locizeBackend)
49      // detect user language
50      // learn more: https://github.com/i18next/i18next-browser-languageDetector
51      .use(i18nextBrowserLanguageDetector)
52      // init i18next
53      // for all options read: https://www.i18next.com/overview/configuration-options
54      .init({
55        ...opts,
56        debug: true,
57        fallbackLng: 'en',
58        backend: locizeOptions,
59        locizeLastUsed: locizeOptions,
60        saveMissing: true
61        // interpolation: {
62        //   // legacy usage
63        //   format: (value, format, lng) => {
64        //     if (value instanceof Date) {
65        //       return moment(value).locale(lng).format(format);
66        //     }
67        //     return value;
68        //   }
69        // }
70      }, (err, t) => {
71        if (err) return console.error(err);
72
73        // new usage
74        i18next.services.formatter.add('LLLL', (value, lng, options) => {
75          return moment(value).locale(lng).format('LLLL');
76        });
77
78        // for options see
79        // https://github.com/i18next/jquery-i18next#initialize-the-plugin
80        jqueryI18next.init(i18next, $, { useOptionsAttr: true });
81
82        // fill language switcher
83        Object.keys(lngs).map((lng) => {
84          const opt = new Option(lngs[lng].nativeName, lng);
85          if (lng === i18next.resolvedLanguage) {
86            opt.setAttribute("selected", "selected");
87          }
88          $('#languageSwitcher').append(opt);
89        });
90        let languageChangedCounter = 0;
91        $('#languageSwitcher').change((a, b, c) => {
92          const chosenLng = $(this).find("option:selected").attr('value');
93          i18next.changeLanguage(chosenLng, () => {
94            rerender();
95            
96            // language changed message
97            languageChangedCounter++;
98            $('#languageChangedNotification').localize({ count: languageChangedCounter })
99            if (languageChangedCounter === 1) {
100              $('#languageChangedNotification').show();
101            }
102          });
103        });
104
105        rerender();
106
107        $('#loader').hide();
108        $('#content').show();
109      });
110  });
111});

Automatic machine translation:

missing key auto
missing key auto

Last used translations filter:

i18next last used
i18next last used

InContext Editor:

i18next incontext
i18next incontext

Now, during development, you'll continue to save missing keys and to make use of lastused feature.

And in production environment, you should disable or remove the saveMissing and lastused functionality, and also the api-key should not exposed.

Caching:

i18next caching
i18next caching

Merging versions:

overwrite version
overwrite version

🧑‍💻 The complete code can be found here.

Check also the code integration part in this YouTube video.

There's also an i18next crash course video.

🎉🥳 Congratulations 🎊🎁

I hope you’ve learned a few new things about i18next, jQuery localization and modern localization workflows.

So if you want to take your i18n topic to the next level, it's worth trying the localization management platform - locize.

The founders of locize are also the creators of i18next. So by using locize you directly support the future of i18next.

👍

Share Now:
Follow Us: