Nextjs——国际化那些事儿

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

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

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

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 constexport 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 headersconst negotiatorHeaders: Record<string, string> = {};// eslint-disable-next-line no-return-assignrequest.headers.forEach((value, key) => (negotiatorHeaders[key] = value));// Use negotiator and intl-localematcher to get best localeconst 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.tsimport {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/xxxexport type Locale = (typeof locales)[number];export default getRequestConfig(async ({locale}) => {// Validate that the incoming `locale` parameter is validif (!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.rhkb.cn/news/391169.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法--初阶

1、tips 1.1、set求交集 {1,2,3} & {2,3} & {1,2} {2} 其实就是位运算&#xff0c; 只有set可以这样使用&#xff0c; list没有这种用法 {1,2,3} | {2,3, 4} | {1,2} {1, 2, 3, 4} 并集 1.2、*与** * 序列(列表、元组)解包&#xff0c;如果是字典&#xff0c;那…

第一个 Flask 项目

第一个 Flask 项目 安装环境创建项目启动程序访问项目参数说明Flask对象的初始化参数app.run()参数 应用程序配置参数使用 Flask 的 config.from_object() 方法使用 Flask 的 config.from_pyfile() 方法使用 Flask 的 config.from_envvar() 方法步骤 1: 设置环境变量步骤 2: 编…

【C++学习第19天】最小生成树(对应无向图)

一、最小生成树 二、代码 1、Prim算法 #include <cstring> #include <iostream> #include <algorithm>using namespace std;const int N 510, INF 0x3f3f3f3f;int n, m; int g[N][N]; int dist[N]; bool st[N];int prim() {memset(dist, 0x3f, sizeof di…

学习分享:解析电商 API 接入的技术重难点及解决方案

在当今电商业务迅速发展的时代&#xff0c;接入电商 API 已成为许多企业提升竞争力和拓展业务的重要手段。然而&#xff0c;在这个过程中&#xff0c;往往会遇到一系列的技术重难点。本文将深入解析这些问题&#xff0c;并提供相应的解决方案。 一、电商 API 接入的技术重难点 …

c++ 初始值设定项列表(initializer_list)

引例 我们在写c代码的时候&#xff0c;多多少少会遇到这样写的&#xff1a; 如果是这样写还好说&#xff1a; 第一个是因为编译器强制匹配参数。 其他都是因为在有对应构造函数的情况下支持的隐式类型转换。 而支持的构造函数是这个&#xff1a; 如果有不懂的可以开这一篇&a…

麦田物语第十八天

系列文章目录 麦田物语第十八天 文章目录 系列文章目录一、(Editor)制作 [SceneName] Attribute 特性二、场景切换淡入淡出和动态 UI 显示一、(Editor)制作 [SceneName] Attribute 特性 在本节课我们编写Unity的特性Attribute来更好的完善我们项目,具体是什么呢,就是当…

深入理解 ReLU 激活函数及其在深度学习中的应用【激活函数、Sigmoid、Tanh】

ReLU&#xff08;Rectified Linear Unit&#xff09;激活函数 ReLU&#xff08;Rectified Linear Unit&#xff09;激活函数是一种广泛应用于神经网络中的非线性激活函数。其公式如下&#xff1a; ReLU ( x ) max ⁡ ( 0 , x ) \text{ReLU}(x) \max(0, x) ReLU(x)max(0,x) 在…

docker部署elasticsearch和Kibana

部署elasticsearch 通过下面的Docker命令即可安装单机版本的elasticsearch&#xff1a; docker run -d \--name es \-e "ES_JAVA_OPTS-Xms512m -Xmx512m" \-e "discovery.typesingle-node" \-v es-data:/usr/share/elasticsearch/data \-v es-plugins:/u…

《LeetCode热题100》---<5.①普通数组篇五道>

本篇博客讲解LeetCode热题100道普通数组篇中的五道题 第一道&#xff1a;最大子数组和&#xff08;中等&#xff09; 第二道&#xff1a;合并区间&#xff08;中等&#xff09; 第一道&#xff1a;最大子数组和&#xff08;中等&#xff09; 法一&#xff1a;贪心算法 class So…

我的256天创作纪念日

