news 2026/9/12 13:04:29

Refine v5 + Next.js 国际化实战:i18n Provider 与 next-intl 的完整接入指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Refine v5 + Next.js 国际化实战:i18n Provider 与 next-intl 的完整接入指南

Refine v5 + Next.js 国际化实战:i18n Provider 与 next-intl 的完整接入指南

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

导读

本篇技术指南以 Refine 官方示例 i18n-nextjs 为蓝本,完整讲解在Next.js App Router应用中为 Refine 后台管理面板接入多语言(i18n)能力的全过程:从 i18n Provider 的定义、next-intl 的服务端集成、语言 Cookie 的读写,到语言切换器(Language Switcher)与翻译资源文件的组织方式。读完本文,你将掌握如何在 Refine v5 项目中实现「前端语言切换 + 服务端 Cookie 持久化 + 全量 UI 文案本地化」的完整国际化方案。

Refine 的 i18n Provider 允许你为 Web 应用添加多语言选项。Refine 的默认语言是英文,你可以将自定义翻译集成到项目中以支持不同的语言偏好。本示例(英文 + 德语双语言)展示了如何在实际项目中使用不同的语言选项与翻译文件。

一、示例整体架构:Refine + Next.js + next-intl

在动手之前,先理清这个示例的技术栈与分工。查看示例的 package.json 可以看到它依赖了如下关键包:

依赖包作用
@refinedev/coreRefine 核心框架,提供I18nProvider类型与useTranslationHook
@refinedev/nextjs-routerRefine 的 Next.js 路由适配器
@refinedev/antdAnt Design 的 Refine UI 集成
@refinedev/simple-rest演示用的 REST 数据提供器
next-intlNext.js 官方生态的国际化库,负责消息加载与useTranslations
js-cookie客户端读写语言 Cookie

整个架构可以概括为一条「三方协作」链路:

  • next-intl 负责翻译资源:在服务端读取当前语言,加载对应的 JSON 消息文件,并通过NextIntlClientProvider注入到组件树;
  • Refine 的I18nProvider负责桥接:把 next-intl 的翻译能力封装成 Refine 约定的translate/getLocale/changeLocale三个接口,供 Refine 内部组件(按钮、表格、通知等)调用;
  • Cookie 负责语言持久化:服务端通过next/headerscookies()读写NEXT_LOCALE,客户端通过js-cookie同步写入,保证刷新页面后语言选择不丢失。

下面我们逐层展开实现细节。

二、i18n Provider:Refine 国际化的核心接口

Refine 之所以能做到「UI 框架无关的国际化」,是因为它在核心层抽象了I18nProvider接口。在 _refine_context.tsx 中可以看到,Refine v5 的I18nProvider只需要实现三个方法:

const i18nProvider: I18nProvider = { translate: (key: string, options: any) => t(key, options), getLocale: useLocale, changeLocale: setUserLocale, };

三个方法的职责如下:

方法说明本示例的实现
translate(key, options)根据 key 返回翻译文本,options用于插值(如{statusCode}直接委托给 next-intl 的useTranslations()返回的t()函数
getLocale()返回当前激活的语言代码委托给 next-intl 的useLocale()
changeLocale(locale)切换语言调用setUserLocale写入 Cookie(详见下一节)

把这个i18nProvider作为i18nProvider属性传给<Refine>组件即可生效:

<Refine routerProvider={routerProvider} dataProvider={dataProvider} notificationProvider={useNotificationProvider} authProvider={authProviderClient} i18nProvider={i18nProvider} resources={[ { name: "blog_posts", list: "/blog-posts", create: "/blog-posts/create", ... }, { name: "categories", list: "/categories", create: "/categories/create", ... }, ]} options={{ syncWithLocation: true, warnWhenUnsavedChanges: true, }} >

关于translate的 key 解析约定:Refine 内部会以pages.login.titlebuttons.save这类「点分路径」作为 key 去查找翻译,options则支持类似{statusCode}{seconds}{resource}的模板插值。这一点从本示例的翻译文件中可以验证(详见下文翻译资源一节)。

从源码结构看,_refine_context.tsx是一个"use client"组件,它通过useTranslations()拿到 next-intl 的翻译函数。这意味着:翻译资源的加载发生在服务端,而消费发生在客户端组件中,这正是 next-intl 在 App Router 下的标准用法。

三、语言持久化:服务端 Cookie 读写(NEXT_LOCALE)

语言切换之后,用户刷新页面不能回到默认语言,这是国际化体验的关键点。本示例的方案是:把当前语言写入一个名为NEXT_LOCALE的 Cookie

服务端:读 Cookie 获取当前语言

i18n/index.ts 是一个"use server"模块,使用 Next.js 的next/headers提供的cookies()API:

"use server"; import { cookies } from "next/headers"; import { DEFAULT_LOCALE } from "./config"; import { I18N_COOKIE_NAME } from "./config"; export async function getUserLocale() { return cookies().get(I18N_COOKIE_NAME)?.value || DEFAULT_LOCALE; } export async function setUserLocale(locale: string) { cookies().set(I18N_COOKIE_NAME, locale); }

常量配置:Cookie 名与默认语言

i18n/config.ts 将两个魔法值收敛为常量,便于统一维护:

export const I18N_COOKIE_NAME = "NEXT_LOCALE"; export const DEFAULT_LOCALE = "en";

客户端:切换语言时同步写入 Cookie

在 components/header/index.tsx 中,语言菜单项点击后同时做两件事——调用 Refine 的changeLocale切换 UI,并用js-cookie把语言写进同名 Cookie,保持前后端一致:

const languageMenuItems: MenuProps["items"] = ["en", "de"] .sort() .map((lang: string) => ({ key: lang, onClick: () => { changeLocale(lang); Cookies.set("NEXT_LOCALE", lang); }, label: lang === "en" ? "English" : "German", icon: ( <span style={{ marginRight: 8 }}> <Avatar size={16} src={`/images/flags/${lang}.svg`} /> </span> ), }));

这里changeLocale是 RefineuseTranslation()Hook 返回的方法,内部会触发i18nProvider.changeLocale(即服务端setUserLocale),再配合js-cookie的客户端写入,保证SSR 渲染与服务端请求都能拿到最新语言。语言标识(如ende)同时用于拼装国旗图标路径/images/flags/${lang}.svg

值得注意:useTranslation()同时返回getLocale()用于读取当前语言,下拉菜单的selectedKeys就来自它:selectedKeys: currentLocale ? [currentLocale] : []

四、请求配置与翻译资源加载(getRequestConfig)

next-intl 需要一个「请求配置」来告诉它:当前语言是什么、翻译消息从哪里加载。示例中的 i18n/request.ts 是一个"use server"模块:

"use server"; import { getUserLocale } from "@i18n"; import { getRequestConfig } from "next-intl/server"; export default getRequestConfig(async () => { const locale = await getUserLocale(); return { locale, messages: (await import(`../../public/locales/${locale}/common.json`)) .default, }; });

关键点:

  • getUserLocale()从 Cookie 读取当前语言,缺省回退到DEFAULT_LOCALEen);
  • 使用动态import加载public/locales/{locale}/common.json,这就是翻译消息的来源;
  • getRequestConfig是 next-intl 在 App Router 服务端的标准配置入口,next-intl 插件会自动调用它。

为了让 next-intl 真正介入 Next.js 构建,还需要在 next.config.mjs 中启用插件:

import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin(); /** @type {import('next').NextConfig} */ const nextConfig = { transpilePackages: ["@refinedev/antd"], }; export default withNextIntl(nextConfig);

其中transpilePackages: ["@refinedev/antd"]是为了让 Next.js 正确转译 Refine 的 antd 包。

五、根布局:把语言与消息注入整个应用

一切准备就绪后,在 src/app/layout.tsx(App Router 根布局)中把语言、消息、Refine 上下文串起来:

export default async function RootLayout({ children }) { const cookieStore = cookies(); const theme = cookieStore.get("theme"); const locale = await getLocale(); const messages = await getMessages(); return ( <html lang={locale}> <body> <AntdRegistry> <NextIntlClientProvider locale={locale} messages={messages}> <RefineContext themeMode={theme?.value}>{children}</RefineContext> </NextIntlClientProvider> </AntdRegistry> </body> </html> ); }

要点解读:

  • getLocale()/getMessages()来自next-intl/server,分别返回当前语言与全部翻译消息;
  • <html lang={locale}>同步更新 HTML 的lang属性,有利于可访问性与 SEO;
  • NextIntlClientProvider把 locale 与 messages 注入客户端组件树,之后useTranslations()useLocale()才能工作;
  • <RefineContext>是上一节定义的客户端组件,内部消费这些上下文并把i18nProvider传给<Refine>

