Refine i18nProvider 完整指南:为 React 管理后台构建多语言国际化方案
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本文聚焦 Refine 框架的国际化(i18n)核心抽象 ——i18nProvider,讲解其接口契约、三个核心方法(translate/changeLocale/getLocale)的实现细节、如何接入<Refine />组件、如何在业务组件中通过useTranslation系列 hooks 调用翻译能力,以及如何通过翻译文件覆盖 Refine 内置组件文案。读完本文,你将能够在自己的 React 管理后台中接入任意 i18n 库(如 react-i18next),并实现完整的运行时多语言切换能力。
i18nProvider:Refine 的国际化抽象层
国际化(Internationalization,简称 i18n)允许软件针对不同地区与语言进行本地化适配。Refine 本身不绑定任何 i18n 框架,而是定义了一个轻量的 provider 契约 ——i18nProvider,你可以在它之上接入 react-i18next、i18next、polyglot 等任意成熟方案。
Refine 期望的I18nProvider类型定义如下(该类型实际声明于 packages/core/src/contexts/i18n/types.ts):
import { I18nProvider } from "@refinedev/core"; const i18nProvider: I18nProvider = { translate: (key: string, options?: any, defaultMessage?: string) => string, changeLocale: (lang: string, options?: any) => Promise, getLocale: () => string, };从源码可以看到,这三个方法的类型分别为:
type TranslateFunction = ( key: string, options?: any, defaultMessage?: string, ) => string; type ChangeLocaleFunction = ( locale: string, options?: any, ) => Promise<any> | any; type GetLocaleFunction = () => string;也就是说,provider 需要暴露一个同步返回字符串的translate、一个负责切换语言并返回 Promise 的changeLocale、以及一个返回当前语言标识的getLocale。translate承担 "把 key 翻译成文案",changeLocale承担 "运行时切换语言",getLocale承担 "查询当前语言状态" —— 三者组合起来就构成了完整的翻译能力闭环。
注册 i18nProvider:接入<Refine />
创建好i18nProvider之后,将它作为 prop 传给<Refine />组件即可全局启用:
import { Refine } from "@refinedev/core"; import i18nProvider from "./i18nProvider"; const App: React.FC = () => { return ( <Refine i18nProvider={i18nProvider} /* 其他 providers,如 dataProvider、authProvider 等 */ > {/* 应用内容 */} </Refine> ); };在 Refine 源码中,I18nProvider类型被IRefineOptions对应的 contexts/refine/types.ts 引用,并通过 contexts/i18n/index.tsx 中的I18nContextProvider注入到 React Context 中:
export const I18nContext = React.createContext<II18nContext>({}); export const I18nContextProvider: React.FC<PropsWithChildren<II18nContext>> = ({ i18nProvider, children, }) => { return ( <I18nContext.Provider value={{ i18nProvider }}> {children} </I18nContext.Provider> ); };注册完成后,你就可以通过useTranslationhook 在任意组件中获得翻译能力。注意:I18nContext的默认值是空对象,未传入i18nProvider时,部分 hook(如useGetLocale)会直接抛出错误,这一点在后面的源码分析中会详细说明。
三个核心方法详解
translate:函数重载与回退逻辑
translate将参数透传给i18nProvider.translate,并期望返回字符串。它支持两种函数签名(函数重载):
function translate(key: string, options?: any, defaultMessage?: string): string; function translate(key: string, defaultMessage?: string): string;第一种用法传入key、options、defaultMessage三个参数;第二种用法传入key和defaultMessage两个参数,options为可选参数。
签名一:key+defaultMessage
import { I18nProvider } from "@refinedev/core"; import { useTranslation } from "react-i18next"; const { t } = useTranslation(); const i18nProvider: I18nProvider = { translate: (key: string, defaultMessage?: string) => t(key, defaultMessage), // ... };在业务组件中调用:
import { useTranslation } from "@refinedev/core"; const { translate } = useTranslation(); // 若 "posts.fields.title" 在翻译文件中存在则返回对应文案,否则返回默认值 "Title" translate("posts.fields.title", "Title");签名二:key+options+defaultMessage
import { I18nProvider } from "@refinedev/core"; import { useTranslation } from "react-i18next"; const { t } = useTranslation(); const i18nProvider: I18nProvider = { translate: (key: string, options?: any, defaultMessage?: string) => t(key, defaultMessage, options), // ... };import { useTranslation } from "@refinedev/core"; const { translate } = useTranslation(); // options 可用于指定命名空间或插值变量 const title = translate("posts.fields.title", { ns: "resources" }, "Title");从源码 packages/core/src/hooks/i18n/useTranslate.ts 可以看出,hook 内部对三种入参形态做了统一的兜底处理:
function translate( key: string, options?: string | any, defaultMessage?: string, ) { return ( i18nProvider?.translate(key, options, defaultMessage) ?? defaultMessage ?? (typeof options === "string" && typeof defaultMessage === "undefined" ? options : key) ); }这段代码揭示了两个重要的回退规则:当i18nProvider未定义时,translate不会崩溃,而是依次回退到defaultMessage、字符串形式的options,最后回退到key本身;也就是说,即使你暂时没有接入任何 i18n 框架,组件中使用translate("posts.fields.title", "Title")也会安全地渲染出 "Title"。这个设计让组件在翻译缺失时依然可用,非常适合渐进式接入国际化。
changeLocale:运行时切换语言
changeLocale接收新的 locale 标识,透传给i18nProvider.changeLocale,并返回一个 Promise。它的类型签名如下:
changeLocale: (locale: string, options?: any) => Promise<any>;典型的实现(基于 react-i18next)会在内部调用i18n.changeLanguage(locale):
import { I18nProvider } from "@refinedev/core"; import i18n from "./i18n"; const i18nProvider: I18nProvider = { // ... changeLocale: (lang: string) => i18n.changeLanguage(lang), // ... };getLocale:读取当前语言
getLocale期望返回一个字符串,即从i18nProvider中读取当前 locale:
getLocale: () => string;典型实现:
const i18nProvider: I18nProvider = { // ... getLocale: () => i18n.language, // ... };在业务组件中使用 useTranslation 系列 hooks
useTranslation是 Refine 提供的统一入口,它内部组合了三个更细粒度的 hook。源码 packages/core/src/hooks/i18n/useTranslation.tsx 清楚地展示了这一点:
export const useTranslation = () => { const translate = useTranslate(); const changeLocale = useSetLocale(); const getLocale = useGetLocale(); return { translate, changeLocale, getLocale, }; };语言切换组件示例
下面是一个完整可用的多语言切换组件(文档示例),它同时使用了translate、changeLocale、getLocale三个方法:
import { useTranslation } from "@refinedev/core"; export const MyComponent = () => { const { translate, getLocale, changeLocale } = useTranslation(); const currentLocale = getLocale(); return ( <div> <h1>{translate("languages")}</h1> <button onClick={() => changeLocale("en")} disabled={currentLocale === "en"} > English </button> <button onClick={() => changeLocale("de")} disabled={currentLocale === "de"} > German </button> </div> ); };三个细分 hook 的行为差异
useTranslate(useTranslate.ts):返回translate函数,带上述回退逻辑,未接入 provider 时不会报错。useSetLocale(useSetLocale.ts):返回changeLocale函数,内部用useCallback包裹:
export const useSetLocale = () => { const { i18nProvider } = useContext(I18nContext); return useCallback((lang: string) => i18nProvider?.changeLocale(lang), []); };useGetLocale(useGetLocale.ts):返回getLocale函数,但注意 —— 与useTranslate不同,当i18nProvider未定义时它会抛出明确错误:
export const useGetLocale: UseGetLocaleType = () => { const { i18nProvider } = useContext(I18nContext); if (!i18nProvider) { throw new Error( "useGetLocale cannot be called without i18n provider being defined.", ); } return useCallback(() => i18nProvider.getLocale(), []); };因此,如果你的应用中存在未接入i18nProvider的页面或组件,应避免直接调用useGetLocale,或在使用前确认 provider 已经注册。
如果你只需要翻译单个文本,也可以使用useTranslate的简化形态:
import { useTranslate } from "@refinedev/core"; export const MyComponent = () => { const translate = useTranslate(); return <button>{translate("my.translate.text")}</button>; };覆盖内置组件文案:Translation 文件全量清单
Refine 的所有内置组件(登录页、按钮、表格、通知、面包屑等)都支持 i18n。这意味着你不需要修改任何组件源码,只需创建自己的翻译文件,即可覆盖 Refine 的默认文本。完整的可覆盖翻译 key 清单维护在 documentation/docs/partials/_partial-translation-file-en.md 中,以下为其完整内容(可直接作为locales/en/common.json的起点):
{ "pages": { "login": { "title": "Sign in to your account", "signin": "Sign in", "signup": "Sign up", "divider": "or", "fields": { "email": "Email", "password": "Password" }, "errors": { "validEmail": "Invalid email address", "requiredEmail": "Email is required", "requiredPassword": "Password is required" }, "buttons": { "submit": "Login", "forgotPassword": "Forgot password?", "noAccount": "Don’t have an account?", "rememberMe": "Remember me" } }, "forgotPassword": { "title": "Forgot your password?", "fields": { "email": "Email" }, "errors": { "validEmail": "Invalid email address", "requiredEmail": "Email is required" }, "buttons": { "submit": "Send reset instructions" } }, "register": { "title": "Sign up for your account", "fields": { "email": "Email", "password": "Password" }, "errors": { "validEmail": "Invalid email address", "requiredEmail": "Email is required", "requiredPassword": "Password is required" }, "buttons": { "submit": "Register", "haveAccount": "Have an account?" } }, "updatePassword": { "title": "Update password", "fields": { "password": "New Password", "confirmPassword": "Confirm new password" }, "errors": { "confirmPasswordNotMatch": "Passwords do not match", "requiredPassword": "Password required", "requiredConfirmPassword": "Confirm password is required" }, "buttons": { "submit": "Update" } }, "error": { "info": "You may have forgotten to add the {{action}} component to {{resource}} resource.", "404": "Sorry, the page you visited does not exist.", "resource404": "Are you sure you have created the {{resource}} resource.", "backHome": "Back Home" } }, "actions": { "list": "List", "create": "Create", "edit": "Edit", "show": "Show" }, "buttons": { "create": "Create", "save": "Save", "logout": "Logout", "delete": "Delete", "edit": "Edit", "cancel": "Cancel", "confirm": "Are you sure?", "filter": "Filter", "clear": "Clear", "refresh": "Refresh", "show": "Show", "undo": "Undo", "import": "Import", "clone": "Clone", "notAccessTitle": "You don't have permission to access" }, "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}}", "deleteError": "Error when deleting {{resource}} (status code: {{statusCode}})", "editSuccess": "Successfully edited {{resource}}", "editError": "Error when editing {{resource}} (status code: {{statusCode}})", "importProgress": "Importing: {{processed}}/{{total}}" }, "loading": "Loading", "tags": { "clone": "Clone" }, "dashboard": { "title": "Dashboard" }, "posts": { "posts": "Posts", "fields": { "id": "Id", "title": "Title", "category": "Category", "status": { "title": "Status", "published": "Published", "draft": "Draft", "rejected": "Rejected" }, "content": "Content", "createdAt": "Created At" }, "titles": { "create": "Create Post", "edit": "Edit Post", "list": "Posts", "show": "Show Post" } }, "table": { "actions": "Actions" }, "documentTitle": { "default": "refine", "suffix": " | Refine", "post": { "list": "Posts | Refine", "show": "#{{id}} Show Post | Refine", "edit": "#{{id}} Edit Post | Refine", "create": "Create new Post | Refine", "clone": "#{{id}} Clone Post | Refine" } }, "autoSave": { "success": "saved", "error": "auto save failure", "loading": "saving...", "idle": "waiting for changes" } }几点使用建议:
- 命名空间与默认 key:上面的 JSON 以
common作为默认命名空间;如果你的 i18n 配置把defaultNS设为common,Refine 内置组件的文案就会自动从这里解析。 - 插值变量:注意
{{action}}、{{resource}}、{{statusCode}}、{{seconds}}、{{processed}}、{{total}}、{{id}}这类占位符,它们由 Refine 在调用translate时通过options传入,翻译文件必须原样保留。 documentTitle特殊项:它用于控制浏览器标签页标题,例如post.list会在列表页显示 "Posts | Refine",这要求你的路由 provider 支持动态文档标题。autoSave特殊项:当表单开启自动保存(autoSave)时会用到这组文案,分别对应已保存、保存失败、保存中和等待修改四种状态。
集成真实 i18n 框架:以 react-i18next 为例
仓库中的 i18n-react 示例 演示了如何用 i18next + react-i18next 构建完整的i18nProvider。其 src/i18n.ts 是初始化 i18next 的典型配置:
import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import Backend from "i18next-xhr-backend"; import detector from "i18next-browser-languagedetector"; i18n .use(Backend) .use(detector) .use(initReactI18next) .init({ supportedLngs: ["en", "de"], backend: { loadPath: "/locales/{{lng}}/{{ns}}.json", }, ns: ["common"], defaultNS: "common", fallbackLng: ["en", "de"], }); export default i18n;该配置的关键点:
supportedLngs: ["en", "de"]:声明支持的语言,示例项目支持英语和德语;backend.loadPath:按/locales/{{lng}}/{{ns}}.json的路径模式按需加载翻译文件,{{lng}}是语言代码、{{ns}}是命名空间;ns/defaultNS:声明并使用common命名空间,与上一节翻译文件中的命名空间保持一致;fallbackLng:当某个语言缺少翻译时回退到可用语言,避免界面出现空白。
基于此,i18nProvider可以这样实现:
import i18n from "./i18n"; import { I18nProvider } from "@refinedev/core"; const i18nProvider: I18nProvider = { translate: (key, options, defaultMessage) => i18n.t(key, defaultMessage, options), changeLocale: (lang) => i18n.changeLanguage(lang), getLocale: () => i18n.language, };如果你想参考 Next.js 场景下的集成方式,仓库中还提供了 i18n-nextjs 示例,展示了在服务端渲染框架中如何组织 i18n。
FAQ:如何自动化生成多语言翻译文件
为每种语言手工维护翻译 JSON 很繁琐。社区中有一种成熟的思路:在 CI 流水线中加入基于 DeepL(AI 翻译服务)的自动化步骤,由机器人自动把locales/en下的 JSON 翻译成其他语言并提交回仓库。具体做法是引入一个 GitHub Action,监听翻译文件的变更事件,触发 DeepL 翻译任务,将产物写回locales目录。这样开发者只需维护单一语言(如英语)的翻译源文件,其余语言由流水线自动同步,既保证翻译质量的一致性,也减少了人工重复劳动。
参考示例
- i18n-react 示例:基于 react-i18next 的完整 i18nProvider 实现,支持 en/de 双语切换;
- i18n-nextjs 示例:Next.js 场景下的 i18n 集成;
- useTranslation hook 文档:
useTranslation/useTranslate/useSetLocale/useGetLocale的详细用法; - 核心类型定义:
I18nProvider接口的权威源码; - 翻译文件全量清单:Refine 内置组件可覆盖的全部翻译 key。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考