Starter Kits

Multi-Language Blog

The Multi-Language Blog Starter Kit is similar to the standard blog kit but with the additional technical setup required for multi-language support using the Next-Intl package.

All our starter kits follow a similar setup, so if you've followed the setup guide using the Minimal kit so far, you should be able to switch to this one without much hassle and just configure the additional multi-language setting to suit your needs.

View Demo

Explore the live demo site.

Contento Library

Deploy this kit in Contento.

GitHub Repo

Clone the repo on GitHub.

Given the only difference between this kit and the The Blog starter kit is the addition of multi-language support we will only cover those differences here. Read the Minimal and the The Blog starter kit guides for more info on the rest of the codebase.

i18n in Contento

Settings

This starter kit comes with the Internationalisation and Auto Translation settings switched on and configured in the Contento site settings. English (en) is set as the default language and French (fr) as an example for content in another language.

BETA

As Internationalisation is currently in beta, the default language must remain as en for the time being.

Any other languages you wish to add can be chosen from the dropdown list in the settings and then will need to be updated in the codebase settings which we will cover later in this guide.

URI's

The {language} prefix has been added to the URI pattern of each content_type that requires dynamic language routing. In this case the general_page, blog_landing, blog_post, blog_category and authors all have had the {language} prefix added to the URI in their content type settings.

Content

Once the internationalisation settings have been switched on and configured in your site settings, you can then opt out of it for individual content types. In this kit all content types have internationalisation turned on.

As such when you look at a piece of content in the content tab, you will see the language options in the right hand sidebar (in this case English and French).

We also have Auto Translation switched on in this kit, so our French content was automatically translated from the English content. This happens first when you create a new French version of an English page, and then will stay in sync if you update the English version.

Content Links

Currently content links that are selected in a translated piece of content will link to the content selected in the default language (en).

You can see an example of this in the French blog post content for the author and category fields.

In the future the API will swap in expanded related content in the chosen language, rather than just the default.

API Calls

In the lib > contento.ts file, the main createClient helper function that initialises the Contento Client now has an additional optional language parameter which is a string.

lib > contento.ts

export function createClient(isPreview: boolean = false, language?: string) {
  return createContentoClient({
    apiURL: process.env.CONTENTO_API_URL ?? '',
    apiKey: process.env.CONTENTO_API_KEY ?? '',
    siteId: process.env.CONTENTO_SITE_ID ?? '',
    isPreview: isPreview,
    language: language,
  })
}

When you call createClient() now you can pass in the language code that you want to fetch content in e.g. createClient(false, 'fr').. This will usually be set via a dynamic parameter, which is how we are doing it in this kit.

In lib > contento.ts you can see examples of this in use at the bottom of the page for the getBlogPosts and getBlogCategories functions.

export async function getBlogPosts(language?: string): Promise<ContentData[]> {
  return await createClient(false, language)
    .getContentByType({
      contentType: 'blog_post',
    })
    .then((response: ContentAPIResponse) => {
      return response.content
    })
    .catch(() => {
      return []
    })
}

There is also an example in app > [locale] > blog > [slug] of using the createClient helper function within a component and passing in the dynamically set locale from the params object.

type Props = {
  params: {
    locale: string
    slug: string
  }
}

const post = await createClient(draftMode().isEnabled, params.locale)
  .getContentBySlug(params.slug, 'blog_post')
  .catch(() => {
    notFound()
  })

The generateStaticParams function that is used to make slugs available for static rendering has been adapted to output slugs with a locale for each language available in the locales settings in i18n > routing.ts.


import { routing } from '@/i18n/routing'

type LocaleContent = {
  slug: string | null
  locale: string
}

export async function generateStaticParams() {
  return await client
    .getContent({
      params: {
        content_type: ['general_page'],
        limit: '100',
      },
    })
    .then((response: ContentAPIResponse) => {
      const localeArray = [] as LocaleContent[]

      response.content.forEach((content: ContentData) => {
        routing.locales.forEach((locale) => {
          localeArray.push({
            slug: content.slug,
            locale: locale,
          })
        })
      })
      return localeArray
    })
    .catch(() => {
      return []
    })
}

Finally, the generateMetadata function is also now receiving the language within the createClient helper function. In this case we can access props directly and pull it off params.

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  return await createClient(false, params.locale)
    .getContent({
      params: {
        content_type: ['general_page'],
        slug: params.slug,
        limit: '1',
      },
    })
    .then((response: ContentAPIResponse) => {
      return generateSeo(response.content[0])
    })
    .catch(() => {
      return {}
    })
}

Next-Intl

The popular Next-Intl package has been used to handle internationalisation for this starter kit using their i18n routing for app router method but without a src folder.

Below you will find the key areas it has been implemented but please refer to their documentation for further details or if you wish to make your own adjustments.

Locale

This starter kit uses a {language} prefix method for routing e.g. https://www.yourwebsite/blog for the default language (English) and https://www.yourwebsite/fr/blog for the translated French content. There are other methods available, but this is our recommended setup.

