Nextjs——国际化那些事儿

news2025/1/21 9:29:07

背景: 某一天,产品经理跟我说,我们的产品需要搞国际化

国际化的需求说白了就是把项目中的文案翻译成不同的语言,用户想用啥语言来浏览网页就用啥语言,虽然说英语是通用语言,但国际化了嘛,产品才能更好的向全球推广(像我这种英文渣渣,看文档的时候有中文版绝对不会去翻英文(除非中文翻译得太烂))。那就开干吧

国际化在代码层面的实现上原理其实很简单,最基础的是准备好语言物料,也就是需要翻译成多少种语言,保证项目中每种语言都有对应的翻译版本,然后就是用户选择什么语言,页面就显示对应的文案,完美~

1. 用户自己解决

如果用户有翻译插件,那问题就会变得很简单,点开页面,翻译软件就能自动识别文案并翻译,没我们什么事,在chrome浏览器中,google translate就做得很好,打开很多页面的时候,google translate都能识别当前语言和系统语言是否一致,并且提供翻译提醒。但是……google translate也不是万能的,next.js网站中,使用google translate会导致网站崩溃,但后来人家也说了,这不是next的锅,锅在react那,并且已持续多年。
issue传送:Make React resilient to DOM mutations from Google Translate
google translate的兼容性都那么不好,其他的翻译插件兼容性如何也就成了未知数,为了网站的稳定性,减少网站崩溃次数,建议在head中关闭翻译功能。

<meta name="google" content="notranslate" />

2. 第三方SaaS

既然不能靠用户,那就只能自己来了,那有没有开发成本小,低代码或者零代码就能实现呢?世界人民如此聪明,当然有了。现在市面上有很多成熟的SaaS国际化软件,当时经过各种比较之后选了Weglot添加链接描述进行测试(不是广告,比较之后发现它比较好上手而已)。它的原理也比较简单:获取页面上它觉得需要进行翻译的元素,通过接口将翻译之后的内容返回并在页面上进行替换,具体的操作步骤如下:

    1. 在网站内注册,创建自己的项目,把js代码以及项目的key注入代码
// useWeglot
// 在最外层的layout或者是需要国际化的目录的根文件中使用
"use client";

import { useEffect } from "react";

export default function useWeglot() {
  useEffect(() => {
    const initializeWeglot = () => {
      if (typeof window?.Weglot !== "undefined") {
        window?.Weglot.initialize({
          api_key: "这里写你的key", 
          originalLanguage: "en",
          auto_switch_fallback: "zh",
        });
      }
    };

    const loadWeglotScript = () => {
      const existingScript = document.getElementById("weglot-script");
      if (!existingScript) {
        const script = document.createElement("script");
        script.id = "weglot-script";
        script.src = "https://cdn.weglot.com/weglot.min.js";
        script.async = true;
        script.onload = initializeWeglot;
        document.body.appendChild(script);
      } else {
        initializeWeglot();
      }
    };
    loadWeglotScript();
  }, []);

  return null;
}

在这里插入图片描述

    1. 自定义规则,将需要翻译和不需要翻译的部分都用规则标记上(比如class),这样翻译时它就能避开一些本来不需要翻译的部分,比如接口返回的数据
      在这里插入图片描述
      在这里插入图片描述
    1. 微调,默认都是机翻,有些翻译得不是很通顺的地方可以人工调整
      在这里插入图片描述
      如果是简单的项目,它还是很好用的,代码侵入性小,翻译之后的文本也全部都维护到weglot里,通过接口返回,需要翻译修改时直接在网站中改完实时更新,特别省事。
      但缺点也是显而易见的,首先,它免费额度不太大,项目比较大,涉及语言比较多的话价格也不便宜;其次,因为是接口返回的翻译内容,用户第一次进入页面时会先出现原本的语言,再跳到对应的语言,首次加载会闪下(可能有优化方案,知道的朋友踢我下);而且因为是代码读取元素内容,很有可能出现多翻译或者少翻的情况,在实际开发中调试也需要一定时间。

3. 自己开发

啰里八嗦了一大堆终于到了正题,经过了一系列的研究和探讨,最终决定了,自己开发!!!代码开发最主要是确认两个问题,一是系统要如何获取语言,二是系统如何跟翻译好的文本进行交互,这两个问题解决完,国际化也就完成了大半。

如何获取语言

世界上有很多很多的语言种类,国际上对语言有一个编码规范(ISO 639,有兴趣的可以了解下,比如简体中文为啥有些地方直接写zh,有时候是zh-CN),在编码的时候,也是遵循这套标准定义,规范好了,那通过什么方式告诉系统,当前是什么语言呢?通常有以下两种:

