哎哟也 發表於 2024-4-21 15:48:00

next.js app目录 i18n国际化简单实现

<p>最近在用next写一个多语言的项目,找了好久没找到简单实现的教程,实践起来感觉都比较复杂,最后终于是在官方文档找到了,结合网上找到的代码demo,终于实现了,在这里简单总结一下。</p>
<p>此教程适用于比较简单的项目实现,如果你是刚入门next,并且不想用太复杂的方式去实现一个多语言项目,那么这个教程就挺适合你的。</p>
<p>此教程适用于app目录的next项目。</p>
<p>先贴一下参阅的连接:</p>
<p>官方教程: next i18n 文档</p>
<p>可参阅的代码demo</p>
<h1 id="实现思路">实现思路</h1>
<p>结合文件结构解说一下大致逻辑:</p>
<p><img src="https://img2024.cnblogs.com/blog/1573343/202404/1573343-20240421154425266-632453452.png"></p>
<ul>
<li><code>i18n-config.ts</code>只是一个全局管理多语言简写的<strong>枚举</strong>文件,其他文件可以引用这个文件,这样就不会出现不同文件对不上的情况。</li>
<li><code>middleware.ts</code>做了一层拦截,在用户访问<code>localhost:3000</code>的时候能通过请求头判断用户常用的语言,配合app目录多出来的<code></code>目录,从而实现跳转到<code>localhost:3000/zh</code>这样。</li>
<li><code>dictionaries</code>文件夹下放各语言的json字段,通过字段的引用使页面呈现不同的语种。<br>
事实上每个页面的<code>layout.tsx</code>和<code>page.tsx</code>都会将语言作为参数传入,在对应的文件里,再调用<code>get-dictionaries.ts</code>文件里的方法就能读取到对应的json文件里的内容了。</li>
</ul>
<p>大致思路是这样,下面贴对应的代码。</p>
<p><code>/i18n-config.ts</code></p>
<pre><code class="language-ts">export const i18n = {
    defaultLocale: "en",
    // locales: ["en", "zh", "es", "hu", "pl"],
    locales: ["en", "zh"],
} as const;

export type Locale = (typeof i18n)["locales"];
</code></pre>
<p><code>/middleware.ts</code>,需要先安装两个依赖,这两个依赖用于判断用户常用的语言:</p>
<pre><code class="language-shell">npm install @formatjs/intl-localematcher
npm install negotiator
</code></pre>
<p>然后才是<code>/middleware.ts</code>的代码:</p>
<pre><code class="language-ts">import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

import { i18n } from "./i18n-config";

import { match as matchLocale } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";

function getLocale(request: NextRequest): string | undefined {
// Negotiator expects plain object so we need to transform headers
const negotiatorHeaders: Record&lt;string, string&gt; = {};
request.headers.forEach((value, key) =&gt; (negotiatorHeaders = value));

// @ts-ignore locales are readonly
const locales: string[] = i18n.locales;

// Use negotiator and intl-localematcher to get best locale
let languages = new Negotiator({ headers: negotiatorHeaders }).languages(
    locales,
);

const locale = matchLocale(languages, locales, i18n.defaultLocale);

return locale;
}

export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;

// // `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually.
// // If you have one
// if (
//   [
//   '/manifest.json',
//   '/favicon.ico',
//   // Your other files in `public`
//   ].includes(pathname)
// )
//   return

// Check if there is any supported locale in the pathname
const pathnameIsMissingLocale = i18n.locales.every(
    (locale) =&gt;
      !pathname.startsWith(`/${locale}/`) &amp;&amp; pathname !== `/${locale}`,
);

// Redirect if there is no locale
if (pathnameIsMissingLocale) {
    const locale = getLocale(request);

    // e.g. incoming request is /products
    // The new URL is now /en-US/products
    return NextResponse.redirect(
      new URL(
      `/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`,
      request.url,
      ),
    );
}
}

export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
</code></pre>
<p><code>/dictionaries</code>下的因项目而异,可以看个参考:<br>
<img src="https://img2024.cnblogs.com/blog/1573343/202404/1573343-20240421154505906-97206843.png"></p>
<p>文件以语言简写命名,<code>/i18n-config.ts</code>里的<code>locales</code>有什么语言,这里就有多少个对应的文件就行了。</p>
<p><code>/get-dictionaries.ts</code></p>
<pre><code class="language-ts">import "server-only";
import type { Locale } from "./i18n-config";

// We enumerate all dictionaries here for better linting and typescript support
// We also get the default import for cleaner types
const dictionaries = {
en: () =&gt; import("./dictionaries/en.json").then((module) =&gt; module.default),
zh: () =&gt; import("./dictionaries/zh.json").then((module) =&gt; module.default),
};