The addition of a [locale] folder to the routing structure supports the dynamic {language} prefix in the URI settings. The page.tsx, layout.tsx and not-found.tsx files and blog folder have been moved into the app > [locale] folder.

Middleware

The matcher array in middleware.ts has pathnames for en and fr, if you have updated the language options in Contento then add the corresponding language code to this file e.g. '/(de|fr|en)/:path\*' if you added German.

middleware.ts

export const config = {
  // Match only internationalized pathnames
  matcher: [
    // Match root paths
    '/',
    '/(fr|en)/:path*',
    // Match all pathnames except for
    // - … if they start with `/api`, `/_next` or `/_vercel`
    // - … the ones containing a dot (e.g. `favicon.ico`)
    '/((?!api|_next|_vercel|.*\\..*).*)',
  ],
}

i18n folder

The i18n folder contains the files routing.ts and request.ts.

Routing

The defineRouting function provided by Next-Intl can be seen in i18n > routing.ts. It has settings for the following:

locales

This is a list of all the supported locale codes that are set up in Contento for this kit. This examples uses just en and fr, if you add your own additional language options then please add the associated locale code to this array.

defaultLocale

This setting sets a default locale that will be used if a user tries to access a locale that is not available. This is set to en in line with the requirements in Contento that currently the default languages remains English.

localePrefix

This being set to as-needed allows the slugs for the default language en to be shown without a prefix e.g https://www.yourwebsite.com/en becomes https://www.yourwebsite.com.

Other Info

The Locale type is used in the <LanguageSelector /> component in the Header.tsx file.

The createSharedPathnamesNavigation function creates a wrapper around the commonly used Next.js navigation API's so that the locale prefix routing configuration is handled appropriately when clicking links and redirecting etc.

i18n > routing.ts

import { defineRouting } from 'next-intl/routing'
import { createSharedPathnamesNavigation } from 'next-intl/navigation'

export const routing = defineRouting({
  // A list of all locales that are supported
  locales: ['en', 'fr'],

  // Used when no locale matches
  defaultLocale: 'en',

  // Routes /en as the default to /
  localePrefix: 'as-needed',
})

export type Locale = typeof routing.locales[number]

// Lightweight wrappers around Next.js' navigation APIs
// that will consider the routing configuration
export const { Link, redirect, usePathname, useRouter } =
  createSharedPathnamesNavigation(routing)

Requests

The getRequestConfig function provided by Next-Intl can be seen in the i18n > request.ts file. This function is used for static language translation for things that do not need to be editable in Contento. It validates if the locale being requested is one available in the locales settings in the defineRouting function in i18n > routing.ts and if it is, returns any locale dictionaries available for that locale that can be found in the messages folder. Otherwise, it returns notFound().

i18n > request.ts

export default getRequestConfig(async ({ locale }) => {
  // Validate that the incoming `locale` parameter is valid
  if (!routing.locales.includes(locale as any)) notFound()

  return {
    messages: (await import(`../messages/${locale}.json`)).default,
  }
})

Messages

The messages folder contains dictionary files for both English and French to show an example of how static translation can be done for content on the website that does not need to be editable in Contento and is typically hard-coded.

It is used as an example for the Copyright in the footer and the CTA button on the Blog Cards on the Blog Landing page.

There are json files for each language available such as en.json and fr.json.

They are accessed within a component by using the useTranslations hook provided by Next-Intl, which then declares the part of the dictionary object you wish to access and then references the key for the text you wish to output within the JSX.

messages > fr.json

{
  "FooterInfo": {
    "copyright": "© Droits d'auteur - Traduction statique exemple"
  },
  "Blog": {
    "cta": "En savoir plus"
  }
}

components > Footer.tsx

import { useTranslations } from 'next-intl'

export default function Footer({ footerNav }: { footerNav: ContentData }) {
  const t = useTranslations('FooterInfo')
  return (
    <div className="flex w-full flex-col space-y-9 bg-zinc-200/40 px-6 py-9 md:flex-row md:items-center md:justify-between md:space-y-0 md:px-28">
      <FooterNav footerNav={footerNav} />
      <div>
        <p className="text-md font-semibold text-zinc-600">
          {t('copyright')} {new Date().getFullYear()}
        </p>
      </div>
    </div>
  )
}

Accessing all locales in generateStaticParams()

The unstable_setRequestLocale() function from Next-Intl is used in all page() components within page.tsx files where a generateStaticParams() function is in use for static rendering. This is the recommended method from Next-Intl, which provides a temporary API to distribute the locale received from params.

Please check out their documentation for more information.

app > [locale] > page.tsx

export default async function page({ params }: Props) {
  unstable_setRequestLocale(params.locale)

  const response = await createClient(draftMode().isEnabled, params.locale)
    .getContent({
      params: {
        content_type: ['general_page'],
        slug: params.slug,
        limit: '1',
      },
    })
    .catch(() => {
      notFound()
    })

  const content = response.content[0]

  return <GeneralPage initialContent={content} />
}
Hollie Duncan

Written by Hollie Duncan

Updated

Previous
The Blog