Google 建议对每种语言版本的网页使用不同的网址,而不是使用 Cookie 或浏览器设置来调整网页上的内容语言。

    1. 路由
    • 通过host获取,比如维基百科:https://zh.wikipedia.org/
    • 放在pathname中,比如MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript
    • 放在参数中,比如google开发者平台:https://developers.google.com/?hl=zh-cn
      google 对国际化网站有一些建议:
      在这里插入图片描述
    1. 缓存(cookie或者localStorage)
      从上面来看就是google不太推荐用这种形式,但它在实现上相对简单,只需要存在浏览器本地,运行时读取浏览器数据,渲染出对应的语言信息。需要注意的是,如果后端接口也需要国际化(比如一些接口报错啥的),可以使用cookie,但cookie会涉及数据隐私问题(欧盟标准),一旦用户选择不使用cookie,就不能通过cookie进行语言的切换。
如何进行翻译文本的交互

不管什么形式获取的语言,最后反应到系统程序上都是一个语言代码的形式,用户通过获取这个语言代码,将代码匹配到对应的翻译文本中,就能将对应语言的文案展示出来了。
为了方便,接下来所有文本都以这两个json文件为例:

// dictionaries/en.json
{
	"title":"hello world",
	"desc": {
		name:"xiaoqian"
	}
}

// dictionaries/zh.json
{
	"title":"你好",
	"desc": {
		name:"小千"
	}
}
// i18n-config.ts
export const i18n = {
    defaultLocale: 'en',
    locales: ['en', 'zh'],
  } as const
  
export type Locale = (typeof i18n)['locales'][number]


//  get-dictionary.ts
import type { Locale } from './i18n-config'

// 翻译之后的文本分别保存在dictionaries/en.json 以及dictionaries/zh.json中
const dictionaries = {
  en: () => import('./dictionaries/en.json').then((module) => module.default),
  zh: () => import('./dictionaries/zh.json').then((module) => module.default),
}

export const getDictionary = async (locale: Locale) =>
  dictionaries[locale]?.() ?? dictionaries.en()


// useDict
"use client";
import { useState,useEffect, useCallback,  } from "react";
import { getDictionary } from "@/get-dictionary";
import { Locale } from "@/i18n-config";

const lang = process.env.NEXT_PUBLIC_LANG as Locale;

export default function useDict() {
  const [desc, setDesc] = useState<any>({});

  const getDict = useCallback(async () => {
    const locale = await getDictionary(lang);
    setDesc(locale);
  }, []);

  useEffect(() => {
    getDict();
  }, [getDict]);
  return desc
}
// page.tsx
import useDict from "@/hook/useDict"

export default function Page(){
	const lang = useDict()
return <div>{lang?.desc?.name}</div>
}

这样一个最简单的国际化配置就完成了,不同语言不同域名的场景下可以使用这种配置,每种语言打一个包,部署在不同域名下,好处是不需要添加额外的包,缺点是比较不灵活,比较适合语言不是很多,文字不复杂的情况,一些复杂的场景不好处理。

next-intl

需要处理复杂场景的时候,使用库是最快乐的选择,因为我们想得到想不到的场景它都已经实现了,next官方推荐了三个库next-intl,next-international以及next-i18n-router,按照活跃度和文档可读性等等,我选择使用next-intl,以下的代码也都是在这个库的基础上实现的
老规矩,先上官方文档:https://next-intl-docs.vercel.app/
文档里有page-router以及app-router的实现方式,这里以app-router为例子(因为我就是那个用app-router写代码的大怨种呜呜呜),涉及多种场景,接下来就一一拆解吧~

场景一:使用cookie
总有那么一群人因为各种各样的原因需要使用cookie辅助语言的切换,对国际化配置而言,更人性化的是先检测浏览器语言(因为浏览器语言大概率是用户最常用的语言),根据浏览器语言设置默认语言,这时候需要使用到middleware.ts

// middleware.ts
import { match as matchLocale } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

import { defaultLocale, locales } from "@/utils/i18n";

function getLocale(request: NextRequest): string | undefined {
  const locale = request.cookies.get("xiaoqian-locale");
  // 如果cookie中有预设的国际化值,则返回该值
  if (locale) {
    return locale.value;
  }
  // Negotiator expects plain object so we need to transform headers
  const negotiatorHeaders: Record<string, string> = {};
  // eslint-disable-next-line no-return-assign
  request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));

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

  let loc = "";

