"Enhancing User Experience: Implementing Internationalization and Nested Layouts in Next.js 13.x

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

  1. 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.

  1. 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;
  1. 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).*)'],
};

Answer №1

If you're looking to streamline the sharing of data throughout your application, consider utilizing the react context api. By creating an I18nContext that houses the i18nDictionary components needed by various parts of your app, you can avoid the need for prop drilling.

This approach is often referred to as prop drill avoidance solutions.

Here's an example of how you can implement this:

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;

And when consuming the context:

// src/components/Header.tsx

import { useI18n } from '@/contexts/I18nContext';

export const Header = () => {
  const i18nDictionary = useI18n();
  ...
};

The context itself:

// src/contexts/I18nContext.tsx

import { createContext, useContext } from 'react';

export const I18nContext = createContext(null);

export const useI18n = () => useContext(I18nContext);

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Arranging a 2D array of matchups to ensure an equal distribution of home and away matches for each team

I am in the process of developing a unique UEFA Champions League 24 'Swiss Model' tournament with 36 teams. Each team is set to compete against 8 different opponents, resulting in a total of 144 matches. I already have a list of matchups prepared ...

TypeScript test framework for testing API calls in VS Code extensions

My VS Code extension in TypeScript uses the Axios library for API calls. I have written tests using the Mocha framework, which are run locally and through Github Actions. Recently, I integrated code coverage reporting with `c8` and I am looking to enhanc ...

Create dynamic breadcrumb trails using router paths

I am currently working on developing a streamlined breadcrumbs path for my application. My goal is to achieve this with the least amount of code possible. To accomplish this, I have implemented a method of listening to router events and extracting data fr ...

Axios withCredentials does not support server-side rendering

I have a unique setup where my application uses Laravel as the backend and NextJs 13 (app router) as the frontend. Utilizing Sanctum in Laravel for login, once the user is logged in, Laravel sends two cookies: one is httpOnly and the other is a client cook ...

Mastering the art of theming components using styled-components and Material-UI

Can you integrate the Material-UI "theme"-prop with styled-components using TypeScript? Here is an example of Material-UI code: const useStyles = makeStyles((theme: Theme) => ({ root: { background: theme.palette.primary.main, }, })); I attemp ...

Google Sign-In error: Access denied due to invalid app request

I've been working on implementing user authentication using next-auth.js with firebase. I followed the documentation to set up a sign-in system using Google as the provider. However, despite entering the correct credentials, Google keeps indicating th ...

Adjusting the timeout for a particular operation according to its unique identifier

I am looking for a solution to call a method that posts an answer after an input change in my Angular project. I want to reset the timeout if another input change occurs to avoid multiple posts. Is there a smart way to achieve this? My project involves po ...

What can be done to stop the caching of the route that router.push() redirects to in Next.js middleware?

Encountering an issue with client-side navigation and middleware in the router. It seems that the router is storing the initial redirect path and subsequent navigations bypass the middleware. This behavior ceases upon browser refresh and does not occur in ...

The element 'x' is implicitly bound with a type of 'any'

I've been exploring the world of Nextjs and TypeScript in an attempt to create a Navbar based on a tutorial I found (). Although I've managed to get the menu items working locally and have implemented the underline animation that follows the mou ...

Determination of the input parameters

Currently, I am developing an Angular application that includes a matInput field for user input. The issue I am encountering is that when the user enters a positive or negative value in the field (e.g. +5 or -5), the final output does not reflect the inten ...

In search of assistance with resolving a React Typescript coding issue

I need help converting a non-TypeScript React component to use TypeScript. When attempting this conversion, I encountered the following error: Class 'Component' defines instance member function 'componentWillMount', but ext ...

Trouble integrating Material UI tabs with Next.js Link component

The link component is essential for integrating the tab functionality in nextjs. However, the tabs can still work smoothly even without using the link component. <Tabs value={value} onChange={handleChange} sx= ...

Typescript can represent both optional and required generic union types

Purpose My goal is to establish an optional parameter unless a specific type is provided, in which case the parameter becomes mandatory. Desired Outcome I aim for the get method below to default to having an optional parameter. However, if a type TT is p ...

Is it possible to use the `new Image()` method in Next.js

Is it possible to use the new Image() function within Next.js? I am encountering an error that states it is not defined. I am aware that I can easily use <Image> inside JSX, but in this particular situation, I am unsure if it will work because I nee ...

Having trouble filtering results in Apollo Client cache, attempted to use readQuery but receiving a null response

Exploring the functionality of the Apollo Client cache has been a priority for me lately. My goal is to avoid unnecessary server calls and improve paging efficiency. The technologies in play here are Next.js, Apollo Client on the front-end, and Keystone.js ...

What is the solution for breaking a querySnapshot in Firestore?

Is there a way to exit a querysnapshot loop prematurely? I attempted using a for loop, but I keep encountering the following error message. How can this error be resolved or is there an alternative method to break out of a snapshot loop? code return ...

Incorporating a custom transpiled file format into Typescript imports

I am trying to import a file format .xyz that does not have fixed types for all instances of the format: import { Comment, Article, User } from "./Blog.xyz" However, I keep getting this error message: TS2307: Cannot find module './Blog.xy ...

Obtain the dimensions (width and height) of a collection of images using Angular and TypeScript

Currently, I am facing an issue with my image upload functionality. My goal is to retrieve the width and height of each image before uploading them. However, I've encountered a problem where my function only provides the dimensions for the first image ...

What is the best method to retrieve a secure httponly cookie in a Next.js application?

Our next JS application is making a request to The backend team has configured the response cookie as a secure HttpOnly cookie. const session = await ( await fetch( `https://demo.com/auth/session`, requestOptions )).json(); console.log(&qu ...

The Next.js function is not operational in the cloud serverless function on Vercel, previously known as Zeit

Struggling to connect with MongoDB hosted on the free tier of atlas? Using nextjs for your API, and attempted to deploy on Vercel (formerly known as Zeit), but encountering issues. Local code runs smoothly, but once deployed to the cloud, it fails to funct ...