{"id":35540,"date":"2021-11-04T12:08:00","date_gmt":"2021-11-04T12:08:00","guid":{"rendered":"https:\/\/phrase.com\/?p=35540"},"modified":"2023-03-30T08:18:24","modified_gmt":"2023-03-30T06:18:24","slug":"vue-2-localization","status":"publish","type":"post","link":"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/","title":{"rendered":"Vue 2 Localization with Vue I18n: A Step-by-Step Guide"},"content":{"rendered":"<p>It&#8217;s always a pleasure to work with the <a href=\"https:\/\/vuejs.org\/\">Vue<\/a> JavaScript framework. The elegance of its design, coupled with the robust first-party additions like <a href=\"https:\/\/router.vuejs.org\/\">Vue Router<\/a> for SPA routing and <a href=\"https:\/\/vuex.vuejs.org\/\">Vuex<\/a> for state management, make it a delight to use for building modern browser apps.<br \/>\nOf course, if you&#8217;re here, you probably know this already. You might have an app built with Vue, and you might be wanting to reap the benefits of internationalizing and localizing your app to reach a wider, global market. Well, have no fear.<br \/>\nThis tutorial will walk you through everything you need to dive into Vue localization with the extremely popular <a href=\"https:\/\/kazupon.github.io\/vue-i18n\/\">Vue I18n<\/a> library. Vue I18n plugs into your Vue apps and provides you with translation file management, message formatting, date and number formatting, and more to boot. We&#8217;ll fill in the gaps that the library leaves so that you can have a robust i18n cookbook for your Vue apps.<\/p>\n<p>\u270b\u00a0<em>Heads up \u00bb<\/em> This article covers Vue 2 localization. If you\u2019re interested in Vue 3, check out our guide to <a href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/\">Vue localization<\/a>.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb <\/em>We&#8217;re using the Vue I18n library in this article. If you would rather use i18next, our tutorial on <a href=\"https:\/\/phrase.com\/blog\/posts\/vue-translation-with-vue-i18next\/\">Vue Translation with vue-i18next<\/a> might be useful to you.<\/p>\n<p>\ud83d\udd17<i class=\"blockquote-icon\"> Resource <\/i><i data-stringify-type=\"italic\">\u00bb <\/i>Explore the possibilities other frameworks have to offer and learn everything you need to make your JS applications accessible to international users with our ultimate guide to <a class=\"c-link\" tabindex=\"-1\" href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/\" target=\"_blank\" rel=\"noopener noreferrer\" data-stringify-link=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/\" data-sk=\"tooltip_parent\" data-remove-tab-index=\"true\" aria-describedby=\"sk-tooltip-446\">JavaScript localization<\/a>.<\/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\/vue-2-localization\/#library-versions\" title=\"Library Versions\">Library Versions<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#our-demo-app\" title=\"Our Demo App\">Our Demo App<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#installation\" title=\"Installation\">Installation<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#installing-vue-i18n\" title=\"Installing Vue I18n\">Installing Vue I18n<\/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\/vue-2-localization\/#env\" title=\".env\">.env<\/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\/vue-2-localization\/#srci18njs\" title=\"src\/i18n.js\">src\/i18n.js<\/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\/vue-2-localization\/#srclocalesenjson\" title=\"src\/locales\/en.json\">src\/locales\/en.json<\/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\/vue-2-localization\/#srcmainjs\" title=\"src\/main.js\">src\/main.js<\/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\/vue-2-localization\/#vueconfigjs\" title=\"vue.config.js\">vue.config.js<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#creating-our-demo\" title=\"Creating our Demo\">Creating our Demo<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#getting-the-active-locale\" title=\"Getting the Active Locale\">Getting the Active Locale<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#getting-the-active-locale-inside-a-vue-component\" title=\"Getting the Active Locale Inside a Vue Component\">Getting the Active Locale Inside a Vue Component<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#getting-the-active-locale-outside-of-vue-components\" title=\"Getting the Active Locale Outside of Vue Components\">Getting the Active Locale Outside of Vue Components<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#manually-setting-the-active-locale\" title=\"Manually Setting the Active Locale\">Manually Setting the Active Locale<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#building-a-localelanguage-switcher\" title=\"Building a Locale\/Language Switcher\">Building a Locale\/Language Switcher<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#supported-locales\" title=\"Supported Locales\">Supported Locales<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#detecting-the-users-preferred-locale-in-the-browser\" title=\"Detecting the User&#8217;s Preferred Locale in the Browser\">Detecting the User&#8217;s Preferred Locale in the Browser<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#basic-translation-messages\" title=\"Basic Translation Messages\">Basic Translation Messages<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-19\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#interpolation\" title=\"Interpolation\">Interpolation<\/a><\/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\/vue-2-localization\/#using-html-in-translation-messages\" title=\"Using HTML in Translation Messages\">Using HTML in Translation Messages<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-21\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#plurals\" title=\"Plurals\">Plurals<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-22\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#custom-pluralization\" title=\"Custom Pluralization\">Custom Pluralization<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-23\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#formatting-dates\" title=\"Formatting Dates\">Formatting Dates<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-24\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#formatting-numbers\" title=\"Formatting Numbers\">Formatting Numbers<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-25\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#changing-the-document-language-layout-direction\" title=\"Changing the Document Language &amp; Layout Direction\">Changing the Document Language &amp; Layout Direction<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-26\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#localizing-the-document-title\" title=\"Localizing the Document Title\">Localizing the Document Title<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-27\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#async-lazy-loading-of-translation-files\" title=\"Async (Lazy) Loading of Translation Files\">Async (Lazy) Loading of Translation Files<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-28\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#localized-routes\" title=\"Localized Routes\">Localized Routes<\/a><\/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\/vue-2-localization\/#localized-router-links\" title=\"Localized Router Links\">Localized Router Links<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-30\" href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/#closing-up\" title=\"Closing Up\">Closing Up<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"library-versions\"><\/span><a name=\"library-versions\"><\/a>Library Versions<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We&#8217;ll be using the following libraries (with versions at the time of writing) to work through localizing our Vue app:<\/p>\n<ul>\n<li>Vue <em>vue<\/em> (2.6.11)<\/li>\n<li>Vue Router <em>vue-router<\/em> (3.1.3)<\/li>\n<li>Vue I18n <em>vue-i18n<\/em> (8.15.3)<\/li>\n<li>Vue CLI <em>@vue\/cli<\/em> (4.1.2) \u2014 We use this to install all the libraries above, but you don&#8217;t strictly have to<\/li>\n<\/ul>\n<style type=\"text\/css\"><!--td {border: 1px solid #ccc;}br {mso-data-placement:same-cell;}--><\/style>\n<h2><span class=\"ez-toc-section\" id=\"our-demo-app\"><\/span><a name=\"our-demo-app\"><\/a>Our Demo App<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>During the course of this article, we&#8217;ll build a localized, small Vue SPA. We&#8217;ll call it <em>International Gourmet Coffee<\/em>, and it will be a mock ecommerce storefront specializing in gourmet coffee from around the world. Of course, the point is to showcase Vue I18n and localization, so we won&#8217;t cover features like adding to a cart or checking out. We&#8217;ll just have a couple of pages that demo what we need to get cooking quickly. <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8383 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-en-completed-app-862x1024.png\" alt=\"International Gourmet Coffee demo app | Phrase\" width=\"862\" height=\"1024\" \/><\/p>\n<p style=\"text-align: center;\"><em>This beauty of an app is what we&#8217;ll have at the end of this article<\/em><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> You can grab the code for the entire, completed app from <a href=\"https:\/\/github.com\/PhraseApp-Blog\/vue-i18n-demo\">GitHub<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"installation\"><\/span><a name=\"installation\"><\/a>Installation<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We&#8217;ll start off by installing the Vue CLI, which makes quick work of spinning up new Vue SPA projects.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"1\" data-enlighter-title=\"install-vue-cli.sh \">npm install -g @vue\/cli\n<\/pre>\n<p>With the CLI installed, we can use the global <code>vue<\/code> command to create our demo project.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"2\" data-enlighter-title=\"create-project.sh\">vue create vue-i18n-demo<\/pre>\n<p>When asked for the preset by the Vue CLI, we&#8217;ll select &#8220;Manually select features&#8221; and select the following ones. <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8385 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-vue-cli-create-options-1024x282.png\" alt=\"Vue CLI Manually select features | Phrase\" width=\"1024\" height=\"282\" \/><\/p>\n<p style=\"text-align: center;\"><em>The most important option is adding the Router. Everything else is optional here.<\/em><\/p>\n<h3><span class=\"ez-toc-section\" id=\"installing-vue-i18n\"><\/span>Installing Vue I18n<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Ok, now let&#8217;s install Vue I18n. If we spun up our project with the Vue CLI, installing Vue I18n is a breeze. We simply run one command from the command line.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"3\" data-enlighter-title=\"install-vue-i18n.sh\">vue add i18n\n<\/pre>\n<p>This command will install the Vue I18n CLI plugin and will ask us a few questions about our project so it can create some i18n boilerplate for us. <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8386 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-vue-i18n-installation-options-1024x132.png\" alt=\"Vue I18n CLI plugin questions | Phrase\" width=\"1024\" height=\"132\" \/><\/p>\n<p style=\"text-align: center;\"><em>We&#8217;ll go with all the defaults<\/em><\/p>\n<p>The CLI will create and update some files in our Vue project. Let&#8217;s go through these changes.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"env\"><\/span>.env<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The default and fallback locales are added as environment variables to the <code>.env<\/code> file in our project.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"4\" data-enlighter-title=\".env\">VUE_APP_I18N_LOCALE=en\nVUE_APP_I18N_FALLBACK_LOCALE=en<\/pre>\n<h3><span class=\"ez-toc-section\" id=\"srci18njs\"><\/span>src\/i18n.js<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>A new <code>src\/i18n.js<\/code> file is added that registers Vue I18n as a plugin to our Vue instance via the <code>Vue.use()<\/code> function.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"5\" data-enlighter-title=\"src \u203a i18n.js \">import Vue from \"vue\";\nimport VueI18n from \"vue-i18n\";\nVue.use(VueI18n);\nfunction loadLocaleMessages() {\n  const locales = require.context(\n    \".\/locales\",\n    true,\n    \/[A-Za-z0-9-_,\\s]+\\.json$\/i\n  );\n  const messages = {};\n  locales.keys().forEach(key =&gt; {\n    const matched = key.match(\/([A-Za-z0-9-_]+)\\.\/i);\n    if (matched &amp;&amp; matched.length &gt; 1) {\n      const locale = matched[1];\n      messages[locale] = locales(key);\n    }\n  });\n  return messages;\n}\nexport default new VueI18n({\n  locale: process.env.VUE_APP_I18N_LOCALE || \"en\",\n  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || \"en\",\n  messages: loadLocaleMessages()\n});<\/pre>\n<p>The new file also houses a function, <code>loadLocaleMessages()<\/code>, that scans the <code>src\/locales<\/code> directory for JSON files, and loads them in as translations messages. For example, a file called &#8220;fr.json&#8221; will have its contents loaded as the French (fr) translation messages. Finally, <code>i18n.js<\/code> constructs the <code>VueI8n<\/code> instance we&#8217;ll use in our app, and exports it.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"srclocalesenjson\"><\/span>src\/locales\/en.json<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The Vue I18n CLI will also have placed our first translation message file, based on the options we selected during installation. Since we selected English (en) as our default locale, and <code>locales<\/code> as the directory to store our message files, the CLI will have created a <code>src\/locales\/en.json<\/code> file to contain our English translation messages.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"6\" data-enlighter-title=\"src \u203a locales \u203a en.json \">{\n  \"message\": \"hello i18n !!\"\n}<\/pre>\n<h3><span class=\"ez-toc-section\" id=\"srcmainjs\"><\/span>src\/main.js<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Our entry <code>src\/main.js<\/code> file has the <code>VueI8n<\/code> instance created and added to the <code>Vue<\/code> constructor.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"7\" data-enlighter-title=\"src \u203a main.js\">import Vue from \"vue\";\nimport App from \".\/App.vue\";\nimport router from \".\/router\";\nimport store from \".\/store\";\nimport i18n from \".\/i18n\";\nVue.config.productionTip = false;\nnew Vue({\n  router,\n  store,\n  i18n,\n  render: h =&gt; h(App)\n}).$mount(\"#app\");<\/pre>\n<h3><span class=\"ez-toc-section\" id=\"vueconfigjs\"><\/span>vue.config.js<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The CLI will have added the <code>i18n<\/code> entry under <code>pluginOptions<\/code> in our <code>vue.config.js<\/code> file as well.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"8\" data-enlighter-title=\"vue.config.js \">module.exports = {\n  pluginOptions: {\n    i18n: {\n      locale: \"en\",\n      fallbackLocale: \"en\",\n      localeDir: \"locales\",\n      enableInSFC: false\n    }\n  }\n};<\/pre>\n<p>Of course, the <code>vue-i18n<\/code> package will also have been added to our <code>package.json<\/code> file.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> If you have an existing Vue project, and don&#8217;t want to install the Vue CLI, you can recreate the automated installation above by adding and modifying the files that the Vue I18n CLI plugin does for you. You&#8217;ll, of course, need to install the package manually via <code>npm install --save vue-i18n<\/code>. <a href=\"https:\/\/github.com\/ashour\/vue-i18n-demo\/pull\/1\/files\">Here&#8217;s a convenient one-stop shop PR<\/a> for you to see all the file changes in one place.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"creating-our-demo\"><\/span><a name=\"creating-our-demo\"><\/a>Creating our Demo<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>With Vue and Vue I18n installed, let&#8217;s build the demo app that we&#8217;ll localize using Vue I18n. We&#8217;ll add a simple navigation bar, some coffee data for our home page, and placeholder text in our about page.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"9\" data-enlighter-title=\"src \u203a components \u203a Nav.vue\">&lt;template&gt;\n  &lt;div id=\"nav\"&gt;\n    &lt;img alt=\"Vue logo\" src=\"..\/assets\/logo-circle-sm.png\" \/&gt;\n    &lt;router-link to=\"\/\"&gt;Home&lt;\/router-link&gt;\n    &lt;router-link to=\"\/about\"&gt;About&lt;\/router-link&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;style&gt;\n#nav {\n  display: flex;\n  align-items: center;\n  text-align: left;\n  padding: 1rem;\n  color: #42b983;\n  background-color: #3d536a;\n}\n#nav img {\n  margin-right: 1rem;\n}\n#nav a {\n  margin-right: 1.5rem;\n  font-weight: bold;\n  color: #fff;\n  text-decoration: none;\n}\n&lt;\/style&gt;<\/pre>\n<p>Notice that the <code>&lt;router-link&gt;<\/code>s that Vue gives us as boilerplate are now in our own <code>Nav<\/code> component. The rest is visual flare.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"10\" data-enlighter-title=\"src \u203a App.vue \">&lt;template&gt;\n  &lt;div id=\"app\"&gt;\n    &lt;Nav \/&gt;\n    &lt;div class=\"container\"&gt;\n      &lt;router-view \/&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport Nav from \"@\/components\/Nav\"\nexport default {\n  components: { Nav }\n}\n&lt;\/script&gt;\n&lt;style&gt;\nbody {\n  margin: 0;\n}\n#app {\n  font-family: \"Avenir\", Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  color: #2c3e50;\n}\n#app .container {\n  padding: 1rem;\n}\n&lt;\/style&gt;<\/pre>\n<p>Our new <code>Nav<\/code> component is pulled into our main <code>App<\/code> component. We&#8217;ve also wrapped the <code>App<\/code>&#8216;s <code>&lt;router-view&gt;<\/code> in a <code>#container<\/code> for styling. Now let&#8217;s take care of our home page content. We&#8217;ll simulate a backend by adding a JSON file with our data.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"11\" data-enlighter-title=\"public \u203a data.json \">[\n  {\n    \"id\": 1,\n    \"title\": \"Battlecreek Columbia Coldono\",\n    \"imgUrl\": \"\/img\/battlecreek-coffee-roasters-_1wDmr4dtuk-unsplash.jpg\",\n    \"addedOn\": \"2020-01-02\"\n  },\n  {\n    \"id\": 2,\n    \"title\": \"Battlecreek Yirgacheffee\",\n    \"imgUrl\": \"\/img\/battlecreek-coffee-roasters-HvzR2yXtii4-unsplash.jpg\",\n    \"addedOn\": \"2020-01-05\"\n  },\n  {\n    \"id\": 3,\n    \"title\": \"Primo Passo\",\n    \"imgUrl\": \"\/img\/jon-tyson-KRedbshBxEk-unsplash.jpg\",\n    \"addedOn\": \"2020-01-05\"\n  },\n  {\n    \"id\": 4,\n    \"title\": \"Little Nap Brazil\",\n    \"imgUrl\": \"\/img\/lex-sirikiat-QouiCn7u6kw-unsplash.jpg\",\n    \"addedOn\": \"2020-01-06\"\n  },\n  {\n    \"id\": 5,\n    \"title\": \"Little Nap Blend\",\n    \"imgUrl\": \"\/img\/manki-kim-mv7kxYh5Rko-unsplash.jpg\",\n    \"addedOn\": \"2020-01-07\"\n  },\n  {\n    \"id\": 6,\n    \"title\": \"French Truck Peru Cajamarca\",\n    \"imgUrl\": \"\/img\/ryan-spaulding-_uncFvtOC-4-unsplash.jpg\",\n    \"addedOn\": \"2020-01-08\"\n  }\n]<\/pre>\n<p>Let&#8217;s fetch this data in a <code>Cards<\/code> component and display it.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"12\" data-enlighter-title=\"src \u203a components \u203a Cards.vue \">&lt;template&gt;\n  &lt;div class=\"cards\"&gt;\n    &lt;Card v-bind=\"card\" v-for=\"card in cards\" :key=\"card.id\" \/&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport Card from \".\/Card\"\nexport default {\n  data: () =&gt; ({\n    cards: []\n  }),\n  created() {\n    fetch(\"\/data.json\")\n      .then(response =&gt; response.json())\n      .then(data =&gt; (this.cards = data))\n  },\n  components: { Card }\n}\n&lt;\/script&gt;\n&lt;style scoped&gt;\n.cards {\n  display: flex;\n  justify-content: space-between;\n  flex-wrap: wrap;\n}\n&lt;\/style&gt;<\/pre>\n<p>We can use a presentational <code>Card<\/code> component to display each coffee item as a card.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"13\" data-enlighter-title=\"src \u203a components \u203a Card.vue \">&lt;template&gt;\n  &lt;div class=\"card\"&gt;\n    &lt;h3&gt;{{ title }}&lt;\/h3&gt;\n    &lt;img :src=\"imgUrl\" \/&gt;\n    &lt;p&gt;Added {{ addedOn }}&lt;\/p&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nexport default {\n  props: {\n    id: Number,\n    title: String,\n    imgUrl: String,\n    addedOn: String\n  }\n}\n&lt;\/script&gt;\n&lt;style scoped&gt;\n.card {\n  width: 30%;\n  margin-bottom: 2rem;\n  border: 1px solid #f2f2f2;\n  border-radius: 4px;\n  box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);\n}\nh3 {\n  margin: 0;\n  padding: 0.5em;\n  text-align: center;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\nimg {\n  width: 100%;\n}\np {\n  text-align: center;\n  margin: 0;\n  padding: 0.5em;\n}\n&lt;\/style&gt;<\/pre>\n<p>Now we just need to pull our <code>Cards<\/code> component into our <code>Home<\/code> view.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"14\" data-enlighter-title=\"src \u203a views &gt; Home.vue \">&lt;template&gt;\n  &lt;div class=\"home\"&gt;\n    &lt;h1&gt;International Gourmet Coffee&lt;\/h1&gt;\n    &lt;Cards \/&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport Cards from \"@\/components\/Cards.vue\"\nexport default {\n  name: \"home\",\n  components: {\n    Cards\n  }\n}\n&lt;\/script&gt;<\/pre>\n<p>With these changes in place, we should have something that looks like the following: <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8387 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-home-before-i18n.png\" alt=\"Demo App | Phrase\" width=\"960\" height=\"984\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-home-before-i18n.png 960w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-home-before-i18n-293x300.png 293w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-home-before-i18n-768x787.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/p>\n<p style=\"text-align: center;\"><em>Our little demo is ready for i18n<\/em><\/p>\n<p>We now have a testbed to work with, and we can use it to localize our app using Vue I18n.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> If you want to skip all the setup and get the demo in its current state (before we localize it), check out <a href=\"https:\/\/github.com\/PhraseApp-Blog\/vue-i18n-demo\">this tagged commit<\/a> on GitHub.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"getting-the-active-locale\"><\/span><a name=\"getting-active-locale\"><\/a>Getting the Active Locale<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Retrieving the active locale from Vue I18n is pretty easy. How we retrieve it depends on whether we&#8217;re in a Vue component or not.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"getting-the-active-locale-inside-a-vue-component\"><\/span>Getting the Active Locale Inside a Vue Component<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Let&#8217;s mock a quick language switcher component in our demo app. We won&#8217;t build out the language switching functionality just yet, but we&#8217;ll show the currently active language in our new component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"15\" data-enlighter-title=\"src \u203a components \u203a LocaleSwitcher.vue \">&lt;template&gt;\n  &lt;div class=\"locale-switcher\"&gt;\ud83c\udf10 {{$i18n.locale}}&lt;\/div&gt;\n&lt;\/template&gt;<\/pre>\n<p>As a Vue plugin registered in our Vue instance, the <code>VueI18n<\/code> object is available to all our components via the <code>$i18n<\/code> variable in our templates. The same object is, of course, available in our component <em>scripts<\/em> via <code>this.$i18n<\/code>. You may remember that the Vue i18n CLI plugin setup the <code>VueI8n<\/code> object for us in the <code>i18n.js<\/code> file. One of the several properties that can be accessed on <code>VueI8n<\/code> is the <code>locale<\/code> property, a string that corresponds to the currently active locale.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Check out all of the <code>VueI8n<\/code> properties and methods in the <a href=\"https:\/\/kazupon.github.io\/vue-i18n\/api\/#properties\">official Vue I18n API documentation<\/a>.<\/p>\n<p>Let&#8217;s add our <code>LocaleSwitcher<\/code> to our <code>Nav<\/code> component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"16\" data-enlighter-title=\"src \u203a components \u203a Nav.vue \">&lt;template&gt;\n  &lt;div class=\"nav\"&gt;\n    &lt;div class=\"nav__start\"&gt;\n      &lt;img alt=\"Vue logo\" src=\"..\/assets\/logo-circle-sm.png\" \/&gt;\n      &lt;router-link to=\"\/\"&gt;Home&lt;\/router-link&gt;\n      &lt;router-link to=\"\/about\"&gt;About&lt;\/router-link&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"nav__end\"&gt;\n      &lt;LocaleSwitcher \/&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport LocaleSwitcher from \"@\/components\/LocaleSwitcher\"\nexport default {\n  components: { LocaleSwitcher }\n}\n&lt;\/script&gt;\n&lt;style scoped&gt;\n.nav {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  text-align: left;\n  padding: 1rem;\n  color: #fff;\n  background-color: #3d536a;\n}\n.nav__start,\n.nav__end {\n  display: flex;\n  align-items: center;\n}\n.nav img {\n  margin-right: 1rem;\n}\n.nav a {\n  margin-right: 1.5rem;\n  font-weight: bold;\n  color: #fff;\n  text-decoration: none;\n}\n&lt;\/style&gt;<\/pre>\n<p>With these changes in place, we should see something like the following in our browser. <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8388 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-navbar-with-active-locale-1024x73.png\" alt=\"Active locale render in demo app | Phrase\" width=\"1024\" height=\"73\" \/><\/p>\n<p style=\"text-align: center;\"><em>Rendering the active locale<\/em><\/p>\n<p>We&#8217;ll build out the rest of the <code>LocaleSwitcher<\/code> soon. First, let&#8217;s check out how we can access the Vue I18n instance outside of our components.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"getting-the-active-locale-outside-of-vue-components\"><\/span>Getting the Active Locale Outside of Vue Components<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Whenever we want to access the <code>VueI18n<\/code> object (<code>i18n<\/code>) outside of our Vue components, we just import the instance from our <code>i18n.js<\/code> file. This grants us access not just to the <code>locale<\/code> property, of course, but to <em>all<\/em> of the properties and methods of the <code>VueI8n<\/code> instance.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"17\" data-enlighter-title=\"accessing-i18n-outside-components.js \">import i18n from \"@\/i18n\"\nconsole.log(\"Active locale: \", i18n.locale)<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"manually-setting-the-active-locale\"><\/span><a name=\"manually-setting-active-locale\"><\/a>Manually Setting the Active Locale<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The nice thing about <code>VueI18n<\/code>&#8216;s <code>locale<\/code> property is that it&#8217;s read\/write and reactive. So if we set a new value to the property, the components in our app that are showing translated messages will re-render to show their messages in the newly set locale.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"18\" data-enlighter-title=\"locale-setting-example.vue \">&lt;script&gt;\nexport default {\n  methods: {\n    setLocale(locale) {\n      this.$i18n.locale = locale\n    }\n  }\n}\n&lt;\/script&gt;<\/pre>\n<p>Now that we know how to get and set the active locale, we can get our <code>LocaleSwitcher<\/code> component working.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"building-a-localelanguage-switcher\"><\/span><a name=\"building-locale-language-switcher\"><\/a>Building a Locale\/Language Switcher<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Let&#8217;s update our <code>LocaleSwitcher<\/code> so that it displays a drop-down for selecting the active locale.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"19\" data-enlighter-title=\"src \u203a components \u203a LocaleSwitcher.vue \">&lt;template&gt;\n  &lt;div class=\"locale-switcher\"&gt;\n    &lt;select v-model=\"$i18n.locale\"&gt;\n      &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n      &lt;option value=\"ar\"&gt;Arabic&lt;\/option&gt;\n    &lt;\/select&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;<\/pre>\n<p>The <code>v-model<\/code> Vue directive creates a two-way binding between the <code>&lt;select&gt;<\/code>&#8216;s current value and the active Vue I18n locale: the <code>&lt;select&gt;<\/code> will both <em>show<\/em> the active locale as the currently selected one and <em>update<\/em> the active locale if the user selects a different one. To test our <code>LocaleSwitcher<\/code>, let&#8217;s place some translated messages in our app by localizing our <code>Nav<\/code> component. First, we&#8217;ll add the messages to our message files.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> We&#8217;ll cover translation messages in more detail a bit later. For now, we just want a way to see if our locale actually changes.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"20\" data-enlighter-title=\"src \u203a locales \u203a en.json \">{\n  \"nav\": {\n    \"home\": \"Home\",\n    \"about\": \"About\"\n  }\n}<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"21\" data-enlighter-title=\"src \u203a locales \u203a ar.json \">{\n  \"nav\": {\n    \"home\": \"\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629\",\n    \"about\": \"\u0646\u0628\u0630\u0629 \u0639\u0646\u0627\"\n  }\n}<\/pre>\n<p>With these messages in place, let&#8217;s update our <code>Nav<\/code> component so that it uses them instead of hard-coded values.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"22\" data-enlighter-title=\"src \u203a components \u203a Nav.vue \">&lt;template&gt;\n  &lt;div class=\"nav\"&gt;\n    &lt;div class=\"nav__start\"&gt;\n      &lt;img alt=\"Vue logo\" src=\"..\/assets\/logo-circle-sm.png\" \/&gt;\n      &lt;router-link to=\"\/\"&gt;{{ $t(\"nav.home\") }}&lt;\/router-link&gt;\n      &lt;router-link to=\"\/about\"&gt;{{ $t(\"nav.about\") }}&lt;\/router-link&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"nav__end\"&gt;\n      &lt;LocaleSwitcher \/&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport LocaleSwitcher from \"@\/components\/LocaleSwitcher\"\nexport default {\n  components: { LocaleSwitcher }\n}\n&lt;\/script&gt;\n&lt;style scoped&gt;\n\/* ... *\/\n&lt;\/style&gt;<\/pre>\n<p>We use the <code>$t()<\/code> function that Vue I18n provides to our components for outputting translation messages. More on <code>$t()<\/code> a bit later. With the <code>Nav<\/code> component localized and the <code>LocaleSwitcher<\/code> working, we can now see and select the active locale from the nav bar.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8389 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-language-switcher-1024x140.png\" alt=\"Locale Switcher in demo app | Phrase\" width=\"1024\" height=\"140\" \/><\/p>\n<p style=\"text-align: center;\"><em>Our components react and re-render when our locale changes<\/em><\/p>\n<h2><span class=\"ez-toc-section\" id=\"supported-locales\"><\/span><a name=\"supported-locales\"><\/a>Supported Locales<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Vue I18n doesn&#8217;t have a strict notion of supported locales: a list of locales that we are expected to have translations for. We can add this list ourselves, however.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> The <code>VueI18n<\/code> does have a read-only property called <code>availableLocales<\/code>, which is an array of locale keys that have messages loaded into the <code>VueI8n<\/code> instance. <code>availableLocales<\/code> can be helpful, but is somewhat limited: it doesn&#8217;t have the human-readable names of locales. Worse yet, <code>availableLocales<\/code> will be inaccurate if we use it as a way to determine supported locales if we lazy load these locales in our app. This is because the list will be empty when we first load our app, and before we load any message files. We cover lazy loading later in this article.<\/p>\n<p>A simple config file with locale codes mapped to human-readable names can do the trick.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"23\" data-enlighter-title=\"src \u203a config \u203a supported-locales.js \">export default {\n  en: \"English\",\n  ar: \"\u0639\u0631\u0628\u064a (Arabic)\"\n}<\/pre>\n<p>We can also write a utility function that provides these values in a way that makes them easily consumable by our views.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"24\" data-enlighter-title=\"src \u203a util \u203a i18n \u203a supported-locales.js\">import supportedLocales from \"@\/config\/supported-locales\"\nexport function getSupportedLocales() {\n  let annotatedLocales = []\n  for (const code of Object.keys(supportedLocales)) {\n    annotatedLocales.push({\n      code,\n      name: supportedLocales[code]\n    })\n  }\n  return annotatedLocales\n}<\/pre>\n<p><code>getSupportedLocales()<\/code> uses our configuration object to create an array of the form <code>[{ code: \"en\", name: \"English\"}]<\/code>. We can use this array in the <code>LocaleSwitcher<\/code> we built earlier so that we have a single source of truth for our supported locales.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"25\" data-enlighter-title=\"src \u203a components \u203a LocaleSwitcher.vue \">&lt;template&gt;\n  &lt;div class=\"locale-switcher\"&gt;\n    &lt;select v-model=\"$i18n.locale\"&gt;\n      &lt;option :value=\"locale.code\" v-for=\"locale in locales\" :key=\"locale.code\"&gt;\n        {{locale.name}}\n      &lt;\/option&gt;\n    &lt;\/select&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport { getSupportedLocales } from \"@\/util\/i18n\/supported-locales\"\nexport default {\n  data: () =&gt; ({ locales: getSupportedLocales() })\n}\n&lt;\/script&gt;<\/pre>\n<p>Instead of hard-coding the locales in our switcher, we iterate over the supported locales and display each as an <code>&lt;option&gt;<\/code>. This is simply a refactor, and our <code>LocaleSwitcher<\/code> should behave in exactly the same way it did before. Another helpful feature is being able to check whether a given locale is in our list of supported locales. In fact, we&#8217;ll want this feature in a moment when we retrieve the user&#8217;s locale from the browser. As you can imagine, the logic for this check is trivial.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"26\" data-enlighter-title=\"src \u203a util \u203a i18n \u203a supported-locales.js \">import supportedLocales from \"@\/config\/supported-locales\"\n\/\/ ...\nexport function supportedLocalesInclude(locale) {\n  return Object.keys(supportedLocales).includes(locale)\n}<\/pre>\n<p>\u270b\ud83c\udffd <em>Heads Up \u00bb<\/em> It might be tempting to use Webpack&#8217;s <code>require.context()<\/code> to check the <code>locales<\/code> directory and infer the supported locales based on the message files there, e.g. if <code>\/src\/locales\/fr.json<\/code> exists, then we assume that we support French. However, I&#8217;ve noticed that using <code>require.context()<\/code> this way can interfere with Webpack&#8217;s async\/lazy loading of translations, which we&#8217;ll cover a bit later.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"detecting-the-users-preferred-locale-in-the-browser\"><\/span><a name=\"detecting-users-preferred-locale-in-browser\"><\/a>Detecting the User&#8217;s Preferred Locale in the Browser<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>While not strictly related to Vue I18n per se, detecting the user&#8217;s locale is often a good idea when it comes to <a href=\"https:\/\/phrase.com\/blog\/posts\/how-to-create-good-ux-design-for-multiple-languages\/\">providing a friendly UX<\/a>. We can use the browser&#8217;s <code>navigator.languages<\/code> array to try to detect the user&#8217;s preferred locale.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"27\" data-enlighter-title=\"src \u203a util \u203a i18n \u203a get-browser-locale.js \">export default function getBrowserLocale(options = {}) {\n  const defaultOptions = { countryCodeOnly: false }\n  const opt = { ...defaultOptions, ...options }\n  const navigatorLocale =\n    navigator.languages !== undefined\n      ? navigator.languages[0]\n      : navigator.language\n  if (!navigatorLocale) {\n    return undefined\n  }\n  const trimmedLocale = opt.countryCodeOnly\n    ? navigatorLocale.trim().split(\/-|_\/)[0]\n    : navigatorLocale.trim()\n  return trimmedLocale\n}<\/pre>\n<p>In <code>getBrowserLocale()<\/code>, we first check the global <code>navigator.languages<\/code> array to see if it has any entries. This array will contain the languages that the user has selected in her browser preferences, in priority order. <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8390 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-user-lang-prefs-in-browser-1024x525.png\" alt=\"Webpage language settings | Phrase\" width=\"1024\" height=\"525\" \/><\/p>\n<p style=\"text-align: center;\"><em>This user prefers to see pages in Canadian English, then in Egyptian Arabic<\/em><\/p>\n<p>This array might be empty in the browser preferences, in which case it will be <code>undefined<\/code> when we try to access it via <code>navigator.languages<\/code>. If this happens, we fall back to the <code>navigator.language<\/code> property. In some browsers, <code>navigator.language<\/code> will be the first element in <code>navigator.languages<\/code>, and it will be an empty string if the array is <code>undefined<\/code>. In other browsers, <code>navigator.language<\/code> will be the language of the <em>UI<\/em>, which is not necessarily <code>navigator.languages[0]<\/code>. If we couldn&#8217;t infer the user&#8217;s preferred language from either <code>navigator.languages<\/code> or <code>navigator.language<\/code>, we return <code>undefined<\/code>. In normal cases, however, the <code>navigator<\/code> will usually give us a standard language tag like <code>\"fr-FR\"<\/code>. When our app covers language variants, like French (France) and French (Canada), this can be exactly what we want. However, sometimes we&#8217;re only interested in the language code. Our <code>getBrowserLocale()<\/code> provides a <code>countryCodeOnly<\/code> option that gives us exactly that.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"28\" data-enlighter-title=\"get-browser-locale-examples.js \">\/\/ navigator.languages == [\"ar-SA\", \"en-US\"]\ngetBrowserLocale()                          \/\/ =&gt; \"ar-SA\"\ngetBrowserLocale({ countryCodeOnly: true }) \/\/ =&gt; \"ar<\/pre>\n<p>We can use our new function to set the initial locale of our app based on our user&#8217;s preferences.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"29\" data-enlighter-title=\"src \u203a i18n.js \">import Vue from \"vue\"\nimport VueI18n from \"vue-i18n\"\nimport getBrowserLocale from \"@\/util\/i18n\/get-browser-locale\"\nimport { supportedLocalesInclude } from \".\/util\/i18n\/supported-locales\"\nVue.use(VueI18n)\n\/\/...\nfunction getStartingLocale() {\n  const browserLocale = getBrowserLocale({ countryCodeOnly: true })\n  if (supportedLocalesInclude(browserLocale)) {\n    return browserLocale\n  } else {\n    return process.env.VUE_APP_I18N_LOCALE || \"en\"\n  }\n}\nexport default new VueI18n({\n  locale: getStartingLocale(),\n  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || \"en\",\n  messages: loadLocaleMessages()\n})<\/pre>\n<p>We retrieve the browser locale, check if our app supports it, and if it does we use it as our starting locale. If our app doesn&#8217;t support the browser locale we fall back to the default locale we set in our <code>.env<\/code> file. Now, if the user has set a language that our app supports, we&#8217;ll show the app in that language first.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"basic-translation-messages\"><\/span><a name=\"basic-translation-messages\"><\/a>Basic Translation Messages<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We&#8217;ve already seen how to display basic, reactive translation messages via Vue I18n&#8217;s <code>t()<\/code> function.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"30\" data-enlighter-title=\"basic-translation-messages-example.js\">{\n  \"hello\": \"Hello There!\"\n}\n\/\/ \ud83d\udc46\ud83c\udffdin en.json\n{\n  \"hello\": \"Bonjour!\"\n}\n\/\/ \ud83d\udc46\ud83c\udffdin fr.json\n\/\/ \ud83d\udc47\ud83c\udffd in Vue component\n&lt;h2&gt; {{ $t(\"hello\") }} &lt;\/h2&gt;<\/pre>\n<p>In the above example, the <code>&lt;h2&gt;<\/code> will contain <code>\"Hello There!\"<\/code> when the active locale is English, and <code>\"Bonjour!\"<\/code> when the active locale is French. If the active locale were to change via <code>i18n.locale = newLocale<\/code>, our Vue component would re-render and attempt to show the <code>&lt;h2&gt;<\/code> with <code>newLocale<\/code>&#8216;s translation. In addition to the <code>t()<\/code> function, Vue I18n provides a Vue directive, <code>v-t<\/code>, that can be used to display translated messages. Let&#8217;s use the directive to localize our app&#8217;s header.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"31\" data-enlighter-title=\"src \u203a locales \u203a en.json \">{\n  \"app\": {\n    \"title\": \"International Gourmet Coffee\"\n  },\n  \"nav\": {\n    \"home\": \"Home\",\n    \"about\": \"About\"\n  }\n}<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"32\" data-enlighter-title=\"src \u203a locales \u203a ar.json\">{\n  \"app\": {\n    \"title\": \"\u0642\u0647\u0648\u0629 \u0627\u0644\u0630\u0648\u0627\u0642\u0629 \u0627\u0644\u062f\u0648\u0644\u064a\u0629\"\n  },\n  \"nav\": {\n    \"home\": \"\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629\",\n    \"about\": \"\u0646\u0628\u0630\u0629 \u0639\u0646\u0627\"\n  }\n}<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"33\" data-enlighter-title=\"src \u203a views &gt; Home.vue \">&lt;template&gt;\n  &lt;div class=\"home\"&gt;\n    &lt;h1 v-t=\"'app.title'\" \/&gt;\n    &lt;Cards \/&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\n\/\/ ...\n&lt;\/script&gt;<\/pre>\n<p>Notice that when using <code>v-t<\/code> above, we wrapped its value in two sets of quotes. This is to provide the literal string <code>'app.title'<\/code> to the directive, which causes it to behave like <code>t('app.title')<\/code>. Also, note that whether using <code>t()<\/code> or <code>v-t<\/code>, we can access messages that are nested in our JSON files via dot (<code>.<\/code>) notation.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The <code>t()<\/code> function is versatile, and has a few variants. Check out the <a href=\"https:\/\/kazupon.github.io\/vue-i18n\/api\/#methods\">official API documentation<\/a> to get more details. While you&#8217;re there, you may want to take a look at the <code>v-t<\/code> <a href=\"https:\/\/kazupon.github.io\/vue-i18n\/api\/#v-t\">documentation<\/a> as well. You&#8217;ll find the directive has a few tricks up its sleeve.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> You can completely override the way messages are formatted with Vue I18n. Read the <a href=\"https:\/\/kazupon.github.io\/vue-i18n\/guide\/formatting.html#custom-formatting\">official documentation on custom formatting<\/a> for more info.<\/p>\n<style type=\"text\/css\"><!--td {border: 1px solid #ccc;}br {mso-data-placement:same-cell;}--><\/style>\n<h2><span class=\"ez-toc-section\" id=\"interpolation\"><\/span><a name=\"interpolation\"><\/a>Interpolation<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We often want to inject dynamic values into our translation messages. For example, we may have a greeting in our nav bar that we show to a logged-in user.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"34\" data-enlighter-title=\"src \u203a locales \u203a en.json \">{\n  \"user_greeting\": \"Hello, {name}\"\n}<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"35\" data-enlighter-title=\"src \u203a locales \u203a ar.json \">{\n  \"user_greeting\": \"\u0645\u0631\u062d\u0628\u0627 {name}\"\n}<\/pre>\n<p>Vue I18n will replace the <code>{name}<\/code> placeholder if we give a <code>t()<\/code> object parameter that provides the <code>name<\/code>&#8216;s value.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"36\" data-enlighter-title=\"src \u203a components \u203a Nav.vue \">&lt;template&gt;\n  &lt;div class=\"nav\"&gt;\n    &lt;!-- ... --&gt;\n    &lt;div class=\"nav__end\"&gt;\n      &lt;p class=\"user-greeting\"&gt;\n        {{ $t(\"user_greeting\", { name: \"Adam\" }) }}\n      &lt;\/p&gt;\n      &lt;LocaleSwitcher \/&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\n\/\/ ...\n&lt;\/script&gt;\n&lt;style scoped&gt;\n\/* ... *\/\n&lt;\/style&gt;<\/pre>\n<p>Given the messages above, we get <code>{name}<\/code> replaced with <code>\"Adam\"<\/code>. <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8391 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-navbar-w-interpolated-value-1024x87.png\" alt=\"Demo app bar with name | Phrase\" width=\"1024\" height=\"87\" \/><\/p>\n<p style=\"text-align: center;\"><em>Hey there, Adam<\/em><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Vue I18n also supports list formatting e.g. <code>\"Hello {0}\"<\/code>. Check out the <a href=\"https:\/\/kazupon.github.io\/vue-i18n\/guide\/formatting.html#formatting\">official documentation on formatting<\/a> for more details.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"using-html-in-translation-messages\"><\/span><a name=\"using-html-in-translation-messages\"><\/a>Using HTML in Translation Messages<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Sometimes it&#8217;s much more convenient for <a href=\"https:\/\/phrase.com\/blog\/posts\/a-day-in-the-life-of-a-translator\/\">translators<\/a> to have HTML in translation messages, rather than breaking up translation messages into smaller parts. Let&#8217;s add a footer to our app to explore this.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"37\" data-enlighter-title=\"src \u203a locales \u203a en.json \">{\n  \"footer\": \"Built with Vue and Vue I18n&lt;br \/&gt;Powered by an excessive amount of coffee\"\n}<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"38\" data-enlighter-title=\"src \u203a locales \u203a ar.json \">{\n  \"footer\": \"\u0628\u0646\u064a\u062a \u0628\u0648\u0627\u0633\u0637\u0629 Vue \u0648 Vue I18n&lt;br \/&gt;\u0645\u062f\u0639\u0648\u0645 \u0645\u0646 \u0643\u0645\u064a\u0629 \u0642\u0647\u0648\u0629 \u0645\u0628\u0627\u0644\u063a \u0641\u064a\u0647\u0627\"\n}<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"39\" data-enlighter-title=\"src \u203a components \u203a Footer.vue \">&lt;template&gt;\n  &lt;p class=\"footer\"&gt;{{ $t(\"footer\") }}&lt;\/p&gt;\n&lt;\/template&gt;\n&lt;style scoped&gt;\n.footer {\n  text-align: center;\n  margin-bottom: 1rem;\n}\n&lt;\/style&gt;<\/pre>\n<p>As it is, if we were to pull our new <code>Footer<\/code> component into our <code>App<\/code> root component, we would get less than desirable results. <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8392 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-footer-escaped-html-1024x73.png\" alt=\"HTML code in text in demo app | Phrase\" width=\"1024\" height=\"73\" \/><\/p>\n<p style=\"text-align: center;\"><em>Our HTML is escaped, which isn&#8217;t what we want<\/em><\/p>\n<p>Now we <em>could<\/em> use Vue&#8217;s generally unsafe <code>v-html<\/code> directive to solve this problem.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"40\" data-enlighter-title=\"src \u203a components \u203a Footer.vue \">&lt;template&gt;\n  &lt;p class=\"footer\" v-html=\"$t('footer')\" \/&gt;\n&lt;\/template&gt;<\/pre>\n<p>This certainly works: our <code>Footer<\/code> will now render two lines in the browser instead of one. The issue here is that <code>v-html<\/code> opens up our app to XSS injection attacks. Of course, we would generally trust our translators not to inject malicious code into our app. Still, if we can avoid <code>v-html<\/code> entirely, we can sleep better at night. Enter: component interpolation. We can use Vue I18n&#8217;s <code>&lt;i18n&gt;<\/code> component to avoid <code>v-html<\/code>. First, let&#8217;s update our translation messages.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"41\" data-enlighter-title=\"src \u203a locales \u203a en.json \">{\n  \"footer\": \"Built with Vue and Vue I18n{0}Powered by an excessive amount of coffee\"\n}<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"42\" data-enlighter-title=\"src \u203a locales \u203a ar.json \">{\n  \"footer\": \"\u0628\u0646\u064a\u062a \u0628\u0648\u0627\u0633\u0637\u0629 Vue \u0648 Vue I18n{0}\u0645\u062f\u0639\u0648\u0645 \u0645\u0646 \u0643\u0645\u064a\u0629 \u0642\u0647\u0648\u0629 \u0645\u0628\u0627\u0644\u063a \u0641\u064a\u0647\u0627\"\n}<\/pre>\n<p>Instead of having the <code>&lt;br \/&gt;<\/code> directly in our message, we have a <code>{0}<\/code> placeholder. Now let&#8217;s update our <code>Footer<\/code> to use the <code>&lt;i18n&gt;<\/code> component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"43\" data-enlighter-title=\"src \u203a components \u203a Footer.vue \">&lt;template&gt;\n  &lt;i18n path=\"footer\" tag=\"p\" class=\"footer\"&gt;\n    &lt;br \/&gt;\n  &lt;\/i18n&gt;\n&lt;\/template&gt;<\/pre>\n<p>The <code>\"footer\"<\/code> key corresponds to the one in our translation files and is provided as the <code>path<\/code> prop to <code>&lt;i18n&gt;<\/code>. We also give the component a <code>tag<\/code> prop, which instructs it to render our translated message in a <code>&lt;p&gt;<\/code> tag. Notice that we&#8217;ve included regular HTML, namely our <code>&lt;br \/&gt;<\/code>, <em>within<\/em> the opening and closing tags of the <code>&lt;i18n&gt;<\/code> component. We could place any HTML we like here, and Vue I18n will inject it where the <code>{0}<\/code> placeholder is in our message. With this in place, our footer renders with two lines, exactly like we want. <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8393 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-footer-no-escaped-html.png\" alt=\"HTML in demo app bug fixed | Phrase\" width=\"692\" height=\"146\" \/><\/p>\n<p style=\"text-align: center;\"><em>No escape: We have HTML without exposing ourselves to XSS<\/em><\/p>\n<blockquote><p>\ud83d\udd17 <em>Resource \u00bb<\/em> Read more about component interpolation in <a href=\"https:\/\/kazupon.github.io\/vue-i18n\/guide\/interpolation.html#component-interpolation\">Vue I18n&#8217;s official documentation<\/a>.<\/p><\/blockquote>\n<h2><span class=\"ez-toc-section\" id=\"plurals\"><\/span><a name=\"plurals\"><\/a>Plurals<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Let&#8217;s add a counter that shows the number of likes on each variety of coffee we have. First, we&#8217;ll update our JSON data to provide the number of likes.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"44\" data-enlighter-title=\"public \u203a data.json \">[\n  {\n    \"id\": 1,\n    \"title\": \"Battlecreek Columbia Coldono\",\n    \"imgUrl\": \"\/img\/battlecreek-coffee-roasters-_1wDmr4dtuk-unsplash.jpg\",\n    \"addedOn\": \"2020-01-02\",\n    \"likes\": 3\n  },\n  {\n    \"id\": 2,\n    \"title\": \"Battlecreek Yirgacheffee\",\n    \"imgUrl\": \"\/img\/battlecreek-coffee-roasters-HvzR2yXtii4-unsplash.jpg\",\n    \"addedOn\": \"2020-01-05\",\n    \"likes\": 0\n  },\n  \/\/ ...\n]<\/pre>\n<p>Now we can update our <code>Card<\/code> component to bring in the likes\u0010.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"45\" data-enlighter-title=\"src \u203a components \u203a Card.vue \">&lt;template&gt;\n  &lt;div class=\"card\"&gt;\n    &lt;h3&gt;{{ title }}&lt;\/h3&gt;\n    &lt;img :src=\"imgUrl\" \/&gt;\n    &lt;div class=\"card__footer\"&gt;\n      &lt;p&gt;Added {{ addedOn }}&lt;\/p&gt;\n      &lt;p&gt;{{likes}} people \u2764\ufe0f this&lt;\/p&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nexport default {\n  props: {\n    id: Number,\n    title: String,\n    imgUrl: String,\n    addedOn: String,\n    likes: Number\n  }\n}\n&lt;\/script&gt;\n&lt;style scoped&gt;\n.card {\n  width: 30%;\n  margin-bottom: 2rem;\n  border: 1px solid #f2f2f2;\n  border-radius: 4px;\n  box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);\n}\nh3 {\n  margin: 0;\n  padding: 0.5em;\n  text-align: center;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\nimg {\n  width: 100%;\n}\n.card__footer {\n  text-align: center;\n  margin: 0;\n  padding: 0.5em;\n}\np {\n  margin: 0;\n  font-size: 14px;\n}\np:first-child {\n  margin-bottom: 0.5rem;\n}\n&lt;\/style&gt;<\/pre>\n<p>This works, but our likes message is currently hard-coded. We could use the <code>$t()<\/code> function to localize it as usual, but this won&#8217;t take into account the various plural forms our message can have. In English, for example, we want &#8220;No people \u2665\ufe0f this&#8221;, &#8220;1 person \u2665\ufe0f this&#8221;, and &#8220;3 people \u2665\ufe0f like this&#8221; to display depending on the number of likes. Vue I18n&#8217;s <code>$tc()<\/code> function can help with displaying plurals. To use it, we need to adopt a special syntax in our pluralized message. Let&#8217;s update our English message to see this.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"46\" data-enlighter-title=\"src \u203a locales \u203a en.json \">{\n  \"card\": {\n    \"likes\": \"Nobody \u2764\ufe0f this yet \ud83d\ude15 | {n} person \u2764\ufe0f this | {n} people \u2764\ufe0f this\"\n  }\n}<\/pre>\n<p>English has three plural forms: zero, one, and many. We write all three forms in our message, separated by the <code>|<\/code>. We also use the special <code>{n}<\/code> placeholder to display the number <code>$tc()<\/code> receives. We can now use the function to dynamically display the correct plural form.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"47\" data-enlighter-title=\"src \u203a components \u203a Card.vue \">&lt;template&gt;\n  &lt;div class=\"card\"&gt;\n    &lt;h3&gt;{{ title }}&lt;\/h3&gt;\n    &lt;img :src=\"imgUrl\" \/&gt;\n    &lt;div class=\"card__footer\"&gt;\n      &lt;p&gt;Added {{ addedOn }}&lt;\/p&gt;\n      &lt;p&gt;{{$tc(\"card.likes\", likes)}}&lt;\/p&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\n\/\/ ...\n&lt;\/script&gt;\n&lt;style scoped&gt;\n\/* ... *\/\n&lt;\/style&gt;<\/pre>\n<p><code>$tc()<\/code> is similar to <code>$t()<\/code>, except it takes a second, number argument that determines how it chooses between the plural forms we provided in the respective message. With the above changes in place, we get dynamic plural output.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> We can use <code>{count}<\/code> instead of <code>{n}<\/code>. Vue I18n treats either of them as the special count characrer in plural messages.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8394 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-en-plurals-972x1024.png\" alt=\"Demo app with plurals | Phrase\" width=\"972\" height=\"1024\" \/><\/p>\n<p style=\"text-align: center;\"><em>So much \u2764\ufe0f<\/em><\/p>\n<p>Notice that we haven&#8217;t included our Arabic messages yet. That&#8217;s because we have a bit of a problem with Arabic, and we&#8217;ll need custom logic to solve it.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"custom-pluralization\"><\/span><a name=\"custom-pluralization\"><\/a>Custom Pluralization<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Vue I18n works with languages that have three plural forms out of the box. However, some languages have different plural rules. Arabic, for example, has six plural forms. For these scenarios, we&#8217;ll need to override <code>VueI18n<\/code>&#8216;s <code>getChoiceIndex()<\/code> method.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"48\" data-enlighter-title=\"src \u203a util \u203a i18n \u203a choice-index-for-plural.js \">let defaultChoiceIndex\nexport function setDefaultChoiceIndexGet(fn) {\n  defaultChoiceIndex = fn\n}\n\/**\n * @param choice {number} a choice index given by the input to\n *   $tc: `$tc('path.to.rule', choiceIndex)`\n * @param choicesLength {number} an overall amount of available choices\n * @returns a final choice index to select plural word by\n **\/\nexport function getChoiceIndex(choice, choicesLength) {\n  if (defaultChoiceIndex === undefined) {\n    return choice\n  }\n  \/\/ this === VueI18n instance, so the locale property also exists here\n  if (this.locale !== \"ar\") {\n    return defaultChoiceIndex.apply(this, [choice, choicesLength])\n  }\n  if ([0, 1, 2].includes(choice)) {\n    return choice\n  }\n  if (3 &lt;= choice &amp;&amp; choice &lt;= 10) {\n    return 3\n  }\n  if (11 &lt;= choice &amp;&amp; choice &lt;= 99) {\n    return 4\n  }\n  return 5\n}<\/pre>\n<p>We create a little utility file for ourselves that houses two functions, <code>setDefaultChoiceIndexGet()<\/code> and <code>getChoiceIndex()<\/code>. We&#8217;ll use <code>getChoiceIndex()<\/code> to override the <code>VueI18n<\/code> instance&#8217;s method by the same name. <code>getChoiceIndex()<\/code> does the work of determining which plural form to choose in a given message. We will sometimes want to defer to <code>VueI18n<\/code>&#8216;s original implementation of <code>getChoiceIndex()<\/code>, so we&#8217;ll provide <code>setDefaultChoiceIndexGet()<\/code> so that calling code can provide a reference to that original function for us. <code>getChoiceIndex()<\/code> receives two arguments, <code>choice: number<\/code> and <code>choicesLength: number<\/code> and is expected to return a <code>number<\/code> that it determines to be the index of the plural form to use in a message. The <code>choice<\/code> parameter is the given value of <code>n<\/code> or <code>count<\/code> . The <code>choicesLength<\/code> is the number of choices in the plural message. For example, the message <code>\"foo | bar | man\"<\/code> would give a <code>choiceLength<\/code> of <code>3<\/code>. In our code above, we&#8217;ve decided to keep things simple and ignore <code>choiceLength<\/code>, assuming that all Arabic plural messages will provide all 6 plural forms that are common to Arabic:<\/p>\n<ul>\n<li>0<\/li>\n<li>1<\/li>\n<li>2<\/li>\n<li>3-10<\/li>\n<li>11-99<\/li>\n<li>100+<\/li>\n<\/ul>\n<p>Let&#8217;s use this logic by wiring it up to <code>VueI8n<\/code>&#8216;s prototype.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"49\" data-enlighter-title=\"src \u203a i18n.js \">import Vue from \"vue\"\nimport VueI18n from \"vue-i18n\"\nimport getBrowserLocale from \"@\/util\/i18n\/get-browser-locale\"\nimport { supportedLocalesInclude } from \".\/util\/i18n\/supported-locales\"\nimport {\n  getChoiceIndex,\n  setDefaultChoiceIndexGet\n} from \".\/util\/i18n\/choice-index-for-plural\"\nVue.use(VueI18n)\n\/\/ ...\nsetDefaultChoiceIndexGet(VueI18n.prototype.getChoiceIndex)\nVueI18n.prototype.getChoiceIndex = getChoiceIndex\nexport default new VueI18n({\n  locale: getStartingLocale(),\n  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || \"en\",\n  messages: loadLocaleMessages()\n})<\/pre>\n<p>We make sure to call <code>setDefaultChoiceIndexGet()<\/code> with the original <code>getChoiceIndex()<\/code> before overriding with our own. This is because our <code>getChoiceIndex()<\/code> logic defers to the original implementation for non-Arabic locales. With that in place, we now have proper Arabic pluralization.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"50\" data-enlighter-title=\"src \u203a locales \u203a ar.json \">{\n  \/\/ ...\n  \"card\": {\n    \"likes\": \"\u0644\u0627 \u062a\u0648\u062c\u062f \u2764\ufe0f \u0625\u0644\u0649 \u0627\u0644\u0622\u0646 \ud83d\ude15 | \u0634\u062e\u0635 {n} \u2764\ufe0f \u0647\u0630\u0627 | \u0634\u062e\u0635\u0627\u0646 \u2764\ufe0f \u0647\u0630\u0627 | {n} \u0623\u0634\u062e\u0627\u0635 \u2764\ufe0f \u0647\u0630\u0627 | {n} \u0634\u062e\u0635 \u2764\ufe0f \u0647\u0630\u0627 | {n} \u0634\u062e\u0635 \u2764\ufe0f \u0647\u0630\u0627 \"\n  }\n}<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8395 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-ar-plurals-875x1024.png\" alt=\"Demo app in Arabic | Phrase\" width=\"875\" height=\"1024\" \/><\/p>\n<p style=\"text-align: center;\"><em>Our Arabic plurals are taking their many, correct forms<\/em><\/p>\n<h2><span class=\"ez-toc-section\" id=\"formatting-dates\"><\/span><a name=\"formatting-dates\"><\/a>Formatting Dates<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>As it is, our &#8220;Added on&#8221; dates for our products are displayed as they are in our JSON data. <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8396 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-dates-unformatted.png\" alt=\"Bad Date format in demo app | Phrase \" width=\"328\" height=\"72\" \/><\/p>\n<p style=\"text-align: center;\"><em>Not the prettiest date<\/em><\/p>\n<p>Luckily, Vue I18n taps into the standard <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/DateTimeFormat\">Intl.DateTimeFormat<\/a> to allow for flexible date formatting. First, let&#8217;s create a <code>dateTimeFormats<\/code> object to pass to the <code>VueI8n<\/code> constructor.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"51\" data-enlighter-title=\"src \u203a locales \u203a date-time-formats.js \">const dateTimeFormats = {\n  en: {\n    short: { year: \"numeric\", month: \"short\", day: \"numeric\" }\n  },\n  ar: {\n    short: { year: \"numeric\", month: \"long\", day: \"numeric\" }\n  }\n}\nexport default dateTimeFormats<\/pre>\n<p>We key our object by our supported locale codes. Under each locale key we can have any number of formats we want. In the above code, we provide a <code>\"short\"<\/code> date format, which differs subtly across locales. Notice that the format is defined as an object: this object is passed as the <code>options<\/code> parameter to the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/DateTimeFormat#Syntax\">Intl.DateTimeFormat constructor<\/a>, which handles the formatting. Now we can pass this <code>dateTimeFormats<\/code> object to the Vue I18n constructor for it to take effect.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"52\" data-enlighter-title=\"src \u203a i18n.js \">import Vue from \"vue\"\nimport VueI18n from \"vue-i18n\"\nimport getBrowserLocale from \"@\/util\/i18n\/get-browser-locale\"\nimport { supportedLocalesInclude } from \".\/util\/i18n\/supported-locales\"\nimport {\n  getChoiceIndex,\n  setDefaultChoiceIndexGet\n} from \".\/util\/i18n\/choice-index-for-plural\"\nimport dateTimeFormats from \"@\/locales\/date-time-formats\"\nVue.use(VueI18n)\n\/\/ ...\nexport default new VueI18n({\n  locale: getStartingLocale(),\n  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || \"en\",\n  messages: loadLocaleMessages(),\n  dateTimeFormats\n})<\/pre>\n<p>With our wiring done, we can now use Vue I18n&#8217;s <code>$d()<\/code> function to display the formatted date.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"53\" data-enlighter-title=\"src \u203a components \u203a Card.vue \">&lt;template&gt;\n  &lt;div class=\"card\"&gt;\n    &lt;h3&gt;{{ title }}&lt;\/h3&gt;\n    &lt;img :src=\"imgUrl\" \/&gt;\n    &lt;div class=\"card__footer\"&gt;\n      &lt;p&gt;\n        {{ $t(\"card.added\") }}\n        {{ $d(new Date(addedOn), \"short\") }}\n      &lt;\/p&gt;\n      &lt;p&gt;{{$tc(\"card.likes\", likes)}}&lt;\/p&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\n\/\/ ...\n&lt;\/script&gt;\n&lt;style scoped&gt;\n\/* ... *\/\n&lt;\/style&gt;<\/pre>\n<p>The <code>d()<\/code> function is another that the Vue I18n plugin provides to our components and is responsible for formatting dates. Note that the first parameter to <code>d()<\/code> must be a <code>Date<\/code> object, so we parse our <code>addedOn<\/code> string to a <code>Date<\/code> when we pass it to <code>d()<\/code>. Now our dates are much more presentable. <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8397 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-dates-formatted.png\" alt=\"Localized dates | Phrase\" width=\"588\" height=\"82\" \/><\/p>\n<p style=\"text-align: center;\"><em>Our dates are now formatted per-locale and per our liking<\/em><\/p>\n<h2><span class=\"ez-toc-section\" id=\"formatting-numbers\"><\/span><a name=\"formatting-numbers\"><\/a>Formatting Numbers<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Much like it handles date formatting, Vue I18n delegates its general number formatting to the standard <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/NumberFormat\">Intl.NumberFormat<\/a>. And much like date formatting, we provide a <code>numberFormats<\/code> object to the <code>VueI8n<\/code> constructor to determine our localized number formats.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"54\" data-enlighter-title=\"src \u203a locales \u203a number-formats.js \">const numberFormats = {\n  en: {\n    currency: { style: \"currency\", currency: \"USD\" }\n  },\n  ar: {\n    currency: { style: \"currency\", currency: \"USD\", currencyDisplay: \"code\" }\n  }\n}\nexport default numberFormats<\/pre>\n<p>We use our locale codes as keys. Below each key, we place as many formats as we like. Each format is an object that will be passed to the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/NumberFormat\">Intl.Numberformat constructor<\/a> as an <code>options<\/code> parameter. In the above, we specify currency formats for each of our two supported locales. We assume our currency is US dollars, and ensure that the ISO code (&#8220;USD&#8221;, not the &#8220;$&#8221; symbol) will be shown in Arabic. Let&#8217;s wire up this object to our <code>VueI18n<\/code> constructor.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"55\" data-enlighter-title=\"src \u203a i18n.js \">import Vue from \"vue\"\nimport VueI18n from \"vue-i18n\"\nimport getBrowserLocale from \"@\/util\/i18n\/get-browser-locale\"\nimport { supportedLocalesInclude } from \".\/util\/i18n\/supported-locales\"\nimport {\n  getChoiceIndex,\n  setDefaultChoiceIndexGet\n} from \".\/util\/i18n\/choice-index-for-plural\"\nimport dateTimeFormats from \"@\/locales\/date-time-formats\"\nimport numberFormats from \"@\/locales\/number-formats\"\nVue.use(VueI18n)\n\/\/ ...\nexport default new VueI18n({\n  locale: getStartingLocale(),\n  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || \"en\",\n  messages: loadLocaleMessages(),\n  dateTimeFormats,\n  numberFormats\n})<\/pre>\n<p>With the <code>numberFormats<\/code> option in place, we can now use our formats in our Vue components. Let&#8217;s add a price to our data to demonstrate.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"56\" data-enlighter-title=\"public \u203a data.json \">[\n  {\n    \"id\": 1,\n    \"title\": \"Battlecreek Columbia Coldono\",\n    \"price\": 29.99,\n    \"imgUrl\": \"\/img\/battlecreek-coffee-roasters-_1wDmr4dtuk-unsplash.jpg\",\n    \"addedOn\": \"2020-01-02\",\n    \"likes\": 3\n  },\n  \/\/ ...\n]<\/pre>\n<p>We can reference this price in our <code>Card<\/code> component and use Vue I18n&#8217;s <code>$n()<\/code> to display it.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"57\" data-enlighter-title=\"src \u203a components \u203a Card.vue \">&lt;template&gt;\n  &lt;div class=\"card\"&gt;\n    &lt;h3&gt;{{ title }}&lt;\/h3&gt;\n    &lt;img :src=\"imgUrl\" \/&gt;\n    &lt;div class=\"card__footer\"&gt;\n      &lt;div class=\"card__meta\"&gt;\n        &lt;p class=\"price\"&gt;{{$n(price, \"currency\")}}&lt;\/p&gt;\n        &lt;p&gt;{{ $d(new Date(addedOn), \"short\") }}&lt;\/p&gt;\n      &lt;\/div&gt;\n      &lt;p class=\"likes\"&gt;{{$tc(\"card.likes\", likes)}}&lt;\/p&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nexport default {\n  props: {\n    id: Number,\n    title: String,\n    price: Number,\n    imgUrl: String,\n    addedOn: String,\n    likes: Number\n  }\n}\n&lt;\/script&gt;\n&lt;style scoped&gt;\n.card {\n  width: 30%;\n  margin-bottom: 2rem;\n  border: 1px solid #f2f2f2;\n  border-radius: 4px;\n  box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);\n}\nh3 {\n  margin: 0;\n  padding: 0.5em;\n  text-align: center;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\nimg {\n  width: 100%;\n}\n.card__footer {\n  text-align: center;\n  margin: 0;\n  padding: 0.5em;\n}\n.card__meta {\n  display: flex;\n  justify-content: space-between;\n  align-items: baseline;\n}\np {\n  margin: 0;\n  font-size: 14px;\n}\n.price {\n  margin-left: 0.5rem;\n  font-size: 1.25rem;\n  font-weight: bold;\n}\n.likes {\n  text-align: end;\n  margin-top: 0.5rem;\n}\n&lt;\/style&gt;<\/pre>\n<p>A lot like the <code>d()<\/code> function, <code>n()<\/code> takes a <code>number<\/code> and the key of the format we specified as parameters. We now have prices formatted differently per locale. <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8398 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-prices-formatted-1024x711.png\" alt=\"Demo app with localized price format | Phrase\" width=\"1024\" height=\"711\" \/><\/p>\n<p style=\"text-align: center;\"><em>Now accepting payments from multiple locales<\/em><\/p>\n<h2><span class=\"ez-toc-section\" id=\"changing-the-document-language-layout-direction\"><\/span><a name=\"changing-document-language-layout-direction\"><\/a>Changing the Document Language &amp; Layout Direction<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Arabic, Hebrew, and other languages are written from right to left, and pages presented in those locales need to be laid out in that direction as well. To accomplish this, we can listen for locale changes and update the <code>document.dir<\/code> property in the DOM. While we&#8217;re at it, let&#8217;s update the <code>lang<\/code> property of our document to reflect the currently active locale.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"58\" data-enlighter-title=\"src \u203a util \u203a i18n \u203a document.js \">export function setDocumentDirectionPerLocale(locale) {\n  document.dir = locale === \"ar\" ? \"rtl\" : \"ltr\"\n}\nexport function setDocumentLang(lang) {\n  document.documentElement.lang = lang\n}<\/pre>\n<p>We&#8217;ve added a helper function that takes a locale and sets the document direction. Another sets the <code>lang<\/code> attribute on the <code>HTML<\/code> element for correct meta. We can use these functions in our root <code>App<\/code> component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"59\" data-enlighter-title=\"src \u203a components \u203a App.js \">&lt;template&gt;\n  &lt;div id=\"app\"&gt;\n    &lt;Nav \/&gt;\n    &lt;div class=\"container\"&gt;\n      &lt;router-view \/&gt;\n    &lt;\/div&gt;\n    &lt;Footer \/&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport Nav from \"@\/components\/Nav\"\nimport Footer from \"@\/components\/Footer\"\nimport {\n  setDocumentDirectionPerLocale,\n  setDocumentLang\n} from \"@\/util\/i18n\/document\"\nexport default {\n  components: { Nav, Footer },\n  mounted() {\n    this.$watch(\n      \"$i18n.locale\",\n      (newLocale, oldLocale) =&gt; {\n        if (newLocale === oldLocale) {\n          return\n        }\n        setDocumentLang(newLocale)\n        setDocumentDirectionPerLocale(newLocale)\n      },\n      { immediate: true }\n    )\n  }\n}\n&lt;\/script&gt;\n&lt;style&gt;\n\/* ... *\/\n&lt;\/style&gt;<\/pre>\n<p>When the <code>App<\/code> component mounts to the DOM, we start watching the active locale for changes.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> We use the imperative <code>this.$watch<\/code>, and not the <code>watch<\/code> property of the component, so that we can provide the <code>immediate<\/code> option. When <code>true<\/code>, the <code>immediate<\/code> property causes our watcher to fire once during setup with the current value of the active locale. Otherwise, our watcher won&#8217;t fire on app start, and we won&#8217;t be able to set our document properties on initialization.<\/p>\n<p>With the logic above, we both initialize and sync our document language and direction to the active locale&#8217;s direction at all times. <img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-8399 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/vuei18n202001-layout-direction-811x1024.png\" alt=\"Demo app with proper right to left Arabic implementation | Phrase\" width=\"811\" height=\"1024\" \/><\/p>\n<p style=\"text-align: center;\"><em>English is displaying in left-to-right, Arabic in right-to-left<\/em><\/p>\n<h2><span class=\"ez-toc-section\" id=\"localizing-the-document-title\"><\/span><a name=\"localizing-document-title\"><\/a>Localizing the Document Title<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In a localized SPA we might want to change the document title, which appears in browser tabs, depending on the active locale. We already have an <code>\"app.title\"<\/code> in our message files. We also have a watcher from the previous section that allows us to run logic when the active locale changes. Let&#8217;s add a utility function and that helps wire these things up to localize our document title.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"60\" data-enlighter-title=\"src \u203a util \u203a i18n \u203a document.js \">export function setDocumentDirectionPerLocale(locale) {\n  document.dir = locale === \"ar\" ? \"rtl\" : \"ltr\"\n}\nexport function setDocumentLang(lang) {\n  document.documentElement.lang = lang\n}\nexport function setDocumentTitle(newTitle) {\n  document.title = newTitle\n}<\/pre>\n<p>Now we can use our new function in our <code>App<\/code> component, much like we did with document language &amp; layout direction.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"61\" data-enlighter-title=\"src \u203a App.vue \">&lt;template&gt;\n  &lt;div id=\"app\"&gt;\n    &lt;Nav \/&gt;\n    &lt;div class=\"container\"&gt;\n      &lt;router-view \/&gt;\n    &lt;\/div&gt;\n    &lt;Footer \/&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport Nav from \"@\/components\/Nav\"\nimport Footer from \"@\/components\/Footer\"\nimport {\n  setDocumentDirectionPerLocale,\n  setDocumentTitle,\n  setDocumentLang\n} from \"@\/util\/i18n\/document\"\nexport default {\n  components: { Nav, Footer },\n  mounted() {\n    this.$watch(\n      \"$i18n.locale\",\n      (newLocale, oldLocale) =&gt; {\n        if (newLocale === oldLocale) {\n          return\n        }\n        setDocumentLang(newLocale)\n        setDocumentDirectionPerLocale(newLocale)\n        setDocumentTitle(this.$t(\"app.title\"))\n      },\n      { immediate: true }\n    )\n  }\n}\n&lt;\/script&gt;\n&lt;style&gt;\n\/* ... *\/\n&lt;\/style&gt;<\/pre>\n<p>The browser tab&#8217;s title will now show our app&#8217;s title translated in the currently active locale.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"async-lazy-loading-of-translation-files\"><\/span><a name=\"async-lazy-loading-translation-files\"><\/a>Async (Lazy) Loading of Translation Files<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>If our app&#8217;s message files get past a certain size, and if we bundle them along with our main app bundle, as we&#8217;re currently doing, we&#8217;ll be making our app unnecessarily heavy for our users&#8217; first load. To counter this, we can use Webpack&#8217;s code-splitting feature to lazy load translation files when they&#8217;re needed. We&#8217;ll need to update our <code>i18n.js<\/code> file, and the way we load message files.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"62\" data-enlighter-title=\"src \u203a i18n.js \">import Vue from \"vue\"\nimport VueI18n from \"vue-i18n\"\nimport getBrowserLocale from \"@\/util\/i18n\/get-browser-locale\"\nimport { supportedLocalesInclude } from \".\/util\/i18n\/supported-locales\"\nimport {\n  getChoiceIndex,\n  setDefaultChoiceIndexGet\n} from \".\/util\/i18n\/choice-index-for-plural\"\nimport dateTimeFormats from \"@\/locales\/date-time-formats\"\nimport numberFormats from \"@\/locales\/number-formats\"\nVue.use(VueI18n)\nfunction getStartingLocale() {\n  const browserLocale = getBrowserLocale({ countryCodeOnly: true })\n  if (supportedLocalesInclude(browserLocale)) {\n    return browserLocale\n  } else {\n    return process.env.VUE_APP_I18N_LOCALE || \"en\"\n  }\n}\nsetDefaultChoiceIndexGet(VueI18n.prototype.getChoiceIndex)\nVueI18n.prototype.getChoiceIndex = getChoiceIndex\nconst startingLocale = getStartingLocale()\nconst i18n = new VueI18n({\n  locale: startingLocale,\n  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || \"en\",\n  messages: {},\n  dateTimeFormats,\n  numberFormats\n})\nconst loadedLanguages = []\nexport function loadLocaleMessagesAsync(locale) {\n  if (loadedLanguages.length &gt; 0 &amp;&amp; i18n.locale === locale) {\n    return Promise.resolve(locale)\n  }\n  \/\/ If the language was already loaded\n  if (loadedLanguages.includes(locale)) {\n    i18n.locale = locale\n    return Promise.resolve(locale)\n  }\n  \/\/ If the language hasn't been loaded yet\n  return import(\n    \/* webpackChunkName: \"locale-[request]\" *\/ `@\/locales\/${locale}.json`\n  ).then(messages =&gt; {\n    i18n.setLocaleMessage(locale, messages.default)\n    loadedLanguages.push(locale)\n    i18n.locale = locale\n    return Promise.resolve(locale)\n  })\n}\nloadLocaleMessagesAsync(startingLocale)\nexport default i18n<\/pre>\n<p>First, we remove the <code>loadLocaleMessages()<\/code> function that Vue I18n installed for us; we won&#8217;t need it since we&#8217;re implementing a different way to load translation messages. We grab the starting locale as usual, but we keep it in a variable called <code>startingLocale<\/code>, since we&#8217;ll need that a bit later. Also, instead of <code>export default<\/code>ing the <code>i18n<\/code> instance as soon as we create it, we hold onto it in an <code>i18n<\/code> variable, which we&#8217;ll need to access in our loading function.<\/p>\n<p>To cache the message files we&#8217;ve already loaded, we maintain a <code>loadedLanguages<\/code> array. In our new function, <code>loadLocaleMessagesAsync()<\/code>, we first check if this array has any values. If it does, then we&#8217;ve loaded messages before, so we check if the requested locale is the same as the previous one. If that&#8217;s true as well, we don&#8217;t need to do anything, since presumably we&#8217;ve already loaded and cached the messages for the requested locale.<\/p>\n<p>Otherwise, we check if the requested locale&#8217;s messages have been loaded before. If they have, we simply set the requested locale as the active locale and we&#8217;re done. If we haven&#8217;t loaded messages for the requested locale, however, we need to do that. We perform a dynamic import for the locale message file with <code>import().then()<\/code>. This will cause Webpack to code-split our app bundle so that our locale message files are separated into individual files, and not included in the main app bundle.<\/p>\n<p>The <code>\/* webpackChunkName: \"locale-[request]\" *\/<\/code> comment on the same line as the path to the message file instructs Webpack to give our message files special names when it code-splits them. The Arabic message file will be called <code>locale-ar.json<\/code>, for example, making it easier to track when the file is loaded. Before we export the <code>i18n<\/code> object, we call our new function <code>loadLocaleMessagesAsync<\/code> with the <code>startingLocale<\/code> to ensure that our app is initialized with <em>a<\/em> locale.<\/p>\n<p>\u270b\ud83c\udffd <em>Heads up \u00bb<\/em> By default, the Vue Webpack config will make Webpack prefetch all the async modules, including our locale message files. This kind of defeats the purpose of lazy loading, and we can turn it off by deleting the prefetch plugin using the <code>vue.config.js<\/code> file.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"63\" data-enlighter-title=\"vue.config.js \">module.exports = {\n  chainWebpack: config =&gt; {\n    config.plugins.delete(\"prefetch\")\n  },\n  pluginOptions: {\n    i18n: {\n      locale: \"en\",\n      fallbackLocale: \"en\",\n      localeDir: \"locales\",\n      enableInSFC: false\n    }\n  }\n}<\/pre>\n<p>Now let&#8217;s update our <code>App.vue<\/code> component to handle the new, asynchronous loading of message files.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"64\" data-enlighter-title=\"src \u203a App.vue \">&lt;template&gt;\n  &lt;div id=\"app\"&gt;\n    &lt;div v-if=\"isLoading\"&gt;Loading...&lt;\/div&gt;\n    &lt;div v-else&gt;\n      &lt;Nav v-on:localeChange=\"loadLocaleMessages\" \/&gt;\n      &lt;div class=\"container\"&gt;\n        &lt;router-view \/&gt;\n      &lt;\/div&gt;\n      &lt;Footer \/&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport Nav from \"@\/components\/Nav\"\nimport Footer from \"@\/components\/Footer\"\nimport {\n  setDocumentDirectionPerLocale,\n  setDocumentTitle,\n  setDocumentLang\n} from \"@\/util\/i18n\/document\"\nimport { loadLocaleMessagesAsync } from \"@\/i18n\"\nexport default {\n  data: () =&gt; ({\n    isLoading: true\n  }),\n  mounted() {\n    this.loadLocaleMessages(this.$i18n.locale)\n  },\n  methods: {\n    loadLocaleMessages(locale) {\n      this.isLoading = true\n      loadLocaleMessagesAsync(locale).then(() =&gt; {\n        setDocumentLang(locale)\n        setDocumentDirectionPerLocale(locale)\n        setDocumentTitle(this.$t(\"app.title\"))\n        this.isLoading = false\n      })\n    }\n  },\n  components: { Nav, Footer }\n}\n&lt;\/script&gt;\n&lt;style&gt;\n\/* ... *\/\n&lt;\/style&gt;<\/pre>\n<p>We need a loading state while a given message file is coming down the pipe. Otherwise, we&#8217;ll attempt to display component UI before any messages are available. Notice that we&#8217;re also subscribed to a <code>localeChange<\/code> event on the <code>Nav<\/code> component. We&#8217;ll get to that in a minute. Suffice it to say for now that we&#8217;ll simply be informed when a new locale is selected from the <code>LocaleSwitcher<\/code> via this event. When our <code>App<\/code> mounts, and again whenever the user selects a new locale, we call the new <code>loadLocaleMessages()<\/code> component method, which in turn calls the <code>loadLocaleMessagesAsync()<\/code> function in our i18n module, and sets the document attributes per the active locale once the message file has loaded.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"65\" data-enlighter-title=\"src \u203a components \u203a Nav.vue \">&lt;template&gt;\n  &lt;div class=\"nav\"&gt;\n    &lt;div class=\"nav__start\"&gt;\n      &lt;!-- ... --&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"nav__end\"&gt;\n      &lt;p class=\"user-greeting\"&gt;{{ $t(\"user_greeting\", { name: \"Adam\" }) }}&lt;\/p&gt;\n      &lt;LocaleSwitcher v-on:change=\"$emit('localeChange', $event)\" \/&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport LocaleSwitcher from \"@\/components\/LocaleSwitcher\"\nexport default {\n  components: { LocaleSwitcher }\n}\n&lt;\/script&gt;\n&lt;style scoped&gt;\n\/* ... *\/\n&lt;\/style&gt;<\/pre>\n<p>Our <code>Nav<\/code> component passes through a <code>localeChange<\/code> event to its parent (our <code>App<\/code> in this case). To do so, it subscribes to a <code>change<\/code> event on the <code>LocaleSwitcher<\/code> component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"66\" data-enlighter-title=\"src \u203a components \u203a LocaleSwitcher.vue \">&lt;template&gt;\n  &lt;div class=\"locale-switcher\"&gt;\n    &lt;select\n      :value=\"$i18n.locale\"\n      @change.prevent=\"$emit('change', $event.target.value)\"\n    &gt;\n      &lt;option :value=\"locale.code\" v-for=\"locale in locales\" :key=\"locale.code\"&gt;\n         {{locale.name}}\n       &lt;\/option&gt;\n    &lt;\/select&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport { getSupportedLocales } from \"@\/util\/i18n\/supported-locales\"\nimport { loadLocaleMessagesAsync } from \"@\/i18n\"\nexport default {\n  data: () =&gt; ({ locales: getSupportedLocales() })\n}\n&lt;\/script&gt;<\/pre>\n<p>No longer setting <code>i18n.locale<\/code> directly, our <code>LocaleSwitcher<\/code> simply emits a <code>change<\/code> event whenever the user selects a new locale. And with that in place, our app is now code-split so that locale message files are lazy-loaded on request. On apps with larger message files and\/or many locales, lazy loading should yield significant improvements to the initial app load, which is great for UX.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"localized-routes\"><\/span><a name=\"localized-routes\"><\/a>Localized Routes<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In an SPA we may well want our routes to be localized. Instead of <code>\/foo<\/code>, we would have <code>\/en\/foo<\/code> and <code>\/ar\/foo<\/code>. The locale code in the URI would determine the active locale of our app. This isn&#8217;t too hard to do with Vue&#8217;s first party, <a href=\"https:\/\/router.vuejs.org\/\">Vue Router<\/a>, which we&#8217;re already using in our app (albeit without localization).<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"67\" data-enlighter-title=\"src \u203a router \u203a index.js \">import Vue from \"vue\"\nimport VueRouter from \"vue-router\"\nimport Home from \"..\/views\/Home.vue\"\nVue.use(VueRouter)\nconst routes = [\n  {\n    path: \"\/\",\n    name: \"home\",\n    component: Home\n  },\n  {\n    path: \"\/about\",\n    name: \"about\",\n    \/\/ route level code-splitting\n    \/\/ this generates a separate chunk (about.[hash].js) for this route\n    \/\/ which is lazy-loaded when the route is visited.\n    component: () =&gt;\n      import(\/* webpackChunkName: \"about\" *\/ \"..\/views\/About.vue\")\n  }\n]\nconst router = new VueRouter({\n  mode: \"history\",\n  base: process.env.BASE_URL,\n  routes\n})\nexport default router<\/pre>\n<p>This is how the Vue CLI sets up our app when we select the router option during installation. We&#8217;ll need to reorganize these routes to introduce a <code>:locale<\/code> route parameter.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"68\" data-enlighter-title=\"src \u203a router \u203a index.js \">import Vue from \"vue\"\nimport VueRouter from \"vue-router\"\nimport Home from \"..\/views\/Home.vue\"\nVue.use(VueRouter)\nconst routes = [\n  {\n    path: \"\/\",\n    name: \"home\",\n    component: Home\n  },\n  {\n    path: \"\/about\",\n    name: \"about\",\n    \/\/ route level code-splitting\n    \/\/ this generates a separate chunk (about.[hash].js) for this route\n    \/\/ which is lazy-loaded when the route is visited.\n    component: () =&gt;\n      import(\/* webpackChunkName: \"about\" *\/ \"..\/views\/About.vue\")\n  }\n]\nconst router = new VueRouter({\n  mode: \"history\",\n  base: process.env.BASE_URL,\n  routes\n})\nexport default router<\/pre>\n<p>We&#8217;ve made our root <code>\/<\/code> route redirect to <code>\/en<\/code> or <code>\/ar<\/code>, depending on locale resolution in our <code>i18n<\/code> module. All our app&#8217;s routes are now nested under this latter, <code>:\/locale<\/code>, route. This means that our routes are always localized, e.g. our about page route is either <code>\/en\/about<\/code> or <code>\/ar\/about<\/code>. We use the <code>router.beforeEach()<\/code> <a href=\"https:\/\/router.vuejs.org\/guide\/advanced\/navigation-guards.html#global-before-guards\">navigation guard<\/a> to catch any route changes, and watch for the case when a route change means that we&#8217;ve changed locales. When this happens, we call our usual locale loading logic, starting with <code>loadLocaleMessagesAsync()<\/code>, which was previously in our <code>App<\/code> component. Notice that the top <code>\/:locale<\/code> route is associated with a <code>Root<\/code> component. This is just a container with a <code>&lt;router-view&gt;<\/code> to render the component&#8217;s children.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"69\" data-enlighter-title=\"src \u203a router \u203a Root.vue \">&lt;template&gt;\n  &lt;router-view \/&gt;\n&lt;\/template&gt;<\/pre>\n<p>Our locale switching effectively happens in the <code>router.beforEach()<\/code> navigation guard now, so we&#8217;ll need to update our other modules to accommodate that. First, let&#8217;s add a simple global event bus. This will allow any module in our app to listen for async locale message file loading events, one for begin loading and another for end loading, which we&#8217;ll add in a moment.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"70\" data-enlighter-title=\"src \u203a EventBus.js \">import Vue from \"vue\"\nconst EventBus = new Vue()\nexport default EventBus<\/pre>\n<p>A separate <code>Vue<\/code> instance can serve as a perfect event bus. Now we can use this to emit our events in our <code>i18n<\/code> module.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"71\" data-enlighter-title=\"src \u203a i18n.js \">import Vue from \"vue\"\nimport VueI18n from \"vue-i18n\"\nimport getBrowserLocale from \"@\/util\/i18n\/get-browser-locale\"\nimport { supportedLocalesInclude } from \".\/util\/i18n\/supported-locales\"\nimport {\n  getChoiceIndex,\n  setDefaultChoiceIndexGet\n} from \".\/util\/i18n\/choice-index-for-plural\"\nimport dateTimeFormats from \"@\/locales\/date-time-formats\"\nimport numberFormats from \"@\/locales\/number-formats\"\nimport EventBus from \"@\/EventBus\"\nVue.use(VueI18n)\n\/\/ ...\nconst loadedLanguages = []\nexport function loadLocaleMessagesAsync(locale) {\n  EventBus.$emit(\"i18n-load-start\")\n  if (loadedLanguages.length &gt; 0 &amp;&amp; i18n.locale === locale) {\n    EventBus.$emit(\"i18n-load-complete\")\n    return Promise.resolve(locale)\n  }\n  \/\/ If the language was already loaded\n  if (loadedLanguages.includes(locale)) {\n    i18n.locale = locale\n    EventBus.$emit(\"i18n-load-complete\")\n    return Promise.resolve(locale)\n  }\n  \/\/ If the language hasn't been loaded yet\n  return import(\n    \/* webpackChunkName: \"locale-[request]\" *\/ `@\/locales\/${locale}.json`\n  ).then(messages =&gt; {\n    i18n.setLocaleMessage(locale, messages.default)\n    loadedLanguages.push(locale)\n    i18n.locale = locale\n    EventBus.$emit(\"i18n-load-complete\")\n    return Promise.resolve(locale)\n  })\n}\nexport default i18n<\/pre>\n<p>We simply <code>$emit()<\/code> an <code>\"i18n-load-start\"<\/code> event at the beginning of our <code>\u0010loadLocaleMessagesAsync()<\/code> function. Whenever we resolve our locale successfully, we emit an <code>\"i18n-load-complete\"<\/code> event. Now we have a global notification whenever we start and complete a locale message file load.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"72\" data-enlighter-title=\"src \u203a App.vue \">&lt;template&gt;\n  &lt;div id=\"app\"&gt;\n    &lt;div v-if=\"isLoading\"&gt;Loading...&lt;\/div&gt;\n    &lt;div v-else&gt;\n      &lt;Nav \/&gt;\n      &lt;div class=\"container\"&gt;\n        &lt;router-view \/&gt;\n      &lt;\/div&gt;\n      &lt;Footer \/&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport Nav from \"@\/components\/Nav\"\nimport Footer from \"@\/components\/Footer\"\nimport EventBus from \"@\/EventBus\"\nexport default {\n  data: () =&gt; ({\n    isLoading: true\n  }),\n  mounted() {\n    EventBus.$on(\"i18n-load-start\", () =&gt; (this.isLoading = true))\n    EventBus.$on(\"i18n-load-complete\", () =&gt; (this.isLoading = false))\n  },\n  components: { Nav, Footer }\n}\n&lt;\/script&gt;\n&lt;style&gt;\n\/* ... *\/\n&lt;\/style&gt;<\/pre>\n<p>We pull our <code>EventBus<\/code> into our <code>App<\/code> and use it to determine whether we should show our UI or our loading state. Our <code>App<\/code> component is no longer responsible for loading message files. That responsibility has moved over to our routing. All the <code>App<\/code> component is doing now making sure that the UI doesn&#8217;t look broken while we&#8217;re loading message files. To round out our localized routing logic, let&#8217;s update our <code>LocaleSwitcher<\/code> to redirect to a localized route when the user changes her language.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"73\" data-enlighter-title=\"src \u203a components \u203a LocaleSwitcher.vue \">&lt;template&gt;\n  &lt;div class=\"locale-switcher\"&gt;\n    &lt;select :value=\"$i18n.locale\" @change.prevent=\"changeLocale\"&gt;\n      &lt;option :value=\"locale.code\" v-for=\"locale in locales\" :key=\"locale.code\"&gt;\n        {{locale.name}}\n      &lt;\/option&gt;\n    &lt;\/select&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport { getSupportedLocales } from \"@\/util\/i18n\/supported-locales\"\nexport default {\n  data: () =&gt; ({ locales: getSupportedLocales() }),\n  methods: {\n    changeLocale(e) {\n      const locale = e.target.value\n      this.$router.push(`\/${locale}`)\n    }\n  }\n}\n&lt;\/script&gt;<\/pre>\n<p>We use the <code>$router<\/code> object\u2014which Vue makes available to our components when we register the Vue Router plugin\u2014to programmatically navigate to <code>\/ar<\/code> if the user selects Arabic. That about does it. We now have localized routes in our Vue SPA.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"localized-router-links\"><\/span><a name=\"localized-router-links\"><\/a>Localized Router Links<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Continuing our localized routing from the last section, let&#8217;s take a look at the current state of our <code>Nav<\/code> component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"74\" data-enlighter-title=\"src \u203a components \u203a Nav.vue \">&lt;template&gt;\n  &lt;div class=\"nav\"&gt;\n    &lt;div class=\"nav__start\"&gt;\n      &lt;img alt=\"Vue logo\" src=\"..\/assets\/logo-circle-sm.png\" \/&gt;\n      &lt;router-link to=\"\/\"&gt;{{ $t(\"nav.home\") }}&lt;\/router-link&gt;\n      &lt;router-link to=\"\/about\"&gt;{{ $t(\"nav.about\") }}&lt;\/router-link&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"nav__end\"&gt;\n      &lt;p class=\"user-greeting\"&gt;{{ $t(\"user_greeting\", { name: \"Adam\" }) }}&lt;\/p&gt;\n      &lt;LocaleSwitcher \/&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport LocaleSwitcher from \"@\/components\/LocaleSwitcher\"\nexport default {\n  components: { LocaleSwitcher }\n}\n&lt;\/script&gt;\n&lt;style scoped&gt;\n\/* ... *\/\n&lt;\/style&gt;<\/pre>\n<p>Notice that our <code>&lt;router-link&gt;<\/code>s aren&#8217;t really localized. The <code>\"\/about\"<\/code> link will currently cause our app to error out since we only support <code>\"\/en\/about\"<\/code> and <code>\"\/ar\/about\"<\/code>. Let&#8217;s write a quick <code>LocalizedLink<\/code> component that we can use to solve this.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"75\" data-enlighter-title=\"src \u203a components \u203a LocalizedLink.vue \">&lt;template&gt;\n  &lt;router-link :to=\"getTo()\"&gt;\n    &lt;slot \/&gt;\n  &lt;\/router-link&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nexport default {\n  props: [\"to\"],\n  methods: {\n    getTo() {\n      if (typeof this.to !== \"string\") {\n        return this.to\n      }\n      const locale = this.$route.params.locale\n      \/\/ we strip leading and trailing slashes and prefix\n      \/\/ the current locale\n      return `\/${locale}\/${this.to.replace(\/^\\\/|\\\/$\/g, \"\")}`\n    }\n  }\n}\n&lt;\/script&gt;<\/pre>\n<p>We wrap a <code>&lt;router-link&gt;<\/code> with our own custom logic, transforming any <code>to<\/code> string parameter we receive into one with a locale prefix. So if we get <code>to=\"about\"<\/code>, and the current locale is Arabic, we pass <code>to=\"ar\/about\"<\/code> to <code>&lt;router-link&gt;<\/code>. We can now use our <code>LocalizedLink<\/code> to fix the links in our <code>Nav<\/code> component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"76\" data-enlighter-title=\"src \u203a components \u203a Nav.vue \">&lt;template&gt;\n  &lt;div class=\"nav\"&gt;\n    &lt;div class=\"nav__start\"&gt;\n      &lt;img alt=\"Vue logo\" src=\"..\/assets\/logo-circle-sm.png\" \/&gt;\n      &lt;LocalizedLink to=\"\/\"&gt;{{ $t(\"nav.home\") }}&lt;\/LocalizedLink&gt;\n      &lt;LocalizedLink to=\"\/about\"&gt;{{ $t(\"nav.about\") }}&lt;\/LocalizedLink&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"nav__end\"&gt;\n      &lt;p class=\"user-greeting\"&gt;{{ $t(\"user_greeting\", { name: \"Adam\" }) }}&lt;\/p&gt;\n      &lt;LocaleSwitcher \/&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n&lt;script&gt;\nimport LocaleSwitcher from \"@\/components\/LocaleSwitcher\"\nimport LocalizedLink from \"@\/components\/LocalizedLink\"\nexport default {\n  components: { LocaleSwitcher, LocalizedLink }\n}\n&lt;\/script&gt;\n&lt;style scoped&gt;\n\/* ... *\/\n&lt;\/style&gt;<\/pre>\n<p>Our links are working after this change. Now we can just pull in and use <code>LocalizedLink<\/code> without worrying about locales.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> You can grab the code for the entire, completed app from <a href=\"https:\/\/github.com\/PhraseApp-Blog\/vue-i18n-demo\">GitHub<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"closing-up\"><\/span><a name=\"closing-up\"><\/a>Closing Up<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>That&#8217;s it for this one, folks. We hope you enjoyed it, and that you learned a thing or two about Vue localization with the Vue I18n library. And if you want to give your team the ultimate translation experience, check out the <a href=\"https:\/\/phrase.github.io\/vue-i18n-phrase-in-context-editor\">Vue I18n Phrase in-context editor<\/a>.<br \/>\nInstalled with a few lines of code, the Phrase in-context editor will power up your Vue app with a powerful toolset, allowing your translators to work on app localization within the context of your app! This means no going back and forth between your app and the translation tooling: it\u2019s all in one place. And don&#8217;t worry, you can control whether translations happen in a sandbox or in the production version of your app.<br \/>\nThe Vue I18n in-context editor comes with Phrase, a comprehensive localization solution with powerful features like a CLI, translation syncing, a beautiful web console for translators, and much more. Take a look at all <a href=\"https:\/\/phrase.com\/roles\/developers\/\">Phrase features for developers<\/a>, and see for yourself how it can make your life easier.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Get an insight into Vue 2 localization and learn how to plug the Vue I18n library into your app to make it ready for global 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-35540","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\/35540"}],"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=35540"}],"version-history":[{"count":11,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/35540\/revisions"}],"predecessor-version":[{"id":44589,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/35540\/revisions\/44589"}],"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=35540"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/categories?post=35540"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}