// matchLocale可能会报错,需要用try catch包括,确保在匹配不上语言时使用默认语言而不是直接报错
  try {
    loc = matchLocale(languages, locales, defaultLocale);
  } catch (error) {
    loc = defaultLocale;
  }

  return loc;
}

export function middleware(request: NextRequest) {
  const locale = getLocale(request);
  const response = NextResponse.next();
  // 执行了setcookie之后,所有的缓存会被清空
  response.cookies.set("xiaoqian-locale", locale as string);
  return response;
}

对应的i8n配置和之前的版本也很类似

import { getRequestConfig } from "next-intl/server";

export const defaultLocale = "en" as const;
export const locales = [
  "en",
  "zh",
] as const;

export type Locale = (typeof locales)[number];

export default getRequestConfig(async () => {
  // Provide a static locale, fetch a user setting,
  // read from `cookies()`, `headers()`, etc.
  const { cookies } = await import("next/headers");
  const cookieStore = cookies();
  const lang = cookieStore.get("xiaoqian-locale");
  const locale = lang?.value || "en";
  // const locale = "en";

  return {
    locale,
    messages: (await import(`../../dictionaries/${locale}.json`)).default,
  };
});

使用方法很简单

// use client
import {useTranslations} from 'next-intl'

export default function Page(){
// 可以使用命名空间,就不需要些一连串的(a.b.c.d)了
	const t = useTranslations("desc")
	return <div>{t("name")}</div> // = desc.name
}

// use server
import { getLocale, getTranslations } from "next-intl/server";

export default function Page(){
	const locale = await getLocale();
  	const t = await getTranslations({ locale, namespace: "desc" });
	return <div>{t("name")}</div> // = desc.name
}

深度用法:https://next-intl-docs.vercel.app/docs/usage/messages

场景二:以子目录的形式
复习一下,子目录也就是xiaoqian.com/en/xxxxiaoqian.com/zh/xxx这种形式,其实这种形式也使用了cookie(NEXT_LOCALE),比起第一种场景的好处是,cookie的存储过程next-intl已经处理完毕,不需要我们操心

import createMiddleware from 'next-intl/middleware';

import { localePrefix,defaultLocale, locales,pathnames } from './utils/i18n';
 
export default createMiddleware({
    defaultLocale,
    locales,
    localePrefix,
    alternateLinks: false, // 默认语言不需要加子目录时需要加上这个配置
    pathnames,
});


  export const config = {
    matcher: [
      "/",
      `/(en|zh)/:path*`,
      "/((?!api|_next|_vercel|.*\\..*).*)",
    ],
  };

一些配置文件

// i18n.ts

import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {Pathnames, LocalePrefix} from 'next-intl/routing';

export const defaultLocale = 'en' as const;
export const locales = ['en', 'zh'] as const;

export const pathnames: Pathnames<typeof locales> = {
  '/': '/',
  '/channels': {
    'en': '/channels',
    'zh': '/channels',
  }
};

export const localePrefix: LocalePrefix<typeof locales> = 'as-needed'; 
// 默认语言不需要加子目录时需要加上这个配置
// 比如默认中文:xiaoqian.com/xxx, 切换成英文才需要xiaoqian.com/en/xxx

export type Locale = (typeof locales)[number];
 
export default getRequestConfig(async ({locale}) => {
  // Validate that the incoming `locale` parameter is valid
  if (!locales.includes(locale as any)) notFound();
 
  return {
    messages: (await (locale === 'en'
    ? // When using Turbopack, this will enable HMR for `en`
      import('../../dictionaries/en.json')
    : import(`../../dictionaries/${locale}.json`))).default
  };
});


// navigation.ts
import {createLocalizedPathnamesNavigation} from 'next-intl/navigation';
import {locales, pathnames, localePrefix} from './i18n-config';

export const {Link, getPathname, redirect, usePathname, useRouter} =
  createLocalizedPathnamesNavigation({
    locales,
    pathnames,
    localePrefix
  });

使用时会稍微复杂一点点

  1. 路由需要进行配置,代码结构需要类似这种:
    2.在这里插入图片描述
  2. 路由切换时需要使用声明的Link而不是原生Link
import {Link} from "@/utils/i18n/navigation";
import {useTranslations} from 'next-intl';


export default function Home() {
 const t = useTranslations()
  return (
    <main>
      <div>
      {t("desc.name")}
      </div>
      <Link href="/channels">Channels</Link>
    </main>
  );
}

