{"id":68190,"date":"2023-10-24T08:37:10","date_gmt":"2023-10-24T06:37:10","guid":{"rendered":"https:\/\/phrase.com\/?p=68190"},"modified":"2024-04-29T22:16:36","modified_gmt":"2024-04-29T20:16:36","slug":"next-js-app-router-localization-next-intl","status":"publish","type":"post","link":"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/","title":{"rendered":"Next.js 13\/14 App Router Localization with next-intl"},"content":{"rendered":"\n<div id=\"acf\/text-block_e5d08ebe494f3f69e6fafa4c27aaf919\" 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> by Vercel has become <strong>the<\/strong> React framework. Next.js\u2019 downloads <a href=\"https:\/\/npmtrends.com\/next-vs-react-scripts\">have surpassed those of create-react-app<\/a> and the <a href=\"https:\/\/react.dev\/learn\/start-a-new-react-project#nextjs-pages-router\">official React documentation<\/a> recommends the framework for spinning up new React projects. Little wonder: Next.js solves previously headache-inducing problems like routing and server-side rendering with elegance, making modern React development fun again.<\/p>\n<p>The introduction of the App Router in Next.js 13 added the flexibility and convenience of <a href=\"https:\/\/nextjs.org\/docs\/app\/building-your-application\/rendering\/server-components\">React Server Components<\/a> (RSC)\u2014it also complicated Next.js internationalization (i18n). Thankfully, the\u00a0<a href=\"https:\/\/next-intl-docs.vercel.app\/\">next-intl<\/a>\u00a0library by <a href=\"https:\/\/github.com\/amannn\">Jan Amann<\/a> simplifies Next.js 13\/14 App Router i18n, offering robust i18n solutions, including translation management, localized formatting, and excellent support for server components.<\/p>\n<p>\ud83d\udd17 <strong>Learn more \u00bb<\/strong> Internationalization (i18n) and localization (l10n) allow us to make our apps available in different languages and regions, often for more profit. If you\u2019re new to i18n and l10n, check out our guide to\u00a0<a href=\"https:\/\/phrase.com\/blog\/posts\/i18n-a-simple-definition\/\">internationalization<\/a>.<\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong> For localizing a Next.js app using the Pages Router and the i18next library, refer to our step-by-step guide to\u00a0<a href=\"https:\/\/phrase.com\/blog\/posts\/nextjs-i18n\/\">Next.js internationalization<\/a>.<\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong> If you want to use the Format.js\/react-intl library with the Pages Router instead, check out <a href=\"https:\/\/phrase.com\/blog\/posts\/next-js-l10n-format-js-react-intl\/\">Next.js Localization with Format.JS\/react-intl<\/a>.<\/p>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_69_1 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Overview<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#our-demo-app\" title=\"Our demo app\">Our demo app<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#package-versions-used\" title=\"Package versions used\">Package versions used<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#building-the-demo\" title=\"Building the demo\">Building the demo<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-localize-my-nextjs-app-with-next-intl\" title=\"How do I localize my Next.js app with next-intl?\">How do I localize my Next.js app with next-intl?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-install-and-configure-next-intl\" title=\"How do I install and configure next-intl?\">How do I install and configure next-intl?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-translate-basic-text\" title=\"How do I translate basic text?\">How do I translate basic text?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-configure-localized-routing\" title=\"How do I configure localized routing?\">How do I configure localized routing?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-use-localized-links\" title=\"How do I use localized links?\">How do I use localized links?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-build-a-language-switcher\" title=\"How do I build a language switcher?\">How do I build a language switcher?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-automatically-detect-the-users-locale\" title=\"How do I automatically detect the user\u2019s locale?\">How do I automatically detect the user\u2019s locale?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-localize-client-components\" title=\"How do I localize Client Components?\">How do I localize Client Components?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-localize-async-components\" title=\"How do I localize async components?\">How do I localize async components?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-ensure-static-rendering\" title=\"How do I ensure static rendering?\">How do I ensure static rendering?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-work-with-right-to-left-languages\" title=\"How do I work with right-to-left languages?\">How do I work with right-to-left languages?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-work-with-dynamic-values-in-translation-messages\" title=\"How do I work with dynamic values in translation messages?\">How do I work with dynamic values in translation messages?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-work-with-localized-plurals\" title=\"How do I work with localized plurals?\">How do I work with localized plurals?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-localize-numbers\" title=\"How do I localize numbers?\">How do I localize numbers?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-localize-dates-and-times\" title=\"How do I localize dates and times?\">How do I localize dates and times?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-19\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-include-html-in-my-translation-messages\" title=\"How do I include HTML in my translation messages?\">How do I include HTML in my translation messages?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-20\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-localize-page-metadata\" title=\"How do I localize page metadata?\">How do I localize page metadata?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-21\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#how-do-i-make-my-message-keys-type-safe\" title=\"How do I make my message keys type-safe?\">How do I make my message keys type-safe?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-22\" href=\"https:\/\/phrase.com\/blog\/posts\/next-js-app-router-localization-next-intl\/#push-the-boundaries-of-nextjs-localization\" title=\"Push the boundaries of Next.js localization\">Push the boundaries of Next.js localization<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"our-demo-app\"><\/span>Our demo app<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We\u2019ll build a minimalistic mock weather app to help us work through our i18n problems, creatively named <strong>Next.js Weather<\/strong><strong>.<\/strong><\/p>\n<figure id=\"attachment_82308\" aria-describedby=\"caption-attachment-82308\" style=\"width: 1024px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-large wp-image-82308\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/home-before-i18n-1024x516.png\" alt=\"Our home page shown before localization, titled 'Next.js Weather'. The application displays today's weather as 'Sunny 22\u00b0C' for 'Monday April 15 2024'. The interface has a dark background with a sun icon and large white text presenting the temperature. At the top of the interface are navigation tabs labeled 'This week' and 'About'. The browser window shows 'localhost:3000' in the address bar.\" width=\"1024\" height=\"516\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/home-before-i18n-1024x516.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/home-before-i18n-300x151.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/home-before-i18n-768x387.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/home-before-i18n.png 1148w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption id=\"caption-attachment-82308\" class=\"wp-caption-text\">Our home page before localization.<\/figcaption><\/figure>\n<p>Let\u2019s build this app and localize it step-by-step.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"package-versions-used\"><\/span>Package versions used<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We use the following NPM packages in this guide.<\/p>\n<table style=\"width: 100%;\">\n<thead>\n<tr>\n<td style=\"width: 33.3%;\">Library<\/td>\n<td>Version used<\/td>\n<td>Description<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"width: 33.3%;\">typescript<\/td>\n<td>5.4<\/td>\n<td>Our programming language of choice.<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.3%;\">next<\/td>\n<td>14.2<\/td>\n<td>The full-stack React framework.<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.3%;\">react<\/td>\n<td>18.2<\/td>\n<td>A somewhat popular UI library.<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.3%;\">next-intl<\/td>\n<td>3.11<\/td>\n<td>Our i18n library.<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.3%;\">rtl-detect<\/td>\n<td>1.1<\/td>\n<td>For detecting right-to-left languages.<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.3%;\">tailwindcss<\/td>\n<td>3.4<\/td>\n<td>For styling; largely optional for our purposes.<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 33.3%;\">clsx<\/td>\n<td>2.1<\/td>\n<td>For dynamic CSS class assignment; largely optional for our purposes.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong>\u00a0<a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-intl-demo\/tree\/start?tab=readme-ov-file\">Get the complete code for our demo from GitHub<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"building-the-demo\"><\/span>Building the demo<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>With that out of the way, let\u2019s get building. We\u2019ll spin up a fresh Next.js app from the command line:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npx create-next-app@latest\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_0dbd6830099c5d8a9ed2c9b4f1630e4d\" 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 usual setup Q&#038;A follows. Here\u2019s what I entered:<\/p>\n<ul>\n<li>Would you like to use TypeScript? <strong>Yes<\/strong><\/li>\n<li>Would you like to use ESLint? <strong>Yes<\/strong><\/li>\n<li>Would you like to use Tailwind CSS? <strong>Yes<\/strong><\/li>\n<li>Would you like to use <code>src\/<\/code> directory? <strong>No<\/strong><\/li>\n<li>Would you like to use App Router? (recommended) <strong>Yes<\/strong><\/li>\n<li>Would you like to customize the default import alias (@\/*)? <strong>No<\/strong><\/li>\n<\/ul>\n<p>After our new app has spun up, let\u2019s override the default Next.js boilerplate:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ app\/layout.tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> Header <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/app\/_components\/Header\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> type { Metadata } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> <span class=\"hljs-string\">\".\/globals.css\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> metadata: Metadata = {\n  <span class=\"hljs-attr\">title<\/span>: <span class=\"hljs-string\">\"Next.js Weather\"<\/span>,\n  <span class=\"hljs-attr\">description<\/span>:\n    <span class=\"hljs-string\">\"A weather app built with Next.js and next-intl\"<\/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\">RootLayout<\/span>(<span class=\"hljs-params\">{\n  children,\n}: Readonly&lt;{\n  children: React.ReactNode;\n}&gt;<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">html<\/span> <span class=\"hljs-attr\">lang<\/span>=<span class=\"hljs-string\">\"en\"<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">body<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Header<\/span> \/&gt;<\/span>\n        {children}\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">body<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">html<\/span>&gt;<\/span><\/span>\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_31a3980131f0b125d6b0dea02e7d9ed1\" 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 <strong>Note \u00bb<\/strong> We\u2019re omitting styles here for brevity (unless they directly pertain to i18n). You can <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-intl-demo\/tree\/start?tab=readme-ov-file\">get complete code listings from our GitHub repo<\/a> (including styles).<\/p>\n<p>\u00a0The simple <code>Header<\/code> in our <code>RootLayout<\/code> is a navbar that tops all our pages. <\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ app\/_components\/Header.tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> Link <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next\/link\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">Header<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">header<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">nav<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">ul<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Link<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\"\/\"<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n              Next.js Weather\n            <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Link<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Link<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\"\/week\"<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n              This week\n            <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Link<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Link<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\"\/about\"<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n              About\n            <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Link<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">ul<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">nav<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">header<\/span>&gt;<\/span><\/span>\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_8e93bea2c4e3111fe49d1168db1b6d70\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82323 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/header-before-i18n.png\" alt=\"Navigation header with links titled 'Next.js Weather', 'This week', and 'About', on a dark blue background.\" width=\"946\" height=\"94\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/header-before-i18n.png 946w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/header-before-i18n-300x30.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/header-before-i18n-768x76.png 768w\" sizes=\"(max-width: 946px) 100vw, 946px\" \/><\/p>\n<p>Our home page lists some mock hard-coded data for \u201ctoday\u2019s weather\u201d.<\/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=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ app\/page.tsx<\/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\">Home<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">main<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h1<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n        Today<span class=\"hljs-symbol\">&amp;apos;<\/span>s weather\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h2<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>Monday April 15 2024<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h2<\/span>&gt;<\/span>\n\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">section<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\u2600\ufe0f<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>Sunny<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>22\u00b0C<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">section<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">main<\/span>&gt;<\/span><\/span>\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_3b4ad7c2580eefbc5d3a9c72e489e22b\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82329 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/home-zoom-before-i18n.png\" alt=\"A screenshot of our home page showing sunny weather with a sun icon and a temperature of 22\u00b0C for Monday, April 15, 2024, on a dark background.\" width=\"880\" height=\"354\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/home-zoom-before-i18n.png 880w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/home-zoom-before-i18n-300x121.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/home-zoom-before-i18n-768x309.png 768w\" sizes=\"(max-width: 880px) 100vw, 880px\" \/><\/p>\n<h3>Weekly weather<\/h3>\n<p>To simulate an async server component, let\u2019s build a page at the <code>\/week<\/code> route that loads some mock JSON data: daily weather forecasts for the week. Here\u2019s what our JSON looks like:<\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ app\/_data\/week.json<\/span>\n\n{\n  <span class=\"hljs-attr\">\"weeklyWeather\"<\/span>: &#91;\n    {\n      <span class=\"hljs-attr\">\"dateTime\"<\/span>: <span class=\"hljs-string\">\"2024-04-15T12:00:00Z\"<\/span>,\n      <span class=\"hljs-attr\">\"condition\"<\/span>: <span class=\"hljs-string\">\"sunny\"<\/span>,\n      <span class=\"hljs-attr\">\"conditionIcon\"<\/span>: <span class=\"hljs-string\">\"\u2600\ufe0f\"<\/span>,\n      <span class=\"hljs-attr\">\"temperature\"<\/span>: {\n        <span class=\"hljs-attr\">\"celsius\"<\/span>: <span class=\"hljs-number\">22<\/span>,\n        <span class=\"hljs-attr\">\"fahrenheit\"<\/span>: <span class=\"hljs-number\">71.6<\/span>\n      }\n    },\n    {\n      <span class=\"hljs-attr\">\"dateTime\"<\/span>: <span class=\"hljs-string\">\"2024-04-16T12:00:00Z\"<\/span>,\n      <span class=\"hljs-attr\">\"condition\"<\/span>: <span class=\"hljs-string\">\"partlyCloudy\"<\/span>,\n      <span class=\"hljs-attr\">\"conditionIcon\"<\/span>: <span class=\"hljs-string\">\"\u26c5\ufe0f\"<\/span>,\n      <span class=\"hljs-attr\">\"temperature\"<\/span>: {\n        <span class=\"hljs-attr\">\"celsius\"<\/span>: <span class=\"hljs-number\">20<\/span>,\n        <span class=\"hljs-attr\">\"fahrenheit\"<\/span>: <span class=\"hljs-number\">68<\/span>\n      }\n    },\n    <span class=\"hljs-comment\">\/\/ ...<\/span>\n  ]\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\">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_353f3e555c1fe24f488f3d08ae03db49\" 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 <strong>Resource<\/strong> <strong>\u00bb<\/strong> <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-intl-demo\/blob\/start\/app\/_data\/week.json\">Get the entire JSON file<\/a> from GitHub. <strong><\/strong>\u00a0<\/p>\n<p>Of course, we\u2019ll need to declare some TypeScript types for it:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ types.ts<\/span>\n\nexport <span class=\"hljs-class\"><span class=\"hljs-keyword\">interface<\/span> <span class=\"hljs-title\">WeeklyWeatherRoot<\/span> <\/span>{\n  weeklyWeather: WeeklyWeather&#91;];\n}\n\nexport <span class=\"hljs-class\"><span class=\"hljs-keyword\">interface<\/span> <span class=\"hljs-title\">WeeklyWeather<\/span> <\/span>{\n  dateTime: string;\n  condition: string;\n  conditionIcon: string;\n  temperature: Temperature;\n}\n\nexport <span class=\"hljs-class\"><span class=\"hljs-keyword\">interface<\/span> <span class=\"hljs-title\">Temperature<\/span> <\/span>{\n  celsius: number;\n  fahrenheit: number;\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\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_0deed0116534add2a98f139a43a251e9\" 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 create a page component that pulls and displays this data.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ app\/week\/page.tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> type { WeeklyWeatherRoot } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/types\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { promises <span class=\"hljs-keyword\">as<\/span> fs } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"fs\"<\/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\">Week<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> fileContents = <span class=\"hljs-keyword\">await<\/span> fs.readFile(\n    <span class=\"hljs-string\">`<span class=\"hljs-subst\">${process.cwd()}<\/span>\/app\/_data\/week.json`<\/span>,\n    <span class=\"hljs-string\">\"utf-8\"<\/span>,\n  );\n  <span class=\"hljs-keyword\">const<\/span> { weeklyWeather } = <span class=\"hljs-built_in\">JSON<\/span>.parse(\n    fileContents,\n  ) <span class=\"hljs-keyword\">as<\/span> WeeklyWeatherRoot;\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">main<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h1<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n        This week<span class=\"hljs-symbol\">&amp;apos;<\/span>s weather\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n        {weeklyWeather.map((day) =&gt; (\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">section<\/span> <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{day.dateTime}<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h2<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n              {new Date(day.dateTime).toString()}\n            <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h2<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n              <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n                <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n                  {day.conditionIcon}\n                <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n                <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n                  {day.condition}\n                <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n                <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n                  {day.temperature.celsius}\u00b0C\n                <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n              <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">section<\/span>&gt;<\/span>\n        ))}\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">main<\/span>&gt;<\/span><\/span>\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_2ae1f35470800e4e49f0b5a84e629265\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><img loading=\"lazy\" decoding=\"async\" class=\"size-large wp-image-82335 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/week-before-i18n-1024x1020.png\" alt=\"Weekly weather forecast on our site, showing sunny 22\u00b0C on Monday, partly cloudy 20\u00b0C on Tuesday, cloudy 18\u00b0C on Wednesday, and rainy 16\u00b0C on Thursday. Each entry includes the date and time with a GMT+0100 timezone. The site is accessed via a local server at 'localhost:3000\/week' (shown in browser address bar).\" width=\"1024\" height=\"1020\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/week-before-i18n-1024x1020.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/week-before-i18n-300x300.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/week-before-i18n-150x150.png 150w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/week-before-i18n-768x765.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/week-before-i18n.png 1148w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p>The formatting could be better here; we\u2019ll improve it as we localize the page. Speaking of which, let\u2019s get to localization!<\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong> <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-intl-demo\/tree\/start?tab=readme-ov-file\">Get the code for our entire starter app<\/a> (before localization) from GitHub.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-my-nextjs-app-with-next-intl\"><\/span>How do I localize my Next.js app with next-intl?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Let\u2019s look at the recipe for localizing a Next.js app with next-intl:<\/p>\n<p>1. Install and set up next-intl.<br \/>\n2. Use\u00a0<code>t()<\/code>\u00a0for component string translations.<br \/>\n3. Configure localized routing.<br \/>\n4. Build a language switcher.<br \/>\n5. Localize Client Components.<br \/>\n6. Localize Async components.<br \/>\n7. Enable static rendering.<br \/>\n8. Apply localized formatters for dates\/numbers.<br \/>\n9. Localize page metadata.<\/p>\n<p>We\u2019ll work through these steps one at a time.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-install-and-configure-next-intl\"><\/span>How do I install and configure next-intl?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>First things first, let\u2019s install the next-intl NPM package from the command line:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm install next-intl\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><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_147562d40212aa18339e6cfc5260c12e\" 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>Adding the locale route segment<\/h3>\n<p>Next, we\u2019ll move all our pages under a <code>[locale]<\/code> <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/routing\/dynamic-routes\">dynamic route segment<\/a>. This will allow us to turn a given <code>\/foo<\/code> route to localized routes like <code>\/en-us\/foo<\/code> (English USA) and <code>\/ar-eg\/foo<\/code> (Arabic Egypt).<\/p>\n<p>\ud83d\uddd2\ufe0f <strong>Note \u00bb<\/strong> A locale indicates a language and a region, represented by a code like\u00a0<code>fr-ca<\/code>\u00a0for French as used in Canada. While \u201clanguage\u201d and \u201clocale\u201d are sometimes used interchangeably, it\u2019s important to know the difference. Here\u2019s a handy\u00a0<a href=\"https:\/\/saimana.com\/list-of-country-locale-code\/\">list of locale codes<\/a>.<\/p>\n<p>This is the relevant project hierarchy <strong>before<\/strong> the <code>[locale]<\/code> segment:<\/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=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">.\n\u2514\u2500\u2500 app\n    \u251c\u2500\u2500 about\n    \u2502   \u2514\u2500\u2500 page.tsx\n    \u251c\u2500\u2500 layout.tsx\n    \u251c\u2500\u2500 page.tsx\n    \u2514\u2500\u2500 week\n        \u2514\u2500\u2500 page.tsx\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><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_2f0593081a69c0b66fb11a83528d3f4e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Let\u2019s create a new subdirectory called <code>[locale]<\/code> under <code>app<\/code> (or <code>src\/app<\/code> if you\u2019re using a <code>src<\/code> directory) and place our routed pages there. Here is our hierarchy <strong>after<\/strong> the <code>[locale]<\/code> route segment:<\/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=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">.\n\u2514\u2500\u2500 app\n    \u2514\u2500\u2500 &#91;locale]\n        \u251c\u2500\u2500 about\n        \u2502   \u2514\u2500\u2500 page.tsx\n        \u251c\u2500\u2500 layout.tsx\n        \u251c\u2500\u2500 page.tsx\n        \u2514\u2500\u2500 week\n            \u2514\u2500\u2500 page.tsx\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">plaintext<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">plaintext<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_09990058e6f84e0ac2d1111e58c3d11b\" 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 <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0Be sure to update your module <code>import<\/code>s if need be.<\/p>\n<h3>Adding translation message files<\/h3>\n<p>We\u2019ll need to pull our hard-coded strings out of our components and into translation files, one for each locale we want to support. Let\u2019s add a new directory called <code>locales<\/code> under our project root with two new JSON files under it:<\/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=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">.\n\u2514\u2500\u2500 locales\n    \u251c\u2500\u2500 ar-eg.json\n    \u2514\u2500\u2500 en-us.json\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><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_170c775db24e892e9e034c98687fc47d\" 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 <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0In this tutorial, we\u2019re supporting <code>en-us<\/code> (English USA) and <code>ar-eg<\/code> (Arabic Egypt). Feel free to add any locales you want here.<\/p>\n<p>Let\u2019s start small and translate our app\u2019s title in the <code>Header<\/code> component. We need to add a key\/value pair for each translation message, making sure we use the same keys across locale files:<\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ locales\/en-us.json<\/span>\n\n{\n  <span class=\"hljs-attr\">\"Header\"<\/span>: {\n    <span class=\"hljs-attr\">\"appTitle\"<\/span>: <span class=\"hljs-string\">\"Next.js Weather\"<\/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\">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_f81f4a8284a2a473aff047f7aa82792e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ locales\/ar-eg.json<\/span>\n\n{\n  <span class=\"hljs-attr\">\"Header\"<\/span>: {\n    <span class=\"hljs-attr\">\"appTitle\"<\/span>: <span class=\"hljs-string\">\"\u062a\u0642\u0635 \u0646\u0643\u0633\u062a \u0686\u0649 \u0625\u0633\"<\/span>,\n  }\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\">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_14edb44b6983968728ddfcd86a9e5715\" 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 <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0We\u2019ve namespaced our <code>appTitle<\/code> translation under <code>Header<\/code> to associate it with the Header component. This message structure is recommended by next-intl but is not necessary. Read the <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/usage\/messages#structuring-messages\">Structuring messages<\/a> section of the next-intl documentation to learn more.<\/p>\n<p>We&#8217;re on the right track, but there&#8217;s still a bit to do before we can use our new translations. First up, we need to integrate next-intl with our Next.js app.<\/p>\n<h3>Setting up configuration files<\/h3>\n<p>We need to create a few small setup files to get next-intl working smoothly. This includes adding a plugin to our <code>next.config.mjs<\/code> file.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ next.config.mjs\n\n<span class=\"hljs-addition\">+ import createNextIntlPlugin from \"next-intl\/plugin\";<\/span>\n<span class=\"hljs-addition\">+ const withNextIntl = createNextIntlPlugin();<\/span>\n\n  \/** @type {import('next').NextConfig} *\/\n  const nextConfig = {};\n\n<span class=\"hljs-deletion\">- export default nextConfig;<\/span>\n<span class=\"hljs-addition\">+ export default withNextIntl(nextConfig);<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">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_8f30721ad1552fd772c271e3c531a19b\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>We\u2019ll often refer to our app\u2019s supported locales, so it\u2019s wise to configure them in a single, central file. Let\u2019s create a new file called <code>i18n.config.ts<\/code> at the root of our project to house our config.<\/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=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ i18n.config.ts<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> locales = &#91;<span class=\"hljs-string\">\"en-us\"<\/span>, <span class=\"hljs-string\">\"ar-eg\"<\/span>] <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-keyword\">const<\/span>;\n<span class=\"hljs-keyword\">export<\/span> type Locale = (<span class=\"hljs-keyword\">typeof<\/span> locales)&#91;number];\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><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_51f18a9e62e6d48e7aab5268fdc6a6a6\" 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-intl uses a special <code>i18n.ts<\/code> configuration file to load translations\u2014not to be confused with the previous <code>i18n.config.ts<\/code>. By default, this file <strong>must<\/strong> be in the project route and called <code>i18n.ts<\/code>, although <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/getting-started\/app-router#i18nts\">this is configurable<\/a>.<\/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=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ i18n.ts<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { getRequestConfig } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next-intl\/server\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { notFound } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next\/navigation\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { locales, type Locale } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/i18n.config\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Load the translation file for the active locale<\/span>\n<span class=\"hljs-comment\">\/\/ on each request and make it available to our<\/span>\n<span class=\"hljs-comment\">\/\/ pages, components, etc.<\/span>\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> getRequestConfig(<span class=\"hljs-keyword\">async<\/span> ({ locale }) =&gt; {\n  <span class=\"hljs-keyword\">if<\/span> (!locales.includes(locale <span class=\"hljs-keyword\">as<\/span> Locale)) {\n    <span class=\"hljs-keyword\">return<\/span> notFound();\n  }\n\n  <span class=\"hljs-keyword\">return<\/span> {\n    <span class=\"hljs-attr\">messages<\/span>: (<span class=\"hljs-keyword\">await<\/span> <span class=\"hljs-keyword\">import<\/span>(<span class=\"hljs-string\">`.\/locales\/<span class=\"hljs-subst\">${locale}<\/span>.json`<\/span>))\n      .default,\n  };\n});\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">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_c5a8f9d8fb7324d7f29e99387b61ec50\" 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>One last boilerplate is wiring up a Next.js <a href=\"https:\/\/nextjs.org\/docs\/app\/building-your-application\/routing\/middleware\">middleware<\/a> that next-intl provides. Let\u2019s create a <code>middleware.ts<\/code> file at the root of our project and set it up with some starter config.<\/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=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ middleware.ts<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> createMiddleware <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next-intl\/middleware\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { locales } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/i18n.config\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> createMiddleware({\n  <span class=\"hljs-comment\">\/\/ Use this locale when we can't match<\/span>\n  <span class=\"hljs-comment\">\/\/ another with our user's preferred locales<\/span>\n  <span class=\"hljs-comment\">\/\/ and when no locale is explicitly set.<\/span>\n  <span class=\"hljs-attr\">defaultLocale<\/span>: <span class=\"hljs-string\">\"en-us\"<\/span>,\n\n  <span class=\"hljs-comment\">\/\/ List all supported locales (en-us, ar-eg).<\/span>\n  locales,\n\n  <span class=\"hljs-comment\">\/\/ Automatic locale detection is enabled by<\/span>\n  <span class=\"hljs-comment\">\/\/ default. We're disabling it to keep things<\/span>\n  <span class=\"hljs-comment\">\/\/ simple for now. We'll enable it later when<\/span>\n  <span class=\"hljs-comment\">\/\/ we cover locale detection.<\/span>\n  <span class=\"hljs-attr\">localeDetection<\/span>: <span class=\"hljs-literal\">false<\/span>,\n});\n\n<span class=\"hljs-comment\">\/\/ Our middleware only applies to routes that<\/span>\n<span class=\"hljs-comment\">\/\/ match the following:<\/span>\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> config = {\n  <span class=\"hljs-attr\">matcher<\/span>: &#91;\n    <span class=\"hljs-comment\">\/\/ Match all pathnames except for<\/span>\n    <span class=\"hljs-comment\">\/\/ - \u2026 if they start with `\/api`, `\/_next` or `\/_vercel`<\/span>\n    <span class=\"hljs-comment\">\/\/ - \u2026 the ones containing a dot (e.g. `favicon.ico`)<\/span>\n    <span class=\"hljs-string\">\"\/((?!api|_next|_vercel|.*\\\\..*).*)\"<\/span>,\n  ],\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\">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_7f111292ecbfae70f7c6c5a84c778a24\" 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 Note \u00bb I used a different middleware route matcher than the <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/routing\/middleware#matcher-config\">next-intl recommended<\/a> <code>\"\/(en-us|ar-eg)\/:path*\"<\/code>. This approach avoids duplicating the locale list and works well in general. Just take care to <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/routing\/middleware#matcher-no-prefix\">add a specific matcher for any localized routes containing a dot<\/a> (<code>.<\/code>).<\/p>\n<p>\ud83d\uddd2\ufe0f <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0If you\u2019re using other middleware, check out the next-intl documentation on <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/routing\/middleware#composing-other-middlewares\">Composing other middlewares<\/a>.<\/p>\n<h3>A note on the NEXT_LOCALE cookie<\/h3>\n<p>Despite disabling locale detection, next-intl uses a <code>NEXT_LOCALE<\/code> cookie to store the current locale, ensuring users consistently see their preferred language on subsequent visits. This is useful for maintaining user preferences but can lead to confusion during development. Consider deleting the <code>NEXT_LOCALE<\/code> cookie via your browser&#8217;s dev tools when you test initial route access.<\/p>\n<h3>A basic test<\/h3>\n<p>Phew! That was a good amount of configuration. It\u2019s well worth the effort, however, considering how much custom code and headaches next-intl saves us. Let\u2019s put the library to the test and use the translation files we added above. Here they are again to refresh our memory:<\/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=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ locales\/en-us.json<\/span>\n\n{\n  <span class=\"hljs-attr\">\"Header\"<\/span>: {\n    <span class=\"hljs-attr\">\"appTitle\"<\/span>: <span class=\"hljs-string\">\"Next.js Weather\"<\/span>\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><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_f81f4a8284a2a473aff047f7aa82792e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ locales\/ar-eg.json<\/span>\n\n{\n  <span class=\"hljs-attr\">\"Header\"<\/span>: {\n    <span class=\"hljs-attr\">\"appTitle\"<\/span>: <span class=\"hljs-string\">\"\u062a\u0642\u0635 \u0646\u0643\u0633\u062a \u0686\u0649 \u0625\u0633\"<\/span>,\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">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_d418638735eb22da3a4b0cce792a6170\" 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 simplest way to add these translations to our components is via next-intl\u2019s <code>useTranslations()<\/code> hook:<\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/_components\/Header.tsx\n\n<span class=\"hljs-addition\">+ import { useTranslations } from \"next-intl\";<\/span>\n  import Link from \"next\/link\";\n\n  export default function Header() {\n<span class=\"hljs-addition\">+   \/\/ t will be scoped to \"Header\"...<\/span>\n<span class=\"hljs-addition\">+   const t = useTranslations(\"Header\");<\/span>\n\n    return (\n      &lt;header className=\"...\"&gt;\n        &lt;nav&gt;\n          &lt;ul className=\"...\"&gt;\n            &lt;li&gt;\n              &lt;Link\n                href=\"\/\"\n                className=\"...\"\n              &gt;\n<span class=\"hljs-deletion\">-               Next.js Weather<\/span>\n<span class=\"hljs-addition\">+               {\/* ...because we're scoped to \"Header\", this<\/span>\n<span class=\"hljs-addition\">+                   returns \"Header.appTitle\" from the active<\/span>\n<span class=\"hljs-addition\">+                   locale's translation file. *\/}<\/span>\n<span class=\"hljs-addition\">+               {t(\"appTitle\")}<\/span>\n             &lt;\/Link&gt;\n            &lt;\/li&gt;\n            {\/* ... *\/}\n          &lt;\/ul&gt;\n        &lt;\/nav&gt;\n      &lt;\/header&gt;\n    );\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">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_998ad0586762afdda46c053e96848d50\" 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 visit <code>\/en-us<\/code>, our app should look exactly as it did before. If we visit <code>\/ar-eg<\/code>, however, we should see our app title in Arabic.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82341 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/app-title-ar.png\" alt=\"Website header with the navigation tabs 'This week' and 'About' in English, and the site title in Arabic script \u2018\u062a\u0642\u0635 \u0646\u0643\u0633\u062a \u0686\u0649 \u0625\u0633\u2019 which translates to 'Next JS Weather' on a dark background.\" width=\"622\" height=\"218\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/app-title-ar.png 622w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/app-title-ar-300x105.png 300w\" sizes=\"(max-width: 622px) 100vw, 622px\" \/><\/p>\n<h3>Accessing the locale param<\/h3>\n<p>next-intl ensures that our routes are always localized under a <code>\/[locale]<\/code> route param. We can access this param as usual in our layouts and pages. Let\u2019s use the param to make our <code>&lt;html lang&gt;<\/code> attribute reflect the active locale.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/layout.tsx\n\n  import Header from \"@\/app\/_components\/Header\";\n  import \"@\/app\/globals.css\";\n  import type { Metadata } from \"next\";\n\n  export const metadata: Metadata = {\n    title: \"Next.js Weather\",\n    description:\n      \"A weather app built with Next.js and next-intl\",\n  };\n\n<span class=\"hljs-addition\">+ \/\/ Rename for clarity<\/span>\n<span class=\"hljs-deletion\">- export default function RootLayout({<\/span>\n<span class=\"hljs-addition\">+ export default function LocaleLayout({<\/span>\n    children,\n<span class=\"hljs-addition\">+   params: { locale },<\/span>\n  }: Readonly&lt;{\n    children: React.ReactNode;\n<span class=\"hljs-addition\">+   params: { locale: string };<\/span>\n  }&gt;) {\n    return (\n<span class=\"hljs-deletion\">-     &lt;html lang=\"en\"&gt;<\/span>\n<span class=\"hljs-addition\">+     &lt;html lang={locale}&gt;<\/span>\n        &lt;body className=\"...\"&gt;\n          &lt;Header \/&gt;\n          {children}\n        &lt;\/body&gt;\n      &lt;\/html&gt;\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\">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_8f7a1bebb5b267a648bb86e2a916483e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>When we run our app now and visit <code>\/<\/code>, we\u2019ll be redirected to <code>\/en-us<\/code>. If we open our browser dev tools, we should see the <code>&lt;html lang&gt;<\/code> element reflecting the active locale in the route.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-82347\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/html-locale-en-us.png\" alt=\"The browser dev tools inspector showing &lt;html lang=\u201den-us\u201d&gt;.\" width=\"856\" height=\"220\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/html-locale-en-us.png 856w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/html-locale-en-us-300x77.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/html-locale-en-us-768x197.png 768w\" sizes=\"(max-width: 856px) 100vw, 856px\" \/><\/p>\n<p>If we visit <code>\/ar-eg<\/code> we should see our <code>&lt;html lang&gt;<\/code> update accordingly.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82353 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/html-lang-ar-eg.png\" alt=\"The browser dev tools inspector showing &lt;html lang=\u201dar-eg\u201d&gt;.\" width=\"854\" height=\"224\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/html-lang-ar-eg.png 854w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/html-lang-ar-eg-300x79.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/html-lang-ar-eg-768x201.png 768w\" sizes=\"(max-width: 854px) 100vw, 854px\" \/><\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-translate-basic-text\"><\/span>How do I translate basic text?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We covered this when we configured and set up next-intl. Let\u2019s go over it one more time, however. Our <code>Header<\/code> component needs its navigation links localized anyway.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/en-us.json\n\n \/\/ next-intl incentivizes structuring our translation\n \/\/ keys so that they\u2019re namespaced by component or page.\n {\n   \"Header\": {\n     \"appTitle\": \"Next.js Weather\",\n<span class=\"hljs-addition\">+    \"navLinks\": {<\/span>\n<span class=\"hljs-addition\">+      \"week\": \"This week\",<\/span>\n<span class=\"hljs-addition\">+      \"about\": \"About\"<\/span>\n<span class=\"hljs-addition\">+    }<\/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\">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_f81f4a8284a2a473aff047f7aa82792e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-23\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/ar-eg.json\n\n {\n   \"Header\": {\n     \"appTitle\": \"Next.js Weather\",\n<span class=\"hljs-addition\">+    \"navLinks\": {<\/span>\n<span class=\"hljs-addition\">+      \"week\": \"\u0647\u0630\u0627 \u0627\u0644\u0623\u0633\u0628\u0648\u0639\",<\/span>\n<span class=\"hljs-addition\">+      \"about\": \"\u0646\u0628\u0630\u0629 \u0639\u0646\u0627\"<\/span>\n<span class=\"hljs-addition\">+    }<\/span>\n   }\n }\n<\/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_015715dfe0674471aea3e7c3782d1d70\" 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>useTranslations<\/code> hook allows to pull translations for the active locale using its returned <code>t()<\/code> function.<\/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\">\/\/ app\/_components\/Header.tsx\n\n<span class=\"hljs-addition\">+ \/\/ We added this before; higlighting for clarity.<\/span>\n<span class=\"hljs-addition\">+ import { useTranslations } from \"next-intl\";<\/span>\n  import Link from \"next\/link\";\n\n  export default function Header() {\n<span class=\"hljs-addition\">+   \/\/ We added this before; higlighting for clarity.<\/span>\n<span class=\"hljs-addition\">+   const t = useTranslations(\"Header\");<\/span>\n\n    return (\n      &lt;header className=\"...\"&gt;\n        &lt;nav&gt;\n          &lt;ul className=\"...\"&gt;\n            &lt;li&gt;\n              &lt;Link href=\"\/\" className=\"...\"&gt;\n                {t(\"appTitle\")}\n              &lt;\/Link&gt;\n            &lt;\/li&gt;\n            &lt;li&gt;\n              &lt;Link href=\"\/week\" className=\"...\"&gt;\n<span class=\"hljs-deletion\">-               This week<\/span>\n<span class=\"hljs-addition\">+               {\/* Note that we can refine into our<\/span>\n<span class=\"hljs-addition\">+                   translation objects with `.` *\/}<\/span>\n<span class=\"hljs-addition\">+               {t(\"navLinks.week\")}<\/span>\n              &lt;\/Link&gt;\n            &lt;\/li&gt;\n            &lt;li&gt;\n              &lt;Link href=\"\/about\" className=\"...\"&gt;\n<span class=\"hljs-deletion\">-               About<\/span>\n<span class=\"hljs-addition\">+               {t(\"navLinks.about\")}<\/span>\n              &lt;\/Link&gt;\n            &lt;\/li&gt;\n          &lt;\/ul&gt;\n        &lt;\/nav&gt;\n      &lt;\/header&gt;\n    );\n  }\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-24\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_be7386d30a2218fc163031dfe244cb45\" 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>There, now the navbar in our header is completely localized.<\/p>\n<figure id=\"attachment_82359\" aria-describedby=\"caption-attachment-82359\" style=\"width: 924px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82359\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/header-en.png\" alt=\"Our webpage header with navigation links reading 'Next.js Weather', 'This week', and 'About' on a dark blue background.\" width=\"924\" height=\"92\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/header-en.png 924w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/header-en-300x30.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/header-en-768x76.png 768w\" sizes=\"(max-width: 924px) 100vw, 924px\" \/><figcaption id=\"caption-attachment-82359\" class=\"wp-caption-text\">When we visit \/en-us we see the nav links in English.<\/figcaption><\/figure>\n<figure id=\"attachment_82365\" aria-describedby=\"caption-attachment-82365\" style=\"width: 940px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82365\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/header-ar.png\" alt=\"A simple webpage header with navigation links reading (in Arabic) 'Next.js Weather', 'This week', and 'About' on a dark blue background.\" width=\"940\" height=\"94\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/header-ar.png 940w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/header-ar-300x30.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/header-ar-768x77.png 768w\" sizes=\"(max-width: 940px) 100vw, 940px\" \/><figcaption id=\"caption-attachment-82365\" class=\"wp-caption-text\">When we visit \/ar-eg we see the nav links in Arabic.<\/figcaption><\/figure>\n<p>\ud83d\uddd2\ufe0f <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0We can access <strong>all<\/strong> translations in a file by calling <code>useTranslations()<\/code> with no params. If we did so in the above example, we would have to prefix the keys given <code>t()<\/code> with <code>\"Header.\"<\/code> e.g. <code>t(\"Header.navLinks.week\")<\/code>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-configure-localized-routing\"><\/span>How do I configure localized routing?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>next-intl\u2019s middleware defaults to managing localized routing with a <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/routing\/middleware#prefix-based-routing\">prefixed-based strategy<\/a>, where routes are prefixed with the active locale, like <code>\/en-us\/about<\/code>.<\/p>\n<p>\ud83d\uddd2\ufe0f <strong>Note<\/strong> <strong>\u00bb<\/strong> next-intl provides a <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/routing\/middleware#domain-based-routing\">domain-based routing<\/a> strategy that is beyond the scope of this article.<\/p>\n<h3>Configuring the locale prefix<\/h3>\n<p>Our default setting uses a forced locale prefix, meaning routes like <code>\/about<\/code> automatically redirect to <code>\/en-us\/about<\/code>. This makes localized routing consistent, but we can adjust it. By changing the setting to <code>as-needed<\/code>, the default locale won\u2019t have a prefix, but all others will.<\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ middleware.ts\n\nimport createMiddleware from \"next-intl\/middleware\";\nimport { locales } from \".\/i18n.config\";\n\nexport default createMiddleware({\n  defaultLocale: \"en-us\",\n  locales,\n  localeDetection: false,\n<span class=\"hljs-addition\">+ localePrefix: \"as-needed\",<\/span>\n});\n\nexport const config = {\n  matcher: &#91;\n    \"\/((?!api|_next|_vercel|.*\\\\..*).*)\",\n  ],\n};\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_d670442a0e6fdd92facacdbd8fd07152\" 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 the <code>as-needed<\/code> setting, visiting <code>\/<\/code> or <code>\/week<\/code> directly shows content in the default English locale without the <code>\/en-us<\/code> prefix. To access content in Arabic, such as the weekly weather, we must explicitly use the <code>ar-eg<\/code> prefix e.g. <code>\/ar-eg\/week<\/code>.<\/p>\n<p>For this tutorial, we\u2019ll revert the prefix setting to the forced <code>&quot;always&quot;<\/code> mode:<\/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\">\/\/ middleware.ts\n\nimport createMiddleware from \"next-intl\/middleware\";\nimport { locales } from \".\/i18n.config\";\n\nexport default createMiddleware({\n  defaultLocale: \"en-us\",\n  locales,\n  localeDetection: false,\n<span class=\"hljs-deletion\">- localePrefix: \"as-needed\",<\/span>\n<span class=\"hljs-addition\">+ \/\/ `\"always\"` is the default; we can<\/span>\n<span class=\"hljs-addition\">+ \/\/ omit the `localePrefix` option<\/span>\n<span class=\"hljs-addition\">+ \/\/ entirely and get the same result.<\/span>\n<span class=\"hljs-addition\">+ localePrefix: \"always\"<\/span>\n});\n\nexport const config = {\n  matcher: &#91;\n    \"\/((?!api|_next|_vercel|.*\\\\..*).*)\",\n  ],\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_c41c5b287649d8f0365d1e2d2ac4501e\" 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 <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0We can also set <code>localePrefix: \"never\"<\/code> which disables prefixes entirely. This option relies on a cookie for determining the locale. See the <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/routing\/middleware#locale-prefix-never\">Never using a prefix<\/a> section of the next-intl docs for more information.<\/p>\n<p>\ud83d\uddd2\ufe0f <strong>Note<\/strong> <strong>\u00bb<\/strong> For SEO or usability, some consider localizing their pathnames, such as translating <code>\/ar-eg\/about<\/code> to <code>\/ar-eg\/\u0646\u0628\u0630\u0629-\u0639\u0646\u0627<\/code>. For details on how to set this up and ensure proper navigation, check out the <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/routing\/middleware#localizing-pathnames\">Localizing pathnames<\/a> section in the next-intl documentation.<\/p>\n<h3>Alternate links<\/h3>\n<p>next-intl automatically generates alternate URLs, which are crucial for SEO as they inform search engines about available page translations. These appear in the <code>link<\/code> header in HTTP response headers.<\/p>\n<figure id=\"attachment_82371\" aria-describedby=\"caption-attachment-82371\" style=\"width: 1024px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-large wp-image-82371\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/alt-links-1024x426.png\" alt=\"A screenshot of HTTP response headers, displaying various server responses such as 'Cache-Control', 'Content-Encoding', and 'Content-Type'. Notably, there's a 'link' header specifying alternate versions of the page for English US ('en-us') and Arabic Egypt ('ar-eg') locales. A 'set-cookie' header sets the 'NEXT_LOCALE' to 'en-us'. The response also includes 'X-Powered-By: Next.js' indicating the technology used.\" width=\"1024\" height=\"426\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/alt-links-1024x426.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/alt-links-300x125.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/alt-links-768x320.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/alt-links.png 1388w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption id=\"caption-attachment-82371\" class=\"wp-caption-text\">An HTTP response from our app, showing an alternate ar-eg links in the link header.<\/figcaption><\/figure>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong> You can disable these alternate links, or customize them using your own middleware logic. The <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/routing\/middleware#alternate-links\">Alternate links<\/a> section of the next-intl docs can guide you here.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-use-localized-links\"><\/span>How do I use localized links?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Our Next.js <code>Link<\/code> components don&#8217;t automatically include locale prefixes like <code>\/ar-eg\/week<\/code> for Arabic pages\u2014they link directly to <code>\/week<\/code>. Although next-intl middleware corrects this by redirecting based on the active locale, links copied directly from our pages and shared lose their locale context. For consistent navigation and better SEO, it&#8217;s best to include locale prefixes in URLs explicitly.<\/p>\n<p>Luckily, next-intl makes this easy by providing a drop-in replacement to Next.js\u2019 <code>Link<\/code>. We can expose it from our <code>i18n.config.ts<\/code> file and use it in our app\u2019s components.<\/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\">\/\/ i18n.config.ts\n<span class=\"hljs-addition\">+ import { createSharedPathnamesNavigation } from \"next-intl\/navigation\";<\/span>\n\n  export const locales = &#91;\"en-us\", \"ar-eg\"] as const;\n  export type Locale = (typeof locales)&#91;number];\n\n<span class=\"hljs-addition\">+ export const { Link } =<\/span>\n<span class=\"hljs-addition\">+  createSharedPathnamesNavigation({ locales });<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-27\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">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_a0a1bddfbe401ba49a6ef3738e75546e\" 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>All it takes to use the next-intl-provided <code>Link<\/code> is changing our module imports in our components.<\/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\">\/\/ app\/_components\/Header.tsx\n\n  import { useTranslations } from \"next-intl\";\n<span class=\"hljs-deletion\">- import Link from \"next\/link\";<\/span>\n<span class=\"hljs-addition\">+ import { Link } from \"@\/i18n.config\";<\/span>\n\n  export default function Header() {\n    const t = useTranslations(\"Header\");\n\n    \/\/ ...\n  }\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_69754fd3df2570f48675c02bf276dee7\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>That\u2019s it! Now when we\u2019re on an Arabic page, our <code>Link<\/code>s will explicitly point to the Arabic version of our pages.<\/p>\n<figure id=\"attachment_82377\" aria-describedby=\"caption-attachment-82377\" style=\"width: 1024px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-large wp-image-82377\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/localized-links-1024x487.png\" alt=\"The image shows a section of the rendered HTML of our site header with navigation links. The header contains a navigation bar with a list of three links. Each link is wrapped in list item tags. The first link is to the homepage with Arabic text indicating &quot;Home.&quot; The second and third links are to the &quot;Week&quot; and &quot;About Us&quot; sections of the site, also in Arabic. All links are prefixed with the &quot;\/ar-eg\/&quot; locale.\" width=\"1024\" height=\"487\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/localized-links-1024x487.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/localized-links-300x143.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/localized-links-768x365.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/localized-links.png 1076w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption id=\"caption-attachment-82377\" class=\"wp-caption-text\">Our nav links when we\u2019re on an Arabic page.<\/figcaption><\/figure>\n<p>Of course, when we\u2019re on an English page, our <code>Link<\/code>s explicitly point to the <code>\/en-us\/foo<\/code> versions of our pages.<\/p>\n<p>\ud83d\uddd2\ufe0f <strong>Note<\/strong> <strong>\u00bb<\/strong> If you&#8217;re using localized pathnames that vary by locale (like <code>\/en-us\/about<\/code> vs. <code>\/ar-eg\/\u0646\u0628\u0630\u0629-\u0639\u0646\u0627<\/code>), switch to using the <code>createLocalizedPathnamesNavigation<\/code> function for accurate navigation. For more details, check the next-intl <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/routing\/navigation#localized-pathnames\">documentation on Localized pathnames<\/a>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-build-a-language-switcher\"><\/span>How do I build a language switcher?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>It&#8217;s useful to allow users to manually select their locale (even though we&#8217;ll attempt to automatically detect the best match from browser settings a bit later). In this section, we&#8217;ll create a Client Component with a <code>&lt;select&gt;<\/code> dropdown as a locale switcher.<\/p>\n<p>We\u2019ll use next-intl\u2019s programmatic navigation functions in our locale switcher. Let\u2019s start by exposing these functions from our <code>i18n.config.ts<\/code> file.<\/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\">\/\/ i18n.config.ts\n\n  import { createSharedPathnamesNavigation } from \"next-intl\/navigation\";\n\n  export const locales = &#91;\"en-us\", \"ar-eg\"] as const;\n  export type Locale = (typeof locales)&#91;number];\n\n<span class=\"hljs-deletion\">- export const { Link } =<\/span>\n<span class=\"hljs-addition\">+ export const { Link, usePathname, useRouter } =<\/span>\n    createSharedPathnamesNavigation({ locales });\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-29\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_9cb612ca3e97b736e17e5f27119a7b59\" 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 we\u2019re in <code>i18n.config.ts<\/code> let\u2019s write a little map containing human-friendly names for our locales; we\u2019ll use this in our <code>&lt;select&gt;<\/code> dropdown.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-30\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ i18n.config.ts\n\n  import { createSharedPathnamesNavigation } from \"next-intl\/navigation\";\n\n  export const locales = &#91;\"en-us\", \"ar-eg\"] as const;\n  export type Locale = (typeof locales)&#91;number];\n\n<span class=\"hljs-addition\">+ export const localeNames: Record&lt;Locale, string&gt; = {<\/span>\n<span class=\"hljs-addition\">+   \"en-us\": \"English\",<\/span>\n<span class=\"hljs-addition\">+   \"ar-eg\": \"\u0627\u0644\u0639\u0631\u0628\u064a\u0629 (Arabic)\",<\/span>\n<span class=\"hljs-addition\">+ };<\/span>\n\nexport const { Link, usePathname, useRouter } =\n  createSharedPathnamesNavigation({ locales });\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-30\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_4620d6817f2f2ad49062e8b8f8d690c3\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Alright, we\u2019re ready to write our <code>LocaleSwitcher<\/code>.<\/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=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ app\/_components\/LocaleSwitcher.tsx<\/span>\n\n<span class=\"hljs-string\">\"use client\"<\/span>;\n\n<span class=\"hljs-keyword\">import<\/span> {\n  localeNames,\n  locales,\n  usePathname,\n  useRouter,\n  type Locale,\n} <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/i18n.config\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">LocaleSwitcher<\/span>(<span class=\"hljs-params\">{\n  locale,\n}: {\n  locale: Locale;\n}<\/span>) <\/span>{\n  <span class=\"hljs-comment\">\/\/ `pathname` will contain the current<\/span>\n  <span class=\"hljs-comment\">\/\/ route without the locale e.g. `\/about`...<\/span>\n  <span class=\"hljs-keyword\">const<\/span> pathname = usePathname();\n  <span class=\"hljs-keyword\">const<\/span> router = useRouter();\n\n  <span class=\"hljs-keyword\">const<\/span> changeLocale = (\n    event: React.ChangeEvent&lt;HTMLSelectElement&gt;,\n  ) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> newLocale = event.target.value <span class=\"hljs-keyword\">as<\/span> Locale;\n\n    <span class=\"hljs-comment\">\/\/ ...if the user chose Arabic (\"ar-eg\"),<\/span>\n    <span class=\"hljs-comment\">\/\/ router.replace() will prefix the pathname<\/span>\n    <span class=\"hljs-comment\">\/\/ with this `newLocale`, effectively changing<\/span>\n    <span class=\"hljs-comment\">\/\/ languages by navigating to `\/ar-eg\/about`.<\/span>\n    router.replace(pathname, { <span class=\"hljs-attr\">locale<\/span>: newLocale });\n  };\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">select<\/span>\n        <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{locale}<\/span>\n        <span class=\"hljs-attr\">onChange<\/span>=<span class=\"hljs-string\">{changeLocale}<\/span>\n        <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>\n      &gt;<\/span>\n        {locales.map((loc) =&gt; (\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">option<\/span> <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{loc}<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{loc}<\/span>&gt;<\/span>\n            {localeNames&#91;loc]}\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">option<\/span>&gt;<\/span>\n        ))}\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">select<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-31\"><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_4d99d6e2e706024415233f1e1abc9e7c\" 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 <strong>Resource<\/strong> <strong>\u00bb<\/strong> Check out <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/routing\/navigation\">next-intl\u2019s navigation options<\/a> on the official docs.<\/p>\n<p>Let\u2019s add our new <code>LocaleSwitcher<\/code> to the <code>Header<\/code> component.<\/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\">\/\/ app\/_components\/Header.tsx\n\n<span class=\"hljs-deletion\">- import { Link } from \"@\/i18n.config\";<\/span>\n<span class=\"hljs-addition\">+ import { Link, type Locale } from \"@\/i18n.config\";<\/span>\n<span class=\"hljs-deletion\">- import { useTranslations } from \"next-intl\";<\/span>\n<span class=\"hljs-addition\">+ import { useLocale, useTranslations } from \"next-intl\";<\/span>\n<span class=\"hljs-addition\">+ import LocaleSwitcher from \".\/LocaleSwitcher\";<\/span>\n\n  export default function Header() {\n    const t = useTranslations(\"Header\");\n\n<span class=\"hljs-addition\">+   \/\/ Retrieves the active locale.<\/span>\n<span class=\"hljs-addition\">+   const locale = useLocale() as Locale;<\/span>\n\n    return (\n      &lt;header className=\"...\"&gt;\n        &lt;nav&gt;\n          {\/* ... *\/}\n        &lt;\/nav&gt;\n<span class=\"hljs-addition\">+       &lt;LocaleSwitcher locale={locale} \/&gt;<\/span>\n      &lt;\/header&gt;\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_90d76051353bf91b25192aad0fb3a160\" 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\u2019t call next-intl\u2019s <code>useLocale()<\/code> hook directly from a Client Component, so we\u2019re passing the locale as a prop from the <code>Header<\/code> component. We\u2019ll cover Client Components in greater detail a bit later.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82383 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/locale-switcher.gif\" alt=\"An animation showing our website on the \u201chttp:\/\/localhost:3000\/en-us\/about\u201d page, showing the About page in English with a new language switcher dropdown in the header. The mouse cursor clicks on the dropdown and changes the selection from \u201cEnglish\u201d to \u201cArabic\u201d, causing the browser URL to change from \u201clocalhost:3000\/en-us\/about\u201d to \u201clocalhost:3000\/ar-eg\/about\u201d and showing the About page in Arabic. The mouse cursor then selects \u201cEnglish\u201d from the dropdown, ad infinitum.\" width=\"600\" height=\"246\" \/><\/p>\n<p>\ud83d\uddd2\ufe0f <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0Remember that <code>NEXT_LOCALE<\/code> cookie? It\u2019s still being set by next-intl, updating to reflect the user\u2019s selected locale. This ensures if a user selects Arabic (<code>ar-eg<\/code>) from the dropdown, they will be automatically directed to the Arabic version of the site on future visits.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-automatically-detect-the-users-locale\"><\/span>How do I automatically detect the user\u2019s locale?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Modern browsers allow us to select a list of languages we prefer to see our web pages displayed in.<\/p>\n<p><figure id=\"attachment_82389\" aria-describedby=\"caption-attachment-82389\" style=\"width: 1024px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-large wp-image-82389\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/firefox-lang-prefs-1024x692.png\" alt=\"The Firefox\u2019s dialog box for Webpage Language Settings. It explains that web pages can be offered in multiple languages and allows users to choose their preferred languages for displaying web pages. Two languages are listed: &quot;English (Canada) [en-ca]&quot; is highlighted, indicating it is the top preference, followed by &quot;Arabic (Egypt) [ar-eg].&quot; On the right side, there are buttons to &quot;Move Up,&quot; &quot;Move Down,&quot; or &quot;Remove&quot; the selected language. Below the language list is a button to &quot;Select a language to add...&quot; and at the bottom, there are &quot;Cancel&quot; and &quot;OK&quot; buttons, with &quot;OK&quot; highlighted in blue, suggesting it is the default action button.\" width=\"1024\" height=\"692\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/firefox-lang-prefs-1024x692.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/firefox-lang-prefs-300x203.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/firefox-lang-prefs-768x519.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/firefox-lang-prefs.png 1312w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption id=\"caption-attachment-82389\" class=\"wp-caption-text\">Firefox\u2019s language preferences dialog.<\/figcaption><\/figure>These languages are included in the <code>Accept-Language<\/code> header sent with every HTTP request. next-intl utilizes this to automatically detect the visitor&#8217;s locale, a feature we previously disabled to simplify our reasoning about localized locales and navigation. Let&#8217;s re-enable it now.<\/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\">\/\/ middleware.ts\n\nimport createMiddleware from \"next-intl\/middleware\";\nimport { locales } from \".\/i18n.config\";\n\nexport default createMiddleware({\n  defaultLocale: \"en-us\",\n  locales,\n<span class=\"hljs-addition\">+ \/\/ This is the default. We can omit the<\/span>\n<span class=\"hljs-addition\">+ \/\/ option entirely and get the same result.<\/span>\n<span class=\"hljs-addition\">+ localeDetection: true,<\/span>\n});\n\nexport const config = {\n  \/\/ ...\n};\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-33\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">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_89bbb76ca9b1a43e83a9bda10900bdeb\" 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 <strong>Resource<\/strong> <strong>\u00bb<\/strong> \u00a0We cover locale detection in our dedicated guide, <a href=\"https:\/\/phrase.com\/blog\/posts\/detecting-a-users-locale\/\">Detecting a User\u2019s Locale in a Web App<\/a>.<\/p>\n<p>next-intl uses a cascade to determine the active locale, stopping when it resolves the locale at any step:<\/p>\n<p>1. Locale prefix in the request URI (e.g., <code>\/ar-eg\/about<\/code>).<br \/>\n2. The <code>NEXT_LOCALE<\/code> cookie, if it exists.<br \/>\n3. A locale matched from the <code>Accept-Language<\/code> header (we just enabled this).<br \/>\n4. The <code>defaultLocale<\/code> configured in the middleware.<\/p>\n<p>When <code>localeDetection<\/code> is active, next-intl tries to match the browser&#8217;s language preferences with our configured locales, optimizing for the closest linguistic and regional fit. For example, English variants match to <code>en-us<\/code>, and Arabic preferences to <code>ar-eg<\/code>, ensuring users see the most relevant language version of the site.<\/p>\n<h3>Notes and resources<\/h3>\n<ul>\n<li>Under the hood, next-intl uses the <a href=\"https:\/\/formatjs.io\/docs\/polyfills\/intl-localematcher\/\">@formatjs\/intl-localematcher<\/a> best-fit algorithm.<\/li>\n<li>When testing automatic locale matching remember to delete the <code>NEXT_LOCALE<\/code> cookie, since it will take precedence over user browser preferences when resolving the locale.<\/li>\n<li>Read more about <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/routing\/middleware#prefix-locale-detection\">Locale detection<\/a> in the next-intl docs.<\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-client-components\"><\/span>How do I localize Client Components?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Next.js&#8217; App Router typically uses <a href=\"https:\/\/nextjs.org\/docs\/app\/building-your-application\/rendering\/server-components\">React Server Components<\/a> for server rendering, boosting performance and security. When components need browser-specific features like DOM events or React state, they can be designated as <a href=\"https:\/\/nextjs.org\/docs\/app\/building-your-application\/rendering\/client-components\">Client Components<\/a>, which includes them in the client bundle and forces them to render in the browser.<\/p>\n<p>While next-intl supports Server Components by default, it also offers ways to localize Client Components. Let\u2019s add a mock weather alert Client Component to our home page to demonstrate. The component will have an accordion folding\/unfolding UI that needs React state, which is only available in Client Components.<\/p>\n<p>\ud83d\uddd2\ufe0f <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0We made the language switcher we built a Client Component since we needed to listen to the DOM change event firing from its <code>&lt;select&gt;<\/code>.<\/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=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ app\/_components\/WeatherAlerts.tsx<\/span>\n\n<span class=\"hljs-comment\">\/\/ Tell Next.js that this is a Client<\/span>\n<span class=\"hljs-comment\">\/\/ Component.<\/span>\n<span class=\"hljs-string\">\"use client\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ ...<\/span>\n<span class=\"hljs-keyword\">import<\/span> { useState } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react\"<\/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\">WeatherAlerts<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-comment\">\/\/ We can only use React state in Client<\/span>\n  <span class=\"hljs-comment\">\/\/ Components.<\/span>\n  <span class=\"hljs-keyword\">const<\/span> &#91;isOpen, setIsOpen] = useState(<span class=\"hljs-literal\">false<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> toggleAlerts = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> setIsOpen(!isOpen);\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>\n        <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>\n        <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{toggleAlerts}<\/span>\n      &gt;<\/span>\n        Weather Alerts\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">span<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n          <span class=\"hljs-symbol\">&amp;#9660;<\/span> {\/* Chevron down icon *\/}\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">span<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      {isOpen &amp;&amp; (\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...&gt;\n          &lt;p className=\"<\/span><span class=\"hljs-attr\">...<\/span>\"&gt;<\/span>\n            \ud83c\udf29\ufe0f Severe Thunderstorm Warning until 09:00 PM\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n            \ud83c\udf28\ufe0f Blizzard Warning in effect from 01:00 AM\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n            \ud83c\udf0a Coastal Flood Advisory from noon today to\n            10:00 PM\n          <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      )}\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-34\"><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_c37a9409f8a715c08a6d28bf8bb062d4\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Let\u2019s drop our new <code>WeatherAlerts<\/code> component into our home page.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-35\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/page.tsx\n\n<span class=\"hljs-addition\">+ import WeatherAlerts from \"..\/_components\/WeatherAlerts\";<\/span>\n\n  export default function Home() {\n    return (\n      &lt;main&gt;\n         {\/* ... *\/}\n\n        &lt;section className=\"...\"&gt;\n           &lt;div className=\"...\"&gt;\n             &lt;p className=\"...\"&gt;\u2600\ufe0f&lt;\/p&gt;\n             &lt;p className=\"...\"&gt;Sunny&lt;\/p&gt;\n             &lt;p className=\"...\"&gt;22\u00b0C&lt;\/p&gt;\n           &lt;\/div&gt;\n         &lt;\/section&gt;\n\n<span class=\"hljs-addition\">+      &lt;WeatherAlerts \/&gt;<\/span>\n     &lt;\/main&gt;\n   );\n }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-35\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_340ef3ab91c0b5a681a4620b0f8f9d71\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82395 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/weather-alerts.gif\" alt=\"Looping animation of the weather alerts opening and closing, displaying the alert details in its open state.\" width=\"600\" height=\"434\" \/><\/p>\n<p>Since <code>useTranslations<\/code> pulls translation messages on the server, we can\u2019t use it in our Client Components. Fortunately, next-intl provides alternative ways for making our translations available to Client Components:<\/p>\n<p>1. <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/environments\/server-client-components#option-1-passing-translations-to-client-components\">Passing translations to Client Components<\/a><br \/>\n2. <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/environments\/server-client-components#option-2-moving-state-to-the-server-side\">Moving state to the server side<\/a><br \/>\n3. <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/environments\/server-client-components#option-3-providing-individual-messages\">Providing individual messages<\/a><br \/>\n4. <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/environments\/server-client-components#option-4-providing-all-messages\">Providing all messages<\/a><\/p>\n<p>We\u2019ll focus on 1. passing translations directly, which allows Client Components to receive props from Server Components. This approach works well for our <code>WeatherAlerts<\/code> component, where translations are fetched server-side and all interactive elements are managed client-side.<\/p>\n<p>First, let\u2019s rename our <code>WeatherAlerts<\/code> component to <code>ClientWeatherAlerts<\/code> and make it a presentational component.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-36\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/_components\/WeatherAlerts\/ClientWeatherAlerts.tsx\n\n \"use client\";\n\n \/\/ ...\n import { useState } from \"react\";\n\n<span class=\"hljs-deletion\">- export default function WeatherAlerts() {<\/span>\n<span class=\"hljs-addition\">+ export default function ClientWeatherAlerts({<\/span>\n<span class=\"hljs-addition\">+   title,<\/span>\n<span class=\"hljs-addition\">+   children,<\/span>\n<span class=\"hljs-addition\">+ }: Readonly&lt;{ title: string; children: React.ReactNode }&gt;) {<\/span>\n    const &#91;isOpen, setIsOpen] = useState(false);\n    const toggleIsOpen = () =&gt; setIsOpen(!isOpen);\n\n    return (\n      &lt;div&gt;\n        &lt;div\n          className=\"...\"\n          onClick={toggleIsOpen}\n        &gt;\n<span class=\"hljs-deletion\">-         Weather Alerts<\/span>\n<span class=\"hljs-addition\">+         {title}<\/span>\n          &lt;span className=\"...\"&gt;\n            &amp;#9660; {\/* Chevron down icon *\/}\n          &lt;\/span&gt;\n        &lt;\/div&gt;\n        {isOpen &amp;&amp; (\n          &lt;div className=\"...\"&gt;\n<span class=\"hljs-deletion\">-           &lt;p className=\"...\"&gt;<\/span>\n<span class=\"hljs-deletion\">-             \ud83c\udf29\ufe0f Severe Thunderstorm Warning until 09:00 PM<\/span>\n<span class=\"hljs-deletion\">-           &lt;\/p&gt;<\/span>\n<span class=\"hljs-deletion\">-           \/\/ ...<\/span>\n\n<span class=\"hljs-addition\">+           {children}<\/span>\n          &lt;\/div&gt;\n        )}\n      &lt;\/div&gt;\n    );\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-36\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_831f97c4550f308566c88eadfc5284e2\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now we can create a new <code>ServerWeatherAlerts<\/code> component that injects our server-side translations, and mock fetched alerts, into our Client Component.<\/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=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ app\/_components\/WeatherAlerts\/ServerWeatherAlerts.tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { type Locale } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/i18n.config\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { useLocale, useTranslations } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next-intl\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> ClientWeatherAlerts <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/ClientWeatherAlerts\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ In a production app, we would likely<\/span>\n<span class=\"hljs-comment\">\/\/ be fetching these from some service.<\/span>\n<span class=\"hljs-keyword\">const<\/span> mockWeatherAlerts = {\n  <span class=\"hljs-string\">\"en-us\"<\/span>: &#91;\n    <span class=\"hljs-string\">\"\ud83c\udf29\ufe0f Severe Thunderstorm Warning until 09:00 PM\"<\/span>,\n    <span class=\"hljs-string\">\"\ud83c\udf28\ufe0f Blizzard Warning in effect from 01:00 AM\"<\/span>,\n    <span class=\"hljs-string\">\"\ud83c\udf0a Coastal Flood Advisory from noon today to 10:00 PM\"<\/span>,\n  ],\n  <span class=\"hljs-string\">\"ar-eg\"<\/span>: &#91;\n    <span class=\"hljs-string\">\"\ud83c\udf29\ufe0f \u062a\u062d\u0630\u064a\u0631 \u0645\u0646 \u0639\u0627\u0635\u0641\u0629 \u0631\u0639\u062f\u064a\u0629 \u0634\u062f\u064a\u062f\u0629 \u062d\u062a\u0649 \u0627\u0644\u0633\u0627\u0639\u0629 09:00 \u0645\u0633\u0627\u0621\u064b\"<\/span>,\n    <span class=\"hljs-string\">\"\ud83c\udf28\ufe0f \u062a\u062d\u0630\u064a\u0631 \u0645\u0646 \u0639\u0627\u0635\u0641\u0629 \u062b\u0644\u062c\u064a\u0629 \u0642\u0627\u0626\u0645\u0629 \u0628\u062f\u0621\u064b\u0627 \u0645\u0646 \u0627\u0644\u0633\u0627\u0639\u0629 01:00 \u0635\u0628\u0627\u062d\u064b\u0627\"<\/span>,\n    <span class=\"hljs-string\">\"\ud83c\udf0a \u062a\u0646\u0628\u064a\u0647 \u0645\u0646 \u0641\u064a\u0636\u0627\u0646 \u0633\u0627\u062d\u0644\u064a \u0645\u0646 \u0627\u0644\u0638\u0647\u064a\u0631\u0629 \u0627\u0644\u064a\u0648\u0645 \u062d\u062a\u0649 \u0627\u0644\u0633\u0627\u0639\u0629 10:00 \u0645\u0633\u0627\u0621\u064b\"<\/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\">ServerWeatherAlerts<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> t = useTranslations(<span class=\"hljs-string\">\"WeatherAlerts\"<\/span>);\n\n  <span class=\"hljs-keyword\">const<\/span> locale = useLocale() <span class=\"hljs-keyword\">as<\/span> Locale;\n  <span class=\"hljs-keyword\">const<\/span> alerts = mockWeatherAlerts&#91;locale];\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"hljs-comment\">\/\/ Pass the translation message as a prop.<\/span>\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">ClientWeatherAlerts<\/span> <span class=\"hljs-attr\">title<\/span>=<span class=\"hljs-string\">{t(<\/span>\"<span class=\"hljs-attr\">title<\/span>\")}&gt;<\/span>\n      {\/* Inject alerts as children. *\/}\n      {alerts.map((alert) =&gt; (\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span> <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{alert}<\/span>&gt;<\/span>\n          {alert}\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n      ))}\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">ClientWeatherAlerts<\/span>&gt;<\/span><\/span>\n  );\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\">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_73a084db5777ce85f482b58b0c941e14\" 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 use the <code>useTranslations<\/code> and <code>useLocale<\/code> hooks as normal in our Server Component, allowing us to grab our translations and pass them as props to our Client Component. Here are the new translation messages:<\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/en-us.json\n{\n  \/\/ ...\n<span class=\"hljs-addition\">+ \"WeatherAlerts\": {<\/span>\n<span class=\"hljs-addition\">+   \"title\": \"Weather Alerts\"<\/span>\n<span class=\"hljs-addition\">+ }<\/span>\n}\n\n\/\/ locales\/ar-eg.json\n{\n  \/\/ ...\n<span class=\"hljs-addition\">+ \"WeatherAlerts\": {<\/span>\n<span class=\"hljs-addition\">+   \"title\": \"\u062a\u0646\u0628\u064a\u0647\u0627\u062a \u0627\u0644\u0637\u0642\u0633\"<\/span>\n<span class=\"hljs-addition\">+ }<\/span>\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-38\"><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_b086c38c0fbee990de0f0f2fa0e93003\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Let\u2019s swap our new Server Component into our home page to see our changes in action.<\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/page.tsx\n\n<span class=\"hljs-deletion\">- import WeatherAlerts from \"..\/_components\/WeatherAlerts\";<\/span>\n<span class=\"hljs-addition\">+ import ServerWeatherAlerts from \"..\/_components\/WeatherAlerts\/ServerWeatherAlerts\";<\/span>\n\n  export default function Home() {\n    return (\n      &lt;main&gt;\n        {\/* ... *\/}\n\n        &lt;section className=\"...\"&gt;\n          {\/* ... *\/}\n        &lt;\/section&gt;\n<span class=\"hljs-deletion\">-        &lt;WeatherAlerts \/&gt;<\/span>\n<span class=\"hljs-addition\">+        &lt;ServerWeatherAlerts \/&gt;<\/span>\n      &lt;\/main&gt;\n    );\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-39\"><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_c724b2d9f8cc569dfcb687b9332fe9d9\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82401 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/localized-weather-alerts.gif\" alt=\"Looping animation showing the weather alerts component in English opening to reveal the individual alerts. The app language is switched to Arabic to show the Arabic version of the weather alerts as it opens, ad infinitum.\" width=\"600\" height=\"567\" \/><\/p>\n<p>This composition pattern of wrapping Client Components inside Server Components allows our translations to load only on the server. The i18n library is never added to the client bundle, making our initial app load and client bundle as performant as possible.<\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong> Again, there are other ways next-intl provides for localizing Client Components. The <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/environments\/server-client-components#using-internationalization-in-client-components\">Using internationalization in Client Components<\/a> section of the next-intl docs covers them in detail.<\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong> Reminder that you can <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-intl-demo\/tree\/main?tab=readme-ov-file\">get all the app code we cover here<\/a> from our GitHub repo.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-async-components\"><\/span>How do I localize async components?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Up to this point, we\u2019ve localized synchronous Server Components, which work out-of-the-box with next-intl. However, our weekly forecast page component needs to fetch data, which makes it a special case: an async component.<\/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=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"> <span class=\"hljs-comment\">\/\/ app\/&#91;locale]\/week\/page.tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> type { WeeklyWeatherRoot } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/types\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { promises <span class=\"hljs-keyword\">as<\/span> fs } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"fs\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Note the `async` keyword here.<\/span>\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\">Week<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-comment\">\/\/ We `await` our file read.<\/span>\n  <span class=\"hljs-keyword\">const<\/span> fileContents = <span class=\"hljs-keyword\">await<\/span> fs.readFile(\n    <span class=\"hljs-string\">`<span class=\"hljs-subst\">${process.cwd()}<\/span>\/app\/_data\/week.json`<\/span>,\n    <span class=\"hljs-string\">\"utf-8\"<\/span>,\n  );\n  <span class=\"hljs-keyword\">const<\/span> { weeklyWeather } = <span class=\"hljs-built_in\">JSON<\/span>.parse(\n    fileContents,\n  ) <span class=\"hljs-keyword\">as<\/span> WeeklyWeatherRoot;\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">main<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h1<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n        This week<span class=\"hljs-symbol\">&amp;apos;<\/span>s weather\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"...\"<\/span>&gt;<\/span>\n        {weeklyWeather.map((day) =&gt; (\n          \/\/ Display the day data\n        ))}\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">main<\/span>&gt;<\/span><\/span>\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-40\"><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_2b912d72af45a28086350c6b3e862de2\" 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>Async components like this throw an error if we call the <code>useTranslations<\/code> hook from within them.<\/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=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">\u2a2f Internal error: Error: Expected a suspended thenable.\nThis is a bug in React. Please file an issue.\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-41\"><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_b069d0b7926a05dae295fb9231806278\" 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>Easy fix, however: next-intl provides an async drop-in replacement for <code>useTranslations<\/code> called <code>getTranslations<\/code>. We\u2019ll use this function to localize our weekly weather page.<\/p>\n<p>First, let\u2019s add our new translations:<\/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=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/en-us.json\n\n  {\n    \/\/ ...\n<span class=\"hljs-addition\">+   \"Week\": {<\/span>\n<span class=\"hljs-addition\">+     \"title\": \"This week's weather\",<\/span>\n<span class=\"hljs-addition\">+     \"sunny\": \"Sunny\",<\/span>\n<span class=\"hljs-addition\">+     \"cloudy\": \"Cloudy\",<\/span>\n<span class=\"hljs-addition\">+     \"rainy\": \"Rainy\",<\/span>\n<span class=\"hljs-addition\">+     \"partlyCloudy\": \"Partly Cloudy\",<\/span>\n<span class=\"hljs-addition\">+     \"showers\": \"Showers\",<\/span>\n<span class=\"hljs-addition\">+     \"thunderstorms\": \"Thunderstorms\"<\/span>\n    }\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-42\"><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_f81f4a8284a2a473aff047f7aa82792e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-43\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/ar-eg.json\n\n  {\n    \/\/ ...\n<span class=\"hljs-addition\">+   \"Week\": {<\/span>\n<span class=\"hljs-addition\">+     \"title\": \"\u0637\u0642\u0635 \u0627\u0644\u0623\u0633\u0628\u0648\u0639\",<\/span>\n<span class=\"hljs-addition\">+     \"sunny\": \"\u0645\u0634\u0645\u0633\",<\/span>\n<span class=\"hljs-addition\">+     \"cloudy\": \"\u063a\u0627\u0626\u0645\",<\/span>\n<span class=\"hljs-addition\">+     \"rainy\": \"\u0645\u0645\u0637\u0631\",<\/span>\n<span class=\"hljs-addition\">+     \"partlyCloudy\": \"\u063a\u0627\u0626\u0645 \u062c\u0632\u0626\u064a\u0627\",<\/span>\n<span class=\"hljs-addition\">+     \"showers\": \"\u0632\u062e\u0627\u062a \u0645\u0637\u0631\u064a\u0629\",<\/span>\n<span class=\"hljs-addition\">+     \"thunderstorms\": \"\u0639\u0648\u0627\u0635\u0641 \u0631\u0639\u062f\u064a\u0629\"<\/span>\n    }\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-43\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_753fe230c77661be896057edeeda1d47\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now we need to import the async <code>getTranslations<\/code> function and use it like <code>useTranslations<\/code>.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-44\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/week\/page.tsx\n\n  import type { WeeklyWeatherRoot } from \"@\/types\";\n  import { promises as fs } from \"fs\";\n<span class=\"hljs-addition\">+ import { getTranslations } from \"next-intl\/server\";<\/span>\n\n  export default async function Week() {\n    const fileContents = await fs.readFile(\n      `${process.cwd()}\/app\/_data\/week.json`,\n      \"utf-8\",\n    );\n    const { weeklyWeather } = JSON.parse(\n      fileContents,\n    ) as WeeklyWeatherRoot;\n\n<span class=\"hljs-addition\">+  \/\/ We have to `await` here.<\/span>\n<span class=\"hljs-addition\">+  const t = await getTranslations(\"Week\");<\/span>\n\n    return (\n      &lt;main&gt;\n        &lt;h1 className=\"...\"&gt;\n<span class=\"hljs-deletion\">-         This week&amp;apos;s weather<\/span>\n<span class=\"hljs-addition\">+         {t(\"title\")}<\/span>\n        &lt;\/h1&gt;\n        &lt;div className=\"...\"&gt;\n          {weeklyWeather.map((day) =&gt; (\n            &lt;section key={day.dateTime} className=\"...\"&gt;\n              &lt;h2 className=\"...\"&gt;\n                {new Date(day.dateTime).toString()}\n              &lt;\/h2&gt;\n              &lt;div&gt;\n                &lt;div className=\"...\"&gt;\n                  &lt;p className=\"...\"&gt;\n                    {day.conditionIcon}\n                  &lt;\/p&gt;\n                  &lt;p className=\"...\"&gt;\n<span class=\"hljs-deletion\">-                   day.condition<\/span>\n<span class=\"hljs-addition\">+                   {\/* \"sunny\" | \"partlyCloudy\" | ... *\/}<\/span>\n<span class=\"hljs-addition\">+                   {t(day.condition)}<\/span>\n                  &lt;\/p&gt;\n                  &lt;p className=\"...\"&gt;\n                    {day.temperature.celsius}\u00b0C\n                  &lt;\/p&gt;\n                &lt;\/div&gt;\n              &lt;\/div&gt;\n            &lt;\/section&gt;\n          ))}\n        &lt;\/div&gt;\n      &lt;\/main&gt;\n    );\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-44\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_e6c258c60bd3cb06f43ca099c4a846e9\" 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 <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0We need to use <code>getTranslations<\/code> in <strong>all<\/strong> async components, whether they\u2019re page or shared components.<\/p>\n<figure id=\"attachment_82407\" aria-describedby=\"caption-attachment-82407\" style=\"width: 1016px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82407\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-week.png\" alt=\"The English version weekly weather page, showing its title, \u201cThis week\u2019s weather\u201d and daily conditions \u201csunny, partly cloudy, etc.\u201d in English.\" width=\"1016\" height=\"572\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-week.png 1016w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-week-300x169.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-week-768x432.png 768w\" sizes=\"(max-width: 1016px) 100vw, 1016px\" \/><figcaption id=\"caption-attachment-82407\" class=\"wp-caption-text\">Our weekly weather in English.<\/figcaption><\/figure>\n<figure id=\"attachment_82413\" aria-describedby=\"caption-attachment-82413\" style=\"width: 988px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82413\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-week.png\" alt=\"The Arabic version weekly weather page, showing its title, \u201c\u0637\u0642\u0635 \u0627\u0644\u0623\u0633\u0628\u0648\u0639\u201d and daily conditions \u201c\u0645\u0634\u0645\u0633, \u063a\u0627\u0626\u0645 \u062c\u0632\u0626\u064a\u0627, etc.\u201d in Arabic.\" width=\"988\" height=\"584\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-week.png 988w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-week-300x177.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-week-768x454.png 768w\" sizes=\"(max-width: 988px) 100vw, 988px\" \/><figcaption id=\"caption-attachment-82413\" class=\"wp-caption-text\">Our weekly weather in Arabic.<\/figcaption><\/figure>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong> The next-intl <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/environments\/server-client-components#async-components\">Async components<\/a> documentation section covers other async server functions like <code>getLocale<\/code> and <code>getFormatter<\/code>.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-ensure-static-rendering\"><\/span>How do I ensure static rendering?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Next.js defaults Server Components to static rendering. We can see this if we build our site <strong>before it was localized by next-intl<\/strong> (use the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-intl-demo\/tree\/start?tab=readme-ov-file\">start branch of our GitHub repo<\/a> if you want to try this).<\/p>\n<p>From the command line:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-45\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm run build\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-45\"><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_1ac9498715309e0b4a60c9ca98ff5182\" 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_82419\" aria-describedby=\"caption-attachment-82419\" style=\"width: 1016px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82419\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/build-report-before-i18n.png\" alt=\"This image displays a terminal output showing the route size report for a web application. The output includes a table with columns labeled &quot;Route (app)&quot;, &quot;Size&quot;, and &quot;First Load JS&quot;. The table lists routes for &quot;\/&quot;, &quot;\/_not-found&quot;, &quot;\/about&quot;, and &quot;\/week&quot;, each with sizes ranging from 145 B to 871 B and first load JavaScript sizes all at approximately 87 kB. Additionally, there's a section titled &quot;+ First Load JS shared by all&quot; with entries for chunks such as &quot;23-3032740b29323cf3.js&quot; and &quot;fd9d1056-2821b0fcab0c3d8bd.js&quot; with sizes of 31.3 kB and 53.6 kB respectively, and &quot;other shared chunks (total)&quot; of 1.86 kB. Below the table, a line reads &quot;o (Static) prerendered as static content&quot;, indicating that the listed routes are statically generated.\" width=\"1016\" height=\"398\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/build-report-before-i18n.png 1016w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/build-report-before-i18n-300x118.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/build-report-before-i18n-768x301.png 768w\" sizes=\"(max-width: 1016px) 100vw, 1016px\" \/><figcaption id=\"caption-attachment-82419\" class=\"wp-caption-text\">Next.js build report in the terminal.<\/figcaption><\/figure>\n<p>All our routes are statically prerendered, which means they can be cached on the server, speeding up our page loads and saving compute time on the server.<\/p>\n<p>If we build the site in its current state, <strong>after localization with next-intl<\/strong>, we see a different story. (Use the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-intl-demo\/tree\/main?tab=readme-ov-file\">main branch of the GitHub repo<\/a> if you want to try this yourself).<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82425 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/build-report-after-i18n.png\" alt=\"This image shows a build report with marked routes: &quot;\/_not-found&quot; is static, while &quot;\/[locale]&quot;, &quot;\/[locale]\/about&quot;, and &quot;\/[locale]\/week&quot; are dynamic. Sizes vary from 137 B to 1.12 kB, with shared JavaScript chunks contributing to first load performance. Middleware is also listed, adding to the complexity of the build.\" width=\"1004\" height=\"486\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/build-report-after-i18n.png 1004w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/build-report-after-i18n-300x145.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/build-report-after-i18n-768x372.png 768w\" sizes=\"(max-width: 1004px) 100vw, 1004px\" \/><\/p>\n<p>Due to next-intl APIs loading translations, our routes <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/getting-started\/app-router#static-rendering\">implicitly opt-in to dynamic rendering<\/a> for each request. The next-intl team plans to address this in future updates, but they\u2019ve provided a temporary workaround. Let\u2019s implement it.<\/p>\n<p>First, we need to address the <code>[locale]<\/code> dynamic route param. Next.js doesn\u2019t know how to fill values in that route segment during building unless we tell via the <a href=\"https:\/\/nextjs.org\/docs\/app\/api-reference\/functions\/generate-static-params\">generateStaticParams<\/a> function. Let\u2019s add this function to our layout.<\/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\">\/\/ app\/&#91;locale]\/layout.tsx\n\n  import Header from \"@\/app\/_components\/Header\";\n  import \"@\/app\/globals.css\";\n<span class=\"hljs-deletion\">- import { type Locale } from \"@\/i18n.config\";<\/span>\n<span class=\"hljs-addition\">+ import { type Locale, locales } from \"@\/i18n.config\";<\/span>\n  import type { Metadata } from \"next\";\n\n<span class=\"hljs-addition\">+ export function generateStaticParams() {<\/span>\n<span class=\"hljs-addition\">+   return locales.map((locale) =&gt; ({ locale }));<\/span>\n<span class=\"hljs-addition\">+ }<\/span>\n\n  export const metadata: Metadata = {\n    \/\/ ...\n  };\n\n  export default function LocaleLayout({\n    children,\n    params: { locale },\n  }: Readonly&lt;{\n    children: React.ReactNode;\n    params: { locale: Locale };\n  }&gt;) {\n    return (\n      \/\/ ...\n    );\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_21edeafc0dabc64bb4114502df53b4d0\" 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 need to call next-intl\u2019s workaround <code>unstable_setRequestLocale<\/code> function, which makes the current locale available to all its APIs.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-47\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/layout.tsx\n\n  import Header from \"@\/app\/_components\/Header\";\n  import \"@\/app\/globals.css\";\n  import { type Locale, locales } from \"@\/i18n.config\";\n  import type { Metadata } from \"next\";\n<span class=\"hljs-addition\">+ import { unstable_setRequestLocale } from \"next-intl\/server\";<\/span>\n\n  export function generateStaticParams() {\n    return locales.map((locale) =&gt; ({ locale }));\n  }\n\n  export const metadata: Metadata = {\n    \/\/ ...\n  };\n\n  export default function LocaleLayout({\n    children,\n    params: { locale },\n  }: Readonly&lt;{\n    children: React.ReactNode;\n    params: { locale: Locale };\n  }&gt;) {\n<span class=\"hljs-addition\">+   unstable_setRequestLocale(locale);<\/span>\n    return (\n      \/\/ ...\n    );\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-47\"><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_76757aa592eec7297bfebb30797a82fd\" 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>Because Next.js can render layouts separately from pages, we need to call <code>unstable_setRequestLocale<\/code> in the layout and <strong>all pages.<\/strong><\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-48\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/page.tsx\n\n<span class=\"hljs-addition\">+ import { unstable_setRequestLocale } from \"next-intl\/server\";<\/span>\n  import WeatherAlerts from \"..\/_components\/WeatherAlerts\/WeatherAlerts\";\n\n<span class=\"hljs-deletion\">- export default function Home() {<\/span>\n<span class=\"hljs-addition\">+ export default function Home({<\/span>\n<span class=\"hljs-addition\">+   params: { locale },<\/span>\n<span class=\"hljs-addition\">+ }: Readonly&lt;{ params: { locale: string } }&gt;) {<\/span>\n\n<span class=\"hljs-addition\">+   unstable_setRequestLocale(locale);<\/span>\n\n    return (\n      &lt;main&gt;\n        {\/* ... *\/}\n      &lt;\/main&gt;\n    );\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-48\"><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_12d4a55262e40985f3bd5059a3a4e955\" 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>Repeat the above for each page in your app, adding the <code>locale<\/code> param and passing it to <code>unstable_setRequestedLocale<\/code>.<\/p>\n<p>\ud83d\uddd2\ufe0f <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0You need to call <code>unstable_setRequestedLocale<\/code> before using any next-intl API like <code>useTranslations<\/code> or you\u2019ll get errors when you build.<\/p>\n<p>With this in place, running <code>npm run build<\/code> should reveal that we\u2019re getting static rendering again.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-82431\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/build-report-after-optimization.png\" alt=\"This image displays a build report with routes using Static Site Generation (SSG). The &quot;\/[locale]&quot;, &quot;\/[locale]\/about&quot;, and &quot;\/[locale]\/week&quot; routes, along with their English and Arabic versions, use SSG for optimized performance. Details on script sizes for initial loads and shared chunks are also included.\" width=\"1000\" height=\"660\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/build-report-after-optimization.png 1000w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/build-report-after-optimization-300x198.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/build-report-after-optimization-768x507.png 768w\" sizes=\"(max-width: 1000px) 100vw, 1000px\" \/><\/p>\n<h3>Some notes on static rendering<\/h3>\n<ul>\n<li>The <code>unstable_setRequestLocale<\/code> is a temporary solution due to Next.js limitations: Server Components can&#8217;t directly access route parameters like <code>locale<\/code>. Future next-intl updates may eliminate this requirement. Find out more in the next-intl <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/getting-started\/app-router#static-rendering\">Static rendering<\/a> documentation.<\/li>\n<li>Using <code>generateStaticParams<\/code> to specify <strong>every supported locale<\/strong> can impact build performance, especially for sites with many locales. It&#8217;s often more efficient to specify only the default locale and dynamically generate others as needed.<\/li>\n<li>Statically exporting your site with next-intl has some important limitations. Refer to the <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/routing\/middleware#usage-without-middleware-static-export\">Usage without middleware (static export)<\/a> section in the next-intl documentation for guidance.<\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-work-with-right-to-left-languages\"><\/span>How do I work with right-to-left languages?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The Arabic version of our site has been looking awkward in its left-to-right (<code>ltr<\/code>) orientation so far. Arabic, Hebrew, Maldivian, and other languages are laid out right-to-left (<code>rtl<\/code>). Let\u2019s accommodate <code>rtl<\/code> languages in our app. We\u2019ll do so via a simple custom hook.<\/p>\n<p>First, let\u2019s grab the rtl-detect NPM package, which we\u2019ll use in our hook.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-49\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">npm install rtl-detect\nnpm install --save-dev @types\/rtl-detect\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-49\"><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_90c2e5a772697350b27b53f5fddfd2b1\" 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>On to our custom hook:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-50\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ app\/_hooks\/useTextDirection.ts<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { useLocale } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next-intl\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { isRtlLang } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"rtl-detect\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> type TextDirection = <span class=\"hljs-string\">\"ltr\"<\/span> | <span class=\"hljs-string\">\"rtl\"<\/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\">useTextDirection<\/span>(<span class=\"hljs-params\"><\/span>): <span class=\"hljs-title\">TextDirection<\/span> <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> locale = useLocale();\n  <span class=\"hljs-keyword\">return<\/span> isRtlLang(locale) ? <span class=\"hljs-string\">\"rtl\"<\/span> : <span class=\"hljs-string\">\"ltr\"<\/span>;\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-50\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_875dfb4c592bdd1b8366527f63ad4e9d\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>We can now use our new hook in our app layout to ensure the <code>&lt;html dir&gt;<\/code> attribute matches the active locale.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-51\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/layout.tsx\n\n  \/\/ ...\n  import type { Metadata } from \"next\";\n  import { unstable_setRequestLocale } from \"next-intl\/server\";\n<span class=\"hljs-addition\">+ import useTextDirection from \"..\/_hooks\/useTextDirection\";<\/span>\n\n  \/\/ ...\n\n  export default function LocaleLayout(\n    \/\/ ...\n  ) {\n    unstable_setRequestLocale(locale);\n<span class=\"hljs-addition\">+   \/\/ Make sure this comes after the<\/span>\n<span class=\"hljs-addition\">+   \/\/ unstable_setRequestLocale call<\/span>\n<span class=\"hljs-addition\">+   \/\/ to avoid build errors.<\/span>\n<span class=\"hljs-addition\">+   const dir = useTextDirection();<\/span>\n\n    return (\n<span class=\"hljs-deletion\">-     &lt;html lang={locale}&gt;<\/span>\n<span class=\"hljs-addition\">+     &lt;html lang={locale} dir={dir}&gt;<\/span>\n        &lt;body className=\"...\"&gt;\n          &lt;Header \/&gt;\n          {children}\n        &lt;\/body&gt;\n      &lt;\/html&gt;\n    );\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-51\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_4187c423a810255dd4bca0826f27ef69\" 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 in place, the <code>html<\/code> tag will have <code>dir=\"ltr\"<\/code> when we\u2019re on an English route and <code>dir=\"rtl\"<\/code> for Arabic routes.<\/p>\n<figure id=\"attachment_82437\" aria-describedby=\"caption-attachment-82437\" style=\"width: 1024px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-large wp-image-82437\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/rtl-home-1024x665.png\" alt=\"Our home page flowing in a right-to-left orientation.\" width=\"1024\" height=\"665\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/rtl-home-1024x665.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/rtl-home-300x195.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/rtl-home-768x499.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/rtl-home.png 1148w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption id=\"caption-attachment-82437\" class=\"wp-caption-text\">Browsers automatically flow the page right-to-left when dir=&#8221;rtl&#8221;.<\/figcaption><\/figure>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong> There\u2019s more to <code>rtl<\/code> than just setting the <code>&lt;html dir&gt;<\/code> attribute. We often need to consider which CSS selectors to use for a locale or text direction and how they affect our layouts. Our <a href=\"https:\/\/phrase.com\/blog\/posts\/how-do-i-use-a-css-file-for-site-localization\/\">CSS Localization<\/a> guide goes into detail about this and more.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-work-with-dynamic-values-in-translation-messages\"><\/span>How do I work with dynamic values in translation messages?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>So far we\u2019ve used static text in our translation messages. To inject runtime values into a translation we can use the\u00a0<code>{variable}<\/code>\u00a0interpolation syntax. Let\u2019s add a mock user greeting to our home page to demonstrate.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-52\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/en-us.json\n\n{\n  \/\/ ...\n  \"WeatherAlerts\": {\n    \"title\": \"Weather Alerts\"\n  },\n<span class=\"hljs-addition\">+ \"Home\": {<\/span>\n<span class=\"hljs-addition\">+   \/\/ `{name}` will be replaced at runtime<\/span>\n<span class=\"hljs-addition\">+   \"userGreeting\": \"\ud83d\udc4b Welcome, {name}!\"<\/span>\n<span class=\"hljs-addition\">+ },<\/span>\n  \/\/ ...\n }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-52\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_f81f4a8284a2a473aff047f7aa82792e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-53\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/ar-eg.json\n\n{\n  \/\/ ...\n  \"WeatherAlerts\": {\n    \"title\": \"\u062a\u0646\u0628\u064a\u0647\u0627\u062a \u0627\u0644\u0637\u0642\u0633\"\n  },\n<span class=\"hljs-addition\">+ \"Home\": {<\/span>\n<span class=\"hljs-addition\">+   \"userGreeting\": \"\ud83d\udc4b \u0645\u0631\u062d\u0628\u0627\u064b {name}\"<\/span>\n<span class=\"hljs-addition\">+ },<\/span>\n  \/\/ ...\n }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-53\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_f81f4a8284a2a473aff047f7aa82792e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-54\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/page.tsx\n\n<span class=\"hljs-addition\">+ import { useTranslations } from \"next-intl\";<\/span>\n  import { unstable_setRequestLocale } from \"next-intl\/server\";\n  import WeatherAlerts from \"..\/_components\/WeatherAlerts\/WeatherAlerts\";\n\n  export default function Home({\n    params: { locale },\n  }: Readonly&lt;{ params: { locale: string } }&gt;) {\n    unstable_setRequestLocale(locale);\n\n<span class=\"hljs-addition\">+   const t = useTranslations(\"Home\");<\/span>\n\n    return (\n      &lt;main&gt;\n<span class=\"hljs-addition\">+       &lt;p className=\"...\"&gt;<\/span>\n<span class=\"hljs-addition\">+         \/\/ We supply a key\/value map of<\/span>\n<span class=\"hljs-addition\">+         \/\/ dynamic values we want to replace.<\/span>\n<span class=\"hljs-addition\">+         {t(\"userGreeting\", { name: \"Noor\" })}<\/span>\n<span class=\"hljs-addition\">+       &lt;\/p&gt;<\/span>\n\n        {\/* ... *\/}\n      &lt;\/main&gt;\n    );\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-54\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_ed4d210d66b87c338ff3d9507d917a23\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>We can have as many <code>{variable}<\/code>s in our message as we desire. We just need to make sure that we have an equivalent <code>{variable: \"Value\"}<\/code> in the second param passed to <code>t()<\/code> or our message won\u2019t render correctly.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82443 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/interpolation-en.png\" alt=\"Our English home page, showing the new user greeting reading \u201cWelcome, Noor!\u201d\" width=\"970\" height=\"272\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/interpolation-en.png 970w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/interpolation-en-300x84.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/interpolation-en-768x215.png 768w\" sizes=\"(max-width: 970px) 100vw, 970px\" \/><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82449 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/interpolation-ar.png\" alt=\"Our Arabic home page, showing the new user greeting reading, \u201c\u0645\u0631\u062d\u0628\u0627\u064b Noor\u201d.\" width=\"956\" height=\"276\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/interpolation-ar.png 956w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/interpolation-ar-300x87.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/interpolation-ar-768x222.png 768w\" sizes=\"(max-width: 956px) 100vw, 956px\" \/>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong>\u00a0Reminder that you can <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-intl-demo\/tree\/main?tab=readme-ov-file\">get all the code for our demo app<\/a> from our GitHub repo.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-work-with-localized-plurals\"><\/span>How do I work with localized plurals?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Plurals often need special treatment in translation messages. We need to provide the different plural forms (\u201done message\u201d, \u201cthree messages\u201d) and an integer count that is used to select the correct form. next-intl wisely implements plurals with the flexible <a href=\"https:\/\/phrase.com\/blog\/posts\/guide-to-the-icu-message-format\/\">ICU Message Format<\/a>, which looks like the following:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-55\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\"messageCount\":\n  \"{count, plural,\n    one {One message}\n    other {# messages}\n  }\"\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-55\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_74e4dedd2b97edf1ca4359a218daaf6c\" 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>English has two plural forms: <code>one<\/code> and <code>other<\/code>, which we specify above.  We can then utilize the above English message with a call like <code>t(&quot;messageCount&quot;, { count: 4 })<\/code>. next-intl would then select the <code>other<\/code> form and render \u201c4 messages\u201d. Note that the <code>count<\/code> variable will replace the special <code>#<\/code> character in our messages.<\/p>\n<p>\ud83d\uddd2\ufe0f <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0We can\u2019t have line breaks in our JSON files. The above line breaks were added to clarify the formatting. We\u2019ll see a message like this in JSON in a moment.<\/p>\n<p>For a concrete example, let\u2019s add a weather alert counter to our weekly weather page. First, we\u2019ll add the plural translation messages.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-56\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/en-us.json\n\n{\n  \/\/ ...\n  \"Home\": {\n    \"userGreeting\": \"\ud83d\udc4b Welcome, {name}!\"\n  },\n  \"Week\": {\n<span class=\"hljs-addition\">+   \"alertCount\": \"{count, plural, =0 {No alerts} one {One alert!} other {# alerts!}}\",<\/span>\n    \"sunny\": \"Sunny\",\n    \"cloudy\": \"Cloudy\",\n    \/\/ ...\n  }\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-56\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_76641a20e0c7b77cb294903c9e843069\" 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 use a special <code>=n<\/code> plural form where <code>n<\/code> is an integer override, allowing us to target a specific <code>count<\/code> value not covered by the language\u2019s built-in plural forms. The <code>=0<\/code> form above is a special zero count message that will override the <code>other<\/code> form when <code>count === 0<\/code>.<\/p>\n<p>Now for the Arabic message. Unlike English, Arabic has <strong>six<\/strong> plural forms! <\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-57\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/ar-eg.json\n\n{\n  \/\/ ...\n  \"Home\": {\n    \"userGreeting\": \"\ud83d\udc4b \u0645\u0631\u062d\u0628\u0627\u064b {name}\"\n  },\n  \"Week\": {\n<span class=\"hljs-addition\">+   \"alertCount\": \"{count, plural, zero {\u0644\u0627 \u062a\u0648\u062c\u062f \u062a\u0646\u0628\u064a\u0647\u0627\u062a} one {\u064a\u0648\u062c\u062f \u062a\u0646\u0628\u064a\u0647 \u0648\u0627\u062d\u062f} two {\u064a\u0648\u062c\u062f \u062a\u0646\u0628\u064a\u0647\u0627\u0646} few {\u064a\u0648\u062c\u062f {count, number} \u062a\u0646\u0628\u064a\u0647\u0627\u062a} many {\u064a\u0648\u062c\u062f {count, number} \u062a\u0646\u0628\u064a\u0647} other {\u064a\u0648\u062c\u062f {count, number} \u062a\u0646\u0628\u064a\u0647}}\",<\/span>\n    \"sunny\": \"\u0645\u0634\u0645\u0633\",\n    \/\/ ...\n  }\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-57\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_863266cdb763c2835446eb0db304c92e\" 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>Note how we\u2019re using <code>{count, number}<\/code> instead of <code>#<\/code> for <code>count<\/code> interpolation in our Arabic messages. This practice ensures our numbers are rendered in the correct numeral system for the active locale.<\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong>\u00a0The canonical source for languages\u2019 plural forms is the <a href=\"https:\/\/www.unicode.org\/cldr\/charts\/42\/supplemental\/language_plural_rules.html\">CLDR Language Plural Rules chart<\/a>.<\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong>\u00a0This <a href=\"https:\/\/format-message.github.io\/icu-message-format-for-translators\/editor.html\">Online ICU Message Editor<\/a> is handy when formatting plural forms.<\/p>\n<p>Alright, let\u2019s add the weekly alert counter to utilize these messages.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-58\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/week\/page.tsx\n\n\/\/ ...\n\nexport default async function Week({\n  params: { locale },\n}: Readonly&lt;{ params: { locale: string } }&gt;) {\n  \/\/ ...\n\n  const t = await getTranslations(\"Week\");\n\n  return (\n    &lt;main&gt;\n      &lt;div className=\"...\"&gt;\n        &lt;h1 className=\"...\"&gt;{t(\"title\")}&lt;\/h1&gt;\n<span class=\"hljs-addition\">+       &lt;p className=\"...\"&gt;<\/span>\n<span class=\"hljs-addition\">+         {t(\"alertCount\", { count: 3 })}<\/span>\n<span class=\"hljs-addition\">+       &lt;\/p&gt;<\/span>\n      &lt;\/div&gt;\n      &lt;div className=\"...\"&gt;\n        {\/* ... *\/}\n      &lt;\/div&gt;\n    &lt;\/main&gt;\n  );\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-58\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_73724181df4a7aeb9217f26bd550b728\" 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>Given the value of <code>count<\/code> and the <code>active<\/code> locale, we\u2019ll see our new alert counter rendered with the correct plural form.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-large wp-image-82455 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/plural-forms-1024x756.png\" alt=\"The image illustrates the pluralization rules for both Arabic and English. Arabic has six categories for plurals\u2014zero, one, two, few, many, and other\u2014each with its own unique phrase for 'alerts'. English, in contrast, has three categories\u2014zero (no alerts), one (one alert), and other (used for numbers greater than one, e.g., three alerts).\" width=\"1024\" height=\"756\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/plural-forms-1024x756.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/plural-forms-300x221.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/plural-forms-768x567.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/plural-forms.png 1184w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<h3>Notes and resources<\/h3>\n<ul>\n<li>For more on the kinds of plurals next-intl supports, check out the <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/usage\/messages#cardinal-pluralization\">Cardinal pluralization<\/a> and <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/usage\/messages#ordinal-pluralization\">Ordinal pluralization<\/a> sections of the official docs.<\/li>\n<li>If you\u2019re wondering what the ICU Message Format is, our <a href=\"https:\/\/phrase.com\/blog\/posts\/guide-to-the-icu-message-format\/\">Practical Guide to the ICU Message Format<\/a> has you covered.<\/li>\n<li>We cover plural localization in greater detail in our dedicated <a href=\"https:\/\/phrase.com\/blog\/posts\/pluralization\/\">Guide to Localizing Plurals<\/a>.<\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-numbers\"><\/span>How do I localize numbers?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Not all regions use Western Arabic numerals (1, 2, 3). For instance, Tamil employs the Tamil numeral system (\u0be6, \u0be7, \u0be8, \u0be9). Symbols for currency and large number separators vary by locale: In India, numbers are often separated by lakhs and crores (1,00,000 and 1,00,00,000) instead of thousands and millions (1,000 and 1,000,000).<\/p>\n<p>\ud83d\uddd2\ufe0f <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0Numbers and dates are often\u00a0<strong>region<\/strong>-specific not language-specific, so use region-specific locale codes. Use\u00a0<code>en-us<\/code>, not\u00a0<code>en<\/code>, for example.<\/p>\n<p>next-intl provides two main ways to format numbers: standalone and in messages. Standalone numbers are formatted using the <code>format.number()<\/code> function. Let\u2019s use this on our home page to localize the day\u2019s temperature.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-59\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/app\/&#91;locale]\/page.tsx\n\n<span class=\"hljs-deletion\">- import { useTranslations } from \"next-intl\";<\/span>\n<span class=\"hljs-addition\">+ \/\/ Import the `useFormatter` hook<\/span>\n<span class=\"hljs-addition\">+ import { useFormatter, useTranslations } from \"next-intl\";<\/span>\n  \/\/ ...\n\n  export default function Home({\n    params: { locale },\n  }: Readonly&lt;{ params: { locale: string } }&gt;) {\n    const t = useTranslations(\"Home\");\n<span class=\"hljs-addition\">+   const format = useFormatter();<\/span>\n\n    unstable_setRequestLocale(locale);\n\n   return (\n    &lt;main&gt;\n      {\/* ... *\/}\n\n      &lt;section className=\"...\"&gt;\n        &lt;div className=\"...\"&gt;\n          &lt;p className=\"...\"&gt;\u2600\ufe0f&lt;\/p&gt;\n          &lt;p className=\"...\"&gt;\n            {t(\"sunny\")}\n          &lt;\/p&gt;\n<span class=\"hljs-deletion\">-         &lt;p className=\"...\"&gt;22\u00b0C&lt;\/p&gt;<\/span>\n<span class=\"hljs-addition\">+         &lt;p className=\"...\"&gt;<\/span>\n<span class=\"hljs-addition\">+           {format.number(22, {<\/span>\n<span class=\"hljs-addition\">+             style: \"unit\",<\/span>\n<span class=\"hljs-addition\">+             unit: \"celsius\",<\/span>\n<span class=\"hljs-addition\">+           })}<\/span>\n<span class=\"hljs-addition\">+         &lt;\/p&gt;<\/span>\n        &lt;\/div&gt;\n      &lt;\/section&gt;\n\n      &lt;WeatherAlerts \/&gt;\n    &lt;\/main&gt;\n  );\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-59\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_0428b4f11065a521fdb439f2eb3b3856\" 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 <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0In async components, we need to use <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/environments\/server-client-components#async-components\">getFormatter<\/a> instead of <code>useFormatter<\/code>.<\/p>\n<p>Under the hood next-intl uses the standard <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/NumberFormat\">Intl.NumberFormat<\/a> to format our numbers, and it passes any options in the second param to <code>format.number()<\/code> along to <code>Intl.NumberFormat<\/code>.<\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong>\u00a0See the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/NumberFormat\/NumberFormat#parameters\">Intl.NumberFormat constructor Parameters<\/a> section of the MDN docs for a listing of all available number formatting options.<\/p>\n<figure id=\"attachment_82461\" aria-describedby=\"caption-attachment-82461\" style=\"width: 918px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82461\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-standalone-numbers.png\" alt=\"An excerpt of our Arabic home page displays a weather icon with a sun and, adjacent to the Arabic word for 'sunny.' The temperature is shown as &quot;\u0662\u0662\u00b0\u0645&quot; within a highlighted box.\" width=\"918\" height=\"248\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-standalone-numbers.png 918w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-standalone-numbers-300x81.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-standalone-numbers-768x207.png 768w\" sizes=\"(max-width: 918px) 100vw, 918px\" \/><figcaption id=\"caption-attachment-82461\" class=\"wp-caption-text\">The Arabic temperature shown in the Eastern Arabic numerals native to Arabic.<\/figcaption><\/figure>\n<figure id=\"attachment_82467\" aria-describedby=\"caption-attachment-82467\" style=\"width: 920px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82467\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-standalone-numbers.png\" alt=\"An excerpt of the home page displaying a weather icon of a sun, accompanied by the word &quot;Sunny&quot; and the temperature &quot;22\u00b0C&quot; within a outlined box.\" width=\"920\" height=\"254\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-standalone-numbers.png 920w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-standalone-numbers-300x83.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-standalone-numbers-768x212.png 768w\" sizes=\"(max-width: 920px) 100vw, 920px\" \/><figcaption id=\"caption-attachment-82467\" class=\"wp-caption-text\">English numbers retain their formatting in Western Arabic numerals.<\/figcaption><\/figure>\n<p>We sometimes need our numbers embedded in translation messages, and next-intl allows us to do this via <a href=\"https:\/\/unicode-org.github.io\/icu\/userguide\/format_parse\/numbers\/skeletons.html\">ICU skeletons<\/a>. These are just formatting patterns that start with <code>::<\/code>. Let\u2019s see them in action as we add a mock \u201cwe\u2019ve been live for this long\u201d message to our About page.<\/p>\n<p>First, the messages:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-60\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/en-us.json\n\n{\n  \/\/ ...\n  \"Week\": {\n    \/\/ ...\n    \"showers\": \"Showers\",\n    \"thunderstorms\": \"Thunderstorms\"\n  },\n  \"About\": {\n<span class=\"hljs-addition\">+   \"liveDuration\": \"We've been live for {duration, number, ::precision-integer} seconds.\"<\/span>\n  }\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-60\"><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_7cca825e22586389fdc23c6872d12486\" 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>Note the <code>{variableName, number [, ::skeleton]}<\/code> syntax. The <code>::precision-integer<\/code> skeleton causes the number to be rounded to the nearest whole number.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-61\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/ar-eg.json\n\n{\n  \/\/ ...\n  \"Week\": {\n    \/\/ ...\n    \"showers\": \"\u0632\u062e\u0627\u062a \u0645\u0637\u0631\u064a\u0629\",\n    \"thunderstorms\": \"\u0639\u0648\u0627\u0635\u0641 \u0631\u0639\u062f\u064a\u0629\"\n  },\n  \"About\": {\n<span class=\"hljs-addition\">+   \"liveDuration\": \"\u0644\u0642\u062f \u0643\u0646\u0627 \u0639\u0644\u0649 \u0627\u0644\u0647\u0648\u0627\u0621 \u0645\u0628\u0627\u0634\u0631\u0629 \u0644\u0645\u062f\u0629 {duration, number, ::precision-integer} \u062b\u0627\u0646\u064a\u0629.\"<\/span>\n  }\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-61\"><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_dd8699c32ff8d59fca2b3e731983d3e6\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Let\u2019s use these messages on our About page.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-62\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/about\/page.tsx\n\nimport { useTranslations } from \"next-intl\";\nimport { unstable_setRequestLocale } from \"next-intl\/server\";\n\nexport default function About({\n  params: { locale },\n}: Readonly&lt;{ params: { locale: string } }&gt;) {\n  unstable_setRequestLocale(locale);\n\n  const t = useTranslations(\"About\");\n\n  return (\n    &lt;main&gt;\n      {\/* ... *\/}\n      &lt;p className=\"...\"&gt;\n<span class=\"hljs-addition\">+       {t(\"liveDuration\", { duration: 17280000.45 })}<\/span>\n      &lt;\/p&gt;\n    &lt;\/main&gt;\n  );\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-62\"><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_16a79962204236eff26a012b16050ee0\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82473 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-number-in-message.png\" alt=\"A screenshot of a portion of our About page reading  &quot;\u0644\u0642\u062f \u0643\u0646\u0627 \u0639\u0644\u0649 \u0627\u0644\u0645\u0648\u0639\u062f \u0645\u0628\u0627\u0634\u0631\u0629\u064b \u0644\u0645\u062f\u0629 \u0662\u0663\u066c\u0663\u0664\u0666 \u062f\u0642\u064a\u0642\u0629\u201d, which is the Arabic translation of our new message. The number in the message is shown in Eastern Arabic numerals and a whole number (no fractional decimals).\" width=\"942\" height=\"112\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-number-in-message.png 942w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-number-in-message-300x36.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-number-in-message-768x91.png 768w\" sizes=\"(max-width: 942px) 100vw, 942px\" \/><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82479 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-number-in-message.png\" alt=\"A screenshot of a portion of our English About page reading &quot;We\u2019ve been live for 17,280,000 seconds.\u201d The number in the message is shown in Western Arabic numerals and a whole number (no fractional decimals).\" width=\"950\" height=\"114\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-number-in-message.png 950w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-number-in-message-300x36.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-number-in-message-768x92.png 768w\" sizes=\"(max-width: 950px) 100vw, 950px\" \/><\/p>\n<p>ICU skeletons offer a lot of convenience and flexibility. Prebuilt skeletons like <code>::currency\/USD<\/code> or <code>::percent<\/code> will use appropriate formatting for the active locale. In addition, ICU skeletons can offer granular control over number formats. For example, the <code>::.##\/@##r<\/code> skeleton will format a number with at most 2 fraction digits, but guarantee at least 3 significant digits.<\/p>\n<h3>Notes and resources<\/h3>\n<ul>\n<li>See the <a href=\"https:\/\/unicode-org.github.io\/icu\/userguide\/format_parse\/numbers\/skeletons.html\">Number Skeletons<\/a> page in the ICU documentation for available skeletons. <strong>Heads up<\/strong>, however: While many ICU skeletons work with next-intl, not all do. Be sure to test out the skeletons to ensure they work.<\/li>\n<li>next-intl allows us to define global custom formats for reuse across our app. Check out the <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/usage\/configuration#formats\">Formats documentation<\/a> for more details.<\/li>\n<li>Our <a href=\"https:\/\/phrase.com\/blog\/posts\/number-localization\/\">Concise Guide to Number Localization<\/a> covers numeral systems, separators, currency, and more.<\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-dates-and-times\"><\/span>How do I localize dates and times?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Date formatting also varies across regions. For instance, the format often used in the United States is <code>MM\/DD\/YYYY<\/code>, whereas many European countries use <code>DD\/MM\/YYYY<\/code>.<\/p>\n<p>\ud83d\uddd2\ufe0f <strong>Note<\/strong> <strong>\u00bb<\/strong>\u00a0Date and time localization is similar to number localization in many ways, so we encourage you to read the previous section if you haven\u2019t already.<\/p>\n<p>Much like numbers, next-intl gives us two main ways of formatting dates and times: standalone and in messages. We\u2019ll cover standalone formatting first. Let\u2019s localize the date on our home page as we do.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-63\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/page.tsx\n\nimport { useFormatter, useTranslations } from \"next-intl\";\n\/\/ ...\n\nexport default function Home({\n  params: { locale },\n}: Readonly&lt;{ params: { locale: string } }&gt;) {\n  const t = useTranslations(\"Home\");\n<span class=\"hljs-addition\">+ \/\/ We use the same `userFormatter`<\/span>\n<span class=\"hljs-addition\">+ \/\/ hook that used did for numbers.<\/span>\n  const format = useFormatter();\n\n  unstable_setRequestLocale(locale);\n\n  return (\n    &lt;main&gt;\n      {\/* ... *\/}\n\n      &lt;h1 className=\"...\"&gt;{t(\"title\")}&lt;\/h1&gt;\n      &lt;h2 className=\"...\"&gt;\n<span class=\"hljs-deletion\">-       Monday April 15 2024<\/span>\n<span class=\"hljs-addition\">+       {format.dateTime(new Date(\"2024-04-15\"), {<\/span>\n<span class=\"hljs-addition\">+         dateStyle: \"full\",<\/span>\n<span class=\"hljs-addition\">+       })}<\/span>\n      &lt;\/h2&gt;\n\n      {\/* ... *\/}\n    &lt;\/main&gt;\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-63\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">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_7b1e827c23e7498ed69f4cf94a8b15c1\" 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>format.dateTime<\/code> function is locale-aware, and will format its given <code>Date<\/code> object per the rules of the active locale. Under the hood, next-intl uses the standard <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/DateTimeFormat\">Intl.DateTimeFormat<\/a>, passing it any options we give it as the second param to <code>t()<\/code>.<\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong>\u00a0See the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/DateTimeFormat\/DateTimeFormat#parameters\">Intl.DateTimeFormat constructor Paramete<\/a><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/Intl\/NumberFormat\/NumberFormat#parameters\">rs<\/a> section of the MDN docs for a listing of available datetime formatting options.<\/p>\n<figure id=\"attachment_82485\" aria-describedby=\"caption-attachment-82485\" style=\"width: 430px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82485\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-date-full.png\" alt=\"A screenshot of part of our English home page showing the text \u201cToday\u2019s weather\u201d with the formatted date underneath reading \u201cMonday, April 15, 2024.\u201d\" width=\"430\" height=\"106\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-date-full.png 430w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-date-full-300x74.png 300w\" sizes=\"(max-width: 430px) 100vw, 430px\" \/><figcaption id=\"caption-attachment-82485\" class=\"wp-caption-text\">The full datetime format for English United States.<\/figcaption><\/figure>\n<figure id=\"attachment_82491\" aria-describedby=\"caption-attachment-82491\" style=\"width: 346px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82491\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-date-full.png\" alt=\"A screenshot of part of our Arabic home page showing the Arabic text meaning \u201cToday\u2019s weather\u201d with the formatted date underneath reading \u201c\u0627\u0644\u0627\u062b\u0646\u064a\u0646\u060c \u0661\u0665 \u0623\u0628\u0631\u064a\u0644 \u0662\u0660\u0662\u0664\u201d (the Arabic date formatted in full).\" width=\"346\" height=\"104\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-date-full.png 346w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-date-full-300x90.png 300w\" sizes=\"(max-width: 346px) 100vw, 346px\" \/><figcaption id=\"caption-attachment-82491\" class=\"wp-caption-text\">The full datetime format for Arabic Egypt.<\/figcaption><\/figure>\n<p>We can embed dates in our translation messages. Let\u2019s add a message to our weekly forecast page that displays the day\u2019s date to demonstrate.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-64\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/en-us.json\n\n{\n  \/\/ ...\n  \"Week\": {\n    \"title\": \"This week's weather\",\n<span class=\"hljs-addition\">+   \"dayDate\": \"{dayDate, date, ::EEEE}\",<\/span>\n    \/\/ ...\n  },\n  \/\/ ...\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-64\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_f81f4a8284a2a473aff047f7aa82792e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-65\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/ar-eg.json\n\n{\n  \/\/ ...\n  \"Week\": {\n    \"title\": \"\u0627\u0644\u0637\u0642\u0633 \u0644\u0647\u0630\u0627 \u0627\u0644\u0623\u0633\u0628\u0648\u0639\",\n<span class=\"hljs-addition\">+   \"dayDate\": \"{dayDate, date, ::EEEE}\",<\/span>\n    \/\/ ...\n  },\n  \/\/ ...\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-65\"><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_7b386206a24840735c0a230febb66f1c\" 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 use the ICU <code>{variable, date [, ::skeleton]}<\/code> syntax to format dates in our message. The <code>::EEEE<\/code> ICU datetime skeleton above should display the date as a long weekday e.g. \u201cThursday\u201d. <\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong>\u00a0next-intl only supports a subset of ICU datetime skeletons. See the <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/usage\/dates-times#dates-and-times-within-messages\">Dates and times within messages<\/a> section of the documentation for a listing of supported skeletons.<\/p>\n<p>Let\u2019s use our new messages on our weekly forecast page.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-66\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/week\/page.tsx\n\n  import type { WeeklyWeatherRoot } from \"@\/types\";\n  import {\n<span class=\"hljs-addition\">+   getFormatter,<\/span>\n    getTranslations,\n    unstable_setRequestLocale,\n  } from \"next-intl\/server\";\n\n  export default async function Week({\n    params: { locale },\n  }: Readonly&lt;{ params: { locale: string } }&gt;) {\n    \/\/ ...\n\n    const t = await getTranslations(\"Week\");\n\n<span class=\"hljs-addition\">+   \/\/ Remember to `await` here.<\/span>\n<span class=\"hljs-addition\">+   const format = await getFormatter();<\/span>\n\n    return (\n      &lt;main&gt;\n        {\/* ... *\/}\n        &lt;div className=\"...\"&gt;\n          {weeklyWeather.map((day) =&gt; (\n            &lt;section key={day.dateTime} className=\"...\"&gt;\n              &lt;h2 className=\"...\"&gt;\n<span class=\"hljs-deletion\">-               {new Date(day.dateTime).toString()}<\/span>\n<span class=\"hljs-addition\">+               {t(\"dayDate\", {<\/span>\n<span class=\"hljs-addition\">+                 dayDate: new Date(day.dateTime),<\/span>\n<span class=\"hljs-addition\">+               })}<\/span>\n              &lt;\/h2&gt;\n              {\/* ... *\/}\n          &lt;\/section&gt;\n        ))}\n      &lt;\/div&gt;\n    &lt;\/main&gt;\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-66\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_3a834f4c1e3d2e483f22f07b286df021\" 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>Recall that we\u2019ll get an error if we call the <code>useFormatter<\/code> hook within our async <code>Week<\/code> component. We have to use the <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/environments\/server-client-components#async-components\">async getFormatter<\/a> instead.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-82497 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-date-in-message.png\" alt=\"A screenshot of our \/en-us\/week page showing the first days of the week with their dates formatted as \u201cMon\u201d, \n\u201dTue\u201d, and \u201cWed\u201d.\" width=\"648\" height=\"718\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-date-in-message.png 648w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-date-in-message-271x300.png 271w\" sizes=\"(max-width: 648px) 100vw, 648px\" \/><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-82503 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-date-in-message.png\" alt=\"A screenshot of our \/ar-eg\/week page showing the first days of the week with their dates formatted as \u201c\u0627\u0644\u0627\u062b\u0646\u064a\u0646\u201d,\n\u201d\u0627\u0644\u062b\u0627\u062b\u0627\u0621\u201d, and \u201c\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621\u201d, which are the Arabic words for \u201cMonday\u201d, \u201cTuesday\u201d and \u201cWednesday\u201d, respectively.\" width=\"534\" height=\"734\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-date-in-message.png 534w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-date-in-message-218x300.png 218w\" sizes=\"(max-width: 534px) 100vw, 534px\" \/><\/p>\n<h3>Notes and resources<\/h3>\n<ul>\n<li>The eagle-eyed reader will have noticed that the English format above does <strong>not <\/strong>show the full day of the week (\u201dMonday\u201d), but the short version instead (\u201dMon\u201d). This seems to be an issue with next-intl at the time of writing. <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-intl-demo\/blob\/64863b7b8a0a920f14a7c1a53dab8faa69cbf59b\/i18n.ts#L30\">We were able to work around it by using a custom global format<\/a>.<\/li>\n<li>By default, the server\u2019s time zone is used when formatting dates. See the <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/usage\/configuration#time-zone\">Time zone<\/a> documentation under Global configuration if you want to use a different time zone.<\/li>\n<li>In addition to absolute datetime formatting, next-intl provides options for formatting <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/usage\/dates-times#relative-times\">relative times<\/a> and <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/usage\/dates-times#date-time-ranges\">date ranges<\/a>.<\/li>\n<li>Our <a href=\"https:\/\/phrase.com\/blog\/posts\/date-time-localization\/\">Guide to Date and Time Localization<\/a> covers the subject in detail.<\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-include-html-in-my-translation-messages\"><\/span>How do I include HTML in my translation messages?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Different languages have different grammar, so it\u2019s often wise to leave internal links or style emphases (italics, bold) for translators to place in messages. However, we don\u2019t want translators to worry about the intricacies of HTML.<\/p>\n<p>next-intl solves this with the <code>t.rich<\/code> function. Let\u2019s use the function to localize the description text in our About page. We can include a link in the description message for each locale:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-67\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/en-us.json\n\n{\n  \/\/ ...\n  \"About\": {\n    \"title\": \"About\",\n<span class=\"hljs-addition\">+   \"description\": \"This is a minimalistic mock weather app built with &lt;linkToNext&gt;Next.js&lt;\/linkToNext&gt;.\",<\/span>\n    \/\/ ...\n  }\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-67\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_f81f4a8284a2a473aff047f7aa82792e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-68\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/ar-eg.json\n\n{\n  \/\/ ...\n  \"About\": {\n    \"title\": \"\u0646\u0628\u0630\u0629 \u0639\u0646\u0627\",\n<span class=\"hljs-addition\">+       \"description\": \"\u0647\u0630\u0627 \u062a\u0637\u0628\u064a\u0642 \u0637\u0642\u0633 \u0648\u0647\u0645\u064a \u0628\u0633\u064a\u0637 \u062a\u0645 \u0625\u0646\u0634\u0627\u0624\u0647 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 &lt;linkToNext&gt;\u0646\u0643\u0633\u062a \u0686\u0649 \u0623\u0633&lt;\/linkToNext&gt;.\",<\/span>\n    \/\/ ...\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-68\"><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_509ae9c78dcf7a7cba66b5ea4a7aef40\" 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>Note that our messages use a custom <code>&lt;linkToNext&gt;<\/code> tag to indicate the text we\u2019re linking. We can call this tag whatever we want. We can also use as many tags as we want in a message as long as we resolve them when we call <code>t.rich<\/code>.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-69\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/about\/page.tsx\n\nimport { useTranslations } from \"next-intl\";\nimport { unstable_setRequestLocale } from \"next-intl\/server\";\n\nexport default function About({\n  params: { locale },\n}: Readonly&lt;{ params: { locale: string } }&gt;) {\n  unstable_setRequestLocale(locale);\n\n  const t = useTranslations(\"About\");\n\n  return (\n    &lt;main&gt;\n      &lt;h1 className=\"...\"&gt;{t(\"title\")}&lt;\/h1&gt;\n      &lt;p className=\"...\"&gt;\n<span class=\"hljs-deletion\">-       This is a minimalistic mock weather app...<\/span>\n<span class=\"hljs-addition\">+       {t.rich(\"description\", {<\/span>\n<span class=\"hljs-addition\">+         linkToNext: (chunks) =&gt; (<\/span>\n<span class=\"hljs-addition\">+           &lt;a<\/span>\n<span class=\"hljs-addition\">+             href=\"https:\/\/nextjs.org\"<\/span>\n<span class=\"hljs-addition\">+             className=\"text-sky-200 underline\"<\/span>\n<span class=\"hljs-addition\">+           &gt;<\/span>\n<span class=\"hljs-addition\">+             {chunks}<\/span>\n<span class=\"hljs-addition\">+           &lt;\/a&gt;<\/span>\n<span class=\"hljs-addition\">+         ),<\/span>\n<span class=\"hljs-addition\">+       })}<\/span>\n      &lt;\/p&gt;\n      {\/* ... *\/}\n    &lt;\/main&gt;\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-69\"><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_0c6f5aa327ca19eb8d1213959f8c32bb\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p><code>t.rich<\/code> takes two parameters: the key of our translation message and a map of tags to resolvers. Each resolver is a simple function that takes as a <code>chunks<\/code> param, a string of the inner contents between <code>&lt;tag&gt;<\/code> and <code>&lt;\/tag&gt;<\/code> in the translation message. This allows using any React component when resolving our custom <code>linkToNext<\/code> tag. Here we\u2019re using simple JSX that outputs an <code>&lt;a&gt;<\/code> tag.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82509 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-rich-text.png\" alt=\"A section of our English About page showing the header, reading &quot;About,&quot; followed by the sentence &quot;This is a minimalistic mock weather app built with Next.js.&quot; The word &quot;Next.js&quot; is presented in blue and underlined.\" width=\"856\" height=\"170\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-rich-text.png 856w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-rich-text-300x60.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-rich-text-768x153.png 768w\" sizes=\"(max-width: 856px) 100vw, 856px\" \/><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82515 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-rich-text.png\" alt=\"A section of our Arabic About page showing the header, reading &quot;\u0646\u0630\u0629 \u0639\u0646\u0627,&quot; (\u201dAbout\u201d) followed by the Arabic translation for &quot;This is a minimalistic mock weather app built with Next.js.&quot; The word &quot;Next.js&quot; is presented in blue and underlined.\" width=\"922\" height=\"168\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-rich-text.png 922w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-rich-text-300x55.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-rich-text-768x140.png 768w\" sizes=\"(max-width: 922px) 100vw, 922px\" \/><\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong>\u00a0The <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/usage\/messages#rich-text\">Rich text<\/a> section of the next-intl documentation covers tag reuse, self-closing tags, and more.<\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong>\u00a0Sometimes we need to output a raw string; we can achieve this with the <code>t.markup<\/code> function. Find out more in the <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/usage\/messages#html-markup\">HTML markup<\/a> section of the next-intl docs.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-localize-page-metadata\"><\/span>How do I localize page metadata?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Since metadata is managed outside of component rendering, we must utilize next-intl\u2019s async\u00a0<code>getTranslator<\/code>\u00a0function to localize it. We\u2019ll need to use Next.js\u2019 async <a href=\"https:\/\/nextjs.org\/docs\/app\/api-reference\/functions\/generate-metadata\">generateMetadata<\/a> function as well.<\/p>\n<p>Let\u2019s localize our layout\u2019s metadata to demonstrate. First, our messages:<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-70\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/en-us.json\n\n{\n  \/\/ ...\n<span class=\"hljs-addition\">+ \"Layout\": {<\/span>\n<span class=\"hljs-addition\">+   \"metaData\": {<\/span>\n<span class=\"hljs-addition\">+     \"title\": \"Next.js Weather\",<\/span>\n<span class=\"hljs-addition\">+     \"description\": \"A weather app built with Next.js and next-intl\"<\/span>\n<span class=\"hljs-addition\">+   }<\/span>\n<span class=\"hljs-addition\">+ },<\/span>\n  \/\/ ...\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-70\"><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_f81f4a8284a2a473aff047f7aa82792e\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-71\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ locales\/ar-eg.json\n\n{\n  \/\/ ...\n<span class=\"hljs-addition\">+ \"Layout\": {<\/span>\n<span class=\"hljs-addition\">+   \"metaData\": {<\/span>\n<span class=\"hljs-addition\">+     \"title\": \"\u062a\u0642\u0635 \u0646\u0643\u0633\u062a \u0686\u0649 \u0625\u0633\",<\/span>\n<span class=\"hljs-addition\">+     \"description\": \"\u062a\u0637\u0628\u064a\u0642 \u0637\u0642\u0633 \u0648\u0647\u0645\u064a \u0628\u0633\u064a\u0637 \u062a\u0645 \u0625\u0646\u0634\u0627\u0624\u0647 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 Next.js \u0648 next-intl.\"<\/span>\n<span class=\"hljs-addition\">+   }<\/span>\n<span class=\"hljs-addition\">+ },<\/span>\n  \/\/ ...\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-71\"><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_ba2ba2a21e2c3dae7ead3ee031c66b27\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now we can use these messages to localize our layout\u2019s metadata.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-72\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ app\/&#91;locale]\/layout.tsx\n\n \/\/ ...\n import { Locale, locales } from \"@\/i18n.config\";\n import {\n<span class=\"hljs-addition\">+  getTranslations,<\/span>\n   unstable_setRequestLocale,\n } from \"next-intl\/server\";\n import useTextDirection from \"..\/_hooks\/useTextDirection\";\n\n export function generateStaticParams() {\n   return locales.map((locale) =&gt; ({ locale }));\n }\n\n<span class=\"hljs-deletion\">- export const metadata: Metadata = {<\/span>\n<span class=\"hljs-deletion\">-   title: \"Next.js Weather\",<\/span>\n<span class=\"hljs-deletion\">-   description:<\/span>\n<span class=\"hljs-deletion\">-     \"A weather app built with Next.js and next-intl\",<\/span>\n<span class=\"hljs-deletion\">- };<\/span>\n\n<span class=\"hljs-addition\">+ \/\/ We pull in the current locale<\/span>\n<span class=\"hljs-addition\">+ \/\/ generated from `generateStaticParms`<\/span>\n<span class=\"hljs-addition\">+ \/\/ or the current request route.<\/span>\n<span class=\"hljs-addition\">+ export async function generateMetadata({<\/span>\n<span class=\"hljs-addition\">+   params: { locale },<\/span>\n<span class=\"hljs-addition\">+ }: {<\/span>\n<span class=\"hljs-addition\">+   params: { locale: Locale };<\/span>\n<span class=\"hljs-addition\">+ }) {<\/span>\n<span class=\"hljs-addition\">+   const t = await getTranslations({<\/span>\n<span class=\"hljs-addition\">+     locale,<\/span>\n<span class=\"hljs-addition\">+     namespace: \"Layout.metaData\",<\/span>\n<span class=\"hljs-addition\">+   });<\/span>\n<span class=\"hljs-addition\">+<\/span>\n<span class=\"hljs-addition\">+   return {<\/span>\n<span class=\"hljs-addition\">+     title: t(\"title\"),<\/span>\n<span class=\"hljs-addition\">+     description: t(\"description\"),<\/span>\n<span class=\"hljs-addition\">+   };<\/span>\n<span class=\"hljs-addition\">+ }<\/span>\n\n export default function LocaleLayout(\n  \/\/ ...\n ) {\n   \/\/ ...\n }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-72\"><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_1cbabe1092379586fe1d65c941eed603\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>Now we can see our site\u2019s title and description translated to the active locale.<\/p>\n<figure id=\"attachment_82521\" aria-describedby=\"caption-attachment-82521\" style=\"width: 1024px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-large wp-image-82521\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-metadata-1024x124.png\" alt=\"A screenshot of the rendered HTML of our home page, showing the &lt;title&gt; and &lt;meta name=\u201ddescription\u201d&gt; attributes with inner values of \u201cNext.js weather\u201d and \u201cA weather app built with Next.js and next-intl\u201d, respectively.\" width=\"1024\" height=\"124\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-metadata-1024x124.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-metadata-300x36.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-metadata-768x93.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/en-metadata.png 1119w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption id=\"caption-attachment-82521\" class=\"wp-caption-text\">Rendered HTML of our English home page.<\/figcaption><\/figure>\n<figure id=\"attachment_82527\" aria-describedby=\"caption-attachment-82527\" style=\"width: 1024px\" class=\"wp-caption aligncenter\"><img loading=\"lazy\" decoding=\"async\" class=\"size-large wp-image-82527\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-metadata-1024x98.png\" alt=\"A screenshot of the rendered HTML of our home page, showing the &lt;title&gt; and &lt;meta name=\u201ddescription\u201d&gt; attributes with inner values of our Arabic translations, \u201c\u062a\u0642\u0635 \u0646\u0643\u0633\u062a \u0686\u0649 \u0625\u0633\u201d and \u201c\u062a\u0637\u0628\u064a\u0642 \u0637\u0642\u0633 \u0648\u0647\u0645\u064a \u0628\u0633\u064a\u0637 \u062a\u0645 \u0625\u0646\u0634\u0627\u0624\u0647 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 Next.js \u0648 next-intl.\u201d, respectively.\" width=\"1024\" height=\"98\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-metadata-1024x98.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-metadata-300x29.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-metadata-768x74.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/ar-metadata.png 1086w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption id=\"caption-attachment-82527\" class=\"wp-caption-text\">Rendered HTML of our Arabic home page.<\/figcaption><\/figure>\n<p>Of course, we can override the layout values by applying the above recipe to any of our page components.<\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong>\u00a0The <a href=\"https:\/\/next-intl-docs.vercel.app\/docs\/environments\/metadata-route-handlers\">Metadata &amp; Route Handlers<\/a> documentation covers metadata files (like OpenGraph images), route handlers, and more.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"how-do-i-make-my-message-keys-type-safe\"><\/span>How do I make my message keys type-safe?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>next-intl supports TypeScript out-of-the-box, but that doesn\u2019t cover our app-specific message keys. By default, these are treated as a string type. We can tighten this up and make <code>t()<\/code> only accept keys we\u2019ve defined in our default translation file. To accomplish this we need to redeclare next-intl\u2019s <a href=\"https:\/\/github.com\/amannn\/next-intl\/blob\/0d5280346cf6d0059d05f4f11a9ac27bb60f999d\/packages\/use-intl\/types\/index.d.ts#L3\">IntlMessages<\/a> type.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-73\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">\/\/ types.ts\n\n<span class=\"hljs-addition\">+ import enUSMessages from \".\/locales\/en-us.json\";<\/span>\n\n<span class=\"hljs-addition\">+ type Messages = typeof enUSMessages;<\/span>\n<span class=\"hljs-addition\">+ declare global {<\/span>\n<span class=\"hljs-addition\">+   interface IntlMessages extends Messages {}<\/span>\n<span class=\"hljs-addition\">+ }<\/span>\n\n  export interface WeeklyWeatherRoot {\n    weeklyWeather: WeeklyWeather&#91;];\n  }\n\n  \/\/ ...\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-73\"><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_7bcec6177922ae2bf4cc6e041304aefb\" 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 particular app feeds weather condition keys to <code>t()<\/code> to map a key like <code>&quot;sunny&quot;<\/code> to its translation, <code>&quot;Sunny&quot;<\/code> (<code>en-us<\/code>) or <code>&quot;\u0645\u0634\u0645\u0633&quot;<\/code> (<code>ar-eg<\/code>). This means we need to update our <code>types.ts<\/code> file so that we\u2019re always passing a compatible key type to <code>t()<\/code>.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-74\" data-shcb-language-name=\"Diff\" data-shcb-language-slug=\"diff\"><span><code class=\"hljs language-diff\">  import enus from \".\/locales\/en-us.json\";\n\n  type Messages = typeof enus;\n  declare global {\n    interface IntlMessages extends Messages {}\n  }\n\n  export interface WeeklyWeatherRoot {\n    weeklyWeather: WeeklyWeather&#91;];\n  }\n\n<span class=\"hljs-addition\">+ export type Condition =<\/span>\n<span class=\"hljs-addition\">+   | \"sunny\"<\/span>\n<span class=\"hljs-addition\">+   | \"cloudy\"<\/span>\n<span class=\"hljs-addition\">+   | \"rainy\"<\/span>\n<span class=\"hljs-addition\">+   | \"thunderstorms\"<\/span>\n<span class=\"hljs-addition\">+   | \"showers\";<\/span>\n\n  export interface WeeklyWeather {\n    dateTime: string;\n<span class=\"hljs-deletion\">-   condition: string;<\/span>\n<span class=\"hljs-addition\">+   condition: Condition;<\/span>\n    conditionIcon: string;\n    temperature: Temperature;\n  }\n\n\/\/ ...\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-74\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Diff<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">diff<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div id=\"acf\/text-block_8048f836e10882a866f7dd9515bdcdaa\" 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 in place, if we try to call <code>t()<\/code> with a key <strong>not <\/strong>defined in our English message file, TypeScript will give us a type error.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-large wp-image-82533 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/type-safe-keys-1024x470.png\" alt=\"A screenshot of VS Code, showing a call of t(\u201dfoo.bar\u201d) resulting in a TypeScript error reading \u201cArgument of type '&quot;foo.bar&quot;' is not assignable to parameter of type 'MessageKeys&lt;{ metaData: { title: string; description: string; }; title: string;\u2026\u201d\" width=\"1024\" height=\"470\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/type-safe-keys-1024x470.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/type-safe-keys-300x138.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/type-safe-keys-768x352.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/type-safe-keys.png 1422w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p>That will do it for this tutorial. Here\u2019s a look at our final localized app.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-82539 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2024\/04\/app-final.gif\" alt=\"Looping animations of all the pages in our app localized in English and Arabic.\" width=\"600\" height=\"425\" \/><\/p>\n<p>\ud83d\udd17 <strong>Resource<\/strong> <strong>\u00bb<\/strong>\u00a0You can <a href=\"https:\/\/github.com\/PhraseApp-Blog\/next-intl-demo\/tree\/main\">get all the code of the demo app<\/a> from our GitHub repo.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"push-the-boundaries-of-nextjs-localization\"><\/span>Push the boundaries of Next.js localization<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We hope you\u2019ve found this guide to localizing Next.js with the App Router and next-intl library helpful.<\/p>\n<p>When you\u2019re all set to start the translation process, rely on the Phrase Localization Platform to handle the heavy lifting. A specialized software localization platform, the Phrase Localization Platform comes with dozens of tools designed to automate your translation process and native integrations with platforms like GitHub, GitLab, and Bitbucket.<\/p>\n<p>With its user-friendly strings editor, translators can effortlessly access your content and transfer it into your target languages. Once your translations are set, easily integrate them back into your project with a single command or automated sync.<\/p>\n<p>This way, you can stay focused on what you love\u2014your code. <a href=\"https:\/\/eu.phrase.com\/idm-ui\/signup?uiLang=en-US\">Sign up for a free trial<\/a> and see for yourself why developers appreciate using The Phrase Localization Platform for software localization.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Take a deep dive into Next.js localization using an App Router demo and next-intl. We&#8217;ll explore routing, Server and Client Components, and much more.<\/p>\n","protected":false},"author":41,"featured_media":72432,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_stopmodifiedupdate":false,"_modified_date":"","_searchwp_excluded":"","footnotes":""},"categories":[40],"class_list":["post-68190","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\/68190"}],"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=68190"}],"version-history":[{"count":60,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/68190\/revisions"}],"predecessor-version":[{"id":82551,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/68190\/revisions\/82551"}],"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=68190"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/categories?post=68190"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}