{"id":8775,"date":"2022-11-04T03:51:00","date_gmt":"2022-11-04T03:51:00","guid":{"rendered":"https:\/\/phrase.com\/blog\/?p=8370"},"modified":"2023-09-22T15:51:20","modified_gmt":"2023-09-22T13:51:20","slug":"ultimate-guide-to-vue-localization-with-vue-i18n","status":"publish","type":"post","link":"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/","title":{"rendered":"A Comprehensive Guide to Vue Localization"},"content":{"rendered":"<p>Arguably the most approachable among the big three UI frameworks, Evan You\u2019s <a href=\"https:\/\/vuejs.org\/\">Vue<\/a> seems an unlikely contender among the giant Meta\u2019s React and Google\u2019s Angular. Yet this brainchild of one man has seen <a href=\"https:\/\/npmtrends.com\/@angular\/core-vs-react-vs-vue\">adoption that matches Angular\u2019s<\/a>, thanks to its gentle learning curve, first-class dev experience, and production-ready features.<\/p>\n<p>With its popularity, Vue has given birth to a rich ecosystem of plugins, extensions, and services. Vue app internationalization (i18n) \u2014 presumably the reason you\u2019re here \u2014 sees the robust third-party <a href=\"https:\/\/vue-i18n.intlify.dev\/\">Vue I18n<\/a> plugin as the <a href=\"https:\/\/npmtrends.com\/vue-i18n-vs-vue-i18next-vs-vuex-i18n\">apparent go-to<\/a>. In this hands-on guide, we\u2019ll use Vue I18n to internationalize a little demo app, covering everything you need to get started with Vue localization. Let\u2019s go.<\/p>\n<p>\u270b <em>Heads up \u00bb<\/em> This article covers Vue 3 localization. If you\u2019re interested in Vue 2, check out <a href=\"https:\/\/phrase.com\/blog\/posts\/vue-2-localization\/\">Vue 2 Localization with Vue I18n: A Step-by-Step Guide<\/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 <a href=\"https:\/\/phrase.com\/blog\/posts\/vue-translation-with-vue-i18next\/\">Deep Dive: Vue Translation with vue-i18next<\/a> might be useful to you.<\/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\/ultimate-guide-to-vue-localization-with-vue-i18n\/#library-versions-used\" title=\"Library versions used\">Library versions used<\/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\/ultimate-guide-to-vue-localization-with-vue-i18n\/#our-demo\" title=\"Our demo\">Our demo<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/#attributions\" title=\"Attributions\">Attributions<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-install-and-set-up-vue-i18n\" title=\"How do I install and set up Vue I18n?\">How do I install and set up Vue I18n?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-translate-messages-in-my-components\" title=\"How do I translate messages in my components?\">How do I translate messages in my components?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-work-with-dynamic-values-in-my-translation-messages\" title=\"How do I work with dynamic values in my translation messages?\">How do I work with dynamic values in my translation messages?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-translate-strings-in-my-component-javascript\" title=\"How do I translate strings in my component JavaScript?\">How do I translate strings in my component JavaScript?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-work-with-html-within-my-translation-messages\" title=\"How do I work with HTML within my translation messages?\">How do I work with HTML within my translation messages?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-work-with-plurals-in-my-translations\" title=\"How do I work with plurals in my translations?\">How do I work with plurals in my translations?<\/a><\/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\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-format-localized-numbers\" title=\"How do I format localized numbers?\">How do I format localized numbers?<\/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\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-format-localized-dates-and-times\" title=\"How do I format localized dates and times?\">How do I format localized dates and times?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-retrieve-the-active-locale\" title=\"How do I retrieve the active locale?\">How do I retrieve the active locale?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/#refactoring-the-i18n-library\" title=\"Refactoring the i18n library\">Refactoring the i18n library<\/a><\/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\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-localize-my-routes\" title=\"How do I localize my routes?\">How do I localize my routes?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-build-a-reusable-localized-link-component\" title=\"How do I build a reusable localized link component?\">How do I build a reusable localized link component?<\/a><\/li><\/ul><\/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\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-build-a-language-switcher-ui\" title=\"How do I build a language switcher UI?\">How do I build a language switcher UI?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/#binding-directly-to-i18nlocale\" title=\"Binding directly to i18n.locale\">Binding directly to i18n.locale<\/a><\/li><\/ul><\/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\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-load-my-translation-files-asynchronously\" title=\"How do I load my translation files asynchronously?\">How do I load my translation files asynchronously?<\/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\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-work-with-locale-fallback\" title=\"How do I work with locale fallback?\">How do I work with locale fallback?<\/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\/ultimate-guide-to-vue-localization-with-vue-i18n\/#how-do-i-localize-my-vue-app-with-the-composition-api\" title=\"How do I localize my Vue app with the Composition API?\">How do I localize my Vue app with the Composition API?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-21\" href=\"https:\/\/phrase.com\/blog\/posts\/ultimate-guide-to-vue-localization-with-vue-i18n\/#refactoring-i18n-to-use-the-composition-api\" title=\"Refactoring i18n to use the Composition API\">Refactoring i18n to use the Composition API<\/a><\/li><\/ul><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"library-versions-used\"><\/span>Library versions used<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We\u2019ve used the following NPM packages in this article (versions in parentheses).<\/p>\n<ul>\n<li>Vue (3.2) \u2014 our UI framework<\/li>\n<li>Vue router (4.1) \u2014 the official Vue SPA router<\/li>\n<li>Vue I18n (9.2) \u2014\u00a0Vue\u2019s third-party go-to i18n library<\/li>\n<li>Tailwind CSS (3.1) \u2014 used for styling and optional for our purposes<\/li>\n<\/ul>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> To focus on the i18n, we won\u2019t show any CSS styling in this article. You can find all styling code in the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/vue3-i18n-2022\">full code of our article<\/a> on GitHub.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"our-demo\"><\/span>Our demo<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Our humble demo, <em>Mushahed<\/em>, is based on data from the <a href=\"http:\/\/open-notify.org\/\">Open Notify<\/a> space APIs.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-35546\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/before-i18n.jpg\" alt=\"Our demo app celebrates the world\u2019s brave astronauts\" width=\"1170\" height=\"1238\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/before-i18n.jpg 1170w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/before-i18n-284x300.jpg 284w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/before-i18n-968x1024.jpg 968w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/before-i18n-768x813.jpg 768w\" sizes=\"(max-width: 1170px) 100vw, 1170px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Our demo app celebrates the world\u2019s brave astronauts<\/span><\/p>\n<h3><span class=\"ez-toc-section\" id=\"attributions\"><\/span>Attributions<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Shout outs to the following people and organizations for providing their assets for free.<\/p>\n<ul>\n<li>We\u2019ve used the <a href=\"https:\/\/thenounproject.com\/icon\/satellite-2801958\/\">Satellite<\/a> icon by <a href=\"https:\/\/thenounproject.com\/akritibhusal\/\">Akriti Bhusal<\/a> on The Noun Project for our demo brand icon.<\/li>\n<li><a href=\"https:\/\/en.wikipedia.org\/wiki\/Cai_Xuzhe#\/media\/File:%E8%88%AA%E5%A4%A9%E5%91%98%E8%94%A1%E6%97%AD%E5%93%B2_Cai_Xuzhe.jpg\">Cai Xuzhe\u2019s photo<\/a> and <a href=\"https:\/\/en.wikipedia.org\/wiki\/Chen_Dong_(taikonaut)#\/media\/File:%E8%88%AA%E5%A4%A9%E5%91%98%E9%99%88%E5%86%AC_Chen_Dong.jpg\">Chen Dong\u2019s photo<\/a> are copyrighted by China News Service and used under the <a href=\"https:\/\/creativecommons.org\/licenses\/by\/3.0\/\">CC BY 3.0<\/a> license.<\/li>\n<li><a href=\"https:\/\/en.wikipedia.org\/wiki\/Liu_Yang_(taikonaut)#\/media\/File:Liu_Yang_-_UNOOSA_50_Years_of_Women_in_Space_NHM_Vienna_2013_b.jpg\">Liu Yang\u2019s photo<\/a> is copyrighted by Manfred Werner (<a href=\"https:\/\/commons.wikimedia.org\/wiki\/User:Tsui\">Tsui<\/a>) and used under the <a href=\"https:\/\/creativecommons.org\/licenses\/by-sa\/3.0\/\">CC BY-SA 3.0<\/a> license.<\/li>\n<li>All other astronaut photos are public domain.<\/li>\n<\/ul>\n<p>Our demo is a Vue SPA spun up with <code>npm init vue@latest<\/code>. We added router support and opted out of TypeScript when the project was being scaffolded. After the requisite gutting of the boilerplate components added by the scaffolding tool, we built this little hierarchy:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\" data-enlighter-linenumbers=\"false\">.\n\u2514\u2500\u2500 src\/\n    \u251c\u2500\u2500 components\/\n    \u2502   \u251c\u2500\u2500 AstroCard.vue\n    \u2502   \u251c\u2500\u2500 Astronauts.vue\n    \u2502   \u251c\u2500\u2500 Coords.vue\n    \u2502   \u251c\u2500\u2500 Footer.vue\n    \u2502   \u2514\u2500\u2500 Nav.vue\n    \u251c\u2500\u2500 router\/\n    \u2502   \u2514\u2500\u2500 index.js\n    \u251c\u2500\u2500 views\/\n    \u2502   \u251c\u2500\u2500 HomeView.vue\n    \u2502   \u2514\u2500\u2500 AboutView.vue\n    \u2514\u2500\u2500 App.vue\n\n<\/pre>\n<div><\/div>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-35585\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/components.png\" alt=\"Our demo's component breakdown\" width=\"1170\" height=\"720\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/components.png 1170w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/components-300x185.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/components-1024x630.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/components-768x473.png 768w\" sizes=\"(max-width: 1170px) 100vw, 1170px\" \/><\/div>\n<div><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Our demo&#8217;s component breakdown<\/span><\/div>\n<div><\/div>\n<div><\/div>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> Our <code>App<\/code> houses a Vue <code>&lt;RouterView&gt;<\/code> and we\u2019re using <code>&lt;RouterLink&gt;<\/code>s in our nav. We\u2019ll look at routing a bit later.<\/p>\n<p>Let\u2019s take a closer look at our <code>&lt;Astronauts&gt;<\/code> component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"f46a1c80-3b46-4d90-b3ce-21c32a0f7375\" data-enlighter-title=\"src\/components\/Astronauts.vue\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\nimport AstroCard from '.\/AstroCard.vue'\n\nexport default {\n  components: { AstroCard },\n\n  data() {\n    return {\n      loading: true,\n      astros: [],\n    }\n  },\n\n  created() {\n    fetch('\/data\/astronauts.json')\n      .then((res) =&gt; res.json())\n      .then((data) =&gt; {\n        this.astros = data\n        this.loading = false\n      })\n  },\n}\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;!-- No i18n: Hard-coded English --&gt;\n  &lt;p v-if=\"loading\"&gt;Loading...&lt;\/p&gt;\n\n  &lt;div v-else&gt;\n    &lt;div&gt;\n      &lt;h2&gt;\n        &lt;!-- No i18n: Hard-coded plural --&gt;\n        \ud83e\uddd1\u200d\ud83d\ude80 {{ astros.length }} people in space\n      &lt;\/h2&gt;\n\n      &lt;p&gt;\n       &lt;!-- No i18n: Hard-coded date --&gt;\n        Updated Jul 26, 2022\n      &lt;\/p&gt;\n    &lt;\/div&gt;\n\n    &lt;div&gt;\n      &lt;AstroCard\n        v-for=\"astro in astros\"\n        :key=\"astro.id\"\n        :name=\"astro.name\"\n        :nationality=\"astro.nationality\"\n        :craft=\"astro.craft\"\n        :photoUrl=\"astro.photoUrl\"\n      \/&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>When <code>&lt;Astronauts&gt;<\/code> is created, we load our astronaut data from <code>public\/data\/astronauts.json<\/code> and feed it to instances of the presentational <code>&lt;AstroCard&gt;<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"a8b510ef-8483-4a34-8566-a9a67505f7f9\" data-enlighter-title=\"public\/data\/astronauts.json\">[\n  \/\/ ...\n\n  {\n    \"id\": 7,\n    \"name\": \"Jessica Watkins\",\n    \"photoUrl\": \"j-watkins.jpg\",\n    \"nationality\": \"USA \ud83c\uddfa\ud83c\uddf8\",\n    \"craft\": \"ISS\"\n  },\n\n  \/\/ ...\n]\n\n<\/pre>\n<div><\/div>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-35670\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/astro-cards.png\" alt=\"Our &lt;AstroCard&gt; instances rendering astronaut data\" width=\"1170\" height=\"348\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/astro-cards.png 1170w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/astro-cards-300x89.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/astro-cards-1024x305.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/astro-cards-768x228.png 768w\" sizes=\"(max-width: 1170px) 100vw, 1170px\" \/><\/div>\n<div><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Our &lt;AstroCard&gt; instances rendering astronaut data<\/span><\/div>\n<div><\/div>\n<div><\/div>\n<p>Note that our UI strings are all hard-coded in English at this point. Let\u2019s take care of this and localize our app.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> We\u2019re omitting much of our demo starter code for brevity. You can get all of it from <a href=\"https:\/\/github.com\/PhraseApp-Blog\/vue3-i18n-2022\/tree\/start-options\">the start-options branch of our GitHub repo<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-install-and-set-up-vue-i18n\"><\/span>How do I install and set up Vue I18n?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>This will shock you: We start with an NPM install on the command line from the root of our Vue project.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">$ npm install vue-i18n@9 \n\n<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> You\u2019ll want v9+ of Vue I18n if you\u2019re working with Vue 3. Vue 2 uses Vue i18n v8.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Check out <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/installation.html\">all the ways to install Vue I18n<\/a> in the official documentation.<\/p>\n<p>Once NPM has done its thing, we need to create a Vue I18n instance, configure it, and register it as a plugin with our Vue instance. Let\u2019s construct the Vue I18n instance in a new module. We\u2019ll create a directory called <code>src\/i18n<\/code> and place an <code>index.js<\/code> file within.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"8b3f170d-82c6-456a-be67-f069c7e3b6d0\" data-enlighter-title=\"src\/i18n\/index.js\">import { createI18n } from 'vue-i18n'\n\nconst i18n = createI18n({\n  \/\/ default locale\n  locale: 'en',\n\n  \/\/ translations\n  messages: {\n    en: {\n      appTitle: 'Mushahed',\n    },\n    ar: {\n      appTitle: '\u0645\u0634\u0627\u0647\u062f',\n    },\n  },\n})\n\nexport default i18n\n\n<\/pre>\n<p>We pass our translation <code>messages<\/code> to the i18n object that we construct with <code>createI18n()<\/code>. The initial locale, the one our app defaults to on first load, is set via the <code>locale<\/code> config option.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> I\u2019m supporting English (<code>en<\/code>) and Arabic (<code>ar<\/code>) in my app. Feel free to support any languages you want here. Use a standard <a href=\"https:\/\/en.wikipedia.org\/wiki\/Codes_for_constructed_languages\">BCP 47<\/a> language tag (like <code>en<\/code>) or a language tag with a region subtag (like <code>en-US<\/code>) to identify your translation locales.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> All config options for <code>createI18n()<\/code> are available in the <a href=\"https:\/\/vue-i18n.intlify.dev\/api\/general.html#createi18n\">official API documentation<\/a>.<\/p>\n<p>Our Vue instance now needs to register our <code>i18n<\/code> object as a plugin with a <code>use()<\/code> call.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"41e4b20f-dbec-4421-b11b-8245a1cc9f3d\" data-enlighter-title=\"src\/main.js\" data-enlighter-highlight=\"4,10\">import { createApp } from 'vue'\nimport App from '.\/App.vue'\nimport router from '.\/router'\nimport i18n from '.\/i18n'\n\nimport '.\/assets\/main.css'\n\nconst app = createApp(App)\n\napp.use(i18n)\napp.use(router)\n\napp.mount('#app')\n\n<\/pre>\n<p>That should round out our setup. Let\u2019s test our i18n by internationalizing the app title in our <code>&lt;Nav&gt;<\/code> component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"f8cab4c7-66b5-4098-a044-1af525ade08d\" data-enlighter-title=\"src\/components\/Nav.vue\" data-enlighter-highlight=\"10\">&lt;script setup&gt;\nimport { RouterLink } from 'vue-router'\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;nav&gt;\n    &lt;img alt=\"Mushahed logo\" src=\"@\/assets\/logo.svg\" \/&gt;\n\n    &lt;!-- App title is hard-coded in English --&gt;\n    &lt;span&gt;Mushahed&lt;\/span&gt;\n\n    &lt;RouterLink to=\"\/\"&gt;Home&lt;\/RouterLink&gt;\n    &lt;RouterLink to=\"\/about\"&gt;About&lt;\/RouterLink&gt;\n  &lt;\/nav&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>We&#8217;ll replace the hard-coded text with the following.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"b3993f31-5b3c-4784-a7ef-c05a4912e72b\" data-enlighter-title=\"src\/components\/Nav.vue\" data-enlighter-highlight=\"9\">&lt;script setup&gt;\nimport { RouterLink } from 'vue-router'\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;nav&gt;\n    &lt;!-- ... --&gt;\n\n    &lt;span&gt;{{ $t('appTitle') }}&lt;\/span&gt;\n\n    &lt;!-- ... --&gt;\n  &lt;\/nav&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>Available to all our components now is Vue I18n\u2019s <code>$t()<\/code> translation function. Calling <code>$t('appTitle')<\/code> when the active locale is English (<code>en<\/code>) will cause <code>$t()<\/code> to return the message we specified at <code>messages.en.appTitle<\/code> above. When the active locale is Arabic (<code>ar<\/code>), <code>messages.ar.appTitle<\/code> is returned.<\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> Check out the myriad ways to use <code>$t()<\/code> in the <a href=\"https:\/\/vue-i18n.intlify.dev\/api\/injection.html#t-key\">official API listing<\/a>.<\/p>\n<p>If we reload our app now we should see no change: That\u2019s because our initial locale is configured to English. Let\u2019s switch it to Arabic.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"0fec982d-5b87-4680-aa27-20007be78617\" data-enlighter-title=\"src\/i18n\/index.js\" data-enlighter-highlight=\"4\">import { createI18n } from 'vue-i18n'\n\nconst i18n = createI18n({\n  locale: 'ar',\n  messages: {\n\ten: {\n      appTitle: 'Mushahed',\n    },\n    ar: {\n      appTitle: '\u0645\u0634\u0627\u0647\u062f',\n    },\n  },\n})\n\nexport default i18n\n\n<\/pre>\n<p>Et voil\u00e0!<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35677\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-app-name.png\" alt=\"Our app name translated to Arabic\" width=\"236\" height=\"139\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Our app name translated to Arabic<\/span><\/p>\n<p>That\u2019s all it takes to start working with Vue I18n in our apps. Of course, we probably want to keep adding translation messages as we grow our app. To keep things tidy, let\u2019s refactor our <code>messages<\/code> object to its own module.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"d477649c-4a69-4832-8439-62925ce475ee\" data-enlighter-title=\"src\/i18n\/messages.js\">export default {\n  en: {\n    appTitle: 'Mushahed',\n  },\n  ar: {\n    appTitle: '\u0645\u0634\u0627\u0647\u062f',\n  },\n}\n\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"3aaa9847-5b45-4f7f-a82a-80bfd89d10ad\" data-enlighter-title=\"src\/i18n\/index.js\" data-enlighter-highlight=\"2,6\">import { createI18n } from 'vue-i18n'\nimport messages from '.\/messages'\n\nconst i18n = createI18n({\n  locale: 'ar',\n  messages,\n})\n\nexport default i18n\n\n<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> When developing with Vue I18n, you might get a warning in your browser console that says, \u201cYou are running the esm-bundler build of vue-i18n\u2026\u201d. This is a <a href=\"https:\/\/github.com\/intlify\/vue-i18n-next\/issues\/391\">known issue and may be fixed by the time you read this<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-translate-messages-in-my-components\"><\/span>How do I translate messages in my components?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We touched on this when we tested our Vue I18n installation above, but it bears repeating. It takes two steps:<\/p>\n<ol>\n<li>In our <code>messages<\/code> object, under each of our locales, we add translations with a shared key.<\/li>\n<li>We use <code>$t(key)<\/code> in our component templates to render the translation corresponding to the active locale.<\/li>\n<\/ol>\n<p>Let\u2019s apply this by localizing the rest of our <code>&lt;Nav&gt;<\/code> component. We&#8217;ll need a few more translations to start.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"1d01209e-9184-4ffc-9bf5-f7051a386d99\" data-enlighter-title=\"src\/i18n\/messages.js\">export default {\n  en: {\n    \/\/ Use same keys as ar\n    appTitle: 'Mushahed',\n    logo: 'Mushahed logo',\n    home: 'Home',\n    about: 'About',\n  },\n  ar: {\n    \/\/ Use same keys as en\n    appTitle: '\u0645\u0634\u0627\u0647\u062f',\n    logo: '\u0631\u0645\u0632 \u0645\u0634\u0627\u0647\u062f',\n    home: '\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629',\n    about: '\u0646\u0628\u0630\u0629 \u0639\u0646\u0627',\n  },\n}\n\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"43ce4119-f446-47a0-9501-75c1aa9f8716\" data-enlighter-title=\"src\/components\/Nav.vue\" data-enlighter-highlight=\"7,10,11\">&lt;script setup&gt;\nimport { RouterLink } from 'vue-router'\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;nav&gt;\n    &lt;img :alt=\"$t('logo')\" src=\"@\/assets\/logo.svg\" \/&gt;\n    &lt;span&gt;{{ $t('appTitle') }}&lt;\/span&gt;\n\n    &lt;RouterLink to=\"\/\"&gt;{{ $t('home') }}&lt;\/RouterLink&gt;\n    &lt;RouterLink to=\"\/about\"&gt;{{ $t('about') }}&lt;\/RouterLink&gt;\n  &lt;\/nav&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>We can use <code>$t()<\/code> with the <code>{{ }}<\/code> syntax to translate the inner text of an element. When translating an attribute, the <code>:attribute<\/code> binding shorthand comes in handy.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35684\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-basic-translation.png\" alt=\"Our app name translated to Arabic\" width=\"546\" height=\"140\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-basic-translation.png 546w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-basic-translation-300x77.png 300w\" sizes=\"(max-width: 546px) 100vw, 546px\" \/><\/p>\n<div>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Our navigation component when Arabic is the active locale<\/span><\/p>\n<\/div>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-work-with-dynamic-values-in-my-translation-messages\"><\/span>How do I work with dynamic values in my translation messages?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>A common use case, interpolating values that change at runtime in our messages is easy with Vue I18n. Our <code>&lt;Coords&gt;<\/code> component, which shows the coordinates of the International Space Station (ISS) at a given time, is a perfect place to demonstrate.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"dc1f8952-7379-4a1d-810b-8f42370a1e52\" data-enlighter-title=\"src\/components\/Coords.vue\">&lt;script&gt;\nexport default {\n  data() {\n    return {\n      coords: {\n        latitude: 49.5908,\n        longitude: 122.8927,\n      },\n      datetime: new Date(1658828129000),\n    }\n  },\n}\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;p&gt;\n    The ISS was over {{ coords.latitude }}\u00b0 N, {{ coords.longitude }}\u00b0 E on {{ datetime }}\n  &lt;\/p&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<div><\/div>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-35691\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/en-interpolation.png\" alt=\"Hard-coded English interpolation\" width=\"1952\" height=\"168\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/en-interpolation.png 1952w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/en-interpolation-300x26.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/en-interpolation-1024x88.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/en-interpolation-768x66.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/en-interpolation-1536x132.png 1536w\" sizes=\"(max-width: 1952px) 100vw, 1952px\" \/><\/p>\n<p>We\u2019ve hard-coded the coordinate and datetime values above for clarity, but in a real app these would likely be fetched from an API and updated on component <code>created<\/code>. Vue I18n accommodates these dynamic values in its messages via a <code>{placeholder}<\/code> syntax.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"1cf76188-52c0-431b-aec6-195f6f0e91be\" data-enlighter-title=\"src\/i18n\/messages.js\" data-enlighter-highlight=\"4,8,9\" data-enlighter-linenumbers=\"false\">export default {\n  en: {\n    \/\/ ...\n    issPosition: 'The ISS was over {latitude}\u00b0 N, {longitude}\u00b0 E on {datetime}',\n  },\n  ar: {\n    \/\/ ...\n    issPosition:\n      '\u0643\u0627\u0646\u062a \u0645\u062d\u0637\u0629 \u0627\u0644\u0641\u0636\u0627\u0621 \u0627\u0644\u062f\u0648\u0644\u064a\u0629 \u0641\u0648\u0642 {latitude} \u062f\u0631\u062c\u0629 \u0634\u0645\u0627\u0644\u0627 \u0648 {longitude} \u062f\u0631\u062c\u0629 \u0634\u0631\u0642\u0627 \u064a\u0648\u0645 {datetime}',\n  },\n}\n\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"166ceb1f-120d-4745-a8b6-aab60e321576\" data-enlighter-title=\"src\/components\/Coords.vue\" data-enlighter-highlight=\"17,18,19,20,21\">&lt;script&gt;\nexport default {\n  data() {\n    return {\n      coords: {\n        latitude: 49.5908,\n        longitude: 122.8927,\n      },\n      datetime: new Date(1658828129000),\n    }\n  },\n}\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;p&gt;\n    {{ $t('issPosition', {\n        latitude: coords.latitude,\n        longitude: coords.longitude,\n        datetime,\n      }) }}\n  &lt;\/p&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>Passing a second argument to <code>$t()<\/code> \u2014 a map of key\/value pairs where the keys match the ones in our translation messages \u2014 renders these messages with the injected values.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35698\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-interpolation.png\" alt=\"An Arabic message with interpolated dynamic values\" width=\"1838\" height=\"132\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-interpolation.png 1838w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-interpolation-300x22.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-interpolation-1024x74.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-interpolation-768x55.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-interpolation-1536x110.png 1536w\" sizes=\"(max-width: 1838px) 100vw, 1838px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">An Arabic message with interpolated dynamic values<\/span><\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> The numbers and date above are <em>not<\/em> in Arabic. We\u2019ll take care of this a bit later.<\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/essentials\/syntax.html#interpolations\">Learn all the ways you can interpolate in messages<\/a> from the official Vue I18n documentation.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-translate-strings-in-my-component-javascript\"><\/span>How do I translate strings in my component JavaScript?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The <code>$t()<\/code> function is available to our component JavaScript via <code>this.$t()<\/code>. Let\u2019s use this to refactor our <code>&lt;Coords&gt;<\/code> component and move that chunky <code>$t()<\/code> call to our component <code>&lt;script&gt;<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"30cbef27-76f1-4cfb-a6fc-5a2df8a863e7\" data-enlighter-title=\"src\/components\/Coords.vue\" data-enlighter-highlight=\"13,14,15,16,17,18,19,20,21,26\">&lt;script&gt;\nexport default {\n  data() {\n    return {\n      coords: {\n        latitude: 49.5908,\n        longitude: 122.8927,\n      },\n      datetime: new Date(1658828129000),\n    }\n  },\n\n  computed: {\n    issPosition() {\n      return this.$t('issPosition', {\n        latitude: this.coords.latitude,\n        longitude: this.coords.longitude,\n        datetime: this.datetime,\n      })\n    },\n  },\n}\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;p&gt;{{ issPosition }}&lt;\/p&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-work-with-html-within-my-translation-messages\"><\/span>How do I work with HTML within my translation messages?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>On occasion, we will need to place HTML inside our translation messages. Our <code>&lt;Footer&gt;<\/code> is a good example.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"ca92d139-d0be-4cef-ae6b-be2d308182ae\" data-enlighter-title=\"src\/components\/Footer.vue\">&lt;template&gt;\n  &lt;p&gt;\n    Created with\n    &lt;a href=\"https:\/\/vuejs.org\/\"&gt;Vue&lt;\/a&gt; for a\n    &lt;a href=\"https:\/\/phrase.com\/blog\"&gt;Phrase blog&lt;\/a&gt;\n    tutorial.\n  &lt;\/p&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>Localizing this string is tricky because the locations of the embedded links can differ depending on the translation language. We could just place the <code>&lt;a&gt;<\/code> tags directly in our translation messages and exploit Vue\u2019s unsafe <code>v-html<\/code> directive to output the translations. This would expose us to XSS attacks if we\u2019re not careful, however.<\/p>\n<p>Vue I18n offers a better solution: Its <code>&lt;i18n-t&gt;<\/code> component allows us to render its children, including HTML elements, inside our messages via placeholders.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"21ed5283-7971-4493-9a53-22c2f012c89f\" data-enlighter-title=\"src\/i18n\/messages.js\" data-enlighter-linenumbers=\"false\">export default {\n  en: {\n    \/\/ ...\n    footer: 'Created with {0} for a {1}.',\n    vue: 'Vue',\n    phraseBlogTutorial: 'Phrase blog tutorial',\n  },\n  ar: {\n    \/\/ ...\n    footer: '.\u062a\u0645 \u0625\u0646\u0634\u0627\u0626\u0647 \u0628\u0648\u0627\u0633\u0637\u0629 {0} \u0644\u064a\u0635\u0627\u062d\u0628 {1}',\n    vue: '\u06a4\u064a\u0648',\n    phraseBlogTutorial: '\u062f\u0631\u0633 \u0639\u0644\u0649 \u0645\u062f\u0648\u0646\u0629 \u0641\u0631\u064a\u0632',\n  },\n}\n\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"f050d163-5460-491b-b10c-9612ba3e35af\" data-enlighter-title=\"src\/components\/Footer.vue\">&lt;script&gt;\nexport default {\n  data() {\n    return {\n      vueUrl: 'https:\/\/vuejs.org\/',\n      phraseBlogUrl: 'https:\/\/phrase.com\/blog',\n    }\n  },\n}\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;i18n-t keypath=\"footer\" tag=\"p\" scope=\"global\"&gt;\n    &lt;a :href=\"vueUrl\"&gt;{{ $t('vue') }}&lt;\/a&gt;\n    &lt;a :href=\"phraseBlogUrl\"&gt;{{ $t('phraseBlogTutorial') }}&lt;\/a&gt;\n  &lt;\/i18n-t&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>We pass <code>&lt;i18n-t&gt;<\/code> a <code>keypath<\/code> prop with the key of our parent translation message, <code>footer<\/code> in this case. For rendering, we let <code>&lt;i18n-t&gt;<\/code> know that we want it to output a surrounding <code>&lt;p&gt;<\/code> via the <code>tag<\/code> prop.<\/p>\n<p>Within our parent message, we specify placeholders using <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/essentials\/syntax.html#list-interpolation\">list interpolation<\/a>, meaning we index our placeholders starting with <code>{0}<\/code> and moving on to <code>{1}<\/code>, etc. Order matters here: The first <code>&lt;a&gt;<\/code> inside <code>&lt;i18n-t&gt;<\/code> will replace the <code>{0}<\/code> placeholder, the second will replace <code>{1}<\/code>, and so on. This allows us to control the order our HTML elements appear in each language\u2019s translation message.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> If we don\u2019t explicitly set the <code>scope=\"global\"<\/code> prop on the <code>&lt;i18n-t&gt;<\/code> component we will get a console warning reading, \u201c[intlify] Not found parent scope. use the global scope.\u201d<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/advanced\/component.html\">Component Interpolation\u00a0<\/a>section of the official guide covers the <code>&lt;i18n-t&gt;<\/code> component in further detail.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-work-with-plurals-in-my-translations\"><\/span>How do I work with plurals in my translations?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The two English plural forms are simple: \u201c<em>a satellite<\/em> <em>is<\/em> orbiting above\u201d; \u201c<em>three satellites<\/em> <em>are<\/em> orbiting above\u201d. Other languages are more complex. Some have four plural forms. Welsh and Arabic have six. Vue I18n handles simple plurals, like those of English, out of the box. We can extend the plugin to handle complex plurals. We\u2019ll cover both simple and complex plurals here.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The <a href=\"https:\/\/unicode-org.github.io\/cldr-staging\/charts\/latest\/supplemental\/language_plural_rules.html\">CLDR Language Plural Rules<\/a> reference is canon for languages\u2019 plural forms.<\/p>\n<p>Let\u2019s revisit the header of our <code>&lt;Astronauts&gt;<\/code> component for a moment.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35706\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/en-astros-header.png\" alt=\"An astronaut counter\" width=\"532\" height=\"128\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/en-astros-header.png 532w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/en-astros-header-300x72.png 300w\" sizes=\"(max-width: 532px) 100vw, 532px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">An astronaut counter<\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"0f82150c-6c88-4149-88a4-d5a1610ac55a\" data-enlighter-title=\"src\/components\/Astronauts.vue\" data-enlighter-highlight=\"7\">&lt;script&gt;\n\/\/ We populate the astros array here...\n&lt;\/script&gt;\n&lt;template&gt;\n  &lt;div&gt;\n    &lt;div&gt;\n      &lt;h2&gt;\ud83e\uddd1\u200d\ud83d\ude80 {{ astros.length }} people in space&lt;\/h2&gt;\n\n      &lt;p&gt;Updated Jul 26, 2022&lt;\/p&gt;\n    &lt;\/div&gt;\n\n    &lt;!-- ... --&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>Our astronaut counter is hard-coded and ripe for localization. Let\u2019s add an English message for it.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"1635ae22-202e-4e23-bdcb-682942132b8e\" data-enlighter-title=\"src\/i18n\/messages.js\">export default {\n  en: {\n    \/\/ ...\n    peopleInSpace:\n      '{n} person in space | {n} people in space',\n  },\n  ar: {\n    \/\/ ...\n  }\n}\n\n<\/pre>\n<p>Vue I18n expects plural forms to be separated by a pipe (<code>|<\/code>) character. We\u2019ve specified the two\u00a0plural forms for English above. The <code>{n}<\/code> placeholder will be replaced by an integer counter when we retrieve the plural message.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"9ceae467-bec3-42d2-9409-66737ccd09ed\" data-enlighter-title=\"src\/components\/Astronauts.vue\" data-enlighter-highlight=\"7\">&lt;script&gt;\n\/\/ We populate the astros array here...\n&lt;\/script&gt;\n&lt;template&gt;\n  &lt;div&gt;\n    &lt;div&gt;\n      &lt;h2&gt;\ud83e\uddd1\u200d\ud83d\ude80 {{ $tc('peopleInSpace', astros.length) }}&lt;\/h2&gt;\n\n      &lt;p&gt;Updated Jul 26, 2022&lt;\/p&gt;\n    &lt;\/div&gt;\n\n    &lt;!-- ... --&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p><code>$tc()<\/code>, another translation function injected by Vue i18n into all of our components, chooses the correct plural form based on its second parameter, the integer counter.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35711\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/en-plurals.png\" alt=\"Renders of English plural forms. Note that {n} is replaced with our counter.\" width=\"494\" height=\"344\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/en-plurals.png 494w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/en-plurals-300x209.png 300w\" sizes=\"(max-width: 494px) 100vw, 494px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Renders of English plural forms. Note that {n} is replaced with our counter.<\/span><\/p>\n<p>Two forms work fine for English, but our Arabic translation will need six plural variants.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"13d09d7a-6642-4f18-8bb1-7c8d5c2896be\" data-enlighter-title=\"src\/i18n\/messages.js\">export default {\n  en: {\n    \/\/ ...\n  },\n  ar: {\n    \/\/ ...\n    peopleInSpace:\n      '\u0644\u0627 \u064a\u0648\u062c\u062f \u0623\u062d\u062f \u0641\u064a \u0627\u0644\u0641\u0636\u0627\u0621 | \u064a\u0648\u062c\u062f \u0634\u062e\u0635 {n} \u0641\u064a \u0627\u0644\u0641\u0636\u0627\u0621 | \u064a\u0648\u062c\u062f \u0634\u062e\u0635\u0627\u0646 \u0641\u064a \u0627\u0644\u0641\u0636\u0627\u0621 | \u062a\u0648\u062c\u062f {n} \u0623\u0634\u062e\u0627\u0635 \u0641\u064a \u0627\u0644\u0641\u0636\u0627\u0621 | \u064a\u0648\u062c\u062f {n} \u0634\u062e\u0635 \u0641\u064a \u0627\u0644\u0641\u0636\u0627\u0621 | \u064a\u0648\u062c\u062f {n} \u0634\u062e\u0635 \u0641\u064a \u0627\u0644\u0641\u0636\u0627\u0621',\n  },\n}\n\n<\/pre>\n<p>On its own Vue I18n only works with English-like plurals, so we need to add a custom extension function that handles Arabic\u2019s six forms.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"024323cc-bd26-473b-a267-d6dbdb1f8ae1\" data-enlighter-title=\"src\/i18n\/plurals.js\">export function arabicPluralRules(choice) {\n  const name = new Intl.PluralRules('ar').select(choice)\n\n  return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name]\n}\n\n<\/pre>\n<p>JavaScript\u2019s standard <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/PluralRules\/PluralRules\">Intl.PluralRules<\/a> object handles complex plurals wonderfully. All we have to do is give it a locale when constructing it, then call its <code>select()<\/code> method with an integer counter. The method returns the name of the correct form for the given language. For example, <code>new Intl.PluralRules('ar').select(5)<\/code> returns the correct <code>few<\/code> form.<\/p>\n<p>Vue I18n needs an integer index to select the correct form in our translation messages, so our custom plural selector needs to map the CLDR plural form name (<code>few<\/code>) to a zero-based index (<code>3<\/code>). The index selects from our pipe-separated message. So <code>3<\/code> would select our fourth variant from the <code>peopleInSpace<\/code> message above.<\/p>\n<p>All we have to do now is wire up our Arabic plural rule selector when constructing the Vue I18n instance.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"20b93d17-a714-4f37-9fbb-cd5f47fbe91b\" data-enlighter-title=\"src\/i18n\/index.js\" data-enlighter-highlight=\"3,11,12,13\">import { createI18n } from 'vue-i18n'\nimport messages from '.\/messages'\nimport { arabicPluralRules } from '.\/plurals'\n\nconst i18n = createI18n({\n  locale: 'ar',\n  messages,\n  \/\/ Vue I18n allows us to extend its plural\n  \/\/ formatting by providing one form selector\n  \/\/ function per locale\n  pluralizationRules: {\n    ar: arabicPluralRules,\n  },\n})\n\nexport default i18n\n\n<\/pre>\n<p>With our selector wired up, our Arabic plurals should work like a charm.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35716\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-plurals.png\" alt=\"Renders of Arabic plural forms. Note that {n} is replaced with our counter.\" width=\"1000\" height=\"704\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-plurals.png 1000w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-plurals-300x211.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-plurals-768x541.png 768w\" sizes=\"(max-width: 1000px) 100vw, 1000px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Renders of Arabic plural forms. Note that {n} is replaced with our counter.<\/span><\/p>\n<p>\u270b <em>Heads up \u00bb<\/em> You may have noticed that the interpolated counter is being displayed in Western Arabic numerals (1, 2, etc.). However, Arabic uses Eastern Arabic numerals (\u0661\u060c \u0662\u060c \u0663\u060c etc.). While not a showstopper, I have <a href=\"https:\/\/github.com\/intlify\/vue-i18n-next\/issues\/1090\">logged this issue on the Vue i18n GitHub<\/a> if you care to follow it.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Learn more from the <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/essentials\/pluralization.html#custom-pluralization\">official guide on pluralization<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-format-localized-numbers\"><\/span>How do I format localized numbers?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Different locales use different numeral systems, thousands separators, and symbols when representing numbers. JavaScript\u2019s built-in <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/NumberFormat\">Intl.NumberFormat<\/a> object handles all this for us and is used under the hood by Vue I18n. We just need to give Vue I18n preconfigured number formats, which the plugin in turn passes to <code>Intl.NumberFormat<\/code>. The formats we registered are then available to use in our components.<\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> Our <a href=\"https:\/\/phrase.com\/blog\/posts\/number-localization\/\">Concise Guide to Number Localization<\/a> covers numeral systems, separators, and more.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"53dff551-57ad-43af-998b-95d5e8a2142f\" data-enlighter-title=\"src\/i18n\/numbers.js\">\/\/ We specify the formats our app will use\nexport const numberFormats = {\n  'en-US': {\n    \/\/ A named format\n    coords: {\n      \/\/ These options are passed to Intl.NumberFormat\n      style: 'decimal',\n      minimumSignificantDigits: 6,\n      maximumSignificantDigits: 6,\n    },\n  },\n  'ar-EG': {\n    coords: {\n      style: 'decimal',\n      minimumSignificantDigits: 6,\n      maximumSignificantDigits: 6,\n    },\n  },\n}\n\n<\/pre>\n<p>We need to register our number formats with the Vue I18n object during construction.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-highlight=\"3,9\">import { createI18n } from 'vue-i18n'\nimport messages from '.\/messages'\nimport { numberFormats } from '.\/numbers'\nimport { arabicPluralRules } from '.\/plurals'\n\nconst i18n = createI18n({\n  locale: 'en-US',\n  messages,\n  numberFormats,\n  pluralizationRules: {\n    'ar-EG': arabicPluralRules,\n  },\n})\n\nexport default i18n\n\n<\/pre>\n<p>Now we can use the injected <code>$n()<\/code> function to format localized numbers in our component templates.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">&lt;!-- We specify the named format as the second param --&gt;\n&lt;p&gt;{{ $n(49.5908, 'coords') }}&lt;\/p&gt;\n\n&lt;!-- =&gt; \"49.5908\" when locale is en-US --&gt;\n&lt;!-- =&gt; \"\u0664\u0669\u060c\u0665\u0669\u0660\u0668\" when locale is ar-EG --&gt;\n\n<\/pre>\n<p>\u270b <em>Heads up \u00bb<\/em> You may have noticed that we swapped <code>en<\/code> with <code>en-US<\/code> and <code>ar<\/code> with <code>ar-EG<\/code> in our configuration above. This is because <em>number formatting is region-specific, not language-specific<\/em>. Adding countries or regions to our locale tags means we can control the output of localized number formatting. Otherwise, we risk the browser using a default region. Of course, we have to update our <code>messages<\/code> so that they\u2019re keyed with <code>en-US<\/code> and <code>ar-EG<\/code> as well.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"ff97c110-26bc-45ae-bc67-623aa2cf2e9e\" data-enlighter-title=\"src\/i18n\/messages.js\" data-enlighter-highlight=\"2,6\">export default {\n  'en-US': {\n    appTitle: 'Mushahed',\n    \/\/ ...\n  },\n  'ar-EG': {\n    appTitle: '\u0645\u0634\u0627\u0647\u062f',\n    \/\/ ...\n  },\n}\n\n<\/pre>\n<p>Let\u2019s update our <code>&lt;Coords&gt;<\/code> component to format the ISS coordinates in the active locale\u2019s number format.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"acf8121b-a239-477c-89d7-b36b27cb9c89\" data-enlighter-title=\"src\/components\/Coords.vue\" data-enlighter-highlight=\"18,19\">&lt;script&gt;\nexport default {\n  data() {\n    return {\n      coords: null,\n      datetime: '',\n    }\n  },\n\n  created() {\n    \/\/ We fetch the coordinate data and set\n    \/\/ this.coords and this.datetime here...\n  },\n\n  computed: {\n    issPosition() {\n      return this.$t('issPosition', {\n        latitude: this.$n(this.coords.latitude, 'coords'),\n        longitude: this.$n(this.coords.longitude, 'coords'),\n        datetime: this.datetime,\n      })\n    },\n  },\n}\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;p&gt;{{ issPosition }}&lt;\/p&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<div>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35721\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/number-formatting.png\" alt=\"Renders of Arabic plural forms. Note that {n} is replaced with our counter.\" width=\"1782\" height=\"303\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/number-formatting.png 1782w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/number-formatting-300x51.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/number-formatting-1024x174.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/number-formatting-768x131.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/number-formatting-1536x261.png 1536w\" sizes=\"(max-width: 1782px) 100vw, 1782px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Renders of Arabic plural forms. Note that {n} is replaced with our counter.<\/span><\/p>\n<\/div>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Get more details from the <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/essentials\/number.html\">Number Formatting\u00a0<\/a>section of the Vue I18n documentation.<\/p>\n<p>The date above looks very English in the otherwise Arabic message, doesn\u2019t it? No worries. Guess what\u2019s next?<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-format-localized-dates-and-times\"><\/span>How do I format localized dates and times?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Much like number formatting, date formatting is <em>region-specific<\/em>. The US and Canada both use English, but the 9th of September, 2022 can be 9\/4\/2022 in the US and 2022-09-04 in Canada. To work with localized dates correctly, we follow a recipe much like we did with dates. We provide Vue I18n with named datetime formats, which the plugin passes as options to the standard <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/DateTimeFormat\">Intl.DateTimeFormat<\/a>. We then use these registered formats in our components.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> Date localization is very similar to number localization, so this section builds on the last.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"0034e3ba-e911-455e-aa9b-51aa1b594152\" data-enlighter-title=\"src\/i18n\/datetimes.js\">export const datetimeFormats = {\n  'en-US': {\n    full: {\n      \/\/ These options are passed to Intl.DateTimeFormat\n      dateStyle: 'full',\n      timeStyle: 'full',\n    },\n    short: {\n      year: 'numeric',\n      month: 'short',\n      day: 'numeric',\n    },\n  },\n  'ar-EG': {\n    full: {\n      dateStyle: 'full',\n      timeStyle: 'full',\n    },\n    short: {\n      year: 'numeric',\n      month: 'long',\n      day: 'numeric',\n    },\n  },\n}\n\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"b47f9bc6-fff8-4e93-a1be-22141a8229c9\" data-enlighter-title=\"src\/i18n\/index.js\" data-enlighter-highlight=\"4,11\">import { createI18n } from 'vue-i18n'\nimport messages from '.\/messages'\nimport { numberFormats } from '.\/numbers'\nimport { datetimeFormats } from '.\/datetimes'\nimport { arabicPluralRules } from '.\/plurals'\n\nconst i18n = createI18n({\n  locale: 'en-US',\n  messages,\n  numberFormats,\n  datetimeFormats,\n  pluralizationRules: {\n    'ar-EG': arabicPluralRules,\n  },\n})\n\nexport default i18n\n\n<\/pre>\n<p>With formats specified and registered, we can use the injected <code>$d()<\/code> function to display localized dates in our components. Let\u2019s round out our <code>&lt;Coords&gt;<\/code> component with proper date formatting.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"8015f894-4028-469e-b43e-a9a75ff6566c\" data-enlighter-title=\"src\/components\/Coords.vue\" data-enlighter-highlight=\"10\">&lt;script&gt;\nexport default {\n  \/\/ ...\n\n  computed: {\n    issPosition() {\n      return this.$t('issPosition', {\n        latitude: this.$n(this.coords.latitude, 'coords'),\n        longitude: this.$n(this.coords.longitude, 'coords'),\n        datetime: this.$d(this.datetime, 'full'),\n      })\n    },\n  },\n}\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;p&gt;{{ issPosition }}&lt;\/p&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<div>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35726\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/full-date-format.png\" alt=\"American English and Egyptian Arabic full date formats\" width=\"1792\" height=\"312\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/full-date-format.png 1792w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/full-date-format-300x52.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/full-date-format-1024x178.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/full-date-format-768x134.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/full-date-format-1536x267.png 1536w\" sizes=\"(max-width: 1792px) 100vw, 1792px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">American English and Egyptian Arabic full date formats<\/span><\/p>\n<\/div>\n<p>While we\u2019re at it, let\u2019s format our <code>&lt;Astronauts&gt;<\/code> header to show localized short dates in its \u201cUpdated\u201d message.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"5ed38931-0864-4427-83d0-135ba26967c6\" data-enlighter-title=\"src\/i18n\/messages.js\" data-enlighter-highlight=\"4,8\">export default {\n  'en-US': {\n    \/\/ ...\n    updatedAt: 'Updated {date}',\n  },\n  'ar-EG': {\n    \/\/ ...\n    updatedAt: '\u0623\u062e\u0631 \u062a\u062d\u062f\u064a\u062b {date}',\n  },\n}\n\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"029a09ab-5a80-45f4-b8ef-56b4201254d5\" data-enlighter-title=\"src\/components\/Astronauts.vue\" data-enlighter-highlight=\"8\">&lt;script&gt;\n\/\/ ...\n&lt;\/script&gt;\n&lt;template&gt;\n  &lt;!-- ... --&gt;\n  &lt;h2&gt;\ud83e\uddd1\u200d\ud83d\ude80 {{ $tc('peopleInSpace', astros.length) }}&lt;\/h2&gt;\n  &lt;p&gt;\n    {{ $t('updatedAt', { date: $d(updated, 'short') }) }}\n  &lt;\/p&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<div>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35731\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/short-date-format.png\" alt=\"American and Egyptian short dates rendered\" width=\"1866\" height=\"938\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/short-date-format.png 1866w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/short-date-format-300x151.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/short-date-format-1024x515.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/short-date-format-768x386.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/short-date-format-1536x772.png 1536w\" sizes=\"(max-width: 1866px) 100vw, 1866px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">American and Egyptian short dates rendered<\/span><\/p>\n<\/div>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The official Vue I18n guide covers more <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/essentials\/datetime.html\">date formatting options<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-retrieve-the-active-locale\"><\/span>How do I retrieve the active locale?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Sometimes we need to make decisions based on the runtime locale of the app. With Vue I18n we can get the active locale simply via <code>i18n.locale<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-highlight=\"5,13\">&lt;script&gt;\nexport default {\n  methods: {\n    activeLocale() {\n      return this.$i18n.locale\n    }\n  }\n}\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;!-- Assuming active locale is en-US --&gt;\n  &lt;p&gt;{{ $i18n.locale}}&lt;\/p&gt; &lt;!-- =&gt; &lt;p&gt;en-US&lt;\/p&gt; --&gt;\n\n  &lt;p&gt;{{ activeLocale() }}&lt;\/p&gt; &lt;!-- =&gt; &lt;p&gt;en-US&lt;\/p&gt; --&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>We can also assign a new value to <code>$i18n.locale<\/code> to set a new active locale. We\u2019ll see this in action momentarily.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"refactoring-the-i18n-library\"><\/span>Refactoring the i18n library<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In the next few sections we\u2019ll tackle some advanced topics like localized routes and asynchronous translation file loading. These will be easier to implement if we refactor our little i18n library so that we can control how locales are set and loaded.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"87591513-e0dd-402c-a0f8-0e616db8b311\" data-enlighter-title=\"src\/i18n\/index.js\" data-enlighter-linenumbers=\"false\">import { createI18n } from 'vue-i18n'\nimport { messages } from '.\/messages'\nimport { numberFormats } from '.\/numbers'\nimport { arabicPluralRules } from '.\/plurals'\nimport { datetimeFormats } from '.\/datetimes'\n\n\/\/ Set and expose the default locale\nexport const defaultLocale = 'en-US'\n\n\/\/ Private instance of VueI18n object\nlet _i18n\n\n\/\/ Initializer\nfunction setup(options = { locale: defaultLocale }) {\n  _i18n = createI18n({\n    locale: options.locale,\n    fallbackLocale: defaultLocale,\n    messages,\n    numberFormats,\n    datetimeFormats,\n    pluralizationRules: {\n      'ar-EG': arabicPluralRules,\n    },\n  })\n\n  setLocale(options.locale)\n\n  return _i18n\n}\n\n\/\/ Sets the active locale. \nfunction setLocale(newLocale) {\n  _i18n.global.locale = newLocale\n}\n\n\/\/ Public interface\nexport default {\n  \/\/ Expose the VueI18n instance via a getter\n  get vueI18n() {\n    return _i18n\n  },\n  setup,\n  setLocale,\n}\n\n<\/pre>\n<p>\ud83d\uddd2\ufe0f <em>Note \u00bb<\/em> <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/essentials\/scope.html#scope-and-locale-changing\">Vue I18n supports scoping<\/a> which we can use to change the locale for a subset of our app\u2019s component hierarchy. We use <code>i18n.global<\/code> to access Vue I18n\u2019s global, app-wide scope. This is Vue I18n&#8217;s default scope, and we\u2019ll only work with global scope in this article.<\/p>\n<p>Let\u2019s see how the rest of our app changes based on this refactor.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"aafdef48-68be-49c2-bb02-1e217bf669dc\" data-enlighter-title=\"src\/main.js\" data-enlighter-highlight=\"10,12\">import { createApp } from 'vue'\nimport App from '.\/App.vue'\nimport router from '.\/router'\nimport i18n from '.\/i18n'\nimport '.\/assets\/main.css'\n\nconst app = createApp(App)\n\n\/\/ Explicitly initialize the i18n library\ni18n.setup()\n\/\/ Pass the VueI18n instance as a plugin to use()\napp.use(i18n.vueI18n)\napp.use(router)\n\napp.mount('#app') \n\n<\/pre>\n<p>Nothing else in our app needs to change, yet our refactor will allow us to build more complex features in the following sections more easily.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-my-routes\"><\/span>How do I localize my routes?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>It\u2019s often a good idea to make sure that our URLs reflect the associated content. Localized URLs can mean that <code>\/en-US\/foo<\/code> and <code>\/ar-EG\/foo<\/code> point to the English and Arabic versions of the <code>foo<\/code> page, respectively. Let\u2019s get this working in our demo app.<\/p>\n<p>First, let\u2019s take a look at how we\u2019ve configured the routes in our demo.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"dd5a3338-1c85-439c-ae5f-f4d945a9c307\" data-enlighter-title=\"src\/router\/index.js\">import { createRouter, createWebHistory } from 'vue-router'\nimport HomeView from '..\/views\/HomeView.vue'\n\nconst router = createRouter({\n  history: createWebHistory(import.meta.env.BASE_URL),\n  routes: [\n    {\n      path: '\/',\n      name: 'home',\n      component: HomeView\n    },\n    {\n      path: '\/about',\n      name: 'about',\n      \/\/ Lazy-loaded via code-splitting\n      component: () =&gt; import('..\/views\/AboutView.vue')\n    }\n  ]\n})\n\nexport default router\n\n<\/pre>\n<p>Our relatively simple setup has the <code>\/<\/code> route loading our <code>&lt;HomeView&gt;<\/code> and <code>\/about<\/code> loading our <code>&lt;AboutView&gt;<\/code>. The components are loaded inside a <code>&lt;router-view&gt;<\/code> in the root <code>&lt;App&gt;<\/code> component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"0484ba96-b943-4761-b408-4602cd1f33c7\" data-enlighter-title=\"src\/App.vue\" data-enlighter-highlight=\"13\">&lt;script setup&gt;\nimport { RouterView } from 'vue-router'\nimport Nav from '.\/components\/Nav.vue'\nimport Footer from '.\/components\/Footer.vue'\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;div&gt;\n    &lt;header&gt;\n      &lt;Nav \/&gt;\n    &lt;\/header&gt;\n\n    &lt;RouterView \/&gt;\n\n    &lt;Footer \/&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>Let\u2019s localize these routes so that <code>\/en-US\/about<\/code> shows the English About page and <code>\/ar-EG\/about<\/code> shows the Arabic About page.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"c13f5146-3908-423f-9c06-301a1c602eaf\" data-enlighter-title=\"src\/router\/index.js\" data-enlighter-highlight=\"2\">import { createRouter, createWebHistory } from 'vue-router'\nimport { defaultLocale } from '..\/i18n'\nimport HomeView from '..\/views\/HomeView.vue'\n\nconst router = createRouter({\n  history: createWebHistory(import.meta.env.BASE_URL),\n  routes: [\n    \/\/ The root path always redirects to a\n    \/\/ localized route\n    {\n      path: '\/',\n      redirect: `\/${defaultLocale}`,\n    },\n    \/\/ All paths under the root are localized\n    {\n      path: '\/:locale',\n      children: [\n        {\n          \/\/ The empty path specifies the default\n          \/\/ child route component\n          path: '',\n          component: HomeView,\n        },\n        {\n          \/\/ Using the relative 'about' not the absolute\n          \/\/ '\/about' allows us to include the :locale\n          \/\/ param from the parent.\n          path: 'about',\n          component: () =&gt; import('..\/views\/AboutView.vue'),\n        },\n      ],\n    },\n  ],\n})\n\nexport default router\n\n<\/pre>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The <a href=\"https:\/\/router.vuejs.org\/guide\/\">official Vue Router guide<\/a> is a great start to learn the basics of Vue routing.<\/p>\n<p>With these changes, if we now visit <code>\/<\/code>, we will be redirected to <code>\/en-US<\/code> (assuming <code>en-US<\/code> is our configured default locale). <code>\/en-US<\/code> is our localized root route. It shows the <code>&lt;HomeView&gt;<\/code> in the <code>&lt;App&gt;<\/code>\u2019s <code>&lt;router-view&gt;<\/code> by default. <code>\/en-US\/about<\/code> shows the <code>&lt;AboutView&gt;<\/code>.<\/p>\n<p>However, if we visit <code>\/ar-EG<\/code> , or any other Arabic route, we\u2019re greeted with English translations. That\u2019s because we\u2019re not switching the active locale when the <code>:locale<\/code> <a href=\"https:\/\/router.vuejs.org\/guide\/essentials\/dynamic-matching.html\">route parameter<\/a> changes. Let\u2019s remedy this using a <code>beforeEach<\/code> <a href=\"https:\/\/router.vuejs.org\/guide\/advanced\/navigation-guards.html\">router navigation guard<\/a>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-highlight=\"2,9,10,11,14,15,16,18,19\">import { createRouter, createWebHistory } from 'vue-router'\nimport i18n, { defaultLocale } from '..\/i18n'\n\/\/ ...\n\nconst router = createRouter({\n  \/\/ ...\n})\n\nrouter.beforeEach((to, from) =&gt; {\n  const newLocale = to.params.locale\n  const prevLocale = from.params.locale\n\n  \/\/ If the locale hasn't changed, do nothing\n  if (newLocale === prevLocale) {\n    return\n  }\n\n  i18n.setLocale(newLocale)\n})\n\nexport default router\n\n<\/pre>\n<p>The Vue router\u2019s handy global <a href=\"https:\/\/router.vuejs.org\/api\/interfaces\/router.html#beforeeach\">beforeEach()<\/a> guard runs before any navigation, ensuring that when the locale param in the URL changes we\u2019ll know about it. We pass an anonymous callback to the guard, and use our new <code>setLocale()<\/code> function to update Vue I18n\u2019s active locale when the locale param changes. This means that when we hit <code>\/ar-EG\/about<\/code>, we\u2019ll see the Arabic version of the About page.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35736\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-localized-route.png\" alt=\"Our Arabic routes now show Arabic translations\" width=\"1558\" height=\"582\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-localized-route.png 1558w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-localized-route-300x112.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-localized-route-1024x383.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-localized-route-768x287.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-localized-route-1536x574.png 1536w\" sizes=\"(max-width: 1558px) 100vw, 1558px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Our Arabic routes now show Arabic translations<\/span><\/p>\n<h3><span class=\"ez-toc-section\" id=\"how-do-i-build-a-reusable-localized-link-component\"><\/span>How do I build a reusable localized link component?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>One problem with our current localized routing solution is that we would need to inject the <code>:locale<\/code> route parameter manually every time we create a router link.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"844170d0-c44e-4374-8cd7-e2d54c4ab196\" data-enlighter-title=\"src\/components\/Nav.vue\" data-enlighter-highlight=\"8,12\">&lt;script setup&gt;\nimport { RouterLink } from 'vue-router'\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;nav&gt;\n    &lt;!-- ... --&gt;\n    &lt;router-link :to=\"`\/${$i18n.locale}`\"&gt;\n      {{ $t('home') }}\n    &lt;\/router-link&gt;\n\n    &lt;router-link :to=\"`\/${$i18n.locale}\/about`\"&gt;\n      {{ $t('about') }}\n    &lt;\/router-link&gt;\n    &lt;\/nav&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>This doesn\u2019t scale very well and is error-prone. Let\u2019s DRY (Don\u2019t Repeat Yourself) this up by wrapping Vue\u2019s <code>&lt;router-link&gt;<\/code> in a custom component that handles route localization automatically.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"html\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"03b5aa1d-9beb-47fb-9fb5-9bfcf22672e9\" data-enlighter-title=\"src\/components\/l10n\/LocalizedLink.vue\">&lt;script&gt;\nimport { RouterLink } from 'vue-router'\n\nexport default {\n  \/\/ Expose the to prop to accept relative,\n  \/\/ non-localized URIs\n  props: ['to'],\n\n  components: { RouterLink },\n\n  computed: {\n    localizedUrl() {\n      \/\/ The root \/ route is special since it's\n      \/\/ absolute\n      return this.to === '\/'\n        ? `\/${this.$i18n.locale}`\n        : `\/${this.$i18n.locale}\/${this.to}`\n    },\n  },\n}\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;!-- Internally, we're just using Vue's \n       good old router link --&gt;\n  &lt;router-link :to=\"localizedUrl\"&gt;\n    &lt;slot&gt;&lt;\/slot&gt;\n  &lt;\/router-link&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>Our new <code>&lt;LocalizedLink&gt;<\/code> is almost a drop-in replacement for <code>&lt;router-link&gt;<\/code>s. We just need to be careful to use <em>relative<\/em> URLs for anything other than the root route.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-highlight=\"10,17\">&lt;script setup&gt;\nimport { RouterLink } from 'vue-router'\nimport LocalizedLink from '.\/l10n\/LocalizedLink.vue'\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;nav&gt;\n    &lt;!-- ... --&gt;\n\n    &lt;LocalizedLink to=\"\/\"&gt;\n      {{ $t('home') }}\n    &lt;\/LocalizedLink&gt;\n    &lt;!-- When active locale is ar-EG, renders to\n         \/ar-EG --&gt;\n\n    &lt;!-- Notice that we point to about not \/about --&gt;\n    &lt;LocalizedLink to=\"about\"&gt;\n      {{ $t('about') }}\n    &lt;\/LocalizedLink&gt;\n    &lt;!-- When active locale is ar-EG, renders to\n         \/ar-EG\/about --&gt;\n  &lt;\/nav&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-build-a-language-switcher-ui\"><\/span>How do I build a language switcher UI?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>To allow our site visitors the ability to select their locales, let\u2019s build a language switcher dropdown component that makes use of our localized routes. First, we\u2019ll configure and expose our app\u2019s supported locales in our i18n library.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"b6bea5d1-a8da-40fa-b9e5-03fbb26a663e\" data-enlighter-title=\"src\/i18n\/index.js\">\/\/ ...\n\n\/\/ Using a { localeCode: localeData } structure\n\/\/ allows us to add metadata, like a name, to each\n\/\/ locale as our needs grow.\nexport const supportedLocales = {\n  'en-US': { name: 'English' },\n  'ar-EG': { name: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Arabic)' },\n}\n\n\/\/ ...\n\n<\/pre>\n<p>We can now import our <code>supportedLocales<\/code> and use them in a new <code>&lt;LocaleSwitcher&gt;<\/code> component, which wraps a humble <code>&lt;select&gt;<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"fe340862-4870-4539-9da4-4b7771361dde\" data-enlighter-title=\"src\/components\/l10n\/LocaleSwitcher.vue\">&lt;script&gt;\nimport { supportedLocales } from '..\/..\/i18n'\n\nexport default {\n  methods: {\n    \/\/ Called when the user selects a new locale\n    \/\/ from the dropdown\n    onLocaleChange(event_) {\n      const newLocale = event_.target.value\n\n      \/\/ If the selected locale is the same as the\n      \/\/ active one, do nothing\n      if (newLocale === this.$i18n.locale) {\n        return\n      }\n\n      \/\/ Navigate to the localized root route for\n      \/\/ the chosen locale\n      this.$router.push(`\/${newLocale}`)\n    },\n  },\n  computed: {\n    \/\/ Transfrom our supportedLocales object to \n    \/\/ an array of [{ code: 'en-US', name: 'English' }, ...]\n    locales() {\n      return Object.keys(supportedLocales).map((code) =&gt; ({\n        code,\n        name: supportedLocales[code].name,\n      }))\n    },\n  },\n}\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;select\n    :value=\"$i18n.locale\"\n    @change=\"onLocaleChange($event)\"\n  &gt;\n    &lt;option \n      v-for=\"locale in locales\"\n      :key=\"locale.code\"\n      :value=\"locale.code\"\n    &gt;\n      {{ locale.name }}\n    &lt;\/option&gt;\n  &lt;\/select&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>\ud83e\udd3f <em>Go Deeper \u00bb<\/em> We\u2019re using <code>$router.push()<\/code> in our <code>&lt;LocaleSwitcher&gt;<\/code> to navigate to the chosen locale\u2019s root route. Learn more about <a href=\"https:\/\/router.vuejs.org\/guide\/essentials\/navigation.html\">Vue Router\u2019s programmatic navigation<\/a> in the official guide.<\/p>\n<p>Now we can drop our new component into our app\u2019s navigation bar for our users.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"a3985af2-8834-44fb-ab11-421f5414ca2a\" data-enlighter-title=\"src\/components\/Nav.vue\" data-enlighter-highlight=\"3,15\">&lt;script setup&gt;\nimport LocalizedLink from '.\/l10n\/LocalizedLink.vue'\nimport LocaleSwitcher from '.\/l10n\/LocaleSwitcher.vue'\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;div&gt;\n    &lt;nav&gt;\n      &lt;img :alt=\"$t('logo')\" src=\"@\/assets\/logo.svg\"\/&gt;\n      &lt;span class=\"font-bold text-purple-300\"&gt;{{ $t('appTitle') }}&lt;\/span&gt;\n      &lt;LocalizedLink to=\"\/\"&gt;{{ $t('home') }}&lt;\/LocalizedLink&gt;\n      &lt;LocalizedLink to=\"about\"&gt;{{ $t('about') }}&lt;\/LocalizedLink&gt;\n    &lt;\/nav&gt;\n\n    &lt;LocaleSwitcher \/&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<div><\/div>\n<div>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35743\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/locale-switcher.gif\" alt=\"Our language switcher component in action\" width=\"600\" height=\"305\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Our language switcher component in action<\/span><\/p>\n<\/div>\n<h3><span class=\"ez-toc-section\" id=\"binding-directly-to-i18nlocale\"><\/span>Binding directly to i18n.locale<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>If you\u2019re not using localized routes, you can bind directly to <code>$i18n.locale<\/code> as follows.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"41fe9fe0-4f94-49a8-9e52-33747e0a2516\" data-enlighter-title=\"src\/components\/l10n\/LocaleSwitcher.vue\" data-enlighter-highlight=\"15\">&lt;script&gt;\nimport { supportedLocales } from '..\/..\/i18n'\n\nexport default {\n  computed: {\n    locales() {\n      \/\/ ...\n    },\n  },\n}\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;!-- Using Vue's v-model for two-way binding --&gt;\n  &lt;select v-model=\"$i18n.locale\"&gt;\n    &lt;option\n      v-for=\"locale in locales\"\n      :key=\"locale.code\"\n      :value=\"locale.code\"\n    &gt;\n      {{ locale.name }}\n    &lt;\/option&gt;\n  &lt;\/select&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>\ud83e\udd3f <em>Go Deeper \u00bb<\/em> You can learn more about <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/essentials\/scope.html#locale-changing\">locale changing<\/a> in Vue I18n\u2019s documentation.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-load-my-translation-files-asynchronously\"><\/span>How do I load my translation files asynchronously?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>As our apps grow, and we add more supported locales, we risk bloating our main bundle with all our translation messages. Realistically we will only need messages for the current visitor\u2019s chosen locale. We can make our main bundle leaner by downloading only the active locale\u2019s messages when needed.<\/p>\n<p>Let\u2019s add this async translation loading to our demo app. We\u2019ll start by breaking our <code>messages.js<\/code> file up into per-locale JSON files.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"6adcdd2b-5adc-4450-90e0-07d43db45935\" data-enlighter-title=\"src\/translations\/en-US.json\">{\n  \"appTitle\": \"Mushahed\",\n  \"home\": \"Home\",\n  \"about\": \"About\",\n  \/\/ ...\n}\n\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"b3ab72c4-53ed-4fd4-9993-b9088708df09\" data-enlighter-title=\"src\/translations\/ar-EG.json\">{\n  \"appTitle\": \"\u0645\u0634\u0627\u0647\u062f\",\n  \"home\": \"\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629\",\n  \"about\": \"\u0646\u0628\u0630\u0629 \u0639\u0646\u0627\",\n  \/\/ ...\n}\n\n<\/pre>\n<p>Next we\u2019ll add a loading function to our i18n library.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"43afe9a1-2199-4230-8952-9f2c9f3ee3d3\" data-enlighter-title=\"src\/i18n\/index.js\" data-enlighter-highlight=\"1,9,10,11,12,14,16,17,25\">import { nextTick } from 'vue'\n\n\/\/ ...\n\nlet _i18n\n\n\/\/ ...\n\nasync function loadMessagesFor(locale) {\n  const messages = await import(\n    \/* webpackChunkName: \"locale-[request]\" *\/ `..\/translations\/${locale}.json`\n  )\n\n  _i18n.global.setLocaleMessage(locale, messages.default)\n\n  return nextTick()\n}\n\nexport default {\n  get vueI18n() {\n    return _i18n\n  },\n  setup,\n  setLocale,\n  loadMessagesFor,\n}\n\n<\/pre>\n<p><code>loadMessagesFor()<\/code> uses Webpack\u2019s async <a href=\"https:\/\/v4.webpack.js.org\/guides\/code-splitting\/#dynamic-imports\">code splitting and dynamic imports<\/a> to asynchronously load the translation file for the given locale. Once the translation file has loaded, it feeds the file\u2019s messages to Vue I18n, associating them with the given locale. Finally, to ensure that Vue has updated the DOM before we resolve, we return the Promise from <a href=\"https:\/\/vuejs.org\/api\/general.html#nexttick\">nextTick()<\/a>.<\/p>\n<p>Now we can update the <code>beforeEach()<\/code> navigation guard in our router to load the locale\u2019s messages before rendering a route\u2019s associated component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-highlight=\"11,21\">import { createRouter, createWebHistory } from 'vue-router'\nimport i18n, { defaultLocale } from '..\/i18n'\n\n\/\/ ...\n\nconst router = createRouter({\n  \/\/ ...\n})\n\n\/\/ We make the callback function async...\nrouter.beforeEach(async (to, from) =&gt; {\n  const newLocale = to.params.locale\n  const prevLocale = from.params.locale\n\n  if (newLocale === prevLocale) {\n    return\n  }\n\n  \/\/ ...so we can wait for the messages to load\n  \/\/ before we continue\n  await i18n.loadMessagesFor(newLocale)\n\n  i18n.setLocale(newLocale)\n})\n\nexport default router\n\n<\/pre>\n<p>If we reload our app now we shouldn\u2019t see any major changes. However, if we open the network tab in our browser\u2019s developer tools, we should see that a message JSON file loads in when we switch locales.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35748\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/aync-loading-network-tab.png\" alt=\"Using Webpack's code splitting to asynchronously load a locale's translations\" width=\"1816\" height=\"138\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/aync-loading-network-tab.png 1816w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/aync-loading-network-tab-300x23.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/aync-loading-network-tab-1024x78.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/aync-loading-network-tab-768x58.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/aync-loading-network-tab-1536x117.png 1536w\" sizes=\"(max-width: 1816px) 100vw, 1816px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Using Webpack&#8217;s code splitting to asynchronously load a locale&#8217;s translations<\/span><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Read more about async\/lazy loading in Vue I18n\u2019s <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/advanced\/lazy.html\">Lazy loading guide<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-work-with-locale-fallback\"><\/span>How do I work with locale fallback?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>You may have noticed some console warnings after implementing asynchronous translation loading above. The warnings occur when you load an <code>en-US<\/code> route for the first time.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35753\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/fallback-console-warnings.png\" alt=\"Vue I18n attempting to fall back on a more general locale when it can't find a translation message\" width=\"1178\" height=\"158\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/fallback-console-warnings.png 1178w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/fallback-console-warnings-300x40.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/fallback-console-warnings-1024x137.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/fallback-console-warnings-768x103.png 768w\" sizes=\"(max-width: 1178px) 100vw, 1178px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Vue I18n attempting to fall back on a more general locale when it can&#8217;t find a translation message<\/span><\/p>\n<p>What\u2019s happening is that Vue I18n can\u2019t find any <code>en-US<\/code> message when the app first loads. The <code>en-US<\/code> messages load in a HTTP request separate from the main bundle, so they may not be available when the app first loads. We\u2019ll address this in a minute.<\/p>\n<p>Notice, however, that Vue I18n tries to find the <code>logo<\/code> message in a general <code>en<\/code> locale when it can\u2019t find it in the region-specific <code>en-US<\/code> locale. This is the library\u2019s default fallback behaviour. It can come in quite handy when one of our locales is missing translations.<\/p>\n<p>\ud83e\udd3f <em>Go Deeper \u00bb<\/em> Check out all of the options Vue I18n gives you for fallback in the <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/essentials\/fallback.html#fallbacking\">Fallbacking guide<\/a>.<\/p>\n<p>A <code>fallbackLocale<\/code> config option is available to us as a bottom catch-all: Any locales we list under <code>fallbackLocale<\/code> will be used to display a message if one can\u2019t otherwise be found. Let\u2019s use this option to ensure that we fall back on English in our app.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"04d3ea19-3125-40a3-b6a1-5a021092308d\" data-enlighter-title=\"src\/i18n\/index.js\" data-enlighter-highlight=\"6,15,16\">\/\/ ...\nimport { createI18n } from 'vue-i18n'\nimport { numberFormats } from '.\/numbers'\nimport { arabicPluralRules } from '.\/plurals'\nimport { datetimeFormats } from '.\/datetimes'\nimport defaultMessages from '..\/translations\/en-US.json'\n\nexport const defaultLocale = 'en-US'\n\nlet _i18n\n\nfunction setup(options = { locale: defaultLocale }) {\n  _i18n = createI18n({\n    locale: options.locale,\n    fallbackLocale: defaultLocale,\n    messages: { [defaultLocale]: defaultMessages },\n    numberFormats,\n    datetimeFormats,\n    pluralizationRules: {\n      'ar-EG': arabicPluralRules,\n    },\n  })\n\n  setLocale(options.locale)\n\n  return _i18n\n}\n\n\/\/ ...\n\n<\/pre>\n<p>We <code>import<\/code> our <code>en-US<\/code> translation messages and pass them into Vue I18n\u2019s <code>messages<\/code> option. This will include our English messages in the main bundle, ensuring that our app won\u2019t have to wait for them to load asynchronously. Setting <code>en-US<\/code> as the <code>fallbackLocale<\/code> ensures that the equivalent English message will be shown instead of a missing message in another locale.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35758\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-fallback-to-en.png\" alt=\"The English message for &quot;home&quot; is used instead of the missing Arabic message\" width=\"564\" height=\"138\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-fallback-to-en.png 564w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-fallback-to-en-300x73.png 300w\" sizes=\"(max-width: 564px) 100vw, 564px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">The English message for &#8220;home&#8221; is used instead of the missing Arabic message<\/span><\/p>\n<div><strong style=\"color: #ff6600;\">\u00a0<\/strong><\/div>\n<div>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35763\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-fallback-console-warnings.png\" alt=\"Handy console warnings reveal Vue I18n's fallback chain\" width=\"1194\" height=\"220\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-fallback-console-warnings.png 1194w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-fallback-console-warnings-300x55.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-fallback-console-warnings-1024x189.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/ar-fallback-console-warnings-768x142.png 768w\" sizes=\"(max-width: 1194px) 100vw, 1194px\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Handy console warnings reveal Vue I18n&#8217;s fallback chain<\/span><\/p>\n<\/div>\n<p>With that in place, our little demo app is internationalized.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35768\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/options-complete.gif\" alt=\"Our internationalized demo app\" width=\"600\" height=\"492\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Our internationalized demo app<\/span><\/p>\n<p>\ud83d\udd17 <em>Resource<\/em> You can <a href=\"https:\/\/github.com\/PhraseApp-Blog\/vue3-i18n-2022\/tree\/i18n-options\">get all of the code<\/a> of the internationalized Options API demo app we built above from GitHub. The demo code includes some features we didn\u2019t have space for in this article, like listening for locale changes to reload data and right-to-left language support.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-my-vue-app-with-the-composition-api\"><\/span>How do I localize my Vue app with the Composition API?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Everything we\u2019ve covered in this article so far pertains to Vue\u2019s object-oriented <a href=\"https:\/\/vuejs.org\/guide\/introduction.html#api-styles\">Options API<\/a>. If your app is using the Composition API in Vue 3, we got you covered in this section.<\/p>\n<p>\u270b <em>Heads up \u00bb<\/em> Vue I18n is designed to work with <em>either<\/em> the Options API or Composition API, <em>but not both<\/em>. The Vue I18n Options API is the default and is called the <em>Legacy API<\/em>. Read the <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/migration\/vue3.html#migration-to-composition-api-from-legacy-api\">Migration to Composition API from Legacy API<\/a> guide for information about limitations and caveats.<\/p>\n<p>Before we refactor our I18n code, let\u2019s briefly look at how we would refactor our Vue components (sans I18n) from the Options API to the Composition API.<\/p>\n<p>\ud83d\uddd2\ufe0f <em>Note \u00bb<\/em> The following sections build on what we\u2019ve already covered in this article. If you\u2019re new to Vue I18n it\u2019s recommended that you read the rest of the article before continuing.<\/p>\n<p>Only three files in our demo need to be refactored for Composition: <code>AstroCard.vue<\/code>, <code>Astronauts.vue<\/code>, and <code>Coords.vue<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"4413e045-ce28-4dc3-95c1-1bbadddb9974\" data-enlighter-title=\"src\/components\/AstroCard.vue\">&lt;-- Use script setup syntactic sugar for single-file components --&gt;\n&lt;script setup&gt;\n\n\/\/ Import functions from Vue\nimport { computed } from 'vue'\n\n\/\/ Use macro to define `props` \nconst props = defineProps({\n  name: String,\n  photoUrl: String,\n  nationality: String,\n  craft: String,\n})\n\n\/\/ Create computed property using computed()\n\/\/ and refactor prop reference to use `props.X`\nconst fullPhotoUrl = computed(() =&gt; `\/img\/astros\/${props.photoUrl}`)\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;!-- Nothing changes in the template --&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"cb1897b1-720e-4c01-a14e-4a4b0a02a239\" data-enlighter-title=\"src\/components\/Astronauts.vue\">&lt;script setup&gt;\nimport { ref } from 'vue'\nimport AstroCard from '.\/AstroCard.vue'\n\n\/\/ Use ref to define reactive data\nconst loading = ref(true)\nconst astros = ref([])\n\n\/\/ Logic that would run in created() is\n\/\/ written directly at the top level\nfetch('\/data\/astronauts.json')\n  .then((res) =&gt; res.json())\n  .then((data) =&gt; {\n    \/\/ Remember to use .value when getting\/setting\n    \/\/ values defined with ref()\n    astros.value = data\n    loading.value = false\n  })\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;!-- Nothing changes in the template --&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> We\u2019ll stop here to keep things brief and get to the i18n. You can <a href=\"https:\/\/github.com\/PhraseApp-Blog\/vue3-i18n-2022\/compare\/start-options...start-composition\">view the diff for the Composition API refactor<\/a> (before i18n) on our GitHub repo.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"refactoring-i18n-to-use-the-composition-api\"><\/span>Refactoring i18n to use the Composition API<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>There\u2019s not <em>too<\/em> much to change if we want to use Vue I18n\u2019s Composition API. Here\u2019s what we\u2019ll cover:<\/p>\n<ul>\n<li>Setting <code>legacy: false<\/code> when creating the VueI18n instance.<\/li>\n<li>Refactoring <code>vueI18n.global.locale<\/code> to <code>vueI18n.global.locale.value<\/code>.<\/li>\n<li>Refactoring <code>tc()<\/code> calls to <code>t()<\/code> for plurals.<\/li>\n<li>Refactoring all <code>this.X<\/code> calls to their functional equivalents in our component <code>&lt;script&gt;<\/code>s.<\/li>\n<\/ul>\n<p>Let\u2019s get to it.<\/p>\n<h4>Turning off legacy mode<\/h4>\n<p>By default Vue I18n is in \u201clegacy\u201d mode, where <code>createI18n()<\/code> returns a <code>VueI8n<\/code> object instance. We want the factory function to create a <a href=\"https:\/\/vue-i18n.intlify.dev\/api\/composition.html#composer\">Composer<\/a> instance, which provides functions like <code>t()<\/code> and <code>n()<\/code> to our composition component.<\/p>\n<p>To accomplish this, we just need to pass one option to <code>createI18n()<\/code>, setting <code>legacy: false<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"2db6da9b-2001-4a30-a938-0151f3d81556\" data-enlighter-title=\"src\/i18n\/index.js\" data-enlighter-highlight=\"12\">\/\/ ...\nimport { createI18n } from 'vue-i18n'\n\nexport const defaultLocale = 'en-US'\n\n\/\/ ...\n\nlet _i18n\n\nfunction setup(options = { locale: defaultLocale }) {\n  _i18n = createI18n({\n    legacy: false,\n    \/\/ Nothing else changes in our options\n  })\n\n  setLocale(options.locale)\n\n  return _i18n\n}\n\n\/\/ ... \n\n<\/pre>\n<h4>Using reactive properties<\/h4>\n<p>As soon as we start using the Composition with <code>legacy: false<\/code> we need to refactor our calls to Vue I18n\u2019s <code>locale<\/code>, since <code>locale<\/code> now acts like a <a href=\"https:\/\/vuejs.org\/guide\/essentials\/reactivity-fundamentals.html#reactive-variables-with-ref\">reactive ref<\/a>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"fb01cc97-f5b8-41fc-9ba5-908adb949795\" data-enlighter-title=\"src\/i18n\/index.js\" data-enlighter-highlight=\"6\">\/\/ ...\n\nfunction setLocale(newLocale) {\n  \/\/ Just like any Vue reactive ref, we have\n  \/\/ to get\/set it with .value\n  _i18n.global.locale.value = newLocale\n  setDocumentAttributesFor(newLocale)\n}\n\n\/\/ ...\n\n<\/pre>\n<p>That\u2019s all we we have to change in our i18n library. The remaining updates will be in our components.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Check out the <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/advanced\/composition.html\">Composition API<\/a> guide for additional info.<\/p>\n<h4>Using t() instead of tc() for plural messages<\/h4>\n<p>Vue I18n\u2019s Composer instance doesn\u2019t have a <code>tc()<\/code> function for outputting plural messages; switching to the Composition API will cause Vue I18n to throw an error whenever we attempt to use <code>tc()<\/code>. Instead, we can just the regular <code>t()<\/code> function as a drop-in replacement.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"html\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"bc6046ca-e556-46f8-8bc9-1d8aa5938011\" data-enlighter-title=\"src\/components\/Astronauts.vue\" data-enlighter-highlight=\"12\">&lt;script setup&gt;\n  \/\/ ...\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;!-- ... --&gt;\n\n  &lt;div&gt;\n    &lt;div&gt;\n      &lt;h2&gt;\n      &lt;!-- Use $t() instead of $tc(): works exactly the same --&gt;\n        \ud83e\uddd1\u200d\ud83d\ude80 {{ $t('peopleInSpace', astros.length) }}\n      &lt;\/h2&gt;\n\n      &lt;p&gt;\n        {{ $t('updatedAt', { date: $d(updated, 'short') }) }}\n      &lt;\/p&gt;\n    &lt;\/div&gt;\n\n    &lt;!-- ... --&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>\ud83d\uddd2\ufe0f <em>Note \u00bb<\/em> <code>$t()<\/code>, <code>$d()<\/code>, and <code>$n()<\/code> work in component <code>&lt;template&gt;<\/code>s in both Legacy and Composition modes. This is because, by default, Vue I18n <a href=\"https:\/\/vue-i18n.intlify.dev\/guide\/advanced\/composition.html#implicit-with-injected-properties-and-functions\">injects them globally<\/a> in both modes. This is <em>not<\/em> the case with component <code>&lt;scripts&gt;<\/code>, where <code>$t()<\/code>, <code>$d()<\/code> etc. are not available in Composition mode. We\u2019ll deal with that next.<\/p>\n<h4>Using localization functions in component scripts<\/h4>\n<p>In our <code>Coords<\/code> component we\u2019re using <code>this.$t()<\/code>, <code>this.$d()<\/code> and <code>this.$n()<\/code> to retrieve translation messages and localized dates and numbers, respectively.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"3993da28-5b01-454b-b85f-a5e87b806c3b\" data-enlighter-title=\"src\/components\/Coords.vue\" data-enlighter-highlight=\"19,20,21,22,23\">&lt;script&gt;\nexport default {\n  data() {\n    return {\n      loading: true,\n      coords: null,\n      datetime: '',\n    }\n  },\n\n  created() {\n    \/\/ We fetch coordinate data from the network here...\n  },\n\n  computed: {\n    issPosition() {\n      const { latitude, longitude } = this.coords\n     \n      return this.$t('issPosition', {\n        latitude: this.$n(latitude, 'coords'),\n        longitude: this.$n(longitude, 'coords'),\n        datetime: this.$d(this.datetime, 'full'),\n      })\n    },\n  },\n}\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;!-- Display coordinate data --&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>When we switch the Composition API, we no longer have <code>this<\/code> pointing to the component instance, so we can\u2019t use <code>this.$t()<\/code> and its ilk anymore. Vue I18n provides a <code>useI18n()<\/code> function that returns the Composer instance, which includes <code>t()<\/code> and company. Let\u2019s see it in action.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"42e79f8b-0396-4169-bd30-0e50c67456da\" data-enlighter-title=\"src\/components\/Coords.vue\" data-enlighter-highlight=\"3,6,18,19,20,21,22\">&lt;script setup&gt;\nimport { ref, computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\n\/\/ Destructure functions from returned Composer instance\nconst { t, n, d } = useI18n()\n\nconst loading = ref(true)\nconst coords = ref(null)\nconst datetime = ref('')\n\n\/\/ We fetch coordinate data from the network here...\n\nconst issPosition = computed(() =&gt; {\n  const { latitude, longitude } = coords.value\n\n  \/\/ Use functions without `this`\n  return t('issPosition', {\n    latitude: n(latitude, 'coords'),\n    longitude: n(longitude, 'coords'),\n    datetime: d(datetime.value, 'full'),\n  })\n})\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;!-- ... --&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>\u270b <em>Heads up \u00bb<\/em> Don\u2019t use the $ prefix with the functional variants in component <code>&lt;script&gt;<\/code>s.<\/p>\n<h4>Using the reactive <code>locale<\/code> property<\/h4>\n<p>Just like <code>this.$t()<\/code> needed to be refactored, so to does <code>this.$i18n.locale<\/code>. We can get and set the active <code>locale<\/code> by destructuring it from the Composer instance in our components. Let\u2019s refactor our <code>LocaleSwitcher<\/code> and <code>LocalizedLink<\/code> components to use the reactive <code>locale<\/code> property.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"html\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"75f0ca44-b299-4027-bdbc-2467415880dd\" data-enlighter-title=\"src\/components\/l10n\/LocaleSwitcher.vue\" data-enlighter-highlight=\"3,8,15,34\">&lt;script setup&gt;\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport { useRouter } from 'vue-router'\nimport { supportedLocales } from '..\/..\/i18n'\n\nconst router = useRouter()\nconst { locale } = useI18n()\n\nfunction onLocaleChange(event_) {\n  const newLocale = event_.target.value\n\n  \/\/ Just like other reactive refs, we need to \n  \/\/ use locale.value to get\/set the active locale\n  if (newLocale === locale.value) {\n    return\n  }\n\n  router.push(`\/${newLocale}`)\n}\n\nconst locales = computed(() =&gt;\n  Object.keys(supportedLocales).map((code) =&gt; ({\n    code,\n    name: supportedLocales[code].name,\n  }))\n)\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;!-- Notice that $i18n.locale is still available in\n       our component templates --&gt;\n  &lt;select\n    :value=\"$i18n.locale\"\n    @change=\"onLocaleChange($event)\"\n  &gt;\n    &lt;option v-for=\"locale in locales\" :key=\"locale.code\" :value=\"locale.code\"&gt;\n      {{ locale.name }}\n    &lt;\/option&gt;\n  &lt;\/select&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"7c490ef2-f10f-4ad4-bf85-10f7ef1e325a\" data-enlighter-title=\"src\/components\/l10n\/LocalizedLink.vue\" data-enlighter-highlight=\"4,8,11\">&lt;script setup&gt;\nimport { computed } from 'vue'\nimport { RouterLink } from 'vue-router'\nimport { useI18n } from 'vue-i18n'\n\nconst props = defineProps(['to'])\n\nconst { locale } = useI18n()\n\nconst localizedUrl = computed(() =&gt;\n  props.to === '\/' ? `\/${locale.value}` : `\/${locale.value}\/${props.to}`\n)\n&lt;\/script&gt;\n\n&lt;template&gt;\n  &lt;router-link :to=\"localizedUrl\"&gt;\n    &lt;slot&gt;&lt;\/slot&gt;\n  &lt;\/router-link&gt;\n&lt;\/template&gt;\n\n<\/pre>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The Composer instance exposes other properties: Check out the <a href=\"https:\/\/vue-i18n.intlify.dev\/api\/composition.html\">API docs<\/a> for a comprehensive listing.<\/p>\n<p>And with that, our refactor is complete.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-35768\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/11\/options-complete.gif\" alt=\"Our internationalized demo app\" width=\"600\" height=\"492\" \/><\/p>\n<p><span style=\"display: block; text-align: center; font-style: italic; font-size: 80%;\">Our demo works exactly as it did in Legacy mode<\/span><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> You can get the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/vue3-i18n-2022\/tree\/i18n-composition\">complete code for the Composition i18n demo<\/a> from GitHub.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> If you&#8217;re interested in general JavaScript i18n, including other UI and i18n libraries, you might enjoy our <a href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/\">Ultimate Guide to JavaScript Localization<\/a>.<\/p>\n<p>That about does it for our Vue 3 i18n demo. We hope you enjoyed it and learned a few things along the way. If you\u2019re looking to take your i18n game to the next level, check out <a href=\"https:\/\/phrase.com\">Phrase<\/a>. Phrase supports Vue I18n out of the box with its In-Context Editor, allowing your translators to update messages directly in your app. The fully-featured Phrase web console, with machine learning and smart suggestions, is a joy for translators to use. Once translations are ready, they can sync back to your project automatically \u2014 Phrase comes with a CLI and syncs with Bitbucket, GitHub, and GitLab. You set it and forget it, leaving you to focus on the code you love. Check out <a href=\"https:\/\/phrase.com\/roles\/developers\/\">all the features<\/a> Phrase has to offer, and <a href=\"https:\/\/eu.phrase.com\/idm-ui\/signup\">give it a spin<\/a> with a 14-day free trial.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Dive into Vue localization and learn how to plug the Vue I18n library into your app, so you can make it accessible to a global user base.<\/p>\n","protected":false},"author":41,"featured_media":35846,"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-8775","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\/8775"}],"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=8775"}],"version-history":[{"count":9,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/8775\/revisions"}],"predecessor-version":[{"id":65133,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/8775\/revisions\/65133"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media\/35846"}],"wp:attachment":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media?parent=8775"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/categories?post=8775"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}