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} />
}