- Published on
- ยท15 min read
Next.js - Creating a Multilingual (i18n) Blog Part 2
I apologize in advance for any awkward expressions in English. ๐
English is not my native language, and I have relied on ChatGPT's assistance to proceed with the translation.
Overview
In the Next.js - Creating a Multilingual (i18n) Blog Part 1 post, we handled multilingual support for the site, excluding blog posts.
For blog posts, we read them from .md(x) files and display them. It's not feasible to manage them using {t('key')}
as discussed earlier.
We need to display registered posts according to the selected language and show post lists, tag lists, etc., differently.
As the content has grown longer, we'll split it into Parts 1 through 4.
In Part 2, we'll explore how to dynamically display blog posts in the selected language.
- Part 1: Installing next-i18next and Multilingual Site Setup
- Part 2: Multilingual Handling for Registered Posts
- Part 3: Handling Rss and Sitemap
- Part 4: Language, Alternate Tags, and Date Format Handling for SEO
I created this project with reference to gitHub, i18n-tailwind-nextjs-starter-blog.
If you've decided to use the Tailwind CSS Blog Starter template and are starting your project from scratch, it might be a good idea to start with the above project.
In my case, since the project was already in progress, I proceeded with it by referring to the mentioned project.
Distinguishing Languages by File Names
- For the default language (
ko
), you can keep the file names the same as before.
(e.g., nextjs-blog-multi-language-part2.mdx) - For other languages (
en
), append.{locale}
to the default language file name.
(e.g., nextjs-blog-multi-language-part2.en
.mdx)
Modifying Common Functions
Modify the common functions used to fetch the post list or retrieve post content based on the slug.
lib/mdx.js
- Add
filterLocaleFiles
- Filter the file list to only include files that match the selected language.
- If it's the default locale, filter files that don't have
.{locale}
appended to their names.
// Filtering Files by Language
function filterLocaleFiles(files, locale, defaultLocale, locales) {
let filterFiles = files
if (locales && defaultLocale && locales) {
filterFiles =
locale !== defaultLocale
? files.filter((path) => path.includes(`.${locale}.`))
: files.filter((path) => !locales.some((locale) => path.includes(`.${locale}.`)))
}
return filterFiles
}
- Modify
getFiles
- Use the newly added
filterLocaleFiles
to filter files that match the current language.
- Use the newly added
export function getFiles(type, locale, defaultLocale, locales) { // Add locale, defaultLocale, locales
const prefixPaths = path.join(root, 'data', type)
const files = getAllFilesRecursively(prefixPaths)
const filterFiles = filterLocaleFiles(files, locale, defaultLocale, locales) // Add
return filterFiles.map((file) => file.slice(prefixPaths.length + 1).replace(/\\/g, '/')) // Change files -> filterFiles
}
- Modify
formatSlug
- Remove the locale (e.g., .en) from the slug (file name).
export function formatSlug(slug, locales = null) {
let result = slug.replace(/\.(mdx|md)/, '')
// Added: Removing the locale from the Slug
if (locales && locales.length > 0) {
locales.forEach((locale) => {
result = result.replace(`.${locale}`, '')
})
}
return result
}
- Modify
getFileBySlug
- To be able to read files that match the selected language, add the locale to the slug.
export async function getFileBySlug(type, slug, locale = null, defaultLocale = null) { // Add locale = null, defaultLocale = null
// Modify it to fetch the files for the selected language if it's not the default locale.
const mdxPath =
locale === defaultLocale
? path.join(root, 'data', type, `${slug}.mdx`)
: path.join(root, 'data', type, `${slug}.${locale}.mdx`)
const mdPath =
locale === defaultLocale
? path.join(root, 'data', type, `${slug}.md`)
: path.join(root, 'data', type, `${slug}.${locale}.md`)
const source = fs.existsSync(mdxPath)
? fs.readFileSync(mdxPath, 'utf8')
: fs.readFileSync(mdPath, 'utf8')
// No other changes are made.
}
- Modify
getAllFilesFrontMatter
- Use
filterLocaleFiles
to filter files that match the current language.
- Use
export async function getAllFilesFrontMatter(folder, locale, defaultLocale, locales) { // Add locale, defaultLocale, locales
const prefixPaths = path.join(root, 'data', folder)
const files = getAllFilesRecursively(prefixPaths)
const filterFiles = filterLocaleFiles(files, locale, defaultLocale, locales) // Add
const allFrontMatter = []
filterFiles.forEach((file) => { // Change filterFiles
// ...
if (frontmatter.draft !== true) {
allFrontMatter.push({
...frontmatter,
slug: formatSlug(fileName, locales), // Add locales to formatSlug
date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
})
}
})
return allFrontMatter.sort((a, b) => dateSortDesc(a.date, b.date))
}
lib/tags.js
- Modify
getAllTags
- Add parameters for locale, defaultLocale, and locales to ensure that filtering is applied.
export async function getAllTags(type, locale, defaultLocale, locales) { // Add locale, defaultLocale, locales
const files = await getFiles(type, locale, defaultLocale, locales) // Add locale, defaultLocale, locales
// ...
}
Modifying Files in the pages Folder
pages/index.js
- Add parameters for locale, defaultLocale, and locales to ensure filtering is applied.
export async function getStaticProps({ locale, defaultLocale, locales }) { // Add locale, defaultLocale, locales
const posts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales) // Add locale, defaultLocale, locales
return { props: { posts } }
}
You can see that the main page list is filtered by language.
pages/blog.js
- Add parameters for locale, defaultLocale, and locales to ensure filtering is applied.
export async function getStaticProps({ locale, defaultLocale, locales }) { // Add defaultLocale, locales
const posts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales) // Add locale, defaultLocale, locales
// ...
}
You can confirm that the entire post list is filtered by language.
pages/blog/page/[page].js
- For dynamic routing, modify the
getStaticPaths
part to generate paths for all languages. (If there are no paths, it will result in a 404 error.)
import { getAllFilesFrontMatter, getFiles } from '@/lib/mdx' // Add getFiles
export async function getStaticPaths({ defaultLocale, locales }) { // Add defaultLocale, locales
// Generate paths for all languages
const localePaths = (
await Promise.all(
locales.map(async (locale) => {
// Change getAllFilesFrontMatter -> getFiles, Add locale, defaultLocale, locales
const posts = await getFiles('blog', locale, defaultLocale, locales)
const totalPages = Math.ceil(posts.length / POSTS_PER_PAGE)
return Array.from({ length: totalPages }, (_, i) => ({
params: { page: (i + 1).toString() },
locale,
}))
})
)
).flat()
// Modify paths
return {
paths: localePaths.map(({ params, locale }) => ({
params,
locale,
})),
fallback: false,
}
}
export async function getStaticProps(context) {
// Add locale, defaultLocale, locales
const {
params: { page },
locale,
defaultLocale,
locales,
} = context
const posts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales) // Add locale, defaultLocale, locales
// ...
}
You can confirm that pagination is generated for each language.
pages/tags.js
- Add parameters for locale, defaultLocale, and locales to ensure filtering is applied.
export async function getStaticProps({ locale, defaultLocale, locales }) { // Add locale, defaultLocale, locales
const tags = await getAllTags('blog', locale, defaultLocale, locales) // Add locale, defaultLocale, locales
return { props: { tags } }
}
You can confirm that tags are generated for each language.
pages/tags/[tag].js
- For dynamic routing, modify the
getStaticPaths
part to generate paths for all languages.
export async function getStaticPaths({ locales, defaultLocale }) { // Add locales, defaultLocale
// Generate paths for all languages
const localeTags = (
await Promise.all(
locales.map(async (locale) => {
const tags = await getAllTags('blog', locale, defaultLocale, locales)
return Object.keys(tags).map((tag) => ({
params: { tag },
locale,
}))
})
)
).flat()
// Modify paths
return {
paths: localeTags.map(({ params, locale }) => ({
params,
locale,
})),
fallback: false,
}
}
export async function getStaticProps({ params, locale, defaultLocale, locales }) { // Add locale, defaultLocale, locales
const allPosts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales) // Add locale, defaultLocale, locales
// ...
}
You can confirm that when selecting a tag, it is generated in each language.
pages/blog/[...slug].js
- For dynamic routing, make sure to modify the
getStaticPaths
part to generate paths for all languages. - Don't forget to add the
locales
parameter when callingformatSlug
.
export async function getStaticPaths({ locales, defaultLocale }) { // Add locales, defaultLocale
// Generate paths for all languages
const localePosts = (
await Promise.all(
locales.map(async (locale) => {
const posts = getFiles('blog', locale, defaultLocale, locales)
return posts.map((post) => ({
params: {
slug: formatSlug(post, locales).split('/'), // Add locales
},
locale,
}))
})
)
).flat()
// Modify paths
return {
paths: localePosts.map(({ params, locale }) => ({
params,
locale,
})),
fallback: false,
}
}
export async function getStaticProps({ params, locale, defaultLocale, locales }) { // Add locale, defaultLocale, locales
const allPosts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales) // Add locale, defaultLocale, locales
const postIndex = allPosts.findIndex(
(post) => formatSlug(post.slug, locales) === params.slug.join('/') // Add locales
)
// ...
const post = await getFileBySlug('blog', params.slug.join('/'), locale, defaultLocale) // Add locale, defaultLocale
// ...
}
You can confirm that posts are displayed in the selected language. (If there are no posts written in the selected language, a 404 error occurs.)
Speed Issue on localhost
It's running slowly when checking on localhost. (I haven't tested it in production yet.)
The cause of this can be found in the Next.js official documentation: getStaticPaths.
Here's an excerpt from the documentation:
In development (next dev), getStaticPaths will be called on every request.
Summary
I've confirmed that visible parts like the list and post viewing are working correctly. (There might still be some missing or erroneous parts. ๐ฅ)
Now, what's left is adding the invisible parts such as SEO, Sitemap, RSS, etc.
In Next.js - Creating a Multilingual (i18n) Blog Part 3, we'll explore how to make SiteMap and RSS multilingual.
Code
The updated code can be found in the commit history.