六、翻译资源文件:common.json 的组织与占位符

示例把翻译消息放在public/locales/{locale}/common.json,目前提供了两套:英语 en/common.json 与德语 de/common.json。

以英语文件为例,翻译 key 的组织完全对齐 Refine 内部约定,覆盖了后台最常见的文案场景:

{ "ID": "ID", "pages": { "login": { "title": "Sign in to your account", "fields": { "email": "Email", "password": "Password" }, "errors": { "requiredEmail": "Email is required", "requiredPassword": "Password is required", "validEmail": "Invalid email address" }, "buttons": { "submit": "Login", "forgotPassword": "Forgot password?", "noAccount": "Don’t have an account?", "rememberMe": "Remember me" } }, "forgotPassword": { "title": "Forgot your password?", "buttons": { "submit": "Send reset instructions" } }, "register": { "title": "Sign up for your account" }, "updatePassword": { "title": "Update password" } }, "actions": { "list": "List", "create": "Create", "edit": "Edit", "show": "Show", "delete": "Delete" }, "buttons": { "create": "Create", "save": "Save", "logout": "Logout", "delete": "Delete", "edit": "Edit", "cancel": "Cancel", "confirm": "Are you sure?", "import": "Import", "clone": "Clone" }, "warnWhenUnsavedChanges": "Are you sure you want to leave? You have unsaved changes.", "notifications": { "success": "Successful", "error": "Error (status code: {statusCode})", "undoable": "You have {seconds} seconds to undo", "createSuccess": "Successfully created {resource}", "createError": "There was an error creating {resource} (status code: {statusCode})", "deleteSuccess": "Successfully deleted {resource}", "importProgress": "Importing: {processed}/{total}" }, "loading": "Loading", "blog_posts": { "blog_posts": "Blog Posts", "fields": { "id": "Id", "title": "Title", "category": "Category", "content": "Content" }, "titles": { "create": "Create Post", "edit": "Edit Post", "list": "Posts", "show": "Show Post" } }, "categories": { "categories": "Categories", "fields": { "id": "Id", "title": "Title" }, "titles": { "create": "Create Category", "edit": "Edit Category", "list": "Categories", "show": "Show Category" } } }

德语文件 de/common.json 的 key 结构与英文完全一致,只是 value 换成了德语(例如登录标题变为"Melden Sie sich bei Ihrem Konto an""buttons.save": "Speichern")。

这里有两个值得注意的机制:

  1. 占位符插值notifications.error中的{statusCode}undoable中的{seconds}createSuccess中的{resource}就是 Refine 在调用translate(key, options)时传入的options,最终由 next-intl 的t()完成插值;
  2. 资源级(resource)文案blog_posts/categories前缀下的titles.listtitles.create等,会被列表、创建、编辑等页面按资源名自动匹配,是让表格标题、页面标题随语言切换的关键。

七、语言切换器:Header 中的 Dropdown 实现

示例在顶栏(Header)中放置了一个语言下拉切换器,完整代码位于 components/header/index.tsx。它的组成包括:

  • 语言菜单:由["en", "de"]数组生成,每项带国旗图标(public/images/flags/en.svgpublic/images/flags/de.svg);
  • 当前语言显示:通过getLocale()读取并展示(English / German);
  • 切换动作changeLocale(lang)+Cookies.set("NEXT_LOCALE", lang)
  • 配套功能:该 Header 还整合了明暗主题切换(ColorModeContext)与用户信息展示(useGetIdentity),展示 Refine + antd 布局的常规组合用法。
const { getLocale, changeLocale } = useTranslation(); const currentLocale = getLocale(); // ... <Dropdown menu={{ items: languageMenuItems, selectedKeys: currentLocale ? [currentLocale] : [], }} > <Button type="text"> <Space> <Avatar size={16} src={`/images/flags/${currentLocale}.svg`} /> <Typography.Text> {currentLocale === "en" ? "English" : "German"} </Typography.Text> <DownOutlined /> </Space> </Button> </Dropdown>

其中useTranslation来自@refinedev/core,这正是 Refine 暴露给开发者(以及 Refine 内部组件)的统一国际化入口。通过它,业务代码无需关心底层是 next-intl、react-i18next 还是其他库。

八、本地运行与验证

