Internationalization implementation with Next JS (13.5.6) & typescript

Hey there!

I've recently started working on our website, which is built using Next JS and TypeScript (specifically, Next 13.5.6). I followed this tutorial to set it up: https://www.youtube.com/watch?v=nfZu56KsK_Q

My current challenge is to implement internationalization with Prismic, so we can smoothly handle both English and French languages on our website. However, I've scoured YouTube and community forums, but I can't seem to find any tutorials or guidance on this specific topic.

That's why I'm reaching out here – I'm wondering if there's anyone who knows how to tackle this and might be interested in helping me out. :blush:

You can find the repository here: https://github.com/polyphoniamusic/polyphonia-prismic-website

P.S. Since I'm still a beginner, please bear in mind that my code might not be top-notch just yet :grin:

A big thanks in advance!

Andy

Hi Andy,

We've got documentation on how to set this up here :slight_smile:

Thanks.

Hey Phil,

Thank you again for replying.

I already read this doc a few days ago, but since the project uses typescript (and because I'm a beginner/noob aha) I can't find a way to adapt it to my code :/

Hey Andy

In your code I see that your are using the new Next.js App router.
Therefore the current Prismic Next.js internationalization documentation is outdated.

I believe that the following blog post might help you out. :slight_smile:

1 Like

Hello Andre,

Thank you so much for this! :blush:

I followed all the tutorial but unfortunately I still have some errors;

There is no homepage appearing when I go to:

  • localhost:3000 & localhost:3000/fr-fr
    (but there is one for this path: localhost:3000/en-US)

But all the other paths are working (for example):

  • localhost:3000/fr-fr/blog & localhost:3000/en-US/blog
  • localhost:3000/fr-fr/blog/spotify-new-feature-discovery-mode & localhost:3000/en-US/blog/spotify-new-feature-discovery-mode
  • localhost:3000/fr-fr/artists & localhost:3000/en-US/artists
  • localhost:3000/fr-fr/artists/blurblur & localhost:3000/en-US/artists/blurblur

It seems like the main language found is french, which is great, but no homepage are being loaded for this one :confused:

Here is the link to the repo with the tutorial edits added: https://github.com/polyphoniamusic/polyphonia-prismic-website/tree/production

Would you consider looking at what I've done to tell me if I've forgotten or done something wrong?

Thank you so much for your help! :face_holding_back_tears:

Here is the updated repo, where I still have the problem of my latest message :slight_smile: https://github.com/polyphoniamusic/polyphonia-website-prismic-lang

@polyphoniamusic.mgmt

The sample project should help you out with this:

Hello, I'm new to the world of php and I'm discovering the power of React/NextJs/Prismic. I followed the Next.js Full Website Tutorial Course which is just incredible. Thanks Alex Trost :)
I then internalized my POC. It works well for "classic" slice. But on the other hand, I'm having trouble with language switching in a slice with "content relationship" like the Testimonials slice.
How can I switch languages? For now, I'm passing it in raw.

Here's my current code

const Testimonials =  async ({ slice }: TestimonialsProps): Promise<JSX.Element> => {

  const client = createClient();
  const lang = "en-us";
  const testimonials = await Promise.all(
      slice.items.map((item) =>{
        if(
            isFilled.contentRelationship(item.testimonial) && item.testimonial.uid
        ){
          return client.getByUID("testimonial",item.testimonial.uid, { lang })
        }
      } )
  )

  return ();

};

export default Testimonials;

Hey Micha! So glad you enjoyed the course!

I think the easiest way to get the lang to your Testimonials slice would be to use the context prop on the SliceZone component.

I'm assuming you've changed your page to match this file: nextjs-starter-prismic-multi-language/src/app/[lang]/page.js at adbba0fd6d1c01e958d9ed2ce2bbbff844245633 · prismicio-community/nextjs-starter-prismic-multi-language · GitHub
As well as added the middleware and whatever else the multi-lang starter has, as you said you got it working. (Awesome job btw!)

Now you need to add lang to the SliceZone. I'm copying the code from the multi-page starter:

