{"id":12225,"date":"2020-09-04T08:23:35","date_gmt":"2020-09-04T06:23:35","guid":{"rendered":"https:\/\/phrase.com\/blog\/?p=12225"},"modified":"2024-01-25T18:47:42","modified_gmt":"2024-01-25T17:47:42","slug":"localizing-react-apps-with-i18next","status":"publish","type":"post","link":"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/","title":{"rendered":"A Guide to React Localization with i18next"},"content":{"rendered":"\n<div id=\"acf\/text-block_0d97bd89c088323e5d927eec7aef2b44\" 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><a href=\"https:\/\/react.dev\/\">React<\/a> is so ubiquitous today that it might as well be a standard for building web apps (and myriad other UI). But part of React\u2019s success is its hyper-focus on component-based, reactive UI: To build complete apps, we often have to compose our own frameworks around React. So what happens when your React app needs to speak other languages? If you\u2019re here, you might be thinking about the <a href=\"https:\/\/npmtrends.com\/react-i18next-vs-react-intl-vs-react-redux-i18n\">most popular<\/a> internationalization library, <a href=\"https:\/\/www.i18next.com\/\">i18next<\/a>. It\u2019s a wise choice: In addition to its popularity, i18next is mature and extensible. Any internationalization problem you can think of is probably already solved with i18next or one of its many plugins.<\/p>\n<p>In this tutorial, we&#8217;ll explore how to leverage i18next with React to create dynamic, multilingual applications. We&#8217;ll cover everything from basic setup to advanced features, ensuring your React app is speaking multiple languages in no time.<\/p>\n<p><!-- notionvc: 9dc22343-afec-4441-8a00-3587d5ba8a9a --><\/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\/localizing-react-apps-with-i18next\/#internationalization-and-localization\" title=\"Internationalization and localization\">Internationalization and localization<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#prerequisites-and-package-versions\" title=\"Prerequisites and package versions\">Prerequisites and package versions<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#the-demo-app-a-react-and-i18next-playground\" title=\"The demo app: a React and i18next playground\">The demo app: a React and i18next playground<\/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\/localizing-react-apps-with-i18next\/#how-do-i-install-and-configure-i18next-for-my-react-app\" title=\"How do I install and configure i18next for my React app?\">How do I install and configure i18next for my React app?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#a-quick-test-translating-a-component\" title=\"A quick test: Translating a component\">A quick test: Translating a component<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#namespaces\" title=\"Namespaces\">Namespaces<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#locale-codes-en-ar-etc\" title=\"Locale codes (en, ar, etc.)\">Locale codes (en, ar, etc.)<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#how-do-i-load-translation-files-asynchronously\" title=\"How do I load translation files asynchronously?\">How do I load translation files asynchronously?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#how-do-i-get-and-set-the-active-locale\" title=\"How do I get and set the active locale?\">How do I get and set the active locale?<\/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\/localizing-react-apps-with-i18next\/#how-do-i-build-a-language-switcher\" title=\"How do I build a language switcher?\">How do I build a language switcher?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#how-do-i-automatically-detect-the-users-language\" title=\"How do I automatically detect the user\u2019s language?\">How do I automatically detect the user\u2019s language?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#detection-sources\" title=\"Detection sources\">Detection sources<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#caching-the-resolved-locale\" title=\"Caching the resolved locale\">Caching the resolved locale<\/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\/localizing-react-apps-with-i18next\/#how-do-i-work-with-right-to-left-languages\" title=\"How do I work with right-to-left languages?\">How do I work with right-to-left languages?<\/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\/localizing-react-apps-with-i18next\/#how-do-i-localize-the-document-title\" title=\"How do I localize the document title?\">How do I localize the document title?<\/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\/localizing-react-apps-with-i18next\/#how-do-i-work-with-dynamic-values-in-my-translations\" title=\"How do I work with dynamic values in my translations?\">How do I work with dynamic values in my translations?<\/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\/localizing-react-apps-with-i18next\/#how-do-i-work-with-plurals-in-my-translations\" title=\"How do I work with plurals in my translations?\">How do I work with plurals in my translations?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#working-with-complex-plurals\" title=\"Working with complex plurals\">Working with complex plurals<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-19\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#using-the-correct-count-numerals\" title=\"Using the correct count numerals\">Using the correct count numerals<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-20\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#how-do-i-format-localized-numbers\" title=\"How do I format localized numbers?\">How do I format localized numbers?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-21\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#a-note-on-regional-formatting\" title=\"A note on regional formatting\">A note on regional formatting<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-22\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#numbers-in-messages\" title=\"Numbers in messages\">Numbers in messages<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-23\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#custom-formatters\" title=\"Custom formatters\">Custom formatters<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-24\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#how-do-i-format-localized-dates\" title=\"How do I format localized dates?\">How do I format localized dates?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-25\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#a-custom-datetime-formatter\" title=\"A custom datetime formatter\">A custom datetime formatter<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-26\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#modifying-the-datetime-format\" title=\"Modifying the datetime format\">Modifying the datetime format<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-27\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#further-reading\" title=\"Further reading\">Further reading<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-28\" href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/#up-your-localization-game\" title=\"Up your localization game\">Up your localization game<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"internationalization-and-localization\"><\/span>Internationalization and localization<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Internationalization (i18n) and localization (l10n) allow us to make our apps available in different languages and to different regions, often for more profit. If you\u2019re new to i18n and l10n, check out our guide to <a href=\"https:\/\/phrase.com\/blog\/posts\/i18n-a-simple-definition\/\">internationalization<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"prerequisites-and-package-versions\"><\/span>Prerequisites and package versions<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We assume you&#8217;re familiar with React development, and other than that we try to explain the i18n code as clearly as possible.<\/p>\n<p>If you want to <a href=\"https:\/\/github.com\/mdurmusphrase\/react-i18next-iv\">run the demo app<\/a> on your local machine (you don\u2019t have to), you&#8217;ll need a fairly recent version of <a href=\"https:\/\/nodejs.org\/en\">Node.js<\/a>. Speaking of which, here are the packages we&#8217;re using in this guide (with versions at the time of writing):<\/p>\n<ul>\n<li><strong>vite <\/strong>(5.0) \u2014 Our super fast module bundler (you can use create-react-app \/ Webpack or whatever you like).<!-- notionvc: b5e3168f-dcd8-4656-86e2-5f26c6474077 --><\/li>\n<li><strong>typescript\u00a0<\/strong>(5.3) \u2014 We\u2019ll write in TypeScript, but you don\u2019t have to. Use plain JavaScript if you want.<\/li>\n<li><strong>react\u00a0<\/strong>(18.2)<\/li>\n<li><strong>i18next <\/strong>(23.7) \u2014 Our i18n library.<\/li>\n<li><strong>i18next-http-backend <\/strong>(2.4) \u2014 Loads translation files.<!-- notionvc: 001a1cb8-bb0c-4fc4-9dc6-a5fd88edd572 --><\/li>\n<li><strong>i18next-browser-languagedetector<\/strong> (7.2) \u2014 Detects the user&#8217;s locale (language).<\/li>\n<li><strong>tailwindcss<\/strong> (3.4) \u2014 Used for styling; largely optional here.<\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"the-demo-app-a-react-and-i18next-playground\"><\/span>The demo app: a React and i18next playground<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We\u2019ve prepared an interactive demo as a companion to this article that you can <a href=\"https:\/\/stackblitz.com\/~\/github.com\/mdurmusphrase\/react-i18next-iv\">access directly on StackBlitz<\/a>. Alternatively, you can <a href=\"https:\/\/github.com\/mdurmusphrase\/react-i18next-iv\">grab the project code from GitHub<\/a> and run it on your machine; just make sure you have a recent version of <a href=\"https:\/\/nodejs.org\/en\">Node.js<\/a> installed.<\/p>\n<figure id=\"attachment_73514\" aria-describedby=\"caption-attachment-73514\" style=\"width: 1168px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-73514\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-demo.png\" alt=\"Our demo app in the default language, English.\" width=\"1168\" height=\"868\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-demo.png 1168w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-demo-300x223.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-demo-1024x761.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-demo-768x571.png 768w\" sizes=\"(max-width: 1168px) 100vw, 1168px\" \/><figcaption id=\"caption-attachment-73514\" class=\"wp-caption-text\">Our demo app in the default language, English.<\/figcaption><\/figure>\n<figure id=\"attachment_73524\" aria-describedby=\"caption-attachment-73524\" style=\"width: 1168px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-73524\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-demo.png\" alt=\"Our demo app localized to Arabic.\" width=\"1168\" height=\"868\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-demo.png 1168w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-demo-300x223.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-demo-1024x761.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-demo-768x571.png 768w\" sizes=\"(max-width: 1168px) 100vw, 1168px\" \/><figcaption id=\"caption-attachment-73524\" class=\"wp-caption-text\">Our demo app localized to Arabic.<\/figcaption><\/figure>\n<p><span class=\"small\" role=\"img\" aria-label=\"\ud83d\udce3\">\ud83d\udce3<\/span> <em>Shoutout<\/em> <span class=\"notion-enable-hover\" data-token-index=\"0\">\u00bb<\/span><!-- notionvc: 1d6a0bea-a0bd-48e3-ac4d-450cce982694 -->\u00a0Thanks to <a class=\"notion-link-token notion-focusable-token notion-enable-hover\" tabindex=\"0\" href=\"http:\/\/rawpixel.com\/\" rel=\"noopener noreferrer\" data-token-index=\"3\"><span class=\"link-annotation-unknown-block-id--686252541 small\">rawpixel.com<\/span><\/a> for providing their <a class=\"notion-link-token notion-focusable-token notion-enable-hover\" tabindex=\"0\" href=\"https:\/\/www.freepik.com\/free-vector\/grid-pattern-background-minimal-black-white-simple-design-vector_20170457.htm#query=blueprint%20grid&amp;position=13&amp;from_view=keyword&amp;track=ais&amp;uuid=dbeb268e-a3dc-4c51-93ba-ad6b7df65f73\" rel=\"noopener noreferrer\" data-token-index=\"5\"><span class=\"link-annotation-unknown-block-id--427659442 small\">Grid Background for free on Freepik<\/span><\/a>, which we\u2019re using in our demo app.<!-- notionvc: e5776af6-6168-4423-807b-af7629423f44 --><\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-install-and-configure-i18next-for-my-react-app\"><\/span>How do I install and configure i18next for my React app?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Installation is with NPM (or your preferred equivalent), of course.<\/p>\n<p><!-- notionvc: 726ba4f1-f775-40db-83d3-adea92b037ef --><\/p>\n<p><!-- notionvc: fccfffab-2f90-4e06-b166-665f2fa31fb8 --><\/p>\n<p><!-- notionvc: b6401902-a4fb-4208-8384-b747efb096c9 --><\/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\">npm install react-i18next i18next<\/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_9945996151eccb8b67ec6d59cb40bcc2\" 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>i18next<\/code> is the core library, but the i18next team also provides an official extension for React, <code>react-i18next<\/code>. With both packages installed, we get a custom React hook and components that allow us to work with i18next quickly and easily in our React projects.<\/p>\n<p>Let\u2019s configure these libraries and wire them up to our React app. We\u2019ll create a new directory, <code>src\/i18n<\/code>, and put a config file in there.<\/p>\n<p><!-- notionvc: 6b824031-28f7-4dd9-b97e-396057907bbe --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/config.ts<\/span>\n\n<span class=\"hljs-comment\">\/\/ Core i18next library.<\/span>\n<span class=\"hljs-keyword\">import<\/span> i18n <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"i18next\"<\/span>;                      \n<span class=\"hljs-comment\">\/\/ Bindings for React: allow components to<\/span>\n<span class=\"hljs-comment\">\/\/ re-render when language changes.<\/span>\n<span class=\"hljs-keyword\">import<\/span> { initReactI18next } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-i18next\"<\/span>;\n\ni18n\n  <span class=\"hljs-comment\">\/\/ Add React bindings as a plugin.<\/span>\n  .use(initReactI18next)\n  <span class=\"hljs-comment\">\/\/ Initialize the i18next instance.<\/span>\n  .init({\n    <span class=\"hljs-comment\">\/\/ Config options<\/span>\n\n    <span class=\"hljs-comment\">\/\/ Specifies the default language (locale) used<\/span>\n    <span class=\"hljs-comment\">\/\/ when a user visits our site for the first time.<\/span>\n    <span class=\"hljs-comment\">\/\/ We use English here, but feel free to use<\/span>\n    <span class=\"hljs-comment\">\/\/ whichever locale you want.                   <\/span>\n    lng: <span class=\"hljs-string\">\"en\"<\/span>,\n\n    <span class=\"hljs-comment\">\/\/ Fallback locale used when a translation is<\/span>\n    <span class=\"hljs-comment\">\/\/ missing in the active locale. Again, use your<\/span>\n    <span class=\"hljs-comment\">\/\/ preferred locale here. <\/span>\n    fallbackLng: <span class=\"hljs-string\">\"en\"<\/span>,\n\n    <span class=\"hljs-comment\">\/\/ Enables useful output in the browser\u2019s<\/span>\n    <span class=\"hljs-comment\">\/\/ dev console.<\/span>\n    debug: <span class=\"hljs-literal\">true<\/span>,\n\n    <span class=\"hljs-comment\">\/\/ Normally, we want `escapeValue: true` as it<\/span>\n    <span class=\"hljs-comment\">\/\/ ensures that i18next escapes any code in<\/span>\n    <span class=\"hljs-comment\">\/\/ translation messages, safeguarding against<\/span>\n    <span class=\"hljs-comment\">\/\/ XSS (cross-site scripting) attacks. However,<\/span>\n    <span class=\"hljs-comment\">\/\/ React does this escaping itself, so we turn <\/span>\n    <span class=\"hljs-comment\">\/\/ it off in i18next.<\/span>\n    interpolation: {\n      escapeValue: <span class=\"hljs-literal\">false<\/span>,\n    },\n\n    <span class=\"hljs-comment\">\/\/ Translation messages. Add any languages<\/span>\n    <span class=\"hljs-comment\">\/\/ you want here.<\/span>\n    resources: {\n      <span class=\"hljs-comment\">\/\/ English<\/span>\n      en: {\n        <span class=\"hljs-comment\">\/\/ `translation` is the default namespace.<\/span>\n        <span class=\"hljs-comment\">\/\/ More details about namespaces shortly.<\/span>\n        translation: {\n          hello_world: <span class=\"hljs-string\">\"Hello, World!\"<\/span>,\n        },\n      },\n      <span class=\"hljs-comment\">\/\/ Arabic<\/span>\n      ar: {\n        translation: {\n          hello_world: <span class=\"hljs-string\">\"\u0645\u0631\u062d\u0628\u0627\u064b \u0628\u0627\u0644\u0639\u0627\u0644\u0645!\"<\/span>,\n        },\n      },\n    },\n  });\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> i18n;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><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_e2a16186e5632a8eb577d4752eac51cf\" 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 initialize and configure an i18next instance for our basic setup. i18next is highly configurable, so check out the <a href=\"https:\/\/www.i18next.com\/overview\/configuration-options\">Configuration Options<\/a> docs to tweak to your heart\u2019s content.<\/p>\n<p>Alright, let\u2019s import this file into our app\u2019s entry point to get it wired up.<\/p>\n<p><!-- notionvc: 4338ebdc-35db-42dd-a1fc-068e01917f83 --><\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/main.tsx\n\n\/\/ \ud83d\udc46 This might be index.tsx or\n\/\/ index.js in your app.\n\n  import React from \"react\";\n  import ReactDOM from \"react-dom\/client\";\n  import App from \".\/App.tsx\";\n<span class=\"hljs-addition\">+ import \".\/i18n\/config.ts\";<\/span>\n  import \".\/index.css\";\n\n  ReactDOM.createRoot(document.getElementById(\"root\")!).render(\n    &lt;React.StrictMode&gt;\n      &lt;App \/&gt;\n    &lt;\/React.StrictMode&gt;,\n  );<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><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_a6172d2bf3873ca0905211e1bfe1b320\" 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>When we run our app now, we should see some output in our browser\u2019s dev console telling us that i18next has initialized correctly. This is thanks to the <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">debug: true<\/span><\/code> option we added to our config.<!-- notionvc: d646adb7-51dd-4ceb-81cd-5ae4dcc848e1 --><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-73557\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/console-output.png\" alt=\"Browser dev tools console output showing that i18n is initializing.\" width=\"1444\" height=\"447\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/console-output.png 1444w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/console-output-300x93.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/console-output-1024x317.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/console-output-768x238.png 768w\" sizes=\"(max-width: 1444px) 100vw, 1444px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"a-quick-test-translating-a-component\"><\/span>A quick test: Translating a component<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>So how does this all work in a React component? Let\u2019s give it a go.<\/p>\n<p><!-- notionvc: 70047462-45c2-4493-86cf-cacb88428df8 --><\/p>\n\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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ src\/App.tsx<\/span>\n\n<span class=\"hljs-comment\">\/\/ React hook that ensures components are<\/span>\n<span class=\"hljs-comment\">\/\/ re-rendered when locale changes.<\/span>\n<span class=\"hljs-keyword\">import<\/span> { useTranslation } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-i18next\"<\/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-comment\">\/\/ The `t()` function gives us<\/span>\n  <span class=\"hljs-comment\">\/\/ access to the active locale's<\/span>\n  <span class=\"hljs-comment\">\/\/ translations.<\/span>\n  <span class=\"hljs-keyword\">const<\/span> { t } = useTranslation();\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;div className=<span class=\"hljs-string\">\"...\"<\/span>&gt;\n      {<span class=\"hljs-comment\">\/* We pass the key we provided under\n          `resources.translation` in \n          src\/i18n\/config.ts *\/<\/span>}\n      &lt;h2&gt;{t(<span class=\"hljs-string\">\"hello_world\"<\/span>)}&lt;<span class=\"hljs-regexp\">\/h2&gt;\n    &lt;\/<\/span>div&gt;\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-4\"><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_226e54061d7cba30a2379e00b76d5764\" 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 load our app now, we should see our English \u201cHello, World!\u201d message.<!-- notionvc: 227a4734-26f1-4803-94d5-8bdbebd98548 --><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-73565\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-hello-world.png\" alt=\"An English translation message.\" width=\"330\" height=\"110\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-hello-world.png 330w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-hello-world-300x100.png 300w\" sizes=\"(max-width: 330px) 100vw, 330px\" \/><\/p>\n<p>And if we change the default locale to Arabic, our component re-renders and shows us the Arabic <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">hello_world<\/span><\/code> translation.<!-- notionvc: 77c8b154-049d-44a2-bfa1-84a65cde07bc --><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-73571\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-hello-world.png\" alt=\"The same message, translated to Arabic.\" width=\"312\" height=\"136\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-hello-world.png 312w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-hello-world-300x131.png 300w\" sizes=\"(max-width: 312px) 100vw, 312px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"namespaces\"><\/span>Namespaces<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>You may have noticed the <code>translation<\/code> namespace under <code>resources<\/code> when we configured i18next. Namespaces are effectively groups, and i18next uses them to allow splitting translations into logical collections for bigger apps (e.g. admin, public).<\/p>\n<p>This can make apps more performant when each namespace houses its translations into a separate file, and a namespace&#8217;s file is only loaded when needed, ideally asynchronously. (We\u2019ll cover async file loading shortly).<\/p>\n<p><code>translation<\/code> is the default namespace used by i18next, and it\u2019s the only one we\u2019ll be using in this article. Feel free to check out the <a href=\"https:\/\/www.i18next.com\/principles\/namespaces\">Namespaces<\/a> docs page if you want to dive deeper here.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"locale-codes-en-ar-etc\"><\/span>Locale codes (en, ar, etc.)<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>A locale defines a language, a region, and sometimes more. Locales typically use <a href=\"https:\/\/en.wikipedia.org\/wiki\/IETF_language_tag\">IETF BCP 47 language tags<\/a>, like <code>en<\/code> for English, <code>fr<\/code> for French, and <code>es<\/code> for Spanish. Adding a region with the ISO Alpha-2 code (e.g., <code>BH<\/code> for Bahrain, <code>CN<\/code> for China, <code>US<\/code> for the United States) is recommended for accurate date and number localization. So a complete locale might look like <code>en-US<\/code> for American English or <code>zh-CN<\/code> for Chinese as used in China.<\/p>\n<p>\ud83d\udd17 Explore more language tags on <a href=\"https:\/\/en.wikipedia.org\/wiki\/List_of_ISO_639-1_codes\">Wikipedia<\/a> and find country codes through the ISO&#8217;s <a href=\"https:\/\/www.iso.org\/obp\/ui\/#search\">search tool<\/a>.<\/p>\n<p>\u270b i18next&#8217;s locale resolution tends to work best when we have language-only locales, like <code>en<\/code> and <code>ar<\/code>. We stick to those in this article, but be aware that when we localized dates and numbers, the browser is making the choice of region for formatting. Given ar, one browser might choose to format dates for Arabic-Egypt (ar-EG) where another chooses Saudi Arabia (ar-SA). We solve this issue a bit later in this article when we write our own custom number and date formatters.<\/p>\n<p><!-- notionvc: 8438df30-ec6d-4a41-bfa7-1da618832eaa --><\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-load-translation-files-asynchronously\"><\/span>How do I load translation files asynchronously?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We\u2019re currently inlining translations into our i18n config file.<\/p>\n<p><!-- notionvc: e7c53f9d-28da-4a08-9d73-76dfb11864d0 --><\/p>\n\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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/config.ts<\/span>\n<span class=\"hljs-keyword\">import<\/span> i18n <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"i18next\"<\/span>;\n<span class=\"hljs-comment\">\/\/ ...<\/span>\n\ni18n\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  .init({\n    <span class=\"hljs-comment\">\/\/...<\/span>\n    resources: {\n      en: {\n        translation: {\n          hello_world: <span class=\"hljs-string\">\"Hello, World!\"<\/span>,\n        },\n      },\n      ar: {\n        translation: {\n          hello_world: <span class=\"hljs-string\">\"\u0645\u0631\u062d\u0628\u0627\u064b \u0628\u0627\u0644\u0639\u0627\u0644\u0645!\"<\/span>,\n        },\n      },\n    },\n  });\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> i18n;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><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_beb92cd8f9758bbd96997670bf202682\" 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 can work well for a couple of languages and a handful of translation messages, but as you can imagine this solution doesn\u2019t scale very well. As more languages and translations are added, our config file would get bloated, slowing down our initial app load.<\/p>\n<p>Moreover, we often want to hand off a single language file to a translator, and this approach doesn\u2019t accommodate that very well.<\/p>\n<p>Let\u2019s split our translations into separate files, one per locale. While we\u2019re at it, we\u2019ll ensure that the active locale&#8217;s translation file will load asynchronously from the network. This will speed up our app as it scales and allow us to provide each translator only the file(s) of their language.<\/p>\n<p>First, let\u2019s install the <a href=\"https:\/\/github.com\/i18next\/i18next-http-backend\">official i18next HTTP API backend plugin<\/a>. It\u2019s the simplest way to download translation files from the network and connect them with i18next.<\/p>\n<p><!-- notionvc: 17c39961-18fb-462c-95d4-f63313f20e5c --><\/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 i18next-http-backend<\/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_d7f14b2eb8263118fbb910e78ffcefd4\" 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, we\u2019ll configure the backend.<!-- notionvc: ddc0315a-5d85-4a54-a43a-484880cd4d97 --><\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/config.ts\n\n  import i18n from \"i18next\";\n<span class=\"hljs-addition\">+ import HttpApi from \"i18next-http-backend\";<\/span>\n  import { initReactI18next } from \"react-i18next\";\n\n  i18n\n<span class=\"hljs-addition\">+   \/\/ Wire up the backend as a plugin.<\/span>\n<span class=\"hljs-addition\">+   .use(HttpApi)<\/span>\n    .use(initReactI18next)\n    .init({\n      lng: \"en\",\n      fallbackLng: \"en\",\n      debug: true,\n      interpolation: {\n        escapeValue: false,\n      },\n<span class=\"hljs-deletion\">-     \/\/ Remove the inlined translations.<\/span>\n<span class=\"hljs-deletion\">-     resources: {<\/span>\n<span class=\"hljs-deletion\">-       en: {<\/span>\n<span class=\"hljs-deletion\">-         translation: {<\/span>\n<span class=\"hljs-deletion\">-           hello_world: \"Hello, World!\",<\/span>\n<span class=\"hljs-deletion\">-         },<\/span>\n<span class=\"hljs-deletion\">-       },<\/span>\n<span class=\"hljs-deletion\">-       ar: {<\/span>\n<span class=\"hljs-deletion\">-         translation: {<\/span>\n<span class=\"hljs-deletion\">-           hello_world: \"\u0645\u0631\u062d\u0628\u0627\u064b \u0628\u0627\u0644\u0639\u0627\u0644\u0645!\",<\/span>\n<span class=\"hljs-deletion\">-         },<\/span>\n<span class=\"hljs-deletion\">-       },<\/span>\n<span class=\"hljs-deletion\">-     },<\/span>\n    });\n\n  export default i18n;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><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_06c58cebdb4fffb6daf5dde637595cea\" 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 that we\u2019ve removed the inlined translations, we better add them in separate files. By default, the HTTP API backend will look for a translation file at a given URL when i18next initializes. If our active locale is English, it will look for the file at a public URL relative to the root of our website: <code>http:\/\/example.com\/locales\/en\/translation.json<\/code><\/p>\n<p>\ud83d\uddd2\ufe0f Remember, <code>translation<\/code> is the default namespace.<\/p>\n<p>Let\u2019s add our files where the backend will expect them, placing them under the <code>public<\/code> directory so that they\u2019re available for download on the network.<\/p>\n<p><!-- notionvc: 7732ee23-892a-4201-ad6c-4f84d3a7f4e3 --><\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ public\/locales\/en\/translation.json<\/span>\n{\n  <span class=\"hljs-attr\">\"hello_world\"<\/span>: <span class=\"hljs-string\">\"Hello, World!\"<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/ public\/locales\/ar\/translation.json<\/span>\n{\n  <span class=\"hljs-attr\">\"hello_world\"<\/span>: <span class=\"hljs-string\">\"\u0645\u0631\u062d\u0628\u0627\u064b \u0628\u0627\u0644\u0639\u0627\u0644\u0645!\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><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_528d47b8bc8264cbc7eb666cf0626003\" 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 reload our app now, everything should work as it did before. However, if we look at the network tab in our browser\u2019s dev console, we\u2019ll notice some new requests.<!-- notionvc: 0147acc4-baa7-432d-bcd0-66b45664b5ed --><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-73579\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/translation-file-request.png\" alt=\"HTTP request for our translation file.\" width=\"614\" height=\"400\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/translation-file-request.png 614w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/translation-file-request-300x195.png 300w\" sizes=\"(max-width: 614px) 100vw, 614px\" \/><\/p>\n<p>One last thing here: Let\u2019s add a <a class=\"notion-link-token notion-focusable-token notion-enable-hover\" tabindex=\"0\" href=\"https:\/\/react.dev\/reference\/react\/Suspense\" rel=\"noopener noreferrer\" data-token-index=\"1\"><span class=\"link-annotation-unknown-block-id--581953210\">React Suspense<\/span><\/a> boundary so that on slow connections our users will get a helpful indicator while our active translation files downloading.<!-- notionvc: 45480527-9509-468c-a4c4-f8a5f37de534 --><\/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\/main.tsx\n\nimport React from \"react\";\nimport ReactDOM from \"react-dom\/client\";\nimport App from \".\/App.tsx\";\nimport \".\/i18n\/config.ts\";\nimport \".\/index.css\";\n\nReactDOM.createRoot(document.getElementById(\"root\") as HTMLElement).render(\n  &lt;React.StrictMode&gt;\n<span class=\"hljs-addition\">+   &lt;React.Suspense fallback={&lt;div&gt;Loading...&lt;\/div&gt;}&gt;<\/span>\n      &lt;App \/&gt;\n<span class=\"hljs-addition\">+   &lt;\/React.Suspense&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_f660cea4c457f27b052fa3b8284fc914\" 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 if our translation file is taking a while to load, the user will see a \u201cLoading\u2026\u201d message instead of an empty page.<\/p>\n<p>\u270b\u00a0The fallback locale(s) will always be loaded. So, with our current configuration, if the active locale is Arabic (<code>ar<\/code>), both Arabic and English (<code>en<\/code>) translation files will be loaded. This is because we designated <code>en<\/code> as the fallback locale when we configured i18next. If we\u2019re missing an Arabic translation, we want our users to its English counterpart, so this makes sense.<\/p>\n<p>\ud83d\udd17 You can override the translation file path and other options when you configure the HTTP API backend. Learn more on the <a href=\"https:\/\/github.com\/i18next\/i18next-http-backend\">official plugin page<\/a>.<!-- notionvc: 0db5ed81-f1e1-4e18-bfbe-a214c38c98cb --><\/p>\n<p>That&#8217;s the basics of async translation file loading done. With a few lines of code, we\u2019ve made our app significantly more scaleable.<\/p>\n<p><!-- notionvc: 188fc021-e2b1-4524-ad8b-fb290204eee7 --><\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-get-and-set-the-active-locale\"><\/span>How do I get and set the active locale?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In our components and custom hooks, we can use the <code>useTranslation()<\/code> hook to get the i18next instance, called <code>i18n<\/code>. This object allows us to retrieve and set the active locale.<\/p>\n<p><!-- notionvc: 0892c9d7-a11d-489b-913e-7f322e83fe8c --><\/p>\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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ In our components or hooks<\/span>\n<span class=\"hljs-keyword\">import<\/span> { useTranslation } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-i18next\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ ...<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> { i18n } = useTranslation();\n\n<span class=\"hljs-keyword\">const<\/span> activeLocale = i18n.resolvedLanguage; \n<span class=\"hljs-comment\">\/\/ =&gt; \"en\" when active locale is English<\/span>\n\ni18n.changeLanguage(<span class=\"hljs-string\">\"ar\"<\/span>);\n<span class=\"hljs-comment\">\/\/ Active locale is now Arabic; components<\/span>\n<span class=\"hljs-comment\">\/\/ will re-render to reflect this.<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><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_90fd114979978ccbd0087d977eaa41da\" 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 have two properties for accessing the active locale:<\/p>\n<ul>\n<li><code>i18n.language<\/code> is either the detected language (if we\u2019re using browser detection, more on that later) or the one set directly via <code>i18n.changeLanguage()<\/code>.<\/li>\n<li><code>i18n.resolvedLanguage<\/code> is the <em>actual<\/em> language used, after resolving fallback, and the one that has a corresponding translation file.<\/li>\n<\/ul>\n<p>For example, let\u2019s say we called <code>i18n.changeLanguage(\"ar-SA\")<\/code>, attempting to change the language in our app to Saudi Arabian Arabic. We don\u2019t have any <code>ar-SA<\/code> translation file, so i18next will fall back to our <code>ar<\/code> file. In this case:<\/p>\n<ul>\n<li><code>i18n.language === \"ar-SA\"<\/code><\/li>\n<li><code>i18n.resolvedLanguage === \"ar\"<\/code><\/li>\n<\/ul>\n<p>\ud83d\udd17 Read more about the <a href=\"https:\/\/www.i18next.com\/overview\/api#resolvedlanguage\">resolved language<\/a> in the official docs.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-build-a-language-switcher\"><\/span>How do I build a language switcher?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We often want a way for users to manually select their preferred locale. We can use the <code>i18n<\/code> members we just covered, <code>i18n.resolvedLanguage<\/code> and <code>i18n.changeLanguage()<\/code>, to build a locale-switching UI for our users.<\/p>\n<p>First, let\u2019s add a supported languages object in our configuration.<\/p>\n<p><!-- notionvc: 6430d396-2921-4b44-8fc6-a00b758c4c8e --><\/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\/i18n\/config.ts\n\n  import i18n from \"i18next\";\n  import HttpApi from \"i18next-http-backend\";\n  import { initReactI18next } from \"react-i18next\";\n\n<span class=\"hljs-addition\">+ \/\/ Add names for each locale to<\/span>\n<span class=\"hljs-addition\">+ \/\/ show the user in our locale<\/span>\n<span class=\"hljs-addition\">+ \/\/ switcher.<\/span>\n<span class=\"hljs-addition\">+ export const supportedLngs = {<\/span>\n<span class=\"hljs-addition\">+   en: \"English\",<\/span>\n<span class=\"hljs-addition\">+   ar: \"Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)\",<\/span>\n<span class=\"hljs-addition\">+ };<\/span>\n\n  i18n\n    .use(HttpApi)\n    .use(initReactI18next)\n    .init({\n      lng: \"en\",\n      fallbackLng: \"en\",\n<span class=\"hljs-addition\">+     \/\/ Explicitly tell i18next our<\/span>\n<span class=\"hljs-addition\">+     \/\/ supported locales.<\/span>\n<span class=\"hljs-addition\">+     supportedLngs: Object.keys(supportedLngs),<\/span>\n      debug: true,\n      interpolation: {\n        escapeValue: false,\n      },\n    });\n\n  export default i18n;<\/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_2c8f105effed55cf772af2bbbaaa2cbb\" 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 export our <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">supportedLngs<\/span> object because we\u2019ll need it in our locale switcher. Speaking of which:<!-- notionvc: e3fa0289-d637-4401-b308-10e82332751a --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/LocaleSwitcher.tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { useTranslation } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-i18next\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { supportedLngs } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/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\">LocaleSwitcher<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> { i18n } = useTranslation();\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;div className=<span class=\"hljs-string\">\"...\"<\/span>&gt;\n      &lt;div className=<span class=\"hljs-string\">\"...\"<\/span>&gt;\n        &lt;select\n          value={i18n.resolvedLanguage}\n          onChange={<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">e<\/span><\/span>) =&gt;<\/span> i18n.changeLanguage(e.target.value)}\n        &gt;\n          {<span class=\"hljs-built_in\">Object<\/span>.entries(supportedLngs).map(<span class=\"hljs-function\">(<span class=\"hljs-params\">&#91;<span class=\"hljs-params\">code<\/span>, <span class=\"hljs-params\">name<\/span>]<\/span>) =&gt;<\/span> (\n            &lt;option value={code} key={code}&gt;\n              {name}\n            &lt;<span class=\"hljs-regexp\">\/option&gt;\n          ))}\n        &lt;\/<\/span>select&gt;\n      &lt;<span class=\"hljs-regexp\">\/div&gt;\n    &lt;\/<\/span>div&gt;\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\">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_0affdc708b4f849d2148dc251d043276\" 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 Get the full code listing for <code>LocaleSwitcher<\/code>, including icon and styles, from <a href=\"https:\/\/github.com\/mdurmusphrase\/react-i18next-iv\/blob\/main\/src\/i18n\/LocaleSwitcher.tsx\">our GitHub repo<\/a>.<\/p>\n<p>We use <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Object\/entries\">Object.entries<\/a> to convert our <code>{ en: \"English\", ...}<\/code> object to an array of arrays, <code>[[\"en\", \"English\"], ...]<\/code>. We then destructure the elements of this array, converting them to <code>&lt;option value=\"en\"&gt;English&lt;\/option&gt;<\/code> elements for our <code>&lt;select&gt;<\/code>.<\/p>\n<p>Plugging this <code>&lt;LocaleSwitcher&gt;<\/code> into our app header, we get this:<\/p>\n<p><!-- notionvc: e6ff3c30-a3c7-4cc3-90fb-ba598eb539da --><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-73589\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/locale-switcher.gif\" alt=\"Switching between English and Arabic using the locale switcher UI.\" width=\"600\" height=\"158\" \/><\/p>\n<p><span role=\"img\" aria-label=\"\ud83d\udce3\">\ud83d\udce3<\/span> Thank you to The Icon Z for providing their <a class=\"notion-link-token notion-focusable-token notion-enable-hover\" tabindex=\"0\" href=\"https:\/\/thenounproject.com\/icon\/language-3325148\/\" rel=\"noopener noreferrer\" data-token-index=\"3\"><span class=\"link-annotation-unknown-block-id-1717553627\">Language icon on The Noun Project<\/span><\/a>.<!-- notionvc: 643f53df-182c-4e25-b4d1-eaeec95c4fb9 --><\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-automatically-detect-the-users-language\"><\/span>How do I automatically detect the user\u2019s language?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Our solution so far has relied on the user understanding the default language (English in our case) and then selecting a different one if need be. But not everyone can read English. It might be a good idea to detect the language preferences in the user\u2019s browser and present our website in a language as close to that as possible.<\/p>\n<p>Luckily, an <a href=\"https:\/\/github.com\/i18next\/i18next-browser-languageDetector\">official i18next language detection plugin<\/a> makes automatic language detection a breeze. Let\u2019s install it.<\/p>\n<p><!-- notionvc: fc1183af-aef9-45dd-99a0-a56779a17465 --><\/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=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm install i18next-browser-languagedetector<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><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_5f2e93b56773900884490d4d7669d20e\" 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 need to wire up the plugin when we configure the i18next instance.<!-- notionvc: fca83e7d-d5e2-42b0-88e2-a88dc54d54d2 --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/config.ts\n\n  import i18n from \"i18next\";\n<span class=\"hljs-addition\">+ import LanguageDetector from \"i18next-browser-languagedetector\";<\/span>\n  import HttpApi from \"i18next-http-backend\";\n  import { initReactI18next } from \"react-i18next\";\n\n  export const supportedLngs = {\n    en: \"English\",\n    ar: \"Arabic (\u0627\u0644\u0639\u0631\u0628\u064a\u0629)\",\n  };\n\n  i18n\n    .use(HttpApi)\n<span class=\"hljs-addition\">+   .use(LanguageDetector)<\/span>\n    .use(initReactI18next)\n    .init({\n<span class=\"hljs-deletion\">-     \/\/ We need to remove this explicit setting<\/span>\n<span class=\"hljs-deletion\">-     \/\/ of the the active locale, or it will<\/span>\n<span class=\"hljs-deletion\">-     \/\/ override the auto-detected locale.<\/span>\n<span class=\"hljs-deletion\">-     lng: \"en\",<\/span>\n      fallbackLng: \"en\",\n      supportedLngs: Object.keys(supportedLngs),\n      debug: true,\n      interpolation: {\n        escapeValue: false,\n      },\n    });\n\n  export default i18n;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><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_d31f535ddec9303d1d60d09f2748bebc\" 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 keep the <code>lng<\/code> setting in our config, it will always override the detected locale.<\/p>\n<p>OK, let\u2019s test out this new setup. We can open our browser\u2019s language settings and make sure that Arabic is at the top of the list (or any language you support in your app).<\/p>\n<p><!-- notionvc: 924295cc-50f3-4c0c-b858-5be939269d3c --><\/p>\n<figure id=\"attachment_73595\" aria-describedby=\"caption-attachment-73595\" style=\"width: 1318px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-73595\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/browser-lang-settings.png\" alt=\"Firefox language settings showing Egyptian Arabic at the top of the preference list.\" width=\"1318\" height=\"914\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/browser-lang-settings.png 1318w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/browser-lang-settings-300x208.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/browser-lang-settings-1024x710.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/browser-lang-settings-768x533.png 768w\" sizes=\"(max-width: 1318px) 100vw, 1318px\" \/><figcaption id=\"caption-attachment-73595\" class=\"wp-caption-text\">Firefox language settings showing Egyptian Arabic at the top of the preference list.<\/figcaption><\/figure>\n<p>If we visit our website now, we\u2019ll see it displayed in Arabic. This is the language detector doing its work: It\u2019s reading the browser setting exposed in the JavaScript <code>navigator<\/code> object and matching it with one of our app\u2019s supported languages. Here, <code>ar-EG<\/code> causes a fallback to our supported <code>ar<\/code>, so our site is presented in Arabic.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"detection-sources\"><\/span>Detection sources<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>By default, the <code>i18next-browser-languagedetector<\/code> goes through a cascade of sources when auto-detecting the locale. If it finds a locale in one of these it stops and resolves to that locale. Otherwise, it keeps going down the list. Here is the <a href=\"https:\/\/github.com\/i18next\/i18next-browser-languageDetector\/blob\/9efebe6ca0271c3797bc09b84babf1ba2d9b4dbb\/src\/index.js#L11\">default cascade<\/a>:<\/p>\n<ol>\n<li>The URL query string. You can try this: Visit <code>\/?lng=en<\/code> and the site should switch to English.<\/li>\n<li>A cookie (named <code>i18next<\/code> by default) that stores the value of the last resolved locale.<\/li>\n<li>A <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Window\/localStorage\">localeStorage<\/a> key (named <code>i18nextLng<\/code> by default) that stores the value of the last resolved locale.<\/li>\n<li>A <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Window\/sessionStorage\">sessionStorage<\/a> key (named <code>i18nextLng<\/code> by default) that stores the value of the last resolved locale.<\/li>\n<li>The <code>navigator<\/code> object, which exposes the languages in the browser settings.<\/li>\n<li>The <code>&lt;html lang&gt;<\/code> attribute.<\/li>\n<\/ol>\n<p>\ud83d\udd17 Much like other i18next features, the detector is highly configurable, and even supports detection from the URL path or subdomain. It even allows for custom detection logic. Check out the <a href=\"https:\/\/github.com\/i18next\/i18next-browser-languageDetector\/tree\/master\">official detector docs<\/a> for more info.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"caching-the-resolved-locale\"><\/span>Caching the resolved locale<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The default behavior of the detector is to cache the last locale it resolved in localStorage. So the first time a user visits our site, the detector will go through its normal cascade and land on a locale (likely one configured in the user&#8217;s browser or one of our fallbacks). Every time the user visits after that, the detector will read the value it stored in localStorage and use <em>that<\/em> locale without looking further.<\/p>\n<p>This caching behavior happens on <code>i18next.init()<\/code> and <code>i18n.changeLanguage()<\/code>, so if the user manually selects a locale she prefers, this will override any other detection.<\/p>\n<p>So basically we make a best effort to detect the user\u2019s locale, but ultimately leave the choice to the user.<\/p>\n<p>\ud83e\udd3f We dive into detecting the user\u2019s locale on the server and browser, and even get into geolocation, in our dedicated guide, <a href=\"https:\/\/phrase.com\/blog\/posts\/detecting-a-users-locale\/\">Detecting a User\u2019s Locale in a Web App<\/a>.<\/p>\n<p><!-- notionvc: f9459e17-ccc5-428f-af81-9d1799cbfacf --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n\n<div id=\"acf\/text-block_c59095d8d9dc5d9c3b9fdea36db0caa9\" 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-right-to-left-languages\"><\/span>How do I work with right-to-left languages?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Hundreds of millions of people in the world read and speak right-to-left languages like Arabic, Hebrew, Urdu, and more. Yet RTL (right-to-left) is often an afterthought in localization efforts. Some years ago supporting RTL was a bit of a pain. Today modern browsers simplify the process of supporting RTL document flow, handling most of the complexities. Developers only need to perform minimal adjustments in their applications to fully support RTL.<\/p>\n<p>i18next can detect the directionality of a given locale via its<code>i18n.dir()<\/code> method. Let\u2019s use it to write a custom hook for setting the document direction depending on the active locale.<\/p>\n<p><!-- notionvc: 81765e4a-d11e-4372-aa46-d93bee2792d7 --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/useLocalizeDocumentAttributes.ts<\/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> { useTranslation } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-i18next\"<\/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\">useLocalizeDocumentAttributes<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> { i18n } = useTranslation();\n\n  useEffect(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    <span class=\"hljs-keyword\">if<\/span> (i18n.resolvedLanguage) {\n   \n      <span class=\"hljs-comment\">\/\/ Set the &lt;html lang&gt; attribute.<\/span>\n      <span class=\"hljs-built_in\">document<\/span>.documentElement.lang = i18n.resolvedLanguage;\n\n      <span class=\"hljs-comment\">\/\/ Set the &lt;html dir&gt; attribute.<\/span>\n      <span class=\"hljs-built_in\">document<\/span>.documentElement.dir = i18n.dir(i18n.resolvedLanguage);\n    }\n  }, &#91;i18n, i18n.resolvedLanguage]);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><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_3a74295eb3e49f58e75dd195fc13a8da\" 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\uddd2\ufe0f Recall that <code>resolvedLanguage<\/code> is the language we support that best matches the selected or detected language. The <code>resolvedLanguage<\/code> is effectively the active locale.<\/p>\n<p>To change the overall document direction to RTL, we just need to set <code>&lt;html dir=\"rtl\"&gt;<\/code>. All modern browsers support this and will reflow the page accordingly. We access the <code>&lt;html&gt;<\/code> element via <code>document.documentElement<\/code>.<\/p>\n<p>\ud83d\uddd2\ufe0f\u00a0It\u2019s also good practice to set the <code>&lt;html lang&gt;<\/code> attribute (it helps with accessibility, user experience, search engine optimization, and more).<\/p>\n<p>Let\u2019s add our new hook to our <code>&lt;App&gt;<\/code> component to see it in action.<\/p>\n<p><!-- notionvc: 7011d994-2542-4208-97d2-247d5c68555f --><\/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\/App.tsx\n\n  import { useTranslation } from \"react-i18next\";\n<span class=\"hljs-addition\">+ import useLocalizeDocumentAttributes from \".\/i18n\/useLocalizeDocumentAttributes\";<\/span>\n  import Header from \".\/layout\/Header\";\n \n  function App() {\n    const { t } = useTranslation();\n \n<span class=\"hljs-addition\">+   useLocalizeDocumentAttributes();<\/span>\n \n    return (\n      &lt;div className=\"...\"&gt;\n        {\/* ... *\/} \n      &lt;\/div&gt;\n    );\n }\n\n export default App;<\/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_dfc92c507dc20e1aa10db734e989c3de\" 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_74108\" aria-describedby=\"caption-attachment-74108\" style=\"width: 600px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-74108\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/rtl.gif\" alt=\"\" width=\"600\" height=\"215\" \/><figcaption id=\"caption-attachment-74108\" class=\"wp-caption-text\">Our app now flows right-to-left when shown in Arabic.<\/figcaption><\/figure>\n<p>\ud83d\udd17\u00a0<code>i18n.dir()<\/code> is part of the <a href=\"https:\/\/www.i18next.com\/overview\/api#dir\">i18n object API<\/a>.<\/p>\n<p>\ud83d\uddd2\ufe0f\u00a0There\u2019s a bit more to RTL than just flipping the <code>&lt;html dir&gt;<\/code>. For example, we often need to make sure that our horizontal spacing (margin, padding) is in the correct direction. Using <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_logical_properties_and_values\">CSS logical properties<\/a> (<code>margin-block-start<\/code> instead of <code>margin-left<\/code>) can help with this.<\/p>\n<p><!-- notionvc: a9989852-3704-4160-a4a4-e05cb1349ee0 --><\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-the-document-title\"><\/span>How do I localize the document title?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Of course, our page\u2019s <code>&lt;title&gt;<\/code> is very important for search engine optimization (SEO) and our user\u2019s experience \u2014\u00a0the title is shown in the browser tab after all. Localizing a page title is straightforward. First, we add translation messages for the page\u2019s title:<\/p>\n<p><!-- notionvc: ef662022-a8b0-4601-b207-9ab2f94f7b67 --><\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ public\/locales\/en\/translation.json\n {\n<span class=\"hljs-addition\">+  \"app_title\": \"React + i18next Playground\",<\/span>\n   \"hello_world\": \"Hello, World!\"\n }\n\n\/\/ public\/locales\/ar\/translation.json\n {\n<span class=\"hljs-addition\">+  \"app_title\": \"\u0645\u0644\u0639\u0628 \u0631\u064a\u0623\u0643\u062a \u0648 \u0623\u064a \u0625\u064a\u062a\u064a\u0646 \u0646\u0643\u0633\u062a\",<\/span>\n   \"hello_world\": \"\u0645\u0631\u062d\u0628\u0627\u064b \u0628\u0627\u0644\u0639\u0627\u0644\u0645!\"\n }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><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_4b2ed4327556777425c20345d0d30f2c\" 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, we update our <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">useLocalizeDocumentAttributes<\/span><\/code> hook to set the document title.<!-- notionvc: 421218a2-868c-405a-8bfc-dad292289b99 --><\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/useLocalizeDocumentAttributes.ts\n\n import { useEffect } from \"react\";\n import { useTranslation } from \"react-i18next\";\n \n export default function useLocalizeDocumentAttributes() {\n<span class=\"hljs-deletion\">-  const { i18n } = useTranslation();<\/span>\n<span class=\"hljs-addition\">+  const { t, i18n } = useTranslation();<\/span>\n \n   useEffect(() =&gt; {\n     if (i18n.resolvedLanguage) {\n       document.documentElement.lang = i18n.resolvedLanguage;\n       document.documentElement.dir = i18n.dir(i18n.resolvedLanguage);\n     }\n\n<span class=\"hljs-addition\">+    \/\/ \ud83d\udc47 Localize document title.<\/span>\n<span class=\"hljs-addition\">+    document.title = t(\"app_title\");<\/span>\n\n<span class=\"hljs-deletion\">-  }, &#91;i18n, i18n.resolvedLanguage]);<\/span>\n<span class=\"hljs-addition\">+  }, &#91;i18n, i18n.resolvedLanguage, t]);<\/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\">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_571c279112b2a7682cdb5b0f3a636d57\" 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_74114\" aria-describedby=\"caption-attachment-74114\" style=\"width: 1168px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-74114\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-title.png\" alt=\"Our page\u2019s title shown in English.\" width=\"1168\" height=\"550\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-title.png 1168w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-title-300x141.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-title-1024x482.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-title-768x362.png 768w\" sizes=\"(max-width: 1168px) 100vw, 1168px\" \/><figcaption id=\"caption-attachment-74114\" class=\"wp-caption-text\">Our page\u2019s title shown in English.<\/figcaption><\/figure>\n<figure id=\"attachment_74120\" aria-describedby=\"caption-attachment-74120\" style=\"width: 1168px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-74120\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-title.png\" alt=\"Our page\u2019s title shown in Arabic.\" width=\"1168\" height=\"550\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-title.png 1168w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-title-300x141.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-title-1024x482.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-title-768x362.png 768w\" sizes=\"(max-width: 1168px) 100vw, 1168px\" \/><figcaption id=\"caption-attachment-74120\" class=\"wp-caption-text\">Our page\u2019s title shown in Arabic.<\/figcaption><\/figure>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-work-with-dynamic-values-in-my-translations\"><\/span>How do I work with dynamic values in my translations?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We often have strings that must be injected into translation messages at runtime. A good example of this is the logged in user\u2019s name e.g. \u201cHello, <code>username<\/code>!\u201d where <code>username<\/code> should be replaced at runtime and not hard-coded into the message.<\/p>\n<p>i18next supports this through interpolation: specifying a variable in a translation message and swapping it out at runtime. We use a <code>{{variable}}<\/code> syntax in our messages to achieve this. Let\u2019s add a new translation message with interpolated variables:<\/p>\n<p><!-- notionvc: db77ee7f-3b18-4895-84ba-a818ced4cdc6 --><\/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\">\/\/ public\/locales\/en\/translation.json\n\n  {\n    \"app_title\": \"React + i18next Playground\",\n    \"hello_world\": \"Hello, World!\"\n<span class=\"hljs-addition\">+   \"user_greeting\": \"Hello, {{firstName}} {{lastName}} \ud83d\udc4b\"<\/span>\n  }\n\n\/\/ public\/locales\/ar\/translation.json\n \n  {\n    \"app_title\": \"\u0645\u0644\u0639\u0628 \u0631\u064a\u0623\u0643\u062a \u0648 \u0623\u064a \u0625\u064a\u062a\u064a\u0646 \u0646\u0643\u0633\u062a\",\n    \"hello_world\": \"\u0645\u0631\u062d\u0628\u0627\u064b \u0628\u0627\u0644\u0639\u0627\u0644\u0645!\",\n<span class=\"hljs-addition\">+   \"user_greeting\": \"\u0623\u0647\u0644\u0627\u064b \u0628\u0643 {{firstName}} {{lastName}} \ud83d\udc4b\"<\/span>\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_51ade81fadbd24869da353605cbe247f\" 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 the <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">firstName<\/span><\/code> and <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">lastName<\/span><\/code> identifiers as params when we call <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"5\">t()<\/span><\/code>, swapping in their actual values at runtime.<!-- notionvc: fa0a927a-af64-4874-846e-fb7f42973b94 --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ In our component<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { useTranslation } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-i18next\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ A pretend service just to demonstrate.<\/span>\n<span class=\"hljs-keyword\">import<\/span> { useLoggedInUser } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"..\/some\/fake\/service\"<\/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\">MyComponent<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> { t } = useTranslation();\n  <span class=\"hljs-keyword\">const<\/span> user = useLoggedInUser();\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;p&gt;\n      {t(<span class=\"hljs-string\">\"user_greeting\"<\/span>, {\n        <span class=\"hljs-comment\">\/\/ \ud83d\udc47 Interpolate these at runtime.<\/span>\n        firstName: user.firstName,\n        lastName: user.LastName,\n      })\n    &lt;<span class=\"hljs-regexp\">\/p&gt;\n  );\n}<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><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_67bde8bac7e1e0ab7e1f32d3cb8add0c\" 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 second param to <code>t()<\/code> can be a map of values to swap into the translation message at runtime. We can have as many values as we want in a message; we just need to remember to include a key\/value in our map for each one we have in our message.<\/p>\n<p>In our <a href=\"https:\/\/stackblitz.com\/~\/github.com\/mdurmusphrase\/react-i18next-iv\">live StackBlitz demo<\/a>, we use text inputs to create the interpolated values at runtime.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-74126\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/interpolation.gif\" alt=\"As we type, the values of the input fields are interpolated into a translation message at runtime.\" width=\"600\" height=\"227\" \/><\/p>\n<p>\ud83d\udd17\u00a0<a href=\"https:\/\/stackblitz.com\/~\/github.com\/mdurmusphrase\/react-i18next-iv\">Try the StackBlizt demo<\/a> for yourself. You can also <a href=\"https:\/\/github.com\/mdurmusphrase\/react-i18next-iv\/blob\/main\/src\/playgrounds\/Interpolation.tsx\">grab the interpolation code from GitHub<\/a>.<\/p>\n<p>\ud83e\udd3f\u00a0As usual, i18next provides a lot of options for interpolation, including changing the <code>{{}}<\/code> specifiers, passing in entire objects, unescaping HTML, and more. The official <a href=\"https:\/\/www.i18next.com\/translation-function\/interpolation\">Interpolation<\/a> docs have you covered here.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-work-with-plurals-in-my-translations\"><\/span>How do I work with plurals in my translations?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>While English has straightforward singular and plural forms (like &#8220;apple&#8221; vs &#8220;apples&#8221;), other languages like Russian and Arabic have more complex pluralization rules. Luckily, i18next makes this complexity easy to navigate. For a plural message, we simply need to provide each of its <em>plural forms<\/em> in the current language.<\/p>\n<p><!-- notionvc: 3e9e4d81-aaeb-4d35-a3e0-469a71889ab7 --><\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ public\/locales\/en\/translation.json<\/span>\n\n{\n    <span class=\"hljs-comment\">\/\/ ...<\/span>\n    <span class=\"hljs-attr\">\"trees_grown_one\"<\/span>: <span class=\"hljs-string\">\"We have grown one tree \ud83c\udf33\"<\/span>,\n    <span class=\"hljs-attr\">\"trees_grown_other\"<\/span>: <span class=\"hljs-string\">\"We have grown {{count}} trees \ud83c\udf33\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><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_82e1a6edca74323286ec09092a5d936d\" 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, English has two plural forms, <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">one<\/span><\/code> and <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">other<\/span><\/code>. i18next uses a suffix syntax to specify all the plural forms for a message, just like you see above. In our components, we use these as a <span class=\"notion-enable-hover\" data-token-index=\"5\">single<\/span> message, dropping the suffixes from the key.<!-- notionvc: 1b62e7e6-1ae5-4e9e-b401-894b71e9d0a2 --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">{<span class=\"hljs-comment\">\/* Notice how we don't use _one or _other here. *\/<\/span>}\n&lt;p&gt;{t(<span class=\"hljs-string\">\"trees_grown\"<\/span>, { count: <span class=\"hljs-number\">3<\/span> })}&lt;<span class=\"hljs-regexp\">\/p&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><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_6b92c4e3dc90ca4fb0b5dbbe3e3e0d2b\" 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>When i18next sees the special <code>count<\/code> variable, it knows it\u2019s working with a plural message, and resolves the appropriate plural form based on the <code>count<\/code>.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-74132\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-plurals.gif\" alt=\"As we change the value of count in the number field, the English plural form is automatically selected by i18next at runtime.\" width=\"600\" height=\"256\" \/><\/p>\n<p>\ud83d\uddd2\ufe0f\u00a0You may have noticed that the zero case above has a separate message. While <code>zero<\/code> is not an official plural form in English, i18next always supports the zero case. For the above, we just added a <code>trees_grown_zero<\/code> message to our English translation file.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"working-with-complex-plurals\"><\/span>Working with complex plurals<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>While many languages have <code>one | other<\/code> plural forms, many others don\u2019t. Arabic, for example, has six plural forms. i18next makes it easy to add these using the same suffix syntax.<\/p>\n<p><!-- notionvc: c67ee5eb-f476-4842-8a75-b1475262bd70 --><\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ public\/locales\/ar\/translation.json<\/span>\n\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-attr\">\"trees_grown_zero\"<\/span>: <span class=\"hljs-string\">\"\u0644\u0645 \u0646\u0632\u0631\u0639 \u0623\u064a \u0634\u062c\u0631\u0629 \u0628\u0639\u062f\"<\/span>,\n  <span class=\"hljs-attr\">\"trees_grown_one\"<\/span>: <span class=\"hljs-string\">\"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 \u0634\u062c\u0631\u0629 \u0648\u0627\u062d\u062f\u0629 \ud83c\udf33\"<\/span>,\n  <span class=\"hljs-attr\">\"trees_grown_two\"<\/span>: <span class=\"hljs-string\">\"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 \u0634\u062c\u0631\u062a\u064a\u0646 \ud83c\udf33\"<\/span>,\n  <span class=\"hljs-attr\">\"trees_grown_few\"<\/span>: <span class=\"hljs-string\">\"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 {{count}} \u0623\u0634\u062c\u0627\u0631 \ud83c\udf33\"<\/span>,\n  <span class=\"hljs-attr\">\"trees_grown_many\"<\/span>: <span class=\"hljs-string\">\"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 {{count}} \u0634\u062c\u0631\u0629 \ud83c\udf33\"<\/span>,\n  <span class=\"hljs-attr\">\"trees_grown_other\"<\/span>: <span class=\"hljs-string\">\"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 {{count}} \u0634\u062c\u0631\u0629 \ud83c\udf33\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><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_900284a97a96e47b3ef66214e91b1b8d\" 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 don\u2019t need to change our <code>t()<\/code> call at all here. <code>t(\"trees_grown\", { count: 1 })<\/code> works just as it did before. i18next sees <code>count<\/code> and knows to resolve a plural form in the active locale (Arabic in this case).<\/p>\n<figure id=\"attachment_74146\" aria-describedby=\"caption-attachment-74146\" style=\"width: 962px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-74146 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-plural-western-count.png\" alt=\"As we change the count variable, i18next automatically selects the correct Arabic plural form for the message.\" width=\"962\" height=\"236\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-plural-western-count.png 962w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-plural-western-count-300x74.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-plural-western-count-768x188.png 768w\" sizes=\"(max-width: 962px) 100vw, 962px\" \/><figcaption id=\"caption-attachment-74146\" class=\"wp-caption-text\">A count of 3 will resolve to the Arabic plural few form.<\/figcaption><\/figure>\n<p>\ud83d\uddd2\ufe0f\u00a0You might be wondering where to find the plural forms for a language you\u2019re not familiar with. There\u2019s a <a href=\"https:\/\/jsfiddle.net\/6bpxsgd4\">handy web tool<\/a> that you can use to get the correct suffixes for a language. The canonical source is the <a href=\"https:\/\/www.unicode.org\/cldr\/charts\/42\/supplemental\/language_plural_rules.html\">Language Plural Rules<\/a> listing for the CLDR (Common Locale Data Repository).<\/p>\n<h3><span class=\"ez-toc-section\" id=\"using-the-correct-count-numerals\"><\/span>Using the correct count numerals<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Let\u2019s fix one issue here before we move on: In the Arabic messages above, we\u2019re showing the same numerals we use in English (0, 1, 2, \u2026). However, in many Arabic regions, the official numerals are Eastern Arabic numerals (\u0660,\u0661,\u0662, \u2026). To ensure that i18next formats our counts appropriately for the active locale, we must specify the <code>number<\/code> format in our translation messages.<\/p>\n<p><!-- notionvc: d957f357-c743-453d-b405-6120a0195474 --><\/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\">\/\/ public\/locales\/en\/translation.json\n\n {\n     \/\/ ...\n     \"trees_grown_one\": \"We have grown one tree \ud83c\udf33\",\n<span class=\"hljs-deletion\">-    \"trees_grown_other\": \"We have grown {{count}} trees \ud83c\udf33\"<\/span>\n<span class=\"hljs-addition\">+    \"trees_grown_other\": \"We have grown {{count, number}} trees \ud83c\udf33\"<\/span>\n }\n\n\/\/ public\/locales\/ar\/translation.json\n\n {\n   \/\/ ...\n   \"trees_grown_zero\": \"\u0644\u0645 \u0646\u0632\u0631\u0639 \u0623\u064a \u0634\u062c\u0631\u0629 \u0628\u0639\u062f\",\n   \"trees_grown_one\": \"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 \u0634\u062c\u0631\u0629 \u0648\u0627\u062d\u062f\u0629 \ud83c\udf33\",\n   \"trees_grown_two\": \"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 \u0634\u062c\u0631\u062a\u064a\u0646 \ud83c\udf33\",\n<span class=\"hljs-deletion\">-  \"trees_grown_few\": \"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 {{count}} \u0623\u0634\u062c\u0627\u0631 \ud83c\udf33\",<\/span>\n<span class=\"hljs-addition\">+  \"trees_grown_few\": \"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 {{count, number}} \u0623\u0634\u062c\u0627\u0631 \ud83c\udf33\",<\/span>\n<span class=\"hljs-deletion\">-  \"trees_grown_many\": \"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 {{count}} \u0634\u062c\u0631\u0629 \ud83c\udf33\",<\/span>\n<span class=\"hljs-addition\">+  \"trees_grown_many\": \"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 {{count, number}} \u0634\u062c\u0631\u0629 \ud83c\udf33\",<\/span>\n<span class=\"hljs-deletion\">-  \"trees_grown_other\": \"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 {{count}} \u0634\u062c\u0631\u0629 \ud83c\udf33\"<\/span>\n<span class=\"hljs-addition\">+  \"trees_grown_other\": \"\u0644\u0642\u062f \u0632\u0631\u0639\u0646\u0627 {{count, number}} \u0634\u062c\u0631\u0629 \ud83c\udf33\"<\/span>\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_b2610f73897ccd2e841c5cbee6a510f0\" 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 update, our Arabic plurals look correct.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-74140\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-plurals.gif\" alt=\"Arabic plural translations showing the interpolated count in Eastern Arabic numerals.\" width=\"600\" height=\"248\" \/><\/p>\n<p>\u270b\u00a0In fact, this isn\u2019t a bullet-proof solution for rendering our localized <code>count<\/code>, since we\u2019re currently relying on the browser\u2019s default <em>region<\/em> for Arabic when formatting the number. We fix this in the next section when we implement a custom number formatter.<\/p>\n<p>\ud83e\udd3f\u00a0Go deeper with <a href=\"https:\/\/phrase.com\/blog\/posts\/pluralization\/\">Pluralization: A Guide to Localizing Plurals<\/a>, where we cover the powerful ICU Message Syntax and ordinal plurals, all while using i18next.<\/p>\n<p>\ud83d\udd17\u00a0Play with plurals in our <a href=\"https:\/\/stackblitz.com\/~\/github.com\/mdurmusphrase\/react-i18next-iv\">StackBlitz demo<\/a>. You can also <a href=\"https:\/\/github.com\/mdurmusphrase\/react-i18next-iv\/blob\/main\/src\/playgrounds\/Plurals.tsx\">grab the plural code from GitHub<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-format-localized-numbers\"><\/span>How do I format localized numbers?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>i18n is more than just string translations. Working with numbers and dates is crucial for most apps, and each region of the world handles number and date formatting differently.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"a-note-on-regional-formatting\"><\/span>A note on regional formatting<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Number and date formatting are determined by region, not just language. For example, the US and Canada both use English but have different date formats and measurement units. So it&#8217;s better to use a qualified locale (like <code>en-US<\/code>) instead of just a language code (<code>en<\/code>).<\/p>\n<p>Using a language code alone, such as <code>ar<\/code> for Arabic, can lead to inconsistencies. Different browsers might default to various regions, like Saudi Arabia (<code>ar-SA<\/code>) or Egypt (<code>ar-EG<\/code>), resulting in varied date formats due to distinct regional calendars.<\/p>\n<p>In our tutorials, we usually use qualified locales (like <code>en-US<\/code>, <code>ar-EG<\/code>) to avoid such ambiguities. However, i18next can be challenging when working with fallbacks to a qualified locale: It <em>always<\/em> wants to fall back to the language-only version, so it\u2019s difficult to set a qualified locale as a default fallback. So we\u2019ll keep <code>en<\/code> and <code>ar<\/code> as our supported app locales and use custom formatters to enforce explicit regional formatting (more on custom formatters shortly).<\/p>\n<p>\ud83d\uddd2\ufe0f\u00a0Alternatively, we could use custom fallback logic to override i18next\u2019s insistence on language-only fallbacks. See the i18next docs <a href=\"https:\/\/www.i18next.com\/principles\/fallback#fallback-to-different-languages\">Fallback<\/a> section for an example of writing a custom fallback function.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"numbers-in-messages\"><\/span>Numbers in messages<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>We touched on number formatting in our translation messages earlier when we formatted the <code>count<\/code> variable in our plurals. At its most basic, providing the <code>number<\/code> format specifier to an interpolated number in a message gets us default number formatting:<\/p>\n<p><!-- notionvc: 6659ce46-7a5a-4fed-9917-79976d3fc36c --><\/p>\n\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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ public\/locales\/en\/translation.json<\/span>\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-attr\">\"simple_number\"<\/span>: <span class=\"hljs-string\">\"A simple number (default formatting): {{value, number}}\"<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/ public\/locales\/ar\/translation.json<\/span>\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-attr\">\"simple_number\"<\/span>: <span class=\"hljs-string\">\"\u0631\u0642\u0645 \u0628\u0627\u0644\u062a\u0646\u0633\u064a\u0642 \u0627\u0644\u0625\u0641\u062a\u0631\u0627\u0636\u064a: {{value, number}}\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><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_d0dbc83f00fa37a3da2fee2db383aade\" 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 in our components:<!-- notionvc: 78c89549-cdc7-4a2e-94f4-314ff73e2fcb --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">&lt;p&gt;{t(<span class=\"hljs-string\">\"simple_number\"<\/span>, { value: <span class=\"hljs-number\">2.04<\/span> })&lt;<span class=\"hljs-regexp\">\/p&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-26\"><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_52b5e8b9d893a4b8a32be8590a418e90\" 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_74154\" aria-describedby=\"caption-attachment-74154\" style=\"width: 568px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-74154 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-simple-number.png\" alt=\"A number in an English translation message, using default formatting.\" width=\"568\" height=\"176\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-simple-number.png 568w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-simple-number-300x93.png 300w\" sizes=\"(max-width: 568px) 100vw, 568px\" \/><figcaption id=\"caption-attachment-74154\" class=\"wp-caption-text\">A number in an English translation message, using default formatting.<\/figcaption><\/figure>\n<figure id=\"attachment_74160\" aria-describedby=\"caption-attachment-74160\" style=\"width: 586px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-74160\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-simple-number.png\" alt=\"A number in an Arabic translation message, using default formatting.\" width=\"586\" height=\"120\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-simple-number.png 586w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-simple-number-300x61.png 300w\" sizes=\"(max-width: 586px) 100vw, 586px\" \/><figcaption id=\"caption-attachment-74160\" class=\"wp-caption-text\">A number in an Arabic translation message, using default formatting.<\/figcaption><\/figure>\n<p>The <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">number<\/span> formatter that i18next provides uses the JavaScript standard <a class=\"notion-link-token notion-focusable-token notion-enable-hover\" tabindex=\"0\" href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/NumberFormat\" rel=\"noopener noreferrer\" data-token-index=\"3\"><span class=\"link-annotation-unknown-block-id-1627935355\">Intl.NumberFormat<\/span><\/a> object under the hood. <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"5\">Intl.NumberFormat<\/span> allows for <a class=\"notion-link-token notion-focusable-token notion-enable-hover\" tabindex=\"0\" href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/NumberFormat\/NumberFormat#parameters\" rel=\"noopener noreferrer\" data-token-index=\"7\"><span class=\"link-annotation-unknown-block-id-877006329\">many formatting options<\/span><\/a>, and they\u2019re all available to us when we use the <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"9\">number<\/span> formatter. We just need to specify the options using a <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"11\">number(option1: value1; option2: value2 ...)<\/span><\/code> syntax. Here are some examples:<!-- notionvc: ffe759d9-5bf8-4780-b3e2-bd97e5f262e6 --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ public\/locales\/en\/translation.json<\/span>\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-string\">\"simple_number\"<\/span>: <span class=\"hljs-string\">\"A simple number (default formatting): {{value, number}}\"<\/span>,\n  <span class=\"hljs-string\">\"percent\"<\/span>: <span class=\"hljs-string\">\"Percentage (use values between 0 and 1): {{value, number(style: percent)}}\"<\/span>,\n  <span class=\"hljs-string\">\"custom_number\"<\/span>: <span class=\"hljs-string\">\"Custom formatting: {{value, number(minimumFractionDigits: 2; maximumFractionDigits: 4; signDisplay: always)}}\"<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/ In our components<\/span>\n&lt;p&gt;{t(<span class=\"hljs-string\">\"simple_number\"<\/span>, value: <span class=\"hljs-number\">0.333333<\/span>)}&lt;<span class=\"hljs-regexp\">\/p&gt;\n&lt;p&gt;{t(\"percent\", value: 0.333333)}&lt;\/<\/span>p&gt;\n&lt;p&gt;{t(<span class=\"hljs-string\">\"custom_number\"<\/span>, value: <span class=\"hljs-number\">0.333333<\/span>)}&lt;<span class=\"hljs-regexp\">\/p&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-27\"><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_ed197580d351afb9e3cbb9b05029bc41\" 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><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-74166\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-number-formats-in-messages.png\" alt=\"The number 0.333333 formatted in different ways, depending in the format specifier in the message.\" width=\"950\" height=\"548\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-number-formats-in-messages.png 950w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-number-formats-in-messages-300x173.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-number-formats-in-messages-768x443.png 768w\" sizes=\"(max-width: 950px) 100vw, 950px\" \/><\/p>\n<p>With <code>number(...)<\/code>, any key\/value pair between the parentheses will be passed in the second, <code>options<\/code> param of the <code>Intl.NumberFormat<\/code> constructor.<\/p>\n<p>\ud83d\udd17\u00a0Check out the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/NumberFormat\/NumberFormat\">MDN docs for Intl.NumberFormat<\/a> to see all the options available to you.<\/p>\n<p>Of course, we can\u2019t forget our other language(s):<\/p>\n<p><!-- notionvc: 65419a96-0116-44e0-8d51-d3994e275d21 --><\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ public\/locales\/ar\/translation.json<\/span>\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-attr\">\"simple\"<\/span>: <span class=\"hljs-string\">\"\u0631\u0642\u0645 \u0628\u0627\u0644\u062a\u0646\u0633\u064a\u0642 \u0627\u0644\u0625\u0641\u062a\u0631\u0627\u0636\u064a: {{value, number}}\"<\/span>,\n  <span class=\"hljs-attr\">\"percent\"<\/span>: <span class=\"hljs-string\">\"\u0627\u0644\u0646\u0633\u0628\u0629 \u0627\u0644\u0645\u0626\u0648\u064a\u0629 (\u0627\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u0642\u064a\u0645 \u0628\u064a\u0646 \u0660  \u0648 \u0661): {{value, number(style: percent)}}\"<\/span>,\n  <span class=\"hljs-attr\">\"custom_number\"<\/span>: <span class=\"hljs-string\">\"\u062a\u0646\u0633\u064a\u0642 \u0645\u062e\u0635\u0635: {{value, number(minimumFractionDigits: 2; maximumFractionDigits: 4; signDisplay: always)}}\"<\/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\">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_de40d811d99c274bcbeaf7351024aa90\" 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><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-74172\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-numbers-in-messages.png\" alt=\"The number 0.333333 represented in different formats in Arabic messages, depending on the format specified in each message.\" width=\"1010\" height=\"548\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-numbers-in-messages.png 1010w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-numbers-in-messages-300x163.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-numbers-in-messages-768x417.png 768w\" sizes=\"(max-width: 1010px) 100vw, 1010px\" \/><\/p>\n<p>\ud83e\udd3f\u00a0We can of course format currency this way as well. i18next also provides a currency shortcut: <code>{{value, currency(USD)}}<\/code>. Read about that and more, including how to pass number formatting options when calling <code>t()<\/code>, in the official <a href=\"https:\/\/www.i18next.com\/translation-function\/formatting#number\">Number Formatting<\/a> docs.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"custom-formatters\"><\/span>Custom formatters<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>To solve the region ambiguity issue we mentioned in the above section, we can make use of i18next\u2019s custom formatters, providing one of our own for numbers. This custom formatter will override the default <code>number<\/code> formatter provided by i18next, ensuring we <em>always<\/em> use a qualified locale (e.g. <code>ar-EG<\/code>) when formatting numbers.<\/p>\n<p>Remember, if we <em>don\u2019t<\/em> do this, we leave it up to the browser to decide the region. For example, the Arc browser (Chrome-based) displays our Arabic numbers as 0, 1, 2 numerals. As we mentioned earlier, many Arabic regions prefer the \u0661\u060c \u0662\u060c \u0663 (Eastern Arabic) numeral system.<\/p>\n<figure id=\"attachment_74184\" aria-describedby=\"caption-attachment-74184\" style=\"width: 934px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-74184\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/arc-ar-numbers.png\" alt=\"The Arc browser does not use Eastern Arabic numerals for Arabic numerals by default.\" width=\"934\" height=\"380\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/arc-ar-numbers.png 934w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/arc-ar-numbers-300x122.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/arc-ar-numbers-768x312.png 768w\" sizes=\"(max-width: 934px) 100vw, 934px\" \/><figcaption id=\"caption-attachment-74184\" class=\"wp-caption-text\">The Arc browser does not use Eastern Arabic numerals for Arabic numerals by default.<\/figcaption><\/figure>\n<p>This is likely due to the default <em>region<\/em> that Arc assumes for Arabic.<\/p>\n<p>OK, onto the fix. Let\u2019s create a new file to store our custom formatters and add our number formatter to it.<\/p>\n<p><!-- notionvc: e7d1f96b-fdaf-4b17-91fd-0ab9fec1ba1a --><\/p>\n\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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ src\/i18n\/formatters.tsx <\/span>\n\n<span class=\"hljs-comment\">\/**\n * Returns the default qualified locale code\n * (language-REGION) for the given locale.\n *\n * @param lng - The language code.\n * @returns The qualified locale code, \n*  including region.\n *\/<\/span>\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">qualifiedLngFor<\/span>(<span class=\"hljs-params\">lng: <span class=\"hljs-built_in\">string<\/span><\/span>): <span class=\"hljs-title\">string<\/span> <\/span>{\n  <span class=\"hljs-keyword\">switch<\/span> (lng) {\n    <span class=\"hljs-comment\">\/\/ Use Egypt as the default formatting<\/span>\n    <span class=\"hljs-comment\">\/\/ region for Arabic.<\/span>\n    <span class=\"hljs-keyword\">case<\/span> <span class=\"hljs-string\">\"ar\"<\/span>:\n      <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">\"ar-EG\"<\/span>; \n    <span class=\"hljs-comment\">\/\/ Use USA as the default formatting<\/span>\n    <span class=\"hljs-comment\">\/\/ region for English.<\/span>\n    <span class=\"hljs-keyword\">case<\/span> <span class=\"hljs-string\">\"en\"<\/span>:\n      <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">\"en-US\"<\/span>;\n    <span class=\"hljs-keyword\">default<\/span>:\n      <span class=\"hljs-keyword\">return<\/span> lng;\n  }\n}\n\n<span class=\"hljs-comment\">\/**\n * Formats a number.\n *\n * @param value - The number to format.\n * @param lng - The language to format the number in.\n * @param options - passed to Intl.NumberFormat.\n * @returns The formatted number.\n *\/<\/span>\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">number<\/span>(<span class=\"hljs-params\">\n  value: <span class=\"hljs-built_in\">number<\/span>,\n  lng: <span class=\"hljs-built_in\">string<\/span> | <span class=\"hljs-literal\">undefined<\/span>,\n  options?: <span class=\"hljs-built_in\">Intl<\/span>.NumberFormatOptions,\n<\/span>): <span class=\"hljs-title\">string<\/span> <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Intl<\/span>.NumberFormat(\n    qualifiedLngFor(lng!),\n    options,\n  ).format(value);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-29\"><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_0374b8e087fc5ba4d987eee15294dfc4\" 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 wire the new number formatter up to our i18next instance:<!-- notionvc: bc119f5f-ac25-4f22-9fb1-13854d0a2d67 --><\/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\/i18n\/config.ts\n\n  import i18next from \"i18next\";\n  import LanguageDetector from \"i18next-browser-languagedetector\";\n  import HttpApi from \"i18next-http-backend\";\n  import { initReactI18next } from \"react-i18next\";\n<span class=\"hljs-addition\">+ import { number } from \".\/formatters\";<\/span>\n\n  \/\/ ...\n\n  i18next\n    .use(HttpApi)\n    .use(LanguageDetector)\n    .use(initReactI18next)\n    .init({\n      \/\/ ...\n    });\n\n<span class=\"hljs-addition\">+ i18next.services.formatter?.add(\"number\", number);<\/span>\n\n  export default i18next;<\/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_690b2ce9e7416be70a5063e10f7cafab\" 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 whenever we use the <code>number<\/code> or <code>number(...)<\/code> specifiers in our translation messages, i18next will use our custom number formatter instead of its default. The library will pass our formatter function the following:<\/p>\n<ul>\n<li><code>value<\/code> \u2014\u00a0The runtime number to be interpolated and formatted.<\/li>\n<li><code>lng<\/code> \u2014 The active resolved locale, <code>en<\/code> or <code>ar<\/code>.<\/li>\n<li><code>options<\/code> \u2014\u00a0Any options we specified between parentheses when calling <code>number(...)<\/code>. These are parsed to an options object ready for the <code>Intl.NumberFormat<\/code> constructor.<\/li>\n<\/ul>\n<p>\ud83d\udd17\u00a0Check out the <a href=\"https:\/\/www.i18next.com\/translation-function\/formatting#adding-custom-format-function\">Adding custom format function<\/a> section in the official docs for more info, including how to cache custom formatters for performance.<\/p>\n<p>In our formatter function, we take these values and use our own instance of the standard <code>Intl.NumberFormat<\/code> object to format the number. We ensure that the locale passed to <code>Intl.NumberFormat<\/code> always has an explicit region by using <code>qualifiedLngFor()<\/code>.<\/p>\n<p>With our new formatter configured, the Arc browser will now adhere to the Egypt region when formatting our Arabic numbers. In fact, this behavior should now be consistent across <strong>all<\/strong> browsers.<\/p>\n<figure id=\"attachment_74202\" aria-describedby=\"caption-attachment-74202\" style=\"width: 910px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-74202\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-custom-number-formatter.png\" alt=\"The Arc browser now uses Eastern Arabic numerals to render Arabic numbers (except currency).\" width=\"910\" height=\"370\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-custom-number-formatter.png 910w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-custom-number-formatter-300x122.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-custom-number-formatter-768x312.png 768w\" sizes=\"(max-width: 910px) 100vw, 910px\" \/><figcaption id=\"caption-attachment-74202\" class=\"wp-caption-text\">The Arc browser now uses Eastern Arabic numerals to render Arabic numbers (except currency).<\/figcaption><\/figure>\n<p>Notice that our currency value is still using Western Arabic numerals (1, 2, 3). This is because we\u2019re formatting it using i18next\u2019s shorthand <code>currency<\/code> formatter:<\/p>\n<p><!-- notionvc: c434b62d-a965-4101-99d3-21749f99186b --><\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ public\/locales\/ar\/translation.json<\/span>\n\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-attr\">\"currency\"<\/span>: <span class=\"hljs-string\">\"\u062a\u0646\u0633\u064a\u0642 \u0627\u0644\u0639\u0645\u0644\u0629: {{value, currency(USD)}}\"<\/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\">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_7a94ecb1c3d04ca4bf3e29c2c26e93bb\" 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>To fix this, we just need to add another custom formatter that overrides <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">currency<\/span><\/code>:<!-- notionvc: f6a15407-08c9-4eb1-a900-c9275b6c94a9 --><\/p>\n\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\/i18n\/formatters.tsx\n\n  function qualifiedLngFor(lng: string): string {\n    switch (lng) {\n      case \"ar\":\n        return \"ar-EG\";\n      case \"en\":\n        return \"en-US\";\n      default:\n        return lng;\n    }\n  }\n\n  export function number(\n    value: number,\n    lng: string | undefined,\n    options?: Intl.NumberFormatOptions,\n  ): string {\n    return new Intl.NumberFormat(\n      qualifiedLngFor(lng!),\n      options,\n    ).format(value);\n  }\n\n<span class=\"hljs-addition\">+ export function currency(<\/span>\n<span class=\"hljs-addition\">+   value: number,<\/span>\n<span class=\"hljs-addition\">+   lng: string | undefined,<\/span>\n<span class=\"hljs-addition\">+   options?: Intl.NumberFormatOptions,<\/span>\n<span class=\"hljs-addition\">+ ): string {<\/span>\n<span class=\"hljs-addition\">+   \/\/ Use the number formatter above...<\/span>\n<span class=\"hljs-addition\">+   return number(value, lng, {<\/span>\n<span class=\"hljs-addition\">+     \/\/ ...but ensure we're formatting<\/span>\n<span class=\"hljs-addition\">+     \/\/ as currency.<\/span>\n<span class=\"hljs-addition\">+     style: \"currency\",<\/span>\n<span class=\"hljs-addition\">+     ...options,<\/span>\n<span class=\"hljs-addition\">+   });<\/span>\n<span class=\"hljs-addition\">+ }<\/span><\/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_e60aa4ac56cff9876b1f5d00969edb7c\" 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>Of course, we have to wire up this new formatter for it to take effect:<!-- notionvc: b670e94d-2326-4fd2-bd46-3175e279d96b --><\/p>\n\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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ src\/i18n\/config.ts\n\n  \/\/...\n<span class=\"hljs-deletion\">- import { number } from \".\/formatters\";<\/span>\n<span class=\"hljs-addition\">+ import { currency, number } from \".\/formatters\";<\/span>\n\n  \/\/ ...\n\n  i18next\n    .use(HttpApi)\n    .use(LanguageDetector)\n    .use(initReactI18next)\n    .init({\n      \/\/ ...\n    });\n\n  i18next.services.formatter?.add(\"number\", number);\n<span class=\"hljs-addition\">+ i18next.services.formatter?.add(\"currency\", currency);<\/span>\n\n  export default i18next;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-33\"><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_9e75d2d416b3a9362f3817182f15550b\" 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 ensures that our <code>currency<\/code> shorthand formatter adheres to our new explicit regions when formatting:<\/p>\n<figure id=\"attachment_74210\" aria-describedby=\"caption-attachment-74210\" style=\"width: 920px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-74210\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-currency-formatter.png\" alt=\"The Arc browser renders our currency(USD) format using Eastern Arabic numerals when the active locale is Arabic.\" width=\"920\" height=\"368\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-currency-formatter.png 920w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-currency-formatter-300x120.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-currency-formatter-768x307.png 768w\" sizes=\"(max-width: 920px) 100vw, 920px\" \/><figcaption id=\"caption-attachment-74210\" class=\"wp-caption-text\">The Arc browser renders our currency(USD) format using Eastern Arabic numerals when the active locale is Arabic.<\/figcaption><\/figure>\n<p>\ud83d\udd17\u00a0Check out our <a href=\"https:\/\/stackblitz.com\/~\/github.com\/mdurmusphrase\/react-i18next-iv\">live StackBlitz demo<\/a> to play with localized number formats, including standalone numbers (outside of translation messages). You can always grab that <a href=\"https:\/\/github.com\/mdurmusphrase\/react-i18next-iv\/blob\/main\/src\/playgrounds\/Numbers.tsx\">number code from GitHub<\/a> as well.<\/p>\n<p>\ud83e\udd3f\u00a0Our <a href=\"https:\/\/phrase.com\/blog\/posts\/number-localization\/\">Concise Guide to Number Localization<\/a> covers numeral systems and other goodies relating to localized numbers in detail.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-format-localized-dates\"><\/span>How do I format localized dates?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Rounding out our foray into formatting, let\u2019s localize our dates.<\/p>\n<p>\u270b\u00a0This section builds on the previous one, so please read the numbers section before this one.<\/p>\n<p>As you probably guessed, i18next provides a built-in date formatter for our messages. It\u2019s called <code>datetime<\/code> and uses the standard <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/DateTimeFormat\">Intl.DateTimeFormat<\/a> object under the hood. Here\u2019s how to use it:<\/p>\n<p><!-- notionvc: 09049a72-30d3-4277-858a-ae5833863018 --><\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ public\/locales\/en\/translation.json<\/span>\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-attr\">\"simple_date\"<\/span>: <span class=\"hljs-string\">\"A simple date (default formatting): {{value, datetime}}\"<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/ public\/locales\/ar\/translation.json<\/span>\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-attr\">\"simple_date\"<\/span>: <span class=\"hljs-string\">\"\u062a\u0627\u0631\u064a\u062e \u0628\u0633\u064a\u0637 (\u0627\u0644\u062a\u0646\u0633\u064a\u0642 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a): {{value, datetime}}\"<\/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\">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_5586694c653cbaace34de75d06d2cd16\" 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>In our components, we need to provide either a <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">Date<\/span> object, or <a class=\"notion-link-token notion-focusable-token notion-enable-hover\" tabindex=\"0\" href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Date\/UTC\" rel=\"noopener noreferrer\" data-token-index=\"3\"><span class=\"link-annotation-unknown-block-id-1575434810\">UTC timestamp<\/span><\/a>, as the value to format:<!-- notionvc: 8b28a52f-abce-4064-8a29-07c669daa022 --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ In our components<\/span>\n\n{<span class=\"hljs-comment\">\/* Using a `Date` object. *\/<\/span>}\n&lt;p&gt;\n  {t(<span class=\"hljs-string\">\"simple_date\"<\/span>, {\n    value: <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>(<span class=\"hljs-string\">\"2024-01-25\"<\/span>),\n   })}\n&lt;<span class=\"hljs-regexp\">\/p&gt;\n\n{\/<\/span>* Using a UTC timestamp <span class=\"hljs-string\">`number`<\/span> *<span class=\"hljs-regexp\">\/}\n&lt;p&gt;\n  {t(\"simple_date\", { value: 1706140800000 })}\n&lt;\/<\/span>p&gt;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-35\"><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_97c68efd098738c67bfea8b9a204200e\" 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 dates above are equivalent, so the <code>datetime<\/code> formatter treats them as the same:<\/p>\n<figure id=\"attachment_74216\" aria-describedby=\"caption-attachment-74216\" style=\"width: 1010px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-74216\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-simple-date.png\" alt=\"The default datetime formatter for English (en) rendered in Firefox.\" width=\"1010\" height=\"252\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-simple-date.png 1010w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-simple-date-300x75.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-simple-date-768x192.png 768w\" sizes=\"(max-width: 1010px) 100vw, 1010px\" \/><figcaption id=\"caption-attachment-74216\" class=\"wp-caption-text\">The default datetime formatter for English (en) rendered in Firefox.<\/figcaption><\/figure>\n<figure id=\"attachment_74222\" aria-describedby=\"caption-attachment-74222\" style=\"width: 1026px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-74222\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-simple-date.png\" alt=\"The default datetime formatter for Arabic (ar) rendered in Firefox.\" width=\"1026\" height=\"250\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-simple-date.png 1026w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-simple-date-300x73.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-simple-date-1024x250.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-simple-date-768x187.png 768w\" sizes=\"(max-width: 1026px) 100vw, 1026px\" \/><figcaption id=\"caption-attachment-74222\" class=\"wp-caption-text\">The default datetime formatter for Arabic (ar) rendered in Firefox.<\/figcaption><\/figure>\n<h3><span class=\"ez-toc-section\" id=\"a-custom-datetime-formatter\"><\/span>A custom datetime formatter<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Just like numbers, dates are region-specific. Some regions might share a language but use entirely different calendars. And just like numbers, if we don\u2019t specify a date to <code>Intl.DateTimeFormat<\/code> \u2014 used by i18next\u2019s <code>datetime<\/code> formatter under the hood \u2014\u00a0we let each browser decide the region for us. This can cause inconsistent formatting across browsers. For example, here\u2019s how the Arc browser formats the above Arabic dates.<\/p>\n<figure id=\"attachment_74228\" aria-describedby=\"caption-attachment-74228\" style=\"width: 938px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-74228 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-arc-format.png\" alt=\"By default, the Chrome-based Arc browser formats Arabic (ar) dates in the Gregorian calendar and doesn\u2019t use the Eastern Arabic numeral system.\" width=\"938\" height=\"238\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-arc-format.png 938w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-arc-format-300x76.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-arc-format-768x195.png 768w\" sizes=\"(max-width: 938px) 100vw, 938px\" \/><figcaption id=\"caption-attachment-74228\" class=\"wp-caption-text\">By default, the Chrome-based Arc browser formats Arabic (ar) dates in the Gregorian calendar and doesn\u2019t use the Eastern Arabic numeral system.<\/figcaption><\/figure>\n<p>We\u2019ve already solved this problem for numbers. We just need to add a new formatter for our datetimes.<\/p>\n<p><!-- notionvc: ea611d0c-38c8-4ff1-b354-916dc49eafb9 --><\/p>\n\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\/i18n\/formatters.tsx\n\n\/\/ ...\nfunction qualifiedLngFor(lng: string): string {\n  switch (lng) {\n    case \"ar\":\n      return \"ar-EG\";\n    case \"en\":\n      return \"en-US\";\n    default:\n      return lng;\n  }\n}\n\n\/\/ ...\n\n<span class=\"hljs-addition\">+ \/**<\/span>\n<span class=\"hljs-addition\">+  * Formats a datetime.<\/span>\n<span class=\"hljs-addition\">+  *<\/span>\n<span class=\"hljs-addition\">+  * @param value - The datetime to format.<\/span>\n<span class=\"hljs-addition\">+  * @param lng - The language to format the number in.<\/span>\n<span class=\"hljs-addition\">+  * @param options - passed to Intl.DateTimeFormat.<\/span>\n<span class=\"hljs-addition\">+  * @returns The formatted datetime.<\/span>\n<span class=\"hljs-addition\">+  *\/<\/span>\n<span class=\"hljs-addition\">+  export function datetime(<\/span>\n<span class=\"hljs-addition\">+    value: Date | number,<\/span>\n<span class=\"hljs-addition\">+    lng: string | undefined,<\/span>\n<span class=\"hljs-addition\">+    options?: Intl.DateTimeFormatOptions,<\/span>\n<span class=\"hljs-addition\">+  ): string {<\/span>\n<span class=\"hljs-addition\">+    return new Intl.DateTimeFormat(<\/span>\n<span class=\"hljs-addition\">+      qualifiedLngFor(lng!),<\/span>\n<span class=\"hljs-addition\">+      options,<\/span>\n<span class=\"hljs-addition\">+    ).format(value);<\/span>\n<span class=\"hljs-addition\">+  }<\/span>\n\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_8bbe77ebd53acf2f7925d6c835113e0a\" 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 custom date formatter uses <code>qualifiedLngFor<\/code> just like our custom number formatters. It\u2019s always explicitly using a region when formatting dates: USA for English and Egypt for Arabic.<\/p>\n<p>We add our formatter to the <code>i18next<\/code> instance to override the default <code>datetime<\/code> formatter:<\/p>\n<p><!-- notionvc: fd4b1dd8-da3c-49ac-af10-094fadd0e2be --><\/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\/i18n\/config.ts\n\n  \/\/...\n<span class=\"hljs-deletion\">- import { currency, number } from \".\/formatters\";<\/span>\n<span class=\"hljs-addition\">+ import { datetime, currency, number } from \".\/formatters\";<\/span>\n\n  \/\/ ...\n\n  i18next\n    .use(HttpApi)\n    .use(LanguageDetector)\n    .use(initReactI18next)\n    .init({\n      \/\/ ...\n    });\n\n  i18next.services.formatter?.add(\"number\", number);\n  i18next.services.formatter?.add(\"currency\", currency);\n<span class=\"hljs-addition\">+ i18next.services.formatter?.add(\"datetime\", datetime);<\/span>\n\n  export default i18next;<\/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_10b3946586ab48d8a2f09757a7a5b34b\" 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_74234\" aria-describedby=\"caption-attachment-74234\" style=\"width: 952px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-74234\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-arc-date-eastern-numerals-3.png\" alt=\"With Egypt explicitly set as the default region for Arabic, the Arc browser will now format our dates using Egypt\u2019s standards. This includes using Eastern Arabic numerals when formatting numbers.\" width=\"952\" height=\"258\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-arc-date-eastern-numerals-3.png 952w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-arc-date-eastern-numerals-3-300x81.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-arc-date-eastern-numerals-3-768x208.png 768w\" sizes=\"(max-width: 952px) 100vw, 952px\" \/><figcaption id=\"caption-attachment-74234\" class=\"wp-caption-text\">With Egypt explicitly set as the default region for Arabic, the Arc browser will now format our dates using Egypt\u2019s standards. This includes using Eastern Arabic numerals when formatting numbers.<\/figcaption><\/figure>\n<h3><span class=\"ez-toc-section\" id=\"modifying-the-datetime-format\"><\/span>Modifying the datetime format<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Much like numbers, we can specify options for the <code>datetime<\/code> formatter that will be passed onto the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/DateTimeFormat\/DateTimeFormat#parameters\">Intl.DateTimeFormat constructor<\/a>.<\/p>\n<p><!-- notionvc: 9d09b31b-67d2-49de-b8e4-8808686d05d8 --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ public\/locales\/en\/translation.json<\/span>\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-string\">\"long_date\"<\/span>: <span class=\"hljs-string\">\"Long date format: {{value, datetime(dateStyle: long)}}\"<\/span>,\n  <span class=\"hljs-string\">\"custom_date\"<\/span>: <span class=\"hljs-string\">\"Custom date format: {{value, datetime(weekday: long; year: 2-digit; month: short; day: numeric)}}\"<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/ In our components<\/span>\n&lt;p&gt;\n  {t(<span class=\"hljs-string\">\"long_date\"<\/span>, { value: <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>(<span class=\"hljs-string\">\"2024-01-25\"<\/span>) })}\n&lt;<span class=\"hljs-regexp\">\/p&gt;\n&lt;p&gt;\n  {t(\"custom_date\", {\n    value: new Date(\"2024-01-25\"),\n  })}\n&lt;\/<\/span>p&gt;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-38\"><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_ba8017d50f839539a43bd188dd33175a\" 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><img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-74240 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-custom-date-formats.png\" alt=\"The same date formatted in two different ways, each specified in an English translation message.\" width=\"754\" height=\"240\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-custom-date-formats.png 754w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/en-custom-date-formats-300x95.png 300w\" sizes=\"(max-width: 754px) 100vw, 754px\" \/><\/p>\n<p>\ud83d\udd17\u00a0See the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/DateTimeFormat#constructor\">MDN docs for Intl.DateTimeFormat<\/a> for all available formatting options.<\/p>\n<p>And here\u2019s the Arabic version:<\/p>\n<p><!-- notionvc: 9e37237e-740b-4a5f-a4e3-89e52c2b3b53 --><\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ public\/locales\/ar\/translation.json<\/span>\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-attr\">\"long_date\"<\/span>: <span class=\"hljs-string\">\"\u062a\u0646\u0633\u064a\u0642 \u0627\u0644\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0637\u0648\u064a\u0644: {{value, datetime(dateStyle: long)}}\"<\/span>,\n  <span class=\"hljs-attr\">\"custom_date\"<\/span>: <span class=\"hljs-string\">\"\u062a\u0646\u0633\u064a\u0642 \u0627\u0644\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0645\u062e\u0635\u0635: {{value, datetime(weekday: long; year: 2-digit; month: short; day: numeric)}}\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-39\"><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_d6295e95f14cb3ad5337dc27b593fe55\" 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_74246\" aria-describedby=\"caption-attachment-74246\" style=\"width: 938px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-74246\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-custom-date-formats.png\" alt=\"The same date formatted in two different ways, each specified by a format in an Arabic translation message.\" width=\"938\" height=\"252\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-custom-date-formats.png 938w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-custom-date-formats-300x81.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/ar-custom-date-formats-768x206.png 768w\" sizes=\"(max-width: 938px) 100vw, 938px\" \/><figcaption id=\"caption-attachment-74246\" class=\"wp-caption-text\">The same date formatted in two different ways, each specified by a format in an Arabic translation message.<\/figcaption><\/figure>\n<p>\ud83d\udd17\u00a0Check out the i18next <a href=\"https:\/\/www.i18next.com\/translation-function\/formatting\">Formatting<\/a> guide for more info about date formatting, including relative dates.<\/p>\n<p>\ud83d\udd17\u00a0Our <a href=\"https:\/\/stackblitz.com\/~\/github.com\/mdurmusphrase\/react-i18next-iv\">live StackBlitz demo<\/a> has a date playground where you can try different dates and see them in different formats and locales.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-74252 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/01\/demo-dates.gif\" alt=\"An animation of dates being selected from a date picker as localized date formats update in lockstep.\" width=\"600\" height=\"708\" \/><\/p>\n<p>\ud83d\udd17\u00a0Get the <a href=\"https:\/\/github.com\/mdurmusphrase\/react-i18next-iv\/blob\/main\/src\/playgrounds\/Dates.tsx\">date code<\/a>, and the code for the rest of the demo app, from <a href=\"https:\/\/github.com\/mdurmusphrase\/react-i18next-iv\">our GitHub repo<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"further-reading\"><\/span>Further reading<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Here are some more of our articles on React + i18next:<\/p>\n<ul>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/nextjs-i18n\/\">A Step-by-Step Guide to Next.js Internationalization<\/a> covers Next.js Page Router localization with i18next.<\/li>\n<li><a href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-storybook-i18next\/\">A React I18n Experiment with Storybook and i18next<\/a> goes through working with i18next to localize components in isolation using Storybook.<\/li>\n<\/ul>\n<p>\ud83d\udd17\u00a0Of course, the <a href=\"https:\/\/react.i18next.com\/\">official React i18next guide<\/a> is always worth checking out.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"up-your-localization-game\"><\/span>Up your localization game<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We hope you found this guide to localizing React apps with i18next fun and helpful.<\/p>\n<p>When you\u2019re 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<p><!-- notionvc: d518d6aa-80eb-422d-874f-423dd01176fb --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>React-i18next is a powerful set of components, hooks, and plugins that sit on top of i18next. Learn how to use it to internationalize your React apps.<\/p>\n","protected":false},"author":41,"featured_media":2612,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_stopmodifiedupdate":false,"_modified_date":"","_searchwp_excluded":"","footnotes":""},"categories":[40],"class_list":["post-12225","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\/12225"}],"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=12225"}],"version-history":[{"count":7,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/12225\/revisions"}],"predecessor-version":[{"id":74294,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/12225\/revisions\/74294"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media\/2612"}],"wp:attachment":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media?parent=12225"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/categories?post=12225"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}