仓库中的 README.MD 提供了两种运行方式,这里整理为可复制的命令:

方式一:通过 create-refine-app 脚手架运行

npm create refine-app@latest -- --example i18n-nextjs

该命令会拉取本示例并初始化依赖(示例要求 Node.js >= 20,见 package.json 的engines字段)。

方式二:在仓库内直接安装并启动

cd examples/i18n-nextjs pnpm install pnpm dev

对应的脚本定义在 package.json:

{ "dev": "cross-env NODE_OPTIONS=--max_old_space_size=4096 refine dev", "build": "refine build", "start": "refine start", "lint": "eslint '**/*.{js,jsx,ts,tsx}'" }

启动后可以验证以下行为:

  1. 打开登录页,默认显示英文;
  2. 点击 Header 右上角的语言下拉,切换到German,登录页标题、按钮、错误提示、表格列头、页面标题全部切换为德语;
  3. 刷新页面,语言选择仍然保留(CookieNEXT_LOCALE生效);
  4. 再切回 English,页面恢复英文。

九、关键源码路径速查

为了方便深入阅读与二次开发,将本示例涉及的核心文件汇总如下:

关注点文件
Refine 应用组装与 i18nProvider 定义src/app/_refine_context.tsx
服务端 Cookie 读写(get/setUserLocale)src/i18n/index.ts
Cookie 名与默认语言常量src/i18n/config.ts
next-intl 请求配置(动态加载消息)src/i18n/request.ts
App Router 根布局(注入 locale/messages)src/app/layout.tsx
Header 语言切换器src/components/header/index.tsx
英文翻译资源public/locales/en/common.json
德文翻译资源public/locales/de/common.json
next-intl 插件配置next.config.mjs

结语

通过本文可以总结出一条清晰的 Refine + Next.js 国际化接入路径:用 next-intl 承担翻译资源的加载与插值,用 Refine 的I18nProvider做桥接,用NEXT_LOCALECookie 打通前后端语言状态。这种组合既保留了 Refine「UI 无关」的国际化抽象(translate/getLocale/changeLocale三接口),又充分利用了 Next.js App Router 的服务端能力,值得在真实后台项目中直接复用。

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 13:04:20

三步搞定网页视频下载:猫抓扩展自动嗅探并保存在线媒体资源

三步搞定网页视频下载&#xff1a;猫抓扩展自动嗅探并保存在线媒体资源 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 猫抓(cat-catch) 是一款浏览…

作者头像 李华
网站建设 2026/9/12 13:03:40

语音情绪识别项目拆解:特征提取、模型选型与配置驱动训练

简介&#xff1a;一套面向深度学习语音情绪识别方向的完整工程包&#xff0c;主要服务于人工智能相关专业的毕业设计、课程设计开发者&#xff0c;覆盖语音数据特征提取、模型训练、预测评估全流程。压缩包共34个文件&#xff0c;包含17个Python脚本&#xff08;如train.py、pr…

作者头像 李华
网站建设 2026/9/12 13:03:25

Teamo增强版Clawdbot:金融数据分析与飞书自动化实践

1. 项目概述&#xff1a;Teamo增强版Clawdbot为何一夜爆火&#xff1f;最近一个名为Teamo增强版Clawdbot的AI工具在技术圈引发热议&#xff0c;它号称能够7x24小时不间断分析股票行情&#xff0c;还能接入飞书实现自动化办公&#xff0c;更神奇的是具备"自我进化"能力…

作者头像 李华
网站建设 2026/9/12 13:01:52

mimalloc 深度解析:替换 C/C++ 系统默认内存分配器的实战与避坑

mimalloc 深度解析&#xff1a;替换 C/C 系统默认内存分配器的实战与避坑 【免费下载链接】mimalloc mimalloc is a compact general purpose allocator with excellent performance. 项目地址: https://gitcode.com/GitHub_Trending/mi/mimalloc mimalloc 是一个紧凑、…

作者头像 李华
网站建设 2026/9/12 13:01:06

研究设计怎么从问题到方法?一篇讲透全链路

研究设计从问题到方法这条链&#xff0c;卡住的人往往不是不会写&#xff0c;而是中间换过手的地方没记账&#xff1a;想答的和答得了的对不上&#xff0c;说出口的概念落不到地上。这篇不复述流程&#xff0c;只把这条链上换的四次账摊开——每次换掉什么、这笔账落在论文哪一…

作者头像 李华