机缘 ————今天一大早收到CSDN的推送消息&#xff0c;告诉我这是我的256天创作纪念日。在这个特别的日子里&#xff0c;我回望自己踏上创作之路的点点滴滴&#xff0c;心中充满了感慨与感激。从最初的懵懂尝试到如今能够自信地分享见解&#xff0c;这段旅程不仅见证了我的成…

【教学类-73-01】20240804镂空瓶子01

背景需求&#xff1a; 瓶子里的春天呀&#xff01; - 小红书 (xiaohongshu.com)https://www.xiaohongshu.com/explore/63ef87f8000000000703acae?app_platformandroid&ignoreEngagetrue&app_version8.47.0&share_from_user_hiddentrue&xsec_sourceapp_share&…

揭秘Matplotlib等高线图:让数据‘高山流水‘间,笑点与深度并存!

1. 引言 在这个数据如山的时代&#xff0c;你是不是也曾在茫茫数海中迷失方向&#xff0c;渴望找到那片隐藏的“数据绿洲”&#xff1f;别怕&#xff0c;今天咱们就来聊聊Matplotlib这位绘图界的魔术师&#xff0c;特别是它那令人叹为观止的等高线图技能。想象一下&#xff0c…

MySQL:数据库用户

数据库用户 在关系型数据库管理系统中&#xff0c;数据库用户&#xff08;USER&#xff09;是指具有特定权限和访问权限的登录账户。每个用户都有自己的用户名和密码&#xff0c;以便系统可以通过认证来识别他们的身份。数据库用户可以登录数据库&#xff0c;在其中执行各种类…

【QT】常用控件-上

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;折纸花满衣 目录 &#x1f449;&#x1f3fb;QWidgetenabledgeometryrect制作上下左右按钮 window frame 的影响window titlewindowIcon代码示例: 通过 qrc 管理图片作为图标 windowOpacitycursor使用qrc自…

【论文笔记】SEED: A Simple and Effective 3D DETR in Point Clouds

原文链接&#xff1a;https://arxiv.org/abs/2407.10749 简介&#xff1a;基于DETR的3D点云检测器难以达到较高的性能&#xff0c;这可能有两个原因&#xff1a;&#xff08;1&#xff09;由于点云的稀疏性和分布不均匀&#xff0c;获取合适的物体查询较为困难&#xff1b;&…

基于微信小程序的微课堂笔记的设计与实现(源码+论文+部署讲解等)

博主介绍&#xff1a;✌全网粉丝10W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流✌ 技术栈介绍&#xff1a;我是程序员阿龙&#xff…

给虚拟机Ubuntu扩展硬盘且不丢数据

1.Ubuntu关机状态下先扩展&#xff0c;如扩展20GB 2.进入ubuntu&#xff0c;切换root登录&#xff0c;必须是root全选&#xff0c;否则启动不了分区工具gparted 将新的20GB创建好后&#xff0c;选择ext4,primary&#xff1b; 3.永久挂载 我的主目录在/并挂载到/dev/sda1 从图…

【C++】C++11之新的类功能与可变参数模板

目录 一、新的默认成员函数 二、新的关键字 2.1 default 2.2 detele 2.3 final和override 三、可变参数模板 3.1 定义 3.2 递归展开参数包 3.3 逗号表达式展开参数包 3.4 emplace_back 一、新的默认成员函数 在C11之前&#xff0c;默认成员函数只有六个&#xff0c;…

【机器学习算法基础】(基础机器学习课程)-11-k-means-笔记

示例案例 为了更好地理解 K-Means 算法&#xff0c;下面通过一个简单的案例进行说明。 假设我们有以下 10 个二维数据点&#xff0c;表示不同商店的销售额&#xff08;单位&#xff1a;千元&#xff09;和顾客数&#xff08;单位&#xff1a;人&#xff09;&#xff1a; [(1…

常见cms漏洞之dedecms

DedeCMS是织梦团队开发PHP 网站管理系统&#xff0c;它以简单、易用、高效为特色&#xff0c;组建出各种各样各具特色的网站&#xff0c;如地方门户、行业门户、政府及企事业站点等。 下载地址请网上自行寻找 搭建方式选择php study 首先搭建环境 #前台http://localhost/dedecm…