export default async function Page({ params: { lang } }) {
  const client = createClient();

  const page = await client.getByUID("page", "home", { lang });
  const navigation = await client.getSingle("navigation", { lang });
  const settings = await client.getSingle("settings", { lang });

  const locales = await getLocales(page, client);

  return (
    <Layout locales={locales} navigation={navigation} settings={settings}>
      <SliceZone slices={page.data.slices} components={components} context={{ lang: lang }} />
    </Layout>
  );
}

Just pay attention to the context={{lang: lang }} bit on the slice zone, you should be able to keep your page component the same beyond that! Also add this to any other SliceZones your project might have, if that applies.

So then your Testimonials component would look more like this:

const Testimonials =  async ({ slice, lang }: TestimonialsProps): Promise<JSX.Element> => {

  const client = createClient();
  // Now getting lang from the page as a prop
  const testimonials = await Promise.all(
      slice.items.map((item) =>{
        if(
            isFilled.contentRelationship(item.testimonial) && item.testimonial.uid
        ){
          return client.getByUID("testimonial",item.testimonial.uid, { lang })
        }
      } )
  )

  return ();

};

export default Testimonials;

Let me know if that works or if you have other questions!

Alex

Hello Alex,
Firstly, delighted to meet you. Your video is so clear and so simple that it makes one want to work with NextJs / Tailwind and especially Prismic after watching it :)
Great job :)

Next, I tested your code and the language switching in context. However, I have the feeling that my context is not being passed into TestimonialsProps.

My page is a bit different from your example because I'm passing the UID in Params. So the params include UID and lang.

type Params = { uid: string; lang: string };

// My [uid]/page.tsx

import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { SliceZone } from '@prismicio/react';
import * as prismic from '@prismicio/client';
import { createClient } from '@/prismicio';
import { components } from '@/slices';
import { getLocales } from '@/utils/getLocales';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import Header from "@/components/Header";
import Footer from "@/components/Footer";

type Params = { uid: string; lang: string };

export default async function Page({ params }: { params: Params }) {
    const client = createClient();
    const lang = params.lang;
    const page = await client.getByUID('page', params.uid, {
            lang: lang,
    })
    .catch(() => notFound());

    const locales = await getLocales(page, client);

    return (
        <>
            <LanguageSwitcher locales={locales} />
            <Header lang={lang} locales={locales} />
            <SliceZone slices={page.data.slices} components={components} context={{ lang: lang }}/>
            <Footer lang={lang} />
        </>
    );
}

And this is my Testimonial Slice

import {Content, isFilled} from "@prismicio/client";
import {JSXMapSerializer, PrismicRichText, SliceComponentProps} from "@prismicio/react";
import Bounded from "@/components/Bounded";
import Heading from "@/components/Heading";
import {createClient} from "@/prismicio";
import {PrismicNextImage} from "@prismicio/next";

/**
 * Props for `Testimonials`.
 */
export type TestimonialsProps = SliceComponentProps<Content.TestimonialsSlice>;

const Testimonials =  async ({ slice, lang }: TestimonialsProps): Promise<JSX.Element> => {

    const client = createClient();
    const testimonials = await Promise.all(
      slice.items.map((item) =>{
        if(
            isFilled.contentRelationship(item.testimonial) && item.testimonial.uid
        ){
          return client.getByUID("testimonial",item.testimonial.uid, { lang })
        }
      } )
  )

	return ();

};

I don't understand what I'm doing wrong.

I think I need to change export type TestimonialsProps

export type TestimonialsProps = SliceComponentProps<Content.TestimonialsSlice>;

Kr

Micha

You can see the files on my GitHub Repo:

Hey Micha! Thanks for the super kind words, I'm thrilled you found the tutorial useful and that you're continuing to build upon it!

I'm SO sorry for steering you in the wrong direction, you had it and I gave you some bad code!

Instead of { slice, lang } it should have been { slice, context } where lang is at context.lang

Thanks for sharing your code, it helped me debug it quickly! Here's the testimonial slice I got working with both languages:

