In the midst of working on a cutting-edge Next.js 13 project that utilizes the new /app
folder routing, I am delving into the realm of setting up internationalization. Within my project's structure, it is organized as follows:
https://i.stack.imgur.com/dgnMY.png
- Main AppLayout (src/app/[lang]/layout.tsx)
This pivotal component receives a lang
props parameter from its route and employs it to retrieve a language-specific dictionary (i18nDictionary
). The core challenge lies in passing this essential i18nDictionary
down to the children components, where the actual components are located.
import React from 'react';
import { getI18nDictionary } from '@/i18n';
type Props = {
children: React.ReactNode;
params: {
lang: string;
};
};
const MainAppLayout = async ({ children, params }: Props) => {
const i18nDictionary = await getI18nDictionary(params.lang);
return (
<html lang={params.lang}>
<body>
{children}
{i18nDictionary.home.hero.title}
</body>
</html>
);
};
export default MainAppLayout;
Observe how {i18nDictionary.home.hero.title}
seamlessly works. My main task now entails finding a way to efficiently relay the i18nDictionary
throughout the pipeline.
- Child AppLayout (src/app/[lang]/(main)/layout.tsx)
This is where my genuine components reside, thus necessitating the acquisition of this vital information here one way or another.
import React from 'react';
import type { Metadata } from 'next/types';
import { InitializeChakra } from '@/components/InitializeChakra';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
export const metadata: Metadata = {
title: 'Test',
description: 'Test',
keywords: 'test',
};
type Props = {
children: React.ReactNode;
};
const ChildAppLayout = ({ children }: Props) => {
return (
<InitializeChakra>
<Header />
{children}
<Footer />
</InitializeChakra>
);
};
export default ChildAppLayout;
- i18nDictionary (src/i18n/index.tsx)
Here you can find my methodical approach to managing internationalization:
import { DEFAULT_LOCALE, type SUPPORTED_LOCALES } from '@/middleware';
export type I18nDictionary = {
[page: string]: {
[section: string]: {
[element: string]: string;
};
};
};
export type I18nDictionaryGetter = () => Promise<I18nDictionary>;
const i18nDictionaries: {
[K in (typeof SUPPORTED_LOCALES)[number]]: I18nDictionaryGetter;
} = {
en: () => import('./en.json').then((module) => module.default),
bg: () => import('./bg.json').then((module) => module.default),
};
export async function getI18nDictionary(
locale: string,
): Promise<I18nDictionary> {
return (i18nDictionaries[locale] || i18nDictionaries[DEFAULT_LOCALE])();
}
Question:
My primary query involves figuring out how to effectively transmit the i18nDictionary
from the Main AppLayout component to its children props, thereby enabling me to leverage it within the Child AppLayout components such as <Header />
?
'use client';
import {
Box,
Flex,
Container,
Stack,
useDisclosure,
IconButton,
useColorModeValue,
Icon,
useColorMode,
Heading,
} from '@chakra-ui/react';
import { CloseIcon, HamburgerIcon, SunIcon, MoonIcon } from '@chakra-ui/icons';
import Link from 'next/link';
import { Logo } from '@/components/Logo';
import { TextUnderline } from '@/components/TextUnderline';
import { MobileNav } from '@/components/Header/MobileNav';
import { DesktopNav } from '@/components/Header/DesktopNav';
export const Header = () => {
const { isOpen: isMobileNavOpen, onToggle } = useDisclosure();
const { colorMode, toggleColorMode } = useColorMode();
return (
<Box as="header">
<Flex
as={'header'}
pos="fixed"
top="0"
w={'full'}
minH={'60px'}
boxShadow={'sm'}
zIndex="999"
justify={'center'}
css={{
backdropFilter: 'saturate(180%) blur(5px)',
backgroundColor: useColorModeValue('rgba(255, 255, 255, 0.8)', 'rgba(26, 32, 44, 0.8)'),
}}
>
<Container as={Flex} maxW={'7xl'} align={'center'}>
<Flex
flex={{ base: '0', md: 'auto' }}
ml={{ base: -2 }}
mr={{ base: 6, md: 0 }}
display={{ base: 'flex', md: 'none' }}
>
<IconButton
onClick={onToggle}
icon={isMobileNavOpen ? <CloseIcon w={3} h={3} /> : <HamburgerIcon w={5} h={5} />}
variant={'ghost'}
size={'sm'}
aria-label={'Toggle Navigation'}
/>
</Flex>
<Flex flex={{ base: 1, md: 'auto' }} justify={{ base: 'start', md: 'start' }}>
<Stack
href="/"
direction="row"
alignItems="center"
spacing={{ base: 2, sm: 4 }}
as={Link}
>
<Icon as={Logo} w={{ base: 8 }} h={{ base: 8 }} />
<Heading as={'h1'} fontSize={'xl'} display={{ base: 'none', md: 'block' }}>
<TextUnderline>Quant</TextUnderline> Logistics
</Heading>
</Stack>
</Flex>
<Stack
direction={'row'}
align={'center'}
spacing={{ base: 6, md: 8 }}
flex={{ base: 1, md: 'auto' }}
justify={'flex-end'}
>
<DesktopNav display={{ base: 'none', md: 'flex' }} />
<IconButton
size={'sm'}
variant={'ghost'}
aria-label={'Toggle Color Mode'}
onClick={toggleColorMode}
icon={colorMode == 'light' ? <SunIcon /> : <MoonIcon />}
/>
</Stack>
</Container>
</Flex>
<MobileNav isOpen={isMobileNavOpen} />
</Box>
);
};
middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
export const DEFAULT_LOCALE = 'en';
export const SUPPORTED_LOCALES = ['en', 'bg'];
export const middleware = (request: NextRequest) => {
// Check if there is any supported locale in the pathname
const pathname = request.nextUrl.pathname;
const pathnameHasLocale = SUPPORTED_LOCALES.some(
(locale: string) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
);
if (pathnameHasLocale) return;
// Redirect if there is no locale
const negotiatorHeaders: Negotiator.Headers = {};
request.headers.forEach((value, key) => {
negotiatorHeaders[key] = value;
});
const languages = new Negotiator({
headers: negotiatorHeaders,
}).languages();
const locale = match(languages, SUPPORTED_LOCALES, DEFAULT_LOCALE);
// e.g. incoming request is /products
// The new URL is now /en/products
return NextResponse.redirect(new URL(`/${locale}/${pathname}`, request.url));
};
export const config = {
// Skip all internal paths (_next)
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};