其他场景
next-intl最好的地方就是它给的例子多啊多啊多,上述两种场景是我为了满足需求修改过的代码,普通场景下的代码网站中都有详细的示例以及源码,示例地址:https://next-intl-docs.vercel.app/examples,请按需使用。

温馨提示

到这里,国际化开发的基本步骤就拆解完了,之前一直觉得配置完就完事了,美滋滋,但实际对代码进行修改的时候才发现,一个个手动改文案翻译文案太痛苦了,next-intl也很贴心的给了两个vscode插件用来进行国际化的检查和翻译,极大增强了工作效率和幸福感,工具果然是提高效率的第一生产力。如果是多人协作开发的话,对于json文件,可以考虑用排序插件(比如Sort JSON),提交之前先排个序,防止代码冲突

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1978526.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学习编程的第二十天,加油!

3&#xff1a;递归与迭送&#xff08;循环是一种迭代&#xff09; &#xff01;&#xff01;&#xff01;递归算有些东西时计算量会很大导致运行时间过久&#xff0c;而使用循环会大大节省时间&#xff0c;但需要注意溢出的情况。 递归的练习&#xff0c;第一张呢不符合我们的…

刷题——不同路径的数目

不同路径的数目(一)_牛客题霸_牛客网 我第一眼&#xff0c;觉得是没有思路的&#xff0c;我也是看别人代码反应过来&#xff0c; 画图可以看出来 外边沿的只有一种到达方式&#xff0c;全部赋值1&#xff0c; 如果有两个方块相接&#xff0c;那就让此方块的左邻和右邻相加&…

线程池ThreadPoolExecutor使用

文章目录 一、基础-Java中线程创建的方式1.1、继承Thread类创建线程1.2、实现Runnable接口创建线程1.3、实现Calable接口创建线程1.4、使用线程池创建线程二、概念-线程池基本概念2.1、并发和井行的主要区别2.1.1、处理任务不同2.1.2、存在不同2.1.3、CPU资源不同2.2、什么是线…

网页保护用户 小tips

在使用创建web开发的过程中&#xff0c;直接使用用户名url&#xff0c;容易造成用户信息的被攻击&#xff0c;例如对方直接访问 ../../.../username 的网页&#xff0c;可以窃取用户信息&#xff0c;然而把usename变成一堆乱码就安全的多 效果&#xff1a; 代码&#xff1a;…

一个 .NET 开源的地图组件库 - Mapsui

前言 今天大姚给大家分享一个.NET开源&#xff08;MIT License&#xff09;、免费、同时支持多平台框架&#xff08;MAUI、WPF、Avalonia、Uno、Blazor、WinUI、Eto、.NET Android 和 .NET iOS&#xff09;地图组件库&#xff1a;Mapsui。 项目源代码 支持的UI框架的NuGet包 …

JavaFX布局-TabPane

JavaFX布局-TabPane 常用属性paddingsidetabClosingPolicytabDragPolicy 实现方式Java实现fxml实现 组织一组tab的容器&#xff0c;可以设置关闭&#xff0c;拖拽等每个tab内容可以设置不同容器数据 常用属性 padding 内边距&#xff0c;可以单独设置上、下、左、右的内边距 …

webfunny埋点系统如何进行部署?

hello 大家webfunny埋点系统做了不少功能更新&#xff0c;平常给大家分享比较多的是**webfunny前端监控系统**&#xff0c;最近有不少技术同学来了解webfunny埋点系统&#xff0c;今天主要给大家分享下webfunny埋点系统部署&#xff0c;分为本地部署和线上部署。 还没有试用和…

翻译: 可视化深度学习反向传播原理一

本期我们来讲反向传播 也就是神经网络学习的核心算法 稍微回顾一下我们之前讲到哪里之后 首先我要撇开公式不提 直观地过一遍 这个算法到底在做什么 然后如果你们有人想认真看里头的数学 下一期影片我会解释这一切背后的微积分 如果你看了前两期影片 或者你已经有足够背景知…

【文件系统】磁盘的物理结构 | 存储结构

目录 0.前言 1.磁盘的物理结构 1.1什么是二进制&#xff08;0/1&#xff09; 1.2磁盘的存放位置 1.3磁盘的结构 2.磁盘的存储结构 2.1❓数据是怎样在磁盘上存储 2.2❓读写的基本单位是什么 2.3❓如何找到一个指定位置的扇区 2.4❓为什么磁盘的机械结构是这样的 0.前…

Audio Spectrogram Transformer (AST)工作介绍

