{"id":15100,"date":"2022-09-14T07:00:43","date_gmt":"2022-09-14T07:00:43","guid":{"rendered":"https:\/\/phraseapp.com\/blog\/?p=1503"},"modified":"2023-09-26T13:48:27","modified_gmt":"2023-09-26T11:48:27","slug":"step-step-guide-javascript-localization","status":"publish","type":"post","link":"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/","title":{"rendered":"The Ultimate Guide to JavaScript Localization"},"content":{"rendered":"<p>Weird, maturing, arguably expressive, and amazing, JavaScript is the <a href=\"https:\/\/insights.stackoverflow.com\/survey\/2020#technology-programming-scripting-and-markup-languages-professional-developers\">most used programming language today<\/a>. Being the language of the browser\u2014and putting in work on the server with <a href=\"https:\/\/nodejs.org\/en\/\">Node<\/a>\u2014JavaScript is all over today\u2019s web stacks.<br \/>\nAnd with many multiplatform mobile frameworks, desktop wrappers, game engines, and even internet-of-things (IoT) frameworks, it\u2019s really JavaScript\u2019s world\u2014we just live in it.<br \/>\nNow, of course, you\u2019re here because you want to take all that \ud83d\udd25 and learn to localize JavaScript applications, enabling them for a global audience. Have no fear: This guide will cover everything you need to know to start browser JavaScript localization.<br \/>\nLet\u2019s rock <code>&amp;&amp;<\/code> roll.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Get all the code accompanying this article from our <a href=\"https:\/\/github.com\/PhraseApp-Blog\/javascript-l10n-ultimate-guide\">GitHub repo<\/a>.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> Internet Explorer (IE), with a <a href=\"https:\/\/kinsta.com\/browser-market-share\/\">2.15% global market share<\/a>, can be considered a legacy browser. For brevity, we\u2019re omitting IE-specific solutions in this guide. If you <em>are<\/em> supporting IE, be sure to check whether the built-in JavaScript features we\u2019re covering in this article require forks or polyfills.<\/p>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_69_1 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Overview<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#how-do-i-localize-a-web-page-with-javascript\" title=\"How do I localize a web page with JavaScript?\">How do I localize a web page with JavaScript?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#loading-translations-asynchronously\" title=\"Loading translations asynchronously\">Loading translations asynchronously<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#creating-a-locale-switcher\" title=\"Creating a locale switcher\">Creating a locale switcher<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#detecting-the-users-preferred-locales-from-the-browser\" title=\"Detecting the user\u2019s preferred locales from the browser\">Detecting the user\u2019s preferred locales from the browser<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#handling-direction-right-to-left-and-right-to-left-languages\" title=\"Handling direction: right-to-left and right-to-left languages\">Handling direction: right-to-left and right-to-left languages<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#basic-translation-messages\" title=\"Basic translation messages\">Basic translation messages<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#interpolation\" title=\"Interpolation\">Interpolation<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#translating-dynamically-after-page-load\" title=\"Translating dynamically after page load\">Translating dynamically after page load<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#plurals\" title=\"Plurals\">Plurals<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#number-formatting\" title=\"Number formatting\">Number formatting<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#date-formatting\" title=\"Date formatting\">Date formatting<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#what-are-some-good-javascript-i18n-libraries-i-can-use\" title=\"What are some good JavaScript i18n libraries I can use?\">What are some good JavaScript i18n libraries I can use?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#how-do-i-localize-a-web-page-with-polyglot\" title=\"How do I localize a web page with Polyglot?\">How do I localize a web page with Polyglot?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#installation\" title=\"Installation\">Installation<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#basic-translations\" title=\"Basic translations\">Basic translations<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#async-translation-file-loading\" title=\"Async translation file loading\">Async translation file loading<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#language-switcher\" title=\"Language switcher\">Language switcher<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#interpolation-2\" title=\"Interpolation\">Interpolation<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-19\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#plurals-2\" title=\"Plurals\">Plurals<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-20\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#how-do-i-localize-a-web-page-with-i18next\" title=\"How do I localize a web page with i18next?\">How do I localize a web page with i18next?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-21\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#installation-2\" title=\"Installation\">Installation<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-22\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#basic-translation-messages-2\" title=\"Basic translation messages\">Basic translation messages<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-23\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#async-translation-loading\" title=\"Async translation loading\">Async translation loading<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-24\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#supported-locales-and-fallback\" title=\"Supported locales and fallback\">Supported locales and fallback<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-25\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#automatically-detecting-the-users-locale\" title=\"Automatically detecting the user\u2019s locale\">Automatically detecting the user\u2019s locale<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-26\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#language-switcher-2\" title=\"Language switcher\">Language switcher<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-27\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#interpolation-3\" title=\"Interpolation\">Interpolation<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-28\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#plurals-3\" title=\"Plurals\">Plurals<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-29\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#how-do-i-localize-a-react-angular-or-vue-app\" title=\"How do I localize a React, Angular, or Vue app?\">How do I localize a React, Angular, or Vue app?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-30\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#angular-localization-articles\" title=\"Angular localization articles\">Angular localization articles<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-31\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#vuejs-localization-articles\" title=\"Vue.js localization articles\">Vue.js localization articles<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-32\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#localization-articles-for-other-frameworks\" title=\"Localization articles for other frameworks\">Localization articles for other frameworks<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-33\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#how-do-i-localize-a-react-app-with-i18next\" title=\"How do I localize a React App with i18next?\">How do I localize a React App with i18next?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-34\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#library-installation\" title=\"Library installation\">Library installation<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-35\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#basic-translations-2\" title=\"Basic translations\">Basic translations<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-36\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#async-translation-file-loading-2\" title=\"Async translation file loading\">Async translation file loading<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-37\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#language-switcher-3\" title=\"Language switcher\">Language switcher<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-38\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#react-localization-articles\" title=\"React localization articles\">React localization articles<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-39\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#how-do-i-localize-a-web-page-with-jquery-and-i18next\" title=\"How do I localize a web page with jQuery and i18next?\">How do I localize a web page with jQuery and i18next?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-40\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#installation-3\" title=\"Installation\">Installation<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-41\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#basic-translations-3\" title=\"Basic translations\">Basic translations<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-42\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#async-translation-file-loading-3\" title=\"Async translation file loading\">Async translation file loading<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-43\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#language-switcher-4\" title=\"Language switcher\">Language switcher<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-44\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#interpolation-4\" title=\"Interpolation\">Interpolation<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-45\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#plurals-4\" title=\"Plurals\">Plurals<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-46\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#how-do-i-localize-a-web-page-with-the-icu-format-using-globalize\" title=\"How do I localize a web page with the ICU format using Globalize?\">How do I localize a web page with the ICU format using Globalize?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-47\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#installation-4\" title=\"Installation\">Installation<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-48\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#basic-translations-4\" title=\"Basic translations\">Basic translations<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-49\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#handling-missing-message-errors\" title=\"Handling missing message errors\">Handling missing message errors<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-50\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#async-translation-file-loading-4\" title=\"Async translation file loading\">Async translation file loading<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-51\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#language-switcher-5\" title=\"Language switcher\">Language switcher<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-52\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#interpolation-5\" title=\"Interpolation\">Interpolation<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-53\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#plurals-5\" title=\"Plurals\">Plurals<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-54\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/#wrapping-up-our-javascript-localization-guide\" title=\"Wrapping up our JavaScript localization guide\">Wrapping up our JavaScript localization guide<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-a-web-page-with-javascript\"><\/span>How do I localize a web page with JavaScript?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>While it might be tempting to grab an off-the-shelf internationalization (i18n) library for your localization needs\u2014and that might in fact be the right choice for your project\u2014you\u2019ll find that vanilla JavaScript can do you just fine for smaller projects. Rolling your own will also give you a nice cookbook of i18n techniques that you can use with <em>any<\/em> library you choose.<\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em><i> <\/i>Our article, <a href=\"https:\/\/phrase.com\/blog\/posts\/i18n-a-simple-definition\/\">What Is I18n: A Simple Definition of Internationalization<\/a>, goes into more detail regarding what internationalization (i18n) and localization (l10n) are.<\/p>\n<p>\u270b\ud83c\udffd <em>Heads up \u00bb<\/em> If you\u2019re building a traditional MPA (multi-page application), it\u2019s often the case that a lot of the localization happens on the server itself. We\u2019re only working with browser localization here. We&#8217;ve got you covered server-side, though, with a <a href=\"https:\/\/phrase.com\/blog\/posts\/nodejs-tutorial-on-creating-multilingual-web-app\/\">Node i18n tutorial<\/a> and a <a href=\"https:\/\/phrase.com\/blog\/posts\/full-stack-javascript-i18n\/\">full-stack JavaScript i18n guide<\/a>.<\/p>\n<p>Alright, let\u2019s say we have a page we want to localize.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"html\" data-enlighter-group=\"e17c768d-5eb9-41e9-a82c-c8758e6dff71\" data-enlighter-title=\"index.html\" data-enlighter-linenumbers=\"false\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;meta charset=\"UTF-8\"&gt;\n  &lt;!-- ... --&gt;\n  &lt;title&gt;My Appy Apperson&lt;\/title&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;h1&gt;My Appy Apperson&lt;\/h1&gt;\n    &lt;p&gt;Welcome to my little spot on the interwebs!&lt;\/p&gt;\n  &lt;\/div&gt;\n  &lt;script src=\"js\/scripts.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15564 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/vanilla-en.png\" alt=\"English version of a JavaScript demo app | Phrase\" width=\"998\" height=\"316\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/vanilla-en.png 998w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/vanilla-en-300x95.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/vanilla-en-768x243.png 768w\" sizes=\"(max-width: 998px) 100vw, 998px\" \/><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> You can get all the code for the app we&#8217;re building in this section from the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/javascript-l10n-ultimate-guide\/tree\/main\/vanilla\">vanilla folder in our GitHub repo<\/a>.<br \/>\n\ud83d\udd17 <em>Resource \u00bb<\/em> I\u2019m using the skeletal <a href=\"http:\/\/getskeleton.com\/\">Skeleton CSS<\/a> library in case you were wondering.<\/p>\n<p>This looks OK, but it isn&#8217;t exactly global-ready, is it? All the content is hard-coded in English. Let\u2019s do some basic i18n here.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-highlight=\"10,11\" data-enlighter-group=\"9ae201e0-a675-4f5e-8ebb-1c941d5f6f4b\" data-enlighter-title=\"index.html\" data-enlighter-linenumbers=\"false\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;meta charset=\"UTF-8\"&gt;\n  &lt;!-- ... --&gt;\n  &lt;title&gt;My Appy Apperson&lt;\/title&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;h1 data-i18n-key=\"app-title\"&gt;My Appy Apperson&lt;\/h1&gt;\n    &lt;p data-i18n-key=\"lead\"&gt;Welcome to my little spot on the interwebs!&lt;\/p&gt;\n  &lt;\/div&gt;\n  &lt;script src=\"js\/scripts.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>Note the <code>data-i18n-key<\/code> attributes we added to our text containers above. We can tap into these when the document loads and replace their text with translations. In fact, let&#8217;s do just that.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"eb3e63cb-cd04-47cf-a52d-41474533e624\" data-enlighter-title=\"\/js\/scripts.js\">\/\/ The active locale\nconst locale = \"en\";\n\/\/ We can have as many locales here as we want,\n\/\/ and use any locales we want. We have English\n\/\/ and Arabic as locales here as examples.\nconst translations = {\n  \/\/ English translations\n  \"en\": {\n    \"app-title\": \"My Appy Apperson\",\n    \"lead\": \"Welcome to my little spot on the interwebs!\",\n  },\n  \/\/ Arabic translations\n  \"ar\": {\n    \"app-title\": \"\u062a\u0637\u0628\u064a\u0642\u064a \u0627\u0644\u0645\u0637\u0628\u0642\",\n    \"lead\": \"\u0623\u0647\u0644\u0627\u064b \u0628\u0643 \u0641\u064a \u0645\u0643\u0627\u0646\u064a \u0627\u0644\u0635\u063a\u064a\u0631 \u0639\u0644\u0649 \u0627\u0644\u0646\u062a.\",\n  },\n};\n\/\/ When the page content is ready...\ndocument.addEventListener(\"DOMContentLoaded\", () =&gt; {\n  document\n    \/\/ Find all elements that have the key attribute\n    .querySelectorAll(\"[data-i18n-key]\")\n    .forEach(translateElement);\n});\n\/\/ Replace the inner text of the given HTML element\n\/\/ with the translation in the active locale,\n\/\/ corresponding to the element's data-i18n-key\nfunction translateElement(element) {\n  const key = element.getAttribute(\"data-i18n-key\");\n  const translation = translations[locale][key];\n  element.innerText = translation;\n}\n<\/pre>\n<p>With this in place, let&#8217;s change the second line above to <code>const locale = \"ar\";<\/code> and reload the page. When the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Window\/DOMContentLoaded_event\">DOMContentLoaded event<\/a> is triggered, our page takes on our Arabic translations.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15566 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/vanilla-ar.png\" alt=\"Arabic version of a JavaScript demo app | Phrase\" width=\"572\" height=\"272\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/vanilla-ar.png 572w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/vanilla-ar-300x143.png 300w\" sizes=\"(max-width: 572px) 100vw, 572px\" \/><\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> <code>\"en\"<\/code> and <code>\"ar\"<\/code> above are the <a href=\"https:\/\/www.loc.gov\/standards\/iso639-2\/php\/code_list.php\">ISO 639-1 codes<\/a> for English and Arabic, respectively. It\u2019s standard to use ISO codes for <a href=\"https:\/\/unicode-org.github.io\/icu\/userguide\/locale\/#language-code\">languages<\/a> and <a href=\"https:\/\/unicode-org.github.io\/icu\/userguide\/locale\/#country-code\">countries<\/a> when localizing.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"loading-translations-asynchronously\"><\/span>Loading translations asynchronously<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>We\u2019re off to a good start with our i18n solution. However, adding locales and translations doesn\u2019t scale well at the moment. As our app grows, we&#8217;d probably want to split our translations into separate, per-locale files. The translation file corresponding to the active locale could then be loaded without the cost of loading the other locales. We can implement this without too much effort.<br \/>\nFirst, let\u2019s move our translations out of our main script and into JSON files, one for each locale we support.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"6063d19b-b975-4367-8a9f-2be85e5d7770\" data-enlighter-title=\"\/lang\/en.json\">{\n  \"app-title\": \"My Appy Apperson\",\n  \"lead\": \"Welcome to my little spot on the interwebs!\"\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"c6570b0b-a294-414a-b00a-5ab4e1103c72\" data-enlighter-title=\"\/lang\/ar.json\">{\n  \"app-title\": \"\u062a\u0637\u0628\u064a\u0642\u064a \u0627\u0644\u0645\u0637\u0628\u0642\",\n  \"lead\": \"\u0623\u0647\u0644\u0627\u064b \u0628\u0643 \u0641\u064a \u0645\u0643\u0627\u0646\u064a \u0627\u0644\u0635\u063a\u064a\u0631 \u0639\u0644\u0649 \u0627\u0644\u0646\u062a.\"\n}\n<\/pre>\n<p>Now let\u2019s rework our script so that we load the JSON files asynchronously when needed.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"eecf242f-182d-40ed-bc04-f77f3246859a\" data-enlighter-title=\"\/js\/scripts.js\" data-enlighter-highlight=\"2,5,8,13,18,19,20,21,22,23,24,25,26,27,28,32,33,34,35,40-44,51\">\/\/ The locale our app first shows\nconst defaultLocale = \"en\";\n\/\/ The active locale\nlet locale;\n\/\/ Gets filled with active locale translations\nlet translations = {};\n\/\/ When the page content is ready...\ndocument.addEventListener(\"DOMContentLoaded\", () =&gt; {\n  \/\/ Translate the page to the default locale\n  setLocale(defaultLocale);\n});\n\/\/ Load translations for the given locale and translate\n\/\/ the page to this locale\nasync function setLocale(newLocale) {\n  if (newLocale === locale) return;\n  const newTranslations =\n    await fetchTranslationsFor(newLocale);\n  locale = newLocale;\n  translations = newTranslations;\n  translatePage();\n}\n\/\/ Retrieve translations JSON object for the given\n\/\/ locale over the network\nasync function fetchTranslationsFor(newLocale) {\n  const response = await fetch(`\/lang\/${newLocale}.json`);\n  return await response.json();\n}\n\/\/ Replace the inner text of each element that has a\n\/\/ data-i18n-key attribute with the translation corresponding\n\/\/ to its data-i18n-key\nfunction translatePage() {\n  document\n    .querySelectorAll(\"[data-i18n-key]\")\n    .forEach(translateElement);\n}\n\/\/ Replace the inner text of the given HTML element\n\/\/ with the translation in the active locale,\n\/\/ corresponding to the element's data-i18n-key\nfunction translateElement(element) {\n  const key = element.getAttribute(\"data-i18n-key\");\n  const translation = translations[key];\n  element.innerText = translation;\n}\n<\/pre>\n<p>If we reload our page now, it looks exactly as it did before. However, under the hood, we\u2019ve made our app a lot more scalable and maintainable.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> We use the handy <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Fetch_API\">Fetch API<\/a> built into modern browsers to grab our JSON files via the network.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"creating-a-locale-switcher\"><\/span>Creating a locale switcher<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Our users have no way to make use of our awesomely asynchronous abilities as of yet. Shall we build a language-switching dropdown for them?<br \/>\nWe\u2019ll add a navbar and house our switcher in said navbar.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-highlight=\"9,18,19,20,21,24\" data-enlighter-group=\"ffdcbc91-e87f-487d-b549-2a88f94eb91a\" data-enlighter-title=\"index.html\" data-enlighter-linenumbers=\"false\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;!-- ... --&gt;\n  &lt;title&gt;My Appy Apperson&lt;\/title&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;nav class=\"navbar\"&gt;\n      &lt;div class=\"container\"&gt;\n        &lt;ul class=\"navbar-list navbar-left\"&gt;\n          &lt;!-- Nav links --&gt;\n        &lt;\/ul&gt;\n        &lt;div class=\"navbar-right\"&gt;\n          &lt;!-- ... --&gt;\n          &lt;select data-i18n-switcher class=\"locale-switcher\"&gt;\n            &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n            &lt;option value=\"ar\"&gt;Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)&lt;\/option&gt;\n          &lt;\/select&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/nav&gt;\n    &lt;h1 data-i18n-key=\"app-title\"&gt;My Appy Apperson&lt;\/h1&gt;\n    &lt;p data-i18n-key=\"lead\"&gt;Welcome to my little spot on the interwebs!&lt;\/p&gt;\n  &lt;\/div&gt;\n  &lt;script src=\"js\/scripts.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>A simple <code>&lt;select&gt;<\/code> can do us here. We can use a <code>data-i18n-switcher<\/code> attribute to hook into from our JavaScript and load the user-selected locale.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"f468dc96-a9e9-47c3-84a4-e59ac95a6d68\" data-enlighter-title=\"\/js\/scripts.js\" data-enlighter-highlight=\"10,18,19,20,21,22,23,24,25,26,27,28\">const defaultLocale = \"en\";\nlet locale;\n\/\/ ...\n\/\/ When the page content is ready...\ndocument.addEventListener(\"DOMContentLoaded\", () =&gt; {\n  setLocale(defaultLocale);\n  bindLocaleSwitcher(defaultLocale);\n});\n\/\/ ...\n\/\/ Whenever the user selects a new locale, we\n\/\/ load the locale's translations and update\n\/\/ the page\nfunction bindLocaleSwitcher(initialValue) {\n  const switcher =\n    document.querySelector(\"[data-i18n-switcher]\");\n  switcher.value = initialValue;\n  switcher.onchange = (e) =&gt; {\n    \/\/ Set the locale to the selected option[value]\n    setLocale(e.target.value);\n  };\n}\n<\/pre>\n<p>The <code>onchange<\/code> event handler allows us to update our page\u2019s translations according to the value of the selected <code>&lt;option&gt;<\/code>. Et voila. Our site visitor can now select their own locale.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15567 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/locale-switcher.gif\" alt=\"Demo app with onchange event handler | Phrase\" width=\"600\" height=\"204\" \/><\/p>\n<p>\ud83d\udce3 <em>Shout out \u00bb<\/em> to <a href=\"https:\/\/thenounproject.com\/search\/?q=translation&amp;i=4380109%0A\">Hary Murdiono JS from the Noun Project<\/a> for his <em>translate<\/em> icon.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"detecting-the-users-preferred-locales-from-the-browser\"><\/span>Detecting the user\u2019s preferred locales from the browser<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Sometimes it\u2019s a good idea to make a guess at the user\u2019s preferred locale before giving them the option to manually select their own. Most people have their browser UI set in their language of choice, often the operating system language.<br \/>\nThis browser UI language can be found in the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Navigator\/language\">navigator<\/a> object, specifically the standard in <code>navigator.language<\/code> string.<br \/>\nThe also-standard\u2014if experimental as I write this\u2014<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Navigator\/languages\">navigator.languages<\/a> array should contain the UI language as its first entry, in addition to any languages the user has explicitly set in her browser\u2019s preferred languages setting.<br \/>\nA little function that queries <code>navigator.languages<\/code> can get us going with browser locale detection.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"905d80b4-e5a7-4a8e-ba58-54f9ba5f8ee9\" data-enlighter-title=\"\/js\/scripts.js\">\/**\n * Retrieve user-preferred locales from the browser\n *\n * @param {boolean} languageCodeOnly - when true, returns\n * [\"en\", \"fr\"] instead of [\"en-US\", \"fr-FR\"]\n * @returns array | undefined\n *\/\nfunction browserLocales(languageCodeOnly = false) {\n  return navigator.languages.map((locale) =&gt;\n    languageCodeOnly ? locale.split(\"-\")[0] : locale,\n  );\n}\n<\/pre>\n<p>Now let\u2019s say the user has French (Canada) and Chinese (Simplified) in their browser settings.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15568 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/browser-locale-prefs-1024x605.png\" alt=\"Browser locale settings | Phrase\" width=\"1024\" height=\"605\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/browser-locale-prefs-1024x605.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/browser-locale-prefs-300x177.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/browser-locale-prefs-768x454.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/browser-locale-prefs.png 1438w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><br \/>\nIn this case, <code>browserLocales()<\/code> will return <code>[\"fr-CA\", \"zh-CN\"]<\/code>. If we call <code>browserLocales(true)<\/code>, we\u2019ll get <code>[\"fr\", \"zh\"]<\/code> instead.<br \/>\nWe can now use this new function to detect the user\u2019s preferred locales when we first load our page.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"72b255d6-6feb-43d4-8437-db90b0dbc34e\" data-enlighter-title=\"\/js\/scripts.js\" data-enlighter-highlight=\"4,9,10,12,14,19,20,21,25,26,27\">\/\/ The locale our app first shows\nconst defaultLocale = \"en\";\nconst supportedLocales = [\"en\", \"ar\"];\n\/\/ ...\ndocument.addEventListener(\"DOMContentLoaded\", () =&gt; {\n  const initialLocale =\n    supportedOrDefault(browserLocales(true));\n  setLocale(initialLocale);\n  bindLocaleSwitcher(initialLocale);\n});\n\/\/ ...\nfunction isSupported(locale) {\n  return supportedLocales.indexOf(locale) &gt; -1;\n}\n\/\/ Retrieve the first locale we support from the given\n\/\/ array, or return our default locale\nfunction supportedOrDefault(locales) {\n  return locales.find(isSupported) || defaultLocale;\n}\n\/\/ ...\nfunction browserLocales(languageCodeOnly = false) {\n  return navigator.languages.map((locale) =&gt;\n    languageCodeOnly ? locale.split(\"-\")[0] : locale,\n  );\n}\n<\/pre>\n<p>Notice that we\u2019ve introduced the concept of <code>supportedLocales<\/code>; these are the only locales that we have translations for. With them, we can fall back on our default locale if none of the user\u2019s preferred locales are in our supported list.<br \/>\nOur app will now be translated to the first locale in the user\u2019s preferred list, with an elegant fallback.<\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> We cover locale detection on both the browser and server in depth in <a href=\"https:\/\/phrase.com\/blog\/posts\/detecting-a-users-locale\/\">Detecting browser language preference with JavaScript<\/a>.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"handling-direction-right-to-left-and-right-to-left-languages\"><\/span>Handling direction: right-to-left and right-to-left languages<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Arabic, Hebrew, Persian, Urdu, and other languages <a href=\"https:\/\/www.worldatlas.com\/articles\/which-languages-are-written-from-right-to-left.html\">use scripts that are written right to left<\/a>. While left-to-right (LTR) languages far outnumber right-to-left (RTL) ones, it\u2019s good to know how to support the latter. Luckily, a lot of the work is done by the browser here; we just have to set the <code>&lt;html dir&gt;<\/code> attribute in our pages.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"6a86b7f1-65ce-4b15-891b-0425324892e3\" data-enlighter-title=\"\/js\/scripts.js\" data-enlighter-highlight=\"16,26,27,28\">\/\/ ...\n\/\/ Load translations for the given locale and translate\n\/\/ the page to this locale\nasync function setLocale(newLocale) {\n  if (newLocale === locale) return;\n  const newTranslations = await fetchTranslationsFor(\n    newLocale,\n  );\n  locale = newLocale;\n  translations = newTranslations;\n  \/\/ Set &lt;html dir&gt; attribute\n  document.documentElement.dir = dir(newLocale);\n  \/\/ Not necessary for direction flow, but for good measure...\n  document.documentElement.lang = newLocale;\n  translatePage();\n}\n\/\/ ...\nfunction dir(locale) {\n  return locale === \"ar\" ? \"rtl\" : \"ltr\";\n}\n\/\/ ...\n<\/pre>\n<p>The <code>&lt;html dir&gt;<\/code> attribute can take the values <code>\"ltr\"<\/code> or <code>\"rtl\"<\/code>. We provide this value via a very simple <code>dir()<\/code> function and set the attribute whenever we switch locales.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15569 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/dom-lang-dir.png\" alt=\"Browser dev tool html attributes | Phrase\" width=\"322\" height=\"208\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/dom-lang-dir.png 322w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/dom-lang-dir-300x194.png 300w\" sizes=\"(max-width: 322px) 100vw, 322px\" \/><br \/>\n<em>If we open our browser dev tools, we can see the <code>&lt;html&gt;<\/code> attributes updating as we switch languages<\/em><\/p>\n<p>The browser will flow the document right to left automatically when we set <code>&lt;html dir=\"rtl\"&gt;<\/code>. However, any of our custom directional styles, e.g. <code>margin-left: 20px<\/code>, will require some flipped, RTL-specific CSS. That\u2019s generally not too tricky; it\u2019s just a bit out of the scope of this article.<\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> Read more about localized CSS in <a href=\"https:\/\/phrase.com\/blog\/posts\/how-do-i-use-a-css-file-for-site-localization\/\">How Do I Use a CSS File for Site Localization?<\/a><\/p>\n<p>With our new code in place, we get an Arabic page Avicenna would approve!<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15570 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/ar-rtl-1024x348.png\" alt=\"Demo app with right alignment Arabic text | Phrase\" width=\"1024\" height=\"348\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/ar-rtl-1024x348.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/ar-rtl-300x102.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/ar-rtl-768x261.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/ar-rtl.png 1330w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"basic-translation-messages\"><\/span>Basic translation messages<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Before we move on to more complex messages, like those with interpolated values and plurals, let\u2019s briefly revisit how we&#8217;ve implemented our translation messages.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">\/\/ In our HTML page\n&lt;h1 data-i18n-key=\"app-title\"&gt;My Appy Apperson&lt;\/h1&gt;\n\/\/ In our JavaScript\nfunction translateElement(element) {\n  const key = element.getAttribute(\"data-i18n-key\");\n  const translation = translations[key];\n  element.innerText = translation;\n}\n\/\/ Given that we've loaded Arabic translations from ar.json:\ntranslations = {\n  \"app-title\": \"\u062a\u0637\u0628\u064a\u0642\u064a \u0627\u0644\u0645\u0637\u0628\u0642\",\n};\ntranslateElement(document.querySelector(\"[data-i18n-key='lead']\"));\n\/\/ renders to:\n&lt;h1 data-i18n-key=\"app-title\"&gt;\u062a\u0637\u0628\u064a\u0642\u064a \u0627\u0644\u0645\u0637\u0628\u0642&lt;\/h1&gt;\n<\/pre>\n<p>That\u2019s our translation system in a nutshell.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"interpolation\"><\/span>Interpolation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>What happens when we have values that change at runtime and need to be injected into our messages? A common example is the name of the currently logged in user. We\u2019ll have to update our translation system to handle cases like these.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-highlight=\"12,13,14,15,16,17\" data-enlighter-group=\"269a8ae8-8a14-4a95-9863-a481f6ae89f3\" data-enlighter-title=\"index.html\" data-enlighter-linenumbers=\"false\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;!-- ... --&gt;\n    &lt;h1 data-i18n-key=\"app-title\"&gt;My Appy Apperson&lt;\/h1&gt;\n    &lt;p\n      data-i18n-key=\"lead\"\n      data-i18n-opt='{\"username\": \"Swoodesh\"}'\n    &gt;\n      Welcome to my little spot on the interwebs, {username}!\n    &lt;\/p&gt;\n  &lt;\/div&gt;\n  &lt;script src=\"js\/scripts.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>We indicate placeholders for values we want to interpolate in our messages with the <code>{variable}<\/code> syntax. A new <code>data-i18n-opt<\/code> attribute stores interpolation key\/value pairs in a valid JSON object.<br \/>\nOf course, we\u2019ll need the placeholders in our language files.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"f78af90a-df90-420c-a3c7-7a289dcede90\" data-enlighter-title=\"\/lang\/en.json\">{\n  \"lead\": \"Welcome to my little spot on the interwebs, {username}!\",\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"9082479b-aff1-4962-b14e-d522963e7eb8\" data-enlighter-title=\"\/lang\/ar.json\">{\n  \"lead\": \"\u0623\u0647\u0644\u0627\u064b \u0628\u0643 \u0641\u064a \u0645\u0643\u0627\u0646\u064a \u0627\u0644\u0635\u063a\u064a\u0631 \u0639\u0644\u0649 \u0627\u0644\u0646\u062a \u064a\u0627 {username}.\",\n}\n<\/pre>\n<p>Now we can modify our <code>translateElement<\/code> function to handle interpolations.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"13c0b59f-dfdf-45cf-8d56-911e8a61422e\" data-enlighter-title=\"\/js\/scripts.js\" data-enlighter-highlight=\"7,8,9,11,12,13,18,19,20,21,22,23,24,25,26,27\">\/\/ ...\nfunction translateElement(element) {\n  const key = element.getAttribute(\"data-i18n-key\");\n  const translation = translations[key];\n  const options = JSON.parse(\n    element.getAttribute(\"data-i18n-opt\")\n  );\n  element.innerText = options\n    ? interpolate(translation, options)\n    : translation;\n}\n\/\/ Convert a message like \"Hello, {name}\" to \"Hello, Chad\"\n\/\/ given the interpolations object {name: \"Chad\"}\nfunction interpolate(message, interpolations) {\n  return Object.keys(interpolations).reduce(\n    (interpolated, key) =&gt;\n      interpolated.replace(\n        new RegExp(`{\\s*${key}\\s*}`, \"g\"),\n        interpolations[key],\n      ),\n    message,\n  );\n}\n\/\/ ...\n<\/pre>\n<p>If we detect a <code>data-i18n-opt<\/code> attribute on the element given to <code>translateElement()<\/code>, we run its translated message through a new <code>interpolate()<\/code> function before updating the element. Now when we load our page, we see the message with the value interpolated.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15571 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/interpolation-en-1024x348.png\" alt=\"Demo app with interpolation in English locale | Phrase\" width=\"1024\" height=\"348\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/interpolation-en-1024x348.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/interpolation-en-300x102.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/interpolation-en-768x261.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/interpolation-en.png 1330w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15572 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/interpolation-ar-1024x348.png\" alt=\"Demo app with interpolation in Arabic locale | Phrase\" width=\"1024\" height=\"348\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/interpolation-ar-1024x348.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/interpolation-ar-300x102.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/interpolation-ar-768x261.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/interpolation-ar.png 1330w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><br \/>\nOf course, having static values in the HTML is of limited use to us. Ideally, we want to be able to interpolate dynamically with JavaScript. That\u2019s not too hard to code up.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"translating-dynamically-after-page-load\"><\/span>Translating dynamically after page load<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Let\u2019s extract a general translation function from <code>translateElement()<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"6597c244-ae4d-4095-a22e-3f67cadd8248\" data-enlighter-title=\"\/js\/scripts.js\" data-enlighter-highlight=\"6,7,9,12,13,14\">\/\/ ...\nfunction translateElement(element) {\n  const key = element.getAttribute(\"data-i18n-key\");\n  const options =\n    JSON.parse(element.getAttribute(\"data-i18n-opt\")) || {};\n  element.innerText = translate(key, options);\n}\nfunction translate(key, interpolations = {}) {\n  return interpolate(translations[key], interpolations);\n}\n\/\/ ...\n<\/pre>\n<p>We\u2019ve simply extracted the code that handles retrieving a message in the active locale, with interpolations, to a new <code>translate()<\/code> function. We can now use this function to update an element\u2019s translation after page load. Let\u2019s say we want to update our lead copy after the user logs in. No big deal.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">const element =\n  document.querySelector(\"[data-i18n-key='lead']\");\n\/\/ Our new function is serving us well here\nelement.innerText =\n  translate(\"lead\", { username: \"Maggie\" });\n\/\/ Store the updated interpolations in the document\n\/\/ in case the element is re-rendered in the future\nelement.setAttribute(\n  \"data-i18n-opt\",\n  JSON.stringify({ username: \"Maggie\" }),\n);\n<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15573 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/dynamic-interpolation-1024x348.png\" alt=\"Demo app with dynamic English translation | Phrase\" width=\"1024\" height=\"348\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/dynamic-interpolation-1024x348.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/dynamic-interpolation-300x102.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/dynamic-interpolation-768x261.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/dynamic-interpolation.png 1330w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><br \/>\nNow we can update elements\u2019 translations at any time from our JavaScript.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"plurals\"><\/span>Plurals<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Not only do we often need to present different messages based on a counter\u2014like \u201c1 follower\u201d or \u201c20,000 follower<em>s<\/em>\u201d\u2014<a href=\"https:\/\/unicode-org.github.io\/cldr-staging\/charts\/latest\/supplemental\/language_plural_rules.html\">different languages have different plural rules<\/a>. While English has two plural forms: <em>one<\/em> and <em>other<\/em>, Arabic has six plural forms, for example. Historically, this meant that implementing plural support for front-end apps wasn\u2019t very easy. Luckily, the now-<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/PluralRules\">standard Intl.PluralRules object<\/a> makes quick work of handling plurals.<br \/>\nLet\u2019s say we\u2019re prolific penmen, and we want to let the world know how many articles we\u2019ve indeed penned.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">&lt;p\n  data-i18n-key=\"article-plural\"\n  data-i18n-opt='{\"count\": 122}'\n&gt;\n  {count} articles written and counting.\n&lt;\/p&gt;\n<\/pre>\n<p>Note that we\u2019re using a convention of ending our plural message key with <code>-plural<\/code>. And, of course, we need a required <code>count<\/code> integer to select the correct plural form. Speaking of plural forms, let\u2019s add them.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"75fdda27-47a8-4750-9436-e1a6d3120c1f\" data-enlighter-title=\"\/lang\/en.json\">{\n  \/\/ English has two plural forms\n  \"article-plural\": {\n    \"one\": \"{count} article and counting\",\n    \"other\": \"{count} articles and counting\"\n  }\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"f45a1ae7-53e1-4816-85d4-c79c3bd740d8\" data-enlighter-title=\"\/lang\/ar.json\">{\n  \/\/ Arabic has six plural forms\n  \"article-plural\": {\n    \"zero\": \"\u0644\u0627 \u062a\u0648\u062c\u062f \u0645\u0642\u0627\u0644\u0627\u062a\",\n    \"one\": \"\u0645\u0642\u0627\u0644 {count}\",\n    \"two\": \"\u0645\u0642\u0627\u0644\u0627\u0646\",\n    \"few\": \"{count} \u0645\u0642\u0627\u0644\u0627\u062a\",\n    \"many\": \"{count} \u0645\u0642\u0627\u0644\",\n    \"other\": \"{count} \u0645\u0642\u0627\u0644\"\n  }\n}\n<\/pre>\n<p>Now let\u2019s update our <code>translate<\/code> function to handle plural messages.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"62d15884-1e5b-4035-b937-4091ed4ac4b9\" data-enlighter-title=\"\/js\/scripts.js\" data-enlighter-highlight=\"6,7,8,9,10,11,26,27,28,29,30\">\/\/ ...\nfunction translate(key, interpolations = {}) {\n  const message = translations[key];\n  if (key.endsWith(\"-plural\")) {\n    return interpolate(\n      pluralFormFor(message, interpolations.count),\n      interpolations,\n    );\n  }\n  return interpolate(message, interpolations);\n}\n\/\/ ...\n\/*\n  Given a forms object like\n  {\n    \"zero\": \"No articles\",\n    \"one\": \"One article\",\n    \"other\": \"{count} articles\"\n  } and a count of 3, returns \"3 articles\"\n*\/\nfunction pluralFormFor(forms, count) {\n  const matchingForm = new Intl.PluralRules(locale).select(count);\n  return forms[matchingForm];\n}\n\/\/ ...\n<\/pre>\n<p>The magic sauce here is the bit that reads <code>new Intl.PluralRules(locale).select(...)<\/code>. The built-in <code>Intl.PluralRules<\/code> object, given a locale, knows that locale\u2019s plural rules. For example, passing <code>\"ar\"<\/code> to the constructor, then calling <code>select(5)<\/code> on the returned object, returns <code>\"few\"<\/code>\u2014the correct form here.<br \/>\nSo with very few lines of code, we have fully globalized plural support \ud83d\ude4c<\/p>\n<p style=\"text-align: center;\" data-wp-editing=\"1\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15574 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/plural-forms-1024x372.png\" alt=\"Pluralization in Arabic and English | Phrase\" width=\"1024\" height=\"372\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/plural-forms-1024x372.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/plural-forms-300x109.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/plural-forms-768x279.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/plural-forms.png 1229w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><em>Our plural message rendered in English and Arabic, given different counts<\/em><\/p>\n<h3><span class=\"ez-toc-section\" id=\"number-formatting\"><\/span>Number formatting<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Three hundred thousand Euros is \u201c\u20ac300,000.00\u201d in English (United States), \u201c300.000,00 \u20ac\u201d in German (Germany), \u201c\u20ac3,00,000.00\u201d in Hindi (Indian)\u2014notice the commas in that last one\u2014and \u201c\u0663\u0660\u0660\u066c\u0660\u0660\u0660\u066b\u0660\u0660 \u20ac\u201d in Arabic (Egypt). How do we manage all these formats? No worries; another <code>Intl<\/code> object that&#8217;s part of the modern JavaScript standard is just what the doctor ordered. <code>Intl.NumberFormat<\/code> to the rescue!<br \/>\nAspiring entrepreneurs that we are, say we want to start an NFT tracking website, with numerical stats, of course.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">&lt;p\n  data-i18n-key=\"nyan-cat-price\"\n  data-i18n-opt='{\"price\": {\"number\" : 5300}}'\n&gt;\n  Nyan Cat (Official) NFT: {price}\n&lt;\/p&gt;\n<\/pre>\n<p>Our <code>data-i18n-opt<\/code> identifies the number value of the <code>{price}<\/code> in our locale files. We\u2019ll be looking for that <code>number<\/code> key when we update our interpolation code in a second. First, let\u2019s provide our message translations.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"15136bc5-7049-41a1-a400-c01c52f8df32\" data-enlighter-title=\"\/lang\/en.json\">{\n  \"nyan-cat-price\": \"Nyan Cat (Official) NFT: {price}\"\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"8be65390-6187-4d54-bf60-d3d7fb8bcf39\" data-enlighter-title=\"\/lang\/ar.json\">{\n  \"nyan-cat-price\": \"\u0646\u064a\u0627\u0646 \u0643\u0627\u062a NFT: {price}\"\n}\n<\/pre>\n<p>OK, let\u2019s update our JavaScript to get this working.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"eebd5a57-2ac6-40b8-9138-1b9ffa458059\" data-enlighter-title=\"\/js\/scripts.js\" data-enlighter-highlight=\"3,4,5,6,13,32-43\">\/\/ ...\nconst fullyQualifiedLocaleDefaults = {\n  en: \"en-US\",\n  ar: \"ar-EG\",\n};\n\/\/ ...\nfunction interpolate(message, interpolations) {\n  return Object.keys(interpolations).reduce(\n    (interpolated, key) =&gt; {\n      const value = formatNumber(interpolations[key]);\n      return interpolated.replace(\n        new RegExp(`{\\s*${key}\\s*}`, \"g\"),\n        value,\n      );\n    },\n    message,\n  );\n}\n\/*\n  Given a value object like\n  {\n    \"number\" : 300000,\n    \"style\": \"currency\",\n    \"currency\": \"EUR\"\n  } and that the active locale is \"en\", returns \"\u20ac300,000.00\"\n*\/\nfunction formatNumber(value) {\n  if (typeof value === \"object\" &amp;&amp; value.number) {\n    const { number, ...options } = value;\n    return new Intl.NumberFormat(\n      fullyQualifiedLocaleDefaults[locale],\n      options,\n    ).format(number);\n  } else {\n    return value;\n  }\n}\n\/\/ ...\n<\/pre>\n<p>When we interpolate our translated messages, we pass the value we\u2019re swapping in through a number formatter first, which uses the built-in <code>Intl.NumberFormat<\/code> object in turn. So, again, with very few lines of code, we have localized number formatting.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15576 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-format-en-1024x450.png\" alt=\"Demo app with US number formatting | Phrase\" width=\"1024\" height=\"450\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-format-en-1024x450.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-format-en-300x132.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-format-en-768x337.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-format-en.png 1330w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15575 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-format-ar-1024x450.png\" alt=\"Demo app with Arabic number formatting | Phrase\" width=\"1024\" height=\"450\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-format-ar-1024x450.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-format-ar-300x132.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-format-ar-768x337.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-format-ar.png 1330w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p>\u270b\ud83c\udffd <em>Heads up \u00bb<\/em> It\u2019s best to pass a fully qualified locale, like <code>\"en-US\"<\/code>, to the <code>Intl.NumberFormat()<\/code> constructor. If we pass in just a language code, like <code>\"en\"<\/code>, each browser will decide what <em>region<\/em> to use to format its numbers: One browser might default to <code>\"en-US\"<\/code> while another goes with <code>\"en-UK\"<\/code>. So we use a <code>fullyQualifiedLocaleDefaults<\/code> map in our <code>formatNumber()<\/code> function to get consistent cross-browser formatting.<\/p>\n<p>Because we\u2019re passing all options defined in our interpolations object to the <code>Intl.NumberFormat()<\/code> constructor, we can make use of its <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/NumberFormat\/NumberFormat#parameters\">myriad formatting options<\/a> anytime we want.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">&lt;p\n  data-i18n-key=\"nyan-cat-price\"\n  data-i18n-opt='{\"price\": {\n    \"number\" : 5300,\n    \"style\": \"currency\",\n    \"currency\": \"EUR\"\n  }}'\n &gt;\n  Nyan Cat (Official) NFT: {price}\n&lt;\/p&gt;\n<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15578 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-options-en.png\" alt=\"Example text US number format | Phrase\" width=\"632\" height=\"94\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-options-en.png 632w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-options-en-300x45.png 300w\" sizes=\"(max-width: 632px) 100vw, 632px\" \/><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15577 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-options-ar.png\" alt=\"Example text Arabic number format | Phrase\" width=\"420\" height=\"104\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-options-ar.png 420w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/11\/number-options-ar-300x74.png 300w\" sizes=\"(max-width: 420px) 100vw, 420px\" \/><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> You might enjoy our <a href=\"https:\/\/phrase.com\/blog\/posts\/number-localization\/\">Concise Guide to Number Localization<\/a>.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"date-formatting\"><\/span>Date formatting<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Similar to numbers, date formatting is region-specific. December 5, 2021, in its short form, is formatted as &#8220;12\/5\/2021&#8221; in English (US), and &#8220;5.12.2021&#8221; in German (Germany), for example. And again, a handy built-in <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/DateTimeFormat\/DateTimeFormat\">Intl.DateTimeFormat<\/a> object can handle the heavy lifting when it comes to date formatting.<br \/>\nSay we want to show the publishing date and time an article of ours.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">&lt;p\n  data-i18n-key=\"publish-date\"\n  data-i18n-opt='{\"publishDate\": {\n    \"date\": \"2021-12-05 15:29:00\"\n  }}'\n&gt;\n  Published on {publishDate}\n&lt;\/p&gt;\n<\/pre>\n<p>The special <code>date<\/code> key in our <code>data-i18n-opt<\/code> object holds the datetime value we want to format. As usual, we\u2019ll want to add our localized messages.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"2a53594b-f0b3-42fa-8a05-45366d8f704a\" data-enlighter-title=\"\/lang\/en.json\">{\n  \/\/ ...\n  \"publish-date\": \"Published {publishDate}\"\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"b7482e03-350d-4750-a944-9eeb661556a1\" data-enlighter-title=\"\/lang\/ar.json\">{\n  \/\/...\n  \"publish-date\": \"\u0646\u0634\u0631 {publishDate}\"\n}\n<\/pre>\n<p>Now let\u2019s update our translation system to look for <code>date<\/code> keys and format their values as localized dates.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"9f97809e-bb75-48e8-85ab-d99b7aee1937\" data-enlighter-title=\"\/js\/scripts.js\" data-enlighter-highlight=\"6,7,8,30,31,32,34,35,37,38,39,40,41,42,43,44\">\/\/ ...\nfunction interpolate(message, interpolations) {\n  return Object.keys(interpolations).reduce(\n    (interpolated, key) =&gt; {\n      const value = formatDate(\n        formatNumber(interpolations[key]),\n      );\n      return interpolated.replace(\n        new RegExp(`{\\s*${key}\\s*}`, \"g\"),\n        value,\n      );\n    },\n    message,\n  );\n}\n\/\/ ...\n\/*\n  Given a value object like\n  {\n    \"date\": \"2021-12-05 15:29:00\",\n    \"dateStyle\": \"long\",\n    \"timeStyle\": \"short\"\n  } and that the current locale is en,\n  returns \"December 5, 2021 at 3:29 PM\"\n*\/\nfunction formatDate(value) {\n  if (typeof value === \"object\" &amp;&amp; value.date) {\n    const { date, ...options } = value;\n    const parsedDate =\n      typeof date === \"string\" ? Date.parse(date) : date;\n    return new Intl.DateTimeFormat(\n      fullyQualifiedLocaleDefaults[locale],\n      options,\n    ).format(parsedDate);\n  } else {\n    return value;\n  }\n}\n\/\/ ...\n<\/pre>\n<p>After passing our value object to our number formatter, we do another pass through our new date formatter. The date formatting options are passed through to the <code>Intl.DateTimeFormat<\/code> constructor, allowing for a <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/DateTimeFormat\/DateTimeFormat#parameters\">decent amounting of flexibility in formatting dates<\/a>.<\/p>\n<p>\u270b\ud83c\udffd <em>Heads up \u00bb<\/em> We use <code>Date.parse()<\/code> above to make sure that our string <code>date<\/code> is converted to a <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Date\">Date object<\/a>, otherwise <code>Intl.DateTimeFormat<\/code> will throw an error.<\/p>\n<p>With that, we have localized date formatting \ud83d\udc4d<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">&lt;p\n  data-i18n-key=\"publish-date\"\n  data-i18n-opt='{\"publishDate\": {\n    \"date\": \"2021-12-05 15:29:00\",\n    \"dateStyle\": \"long\",\n    \"timeStyle\": \"short\"\n  }}'\n&gt;\n  Published on {publishDate}\n&lt;\/p&gt;\n<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15816 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/date-format-en.png\" alt=\"Demo app with US date formatting | Phrase\" width=\"1330\" height=\"696\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/date-format-en.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/date-format-en-300x157.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/date-format-en-1024x536.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/date-format-en-768x402.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15817 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/date-format-ar.png\" alt=\"Demo app with Arabic date formatting | Phrase\" width=\"1330\" height=\"696\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/date-format-ar.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/date-format-ar-300x157.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/date-format-ar-1024x536.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/date-format-ar-768x402.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> If you\u2019re looking for more robust date formatting features, check out our rundown, <a href=\"https:\/\/phrase.com\/blog\/posts\/best-javascript-date-time-libraries\/\">What Is the Best JavaScript Date and Time Library?<\/a> And our <a href=\"https:\/\/phrase.com\/blog\/posts\/a-human-friendly-way-to-display-dates-in-typescript-javascript\/\">Human-friendly Way to Display Dates in TypeScript\/JavaScript<\/a> gets you formatting like \u201c1 hour ago.\u201d<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> If you\u2019re using a declarative framework like React, you can take what we built in this section further and <a href=\"https:\/\/phrase.com\/blog\/posts\/roll-your-own-javascript-i18n-library-with-typescript-part-1\/\">Roll Your Own JavaScript i18n Library with TypeScript<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"what-are-some-good-javascript-i18n-libraries-i-can-use\"><\/span>What are some good JavaScript i18n libraries I can use?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We\u2019ve covered rolling your own JavaScript i18n library above. However, it might make more sense for your project to adopt an off-the-shelf i18n library. There\u2019s no shortage of options here, and in this article, we&#8217;ll walk through using the Polyglot, i18next, and Globalize libraries.<br \/>\nFor an even wider selection, our popular articles can get you started on the right foot:<\/p>\n<ul>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/the-best-javascript-i18n-libraries\/\">The Best JavaScript I18n Libraries<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/best-javascript-date-time-libraries\/\">What Is the Best JavaScript Date and Time Library?<\/a><\/li>\n<\/ul>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> If you happen to be working with legacy gettext, take a look at the <a href=\"https:\/\/github.com\/messageformat\/Jed\">Jed library<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-a-web-page-with-polyglot\"><\/span>How do I localize a web page with Polyglot?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Maintained by Airbnb, <a href=\"https:\/\/airbnb.io\/polyglot.js\/\">Polyglot<\/a> is a small i18n library that solves some localization problems previously unsupported by JavaScript\u2019s standard libraries. Most noteworthy among Polyglot\u2019s features is its excellent handling of plurals. However, as we mentioned earlier, the now built-in <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/PluralRules\/PluralRules\">Intl.PluralRules constructor<\/a> is supported by all modern browsers and handily solves the pluralization problem. Still, the last version of this article featured Polyglot heavily, so we wanted to include this section in case some of our readers still want a Polyglot guide.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> Unless your use case requires Polyglot, check out the alternative <a href=\"#How_do_I_localize_a_web_page_with_i18next\">i18next library in the next section<\/a> before you land on a localization solution.<\/p>\n<p>Without further ado, let\u2019s get our little demo app localized with Airbnb\u2019s localization library.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"125ae0d3-e7f7-4d16-b5c2-2b8f1ebc5198\" data-enlighter-title=\"public\/index.html\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;nav class=\"navbar\"&gt;\n      &lt;div class=\"container\"&gt;\n        &lt;ul class=\"navbar-list navbar-start\"&gt;\n          &lt;li class=\"navbar-item\"&gt;\n            &lt;a href=\"#\" data-i18n-key=\"home\" class=\"navbar-link\"&gt;\n              Home\n            &lt;\/a&gt;\n          &lt;\/li&gt;\n          &lt;li class=\"navbar-item\"&gt;\n            &lt;a href=\"#\" data-i18n-key=\"about\" class=\"navbar-link\"&gt;\n              About\n            &lt;\/a&gt;\n          &lt;\/li&gt;\n        &lt;\/ul&gt;\n        &lt;div class=\"navbar-end\"&gt;\n          &lt;img src=\"img\/translation-icon@2x.png\" class=\"translation-icon\" \/&gt;\n          &lt;select data-i18n-switcher class=\"locale-switcher\"&gt;\n            &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n            &lt;option value=\"ar\"&gt;Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)&lt;\/option&gt;\n          &lt;\/select&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/nav&gt;\n    &lt;h1 data-i18n-key=\"app-title\"&gt;With Polyglot&lt;\/h1&gt;\n    &lt;p data-i18n-key=\"lead\" data-i18n-opt='{\"username\": \"Cadence\"}'&gt;\n      Welcome to my little spot on the interwebs, %{username}!\n    &lt;\/p&gt;\n    &lt;p\n      data-i18n-key=\"article-plural\"\n      data-i18n-opt='{\"smart_count\": 2}'\n    &gt;\n      %{smart_count} articles written and counting.\n    &lt;\/p&gt;\n  &lt;\/div&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15818 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-before-i18n.png\" alt=\"JavaScript demo app with Polyglot | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-before-i18n.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-before-i18n-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-before-i18n-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-before-i18n-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><br \/>\nAlright, let\u2019s get to localizing this puppy with Polyglot.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"installation\"><\/span>Installation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Polyglot has some <a href=\"https:\/\/www.npmjs.com\/\">NPM<\/a> dependencies, so it needs <a href=\"https:\/\/nodejs.org\/en\/\">Node<\/a> installed locally. With Node in place, we can initialize a <code>package.json<\/code> file for our demo app by running the following from the command line.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">npm init -y\n<\/pre>\n<p>In order to bundle our NPM dependencies into a file browsers can read, we\u2019ll install the <a href=\"https:\/\/webpack.js.org\/\">Webpack<\/a> module bundler as a dev dependency. The <a href=\"https:\/\/github.com\/webpack\/webpack-dev-server\">Webpack dev server<\/a> will help us with hot-reloading our bundle in the browser as we develop.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">npm install --save-dev webpack webpack-cli webpack-dev-server\n<\/pre>\n<p>Alright, now let\u2019s install the star of the show: Polyglot.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">npm install node-polyglot\n<\/pre>\n<p>An <code>index.js<\/code> will serve as the entry point for our app, and we can use it to smoke-test the library installations.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"13c36abe-a6bd-4f2f-a7d5-0661798eb598\" data-enlighter-title=\"src\/index.js\">import Polyglot from \"node-polyglot\";\nconsole.log({ Polyglot });\n<\/pre>\n<p>A <code>start<\/code> script in our <code>package.json<\/code> will ease our development by spinning up the dev server with our custom config.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"d15750f8-71f2-491d-bf0e-ad4c16fc24bb\" data-enlighter-title=\"package.json\" data-enlighter-highlight=\"7\">{\n  \"name\": \"polyglot-demo\",\n  \/\/ ...\n  \"scripts\": {\n    \"start\": \"webpack-dev-server --config webpack.config.js\"\n  },\n  \/\/ ...\n  \"devDependencies\": {\n    \"webpack\": \"^5.65.0\",\n    \"webpack-cli\": \"^4.9.1\",\n    \"webpack-dev-server\": \"^4.6.0\"\n  },\n  \"dependencies\": {\n    \"node-polyglot\": \"^2.4.2\"\n  }\n}\n<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> We\u2019re using a relatively simple <code>webpack.config.js<\/code> file to bundle our app and configure the dev server. <a href=\"https:\/\/github.com\/PhraseApp-Blog\/javascript-l10n-ultimate-guide\/blob\/main\/polyglot\/webpack.config.js\">Check it out from our Git repo on GitHub<\/a>.<\/p>\n<p>Now we\u2019ll bring in our bundled JS right before the closing <code>&lt;\/body&gt;<\/code> tag in our <code>public\/index.html<\/code> file.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">&lt;script src=\".\/bundle.js\"&gt;&lt;\/script&gt;\n<\/pre>\n<p>With this in place, we should be able to run our <code>start<\/code> script from the command line to initialize the Webpack dev server.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">npm start\n<\/pre>\n<p>If all goes well, our app should automatically open in the browser. If we open our browser developer tools, we should see console logs similar to the following.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15819 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-webpack-server-browser-output.png\" alt=\"Browser developer tools console logs | Phrase\" width=\"1430\" height=\"342\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-webpack-server-browser-output.png 1430w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-webpack-server-browser-output-300x72.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-webpack-server-browser-output-1024x245.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-webpack-server-browser-output-768x184.png 768w\" sizes=\"(max-width: 1430px) 100vw, 1430px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"basic-translations\"><\/span>Basic translations<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Let\u2019s move on to basic Polyglot usage. Here\u2019s the recipe:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">\/\/ 1. Import the library\nimport Polyglot from \"node-polyglot\";\n\/\/ 2. Create an instance\nconst polyglot = new Polyglot();\n\/\/ 3. Add translation messages for the active locale\npolyglot.extend({\n  \"app-title\": \"With Polyglot\",\n});\n\/\/ 4. Use the messages to translate page elements\nconst element = document.querySelector(\n  \"[data-i18n-key='app-title']\",\n);\n\/\/ polyglot.t() resolves a translation message given\n\/\/ a key\nelement.innerHTML = polyglot.t(\"app-title\");\n<\/pre>\n<p>To switch locales, we can reload the page with the new locale\u2019s messages.<\/p>\n<p>\u270b\ud83c\udffd <em>Heads up \u00bb<\/em> We use <code>innerHTML<\/code> in this article to set element content. Be careful with this attribute in production; make sure to sanitize any HTML that you inject using <code>innerHTML<\/code> to avoid XSS (cross-site scripting) attacks.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-highlight=\"6\">import Polyglot from \"node-polyglot\";\nconst polyglot = new Polyglot();\npolyglot.extend({\n  \"app-title\": \"\u0645\u0639 \u0628\u0648\u0644\u064a\u062c\u0644\u0648\u062a\",\n});\nconst element = document.querySelector(\n  \"[data-i18n-key='app-title']\",\n);\nelement.innerHTML = polyglot.t(\"app-title\");\n<\/pre>\n<h3><span class=\"ez-toc-section\" id=\"async-translation-file-loading\"><\/span>Async translation file loading<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>While the above works fine for the tiniest apps, we could do a bit better by splitting out our translations messages into separate, per-locale JSON files.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"d3cbfa7d-9584-422b-ae6b-da4ae0f6239e\" data-enlighter-title=\"public\/lang\/en.json\">{\n  \"app-title\": \"With Polyglot\",\n  \"home\": \"Home\",\n  \"about\": \"About\"\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"33d548fc-48fe-4e4f-a852-51da5f540ca6\" data-enlighter-title=\"public\/lang\/ar.json\">{\n  \"app-title\": \"\u0645\u0639 \u0628\u0648\u0644\u064a\u062c\u0644\u0648\u062a\",\n  \"home\": \"\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629\",\n  \"about\": \"\u0646\u0628\u0630\u0629 \u0639\u0646\u0627\"\n}\n<\/pre>\n<p>Now we can configure a default locale for our app and load its translations from the network when our page loads.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"72b66df6-d597-40ec-bbda-ef066a489055\" data-enlighter-title=\"src\/index.js\">import Polyglot from \"node-polyglot\";\nconst defaultLocale = \"en\";\nconst polyglot = new Polyglot();\n\/\/ Load translation messages from the network\nasync function loadTranslations(locale) {\n  return await fetch(`\/lang\/${locale}.json`).then(\n    (response) =&gt; response.json(),\n  );\n}\n\/\/ Translate all elements on the page that have our custom\n\/\/ data-i18n-key attribute\nfunction translatePage() {\n  const translatableElements = document.querySelectorAll(\n    \"[data-i18n-key]\",\n  );\n  translatableElements.forEach((el) =&gt; {\n    const key = el.getAttribute(\"data-i18n-key\");\n    el.innerHTML = polyglot.t(key);\n  });\n}\n\/\/ Init\n(async function () {\n  const translations = await loadTranslations(\n    defaultLocale,\n  );\n  polyglot.extend(translations);\n  translatePage();\n})();\n<\/pre>\n<p>With that, we get the following render in the browser.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15820 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-async-en.png\" alt=\"English locale demo app with Polyglot and missing menu items and main title | Phrase\" width=\"1430\" height=\"978\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-async-en.png 1430w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-async-en-300x205.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-async-en-1024x700.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-async-en-768x525.png 768w\" sizes=\"(max-width: 1430px) 100vw, 1430px\" \/><br \/>\nOur navigation menu items and main title are translated into our default locale, English. However, notice the polyglot errors in the console, and how missing keys (<code>lead<\/code> and <code>article-plural<\/code>) display the value of the keys themselves. We\u2019ll add translations for these keys in a minute to rectify this.<br \/>\nIf we change <code>defaultLocale<\/code> to <code>\"ar\"<\/code>, we get the following render.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15821 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-async-ar.png\" alt=\"Arabic locale demo app with Polyglot and missing menu items and main title | Phrase\" width=\"1436\" height=\"978\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-async-ar.png 1436w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-async-ar-300x204.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-async-ar-1024x697.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-async-ar-768x523.png 768w\" sizes=\"(max-width: 1436px) 100vw, 1436px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"language-switcher\"><\/span>Language switcher<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Our app is now more scalable since only the active locale\u2019s translations are loaded. Let\u2019s use this to build a language switcher. We already have the HTML for the switcher in our app:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"dc20fe15-b6c0-48a7-bca9-d5e1be114c97\" data-enlighter-title=\"public\/index.html\" data-enlighter-highlight=\"18,19,20,21\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;nav class=\"navbar\"&gt;\n      &lt;div class=\"container\"&gt;\n\t\t&lt;!-- ... --&gt;\n        &lt;div class=\"navbar-end\"&gt;\n          &lt;img src=\"img\/translation-icon@2x.png\" class=\"translation-icon\" \/&gt;\n          &lt;select data-i18n-switcher class=\"locale-switcher\"&gt;\n            &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n            &lt;option value=\"ar\"&gt;Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)&lt;\/option&gt;\n          &lt;\/select&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/nav&gt;\n    &lt;!-- ... --&gt;\n  &lt;script src=\".\/bundle.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>Hooking into this <code>&lt;select&gt;<\/code> element from our JavaScript allows us to add our locale-switching behavior.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"07b2796c-fc57-4ec8-aa9a-398d7edf394f\" data-enlighter-title=\"src\/index.js\" data-enlighter-highlight=\"11,12,14,16,17,21,22,23,24,26,28,29,30,31,34,35\">import Polyglot from \"node-polyglot\";\nconst defaultLocale = \"en\";\nconst polyglot = new Polyglot();\n\/\/ ...\n\/\/ Load translations for the given locale and translate\n\/\/ page elements for this locale\nasync function loadAndTranslate(locale) {\n  const translations = await loadTranslations(locale);\n  polyglot.replace(translations);\n  translatePage();\n}\n\/\/ Whenever the user switches the active locale, load\n\/\/ this locale's messages into the page\nfunction bindLocaleSwitcher(initialValue) {\n  const switcher = document.querySelector(\n    \"[data-i18n-switcher]\",\n  );\n  switcher.value = initialValue;\n  switcher.onchange = (e) =&gt; {\n    loadAndTranslate(e.target.value);\n  };\n}\n\/\/ Init\nloadAndTranslate(defaultLocale);\nbindLocaleSwitcher(defaultLocale);\n<\/pre>\n<p>We\u2019ve refactored the code that loads our translation messages and translates our page elements to a reusable <code>loadAndTranslate()<\/code> function. A new <code>bindLocaleSwitcher()<\/code> function hooks into the language switcher <code>&lt;select&gt;<\/code>; it uses <code>loadAndTranslate()<\/code> to refresh our translations based on the user-select locale.<\/p>\n<p>\u270b\ud83c\udffd <em>Heads up \u00bb<\/em> <code>polyglot.extend()<\/code> will <em>add<\/em> translation messages to the ones already loaded, so we use <code>polyglot.replace()<\/code> instead to ensure that we only load the active locale translations.<\/p>\n<p>This should get our swanky locale switcher up and running.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15822 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-lang-switcher.gif\" alt=\"Demo app with Polyglot with fixed locale switcher | Phrase\" width=\"600\" height=\"217\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"interpolation-2\"><\/span>Interpolation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Our lead text includes the currently logged-in user\u2019s name (mocked, of course). This kind of interpolated value is handled by Polyglot using a special <code>%{variable}<\/code> syntax by default.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> You can change the characters that denote interpolate values using the <code>interpolation<\/code> <a href=\"https:\/\/airbnb.io\/polyglot.js\/#options-overview\">option passed to the Polyglot constructor<\/a>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"daf6cafc-7812-46e9-bcd7-963798dbd65f\" data-enlighter-title=\"public\/index.html\">&lt;!-- ... --&gt;\n    &lt;p data-i18n-key=\"lead\" data-i18n-opt='{\"username\": \"Cadence\"}'&gt;\n      Welcome to my little spot on the interwebs, %{username}!\n    &lt;\/p&gt;\n&lt;!-- ... --&gt;\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"a78a94fe-dc3d-48c0-bf90-185531c0411d\" data-enlighter-title=\"public\/lang\/en.json\">{\n  \/\/ ...\n  \"lead\": \"Welcome to my little spot on the interwebs, %{username}!\"\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"7868539a-4ab0-4bdc-823e-614ea96bbd06\" data-enlighter-title=\"public\/lang\/ar.json\">{\n  \/\/ ...\n  \"lead\": \"\u0623\u0647\u0644\u0627\u064b \u0628\u0643 \u0641\u064a \u0645\u0643\u0627\u0646\u064a \u0627\u0644\u0635\u063a\u064a\u0631 \u0639\u0644\u0649 \u0627\u0644\u0646\u062a \u064a\u0627 %{username}.\"\n}\n<\/pre>\n<p>A few lines of code can be added to our <code>translatePage()<\/code> function to accommodate interpolations.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"5bd2109a-45be-407b-9277-b08354101d9c\" data-enlighter-title=\"src\/index.js\" data-enlighter-highlight=\"13,14,15,16,17,18,20,21,22\">\/\/ ...\n\/\/ Translate all elements on the page that have a\n\/\/ data-i18n-key attribute\nfunction translatePage() {\n  const translatableElements = document.querySelectorAll(\n    \"[data-i18n-key]\",\n  );\n  translatableElements.forEach((el) =&gt; {\n    const key = el.getAttribute(\"data-i18n-key\");\n    \/\/ Extract interpolation key\/values from the HTML and\n    \/\/ parse them to JSON\n    const interpolations = el.getAttribute(\"data-i18n-opt\");\n    const parsedInterpolations = interpolations\n      ? JSON.parse(interpolations)\n      : {};\n    \/\/ Pass the parsed interpolations to polyglot.t(),\n    \/\/ which automatically handles substitutions\n    el.innerHTML = polyglot.t(key, parsedInterpolations);\n  });\n}\n\/\/ ...\n<\/pre>\n<p>With the above code in place, we now have our interpolated lead paragraph rendered in the active locale.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15824 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-interpolation-en.png\" alt=\"English locale demo app with Polyglot with fixed lead paragraph | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-interpolation-en.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-interpolation-en-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-interpolation-en-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-interpolation-en-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15825 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-interpolation-ar.png\" alt=\"Arabic locale demo app with Polyglot with fixed lead paragraph | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-interpolation-ar.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-interpolation-ar-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-interpolation-ar-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-interpolation-ar-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"plurals-2\"><\/span>Plurals<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Say we want to show the currently logged in user how many messages he or she has received: \u201cYou have 1 new message\u201d or \u201cYou have 12 new messages,\u201d for example. Polyglot handles plurals like this well. We just need to add our translation messages with the special interpolated number value, <code>smart_count<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"dbe32626-2aef-4da8-9c76-db1941115eff\" data-enlighter-title=\"public\/index.html\">&lt;!-- ... --&gt;\n    &lt;p\n      data-i18n-key=\"new-messages\"\n      data-i18n-opt='{\"smart_count\": 12}'\n    &gt;\n      You have %{smart_count} new messages\"\n    &lt;\/p&gt;\n&lt;!-- ... --&gt;\n<\/pre>\n<p>Polyglot uses <code>smart_count<\/code> to select the correct plural form from a translation message depending on the active locale. Plural forms are separated using four pipes <code>||||<\/code> in our messages. English has <code>one<\/code> and <code>other<\/code> plural forms, and we need to provide them in order:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"effada7e-fa5e-4980-bade-8326330e9360\" data-enlighter-title=\"public\/lang\/en.json\">{\n  \/\/ ...\n  \"new-messages\": \"You have %{smart_count} new message |||| You have %{smart_count} new messages\"\n}\n<\/pre>\n<p>Arabic has six plural forms, and we add them to our messages much the same way.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"e551b929-010f-4505-857f-566dad4d8275\" data-enlighter-title=\"public\/lang\/ar.json\">{\n  \/\/ ...\n  \"new-messages\": \"\u0644\u0627 \u062a\u0648\u062c\u062f \u0644\u062f\u064a\u0643 \u0631\u0633\u0627\u0626\u0644 \u062c\u062f\u064a\u062f\u0629 |||| \u0644\u062f\u064a\u0643 \u0631\u0633\u0627\u0644\u0629 \u062c\u062f\u064a\u062f\u0629 |||| \u0644\u062f\u064a\u0643 \u0631\u0633\u0627\u0644\u062a\u0627\u0646 \u062c\u062f\u0627\u062f |||| \u0644\u062f\u064a\u0643 %{smart_count} \u0631\u0633\u0627\u0626\u0644 \u062c\u062f\u064a\u062f\u0629 |||| \u0644\u062f\u064a\u0643 %{smart_count} \u0631\u0633\u0627\u0644\u0629 \u062c\u062f\u064a\u062f\u0629 |||| \u0644\u062f\u064a\u0643 %{smart_count} \u0631\u0633\u0627\u0644\u0629 \u062c\u062f\u064a\u062f\u0629\"\n}\n<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> Check out the <a href=\"#Plurals\">How do I localize a web page with JavaScript? \u279e Plurals<\/a> section for more details on plural forms.<\/p>\n<p>One more thing: by default, Polyglot is blissfully unaware of the active locale, so it won\u2019t know the plural rules of the active locale unless we explicitly specify the locale on load.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"4ffe4b2d-4324-4b53-a227-3d0fa25322c6\" data-enlighter-title=\"src\/index.js\" data-enlighter-highlight=\"8\">\/\/ ...\n\/\/ Load translations for the given locale and translate\n\/\/ page elements for this locale\nasync function loadAndTranslate(locale) {\n  const translations = await loadTranslations(locale);\n  polyglot.locale(locale);\n  polyglot.replace(translations);\n  translatePage();\n}\n\/\/ ...\n<\/pre>\n<p>And that\u2019s it! We now have advanced plural support in our app.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15826 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-plurals-en.png\" alt=\"English locale demo app with Polyglot smart count | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-plurals-en.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-plurals-en-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-plurals-en-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-plurals-en-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15827 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-plurals-ar.png\" alt=\"Arabic locale demo app with Polyglot smart count | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-plurals-ar.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-plurals-ar-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-plurals-ar-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/polyglot-plurals-ar-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Get all the code for our Polyglot app from <a href=\"https:\/\/github.com\/PhraseApp-Blog\/javascript-l10n-ultimate-guide\/tree\/main\/polyglot\">our GitHub repo<\/a>.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The <a href=\"https:\/\/airbnb.io\/polyglot.js\/\">official Polyglot documentation<\/a> is as tight as the library itself.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-a-web-page-with-i18next\"><\/span>How do I localize a web page with i18next?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>As I write this, <a href=\"https:\/\/www.i18next.com\/\">i18next<\/a> is one of the <a href=\"https:\/\/www.npmtrends.com\/globalize-vs-i18next-vs-node-polyglot-vs-react-intl\">most popular JavaScript i18n libraries<\/a>. The \u201c<a href=\"https:\/\/www.i18next.com\/#learn-once-translate-everywhere\">learn once, [use] everywhere<\/a>\u201d library works standalone, and with a slew of JavaScript frameworks. A rich plugin ecosystem means that you\u2019re often an NPM install away from solving a common i18n problem. All this makes i18next an easy recommendation.<br \/>\nAlright, enough jibber-jabber. Let\u2019s revisit our little demo and localize it with i18next.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"ebba1c2b-e62b-4423-8548-a8ccb4bcccdf\" data-enlighter-title=\"public\/index.html\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;nav class=\"navbar\"&gt;\n      &lt;div class=\"container\"&gt;\n        &lt;ul class=\"navbar-list navbar-start\"&gt;\n          &lt;li class=\"navbar-item\"&gt;\n            &lt;a href=\"#\" data-i18n-key=\"home\" class=\"navbar-link\"&gt;\n              Home\n            &lt;\/a&gt;\n          &lt;\/li&gt;\n          &lt;li class=\"navbar-item\"&gt;\n            &lt;a href=\"#\" data-i18n-key=\"about\" class=\"navbar-link\"&gt;\n              About\n            &lt;\/a&gt;\n          &lt;\/li&gt;\n        &lt;\/ul&gt;\n        &lt;div class=\"navbar-end\"&gt;\n          &lt;img src=\"img\/translation-icon@2x.png\" class=\"translation-icon\" \/&gt;\n          &lt;select data-i18n-switcher class=\"locale-switcher\"&gt;\n            &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n            &lt;option value=\"ar\"&gt;Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)&lt;\/option&gt;\n          &lt;\/select&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/nav&gt;\n    &lt;h1 data-i18n-key=\"app-title\"&gt;With i18next&lt;\/h1&gt;\n    &lt;p data-i18n-key=\"lead\" data-i18n-opt='{\"username\": \"Zelda\"}'&gt;\n      Welcome to my little spot on the interwebs, {{username}}!\n    &lt;\/p&gt;\n    &lt;p data-i18n-key=\"new-messages\" data-i18n-opt='{\"count\": 12}'&gt;\n      You have {{count}} new messages\n    &lt;\/p&gt;\n  &lt;\/div&gt;\n  &lt;script src=\".\/bundle.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>Not much new here. Let\u2019s get localizing.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"installation-2\"><\/span>Installation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>We\u2019ll use <a href=\"https:\/\/nodejs.org\/en\/\">Node<\/a> and its <a href=\"https:\/\/www.npmjs.com\/\">NPM<\/a> package manager to install i18next. First, let\u2019s create a <code>package.json<\/code> file to track our project dependencies and NPM scripts by running the following from the command line.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">npm init -y\n<\/pre>\n<p>The <a href=\"https:\/\/webpack.js.org\/\">Webpack<\/a> bundler will allow us to package up i18next, its plugins, and our custom JavaScript and serve them in one file to the browser. Let\u2019s install Webpack, along with its development server, which has a handy hot reload feature that eases development:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">npm install --save-dev webpack webpack-cli webpack-dev-server\n<\/pre>\n<p>We can\u2019t forget our i18n library, of course:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">npm install i18next\n<\/pre>\n<p>A handy <code>npm start<\/code> script can shortcut spinning up our dev server.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"ff750f62-93c9-495b-8b54-ffae0aa84a85\" data-enlighter-title=\"package.json\" data-enlighter-highlight=\"7\">{\n  \"name\": \"i18next-demo\",\n  \/\/...\n  \"scripts\": {\n    \"start\": \"webpack-dev-server --config webpack.config.js\"\n  },\n  \/\/ ...\n  \"devDependencies\": {\n    \"webpack\": \"^5.65.0\",\n    \"webpack-cli\": \"^4.9.1\",\n    \"webpack-dev-server\": \"^4.6.0\"\n  },\n  \"dependencies\": {\n    \"i18next\": \"^21.6.3\"\n  }\n}\n<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> We\u2019re using a relatively simple <code>webpack.config.js<\/code> file to bundle our app and configure the dev server. <a href=\"https:\/\/github.com\/PhraseApp-Blog\/javascript-l10n-ultimate-guide\/blob\/main\/i18next\/webpack.config.js\">Check it out from our Git repo on GitHub<\/a>.<\/p>\n<p>Let\u2019s create an <code>index.js<\/code> entry point for our app and smoke-test i18next to make sure it\u2019s installed correctly.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"c346dcb9-2358-4eb7-b8a7-cbd6553cc9f1\" data-enlighter-title=\"src\/index.js\">import i18next from \"i18next\";\nconsole.log({ i18next });\n<\/pre>\n<p>Now when we run <code>npm start<\/code> from the command line, we should see the Webpack dev server start up and load our app in our browser automatically.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15828 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-post-install.png\" alt=\"Demo App with Webpack dev server loaded | Phrase\" width=\"1434\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-post-install.png 1434w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-post-install-300x214.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-post-install-1024x731.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-post-install-768x548.png 768w\" sizes=\"(max-width: 1434px) 100vw, 1434px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"basic-translation-messages-2\"><\/span>Basic translation messages<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>i18next is flexible in how it accepts translation messages. We do most of our configuration as we initialize the library with <code>i18next.init(...)<\/code>. The most basic setup has us inlining our translation messages under a <code>resources<\/code> option.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"6b18cd08-103b-40a0-965c-4d0635e3b780\" data-enlighter-title=\"src\/index.js\">import i18next from \"i18next\";\ni18next.init({\n  \/\/ The active locale\n  lng: \"en\",\n  \/\/ Enabled useful console output when developing\n  debug: true,\n  \/\/ Translation messages, keyed by locale code\n  resources: {\n    en: {\n      \/\/ By default, i18next expects messages under the\n      \/\/ \"translation\" namespace\n      translation: {\n        \"app-title\": \"With Polyglot\",\n        home: \"Home\",\n        about: \"About\",\n      },\n    },\n    ar: {\n      translation: {\n        \"app-title\": \"\u0645\u0639 \u0628\u0648\u0644\u064a\u062c\u0644\u0648\u062a\",\n        home: \"\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629\",\n        about: \"\u0646\u0628\u0630\u0629 \u0639\u0646\u0627\",\n      },\n    },\n  },\n});\n\/\/ Translate page elements\nconst translatableElements = document.querySelectorAll(\n  \"[data-i18n-key]\",\n);\ntranslatableElements.forEach((el) =&gt; {\n  const key = el.getAttribute(\"data-i18n-key\");\n  el.innerHTML = i18next.t(key);\n});\n<\/pre>\n<p>With the above, we should see no changes when our app reloads in the browser. However, if we change <code>lng<\/code> to <code>\"ar\"<\/code>, we see the following Arabic translations.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15880 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-basic-translations-1.png\" alt=\"Arabic locale demo app with missing key | Phrase\" width=\"1434\" height=\"1270\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-basic-translations-1.png 1434w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-basic-translations-1-300x266.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-basic-translations-1-1024x907.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-basic-translations-1-768x680.png 768w\" sizes=\"(max-width: 1434px) 100vw, 1434px\" \/><\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> The <code>debug: true<\/code> option enables very handy console logs in the browser. Note the missing key messages above, for example.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"async-translation-loading\"><\/span>Async translation loading<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Future-looking devs that we are, let\u2019s make our app more scalable by splitting our translations up into separate files, one per locale.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"c49398ad-5069-4bad-a35c-b2c6c85dd670\" data-enlighter-title=\"public\/lang\/en.json\">{\n  \"app-title\": \"With i18next\",\n  \"home\": \"Home\",\n  \"about\": \"About\"\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"23ed2f14-52e9-41d3-979d-79e3fbd1c07d\" data-enlighter-title=\"public\/lang\/ar.json\">{\n  \"app-title\": \"\u0645\u0639 \u0622\u064a \u0623\u064a\u062a\u064a\u0646 \u0646\u064a\u0643\u0633\u062a\",\n  \"home\": \"\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629\",\n  \"about\": \"\u0646\u0628\u0630\u0629 \u0639\u0646\u0627\"\n}\n<\/pre>\n<p>i18next is mature and has a lot of bases covered; so we don\u2019t need to write our own code to load translation files from the network. The <a href=\"https:\/\/github.com\/i18next\/i18next-http-backend\">official HTTP backend<\/a> plugs into the library and does all the work for us. Let\u2019s install it.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">npm install i18next-http-backend\n<\/pre>\n<p>We can now pull the backend into our <code>index.js<\/code> and <code>use()<\/code> it as we initialize i18next.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"f7604a6a-683e-43b1-b53b-e043f26d4a97\" data-enlighter-title=\"src\/index.js\" data-enlighter-highlight=\"2,4,5,6,7,8,9,10,14,16,17,19,20,21,22,23,24,26,27,40,41,42,43\">import i18next from \"i18next\";\nimport HttpApi from \"i18next-http-backend\";\n\/\/ We make the function async so we can await\n\/\/ the translation file as it pipes down the\n\/\/ network\nasync function initI18next() {\n  \/\/ We use() the backend and await it to load\n  \/\/ the translations from the network\n  await i18next.use(HttpApi).init({\n    lng: \"en\",\n    debug: true,\n    \/\/ Remove inlined `resources`\n    \/\/ Disable loading of dev locale\n    fallbackLng: false,\n    \/\/ Configure Http backend\n    backend: {\n      loadPath: \"\/lang\/{{lng}}.json\",\n    },\n  });\n}\n\/\/ Quick refactor of the page translation code\n\/\/ to a function\nfunction translatePageElements() {\n  const translatableElements = document.querySelectorAll(\n    \"[data-i18n-key]\",\n  );\n  translatableElements.forEach((el) =&gt; {\n    const key = el.getAttribute(\"data-i18n-key\");\n    el.innerHTML = i18next.t(key);\n  });\n}\n\/\/ Init\n(async function () {\n  await initI18next();\n  translatePageElements();\n})();\n<\/pre>\n<p>The <code>backend.loadPath<\/code> option overrides the backend\u2019s default translation file path. A special <code>{{lng}}<\/code> placeholder is replaced with the active locale. For example, when our app first loads, the backend will look for <code>\/lang\/en.json<\/code>, since we specified the default locale as <code>en<\/code> earlier in our config.<br \/>\nThat\u2019s about it. Our translations now load from the network instead of being inlined in our code.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"supported-locales-and-fallback\"><\/span>Supported locales and fallback<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>We often want to specify a list of locales that our app supports and a locale to fall back on when a translation is missing. We can use i18next\u2019s <code>supportLngs<\/code> and <code>fallbackLng<\/code> config options, respectively, to accomplish this.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"80f41c54-7e75-43b6-902e-a1e086c14cf1\" data-enlighter-title=\"src\/index.js\" data-enlighter-highlight=\"7,8\">\/\/ ...\nasync function initI18next() {\n  await i18next.use(HttpApi).init({\n    lng: \"en\",\n    debug: true,\n    supportedLngs: [\"en\", \"ar\"],\n    fallbackLng: \"en\",\n    backend: {\n      loadPath: \"\/lang\/{{lng}}.json\",\n    },\n  });\n}\n\/\/ ...\n<\/pre>\n<p>\u270b\ud83c\udffd <em>Heads up \u00bb<\/em> The fallback locale will <em>always<\/em> be loaded, regardless of the active locale.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"automatically-detecting-the-users-locale\"><\/span>Automatically detecting the user\u2019s locale<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>It\u2019s common to want to detect the user\u2019s browser settings and use her or his locale if we support it. This is normally a bit tricky, but again i18next has an <a href=\"https:\/\/github.com\/i18next\/i18next-browser-languageDetector\">official plugin<\/a> that can sort us out quickly here. We can start by installing it.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">npm install i18next-browser-languagedetector\n<\/pre>\n<p>Much like the HTTP backend, we <code>import<\/code> the detector plugin and <code>use()<\/code> it as we initialize.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"8ffa7f23-6818-4876-8036-17aa252c47cf\" data-enlighter-title=\"src\/index.js\" data-enlighter-highlight=\"3,8,14-16\" data-enlighter-linenumbers=\"false\">import i18next from \"i18next\";\nimport HttpApi from \"i18next-http-backend\";\nimport LanguageDetector from \"i18next-browser-languagedetector\";\nasync function initI18next() {\n  await i18next\n    .use(HttpApi)\n    .use(LanguageDetector)\n    .init({\n      debug: true,\n      supportedLngs: [\"en\", \"ar\"],\n      fallbackLng: \"en\",\n      \/\/ Allow \"en\" to be used for\n      \/\/ \"en-US\", \"en-CA\", etc.\n      nonExplicitSupportedLngs: true,\n      backend: {\n        loadPath: \"\/lang\/{{lng}}.json\",\n      },\n    });\n}\n\/\/ ...\n<\/pre>\n<p>And that\u2019s all it takes to get solid automatic locale detection with i18next.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> You might be wondering what criteria the locale detector is using to determine the user\u2019s locale. We cover this in detail in our <a href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#Automatically_Detecting_the_Users_Language\">Guide to React Localization with i18next <\/a>.<\/p>\n<p>\u270b\ud83c\udffd <em>Heads up \u00bb<\/em> The locale detector will store the locale it detects in the browser\u2019s locale storage by default, and use <em>that<\/em> value when the user visits our site again.<\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> We have a dedicated guide to <a href=\"https:\/\/phrase.com\/blog\/posts\/detecting-a-users-locale\/\">Detecting browser language preference with JavaScript<\/a>, which might pique your interest.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"language-switcher-2\"><\/span>Language switcher<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Automatic locale detection is good and all, but we often need to have a UI for our users to explicitly set their language of choice. We already have the markup for a locale switcher set up.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"0ae136e3-6aab-4d56-8a59-449d5efe8768\" data-enlighter-title=\"public\/index.html\" data-enlighter-highlight=\"18,19,20,21\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;nav class=\"navbar\"&gt;\n      &lt;div class=\"container\"&gt;\n      &lt;!-- ... --&gt;\n        &lt;div class=\"navbar-end\"&gt;\n          &lt;img src=\"img\/translation-icon@2x.png\" class=\"translation-icon\" \/   &gt;\n          &lt;select data-i18n-switcher class=\"locale-switcher\"&gt;\n            &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n            &lt;option value=\"ar\"&gt;Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)&lt;\/option&gt;\n          &lt;\/select&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/nav&gt;\n    &lt;!-- ... --&gt;\n  &lt;\/div&gt;\n  &lt;script src=\".\/bundle.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>Let\u2019s hook into this HTML and use the <code>i18next.changeLanguage()<\/code> function to set our active locale to the one the user chooses. After the locale\u2019s messages are loaded in, we can chain <code>translatePageElements<\/code> to re-render the page with updated translations.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"799f4601-6390-44a3-ba97-66e0fd7f1412\" data-enlighter-title=\"src\/index.js\" data-enlighter-highlight=\"3,4,5,6,8,10,11,12,13,14,15,21\">\/\/ ...\nfunction bindLocaleSwitcher(initialValue) {\n  const switcher = document.querySelector(\n    \"[data-i18n-switcher]\",\n  );\n  switcher.value = initialValue;\n  switcher.onchange = (e) =&gt; {\n    i18next\n      .changeLanguage(e.target.value)\n      .then(translatePageElements);\n  };\n}\n\/\/ Init\n(async function () {\n  await initI18next();\n  translatePageElements();\n  bindLocaleSwitcher(i18next.resolvedLanguage);\n})();\n<\/pre>\n<p>\u270b\ud83c\udffd <em>Heads up \u00bb<\/em> The locale detector could have detected a locale we don\u2019t support, and <em>that<\/em> value will exist in <code>i18next.language<\/code>. We use <code>i18next.resolvedLanguage<\/code> above to ensure that we use the active, <em>supported<\/em> locale when initializing our locale switcher.<\/p>\n<p>Et voila! A language-switching UI:<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15830 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-locale-switcher.gif\" alt=\"Polyglot demo app with a language switching UI | Phrase\" width=\"600\" height=\"217\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"interpolation-3\"><\/span>Interpolation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>By default, i18next uses a <code>{{variable}}<\/code> syntax to denote interpolated values in translation messages.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"5a3233fd-74a1-4c21-aec9-5a5dad9750a3\" data-enlighter-title=\"public\/index.html\">&lt;!-- ... --&gt;\n    &lt;p data-i18n-key=\"lead\" data-i18n-opt='{\"username\": \"Zelda\"}'&gt;\n      Welcome to my little spot on the interwebs, {{username}}!\n    &lt;\/p&gt;\n&lt;!-- ... --&gt;\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"9fedd78c-4683-4235-ba4e-4e91a3117bd9\" data-enlighter-title=\"public\/lang\/en.json\">{\n  \/\/ ...\n  \"lead\": \"Welcome to my little spot on the interwebs, {{username}}!\"\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"7d7ce46a-079e-4239-91c3-3cf21ccf0310\" data-enlighter-title=\"public\/lang\/ar.json\">{\n  \/\/ ...\n  \"lead\": \"\u0623\u0647\u0644\u0627\u064b \u0628\u0643 \u0641\u064a \u0645\u0643\u0627\u0646\u064a \u0627\u0644\u0635\u063a\u064a\u0631 \u0639\u0644\u0649 \u0627\u0644\u0646\u062a \u064a\u0627 {{username}}.\"\n}\n<\/pre>\n<p>We can pull these dynamic values from our HTML attributes and feed them to <code>i18next.t()<\/code>, which handles the interpolation for us.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"85eb09f1-0bcf-49d3-b44a-e803b326513f\" data-enlighter-title=\"src\/index.js\" data-enlighter-highlight=\"11,12,13,14,16\">\/\/ ...\nfunction translatePageElements() {\n  const translatableElements = document.querySelectorAll(\n    \"[data-i18n-key]\",\n  );\n  translatableElements.forEach((el) =&gt; {\n    const key = el.getAttribute(\"data-i18n-key\");\n    const interpolations = el.getAttribute(\"data-i18n-opt\");\n    const parsedInterpolations = interpolations\n      ? JSON.parse(interpolations)\n      : {};\n    el.innerHTML = i18next.t(key, parsedInterpolations);\n  });\n}\n\/\/ ...\n<\/pre>\n<p>With that in place, we get our dynamic values replaced in our messages.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15831 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-interpolation-en.png\" alt=\"Demo app with Polyglot in English locale and interpolated lead paragraph | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-interpolation-en.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-interpolation-en-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-interpolation-en-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-interpolation-en-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15832 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-interpolation-ar.png\" alt=\"Demo app with Polyglot in Arabic locale and interpolated lead paragraph | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-interpolation-ar.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-interpolation-ar-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-interpolation-ar-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-interpolation-ar-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"plurals-3\"><\/span>Plurals<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Under the hood, i18next tries to use the standard <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/PluralRules\/PluralRules\">Intl.PluralRules<\/a> to handle plurals. A special interpolated <code>count<\/code> variable directs the plural form choice, depending on the active locale.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"6f1ac8ea-b04b-4ffa-97ff-0d5d961018fb\" data-enlighter-title=\"public\/index.html\">&lt;!-- ... --&gt;\n    &lt;p data-i18n-key=\"new-messages\" data-i18n-opt='{\"count\": 12}'&gt;\n      You have {{count}} new messages\n    &lt;\/p&gt;\n&lt;!-- ... --&gt;\n<\/pre>\n<p>i18next uses a <code>message_form<\/code> convention for plural message keys. For example, to handle the <code>one<\/code> and <code>other<\/code> forms in an English <code>new-messages<\/code> translation, we can specify the following.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"f5f7aa95-6e4d-4517-8245-ba0595110e31\" data-enlighter-title=\"public\/lang\/en.json\">{\n  \/\/ ...\n  \"new-messages_one\": \"You have {{count}} new message\",\n  \"new-messages_other\": \"You have {{count}} new messages\"\n}\n<\/pre>\n<p>Arabic has six plural forms, and we can specify them much the same way.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"a4211fe0-4e56-4b3c-a0de-83e9fb259bc9\" data-enlighter-title=\"public\/lang\/ar.json\">{\n  \/\/ ...\n  \"new-messages_zero\": \"\u0644\u0627 \u062a\u0648\u062c\u062f \u0644\u062f\u064a\u0643 \u0631\u0633\u0627\u0626\u0644 \u062c\u062f\u064a\u062f\u0629\",\n  \"new-messages_one\": \"\u0644\u062f\u064a\u0643 \u0631\u0633\u0627\u0644\u0629 \u062c\u062f\u064a\u062f\u0629\",\n  \"new-messages_two\": \"\u0644\u062f\u064a\u0643 \u0631\u0633\u0627\u0644\u062a\u0627\u0646 \u062c\u062f\u0627\u062f\",\n  \"new-messages_few\": \"\u0644\u062f\u064a\u0643 {{count}} \u0631\u0633\u0627\u0626\u0644 \u062c\u062f\u064a\u062f\u0629\",\n  \"new-messages_many\": \"\u0644\u062f\u064a\u0643 {{count}} \u0631\u0633\u0627\u0644\u0629 \u062c\u062f\u064a\u062f\u0629\",\n  \"new-messages_other\": \"\u0644\u062f\u064a\u0643 {{count}} \u0631\u0633\u0627\u0644\u0629 \u062c\u062f\u064a\u062f\u0629\"\n}\n<\/pre>\n<p>With little effort, our app can display plural messages.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15833 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-plurals-en.png\" alt=\"Demo app with Polyglot in English locale and pluralization | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-plurals-en.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-plurals-en-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-plurals-en-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-plurals-en-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15834 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-plurals-ar.png\" alt=\"Demo app with Polyglot in Arabic locale and pluralization | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-plurals-ar.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-plurals-ar-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-plurals-ar-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/i18next-plurals-ar-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Get all the code we\u2019ve covered above from our <a href=\"https:\/\/github.com\/PhraseApp-Blog\/javascript-l10n-ultimate-guide\/tree\/main\/i18next\">GitHub repo<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-a-react-angular-or-vue-app\"><\/span>How do I localize a React, Angular, or Vue app?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In recent years, declarative frameworks like <a href=\"https:\/\/reactjs.org\/\">React<\/a>, <a href=\"https:\/\/angular.io\/\">Angular<\/a>, <a href=\"https:\/\/vuejs.org\/\">Vue.js<\/a>, and others have taken the front-end web world by storm. We cover these frameworks extensively on our blog. As React is the most popular among the heavyweights, we\u2019ll provide a quick guide to localizing React apps in a moment here. And for other declarative frameworks, check out our following in-depth articles.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"angular-localization-articles\"><\/span>Angular localization articles<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<ul>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/angular-localization-i18n\/\">Translating Angular Applications with the Built-In I18n Module<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/best-libraries-for-angular-i18n\/\">What Is the Best Angular Library for Internationalization?<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/angular-l10n-with-i18next\/\">Angular L10n with I18next<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/angular-10-tutorial-localization-transloco\/\">Angular 10 Tutorial on Localization with Transloco<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/full-stack-i18n-angular-net-core\/\">Full-Stack I18n with Angular and .NET Core<\/a><\/li>\n<\/ul>\n<h3><span class=\"ez-toc-section\" id=\"vuejs-localization-articles\"><\/span>Vue.js localization articles<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<ul>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/\">The Ultimate Vue 3 Localization Guide<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/vue-translation-with-vue-i18next\/\">Deep Dive: Vue Translation with vue-i18next<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/nuxt-js-tutorial-i18n\/\">The Only Nuxt.js Tutorial on I18n You\u2019ll Ever Need<\/a> (Vue-based)<\/li>\n<\/ul>\n<h3><span class=\"ez-toc-section\" id=\"localization-articles-for-other-frameworks\"><\/span>Localization articles for other frameworks<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>We realize that some of our readers might be using Svelte, Next, or another framework, so we always write about the latest and greatest. Here\u2019s a selection of guides to localizing the app you\u2019re building in your favorite framework:<\/p>\n<ul>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/how-to-localize-a-svelte-app-with-svelte-i18n\/\">How to Localize a Svelte App with svelte-i18n<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/\">A Step-by-Step Guide to Svelte Localization with svelte-i18n v3<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/solidjs-localization-i18next\/\">Localizing SolidJS Applications with I18next<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/localizing-mithril-applications\/\">Localizing Mithril Applications<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/localizing-aureliajs-applications\/\">How to Localize Apps Using the Aurelia Framework<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/localizing-stimulusjs-i18next\/\">Localizing StimulusJS Applications With I18next<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/full-stack-javascript-i18n\/\">Full-Stack JavaScript I18n Step by Step<\/a> (using Next.js and Sails.js)<\/li>\n<\/ul>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> On a more specific note: If you&#8217;re going to be working with international phone numbers, make sure you take a look at the <a href=\"https:\/\/phrase.com\/blog\/posts\/libphonenumber-international-phone-numbers\/\">libphonenumber library<\/a>. It&#8217;s framework-agnostic!<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-a-react-app-with-i18next\"><\/span>How do I localize a React App with i18next?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>As promised, we\u2019ll blast through a quick guide to localizing our React apps with the i18next library. We\u2019ve covered i18next earlier in this article, so we\u2019ll focus on its React integration here.<\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> <a href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/\">A Guide to React Localization with i18next<\/a> goes broader and deeper than our brief overview here.<\/p>\n<p>First, we\u2019ll take our trusty demo app and break it up into React components.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"08d116ad-cce1-45d7-b2aa-3e30e3c99bf5\" data-enlighter-title=\"src\/App.js\">import \".\/App.css\";\nimport Navbar from \".\/layout\/Navbar\";\nfunction App() {\n  return (\n    &lt;div className=\"container\"&gt;\n      &lt;Navbar \/&gt;\n      &lt;h1&gt;React i18n&lt;\/h1&gt;\n      &lt;p&gt;\n        Welcome to my little spot on the interwebs, user\n      &lt;\/p&gt;\n      &lt;p&gt;You have count new messages&lt;\/p&gt;\n    &lt;\/div&gt;\n  );\n}\nexport default App;\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"08acb43f-245a-4d89-b514-85c2056fd238\" data-enlighter-title=\"src\/layout\/Navbar.js\">import LocaleSwitcher from \"..\/features\/LocaleSwitcher\";\nfunction Navbar() {\n  return (\n    &lt;nav className=\"navbar\"&gt;\n      &lt;div className=\"container\"&gt;\n        &lt;ul className=\"navbar-list navbar-start\"&gt;\n          &lt;li className=\"navbar-item\"&gt;\n            &lt;a href=\"#\" className=\"navbar-link\"&gt;\n              Home\n            &lt;\/a&gt;\n          &lt;\/li&gt;\n          &lt;li className=\"navbar-item\"&gt;\n            &lt;a href=\"#\" className=\"navbar-link\"&gt;\n              About\n            &lt;\/a&gt;\n          &lt;\/li&gt;\n        &lt;\/ul&gt;\n        &lt;div className=\"navbar-end\"&gt;\n          &lt;LocaleSwitcher \/&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/nav&gt;\n  );\n}\nexport default Navbar;\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"ebef0bfc-8f59-4073-ba13-411c09225f66\" data-enlighter-title=\"src\/features\/LocaleSwitcher.js\">function LocaleSwitcher() {\n  return (\n    &lt;&gt;\n      &lt;img\n        alt=\"Translation icon\"\n        src=\"img\/translation-icon@2x.png\"\n        className=\"translation-icon\"\n      \/&gt;\n      &lt;select className=\"locale-switcher\"&gt;\n        &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n        &lt;option value=\"ar\"&gt;Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)&lt;\/option&gt;\n      &lt;\/select&gt;\n    &lt;\/&gt;\n  );\n}\nexport default LocaleSwitcher;\n<\/pre>\n<p>The locale switcher does little at present time, but we\u2019ll remedy that soon. For now, we have a solid starter to localize.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15836 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-starter.png\" alt=\"React demo app in English with i18n library | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-starter.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-starter-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-starter-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-starter-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"library-installation\"><\/span>Library installation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>In addition to i18next, we\u2019ll grab the official <a href=\"https:\/\/react.i18next.com\/\">react-i18next <\/a> integration framework, which makes using i18next with React a breeze. From the project root, let\u2019s run the following in the command line to install the libraries.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">npm install i18next react-i18next\n<\/pre>\n<p>Next, let\u2019s initialize i18next, <code>use()<\/code>ing the React integration as we do. The most basic way to provide translations to i18next is using the <code>resources<\/code> option when initializing.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"89a353c7-4c36-417b-a10f-68d97405c398\" data-enlighter-title=\"src\/services\/i18n.js\">import i18next from \"i18next\";\nimport { initReactI18next } from \"react-i18next\";\ni18next.use(initReactI18next).init({\n  resources: {\n    en: {\n      translation: {\n        \"app-title\": \"With React\",\n      },\n    },\n    ar: {\n      translation: {\n        \"app-title\": \"\u0645\u0639 \u0631\u064a\u0623\u0643\u062a\",\n      },\n    },\n  },\n  lng: \"en\",\n  debug: true,\n  interpolation: {\n    escapeValue: false,\n  },\n});\nexport default i18next;\n<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> We set the <code>interpolation.escapeValue<\/code> to <code>false<\/code> to <a href=\"https:\/\/www.i18next.com\/translation-function\/interpolation#unescape\">disable the default escaping that i18next does for protection against XSS attacks<\/a>, since React does that for us anyway.<\/p>\n<p>Let\u2019s pull our module into our root <code>index.js<\/code> so we can initialize i18next when our app loads.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"e0298a27-1532-4aa8-a85d-17ce5bf38526\" data-enlighter-title=\"src\/index.js\" data-enlighter-highlight=\"5\">import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport \".\/lib\/skeleton\/normalize.css\";\nimport \".\/lib\/skeleton\/skeleton.css\";\nimport \".\/services\/i18n\";\nimport App from \".\/App\";\nimport reportWebVitals from \".\/reportWebVitals\";\nReactDOM.render(\n  &lt;React.StrictMode&gt;\n    &lt;App \/&gt;\n  &lt;\/React.StrictMode&gt;,\n  document.getElementById(\"root\"),\n);\n\/\/ ...\n<\/pre>\n<h3><span class=\"ez-toc-section\" id=\"basic-translations-2\"><\/span>Basic translations<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The familiar <code>i18next.t()<\/code> translation function can be used in our React components. We just need to import the <code>useTranslation<\/code> React hook to make <code>t<\/code> available.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-highlight=\"1,6,12\">import { useTranslation } from \"react-i18next\";\nimport Navbar from \".\/layout\/Navbar\";\nimport \".\/App.css\";\nfunction App() {\n  const { t } = useTranslation();\n  return (\n    &lt;div className=\"container\"&gt;\n      &lt;Navbar \/&gt;\n      &lt;h1&gt;{t(\"app-title\")}&lt;\/h1&gt;\n      \/\/ ...\n    &lt;\/div&gt;\n  );\n}\nexport default App;\n<\/pre>\n<p>When our app reloads, everything should look the same. However, if we change the <code>lng<\/code> value to <code>\"ar\"<\/code> in our <code>src\/services\/i18n.js<\/code> initializer, we should see our app title localized to Arabic.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15837 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-basic-ar.png\" alt=\"React demo app with i18n library and Arabic title | Phrase \" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-basic-ar.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-basic-ar-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-basic-ar-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-basic-ar-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"async-translation-file-loading-2\"><\/span>Async translation file loading<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Let\u2019s make our app more scalable by breaking up our translations into per-locale files. The handy, official <a href=\"https:\/\/github.com\/i18next\/i18next-http-backend\">i18next-http-backend<\/a> plugin makes this a quick job for us. Let\u2019s install it.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">npm install i18next-http-backend\n<\/pre>\n<p>The backend will look for files at <code>\/locales\/{{lng}}\/{{ns}}.json<\/code> by default, where <code>{{lng}}<\/code> resolves to the active locale, and <code>{{ns}}<\/code> resolves to the active namespace. Since the default namespace is <code>translation<\/code>, we can put our English translations messages into <code>public\/locales\/en\/translation.json<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"22ff6381-59c4-4834-85c2-b2485183bb89\" data-enlighter-title=\"public\/locales\/en\/translation.json\">{\n  \"app-title\": \"With React\",\n  \"home\": \"Home\",\n  \"about\": \"About\"\n}\n<\/pre>\n<p>Our Arabic file follows the same convention:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"081af770-67c2-44e7-a080-d8082020f2ec\" data-enlighter-title=\"public\/locales\/ar\/translation.json\">{\n  \"app-title\": \"\u0645\u0639 \u0631\u064a\u0623\u0643\u062a\",\n  \"home\": \"\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629\",\n  \"about\": \"\u0646\u0628\u0630\u0629 \u0639\u0646\u0627\"\n}\n<\/pre>\n<p>We now just have to import the backend and <code>use()<\/code> it as we initialize i18next. We\u2019ll also want to remove our inlined translations under the <code>resources<\/code> key since the plugin will load our translation messages from the network now.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"a8be33ed-ee58-4094-849b-8c996a1750fa\" data-enlighter-title=\"src\/services\/i18n.js\" data-enlighter-highlight=\"3,7,10\">import i18next from \"i18next\";\nimport { initReactI18next } from \"react-i18next\";\nimport HttpApi from \"i18next-http-backend\";\ni18next\n  .use(initReactI18next)\n  .use(HttpApi)\n  .init({\n    \/\/ Remove `resources`\n    lng: \"en\",\n    debug: true,\n    interpolation: {\n      escapeValue: false,\n    },\n  });\nexport default i18next;\n<\/pre>\n<p>When our app reloads, we should see the same exact localized rendering. Of course, the active locale\u2019s translations are now piping down the network, so our app is lighter on load, and easier to scale and maintain.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15838 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-async.png\" alt=\"React demo app with asynchronous translation file loading | Phrase\" width=\"1434\" height=\"1234\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-async.png 1434w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-async-300x258.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-async-1024x881.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-async-768x661.png 768w\" sizes=\"(max-width: 1434px) 100vw, 1434px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"language-switcher-3\"><\/span>Language switcher<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Building a language-switching UI is a breeze with React and i18next. We can update our <code>LocaleSwitcher<\/code> component, controlling the <code>&lt;select&gt;<\/code> within to change the active locale to the one the user chooses.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"686af5ef-bb97-4677-b31f-3e6b0710d4f7\" data-enlighter-title=\"src\/features\/LocaleSwitcher.js\" data-enlighter-highlight=\"1,4,16,17,18,19\">import { useTranslation } from \"react-i18next\";\nfunction LocaleSwitcher() {\n  const { i18n } = useTranslation();\n  return (\n    &lt;&gt;\n      &lt;img\n        src=\"img\/translation-icon@2x.png\"\n        alt=\"Translation icon\"\n        className=\"translation-icon\"\n      \/&gt;\n      &lt;select\n        className=\"locale-switcher\"\n        value={i18n.language}\n        onChange={(e) =&gt;\n          i18n.changeLanguage(e.target.value)\n        }\n      &gt;\n        &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n        &lt;option value=\"ar\"&gt;Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)&lt;\/option&gt;\n      &lt;\/select&gt;\n    &lt;\/&gt;\n  );\n}\nexport default LocaleSwitcher;\n<\/pre>\n<p>The i18next React integration ensures that translations are re-rendered when the active locale is changed.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15839 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/react-lang-switcher.gif\" alt=\"React demo app with functioning language switcher | Phrase\" width=\"600\" height=\"217\" \/><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Get the code for everything we\u2019ve built above from <a href=\"https:\/\/github.com\/PhraseApp-Blog\/javascript-l10n-ultimate-guide\/tree\/main\/react\">our GitHub repo<\/a>.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"react-localization-articles\"><\/span>React localization articles<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>We love writing about React in our blog so we\u2019re happy to give you a selection of our deep dives and React-based framework tutorials, all centered on localization:<\/p>\n<ul>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/\">A Guide to React Localization with i18next<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/\">Beginning JavaScript I18n with i18next and Moment.js<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/react-redux-tutorial-internationalization-with-react-i18n-redux\/\">React Redux Tutorial: Internationalization with react-i18n-redux<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/roll-your-own-i18n-solution-react-redux\/\">Roll Your Own i18n Solution with React and Redux<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/localizing-javascript-react-apps-with-linguijs\/\">Localizing JavaScript &amp; React Apps with LinguiJS<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/localized-server-side-rendering-with-react\/\">Localized Server-Side Rendering with React<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/localizing-meteor-applications-react\/\">Localizing Meteor Applications Powered by React<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/react-native-i18n-with-expo-and-i18next-part-1\/\">A Comprehensive Guide to React Native Localization<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/i18n-with-gatsby\/\">All You Need to Know About i18n with Gatsby<\/a> (React-based)<\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/full-stack-javascript-i18n\/\">Full-Stack JavaScript I18n Step by Step<\/a> (using the React-based Next.js)<\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-a-web-page-with-jquery-and-i18next\"><\/span>How do I localize a web page with jQuery and i18next?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>While not as hot as it was some years ago, <a href=\"https:\/\/jquery.com\/\">jQuery<\/a> is still one of the most popular JavaScript libraries in use today. You\u2019ll find localizing jQuery apps is pretty easy with the <a href=\"https:\/\/www.i18next.com\/\">i18next<\/a> library. An <a href=\"https:\/\/github.com\/i18next\/jquery-i18next\">official i18next jQuery plugin<\/a> takes very little work to set up, so let\u2019s use it to localize our trusty demo app.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> We\u2019ve covered <a href=\"#How_do_I_localize_a_web_page_with_i18next\">i18next in more detail<\/a> earlier in this article, so we\u2019ll focus on jQuery integration here.<\/p>\n<p>If you\u2019ve been reading along, the following starter app will look familiar. It serves as a nice foundation for our localization work.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"93953e8f-9e89-4fa3-a120-0357ddc3d91b\" data-enlighter-title=\"index.html\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;nav class=\"navbar\"&gt;\n      &lt;div class=\"container\"&gt;\n        &lt;ul class=\"navbar-list navbar-start\"&gt;\n          &lt;li class=\"navbar-item\"&gt;\n            &lt;a href=\"#\" data-i18n=\"home\" class=\"navbar-link\"&gt;\n              Home\n            &lt;\/a&gt;\n          &lt;\/li&gt;\n          &lt;li class=\"navbar-item\"&gt;\n            &lt;a href=\"#\" data-i18n=\"about\" class=\"navbar-link\"&gt;\n              About\n            &lt;\/a&gt;\n          &lt;\/li&gt;\n        &lt;\/ul&gt;\n        &lt;div class=\"navbar-end\"&gt;\n          &lt;img src=\"img\/translation-icon@2x.png\" class=\"translation-icon\" \/&gt;\n          &lt;select data-i18n-switcher class=\"locale-switcher\"&gt;\n            &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n            &lt;option value=\"ar\"&gt;Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)&lt;\/option&gt;\n          &lt;\/select&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/nav&gt;\n    &lt;h1 data-i18n=\"app-title\"&gt;jQuery i18n&lt;\/h1&gt;\n    &lt;p data-i18n=\"lead\" data-i18n-options='{\"username\": \"Jackie\"}'&gt;\n      Welcome to my little spot on the interwebs, {{username}}!\n    &lt;\/p&gt;\n    &lt;p data-i18n=\"new-messages\" data-i18n-options='{\"count\": 3}'&gt;\n      You have {{count}} new messages\n    &lt;\/p&gt;\n  &lt;\/div&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> By default, the i18next jQuery plugin uses <code>data-i18n<\/code> (not our previous <code>data-i18n-key<\/code>) for its translation keys. This <a href=\"https:\/\/github.com\/i18next\/jquery-i18next#initialize-the-plugin\">can be changed in the plugin options<\/a>.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15883 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-start.png\" alt=\"jQuery demo app | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-start.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-start-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-start-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-start-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/div>\n<p>Time to localize? Let\u2019s go!<\/p>\n<h3><span class=\"ez-toc-section\" id=\"installation-3\"><\/span>Installation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The simplest way to install i18next and its jQuery plugin is to download their minified distribution files and include them in our HTML. You can grab the files at the following locations.<\/p>\n<ul>\n<li><a href=\"https:\/\/code.jquery.com\/jquery-3.6.0.min.js\">jQuery minified<\/a><\/li>\n<li><a href=\"https:\/\/unpkg.com\/i18next\/dist\/umd\/i18next.min.js\">i18next minified<\/a><\/li>\n<li><a href=\"https:\/\/raw.githubusercontent.com\/i18next\/jquery-i18next\/master\/jquery-i18next.min.js\">jQuery i18next plugin minified<\/a><\/li>\n<\/ul>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Check out the <a href=\"https:\/\/github.com\/i18next\/jquery-i18next#introduction\">official documentation of the i18next jQuery plugin<\/a> on GitHub.<\/p>\n<p>After downloading the above files, we can place them in a <code>js\/lib<\/code> directory in our project and pull them into our main HTML page.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"699ed787-7851-4f9d-a589-affb7b514112\" data-enlighter-title=\"index.html\" data-enlighter-highlight=\"13,14,15,17,18\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n   &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;!-- ... --&gt;\n  &lt;\/div&gt;\n  &lt;script src=\".\/js\/lib\/jquery-3.6.0.min.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/js\/lib\/i18next.min.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/js\/lib\/jquery-i18next.min.js\"&gt;&lt;\/script&gt;\n  &lt;!-- Our custom JavaScript, coming up in a second... --&gt;\n  &lt;script src=\".\/js\/scripts.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<h3><span class=\"ez-toc-section\" id=\"basic-translations-3\"><\/span>Basic translations<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>With our libraries installed, we can now write some setup code to get basic localization working.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"e193e879-0e13-4ea2-916d-6237d777b668\" data-enlighter-title=\"js\/scripts.js\">\/\/ Initialize i18next\ni18next.init({\n  lng: \"en\",     \/\/ Initial locale\n  debug: true,   \/\/ Provides helpful console messages\n  resources: {   \/\/ Translations\n    en: {\n      translation: {\n        \"app-title\": \"jQuery + i18next\",\n      },\n    },\n    ar: {\n      translation: {\n        \"app-title\": \"\u062c\u064a \u0643\u0648\u064a\u0631\u064a + \u0622\u064a \u0625\u064a\u062a\u064a\u0646 \u0646\u064a\u0643\u0633\u062a\",\n      },\n    },\n  },\n});\n\/\/ Initialize i18next jQuery plugin\njqueryI18next.init(i18next, $);\n\/\/ Translate page elements\n$(\"body\").localize();\n<\/pre>\n<p>We\u2019ve covered <code>i18next.init(...)<\/code> earlier in this article. Note that here we have a <code>jQueryI18next.init(...)<\/code> call as well. At its most basic, the jQuery i18next plugin <code>init<\/code> function takes the active <code>i18next<\/code> instance, as well as a reference to the jQuery object, <code>$<\/code>.<br \/>\nWhen the plugin is initialized, it adds a <code>localize()<\/code> function to jQuery. Calling <code>$(selector).localize()<\/code> causes the plugin to localize all the elements under the selected hierarchy. For each element, if a <code>data-i18n<\/code> attribute is found, its corresponding translation in the active locale is swapped in.<br \/>\nFor example:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">\/\/ In our JavaScript\ni18next.init({\n  lng: \"en\",\n  resources: {\n    en: {\n      translation: {\n        \"app-title\": \"jQuery + i18next\",\n      },\n    },\n    ar: {\n      translation: {\n        \"app-title\": \"\u062c\u064a \u0643\u0648\u064a\u0631\u064a + \u0622\u064a \u0625\u064a\u062a\u064a\u0646 \u0646\u064a\u0643\u0633\u062a\",\n      },\n    },\n  },\n});\njqueryI18next.init(i18next, $);\n$(\"#main-title\").localize();\n\/\/ In our HTML\n&lt;h1 id=\"main-title\" data-i18n=\"app-title\"&gt;&lt;\/h1&gt;\n\/\/ Renders to:\n&lt;h1 id=\"main-title\" data-i18n=\"app-title\"&gt;jQuery + i18next&lt;\/h1&gt;\n<\/pre>\n<p>Nice and simple \ud83d\ude0a<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15884 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-install-en.png\" alt=\"jQuery demo app with i18next library loaded English locale | Phrase\" width=\"1430\" height=\"1284\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-install-en.png 1430w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-install-en-300x269.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-install-en-1024x919.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-install-en-768x690.png 768w\" sizes=\"(max-width: 1430px) 100vw, 1430px\" \/><\/div>\n<p>And if we change our initial locale to Arabic by changing <code>lng<\/code> to <code>\"ar\"<\/code>, we get an Arabic title instead.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15885 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-install-ar.png\" alt=\"jQuery demo app with i18next library loaded Arabic locale | Phrase\" width=\"1434\" height=\"1280\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-install-ar.png 1434w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-install-ar-300x268.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-install-ar-1024x914.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-install-ar-768x686.png 768w\" sizes=\"(max-width: 1434px) 100vw, 1434px\" \/><\/div>\n<h3><span class=\"ez-toc-section\" id=\"async-translation-file-loading-3\"><\/span>Async translation file loading<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p><em>What about splitting our translation files into separate files, one per locale?<\/em> I hear you asking. No worries, the <a href=\"https:\/\/github.com\/i18next\/i18next-http-backend\">official i18next HTTP backend plugin<\/a> has us covered.<br \/>\nTo install the plugin, we can <a href=\"https:\/\/raw.githubusercontent.com\/i18next\/i18next-http-backend\/master\/i18nextHttpBackend.min.js\">grab the minified distribution script from GitHub<\/a> and place it in our <code>js\/lib<\/code> directory. Of course, we\u2019ll want to pull it into our HTML as well.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"11a726ed-213c-4c67-a446-3420034397dd\" data-enlighter-title=\"index.html\" data-enlighter-highlight=\"15\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n   &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;!-- ... --&gt;\n  &lt;\/div&gt;\n  &lt;script src=\".\/js\/lib\/jquery-3.6.0.min.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/js\/lib\/i18next.min.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/js\/lib\/i18nextHttpBackend.min.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/js\/lib\/jquery-i18next.min.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/js\/scripts.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>Now we can make our app more scalable by moving our translations into separate JSON files.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"39d7927a-c499-4d2a-b8db-34d65015996c\" data-enlighter-title=\"locales\/en\/translation.json\">{\n  \"app-title\": \"With jQuery + i18next\",\n  \"home\": \"Home\",\n  \"about\": \"About\"\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"37d46e00-a79c-4812-82e9-a89ba2ee6aa5\" data-enlighter-title=\"locales\/ar\/translation.json\">{\n  \"app-title\": \"\u0645\u0639 \u062c\u064a \u0643\u0648\u064a\u0631\u064a \u0648 \u0622\u064a \u0625\u064a\u062a\u064a\u0646 \u0646\u064a\u0643\u0633\u062a\",\n  \"home\": \"\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629\",\n  \"about\": \"\u0646\u0628\u0630\u0629 \u0639\u0646\u0627\"\n}\n<\/pre>\n<p>We\u2019ll have to rework our setup code to <code>use()<\/code> the HTTP plugin as we initialize i18next. We\u2019ll also want to wait for our initial locale\u2019s translation file to download before we attempt to translate our page elements.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"19886823-97b7-4f73-8fe9-7f1d0af23104\" data-enlighter-title=\"js\/scripts.js\" data-enlighter-highlight=\"1,2,3,4,5,8,9,21,22,23,24\">\/\/ Wait for translations to come down the network\n\/\/ before initializing the jQuery plugin\nasync function initI18n() {\n  \/\/ Use Http backend plugin to download translations\n  await i18next.use(i18nextHttpBackend).init({\n    lng: \"en\",\n  \/\/ Remove `resources` option, since our translations\n  \/\/ are in JSON files now\n  });\n  jqueryI18next.init(i18next, $);\n}\n\/\/ Refactor to function\nfunction translatePage() {\n  $(\"body\").localize();\n}\n\/\/ Init\n(async function () {\n  \/\/ Wait for i18next to initialize before\n  \/\/ translating page elements\n  await initI18n();\n  translatePage();\n})();\n<\/pre>\n<p>If we reload our app, we notice no difference in the output. However, a closer look at our dev tools Network tab reveals a more maintainable \u201cdownload it when you need it\u201d translation file solution.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15886 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-async.png\" alt=\"jQuery demo app with asynchronous translation file loading | Phrase\" width=\"1432\" height=\"1454\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-async.png 1432w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-async-295x300.png 295w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-async-1009x1024.png 1009w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-async-768x780.png 768w\" sizes=\"(max-width: 1432px) 100vw, 1432px\" \/><\/div>\n<h3><span class=\"ez-toc-section\" id=\"language-switcher-4\"><\/span>Language switcher<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>You may have noticed that we have some HTML looking like a language switcher UI in our demo.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"49b71021-1b5e-49ca-ac02-f2af3f97ab97\" data-enlighter-title=\"index.html\" data-enlighter-highlight=\"18,19,20,21\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;nav class=\"navbar\"&gt;\n      &lt;div class=\"container\"&gt;\n        &lt;!-- ... --&gt;\n        &lt;div class=\"navbar-end\"&gt;\n          &lt;img src=\"img\/translation-icon@2x.png\" class=\"translation-icon\" \/&gt;\n          &lt;select data-i18n-switcher class=\"locale-switcher\"&gt;\n            &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n            &lt;option value=\"ar\"&gt;Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)&lt;\/option&gt;\n          &lt;\/select&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/nav&gt;\n    &lt;!-- ... --&gt;\n  &lt;\/div&gt;\n  &lt;!-- ... --&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>Let\u2019s wire up the <code>&lt;select&gt;<\/code> element above so that our app switches its translations to ones matching the user-selected locale.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"5b4b7415-ce8e-4c83-b94f-a48864669b90\" data-enlighter-title=\"js\/scripts.js\" data-enlighter-highlight=\"3,4,6,7,9,11,12,13,14,15,16,17,18,23\">\/\/ ...\nfunction bindLocaleSwitcher() {\n  const $switcher = $(\"[data-i18n-switcher]\");\n  \/\/ Initial value\n  $switcher.val(i18next.language);\n  $switcher.on(\"change\", async function () {\n    \/\/ Changing the active locale will cause its\n    \/\/ translations to load from the network, so\n    \/\/ we wait for that load before refreshing\n    \/\/ page elements\n    await i18next.changeLanguage($switcher.val());\n    translatePage();\n  });\n}\n(async function () {\n  await initI18n();\n  translatePage();\n  bindLocaleSwitcher();\n})();\n<\/pre>\n<p>Just like that, we have a functional locale switcher.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15887 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-locale-switcher.gif\" alt=\"jQuery demo app with language switcher | Phrase\" width=\"600\" height=\"217\" \/><\/div>\n<h3><span class=\"ez-toc-section\" id=\"interpolation-4\"><\/span>Interpolation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Handling dynamic values in our translation messages comes out of the box with i18next. We just need to provide a <code>data-i18n-options<\/code> JSON map in the element we have interpolations for.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"7d27ccfb-de5d-4985-86c1-a33c28264797\" data-enlighter-title=\"index.html\">&lt;!-- ... --&gt;\n    &lt;p data-i18n=\"lead\" data-i18n-options='{\"username\": \"Jackie\"}'&gt;\n      Welcome to my little spot on the interwebs, {{username}}!\n    &lt;\/p&gt;\n&lt;!-- ... --&gt;\n<\/pre>\n<p>The i18next jQuery plugin doesn\u2019t look for <code>data-i18n-options<\/code> by default; we have to use its <code>useOptionsAttr<\/code> <a href=\"https:\/\/github.com\/i18next\/jquery-i18next#initialize-the-plugin\">config option<\/a> to enable automatic interpolation.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"1094953e-ed26-4d2c-b79f-afb14132c03c\" data-enlighter-title=\"js\/scripts.js\" data-enlighter-highlight=\"4\">async function initI18n() {\n  await i18next.use(i18nextHttpBackend).init({ lng: \"en\" });\n  jqueryI18next.init(i18next, $, { useOptionsAttr: true });\n}\n\/\/ ...\n<\/pre>\n<p>Of course, we\u2019ll want to ensure that we have the <code>{{variable}}<\/code> placeholders that i18next expects in our translation messages.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"2fda0df5-e0a2-4e37-942c-b20d43bacf1a\" data-enlighter-title=\"locales\/en\/translation.json\" data-enlighter-highlight=\"5\">{\n  \"app-title\": \"With jQuery + i18next\",\n  \"home\": \"Home\",\n  \"about\": \"About\",\n  \"lead\": \"Welcome to my little spot on the interwebs, {{username}}!\"\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"8804283f-f7e0-4a9b-82a3-2e3b9d26fb58\" data-enlighter-title=\"locales\/ar\/translation.json\" data-enlighter-highlight=\"5\">{\n  \"app-title\": \"\u0645\u0639 \u062c\u064a \u0643\u0648\u064a\u0631\u064a \u0648 \u0622\u064a \u0625\u064a\u062a\u064a\u0646 \u0646\u064a\u0643\u0633\u062a\",\n  \"home\": \"\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629\",\n  \"about\": \"\u0646\u0628\u0630\u0629 \u0639\u0646\u0627\",\n  \"lead\": \"\u0623\u0647\u0644\u0627\u064b \u0628\u0643 \u0641\u064a \u0645\u0643\u0627\u0646\u064a \u0627\u0644\u0635\u063a\u064a\u0631 \u0639\u0644\u0649 \u0627\u0644\u0646\u062a \u064a\u0627 {{username}}.\"\n}\n<\/pre>\n<p>There we go. Interpolations interpolated.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15888 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-interpolation-en.png\" alt=\"English lead paragraph with interpolation | Phrase\" width=\"824\" height=\"98\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-interpolation-en.png 824w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-interpolation-en-300x36.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-interpolation-en-768x91.png 768w\" sizes=\"(max-width: 824px) 100vw, 824px\" \/><\/div>\n<div><\/div>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15889 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-interpolation-ar.png\" alt=\"Arabic lead paragraph with interpolation | Phrase\" width=\"542\" height=\"102\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-interpolation-ar.png 542w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-interpolation-ar-300x56.png 300w\" sizes=\"(max-width: 542px) 100vw, 542px\" \/><\/div>\n<h3><span class=\"ez-toc-section\" id=\"plurals-4\"><\/span>Plurals<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>We use the same <code>data-i18n-options<\/code> attribute to provide a special <code>count<\/code> number variable when we have plural messages.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> Check out the earlier <a href=\"#Plurals-3\">i18next \u279e Plurals<\/a> section for more details on how the library works with plurals.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"9ba58e5c-c7d1-49b9-8baf-4acd07bade77\" data-enlighter-title=\"index.html\">&lt;!-- ... --&gt;\n    &lt;p data-i18n=\"new-messages\" data-i18n-options='{\"count\": 3}'&gt;\n      You have {{count}} new messages\n    &lt;\/p&gt;\n&lt;!-- ... --&gt;\n<\/pre>\n<p>We use a <code>key_form<\/code> convention when specifying our plural messages. English has two plural forms, <code>one<\/code> and <code>other<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"77d50ab2-ce8e-44a9-b61e-40b6146e4c19\" data-enlighter-title=\"locales\/en\/translation.json\">{\n  \/\/ ...\n  \"new-messages_one\": \"You have {{count}} new message\",\n  \"new-messages_other\": \"You have {{count}} new messages\"\n}\n<\/pre>\n<p>Arabic has six plural forms, and they\u2019re handled automatically by i18next.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"8cf54801-115c-44bd-b70d-4be380c413c6\" data-enlighter-title=\"locales\/ar\/translation.json\">{\n  \/\/ ...\n  \"new-messages_zero\": \"\u0644\u0627 \u062a\u0648\u062c\u062f \u0644\u062f\u064a\u0643 \u0631\u0633\u0627\u0626\u0644 \u062c\u062f\u064a\u062f\u0629\",\n  \"new-messages_one\": \"\u0644\u062f\u064a\u0643 \u0631\u0633\u0627\u0644\u0629 \u062c\u062f\u064a\u062f\u0629\",\n  \"new-messages_two\": \"\u0644\u062f\u064a\u0643 \u0631\u0633\u0627\u0644\u062a\u0627\u0646 \u062c\u062f\u0627\u062f\",\n  \"new-messages_few\": \"\u0644\u062f\u064a\u0643 {{count}} \u0631\u0633\u0627\u0626\u0644 \u062c\u062f\u064a\u062f\u0629\",\n  \"new-messages_many\": \"\u0644\u062f\u064a\u0643 {{count}} \u0631\u0633\u0627\u0644\u0629 \u062c\u062f\u064a\u062f\u0629\",\n  \"new-messages_other\": \"\u0644\u062f\u064a\u0643 {{count}} \u0631\u0633\u0627\u0644\u0629 \u062c\u062f\u064a\u062f\u0629\"\n}\n<\/pre>\n<p>So with no additional code, our app has complex plural support.<\/p>\n<div><strong style=\"color: #ff6600;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15891 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-plurals-ar.png\" alt=\"English locale jQuery demo app with pluralization | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-plurals-ar.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-plurals-ar-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-plurals-ar-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-plurals-ar-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/strong><\/div>\n<div><\/div>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15890 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-plurals-en.png\" alt=\"English locale jQuery demo app with pluralization | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-plurals-en.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-plurals-en-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-plurals-en-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/jquery-plurals-en-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/div>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Get the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/javascript-l10n-ultimate-guide\/tree\/main\/jquery\">full working code for the app above from our GitHub repo<\/a>.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> If you\u2019re looking for an alternative to i18next, check out <a href=\"https:\/\/phrase.com\/blog\/posts\/jquery-i18n-the-advanced-guide\/\">The Advanced Guide to jQuery i18n<\/a> which uses the <a href=\"https:\/\/github.com\/wikimedia\/jquery.i18n\">jQuery.i18n<\/a> library.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-a-web-page-with-the-icu-format-using-globalize\"><\/span>How do I localize a web page with the ICU format using Globalize?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>If you want the near-exhaustive localization coverage of the <a href=\"https:\/\/icu.unicode.org\/home\">International Components for Unicode (ICU)<\/a> and the Unicode <a href=\"https:\/\/cldr.unicode.org\/\">Common Locale Data Repository (CLDR)<\/a> in your JavaScript app, the <a href=\"https:\/\/github.com\/globalizejs\/globalize\">Globalize<\/a> library will certainly get the job done. Let\u2019s run through how to install Globalize and localize our humble demo app with it.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> <a href=\"https:\/\/phrase.com\/blog\/posts\/guide-to-the-icu-message-format\/\">The Missing Guide to the ICU Message Format<\/a> covers what the ICU and CLDR are in more detail.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> Unlike many other libraries we cover in this article, Globalize <em>does<\/em> support Internet Explorer 9+.<\/p>\n<p><em>What demo?<\/em> you say. Our trusty one-pager, of course.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"3120f7b6-18a4-4377-a423-5d0f2490a644\" data-enlighter-title=\"index.html\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;nav class=\"navbar\"&gt;\n      &lt;div class=\"container\"&gt;\n        &lt;ul class=\"navbar-list navbar-start\"&gt;\n          &lt;li class=\"navbar-item\"&gt;\n            &lt;a href=\"#\" data-i18n-key=\"home\" class=\"navbar-link\"&gt;\n              Home\n            &lt;\/a&gt;\n          &lt;\/li&gt;\n          &lt;li class=\"navbar-item\"&gt;\n            &lt;a href=\"#\" data-i18n-key=\"about\" class=\"navbar-link\"&gt;\n              About\n            &lt;\/a&gt;\n          &lt;\/li&gt;\n        &lt;\/ul&gt;\n        &lt;div class=\"navbar-end\"&gt;\n          &lt;img src=\"img\/translation-icon@2x.png\" class=\"translation-icon\" \/&gt;\n          &lt;select data-i18n-switcher class=\"locale-switcher\"&gt;\n            &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n            &lt;option value=\"ar\"&gt;Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)&lt;\/option&gt;\n          &lt;\/select&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/nav&gt;\n    &lt;h1 data-i18n-key=\"app-title\"&gt;With Globalize&lt;\/h1&gt;\n    &lt;p data-i18n-key=\"lead\" data-i18n-opt='{\"username\": \"Stella\"}'&gt;\n      Welcome to my little spot on the interwebs, {username}!\n    &lt;\/p&gt;\n    &lt;p data-i18n-key=\"new-messages\" data-i18n-opt='{\"count\": 100}'&gt;\n      You have # new messages\n    &lt;\/p&gt;\n  &lt;\/div&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<div><\/div>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15892 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-start.png\" alt=\"Globalize demo app | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-start.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-start-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-start-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-start-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/div>\n<p>Let&#8217;s start localizing.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"installation-4\"><\/span>Installation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Globalize is very modular, so it can be a bit cumbersome to install with plain JavaScript. Still, it\u2019s a relatively simple recipe.<\/p>\n<ul>\n<li>Download the <a href=\"https:\/\/github.com\/globalizejs\/globalize\/releases\">latest Globalize release<\/a><\/li>\n<li>Download the <a href=\"https:\/\/github.com\/rxaviers\/cldrjs\/releases\">latest CLDR traverser (cldr.js) release<\/a><\/li>\n<li>Download the <a href=\"https:\/\/github.com\/unicode-org\/cldr-json\/releases\">latest CLDR JSON data release<\/a>\u2014make sure to download the <code>-full<\/code> variant from the release list to follow along here<\/li>\n<\/ul>\n<p>This should get us three ZIP files. Let\u2019s unzip them, rename their unzipped top-level directories, and move them to our project directory. I\u2019ve placed mine under a <code>\/lib<\/code> directory in my project, so my project now looks like:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\" data-enlighter-linenumbers=\"false\">.\n\u251c\u2500\u2500 css\/\n\u251c\u2500\u2500 img\/\n\u251c\u2500\u2500 lib\/\n\u2502   \u251c\u2500\u2500 cldr\/          &lt;&lt; renamed from cldr-x.x.x\n\u2502   \u251c\u2500\u2500 cldr-json\/     &lt;&lt; renamed from cldr-x.x.x-json-full\n\u2502   \u2514\u2500\u2500 globalize\/     &lt;&lt; renamed from globalize.x.x\n\u2514\u2500\u2500 index.html\n<\/pre>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The official docs go through <a href=\"https:\/\/github.com\/globalizejs\/globalize#installation\">different ways to install Globalize<\/a>.<\/p>\n<h4>Using the requirements tool to determine required scripts<\/h4>\n<p>We always need some parts of Globalize and cldr.js; others will depend on the localization features of our app. A handy tool, <a href=\"https:\/\/johnnyreilly.github.io\/globalize-so-what-cha-want\/#\/?currency=true&amp;date=true&amp;message=true&amp;number=true&amp;plural=true&amp;relativeTime=true&amp;unit=true\">So What\u2019cha Want<\/a>, can let us know which files to pull into our project depending on our selected features. We can start by deselecting all features except for <em>message<\/em>.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15893 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-reqs-tool.png\" alt=\"So What\u2019cha Want Globalize Screenshot | Phrase\" width=\"2856\" height=\"1262\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-reqs-tool.png 2856w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-reqs-tool-300x133.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-reqs-tool-1024x452.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-reqs-tool-768x339.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-reqs-tool-1536x679.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-reqs-tool-2048x905.png 2048w\" sizes=\"(max-width: 2856px) 100vw, 2856px\" \/><\/div>\n<p>Note the two file lists near the bottom of the page. The list on the left tells which files to pull in from Globalize and cldr.js; we can use <code>&lt;script&gt;<\/code> tags for these.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"1079e969-94cb-435c-8773-0ad911048d64\" data-enlighter-title=\"index.html\" data-enlighter-highlight=\"13,14,15,16,17,18,20,21\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;!-- ... --&gt;\n  &lt;\/div&gt;\n  &lt;!-- Globalize requirements --&gt;\n  &lt;script src=\".\/lib\/cldr\/dist\/cldr.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/lib\/cldr\/dist\/cldr\/event.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/lib\/cldr\/dist\/cldr\/supplemental.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/lib\/globalize\/dist\/globalize.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/lib\/globalize\/dist\/globalize\/message.js\"&gt;&lt;\/script&gt;\n  &lt;!-- Our app entry point --&gt;\n  &lt;script src=\".\/index.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>The list at the bottom-right of <em>So What&#8217;cha Want<\/em> has CLDR JSON data that we need. This JSON we\u2019ll want to fetch in our code and feed to Globalize via its <code>load()<\/code> function.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"819ea5ad-0a88-42b0-9f9a-e3245b463959\" data-enlighter-title=\"index.js\">\/\/ We'll add more to this list as we go\nconst supplementals = [\"likelySubtags\"];\nasync function loadIntoGlobalize(featureUrls) {\n  await Promise.all(\n    featureUrls.map((url) =&gt; fetchJson(url))\n  ).then((downloaded) =&gt;\n    downloaded.forEach((feature) =&gt; Globalize.load(feature))\n  );\n}\nfunction supplementalUrlsFor(options) {\n  return options.map(\n    (feature) =&gt;\n      `\/lib\/cldr-json\/cldr-core\/supplemental\/${feature}.json`\n  );\n}\nasync function fetchJson(url) {\n  const response = await fetch(url);\n  return await response.json();\n}\n(async function () {\n  \/\/ Load supplemental requirements\n  await loadIntoGlobalize(\n    supplementalUrlsFor(supplementals)\n  );\n})();\n<\/pre>\n<p>That\u2019s about it for setup. OK, not the easiest library to install, but for a project that needs bulletproof localization, it\u2019s worth the effort.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"basic-translations-4\"><\/span>Basic translations<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Using Globalize to translate page elements is a simple three-step process.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"7f6d347f-e2aa-4e6a-b077-f960eea2e6fa\" data-enlighter-title=\"index.js\" data-enlighter-highlight=\"8,9,10,11,12,13,14,15,16,18,19,21,22,23,24\">\/\/ ...\n(async function () {\n  await loadIntoGlobalize(\n    supplementalUrlsFor(supplementals)\n  );\n  \/\/ 1. Load translation messages\n  Globalize.loadMessages({\n    en: {\n      \"app-title\": \"Hello Globalize!\",\n    },\n    ar: {\n      \"app-title\": \"\u0623\u0647\u0644\u0627\u064b \u062c\u0644\u0648\u0628\u0627\u0644\u0627\u064a\u0632\",\n    },\n  });\n  \/\/ 2. Set the default locale\n  Globalize.locale(\"en\");\n  \/\/ 3. Use formatMessage() to get translation by key\n  document.querySelector(\n    \"[data-i18n-key='app-title']\"\n  ).textContent = Globalize.formatMessage(\"app-title\");\n})();\n<\/pre>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The official Globalize docs have a <a href=\"https:\/\/github.com\/globalizejs\/globalize#api\">handy API section<\/a> that lists available functions.<\/p>\n<p>Disco. Our title is displayed using our default locale\u2019s translations.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15894 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-basic-en.png\" alt=\"Globalize demo app English translation | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-basic-en.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-basic-en-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-basic-en-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-basic-en-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/div>\n<p>If we change the above call, so that it reads <code>Globalize.locale(\"ar\")<\/code>, we get our app title in Arabic.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15895 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-basic-ar.png\" alt=\"Globalize demo app Arabic translation | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-basic-ar.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-basic-ar-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-basic-ar-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-basic-ar-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/div>\n<h3><span class=\"ez-toc-section\" id=\"handling-missing-message-errors\"><\/span>Handling missing message errors<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Let\u2019s generalize the code that translates page elements by writing a <code>translatePageElements()<\/code> function. Unlike other i18n libraries, Globalize will throw an error and stop if it encounters a missing message for a given key to <code>formatMessage()<\/code>. Nothing a little <code>try\/catch<\/code> won\u2019t soften, though.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"5ea79d29-3db0-486a-896a-e57c7423c91a\" data-enlighter-title=\"index.js\" data-enlighter-highlight=\"3,4,5,6,8,9,11,12,13,14,15,16,17,19,20,21,22,23,24,25,26,35\">\/\/ ...\nfunction translatePageElements() {\n  const elements = document.querySelectorAll(\n    \"[data-i18n-key]\"\n  );\n  elements.forEach((element) =&gt; {\n    const key = element.getAttribute(\"data-i18n-key\");\n    try {\n      element.innerHTML = Globalize.formatMessage(key);\n    } catch (error) {\n      if (error.code === \"E_MISSING_MESSAGE\") {\n        \/\/ Show console warnings on missing message\n        \/\/ instead of grinding to a halt.\n        console.warn(error.message);\n        \/\/ Show key value on page for missing message\n        element.innerHTML = key;\n      } else {\n        console.error(error);\n      }\n    }\n  });\n}\n(async function () {\n  \/\/ ...\n  Globalize.loadMessages(\/* ... *\/);\n  Globalize.locale(\"en\");\n  translatePageElements();\n})();\n<\/pre>\n<p>Less &#8220;crash on missing message&#8221;, more &#8220;show key value and warn in console&#8221;.<\/p>\n<h3><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15910 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-missing-messages.png\" alt=\"Globalize demo app with warning in code | Phrase\" width=\"1432\" height=\"888\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-missing-messages.png 1432w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-missing-messages-300x186.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-missing-messages-1024x635.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-missing-messages-768x476.png 768w\" sizes=\"(max-width: 1432px) 100vw, 1432px\" \/><\/h3>\n<h3><span class=\"ez-toc-section\" id=\"async-translation-file-loading-4\"><\/span>Async translation file loading<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>As we\u2019ve done with other solutions in this article, let\u2019s break up our translation messages into per-locale JSON files for scalability and maintainability.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"fb73dd42-e042-4d6a-9c1c-e96886cf5603\" data-enlighter-title=\"lang\/en.json\">{\n  \/\/ Globalize expects the locale code to be the top-level key\n  \"en\": {\n    \"app-title\": \"With Globalize\",\n    \"home\": \"Home\",\n    \"about\": \"About\"\n  }\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"a710d68f-9adb-4364-b4ef-437ec0c6ff35\" data-enlighter-title=\"lang\/ar.json\">{\n  \"ar\": {\n    \"app-title\": \"\u0645\u0639 \u062c\u0644\u0648\u0628\u0627\u0644\u0627\u064a\u0632\",\n    \"home\": \"\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629\",\n    \"about\": \"\u0646\u0628\u0630\u0629 \u0639\u0646\u0627\"\n  }\n}\n<\/pre>\n<p>A reusable <code>setLocale()<\/code> function can load our translation file, configure Globalize, and re-render our page elements.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"346671e3-919a-4566-a041-8086dea23f25\" data-enlighter-title=\"index.js\" data-enlighter-highlight=\"1,5,6,7,8,9,10,19\">const defaultLocale = \"en\";\n\/\/ ...\n async function setLocale(locale) {\n  const messages = await fetchJson(`\/lang\/${locale}.json`);\n  Globalize.loadMessages(messages);\n  Globalize.locale(locale);\n  translatePageElements();\n}\n\/\/ ...\n(async function () {\n  await loadIntoGlobalize(\n    supplementalUrlsFor(supplementals)\n  );\n  setLocale(defaultLocale);\n})();\n<\/pre>\n<p>Async translation file loading: done and dusted.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"language-switcher-5\"><\/span>Language switcher<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>We can use the <code>setLocale()<\/code> function we just wrote to get our language switching UI working. You may remember that our demo app already has some HTML for the switcher.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"15ce0f80-f807-4c90-9fbd-22d73a8287f6\" data-enlighter-title=\"index.html\" data-enlighter-highlight=\"18,19,20,21\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;nav class=\"navbar\"&gt;\n      &lt;div class=\"container\"&gt;\n        &lt;!-- ... --&gt;\n        &lt;div class=\"navbar-end\"&gt;\n          &lt;img src=\"img\/translation-icon@2x.png\" class=\"translation-icon\" \/&gt;\n          &lt;select data-i18n-switcher class=\"locale-switcher\"&gt;\n            &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n            &lt;option value=\"ar\"&gt;Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)&lt;\/option&gt;\n          &lt;\/select&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/nav&gt;\n    &lt;!-- ... --&gt;\n  &lt;\/div&gt;\n  &lt;!-- ... --&gt;\n  &lt;script src=\".\/index.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>Some JavaScripting will get the switcher switching.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"5a823850-e4e9-473e-9653-d9a5438b1a71\" data-enlighter-title=\"index.js\" data-enlighter-highlight=\"5,6,7,8,10,11,13,14,15,16,25\">const defaultLocale = \"en\";\n\/\/ ...\nfunction bindLocaleSwitcher() {\n  const switcher = document.querySelector(\n    \"[data-i18n-switcher]\"\n  );\n  \/\/ Globalize.locale() returns the active locale\n  switcher.value = Globalize.locale().locale;\n  switcher.onchange = (e) =&gt; {\n    setLocale(e.target.value);\n  };\n}\n(async function () {\n  await loadIntoGlobalize(\n    supplementalUrlsFor(supplementals)\n  );\n  await setLocale(defaultLocale);\n  bindLocaleSwitcher(defaultLocale);\n})();\n<\/pre>\n<p>And with that, our users can pick their language of choice.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15896 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-locale-switcher.gif\" alt=\"Globalize demo app with language switcher | Phrase\" width=\"600\" height=\"217\" \/><\/div>\n<h4>Showing locale display names in the active locale<\/h4>\n<p>One of the most powerful features of using an ICU library like Globalize is the access to an immensely rich variety of CLDR localization data. For example, we can show the languages in our locale switcher <em>in the active locale<\/em>\u2014English would be \u201c\u0627\u0644\u0625\u0646\u062c\u0644\u064a\u0632\u064a\u0629\u201d in Arabic, for example. We would need to pull in the main CLDR data and then update our <code>select &gt; option<\/code> text.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"eda6ed92-1c6d-4d7e-8d19-cbdab81843d6\" data-enlighter-title=\"index.js\" data-enlighter-highlight=\"2,3,4,11,16,19,20,21,22,23,24,25,26,27,28,29,30,32,33,34,35,36,37,38,40,41,43,44,45,46,48,49,51,52,53,54,55,56\">const defaultLocale = \"ar\";\nconst mains = {\n    localenames: [\"languages\"],\n};\nconst supplementals = \"likelySubtags\"];\nasync function setLocale(locale) {\n  const messages = await fetchJson(`\/lang\/${locale}.json`);\n  Globalize.loadMessages(messages);\n  await loadIntoGlobalize(mainUrlsFor(mains, locale));\n  Globalize.locale(locale);\n  translatePageElements();\n  setLocaleSwitcherDisplayNames();\n}\n\/\/ Given options = {\n\/\/   localenames: [\"languages\"],\n\/\/   dates: [\"ca-generic\", \"ca-gregorian\"],\n\/\/ }\n\/\/ and, locale = \"en\", returns an array like\n\/\/ [\n\/\/    \"\/lib\/cldr-json\/cldr-localenames-full\/main\/en\/languages.json\",\n\/\/    \"\/lib\/cldr-json\/cldr-dates-full\/main\/en\/ca-generic.json\",\n\/\/    \"\/lib\/cldr-json\/cldr-dates-full\/main\/en\/ca-gregorian.json\"\n\/\/ ]\nfunction mainUrlsFor(options, locale) {\n  const result = [];\n  Object.keys(options).forEach((key) =&gt; {\n    options[key].forEach((collection) =&gt; {\n      result.push(\n        `\/lib\/cldr-json\/cldr-${key}-full\/main\/${locale}\/${collection}.json`\n      );\n    });\n  });\n  return result;\n}\nfunction setLocaleSwitcherDisplayNames() {\n  const options = document.querySelectorAll(\n    \"[data-i18n-switcher] option\"\n  );\n  options.forEach((option) =&gt; {\n    const localeCode = option.value;\n    \/\/ Get CLDR main data by path\n    option.textContent = Globalize.cldr.main(\n      `localeDisplayNames\/languages\/${localeCode}`\n    );\n  });\n}\n\/\/ ...\n<\/pre>\n<p>We can use our main JSON loading code whenever we want to pull in CLDR JSON main data. Otherwise, just a bit of DOM manipulation when a new locale is chosen gets us localized locale names (so meta!).<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15897 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-locale-display-names.gif\" alt=\"Globalize demo app with localized locale names | Phrase\" width=\"600\" height=\"217\" \/><\/div>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The official cldr.js docs go through <a href=\"https:\/\/github.com\/rxaviers\/cldrjs#get-item-given-its-path\">how to retrieve CLDR JSON data<\/a> in more detail.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"interpolation-5\"><\/span>Interpolation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Handling dynamic values in our message is handled by the ICU message format via a <code>{variable}<\/code> syntax.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"1c1af1b3-a6c4-4113-9def-94a483d7cc7a\" data-enlighter-title=\"lang\/en.json\">{\n  \"en\": {\n    \/\/ ...\n    \"lead\": \"Welcome to my little spot on the interwebs, {username}!\"\n  }\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"175d373e-1962-4588-80c4-f65b80eff5d2\" data-enlighter-title=\"lang\/ar\">{\n  \"ar\": {\n    \/\/ ...\n    \"lead\": \"\u0623\u0647\u0644\u0627\u064b \u0628\u0643 \u0641\u064a \u0645\u0643\u0627\u0646\u064a \u0627\u0644\u0635\u063a\u064a\u0631 \u0639\u0644\u0649 \u0627\u0644\u0646\u062a \u064a\u0627 {username}.\"\n  }\n}\n<\/pre>\n<p>We can provide key\/value substitution pairs in our HTML.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"677e0228-8329-44a2-bcb6-63c6ee81b5c7\" data-enlighter-title=\"index.html\">&lt;!-- ... --&gt;\n    &lt;p data-i18n-key=\"lead\" data-i18n-opt='{\"username\": \"Stella\"}'&gt;\n      Welcome to my little spot on the interwebs, {username}!\n    &lt;\/p&gt;\n&lt;!-- ... --&gt;\n<\/pre>\n<p>And now we just need to read those key\/value pairs and feed them to <code>Globalize.formatMessage()<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"61ef9e83-97f1-453f-8037-ebf3044facce\" data-enlighter-title=\"index.js\" data-enlighter-highlight=\"11,12,13,14,15,20\">\/\/ ...\nfunction translatePageElements() {\n  const elements = document.querySelectorAll(\n    \"[data-i18n-key]\"\n  );\n  elements.forEach((element) =&gt; {\n    const key = element.getAttribute(\"data-i18n-key\");\n    const interpolations =\n      element.getAttribute(\"data-i18n-opt\");\n    const parsedInterpolations = interpolations\n      ? JSON.parse(interpolations)\n      : {};\n    try {\n      element.innerHTML = Globalize.formatMessage(\n        key,\n        parsedInterpolations\n      );\n    } catch (error) {\n      if (error.code === \"E_MISSING_MESSAGE\") {\n        console.warn(error.message);\n      } else {\n        console.error(error);\n      }\n    }\n  });\n}\n\/\/ ...\n<\/pre>\n<p>No problemo.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15898 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-interpolation-en.png\" alt=\"Globalize demo app with interpolation in English locale | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-interpolation-en.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-interpolation-en-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-interpolation-en-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-interpolation-en-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/div>\n<div><\/div>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15899 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-interpolation-ar.png\" alt=\"Globalize demo app with interpolation in Arabic locale | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-interpolation-ar.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-interpolation-ar-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-interpolation-ar-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-interpolation-ar-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/div>\n<div><\/div>\n<h3><span class=\"ez-toc-section\" id=\"plurals-5\"><\/span>Plurals<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Plural handling in the ICU format is second to none and covers complex plural forms like those in Russian or Arabic. We&#8217;ll need to set up the plural feature before we can use it.<\/p>\n<h4>Adding plural requirements<\/h4>\n<p>Let\u2019s head over to <a href=\"https:\/\/johnnyreilly.github.io\/globalize-so-what-cha-want\/#\/?currency=true&amp;date=true&amp;message=true&amp;number=true&amp;plural=true&amp;relativeTime=true&amp;unit=true\">So What\u2019cha Want<\/a> to figure out what we\u2019ll need to pull in if we enable <em>plural<\/em>.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15900 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-reqs.png\" alt=\"So What\u2019cha Want for Globalize Screenshot | Phrase\" width=\"2876\" height=\"1270\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-reqs.png 2876w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-reqs-300x132.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-reqs-1024x452.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-reqs-768x339.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-reqs-1536x678.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-reqs-2048x904.png 2048w\" sizes=\"(max-width: 2876px) 100vw, 2876px\" \/><\/div>\n<p>Not too bad: first we\u2019ll need to add a <code>&lt;script&gt;<\/code> tag for the feature.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"83186151-0cf9-432c-95cf-e4e7b1dbaf29\" data-enlighter-title=\"index.html\" data-enlighter-highlight=\"18\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n  &lt;!-- ... --&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;!-- ... --&gt;\n  &lt;\/div&gt;\n  &lt;script src=\".\/lib\/cldr\/dist\/cldr.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/lib\/cldr\/dist\/cldr\/event.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/lib\/cldr\/dist\/cldr\/supplemental.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/lib\/globalize\/dist\/globalize.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/lib\/globalize\/dist\/globalize\/message.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/lib\/globalize\/dist\/globalize\/plural.js\"&gt;&lt;\/script&gt;\n  &lt;script src=\".\/index.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>We\u2019ll also need the <code>plurals<\/code> supplemental JSON, and likely the <code>ordinals<\/code> if we want to cover formatting like \u201c3rd\u201d and \u201c4th\u201d in our messages. We\u2019ll just need to add the requirements to our <code>supplementals<\/code> config array. Our app is already set up to load the corresponding JSON for us on load.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"1b2d9cb4-79c6-4aa9-960f-8d34d4764fc3\" data-enlighter-title=\"index.js\" data-enlighter-highlight=\"4,5\">const defaultLocale = \"ar\";\nconst supplementals = [\n  \"likelySubtags\",\n  \"plurals\",\n  \"ordinals\",\n];\n\/\/ ...\n<\/pre>\n<p>Now we can add our plural messages. The ICU plural syntax is largely intuitive: a <code>count<\/code> variable determines the chosen plural form, and a special <code>#<\/code> symbol is replaced with the <code>count<\/code> value on render.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"188a8655-10bf-427b-b559-8905194c51e5\" data-enlighter-title=\"lang\/en.json\">{\n  \"en\": {\n    \/\/ ...\n    \"new-messages\": [\n      \/\/ We can call `count` anything we want, as long as we\n      \/\/ match the key in our call to formatMessage()\n      \"You have {count, plural,\",\n      \"    one {# new message}\",\n      \"  other {# new messages}\",\n      \"}\"\n    ]\n  }\n}\n<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> Globalize allows us to break up multiline messages by using arrays.<\/p>\n<p>The six plural forms in Arabic are handled by the ICU format.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"8039927b-6183-45cb-822a-b52ccadd2653\" data-enlighter-title=\"lang\/ar.json\" data-enlighter-linenumbers=\"false\">{\n  \"ar\": {\n    \/\/ ...\n    \"new-messages\": [\n      \"{count, plural, \",\n      \"   zero {\u0644\u0627 \u062a\u0648\u062c\u062f \u0644\u062f\u064a\u0643 \u0631\u0633\u0627\u0626\u0644 \u062c\u062f\u064a\u062f\u0629}\",\n      \"    one {\u0644\u062f\u064a\u0643 \u0631\u0633\u0627\u0644\u0629 \u062c\u062f\u064a\u062f\u0629}\",\n      \"    two {\u0644\u062f\u064a\u0643 \u0631\u0633\u0627\u0644\u062a\u0627\u0646 \u062c\u062f\u0627\u062f}\",\n      \"    few {\u0644\u062f\u064a\u0643 # \u0631\u0633\u0627\u0626\u0644 \u062c\u062f\u064a\u062f\u0629}\",\n      \"   many {\u0644\u062f\u064a\u0643 # \u0631\u0633\u0627\u0644\u0629 \u062c\u062f\u064a\u062f\u0629}\",\n      \"  other {\u0644\u062f\u064a\u0643 # \u0631\u0633\u0627\u0644\u0629 \u062c\u062f\u064a\u062f\u0629}\",\n      \"}\"\n    ]\n  }\n}\n<\/pre>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> We cover ICU plurals in more detail in <a href=\"https:\/\/phrase.com\/blog\/posts\/guide-to-the-icu-message-format\/#Plurals\">The Missing Guide to the ICU Message Format<\/a>.<\/p>\n<p>Of course, we need to make sure that <code>count<\/code> is supplied to <code>Globalize.formatMessage()<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"e9ab1b69-a2cd-44a1-95cf-06d075283840\" data-enlighter-title=\"index.html\" data-enlighter-linenumbers=\"false\">&lt;!-- ... --&gt;\n    &lt;p data-i18n-key=\"new-messages\" data-i18n-opt='{\"count\": 110}'&gt;\n      You have # new messages\n    &lt;\/p&gt;\n&lt;!-- ... --&gt;\n<\/pre>\n<p>And that\u2019s plurals basically done.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15901 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-en.png\" alt=\"Globalize demo app in English with pluralization | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-en.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-en-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-en-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-en-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/div>\n<div><\/div>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-15902 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-ar.png\" alt=\"Globalize demo app in Arabic with pluralization | Phrase\" width=\"1330\" height=\"480\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-ar.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-ar-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-ar-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2021\/12\/globalize-plural-ar-768x277.png 768w\" sizes=\"(max-width: 1330px) 100vw, 1330px\" \/><\/div>\n<div><\/div>\n<div>\ud83d\udd17 <em>Resource \u00bb<\/em> Get all the Globalize demo code from our <a href=\"https:\/\/github.com\/PhraseApp-Blog\/javascript-l10n-ultimate-guide\/tree\/main\/globalize\">GitHub repo<\/a>.<\/div>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> Globalize\u2019s ICU implementation covers comprehensive date and number formatting. Take a gander at the <a href=\"https:\/\/github.com\/globalizejs\/globalize#api\">official documentation<\/a> for more info.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> We cover more Globalize installation options and use cases in <a href=\"https:\/\/phrase.com\/blog\/posts\/js-i18n-with-globalizejs\/\">JS I18n with Globalize.js<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"wrapping-up-our-javascript-localization-guide\"><\/span>Wrapping up our JavaScript localization guide<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>And that about does it for this one. We hope you&#8217;ve enjoyed this foray into some of the most popular JavaScript localization solutions.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> If you work with Ruby on Rails, you might enjoy <a href=\"https:\/\/phrase.com\/blog\/posts\/localizing-javascript-in-rails-apps\/\">Localizing JavaScript in Rails Apps<\/a>.<\/p>\n<p>And if you\u2019re looking to take your localization process to the next level, take a look at Phrase. Phrase supports the ICU format, all other translation formats we&#8217;ve covered here, and many more. With its CLI and Bitbucket, GitHub, and GitLab sync, your i18n can be on autopilot. The fully-featured Phrase web console, with machine learning and smart suggestions, is a joy for translators to use. Once translations are ready, they can sync back to your project automatically. You set it and forget it, leaving you to focus on the code you love.<\/p>\n<p>Check out all <a href=\"https:\/\/phrase.com\/roles\/developers\/\">Phrase features for developers<\/a> and see for yourself how it can streamline your software localization workflows.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Kick-start your browser JavaScript localization with this comprehensive guide and make your application ready for international users.<\/p>\n","protected":false},"author":41,"featured_media":2612,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_stopmodifiedupdate":false,"_modified_date":"","_searchwp_excluded":"","footnotes":""},"categories":[40],"class_list":["post-15100","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-software-localization"],"acf":[],"_links":{"self":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/15100"}],"collection":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/users\/41"}],"replies":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/comments?post=15100"}],"version-history":[{"count":12,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/15100\/revisions"}],"predecessor-version":[{"id":65306,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/15100\/revisions\/65306"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media\/2612"}],"wp:attachment":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media?parent=15100"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/categories?post=15100"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}