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/core | Refine 核心框架,提供I18nProvider类型与useTranslationHook |
@refinedev/nextjs-router | Refine 的 Next.js 路由适配器 |
@refinedev/antd | Ant Design 的 Refine UI 集成 |
@refinedev/simple-rest | 演示用的 REST 数据提供器 |
next-intl | Next.js 官方生态的国际化库,负责消息加载与useTranslations |
js-cookie | 客户端读写语言 Cookie |
整个架构可以概括为一条「三方协作」链路:
- next-intl 负责翻译资源:在服务端读取当前语言,加载对应的 JSON 消息文件,并通过
NextIntlClientProvider注入到组件树; - Refine 的
I18nProvider负责桥接:把 next-intl 的翻译能力封装成 Refine 约定的translate/getLocale/changeLocale三个接口,供 Refine 内部组件(按钮、表格、通知等)调用; - Cookie 负责语言持久化:服务端通过
next/headers的cookies()读写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.title、buttons.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 渲染与服务端请求都能拿到最新语言。语言标识(如en、de)同时用于拼装国旗图标路径/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_LOCALE(en);- 使用动态
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")。
这里有两个值得注意的机制:
- 占位符插值:
notifications.error中的{statusCode}、undoable中的{seconds}、createSuccess中的{resource}就是 Refine 在调用translate(key, options)时传入的options,最终由 next-intl 的t()完成插值; - 资源级(resource)文案:
blog_posts/categories前缀下的titles.list、titles.create等,会被列表、创建、编辑等页面按资源名自动匹配,是让表格标题、页面标题随语言切换的关键。
七、语言切换器:Header 中的 Dropdown 实现
示例在顶栏(Header)中放置了一个语言下拉切换器,完整代码位于 components/header/index.tsx。它的组成包括:
- 语言菜单:由
["en", "de"]数组生成,每项带国旗图标(public/images/flags/en.svg、public/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}'" }启动后可以验证以下行为:
- 打开登录页,默认显示英文;
- 点击 Header 右上角的语言下拉,切换到German,登录页标题、按钮、错误提示、表格列头、页面标题全部切换为德语;
- 刷新页面,语言选择仍然保留(Cookie
NEXT_LOCALE生效); - 再切回 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),仅供参考