Reputation: 6311
I'm working on a Next.js 13 project with the new /app
folder routing, and attempting to set up internationalization. My project structure looks like this:
This component receives a lang
props parameter from its route and uses it to fetch a language-specific dictionary (i18nDictionary
). The challenge I'm facing is to pass this i18nDictionary
to the children of this component because there we can find my actual components.
import React from 'react';
import { getI18nDictionary } from '@/i18n';
type Props = {
children: React.ReactNode;
params: {
lang: string;
};
};
const AppLayout = 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 AppLayout;
Have a look at {i18nDictionary.home.hero.title}
, that works. I just need to pass the i18nDictionary
down to the pipeline.
Here are my actual components, which means I have to get this 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 AppLayout = ({ children }: Props) => {
return (
<InitializeChakra>
<Header />
{children}
<Footer />
</InitializeChakra>
);
};
export default AppLayout;
Here's my internationalization logic:
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])();
}
How can I pass the i18nDictionary
from the Root AppLayout component to its children props, so that I can use it within the Child AppLayout components, e.g. <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>
);
};
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).*)'],
};
Upvotes: 0
Views: 1100
Reputation: 478
You may consider using react context api, you can create an I18nContext
that provides the i18nDictionary
components of application that needs it
This usually known as prop drill avoiding solutions
Example: Provider part:
src/app/[lang]/layout.tsx
import React from 'react';
import { I18nContext } from '@/contexts/I18nContext';
const AppLayout = async ({ children, params }: Props) => {
const i18nDictionary = await getI18nDictionary(params.lang);
return (
<I18nContext.Provider value={i18nDictionary}>
<html lang={params.lang}>
<body>
{children}
</body>
</html>
</I18nContext.Provider>
);
};
export default AppLayout;
Consumer part:
// src/components/Header.tsx
import { useI18n } from '@/contexts/I18nContext';
export const Header = () => {
const i18nDictionary = useI18n();
...
};
The component:
// src/contexts/I18nContext.tsx
import { createContext, useContext } from 'react';
export const I18nContext = createContext(null);
export const useI18n = () => useContext(I18nContext);
Upvotes: 1