Refine 国际化实战:用德语 common.json 翻译文件覆盖内置 UI 文本(i18n Provider 与 react-i18next 接入指南)
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本篇技术指南以 Refine 官方 i18n 文档中的德语翻译文件(/locales/de/common.json)为核心,系统讲解 Refine 的国际化(i18n)机制:从i18nProvider接口、react-i18next 实例化、语言切换 Header,到内置组件(错误页、通知、文档标题、自动保存指示器)如何消费common.json中的翻译键。读完本文,你将掌握如何为 Refine 后台应用添加完整的多语言支持,并具备用自定义翻译文件覆盖 Refine 所有内置文本的能力。
i18n 机制总览:Refine 如何与任意 i18n 框架协作
Refine 的国际化设计理念是"框架无关":它不绑定任何特定的 i18n 库,而是定义了一个极简的i18nProvider接口作为中间层。只要基于你选定的 i18n 库(如 react-i18next、react-intl 等)实现了这个接口,再注入到<Refine />组件,Refine 内置的所有组件(按钮、表格、通知、错误页、文档标题等)就会自动使用翻译能力。
i18nProvider接口定义在@refinedev/core中,只有三个方法:
import { I18nProvider } from "@refinedev/core"; const i18nProvider: I18nProvider = { translate: (key: string, options?: any, defaultMessage?: string) => string, changeLocale: (lang: string, options?: any) => Promise, getLocale: () => string, };translate(key, options, defaultMessage):根据键返回翻译文本,options用于注入插值变量(如{{resource}});changeLocale(lang):切换语言并返回 Promise;getLocale():返回当前语言标识。
该接口与本文主角德语翻译文件配合的完整教程位于 Refine v5 官方 i18n 指南,同一份德语翻译内容也被 v4 版本文档 复用,说明这套翻译键体系在 Refine 各版本间保持稳定。
完整德语翻译文件:/locales/de/common.json
Refine 的官方指南使用react-i18next框架,翻译文件采用"语言/命名空间"的目录约定,存放在静态资源目录下。德语的完整文件内容如下(本文件即 documentation/docs/partials/_partial-translation-file-de.md 所定义的正文):
{ "pages": { "login": { "title": "Melden Sie sich bei Ihrem Konto an", "signin": "Einloggen", "signup": "Anmelden", "divider": "oder", "fields": { "email": "Email", "password": "Passwort" }, "errors": { "validEmail": "Ungültige E-Mail-Adresse", "requiredEmail": "E-Mail ist erforderlich", "requiredPassword": "Passwort wird benötigt" }, "buttons": { "submit": "Anmeldung", "forgotPassword": "Passwort vergessen?", "noAccount": "Sie haben kein Konto?", "rememberMe": "Erinnere dich an mich" } }, "forgotPassword": { "title": "Haben Sie Ihr Passwort vergessen?", "fields": { "email": "Email" }, "errors": { "validEmail": "Ungültige E-Mail-Adresse", "requiredEmail": "E-Mail ist erforderlich" }, "buttons": { "submit": "Anweisungen zum Zurücksetzen senden" } }, "register": { "title": "Registrieren Sie sich für Ihr Konto", "fields": { "email": "Email", "password": "Passwort" }, "errors": { "validEmail": "Ungültige E-Mail-Adresse", "requiredEmail": "E-Mail ist erforderlich", "requiredPassword": "Passwort wird benötigt" }, "buttons": { "submit": "Registrieren", "haveAccount": "Ein Konto haben?" } }, "updatePassword": { "title": "Kennwort aktualisieren", "fields": { "password": "Neues Passwort", "confirmPassword": "Bestätige neues Passwort" }, "errors": { "confirmPasswordNotMatch": "Passwörter stimmen nicht überein", "requiredPassword": "Passwort wird benötigt", "requiredConfirmPassword": "Das Feld „Passwort bestätigen“ ist erforderlich" }, "buttons": { "submit": "Aktualisieren" } }, "error": { "info": "Sie haben vergessen, {{action}} component zu {{resource}} hinzufügen.", "404": "Leider existiert diese Seite nicht.", "resource404": "Haben Sie die {{resource}} resource erstellt?", "backHome": "Zurück" } }, "actions": { "list": "Aufführen", "create": "Erstellen", "edit": "Bearbeiten", "show": "Zeigen" }, "buttons": { "create": "Erstellen", "save": "Speichern", "logout": "Abmelden", "delete": "Löschen", "edit": "Bearbeiten", "cancel": "Abbrechen", "confirm": "Sicher?", "filter": "Filter", "clear": "Löschen", "refresh": "Erneuern", "show": "Zeigen", "undo": "Undo", "import": "Importieren", "clone": "Klon", "notAccessTitle": "Sie haben keine zugriffsberechtigung" }, "warnWhenUnsavedChanges": "Nicht gespeicherte Änderungen werden nicht übernommen.", "notifications": { "success": "Erfolg", "error": "Fehler (status code: {{statusCode}})", "undoable": "Sie haben {{seconds}} Sekunden Zeit für Undo.", "createSuccess": "{{resource}} erfolgreich erstellt.", "createError": "Fehler beim Erstellen {{resource}} (status code: {{statusCode}})", "deleteSuccess": "{{resource}} erfolgreich gelöscht.", "deleteError": "Fehler beim Löschen {{resource}} (status code: {{statusCode}})", "editSuccess": "{{resource}} erfolgreich bearbeitet.", "editError": "Fehler beim Bearbeiten {{resource}} (status code: {{statusCode}})", "importProgress": "{{processed}}/{{total}} importiert" }, "loading": "Wird geladen", "tags": { "clone": "Klon" }, "dashboard": { "title": "Dashboard" }, "posts": { "posts": "Einträge", "fields": { "id": "Id", "title": "Titel", "category": "Kategorie", "status": { "title": "Status", "published": "Veröffentlicht", "draft": "Draft", "rejected": "Abgelehnt" }, "content": "Inhalh", "createdAt": "Erstellt am" }, "titles": { "create": "Erstellen", "edit": "Bearbeiten", "list": "Einträge", "show": "Eintrag zeigen" } }, "table": { "actions": "Aktionen" }, "documentTitle": { "default": "refine", "suffix": " | Refine", "post": { "list": "Beiträge | Refine", "show": "#{{id}} Beitrag anzeigen | Refine", "edit": "#{{id}} Beitrag bearbeiten | Refine", "create": "Neuen Beitrag erstellen | Refine", "clone": "#{{id}} Beitrag klonen | Refine" } }, "autoSave": { "success": "gespeichert", "error": "fehler beim automatischen speichern", "loading": "speichern...", "idle": "warten auf anderungen" } }对照英文版翻译文件(_partial-translation-file-en.md)可以看出:两份文件的键结构完全一致,仅值不同。这正是 Refine 国际化设计的关键——所有内置组件的文本都是通过固定键查找的,你只需提供同构的common.json即可整体替换界面语言,无需改动任何组件代码。
翻译键分组解析:每个键背后的源码消费方
德语common.json覆盖了 Refine 后台应用的几乎所有内置 UI 文本。结合仓库源码,我们可以逐一确认这些键的真实消费场景,从而在翻译时做到"知其然,也知其所以然"。
pages:认证页与错误页文本
pages.login、pages.forgotPassword、pages.register、pages.updatePassword四组键对应 Refine 内置的认证相关页面(登录、忘记密码、注册、更新密码),涵盖标题、字段标签、校验错误消息和按钮文案。
pages.error组则对应错误页面。在 core 包的错误页组件 中,当路由解析到某个resource与action时,会调用:
translate( "pages.error.info", { action: action, resource: resource.name }, `You may have forgotten to add the "${action}" component to "${resource.name}" resource.`, );注意德语值"Sie haben vergessen, {{action}} component zu {{resource}} hinzufügen."中的{{action}}与{{resource}}就是这里的插值参数。同样的键在 antd 包的错误页 中被复用,pages.error.404、pages.error.backHome分别渲染"页面不存在"标题与"返回首页"按钮。这意味着只改翻译文件,就能让 antd、chakra、mui 等任意 UI 适配层的错误页全部变为德语。
actions 与 buttons:操作命名与按钮文本
actions组(list/create/edit/show)是 CRUD 操作的语义化名称;buttons组则覆盖按钮级文案,其中值得关注的是buttons.notAccessTitle——在 core 包的按钮权限检查 Hook 中,当用户对某资源无操作权限时,按钮的 Tooltip 会读取该键,德语值 "Sie haben keine zugriffsberechtigung" 即"您没有访问权限"。对应的单元测试(button-can-access/index.spec.tsx)也验证了该键的翻译调用。
notifications:数据变更与导入进度通知
notifications组是 Refine 数据 Hook(useCreate、useUpdate、useDelete等)自动弹出通知的文案来源。以创建为例,useCreate.ts 在成功后调用notifications.createSuccess,失败时调用notifications.createError:
// 成功路径 "notifications.createSuccess" // "{{resource}} erfolgreich erstellt." // 失败路径 "notifications.createError" // "Fehler beim Erstellen {{resource}} (status code: {{statusCode}})"德语键中出现的{{resource}}(资源名)与{{statusCode}}(HTTP 状态码)均由数据 Hook 在运行时注入。notifications.importProgress则在 antd 的导入 Hook 中用于显示{{processed}}/{{total}}的导入进度。notifications.undoable用于可撤销操作,见 core 包的 undoableQueue 组件,{{seconds}}为撤销倒计时秒数。
warnWhenUnsavedChanges 与 autoSave:表单状态提示
warnWhenUnsavedChanges用于"未保存更改"离开确认场景。该开关在RefineOptions中定义,默认值为false(见 refine 上下文默认值),开启后表单有未保存修改时展示提示。
autoSave组对应 AutoSaveIndicator 组件,四个状态键分别表示:autoSave.success(已保存)、autoSave.error(保存失败)、autoSave.loading(保存中)、autoSave.idle(等待变更)。注意德语文件中的"warten auf anderungen"与"fehler beim automatischen speichern"存在拼写瑕疵("Änderungen"、"Fehler"),这恰好说明翻译文件完全由开发者掌控,任何文本(包括纠错)都可直接修改common.json生效。
documentTitle:浏览器标签页标题
documentTitle组控制浏览器标签页标题。核心实现在 generateDocumentTitle/index.ts:先读取documentTitle.default(默认 "refine")与documentTitle.suffix(默认 " | Refine"),再按documentTitle.${identifier}.${action}的模式查找资源级标题,{{id}}用于注入记录 ID。德语文件中post资源的五种子键(list/show/edit/create/clone)完整覆盖了博客场景,例如"#{{id}} Beitrag bearbeiten | Refine"即"编辑第 #N 篇文章"。
posts / table / dashboard / tags / loading:业务资源与通用文本
posts组是演示资源(博客文章)的字段标签与页面标题,table.actions是表格操作列标题,dashboard.title是仪表盘标题,loading是通用加载提示,tags.clone是克隆标签。这些键在官方的 Ant Design 示例(i18n 指南)中与PostList页面配合使用,是理解"资源级翻译"的直观样例。
从安装到启用:在 Refine 中接入 react-i18next
下面按照官方指南的完整流程,将德语翻译文件接入应用。示例 UI 库使用 Ant Design(其他 UI 库流程相同),且推荐使用create refine-app初始化项目(CLI 勾选 i18n 支持即可自动配置)。
第一步:安装依赖
在项目目录中安装 react-i18next、i18next 及两个官方插件:
npm install react-i18next i18next i18next-http-backend i18next-browser-languagedetectori18next-http-backend:从 HTTP 地址懒加载翻译文件;i18next-browser-languagedetector:自动检测浏览器语言。
第二步:创建 i18n 实例
新建src/i18n.ts,配置loadPath指向翻译文件目录,声明支持语言与默认命名空间:
import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import Backend from "i18next-http-backend"; import detector from "i18next-browser-languagedetector"; i18n .use(Backend) .use(detector) .use(initReactI18next) .init({ supportedLngs: ["en", "de"], backend: { loadPath: "/locales/{{lng}}/{{ns}}.json", // locale files path }, ns: ["common"], defaultNS: "common", fallbackLng: ["en", "de"], }); export default i18n;要点说明:{{lng}}会被替换为语言代码(en/de),{{ns}}替换为命名空间(common),最终请求/locales/de/common.json——正是本文讲解的德语翻译文件路径。
第三步:用 React.Suspense 包裹应用
翻译文件为懒加载,因此在src/index.tsx中导入 i18n 实例并用React.Suspense包裹应用,加载期间显示 fallback:
import React from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; import "./i18n"; const container = document.getElementById("root"); const root = createRoot(container!); root.render( <React.StrictMode> <React.Suspense fallback="loading"> <App /> </React.Suspense> </React.StrictMode>, );第四步:创建并注入 i18nProvider
在src/App.tsx中基于 react-i18next 的useTranslation实现三方法接口,并传入<Refine>:
import type { I18nProvider } from "@refinedev/core"; import { Refine } from "@refinedev/core"; import { useTranslation } from "react-i18next"; const App: React.FC = () => { const { t, i18n } = useTranslation(); const i18nProvider: I18nProvider = { translate: (key: string, options?: any) => t(key, options), changeLocale: (lang: string) => i18n.changeLanguage(lang), getLocale: () => i18n.language, }; return ( <Refine i18nProvider={i18nProvider} /* ... */ > {/* ... */} </Refine> ); };注入后,Refine 暴露的useTranslationHook 便全局可用,所有内置组件也会通过translate自动读取对应语言的common.json。
第五步:放置翻译文件
项目静态资源目录结构如下,en与de两份common.json内容同构:
|-- public | |-- locales | |-- en | | |-- common.json | |-- de | |-- common.json |-- src |-- package.json |-- tsconfig.json界面切换语言:Header 组件与 useTranslation
接下来创建<Header />组件,用changeLocale实现中英/德英切换。官方示例使用 Ant Design 的Dropdown展示语言列表,并用国旗 SVG 图标辅助识别:
import { DownOutlined } from "@ant-design/icons"; import { useTranslation } from "@refinedev/core"; import { Avatar, Button, Dropdown, Layout, Menu, Space } from "antd"; export const Header: React.FC = () => { const { i18n } = useTranslation(); const { getLocale, changeLocale } = useTranslation(); const currentLocale = getLocale(); const menu = ( <Menu selectedKeys={currentLocale ? [currentLocale] : []}> {[...(i18n.languages || [])].sort().map((lang: string) => ( <Menu.Item key={lang} onClick={() => changeLocale(lang)} icon={ <span style={{ marginRight: 8 }}> <Avatar size={16} src={`/images/flags/${lang}.svg`} /> </span> } > {lang === "en" ? "English" : "German"} </Menu.Item> ))} </Menu> ); return ( <Layout.Header style={{ display: "flex", justifyContent: "flex-end", alignItems: "center", padding: "0px 24px", height: "48px", backgroundColor: "#FFF", }} > <Dropdown overlay={menu}> <Button type="link"> <Space> <Avatar size={16} src={`/images/flags/${currentLocale}.svg`} /> {currentLocale === "en" ? "English" : "German"} <DownOutlined /> </Space> </Button> </Dropdown> </Layout.Header> ); };随后将 Header 挂到ThemedLayout:
import { Refine, Resource } from "@refinedev/core"; import { ThemedLayout } from "@refinedev/antd"; import { useTranslation } from "react-i18next"; import "./i18n"; import { Header } from "components"; const App: React.FC = () => { const { t, i18n } = useTranslation(); const i18nProvider = { translate: (key: string, options?: any) => t(key, options), changeLocale: (lang: string) => i18n.changeLanguage(lang), getLocale: () => i18n.language, }; return ( <Refine i18nProvider={i18nProvider} /* ... */ > <ThemedLayout header={<Header />} > {/* ... */} </ThemedLayout> </Refine> ); };业务页面中的翻译使用:PostList 实例
最后在业务页面中通过useTranslation的translate方法引用common.json的键。以文章列表页为例,表格的"标题""分类""操作"三列分别对应posts.fields.title、posts.fields.category、table.actions,分类数据通过useMany关联查询:
import { useTranslation, useMany, } from "@refinedev/core"; import { List, useTable, TextField, EditButton, ShowButton, } from "@refinedev/antd"; import { Table, Space } from "antd"; import { IPost, ICategory } from "interfaces"; export const PostList: React.FC = () => { const { translate } = useTranslation(); const { tableProps } = useTable<IPost>(); const categoryIds = tableProps?.dataSource?.map((item) => item.category.id) ?? []; const { data, isLoading } = useMany<ICategory>({ resource: "categories", ids: categoryIds, queryOptions: { enabled: categoryIds.length > 0, }, }); return ( <List> <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex="title" title={translate("posts.fields.title")} /> <Table.Column dataIndex={["category", "id"]} title={translate("posts.fields.category")} render={(value) => { if (isLoading) { return <TextField value="Loading..." />; } return ( <TextField value={data?.data.find((item) => item.id === value)?.title} /> ); }} /> <Table.Column<IPost> title={translate("table.actions")} dataIndex="actions" key="actions" render={(_value, record) => ( <Space> <EditButton size="small" recordItemId={record.id} /> <ShowButton size="small" recordItemId={record.id} /> </Space> )} /> </Table> </List> ); };配套的 TypeScript 接口定义(interfaces/index.d.ts)如下:
export interface ICategory { id: number; title: string; } export interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; category: { id: number }; }切换语言时,changeLocale("de")会触发i18n.changeLanguage,随后页面与内置组件通过translate重新取词,实现全界面即时切换为德语。
自定义覆盖的关键点
- 键结构必须同构:要覆盖 Refine 内置文本,只需复制 英文翻译文件 的结构,替换对应语言的值,并将文件放到
/locales/<语言代码>/common.json。所有键均已在上文结合源码逐一验证。 - 插值变量不可省略:
{{resource}}、{{statusCode}}、{{seconds}}、{{processed}}、{{total}}、{{id}}、{{action}}是运行时由 Refine 注入的占位符,翻译时保留它们才能正确显示动态信息。 - fallback 兜底:i18n 实例中的
fallbackLng: ["en", "de"]保证某一语言缺少键时自动回退到另一语言,避免出现裸键名。 - 与 UI 适配层解耦:同一份
common.json同时服务于 core 与 antd 等适配包(如错误页在两个层级都会读取pages.error.*),因此一份翻译即可覆盖全栈 UI 文本。 - 自动保存与文档标题:
autoSave.*与documentTitle.*是容易被遗漏的两组键,前者驱动 AutoSaveIndicator 的四种状态文案,后者决定浏览器标签标题的默认值与后缀,正式上线前建议一并翻译。
按照上述流程,你就可以为 Refine 应用交付完整的德语(或任意语言)体验——从登录注册、错误页到表格按钮与浏览器标题,全部由一份common.json驱动,无需改动任何组件源码。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考