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.
$(function () { // use plugins and options as needed, for options, detail see // https://www.i18next.com i18next // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector .use(i18nextBrowserLanguageDetector) // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ debug: true, fallbackLng: 'en', resources: { en: { translation: { // here we will place our translations... } } } }, (err, t) => { if (err) returnconsole.error(err);
// for options see // https://github.com/i18next/jquery-i18next#initialize-the-plugin jqueryI18next.init(i18next, $, { useOptionsAttr: true });
$(function () { // use plugins and options as needed, for options, detail see // https://www.i18next.com i18next // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector .use(i18nextBrowserLanguageDetector) // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ debug: true, fallbackLng: 'en', resources: { en: { translation: { intro: { title: 'Landing Page', subTitle: 'Some subtitle' } } }, de: { translation: { intro: { title: 'Webseite', subTitle: 'Ein Untertitel' } } } } }, (err, t) => { if (err) returnconsole.error(err);
// for options see // https://github.com/i18next/jquery-i18next#initialize-the-plugin jqueryI18next.init(i18next, $, { useOptionsAttr: true });
// fill language switcher Object.keys(lngs).map((lng) => { const opt = new Option(lngs[lng].nativeName, lng); if (lng === i18next.resolvedLanguage) { opt.setAttribute("selected", "selected"); } $('#languageSwitcher').append(opt); }); $('#languageSwitcher').change((a, b, c) => { const chosenLng = $(this).find("option:selected").attr('value'); i18next.changeLanguage(chosenLng, () => { rerender(); }); });
rerender(); }); });
🥳 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:
$(function () { // use plugins and options as needed, for options, detail see // https://www.i18next.com i18next // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector .use(i18nextBrowserLanguageDetector) // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ debug: true, fallbackLng: 'en', resources: { en: { translation: { head: { title: 'My Awesome Landing-Page', description: 'The description of this awesome landing page.' }, intro: { title: 'Landing Page', subTitle: 'Some subtitle' }, footer: { counter_one: 'Changed language just once', counter_other: 'Changed language already {{count}} times', date: 'It\'s {{date, LLLL}}' } } }, de: { translation: { head: { title: 'Meine grossartige Webseite', description: 'Die Beschreibung dieser grossartigen Webseite.' }, intro: { title: 'Webseite', subTitle: 'Ein Untertitel' }, footer: { counter_one: 'Die Sprache wurde erst ein mal gewechselt', counter_other: 'Die Sprache wurde {{count}} mal gewechselt', date: 'Es ist {{date, LLLL}}' } } } } }, (err, t) => { if (err) returnconsole.error(err);
// define the formatter function i18next.services.formatter.add('LLLL', (value, lng, options) => { return moment(value).locale(lng).format('LLLL'); });
// for options see // https://github.com/i18next/jquery-i18next#initialize-the-plugin jqueryI18next.init(i18next, $, { useOptionsAttr: true });
// fill language switcher Object.keys(lngs).map((lng) => { const opt = new Option(lngs[lng].nativeName, lng); if (lng === i18next.resolvedLanguage) { opt.setAttribute("selected", "selected"); } $('#languageSwitcher').append(opt); }); let languageChangedCounter = 0; $('#languageSwitcher').change((a, b, c) => { const chosenLng = $(this).find("option:selected").attr('value'); i18next.changeLanguage(chosenLng, () => { rerender(); // language changed message languageChangedCounter++; $('#languageChangedNotification').localize({ count: languageChangedCounter }) if (languageChangedCounter === 1) { $('#languageChangedNotification').show(); } }); });
rerender(); }); });
😎 Cool, now we have a language specific date formatting!
English:
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:
resources: { en: { translation: { // ... footer: { counter_one: 'Changed language just once', counter_other: 'Changed language already {{count}} times', date: 'It\'s {{date, LLLL}}', date_afternoon: 'Good afternoon! It\'s {{date, LLLL}}', date_evening: 'Good evening! Today was the {{date, LLLL}}', date_morning: 'Good morning! Today is {{date, LLLL}} | Have a nice day!' } } }, de: { translation: { // ... footer: { counter_one: 'Die Sprache wurde erst ein mal gewechselt', counter_other: 'Die Sprache wurde {{count}} mal gewechselt', date: 'Es ist {{date, LLLL}}', date_afternoon: 'Guten Tag! Es ist {{date, LLLL}}', date_evening: 'Guten Abend! Heute war {{date, LLLL}}', date_morning: 'Guten Morgen! Heute ist {{date, LLLL}} | Wünsche einen schönen Tag!' } } } }
😁 Yeah, It works!
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.
$(function () { // use plugins and options as needed, for options, detail see // https://www.i18next.com i18next // i18next-http-backend // loads translations from your server // https://github.com/i18next/i18next-http-backend .use(i18nextHttpBackend) // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector .use(i18nextBrowserLanguageDetector) // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ debug: true, fallbackLng: 'en' // i.e. if you want to customize a different translation path, // use the loadPath option: // backend: { // loadPath: '/assets/locales/{{lng}}/{{ns}}.json' // } }, (err, t) => { if (err) returnconsole.error(err);
// ... }); });
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 2 3 4
<divid="loader">Loading...</div> <divid="content"style="display: none;"> <!-- your real content --> </div>
1 2
$('#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?
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:
const locizeOptions = { projectId: '8d751621-323e-4bda-94c8-7d2368102e62', apiKey: '302aca54-2ea8-4b9f-b5f0-df1369c59427'// YOU should not expose your apps API key to production!!! };
$(function () { // use plugins and options as needed, for options, detail see // https://www.i18next.com i18next // i18next-locize-backend // loads translations from your project, saves new keys to it (saveMissing: true) // https://github.com/locize/i18next-locize-backend .use(i18nextLocizeBackend) // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector .use(i18nextBrowserLanguageDetector) // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ debug: true, fallbackLng: 'en', backend: locizeOptions }, (err, t) => { if (err) returnconsole.error(err);
// ... }); });
i18next-locize-backend offers a functionality to retrieve the available languages directly from locize, let's use it:
const locizeOptions = { projectId: '8d751621-323e-4bda-94c8-7d2368102e62', apiKey: '302aca54-2ea8-4b9f-b5f0-df1369c59427'// YOU should not expose your apps API key to production!!! };
$(function () { const locizeBackend = new i18nextLocizeBackend(locizeOptions, (err, opts, lngs) => { if (err) returnconsole.error(err);
// use plugins and options as needed, for options, detail see // https://www.i18next.com i18next // i18next-locize-backend // loads translations from your project, saves new keys to it (saveMissing: true) // https://github.com/locize/i18next-locize-backend .use(locizeBackend) // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector .use(i18nextBrowserLanguageDetector) // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ debug: true, fallbackLng: 'en', backend: locizeOptions }, (err, t) => { if (err) returnconsole.error(err);
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!
const locizeOptions = { projectId: '8d751621-323e-4bda-94c8-7d2368102e62', apiKey: '302aca54-2ea8-4b9f-b5f0-df1369c59427'// YOU should not expose your apps API key to production!!! };
i18next.on('editorSaved', rerender); // used for the inContext editor
$(function () { const locizeBackend = new i18nextLocizeBackend(locizeOptions, (err, opts, lngs) => { if (err) returnconsole.error(err);
// use plugins and options as needed, for options, detail see // https://www.i18next.com i18next // locize-editor // InContext Editor of locize .use(locize.locizePlugin) // locize-lastused (do not use this in production) // sets a timestamp of last access on every translation segment on locize // -> safely remove the ones not being touched for weeks/months // https://github.com/locize/locize-lastused .use(locizeLastUsed) // i18next-locize-backend // loads translations from your project, saves new keys to it (saveMissing: true) // https://github.com/locize/i18next-locize-backend .use(locizeBackend) // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector .use(i18nextBrowserLanguageDetector) // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ ...opts, debug: true, fallbackLng: 'en', backend: locizeOptions, locizeLastUsed: locizeOptions, saveMissing: true // interpolation: { // // legacy usage // format: (value, format, lng) => { // if (value instanceof Date) { // return moment(value).locale(lng).format(format); // } // return value; // } // } }, (err, t) => { if (err) returnconsole.error(err);