{"id":70452,"date":"2023-12-11T13:03:18","date_gmt":"2023-12-11T12:03:18","guid":{"rendered":"https:\/\/phrase.com\/?p=70452"},"modified":"2024-11-04T17:58:43","modified_gmt":"2024-11-04T16:58:43","slug":"next-js-l10n-format-js-react-intl","status":"publish","type":"post","link":"https:\/\/phrase.com\/blog\/posts\/next-js-l10n-format-js-react-intl\/","title":{"rendered":"Next.js Localization with Format.JS\/react-intl"},"content":{"rendered":"\n<div id=\"acf\/text-block_ddb1bfcdb769703389c0584b048eef9f\" 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:\/\/nextjs.org\/\">Next.js<\/a> Pages Router localization has become a streamlined feature of the leading full-stack React framework since the advent of Next.js 10, which brought us helpful localized routing capabilities.<\/p>\n<p>Adding the robust <a href=\"https:\/\/formatjs.io\/\">react-intl\/Format.JS<\/a> i18n library takes this a step further, offering a comprehensive solution that includes production-grade translation message management, along with refined date and number formatting.<\/p>\n<p>What sets react-intl\/Format.JS apart from other i18n libraries is its advanced translation message extraction and compilation, allowing our apps to scale gracefully and efficiently. In this tutorial, we explore the ins and outs of localizing Next.js Pages Router apps with react-intl\/Format.JS.<\/p>\n<p><!-- notionvc: a12246c6-57d4-4f33-bb2e-5223a22b48d6 --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n\n<div id=\"acf\/blog-cta-block_bf5286d6d22c529d3667a5bcebecd64a\" class=\"pxblock pxblock--blog-cta bg--green image--orientation-landscape\">\n\t<div class=\"block-container\">\n\t\t\t\t\t<div class=\"image image--align-middle\">\n\t\t\t\t<img loading=\"lazy\" decoding=\"async\" width=\"1260\" height=\"992\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero.png\" class=\"attachment-original size-original\" alt=\"String Management UI visual | Phrase\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero.png 1260w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero-300x236.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero-1024x806.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero-768x605.png 768w\" sizes=\"(max-width: 1260px) 100vw, 1260px\" \/>\t\t\t<\/div>\n\t\t\t\t<div class=\"content\">\n\t\t\t<p class=\"subhead\">Phrase Strings<\/p>\n<p class=\"secondary h6\">Take your web or mobile app global without any hassle<\/p>\n<div class=\"text--copy\">\n<p class=\"small\">Adapt your software, website, or video game for global audiences with the leanest and most realiable software localization platform.<\/p>\n<p><a class=\"btn btn--outline\" href=\"https:\/\/phrase.com\/platform\/strings\/\">Explore Phrase Strings<\/a><\/p>\n<\/div>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n\n<div id=\"acf\/text-block_179ffc528cee905d503682a4ac15e43c\" 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\udca1\u00a0Internationalization (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<p>\ud83d\uddd2\ufe0f\u00a0Format.JS is a set of ICU-compliant JavaScript i18n libraries. react-intl extends Format.JS with handy React components and hooks. In this article, where we focus on React\/Next.js, we will use the terms \u201creact-intl\u201d and \u201cFormat.JS\u201d interchangeably.<\/p>\n<p>\ud83d\udd17 This article is focused on the Next.js Pages Router. If you\u2019re working with the App Router, check out our <a href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/\">Deep Dive into Next.js App Router Localization with next-intl<\/a>\u2014and if you prefer working with the Pages Router and the popular i18next library, our <a href=\"https:\/\/phrase.com\/blog\/posts\/nextjs-i18n\/\">Step-by-Step Guide to Next.js Internationalization<\/a> has you covered.<\/p>\n<h2>Our demo app<\/h2>\n<p>We will use a simple blog to demo the i18n in this guide. Here\u2019s what the app looks like before localization.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-large wp-image-70466\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/blog-post-index-1024x809.png\" alt=\"The blog post index page | Phrase\" width=\"1024\" height=\"809\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/blog-post-index-1024x809.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/blog-post-index-300x237.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/blog-post-index-768x607.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/blog-post-index.png 1250w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-large wp-image-70472\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/single-post-1024x809.png\" alt=\"Single post page | Phrase\" width=\"1024\" height=\"809\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/single-post-1024x809.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/single-post-300x237.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/single-post-768x607.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/single-post.png 1250w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p>\ud83d\udce3 Shoutout \u00bb Thanks to <a href=\"https:\/\/github.com\/scottyg\/\">Scotty G<\/a> for creating the fun Star Wars lorem ipsum generator, <a href=\"https:\/\/forcemipsum.com\/\">Forcem Ipsum<\/a>, which we\u2019ve used for our mock content.<\/p>\n<p>After spinning up a Next.js app with the Pages Router, TypeScript, and Tailwind, we created\/modified the following files to build the demo:<\/p>\n<p><!-- notionvc: 2e486de2-62e8-46dd-94f9-415729fe9bbe --><\/p>\n<p><!-- notionvc: 5ad59591-9ec7-402b-8359-08885737b883 --><\/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=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">.\n\u2514\u2500\u2500 pages\n    \u251c\u2500\u2500 components\n    \u2502   \u2514\u2500\u2500 Layout.tsx  # Wraps our pages\n    \u251c\u2500\u2500 data\n    \u2502   \u2514\u2500\u2500 posts.ts    # Mock blog post data\n    \u251c\u2500\u2500 posts\n    \u2502   \u251c\u2500\u2500 &#91;slug].tsx  # Single blog post\n    \u2502   \u2514\u2500\u2500 index.tsx   # Blog post listing\n    \u2514\u2500\u2500 index.tsx       # Home page<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">plaintext<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">plaintext<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_40cc04864fce4f615ef134cf869d0828\" 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>It\u2019s all bread-and-butter Next.js, and we will get into the details of these files as we internationalize. Let\u2019s get to it.<\/p>\n<h2>Package versions used<\/h2>\n<p>Here are the relevant NPM packages we\u2019ve used in this tutorial.<\/p>\n<table style=\"border-collapse: collapse; width: 100%; height: 1000px;\">\n<thead>\n<tr style=\"height: 50px;\">\n<td style=\"width: 33.3333%; height: 50px;\">Package<\/td>\n<td style=\"width: 33.3333%; height: 50px;\">Version<\/td>\n<td style=\"width: 33.3333%; height: 50px;\">Notes<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr style=\"height: 50px;\">\n<td style=\"width: 33.3333%; height: 50px;\">typescript<\/td>\n<td style=\"width: 33.3333%; height: 50px;\">5.2.<\/td>\n<td style=\"width: 33.3333%; height: 50px;\"><!-- notionvc: 9bf5e2ac-f375-4666-9857-467f2f7ed301 --><\/td>\n<\/tr>\n<tr style=\"height: 50px;\">\n<td style=\"width: 33.3333%; height: 50px;\">next<!-- notionvc: 9aa2f072-34ba-41ef-8d53-8bfa55f2f202 --><\/td>\n<td style=\"width: 33.3333%; height: 50px;\">14,9<\/td>\n<td style=\"width: 33.3333%; height: 50px;\"><\/td>\n<\/tr>\n<tr style=\"height: 50px;\">\n<td style=\"width: 33.3333%; height: 50px;\">react<\/td>\n<td style=\"width: 33.3333%; height: 50px;\">18.2<\/td>\n<td style=\"width: 33.3333%; height: 50px;\"><\/td>\n<\/tr>\n<tr id=\"04e8f793-b712-41ac-87ec-898edd774056\" style=\"height: 50px;\">\n<td id=\"QAQH\" class=\"\" style=\"width: 33.3333%; height: 50px;\">react-intl<\/td>\n<td id=\"]g@v\" class=\"\" style=\"width: 33.3333%; height: 50px;\">6.5<\/td>\n<td id=\"e&lt;yH\" class=\"\" style=\"width: 33.3333%; height: 50px;\">Used for i18n<\/td>\n<\/tr>\n<tr id=\"f91c43e4-2c48-4e65-ada4-bd79e4e79d44\" style=\"height: 140px;\">\n<td id=\"QAQH\" class=\"\" style=\"width: 33.3333%; height: 140px;\">@formatjs\/cli<\/td>\n<td id=\"]g@v\" class=\"\" style=\"width: 33.3333%; height: 140px;\">6.2<\/td>\n<td id=\"e&lt;yH\" class=\"\" style=\"width: 33.3333%; height: 140px;\">Used for message extraction and compilation<\/td>\n<\/tr>\n<tr id=\"b7352f91-c65b-4cb1-8010-8f2954995d9f\" style=\"height: 140px;\">\n<td id=\"QAQH\" class=\"\" style=\"width: 33.3333%; height: 140px;\">babel-plugin-formatjs<\/td>\n<td id=\"]g@v\" class=\"\" style=\"width: 33.3333%; height: 140px;\">10.5<\/td>\n<td id=\"e&lt;yH\" class=\"\" style=\"width: 33.3333%; height: 140px;\">Used for automatic message ID injection<\/td>\n<\/tr>\n<tr id=\"70c40e65-934d-4cb4-9c37-7d7ecbde38cf\" style=\"height: 110px;\">\n<td id=\"QAQH\" class=\"\" style=\"width: 33.3333%; height: 110px;\">nookies<\/td>\n<td id=\"]g@v\" class=\"\" style=\"width: 33.3333%; height: 110px;\">2.5<\/td>\n<td id=\"e&lt;yH\" class=\"\" style=\"width: 33.3333%; height: 110px;\">Used for setting the Next.js locale cookie<\/td>\n<\/tr>\n<tr id=\"61fa9858-ecce-4709-a335-4f748dcddd52\" style=\"height: 110px;\">\n<td id=\"QAQH\" class=\"\" style=\"width: 33.3333%; height: 110px;\">accept-language-parser<\/td>\n<td id=\"]g@v\" class=\"\" style=\"width: 33.3333%; height: 110px;\">1.5<\/td>\n<td id=\"e&lt;yH\" class=\"\" style=\"width: 33.3333%; height: 110px;\">Used for custom locale auto-detection<\/td>\n<\/tr>\n<tr id=\"20823314-e0bb-47fa-890d-4c26fe47bb3a\" style=\"height: 110px;\">\n<td id=\"QAQH\" class=\"\" style=\"width: 33.3333%; height: 110px;\">rtl-detect<\/td>\n<td id=\"]g@v\" class=\"\" style=\"width: 33.3333%; height: 110px;\">1.1<\/td>\n<td id=\"e&lt;yH\" class=\"\" style=\"width: 33.3333%; height: 110px;\">Used for right-to-left (rtl) locale detection<\/td>\n<\/tr>\n<tr id=\"68ed2732-3e58-4bca-90c2-54ab35f2c41d\" style=\"height: 140px;\">\n<td id=\"QAQH\" class=\"\" style=\"width: 33.3333%; height: 140px;\">tailwindcss<\/td>\n<td id=\"]g@v\" class=\"\" style=\"width: 33.3333%; height: 140px;\">3.3<\/td>\n<td id=\"e&lt;yH\" class=\"\" style=\"width: 33.3333%; height: 140px;\">Used for styling (and out of the scope of this tutorial)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>How do I localize my Pages Router app with react-intl?<\/h2>\n<p>Next.js Pages Router apps need to manage server-side rendering (SSR) using <code>getStaticProps()<\/code> and static site generation (SSG) with <code>getStaticPaths()<\/code>. With that in mind, here are the basic steps to localizing a Next.js Pages router with react-intl:<\/p>\n<ol>\n<li>Configure built-in Next.js i18n routing.<\/li>\n<li>Install and configure react-intl.<\/li>\n<li>Localize page\/component strings using react-intl.<\/li>\n<li>Extract translation messages from our codebase using the Format.JS CLI.<\/li>\n<li>Translate messages into supported languages.<\/li>\n<li>Load translation messages on the server using <code>getStaticProps()<\/code>.<\/li>\n<li>Format dates and numbers using react-intl.<\/li>\n<\/ol>\n<p>In the following sections, we will cover these steps and more, including compiling messages for production, writing custom middleware for locale auto-detection, and adding a language selector for our site visitors.<\/p>\n<p>Let\u2019s work through in one step at a time.<\/p>\n<h2>How do I configure the built-in Next.js i18n routing?<\/h2>\n<p>Recent versions of Next.js come with localized routing out-of-the-box. This automatically adds our supported locales to routes, e.g. <code>\/fr\/posts<\/code> for our posts page in French.<\/p>\n<p>Let\u2019s see how this works by adding it to our demo. We will support English (USA) and Arabic (Egypt) here. Feel free to use any locales you want.<\/p>\n<h3>A note on locales<\/h3>\n<p>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, and <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 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>To enable localized routing, we need to add an <code>i18n<\/code> section to our <code>next.config.js<\/code>:<\/p>\n<p><!-- notionvc: 1e5cbe34-f204-4bc3-836f-6a35f3cbb970 --><\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/** @type {import('next').NextConfig} *\/\nconst nextConfig = {\n  reactStrictMode: true,\n<span class=\"hljs-addition\">+ i18n: {<\/span>\n<span class=\"hljs-addition\">+   locales: &#91;\"en-US\", \"ar-EG\"], \/\/ required<\/span>\n<span class=\"hljs-addition\">+   defaultLocale: \"en-US\",      \/\/ required<\/span>\n<span class=\"hljs-addition\">+ },<\/span>\n};\n\nmodule.exports = nextConfig;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><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_2b85770498ec198d8fa7e30221da1ab4\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>\u270b Both <code>locales<\/code> and <code>defaultLocale<\/code> are required in Next.js. If either is missing or invalid, Next.js will trigger an error during development server runs and production builds.<\/p>\n<p>Just with that, Next.js creates an <code>\/ar-EG<\/code> prefix for all our routes. We started with these routes in our app:<\/p>\n<table style=\"border-collapse: collapse; width: 100%; height: 199px;\">\n<thead>\n<tr style=\"height: 50px;\">\n<td style=\"width: 33.3333%; height: 50px;\">Route<\/td>\n<td style=\"width: 33.3333%; height: 50px;\">Description<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr id=\"5ffabe8b-6660-4816-a063-309e01012936\" style=\"height: 50px;\">\n<td id=\"yixP\" class=\"\" style=\"width: 33.3333%; height: 50px;\"><code>\/<\/code><\/td>\n<td id=\"au[i\" class=\"\" style=\"width: 33.3333%; height: 50px;\">home page<\/td>\n<\/tr>\n<tr id=\"bda5b2c7-3953-4fbf-9883-5fc845f8a0d5\" style=\"height: 51px;\">\n<td id=\"yixP\" class=\"\" style=\"width: 33.3333%; height: 51px;\"><code>\/posts<\/code><\/td>\n<td id=\"au[i\" class=\"\" style=\"width: 33.3333%; height: 51px;\">blog post index<\/td>\n<\/tr>\n<tr id=\"011af3df-7807-4fa8-8cfc-82ac6d526217\" style=\"height: 48px;\">\n<td id=\"yixP\" class=\"\" style=\"width: 33.3333%; height: 48px;\"><code>\/posts\/foo-bar<\/code><\/td>\n<td id=\"au[i\" class=\"\" style=\"width: 33.3333%; height: 48px;\">single blog post<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>After adding the above i18n config, we now have the following routes:<!-- notionvc: ffc87378-98b4-4519-8fd5-1cbc7e1c1a83 --><\/p>\n<table style=\"border-collapse: collapse; width: 100%; height: 427px;\">\n<thead>\n<tr style=\"height: 50px;\">\n<td style=\"width: 33.3333%; height: 50px;\">Route<\/td>\n<td style=\"width: 33.3333%; height: 50px;\">Active locale<\/td>\n<td style=\"width: 33.3333%; height: 50px;\">Description<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr id=\"e9357a60-7283-4dbe-84ee-2b1136431fae\" style=\"height: 49px;\">\n<td id=\"yixP\" class=\"\" style=\"width: 33.3333%; height: 49px;\"><code>\/<\/code><\/td>\n<td id=\"E?OE\" class=\"\" style=\"width: 33.3333%; height: 49px;\">English (en-US)<\/td>\n<td id=\"au[i\" class=\"\" style=\"width: 33.3333%; height: 49px;\">home page<\/td>\n<\/tr>\n<tr id=\"93238370-8217-427d-a9e1-1fd206ecdb1e\" style=\"height: 52px;\">\n<td id=\"yixP\" class=\"\" style=\"width: 33.3333%; height: 52px;\"><code>\/ar-EG<\/code><\/td>\n<td id=\"E?OE\" class=\"\" style=\"width: 33.3333%; height: 52px;\">Arabic (ar-EG)<\/td>\n<td id=\"au[i\" class=\"\" style=\"width: 33.3333%; height: 52px;\">home page<\/td>\n<\/tr>\n<tr id=\"c9e49375-a53d-47d2-95cb-36953d51836c\" style=\"height: 50px;\">\n<td id=\"yixP\" class=\"\" style=\"width: 33.3333%; height: 50px;\"><code>\/posts<\/code><\/td>\n<td id=\"E?OE\" class=\"\" style=\"width: 33.3333%; height: 50px;\">English (en-US)<\/td>\n<td id=\"au[i\" class=\"\" style=\"width: 33.3333%; height: 50px;\">blog post index<\/td>\n<\/tr>\n<tr id=\"45613628-702d-4d90-ab93-ae96b90564da\" style=\"height: 48px;\">\n<td id=\"yixP\" class=\"\" style=\"width: 33.3333%; height: 48px;\"><code>\/ar-EG\/posts<\/code><\/td>\n<td id=\"E?OE\" class=\"\" style=\"width: 33.3333%; height: 48px;\">Arabic (ar-EG)<\/td>\n<td id=\"au[i\" class=\"\" style=\"width: 33.3333%; height: 48px;\">blog post index<\/td>\n<\/tr>\n<tr id=\"7916a8e0-d0e5-407c-9362-60f2a9913022\" style=\"height: 74px;\">\n<td id=\"yixP\" class=\"\" style=\"width: 33.3333%; height: 74px;\"><code>\/posts\/foo-bar<\/code><\/td>\n<td id=\"E?OE\" class=\"\" style=\"width: 33.3333%; height: 74px;\">English (en-US)<\/td>\n<td id=\"au[i\" class=\"\" style=\"width: 33.3333%; height: 74px;\">single blog post<\/td>\n<\/tr>\n<tr id=\"a2634f86-5240-444b-83a8-60f1d1e1ce5c\" style=\"height: 104px;\">\n<td id=\"yixP\" class=\"\" style=\"width: 33.3333%; height: 104px;\"><code>\/ar-EG\/posts\/foo-bar<\/code><\/td>\n<td id=\"E?OE\" class=\"\" style=\"width: 33.3333%; height: 104px;\">Arabic (ar-EG)<\/td>\n<td id=\"au[i\" class=\"\" style=\"width: 33.3333%; height: 104px;\">single blog post<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>For every locale included in the <code>locales<\/code> array of our config, a corresponding route prefix is created. The default route (<code>en-US<\/code> in our case) doesn&#8217;t receive any prefix.<\/p>\n<p>\ud83d\uddd2\ufe0f\u00a0It&#8217;s worth noting, though, that the <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/routing\/internationalization#prefixing-the-default-locale\">Next.js documentation<\/a> offers a workaround for redirecting unprefixed URLs, e.g. <code>\/<\/code>, to prefixed URLs, such as <code>\/en-US<\/code>. This approach, however, comes with its own set of challenges, such as the need to juggle a placeholder <code>default<\/code> locale in the supported locales list, and potential errors during production builds when Next.js attempts to build pages with this placeholder <code>default<\/code> locale.<\/p>\n<p>Moving on, Next.js also provides access to the active locale through its router object. Let\u2019s make use of this in our single post page to show the translated blog post.<\/p>\n<p>Our hard-coded mock data is already localized:<\/p>\n<p><!-- notionvc: 5dda1e62-4798-48ec-b94d-bc30c1b7ea10 --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ data\/posts.ts<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> <span class=\"hljs-keyword\">type<\/span> { Post } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/types\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> posts: Post&#91;] = &#91;\n  {\n    slug: <span class=\"hljs-string\">\"marching-into-detention-area\"<\/span>,\n    date: <span class=\"hljs-string\">\"2023-11-10\"<\/span>,\n    translations: {\n      <span class=\"hljs-string\">\"en-US\"<\/span>: {\n         title: <span class=\"hljs-string\">\"Marching into the...\"<\/span>,\n         content: <span class=\"hljs-string\">\"What an incredible...\"<\/span>,\n      },\n      <span class=\"hljs-string\">\"ar-EG\"<\/span>: {\n         title: <span class=\"hljs-string\">\"\u0625\u0646 \u0627\u0644\u0632\u062d\u0641 \u0625\u0644\u0649 \u0645\u0646\u0637\u0642\u0629 \u0627\u0644\u0627\u062d\u062a\u062c\u0627\u0632 \u0644\u0645...\"<\/span>,\n         content: <span class=\"hljs-string\">\"\u064a\u0627 \u0644\u0647\u0627 \u0645\u0646 \u0631\u0627\u0626\u062d\u0629 \u0645\u0630\u0647\u0644\u0629 \u0627\u0643\u062a\u0634\u0641\u062a\u0647\u0627...\"<\/span>,\n      },\n    },\n  },\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n]<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><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_1d3aa8e3f059ccf0aabcb2e51422eee7\" 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>Using the <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">locale<\/span> route param from Next.js, we can refine this data and show the appropriate content for the active locale.<!-- notionvc: 85c53e88-1a61-4b24-a827-275da5b962f5 --><\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ pages\/posts\/&#91;slug].tsx\n\nimport Layout from \"@\/components\/Layout\";\nimport { posts } from \"@\/data\/posts\";\nimport type { Post } from \"@\/types\";\nimport type {\n  GetStaticPaths,\n  GetStaticProps,\n  GetStaticPropsContext,\n} from \"next\";\nimport Link from \"next\/link\";\nimport { useRouter } from \"next\/router\";\n\ntype SinglePostProps = {\n  post: Post;\n};\n\nexport const getStaticPaths: GetStaticPaths&lt;{\n  slug: string;\n}&gt; = () =&gt; {\n  return {\n    paths: posts.map((post) =&gt; ({\n      params: { slug: post.slug },\n    })),\n    fallback: true,\n  };\n};\n\nexport const getStaticProps = (async ({\n  params,\n}: GetStaticPropsContext) =&gt; {\n  const post = posts.find(\n    (post) =&gt; post.slug <span class=\"hljs-comment\">=== params?.slug,<\/span>\n  );\n\n  if (!post) return { notFound: true };\n\n  return { props: { post } };\n}) satisfies GetStaticProps&lt;SinglePostProps&gt;;\n\nexport default function SinglePost({\n  post,\n}: SinglePostProps) {\n  const router = useRouter();\n\n<span class=\"hljs-addition\">+ const locale = router.locale!;<\/span>\n<span class=\"hljs-addition\">+ \/\/ =&gt; \"en-US\" when route is \/posts\/foo<\/span>\n<span class=\"hljs-addition\">+ \/\/ =&gt; \"ar-EG\" when route is \/ar-EG\/posts\/foo<\/span>\n\n  if (router.isFallback) {\n    return &lt;div&gt;Loading...&lt;\/div&gt;;\n  }\n\n  return (\n    &lt;Layout&gt;\n      &lt;!-- ... --&gt;\n\n      &lt;!-- We previously hard-coded the locale --&gt;\n<span class=\"hljs-deletion\">-     &lt;h1 className=\"...\"&gt;{post.translations&#91;\"en-US\"].title}&lt;\/h1&gt;<\/span>\n<span class=\"hljs-addition\">+     &lt;h1 className=\"...\"&gt;{post.translations&#91;locale].title}&lt;\/h1&gt;<\/span>\n      &lt;p className=\"...\"&gt;{post.date}&lt;\/p&gt;\n      &lt;div className=\"...\"&gt;\n<span class=\"hljs-deletion\">-        &lt;p&gt;{post.translations&#91;\"en-US\"].content}&lt;\/p&gt;<\/span>\n<span class=\"hljs-addition\">+        &lt;p&gt;{post.translations&#91;locale].content}&lt;\/p&gt;<\/span>\n      &lt;\/div&gt;\n    &lt;\/Layout&gt;\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">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_85d4920fdfa339aabe19f1e3a9cc8975\" 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, we\u2019re showing the post translated to the active locale.<!-- notionvc: b19dd5e8-3d1a-41b1-b1ce-b3b19f0e727a --><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-large wp-image-70490\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-content-1024x569.png\" alt=\"When the route default to English | Phrase\" width=\"1024\" height=\"569\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-content-1024x569.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-content-300x167.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-content-768x427.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-content-1536x854.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-content.png 1778w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-large wp-image-70496\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-content-1024x569.png\" alt=\"The route is Arabic | Phrase\" width=\"1024\" height=\"569\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-content-1024x569.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-content-300x167.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-content-768x427.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-content-1536x854.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-content.png 1778w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<h3>On localized dynamic routes and getStaticPaths()<\/h3>\n<p>For dynamic routes like <code>[slug].tsx<\/code>, when using <code>fallback: false<\/code> to generate all locale variants during the build, we need to include <em>all<\/em> locales in <code>getStaticPaths()<\/code>:<\/p>\n<p><!-- notionvc: 846dc5ca-322a-4966-838a-7509d03206bf --><\/p>\n<p>&nbsp;<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ pages\/_app.tsx\n\n<span class=\"hljs-deletion\">- import arMessages from \"@\/lang\/compiled\/ar-EG.json\";<\/span>\n<span class=\"hljs-deletion\">- import enMessages from \"@\/lang\/compiled\/en-US.json\";<\/span>\n  import \"@\/styles\/globals.css\";\n  import type { AppProps } from \"next\/app\";\n  import { useRouter } from \"next\/router\";\n  import { useEffect } from \"react\";\n  import { IntlProvider } from \"react-intl\";\n\n<span class=\"hljs-deletion\">- const messages = {<\/span>\n<span class=\"hljs-deletion\">-   \"en-US\": { ...enMessages },<\/span>\n<span class=\"hljs-deletion\">-   \"ar-EG\": { ...arMessages },<\/span>\n<span class=\"hljs-deletion\">- };<\/span>\n\n  export default function App({ Component, pageProps }: AppProps) {\n    const { locale, defaultLocale } = useRouter();\n\n    return (\n      &lt;IntlProvider\n        locale={locale!}\n        defaultLocale={defaultLocale!}\n<span class=\"hljs-deletion\">-       messages={messages&#91;locale as keyof typeof messages]}<\/span>\n<span class=\"hljs-addition\">+       messages={pageProps.localeMessages}<\/span>\n      &gt;\n        &lt;Component {...pageProps} \/&gt;\n      &lt;\/IntlProvider&gt;\n    );\n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">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<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> getStaticPaths: GetStaticPaths = ({\n  locales,\n}: GetStaticPathsContext) =&gt; {\n  <span class=\"hljs-keyword\">return<\/span> {\n    paths: locales!.flatMap(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">locale<\/span><\/span>) =&gt;<\/span>\n      posts.map(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">post<\/span><\/span>) =&gt;<\/span> ({\n        params: { slug: post.slug },\n        locale,\n      })),\n    ),\n    fallback: <span class=\"hljs-literal\">false<\/span>,\n  };\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><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_57e66071b91d8163a408676b8a777326\" 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>Here, <code>locales<\/code> refers to the array defined in <code>next.config.js<\/code>. However, this approach can slow down our production build due to the large number of page variants (equal to the number of slugs times the number of locales).<\/p>\n<p>As an alternative, omitting <code>locale<\/code> only builds variants for the default locale, with <code>fallback: true<\/code> dynamically generating other locale variants on route visits.<\/p>\n<p>\ud83d\udd17 Learn more in the official doc, <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/routing\/internationalization#dynamic-routes-and-getstaticprops-pages\">Dynamic Routes and getStaticProps Pages<\/a>.<\/p>\n<p>\u270b Be aware that localized routes <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/routing\/internationalization#how-does-this-work-with-static-generation\">do not work with pure static site generation<\/a> (SSG), i.e., <code>output: \"export\"<\/code>.<\/p>\n<h3>Localized links<\/h3>\n<p>Next.js automatically localizes <code>&lt;Link&gt;<\/code> elements. For example, <code>&lt;Link href=\"\/about\"&gt;<\/code> will actually point to <code>\/ar-EG\/about<\/code> when the active locale is <code>ar-EG<\/code>. Additionally, you can <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/routing\/internationalization#transition-between-locales\">switch locales<\/a> by using the <code>locale<\/code> prop on <code>&lt;Link&gt;<\/code>. (We will discuss locale switching using the Next.js router a bit later).<\/p>\n<h3>Automatic locale detection<\/h3>\n<p>Next.js <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/routing\/internationalization#automatic-locale-detection\">automatically detects a user&#8217;s locale<\/a> when they visit the home route <code>\/<\/code> by using the HTTP <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Accept-Language\">Accept-Language<\/a> header, which reflects the browser and operating system language settings.<\/p>\n<p>For instance, if a user with Arabic (Egypt) (<code>ar-EG<\/code>) as their top browser language preference visits <code>\/<\/code>, they will get redirected to <code>\/ar-EG<\/code>. However, this detection isn&#8217;t loosely matched; a user with Arabic (Syria) (<code>ar-SY<\/code>) won&#8217;t be redirected to <code>\/ar-EG<\/code>, despite Syrians and Egyptians sharing the same written Arabic. We will address this when we tackle custom locale detection later.<\/p>\n<p>\ud83e\udd3f Go deeper **with <a href=\"https:\/\/phrase.com\/blog\/posts\/detecting-a-users-locale\/\">Detecting a User\u2019s Locale in a Web App<\/a>.<\/p>\n<p>Localized routes alone don&#8217;t fully internationalize an app; managing UI translations, dates, and numbers is also crucial. This is where react-intl comes in.<\/p>\n<h2>How do I install and configure react-intl in my Next.js Pages Router app?<\/h2>\n<p>Installing react-intl is easy enough.<\/p>\n<p><!-- notionvc: be88f68b-a458-43d5-8ec0-dc4f3939f9db --><\/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=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm install react-intl<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><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_d8d2b6576b59ccd690ce08d53361713c\" 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 make our active locale and translation messages available to our pages and components, we need to wrap them with react-intl\u2019s <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;IntlProvider&gt;<\/span>.<!-- notionvc: 7e621322-d61c-46ac-ab11-4c65c1d67aed --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ pages\/_app.tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> <span class=\"hljs-string\">\"@\/styles\/globals.css\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> <span class=\"hljs-keyword\">type<\/span> { AppProps } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next\/app\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Import IntlProvider<\/span>\n<span class=\"hljs-keyword\">import<\/span> { IntlProvider } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-intl\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Add translations<\/span>\n<span class=\"hljs-keyword\">const<\/span> messages = {\n  <span class=\"hljs-string\">\"en-US\"<\/span>: {\n    hello: <span class=\"hljs-string\">\"Hello, World!\"<\/span>,\n  },\n  <span class=\"hljs-string\">\"ar-EG\"<\/span>: {\n    hello: <span class=\"hljs-string\">\"\u0645\u0631\u062d\u0628\u0627\u064b \u0628\u0627\u0644\u0639\u0627\u0644\u0645!\"<\/span>,\n  },\n};\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\">App<\/span>(<span class=\"hljs-params\">{ Component, pageProps }: AppProps<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> { locale, defaultLocale } = useRouter();\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"hljs-comment\">\/\/ Wrap page component with IntlProvider<\/span>\n    &lt;IntlProvider\n      locale={locale!}\n      defaultLocale={defaultLocale!}\n      messages={messages&#91;locale <span class=\"hljs-keyword\">as<\/span> keyof <span class=\"hljs-keyword\">typeof<\/span> messages]}\n    &gt;\n      &lt;Component {...pageProps} \/&gt;\n    &lt;<span class=\"hljs-regexp\">\/IntlProvider&gt;\n  );\n}<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">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_f2d38f3c266eb2a715d523e9e3471fad\" 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 configuring <code>&lt;IntlProvider&gt;<\/code>:<\/p>\n<ul>\n<li><code>locale<\/code> is used for date and number formatting.<\/li>\n<li><code>defaultLocale<\/code> is used for fallback when a message isn\u2019t provided in the active locale (this is only for date and number formatting consistency; in fact, each message will provide its own default fallback).<\/li>\n<li><code>messages<\/code> are the translation messages for the active locale (it\u2019s up to the developer to ensure this is the case).<\/li>\n<\/ul>\n<p>OK, let\u2019s take react-intl for a test run to see if it\u2019s working.<\/p>\n<p>Our home page has a heading we can replace with our new \u201cHello, World\u201d translation.<\/p>\n<p><!-- notionvc: b901d480-de92-4ae2-b9f8-6124532381fb --><\/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\">\/\/ pages\/index.tsx\n\n  import Layout from \"@\/components\/Layout\";\n  import type { GetStaticProps } from \"next\/types\";\n<span class=\"hljs-addition\">+ import { FormattedMessage } from \"react-intl\";<\/span>\n\n\/\/ ...\n\n  export default function Home(...) {\n    return (\n      &lt;Layout&gt;\n        &lt;h1 className=\"...\"&gt;\n<span class=\"hljs-deletion\">-         Hello i18n!<\/span>\n<span class=\"hljs-addition\">+         &lt;FormattedMessage id=\"hello\" \/&gt;<\/span>\n        &lt;\/h1&gt;\n        &lt;p className=\"...\"&gt;\n          This is a Next.js demo of i18n with react-intl.\n        &lt;\/p&gt;\n\n        {\/* ... *\/}\n\n      &lt;\/Layout&gt;\n    );\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_db8b72bc15772bae82621bc1f694cc4f\" 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 <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;FormattedMessage&gt;<\/span> component is aware of the messages provided by <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"3\">&lt;IntlProvider&gt;<\/span> and can reference any of them by its <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"5\">id<\/span>.<!-- notionvc: 748d21b8-e573-4cc4-8611-47c27b7fc047 --><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-large wp-image-70504\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-trans-1024x370.png\" alt=\"Header renders our new English translation | Phrase\" width=\"1024\" height=\"370\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-trans-1024x370.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-trans-300x108.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-trans-768x277.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-trans.png 1064w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-large wp-image-70510\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-trans-1024x364.png\" alt=\"The header renders our Arabic translation | Phrase\" width=\"1024\" height=\"364\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-trans-1024x364.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-trans-300x107.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-trans-768x273.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-trans.png 1096w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<h2>How do I extract and compile translation messages with the Format.JS CLI?<\/h2>\n<p><!-- notionvc: 20efc784-34d6-4942-9ddd-5fe6a994862f --><\/p>\n<p>The key advantage of react-intl\/Format.JS over other i18n libraries is its support for a message extraction and optional compilation workflow, which is particularly beneficial for large projects with multiple locales and teams, including dedicated translators. The workflow involves:<\/p>\n<ol>\n<li>Defining translation messages in the default locale (e.g., English) within the codebase.<\/li>\n<li>Using a CLI to extract these messages into a translation file.<\/li>\n<li>Sharing this file with translators, typically via an automation script.<\/li>\n<li>Translators then provide translations for all supported locales, often using Translation Management Software (TMS) like Phrase.<\/li>\n<li>Another automation script pulls the latest translations back into the codebase.<\/li>\n<li>Optionally, translation messages are compiled into a more efficient format for production.<\/li>\n<\/ol>\n<p>This workflow streamlines the coordination of internationalization and localization efforts for larger teams. In this section, we will focus on the initial steps: defining messages, extracting them, and compiling them.<\/p>\n<p>First, we need to install the Format.JS CLI and the Format.JS Babel plugin. The former will give us the extraction and compilation commands, while the latter will allow Format.JS to create IDs for our messages automatically. This saves us the need to explicitly connect messages in our components with IDs in translation files.<\/p>\n<p>Let\u2019s see this in action. First, we will install both packages as dev dependencies.<\/p>\n<p><!-- notionvc: b08d0be6-e704-4b98-83ad-6ce4acefe682 --><\/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=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm install --save-dev @formatjs\/cli babel-plugin-formatjs<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><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_eef4ec6430684f2d8080d47fa8210458\" 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 will add a <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">.babelrc<\/span> to the root of our project, which allows us to wire up the Format.JS plugin.<!-- notionvc: ce29857f-3cb4-4411-8294-fa20bc516b52 --><\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ .babelrc<\/span>\n\n{\n  <span class=\"hljs-attr\">\"presets\"<\/span>: &#91;<span class=\"hljs-string\">\"next\/babel\"<\/span>],\n  <span class=\"hljs-attr\">\"plugins\"<\/span>: &#91;\n    &#91;\n      <span class=\"hljs-string\">\"formatjs\"<\/span>,\n      {\n        <span class=\"hljs-attr\">\"ast\"<\/span>: <span class=\"hljs-literal\">true<\/span>\n      }\n    ]\n  ]\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><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_81c80b231f41186241616addeffbcd23\" 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>For faster runtime performance, we will pre-compile our translation messages into <a href=\"https:\/\/formatjs.io\/docs\/tooling\/babel-plugin\/#ast\">AST (Abstract Syntax Tree)<\/a> format. This format will only be used by Format.JS. Our source translation files handled by translators will be in a normal JSON key\/value format.<\/p>\n<h3>A note on adding Babel config to a Next.js project<\/h3>\n<p>Next.js will automatically pick up our new <code>.babelrc<\/code> file and use it as an override for its Babel config, which is why we needed to explicitly add the <code>next\/babel<\/code> plugin above. Otherwise, our Next.js environment doesn\u2019t work at all!<\/p>\n<p>Also, note that manually configuring Babel will cause Next.js to <a href=\"https:\/\/nextjs.org\/docs\/architecture\/nextjs-compiler#unsupported-features\">opt out of its own performant SWC compiler<\/a>. One thing I noticed <em>doesn\u2019t<\/em> work without SWC is Next.js\u2019 <a href=\"https:\/\/nextjs.org\/docs\/app\/building-your-application\/optimizing\/fonts\">automatic font optimization<\/a> with <code>next\/font<\/code>. Otherwise, my Next.js app continued to work fine with the Babel override.<\/p>\n<p>\ud83d\uddd2\ufe0f\u00a0Format.JS does provide a <a href=\"https:\/\/formatjs.io\/docs\/tooling\/swc-plugin\">SWC plugin<\/a> which could, theoretically, be wired up to Next.js using the latter\u2019s <a href=\"https:\/\/nextjs.org\/docs\/architecture\/nextjs-compiler#swc-plugins-experimental\">SWC plugin configuration<\/a>. Since the SWC plugin config is experimental at the time of writing, we\u2019ve chosen to go with the stable Babel override here.<\/p>\n<h3>Extracting messages<\/h3>\n<p>Now let\u2019s add our extraction script to <code>package.json<\/code>.<\/p>\n<p><!-- notionvc: 3bef6a5d-7012-4974-9680-32ee0b45024c --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ package.json\n\n{\n  \/\/ ...\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n<span class=\"hljs-addition\">+   \"intl:extract\": \"formatjs extract '{pages,components,sections}\/**\/*.{js,jsx,ts,tsx}' --out-file lang\/src\/en-US.json --format simple\",<\/span>\n  },\n  \/\/ ...\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_73a1f1e8d3090978c7a76d06da489a84\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Our new <code>intl:extract<\/code> command will tell the Format.JS CLI to look through our source files and output a translation file for our default language, <code>en-US<\/code>.<\/p>\n<p>\ud83d\uddd2\ufe0f\u00a0We designate the <code>simple<\/code> format to output our translation file in simple key\/value JSON. <a href=\"https:\/\/github.com\/formatjs\/formatjs\/tree\/main\/packages\/cli-lib\/src\/formatters\">Other formats are available<\/a>, however. We can even <a href=\"https:\/\/formatjs.io\/docs\/tooling\/cli#--format-path\">provide custom formats<\/a>.<\/p>\n<p>OK, let\u2019s give our new command a spin. First, we will update our messages to remove explicit IDs and ensure each has an English <code>defaultMessage<\/code>.<\/p>\n<p><!-- notionvc: 0a6a6aa9-0a80-483a-903a-6829ad402c56 --><\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">import Layout from \"@\/components\/Layout\";\nimport { FormattedMessage } from \"react-intl\";\n\n\/\/ ...\n\nexport default function Home(...) {\n  return (\n    &lt;&gt;\n      &lt;Layout&gt;\n        &lt;h1 className=\"...\"&gt;\n<span class=\"hljs-deletion\">-         &lt;FormattedMessage id=\"hello\" \/&gt;<\/span>\n<span class=\"hljs-addition\">+         &lt;FormattedMessage defaultMessage=\"Hello i18n!\" \/&gt;<\/span>\n        &lt;\/h1&gt;\n      &lt;\/Layout&gt;\n    &lt;\/&gt;\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><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_a827a276263afb3c7ff35b6f720b6ccd\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Notice how this change makes the message much more readable. Now let\u2019s run our new extraction command.<!-- notionvc: c5c0f814-e495-4333-b76e-029f959ec30d --><\/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=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm run intl:extract<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><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_b79e14e7b847e5ccb665e8b3c205b192\" 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 all goes well, we should have a new file in our project, <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">lang\/src\/en-US.json<\/span>:<!-- notionvc: 033801ef-4107-4fad-9734-4b37ee1aaf1e --><\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ lang\/src\/en-US.json<\/span>\n\n{\n  <span class=\"hljs-attr\">\"RohNOo\"<\/span>: <span class=\"hljs-string\">\"Hello i18n!\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><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_22bed293cb4ef44683fe01d70d8568fc\" 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 Format.JS CLI will automatically generate a unique ID for any message it finds in the source code that doesn\u2019t have an explicit ID. It will also assume that the <code>defaultMessage<\/code> value is the translation in the default locale.<\/p>\n<p>We can now create a copy of the English translation file for each of our other supported locales. (In production, we would probably automate this step and upload all the files to a TMS like Phrase).<\/p>\n<p><!-- notionvc: 5e87e944-3ae1-411b-9066-955db90e1e8c --><\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ lang\/src\/ar-EG.json <\/span>\n\n{\n  <span class=\"hljs-attr\">\"RohNOo\"<\/span>: <span class=\"hljs-string\">\"\u0623\u0647\u0644\u0627\u064b \u0628\u0627\u0644\u062a\u062f\u0648\u064a\u0644!\"<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><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_17b9d27065ac611a58fd508c40044e22\" 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 load our new message files into our app.<!-- notionvc: 0da16eae-f91c-407f-ab53-88ee15335d7e --><\/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\">\/\/ pages\/_app.tsx\n\n<span class=\"hljs-addition\">+ import arMessages from \"@\/lang\/src\/ar-EG.json\";<\/span>\n<span class=\"hljs-addition\">+ import enMessages from \"@\/lang\/src\/en-US.json\";<\/span>\n  import \"@\/styles\/globals.css\";\n  import type { AppProps } from \"next\/app\";\n  import { useRouter } from \"next\/router\";\n  import { IntlProvider } from \"react-intl\";\n\n  const messages = {\n<span class=\"hljs-deletion\">-   \"en-US\": { ... }, \/\/ inlined messages<\/span>\n<span class=\"hljs-addition\">+   \"en-US\": { ...enMessages },<\/span>\n<span class=\"hljs-deletion\">-   \"ar-EG: { ... },  \/\/ inlined messages<\/span>\n<span class=\"hljs-addition\">+   \"ar-EG\": { ...arMessages },<\/span>\n  };\n\nexport default function App({ Component, pageProps }: AppProps) {\n  const router = useRouter();\n  const { locale, defaultLocale } = useRouter();\n\n  return (\n    &lt;IntlProvider\n      locale={locale!}\n      defaultLocale={router.defaultLocale}\n      messages={messages&#91;locale as keyof typeof messages]}\n    &gt;\n      &lt;Component {...pageProps} \/&gt;\n    &lt;\/IntlProvider&gt;\n  );\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_90c7346c6de24240847c56ee8e2df3cb\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>If we run our app now, we should see our English and Arabic translations.<!-- notionvc: a6de2376-69e8-41cd-96c1-729c16871ff9 --><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-large wp-image-70518\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-auto-id-1024x201.jpg\" alt=\"English route header | Phrase\" width=\"1024\" height=\"201\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-auto-id-1024x201.jpg 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-auto-id-300x59.jpg 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-auto-id-768x151.jpg 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/en-auto-id.jpg 1152w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-large wp-image-70524\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-auto-id-1024x219.jpg\" alt=\"Arabic route header | Phrase\" width=\"1024\" height=\"219\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-auto-id-1024x219.jpg 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-auto-id-300x64.jpg 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-auto-id-768x164.jpg 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ar-auto-id.jpg 1068w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p>Behind the scenes the Format.JS Babel plugin is doing its magic, injecting the message ID in the <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">&lt;FormattedMessage&gt;<\/span> component. We can see this if we poke in with our React dev tools.<!-- notionvc: 46d4bc72-6df3-446f-a281-5dcb8b03ecc0 --><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-large wp-image-70530\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/cli-id-inject-1024x506.png\" alt=\"React dev tools browser extension shows an automatically injected ID | Phrase\" width=\"1024\" height=\"506\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/cli-id-inject-1024x506.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/cli-id-inject-300x148.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/cli-id-inject-768x380.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/cli-id-inject-1536x760.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/cli-id-inject.png 1646w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<h3>Aliasing and custom components\/functions<\/h3>\n<p>The Format.JS CLI and babel plugins will automatically work with <code>&lt;FormattedMessage&gt;<\/code> and the imperative <a href=\"https:\/\/formatjs.io\/docs\/react-intl\/api#formatmessage\">intl.formatMessage<\/a> (we will cover the latter a bit later). Aliasing them <em>will not work<\/em> out-of-the-box however:<\/p>\n<p><!-- notionvc: 44349982-2158-43f1-a805-22fb697b9d89 --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ \u26d4\ufe0f\ud83d\udc47 Will not work out-of-the-box. (See below).<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { FormattedMessage } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-intl\"<\/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\">Aliased<\/span>(<span class=\"hljs-params\">{ defaultMessage }<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">FormattedMessage<\/span> <span class=\"hljs-attr\">defaultMessage<\/span>=<span class=\"hljs-string\">{defaultMessage}<\/span> \/&gt;<\/span><\/span>;\n}\n\n<span class=\"hljs-comment\">\/\/ In some other component<\/span>\n&lt;Aliased defaultMessage=<span class=\"hljs-string\">\"I am a translation message\"<\/span> \/&gt;\n\n<span class=\"hljs-comment\">\/\/ \u26d4\ufe0f\ud83d\udc46 Will not work! (See below).<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_f24f90ff34bab54322387084cd727c2c\" 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 enable message extraction and automatic ID generation for your aliases or custom components\/functions, refer to the <code>additionalComponentNames<\/code> and <code>additionalFunctionNames<\/code> options in the <a href=\"https:\/\/formatjs.io\/docs\/tooling\/cli\/#--additional-component-names-comma-separated-names\">CLI docs<\/a> and <a href=\"https:\/\/formatjs.io\/docs\/tooling\/babel-plugin\/#additionalcomponentnames\">Babel plugin docs<\/a>.<\/p>\n<h3>Compiling messages<\/h3>\n<p>In production, we can speed up our app with Format.JS message compilation, which does two things:<\/p>\n<ul>\n<li>Compiles the translation message to a performant AST format.<\/li>\n<li>Allows us to remove the ICU MessageFormat parser in production environments, further speeding up our app.<\/li>\n<\/ul>\n<p>The Format.JS Babel plugin has been compiling our messages to AST on the fly during development. We can see this when we look at one of our message components, like <code>&lt;FormattedMessage&gt;<\/code>, using the React dev tools.<\/p>\n<p><!-- notionvc: 596c0232-352e-4110-9bca-a74c8e58f623 --><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-70538\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ast-message.png\" alt=\"Our defaultMessage is compiled into an AST | Phrase\" width=\"578\" height=\"184\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ast-message.png 578w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/ast-message-300x96.png 300w\" sizes=\"(max-width: 578px) 100vw, 578px\" \/><\/p>\n<p>Normally, we define our translation messages using the robust ICU Message syntax. We will get into ICU a bit later. For now, just know that parsing and compiling this syntax can be a bit expensive, so pre-compiling our messages for production can make our app more performant. We do this using the Format.JS CLI.<\/p>\n<p>Let\u2019s add a new compile command to our <code>package.json<\/code><\/p>\n<p><!-- notionvc: c645e6e4-6dcc-41ac-a903-4469a4416996 --><\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ package.json<\/span>\n\n{\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  <span class=\"hljs-attr\">\"scripts\"<\/span>: {\n    <span class=\"hljs-attr\">\"dev\"<\/span>: <span class=\"hljs-string\">\"next dev\"<\/span>,\n    <span class=\"hljs-attr\">\"build\"<\/span>: <span class=\"hljs-string\">\"next build\"<\/span>,\n    <span class=\"hljs-attr\">\"start\"<\/span>: <span class=\"hljs-string\">\"next start\"<\/span>,\n    <span class=\"hljs-attr\">\"lint\"<\/span>: <span class=\"hljs-string\">\"next lint\"<\/span>,\n    <span class=\"hljs-attr\">\"intl:extract\"<\/span>: <span class=\"hljs-string\">\"formatjs extract '{pages,components,sections}\/**\/*.{js,jsx,ts,tsx}' --out-file lang\/src\/en-US.json --format simple\"<\/span>,\n+   <span class=\"hljs-attr\">\"intl:compile\"<\/span>: <span class=\"hljs-string\">\"formatjs compile-folder lang\/src lang\/compiled --format simple --ast\"<\/span>\n  },\n  <span class=\"hljs-comment\">\/\/ ...<\/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\">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_5a9746edc6e2469afc1d7242fabfb799\" 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>Format.JS\u2019s <code>compile-folder<\/code> command will scan all message files in one folder and compile them to another folder.<\/p>\n<p>When using the compiler, it&#8217;s important to specify the source message format with the <code>--format<\/code> flag. Since the <code>simple<\/code> format was used for message extraction, this same format should be provided to the compiler.<\/p>\n<p>We also add the <code>--ast<\/code> flag to get compiled messages in AST format instead of the default string format.<\/p>\n<p>Let\u2019s run the command.<\/p>\n<p><!-- notionvc: c759dee9-c59f-48b5-a8ad-5cb50ce9fe8a --><\/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=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm run intl:compile<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><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_ff17be6aa99e4fb9ed01902f11e55719\" 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 all goes well, our messages should be compiled under the <span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">lang\/compiled<\/span> directory:<!-- notionvc: ea0ef97f-da74-40a1-8962-8dff82fc4aea --><\/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\">\/\/ lang\/compiled\/en-US.json<\/span>\n\n{\n  <span class=\"hljs-attr\">\"RohNOo\"<\/span>: &#91;\n    {\n      <span class=\"hljs-attr\">\"type\"<\/span>: <span class=\"hljs-number\">0<\/span>,\n      <span class=\"hljs-attr\">\"value\"<\/span>: <span class=\"hljs-string\">\"Hello i18n!\"<\/span>\n    }\n  ]\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ lang\/compiled\/ar-EG.json<\/span>\n\n{\n  <span class=\"hljs-attr\">\"RohNOo\"<\/span>: &#91;\n    {\n      <span class=\"hljs-attr\">\"type\"<\/span>: <span class=\"hljs-number\">0<\/span>,\n      <span class=\"hljs-attr\">\"value\"<\/span>: <span class=\"hljs-string\">\"\u0623\u0647\u0644\u0627\u064b \u0628\u0627\u0644\u062a\u062f\u0648\u064a\u0644!\"<\/span>\n    }\n  ]\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">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_1789199d691e1693672a1f199addcbf3\" 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>As you continue building your app and adding translation messages, you\u2019ll notice that messages compiled into AST format retain a simplified object structure that doesn\u2019t need to be parsed.<\/p>\n<p>So we can remove the expensive ICU MessageFormat parser from our production environment:<\/p>\n<p><!-- notionvc: 41d581b3-bfd4-485a-a157-cf1416212786 --><\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ next.config.js\n\n\/** @type {import('next').NextConfig} *\/\nconst nextConfig = {\n  reactStrictMode: true,\n  i18n: {\n    locales: &#91;\"en-US\", \"ar-EG\"],\n    defaultLocale: \"en-US\",\n  },\n<span class=\"hljs-addition\">+ webpack: (config, { dev, ...other }) =&gt; {<\/span>\n<span class=\"hljs-addition\">+   if (!dev) {<\/span>\n<span class=\"hljs-addition\">+      config.resolve.alias&#91;\"@formatjs\/icu-messageformat-parser\"] =<\/span>\n<span class=\"hljs-addition\">+       \"@formatjs\/icu-messageformat-parser\/no-parser\";<\/span>\n<span class=\"hljs-addition\">+   }<\/span>\n<span class=\"hljs-addition\">+   return config;<\/span>\n<span class=\"hljs-addition\">+ },<\/span>\n};\n\nmodule.exports = nextConfig;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><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_14efd622378935ac6149a892388fd463\" 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 speeds up our app in production by <a href=\"https:\/\/formatjs.io\/docs\/guides\/advanced-usage#react-intl-without-parser-40-smaller\">reducing the Format.JS bundle size by ~40%.<\/a><\/p>\n<p>We can see our new config in action by swapping in our compiled messages.<\/p>\n<p><!-- notionvc: 419f304a-ceb1-4013-8cb5-0f6644f7a60e --><\/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\">\/\/ pages\/_app.tsx\n\n<span class=\"hljs-deletion\">- import arMessages from \"@\/lang\/src\/ar-EG.json\";<\/span>\n<span class=\"hljs-addition\">+ import arMessages from \"@\/lang\/compiled\/ar-EG.json\";<\/span>\n<span class=\"hljs-deletion\">- import enMessages from \"@\/lang\/src\/en-US.json\";<\/span>\n<span class=\"hljs-addition\">+ import enMessages from \"@\/lang\/compiled\/en-US.json\";<\/span>\n\n\/\/ ...\n\nexport default function App({ Component, pageProps }: AppProps) {\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_94f10311131fd7cf815b5ad7774547f2\" 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>Running <code>npm run build &amp;&amp; npm start<\/code> from the command line allows us to test our production environment. Looking at our browser network tab, we can see the difference in app bundle size:<\/p>\n<p><!-- notionvc: 8db20514-c7c5-40a5-823b-26973488ba0c --><\/p>\n<figure id=\"attachment_72302\" aria-describedby=\"caption-attachment-72302\" style=\"width: 1256px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-72302\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/app-bundle-with-parser.png\" alt=\"\" width=\"1256\" height=\"510\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/app-bundle-with-parser.png 1256w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/app-bundle-with-parser-300x122.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/app-bundle-with-parser-1024x416.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/app-bundle-with-parser-768x312.png 768w\" sizes=\"(max-width: 1256px) 100vw, 1256px\" \/><figcaption id=\"caption-attachment-72302\" class=\"wp-caption-text\">Our app bundle size with the ICU MessageFormat parser bundled, weighing in at 44.9 KB<\/figcaption><\/figure>\n<figure id=\"attachment_72314\" aria-describedby=\"caption-attachment-72314\" style=\"width: 1246px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-72314\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/app-bundle-no-parser.png\" alt=\"\" width=\"1246\" height=\"510\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/app-bundle-no-parser.png 1246w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/app-bundle-no-parser-300x123.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/app-bundle-no-parser-1024x419.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/app-bundle-no-parser-768x314.png 768w\" sizes=\"(max-width: 1246px) 100vw, 1246px\" \/><figcaption id=\"caption-attachment-72314\" class=\"wp-caption-text\">Our app bundle size with the ICU MessageFormat parser removed, dropping by 20% to 35.86 KB<\/figcaption><\/figure>\n<p>\ud83d\udd17\u00a0Read more about pre-compiling and performance in the <a href=\"https:\/\/formatjs.io\/docs\/guides\/advanced-usage\/\">Format.JS Advanced Usage docs<\/a>.<\/p>\n<p><!-- notionvc: 09a6907c-7b02-4058-97ce-bbfa21f73aaf --><\/p>\n<p>We\u2019ve made our workflow and our production app leaner by using the wonderful Format.JS CLI and the Format.JS Babel plugin. We can do even better by only loading the translation file we need (instead of all of them) and doing so only on the server (instead of potentially reloading on the client). We will look at that next.<\/p>\n<p><!-- notionvc: d384ee19-2a26-4168-9855-a231ef0401eb --><\/p>\n<h2>How do I load a translation file on the server?<\/h2>\n<p>We\u2019re currently loading the translation message files for <em>all<\/em> our supported locales while only using the file for our active locale, which doesn\u2019t scale well. We can solve this problem by using our pages\u2019 <code>getStaticProps()<\/code> to load only the active locale\u2019s translation file on the server.<\/p>\n<p>We have to do this in each of our pages, so let\u2019s write a helper function that we can reuse.<\/p>\n<p><!-- notionvc: 3156b43e-e065-4276-9730-fde630553d7c --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ i18n\/get-locale-messages.ts<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> fs <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"fs\/promises\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> path <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"path\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">getLocaleMessages<\/span>(<span class=\"hljs-params\">\n  locale: <span class=\"hljs-built_in\">string<\/span> | <span class=\"hljs-literal\">undefined<\/span>,\n<\/span>): <span class=\"hljs-title\">Promise<\/span>&lt;<span class=\"hljs-title\">Record<\/span>&lt;<span class=\"hljs-title\">string<\/span>, <span class=\"hljs-title\">string<\/span>&gt;&gt; <\/span>{\n  <span class=\"hljs-keyword\">if<\/span> (!locale) {\n    <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">\"Locale is missing.\"<\/span>);\n  }\n\n  <span class=\"hljs-keyword\">const<\/span> messageFilePath = path.join(\n    process.cwd(),\n    <span class=\"hljs-string\">\"lang\"<\/span>,\n    process.env.NODE_ENV === <span class=\"hljs-string\">\"development\"<\/span>\n      ? <span class=\"hljs-string\">\"src\"<\/span>\n      : <span class=\"hljs-string\">\"compiled\"<\/span>,\n    <span class=\"hljs-string\">`<span class=\"hljs-subst\">${locale}<\/span>.json`<\/span>,\n  );\n\n  <span class=\"hljs-keyword\">const<\/span> messages = <span class=\"hljs-keyword\">await<\/span> fs.readFile(\n    messageFilePath,\n    <span class=\"hljs-string\">\"utf8\"<\/span>,\n  );\n\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-built_in\">JSON<\/span>.parse(messages);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><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_42ade22df5216c80bb52c0939bfe4ce4\" 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 load the translation file for a given <code>locale<\/code>, we check to see if we\u2019re in development or production. In development, we load <code>lang\/src\/en-US.json<\/code> (or <code>ar-EG.json<\/code>). In production, we load the compiled <code>lang\/compiled\/en-US.json<\/code>.<\/p>\n<p><!-- notionvc: b76c72ea-8d1a-4271-b542-8cfcf3430bf4 --><\/p>\n<p>We can now use <code>getLocaleMessage()<\/code> to load our translation file in our home page.<\/p>\n<p><!-- notionvc: 064f20d3-9bd0-440d-91fb-c26d58884c92 --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-26\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ pages\/index.tsx\n\n<span class=\"hljs-addition\">+ import getLocaleMessages from \"@\/i18n\/get-locale-messages\";<\/span>\n  import type { \n    GetStaticProps,\n    GetStaticPropsContext,\n  } from \"next\/types\";\n  import { FormattedMessage } from \"react-intl\";\n\n  type HomeProps = {\n<span class=\"hljs-addition\">+   localeMessages: Record&lt;string, string&gt;;<\/span>\n    date: string;\n  };\n\n  export const getStaticProps: GetStaticProps&lt;\n    HomeProps\n  &gt; = async ({ \n<span class=\"hljs-addition\">+   locale<\/span>\n  }: GetStaticPropsContext) =&gt; {\n    return {\n      props: {\n<span class=\"hljs-addition\">+       localeMessages: await getLocaleMessages(locale),<\/span>\n        date: new Date().toString(),\n      },\n    };\n  };\n\nexport default function Home({ date }: HomeProps) {\n  return (\n    &lt;Layout&gt;\n      &lt;h1 className=\"...\"&gt;\n        &lt;FormattedMessage defaultMessage=\"Hello i18n!\" \/&gt;\n      &lt;\/h1&gt;\n\n      {\/* ... *\/}\n\n    &lt;\/Layout&gt;\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-26\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_ee48ed50e920e7af27e118d46fed6072\" 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.js provides a <code>locale<\/code> param with the value of the active locale to <code>getStaticProps()<\/code>. We use this param to load the translation messages corresponding to the active locale into the <code>localeMessages<\/code> page prop.<\/p>\n<p>We can now use <code>localeMessages<\/code> in our root <code>App<\/code> component, passing it to react-intl\u2019s <code>&lt;IntlProvider&gt;<\/code>.<\/p>\n<p><!-- notionvc: 940c2350-062d-4beb-b675-cba0bac88d21 --><\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ pages\/_app.tsx\n\n<span class=\"hljs-deletion\">- import arMessages from \"@\/lang\/compiled\/ar-EG.json\";<\/span>\n<span class=\"hljs-deletion\">- import enMessages from \"@\/lang\/compiled\/en-US.json\";<\/span>\n  import \"@\/styles\/globals.css\";\n  import type { AppProps } from \"next\/app\";\n  import { useRouter } from \"next\/router\";\n  import { useEffect } from \"react\";\n  import { IntlProvider } from \"react-intl\";\n\n<span class=\"hljs-deletion\">- const messages = {<\/span>\n<span class=\"hljs-deletion\">-   \"en-US\": { ...enMessages },<\/span>\n<span class=\"hljs-deletion\">-   \"ar-EG\": { ...arMessages },<\/span>\n<span class=\"hljs-deletion\">- };<\/span>\n\n  export default function App({ Component, pageProps }: AppProps) {\n    const { locale, defaultLocale } = useRouter();\n\n    return (\n      &lt;IntlProvider\n        locale={locale!}\n        defaultLocale={defaultLocale!}\n<span class=\"hljs-deletion\">-       messages={messages&#91;locale as keyof typeof messages]}<\/span>\n<span class=\"hljs-addition\">+       messages={pageProps.localeMessages}<\/span>\n      &gt;\n        &lt;Component {...pageProps} \/&gt;\n      &lt;\/IntlProvider&gt;\n    );\n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-27\"><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_a9df3be4c57d9caad5ddb042efbe0fde\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>The <code>App<\/code> component has access to each page component\u2019s props through <code>pageProps<\/code>. So our server-loaded messages from the home page are available to <code>App<\/code> via <code>pageProps.localeMessages<\/code>.<\/p>\n<p>With that, we\u2019ve eliminated the need to load <em>all<\/em> of our translation files. We\u2019ve also limited expensive file loading and parsing to the server by using pages\u2019 <code>getStaticProps()<\/code>.<\/p>\n<p>\u270b\u00a0Of course, for this to work, we have to add <code>localeMessages: await getLocaleMessages(locale)<\/code> to each page in our app. Here is our posts index as another example:<\/p>\n<p><!-- notionvc: 8153835e-7ba8-4f98-b762-3b8f0b48c47d --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-28\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ pages\/posts\/index.tsx\n\n  import Layout from \"@\/components\/Layout\";\n  import { posts } from \"@\/data\/posts\";\n<span class=\"hljs-addition\">+ import getLocaleMessages from \"@\/i18n\/get-locale-messages\";<\/span>\n  import type { Post } from \"@\/types\";\n  import type { \n    GetStaticProps,\n    GetStaticPropsContext\n  } from \"next\";\n  import Link from \"next\/link\";\n\n  type PostIndexProps = {\n<span class=\"hljs-addition\">+   localeMessages: Record&lt;string, string&gt;;<\/span>\n    posts: Post&#91;];\n  };\n\n  export const getStaticProps: GetStaticProps&lt;\n    PostIndexProps\n  &gt; = async ({ \n<span class=\"hljs-addition\">+   locale<\/span>\n  }: GetStaticPropsContext) =&gt; {\n    return {\n      props: {\n<span class=\"hljs-addition\">+       localeMessages: await getLocaleMessages(locale),<\/span>\n        posts,\n      },\n    };\n  };\n\n  export default function PostIndex({\n    posts,\n  }: PostIndexProps) {\n    \/\/ Render the posts...\n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-28\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_570830f40972fca4a3658a6f4f2359da\" 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<h3>Loading translation messages in the App component<\/h3>\n<p>We only need to add a few lines per page to load our translations on the server. However, you might be wondering how we make all this DRY (Don\u2019t Repeat Yourself). One possible solution is using the <a href=\"https:\/\/nextjs.org\/docs\/pages\/api-reference\/functions\/get-initial-props\">legacy getInitialProps()<\/a> function in the App component.<\/p>\n<p><!-- notionvc: 89efd8ea-1208-4e76-b540-1c2036451775 --><\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ pages\/_app.tsx\n\n  import \"@\/styles\/globals.css\";\n<span class=\"hljs-deletion\">- import type { AppProps } from \"next\/app\";<\/span>\n<span class=\"hljs-addition\">+ import type { AppContext, AppProps } from \"next\/app\";<\/span>\n  import App from \"next\/app\";\n  import { useRouter } from \"next\/router\";\n  import { IntlProvider } from \"react-intl\";\n\n<span class=\"hljs-deletion\">- export default function App({<\/span>\n<span class=\"hljs-addition\">+ export default function MyApp({<\/span>\n    Component,\n    pageProps,\n<span class=\"hljs-addition\">+   messages,<\/span>\n<span class=\"hljs-deletion\">- }: AppProps) {<\/span>\n<span class=\"hljs-addition\">+ }: AppProps &amp; { messages: Record&lt;string, string&gt; }) {<\/span>\n    const { locale, defaultLocale } = useRouter();\n\n    return (\n      &lt;IntlProvider\n        locale={locale!}\n        defaultLocale={defaultLocale!}\n<span class=\"hljs-deletion\">-       messages={pageProps.localeMessages}<\/span>\n<span class=\"hljs-addition\">+       messages={messages}<\/span>\n      &gt;\n        &lt;Component {...pageProps} \/&gt;\n      &lt;\/IntlProvider&gt;\n    );\n  }\n\n<span class=\"hljs-addition\">+ \/\/ \ud83d\udc47 Load the messages for the active locale<\/span>\n<span class=\"hljs-addition\">+ MyApp.getInitialProps = async (ctx: AppContext) =&gt; {<\/span>\n<span class=\"hljs-addition\">+   const { locale } = ctx.router;<\/span>\n<span class=\"hljs-addition\">+<\/span>\n<span class=\"hljs-addition\">+   const subdirectory =<\/span>\n<span class=\"hljs-addition\">+     process.env.NODE_ENV === \"development\"<\/span>\n<span class=\"hljs-addition\">+       ? \"src\"<\/span>\n<span class=\"hljs-addition\">+       : \"compiled\";<\/span>\n<span class=\"hljs-addition\">+<\/span>\n<span class=\"hljs-addition\">+   const messages: Record&lt;string, string&gt; = await import(<\/span>\n<span class=\"hljs-addition\">+     `@\/lang\/${subdirectory}\/${locale}`<\/span>\n<span class=\"hljs-addition\">+   );<\/span>\n<span class=\"hljs-addition\">+<\/span>\n<span class=\"hljs-addition\">+   const appProps = await App.getInitialProps(ctx);<\/span>\n<span class=\"hljs-addition\">+<\/span>\n<span class=\"hljs-addition\">+   return { ...appProps, messages };<\/span>\n<span class=\"hljs-addition\">+ };<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-29\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_ecdf40b48d0e8e8b801328a3a952f5ae\" 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, we can remove all the <code>getLocaleMessage()<\/code> calls in our pages, encapsulating locale loading in the <code>App<\/code> component.<\/p>\n<p>This DRYing up comes at a cost, however: using <code>getInitialProps()<\/code> on the <code>App<\/code> component causes Next.js to <a href=\"https:\/\/nextjs.org\/docs\/messages\/opt-out-auto-static-optimization\">opt out<\/a> of <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/rendering\/automatic-static-optimization\">Automatic Static Optimization<\/a>. This means that simple pages that have no <code>getServerSideProps()<\/code> or <code>getInitialProps()<\/code> will not be pre-rendered (and cached in a CDN) as usual. We can see this when we build our app.<\/p>\n<p>\ud83d\uddd2\ufe0f\u00a0I added a simple <em>About page<\/em> to test here<\/p>\n<figure id=\"attachment_72320\" aria-describedby=\"caption-attachment-72320\" style=\"width: 1142px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-72320\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/build-w-getstaticprops.png\" alt=\"\" width=\"1142\" height=\"758\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/build-w-getstaticprops.png 1142w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/build-w-getstaticprops-300x199.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/build-w-getstaticprops-1024x680.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/11\/build-w-getstaticprops-768x510.png 768w\" sizes=\"(max-width: 1142px) 100vw, 1142px\" \/><figcaption id=\"caption-attachment-72320\" class=\"wp-caption-text\">By default, simple pages are statically rendered<\/figcaption><\/figure>\n<p><!-- notionvc: ee24b81d-dad5-4508-b210-0f3450efc5ac --><\/p>\n<p>&nbsp;<\/p>\n<figure id=\"attachment_72328\" aria-describedby=\"caption-attachment-72328\" style=\"width: 1130px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-72328\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/build-w-app-load.png\" alt=\"\" width=\"1130\" height=\"760\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/build-w-app-load.png 1130w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/build-w-app-load-300x202.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/build-w-app-load-1024x689.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/build-w-app-load-768x517.png 768w\" sizes=\"(max-width: 1130px) 100vw, 1130px\" \/><figcaption id=\"caption-attachment-72328\" class=\"wp-caption-text\">With App component <code>getInitialProps()<\/code> translation loading, simple pages become dynamically rendered<\/figcaption><\/figure>\n<p>So it\u2019s a tradeoff either way. In this tutorial, we will stick to per-page <code>getStaticProps<\/code> translation loading, since it\u2019s only a few lines per page for an overall performance gain. The choice is yours, of course.<\/p>\n<p>\ud83d\udd17\u00a0If you want a closer look at the <code>getInitialProps()<\/code> solution, we have a <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-react-intl-2023\/tree\/load-messages-in-app\">dedicated branch<\/a> in our GitHub repo that covers it. You can also see a handy <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-react-intl-2023\/compare\/load-messages-in-app\">diff that focuses on loading code<\/a>.<\/p>\n<p>\ud83d\uddd2\ufe0f\u00a0Server Components, introduced in Next.js with the App Router, can be really helpful here. We cover this in <a href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/\">A Deep Dive into Next.js App Router Localization with next-intl<\/a>. (Do note that at the time of writing, unlike react-intl, next-intl doesn\u2019t cover message extraction\/compilation).<\/p>\n<h2>How do I build a language switcher?<\/h2>\n<p>We\u2019ve laid the plumbing of react-intl + Next.js, making sure we\u2019re loading translations as efficiently as possible. Let\u2019s switch gears a bit and make a locale switcher for our site visitors since we often need a way for our users to manually select their own language.<\/p>\n<p>We will add a <code>LocaleSwitcher<\/code> component that takes care of this.<\/p>\n<p><!-- notionvc: c36db1ca-c83f-462f-9776-401bd0d64b58 --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ components\/LocaleSwitcher.tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { useRouter } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next\/router\"<\/span>;\n\n<span class=\"hljs-keyword\">const<\/span> localeNames: Record&lt;<span class=\"hljs-built_in\">string<\/span>, <span class=\"hljs-built_in\">string<\/span>&gt; = {\n  <span class=\"hljs-string\">\"en-US\"<\/span>: <span class=\"hljs-string\">\"English\"<\/span>,\n  <span class=\"hljs-string\">\"ar-EG\"<\/span>: <span class=\"hljs-string\">\"(Arabic) \u0627\u0644\u0639\u0631\u0628\u064a\u0629\"<\/span>,\n};\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> router = useRouter();\n\n  <span class=\"hljs-comment\">\/\/ `locales` list is configured in next.config.js<\/span>\n  <span class=\"hljs-keyword\">const<\/span> { locale, locales } = router;\n\n  <span class=\"hljs-keyword\">const<\/span> handleLocaleChange = (\n    e: React.ChangeEvent&lt;HTMLSelectElement&gt;,\n  ) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> locale = e.target.value;\n    router.push(router.pathname, router.asPath, { locale });\n  };\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;div&gt;\n      &lt;select\n        value={locale}\n        onChange={handleLocaleChange}\n        className=<span class=\"hljs-string\">\"...\"<\/span>\n      &gt;\n        {locales!.map(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">locale<\/span><\/span>) =&gt;<\/span> (\n          &lt;option key={locale} value={locale}&gt;\n            {localeNames&#91;locale]}\n          &lt;<span class=\"hljs-regexp\">\/option&gt;\n        ))}\n      &lt;\/<\/span>select&gt;\n    &lt;<span class=\"hljs-regexp\">\/div&gt;\n  );\n}<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-30\"><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_95840217652c1321606a7ff6cfe51d50\" 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>Like the <code>&lt;Link&gt;<\/code> component, the Next.js router takes a <code>locale<\/code> option that makes the router change the locale route prefix for a given route path. For example, if we call <code>router.push(\"\/posts\", \"\/posts\", { locale: \"ar-EG\" })<\/code>, we will be navigated to <code>\/ar-EG\/posts<\/code>.<\/p>\n<p>\ud83d\udd17\u00a0See the <a href=\"https:\/\/nextjs.org\/docs\/pages\/api-reference\/functions\/use-router#routerpush\">router.push() docs<\/a> for more info.<\/p>\n<p>If we place our new <code>&lt;LocaleSwitcher&gt;<\/code> in the header section of our <code>&lt;Layout&gt;<\/code>, we can see this in action.<\/p>\n<p><!-- notionvc: 60a9da29-d423-4207-b1b3-05670b366f06 --><\/p>\n<figure id=\"attachment_72338\" aria-describedby=\"caption-attachment-72338\" style=\"width: 600px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-72338\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/locale-switcher.gif\" alt=\"\" width=\"600\" height=\"414\" \/><figcaption id=\"caption-attachment-72338\" class=\"wp-caption-text\">A language-switching UI makes it easy for our site visitors to manually select their locale<\/figcaption><\/figure>\n<h3>Setting the NEXT_LOCALE cookie<\/h3>\n<p>Right now, our user\u2019s selected locale won\u2019t be remembered the next time they visit our site. Recall that Next.js i18n has built-in locale detection. We can <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/routing\/internationalization#leveraging-the-next_locale-cookie\">override this detection by setting a NEXT_LOCALE cookie<\/a>.<\/p>\n<p>Let\u2019s add that logic to our <code>LocaleSwitcher<\/code>. We will use the popular <a href=\"https:\/\/github.com\/maticzav\/nookies#readme\">nookies<\/a> package, which allows to set cookies easily from the browser.<\/p>\n<p>Let\u2019s install it.<\/p>\n<p><!-- notionvc: 27a6366c-7900-49d1-aecf-b71d7e552bd5 --><\/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=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm install nookies<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-31\"><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_1d03645970f1b0e94d01ba876e81301d\" 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 can use nookies\u2019 <code><span class=\"notion-enable-hover\" spellcheck=\"false\" data-token-index=\"1\">setCookie()<\/span><\/code> in our component.<!-- notionvc: 9d2cb799-fbbb-4ca3-970e-7871744eed35 --><\/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\">\/\/ components\/LocaleSwitcher.tsx\n\n  import { useRouter } from \"next\/router\";\n<span class=\"hljs-addition\">+ import { setCookie } from \"nookies\";<\/span>\n\n  \/\/ ...\n\n  export default function LocaleSwitcher() {\n    const router = useRouter();\n    const { locale, locales } = router;\n\n    const handleLocaleChange = (\n      e: React.ChangeEvent&lt;HTMLSelectElement&gt;,\n    ) =&gt; {\n      const locale = e.target.value;\n    \n<span class=\"hljs-addition\">+     setCookie(null, \"NEXT_LOCALE\", locale, {<\/span>\n<span class=\"hljs-addition\">+       sameSite: \"Strict\",<\/span>\n<span class=\"hljs-addition\">+       path: \"\/\",<\/span>\n<span class=\"hljs-addition\">+       \/\/ Set the lifetime of the cookie to one year<\/span>\n<span class=\"hljs-addition\">+       maxAge: 365 * 24 * 60 * 60,<\/span>\n<span class=\"hljs-addition\">+     });<\/span>\n\n      router.push(router.pathname, router.asPath, { locale });\n    };\n\n    return (\n      \/\/ ...\n    );\n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-32\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_b8f2a6a8bffa638901f95509887fca44\" 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 change the locale manually with the switcher, we can see a cookie being set in our browser dev tools.<!-- notionvc: c0feb39f-41c3-468c-9ff5-0686188b5d4c --><\/p>\n<figure id=\"attachment_72344\" aria-describedby=\"caption-attachment-72344\" style=\"width: 2108px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-72344\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/cookie-set.png\" alt=\"\" width=\"2108\" height=\"356\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/cookie-set.png 2108w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/cookie-set-300x51.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/cookie-set-1024x173.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/cookie-set-768x130.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/cookie-set-1536x259.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/cookie-set-2048x346.png 2048w\" sizes=\"(max-width: 2108px) 100vw, 2108px\" \/><figcaption id=\"caption-attachment-72344\" class=\"wp-caption-text\">Our browser dev tools confirm that the NEXT_LOCALE cookie is being set<\/figcaption><\/figure>\n<p>With this setup, when a visitor manually selects a locale, the NEXT_LOCALE cookie stores their choice. Next.js&#8217; locale auto-detection then prioritizes the cookie&#8217;s value over its own detected locale. This ensures that the visitor sees the site in their previously selected language during subsequent visits.<\/p>\n<h2>How do I automatically detect the user\u2019s locale?<\/h2>\n<p>Next.js i18n has automatic locale detection built in. In fact, it\u2019s on by default. However, as we\u2019ve mentioned before, this detection can be a bit <em>too<\/em> precise.<\/p>\n<p>For example, if a visitor sets <code>ar-SY<\/code> (Arabic as it is used in Syria) in their browser language preferences, Next.js won\u2019t serve them the <code>ar-EG<\/code> version of our website. This is a missed opportunity because Egypt and Syria share the same written Arabic.<\/p>\n<p>To achieve loose locale matching, where only the language is matched, we can roll our own locale auto-detection via <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/routing\/middleware\">custom Next.js middleware<\/a>.<\/p>\n<p>First, let\u2019s disable Next\u2019s built-in locale auto-detection.<\/p>\n<p><!-- notionvc: 77deed51-fb14-4079-a434-cc2b91b79302 --><\/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\">\/\/ next.config.js\n\n\/** @type {import('next').NextConfig} *\/\nconst nextConfig = {\n  reactStrictMode: true,\n  i18n: {\n    locales: &#91;\"en-US\", \"ar-EG\"],\n    defaultLocale: \"en-US\",\n<span class=\"hljs-addition\">+   localeDetection: false,<\/span>\n  },\n  webpack: (config, { dev, ...other }) =&gt; {\n    \/\/ ...\n};\n\nmodule.exports = nextConfig;<\/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_c10d026787fc820a156f5572599bb7d6\" 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 make our work easier, we will use the <a class=\"notion-link-token notion-focusable-token notion-enable-hover\" tabindex=\"0\" href=\"https:\/\/github.com\/opentable\/accept-language-parser\" rel=\"noopener noreferrer\" data-token-index=\"1\"><span class=\"link-annotation-unknown-block-id-1826578404\">accept-language-parser<\/span><\/a> package to parse and match the user\u2019s locale from the Accept-Language HTTP header. Let\u2019s install the package.<!-- notionvc: fbfcc10a-aa71-4f37-a067-412078325017 --><\/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=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm install accept-language-parser<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-34\"><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_6e715ded133f5dd394ce331326f7ced9\" 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 utilize the package to provide a <code>bestMatch()<\/code> function that we will use in our new middleware momentarily.<\/p>\n<p><!-- notionvc: f7d238a3-bf99-4860-8c09-13374467fe6d --><\/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\">\/\/ i18n\/best-match.ts<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> nextConfig <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/next.config\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> acceptLanguageParser <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"accept-language-parser\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">bestMatch<\/span>(<span class=\"hljs-params\">\n  acceptLanguageHeader: <span class=\"hljs-built_in\">string<\/span> | <span class=\"hljs-literal\">null<\/span>,\n<\/span>): <span class=\"hljs-title\">string<\/span> | <span class=\"hljs-title\">null<\/span> <\/span>{\n  <span class=\"hljs-keyword\">if<\/span> (!nextConfig.i18n) {\n    <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(\n      <span class=\"hljs-string\">\"Please add i18n config to next.config.js\"<\/span>,\n    );\n  }\n\n  <span class=\"hljs-keyword\">if<\/span> (!acceptLanguageHeader) {\n    <span class=\"hljs-keyword\">return<\/span> nextConfig.i18n.defaultLocale;\n  }\n\n  <span class=\"hljs-keyword\">const<\/span> supportedLocales = nextConfig.i18n.locales;\n\n  <span class=\"hljs-keyword\">const<\/span> bestMatch = acceptLanguageParser.pick(\n    supportedLocales,\n    acceptLanguageHeader,\n    { loose: <span class=\"hljs-literal\">true<\/span> },\n  );\n  <span class=\"hljs-keyword\">return<\/span> bestMatch;\n}<\/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_363338075001b61461f8c651dad9cfc2\" 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 pull in our <code>next.config.js<\/code> i18n values and use the accept-language-parser\u2019s <code>pick()<\/code> function to find the best matching locale.<\/p>\n<p>The <code>acceptLanguage<\/code> string parameter should match the format of a standard HTTP Accept-Language header, such as <code>\"fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5\"<\/code>. This represents a prioritized list of user-preferred locales, indicating their language preferences and environment.<\/p>\n<p>\ud83d\udd17\u00a0Learn more about the Accept-Language header in our <a href=\"https:\/\/phrase.com\/blog\/posts\/detecting-a-users-locale\/\">locale detection guide<\/a>.<\/p>\n<p>Given <code>acceptLanguage<\/code> and a list of app-supported locales (<code>en-US<\/code>, <code>ar-EG<\/code> in our case), <code>pick()<\/code> will try to find the best match in the supported locales list. By setting the <code>loose<\/code> option to <code>true<\/code>, we ensure that this best match is by language regardless of region, e.g. <code>ar-SY<\/code> in the Accept-Language header will match the supported <code>ar-EG<\/code>.<\/p>\n<p>OK, armed with <code>bestMatch()<\/code>, let\u2019s write our custom middleware.<\/p>\n<p><!-- notionvc: 42aa3954-b9a5-4674-9beb-e50d3cdaaf2c --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ middleware.ts<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { bestMatch } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/i18n\/best-match\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> {\n  NextResponse,\n  <span class=\"hljs-keyword\">type<\/span> NextRequest,\n} <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next\/server\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">middleware<\/span>(<span class=\"hljs-params\">req: NextRequest<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> matchedLocale = bestMatch(\n    req.headers.get(<span class=\"hljs-string\">\"Accept-Language\"<\/span>),\n  );\n\n  <span class=\"hljs-keyword\">const<\/span> { locale } = req.nextUrl;\n\n  <span class=\"hljs-keyword\">if<\/span> (locale !== matchedLocale) {\n    <span class=\"hljs-keyword\">return<\/span> NextResponse.redirect(\n      <span class=\"hljs-keyword\">new<\/span> URL(\n        <span class=\"hljs-string\">`\/<span class=\"hljs-subst\">${matchedLocale}<\/span><span class=\"hljs-subst\">${req.nextUrl.pathname}<\/span>`<\/span>,\n        req.nextUrl,\n      ),\n    );\n  }\n}\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> config = {\n  <span class=\"hljs-comment\">\/\/ Match the root route and any other route except<\/span>\n  <span class=\"hljs-comment\">\/\/ internal Next.js routes and public files.<\/span>\n  matcher: &#91;\n    <span class=\"hljs-string\">\"\/\"<\/span>,\n    <span class=\"hljs-string\">\"\/((?!api|_next\/static|_next\/image|favicon.ico).*)\"<\/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\">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_c6f8f38ef6f392d6689f9fae0299a9c6\" 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 middleware, we check to see if the best-matched locale is different from the current locale in the URL. If it is, we redirect to the URL with the best-matched locale swapped in.<\/p>\n<p>For example, if a site visitor has <code>pt-BR<\/code> (Portuguese in Brazil) as a browser language preference, and our app supports the locale, visiting <code>\/foo<\/code> or <code>\/fr-CA\/foo<\/code> would redirect the visitor to <code>\/pt-BR\/foo<\/code>.<\/p>\n<h3>Minding the NEXT_LOCALE cookie override<\/h3>\n<p>Remember the NEXT_LOCALE cookie we set in our locale switcher? The built-in Next.js locale detector will use it as an override, but we\u2019re not doing the same in our custom middleware. This means that if a visitor manually selects a locale from our switcher, their choice will be overridden by our auto-detection.<\/p>\n<p>Let\u2019s fix this.<\/p>\n<p><!-- notionvc: 0c93ed2e-fd4c-48ef-98de-1e5e19a9b2d2 --><\/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\">\/\/ middleware.ts\n\nimport {\n  NextResponse,\n  type NextRequest,\n} from \"next\/server\";\nimport { bestMatch } from \".\/i18n\/best-match\";\n\nexport function middleware(req: NextRequest) {\n  const { locale } = req.nextUrl;\n\n<span class=\"hljs-addition\">+ \/\/ If a locale was manually selected by the visitor<\/span>\n<span class=\"hljs-addition\">+ \/\/ during a previous visit, use *that* locale and<\/span>\n<span class=\"hljs-addition\">+ \/\/ don't attempt to auto-detect a best match.<\/span>\n<span class=\"hljs-addition\">+ const storedLocale = req.cookies.get(\"NEXT_LOCALE\");<\/span>\n<span class=\"hljs-addition\">+<\/span>\n<span class=\"hljs-addition\">+ if (storedLocale) {<\/span>\n<span class=\"hljs-addition\">+   if (storedLocale.value !== locale) {<\/span>\n<span class=\"hljs-addition\">+     return NextResponse.redirect(<\/span>\n<span class=\"hljs-addition\">+       new URL(<\/span>\n<span class=\"hljs-addition\">+         `\/${storedLocale.value}${req.nextUrl.pathname}`,<\/span>\n<span class=\"hljs-addition\">+         req.nextUrl,<\/span>\n<span class=\"hljs-addition\">+       ),<\/span>\n<span class=\"hljs-addition\">+     );<\/span>\n<span class=\"hljs-addition\">+   } else {<\/span>\n<span class=\"hljs-addition\">+     return;<\/span>\n<span class=\"hljs-addition\">+   }<\/span>\n<span class=\"hljs-addition\">+ }<\/span>\n\n  const matchedLocale = bestMatch(\n    req.headers.get(\"Accept-Language\"),\n  );\n\n  if (locale !== matchedLocale) {\n    return NextResponse.redirect(\n      new URL(\n        `\/${matchedLocale}${req.nextUrl.pathname}`,\n        req.nextUrl,\n      ),\n    );\n  }\n}\n\nexport const config = {\n  matcher: &#91;\n    \"\/\",\n    \"\/((?!api|_next\/static|_next\/image|favicon.ico).*)\",\n  ],\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-37\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_bcebde9a1a0e02879b3d39dc0ac772a8\" 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 this, we check for a previously selected locale and only auto-detect when we <em>don\u2019t<\/em> find one.<\/p>\n<h2>How do I localize SSR and SSG pages?<\/h2>\n<p>We\u2019ve been working with server-side rendering (SSR) and static site generation (SSG) throughout this guide, so we\u2019ve covered most of the basics. Here\u2019s a quick recap:<\/p>\n<ul>\n<li>In the context of static generation with Next.js i18n Routing, locale information is accessible via the Next.js router. Properties available are <code>locale<\/code> (the active locale), <code>locales<\/code> (all supported locales), and <code>defaultLocale<\/code>.<\/li>\n<li>When pre-rendering pages with <code>getStaticProps<\/code> or <code>getServerSideProps<\/code>, this locale information is provided in the context param passed to these functions.<\/li>\n<li>Similarly, when using <code>getStaticPaths<\/code>, the supported <code>locales<\/code> and <code>defaultLocale<\/code> are included in the context parameter of the function.<\/li>\n<li>Next.js i18n routing does <em>not<\/em> work with <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/deploying\/static-exports\">static exports<\/a>, ie. <code>output: 'export'<\/code>.<\/li>\n<\/ul>\n<p>\ud83d\udd17\u00a0Read <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/routing\/internationalization#dynamic-routes-and-getstaticprops-pages\">How does [i18n] work with Static Generation?<\/a> in the Next.js docs.<\/p>\n<p>Let\u2019s take a look at the code of the single post page to see all this in action.<\/p>\n<p><!-- notionvc: 5415e596-6bac-4c78-84b6-df17bc9d16e3 --><\/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\">\/\/ pages\/posts\/&#91;slug].tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> Layout <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/components\/Layout\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Our mock data<\/span>\n<span class=\"hljs-keyword\">import<\/span> { posts } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/data\/posts\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ For loading our UI translation messages<\/span>\n<span class=\"hljs-keyword\">import<\/span> getLocaleMessages <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/i18n\/get-locale-messages\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ TypeScript types<\/span>\n<span class=\"hljs-keyword\">import<\/span> <span class=\"hljs-keyword\">type<\/span> { TranslatedPost } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/types\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> <span class=\"hljs-keyword\">type<\/span> {\n  GetStaticPaths,\n  GetStaticProps,\n  GetStaticPropsContext,\n} <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next\"<\/span>;\n\n<span class=\"hljs-keyword\">import<\/span> Link <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next\/link\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { useRouter } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next\/router\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ react-intl<\/span>\n<span class=\"hljs-keyword\">import<\/span> { FormattedMessage } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-intl\"<\/span>;\n\n<span class=\"hljs-keyword\">type<\/span> SinglePostProps = {\n  localeMessages: Record&lt;<span class=\"hljs-built_in\">string<\/span>, <span class=\"hljs-built_in\">string<\/span>&gt;;\n  post: TranslatedPost;\n};\n\n<span class=\"hljs-comment\">\/\/ Since we're not providing locales in our<\/span>\n<span class=\"hljs-comment\">\/\/ returned `paths`, Next.js will only <\/span>\n<span class=\"hljs-comment\">\/\/ pre-build a version of this page translated<\/span>\n<span class=\"hljs-comment\">\/\/ to our default locale (en-US) during <\/span>\n<span class=\"hljs-comment\">\/\/ production builds.<\/span>\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> getStaticPaths: GetStaticPaths&lt;{\n  slug: <span class=\"hljs-built_in\">string<\/span>;\n}&gt; = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> {\n    paths: posts.map(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">post<\/span><\/span>) =&gt;<\/span> ({\n      params: { slug: post.slug },\n    })),\n\n    <span class=\"hljs-comment\">\/\/ Ensure that all locale translations<\/span>\n    <span class=\"hljs-comment\">\/\/ are served when requested.<\/span>\n    fallback: <span class=\"hljs-literal\">true<\/span>,\n  };\n};\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> getStaticProps = (<span class=\"hljs-keyword\">async<\/span> ({\n  params,\n  locale,\n}: GetStaticPropsContext) =&gt; {\n  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> params?.slug !== <span class=\"hljs-string\">\"string\"<\/span> || !locale) {\n    <span class=\"hljs-keyword\">return<\/span> { notFound: <span class=\"hljs-literal\">true<\/span> };\n  }\n\n  <span class=\"hljs-keyword\">const<\/span> foundPost = posts.find(\n    <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">p<\/span><\/span>) =&gt;<\/span> p.slug === params.slug,\n  );\n  <span class=\"hljs-keyword\">if<\/span> (!foundPost) {\n    <span class=\"hljs-keyword\">return<\/span> { notFound: <span class=\"hljs-literal\">true<\/span> };\n  }\n\n\t<span class=\"hljs-comment\">\/\/ Only get the translations for the<\/span>\n  <span class=\"hljs-comment\">\/\/ active `locale`.<\/span>\n  <span class=\"hljs-keyword\">const<\/span> post = {\n    date: foundPost.date,\n    slug: foundPost.slug,\n    ...foundPost.translations&#91;locale],\n  };\n\n  <span class=\"hljs-keyword\">return<\/span> {\n    props: {\n      <span class=\"hljs-comment\">\/\/ Load the active locale's translation<\/span>\n      <span class=\"hljs-comment\">\/\/ messages for the UI view (react-intl).<\/span>\n      localeMessages: <span class=\"hljs-keyword\">await<\/span> getLocaleMessages(locale),\n      post,\n    },\n  };\n}) satisfies GetStaticProps&lt;SinglePostProps&gt;;\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\">SinglePost<\/span>(<span class=\"hljs-params\">{\n  post,\n}: SinglePostProps<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> router = useRouter();\n\n  <span class=\"hljs-comment\">\/\/ Just a reminder that we can access<\/span>\n  <span class=\"hljs-comment\">\/\/ i18n config values here as well.<\/span>\n  <span class=\"hljs-keyword\">const<\/span> { locale, defaultLocale, locales } = router;\n\n  <span class=\"hljs-keyword\">if<\/span> (router.isFallback) {\n    <span class=\"hljs-keyword\">return<\/span> &lt;div&gt;Loading...&lt;<span class=\"hljs-regexp\">\/div&gt;;\n  }\n\n  return (\n    &lt;Layout&gt;\n      &lt;div className=\"...\"&gt;\n        &lt;Link href=\"\/<\/span>posts<span class=\"hljs-string\">\" className=\"<\/span>...<span class=\"hljs-string\">\"&gt;\n          &lt;FormattedMessage defaultMessage=\"<\/span>Back to post index<span class=\"hljs-string\">\" \/&gt;\n        &lt;\/Link&gt;\n      &lt;\/div&gt;\n\n      &lt;h1 className=\"<\/span>...<span class=\"hljs-string\">\"&gt;{post.title}&lt;\/h1&gt;\n\n      &lt;p className=\"<\/span>...<span class=\"hljs-string\">\"&gt;{post.date}&lt;\/p&gt;\n\n      &lt;div className=\"<\/span>...<span class=\"hljs-string\">\"&gt;\n        &lt;p&gt;{post.content}&lt;\/p&gt;\n      &lt;\/div&gt;\n    &lt;\/Layout&gt;\n  );\n}<\/span><\/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_15bb781c7f6822775bd56c593f98f05a\" 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\u00a0Get all the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-react-intl-2023\">code for the localized demo app<\/a> from our GitHub repo.<\/p>\n<p><!-- notionvc: 1b620008-2cb9-484b-8fd4-64e10bebfb07 --><\/p>\n<h2>How do localize my pages and components with react-intl?<\/h2>\n<p>We\u2019ve touched on this earlier, but there\u2019s more to localizing our views than basic messages with <code>&lt;FormattedMessage&gt;<\/code>. In this section, we briefly cover interpolation, plurals, and date and number formatting.<\/p>\n<p>\ud83d\udd17\u00a0We cover all this in more detail in our <a href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/\">Guide to Localizing React Apps with react-intl\/FormatJS<\/a>.<\/p>\n<h3>Basic translations<\/h3>\n<p>Translating basic strings often means adding a <code>&lt;FormattedMessage&gt;<\/code> with a <code>defaultMessage<\/code>.<\/p>\n<p><!-- notionvc: e7dc1413-ddbf-44b3-b12d-0ea31bbb84b5 --><\/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=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ In a component<\/span>\n<span class=\"hljs-keyword\">import<\/span> { FormattedMessage } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-intl\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ ...<\/span>\n\n&lt;p className=<span class=\"hljs-string\">\"...\"<\/span>&gt;\n  &lt;FormattedMessage defaultMessage=<span class=\"hljs-string\">\"Another look at Darth Vader\"<\/span> \/&gt;\n&lt;<span class=\"hljs-regexp\">\/p&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-39\"><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_2e84b9a408cb66f279cb54fed544aa0c\" 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 then extract, translate, and compile to see our message in different locales.<\/p>\n<p>An imperative <code>intl.formatMessage()<\/code> also exists, and it&#8217;s especially handy for localizing attributes and props.<\/p>\n<p><!-- notionvc: e4167933-0848-43ae-a733-a77bce826e82 --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-40\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ In a component<\/span>\n<span class=\"hljs-keyword\">import<\/span> { useIntl } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-intl\"<\/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> intl = useIntl();\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;img\n\t\t\tsrc=<span class=\"hljs-string\">\"...\"<\/span>\n      alt={intl.formatMessage({ \n\t\t\t  defaultMessage: <span class=\"hljs-string\">\"Picture of Chewbacca, unimpressed.\"<\/span>\n      })}\n    \/&gt;\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-40\"><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_132a7eadce5ec087a5a59d5a103a4d41\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>\u270b\u00a0Remember that aliasing <code>&lt;FormattedMessage&gt;<\/code> or <code>intl.formatMessage()<\/code> won\u2019t work with extraction without setting extra options. See the <em>Aliasing and custom components\/functions<\/em> section above for more details.<\/p>\n<h3>Localizing page metadata<\/h3>\n<p><code>intl.formatMessage()<\/code> is the only way to localize page metadata since using <code>&lt;FormatMessage&gt;<\/code> will throw an error when used inside Next\u2019s <code>&lt;Head&gt;<\/code> component.<\/p>\n<p><!-- notionvc: 6758d6d5-6fa5-4f77-a3d3-d9d9b3f86837 --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-41\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ pages\/posts\/index.tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> Layout <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/components\/Layout\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> Head <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next\/head\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { useIntl } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-intl\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ ...<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">PostIndex<\/span>(<span class=\"hljs-params\">{ posts }: PostIndexProps<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> intl = useIntl();\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;&gt;\n      &lt;Head&gt;\n        &lt;title&gt;\n          {intl.formatMessage({\n            defaultMessage: <span class=\"hljs-string\">\"Posts | r.intl\"<\/span>,\n          })}\n        &lt;<span class=\"hljs-regexp\">\/title&gt;\n        &lt;meta\n          name=\"description\"\n          content={intl.formatMessage({\n            defaultMessage: \"Our latest posts.\",\n          })}\n        \/<\/span>&gt;\n      &lt;<span class=\"hljs-regexp\">\/Head&gt;&gt;\n      &lt;Layout&gt;\n        {\/<\/span>* ... *<span class=\"hljs-regexp\">\/}\n      &lt;\/<\/span>Layout&gt;\n    &lt;<span class=\"hljs-regexp\">\/&gt;\n  );\n}<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-41\"><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_6e4bc56f948124a811e51e542cdff9bf\" 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<h3>Interpolation<\/h3>\n<p>To inject runtime values in our translation messages, we designate their locations with the <a href=\"https:\/\/phrase.com\/blog\/posts\/guide-to-the-icu-message-format\/\">ICU<\/a> <code>{variable}<\/code> syntax, then provide named values as params.<\/p>\n<p><!-- notionvc: 74ad996e-ce0d-48e9-bb7f-310a233d31dd --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-42\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">{<span class=\"hljs-comment\">\/* Imperative *\/<\/span>}\n&lt;p&gt;\n  {intl.formatMessage(\n    {\n      defaultMessage:\n        <span class=\"hljs-string\">\"This is a {next} demo of i18n with {reactIntl}\"<\/span>,\n    },\n    {\n       next: <span class=\"hljs-string\">\"Next.js\"<\/span>,\n       reactIntl: <span class=\"hljs-string\">\"react-intl\"<\/span>,\n    },\n  )}\n&lt;<span class=\"hljs-regexp\">\/p&gt;\n\n{\/<\/span>* Declarative *<span class=\"hljs-regexp\">\/}\n&lt;p&gt;\n  &lt;FormattedMessage\n    defaultMessage=\"This is a {next} demo of i18n with {reactIntl}\"\n    values={{\n      next: \"Next.js\",\n      reactIntl: \"react-intl\",\n    }}\n  \/<\/span>&gt;\n&lt;<span class=\"hljs-regexp\">\/p&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-42\"><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_b3f0633f32db2b2ccda3f563ea33ecf6\" 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\u00a0react-intl\/Format.JS implements the ICU (International Components for Unicode), a localization standard found in many environments. Learn more about it in <a href=\"https:\/\/phrase.com\/blog\/posts\/guide-to-the-icu-message-format\/\">The Missing Guide to the ICU Message Format<\/a>.<\/p>\n<h3>Plurals<\/h3>\n<p>One of the best things about the ICU Message Format is its robust support for plurals across locales.<\/p>\n<p>\ud83e\udd3f\u00a0Different languages have significantly different pluralization rules. Our <a href=\"https:\/\/phrase.com\/blog\/posts\/pluralization\/\">Guide to Localizing Plurals<\/a> goes deeper into that subject.<\/p>\n<p><!-- notionvc: 2160bd3c-fec9-4b7c-b1be-d9f5c5253d2c --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-43\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ components\/Plurals.tsx<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> &#91;messageCount, setMessageCount] =\n    useState&lt;<span class=\"hljs-built_in\">number<\/span>&gt;(<span class=\"hljs-number\">0<\/span>);\n\n<span class=\"hljs-comment\">\/\/ ...<\/span>\n\n&lt;span&gt;\n  &lt;FormattedMessage\n    defaultMessage={<span class=\"hljs-string\">`{count, plural,\n      =0 {You have no messges.}\n      one {You have one message.}\n      other {You have # messages.}}`<\/span>}\n      values={{ count: messageCount }}\n  \/&gt;\n&lt;<span class=\"hljs-regexp\">\/span&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-43\"><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_598cdf47a3382ab499da0e8d7a570217\" 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_72350\" aria-describedby=\"caption-attachment-72350\" style=\"width: 600px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-72350\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/plurals.gif\" alt=\"\" width=\"600\" height=\"357\" \/><figcaption id=\"caption-attachment-72350\" class=\"wp-caption-text\">While English has 3 plural forms, Arabic has 6<\/figcaption><\/figure>\n<p>\ud83d\udd17\u00a0See the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-react-intl-2023\/blob\/main\/pages-router\/complete\/components\/Plurals.tsx\">complete code for the above Plurals component on GitHub<\/a>.<\/p>\n<h3>Date formatting<\/h3>\n<p>Under the hood, Format.JS uses the standard <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/DateTimeFormat\">Intl.DateTimeFormat<\/a> for its localized date formatting. This means that we can pass options to react-intl\u2019s formatter that are used in turn by <code>Intl.DateTimeFormat<\/code>.<\/p>\n<p><!-- notionvc: c1001a80-6620-4707-afe5-4bedd2feb4f0 --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-44\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ components\/Dates.tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { FormattedDate, useIntl } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-intl\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ The `date` prop could be of type `Date`<\/span>\n<span class=\"hljs-comment\">\/\/ here as well: The following code would<\/span>\n<span class=\"hljs-comment\">\/\/ still work fine.<\/span>\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\">Dates<\/span>(<span class=\"hljs-params\">{ date }: { date: <span class=\"hljs-built_in\">string<\/span> }<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> intl = useIntl();\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;&gt;\n      &lt;span className=<span class=\"hljs-string\">\"...\"<\/span>&gt;\n        {intl.formatDate(date)}\n      &lt;<span class=\"hljs-regexp\">\/span&gt;\n\n      &lt;span className=\"...\"&gt;\n        &lt;FormattedDate value={date} dateStyle=\"long\" \/<\/span>&gt;\n      &lt;<span class=\"hljs-regexp\">\/span&gt;\n\n      &lt;span className=\"...\"&gt;\n        {intl.formatDate(date, {\n          year: \"2-digit\",\n          month: \"short\",\n          day: \"2-digit\",\n        })}\n      &lt;\/<\/span>span&gt;\n    &lt;<span class=\"hljs-regexp\">\/&gt;\n  );\n}<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-44\"><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_379f58590a5bf79cdb01d009489f9ce0\" 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_72364\" aria-describedby=\"caption-attachment-72364\" style=\"width: 716px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-72364\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/en-dates.png\" alt=\"\" width=\"716\" height=\"296\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/en-dates.png 716w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/en-dates-300x124.png 300w\" sizes=\"(max-width: 716px) 100vw, 716px\" \/><figcaption id=\"caption-attachment-72364\" class=\"wp-caption-text\">Our various date formats rendered for the <code>en-US<\/code> locale<\/figcaption><\/figure>\n<figure id=\"attachment_72370\" aria-describedby=\"caption-attachment-72370\" style=\"width: 708px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-72370 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/ar-dates.png\" alt=\"\" width=\"708\" height=\"258\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/ar-dates.png 708w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/ar-dates-300x109.png 300w\" sizes=\"(max-width: 708px) 100vw, 708px\" \/><figcaption id=\"caption-attachment-72370\" class=\"wp-caption-text\">Our various date formats rendered for the <code>ar-EG<\/code> locale<\/figcaption><\/figure>\n<p>\u270b\u00a0Safari on macOS will throw a hydration error in its developer console for the above long date in Arabic since it adds that comma in the date format on the client\u2014and the Node.js server <em>doesn\u2019t.<\/em> We could solve this by formatting dates on the server using <code>getStaticProps()<\/code> and passing them to pages and components as pre-formatted strings. Alternatively, we could use a library like <a href=\"https:\/\/date-fns.org\/\">date-fns<\/a>.<\/p>\n<p>\ud83d\udd17\u00a0Grab the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-react-intl-2023\/blob\/main\/pages-router\/complete\/components\/Dates.tsx\">full code for the above Date component from our GitHub repo<\/a>.<\/p>\n<h3>Number formatting<\/h3>\n<p>Again, Format.JS uses the standard <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/NumberFormat\">Intl.NumberFormat<\/a> underneath the hood.<\/p>\n<p><!-- notionvc: fa8b379b-9e8f-416b-b363-72aa87975bb6 --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-45\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ components\/Numbers.tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { FormattedNumber, useIntl } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react-intl\"<\/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\">Numbers<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> intl = useIntl();\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;&gt;\n      &lt;span className=<span class=\"hljs-string\">\"...\"<\/span>&gt;\n        &lt;FormattedNumber value={<span class=\"hljs-number\">1234.56<\/span>} \/&gt;\n      &lt;<span class=\"hljs-regexp\">\/span&gt;\n      \n      &lt;span className=\"...\"&gt;\n        {intl.formatNumber(1234.56, {\n          style: \"currency\",\n          currency: \"EUR\",\n        })}\n      &lt;\/<\/span>span&gt;\n      \n      &lt;span className=<span class=\"hljs-string\">\"...\"<\/span>&gt;\n        {intl.formatNumber(<span class=\"hljs-number\">0.98<\/span>, { style: <span class=\"hljs-string\">\"percent\"<\/span> })}\n      &lt;<span class=\"hljs-regexp\">\/span&gt;\n    &lt;\/<\/span>&gt;\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-45\"><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_4d4e6175e60b71bc55092cc8301cf88a\" 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_72376\" aria-describedby=\"caption-attachment-72376\" style=\"width: 734px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-72376\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/ar-numbers.png\" alt=\"\" width=\"734\" height=\"236\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/ar-numbers.png 734w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/ar-numbers-300x96.png 300w\" sizes=\"(max-width: 734px) 100vw, 734px\" \/><figcaption id=\"caption-attachment-72376\" class=\"wp-caption-text\">Our number formats rendered in the <code>en-US<\/code> locale<\/figcaption><\/figure>\n<figure id=\"attachment_72382\" aria-describedby=\"caption-attachment-72382\" style=\"width: 718px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-72382\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/en-numbers.png\" alt=\"\" width=\"718\" height=\"234\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/en-numbers.png 718w, https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/en-numbers-300x98.png 300w\" sizes=\"(max-width: 718px) 100vw, 718px\" \/><figcaption id=\"caption-attachment-72382\" class=\"wp-caption-text\">Our number formats rendered in the <code>ar-EG<\/code> locale<\/figcaption><\/figure>\n<p>\ud83d\udd17\u00a0Get the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-react-intl-2023\/blob\/main\/pages-router\/complete\/components\/Numbers.tsx\">complete code of the above Number component from GitHub<\/a>.<\/p>\n<p>\ud83e\udd3f\u00a0This article is getting a bit long, so we had to go through view localization very quickly. We invite you to look more deeply into these topics in <a href=\"https:\/\/phrase.com\/blog\/posts\/react-i18n-format-js\/\">A Guide to Localizing React Apps with react-intl\/FormatJS<\/a>.<\/p>\n<h2>How do I work with right-to-left languages?<\/h2>\n<p>The simplest solution to setting the document direction in the browser is through a <code>useEffect<\/code> in the <code>App<\/code> component.<\/p>\n<p><!-- notionvc: 03a9e492-f204-48c1-b46a-fd2449d1da6b --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-46\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ pages\/_app.tsx\n\n  import \"@\/styles\/globals.css\";\n  import type { AppProps } from \"next\/app\";\n  import { useRouter } from \"next\/router\";\n<span class=\"hljs-addition\">+ import { useEffect } from \"react\";<\/span>\n  import { IntlProvider } from \"react-intl\";\n\n  export default function App({\n    Component,\n    pageProps,\n  }: AppProps) {\n    const { locale, defaultLocale } = useRouter();\n\n<span class=\"hljs-addition\">+    useEffect(() =&gt; {<\/span>\n<span class=\"hljs-addition\">+      \/\/ We set the `lang` attribute while<\/span>\n<span class=\"hljs-addition\">+      \/\/ we're at it.<\/span>\n<span class=\"hljs-addition\">+      document.documentElement.lang = locale!;<\/span>\n<span class=\"hljs-addition\">+<\/span>\n<span class=\"hljs-addition\">+      document.documentElement.dir =<\/span>\n<span class=\"hljs-addition\">+        locale === \"ar-EG\" ? \"rtl\" : \"ltr\";<\/span>\n<span class=\"hljs-addition\">+    }, &#91;locale]);<\/span>\n\n     return (\n       &lt;IntlProvider\n         locale={locale!}\n         defaultLocale={defaultLocale}\n         messages={pageProps.localeMessages}\n       &gt;\n         &lt;Component {...pageProps} \/&gt;\n       &lt;\/IntlProvider&gt;\n     );\n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-46\"><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_9bea4dd6baa020788ce8d7b2e8a38369\" 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>While it may seem that calling <code>useEffect()<\/code> inside of <code>App<\/code> would make all of our pages client-side-rendered, I found that this was not the case.<\/p>\n<p>However, if you\u2019re looking for a different approach, you could use <code>getInitialProps()<\/code> in the root <code>Document<\/code> component (at <code>pages\/_document.tsx<\/code>).<\/p>\n<p>Alternatively, you could set <code>document.documentElement.dir<\/code> in the <code>LocaleSwitcher<\/code>.<\/p>\n<p>And with that, our demo app is localized!<\/p>\n<p><!-- notionvc: 121fb9e8-7300-4303-9f86-2e28ff333560 --><\/p>\n<figure id=\"attachment_72388\" aria-describedby=\"caption-attachment-72388\" style=\"width: 600px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-72388\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2023\/12\/final-app.gif\" alt=\"\" width=\"600\" height=\"422\" \/><figcaption id=\"caption-attachment-72388\" class=\"wp-caption-text\">Our demo app pages translated into English and Arabic<\/figcaption><\/figure>\n<p>\ud83d\udd17\u00a0Get the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-react-intl-2023\/tree\/main\">completed code for our demo app<\/a> from our GitHub repo.<\/p>\n<h2>Conclusion<\/h2>\n<p>There\u2019s certainly a good amount of setup to get a Next.js app localized with react-intl. Once configured, however, we get robust, efficient i18n on both server and client. With the automation options that the react-intl CLI provides, we can scale our localization with lean workflows that keep us focused on the creative code we love.<\/p>\n<p><!-- notionvc: b559d17b-59f9-493c-bd05-c138ecdaba1f --><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Explore the ins and outs of localizing Next.js Pages Router apps with react-intl\/Format.JS.<\/p>\n","protected":false},"author":41,"featured_media":72432,"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-70452","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\/70452"}],"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=70452"}],"version-history":[{"count":35,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/70452\/revisions"}],"predecessor-version":[{"id":94277,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/70452\/revisions\/94277"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media\/72432"}],"wp:attachment":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media?parent=70452"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/categories?post=70452"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}