Logo
Published on
·15 min read

Next.js - Creating a Multilingual (i18n) Blog Part 4

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 Next.js - Creating a Multilingual (i18n) Blog Part 3, we made modifications to RSS and Sitemap.

As the content has grown longer, we've split it into Parts 1 through 4.

In Part 4, we will add HTML lang settings and configure alternate settings for the added languages.


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.

html lang

When you view the page source, you can see that it is fixed as follows regardless of the selected language.

<html lang="en" class="scroll-smooth">

Let's make this part automatically change depending on the selected language.

pages/_document.js

  • When you open the file, you'll see that the code is fixed as <Html lang="en">. Remove the lang=".." part.
class MyDocument extends Document {
  render() {
    return (
      <Html className="scroll-smooth"> // Remove lang="en"
        // ...
      </Html>
    )
  }
}

export default MyDocument

After removing it, when you check the page source again, you will see that lang=".." is automatically added according to the selected language.

This is possible for the following reasons.

Search Engine Optimization

Since Next.js knows what language the user is visiting it will automatically add the lang attribute to the <html> tag.

For more detailed information on this, you can refer to the Next.js Internationalization (i18n) Routing: Search Engine Optimization section.

siteMetadata

data/siteMetadata.js

  • If you want to display the text specified in siteMetadata differently for each language, you can configure it by separating it by language as in the description field below.
const siteMetadata = {
  title: 'HLog',
  author: 'hlog',
  headerTitle: 'HLog',
  description: {
    ko: '생활 정보, 개발 정보 등 여러가지 관심사를 다루는 블로그입니다.',
    en: 'This is a blog that covers various interests such as lifestyle information, development updates, and more.',
  },
  // ...
}
  • Modify all places that use siteMetadata.description to accept locale and display it accordingly.
  • Let's take the pages/index.js page code as an example.
export async function getStaticProps({ locale, defaultLocale, locales }) {
  const posts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales)

  return { props: { posts, locale } } // Add locale
}

export default function Home({ posts, locale }) { // Add locale
  return (
    <>
      <PageSEO title={siteMetadata.title} description={siteMetadata.description[locale]} /> // Add locale
      <div className="...">
        <div className="...">
          <p className="...">
            {siteMetadata.description[locale]} // Add locale
          </p>
        </div>
      </div>
      // ...
    </>
  )
}

alternate

When you have pages that use multiple languages, you can use alternate to inform the user.

pages/_document.js

Remove the alternate code as follows. (Remove it here and move the placement to CommonSEO component to add it according to the language.)

{/*<link rel="alternate" type="application/rss+xml" href="/feed.xml" />*/}

components/SEO.js

  • Modify the common SEO component, which is used universally, to receive the list of available languages as the availableLocales parameter.

  • Modify CommonSEO

const CommonSEO = ({
  // ...
  availableLocales, // Add availableLocales
}) => {
  const router = useRouter()
  return (
    <Head>
      // ...
      // Add the alternate tags for the list of available languages.
      {availableLocales &&
        availableLocales.length > 1 &&
        availableLocales.map((locale) => (
          <link
            rel="alternate"
            hrefLang={locale}
            href={`${siteMetadata.siteUrl}${locale === router.defaultLocale ? '' : `/${locale}`}${
              router.asPath
            }`}
            key={locale}
          />
        ))}
      <link
        rel="alternate"
        type="application/rss+xml"
        href={`/feed${router.locale === router.defaultLocale ? '' : `.${router.locale}`}.xml`}
      />
    </Head>
  )
}
  • Modify PageSEO
export const PageSEO = ({ title, description, availableLocales }) => { // Add availableLocales
  // ...
  return (
    <CommonSEO
      // ...
      availableLocales={availableLocales} // Add availableLocales
    />
  )
}
  • Modify TagSEO
export const TagSEO = ({ title, description, availableLocales }) => { // Add availableLocales
  // ...
  return (
    <>
      <CommonSEO
        // ...
        availableLocales={availableLocales} // Add availableLocales
      />
      <Head>
        <link
          // ...
          // Set the feed URL according to the current language.
          href={`${siteMetadata.siteUrl}${router.asPath}/feed${
            router.locale === router.defaultLocale ? '' : `.${router.locale}`
          }.xml`}
        />
      </Head>
    </>
  )
}
  • Modify BlogSEO
export const BlogSEO = ({
  // ...
  availableLocales, // Add availableLocales
}) => {
  // ...

  return (
    <>
      <CommonSEO
        // ...
        availableLocales={availableLocales} // Add availableLocales
      />
      // ...
    </>
  )
}

Update Pages Using PageSEO

  • Pass the availableLocales parameter to pages that use the PageSEO component.
  • Taking pages/blog.js as an example, it would look like this.
// ...

export async function getStaticProps({ locale, defaultLocale, locales }) {
  // ...

  return {
    props: {
      // ...
      availableLocales: locales, // Add availableLocales
    },
  }
}

export default function Blog({ posts, initialDisplayPosts, pagination, locale, availableLocales }) { // Add availableLocales

  return (
    <>
      <PageSEO
        // ...
        availableLocales={availableLocales} // Add availableLocales
      />
      // ...
    </>
  )
}

