😱 Static HTML Export with i18n compatibility in Next.js 😱 BUT, you may have heard about this: So what can we do now? The recipe The outcome The voluntary part 🎉🥳 Congratulations 🎊🎁 👍 Looking for an optimized Next.js translations setup?

2025-06-04

😱 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,
  }


Enter fullscreen mode Exit fullscreen mode

[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)
      }
    }
  }


Enter fullscreen mode Exit fullscreen mode

4. 在您​​的页面中使用getStaticPathsmakeStaticProps,如下所示:



  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


Enter fullscreen mode Exit fullscreen mode

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
  })


Enter fullscreen mode Exit fullscreen mode

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 <></>
  }


Enter fullscreen mode Exit fullscreen mode

8. 对于目录中的每个页面文件[locale],特别是index.js文件,创建一个具有相同名称且内容如下的文件:



  import { Redirect } from '../lib/redirect'
  export default Redirect


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

10.next/link Link用适当的../components/Link Link导入替换所有导入:



  - import Link from 'next/link'
  + import Link from '../../components/Link'


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode


  // 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>
    )
  }


Enter fullscreen mode Exit fullscreen mode

结果

结果

如果您现在启动您的项目(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


Enter fullscreen mode Exit fullscreen mode

是的,不再有i18n support is not compatible with next export错误了!!!

恭喜!现在您可以将目录内容“部署”out到任何静态 Web 服务器。

🧑‍💻完整代码可以在这里找到。

自愿部分

翻译工作流程

连接到一个很棒的翻译管理系统并在代码之外管理您的翻译。

让我们使用locize同步翻译文件
这可以按需完成,也可以在 CI 服务器上完成,或者在部署应用程序之前完成。

要达到这一步需要做什么:

  1. 在 locize:在https://locize.app/register注册并登录
  2. 在 locize 中:创建一个新项目
  3. 在 locize 中:添加所有其他语言(这也可以通过API完成)
  4. 安装locize-clinpm i locize-cli

使用locize-cli

使用locize sync命令将您的本地存储库(public/locales)与 locize 上发布的内容同步。

或者,您也可以在捆绑应用程序之前使用locize download命令始终将已发布的 locize 翻译下载到您的本地存储库( )。public/locales

🎉🥳 恭喜 🎊🎁

我希望您已经了解了一些有关静态站点生成 (SSG)、Next.jsnext-i18nexti18next现代本地化工作流程的新知识。

因此,如果您想将 i18n 主题提升到一个新的水平,那么值得尝试本地化管理平台 - locize

locize的创始人也是i18next的创造者。因此,使用locize就是直接支持i18next的未来

👍


正在寻找优化的 Next.js 翻译设置?

下一个-i18next
在这里您可以找到一篇关于如何最好地使用 next-i18next 进行客户端翻译下载和 SEO 优化的博客文章。

文章来源:https://dev.to/adrai/static-html-export-with-i18n-compatibility-in-nextjs-8cd
PREV
Web 图标系统——深入指南
NEXT
使用 AWS 进行无服务器速成课程 - 使用 Lambda 和 Aurora 无服务器构建 API 启动 MySQL 更改用户身份验证 根据堆栈溢出需要编辑密码身份验证 确保更改 db.js DB_PASSWORD