发布于 2026-01-06 0 阅读
0

使用 Next.js 构建多语言网站 - 第三部分

使用 Next.js 构建多语言网站 - 第三部分

如果你直接跳转到这第三部分,而没有看过第一部分第二部分,我强烈建议你先阅读一下。在前一部分中,我们讨论了各种语言的内容创建和列表,并结束了这个项目。

不过,有人评论说,如果能添加翻译后的网址别名就更好了,例如:英文版“关于”页面打开地址是[site.com/en/about此处应填写网址],对应的葡萄牙语版本打开地址是[site.com/pt/sobre此处应填写葡萄牙语版本]。本文将向您展示如何实现此功能。让我们开始吧!

但首先……

在之前的文章中,我们已经实现了语言切换功能。但是,页面刷新后,语言会自动恢复为默认语言,这造成了一些不便。这种行为并不理想,因此解决这个问题非常重要。幸运的是,实现起来并不难,只需几行代码即可。

本地存储

本地存储是 JavaScript 提供的一种将信息保存在用户浏览器中的方式,以便下次访问时可以使用这些信息。许多人使用它来进行简单的身份验证或保存选项,例如浅色和深色模式。

这里使用的逻辑与主题切换并无二致,区别在于保存的是语言设置。只需要对两个文件进行少量修改:Header组件文件和LanguageProvider语言上下文文件。如果您之前从未阅读过前两篇文章,导致本文内容难以理解,那么请注意,我在文章开头已经提醒过您了!请先阅读前两篇文章,然后再回到这里!

以下是头部组件的代码

import { useContext } from "react"
import { useRouter } from "next/router"

import Navigation from "../Navigation"
import Logo from "../Logo"
import { LanguageContext, locales } from "../../intl/LanguageProvider"

interface Props {
  className?: string
  children?: React.ReactNode
}

const Header: React.FC<Props> = ({ className, children }) => {
  const headerClass = className || "header"
  const [locale, setLocale] = useContext(LanguageContext)
  const router = useRouter()

  function handleLocaleChange(language: string) {
    if (!window) {
      return
    }

    const regex = new RegExp(`^/(${locales.join("|")})`)
    localStorage.setItem("lang", language) // This line saves the language option!
    setLocale(language)

    router.push(router.pathname, router.asPath.replace(regex, `/${language}`))
  }

  return (
    <header className={headerClass}>
      <Logo link={`/`} />
      <Navigation />
      {children}
      <div className="lang">
        <button onClick={() => handleLocaleChange("en")}>EN</button>
        <button onClick={() => handleLocaleChange("pt")}>PT</button>
      </div>
    </header>
  )
}

export default Header
Enter fullscreen mode Exit fullscreen mode

头部localStorage.setItem ('lang', language)点击相应的按钮即可保存语言选择。这种方法实际上是在代码中添加一个名为“lang”的键,其中包含所选语言的缩写。您可以在浏览器开发者工具的“应用程序”区域的“本地存储”部分查看此信息

LanguageProvider如下所示

import { createContext, useEffect, useState } from "react"

export const defaultLocale = "pt"
export const locales = ["pt", "en"]
export const LanguageContext = createContext([])

export const LanguageProvider: React.FC = ({ children }) => {
  const [locale, setLocale] = useState("pt")

  useEffect(() => {
    if (!window) {
      return
    }
    // Captures the language information saved by the Header component
    const language = localStorage.getItem("lang") || locale
    setLocale(language)
  }, [locale])

  return (
    <LanguageContext.Provider value={[locale, setLocale]}>
      {children}
    </LanguageContext.Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode

localStorage.getItem ('lang')方法会获取已保存的语言选择信息,并在信息存在时应用该信息。这样,在更新页面时,您选择的语言就会保留下来。

最后……让我们创建翻译后的slug……

您可以在文件夹中创建文件/pages,并根据需要设置标题,例如/kontakt.tsx德语联系页面。这样做完全可行,但说实话,这并非最佳方案。我们应该提供一种动态创建页面的方法,使用标准模板,并根据语言更改页面内容和别名。

仔细想想,我们在这个项目中对文章区域也做了类似的事情。为了实现这一点,只需修改我们为文章创建的库(/lib/posts.ts),使其包含我们新翻译的页面即可。但为了避免代码重复,我没有创建一个/lib/pages.ts内容几乎相同的文件/lib/posts,而是决定将所有内容统一到一个名为的库中lib/files.ts

该文件的内容如下:

import fs from "fs"
import path from "path"
import matter, { GrayMatterFile } from "gray-matter"
import remark from "remark"
import html from "remark-html"

const postsDirectory = path.resolve(process.cwd(), "content", "posts")
const pagesDirectory = path.resolve(process.cwd(), "content", "pages")

// Collects all file names in the folders specified with the sctructure ['en/filename.md']
export function getAllFileNames(directoryPath: string, filesList = []) {
  const files = fs.readdirSync(directoryPath)

  files.forEach((file) => {
    if (fs.statSync(`${directoryPath}/${file}`).isDirectory()) {
      filesList = getAllFileNames(`${directoryPath}/${file}`, filesList)
    } else {
      filesList.push(path.join(path.basename(directoryPath), "/", file))
    }
  })

  const filteredList = filesList.filter((file) => file.includes(".md"))
  return filteredList
}

// Sorts posts by date
export function getSortedPostData() {
  const fileNames = getAllFileNames(postsDirectory)

  const allPostsData = fileNames.map((fileName) => {
    const id = fileName.split("/")[1].replace(/\.md$/, "")
    const fullPath = path.join(postsDirectory, fileName)
    const fileContents = fs.readFileSync(fullPath, "utf-8")
    const frontMatter: GrayMatterFile<string> = matter(fileContents)

    return {
      id,
      ...(frontMatter.data as {
        lang: string
        date: string
        category: string
      }),
    }
  })

  return allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1
    } else {
      return -1
    }
  })
}

// IDs for posts or pages
export function getAllIds(type = "post") {
  const dir = type === "page" ? pagesDirectory : postsDirectory
  const fileNames = getAllFileNames(dir)

  return fileNames.map((fileName) => ({
    params: {
      id: fileName.split("/")[1].replace(/\.md$/, ""),
      lang: fileName.split("/")[0],
    },
  }))
}

// Collects data from the markdown file and makes it available
export async function getContentData(id: string, type = "post") {
  const dir = type === "page" ? pagesDirectory : postsDirectory
  const fullPath = path.join(dir, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, "utf-8")
  const frontMatter = matter(fileContents)

  const processedContent = await remark().use(html).process(frontMatter.content)

  const contentHtml = processedContent.toString()

  return {
    id,
    ...(frontMatter.data as { date: string; title: string }),
    contentHtml,
  }
}
Enter fullscreen mode Exit fullscreen mode

type在一些函数中创建了一个参数,这些函数会被文章和页面使用。这是因为该参数用于指定文件读取的目录。默认情况下,我将其配置为始终搜索文章。由于文件名和函数都已更改,因此需要更新使用新库的文件中的导入语句。

动态页面的模板

这里是另一个名称特殊的页面,用于创建动态路由。此页面的参数是文件的“id”,该id由文件getAllIds()的函数获取lib/files。文件将被命名为[文件名[lang]/[id].tsx]。以下是该文件的完整代码。

import { GetStaticProps, GetStaticPaths, NextPage } from "next"

import { getAllIds, getContentData } from "../../lib/files"
import Layout from "../../components/Layout"

interface PageProps {
  locale: string
  pageData: {
    lang: string
    title: string
    slug: string
    date: string
    category?: string
    contentHtml: string
  }
}

const SitePage: NextPage<PageProps> = ({ pageData }) => {
  const { title, contentHtml } = pageData

  return (
    <Layout title={title}>
      <article className="post-content">
        <h1>{title}</h1>
        <div
          className="post-text"
          dangerouslySetInnerHTML={{ __html: contentHtml }}
        />
      </article>
    </Layout>
  )
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  // Here is the argument to informa "page" as type,
  // so Next.js can search for page files, ignoring posts.
  const pageData = await getContentData(`/${params.lang}/${params.id}`, "page")

  return {
    props: {
      locale: params?.lang || "pt",
      pageData,
    },
  }
}

export const getStaticPaths: GetStaticPaths = async () => {
  // Here is the argument to informa "page" as type,
  // so Next.js can search for page files, ignoring posts.
  const paths = getAllIds("page")

  return {
    paths,
    fallback: false,
  }
}

export default SitePage
Enter fullscreen mode Exit fullscreen mode

有了这个文件,就可以支持使用 Markdown 创建的页面了。Markdown 文件采用以下结构:

---
lang: pt
title: "Sobre"
---

Site made to showcase the creation of a bilingual website using Next.js. The tutorial is in an article on my blog. Feel free to view the source code, fork it, or even use it in your projects.
Enter fullscreen mode Exit fullscreen mode

为了更好地组织文件,我/content在项目根目录创建了一个名为 `<link>` 的目录,并在该目录下创建了两个子目录:`<link>` 和posts`<link>` pages。这两个子目录将分别存放网站支持的每种语言的 Markdown 文件。通过这里提供的代码,页面的创建过程将完全自动化,并基于这种文件结构。

总结

我相信我们现在已经有了一个使用 Next.js 构建的多语言网站的功能非常完善的示例。您可以创建多种语言的内容,并让用户在您的网站上选择一种语言。

欢迎提出意见、建议和问题,请在下方留言。我还提供了 GitHub 上的完整项目仓库链接,方便您查看完整代码。如果您遇到任何错误,也可以在那里提交您的问题。

再见!

链接


如果这篇文章对您有所帮助,请考虑捐赠。您的支持将帮助我创作更多类似的内容!

文章来源:https://dev.to/elvesousa/making-a-multilingual-site-with-next-js-part-3-42kh