export const getDictionary = async (locale: Locale) =&gt; dictionaries?.() ?? dictionaries.en();
</code></pre>
<p>实际使用可以做个参考:<br>
<img src="https://img2024.cnblogs.com/blog/1573343/202404/1573343-20240421154527052-1749610280.png"></p>
<p>到这里其实就实现了,但是下面的事情需要注意:</p>
<p><strong>如果你的项目有集成了第三方需要配知道middleware的地方,比如clerk,需要调试一下是否冲突。</strong></p>
<p>如果你不知道clerk是什么,那么下面可以不用看,下面将以clerk为例,描述一下可能遇到的问题和解决方案。</p>
<h1 id="clerk适配">Clerk适配</h1>
<p>clerk是一个可以快速登录的第三方库,用这个库可以快速实现用户登录的逻辑,包括Google、GitHub、邮箱等的登录。</p>
<p>clerk允许你配置哪些页面是公开的,哪些页面是需要登录之后才能看的,如果用户没登录,但是却访问了需要登录的页面,就会返回401,跳转到登录页面。</p>
<p>就是这里冲突了,因为我们实现多语言的逻辑是,用户访问<code>localhost:3000</code>的时候判断用户常用的语言,从而实现跳转到<code>localhost:3000/zh</code>这样。</p>
<p>这两者实现都在<code>middleware.ts</code>文件中,上面这种配置会有冲突,这两者只有一个能正常跑通,而我们想要的效果是两者都能跑通,既能自动跳转到登录页面,也能自动跳转到常用语言页面。</p>
<p>技术问题定位:这是因为你重写了middleware方法,导致不会执行Clerk的authMiddleware方法,视觉效果上,就是多语言导致了Clerk不会自动跳转登录。</p>
<p>所以要把上面的middleware方法写到authMiddleware方法里的beforeAuth里去,Clerk官方有说明: Clerk authMiddleware说明</p>
<p><img src="https://img2024.cnblogs.com/blog/1573343/202404/1573343-20240421154557705-1647462956.png"></p>
<p>所以现在/middleware.ts文件内的内容变成了:</p>
<pre><code class="language-ts">import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { authMiddleware } from "@clerk/nextjs";
import { i18n } from "./i18n-config";
import { match as matchLocale } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";

function getLocale(request: NextRequest): string | undefined {
// Negotiator expects plain object so we need to transform headers
const negotiatorHeaders: Record&lt;string, string&gt; = {};
request.headers.forEach((value, key) =&gt; (negotiatorHeaders = value));

// @ts-ignore locales are readonly
const locales: string[] = i18n.locales;

// Use negotiator and intl-localematcher to get best locale
let languages = new Negotiator({ headers: negotiatorHeaders }).languages(
    locales,
);

const locale = matchLocale(languages, locales, i18n.defaultLocale);

return locale;
}

export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
// matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};

export default authMiddleware({
publicRoutes: ['/anyone-can-visit-this-route'],
ignoredRoutes: ['/no-auth-in-this-route'],
beforeAuth: (request) =&gt; {
    const pathname = request.nextUrl.pathname;

    // // `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually.
    // // If you have one
    if (
      [
      '/manifest.json',
      '/favicon.ico',
      '/serviceWorker.js',
      '/en/sign-in'
      // Your other files in `public`
      ].includes(pathname)
    )
      return

    // Check if there is any supported locale in the pathname
    const pathnameIsMissingLocale = i18n.locales.every(
      (locale) =&gt;
      !pathname.startsWith(`/${locale}/`) &amp;&amp; pathname !== `/${locale}`,
    );

    // Redirect if there is no locale
    if (pathnameIsMissingLocale) {
      const locale = getLocale(request);

      // e.g. incoming request is /products
      // The new URL is now /en-US/products
      return NextResponse.redirect(
      new URL(
          `/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`,
          request.url,
      ),
      );
    }
}
});
</code></pre>
<p>这样就OK了,大功告成。</p>
<hr>
<p>2024-11-06更新</p>
<p>上面介绍的这种方法,需要通过json去管理多语言。事实上在做了多个多语言的项目之后,每次管理这些json是让我很难受的。</p>
<p>每次zh.json多了一些键值对,我都要翻译对应的en.json, ja.json, ru.json 等等。</p>
<p>用gpt翻译的话,翻译到第一个语种还好,翻译到后面的语种gpt就已经忘记了前面的原文了。</p>
<p>用机器翻译的话,又识别不了json格式,得手动把value值复制出来再粘贴回去,真的会死人。</p>
<p>基于这块的考虑我做了个专门针对这种情况的翻译器,有需要的朋友可以体验一下json翻译器。</p>
<p>对应的还有markdown翻译器。</p>
<p>markdown就更长了,gpt会遗忘上下文,机器翻译长度要求你分成几段,而且翻译出来的结果会丢失一些markdown语法。</p>
<p>这个翻译器会考虑长度问题,可以直接把一整个json或者markdown文件复制进去翻译,一般我的项目都是够用的,如果体验下来觉得还有改进的地方,想提一些建议的朋友可以直接联系网站里的邮箱,看到了的话考虑改进~</p><br><br>
来源:https://www.cnblogs.com/tandk-blog/p/18149028/how-nextjs-app-simply-make-i18n
頁: [1]
查看完整版本: next.js app目录 i18n国际化简单实现