react-admin<TranslatableInputs>多语言表单输入组件完整指南:分语言编辑、校验与编程式赋值
【免费下载链接】react-adminA frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design项目地址: https://gitcode.com/gh_mirrors/re/react-admin
<TranslatableInputs>是 react-admin 提供的多语言表单输入容器组件,它允许开发者把一组"每个字段对应多语言值"的输入(如name.en、name.fr)组织成带语言选项卡的编辑界面。本文基于当前仓库 docs/TranslatableInputs.md 展开,结合ra-ui-materialui与ra-core的源码实现,系统讲解其数据模型、全部 Props、自定义语言选择器、校验联动、以及如何配合react-hook-form的setValue编程式修改各语言值。
一、组件定位与适用场景
在多语言业务系统(如电商商品、CMS 内容、新闻稿)中,同一个字段往往需要保存多个语言的翻译文本。react-admin 对此有两种展示形态:
- 只读展示:使用
<TranslatableFields>分语言展示字段值; - 编辑输入:使用本文的
<TranslatableInputs>,让用户在一个表单内为每种语言分别填写值。
<TranslatableInputs>不要求提供source属性(它本身只是容器),但它要求至少一个带source的子输入组件,同时必须通过locales属性声明要展示的语言。其源码定义位于 packages/ra-ui-materialui/src/input/TranslatableInputs.tsx,底层逻辑(语言切换、记录转换)由ra-core的useTranslatable提供。
组件期望的字段值结构如下(每个字段都是"语言码 → 文本"的对象):
{ id: 1234, name: { en: 'White trousers', fr: 'Pantalon blanc', }, description: { en: 'Slim fit trousers for every day use', fr: 'Pantalon ajusté pour un usage quotidien', } }二、基本用法
将需要多语言编辑的输入组件作为<TranslatableInputs>的子节点传入,并通过locales声明语言:
<TranslatableInputs locales={['en', 'fr']}> <TextInput source="name" /> <RichTextInput source="description" /> </TranslatableInputs>页面渲染效果:上方是一个 Material UI 选项卡条,每个选项卡标签是语言码(如en、fr),下方是对应当前选中语言的一组输入框。切换选项卡时,react-admin 会为该语言的每个子输入生成形如name.en、name.fr的动态source,从而实现"一套字段结构、多语言独立编辑"。
从源码来看,TranslatableInputs的核心渲染流程(TranslatableInputs.tsx#L72-L114)是:
- 调用
useTranslatable({ defaultLocale, locales })获取语言切换上下文; - 通过
TranslatableContextProvider注入该上下文; - 渲染语言选择器(默认是
<TranslatableInputsTabs>,即 Material UI 的 Tabs); - 遍历
locales,为每个语言渲染一个<TranslatableInputsTabContent>,把同一组子输入包进各语言的容器中。
每个TranslatableInputsTabContent(packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx)会做两件关键的事:
- 通过
SourceContextProvider注入一个getSource: source => ${source}.${locale}的上下文,让子输入自动挂上语言后缀; - 通过
RecordContextProvider注入一份"当前语言的值记录"(由getRecordForLocale从原始记录中抽取),因为普通输入组件并不知道语言的存在,它们只从RecordContext取当前字段值。
这里getRecordForLocale的实现值得注意(packages/ra-core/src/i18n/useTranslatable.ts#L55-L74):它递归遍历记录的所有路径(getRecordPaths支持嵌套对象与数组),把形如title.fr的值提升为title,从而构造出仅含指定语言值的记录。这意味着<TranslatableInputs>也适用于嵌套结构(如数组内对象的翻译字段)。
三、Props 总览
<TranslatableInputs>的 Props 定义于 TranslatableInputs.tsx#L116-L125(接口继承UseTranslatableOptions),完整参数如下:
| Prop | 必填 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
locales | 必填 | Array<string> | - | 语言码数组,顺序即选项卡顺序 |
defaultLocale | 可选 | string | 跟随用户语言 | 默认展示的语言 |
fullWidth | 可选 | boolean | true | 设为false时输入组不撑满表单宽度 |
groupKey | 可选 | string | - | 用于可访问性的唯一标识(同页多个实例时必填) |
selector | 可选 | ReactElement | Material UI Tabs | 自定义语言选择器元素 |
StackProps | 可选 | object | {} | 透传给内部 MUI Stack 的属性 |
sx | 可选 | SxProps | - | Material UI 样式快捷方式 |
其余内部还支持className、margin('none' \| 'normal' \| 'dense')。从源码可见组件支持 MUI 主题级定制:其样式类名为RaTranslatableInputs,包含root与fullWidth两个规则,可通过 MUIcomponents覆盖(见 TranslatableInputs.tsx#L146-L162)。
四、defaultLocale:默认展示语言
react-admin 默认取当前用户的语言(useLocaleState中的localeFromUI)作为defaultLocale,这一点在 useTranslatable.ts#L26-L28 中可以看到:
const [localeFromUI] = useLocaleState(); const { defaultLocale = localeFromUI, locales } = options;若希望某个表单固定以特定语言打开,用defaultLocale覆盖:
<TranslatableInputs locales={['en', 'fr']} defaultLocale="fr"> <TextInput source="name" /> <RichTextInput source="description" /> </TranslatableInputs>源码中还有一个细节:selectedLocale为 falsy 时(例如defaultLocale为空字符串),会回退到'en'(useTranslatable.ts#L33)。
五、fullWidth:宽度控制
默认情况下,<TranslatableInputs>组会撑满表单宽度——源码根节点样式为flexGrow: 1,且启用fullWidth时追加width: '100%'(TranslatableInputs.tsx#L136-L144)。如需禁用:
<TranslatableInputs locales={['en', 'fr']} fullWidth={false}> <TextInput source="title" /> <TextInput source="description" /> </TranslatableInputs>六、groupKey:同页多实例的可访问性标识
<TranslatableInputs>内部依赖一组固定的 DOM id 建立"选项卡 ↔ 内容面板"的关联(如translatable-header-${groupKey}${locale}、translatable-content-${groupKey}${locale},见 TranslatableInputsTabContent.tsx#L68-L76)。如果同一页面出现多个<TranslatableInputs>,这些 id 会冲突,因此必须为每个实例提供唯一的groupKey:
<TranslatableInputs locales={['en', 'fr']} groupKey="essential-fields"> <TextInput source="name" /> <RichTextInput source="description" /> </TranslatableInputs>groupKey同时会被拼入FormGroupContext的名称(${groupKey}${locale}),确保各语言分组的表单上下文互不干扰。
七、locales:语言列表与选项卡标签
locales传入字符串数组,每个字符串既是语言码,也会成为输入值的键后缀。数组顺序决定了选项卡从左到右的展示顺序:
<TranslatableInputs locales={['en', 'fr']}> <TextInput source="name" /> <RichTextInput source="description" /> </TranslatableInputs>选项卡默认以语言码作为标签。如果希望显示人类可读的语言名(如 "English" 而非 "en"),可以使用翻译键,格式为ra.locales.[locale_code],例如ra.locales.en、ra.locales.fr。以ra-i18n-polyglot或自定义翻译包在对应语言文件中配置这些键即可。
八、selector:自定义语言选择器
默认选择器是 Material UI Tabs(实现见 TranslatableInputsTabs.tsx:一个AppBar容器内嵌Tabs,每个选项卡的值是语言码)。你可以通过selector属性替换成任意自定义元素,例如一个原生<select>下拉框:
const Selector = () => { const { locales, selectLocale, selectedLocale, } = useTranslatableContext(); const handleChange = event => { selectLocale(event.target.value); }; return ( <select aria-label="Select the locale" onChange={handleChange} value={selectedLocale} > {locales.map(locale => ( <option key={locale} value={locale} // This allows to correctly link the containers for each locale to their labels id={`translatable-header-${locale}`} > {locale} </option> ))} </select> ); }; <TranslatableInputs record={record} resource="products" locales={['en', 'fr']} selector={<Selector />} > <TextInput source="name" /> <RichTextInput source="description" /> </TranslatableInputs>自定义选择器通过useTranslatableContext获取上下文,可用值包括:
locales:语言码数组;selectedLocale:当前选中语言;selectLocale(locale):切换语言的方法;getRecordForLocale(record, locale):抽取指定语言记录的工具函数。
注意:自定义selector若用原生元素,需要自己保证"选项卡 ↔ 面板"的id/aria-labelledby关联(上例中translatable-header-${locale}正是面板aria-labelledby所引用的 id)。useTranslatableContext在脱离TranslatableContextProvider使用时(即组件不在<TranslatableInputs>内部)会抛出错误,这一点由 useTranslatableContext.ts#L39-L43 保证。
九、StackProps:控制内部布局
每个语言的输入内容都被包裹在一个 MUIStack中,StackProps会原样透传({...StackProps},见 TranslatableInputs.tsx#L100-L110)。默认方向为垂直堆叠;设置direction: 'row'可以让输入并排显示:
<TranslatableInputs locales={['en', 'fr']} StackProps={{ direction: 'row' }} > <TextInput source="title" /> <TextInput source="description" sx={{ marginLeft: 2 }} /> </TranslatableInputs>StackProps支持 MUI Stack 的全部属性,包括spacing、direction、alignItems、justifyContent等,适合在空间紧张的表单中做紧凑排版。
十、sx:自定义样式
TranslatableInputs根节点同样支持sx,可用于设置边框、背景、间距等样式:
<TranslatableInputs locales={['en', 'fr']} sx={{ border: 'solid 1px red' }} > <TextInput source="title" /> <TextInput source="description" /> </TranslatableInputs>sx会被应用到根容器(源码中由styled('div')创建的Root),同时仍可通过 MUI 主题的RaTranslatableInputs.styleOverrides做全局覆盖。
十一、校验:错误联动到选项卡
<TranslatableInputs>内的任何输入组件都可以照常使用 react-admin 的校验器。当某个语言的输入出现校验错误时,对应语言的选项卡标签会被标记为"错误"状态(标红高亮):
<TranslatableInputs locales={['en', 'fr']}> <TextInput source="name" validate={[required()]} /> <RichTextInput source="description" validate={[maxLength(100)]} /> </TranslatableInputs>这一行为在仓库测试 packages/ra-ui-materialui/src/input/TranslatableInputs.spec.tsx 中有明确覆盖:测试通过断言tabs[1].classList.contains('RaTranslatableInputsTab-error')来验证含错误输入的选项卡是否正确获得错误样式类;同时测试也验证了未选中语言的面板会附加hidden样式类(display: none)实现隐藏切换。
实现机制上,每个语言的选项卡由TranslatableInputsTab渲染,它会感知所在语言的FormGroupContext状态;校验错误在表单层面按语言分组记录,因此可以精确反映到具体语言选项卡上。
十二、编程式修改多语言值:useSourceContext+setValue
<TranslatableInputs>的表单值依然由react-hook-form管理,因此可以直接调用其setValue方法修改某个输入的值。但难点在于:子输入在表单中注册的name是按语言动态生成的(例如description.en),直接写死字段名无法适配。
react-admin 为此提供了SourceContext(packages/ra-core/src/core/SourceContext.tsx),通过useSourceContext钩子可以在任意层级拿到getSource(source)函数,它会返回该输入在当前上下文中的真实source。在<TranslatableInputs>内,这个函数就是"字段名 + 当前语言后缀"的拼接器(见 TranslatableInputsTabContent.tsx#L41-L56)。
源码细节:
SourceContext默认值是一个"恒等函数"(getSource: source => source),因此在不处于任何特殊上下文时使用也不会报错;而TranslatableInputsTabContent提供的getSource在source为空时会抛出'Children of TranslatableInputs must have a source',从实现层面强制了"子输入必须有source"的约束。
下面示例展示了如何利用getSource拿到各语言的动态name,再用setValue把title的值预填到description:
import { TranslatableInputs, TextInput, useSourceContext } from 'react-admin'; import { useFormContext } from 'react-hook-form'; import { Button } from '@mui/material'; const PrefillWithTitleButton = () => { const sourceContext = useSourceContext(); const { setValue, getValues } = useFormContext(); const onClick = () => { setValue( // sourceContext.getSource('description') will for instance return // 'description.en' sourceContext.getSource('description'), getValues(sourceContext.getSource('title')) ); }; return ( <Button onClick={onClick} size="small" sx={{ maxWidth: 140 }}> Prefill with title </Button> ); }; const MyInputs = () => ( <TranslatableInputs locales={['en', 'fr']}> <TextInput source="title" /> <TextInput source="description" helperText={false} /> <PrefillWithTitleButton /> </TranslatableInputs> );关键点:
useSourceContext()可在<TranslatableInputs>的任何子组件中使用,拿到的是"当前语言上下文"下的getSource;- 由于按钮同样位于
TranslatableInputsTabContent内部,getSource('title')与getSource('description')会自动带上当前选中语言的后缀(如title.fr、description.fr),因此预填逻辑天然跟随用户正在编辑的语言; - 若在
<TranslatableInputs>之外使用useSourceContext,得到的是恒等上下文,getSource直接返回原字段名,符合普通表单的行为。
十三、小结
<TranslatableInputs>是 react-admin 处理多语言编辑场景的标准方案:
- 数据模型:字段值为"语言码 → 文本"的对象,
locales定义语言集合; - 使用要点:子输入必须有
source,多实例必须给groupKey,默认语言可被defaultLocale覆盖; - 交互定制:
selector可完全替换默认 Tabs 选择器,StackProps/sx/MUI 主题覆盖可灵活调整布局与样式; - 校验:各语言独立校验,错误自动标记到对应选项卡;
- 编程式控制:配合
useSourceContext().getSource与react-hook-form的setValue,可在任意语言上下文中精准读写字段值。
相关源码与测试可作为继续深入研究的入口:TranslatableInputs.tsx、TranslatableInputsTabContent.tsx、TranslatableInputsTabs.tsx、useTranslatable.ts、SourceContext.tsx、TranslatableInputs.spec.tsx。只读展示场景可对照参考<TranslatableFields>。
【免费下载链接】react-adminA frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design项目地址: https://gitcode.com/gh_mirrors/re/react-admin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考