{"id":11335,"date":"2020-07-08T11:38:46","date_gmt":"2020-07-08T09:38:46","guid":{"rendered":"https:\/\/phrase.com\/blog\/?p=11335"},"modified":"2024-10-28T13:02:03","modified_gmt":"2024-10-28T12:02:03","slug":"a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3","status":"publish","type":"post","link":"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/","title":{"rendered":"A Step-by-Step Guide to Svelte Localization"},"content":{"rendered":"<p>With <a href=\"https:\/\/github.com\/sveltejs\/svelte\/stargazers\">over 35K stars on GitHub<\/a>, and <a href=\"https:\/\/www.npmjs.com\/search?q=svelte\">over 1,000 NPM packages<\/a> in its ecosystem, I think it\u2019s safe to say that the <a href=\"https:\/\/svelte.dev\/\">Svelte<\/a> JavaScript framework has found a solid niche for itself. And for good reason: unlike Angular, React, and Vue, Svelte has no virtual DOM. It\u2019s effectively a compiler that doesn\u2019t ship with your app, meaning it has almost no footprint in your app bundle. Svelte is also exceptionally minimal and elegant in its design, making it pretty learnable. In fact, if you haven&#8217;t tried out Svelte yet, I urge you to give it a go.<br \/>\nBut I assume I\u2019m preaching to the choir, and that you\u2019ve already been bitten by the Svelte bug. I\u2019ll wager another assumption: you have a Svelte app and you want to internationalize and localize it. Well, look no further. In this article, we\u2019ll build a small demo app, and we\u2019ll use <a href=\"https:\/\/github.com\/kaisermann\">Christian Kaisermann\u2019s<\/a> <a href=\"https:\/\/github.com\/kaisermann\/svelte-i18n\">svelte-i18n library<\/a> to internationalize it. Let\u2019s get to it!<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> We\u2019ve <a href=\"https:\/\/phrase.com\/blog\/posts\/how-to-localize-a-svelte-app-with-svelte-i18n\/\">written about Svelte i18n before<\/a>. In that article, we used svelte-i18n version 1-beta. This article is a refresh that covers svelte-18n version 3.<\/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\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#our-demo-app\" title=\"Our Demo App\">Our Demo App<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#installing-svelte\" title=\"Installing Svelte\">Installing Svelte<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#the-basic-app\" title=\"The Basic App\">The Basic App<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#the-header-and-footer\" title=\"The Header and Footer\">The Header and Footer<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#the-character-list\" title=\"The Character List\">The Character List<\/a><\/li><\/ul><\/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\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#installing-svelte-i18n\" title=\"Installing svelte-i18n\">Installing svelte-i18n<\/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\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#bootstrapping-svelte-i18n\" title=\"Bootstrapping svelte-i18n\">Bootstrapping svelte-i18n<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#translation-message-dictionaries\" title=\"Translation Message Dictionaries\">Translation Message Dictionaries<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#basic-translation\" title=\"Basic Translation\">Basic Translation<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#async-translation-file-loading\" title=\"Async Translation File Loading\">Async Translation File Loading<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#rendering-after-the-locale-is-loaded\" title=\"Rendering After the Locale Is Loaded\">Rendering After the Locale Is Loaded<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#getting-and-setting-the-active-locale\" title=\"Getting and Setting the Active Locale\">Getting and Setting the Active Locale<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#a-simple-language-switcher\" title=\"A Simple Language Switcher\">A Simple Language Switcher<\/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\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#automatically-detecting-the-users-locale\" title=\"Automatically Detecting the User\u2019s Locale\">Automatically Detecting the User\u2019s Locale<\/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\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#supported-locales\" title=\"Supported Locales\">Supported Locales<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#detection-with-fallback\" title=\"Detection with Fallback\">Detection with Fallback<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#updating-our-language-selector\" title=\"Updating our Language Selector\">Updating our Language Selector<\/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\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#layout-direction-left-to-right-and-right-to-left\" title=\"Layout Direction: Left-to-Right and Right-to-Left\">Layout Direction: Left-to-Right and Right-to-Left<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-19\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#localizing-the-document-direction\" title=\"Localizing the Document Direction\">Localizing the Document Direction<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-20\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#loading-css-based-on-direction\" title=\"Loading CSS Based on Direction\">Loading CSS Based on Direction<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-21\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#localizing-the-document-title\" title=\"Localizing the Document Title\">Localizing the Document Title<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-22\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#translation-messages\" title=\"Translation Messages\">Translation Messages<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-23\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#nesting-translation-messages\" title=\"Nesting Translation Messages\">Nesting Translation Messages<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-24\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#interpolation\" title=\"Interpolation\">Interpolation<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-25\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#using-html-in-translation-messages\" title=\"Using HTML in Translation Messages\">Using HTML in Translation Messages<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-26\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#using-global-css-to-style-in-message-elements\" title=\"Using Global CSS to Style In-Message Elements\">Using Global CSS to Style In-Message Elements<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-27\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#plurals\" title=\"Plurals\">Plurals<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-28\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#date-formatting\" title=\"Date Formatting\">Date Formatting<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-29\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#related-articles\" title=\"Related Articles\">Related Articles<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-30\" href=\"https:\/\/phrase.com\/blog\/posts\/a-step-by-step-guide-to-svelte-localization-with-svelte-i18n-v3\/#peace-out\" title=\"Peace Out\">Peace Out<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"our-demo-app\"><\/span>Our Demo App<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Our application, <em>Rebel Voter<\/em>, allows users to upvote and downvote Star Wars characters.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-11363 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-en-complete.png\" alt=\"Finished demo app | Phrase\" width=\"958\" height=\"993\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-en-complete.png 958w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-en-complete-289x300.png 289w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-en-complete-768x796.png 768w\" sizes=\"(max-width: 958px) 100vw, 958px\" \/><br \/>\n<em>What our app will look like when all is said and done<\/em><\/p>\n<p>While <em>Rebel Voter<\/em> won\u2019t actually connect to a server to persist its voting data, it will serve as a great i18n demo for Svelte and svelte-i18n 3. Let\u2019s build it out in the quickness and get to localizing it.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> You can get all of the project\u2019s code from <a href=\"https:\/\/github.com\/PhraseApp-Blog\/svelte-i18n-2020-06\">our companion repo on GitHub<\/a>.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"installing-svelte\"><\/span>Installing Svelte<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>I find the quickest way to install Svelte is through the <code>npx degit<\/code> command from the command line.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">$ npx degit sveltejs\/template rebel-voter\n$ cd rebel-voter\n$ npm install\n<\/pre>\n<p>These commands will scaffold our app using the official Svelte template, and install all the NPM package dependencies we need to start cooking.<br \/>\nWith all that in place, we can run our dev server through <code>npm run dev<\/code>.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"the-basic-app\"><\/span>The Basic App<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>First off, let\u2019s modify the <code>App.svelte<\/code> file that comes with Svelte template so that it looks like the following.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-title=\"src\/App.svelte\" data-enlighter-group=\"a087e3e8-fa29-4e54-831b-71b156d4ccc9\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import Header from '.\/components\/Layout\/Header.svelte';\n  import Footer from '.\/components\/Layout\/Footer.svelte';\n  import CharacterList from\n    '.\/components\/Characters\/CharacterList.svelte';\n&lt;\/script&gt;\n&lt;Header \/&gt;\n&lt;main role=\"main\"&gt;\n  &lt;CharacterList \/&gt;\n&lt;\/main&gt;\n&lt;Footer \/&gt;\n&lt;style&gt;\n  main { padding: 0 1rem; }\n&lt;\/style&gt;\n<\/pre>\n<p>We start with three components. <code>Header<\/code> and <code>Footer<\/code> are presentational layout components. Our app\u2019s heart and soul lie in <code>CharacterList<\/code>. Let\u2019s take a look at the code of all three of these components.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"the-header-and-footer\"><\/span>The Header and Footer<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Our <code>Header<\/code> and <code>Footer<\/code> are simple, presentational components.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"49a88f74-f781-4a52-9d24-61ecc0109328\" data-enlighter-title=\"src\/components\/Layout\/Header.svelte\" data-enlighter-linenumbers=\"false\">&lt;header class=\"hero\"&gt;\n  &lt;div class=\"hero-body\"&gt;\n    &lt;div class=\"container\"&gt;\n      &lt;h1 class=\"title\"&gt;Rebel Voter&lt;\/h1&gt;\n      &lt;h2 class=\"subtitle\"&gt;Your favorite Star Wars characters&lt;\/h2&gt;\n     &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/header&gt;\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"af6fc065-542a-418e-bde2-2e81cfae89a7\" data-enlighter-title=\"src\/components\/Footer.svelte\" data-enlighter-linenumbers=\"false\">&lt;style&gt;\n  \/* Styles are omitted for brevity\n     You can grab them on GitHub. *\/\n&lt;\/style&gt;\n&lt;footer class=\"footer\"&gt;\n    &lt;div class=\"content has-text-centered\"&gt;\n        &lt;p&gt;\n            Companion to a\n            &lt;a href=\"https:\/\/phrase.com\/blog\"&gt;Phrase blog\n            &lt;\/a&gt; article. Made with\n            &lt;a href=\"https:\/\/svelte.dev\/\"&gt;Svelte&lt;\/a&gt; &amp;amp;\n            &lt;a href=\"https:\/\/bulma.io\/\"&gt;Bulma&lt;\/a&gt;.\n        &lt;\/p&gt;\n    &lt;\/div&gt;\n&lt;\/footer&gt;\n<\/pre>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> We\u2019re using the sleek <a href=\"https:\/\/bulma.io\/\">Bulma CSS<\/a> framework for styling here. In fact, most CSS classes you\u2019ll see in this article are Bulma classes.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"the-character-list\"><\/span>The Character List<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Our <code>CharacterList<\/code> component is where the main action begins. The component loads our demo JSON data and presents it.<br \/>\nLet\u2019s take a look at how our data is structured.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"9381efe2-4b66-416e-90f2-536a2c0a181c\" data-enlighter-title=\"public\/data\/characters.json\" data-enlighter-linenumbers=\"false\">[\n  {\n    \"id\": 1,\n    \"name\": \"Luke Skywalker\",\n    \"imageUrl\": \"https:\/\/upload.wikimedia.org\/wikipedia\/en\/9\/9b\/Luke_Skywalker.png\",\n    \"firstAppearedInFilm\": {\n      \"title\": \"A New Hope\",\n      \"releasedAt\": \"1977-05-25\"\n    },\n    \"upVoteCount\": 22,\n    \"downVoteCount\": 4\n  },\n  {\n    \"id\": 2,\n    \"name\": \"Princess Leia\",\n    \"imageUrl\": \"https:\/\/upload.wikimedia.org\/wikipedia\/en\/1\/1b\/Princess_Leia%27s_characteristic_hairstyle.jpg\",\n    \"firstAppearedInFilm\": {\n      \"title\": \"A New Hope\",\n      \"releasedAt\": \"1977-05-25\"\n    },\n    \"upVoteCount\": 19,\n    \"downVoteCount\": 2\n  },\n  \/\/ ...\n]\n<\/pre>\n<p>Now let&#8217;s use this data in our <code>Character<\/code> component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"3f5c586b-291f-41eb-a415-873d74a5ddb5\" data-enlighter-title=\"src\/components\/Characters\/CharacterList.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import Character from '.\/Character.svelte';\n  function fetchCharacters() {\n    return fetch('\/data\/characters.json')\n      .then(response =&gt; response.json());\n  }\n&lt;\/script&gt;\n{#await fetchCharacters()}\n  &lt;p&gt;Loading...&lt;\/p&gt;\n{:then characters}\n  &lt;div class=\"columns is-mobile is-multiline\"&gt;\n    {#each characters as character}\n      &lt;div\n        class=\"column is-one-third-desktop is-half-tablet is-full-mobile\"\n      &gt;\n        &lt;Character {character} \/&gt;\n      &lt;\/div&gt;\n    {\/each}\n  &lt;\/div&gt;\n{:catch error}\n  &lt;p&gt;There was a problem loading characters.&lt;\/p&gt;\n{\/await}\n<\/pre>\n<p>We simply <code>fetch()<\/code> our data, iterate over the array it gives us, and pass each item in the array to <code>Character<\/code> to display it.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"70c18137-d2a6-473d-9438-680928776aa3\" data-enlighter-title=\"src\/components\/Characters\/Character.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import VotingButton from '..\/UI\/VotingButton.svelte';\n  export let character;\n  const {\n      name,\n      imageUrl: src,\n      firstAppearedInFilm,\n  } = character;\n  let {\n    upVoteCount,\n    downVoteCount,\n  } = character;\n&lt;\/script&gt;\n&lt;style&gt;\n  \/* Styles are omitted for brevity\n     You can grab them on GitHub. *\/\n&lt;\/style&gt;\n&lt;div class=\"box\"&gt;\n  &lt;div class=\"columns is-mobile\"&gt;\n    &lt;div class=\"column is-one-quarter img-container\"&gt;\n      &lt;img {src} alt=\"{name}\"&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"column\"&gt;\n      &lt;h3 class=\"is-size-5 is-uppercase name\"&gt;\n        {name}\n      &lt;\/h3&gt;\n      &lt;p class=\"first-appeared\"&gt;\n        First appeared in\n        &lt;span class=\"first-appeared-title\"&gt;\n          {firstAppearedInFilm.title},\n        &lt;\/span&gt;\n        {firstAppearedInFilm.releasedAt}\n      &lt;\/p&gt;\n        &lt;div class=\"buttons has-addons\"&gt;\n          &lt;VotingButton\n            type=\"up\"\n            count={upVoteCount}\n            on:click={() =&gt; upVoteCount += 1}\n          \/&gt;\n          &lt;VotingButton\n            type=\"down\"\n            count={downVoteCount}\n            on:click={() =&gt; downVoteCount += 1}\n          \/&gt;\n        &lt;\/div&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;\n<\/pre>\n<p><code>Character<\/code> controls two <code>VotingButton<\/code>s, which help us demo the dynamic upvoting and downvoting behavior.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"266c329b-f030-43a5-8d67-467343d8a1b2\" data-enlighter-title=\"src\/components\/UI\/VotingButton.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  export let count = 0;\n  export let type = \"up\";\n&lt;\/script&gt;\n&lt;style&gt;\n  .button { min-width: 5rem; }\n  .button-count { font-size: 0.9rem; }\n&lt;\/style&gt;\n&lt;button class=\"button\" on:click&gt;\n  &lt;span class=\"icon\"&gt;\n    &lt;span class=\"far fa-thumbs-{type}\" \/&gt;\n  &lt;\/span&gt;\n  &lt;span class=\"button-count\"&gt;{count}&lt;\/span&gt;\n&lt;\/button&gt;\n<\/pre>\n<p>The <code>fa-thumbs-up<\/code> and <code>fa-thumbs-down<\/code> classes are FontAwesome CSS classes that display a thumbs up icon and a thumbs down icon, respectively.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-11375 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-demo-before-i18n.png\" alt=\"Untranslated demo app | Phrase\" width=\"931\" height=\"949\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-demo-before-i18n.png 931w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-demo-before-i18n-294x300.png 294w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-demo-before-i18n-768x783.png 768w\" sizes=\"(max-width: 931px) 100vw, 931px\" \/><br \/>\n<em>And that\u2019s <\/em>Rebel<em> Voter<\/em>. <em>No one likes Jar Jar \u2639\ufe0f<br \/>\n<\/em><\/p>\n<p>Now we can get to our i18n and l10n.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> If you haven\u2019t been coding along, and you want to start now that we\u2019re tackling i18n, clone yourself a copy of <a href=\"https:\/\/github.com\/PhraseApp-Blog\/svelte-i18n-2020-06\/tree\/start\">our Git repo from GitHub<\/a> and checkout the <code>start<\/code> branch.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"installing-svelte-i18n\"><\/span>Installing svelte-i18n<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>svelte-i18n is installed, as you might imagine, through NPM.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">$ npm install --save svelte-i18n\n<\/pre>\n<blockquote><p>\u270b\ud83c\udffd <em>Heads up \u00bb<\/em> After installing svelte-i18n and running your development server, you may get warnings that say \u201c(!) this has been rewritten to undefined\u201c. <a href=\"https:\/\/rollupjs.org\/guide\/en\/#error-this-is-undefined\">This is a known problem<\/a> when using Rollup with some modules. The code the warnings are referring to seems to work just fine in our case. Still, if you do want to get rid of the warnings, you can modify the project\u2019s Rollup configuration to pass the modules the value of <code>this<\/code> that they expect. In fact, we\u2019ve done all the work for you, and it\u2019s <a href=\"https:\/\/github.com\/PhraseApp-Blog\/svelte-i18n-2020-06\/blob\/e2adf5fd3daeb7c83330524cc04b69f094356677\/rollup.config.js#L18\">in our repo on GitHub<\/a>.<\/p><\/blockquote>\n<h2><span class=\"ez-toc-section\" id=\"bootstrapping-svelte-i18n\"><\/span>Bootstrapping svelte-i18n<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>svelte-i18n is relatively easy to use out of the box. It just needs a wee bit of setup to get going.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"translation-message-dictionaries\"><\/span>Translation Message Dictionaries<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>svelte-i18n works with key\/value translation message dictionaries, one for each language. The library takes these dictionaries in via an <code>addMessages()<\/code> function. We\u2019ll get to that in a moment. Let\u2019s first organize our project by placing our translation dictionaries in per-language files.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"2718f602-cd91-4687-b32e-b72a41de14f3\" data-enlighter-linenumbers=\"false\" data-enlighter-title=\"src\/lang\/en.json\">{\n  \"app_title\": \"Rebel Voter\",\n  \"app_slogan\": \"Your favorite Star Wars characters\"\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"c8d5b69e-9cbe-4ee3-8569-c89360b1ebcb\" data-enlighter-title=\"src\/lang\/ar.json\" data-enlighter-linenumbers=\"false\">{\n  \"app_title\": \"\u0627\u0644\u0646\u0627\u062e\u0628 \u0627\u0644\u0645\u062a\u0645\u0631\u062f\",\n  \"app_slogan\": \"\u0634\u062e\u0635\u064a\u0627\u062a\u0643 \u0627\u0644\u0645\u0641\u0636\u0644\u0629 \u0641\u064a \u062d\u0631\u0628 \u0627\u0644\u0646\u062c\u0648\u0645\"\n}\n<\/pre>\n<p>I\u2019m using English and Arabic as my two app languages here. Feel free to use any languages you want. I hear there are many.<\/p>\n<h4>Working with JSON in Svelte<\/h4>\n<p>At time of writing, if we want to work with JSON files in Svelte, we need to install the JSON Rollup plugin.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">$ npm install --save-dev @rollup\/plugin-json\n<\/pre>\n<p>We also need to configure the plugin via the <code>rollup.config.js<\/code> file.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"cf0c61af-05e4-418d-ad5a-c7f3d3cc14e2\" data-enlighter-title=\"rollup.config.js\">\/\/ ...\nimport json from \"@rollup\/plugin-json\";\n\/\/ ...\nexport default {\n  \/\/ ...\n  plugins: [\n    json(),\n    svelte({\n      \/\/ ...\n    }),\n    \/\/ ...\n};\n\/\/ ...\n<\/pre>\n<p>Now we can import our JSON translation files and give them to svelte-i18n\u2019s <code>addMessages()<\/code>. We also need to call the library\u2019s <code>init()<\/code> function to tell svelte-i18n which language our app should boot with.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"c4fd839f-8559-4daf-945f-616beb264e90\" data-enlighter-title=\"src\/App.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import { addMessages, init } from \"svelte-i18n\";\n  \/\/ ...\n  import en from \".\/lang\/en.json\";\n  import ar from \".\/lang\/ar.json\";\n  addMessages(\"en\", en);\n  addMessages(\"ar\", ar);\n  init({\n    initialLocale: \"en\",\n  });\n&lt;\/script&gt;\n&lt;Header \/&gt;\n  &lt;main role=\"main\"&gt;\n        &lt;CharacterList \/&gt;\n  &lt;\/main&gt;\n&lt;Footer \/&gt;\n<\/pre>\n<h3><span class=\"ez-toc-section\" id=\"basic-translation\"><\/span>Basic Translation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Believe it or not, we\u2019ve basically internationalized our app at this point. We can now pull in the <code>_<\/code> (underscore) <a href=\"https:\/\/svelte.dev\/docs#svelte_store\">Svelte store<\/a> from svelte-i18n to display translated messages instead of hard-coded ones.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"79e43adf-fa18-4a49-9635-686c3538bb8f\" data-enlighter-title=\"src\/components\/Layout\/Header.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import { _ } from \"svelte-i18n\";\n&lt;\/script&gt;\n&lt;header class=\"hero\"&gt;\n    &lt;div class=\"hero-body\"&gt;\n        &lt;div class=\"contiainer\"&gt;\n            &lt;h1 class=\"title\"&gt;{$_(\"app_title\")}&lt;\/h1&gt;\n            &lt;h2 class=\"subtitle\"&gt;{$_(\"app_slogan\")}&lt;\/h2&gt;\n        &lt;\/div&gt;\n    &lt;\/div&gt;\n&lt;\/header&gt;\n<\/pre>\n<p>We simply pass our translation message keys to the <code>$_()<\/code> store. The store will always use the messages from our active locale\/language. <code>$_()<\/code> is, like any other Svelte store, reactive, meaning that it will force our components to re-render if the active locale or translation messages change.<br \/>\nIf we run our app now, everything will look the same. However, if we switch the value of <code>initialLocale<\/code> in our <code>App.svelte<\/code> file to <code>\"ar\"<\/code>, our app\u2019s header will show our Arabic translations.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-11365 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-ar-header.png\" alt=\"Arabic header | Phrase\" width=\"562\" height=\"226\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-ar-header.png 562w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-ar-header-300x121.png 300w\" sizes=\"(max-width: 562px) 100vw, 562px\" \/><br \/>\n<em>Our header in Arabic. That was easy.<\/em><\/p>\n<h2><span class=\"ez-toc-section\" id=\"async-translation-file-loading\"><\/span>Async Translation File Loading<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Our current setup works great for smaller apps. If our translation files get significantly bigger, however, we might start taxing our users\u2019 bandwidth (and patience, since our app may well slow down). This is because we\u2019re currently bundling the translation files for <em>all<\/em> languages in our main app bundle.<br \/>\nWe can, instead, load our translation files when they\u2019re needed, asynchronously. svelte-i18n is supposed to have this functionality built-in. When I tried it, however, I hit a breaking bug. I <a href=\"https:\/\/github.com\/kaisermann\/svelte-i18n\/issues\/80\">logged the bug<\/a>, and hopefully it will get sorted out soon. When it does, we\u2019ll update this article to the built-in svelte-i18n async loading. For the time being, we can roll our own.<br \/>\nThe first thing we have to do is make our translation files available for download by moving them to the <code>public<\/code> directory.<\/p>\n<pre>src\/lang\/en.json \u2192 public\/lang\/en.json\nsrc\/lang\/ar.json \u2192 public\/lang\/ar.json<\/pre>\n<p>We can now write a wrapper around the svelte-i18n functions that downloads our files and configures svelte-i18n to use them.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"6a34308a-7b4f-4cdf-a8de-586c2c17e182\" data-enlighter-title=\"src\/services\/i18n.js\" data-enlighter-linenumbers=\"false\">import { get } from \"svelte\/store\";\nimport {\n  addMessages,\n  locale,\n  init,\n  dictionary,\n  _,\n} from \"svelte-i18n\";\nconst MESSAGE_FILE_URL_TEMPLATE = \"\/lang\/{locale}.json\";\nfunction setupI18n(options) {\n  const { withLocale: locale_ } = options;\n  \/\/ Initialize svelte-i18n\n  init({ initialLocale: locale_ });\n  \/\/ Don't re-download translation files\n  if (!hasLoadedLocale(locale_)) {\n    const messagesFileUrl =\n      MESSAGE_FILE_URL_TEMPLATE.replace(\n        \"{locale}\",\n        locale_,\n      );\n\t\/\/ Download translation file for given locale\/language\n    return loadJson(messagesFileUrl).then((messages) =&gt; {\n      \/\/ Configure svelte-i18n to use the locale\n      addMessages(locale_, messages);\n      locale.set(locale_);\n    });\n  }\n}\nfunction loadJson(url) {\n  return fetch(url).then((response) =&gt; response.json());\n}\nfunction hasLoadedLocale(locale) {\n  \/\/ If the svelte-i18n dictionary has an entry for the\n  \/\/ locale, then the locale has already been added\n  return get(dictionary)[locale];\n}\n\/\/ We expose the svelte-i18n _ store so that our app has\n\/\/ a single API for i18n\nexport { _, setupI18n };\n<\/pre>\n<p>Our module exposes a <code>setupI18n(options: Object)<\/code> function that accepts one option for the moment, <code>withLocale: string<\/code>. <code>withLocale<\/code> is just the language we want to set up by downloading its translation file and initializing svelte-i18n to use it.<br \/>\nWe can now update our <code>App.svelte<\/code> component to use our new module.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"50e312eb-26b1-4514-8f47-ad4e9828ac38\" data-enlighter-title=\"src\/App.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  \/\/ Remove all code that references svelte-i18n\n  \/\/ directly and use our wrapper library instead\n  import { setupI18n } from \".\/services\/i18n\";\n  \/\/ ...\n   setupI18n({ withLocale: \"ar\" });\n&lt;\/script&gt;\n&lt;!-- ... --&gt;\n<\/pre>\n<p>Our app will behave largely as it did before. However, our translation files are no longer bundled in the main app bundle and are download asynchronously as they\u2019re needed.<br \/>\nThere\u2019s a problem with the current state of our code. Our app may look fine on our development machine, but no translated messages will render for our users until the active locale\u2019s translation file is downloaded.<\/p>\n<blockquote><p>\ud83d\uddd2 <em>Note \u00bb<\/em> svelte-i18n is kind enough to warn us about scenarios like this by logging helpful messages in the browser console.<\/p><\/blockquote>\n<h3><span class=\"ez-toc-section\" id=\"rendering-after-the-locale-is-loaded\"><\/span>Rendering After the Locale Is Loaded<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Let\u2019s create a bit of a global state called <code>isLocaleLoaded<\/code> that we can use to check if the last translation file we asked for has finished loading. We can use this state to display loading messages to the user where appropriate.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"0f79bfaa-f722-4003-8b04-edbf5680d810\" data-enlighter-title=\"\/src\/services\/i18n.js\" data-enlighter-linenumbers=\"false\">import { get, derived, writable } from \"svelte\/store\";\n\/\/ ...\nlet _activeLocale;\n\/\/ Internal store for tracking network\n\/\/ loading state\nconst isDownloading = writable(false);\nfunction setupI18n(options) {\n  \/\/ ...\n  if (!hasLoadedLocale(locale_)) {\n    isDownloading.set(true);\n    \/\/ ...\n    return loadJson(messagesFileUrl).then((messages) =&gt; {\n      _activeLocale = locale_;\n      addMessages(locale_, messages);\n      locale.set(locale_);\n      isDownloading.set(false);\n    });\n  }\n}\nconst isLocaleLoaded = derived(\n  [isDownloading, dictionary],\n  ([$isDownloading, $dictionary]) =&gt;\n    !$isDownloading &amp;&amp;\n    $dictionary[_activeLocale] &amp;&amp;\n    Object.keys($dictionary[_activeLocale]).length &gt; 0,\n);\n\/\/ ...\nexport { _, setupI18n, isLocaleLoaded };\n<\/pre>\n<p><code>isLocaleLoaded<\/code> is a <a href=\"https:\/\/svelte.dev\/docs#derived\">derived Svelte store<\/a>: it listens to changes in two other stores, our own <code>isDownloading<\/code> and svelte-i18n\u2019s messages <code>dictionary<\/code>. When our active translation file has finished downloading, <em>and<\/em> svelte-i18n has loaded its translations into its messages dictionary, <code>isLocaleLoaded === true<\/code>.<br \/>\nLike any other Svelte store, we can use <code>isLocaleLoaded<\/code> with the <code>$<\/code> prefix in our components. When we do so, Svelte will subscribe our component to the store and re-render it when the store updates. Svelte will also unsubscribe from the store when our component is destroyed.<br \/>\nOur new store, <code>$isLocaleLoaded<\/code>, is exactly what we need to solve our loading state issue.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"4725bbe6-3baf-41f6-9e1e-830b312ae1d1\" data-enlighter-title=\"\/src\/App.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import { setupI18n, isLocaleLoaded } from \".\/services\/i18n\";\n  \/\/ ...\n  setupI18n({ withLocale: \"ar\" });\n&lt;\/script&gt;\n{#if $isLocaleLoaded}\n  &lt;Header \/&gt;\n  &lt;main role=\"main\"&gt;\n    &lt;CharacterList \/&gt;\n  &lt;\/main&gt;\n  &lt;Footer \/&gt;\n{:else}\n  &lt;p&gt;Loading...&lt;\/p&gt;\n{\/if}\n&lt;!-- ... --&gt;\n<\/pre>\n<p>This UX is a bit better, don\u2019t you agree?<\/p>\n<h2><span class=\"ez-toc-section\" id=\"getting-and-setting-the-active-locale\"><\/span>Getting and Setting the Active Locale<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Retrieving, and manually setting, the active locale in svelte-i18n can be achieved by interacting with the <code>locale<\/code> Svelte store.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import { locale } from \"svelte-i18n\";\n  \/\/ React to locale changes\n  locale.subscribe((newLocale) =&gt; {\n    \/\/ Do something with newLocale, or not.\n  });\n  \/\/ Set the active locale to Canadian French\n  locale.set(\"fr-CA\");\n&lt;\/script&gt;\n<\/pre>\n<p>Of course, we can use the <code>$locale<\/code> syntax in our components&#8217; markup to get easy reactivity.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">&lt;!-- Class will be .button.button-fr-CA when\n     our locale is Canadian French --&gt;\n&lt;div class=\"button button-{$locale}\"&gt;\n  {$_(\"subscribe\")}\n&lt;\/div&gt;\n<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"a-simple-language-switcher\"><\/span>A Simple Language Switcher<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Let\u2019s do something useful with this getting and setting of the active locale. We often want to give the user a way to switch the active locale manually. We can add a <code>LocaleSwitcher<\/code> component to our app to achieve just that.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"417fd00f-8c4d-4530-b4b3-ec2dbba66034\" data-enlighter-title=\"\/src\/components\/UI\/LocaleSelector.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import { createEventDispatcher } from \"svelte\";\n  export let value;\n  const dispatch = createEventDispatcher();\n  function handleLocaleChange(event) {\n    event.preventDefault();\n    dispatch(\"locale-changed\", event.target.value);\n  }\n&lt;\/script&gt;\n&lt;style&gt;\n  \/* ... *\/\n&lt;\/style&gt;\n&lt;div class=\"locale-selector\"&gt;\n  &lt;div class=\"select\"&gt;\n    &lt;select value={value} on:change={handleLocaleChange}&gt;\n      &lt;option value=\"en\"&gt;English&lt;\/option&gt;\n      &lt;option value=\"ar\"&gt;\u0639\u0631\u0628\u064a&lt;\/option&gt;\n    &lt;\/select&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;\n<\/pre>\n<p><code>LocaleSelector<\/code> doesn\u2019t have any internal state. Instead, it exposes a <code>value<\/code> prop that corresponds to the active item in its internal <code>&lt;select&gt;<\/code> dropdown. <code>LocaleSelector<\/code> also fires a custom <code>locale-changed<\/code> event whenever the user selects a new locale via the dropdown.<br \/>\nWe can now use <code>LocaleSelector<\/code> to update our active locale in <code>App.svelte<\/code>.<br \/>\nFirst, let\u2019s expose svelte-i18n\u2019s <code>locale<\/code> store through our own wrapper library.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"24bd868d-11d8-4422-8f0b-cebdabae3a5d\" data-enlighter-title=\"\/src\/services\/i18n.js\" data-enlighter-linenumbers=\"false\">import {\n  addMessages,\n  locale,\n  init,\n  dictionary,\n  _,\n} from \"svelte-i18n\";\n\/\/ ...\nexport { _, setupI18n, isLocaleLoaded, locale };\n<\/pre>\n<p>This keeps our i18n API consistent. Let\u2019s import <code>locale<\/code> and wire it up to our new <code>LocaleSelector<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"3ba97d56-f3e5-4ad3-b728-d1a8688a7576\" data-enlighter-title=\"\/src\/App.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import {\n    setupI18n,\n    isLocaleLoaded,\n    locale,\n  } from \".\/services\/i18n\";\n  \/\/ ...\n  import LocaleSelector from\n    \".\/components\/UI\/LocaleSelector.svelte\";\n  \/\/ ...\n&lt;\/script&gt;\n{#if $isLocaleLoaded}\n  &lt;Header \/&gt;\n  &lt;LocaleSelector\n    value={$locale}\n    on:locale-changed={e =&gt;\n      setupI18n({ withLocale: e.detail }) }\n  \/&gt;\n  &lt;!-- ... --&gt;\n{\/if}\n&lt;!-- ... --&gt;\n<\/pre>\n<p>Et voil\u00e0!<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-11366 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-locale-switcher-1024x478.png\" alt=\"Language switcher | Phrase\" width=\"1024\" height=\"478\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-locale-switcher-1024x478.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-locale-switcher-300x140.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-locale-switcher-768x359.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-locale-switcher-1536x717.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-locale-switcher.png 1853w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><br \/>\n<em>Our users can now select their own language<\/em><\/p>\n<h2><span class=\"ez-toc-section\" id=\"automatically-detecting-the-users-locale\"><\/span>Automatically Detecting the User\u2019s Locale<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>A language dropdown is often necessary, but we sometimes want to guess the user\u2019s preferred language from their browser settings or some other means. svelte-i18n has a few built-in ways to do this. Let\u2019s go with one of the most common: detecting through the browser.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Check out the <a href=\"https:\/\/github.com\/kaisermann\/svelte-i18n\/blob\/main\/docs\/Methods.md\">svelte-i18n documentation<\/a> for all the locale auto-detection methods the library provides.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"supported-locales\"><\/span>Supported Locales<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>When we auto-detect the user\u2019s locale, we\u2019ll have to check whether we provide translations for that locale. If we don\u2019t, we\u2019ll have to show translations in a locale that we do support. Let\u2019s configure this in a single source of truth.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"43d2b8ad-40cb-4291-a44d-74922c982969\" data-enlighter-title=\"src\/config\/l10n.js\" data-enlighter-linenumbers=\"false\">\/\/ Locales our app supports\nconst locales = {\n  en: \"English\",\n  ar: \"\u0639\u0631\u0628\u064a\",\n};\n\/\/ Locale to show when we don't support the\n\/\/ requested locale\nconst fallbackLocale = \"en\";\nexport { locales, fallbackLocale };\n<\/pre>\n<h3><span class=\"ez-toc-section\" id=\"detection-with-fallback\"><\/span>Detection with Fallback<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Now let\u2019s update our custom i18n wrapper library so that it will auto-detect the user\u2019s locale when we don\u2019t explicitly give it one.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"23ca1a07-f7ff-4292-8a70-4b516653588d\" data-enlighter-title=\"src\/services\/i18n.js\" data-enlighter-linenumbers=\"false\">\/\/ ...\n\/\/ options object is now an optional param\nfunction setupI18n(options = {}) {\n  \/\/ If we're given an explicit locale, we use\n  \/\/ it. Otherwise, we attempt to auto-detect\n  \/\/ the user's locale.\n  const locale_ = supported(\n    options.withLocale ||\n      language(getLocaleFromNavigator()),\n  );\n  init({ initialLocale: locale_ });\n  \/\/ ...\n}\n\/\/ ...\n\/\/ Extract the \"en\" bit from fully qualified\n\/\/ locales, like \"en-US\"\nfunction language(locale) {\n  return locale.replace(\"_\", \"-\").split(\"-\")[0];\n}\n\/\/ Check to see if the given locale is supported\n\/\/ by our app. If it isn't, return our app's\n\/\/ configured fallback locale.\nfunction supported(locale) {\n  if (Object.keys(locales).includes(locale)) {\n    return locale;\n  } else {\n    return fallbackLocale;\n  }\n}\n\/\/ ...\n<\/pre>\n<p><code>setupI18n<\/code> can now be called without any parameters, which triggers auto-detection. We\u2019re using svelte-i18n\u2019s <code>getLocaleFromNavigator()<\/code> strategy, which retrieves the highest priority locale the user has configured in her browser options.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-11377 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-firefox-lang-options-1024x531.png\" alt=\"Firefox language settings | Phrase\" width=\"1024\" height=\"531\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-firefox-lang-options-1024x531.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-firefox-lang-options-300x155.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-firefox-lang-options-768x398.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-firefox-lang-options.png 1320w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><br \/>\n<em>Firefox\u2019s language options, also available in all modern browsers.<\/em><\/p>\n<p>Let&#8217;s use auto-detection in our <code>App<\/code> component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"788941d4-ac6d-491a-a0e8-baa1fb1383bc\" data-enlighter-title=\"\/src\/App.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n\/\/ ...\n   setupI18n();\n&lt;\/script&gt;\n{#if $isLocaleLoaded}\n  &lt;!-- ... --&gt;\n  &lt;!-- This stays the same --&gt;\n  &lt;LocaleSelector\n    value={$locale}\n    on:locale-changed={e =&gt;\n      setupI18n({ withLocale: e.detail }) }\n  \/&gt;\n{:else}\n  &lt;!-- ... --&gt;\n{\/if}\n&lt;!-- ... --&gt;\n<\/pre>\n<p>We call <code>setupI18n()<\/code> with no params when initializing our <code>App<\/code>, causing svelte-i18n to auto-detect the user\u2019s language on first load. If the user manually selects a language through our <code>LocaleSelector<\/code>, we call <code>setupI18n({ withLocale: \"fr\" })<\/code>, as we did before, to explicitly set the active locale.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"updating-our-language-selector\"><\/span>Updating our Language Selector<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Let\u2019s refactor our <code>LocaleSelector<\/code> to <code>#each<\/code> over our configured supported locales, instead of using hard-coded magic values.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"a6f16b45-9f73-4ab9-80c4-d1a168407df1\" data-enlighter-title=\"src\/components\/UI\/LocaleSelector.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n import { locales } from \"..\/..\/config\/l10n\";\n \/\/ ...\n&lt;\/script&gt;\n&lt;!-- ... --&gt;\n&lt;div class=\"locale-selector\"&gt;\n  &lt;div class=\"select\"&gt;\n    &lt;select value={value} on:change={handleLocaleChange}&gt;\n      {#each Object.keys(locales) as locale}\n        &lt;option value={locale}&gt;{locales[locale]}&lt;\/option&gt;\n      {\/each}\n    &lt;\/select&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;\n<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"layout-direction-left-to-right-and-right-to-left\"><\/span>Layout Direction: Left-to-Right and Right-to-Left<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Depending on the direction of the active locale, left-to-right (LTR) or right-to-left (RTL), we often need to change the layout of our website. So it\u2019s a good idea to be able to query for the active locale\u2019s direction. We can easily add that functionality with another derived store.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"5f393807-0319-44e3-98fb-0d3b00670f59\" data-enlighter-title=\"src\/services\/i18n.js\" data-enlighter-linenumbers=\"false\">import { get, derived, writable } from \"svelte\/store\";\n\/\/ ...\nconst dir = derived(locale, ($locale) =&gt;\n  $locale === \"ar\" ? \"rtl\" : \"ltr\",\n);\n\/\/ ...\nexport { _, setupI18n, isLocaleLoaded, locale, dir };\n<\/pre>\n<h3><span class=\"ez-toc-section\" id=\"localizing-the-document-direction\"><\/span>Localizing the Document Direction<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>We can now react to <code>$dir<\/code> in our app and set our HTML <code>document<\/code>\u2019s direction accordingly.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"2ad6c307-80b9-4dc8-9286-0e1f11439949\" data-enlighter-title=\"src\/App.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import {\n    setupI18n,\n    isLocaleLoaded,\n    locale,\n    dir,\n  } from \".\/services\/i18n\";\n  \/\/ ...\n  setupI18n();\n  $: if (document.dir !== $dir) {\n    document.dir = $dir;\n  }\n&lt;\/script&gt;\n&lt;!-- ... --&gt;\n<\/pre>\n<p>\ud83d\udcd6 <em>Go deeper \u00bb<\/em> The <code>$:<\/code> syntax declares a Svelte reactive statement, which you can read more about in the official tutorial.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"loading-css-based-on-direction\"><\/span>Loading CSS Based on Direction<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Our styles often vary significantly between LTR and RTL layouts. In the case of our demo app, we\u2019re using the Bulma CSS framework. Bulma provides a default LTR version, and a special RTL version as well.<br \/>\nWe have the Bulma CSS <code>&lt;link&gt;<\/code>ed as the first stylesheet in our document\u2019s <code>&lt;head&gt;<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"html\" data-enlighter-group=\"0a8f27d4-e99e-4a56-b75f-7c2674e6ef7b\" data-enlighter-title=\"public\/index.html\" data-enlighter-linenumbers=\"false\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n  &lt;head&gt;\n    &lt;!-- ... --&gt;\n    &lt;title&gt;Svelte app&lt;\/title&gt;\n    &lt;link\n      rel=\"stylesheet\"\n      href=\"https:\/\/cdn.jsdelivr.net\/npm\/bulma@0.9.0\/css\/bulma.min.css\"\n    \/&gt;\n    &lt;!- ... --&gt;\n  &lt;\/head&gt;\n  &lt;body&gt;&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>However, we want to be able to conditionally choose between <code>bulma.min.css<\/code>, and <code>bulma-rtl.min.css<\/code>, depending on the active locale\u2019s direction. To do this, let\u2019s set the file URL in the relevant <code>&lt;link&gt;<\/code> tag through JavaScript, instead of hard-coding it.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"html\" data-enlighter-group=\"1551317b-6dba-4bcd-958c-f6ab894d7f9a\" data-enlighter-title=\"public\/index.html\" data-enlighter-linenumbers=\"false\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n  &lt;head&gt;\n    &lt;!-- ... --&gt;\n    &lt;title&gt;Svelte app&lt;\/title&gt;\n    &lt;!-- We give our link tag an id so we can\n         reference it in our JavaScript --&gt;\n    &lt;link id=\"bulmaCssLink\" rel=\"stylesheet\" \/&gt;\n    &lt;!- ... --&gt;\n  &lt;\/head&gt;\n  &lt;body&gt;&lt;\/body&gt;\n&lt;\/html&gt;\n<\/pre>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> svelte-i18n automatically overrides the <code>html[lang]<\/code> attribute for us to match its active <code>$locale<\/code>.<\/p>\n<p>Now let\u2019s react to layout direction changes\u00a0in <code>App.svelte<\/code>, and update the <code>&lt;link&gt;<\/code> URL to match the direction.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"a08def65-3e04-4b09-a2cb-5a21acee06a7\" data-enlighter-title=\"src\/App.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import {\n    setupI18n,\n    isLocaleLoaded,\n    locale,\n    dir,\n  } from \".\/services\/i18n\";\n  import { bulmaUrl } from \".\/services\/css\";\n  \/\/ ...\n  setupI18n();\n  $: if (document.dir !== $dir) {\n    document.dir = $dir;\n    document.getElementById(\"bulmaCssLink\").href =\n      bulmaUrl($dir);\n  }\n&lt;\/script&gt;\n&lt;!-- ... --&gt;\n<\/pre>\n<p>We\u2019re using a helper <code>bumlaUrl()<\/code> function to get the CSS file that matches the current direction.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"90f917d3-ea70-477d-b00f-fb5fafcb4978\" data-enlighter-title=\"src\/services\/css.js\" data-enlighter-linenumbers=\"false\">function bulmaUrl(dir = \"ltr\") {\n  const suffix = dir == \"rtl\" ? \"-rtl\" : \"\";\n  return (\n    \"https:\/\/cdn.jsdelivr.net\/npm\/bulma@0.9.0\/css\/\" +\n    `bulma${suffix}.min.css`\n  );\n}\nexport { bulmaUrl };\n<\/pre>\n<p>Now when we switch languages, our app will react and reflow its layout to match the current language\u2019s direction.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-11367 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-rtl-1024x495.png\" alt=\"Arabic right-to-left formatting demo app | Phrase\" width=\"1024\" height=\"495\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-rtl-1024x495.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-rtl-300x145.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-rtl-768x372.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-rtl.png 1170w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><br \/>\n<em>Arabic and several other languages are written in right-to-left<\/em><\/p>\n<h2><span class=\"ez-toc-section\" id=\"localizing-the-document-title\"><\/span>Localizing the Document Title<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Our HTML document <code>&lt;title&gt;<\/code> often needs to be localized. We can do that easily via another reactive statement.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"28ad4833-c1f7-4d07-8110-e203d53575e8\" data-enlighter-title=\"src\/App.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import {\n    setupI18n,\n    isLocaleLoaded,\n    locale,\n    dir,\n    _,\n  } from \".\/services\/i18n\";\n  \/\/ ...\n  $: if ($isLocaleLoaded) {\n    document.title = $_(\"app_title\");\n  }\n&lt;\/script&gt;\n&lt;!-- ... --&gt;\n<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"translation-messages\"><\/span>Translation Messages<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We\u2019ve scratched the surface of what\u2019s possible with svelte-i18n\u2019s translation messages. However, there\u2019s a lot more that we can do them than we\u2019ve seen so far.<br \/>\nIn fact, svelte-i18n is a light Svelte wrapper around the <a href=\"https:\/\/formatjs.github.io\/\">FormatJS<\/a> library, so it provides <a href=\"https:\/\/phrase.com\/blog\/posts\/guide-to-the-icu-message-format\/\">ICU message<\/a> support.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Learn more about how svelte-i18n handles message formatting in the <a href=\"https:\/\/github.com\/kaisermann\/svelte-i18n\/blob\/main\/docs\/Formatting.md\">official formatting doc<\/a>. And if you want an in-depth guide to the ICU message format, check out our article about it, <a href=\"https:\/\/phrase.com\/blog\/posts\/guide-to-the-icu-message-format\/\">The Missing Guide to the ICU Message Format<\/a>. Note that it\u2019s not necessary to understand the ICU format to use svelte-i18n. The format is quite intuitive.<\/p>\n<p>Let\u2019s take a look at our <code>Header<\/code> component again.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"e50c1f0a-fa4c-40f8-badf-20f518fb8a3a\" data-enlighter-title=\"src\/components\/Layout\/Header.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import { _ } from \"svelte-i18n\";\n&lt;\/script&gt;\n&lt;header class=\"hero\"&gt;\n    &lt;div class=\"hero-body\"&gt;\n        &lt;div class=\"container\"&gt;\n            &lt;h1 class=\"title\"&gt;{$_(\"app_title\")}&lt;\/h1&gt;\n            &lt;h2 class=\"subtitle\"&gt;{$_(\"app_slogan\")}&lt;\/h2&gt;\n        &lt;\/div&gt;\n    &lt;\/div&gt;\n&lt;\/header&gt;\n<\/pre>\n<p>You\u2019ll remember that our <code>$_()<\/code> calls are keying into our translation message dictionaries.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"7fe90591-7d56-4468-9303-80d7d4453c5e\" data-enlighter-title=\"src\/lang\/en.json\" data-enlighter-linenumbers=\"false\">{\n  \"app_title\": \"Rebel Voter\",\n  \"app_slogan\": \"Your favorite Star Wars characters\"\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"1bb96a24-fc6c-42ac-8f47-7862adc0615e\" data-enlighter-title=\"src\/lang\/ar.json\" data-enlighter-linenumbers=\"false\">{\n  \"app_title\": \"\u0627\u0644\u0646\u0627\u062e\u0628 \u0627\u0644\u0645\u062a\u0645\u0631\u062f\",\n  \"app_slogan\": \"\u0634\u062e\u0635\u064a\u0627\u062a\u0643 \u0627\u0644\u0645\u0641\u0636\u0644\u0629 \u0641\u064a \u062d\u0631\u0628 \u0627\u0644\u0646\u062c\u0648\u0645\"\n}\n<\/pre>\n<h3><span class=\"ez-toc-section\" id=\"nesting-translation-messages\"><\/span>Nesting Translation Messages<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>We can also group the messages in our translation dictionaries.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">\/\/ In our dictionary, we nest messages under a namespace\n{\n  \"app\": {\n    \"title\": \"Rebel Voter\",\n    \"slogan\": \"Your favorite Star Wars characters\"\n  }\n}\n\/\/ In our component, we use dot notations to refine into\n\/\/ the dictionary\n&lt;h1 class=\"title\"&gt;{$_(\"app.title\")}&lt;\/h1&gt;\n&lt;h2 class=\"subtitle\"&gt;{$_(\"app.slogan\")}&lt;\/h2&gt;\n<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"interpolation\"><\/span>Interpolation<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Oftentimes, we want to inject a dynamic value into a translation message. We can do that with <code>{variable}<\/code> syntax.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">\/\/ In our dictionary\n{\n  \"hello_user\": \"Hello, {name}!\"\n}\n\/\/ In our component\n&lt;p&gt;{$_(\"hello_user\", {values: {name: \"Adam\"}})&lt;\/p&gt;\n<\/pre>\n<p>The second parameter to <code>$_()<\/code> is an options object, which can contain a <code>values<\/code> object itself. <code>values<\/code> holds name\/value pairs where the values will replace their respective named placeholders at runtime.<\/p>\n<p>\u270b\ud83c\udffd <em>Heads Up \u00bb<\/em> If you declare an interpolated value, like <code>{name}<\/code>, in a translation message, you <em>must<\/em> provide a corresponding name\/value pair when you call <code>$_()<\/code>. Otherwise, svelte-i18n will throw an error and your app will crash.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Check out the <a href=\"https:\/\/github.com\/kaisermann\/svelte-i18n\/blob\/main\/docs\/Formatting.md#format-or-_-or-t\">official svelte-i18n docs<\/a> for all the options that <code>$_()<\/code> provides.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"using-html-in-translation-messages\"><\/span>Using HTML in Translation Messages<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Sometimes we want to have HTML <em>within<\/em> our translation messages. svelte-i18n doesn\u2019t accommodate this out of the box. However, we can use interpolation and the unsafe <code>@html<\/code> Svelte directive to work around this.<br \/>\nIn our <code>Footer<\/code>, for example, we may want to have links as part of our translation message. We can add interpolated values, like <code>{phraseUrl}<\/code>, to inject the URLs at runtime, while keeping the link HTML in the message itself.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"18716847-4729-4111-91f6-eb4b88de353c\" data-enlighter-title=\"public\/lang\/en.json\" data-enlighter-linenumbers=\"false\">{\n  \/\/ ...\n  \"footer\": \"Companion to a &lt;a href=\\\"{phraseUrl}\\\"&gt;Phrase blog&lt;\/a&gt; article. Made with &lt;a href=\\\"{svelteUrl}\\\"&gt;Svelte&lt;\/a&gt; &amp;amp; &lt;a href=\\\"{bulmaUrl}\\\"&gt;Bulma&lt;\/a&gt;.\"\n}<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"a04c6734-7753-4faa-83a7-3c793bd52dad\" data-enlighter-title=\"public\/lang\/ar.json\" data-enlighter-linenumbers=\"false\">{\n  \/\/ ...\n  \"footer\": \"\u0645\u0644\u062d\u0642 \u0644\u0645\u0642\u0627\u0644 &lt;a href=\\\"{phraseUrl}\\\"&gt;\u0645\u062f\u0648\u0646\u0629 Phrase&lt;\/a&gt;. \u0635\u0646\u0639 \u0628\u0648\u0627\u0633\u0637\u0629 &lt;a href=\\\"{svelteUrl}\\\"&gt;Svelte&lt;\/a&gt; \u0648 &lt;a href=\\\"{bulmaUrl}\\\"&gt;Bulma&lt;\/a&gt;.\"\n}\n<\/pre>\n<p>In our <code>Footer<\/code> component, we can then assign the URLs as normal <code>values<\/code> that we pass to <code>$_()<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"376cc6eb-64d4-4ee8-8af5-3bd4e24b3fae\" data-enlighter-title=\"src\/components\/Layout\/Footer.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import { _ } from \"..\/..\/services\/i18n\";\n&lt;\/script&gt;\n&lt;!-- ... --&gt;\n&lt;footer class=\"footer\"&gt;\n  &lt;div class=\"content has-text-centered\"&gt;\n    &lt;p&gt;\n      {@html $_(\"footer\", { values: {\n        phraseUrl: \"https:\/\/phrase.com\/blog\",\n        svelteUrl: \"https:\/\/svelte.dev\/\",\n        bulmaUrl: \"https:\/\/bulma.io\/s\" }})}\n    &lt;\/p&gt;\n  &lt;\/div&gt;\n&lt;\/footer&gt;\n<\/pre>\n<p>We\u2019re using the special Svelte <code>@html<\/code> directive here. Normally, Svelte will escape HTML characters when we output them using <code>{}<\/code>. <code>@html<\/code> tells Svelte to skip the escaping step, and to just output HTML characters literally. This is what we want here, since we have <code>&lt;a&gt;<\/code> tags in our translation message.<\/p>\n<p>\u270b\ud83c\udffd <em>Heads up \u00bb<\/em> Be careful with <code>@html<\/code>, since <a href=\"https:\/\/svelte.dev\/docs#html\">Svelte will not sanitize output for XSS attacks <\/a>when you use it.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"using-global-css-to-style-in-message-elements\"><\/span>Using Global CSS to Style In-Message Elements<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>In the code above, when we moved our <code>&lt;a&gt;<\/code> tags inside our translation message, we actually introduced a problem\u2014we lost our custom CSS styles for our <code>&lt;a&gt;<\/code> tags.<br \/>\nThis is because Svelte normally scopes a <code>&lt;style&gt;<\/code>\u2019s selectors so that they don\u2019t leak out of their associated component. To illustrate this, let\u2019s take a look at our <code>Footer<\/code> component before we localized it.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"59ce36d3-f49b-486c-94e7-fbc6c0c08919\" data-enlighter-title=\"src\/components\/Layout\/Footer.svelte\" data-enlighter-linenumbers=\"false\">&lt;style&gt;\n  .footer {\n    margin-top: 2rem;\n    background-color: #4a4a4a;\n    color: #f7f7f7;\n  }\n  a, a:active, a:hover, a:visited {\n    color: #65b6e3;\n  }\n&lt;\/style&gt;\n&lt;footer class=\"footer\"&gt;\n  &lt;div class=\"content has-text-centered\"&gt;\n    &lt;p&gt;\n      Companion to a\n      &lt;a href=\"https:\/\/phrase.com\/blog\"&gt;Phrase blog\n      &lt;\/a&gt; article. Made with\n      &lt;a href=\"https:\/\/svelte.dev\/\"&gt;Svelte&lt;\/a&gt; &amp;amp;\n      &lt;a href=\"https:\/\/bulma.io\/\"&gt;Bulma&lt;\/a&gt;.\n    &lt;\/p&gt;\n  &lt;\/div&gt;\n&lt;\/footer&gt;\n<\/pre>\n<p>The preceding code is actually compiled by Svelte to look something like this at runtime:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">&lt;!-- In our app's CSS bundle --&gt;\n&lt;style&gt;\n.footer.svelte-ub7hie {\n  margin-top: 2rem;\n  background-color: #4a4a4a;\n  color: #f7f7f7;\n}\na.svelte-ub7hie,\na.svelte-ub7hie:active,\na.svelte-ub7hie:hover,\na.svelte-ub7hie:visited {\n    color: #65b6e3;\n}\n&lt;\/style&gt;\n&lt;!-- Our rendered component --&gt;\n&lt;footer class=\"footer svelte-ub7hie\"&gt;\n  &lt;div class=\"content has-text-centered\"&gt;\n    &lt;p&gt;Companion to a\n      &lt;a href=\"https:\/\/phrase.com\/blog\" class=\"svelte-ub7hie\"&gt;Phrase blog&lt;\/a&gt; article. Made with &lt;a href=\"https:\/\/svelte.dev\/\" class=\"svelte-ub7hie\"&gt;Svelte&lt;\/a&gt; &amp;amp; &lt;a href=\"https:\/\/bulma.io\/\" class=\"svelte-ub7hie\"&gt;Bulma&lt;\/a&gt;\n    &lt;\/p&gt;\n  &lt;\/div&gt;\n&lt;\/footer&gt;\n<\/pre>\n<p>Notice that Svelte gave our component\u2019s styled HTML element\u2019s a special hash, <code>ub7hie<\/code>. This scopes the <code>&lt;style&gt;<\/code> selectors we provided to our component. Most of the time this is great. However, if we move HTML out of our component and inject it dynamically at runtime, Svelte doesn\u2019t add this hash to the injected HTML. So the <code>a.svelte-ub7hie<\/code> CSS rules won\u2019t target the HTML <em>inside<\/em> our translation messages.<br \/>\nTo deal with situations like this, Svelte provides <code>:global<\/code> syntax that we can use to remove the normally-injected hash from our CSS selectors.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"ed6d24fc-fa43-44b2-a1f7-2344e9cf7a74\" data-enlighter-title=\"src\/components\/Layout\/Footer.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import { _ } from \"..\/..\/services\/i18n\";\n&lt;\/script&gt;\n&lt;style&gt;\n  .footer {\n    margin-top: 2rem;\n    background-color: #4a4a4a;\n    color: #f7f7f7;\n  }\n  .footer :global(a),\n  .footer :global(a:active),\n  .footer :global(a:hover),\n  .footer :global(a:visited) {\n    color: #65b6e3;\n  }\n&lt;\/style&gt;\n&lt;footer class=\"footer\"&gt;\n  &lt;div class=\"content has-text-centered\"&gt;\n    &lt;p&gt;\n      {@html $_(\"footer\", { values: {\n        phraseUrl: \"https:\/\/phrase.com\/blog\",\n        svelteUrl: \"https:\/\/svelte.dev\/\",\n        bulmaUrl: \"https:\/\/bulma.io\/s\" }})}\n    &lt;\/p&gt;\n  &lt;\/div&gt;\n&lt;\/footer&gt;\n<\/pre>\n<p>With this in place, the scoping hash won\u2019t be added to the style selectors. This means that our CSS will render as just <code>.footer a<\/code>. Our footer links will now match the selectors, and render exactly as they did before we localized the component.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-11368 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-footers-1024x258.png\" alt=\"Translated demo app footer | Phrase\" width=\"1024\" height=\"258\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-footers-1024x258.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-footers-300x76.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-footers-768x193.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-footers.png 1048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><br \/>\n<em>Footers are often overlooked, and sometimes overcooked<\/em><\/p>\n<h2><span class=\"ez-toc-section\" id=\"plurals\"><\/span>Plurals<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Different languages have different plural rules, so it\u2019s nice when an i18n library is aware of these language specifics. Thankfully, svelte-i18n, being built in top of FormatJS, is very much aware of these plural rules.<br \/>\nLet\u2019s use this capability to localize our <code>VotingButton<\/code> component. We\u2019ll add a label that shows the total number of votes, upvotes + downvotes.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"968cd8e7-048a-4a3e-87ab-0d89baa42b3f\" data-enlighter-title=\"src\/components\/UI\/VotingButton.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  \/\/ ...\n  export let character;\n  \/\/ ...\n  let {\n    upVoteCount,\n    downVoteCount,\n  } = character;\n  $: totalVoteCount = upVoteCount + downVoteCount;\n&lt;\/script&gt;\n&lt;!-- ... --&gt;\n&lt;div class=\"box\"&gt;\n  &lt;div class=\"columns is-mobile\"&gt;\n    &lt;!-- ... --&gt;\n    &lt;div class=\"column\"&gt;\n      &lt;!-- ... --&gt;\n      &lt;p class=\"is-size-7\"&gt;{totalVoteCount} votes&lt;\/p&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;\n<\/pre>\n<p>The total votes text is currently hard-coded. Let\u2019s localize it. To add plurals to a translation message in svelte-i18n, we use the ICU message format.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"8b5c2f6f-bf2e-4e51-aae0-e56d550a7569\" data-enlighter-title=\"public\/lang\/en.json\" data-enlighter-linenumbers=\"false\">{\n  \/\/ ...\n  \"total_votes\": \"{n, plural, =0 {No votes yet} one {# vote} other {# votes}}\",\n  \/\/ ...\n}<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-group=\"50aabbcb-ba87-4972-b8fa-07424a02e354\" data-enlighter-title=\"public\/lang\/ar.json\" data-enlighter-linenumbers=\"false\">{\n  \/\/ ...\n  \"total_votes\": \"{n, plural, =0 {\u0644\u0627 \u062a\u0648\u062c\u062f \u0623\u0635\u0648\u0627\u062a \u0628\u0639\u062f} one {\u0635\u0648\u062a #} two {\u0635\u0648\u062a\u0627\u0646} few {# \u0623\u0635\u0648\u0627\u062a} other {# \u0635\u0648\u062a}}\",\n  \/\/ ...\n}\n<\/pre>\n<p>Wrapped in <code>{}<\/code>, a plural expression declares the count variable <code>n<\/code>. We can reference <code>n<\/code> within our messages using the special <code>#<\/code> character.<br \/>\nWithin the expression, we can have as many rule\/message pairs as we want. There are named rules that are shared among languages. English, for example, has two named plural rules: <em>one<\/em> and <em>other<\/em>. Arabic has six plural rules. We don\u2019t have to use all the plural rules for a language, but the <em>other<\/em> rule is always required.<br \/>\nIf we want to target a specific count, like 13, we can use the <code>=13<\/code> syntax in our plural expression. We\u2019ve done this with <code>=0<\/code> rule above. (We could have used the named <code>zero<\/code> rule in this case as well.)<br \/>\nNow that we\u2019ve defined our plural messages, we can use them in our component.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"6bc6ef43-7f91-4fe8-8dfe-cb6c2fb26b9f\" data-enlighter-title=\"src\/components\/Characters\/Character.svelte\" data-enlighter-linenumbers=\"false\">&lt;!-- ... --&gt;\n&lt;div class=\"box\"&gt;\n  &lt;div class=\"columns is-mobile\"&gt;\n    &lt;!-- ... --&gt;\n    &lt;div class=\"column\"&gt;\n      &lt;!-- ... --&gt;\n      &lt;p class=\"is-size-7\"&gt;\n        {$_(\"total_votes\", {values: {n: totalVoteCount}})}\n      &lt;\/p&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;\n<\/pre>\n<p>This will cause svelte-i18n to output the matching plural message for our active locale at runtime. The <code>n<\/code> value is used for selection, and its value swaps in for the <code>#<\/code> character in our messages.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-11369 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-plurals.png\" alt=\"locale-aware plurals | Phrase\" width=\"496\" height=\"261\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-plurals.png 496w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-plurals-300x158.png 300w\" sizes=\"(max-width: 496px) 100vw, 496px\" \/><br \/>\n<em>With very little work, we have locale-aware plurals<\/em><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Learn more about ICU Message plural rules in <a href=\"https:\/\/phrase.com\/blog\/posts\/guide-to-the-icu-message-format\/#Plurals\">The Missing Guide to the ICU Message Format<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"date-formatting\"><\/span>Date Formatting<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In our <code>Character<\/code> component, we\u2019re currently displaying the release dates of Star Wars movies exactly like we\u2019re getting them in the JSON data.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-11370 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-unformatted-dates.png\" alt=\"Unformated date formats | Phrase\" width=\"590\" height=\"46\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-unformatted-dates.png 590w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-unformatted-dates-300x23.png 300w\" sizes=\"(max-width: 590px) 100vw, 590px\" \/><br \/>\n<em>These aren\u2019t the dates you\u2019re looking for<\/em><\/p>\n<p>We can localize these dates, and specify some formatting rules for them, using svelte-i18n\u2019s reactive <code>$date<\/code> store.<br \/>\nLet\u2019s expose the store in our wrapper library.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-group=\"8eece10b-a457-4608-951f-925d6a5a8275\" data-enlighter-title=\"src\/services\/i18n.js\" data-enlighter-linenumbers=\"false\">\/\/ ...\nimport {\n  _,\n  date,\n  init,\n  locale,\n  dictionary,\n  addMessages,\n  getLocaleFromNavigator,\n} from \"svelte-i18n\";\n\/\/ ...\nexport {\n  _,\n  dir,\n  date,\n  locale,\n  setupI18n,\n  isLocaleLoaded,\n};\n<\/pre>\n<p>Now let\u2019s use the store in our <code>Character<\/code> component to localize our dates.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"39e2b285-8b49-4c4d-9cd1-a4df59ddde87\" data-enlighter-title=\"src\/components\/Characters\/Character.svelte\" data-enlighter-linenumbers=\"false\">&lt;script&gt;\n  import { _, date } from \"..\/..\/services\/i18n\";\n  \/\/ ...\n&lt;\/script&gt;\n&lt;!-- ... --&gt;\n&lt;div class=\"box\"&gt;\n  &lt;div class=\"columns is-mobile\"&gt;\n    &lt;!-- ... --&gt;\n    &lt;div class=\"column\"&gt;\n      &lt;!-- ... --&gt;\n      &lt;p class=\"first-appeared\"&gt;\n        &lt;!-- ... --&gt;\n        {$date(\n          new Date(firstAppearedInFilm.releasedAt),\n          { format: \"medium\" }\n        )}\n      &lt;\/p&gt;\n      &lt;!-- ... --&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;\n<\/pre>\n<p><code>$date<\/code> accepts a JavaScript <code>Date<\/code> object, so we parse our date string into one before we pass it in. The store will localize the given date to the active locale, and react when the locale changes, forcing the date output to re-render. <code>$date<\/code> accepts a second, optional argument, which can contain one of preset <code>format<\/code>s. We\u2019ve chosen the medium format here.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> All date formats available to you are in the <a href=\"https:\/\/github.com\/kaisermann\/svelte-i18n\/blob\/master\/docs\/Formatting.md#formats\">official svelte-i18n documentation<\/a>.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-11371 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-formatted-dates.png\" alt=\"Localized and formatted date formats | Phrase\" width=\"977\" height=\"863\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-formatted-dates.png 977w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-formatted-dates-300x265.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-formatted-dates-768x678.png 768w\" sizes=\"(max-width: 977px) 100vw, 977px\" \/><br \/>\n<em>Localized dates with zero effort<\/em><\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> If you need custom date formatting beyond what the preset formats give you, you can <a href=\"https:\/\/github.com\/kaisermann\/svelte-i18n\/blob\/main\/docs\/Formatting.md#accessing-formatters-directly\">access the date formatter directly<\/a> to provide custom formats.<\/p>\n<p>With that in place, we\u2019ve localized our entire demo app ;)<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-11363 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-en-complete.png\" alt=\"Finished demo app in English | Phrase\" width=\"958\" height=\"993\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-en-complete.png 958w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-en-complete-289x300.png 289w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-en-complete-768x796.png 768w\" sizes=\"(max-width: 958px) 100vw, 958px\" \/><br \/>\n<em>The English localization of<\/em> Rebel Voter<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-11373 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-ar-complete.png\" alt=\"Finished demo app in Arabic | Phrase\" width=\"958\" height=\"993\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-ar-complete.png 958w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-ar-complete-289x300.png 289w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/07\/sveltei18n202006-ar-complete-768x796.png 768w\" sizes=\"(max-width: 958px) 100vw, 958px\" \/><br \/>\nRebel Voter <em>in Arabic<\/em><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> You can get all the code for the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/svelte-i18n-2020-06\/\">completed demo on Github<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"related-articles\"><\/span>Related Articles<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We\u2019ve got more JavaScript i18n\/l10n intros and deep dives for your perusing pleasure.<\/p>\n<ul>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/step-step-guide-javascript-localization\/\">The Ultimate Guide to JavaScript Localization<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/the-best-javascript-i18n-libraries\/\">The Best JavaScript I18n Libraries<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/a-human-friendly-way-to-display-dates-in-typescript-javascript\/\">A Human-friendly Way to Display Dates in TypeScript\/JavaScript<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/roll-your-own-javascript-i18n-library-with-typescript-part-1\/\">Roll Your Own JavaScript i18n Library with TypeScript \u2013 Part 1<\/a><\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/roll-your-own-javascript-i18n-library-with-typescript-part-2\/\">Roll Your Own JavaScript i18n Library with TypeScript \u2013 Part 2<\/a><\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"peace-out\"><\/span>Peace Out<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We hope you\u2019ve enjoyed this step-by-step guide to localizing a Svelte app with svelte-i18n. Looking to take your localization game to the next level? Check out <a href=\"https:\/\/phrase.com\">Phrase<\/a>. A professional localization solution, Phrase offers a flexible API, CLI, and a great web console for translators. Automatic GitHub, Bitbucket, and GitLab syncing allow seamless handover of translation files between you and your translation team. And over-the-air translations for mobile apps mean no more waiting for App Store reviews to push new translations through. Check out all of <a href=\"https:\/\/phrase.com\/solutions\/\">Phrase&#8217;s features<\/a> and see for yourself how they can help you take your apps global.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Still quite new but powerful, the Svelte framework has found a solid niche for itself. Here&#8217;s how to implement Svelte localization!<\/p>\n","protected":false},"author":41,"featured_media":2612,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_stopmodifiedupdate":false,"_modified_date":"","_searchwp_excluded":"","footnotes":""},"categories":[40],"class_list":["post-11335","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\/11335"}],"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=11335"}],"version-history":[{"count":7,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/11335\/revisions"}],"predecessor-version":[{"id":93900,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/11335\/revisions\/93900"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media\/2612"}],"wp:attachment":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media?parent=11335"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/categories?post=11335"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}