i18n-locale-routing

Bilingual routing with next-intl (en/zh)

The YouTeacher landing site serves the same pages in English and Chinese. It uses next-intl on top of Next.js App Router, with the locale carried in the URL path rather than in a cookie or a header. The whole thing hangs off a single set of locale definitions and five small pieces that each do one job.

One source of truth for locales

src/i18n/locales.ts declares the supported locales as ['en', 'zh'], names en as the default, and exports an isValidLocale type guard. Every other piece imports from here, so the list of languages lives in exactly one place.

Locale lives in the URL

Routing is locale-prefixed and always explicit. The middleware (src/middleware.ts) wraps next-intl/middleware with localePrefix: 'always', so a bare / is redirected to a prefixed path (/en) and every real page sits under /en/... or /zh/.... Its matcher covers just / and paths beginning with /en or /zh, so static assets and internal routes are left untouched.

The App Router mirrors this with a [locale] dynamic segment. src/app/[locale]/layout.tsx reads the segment, and if it is not a known locale it calls notFound() — an unknown prefix is a 404, not a silent fallback.

Messages load per request

src/i18n/request.ts is the next-intl request config. For each request it takes the resolved requestLocale, falls back to the default when it is missing or invalid, and then dynamically imports the matching message catalog (src/i18n/messages/<locale>.json). Those catalogs are namespaced JSON — metadata, header, hero, and so on — so components ask for translations by namespace and key rather than by flat string ids.

Prerendered per locale

The layout exports generateStaticParams() over the locale list, so both language variants are prerendered at build time instead of rendered on demand. It calls setRequestLocale(locale) to pin the active locale for the request, then loads the messages and hands them to a client Providers wrapper.

generateMetadata pulls the page title and description from the metadata namespace via getTranslations, and sets the SEO plumbing that a bilingual site needs: a per-locale canonical, alternates.languages linking /en and /zh, and an OpenGraph locale of zh_CN or en_US.

The switcher

src/components/Header.tsx renders the in-header language toggle. It reads the current locale with useLocale() and the current path with usePathname(). Switching languages strips the existing locale prefix off the current path and pushes /<newLocale><rest> — so you stay on the same page, just in the other language, rather than being bounced to the home page.

Why it is shaped this way

Locale-in-URL means each language variant is a real, linkable, indexable URL — which is what the canonical and alternates metadata exist to advertise. Keeping the locale list in one file and deriving the middleware matcher, the static params, and the switcher buttons from it means adding a third language is mostly a data change, not a rewrite.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →