用途
<Translate> 组件(来自 packages/sanity/src/core/i18n/Translate.tsx)用于渲染包含标记的本地化资源,例如 'Search for "<Red>{{keyword}}</Red>"'。components 属性把本地化字符串中的标签名映射到 React 组件或原生 HTML 标签名。
当消息不含标记时,应优先使用普通的 t() 函数——<Translate> 的渲染开销更大。
核心规则:绝不要内联定义组件
components 映射中的组件必须是稳定的、模块作用域的组件。绝不要在渲染期间内联定义它们——这样每次渲染都会创建新的组件标识,React 会卸载并重新挂载子树(丢失状态、DOM 和焦点)。这与 react/no-unstable-nested-components 属于同一类 bug,并且对 <Translate> 由仓库内的 oxlint 规则 @repo/i18n/no-inline-translate-components 强制执行(该规则实现在 @repo/oxlint-plugin-i18n 工作区包中,通过 .oxlintrc.json 的 jsPlugins 接入)。
该规则只能看到直接写在 JSX 属性中的对象字面量。以其他方式在渲染期间构建的映射(useMemo、useCallback、工厂调用)同样错误——也应提升到模块作用域,即使规则无法标记它们。
// ❌ 错误 - 每次渲染都是新的组件标识(且无法通过 lint 规则)
;<Translate t={t} i18nKey="key" components={{Badge: ({children}) => <strong>{children}</strong>}} />
// ❌ 错误 - useMemo 无法解决依赖变化时的标识问题
const components = useMemo(() => ({Badge: ({children}) => <b>{children}</b>}), [])
各种形态的修复方式
纯 HTML 包装——映射为字符串形式的原生标签名。字符串永远不会接收 componentProps,因此不会产生多余的 DOM 属性:
<Translate t={t} i18nKey="key" components={{Code: 'code', Emphasis: 'em'}} />
静态标记(固定链接、带样式的包装)——提升为模块作用域组件:
function DocsLink({children}: {children?: ReactNode}) {
return <a href="https://www.sanity.io/docs">{children}</a>
}
;<Translate t={t} i18nKey="key" components={{DocsLink}} />
需要来自渲染数据的组件——将其提升,并通过 componentProps 传递数据。该对象会被转发给映射中的每个非字符串组件(包括 memo 组件等特殊组件),因此只声明每个组件实际读取的 props:
function VersionBadge({children, tone}: {children?: ReactNode; tone?: BadgeTone}) {
return <VersionInlineBadge $tone={tone}>{children}</VersionInlineBadge>
}
;<Translate
t={t}
i18nKey="key"
components={{VersionBadge}}
componentProps={{tone: getReleaseTone(release)}}
/>
注意事项
- componentProps 驱动泛型:字面量值会被拓宽({tone: 'caution'} 推断为 {tone: string})。对必须保持窄类型的字面量使用 as const:componentProps={{tone: 'caution' as const}}。
- TComponentProps 被约束为 object——原始类型和 null 会导致编译错误。
- 组件仅在包装标签(<X>...</X>)中接收 children;自闭合标签(<X/>)渲染组件时不带 children。请将 children 声明为可选。
- 未映射的小写标签只有在 Translate.tsx 的 RECOGNIZED_HTML_TAGS 中列出时才会渲染;其他任何内容都会回退为插值的纯文本并输出控制台警告。
- 也不要在提升后的组件内部调用组件工厂(例如 getVersionInlineBadge)——这会在下一层重新制造渲染时标识问题,React Compiler lint 会标记它。应改为通过 prop 渲染底层组件。
参考实现
- packages/sanity/src/core/releases/tool/detail/ReleaseActivityListItem.tsx——componentProps 传递事件对象。
- packages/@sanity/vision/src/components/VisionGuiResult.tsx——componentProps 向一对自闭合组件传递数据。
- packages/sanity/src/core/studio/components/navbar/search/components/common/FilterLabel.tsx——多个组件共享同一个 componentProps 对象。
- packages/sanity/src/core/i18n/tests/Translate.test.tsx——行为覆盖,包括 memo 组件以及不向原生标签转发。