import { Content, isFilled } from "@prismicio/client";
import {
  JSXMapSerializer,
  PrismicRichText,
  SliceComponentProps,
} from "@prismicio/react";
import Bounded from "@/components/Bounded";
import Heading from "@/components/Heading";
import { createClient } from "@/prismicio";
import { PrismicNextImage } from "@prismicio/next";

const components: JSXMapSerializer = {
  heading2: ({ children }) => (
    <Heading as="h2" size="md" className="text-center mb-12">
      {children}
    </Heading>
  ),
  paragraph: ({ children }) => (
    <p className="text-lg md:test-2xl font-normal font-body text-slate-600 mb-8">
      {children}
    </p>
  ),
};

/**
 * Props for `Testimonials`.
 */
export type TestimonialsProps = SliceComponentProps<Content.TestimonialsSlice>;

/**
 * Component for "Testimonials" Slices.
 */
const Testimonials = async ({
  slice,
  context,
}: TestimonialsProps): Promise<JSX.Element> => {
  const client = createClient();
  const testimonials = await Promise.all(
    slice.items.map((item) => {
      if (
        isFilled.contentRelationship(item.testimonial) &&
        item.testimonial.uid
      ) {
        return client.getByUID("testimonial", item.testimonial.uid, {
          // @ts-ignore
          lang: context.lang,
        });
      }
    })
  );

  return (
    <Bounded
      data-slice-type={slice.slice_type}
      data-slice-variation={slice.variation}
    >
      <PrismicRichText field={slice.primary.heading} components={components} />
      <div className="grid lg:grid-cols-3 grid-cols-1 gap-8">
        {testimonials.map(
          (item, index) =>
            item && (
              <div
                key={index}
                className="border bg-white shadow-lg rounded-lg px-6 md:px-10 py-10 md:py16 grid content-between"
              >
                <PrismicRichText
                  field={item.data.quote}
                  components={components}
                />
                <div className="flex items-center">
                  <PrismicNextImage
                    width={80}
                    height={80}
                    field={item.data.avatar}
                    className="rounded-full mr-4"
                    imgixParams={{ ar: "1:1", fit: "crop" }}
                  />
                  <div>
                    <p className="text-base text-slate-700">{item.data.name}</p>
                    <p className="text-base text-slate-500">
                      {item.data.job_title}
                    </p>
                  </div>
                </div>
              </div>
            )
        )}
      </div>
    </Bounded>
  );
};

export default Testimonials;

You'll notice the // @ts-ignore - That's just to get your page working for now, but I'll ask my colleague @angeloashmore to advise on how best to update that value.

Hopefully this helps!

1 Like

Hey! Here's a simple way to type the context prop:

  1. Create a shared SliceContext type somewhere in your project. Something like src/types.ts works. The type should contain the data you pass to <SliceZone>'s context prop.

    // src/types.ts
    
    export type SliceContext = {
      lang: string
    }
    
  2. Use that type in your slice prop types:

    // src/slices/Testimonials/index.tsx
    import { SliceContext } from "@/types";
    
    export type TestimonialsProps = SliceComponentProps<Content.TestimonialsSlice, SliceContext>;
    
    // ...
    

Now, the context prop passed to the slice component is typed as SliceContext!

2 Likes

Hello,

Nice Testimonials solved ;)

But I still have the probleme with LatestNews.

I define SliceContext

import { SliceContext } from "@/types/SliceContext";

I put the same code as Testimonials but it doesn't work :/

export type LatestNewsProps = SliceComponentProps<Content.LatestNewsSlice, SliceContext>;

/**
 * Component for "LatestNews" Slices.
 */
const LatestNews =  async ({ slice, context }: LatestNewsProps): Promise<JSX.Element> => {

  const client = createClient();

    const news = await client.getAllByType("news", {
    limit: 3,
    lang: context.lang
  });

  return ()

};

export default LatestNews;

@micha Maybe you could restart your TypeScript server? If you are using VS Code, here are instructions.

The code there looks correct, assuming your imports are correct. If it still doesn't work, could you post the whole file, including the imports?

1 Like

It's fixed :) Thanks to both of you. I really like Priscmic. We're starting our first customer project :)
Have a nice day

2 Likes

Really glad to hear that! :slight_smile: