使用 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
在头部,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>
)
}
该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,
}
}
我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
有了这个文件,就可以支持使用 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.
为了更好地组织文件,我/content在项目根目录创建了一个名为 `<link>` 的目录,并在该目录下创建了两个子目录:`<link>` 和posts`<link>` pages。这两个子目录将分别存放网站支持的每种语言的 Markdown 文件。通过这里提供的代码,页面的创建过程将完全自动化,并基于这种文件结构。
总结
我相信我们现在已经有了一个使用 Next.js 构建的多语言网站的功能非常完善的示例。您可以创建多种语言的内容,并让用户在您的网站上选择一种语言。
欢迎提出意见、建议和问题,请在下方留言。我还提供了 GitHub 上的完整项目仓库链接,方便您查看完整代码。如果您遇到任何错误,也可以在那里提交您的问题。
再见!
链接
如果这篇文章对您有所帮助,请考虑捐赠。您的支持将帮助我创作更多类似的内容!
文章来源:https://dev.to/elvesousa/making-a-multilingual-site-with-next-js-part-3-42kh