Audio Spectrogram Transformer (AST)&#xff0c;是一种基于 Transformer 模型的音频分类方法。AST 利用了 Transformer 模型在捕获全局特征方面的优势&#xff0c;将音频信号转换为频谱图进行处理。下面是对 AST 及其相关研究工作的详细介绍&#xff1a; 1.研究背景 传统的音…

SpringAOP-底层实现源码解析

目录 1. Spring AOP原理流程图 2. 动态代理 3. ProxyFactory 4. Advice的分类 5. Advisor的理解 6. 创建代理对象的方式 ProxyFactoryBean BeanNameAutoProxyCreator DefaultAdvisorAutoProxyCreator 7. 对Spring AOP的理解 8. AOP中的概念 9. Advice在Spring AOP中…

文心智能体【MBTI速测小精灵】:趣味速测,精准解析你的性格密码!

文章目录 一、文心智能体平台是什么&#xff1f;二、创建文心智能体智能体创建智能体调试分析智能体基础配置智能体高级配置智能体高级调试 三、文心智能体发布四、文心智能体体验总结 一、文心智能体平台是什么&#xff1f; AgentBuilder文心智能体平台是基于文心大模型的智能…

饮料加速稳定性试验

饮料加速稳定性试验概览 饮料加速稳定性试验是一种通过模拟加速条件来预测饮料在市场销售期间稳定性的测试方法。这种测试对于确保饮料产品在整个保质期内的质量和安全性至关重要。它可以帮助生产企业优化产品配方、改进包装材料、调整储存和运输条件&#xff0c;从而确保产品在…

苍穹外面day13(day10)---订单状态定时处理、来单提醒和客户催单

Spring Task 同学们可以看我这篇文章 Spring Task初学-CSDN博客 订单状态定时处理 新建OrderTask /*** 定时任务类&#xff0c;定时处理订单状态*/ Component Slf4j public class OrderTask {Autowiredprivate OrderMapper orderMapper;/*** 处理超时订单的方法*/Scheduled(c…

【嵌入式之RTOS】什么是消息队列

目录 一、FreeRTOS消息队列的基本概念 二、FreeRTOS消息队列的工作原理 三、FreeRTOS消息队列的特点 四、FreeRTOS消息队列的应用 五、示例 消息队列是一种用于任务间通信的机制&#xff0c;它允许一个任务&#xff08;生产者&#xff09;向消息队列发送消息&#xff0c;而…

使用uniapp+Django开发的在线工具网站

引言 在当今数字化时代&#xff0c;在线工具网站为用户提供了便捷的服务和功能&#xff0c;本文分享了我使用UniApp和Django开发的一款多平台在线工具网站。通过这个项目&#xff0c;我探索了跨平台开发与强大的后端框架结合的优势&#xff0c;实现了用户友好的界面和稳健的功…

【C++】二维数组定义方式

二维数组有四种定义方式 1、数据类型 数组名[行数 ][ 列数 ]; 2、数据类型 数组名[ 行数 ][ 列数 ]{{数据1&#xff0c;数据2}&#xff0c;{数据3&#xff0c;数据4 }}; 3、数据类型 数组名[ 行数 ][ 列数 ]{数据1&#xff0c;数据2&#xff0c;数据3&#xff…

C++ 标准库和标准模板库

参考&#xff1a; C STL 教程 | 菜鸟教程 (runoob.com) C标准库和标准模板库 - 星朝 - 博客园 (cnblogs.com) C强大的功能来源于其丰富的类库及库函数资源。C标准库的内容总共在50个标准头文件中定义。在C开发中&#xff0c;要尽可能地利用标准库完成。这样做的直接好处包括&am…

【Jenkins未授权访问漏洞 】

默认情况下 Jenkins面板中用户可以选择执行脚本界面来操作一些系统层命令&#xff0c;攻击者可通过未授权访问漏洞或者暴力破解用户密码等进入后台管理服务&#xff0c;通过脚本执行界面从而获取服务器权限。 第一步&#xff1a;使用fofa语句搜索 搜索语句&#xff1a; port&…

NICE Seminar(2022-7-17)进化约束优化(中南大学王勇教授)

​​​​​​​ ACO偏向于离散 DE和PSO偏向于连续变量 确定性替换和随机替换 存在缺陷&#xff0c;可行域可能有多个。 Pfea为可行解所占比例 目标空间中的可行域与在决策空间的可行域中的对应关系&#xff1f;&#xff1f;&#xff1f; 维度中套维度&#xff1f;&#xff1…