😱 Next.js 中兼容 i18n 的静态 HTML 导出 😱
但是,你可能听说过这个:
那么我们现在能做什么呢?
食谱
结果
自愿部分
🎉🥳 恭喜 🎊🎁
👍
正在寻找优化的 Next.js 翻译设置?
您知道Next.js吧?——如果不了解,请停止阅读本文并尝试其他方法。
Next.js 太棒了!它提供您所需的所有功能,为您带来最佳的开发者体验……
目录
但是,你可能听说过这个:
错误:i18n 支持与 Next Export 不兼容。有关部署的更多信息,请参阅此处:https://nextjs.org/docs/deployment
如果您使用国际化路由功能并尝试通过执行生成静态 HTML 导出,next export
则会发生这种情况。
此功能需要 Node.js 服务器,或者在构建过程中无法计算的动态逻辑,这就是它不受支持的原因。
例如,如果您使用next-i18next就会出现这种情况。
那么我们现在能做什么呢?
一个显而易见的选择是,放弃静态 HTML 导出并使用 Node.js 服务器或Vercel作为部署环境。
但有时,由于公司或架构指南的限制,必须使用静态 Web 服务器。
那么,放弃国际化可以吗?——其实不然,如果我们这样想,这似乎是一项要求。
那么,不用Next.js也可以吗?——但这通常意味着要重写整个项目。
不使用 i18n 时执行next export
似乎可行。
如果我们不尝试使用国际化路由功能,而是自行进行 i18n 路由,会怎么样?
食谱
要“烹饪”此食谱,您需要以下原料:
- 使用动态路线段功能
- 愿意改变项目文件的结构
- 愿意调整一些代码
- 检测用户语言并进行相应重定向的逻辑
听起来可行。开始吧!
1. 从 中删除 i18n 选项next.config.js
。
- const { i18n } = require('./next-i18next.config')
-
module.exports = {
- i18n,
trailingSlash: true,
}
[locale]
2.在您的页面目录中创建一个文件夹。
a)将所有页面文件移动到该文件夹(不是_app.js
或_document.js
等等)。
b) 如果需要,调整您的导入。
3. 创建一个getStatic.js
文件并将其放置在lib
目录中。
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import i18nextConfig from '../next-i18next.config'
export const getI18nPaths = () =>
i18nextConfig.i18n.locales.map((lng) => ({
params: {
locale: lng
}
}))
export const getStaticPaths = () => ({
fallback: false,
paths: getI18nPaths()
})
export async function getI18nProps(ctx, ns = ['common']) {
const locale = ctx?.params?.locale
let props = {
...(await serverSideTranslations(locale, ns))
}
return props
}
export function makeStaticProps(ns = {}) {
return async function getStaticProps(ctx) {
return {
props: await getI18nProps(ctx, ns)
}
}
}
4. 在您的页面中使用getStaticPaths
和makeStaticProps
,如下所示:
import { useTranslation } from 'next-i18next'
import { getStaticPaths, makeStaticProps } from '../../lib/getStatic'
import { Header } from '../../components/Header'
import { Footer } from '../../components/Footer'
import Link from '../../components/Link'
+ const getStaticProps = makeStaticProps(['common', 'footer'])
+ export { getStaticPaths, getStaticProps }
const Homepage = () => {
const { t } = useTranslation('common')
return (
<>
<main>
<Header heading={t('h1')} title={t('title')} />
<div>
<Link href='/second-page'><button type='button'>{t('to-second-page')}</button></Link>
</div>
</main>
<Footer />
</>
)
}
export default Homepage
5.安装下一个语言检测器。
npm i next-language-detector
6. 创建一个languageDetector.js
文件并将其放置在lib
目录中。
import languageDetector from 'next-language-detector'
import i18nextConfig from '../next-i18next.config'
export default languageDetector({
supportedLngs: i18nextConfig.i18n.locales,
fallbackLng: i18nextConfig.i18n.defaultLocale
})
7. 创建一个redirect.js
文件并将其放置在lib
目录中。
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import languageDetector from './languageDetector'
export const useRedirect = (to) => {
const router = useRouter()
to = to || router.asPath
// language detection
useEffect(() => {
const detectedLng = languageDetector.detect()
if (to.startsWith('/' + detectedLng) && router.route === '/404') { // prevent endless loop
router.replace('/' + detectedLng + router.route)
return
}
languageDetector.cache(detectedLng)
router.replace('/' + detectedLng + to)
})
return <></>
};
export const Redirect = () => {
useRedirect()
return <></>
}
// eslint-disable-next-line react/display-name
export const getRedirect = (to) => () => {
useRedirect(to)
return <></>
}
8. 对于目录中的每个页面文件[locale]
,特别是index.js
文件,创建一个具有相同名称且内容如下的文件:
import { Redirect } from '../lib/redirect'
export default Redirect
9. 创建一个Link.js
组件并将其放置在components
目录中。
import React from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
const LinkComponent = ({ children, skipLocaleHandling, ...rest }) => {
const router = useRouter()
const locale = rest.locale || router.query.locale || ''
let href = rest.href || router.asPath
if (href.indexOf('http') === 0) skipLocaleHandling = true
if (locale && !skipLocaleHandling) {
href = href
? `/${locale}${href}`
: router.pathname.replace('[locale]', locale)
}
return (
<>
<Link href={href}>
<a {...rest}>{children}</a>
</Link>
</>
)
}
export default LinkComponent
10.next/link
Link
用适当的../components/Link
Link
导入替换所有导入:
- import Link from 'next/link'
+ import Link from '../../components/Link'
11.添加或修改您的_document.js
文件以设置正确的 htmllang
属性:
import Document, { Html, Head, Main, NextScript } from 'next/document'
import i18nextConfig from '../next-i18next.config'
class MyDocument extends Document {
render() {
const currentLocale = this.props.__NEXT_DATA__.query.locale || i18nextConfig.i18n.defaultLocale
return (
<Html lang={currentLocale}>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
12.如果您有语言切换器,请创建或调整它:
// components/LanguageSwitchLink.js
import languageDetector from '../lib/languageDetector'
import { useRouter } from 'next/router'
import Link from 'next/link'
const LanguageSwitchLink = ({ locale, ...rest }) => {
const router = useRouter()
let href = rest.href || router.asPath
let pName = router.pathname
Object.keys(router.query).forEach((k) => {
if (k === 'locale') {
pName = pName.replace(`[${k}]`, locale)
return
}
pName = pName.replace(`[${k}]`, router.query[k])
})
if (locale) {
href = rest.href ? `/${locale}${rest.href}` : pName
}
return (
<Link
href={href}
onClick={() => languageDetector.cache(locale)}
>
<button style={{ fontSize: 'small' }}>{locale}</button>
</Link>
);
};
export default LanguageSwitchLink
// components/Footer.js
import { useTranslation } from 'next-i18next'
import { useRouter } from 'next/router'
import LanguageSwitchLink from './LanguageSwitchLink'
import i18nextConfig from '../next-i18next.config'
export const Footer = () => {
const router = useRouter()
const { t } = useTranslation('footer')
const currentLocale = router.query.locale || i18nextConfig.i18n.defaultLocale
return (
<footer>
<p>
<span style={{ lineHeight: '4.65em', fontSize: 'small' }}>{t('change-locale')}</span>
{i18nextConfig.i18n.locales.map((locale) => {
if (locale === currentLocale) return null
return (
<LanguageSwitchLink
locale={locale}
key={locale}
/>
)
})}
</p>
</footer>
)
}
结果
如果您现在启动您的项目(next dev
),您应该会看到或多或少与以前相同的行为。
那么有什么好处呢?
尝试:next build && next export
最后你应该会看到类似这样的内容:
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
info - using build directory: /Users/usr/projects/my-awesome-project/.next
info - Copying "static build" directory
info - No "exportPathMap" found in "/Users/usr/projects/my-awesome-project/next.config.js". Generating map from "./pages"
info - Launching 9 workers
info - Copying "public" directory
info - Exporting (3/3)
Export successful. Files written to /Users/usr/projects/my-awesome-project/out
是的,不再有i18n support is not compatible with next export
错误了!!!
恭喜!现在您可以将目录内容“部署”out
到任何静态 Web 服务器。
🧑💻完整代码可以在这里找到。
自愿部分
连接到一个很棒的翻译管理系统并在代码之外管理您的翻译。
让我们使用locize同步翻译文件。
这可以按需完成,也可以在 CI 服务器上完成,或者在部署应用程序之前完成。
要达到这一步需要做什么:
- 在 locize:在https://locize.app/register注册并登录
- 在 locize 中:创建一个新项目
- 在 locize 中:添加所有其他语言(这也可以通过API完成)
- 安装locize-cli(
npm i locize-cli
)
使用locize-cli
使用locize sync
命令将您的本地存储库(public/locales
)与 locize 上发布的内容同步。
或者,您也可以在捆绑应用程序之前使用locize download
命令始终将已发布的 locize 翻译下载到您的本地存储库( )。public/locales
🎉🥳 恭喜 🎊🎁
我希望您已经了解了一些有关静态站点生成 (SSG)、Next.js、next-i18next、i18next和现代本地化工作流程的新知识。
因此,如果您想将 i18n 主题提升到一个新的水平,那么值得尝试本地化管理平台 - locize。
locize的创始人也是i18next的创造者。因此,使用locize就是直接支持i18next的未来。
👍
正在寻找优化的 Next.js 翻译设置?
在这里您可以找到一篇关于如何最好地使用 next-i18next 进行客户端翻译下载和 SEO 优化的博客文章。