{"id":14669,"date":"2023-10-10T11:37:47","date_gmt":"2023-10-10T09:37:47","guid":{"rendered":"https:\/\/phrase.com\/blog\/?p=14669"},"modified":"2024-02-19T16:23:39","modified_gmt":"2024-02-19T15:23:39","slug":"react-i18n-format-js","status":"publish","type":"post","link":"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/","title":{"rendered":"A Guide to Localizing React Apps with react-intl\/FormatJS"},"content":{"rendered":"\n<div id=\"acf\/text-block_d6861469d884020722ff9e9afc56331e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>At <a href=\"https:\/\/www.npmjs.com\/package\/react\">20M NPM downloads per week<\/a> and integrations in everything from WordPress to native mobile apps, it\u2019s safe to say that <a href=\"https:\/\/react.dev\/\">React<\/a> has become the de facto UI library for the web and beyond.<\/p>\n<p>And when it comes to React app internationalization (i18n) and localization (l10n), the <a href=\"https:\/\/formatjs.io\/docs\/react-intl\/\">react-intl<\/a> library is hard to beat. react-intl sits atop the <a href=\"https:\/\/formatjs.io\/\">FormatJS<\/a> i18n libraries, themselves standards-focused and used by giants like Dropbox and Mozilla. react-intl also boasts <a href=\"https:\/\/www.npmjs.com\/package\/react-intl\">over a million NPM downloads per week<\/a>, so it\u2019s beyond battle-tested.<\/p>\n<p>In this hands-on guide, we\u2019ll show you how to localize a React app step by step using the react-intl library. We&#8217;ll begin with the fundamentals of internationalization, then move on to working with translations, as well as handling date and number formatting.<\/p>\n<p>As we progress, we&#8217;ll delve into more advanced topics such as message extraction and integrating a dedicated software localization platform like Phrase Strings. This integration will help us automate the localization process on a larger scale.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n\n<div id=\"acf\/blog-cta-block_112c76a278f674f1da56016d6cfe623c\" class=\"pxblock pxblock--blog-cta bg--green image--orientation-landscape\">\n\t<div class=\"block-container\">\n\t\t\t\t\t<div class=\"image image--align-middle\">\n\t\t\t\t<img loading=\"lazy\" decoding=\"async\" width=\"1260\" height=\"992\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero.png\" class=\"attachment-original size-original\" alt=\"String Management UI visual | Phrase\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero.png 1260w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero-300x236.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero-1024x806.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero-768x605.png 768w\" sizes=\"(max-width: 1260px) 100vw, 1260px\" \/>\t\t\t<\/div>\n\t\t\t\t<div class=\"content\">\n\t\t\t<p class=\"subhead\">Phrase Strings<\/p>\n<p class=\"secondary h6\">Take your web or mobile app global without any hassle<\/p>\n<div class=\"text--copy\">\n<p class=\"small\">Adapt your software, website, or video game for global audiences with the leanest and most realiable software localization platform.<\/p>\n<p><a class=\"btn btn--outline\" href=\"https:\/\/phrase.com\/platform\/strings\/\">Explore Phrase Strings<\/a><\/p>\n<\/div>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n\n<div id=\"acf\/text-block_f52088d96801bc8cb73ce08c336c78f7\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><span role=\"img\" aria-label=\"\ud83d\udd17\">\ud83d\udd17<\/span> <span class=\"notion-enable-hover\" data-token-index=\"1\">Resource \u00bb <\/span>If you want a comparison of React i18n libraries, check out our best-off list of <a class=\"notion-link-token notion-focusable-token notion-enable-hover\" tabindex=\"0\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-best-libraries\/\" rel=\"noopener noreferrer\" data-token-index=\"3\"><span class=\"link-annotation-unknown-block-id--1843839885\">React libraries for internationalization<\/span><\/a>.<\/p>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_69_1 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Overview<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#what-are-internationalization-i18n-and-localization-l10n\" title=\"What are internationalization (i18n) and localization (l10n)?\">What are internationalization (i18n) and localization (l10n)?<\/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\/react-i18n-format-js\/#what-is-a-locale\" title=\"What is a locale?\">What is a locale?<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#our-demo-app\" title=\"Our demo app\">Our demo app<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#package-versions-used\" title=\"Package versions used\">Package versions used<\/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\/react-i18n-format-js\/#how-do-i-localize-my-app-with-react-intl\" title=\"How do I localize my app with react-intl?\">How do I localize my app with react-intl?<\/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\/react-i18n-format-js\/#how-do-i-install-and-set-up-react-intl\" title=\"How do I install and set up react-intl?\">How do I install and set up react-intl?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#the-intlprovider-component\" title=\"The IntlProvider component\">The IntlProvider component<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#using-translation-files\" title=\"Using translation files\">Using translation files<\/a><\/li><\/ul><\/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\/react-i18n-format-js\/#how-do-i-add-a-language-switcher\" title=\"How do I add a language switcher?\">How do I add a language switcher?<\/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\/react-i18n-format-js\/#how-do-i-work-with-text-direction-ltrrtl\" title=\"How do I work with text direction (LTR\/RTL)?\">How do I work with text direction (LTR\/RTL)?<\/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\/react-i18n-format-js\/#updating-horizontal-styles\" title=\"Updating horizontal styles\">Updating horizontal styles<\/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\/react-i18n-format-js\/#what-are-the-2-ways-of-formatting-in-react-intl\" title=\"What are the 2 ways of formatting in react-intl?\">What are the 2 ways of formatting in react-intl?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#localizing-the-document-title\" title=\"Localizing the document title\">Localizing the document title<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#how-do-i-add-dynamic-values-to-translation-messages\" title=\"How do I add dynamic values to translation messages?\">How do I add dynamic values to translation messages?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#how-do-i-work-with-plurals-in-translation-messages\" title=\"How do I work with plurals in translation messages?\">How do I work with plurals in translation messages?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#how-do-i-localize-numbers\" title=\"How do I localize numbers?\">How do I localize numbers?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#how-do-i-localize-dates\" title=\"How do I localize dates?\">How do I localize dates?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#how-do-i-load-translations-dynamically\" title=\"How do I load translations dynamically?\">How do I load translations dynamically?<\/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\/react-i18n-format-js\/#how-do-i-detect-the-users-preferred-language\" title=\"How do I detect the user\u2019s preferred language?\">How do I detect the user\u2019s preferred language?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-20\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#storing-the-user-selected-locale\" title=\"Storing the user-selected locale\">Storing the user-selected locale<\/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\/react-i18n-format-js\/#how-do-i-include-html-in-my-translation-messages\" title=\"How do I include HTML in my translation messages?\">How do I include HTML 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-22\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#how-do-i-extract-translations-from-my-app\" title=\"How do I extract translations from my app?\">How do I extract translations from my app?<\/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\/react-i18n-format-js\/#additional-packages-used\" title=\"Additional packages used\">Additional packages used<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-24\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#auto-generating-ids\" title=\"Auto-generating IDs\">Auto-generating IDs<\/a><\/li><\/ul><\/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\/react-i18n-format-js\/#how-do-i-integrate-react-intl-with-phrase-strings\" title=\"How do I integrate react-intl with Phrase Strings?\">How do I integrate react-intl with Phrase Strings?<\/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\/react-i18n-format-js\/#creating-the-phrase-strings-project\" title=\"Creating the Phrase Strings project\">Creating the Phrase Strings project<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-27\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#setting-up-the-phrase-strings-cli\" title=\"Setting up the Phrase Strings CLI\">Setting up the Phrase Strings CLI<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-28\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#adding-a-new-language\" title=\"Adding a new language\">Adding a new language<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-29\" href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/#how-do-i-localize-my-react-typescript-app-with-react-intl\" title=\"How do I localize my React TypeScript app with react-intl?\">How do I localize my React TypeScript app with react-intl?<\/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\/react-i18n-format-js\/#take-react-localization-to-the-next-level\" title=\"Take React localization to the next level\">Take React localization to the next level<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"what-are-internationalization-i18n-and-localization-l10n\"><\/span>What are internationalization (i18n) and localization (l10n)?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In practice, internationalization and localization often tend to be used interchangeably, but they don\u2019t mean the same thing. <a href=\"https:\/\/phrase.com\/blog\/posts\/i18n-a-simple-definition\/\">Internationalization<\/a> (i18n) is the process of <em>making an app ready for localization<\/em>: not hard-coding UI strings and using keyed translation instead, for example. <a href=\"https:\/\/phrase.com\/blog\/posts\/how-important-is-localization-for-your-business\/\">Localization<\/a> (l10n) is making an app useable by people of different cultures and languages\u2014this often means the <em>actual<\/em> translation, using correct date formats, etc.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"what-is-a-locale\"><\/span>What is a locale?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>A locale is a <em>geographical region with a specific language<\/em>. We represent a locale using a code: <code>fr-CA<\/code> means French from Canada, while <code>fr-FR<\/code> means French from France. We often interchange the terms <em>language<\/em> and <em>locale,<\/em> but it\u2019s important to know the difference*.*<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Here\u2019s a handy list of<a href=\"https:\/\/saimana.com\/list-of-country-locale-code\/\">\u00a0locale codes<\/a>.<\/p>\n<p>Alright, let\u2019s get practical.<\/p>\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>We\u2019ll work with a small app that we\u2019ll localize step by step. Here it is before any i18n or l10n:<\/p>\n<figure id=\"attachment_66352\" aria-describedby=\"caption-attachment-66352\" style=\"width: 1704px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-66352\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/yomtaba-demo-app-starter.png\" alt=\"Yomtaba demo app screen | Phrase\" width=\"1704\" height=\"1142\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/yomtaba-demo-app-starter.png 1704w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/yomtaba-demo-app-starter-300x201.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/yomtaba-demo-app-starter-1024x686.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/yomtaba-demo-app-starter-768x515.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/yomtaba-demo-app-starter-1536x1029.png 1536w\" sizes=\"(max-width: 1704px) 100vw, 1704px\" \/><figcaption id=\"caption-attachment-66352\" class=\"wp-caption-text\">Our fictional Yomtaba app displays a daily curated recipe for fellow foodies<\/figcaption><\/figure>\n<p>\ud83d\udce3 <em>Shoutout \u00bb<\/em> The burger image was created using <a href=\"https:\/\/www.midjourney.com\">Midjourney<\/a>. Our app icon was created by <a href=\"https:\/\/thenounproject.com\/icon\/recipe-2701716\/\">Kawalan Icon from the Noun Project<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"package-versions-used\"><\/span>Package versions used<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We used the following NPM packages when developing this app.<\/p>\n<table style=\"border-collapse: collapse; width: 100%;\">\n<thead>\n<tr>\n<td style=\"width: 33.3333%;\">Library<\/td>\n<td style=\"width: 33.3333%;\">Version used<\/td>\n<td style=\"width: 33.3333%;\">Description<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"width: 33.3333%;\">react<\/td>\n<td style=\"width: 33.3333%;\">18.2.0<\/td>\n<td style=\"width: 33.3333%;\">Our main UI library<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.3333%;\">react-intl<\/td>\n<td style=\"width: 33.3333%;\">6.4.4<\/td>\n<td style=\"width: 33.3333%;\">Used to localize our app<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.3333%;\">@formatjs\/intl-localematcher<\/td>\n<td style=\"width: 33.3333%;\">0.4.0<\/td>\n<td style=\"width: 33.3333%;\">Helps detect the browser locale<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.3333%;\">tailwindcss<\/td>\n<td style=\"width: 33.3333%;\">3.3.2<\/td>\n<td style=\"width: 33.3333%;\">For styling; optional for our purposes.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Let\u2019s get to building. We\u2019ll spin up our app from the command line using <a class=\"notion-link-token notion-focusable-token notion-enable-hover\" tabindex=\"0\" href=\"https:\/\/create-react-app.dev\/\" rel=\"noopener noreferrer\" data-token-index=\"1\"><span class=\"link-annotation-unknown-block-id--1478048649\">Create React App<\/span><\/a>.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npx create-react-app i18n-demo<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_84f33eaf247166973e0f61931adfaa47\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Our React components are presentational; the root <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;App&gt;<\/span><\/code> houses a <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">&lt;Header&gt;<\/span><\/code> and a <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"5\">&lt;Recipe&gt;<\/span><\/code> card.<\/p>\n<figure id=\"attachment_66365\" aria-describedby=\"caption-attachment-66365\" style=\"width: 1784px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-66365 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/hierarchy.jpg\" alt=\"App component hierarchy screen | Phrase\" width=\"1784\" height=\"910\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/hierarchy.jpg 1784w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/hierarchy-300x153.jpg 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/hierarchy-1024x522.jpg 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/hierarchy-768x392.jpg 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/hierarchy-1536x783.jpg 1536w\" sizes=\"(max-width: 1784px) 100vw, 1784px\" \/><figcaption id=\"caption-attachment-66365\" class=\"wp-caption-text\">Our component hierarchy<\/figcaption><\/figure>\n<p>Let\u2019s quickly run down the code in these components.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/App.js<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> Header <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/components\/Header\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> Recipe <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/components\/Recipe\"<\/span>;\n\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">App<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Header<\/span> \/&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Recipe<\/span> \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>\n  );\n}\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> App;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_91599491e409dd6c105d8f14284c79e7\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><span role=\"img\" aria-label=\"\ud83d\uddd2\ufe0f\">\ud83d\uddd2\ufe0f<\/span> <span class=\"notion-enable-hover\" data-token-index=\"1\">Note \u00bb <\/span>For brevity, we omit all style code in this tutorial, except styles that relate to localization. You can <span class=\"link-annotation-unknown-block-id--1416049944\">get all the code for this demo app from <a href=\"https:\/\/github.com\/PhraseApp-Blog\/react-intl-formatjs-2023\">GitHub<\/a><\/span>, including styles. (The <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"5\">i18n-demo-start<\/span><\/code> directory contains the app before i18n.)<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/components\/Header.js<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">Header<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">header<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span>\n        <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"App logo\"<\/span>\n        <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"\/noun-recipe-2701716.svg\"<\/span>\n      \/&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h1<\/span>&gt;<\/span>Yomtaba<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h2<\/span>&gt;<\/span>\u00b7recipe of the day<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h2<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">header<\/span>&gt;<\/span><\/span>\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_347121fb1792f41eda66bed6d13a4d4d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/components\/Recipe.js<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> Nutrition <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/Nutrition\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">Recipe<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">main<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h2<\/span>&gt;<\/span>Today's recipe<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h2<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h3<\/span>&gt;<\/span>Delightful Vegan Bean Burger<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h3<\/span>&gt;<\/span>\n\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span>\n            <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"\/vegan_burger.jpg\"<\/span>\n            <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"Vegan burger on a wooden plate\"<\/span>\n          \/&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>by Rabia Mousa<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>2023\/6\/20<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>\u23f2\ufe0f 40min<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>\u2764\ufe0f 2291<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Nutrition<\/span> \/&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">main<\/span>&gt;<\/span><\/span>\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\"><\/code><\/span><\/pre>\n\n\n<div id=\"acf\/text-block_347121fb1792f41eda66bed6d13a4d4d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/components\/Nutrition.js<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">Nutrition<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">table<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">thead<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">tr<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">th<\/span> <span class=\"hljs-attr\">colSpan<\/span>=<span class=\"hljs-string\">{2}<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h4<\/span>&gt;<\/span>Nutrition<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h4<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">th<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">th<\/span>&gt;<\/span>% Daily Value<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">th<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">tr<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">thead<\/span>&gt;<\/span>\n\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">tbody<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">tr<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">td<\/span>&gt;<\/span>Calories<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">td<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">td<\/span>&gt;<\/span>151<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">td<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">td<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">td<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">tr<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">tr<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">td<\/span>&gt;<\/span>Fat<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">td<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">td<\/span>&gt;<\/span>1g<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">td<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">td<\/span>&gt;<\/span>2%<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">td<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">tr<\/span>&gt;<\/span>\n        <span class=\"hljs-comment\">&lt;!-- ... --&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">tbody<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">table<\/span>&gt;<\/span><\/span>\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_4ccfcbe4439d41bb9c64bc936f1183e2\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>That\u2019s about it for our starter code. Of course, all of our values are hard-coded into our components, and very much in need of localization. Shall we?<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> <a href=\"https:\/\/github.com\/PhraseApp-Blog\/react-intl-formatjs-2023\">Get all the starter code from GitHub<\/a> if you want to code along as we localize this app. Copy the <code>i18n-demo-start<\/code> directory, run <code>npm install<\/code>, and you should be good to go.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-my-app-with-react-intl\"><\/span>How do I localize my app with react-intl?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>From a bird\u2019s eye view, here\u2019s how we localize our app:<\/p>\n<ol>\n<li>Install react-intl.<\/li>\n<li>Wrap our app hierarchy in an <code>&lt;IntlProvider&gt;<\/code>.<\/li>\n<li>Move hard-coded strings to translation message dictionaries.<\/li>\n<li>Use <code>&lt;FormattedMessage&gt;<\/code> and <code>intl.formatMessage()<\/code> to display these translation messages in our components.<\/li>\n<li>Localize our dates and numbers using react-intl\u2019s <code>&lt;FormattedDate&gt;<\/code> and <code>&lt;FormattedNumber&gt;<\/code> respectively (and their function equivalents).<\/li>\n<\/ol>\n<p>How does all that work in practice? Let\u2019s take a look.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-install-and-set-up-react-intl\"><\/span>How do I install and set up react-intl?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We install react-intl via NPM. From the command line, run the following.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm install react-intl<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_ea5bde356f97b89289193f04300ebf84\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Let\u2019s set up some configuration. We\u2019ll put our i18n logic under a new directory, <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">src\/i18n<\/span><\/code>.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/i18n-config.js<\/span>\n\n<span class=\"hljs-comment\">\/\/ We'll use the English-USA locale when<\/span>\n<span class=\"hljs-comment\">\/\/ our app loads. It will also be used as<\/span>\n<span class=\"hljs-comment\">\/\/ a fallback when there's a missing<\/span>\n<span class=\"hljs-comment\">\/\/ translation in another locale.<\/span>\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> defaultLocale = <span class=\"hljs-string\">\"en-US\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ The locales our app supports. We'll work<\/span>\n<span class=\"hljs-comment\">\/\/ with English-USA and Arabic-Egypt here.<\/span>\n<span class=\"hljs-comment\">\/\/ Feel free to add any locales you want.<\/span>\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> locales = {\n  <span class=\"hljs-comment\">\/\/ English translation message dictionary.<\/span>\n  <span class=\"hljs-string\">\"en-US\"<\/span>: {\n    <span class=\"hljs-comment\">\/\/ \"x.y\" is just a convention for keys, but<\/span>\n    <span class=\"hljs-comment\">\/\/ any string will do here.<\/span>\n    <span class=\"hljs-string\">\"app.title\"<\/span>: <span class=\"hljs-string\">\"Yomtaba\"<\/span>,\n    <span class=\"hljs-string\">\"app.tagline\"<\/span>: <span class=\"hljs-string\">\"recipe of the day\"<\/span>,\n  },\n  <span class=\"hljs-comment\">\/\/ Arabic translation message dictionary.<\/span>\n  <span class=\"hljs-string\">\"ar-EG\"<\/span>: {\n    <span class=\"hljs-comment\">\/\/ Note that a message has to use the<\/span>\n    <span class=\"hljs-comment\">\/\/ same ID\/key across locales.<\/span>\n    <span class=\"hljs-string\">\"app.title\"<\/span>: <span class=\"hljs-string\">\"\u064a\u0648\u0645\u0628\u0627\u062a\u0627\"<\/span>,\n    <span class=\"hljs-string\">\"app.tagline\"<\/span>: <span class=\"hljs-string\">\"\u0648\u0635\u0641\u0629 \u0627\u0644\u064a\u0648\u0645\"<\/span>,\n  },\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_185d07a82a007bf5910e183cf16f4d89\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>We\u2019ll revisit this configuration throughout this guide. Let\u2019s continue our setup.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Installation is also covered in the official <a href=\"https:\/\/formatjs.io\/docs\/getting-started\/installation\">FormatJS installation docs<\/a>.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"the-intlprovider-component\"><\/span>The IntlProvider component<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>All of react-intl\u2019s localization components need to be inside an <code>&lt;IntlProvider&gt;<\/code> to work. <code>&lt;IntlProvider&gt;<\/code> manages the active locale and translations, and ensures that any nested react-intl components show their values in the active locale. Let\u2019s see this in action.<\/p>\n<p>First, we\u2019ll create an <code>&lt;I18n&gt;<\/code> component to encapsulate the <code>&lt;IntlProvider&gt;<\/code>. <code>&lt;I18n&gt;<\/code> will wrap our <code>&lt;App&gt;<\/code> and house i18n logic as we continue to build.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/I18n.js<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { IntlProvider } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-intl\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Import the configuration we created earlier<\/span>\n<span class=\"hljs-keyword\">import<\/span> { defaultLocale, locales } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/i18n-config\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">I18n<\/span>(<span class=\"hljs-params\">props<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    {<span class=\"hljs-comment\">\/* IntlProvider needs to be fed the active `locale`\n        as well as the translation `messages` of the\n        active locale. The `defaultLocale` is a\n        fallback when there is a missing translation. *\/<\/span>}\n    &lt;IntlProvider\n      locale={defaultLocale}\n      defaultLocale={defaultLocale}\n      messages={locales&#91;defaultLocale]}\n    &gt;\n      {props.children}\n    &lt;<span class=\"hljs-regexp\">\/IntlProvider&gt;\n  );\n}<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_751b4bbf05d0cfb6f0fb1a0cdd218b8f\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Alright, now we can wrap our entire app with <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;I18n&gt;<\/span><\/code>; we\u2019ll do so in <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">index.js<\/span><\/code>.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/index.js\n\n  import React from \"react\";\n  import ReactDOM from \"react-dom\/client\";\n  import \".\/index.css\";\n  import App from \".\/App\";\n<span class=\"hljs-addition\">+ import I18n from \".\/i18n\/I18n\";<\/span>\n\n  const root = ReactDOM.createRoot(document.getElementById(\"root\"));\n  root.render(\n    &lt;React.StrictMode&gt;\n<span class=\"hljs-addition\">+      &lt;I18n&gt;<\/span>\n<span class=\"hljs-addition\">+        &lt;App \/&gt;<\/span>\n<span class=\"hljs-addition\">+      &lt;\/I18n&gt;<\/span>\n    &lt;\/React.StrictMode&gt;\n  );<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_b792aad6be8b57d4b54c4e34ee1dcada\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<div class=\"group w-full text-token-text-primary border-b border-black\/10 gizmo:border-0 dark:border-gray-900\/50 gizmo:dark:border-0 bg-gray-50 gizmo:bg-transparent dark:bg-[#444654] gizmo:dark:bg-transparent\" data-testid=\"conversation-turn-5\">\n<div class=\"p-4 justify-center text-base md:gap-6 md:py-6 m-auto\">\n<div class=\"flex flex-1 gap-4 text-base mx-auto md:gap-6 gizmo:gap-3 gizmo:md:px-5 gizmo:lg:px-1 gizmo:xl:px-5 md:max-w-2xl lg:max-w-[38rem] gizmo:md:max-w-3xl gizmo:lg:max-w-[40rem] gizmo:xl:max-w-[48rem] xl:max-w-3xl }\">\n<div class=\"relative flex w-[calc(100%-50px)] flex-col gap-1 gizmo:w-full md:gap-3 lg:w-[calc(100%-115px)] agent-turn\">\n<div class=\"flex flex-grow flex-col gap-3 max-w-full\">\n<div class=\"min-h-[20px] flex flex-col items-start gap-3 whitespace-pre-wrap break-words overflow-x-auto\">\n<div class=\"markdown prose w-full break-words dark:prose-invert light\">\n<p>Our hierarchy now looks like this:<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">.\n\u2514\u2500\u2500 I18n\n    \u2514\u2500\u2500 IntlProvider\n        \u2514\u2500\u2500 App\n            \u2514\u2500\u2500 ...<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">plaintext<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">plaintext<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_677bea359cb77422078596859716be75\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Our entire app is wrapped in a locale-aware <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;IntlProvider&gt;<\/span><\/code>. We can now localize our components. We\u2019ve already added translation messages for our app\u2019s name and tagline; let\u2019s use those to update our <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">&lt;Header&gt;<\/span><\/code>. We\u2019ll use react-intl\u2019s <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"5\">&lt;FormattedMessage&gt;<\/span><\/code> component to do this.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/components\/Header.js\n\n<span class=\"hljs-addition\">+ import { FormattedMessage } from \"react-intl\";<\/span>\n\n  export default function Header() {\n    return (\n      &lt;header&gt;\n        &lt;img\n          alt=\"App logo\"\n          src=\"\/noun-recipe-2701716.svg\"\n        \/&gt;\n        &lt;h1&gt;\n<span class=\"hljs-deletion\">-         Yomtaba<\/span>\n<span class=\"hljs-addition\">+         &lt;FormattedMessage id=\"app.title\" \/&gt;<\/span>\n        &lt;\/h1&gt;\n        \u00b7\n        &lt;h2&gt;\n<span class=\"hljs-deletion\">-         recipe of the day<\/span>\n<span class=\"hljs-addition\">+         &lt;FormattedMessage id=\"app.tagline\" \/&gt;<\/span>\n        &lt;\/h2&gt;\n      &lt;\/header&gt;\n    );\n }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_d8f2c9b8871db3dece75d2ee09c506d2\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>react-intl\u2019s <code>&lt;FormattedMessage&gt;<\/code> component takes an <code>id<\/code> prop and renders the corresponding translation message in the active locale. The wrapping <code>&lt;IntlProvider&gt;<\/code> will ensure that the active locale\u2019s messages are used here.<\/p>\n<p>If we reload our app now, it looks exactly the same as it did before. That\u2019s because the active locale currently defaults to English.<\/p>\n<p>What happens, however, if we change our default locale to Arabic?<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/i18n-config.js\n\n<span class=\"hljs-deletion\">- export const defaultLocale = \"en-US\";<\/span>\n<span class=\"hljs-addition\">+ export const defaultLocale = \"ar-EG\";<\/span>\n\nexport const locales = {\n  \"en-US\": {\n    \"app.title\": \"Yomtaba\",\n    \"app.tagline\": \"recipe of the day\",\n  },\n  \"ar-EG\": {\n    \"app.title\": \"\u064a\u0648\u0645\u0628\u0627\u062a\u0627\",\n    \"app.tagline\": \"\u0648\u0635\u0641\u0629 \u0627\u0644\u064a\u0648\u0645\",\n  },\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_0f5ab9f2aa7e49702bfebc14416d809b\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Presto.<\/p>\n<figure id=\"attachment_66376\" aria-describedby=\"caption-attachment-66376\" style=\"width: 486px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-66376\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/ar-title.png\" alt=\"Arabic translation display on an app screen | Phrase\" width=\"486\" height=\"140\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/ar-title.png 486w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/ar-title-300x86.png 300w\" sizes=\"(max-width: 486px) 100vw, 486px\" \/><figcaption id=\"caption-attachment-66376\" class=\"wp-caption-text\">Our app\u2019s title and tagline displayed in Arabic<\/figcaption><\/figure>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> Read more about **<a href=\"https:\/\/formatjs.io\/docs\/react-intl\/components#formattedmessage\">FormattedMessage<\/a> in the official API docs.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"using-translation-files\"><\/span>Using translation files<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>This is a good start, but we might want a more scaleable solution: All of our translations sitting in one configuration file can quickly get messy.<\/p>\n<p>Let\u2019s move our translations out of <code>i18n-config.js<\/code> and into new, per-locale translation files. We\u2019ll put those under a <code>src\/lang<\/code> directory.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ src\/lang\/en-US.json<\/span>\n\n{\n  <span class=\"hljs-attr\">\"app.title\"<\/span>: <span class=\"hljs-string\">\"Yomtaba\"<\/span>,\n  <span class=\"hljs-attr\">\"app.tagline\"<\/span>: <span class=\"hljs-string\">\"recipe of the day\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ src\/lang\/ar-EG.json<\/span>\n\n{\n  <span class=\"hljs-attr\">\"app.title\"<\/span>: <span class=\"hljs-string\">\"\u064a\u0648\u0645\u062a\u0627\u0628\u0627\"<\/span>,\n  <span class=\"hljs-attr\">\"app.tagline\"<\/span>: <span class=\"hljs-string\">\"\u0648\u0635\u0641\u0629 \u0627\u0644\u064a\u0648\u0645\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_cc93d84b8597549b7cd7d0dfaee7fcb7\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now we can update our configuration to pull these files in.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/i18n-config.js\n\n<span class=\"hljs-addition\">+ import enMessages from \"..\/lang\/en-US.json\";<\/span>\n<span class=\"hljs-addition\">+ import arMessages from \"..\/lang\/ar-EG.json\";<\/span>\n\n  export const defaultLocale = \"en-US\";\n\n  export const locales = {\n    \"en-US\": {\n<span class=\"hljs-addition\">+     messages: enMessages,<\/span>\n<span class=\"hljs-deletion\">-     \"app.title\": \"Yomtaba\",<\/span>\n<span class=\"hljs-deletion\">-     \"app.tagline\": \"recipe of the day\",       <\/span>\n    },\n    \"ar-EG\": {\n<span class=\"hljs-addition\">+     messages: arMessages,<\/span>\n<span class=\"hljs-deletion\">-     \"app.title\": \"\u064a\u0648\u0645\u062a\u0627\u0628\u0627\",<\/span>\n<span class=\"hljs-deletion\">-     \"app.tagline\": \"\u0648\u0635\u0641\u0629 \u0627\u0644\u064a\u0648\u0645\"<\/span>\n    },\n  };<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_32e96efe33daee26098b98381a5e9b2c\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>We round out our refactor with a quick update to our <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;I18n&gt;<\/span><\/code> component.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/I18n.js\n\nimport { IntlProvider } from \"react-intl\";\nimport { defaultLocale, locales } from \".\/i18n-config\";\n\nexport default function I18n(props) {\n  return (\n    &lt;IntlProvider\n      locale={defaultLocale}\n      defaultLocale={defaultLocale}\n<span class=\"hljs-deletion\">-     messages={locales&#91;defaultLocale]}<\/span>\n<span class=\"hljs-addition\">+     messages={locales&#91;defaultLocale].messages}<\/span>\n    &gt;\n      {props.children}\n    &lt;\/IntlProvider&gt;\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_00a065769106ca9193f6ba962a7bc71e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Our app should now work exactly as it did before, except now we can add more translations without bloating our <code>i18n-config.js<\/code> file. We can also easily pass translation files back and forth to translators.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-add-a-language-switcher\"><\/span>How do I add a language switcher?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Let\u2019s give our users a nice UI to be able to select the locale of their choice. Adding a language switcher will also make testing easier as we continue to localize the app. Once done, it will look like this:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-66693 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/language-switcher.png\" alt=\"Language switcher on an app screen | Phrase\" width=\"1256\" height=\"148\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/language-switcher.png 1256w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/language-switcher-300x35.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/language-switcher-1024x121.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/language-switcher-768x90.png 768w\" sizes=\"(max-width: 1256px) 100vw, 1256px\" \/><\/p>\n<p>We\u2019ll need to update <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;IntlProvider&gt;<\/span><\/code>&#8216;s <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">locale<\/span><\/code> and <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"5\">messages<\/span><\/code> props to effectively change the rendered language. The problem is that our language switcher will need to sit deep inside the <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"7\">&lt;IntlProvider&gt;<\/span><\/code>, making our hierarchy look something like this:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">.\n\u2514\u2500\u2500 I18n\n    \u2514\u2500\u2500 IntlProvider\n        \u2514\u2500\u2500 App\n            \u2514\u2500\u2500 Header\n                \u2514\u2500\u2500 LangSwitcher\n                    \u2514\u2500\u2500 select\n                        \u251c\u2500\u2500 option&#91;English]\n                        \u2514\u2500\u2500 option&#91;Arabic]<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">plaintext<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">plaintext<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_4f058ea5f3e632aebf860d47b09c5d00\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>We want to let <code>&lt;IntlProvider&gt;<\/code> know when a new language is selected in the <code>&lt;LangSwitcher&gt;<\/code>.<\/p>\n<p>We could connect an event through the <code>&lt;Header&gt;<\/code> and <code>&lt;App&gt;<\/code> components to the <code>&lt;IntlProvider&gt;<\/code>. This is known as \u201cprop drilling\u201d. It would create an unnecessary dependency chain, making it difficult to move the <code>&lt;LangSwitcher&gt;<\/code> within the app hierarchy later.<\/p>\n<p>To avoid prop drilling, let\u2019s add a piece of React <a href=\"https:\/\/react.dev\/learn\/passing-data-deeply-with-context\">context<\/a> to manage our global locale state.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/LocaleContext.js<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { createContext } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> LocaleContext = createContext({\n  <span class=\"hljs-comment\">\/\/ Defaults that we'll override in a moment. <\/span>\n  <span class=\"hljs-attr\">locale<\/span>: <span class=\"hljs-string\">\"\"<\/span>,\n  <span class=\"hljs-attr\">setLocale<\/span>: <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {},\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_b2051ea281e9f69858994839fde4a0d7\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now let\u2019s wire up this context to our <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;I18n&gt;<\/span><\/code> component.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/I18n.js\n\n  import { IntlProvider } from \"react-intl\";\n  import { defaultLocale, locales } from \".\/i18n-config\";\n<span class=\"hljs-addition\">+ import { useState } from \"react\";<\/span>\n<span class=\"hljs-addition\">+ import { LocaleContext } from \".\/LocaleContext\";<\/span>\n\n  export default function I18n(props) {\n<span class=\"hljs-addition\">+   \/\/ Add the active locale as component state.<\/span>\n<span class=\"hljs-addition\">+   const &#91;locale, setLocale] = useState(defaultLocale);<\/span>\n\n    return (\n<span class=\"hljs-addition\">+     \/\/ Expose the state and its setter to all descendent<\/span>\n<span class=\"hljs-addition\">+     \/\/ components.<\/span>\n<span class=\"hljs-addition\">+     &lt;LocaleContext.Provider value={{ locale, setLocale }}&gt;<\/span>\n        &lt;IntlProvider\n<span class=\"hljs-deletion\">-         locale={defaultLocale}<\/span>\n<span class=\"hljs-addition\">+         locale={locale}<\/span>\n          defaultLocale={defaultLocale}\n<span class=\"hljs-deletion\">-         messages={locales&#91;defaultLocale].messages}<\/span>\n<span class=\"hljs-addition\">+         messages={locales&#91;locale].messages}<\/span>\n        &gt;\n          {props.children}\n        &lt;\/IntlProvider&gt;\n<span class=\"hljs-addition\">+     &lt;\/LocaleContext.Provider&gt;<\/span>\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_0f5c0aab12ce58a24f56ba623feed139\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>We can now use <code>LocaleContext<\/code> in any descendent of <code>&lt;LocaleContext.Provider&gt;<\/code>, allowing us to set the top-level <code>locale<\/code> state practically anywhere in our hierarchy.<\/p>\n<p>This means we can update the active locale in <code>&lt;IntlProvider&gt;<\/code> without passing props up or down our hierarchy. Let\u2019s make use of this in our new <code>&lt;LangSwitcher&gt;<\/code>.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/LangSwitcher.js<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { useContext } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { locales } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/i18n-config\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { LocaleContext } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/LocaleContext\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">LangSwitcher<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-comment\">\/\/ Pull in the top-level locale and its setter.<\/span>\n  <span class=\"hljs-keyword\">const<\/span> { locale, setLocale } = useContext(LocaleContext);\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">select<\/span>\n        <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{locale}<\/span>\n        \/\/ <span class=\"hljs-attr\">Whenever<\/span> <span class=\"hljs-attr\">the<\/span> <span class=\"hljs-attr\">user<\/span> <span class=\"hljs-attr\">selects<\/span> <span class=\"hljs-attr\">a<\/span> <span class=\"hljs-attr\">locale<\/span>, <span class=\"hljs-attr\">update<\/span> <span class=\"hljs-attr\">the<\/span>\n        \/\/ <span class=\"hljs-attr\">top-level<\/span> <span class=\"hljs-attr\">active<\/span> <span class=\"hljs-attr\">locale.<\/span>\n        <span class=\"hljs-attr\">onChange<\/span>=<span class=\"hljs-string\">{(e)<\/span> =&gt;<\/span> setLocale(e.target.value)}\n      &gt;\n        {\/* The keys of the `locales` config object\n            are the locale codes: \"en-US\", \"ar-EG\". *\/}\n        {Object.keys(locales).map((loc) =&gt; (\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">option<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{loc}<\/span> <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{loc}<\/span>&gt;<\/span>\n            {loc}\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">option<\/span>&gt;<\/span>\n        ))}\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">select<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_edffecb934cedad4faa1792390ee2314\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>With that in place, we can select a new locale from the UI, triggering a re-render of all react-intl components, like <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;FormattedMessage&gt;<\/span><\/code>. This effectively switches the active locale. If we tuck our <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">&lt;LangSwitcher&gt;<\/span><\/code> inside our <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"5\">&lt;Header&gt;<\/span><\/code> component, we should see the following.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-66719 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/language-switcher-codes.gif\" alt=\"Language switcher codes on an app screen | Phrase\" width=\"600\" height=\"64\" \/><\/p>\n<p>Of course, we don\u2019t want our users looking at locale codes like <code>en-US<\/code> when selecting their language. Let\u2019s add human-friendly names to our locales.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/i18n-config.js\n\n\/\/ ...\n\nexport const locales = {\n  \"en-US\": {\n<span class=\"hljs-addition\">+   name: \"English\",<\/span>\n    messages: enMessages,\n  },\n  \"ar-EG\": {\n<span class=\"hljs-addition\">+   name: \"Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)\",<\/span>\n    messages: arMessages,\n  },\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_63f2eaf90cbdb5dfc6970a528ee10b3f\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now we can update our <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;LangSwitcher&gt;<\/span><\/code> to use these names.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/LangSwitcher.js\n\n\/\/ ...\n\nexport default function LangSwitcher() {\n  \/\/ ...\n\n  return (\n    &lt;div&gt;\n      &lt;select ...&gt;\n        {Object.keys(locales).map((loc) =&gt; (\n          &lt;option value={loc} key={loc}&gt;\n<span class=\"hljs-deletion\">-           {loc}<\/span>\n<span class=\"hljs-addition\">+           {locales&#91;loc].name}<\/span>\n          &lt;\/option&gt;\n        ))}\n      &lt;\/select&gt;\n    &lt;\/div&gt;\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_be6dacadb564ffbd8d8b41ad00fa7f52\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Our language switcher now looks more readable. And because we used context, our solution is flexible: We can move our <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;LangSwitcher&gt;<\/span><\/code> anywhere within the scope of our <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">&lt;LocalContext.Provider&gt;<\/span><\/code> and it will continue to work.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-66728 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/enhanced-language-switcher.gif\" alt=\"Language switcher on an app screen | Phrase\" width=\"600\" height=\"64\" \/><\/p>\n<p>\ud83d\udce3 <em>Shoutout \u00bb<\/em> Language icon by <a href=\"https:\/\/thenounproject.com\/icon\/language-4114572\/\">jonata hangga on the Noun Project.<\/a><\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The official React docs\u2019 **<a href=\"https:\/\/react.dev\/learn\/passing-data-deeply-with-context\">Passing Data Deeply with Context<\/a> is a good guide on the subject.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-work-with-text-direction-ltrrtl\"><\/span>How do I work with text direction (LTR\/RTL)?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Arabic, Hebrew, Maldivian, and other languages are laid out right-to-left (<code>rtl<\/code>). Most others are left-to-right (<code>ltr<\/code>). Web browsers accommodate this through the <code>&lt;html dir=\"rtl\"&gt;<\/code> attribute, which sets the layout of the whole page. Let\u2019s tap into this attribute in a new custom hook.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-23\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/useDocL10n.js<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { useEffect } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { useIntl } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-intl\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { locales } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/i18n-config\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">useDocL10n<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-comment\">\/\/ Get the active locale from the `intl`<\/span>\n  <span class=\"hljs-comment\">\/\/ instance.<\/span>\n  <span class=\"hljs-keyword\">const<\/span> { locale } = useIntl();\n\n  <span class=\"hljs-comment\">\/\/ Update the &lt;html dir&gt; attr whenever<\/span>\n  <span class=\"hljs-comment\">\/\/ the locale changes.<\/span>\n  useEffect(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    <span class=\"hljs-built_in\">document<\/span>.dir = locales&#91;locale].dir;\n  }, &#91;locale]);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_9c85ac870849e7d05f4edd75e95e899f\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>This is the first time we see the <code>useIntl()<\/code> hook. It returns an <code>intl<\/code> instance that has useful properties, like the active <code>locale<\/code>. (This is the same <code>intl<\/code> instance managed internally by our <code>&lt;IntlProvider&gt;<\/code>). We\u2019ll revisit <code>useIntl()<\/code> throughout the article.<\/p>\n<p>Let\u2019s use our new custom hook in the root <code>&lt;App&gt;<\/code> component.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-24\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/App.js\n\n  import Header from \".\/components\/Header\";\n  import Recipe from \".\/components\/Recipe\";\n<span class=\"hljs-addition\">+ import { useDocL10n } from \".\/i18n\/useDocL10n\";<\/span>\n\n  export default function App() {\n<span class=\"hljs-addition\">+   useDocL10n();<\/span>\n\n    return (\n      &lt;div&gt;\n        &lt;Header \/&gt;\n        &lt;Recipe \/&gt;\n      &lt;\/div&gt;\n    ); \n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-24\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_347121fb1792f41eda66bed6d13a4d4d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-25\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/App.js\n\n  import Header from \".\/components\/Header\";\n  import Recipe from \".\/components\/Recipe\";\n<span class=\"hljs-addition\">+ import { useDocL10n } from \".\/i18n\/useDocL10n\";<\/span>\n\n  export default function App() {\n<span class=\"hljs-addition\">+   useDocL10n();<\/span>\n\n    return (\n      &lt;div&gt;\n        &lt;Header \/&gt;\n        &lt;Recipe \/&gt;\n      &lt;\/div&gt;\n    ); \n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_9312ba3f08f8d8186b1ef27bcef13a05\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Notice that we\u2019re referencing <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">locales[locale].dir<\/span><\/code> in our custom hook above. We need to add these direction configs to make the hook work.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-26\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/i18n-config.js\n\n\/\/ ...\n\nexport const locales = {\n  \"en-US\": {\n    name: \"English\",\n    messages: enMessages,\n<span class=\"hljs-addition\">+   dir: \"ltr\",<\/span>\n  },\n  \"ar-EG\": {\n    name: \"Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)\",\n    messages: arMessages,\n<span class=\"hljs-addition\">+   dir: \"rtl\",<\/span>\n  },\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-26\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_63d7f5007b300bb4ba965196666999cc\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now when Arabic is the active locale, the <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;html&gt;<\/span><\/code> document element will have a <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">dir=\"rtl\"<\/span><\/code> value; when English is active it has <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"5\">dir=\"ltr\"<\/span><\/code>.<\/p>\n<figure id=\"attachment_66737\" aria-describedby=\"caption-attachment-66737\" style=\"width: 1672px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-66737\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/right-to-left-text-direction.png\" alt=\"Right-to-left text direction example | Phrase\" width=\"1672\" height=\"1154\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/right-to-left-text-direction.png 1672w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/right-to-left-text-direction-300x207.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/right-to-left-text-direction-1024x707.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/right-to-left-text-direction-768x530.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/right-to-left-text-direction-1536x1060.png 1536w\" sizes=\"(max-width: 1672px) 100vw, 1672px\" \/><figcaption id=\"caption-attachment-66737\" class=\"wp-caption-text\">Just by setting, our app is laid out right-to-left<\/figcaption><\/figure>\n<p>\ud83d\uddd2\ufe0f <em>Note \u00bb<\/em> You can set the <code>dir<\/code> attribute on many HTML elements to override the page\u2019s <code>dir<\/code>.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"updating-horizontal-styles\"><\/span>Updating horizontal styles<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Setting the <code>dir<\/code> attribute to <code>rtl<\/code> on the <code>&lt;html&gt;<\/code> will flow the page right-to-left. However, we often still need to update CSS that uses properties like <code>margin-left<\/code>. Here\u2019s an example:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-27\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-comment\">\/* Apply when the document is left-to-right. *\/<\/span>\n<span class=\"hljs-selector-attr\">&#91;dir=<span class=\"hljs-string\">\"ltr\"<\/span>]<\/span> <span class=\"hljs-selector-class\">.card<\/span> {\n  <span class=\"hljs-attribute\">padding-left<\/span>: <span class=\"hljs-number\">0.25rem<\/span>;\n}\n\n<span class=\"hljs-comment\">\/* Apply when the document is right-to-left. *\/<\/span>\n<span class=\"hljs-selector-attr\">&#91;dir=<span class=\"hljs-string\">\"rtl\"<\/span>]<\/span> <span class=\"hljs-selector-class\">.card<\/span> {\n  <span class=\"hljs-attribute\">padding-right<\/span>: <span class=\"hljs-number\">0.25rem<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-27\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_0e80aba393b7792362a16e3ddde7db23\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> Alternatively, we can use the newer *<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_logical_properties_and_values\">logical* <em>properties<\/em><\/a>, like <code>padding-inline-start<\/code>, which cover text direction automatically.<\/p>\n<p>\ud83d\uddd2\ufe0f <em>Note \u00bb<\/em> Tailwind CSS has <a href=\"https:\/\/tailwindcss.com\/docs\/hover-focus-and-other-states#rtl-support\">built-in direction modifiers<\/a> e.g. <code>ltr:pr-1 rtl:pl-1<\/code>. The framework also supports <a href=\"https:\/\/tailwindcss.com\/docs\/padding#using-logical-properties\">logical properties<\/a>, like <code>ps-1<\/code> for <code>padding-inline-start<\/code>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"what-are-the-2-ways-of-formatting-in-react-intl\"><\/span>What are the 2 ways of formatting in react-intl?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Let\u2019s review the basic translation message workflow as we look at react-intl\u2019s 2 ways of formatting: declarative and imperative.<\/p>\n<p>We\u2019ll add translations for our recipe headers next.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-28\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/lang\/en-US.json\n\n{\n  \"app.title\": \"Yomtaba\",\n  \"app.tagline\": \"recipe of the day\",\n  \"app.logo_alt\": \"Yomtaba logo\",\n<span class=\"hljs-addition\">+ \"recipe.title_label\": \"Today's recipe\",<\/span>\n<span class=\"hljs-addition\">+ \"recipe.title\": \"Delightful Vegan Bean Burger\",<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-28\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_347121fb1792f41eda66bed6d13a4d4d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-29\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/lang\/ar-EG.json\n\n{\n  \"app.title\": \"\u064a\u0648\u0645\u062a\u0627\u0628\u0627\",\n  \"app.tagline\": \"\u0648\u0635\u0641\u0629 \u0627\u0644\u064a\u0648\u0645\",\n  \"app.logo_alt\": \"\u0631\u0645\u0632 \u064a\u0648\u0645\u062a\u0627\u0628\u0627\",\n<span class=\"hljs-addition\">+ \"recipe.title_label\": \"\u0648\u0635\u0641\u0629 \u0627\u0644\u064a\u0648\u0645\",<\/span>\n<span class=\"hljs-addition\">+ \"recipe.title\": \"\u0628\u0631\u063a\u0631 \u0627\u0644\u0641\u0627\u0635\u0648\u0644\u064a\u0627 \u0627\u0644\u0646\u0628\u0627\u062a\u064a \u0627\u0644\u0631\u0627\u0626\u0639\",<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-29\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_e5a9ecffcad9792669808992b7d949df\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Just as before, we\u2019ll pull these translations into our <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;Recipe&gt;<\/span><\/code> component using react-intl\u2019s <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">&lt;FormattedMessage&gt;<\/span><\/code> component.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-30\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/components\/Recipe.js\n\n<span class=\"hljs-addition\">+ import { FormattedMessage } from \"react-intl\";<\/span>\n \/\/ ...\n\n export default function Recipe() {\n   return (\n     &lt;main&gt;\n       &lt;h2&gt;\n<span class=\"hljs-deletion\">-        Today's recipe<\/span>\n<span class=\"hljs-addition\">+        &lt;FormattedMessage id=\"recipe.title_label\" \/&gt;<\/span>\n       &lt;\/h2&gt;\n       &lt;h3&gt;\n<span class=\"hljs-deletion\">-        Delightful Vegan Bean Burger<\/span>\n<span class=\"hljs-addition\">+        &lt;FormattedMessage id=\"recipe.title\" \/&gt;<\/span>\n       &lt;\/h3&gt;\n\n       {\/* ... *\/}\n     &lt;\/main&gt;\n   );\n }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-30\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_85e5261e2a2dd0df588909f9e16beef2\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>This is a <em>declarative<\/em> way to format messages. In other words, we\u2019re not concerned with <em>how<\/em> messages as formatted; just that the React component, <code>&lt;FormattedMessage&gt;<\/code>, will display a message in the active locale given its <code>id<\/code>.<\/p>\n<p>As it happens, <code>&lt;FormattedMessage&gt;<\/code> uses an <code>intl.formatMessage()<\/code> function under the hood \u2014that\u2019s the <em>how<\/em>, the <em>imperative<\/em> way to format.<\/p>\n<p>We can make use of <code>intl.formatMessage()<\/code> ourselves through the <code>useIntl()<\/code> hook. This comes in handy when we want to translate attributes or props. Let\u2019s translate our recipe image\u2019s <code>alt<\/code> attribute to illustrate.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-31\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/lang\/en-US.json\n\n{\n  \/\/ ...\n  \"recipe.title_label\": \"Today's recipe\",\n  \"recipe.title\": \"Delightful Vegan Bean Burger\",\n<span class=\"hljs-addition\">+ \"recipe.img_alt\": \"Vegan burger on a wooden plate\",<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-31\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_347121fb1792f41eda66bed6d13a4d4d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-32\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/lang\/ar-EG.json\n\n{\n  \/\/ ...\n  \"recipe.title_label\": \"\u0648\u0635\u0641\u0629 \u0627\u0644\u064a\u0648\u0645\",\n  \"recipe.title\": \"\u0628\u0631\u063a\u0631 \u0627\u0644\u0641\u0627\u0635\u0648\u0644\u064a\u0627 \u0627\u0644\u0646\u0628\u0627\u062a\u064a \u0627\u0644\u0631\u0627\u0626\u0639\",\n<span class=\"hljs-addition\">+ \"recipe.img_alt\": \"\u0628\u0631\u063a\u0631 \u0646\u0628\u0627\u062a\u064a \u0639\u0644\u0649 \u0637\u0628\u0642 \u062e\u0634\u0628\u064a\",<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-32\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_347121fb1792f41eda66bed6d13a4d4d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-33\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/components\/Recipe.js<\/span>\n\n- <span class=\"hljs-keyword\">import<\/span> { FormattedMessage } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-intl\"<\/span>;\n+ <span class=\"hljs-keyword\">import<\/span> { FormattedMessage, useIntl } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-intl\"<\/span>;\n  \n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n\n  <span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">Recipe<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n+   <span class=\"hljs-comment\">\/\/ Retrieve the `intl` object holding the active<\/span>\n+   <span class=\"hljs-comment\">\/\/ locale and translation messages. This object<\/span>\n+   <span class=\"hljs-comment\">\/\/ is provided by the top-level `&lt;IntlProvider&gt;`.<\/span>\n+   <span class=\"hljs-keyword\">const<\/span> intl = useIntl();\n\n    <span class=\"hljs-keyword\">return<\/span> (\n      <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">main<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h2<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">FormattedMessage<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"recipe.title_label\"<\/span> \/&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h2<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h3<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">FormattedMessage<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"recipe.title\"<\/span> \/&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h3<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span>\n              <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"\/vegan_burger.jpg\"<\/span>\n<span class=\"hljs-attr\">-<\/span>             <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"Vegan burger on a wooden plate\"<\/span>\n+             <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">{intl.formatMessage({<\/span> <span class=\"hljs-attr\">id:<\/span> \"<span class=\"hljs-attr\">recipe.img_alt<\/span>\" })}\n            \/&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n\n          {\/* ... *\/}\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">main<\/span>&gt;<\/span><\/span>\n    );\n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-33\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_b2fe4cf3c1f9be99a4e51baf51c6e846\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"0\">intl.formatMessage()<\/span><\/code> works much like the <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"2\">&lt;FormattedMessage&gt;<\/span><\/code> component: Given an <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"4\">id<\/span><\/code>, it returns the corresponding translation message in the active locale.<\/p>\n<figure id=\"attachment_66752\" aria-describedby=\"caption-attachment-66752\" style=\"width: 982px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-66752 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/alt-l10n-inspector.png\" alt=\"Recipe image alt tag translated into English and Arabic | Phrase\" width=\"982\" height=\"308\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/alt-l10n-inspector.png 982w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/alt-l10n-inspector-300x94.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/alt-l10n-inspector-768x241.png 768w\" sizes=\"(max-width: 982px) 100vw, 982px\" \/><figcaption id=\"caption-attachment-66752\" class=\"wp-caption-text\">The recipe image alt tag translated into English and Arabic<\/figcaption><\/figure>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Learn more about <code>intl.formatMessage()<\/code> in the <a href=\"https:\/\/formatjs.io\/docs\/react-intl\/api#formatmessage\">FormatJS API docs<\/a>.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"localizing-the-document-title\"><\/span>Localizing the document title<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Now that we know about <code>formatMessage()<\/code>, we can use it to translate our <code>&lt;html title&gt;<\/code> in our <code>useDocL10n()<\/code> custom hook.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-34\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/useDocL10n.js\n\n  import { useEffect } from \"react\";\n  import { useIntl } from \"react-intl\";\n  import { locales } from \".\/i18n-config\";\n\n  export function useDocL10n() {\n<span class=\"hljs-deletion\">-   const { locale } = useIntl();<\/span>\n<span class=\"hljs-addition\">+   const { locale, formatMessage } = useIntl();<\/span>\n\n    useEffect(() =&gt; {\n      document.dir = locales&#91;locale].dir;\n\n<span class=\"hljs-addition\">+     \/\/ Localize the &lt;html title&gt; attribute.<\/span>\n<span class=\"hljs-addition\">+     document.title = formatMessage({ id: \"app.title\" });<\/span>\n\n<span class=\"hljs-deletion\">-   }, &#91;locale]);<\/span>\n<span class=\"hljs-addition\">+   }, &#91;locale, formatMessage]);<\/span>\n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-34\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_4d6a847ba445cb0fba623da80b0672d4\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>With that, our document title is localized.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-add-dynamic-values-to-translation-messages\"><\/span>How do I add dynamic values to translation messages?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We often want to inject runtime values in a translation string. This could be the name of the logged-in user, for example. The ICU message format used by FormatJS makes this interpolation easy. Let\u2019s localize the author string of our recipe to demonstrate.<\/p>\n<p>First, we\u2019ll add new translation messages, and use the <code>{variable}<\/code> syntax for placeholders.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-35\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/lang\/en-US.json\n\n{\n  \/\/ ...\n  \"recipe.title\": \"Delightful Vegan Bean Burger\",\n  \"recipe.img_alt\": \"Vegan burger on a wooden plate\",\n<span class=\"hljs-addition\">+ \/\/ {author} and {publishedAt} will be<\/span>\n<span class=\"hljs-addition\">+ \/\/ replaced at runtime<\/span>\n<span class=\"hljs-addition\">+ \"recipe.author\": \"by {author} on {publishedAt}\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-35\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_347121fb1792f41eda66bed6d13a4d4d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-36\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/lang\/en-US.json\n\n{\n  \/\/ ...\n  \"recipe.title\": \"Delightful Vegan Bean Burger\",\n  \"recipe.img_alt\": \"Vegan burger on a wooden plate\",\n<span class=\"hljs-addition\">+ \/\/ {author} and {publishedAt} will be<\/span>\n<span class=\"hljs-addition\">+ \/\/ replaced at runtime<\/span>\n<span class=\"hljs-addition\">+ \"recipe.author\": \"by {author} on {publishedAt}\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-36\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_04dbec6d4fd1eeba78a0afd34b5cc914\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now we can use the <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">values<\/span><\/code> prop in <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">&lt;FormattedMessage&gt;<\/span><\/code> to interpolate these dynamic values at runtime.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-37\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/components\/Recipe.js\n\nimport { FormattedMessage, useIntl } from \"react-intl\";\n\/\/ ...\n\nexport default function Recipe() {\n  const intl = useIntl();\n\n  return (\n    &lt;main&gt;\n      {\/* ... *\/}\n      &lt;div&gt;\n        {\/* ... *\/}\n\n        &lt;div&gt;\n          &lt;div&gt;\n            &lt;p&gt;\n<span class=\"hljs-addition\">+             {\/* We specify the `values` we want to swap in<\/span>\n<span class=\"hljs-addition\">+                 via a key\/value map. *\/}<\/span>\n<span class=\"hljs-addition\">+             &lt;FormattedMessage<\/span>\n<span class=\"hljs-addition\">+               id=\"recipe.author\"<\/span>\n<span class=\"hljs-addition\">+               values={{ author: \"Rabia Mousa\", publishedAt: \"2023\/06\/20\" }}<\/span>\n<span class=\"hljs-addition\">+             \/&gt;<\/span>\n            &lt;\/p&gt;\n          &lt;\/div&gt;\n\n          {\/* ... *\/}\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/main&gt;\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-37\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_a3c3f9ef8a4bd0ed1cd2dd1264f1224a\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>That will do it.<\/p>\n<figure id=\"attachment_66758\" aria-describedby=\"caption-attachment-66758\" style=\"width: 572px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-66758 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/interpolation-screen-example.png\" alt=\"Interpolation screen example | Phrase\" width=\"572\" height=\"190\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/interpolation-screen-example.png 572w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/interpolation-screen-example-300x100.png 300w\" sizes=\"(max-width: 572px) 100vw, 572px\" \/><figcaption id=\"caption-attachment-66758\" class=\"wp-caption-text\">Interpolated values in English and Arabic messages<\/figcaption><\/figure>\n<p>\ud83d\uddd2\ufe0f <em>Note \u00bb<\/em> The date in Arabic looks off: It\u2019s injected as a hard-coded string and hasn\u2019t been localized. We\u2019ll fix it when we get to date formatting a bit later.<\/p>\n<p>Of course, the imperative <code>intl.formatMessage()<\/code> provides interpolation as well.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-38\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">&lt;p&gt;\n  {<span class=\"hljs-comment\">\/* The second parameter to `formatMessage` can be\n      a map for swapping in dynamic values. *\/<\/span>}\n  {intl.formatMessage(\n    { <span class=\"hljs-attr\">id<\/span>: <span class=\"hljs-string\">\"recipe.author\"<\/span> },\n    { <span class=\"hljs-attr\">author<\/span>: <span class=\"hljs-string\">\"Rabia Mousa\"<\/span>, <span class=\"hljs-attr\">publishedAt<\/span>: <span class=\"hljs-string\">\"2023\/06\/20\"<\/span> }\n  )}\n&lt;<span class=\"hljs-regexp\">\/p&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-38\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_4eedd12e33b925d7f298c9c9e2ea722d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<h2><span class=\"ez-toc-section\" id=\"how-do-i-work-with-plurals-in-translation-messages\"><\/span>How do I work with plurals in translation messages?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>\u201cYou have 1 new message.\u201d<\/p>\n<p>\u201cYou have 29 new messages.\u201d<\/p>\n<p>Ah, plurals. They\u2019re often mishandled in translation. It\u2019s important to realize that different languages have different plural <em>forms<\/em>. While English has two plural forms*,* <code>one<\/code> and <code>other<\/code>, other languages can have more. Arabic, for example, has six.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The <a href=\"https:\/\/www.unicode.org\/cldr\/charts\/42\/supplemental\/language_plural_rules.html\">CLDR Language Plural Rules chart<\/a> is a canonical source for languages\u2019 plural forms.<\/p>\n<p>Let\u2019s add a comment counter to our recipe to showcase localized plurals.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-66764 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/comment-counter.jpg\" alt=\"Comment counter on an app screen | Phrase\" width=\"1208\" height=\"612\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/comment-counter.jpg 1208w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/comment-counter-300x152.jpg 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/comment-counter-1024x519.jpg 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/comment-counter-768x389.jpg 768w\" sizes=\"(max-width: 1208px) 100vw, 1208px\" \/><\/p>\n<p>We\u2019ll add the English translation message first. The ICU message format has excellent support for plurals, using a special syntax:<\/p>\n<p>&nbsp;<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-39\" data-shcb-language-name=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">{count, plural, \n  one {# comment}\n  other {# comments}\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-39\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">plaintext<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">plaintext<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_96cd2f966819b63e95d1235dcc7edfb0\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>\u270b <em>Heads up \u00bb<\/em> The <code>other<\/code> form is always required.<\/p>\n<p>Of course, we need to add this message to our JSON language files. JSON doesn\u2019t support multiline strings, so our message ends up looking like the following.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-40\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/lang\/en-US.json\n\n{\n  \/\/ ...\n  \"recipe.author\": \"by {author}\",\n<span class=\"hljs-addition\">+ \"recipe.comment_count\": \"{count, plural, one {# comment} other {# comments}}\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-40\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_5040a92a7bf6bbf1d5675cd01712c50e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>The <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">count<\/span><\/code> variable in the message represents an integer that we can pass to <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">&lt;FormattedMessage&gt;<\/span><\/code> via its <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"5\">values<\/span><\/code> prop.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-41\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/components\/Recipe.js\n\nimport { FormattedMessage, useIntl } from \"react-intl\";\n\/\/ ...\n\nexport default function Recipe() {\n  \/\/ ...\n\n  return (\n    &lt;main&gt;\n      {\/* ... *\/}\n          &lt;div&gt;\n            &lt;p&gt;\u23f2\ufe0f 40min&lt;\/p&gt;\n            &lt;p&gt;\u2764\ufe0f 2291&lt;\/p&gt;\n\n<span class=\"hljs-addition\">+           &lt;p&gt;<\/span>\n<span class=\"hljs-addition\">+             &lt;FormattedMessage<\/span>\n<span class=\"hljs-addition\">+               id=\"recipe.comment_count\"<\/span>\n<span class=\"hljs-addition\">+               values={{ count: 419 }}<\/span>\n<span class=\"hljs-addition\">+<\/span>\n<span class=\"hljs-addition\">+            \/&gt;<\/span>\n<span class=\"hljs-addition\">+           &lt;\/p&gt;<\/span>\n          &lt;\/div&gt;\n\n      {\/* ... *\/}\n    &lt;\/main&gt;\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-41\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_1ac2ae3764e7ab8251177190ef9195bd\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>FormatJS uses the <code>count<\/code> variable to determine the plural form within the <code>recipe.comment_count<\/code> message. It also replaces instances of <code>#<\/code> with the value of <code>count<\/code> in the string. In English, this results in the following renders for <code>count<\/code> values of <code>0<\/code>, <code>1<\/code>, and <code>2<\/code>, respectively.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-66770\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/en-plurals.png\" alt=\"Plurals in English on an app UI screen | Phrase\" width=\"203\" height=\"219\" \/><\/p>\n<p>Our Arabic translation message is more complex because it includes the language\u2019s six plural forms.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-42\" data-shcb-language-name=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">{count, plural,\n  zero {\u0644\u0627 \u062a\u0648\u062c\u062f \u062a\u0639\u0644\u064a\u0642\u0627\u062a} \n  one {\u062a\u0639\u0644\u064a\u0642 #}\n  two {\u062a\u0639\u0644\u064a\u0642\u064a\u0646 #}\n  few {# \u062a\u0639\u0644\u064a\u0642\u0627\u062a}\n  many {# \u062a\u0639\u0644\u064a\u0642}\n  other {# \u062a\u0639\u0644\u064a\u0642}\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-42\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">plaintext<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">plaintext<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_69bba3a63dfb5484735fca7d2f7bef04\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Again, we need to squish it into one line for our JSON.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-43\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/lang\/ar-EG.json\n\n{\n  \/\/ ...\n  \"recipe.author\": \"\u0645\u0646 {author}\",\n<span class=\"hljs-addition\">+ \"recipe.comment_count\": \"{count, plural, zero {\u0644\u0627 \u062a\u0648\u062c\u062f \u062a\u0639\u0644\u064a\u0642\u0627\u062a} one {\u062a\u0639\u0644\u064a\u0642 #} two {\u062a\u0639\u0644\u064a\u0642\u064a\u0646 #} few {# \u062a\u0639\u0644\u064a\u0642\u0627\u062a} many {# \u062a\u0639\u0644\u064a\u0642} other {# \u062a\u0639\u0644\u064a\u0642}}\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-43\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_3c6543fde6ff30473669ac5e319c3ebb\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>\ud83d\udd17 <em>Resource \u00bb<\/em> I found a handy <a href=\"https:\/\/format-message.github.io\/icu-message-format-for-translators\/editor.html\">Online ICU Message Editor<\/a> that I used for testing my multiline plural strings before copying them into my JSON and removing newline characters.<\/p>\n<p>With our new translation in place, we get perfectly pluralized messages when we switch our active locale to Arabic. Also notice that the Arabic messages have Eastern Arabic numerals (\u0661\u060c \u0662\u060c \u0663), which is correct for the Arabic-Egypt locale.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone  wp-image-66776\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/ar-plurals.png\" alt=\"Plurals in Arabic on an app UI screen | Phrase\" width=\"246\" height=\"268\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/ar-plurals.png 417w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/ar-plurals-276x300.png 276w\" sizes=\"(max-width: 246px) 100vw, 246px\" \/><\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> <a href=\"https:\/\/phrase.com\/blog\/posts\/guide-to-the-icu-message-format\/\">The Missing Guide to the ICU Message Format<\/a> covers all the ins and outs of ICU message plurals. And our <a href=\"https:\/\/phrase.com\/blog\/posts\/number-localization\/\">Concise Guide to Number Localization<\/a> goes into detail regarding numeral systems.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-numbers\"><\/span>How do I localize numbers?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We\u2019ve just seen that we need to display numbers appropriately for the active locale. Not all locales use Western Arabic numerals (1, 2, 3). Bengali, for example, uses the\u00a0Bengali\u2013Assamese\u00a0numeral system (\u09e6,\u00a0\u09e7,\u00a0\u09e8,\u00a0\u09e9). Currency and percent symbols, large number separators (1,000,000), and more are <em>specific to each region<\/em>.<\/p>\n<p>\u270b <em>Heads up \u00bb<\/em> This is why it\u2019s important to use region-specific locale codes when working with numbers and dates. Use <code>en-US<\/code>, not <code>en<\/code>, for example.<\/p>\n<p>Luckily, react-intl\/FormatJS has good number localization support, which <a href=\"https:\/\/formatjs.io\/docs\/react-intl#runtime-requirements\">uses<\/a> the JavaScript standard <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/NumberFormat\">Intl.NumberFormat<\/a> under the hood. Let\u2019s make use of these react-intl features to localize the numbers in our React app.<\/p>\n<p>We\u2019ll start with the recipe \u201clikes\u201d counter.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone  wp-image-66785\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/likes-before-localization.png\" alt=\"Number of likes on an app UI screen before localization | Phrase\" width=\"595\" height=\"121\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/likes-before-localization.png 767w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/likes-before-localization-300x61.png 300w\" sizes=\"(max-width: 595px) 100vw, 595px\" \/><\/p>\n<p>Here we can keep it simple and use react-intl\u2019s <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;FormattedNumber&gt;<\/span><\/code> component.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-44\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/components\/Recipe.js\n\n<span class=\"hljs-deletion\">- import { FormattedMessage, useIntl } from \"react-intl\";<\/span>\n<span class=\"hljs-addition\">+ import { FormattedMessage, FormattedNumber, useIntl } from \"react-intl\";<\/span>\n\n  export default function Recipe() {\n    const intl = useIntl();\n\n    return (\n      &lt;main&gt;\n        {\/* ... *\/}\n\n            &lt;div&gt;\n              &lt;p&gt;\u23f2\ufe0f 40min&lt;\/p&gt;\n\n              &lt;p&gt;\n<span class=\"hljs-deletion\">-               \u2764\ufe0f 2291<\/span>\n<span class=\"hljs-addition\">+               \u2764\ufe0f &lt;FormattedNumber value={2291} \/&gt;<\/span>\n              &lt;\/p&gt;\n\n              &lt;p&gt;\n                &lt;FormattedMessage\n                  id=\"recipe.comment_count\"\n                  values={{ count: 419 }}\n                \/&gt;\n              &lt;\/p&gt;\n            &lt;\/div&gt;\n\n        {\/* ... *\/}\n      &lt;\/main&gt;\n    );\n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-44\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_fd311971a14efca05776950326d66075\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"0\">&lt;FormattedNumber&gt;<\/span><\/code> localizes its given number <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"2\">value<\/span><\/code> to the active locale.<\/p>\n<figure id=\"attachment_66791\" aria-describedby=\"caption-attachment-66791\" style=\"width: 240px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-66791 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/likes-after-localization.png\" alt=\"Number of likes on an app UI screen after localization (Arabic) | Phrase\" width=\"240\" height=\"110\" \/><figcaption id=\"caption-attachment-66791\" class=\"wp-caption-text\">Our likes counter showing Eastern Arabic numerals and the Arabic thousands-separator<\/figcaption><\/figure>\n<p>We can customize <code>&lt;FormattedNumber&gt;<\/code>&#8216;s output: Anything that can be passed to the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/NumberFormat\/NumberFormat#parameters\">Intl.NumberFormat constructor<\/a> <code>options<\/code> parameter can be passed as a prop to <code>&lt;FormattedNumber&gt;<\/code>. Here are some examples:<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-66800\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/number-formats.png\" alt=\"Number format overview | Phrase\" width=\"1362\" height=\"526\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/number-formats.png 1362w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/number-formats-300x116.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/number-formats-1024x395.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/number-formats-768x297.png 768w\" sizes=\"(max-width: 1362px) 100vw, 1362px\" \/><\/p>\n<p>We can pass the same options to react-intl\u2019s imperative equivalent, <code>intl.formatNumber()<\/code>.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-45\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">&lt;p&gt;  \n  {intl.formatNumber(<span class=\"hljs-number\">0.11<\/span>, { <span class=\"hljs-attr\">style<\/span>: <span class=\"hljs-string\">\"percent\"<\/span> })}\n&lt;<span class=\"hljs-regexp\">\/p&gt;\n{\/<\/span>* \n  en-US \u2192 <span class=\"hljs-string\">\"11%\"<\/span>\n  ar-EG \u2192 <span class=\"hljs-string\">\"\u066a\u0661\u0661\"<\/span>\n*<span class=\"hljs-regexp\">\/}<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-45\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_1b021de8d3892c63a625a47a03ba62b0\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>What about numbers interpolated in our translation messages? The ICU message syntax has us covered here.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-46\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ English<\/span>\n{\n  <span class=\"hljs-attr\">\"recipe.ingredient_price\"<\/span>: <span class=\"hljs-string\">\"Estimated cost {cost, number, ::currency\/USD}\"<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/ Arabic<\/span>\n{\n  <span class=\"hljs-attr\">\"recipe.ingredient_price\"<\/span>: <span class=\"hljs-string\">\"\u0627\u0644\u062a\u0643\u0644\u0641\u0629 \u0627\u0644\u062a\u0642\u062f\u064a\u0631\u064a\u0629 {cost, number, ::currency\/USD}\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-46\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_245d3433c3371d9f541f68b7b473044a\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>The syntax is similar to the plural syntax we used earlier. A last, optional, format designator after the <code>::<\/code> is passed to the <code>Intl.NumberFormat<\/code> constructor.<\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> The strange <code>currency\/USD<\/code> syntax is a part of the ICU syntax known as a <a href=\"https:\/\/unicode-org.github.io\/icu\/userguide\/format_parse\/numbers\/skeletons.html\">number skeleton<\/a>.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-47\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ In our components, given the above messages...<\/span>\n&lt;p&gt;\n  <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">FormattedMessage<\/span>\n    <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"recipe.ingredient_price\"<\/span>\n    <span class=\"hljs-attr\">values<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">cost:<\/span> <span class=\"hljs-attr\">18.42<\/span> }}\n  \/&gt;<\/span><\/span>\n&lt;<span class=\"hljs-regexp\">\/p&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-47\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_cba2cd7363a1e5dd4eeeb919dd4bbee4\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>The above would render the following in English and Arabic, respectively.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-66806 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/inline-number-formatting.png\" alt=\"Inline number formatting screen | Phrase\" width=\"292\" height=\"109\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/inline-number-formatting.png 571w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/inline-number-formatting-300x112.png 300w\" sizes=\"(max-width: 292px) 100vw, 292px\" \/><\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-dates\"><\/span>How do I localize dates?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Localizing dates with react-intl is very similar to localizing numbers. We use react-intl\u2019s <code>&lt;FormatteDate&gt;<\/code> and <code>intl.formatDate()<\/code>. In turn, react-intl uses the JavaScript standard <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/DateTimeFormat\">Intl.DateTimeFormat<\/a> under the hood to localize our dates.<\/p>\n<p>\u270b <em>Heads up \u00bb<\/em> Just like numbers, dates are <em>region-specific<\/em>. So favor locales with regions (<code>ar-EG<\/code>) over language-only locales (<code>ar<\/code>).<\/p>\n<p>Let\u2019s revisit our earlier interpolation example.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-48\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ src\/lang\/en-US.json<\/span>\n\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-attr\">\"recipe.author\"<\/span>: <span class=\"hljs-string\">\"by {author} on {publishedAt}\"<\/span>,\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-48\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_347121fb1792f41eda66bed6d13a4d4d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-49\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ src\/lang\/ar-EG.json<\/span>\n\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-attr\">\"recipe.author\"<\/span>: <span class=\"hljs-string\">\"\u0645\u0646 {author} \u0641\u064a {publishedAt}\"<\/span>,\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-49\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_347121fb1792f41eda66bed6d13a4d4d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-50\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/components\/Recipe.js<\/span>\n\n<span class=\"hljs-comment\">\/\/ ...<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">Recipe<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">main<\/span>&gt;<\/span>\n      {\/* ... *\/}\n\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n               <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">FormattedMessage<\/span>\n                 <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"recipe.author\"<\/span>\n                 <span class=\"hljs-attr\">values<\/span>=<span class=\"hljs-string\">{{<\/span> \n                   <span class=\"hljs-attr\">author:<\/span> \"<span class=\"hljs-attr\">Rabia<\/span> <span class=\"hljs-attr\">Mousa<\/span>\",\n                   <span class=\"hljs-attr\">publishedAt:<\/span> \"<span class=\"hljs-attr\">2023<\/span>\/<span class=\"hljs-attr\">06<\/span>\/<span class=\"hljs-attr\">20<\/span>\",\n                 }}\n               \/&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n\n      {\/* ... *\/}\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">main<\/span>&gt;<\/span><\/span>\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-50\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_565322407ad031e261aaa0a38bc73036\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>The above results in <code>publishedAt<\/code> rendering as \u201c2023\/06\/20\u201d in both English and Arabic. We can improve this by localizing the date value.<\/p>\n<p>First, let\u2019s update the <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">publishedAt<\/span><\/code> value itself, changing it to a <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">Date<\/span><\/code> object, which FormatJS needs for date localization.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-51\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/components\/Recipe.js\n\n\/\/ ...\n\nexport default function Recipe() {\n  \/\/ ...\n\n  return (\n    {\/* ... *\/}\n            &lt;p&gt;\n               &lt;FormattedMessage\n                 id=\"recipe.author\"\n                 values={{ \n                   author: \"Rabia Mousa\",\n<span class=\"hljs-deletion\">-                  publishedAt: \"2023\/06\/20\",<\/span>\n<span class=\"hljs-addition\">+                  \/\/ Remember, months are zero-indexed in<\/span>\n<span class=\"hljs-addition\">+                  \/\/ the `Date` constructor<\/span>\n<span class=\"hljs-addition\">+                  \/\/ ie. `0` is January.<\/span>\n<span class=\"hljs-addition\">+                  publishedAt: new Date(2023, 5, 20),<\/span>\n                 }}\n             \/&gt;\n            &lt;\/p&gt;\n    {\/* ... *\/}\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-51\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_3f6e871386667d118b89fea528abdb46\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now let\u2019s use the ICU message syntax to designate a medium-length date in our messages.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-52\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/lang\/en-US.json\n\n{\n  \/\/ ...\n<span class=\"hljs-deletion\">- \"recipe.author\": \"by {author} on {publishedAt}\",<\/span>\n<span class=\"hljs-addition\">+ \"recipe.author\": \"by {author} on {publishedAt, date, medium}\",<\/span>\n  \/\/ ...\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-52\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_347121fb1792f41eda66bed6d13a4d4d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-53\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/lang\/ar-EG.json\n\n{\n  \/\/ ...\n<span class=\"hljs-deletion\">- \"recipe.author\": \"\u0645\u0646 {author} \u0641\u064a {publishedAt}\",<\/span>\n<span class=\"hljs-addition\">+ \"recipe.author\": \"\u0645\u0646 {author} \u0641\u064a {publishedAt, date, medium}\",<\/span>\n  \/\/ ...\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-53\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_698230cb8491432d89eae5e987542187\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>With that, our date is localized.<\/p>\n<figure id=\"attachment_66812\" aria-describedby=\"caption-attachment-66812\" style=\"width: 400px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-66812\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/localized-dates.png\" alt=\"Localized dates on an app UI screen | Phrase\" width=\"400\" height=\"116\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/localized-dates.png 528w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/localized-dates-300x87.png 300w\" sizes=\"(max-width: 400px) 100vw, 400px\" \/><figcaption id=\"caption-attachment-66812\" class=\"wp-caption-text\">Interpolated date localized for English and Arabic, respectively<\/figcaption><\/figure>\n<p>Built-in <code>short<\/code>, <code>medium<\/code>, and <code>long<\/code> date formats are always available to us. We can also use ICU datetime skeletons to granularly control our formats. Let\u2019s say we wanted to remove the day from our recipe publishing date. ICU date skeletons come in handy here:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-54\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/lang\/en-US.json\n\n{\n  \/\/ ...\n<span class=\"hljs-deletion\">- \"recipe.author\": \"by {author} on {publishedAt, date, medium}\",<\/span>\n<span class=\"hljs-addition\">+ \/\/ Use month number (`M`) and two-digit year (`yy`) format.<\/span>\n<span class=\"hljs-addition\">+ \"recipe.author\": \"by {author} on {publishedAt, date, ::Myy}\",<\/span>\n  \/\/ ...\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-54\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_347121fb1792f41eda66bed6d13a4d4d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-55\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/lang\/ar-EG.json\n\n{\n  \/\/ ...\n<span class=\"hljs-deletion\">- \"recipe.author\": \"\u0645\u0646 {author} \u0641\u064a {publishedAt, date, medium}\",<\/span>\n<span class=\"hljs-addition\">+ \/\/ Using inline message formats allows us to format dates<\/span>\n<span class=\"hljs-addition\">+ \/\/ differently for each locale. Here we use the full name of<\/span>\n<span class=\"hljs-addition\">+ \/\/ the month (`MMM`) and the four-digit year (`yyyy`).<\/span>\n<span class=\"hljs-addition\">+ \"recipe.author\": \"\u0645\u0646 {author} \u0641\u064a {publishedAt, date, ::MMMyyyy}\",<\/span>\n  \/\/ ...\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-55\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_cab5ceaf9b7a74f447759890a3967292\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<figure id=\"attachment_66818\" aria-describedby=\"caption-attachment-66818\" style=\"width: 400px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-66818\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/date-skeletons.png\" alt=\"Date skeletons on an app UI screen | Phrase\" width=\"400\" height=\"156\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/date-skeletons.png 488w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/date-skeletons-300x117.png 300w\" sizes=\"(max-width: 400px) 100vw, 400px\" \/><figcaption id=\"caption-attachment-66818\" class=\"wp-caption-text\">English and Arabic translations, respectively, with custom date formats<\/figcaption><\/figure>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> <a href=\"https:\/\/formatjs.io\/docs\/core-concepts\/icu-syntax#supported-datetime-skeleton\">Find all the date skeletons FormatJS supports in the official docs<\/a>.<\/p>\n<p>What if we wanted to format a date outside of a translation message? We can use react-intl\u2019s <code>&lt;FormattedDate&gt;<\/code> component for that. Let\u2019s break our recipe\u2019s publish date out of the translation message.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-56\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/components\/Recipe.js\n\n\/\/ ...\n\nexport default function Recipe() {\n  \/\/ ...\n\n  return (\n    {\/* ... *\/}\n            &lt;p&gt;\n               &lt;FormattedMessage\n                 id=\"recipe.author\"\n                 values={{ \n                   author: \"Rabia Mousa\",\n<span class=\"hljs-deletion\">-                  \/\/ Of course, we need to remove<\/span>\n<span class=\"hljs-deletion\">-                  \/\/ `publishedAt` from our messages<\/span>\n<span class=\"hljs-deletion\">-                  \/\/ as well.<\/span>\n<span class=\"hljs-deletion\">-                  publishedAt: new Date(2023, 5, 20),<\/span>\n                 }}\n               \/&gt;\n            &lt;\/p&gt;\n<span class=\"hljs-addition\">+           &lt;p&gt;<\/span>\n<span class=\"hljs-addition\">+             &lt;FormattedDate<\/span>\n<span class=\"hljs-addition\">+               value={new Date(2023, 5, 20)}<\/span>\n<span class=\"hljs-addition\">+               dateStyle=\"short\"<\/span>\n<span class=\"hljs-addition\">+             \/&gt;<\/span>\n<span class=\"hljs-addition\">+           &lt;\/p&gt;<\/span>\n    {\/* ... *\/}\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-56\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_b0cd63a2af48e4fc4b7f0c480689e983\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Much like <code>&lt;FormattedNumber&gt;<\/code>, <code>&lt;FormattedDate&gt;<\/code> takes formatting props that match the <code>options<\/code> param <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/DateTimeFormat\/DateTimeFormat#parameters\">passed to the Intl.DateTimeFormat constructor<\/a>.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone  wp-image-66827\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/formatted-date.png\" alt=\"Formatted date on an app UI screen | Phrase\" width=\"504\" height=\"220\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/formatted-date.png 1105w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/formatted-date-300x131.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/formatted-date-1024x447.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/formatted-date-768x335.png 768w\" sizes=\"(max-width: 504px) 100vw, 504px\" \/><\/p>\n<p>And, of course, the imperative <code>intl.formatDate()<\/code> can be used to get the same result:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-57\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/components\/Recipe.js\n\n\/\/ ...\n\nexport default function Recipe() {\n  \/\/ ...\n\n  return (\n    {\/* ... *\/}\n            &lt;p&gt;\n<span class=\"hljs-deletion\">-             &lt;FormattedDate<\/span>\n<span class=\"hljs-deletion\">-               value={new Date(2023, 5, 20)}<\/span>\n<span class=\"hljs-deletion\">-               dateStyle=\"short\"<\/span>\n<span class=\"hljs-deletion\">-             \/&gt;<\/span>\n<span class=\"hljs-addition\">+             \/\/ Renders the same result as the above<\/span>\n<span class=\"hljs-addition\">+             {intl.formatDate(new Date(2023, 5, 20), {<\/span>\n<span class=\"hljs-addition\">+               dateStyle: \"short\",<\/span>\n<span class=\"hljs-addition\">+             })}<\/span>\n            &lt;\/p&gt;\n    {\/* ... *\/}\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-57\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_a767ea5dbe80962779a19437f72d05fe\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> react-intl offers many more datetime formatting options, including time, datetime range, relative time, and time zone support. See the <a href=\"https:\/\/formatjs.io\/docs\/react-intl\/components\">component<\/a> and <a href=\"https:\/\/formatjs.io\/docs\/react-intl\/api\">imperative<\/a> API docs for more info (the right-hand sidebars list the relevant components and functions, respectively).<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-load-translations-dynamically\"><\/span>How do I load translations dynamically?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Our current solution still isn\u2019t as scalable as it could be. If we had a hundred, or even twenty locales, we would load them all in our main JavaScript bundle. This would waste bandwidth and slow down our website.<\/p>\n<p>We can fix this. Let\u2019s refactor our translation message logic so that we only load messages for the active locale. We can do this by dynamically loading the appropriate message file once we\u2019ve determined the active locale.<\/p>\n<p>First, let\u2019s remove the static message file imports from our config file. We won\u2019t need them anymore.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-58\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/i18n-config.js\n\n<span class=\"hljs-deletion\">- import enMessages from \"..\/lang\/en-US.json\";<\/span>\n<span class=\"hljs-deletion\">- import arMessages from \"..\/lang\/ar-EG.json\";<\/span>\n\n  export const defaultLocale = \"en-US\";\n\n  export const locales = {\n    \"en-US\": {\n      name: \"English\",\n<span class=\"hljs-deletion\">-     messages: enMessages,<\/span>\n      dir: \"ltr\",\n    },\n    \"ar-EG\": {\n      name: \"Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)\",\n<span class=\"hljs-deletion\">-     messages: arMessages,<\/span>\n      dir: \"rtl\",\n    },\n  }; <\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-58\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_d09fc1eb223c26444ea5277984ff2af5\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Next, let\u2019s update our <code>&lt;I18n&gt;<\/code> wrapper component to load the appropriate translation file dynamically.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-59\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/I18n.js\n\n<span class=\"hljs-deletion\">- import { useState } from \"react\";<\/span>\n<span class=\"hljs-addition\">+ import { useEffect, useState } from \"react\";<\/span>\n  import { IntlProvider } from \"react-intl\";\n<span class=\"hljs-deletion\">- import { defaultLocale, locales } from \".\/i18n-config\";<\/span>\n<span class=\"hljs-addition\">+ import { defaultLocale } from \".\/i18n-config\";<\/span>\n  import { LocaleContext } from \".\/LocaleContext\";\n \n  export default function I18n(props) {\n    const &#91;locale, setLocale] = useState(defaultLocale);\n\n<span class=\"hljs-addition\">+   \/\/ We hold the loaded translation messages in <\/span>\n<span class=\"hljs-addition\">+   \/\/ a piece of state so we can set it in our<\/span>\n<span class=\"hljs-addition\">+   \/\/ useEffect and use it in our JSX.<\/span>\n<span class=\"hljs-addition\">+   const &#91;messages, setMessages] = useState(null);<\/span>\n \n<span class=\"hljs-addition\">+   useEffect(() =&gt; {<\/span>\n<span class=\"hljs-addition\">+     \/\/ Force a re-render that shows our<\/span>\n<span class=\"hljs-addition\">+     \/\/ loading indicator.<\/span>\n<span class=\"hljs-addition\">+     setMessages(null);<\/span>\n<span class=\"hljs-addition\">+<\/span>\n<span class=\"hljs-addition\">+     \/\/ Load the messages using Webpack code-splitting<\/span>\n<span class=\"hljs-addition\">+     \/\/ via dynamic import.<\/span>\n<span class=\"hljs-addition\">+     import(`..\/lang\/${locale}.json`)<\/span>\n<span class=\"hljs-addition\">+       .then((messages_) =&gt; setMessages(messages_))<\/span>\n<span class=\"hljs-addition\">+       .catch((err) =&gt;<\/span>\n<span class=\"hljs-addition\">+         console.error(`Error loading messages for locale ${locale}: `, err)<\/span>\n<span class=\"hljs-addition\">+       );<\/span>\n<span class=\"hljs-addition\">+   }, &#91;locale]);<\/span>\n\n<span class=\"hljs-deletion\">-   return (<\/span>\n<span class=\"hljs-addition\">+   return !messages ? (<\/span>\n<span class=\"hljs-addition\">+     &lt;p&gt;Loading...&lt;\/p&gt;<\/span>\n<span class=\"hljs-addition\">+   ) : (<\/span>\n      &lt;LocaleContext.Provider value={{ locale, setLocale }}&gt;\n        &lt;IntlProvider\n          locale={locale}\n          defaultLocale={defaultLocale}\n<span class=\"hljs-deletion\">-         messages={locales&#91;locale].messages}<\/span>\n<span class=\"hljs-addition\">+         messages={messages}<\/span>\n        &gt;\n          {props.children}\n        &lt;\/IntlProvider&gt;\n      &lt;\/LocaleContext.Provider&gt;\n   );\n }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-59\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_5737865666d8cd9fc65a0fdc10d36a3e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>If we open our browser dev tools and throttle our network speed to simulate a slow connection, we should be able to see our new loading state.<\/p>\n<p style=\"text-align: center;\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-66833 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/async-loading.gif\" alt=\"Asynchronous loading screen | Phrase\" width=\"600\" height=\"367\" \/><\/p>\n<p>Under the hood, the Webpack bundler that Create React App uses has <a href=\"https:\/\/webpack.js.org\/guides\/code-splitting\/\">turned our dynamic import into a chunk<\/a> that loads separately from the main bundle. We can see this if we open our browser <em>Network<\/em> tab.<\/p>\n<figure id=\"attachment_66839\" aria-describedby=\"caption-attachment-66839\" style=\"width: 1410px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-66839 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/async_init_load.png\" alt=\"Asynchronous initial loading | Phrase\" width=\"1410\" height=\"306\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/async_init_load.png 1410w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/async_init_load-300x65.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/async_init_load-1024x222.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/async_init_load-768x167.png 768w\" sizes=\"(max-width: 1410px) 100vw, 1410px\" \/><figcaption id=\"caption-attachment-66839\" class=\"wp-caption-text\">On initial load, our English (default) messages are loaded separately from the main bundle<\/figcaption><\/figure>\n<figure id=\"attachment_66845\" aria-describedby=\"caption-attachment-66845\" style=\"width: 1408px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-66845 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/async_ar_load.png\" alt=\"Async init loading screen (Arabic) | Phrase\" width=\"1408\" height=\"136\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/async_ar_load.png 1408w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/async_ar_load-300x29.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/async_ar_load-1024x99.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/async_ar_load-768x74.png 768w\" sizes=\"(max-width: 1408px) 100vw, 1408px\" \/><figcaption id=\"caption-attachment-66845\" class=\"wp-caption-text\">When we switch to Arabic, Webpack loads our Arabic translations dynamically from the network<\/figcaption><\/figure>\n<p>Our app works almost exactly as before, except now we can have as many locales as we want without bloating our main bundle.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-detect-the-users-preferred-language\"><\/span>How do I detect the user\u2019s preferred language?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Not everyone can read English, and a good i18n strategy should accommodate that. Our user\u2019s preferred languages are available to us through her browser\u2019s <code>navigator<\/code> object. We can use this to load our app shown in the supported language closest to the user\u2019s preference.<\/p>\n<p>Let\u2019s do this step-by-step. First, we\u2019ll write a helper that returns an array of the locales set in the browser.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-60\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/browser-locales.js<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">browserLocales<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> result = &#91;];\n\n  <span class=\"hljs-comment\">\/\/ List of languages the user set in their<\/span>\n  <span class=\"hljs-comment\">\/\/ browser settings.<\/span>\n  <span class=\"hljs-keyword\">if<\/span> (navigator.languages) {\n    <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">let<\/span> lang <span class=\"hljs-keyword\">of<\/span> navigator.languages) {\n      result.push(lang);\n    }\n  }\n\n  <span class=\"hljs-comment\">\/\/ UI language: language of browser and probably<\/span>\n  <span class=\"hljs-comment\">\/\/ operating system.<\/span>\n  <span class=\"hljs-keyword\">if<\/span> (navigator.language) {\n    result.push(navigator.language);\n  }\n\n  <span class=\"hljs-keyword\">return<\/span> result;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-60\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_50258b5dbfc581cde542ce6715aa9570\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><code>navigator.languages<\/code> corresponds to the list you can set in your own browser settings.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-67131\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/firefox_lang_options.png\" alt=\"Firefox language options | Phrase\" width=\"800\" height=\"439\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/firefox_lang_options.png 1330w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/firefox_lang_options-300x165.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/firefox_lang_options-1024x562.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/firefox_lang_options-768x422.png 768w\" sizes=\"(max-width: 800px) 100vw, 800px\" \/><\/p>\n<p>In case none are set in our user\u2019s browser, we fall back on <code>navigator.language<\/code>.<\/p>\n<p>We\u2019ll use the browser locale to match one of our app\u2019s supported locales. FormatJS has a handy locale matcher package, <a href=\"https:\/\/formatjs.io\/docs\/polyfills\/intl-localematcher\/\">intl-localematcher<\/a>, to help with just this. Let\u2019s install it from the command line.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-61\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm install @formatjs\/intl-localematcher<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-61\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_376011e0bdb648b293ce846e2558770b\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now we write a function that takes both the browser locales and our app\u2019s defined locales, and tries to find the closest match.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-62\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/user-locale.js<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { match } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@formatjs\/intl-localematcher\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { defaultLocale, locales } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/i18n-config\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { browserLocales } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/browser-locales\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">userLocale<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> appLocales = <span class=\"hljs-built_in\">Object<\/span>.keys(locales);\n  <span class=\"hljs-keyword\">return<\/span> match(browserLocales(), appLocales, defaultLocale);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-62\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_5dca893594c5da5410a0b3cbe767eb91\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>FormatJS\u2019s <code>match<\/code> is doing the heavy lifting here. Let\u2019s briefly go over how it works.<\/p>\n<p>Say we have Arabic-Morocco (<code>ar-MA<\/code>) and English-Canada (<code>en-CA<\/code>), in that order, as our preferred browser languages.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-67137\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/ar_ma_firefox_settings.png\" alt=\"Language settings for Arabic (Morocco) in Firefox | Phrase\" width=\"800\" height=\"432\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/ar_ma_firefox_settings.png 1312w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/ar_ma_firefox_settings-300x162.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/ar_ma_firefox_settings-1024x553.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/ar_ma_firefox_settings-768x414.png 768w\" sizes=\"(max-width: 800px) 100vw, 800px\" \/><\/p>\n<p>We know that our app\u2019s locales are Arabic-Egypt (<code>ar-EG<\/code>) and English-USA (<code>en-US<\/code>). So in our scenario, the <code>match()<\/code> call above is equivalent to the following.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-63\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"> \n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">userLocale<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> match(\n    &#91;<span class=\"hljs-string\">\"ar-MA\"<\/span>, <span class=\"hljs-string\">\"en-CA\"<\/span>, <span class=\"hljs-string\">\"ar-MA\"<\/span>], <span class=\"hljs-comment\">\/\/ browser locales<\/span>\n    &#91;<span class=\"hljs-string\">\"en-US\"<\/span>, <span class=\"hljs-string\">\"ar-EG\"<\/span>],          <span class=\"hljs-comment\">\/\/ app locales<\/span>\n    <span class=\"hljs-string\">\"en-US\"<\/span>                      <span class=\"hljs-comment\">\/\/ default locale<\/span>\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-63\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_c8ba524baab6f12ae4a4cad4a7dd598a\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><code>match<\/code> looks at the browser locales and realizes that, ideally, our user wants content served in Arabic for the Morocco region. It checks against the second list and sees that while we don\u2019t have that exact flavor of Arabic, we <em>do<\/em> have Egyptian Arabic. That\u2019s the best our app can do, so <code>match<\/code> returns <code>ar-EG<\/code>.<\/p>\n<p>\ud83d\uddd2\ufe0f <em>Note \u00bb<\/em> When all else fails, <code>match<\/code> returns the value of its last param, the default locale.<\/p>\n<p>Alright, let\u2019s make use of this logic by wiring it up to the <code>&lt;I18n&gt;<\/code> component.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-64\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/I18n.js\n\n  import { useEffect, useState } from \"react\";\n  import { IntlProvider } from \"react-intl\";\n  import { defaultLocale } from \".\/i18n-config\";\n  import { LocaleContext } from \".\/LocaleContext\";\n<span class=\"hljs-addition\">+ import { userLocale } from \".\/user-locale\";<\/span>\n\n  export default function I18n(props) {\n<span class=\"hljs-deletion\">-   const &#91;locale, setLocale] = useState(defaultLocale);<\/span>\n<span class=\"hljs-addition\">+   const &#91;locale, setLocale] = useState(userLocale());<\/span>\n    const &#91;messages, setMessages] = useState(null);\n\n    \/\/ ...\n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-64\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_c3cf5f3f6c3d7a3911798a9c19abfab6\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now when a user visits our app, they\u2019ll see it translated in a locale that\u2019s closest to their preferred language.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Learn more in <a href=\"https:\/\/phrase.com\/blog\/posts\/detecting-a-users-locale\/\">Detecting a User\u2019s Locale in a Web App<\/a>.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"storing-the-user-selected-locale\"><\/span>Storing the user-selected locale<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>What if the user wants to pick a language themselves, overriding the one we matched for them? We already have a language selector to allow them to do that. The problem now is that every time they visit our app, we\u2019ll <em>always<\/em> use the detected\/matched locale, ignoring any selection the user made in a previous visit.<\/p>\n<p>We can fix this by utilizing the browser\u2019s <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Window\/localStorage\">localStorage<\/a> (that\u2019s <em>local<\/em> not <em>locale<\/em>). <code>localStorage<\/code> allows us to store key\/value pairs on the browser that we can retrieve on future site visits. Let\u2019s add a simple module to store the user\u2019s selected locale.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-65\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/stored-locale.js<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> K_LOCALE = <span class=\"hljs-string\">\"locale\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Retrieve the locale persisted in the browser.<\/span>\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">getStoredLocale<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> localStorage.getItem(K_LOCALE);\n}\n\n<span class=\"hljs-comment\">\/\/ Persist the given locale in the browser.<\/span>\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">setStoredLocale<\/span>(<span class=\"hljs-params\">newLocale<\/span>) <\/span>{\n  localStorage.setItem(K_LOCALE, newLocale);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-65\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_dc1a02bdd4aeabff013b800924a11722\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now let\u2019s make sure that if a user selects a locale using our language switcher, it gets stored for later retrieval.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-66\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/LangSwitcher.js\n\n  import { useContext } from \"react\";\n  import { locales } from \".\/i18n-config\";\n  import { LocaleContext } from \".\/LocaleContext\";\n<span class=\"hljs-addition\">+ import { setStoredLocale } from \".\/stored-locale\";<\/span>\n\n  export default function LangSwitcher({ onLangChanged }) {\n    const { locale, setLocale } = useContext(LocaleContext);\n\n    return (\n      &lt;div&gt;\n\t\t\t\t{\/* ... *\/}\n        &lt;select\n          value={locale}\n          onChange={(e) =&gt; {\n            setLocale(e.target.value);\n<span class=\"hljs-addition\">+           setStoredLocale(e.target.value);<\/span>\n          }}\n        &gt;\n        {Object.keys(locales).map((loc) =&gt; (\n          &lt;option value={loc} key={loc}&gt;\n            {locales&#91;loc].name}\n          &lt;\/option&gt;\n        ))}\n      &lt;\/select&gt;\n    &lt;\/div&gt;\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-66\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_f726c2ef0136e398b7a22be8512b2c61\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Finally, let\u2019s update our <code>userLocale()<\/code> function to return the stored locale if it finds it.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-67\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/user-locale.js\n\n  import { match } from \"@formatjs\/intl-localematcher\";\n  import { defaultLocale, locales } from \".\/i18n-config\";\n  import { browserLocales } from \".\/browser-locales\";\n<span class=\"hljs-addition\">+ import { getStoredLocale } from \".\/stored-locale\";<\/span>\n\n  export function userLocale() {\n<span class=\"hljs-addition\">+   const storedLocale = getStoredLocale();<\/span>\n<span class=\"hljs-addition\">+   if (storedLocale) return storedLocale;<\/span>\n\n    const appLocales = Object.keys(locales);\n    return match(browserLocales(), appLocales, defaultLocale);\n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-67\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_83c424b1fd975e0402d54b5dd9ea79cc\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>That\u2019s it! Now if the user selects a locale from our language switcher, it will always be used on future visits, regardless of their browser settings.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-include-html-in-my-translation-messages\"><\/span>How do I include HTML in my translation messages?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Sometimes we want to embed links or special styles <em>within<\/em> our translation messages. For example:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-68\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ In a component<\/span>\n\n&lt;p&gt;\n  Try our &lt;a href=<span class=\"hljs-string\">\"\/trial-landing\"<\/span>&gt;premium recipes&lt;<span class=\"hljs-regexp\">\/a&gt; for free!\n&lt;\/<\/span>p&gt;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-68\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_30a9e9df94f5dc14d6a4f4b7696e08e7\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Luckily, react-intl makes it easy to embed markup in our translation messages using <a href=\"https:\/\/formatjs.io\/docs\/react-intl\/components\/#rich-text-formatting\">rich text formatting<\/a>. Here\u2019s how we can use it to translate the above message:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-69\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ English translation<\/span>\n{ \n  <span class=\"hljs-comment\">\/\/ Note the embedded &lt;a&gt; tag.<\/span>\n  <span class=\"hljs-attr\">\"trial\"<\/span>: <span class=\"hljs-string\">\"Try our &lt;a&gt;premium recipes&lt;\/a&gt; for free!\"<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/ Arabic translation<\/span>\n{\n  <span class=\"hljs-attr\">\"trial\"<\/span>: <span class=\"hljs-string\">\"\u062c\u0631\u0628 &lt;a&gt;\u0648\u0635\u0641\u0627\u062a\u0646\u0627 \u0627\u0644\u0645\u0645\u064a\u0632\u0629&lt;\/a&gt; \u0645\u062c\u0627\u0646\u064b\u0627!\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-69\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_347121fb1792f41eda66bed6d13a4d4d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-70\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ In our component<\/span>\n\n<span class=\"hljs-comment\">\/\/ We match the &lt;a&gt; with an `a` in our `values` object.<\/span>\n&lt;p&gt;\n  <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">FormattedMessage<\/span>\n    <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"trial\"<\/span>\n    <span class=\"hljs-attr\">values<\/span>=<span class=\"hljs-string\">{{<\/span>\n      \/\/ `<span class=\"hljs-attr\">a<\/span>` <span class=\"hljs-attr\">is<\/span> <span class=\"hljs-attr\">a<\/span> <span class=\"hljs-attr\">function<\/span> <span class=\"hljs-attr\">that<\/span> <span class=\"hljs-attr\">takes<\/span> <span class=\"hljs-attr\">a<\/span> <span class=\"hljs-attr\">param<\/span> \n      \/\/ <span class=\"hljs-attr\">containing<\/span> <span class=\"hljs-attr\">what<\/span> <span class=\"hljs-attr\">is<\/span> *<span class=\"hljs-attr\">inside<\/span>* &lt;<span class=\"hljs-attr\">a<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">a<\/span>&gt;<\/span>\n      \/\/ in our message, and returns valid JSX.\n      a: (chunks) =&gt; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">a<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\"\/trial-landing\"<\/span>&gt;<\/span>{chunks}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">a<\/span>&gt;<\/span>\n    }}\n  \/&gt;<\/span>\n&lt;<span class=\"hljs-regexp\">\/p&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-70\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_b58dc8f655699473d8da3007e0bffbfb\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>The above would render:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-71\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-comment\">&lt;!-- English --&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n  Try our <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">a<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\"\/trial-landing\"<\/span>&gt;<\/span>premium recipes<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">a<\/span>&gt;<\/span> for free!\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n\n<span class=\"hljs-comment\">&lt;!-- Arabic --&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n  \u062c\u0631\u0628 <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">a<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\"\/trial-landing\"<\/span>&gt;<\/span>\u0648\u0635\u0641\u0627\u062a\u0646\u0627 \u0627\u0644\u0645\u0645\u064a\u0632\u0629<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">a<\/span>&gt;<\/span> \u0645\u062c\u0627\u0646\u064b\u0627!\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-71\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_2ef53dbc3af32ada555bb7284dac8b84\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>We can have as many embedded tags in our messages as we want, as long as they have corresponding resolver functions in <code>FormattedMessage<\/code>&#8216;s <code>value<\/code> prop.<\/p>\n<p>And, of course, we can use rich text formatting in <code>intl.formattedMessage()<\/code> as well.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-72\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ Works just like &lt;FormattedMessage&gt;<\/span>\n&lt;p&gt;\n  {intl.formatMessage(\n    { <span class=\"hljs-attr\">id<\/span>: <span class=\"hljs-string\">\"trial\"<\/span> },\n    {\n       <span class=\"hljs-attr\">a<\/span>: <span class=\"hljs-function\">(<span class=\"hljs-params\">chunks<\/span>) =&gt;<\/span> <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">a<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\"\/trial-landing\"<\/span>&gt;<\/span>{chunks}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">a<\/span>&gt;<\/span><\/span>,\n    }\n  )}\n&lt;<span class=\"hljs-regexp\">\/p&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-72\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_1f381caf46fe49eee64838579201e867\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Read more about rich text formatting in the <a href=\"https:\/\/formatjs.io\/docs\/react-intl\/components\/#rich-text-formatting\">official API documentation.<\/a><\/p>\n<p>\ud83d\uddd2\ufe0f <em>Note \u00bb<\/em> In earlier versions of react-intl, rich text formatting was achieved with a <code>&lt;FormattedHtmlMessage&gt;<\/code> component. This has been deprecated in favor of the above solution.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-extract-translations-from-my-app\"><\/span>How do I extract translations from my app?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>So far, we\u2019ve covered a workflow where we manually set messages in translation files and reference them by ID in our components. As our app scales, this approach can become cumbersome, as we have to manually maintain increasingly large translation files.<\/p>\n<p>As our app scales, we can automate this process. Let\u2019s start with message extraction. Instead of placing messages in translation files ourselves, we can have the FormatJS CLI do it for us.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"additional-packages-used\"><\/span>Additional packages used<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>For our extraction workflow, we will use the following NPM packages:<\/p>\n<table style=\"border-collapse: collapse; width: 100%;\">\n<thead>\n<tr>\n<td style=\"width: 33.3333%;\">Library<\/td>\n<td style=\"width: 33.3333%;\">Version used<\/td>\n<td style=\"width: 33.3333%;\">Description<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"width: 33.3333%;\">@formatjs\/cli@6.1.3<\/td>\n<td style=\"width: 33.3333%;\">6.1.3<\/td>\n<td style=\"width: 33.3333%;\">CLI used to extract and compile translations<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.3333%;\">babel-plugin-formatjs<\/td>\n<td style=\"width: 33.3333%;\">10.5.3<\/td>\n<td style=\"width: 33.3333%;\">Auto-generates message IDs in our app builds<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.3333%;\">react-app-rewired<\/td>\n<td style=\"width: 33.3333%;\">2.2.1<\/td>\n<td style=\"width: 33.3333%;\">Allows overriding Create React App webpack config (for including babel-plugin-formatjs)<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.3333%;\">customize-cra<\/td>\n<td style=\"width: 33.3333%;\">1.0.0<\/td>\n<td style=\"width: 33.3333%;\">Allows overriding Create React App webpack config (for including babel-plugin-formatjs)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>We\u2019ll start by installing the FormatJS CLI, which will allow us to extract our messages. Running the following command from the command line should do it.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-73\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm install --save-dev @formatjs\/cli<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-73\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_b99c53ed1b7f80225ccd7b52b2bc1bd2\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Let\u2019s add a new script to our <code>package.json<\/code> file to make our lives easier.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-74\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ package.json\n\n{\n  \"name\": \"i18n-demo\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \/\/ ...\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\",\n<span class=\"hljs-addition\">+   \"extract\": \"formatjs extract 'src\/**\/*.js*' --out-file src\/lang\/en-US.json --ignore 'src\/lang\/**\/*' --format simple\",<\/span>\n  },\n\n  \/\/ ...\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-74\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_95318dcfd1e1555ba72b10e9b7c4e69b\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Our new <code>extract<\/code> script will use the FormatJS CLI to pull messages out of the <code>.js<\/code> and <code>.jsx<\/code> files in our source directory. The extracted messages will go into our source language file, <code>src\/lang\/en-US.json<\/code>.<\/p>\n<p>\u270b <em>Heads up \u00bb<\/em> To avoid errors during extraction, we use the <code>--ignore<\/code> flag to tell the CLI to skip our translations (<code>lang<\/code>) directory, when looking for messages to extract.<\/p>\n<p>Designating <code>--format simple<\/code> keeps the outputted <code>en-US.json<\/code> file in the <code>{\"id\": \"translation\"}<\/code> format we\u2019ve been using so far. You can use the default FormatJS format by omitting the <code>--format<\/code> option. We\u2019ll use the simple format in this tutorial.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Read about message extraction in the<a href=\"https:\/\/formatjs.io\/docs\/getting-started\/message-extraction\/\"> official docs<\/a>. You might also find the <a href=\"https:\/\/formatjs.io\/docs\/tooling\/cli\">CLI documentation<\/a> handy.<\/p>\n<p>If we run our extraction command now, <code>en-US.json<\/code> will contain ID keys and empty translation messages. That\u2019s because, with an extraction workflow, we need to add our source\/default (English) messages directly to our components. Let\u2019s update our <code>Header<\/code> component to demonstrate.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-75\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/components\/Header.js\n\nimport { FormattedMessage, useIntl } from \"react-intl\";\nimport LangSwitcher from \"..\/i18n\/LangSwitcher\";\n\nexport default function Header() {\n  const intl = useIntl();\n\n  return (\n    &lt;header&gt;\n      &lt;div&gt;\n        &lt;img\n          alt={intl.formatMessage({\n            id: \"app.logo_alt\",\n<span class=\"hljs-addition\">+           defaultMessage: \"Yomtaba logo\",<\/span>\n          })}\n          src=\"\/noun-recipe-2701716.svg\"\n        \/&gt;\n        &lt;h1&gt;\n<span class=\"hljs-deletion\">-         &lt;FormattedMessage id=\"app.title\" \/&gt;<\/span>\n<span class=\"hljs-addition\">+         &lt;FormattedMessage id=\"app.title\" defaultMessage=\"Yomtaba\" \/&gt;<\/span>\n        &lt;\/h1&gt;\n        \u00b7\n        &lt;h2&gt;\n<span class=\"hljs-deletion\">-         &lt;FormattedMessage id=\"app.tagline\" \/&gt;<\/span>\n<span class=\"hljs-addition\">+         &lt;FormattedMessage<\/span>\n<span class=\"hljs-addition\">+           id=\"app.tagline\"<\/span>\n<span class=\"hljs-addition\">+           defaultMessage=\"recipe of the day\"<\/span>\n<span class=\"hljs-addition\">+         \/&gt;<\/span>\n        &lt;\/h2&gt;\n      &lt;\/div&gt;\n\n      &lt;LangSwitcher \/&gt;\n    &lt;\/header&gt;\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-75\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_59a3c0b73a9b439c4f91abe6b581fc6d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>The <code>defaultMessage<\/code>s we pass to <code>formatMessage()<\/code> and <code>FormattedMessage<\/code> are what the <code>extract<\/code> command will pull from our source code. They should be in the app\u2019s default locale, English in our case.<\/p>\n<p>\ud83d\uddd2\ufe0f <em>Note \u00bb<\/em> When a translation is missing from the active locale, the contents of <code>defaultMessage<\/code> will be shown as a fallback.<\/p>\n<p>\u270b <em>Heads up \u00bb<\/em> We need to make sure that the <code>app.title<\/code> declaration in our <code>useDocL10n()<\/code> hook matches the one in the <code>Header<\/code> component. Otherwise, we\u2019ll get a warning that the messages don\u2019t match when we try to extract (and an empty message in our translation file).<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-76\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/useDocL10n.js\n\nimport { useEffect } from \"react\";\nimport { useIntl } from \"react-intl\";\nimport { locales } from \".\/i18n-config\";\n\nexport function useDocL10n() {\n  const { locale, formatMessage } = useIntl();\n\n  useEffect(() =&gt; {\n    document.dir = locales&#91;locale].dir;\n<span class=\"hljs-deletion\">-   document.title = formatMessage({ id: \"app.title\" });<\/span>\n<span class=\"hljs-addition\">+   document.title = formatMessage({<\/span>\n<span class=\"hljs-addition\">+     id: \"app.title\",<\/span>\n<span class=\"hljs-addition\">+     defaultMessage: \"Yomtaba\",<\/span>\n<span class=\"hljs-addition\">+   });<\/span>\n  }, &#91;locale, formatMessage]);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-76\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_85e2deb745245d03c4ce87adf2b64d10\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now let\u2019s run our new command from the command line.<\/p>\n<p>\u270b <em>Heads up \u00bb<\/em> Please make sure that every translation message in your app has a <code>defaultMessage<\/code> before running <code>extract<\/code>, or you will lose its translation in your source locale file.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-77\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm run extract<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-77\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_775c38d6a9109b754767b0994bc1d122\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>This should have overwritten our <code>src\/lang\/en-US.json<\/code> file, which should largely unchanged.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-78\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ src\/lang\/en-US.json<\/span>\n\n{\n  <span class=\"hljs-attr\">\"app.logo_alt\"<\/span>: <span class=\"hljs-string\">\"Yomtaba logo\"<\/span>,\n  <span class=\"hljs-attr\">\"app.tagline\"<\/span>: <span class=\"hljs-string\">\"recipe of the day\"<\/span>,\n  <span class=\"hljs-attr\">\"app.title\"<\/span>: <span class=\"hljs-string\">\"Yomtaba\"<\/span>\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-78\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_de8130b9c4cd36d058562d594351e9a8\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Except now we don\u2019t have to maintain it manually. We can add and update translation messages in our app and <code>npm run extract<\/code> to update the translation file.<\/p>\n<p>\u270b <em>Heads up \u00bb<\/em> The built-in react-intl React components will work fine with <code>extract<\/code>, as well as <code>intl.formatMessage()<\/code>. However, if you use any other components or functions for translation (like your own wrappers) you need to declare them using the CLI\u2019s <a href=\"https:\/\/formatjs.io\/docs\/tooling\/cli#--additional-function-names-comma-separated-names\">additional-function-names<\/a> or <a href=\"https:\/\/formatjs.io\/docs\/tooling\/cli#--additional-component-names-comma-separated-names\">additional-component-names<\/a> options.<\/p>\n<p>Of course, the translation workflow remains the same from here. We copy the <code>en-US.json<\/code> file to <code>ar-EG.json<\/code>, have a translator add the Arabic, and run the app as usual.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The <a href=\"https:\/\/github.com\/PhraseApp-Blog\/react-intl-formatjs-2023\/tree\/main\/i18n-demo-extraction\">i18n-demo-extraction directory in our GitHub repo<\/a> has all of the code that we cover in this section.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"auto-generating-ids\"><\/span>Auto-generating IDs<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>We can further streamline our i18n process by removing message IDs from our source code entirely. Instead, FormatJS can automatically generate message IDs and use them under the hood.<\/p>\n<p>\ud83d\uddd2\ufe0f <em>Note \u00bb<\/em> FormatJS uses a hashing algorithm to generate message IDs that look like <code>\"7pboUV\"<\/code>. It needs to be able to run the same algorithm when our app builds or runs in development so that it always generates the same ID for a given message. We\u2019ll see all this in action as we go.<\/p>\n<p>First, let\u2019s remove the explicit custom IDs from our message definitions.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-79\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/components\/Header.js\n\nimport { FormattedMessage, useIntl } from \"react-intl\";\nimport LangSwitcher from \"..\/i18n\/LangSwitcher\";\n\nexport default function Header() {\n  const intl = useIntl();\n\n  return (\n    &lt;header&gt;\n      &lt;div&gt;\n        &lt;img\n          alt={intl.formatMessage({\n<span class=\"hljs-deletion\">-           id: \"app.logo_alt\",<\/span>\n            defaultMessage: \"Yomtaba logo\",\n          })}\n          src=\"\/noun-recipe-2701716.svg\"\n        \/&gt;\n        &lt;h1&gt;\n          &lt;FormattedMessage \n<span class=\"hljs-deletion\">-           id=\"app.title\"<\/span>\n            defaultMessage=\"Yomtaba\"\n          \/&gt;\n        &lt;\/h1&gt;\n        \u00b7\n        &lt;h2&gt;\n          &lt;FormattedMessage\n<span class=\"hljs-deletion\">-           id=\"app.tagline\"<\/span>\n            defaultMessage=\"recipe of the day\"\n          \/&gt;\n        &lt;\/h2&gt;\n      &lt;\/div&gt;\n\n      &lt;LangSwitcher \/&gt;\n    &lt;\/header&gt;\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-79\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_8659678b79d9e468cea7b2c156cf6753\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Next, let\u2019s modify the <code>extract<\/code> command so that it auto-generates message IDs.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-80\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ package.json \n\n{\n  \"name\": \"i18n-demo\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \/\/ ...\n  },\n  \"scripts\": {\n    \"start\": \"react-app-rewired start\",\n    \"build\": \"react-app-rewired build\",\n    \"test\": \"react-app-rewired test\",\n    \"eject\": \"react-scripts eject\",\n<span class=\"hljs-deletion\">-   \"extract\": \"formatjs extract 'src\/**\/*.js*' --out-file src\/lang\/en-US.json --ignore 'src\/lang\/**\/*' --format simple\"<\/span>\n<span class=\"hljs-addition\">+   \"extract\": \"formatjs extract 'src\/**\/*.js*' --out-file src\/lang\/en-US.json --ignore 'src\/lang\/**\/*' --format simple --id-interpolation-pattern '&#91;sha512:contenthash:base64:6]'\"<\/span>\n  },\n  \/\/ ...\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-80\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_32ac470a6760a4cbd64fe0ae2043eaf8\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>The <code>--id-interpolation-pattern<\/code> option tells the <code>extract<\/code> command to generate a <a href=\"https:\/\/en.wikipedia.org\/wiki\/SHA-2\">SHA-512 hash<\/a> as an ID for any message that doesn\u2019t have an explicit ID. (We don\u2019t have to worry too much about the specifics of the hashing algorithm for our purposes).<\/p>\n<p>Let\u2019s run the <code>npm run extract<\/code> command to see the output. If we look at our English translation file now, we\u2019ll see auto-generated IDs.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-81\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ src\/lang\/en-US.json<\/span>\n\n{\n  <span class=\"hljs-attr\">\"KDE5tg\"<\/span>: <span class=\"hljs-string\">\"Yomtaba\"<\/span>,\n  <span class=\"hljs-attr\">\"LwQO24\"<\/span>: <span class=\"hljs-string\">\"recipe of the day\"<\/span>,\n  <span class=\"hljs-attr\">\"ykciPr\"<\/span>: <span class=\"hljs-string\">\"Yomtaba logo\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-81\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_182772aa557b64cdd2d428c4a957924d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>If we run our app now, we\u2019ll get missing translation errors: FormatJS isn\u2019t using its hashing algorithm at runtime, so it doesn\u2019t know how to connect a message to its translation in a translation file.<\/p>\n<p>To fix this, we need to use FormatJS\u2019s Babel plugin, which means we need to update the Webpack configuration of our app. Create React App hides this configuration by default, so we\u2019ll use <a href=\"https:\/\/github.com\/timarney\/react-app-rewired\/\">react-app-rewired<\/a> and <a href=\"https:\/\/github.com\/arackaf\/customize-cra\">customize-cra<\/a> to hack into it.<\/p>\n<p>\u270b <em>Heads up \u00bb<\/em> Create React App maintains its own build scripts to keep you focused on building your app. When you circumvent and update CRA\u2019s build scripts, you\u2019re responsible for maintaining these updates.<\/p>\n<p>First, we\u2019ll install the packages.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-82\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm install --save-dev babel-plugin-formatjs react-app-rewired customize-cra<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-82\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_49188209ec7554bdc8ecc2887b9edd20\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Next, let\u2019s create a <code>.babelrc<\/code> file to tell Babel to use the FormatJS plugin.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-83\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ .babelrc<\/span>\n\n{\n  <span class=\"hljs-attr\">\"plugins\"<\/span>: &#91;\n    &#91;\n      <span class=\"hljs-string\">\"formatjs\"<\/span>,\n      {\n        <span class=\"hljs-attr\">\"idInterpolationPattern\"<\/span>: <span class=\"hljs-string\">\"&#91;sha512:contenthash:base64:6]\"<\/span>,\n        <span class=\"hljs-attr\">\"ast\"<\/span>: <span class=\"hljs-literal\">true<\/span>\n      }\n    ]\n  ]\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-83\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_8ebb49cfa874b427010f2e16b2bf130a\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now let\u2019s create a <code>config-overrides.js<\/code> file; we\u2019ll use it to configure react-app-rewired so that it feeds our <code>.babelrc<\/code> to Babel when running our app.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-84\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ config-overrides.js<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> { useBabelRc, override } = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">\"customize-cra\"<\/span>);\n\n<span class=\"hljs-built_in\">module<\/span>.exports = override(useBabelRc());<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-84\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_9368919cc97a61548262a15dc8edcfe6\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Our last step here is to bypass Create React App\u2019s stock scripts and use the ones supplied by react-app-rewired.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-85\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ package.json\n\n{\n  \"name\": \"i18n-demo\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \/\/ ...\n  },\n  \"scripts\": {\n<span class=\"hljs-deletion\">-   \"start\": \"react-scripts start\",<\/span>\n<span class=\"hljs-addition\">+   \"start\": \"react-app-rewired start\",<\/span>\n<span class=\"hljs-deletion\">-   \"build\": \"react-scripts build\",<\/span>\n<span class=\"hljs-addition\">+   \"build\": \"react-app-rewired build\",<\/span>\n<span class=\"hljs-deletion\">-   \"test\": \"react-scripts test\",<\/span>\n<span class=\"hljs-addition\">+   \"test\": \"react-app-rewired test\",<\/span>\n    \"eject\": \"react-scripts eject\",\n    \"extract\": \"formatjs extract 'src\/**\/*.js*' --out-file src\/lang\/en-US.json --ignore 'src\/lang\/**\/*' --format simple --id-interpolation-pattern '&#91;sha512:contenthash:base64:6]'\"\n  },\n  \/\/ ...\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-85\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_037ec2105cfd10da55aaa0db1b395ab5\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now when we run our app, FormatJS runs its hash algorithm to re-generate its IDs, and our app works without errors.<\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> We can also compile messages before distributing our app. This has a few benefits, including potentially speeding up our app. Read the <a href=\"https:\/\/formatjs.io\/docs\/getting-started\/message-distribution\">FormatJS Message Distribution<\/a> for more info on compiling.<\/p>\n<p>Streamlining our localization process allows us to focus on the creative code of our app. With one command, we can create a translation source file to hand off to our translators, who can in turn return translations in ten languages, or a hundred.<\/p>\n<p>And instead of maintaining somewhat cryptic message IDs, <code>defaultMessage<\/code>s tell us exactly what a string in our UI says and means.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-86\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ In our components<\/span>\n\n&lt;a href=<span class=\"hljs-string\">\"\/new-ingredient\"<\/span>&gt;\n  {<span class=\"hljs-comment\">\/* Default messages make UI strings clear,\n      and not maintaining IDs or translation files\n      means we work more quickly. *\/<\/span>}\n  &lt;FormattedMessage defaultMessage=<span class=\"hljs-string\">\"Add ingredient\"<\/span> \/&gt;\n&lt;<span class=\"hljs-regexp\">\/a&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-86\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_71a88a525fbf9c2812dc96ad72e5a0aa\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<h2><span class=\"ez-toc-section\" id=\"how-do-i-integrate-react-intl-with-phrase-strings\"><\/span>How do I integrate react-intl with Phrase Strings?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We can make our jobs, and those of our translators, even easier by adopting a software localization platform like Phrase Strings. A single CLI command can push our latest source translations to Phrase Strings, where our translators can use a powerful translation admin panel to translate our app into many locales. With another command, we can pull the updated translations into our app. This keeps us focused on building our app, and not on the localization.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"creating-the-phrase-strings-project\"><\/span>Creating the Phrase Strings project<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>To integrate Phrase Strings into your app, you need to configure a new Phrase Strings project:<\/p>\n<ol>\n<li><a href=\"https:\/\/eu.phrase.com\/idm-ui\/signup\">Create a Phrase account<\/a> (you can start for free).<\/li>\n<li>Login, open Phrase Strings, and click the <strong>New Project<\/strong> button near the top of the screen to create a project.<\/li>\n<li>Configure the project to use the <em><strong>React Intl Simple<\/strong><\/em> translation file format<\/li>\n<li>Add starting languages. In our case, we can add <code>en-US<\/code> first as the default locale, then add <code>ar-EG<\/code>.<\/li>\n<li>Generate an access token from your profile page. (Click the user avatar near the top-right of the screen \u2192 <em>Settings \u2192 Profile<\/em> \u2192 <em>Access tokens<\/em> \u2192 <em>Generate Token<\/em>). Make a copy of this token somewhere safe.<\/li>\n<li>Open your new project and go to <span class=\"notion-enable-hover\" data-token-index=\"1\">More <\/span>\u2192 <span class=\"notion-enable-hover\" data-token-index=\"3\">Settings. <\/span>Check<span class=\"notion-enable-hover\" data-token-index=\"5\"> Enable ICU Message format support<\/span> and click <span class=\"notion-enable-hover\" data-token-index=\"7\">Save<\/span>.<\/li>\n<\/ol>\n<figure id=\"attachment_67159\" aria-describedby=\"caption-attachment-67159\" style=\"width: 600px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-67159 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/create-phrase-strings-project.gif\" alt=\"Creating a project in Phrase Strings | Phrase\" width=\"600\" height=\"393\" \/><figcaption id=\"caption-attachment-67159\" class=\"wp-caption-text\">Creating a project in Phrase Strings<\/figcaption><\/figure>\n<h3><span class=\"ez-toc-section\" id=\"setting-up-the-phrase-strings-cli\"><\/span>Setting up the Phrase Strings CLI<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>That\u2019s it for the Phrase Strings project config. Now let\u2019s set up the Phrase Strings CLI so we can automate the transfer of our translation files to and from our translators.<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> Installation instructions for the Phrase Strings CLI depend on your platform (Windows, macOS, Linux). Just follow the <a href=\"https:\/\/support.phrase.com\/hc\/en-us\/articles\/5784093863964-CLI-Installation-Strings\">CLI installation docs<\/a> and you should be good to go.<\/p>\n<p>CLI installed, let\u2019s use it to connect our React project to Phrase Strings. From our React project root, let\u2019s run the following command from the command line.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-87\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">phrase init<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-87\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_9ef3e5379709c1b92f2ff59d9b48560d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>We\u2019ll be asked to provide the access token we generated in Step 5 above. Let\u2019s paste it in.<\/p>\n<p>We\u2019ll then be given some prompts:<\/p>\n<ol>\n<li><code>Select project<\/code>\u2014Select the Phrase Strings project we created above.<\/li>\n<li><code>Select the format to use for language files you download from Phrase Strings<\/code>\u2014Hit <em>Enter<\/em> to select the project\u2019s default.<\/li>\n<li><code>Enter the path to the language file you want to upload to Phrase<\/code>\u2014Enter <code>.\/src\/lang\/en-US.json<\/code>, since that\u2019s our source translation file.<\/li>\n<li><code>Enter the path to which to download language files from Phrase<\/code>\u2014Enter <code>.\/src\/lang\/&lt;locale_name&gt;.json<\/code>. (<code>&lt;locale_name&gt;<\/code> is a placeholder here, and it allows us to download <em>all<\/em> the translation files for our project: <code>en-US<\/code>, <code>ar-EG<\/code>, etc.).<\/li>\n<li><code>Do you want to upload your locales now for the first time?<\/code>\u2014Hit <em>Enter<\/em> to accept and upload.<\/li>\n<\/ol>\n<p><!-- notionvc: 27e11426-8ba6-4bfa-b4bf-e008a3f1f94d --><\/p>\n<p>\ud83d\uddd2\ufe0f <em>Note \u00bb<\/em> A <code>.phrase.yml<\/code> is created in our project to save the config we created above. It will be used by default when we run commands like <code>phrase push<\/code> or <code>phrase pull<\/code> in our project.<\/p>\n<p>At this point, our <code>en-US.json<\/code> file will get uploaded to Phrase, where our translators can use the powerful Phrase web admin to translate our app.<\/p>\n<figure id=\"attachment_67177\" aria-describedby=\"caption-attachment-67177\" style=\"width: 600px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-67177 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/phrase-strings-translation-editor.gif\" alt=\"Phrase Strings translation editor | Phrase\" width=\"600\" height=\"392\" \/><figcaption id=\"caption-attachment-67177\" class=\"wp-caption-text\">Translation can be a breeze with Phrase\u2019s translation memory and machine translation<\/figcaption><\/figure>\n<p>When our translators are finished, we can run the following command to pull all of their work into our project.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-88\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">phrase pull<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-88\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_f3c19d7e8790212e74c511f8976ed2c0\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Our <code>en-US.json<\/code> and <code>ar-EG.json<\/code> files should now have the latest updates from our translators. We can simply run our app as normal to test our updated translations. Whether you have two locales, or two hundred, you only need to run two commands to keep your translations in sync. And your translators get an excellent environment to work in.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"adding-a-new-language\"><\/span>Adding a new language<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Let\u2019s demonstrate how much easier our lives are now with this new workflow. We\u2019ll add French (<code>fr-FR<\/code>) to our app. Here are the steps:<\/p>\n<ol>\n<li>Tell our translators we want to add <code>fr-FR<\/code> to our app. (They\u2019ll manage all that in Phrase Strings).<\/li>\n<li>Add French in our <code>locales<\/code> config object (see <code>i18n-config.js<\/code> listing below).<\/li>\n<li>Pull the new French translators from Phrase using <code>phrase pull<\/code>.<\/li>\n<\/ol>\n<p>That\u2019s literally it!<\/p>\n<p>Here\u2019s the updated <code>i18n-config.js<\/code>:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-89\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/i18n-config.js\n\nexport const defaultLocale = \"en-US\";\n\nexport const locales = {\n  \"en-US\": {\n    name: \"English\",\n    dir: \"ltr\",\n  },\n  \"ar-EG\": {\n    name: \"Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)\",\n    dir: \"rtl\",\n  },\n<span class=\"hljs-addition\">+ \"fr-FR\": {<\/span>\n<span class=\"hljs-addition\">+   name: \"French (Fran\u00e7ais)\",<\/span>\n<span class=\"hljs-addition\">+   dir: \"ltr\",<\/span>\n<span class=\"hljs-addition\">+ },<\/span>\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-89\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_5b35092332245079c43bd2be4558116e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>And here\u2019s our app in French.<\/p>\n<figure id=\"attachment_67191\" aria-describedby=\"caption-attachment-67191\" style=\"width: 600px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-67191\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/10\/localized-app-french.gif\" alt=\"Screen of an app localized in French  | Phrase\" width=\"600\" height=\"435\" \/><figcaption id=\"caption-attachment-67191\" class=\"wp-caption-text\">Our translators used Phrase Strings to add French\u2014we just used phrase pull to pull in the new translations<\/figcaption><\/figure>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The app with French added is in the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/react-intl-formatjs-2023\/tree\/main\/i18n-demo-extraction\">i18n-demo-extraction directory of our GitHub repo<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-my-react-typescript-app-with-react-intl\"><\/span>How do I localize my React TypeScript app with react-intl?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Using react-intl with TypeScript in a React app is relatively straightforward. An in-depth guide for this is outside the scope of this tutorial. We\u2019ll cover strongly typing message IDs and we\u2019ll share our complete solution code for your perusal.<\/p>\n<p>In fact, most of react-intl will work with TypeScript out of the box. If you want to make your message IDs typed only to your keys, you can add the following code somewhere in your project.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-90\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ Your path may change here. Import the source<\/span>\n<span class=\"hljs-comment\">\/\/ translation file.<\/span>\n<span class=\"hljs-keyword\">import<\/span> messages <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/src\/lang\/en-US.json\"<\/span>;\n\n<span class=\"hljs-keyword\">declare<\/span> global {\n  <span class=\"hljs-keyword\">namespace<\/span> FormatjsIntl {\n    <span class=\"hljs-keyword\">interface<\/span> Message {\n      ids: keyof <span class=\"hljs-keyword\">typeof<\/span> messages;\n    }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-90\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_10f7e3cdd74c54621dcb329f7ab0d71c\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>That\u2019s basically it for react-intl TypeScript integration. Most of our other TypeScript considerations are specific to our demo.<\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> You can <a href=\"https:\/\/github.com\/PhraseApp-Blog\/react-intl-formatjs-2023\/compare\/1aa312227fa75d91300b8d13f1c2196f3c2a4f4f...b9ef0dc439d241a648b96660ecf9e269800b49b6\">find a diff of our TypeScript i18n solution in our GitHub repo<\/a>. The entire TypeScript i18n demo is in an <a href=\"https:\/\/github.com\/PhraseApp-Blog\/react-intl-formatjs-2023\/tree\/main\/i18n-demo-typescript\">i18n-demo-typescript folder<\/a> in the repo as well.<\/p>\n<p>\u270b <em>Heads up \u00bb<\/em> We used TypeScript 4.9.5, since TypeScript 5 was giving us peer dependency errors when we tried installing it in our project. (<code>--force<\/code>ing the version 5 install seemed to work fine, but use at your own risk).<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> If you\u2019re working with TypeScript and want to use the extraction\/auto-generated ID workflow (optionally with Phrase), <a href=\"https:\/\/github.com\/PhraseApp-Blog\/react-intl-formatjs-2023\/compare\/2d788550de231d049a9aaf492259820c8b06f98b...4ba125db78c68adcb87878a96578152558288fd8\">check out this diff<\/a>. An entire Typescript extraction demo is in a dedicated <a href=\"https:\/\/github.com\/PhraseApp-Blog\/react-intl-formatjs-2023\/tree\/main\/i18n-demo-typescript-extraction\">i18n-demo-typescript-extraction<\/a> folder.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"take-react-localization-to-the-next-level\"><\/span>Take React localization to the next level<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We hope you found this guide to localizing React apps with react-intl\/FormatJS enjoyable and informative. If you\u2019re interested in localizing a Next.js app, check out our tutorial on <a href=\"https:\/\/phrase.com\/blog\/posts\/nextjs-i18n\/\">Next.js internationalization<\/a>. If you prefer using i18next, our guide to <a href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/\">React localization with i18next<\/a> should have you covered.<\/p>\n<p>When you&#8217;re ready to start translating, let Phrase Strings take care of the hard work. With plenty of tools to automate your translation process and native integrations with platforms like GitHub, GitLab, and Bitbucket, Phrase Strings makes it simple for translators to pick up your content and manage it in its user-friendly string editor.<\/p>\n<p>Once your translations are ready, you can easily pull them back into your project with a single command\u2014or automatically\u2014so you can stay focused on the code you love. <a href=\"https:\/\/eu.phrase.com\/idm-ui\/signup?uiLang=en-US\">Sign up for a free trial<\/a> and see for yourself why developers love using Phrase Strings for software localization.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n\n<div id=\"acf\/blog-cta-block_c9d4b071f4bf97a448e8c18c87acda9e\" class=\"pxblock pxblock--blog-cta alignfull bg--green image--orientation-square\">\n\t<div class=\"block-container\">\n\t\t\t\t<div class=\"content\">\n\t\t\t<p class=\"h4\" style=\"text-align: center;\">Speak with an expert<\/p>\n<p class=\"small\" style=\"text-align: center;\">Want to learn how our solutions can help you unlock global opportunity? We\u2019d be happy to show you around the Phrase Localization Platform and answer any questions you may have.<\/p>\n<p style=\"text-align: center;\"><a class=\"btn btn--outline\" href=\"https:\/\/phrase.com\/demo\/\">Book a call<\/a><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Dive deep into localizing React apps with react-intl\/FormatJS and discover how Phrase Strings can help you translate your content seamlessly.<\/p>\n","protected":false},"author":41,"featured_media":39386,"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-14669","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\/14669"}],"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=14669"}],"version-history":[{"count":8,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/14669\/revisions"}],"predecessor-version":[{"id":75434,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/14669\/revisions\/75434"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media\/39386"}],"wp:attachment":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media?parent=14669"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/categories?post=14669"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}