pages/tags/[tag].js

  • For the tag pages, since they are dynamically generated, you need to find the list of available languages for each specific tag.
// ...

export async function getStaticProps({ params, locale, defaultLocale, locales }) {
  // ...

  // Find the list of available languages.
  const availableLocales = []
  await Promise.all(
    locales.map(async (local) => {
      const tags = await getAllTags('blog', local, defaultLocale, locales)
      if (tags[params.tag] !== undefined) availableLocales.push(local)
    })
  )

  // ...

  return { props: { posts: filteredPosts, tag: params.tag, availableLocales } } // Add availableLocales
}

export default function Tag({ posts, tag, availableLocales }) { // Add availableLocales
  // ...
  return (
    <>
      <TagSEO
        // ...
        availableLocales={availableLocales} // Add availableLocales
      />
      <ListLayout posts={posts} title={title} />
    </>
  )
}

pages/blog/[...slug].js

  • For blog posts, since they are dynamically generated pages, you need to find the list of available languages for each specific post.

You've added a function to filter the available languages as follows:

  • Modify lib/mdx.js: Added the getAvailableLocalesBySlug function.
export async function getAvailableLocalesBySlug(
  type,
  slug,
  locale = null,
  defaultLocale = null,
  locales = null
) {
  return locales
    .map((locale) => {
      const localeExtention = locale === defaultLocale ? '' : `.${locale}`
      const mdxPath = path.join(root, 'data', type, `${slug}${localeExtention}.mdx`)
      const mdPath = path.join(root, 'data', type, `${slug}${localeExtention}.md`)
      if (fs.existsSync(mdxPath) || fs.existsSync(mdPath)) {
        return locale
      }
    })
    .filter((locale) => locale !== undefined)
}
  • Modify pages/blog/[...slug].js
import {
  getAvailableLocalesBySlug, // Add getAvailableLocalesBySlug
  // ...
} from '@/lib/mdx'
// ...

export async function getStaticProps({ params, locale, defaultLocale, locales }) {

  // ...

  // Call the function for filtering available languages.
  const availableLocales = await getAvailableLocalesBySlug(
    'blog',
    params.slug.join('/'),
    locale,
    defaultLocale,
    locales
  )
  // ...

  return {
    props: {
      // ...
      availableLocales, // Add availableLocales
    },
  }
}

export default function Blog({ post, authorDetails, prev, next, relatedPosts, availableLocales }) { // Add availableLocales
  // ...

  return (
    <>
      {frontMatter.draft !== true ? (
        <MDXLayoutRenderer
          // ...
          availableLocales={availableLocales} // Add availableLocales
        />
      ) : (
        <div className="mt-24 text-center">
          // ...
        </div>
      )}
    </>
  )
}
  • Modify the layout components used in MDXLayoutRenderer.
    • Taking layouts/PostLayout.js as an example, it would look like this.
// ...

export default function PostLayout({
  // ...
  availableLocales, // Add availableLocales
}) {
  // ...

  return (
    <SectionContainer>
      <BlogSEO
        // ...
        availableLocales={availableLocales} // Add availableLocales
      />

      // ...
    </SectionContainer>
  )
}

Change Date Format

  • Modify the code to use locale instead of siteMetadata.locale when calling toLocaleDateString.

  • Taking lib/utils/formatDate.js as an example, it would look like this.

// ...
const formatDate = (date, locale) => { // Add locale
  const options = {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }
  const now = new Date(date).toLocaleDateString(locale ? locale : siteMetadata.locale, options) // Change locale

  return now
}

export default formatDate
  • Let's also provide an example for layouts/PostLayout.js.
// ...
import { useRouter } from 'next/router' // Add useRouter

// ...

export default function PostLayout({ // ...
}) {
  const { slug, fileName, date, title, images, tags } = frontMatter
  const { locale } = useRouter() // Add locale

  return (
    <SectionContainer>
      // ...
      <ScrollTopAndComment />
      <article>
        <div className="...">
          <header className="...">
            <div className="...">
              <dl className="...">
                <div>
                  <dt className="...">Published on</dt>
                  <dd className="...">
                    <time dateTime={date}>
                      {new Date(date).toLocaleDateString(locale, postDateTemplate)} // Change locale
                    </time>
                  </dd>
                </div>
              </dl>
              // ...
            </div>
          </header>
          // ...
        </div>
      </article>
    </SectionContainer>
  )
}

Summary

Now, it seems like we've reached the end of our journey, at least for now.

Initially, I was diving into Next.js with the Tailwind CSS Blog Starter template without fully grasping how it worked. However, as I tackled multilingual support, I began to gain a better understanding of its inner workings.

Even if it's not the Tailwind CSS Blog Starter template, understanding how to filter and generate paths for multilingual support can be valuable knowledge that you can apply to other projects

Code

You can review the code on the multilingual-i18n-blog branch.

The GitHub source above may not be identical to the current blog, but it was created separately as a Git source to assist in understanding the content.