tldraw 自定义翻译与文案覆盖:用 useTranslation 打造品牌化多语言 UI
【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw
在 tldraw SDK 中,内置用户界面(菜单、工具栏、右键菜单等)的所有文案都通过一套翻译键(translation key)机制管理。当你需要让这些文案贴合自己的品牌语气、行业术语,或为应用补充新的语言时,不必 fork 源码,只需通过overrides.translations覆盖对应语言下的翻译键,再用useTranslation钩子在自定义组件里读取同一套字符串。读完本文,你将掌握 tldraw UI 文本定制与多语言扩展的完整链路:从写一个可运行的覆盖示例,到理解底层翻译加载、语言切换与回退机制。
该主题的完整可运行示例位于 custom-language-translations 示例目录,其中 README.md 定义了示例的元信息与说明,核心代码在 CustomLanguageTranslationExample.tsx。在 apps/examples 下执行yarn dev(对应 apps/examples/package.json 中"dev": "vite --host"脚本)即可在浏览器中交互验证本文所有结论。
一、为什么需要覆盖 tldraw 的内置文案
tldraw 的编辑器 UI 是完整的、开箱即用的产品级界面:主菜单、工具栏、右键菜单、样式面板、快捷键弹窗等一应俱全。但这些 UI 的默认文案(如 "Duplicate"、"Delete")不一定符合你的产品语境。常见的定制诉求包括:
- 品牌语气(brand voice):例如把 "Duplicate" 换成更友好的 "Make a copy",把 "Delete" 换成更委婉的 "Remove";
- 领域术语(terminology):面向特定行业(设计、教育、医疗)替换术语表达;
- 本地化(localization):为不同地区用户提供完整语言包,并允许在运行时切换语言。
tldraw 官方的做法是提供两层能力:translations覆盖(override)用于改写和扩充字符串表,useTranslation钩子用于在自定义组件中读取当前语言下的字符串。二者共享同一套翻译键空间,所以自定义组件与内置菜单的文案天然一致。
二、理解翻译键与translations覆盖结构
tldraw 的 UI 文案组织方式非常朴素:一条文案就是一个键值对,键形如action.duplicate、action.delete。全部默认键值集中在 defaultTranslation.ts,而键的联合类型由 TLUiTranslationKey.ts 定义。从 overrides.ts 的类型定义可以看到TLUiOverrides支持三类覆盖,其中翻译覆盖的类型即是按语言区分的键值字典:
export interface TLUiOverrides { actions?(...): TLUiActionsContextType tools?(...): TLUiToolsContextType translations?: TLUiTranslationProviderProps['overrides'] }而TLUiTranslationProviderProps['overrides']的结构为Record<string, Record<string, string>>(参见 useTranslation.tsx),即“语言代码 → 翻译键 → 字符串”。语言代码与 tldraw 内置语言列表一一对应(如en、es、de、zh-cn等),完整列表来自 languages.json。
覆盖时你可以做两件事:
- 改写内置键:例如把英文下的
action.duplicate替换为 "Make a copy"; - 新增自己的键:为自定义组件追加全新键(如
custom.export-all),tldraw 内置表没有该键也不受影响。
未被覆盖的语言会完整回退到 tldraw 的内置默认文案——这一点对“只做局部定制、不想维护全套文案”的产品至关重要。
三、示例代码逐段拆解
下面逐段分析 CustomLanguageTranslationExample.tsx 的核心实现。
3.1 用useTranslation读取当前语言字符串
示例首先定义了一个自定义工具栏CustomToolbar,并放置到TopPanel插槽中。关键是useTranslation()的返回值msg:
function CustomToolbar() { const editor = useEditor() const msg = useTranslation() return ( <div className="tlui-menu custom-language-toolbar"> <TldrawUiButton type="normal" onClick={() => editor.duplicateShapes(editor.getSelectedShapeIds())} > {msg('action.duplicate')} </TldrawUiButton> <TldrawUiButton type="normal" onClick={() => editor.deleteShapes(editor.getSelectedShapeIds())} > {msg('action.delete')} </TldrawUiButton> </div> ) }useTranslation的底层实现(useTranslation.tsx)返回一个经过useCallback记忆化的函数,其签名是msg(id),查找规则为messages[id] ?? id——即先在当前语言的字典中查找,找不到就原样返回键本身,避免渲染出空文案:
export function useTranslation() { const translation = React.useContext(TranslationsContext) const messages = translation?.messages ?? DEFAULT_TRANSLATION // ... 若在 Provider 外使用会 warnOnce 提示 return React.useCallback(function msg(id?) { return messages[id as TLUiTranslationKey] ?? id }, [messages]) }注意:useTranslation依赖 React Context,因此必须在 tldraw 的翻译 Provider(TldrawUiTranslationProvider/TldrawUiContextProvider)内部使用。若不在 Provider 内,useCurrentTranslation会直接抛错(测试中明确断言了这条行为,见 useTranslation.test.tsx);而useMaybeCurrentTranslation与useDirection是“宽容版”,分别返回null与'ltr'。绝大多数情况下,只要你的组件渲染在<Tldraw>内部,就可以直接调用useTranslation。
3.2 定义多语言覆盖字典
接着,示例定义了TLUiOverrides,同时覆盖英文与西班牙文两套语言:
const overrides: TLUiOverrides = { translations: { en: { 'action.duplicate': 'Make a copy', 'action.delete': 'Remove', }, es: { 'action.duplicate': 'Hacer una copia', 'action.delete': 'Eliminar', }, }, }这里的关键点是:action.duplicate与action.delete是 tldraw 内置菜单已经使用的键。由于你的覆盖与内置菜单共享同一键空间,覆盖后你的自定义工具栏与 tldraw 内置菜单(含右键菜单)会同时显示定制文案,全程保持用词统一。
从 overrides.ts 的useMergedTranslationOverrides可以看出,多个覆盖对象支持以“逐语言、逐键Object.assign”的方式深度合并,因此你在多处传入的覆盖不会互相覆盖掉同语言下其他键的改动。
3.3 通过组件插槽挂载自定义 UI
为了让自定义工具栏真正出现,需要利用组件覆盖(TLComponents),把CustomToolbar填入TopPanel插槽:
const components: TLComponents = { TopPanel: CustomToolbar, }官方建议把components与overrides对象都定义在 React 组件外部(模块顶层),保证它们是稳定引用(stable reference),避免每次渲染都生成新对象而触发不必要的重渲染。
3.4 传入<Tldraw>
最后把它们作为 props 传给Tldraw组件即可:
export default function CustomLanguageTranslationExample() { return ( <div className="tldraw__editor"> <Tldraw overrides={overrides} components={components} /> </div> ) }配套样式位于 custom-language-translations.css:.custom-language-toolbar设置了pointer-events: all(确保在画布上层可点击)并用display: flex+margin: 8px做简单排版。
3.5 运行验证
示例的交互验证方式(README 中明确说明)有两条路径:
- 右键菜单验证:创建任意图形并右键点击,上下文菜单中会看到覆盖后的 "Make a copy" 与 "Remove";
- 语言切换验证:在主菜单(Main Menu)的 Language 子菜单中选择 Spanish,切换到
es后,右键菜单与自定义工具栏都会显示西班牙文 "Hacer una copia" 与 "Eliminar"。
四、底层原理:翻译是怎么加载与合并的
了解底层实现有助于判断“覆盖生效时机”与“未覆盖语言会怎样”,这由 useTranslation.tsx 与 translations.ts 共同决定。
4.1 Provider 初始化与动态加载
TldrawUiTranslationProvider(useTranslation.tsx)的初始化逻辑是:
- 初始态固定为英文:如果存在
overrides['en'],则 messages 为{ ...DEFAULT_TRANSLATION, ...overrides['en'] },否则直接用DEFAULT_TRANSLATION; - 随后通过
fetchTranslation(locale, getAssetUrl)异步加载目标语言的 JSON 语言包; - 若存在
overrides[locale],加载完成后以{ ...translation.messages, ...overrides[locale] }的形式把你的覆盖叠加到该语言完整词典之上。
由此可见:覆盖采用“浅合并后追加”语义,你只需列出需要改动的键,其余键保持内置语言包原样。
4.2 语言包获取与回退规则
fetchTranslation(translations.ts)实现了几条关键规则:
- 语言包 JSON 通过 asset URL 网络加载(
assetUrls.translations[locale]),实际文件位于 assets/translations(例如en.json、es.json、zh-cn.json); - 找不到语言、加载失败或没有 messages 时一律回退到英文内置词典
EN_TRANSLATION; - 语言包按“英文兜底 + 增量覆盖”合并:
{ ...EN_TRANSLATION.messages, ...messages },即非英文语言即使缺了某些键,也会自动用英文补全(这一点对该语言相关的源码注释也有明确说明); - 开发环境下若某语言缺少若干键,会在控制台逐条 warn,方便你在开发时发现漏译;
- 文本方向由
RTL_LANGUAGES(包含ar、fa、he、ur、ku)推导,命中即为'rtl',否则'ltr',并通过useDirection暴露给布局使用。
4.3 语言切换是怎么持久化的
语言切换不是一次性的临时状态。从 LanguageMenu.tsx 可以看到,语言子菜单会遍历LANGUAGES渲染复选框列表,当前项由editor.user.getLocale()驱动;选中某语言后调用editor.user.updateUserPreferences({ locale }),同时上报change-languageUI 事件。用户的locale偏好最终序列化到用户偏好存储(字段定义在 TLUserPreferences.ts 中),因此在刷新页面、重开编辑器后语言选择依然保留。也就是说:用户在主菜单切换到的语言,会作为“当前语言”驱动整个useTranslation的查找上下文,你的es覆盖也会随之即时生效。
五、最佳实践与注意事项
结合示例代码与底层实现,可以沉淀出几条可复用的经验:
- 自定义组件统一走
useTranslation:不要硬编码与内置菜单相关的文案,否则右键菜单或面板切换语言后你的组件会“掉队”。示例里自定义按钮与内置菜单共用action.duplicate/action.delete,是保持全 UI 一致性的标准姿势。 - 新增业务键用独立命名空间:例如
overrides.en['myapp.export-all'],避免与 tldraw 未来新增的内置键撞车。 - 覆盖与组件对象放在模块顶层:保证引用稳定,减少 React 重渲染与合并开销。
- 利用“英文兜底”语义:如果只做英文品牌化改写,
en覆盖会在初始化阶段同步合并(无需等异步加载);如果做其他语言,需保证该语言包可被加载,否则会自动回退英文。 - 保持必要的 UI 结构:自定义工具栏放入
TopPanel等插槽时,建议沿用tlui-menu样式类并配置pointer-events: all,否则浮层可能无法响应点击。 - 键名可以按需查询:想知道某个内置菜单项对应的键名,可对照 defaultTranslation.ts 中
action.*、menu.*、style-panel.*等命名分组查找。
六、小结
本文以仓库自带的 Custom Language Translations 示例为主线,完整讲解了 tldraw 文案定制的两条核心路径:通过overrides.translations改写或扩充各语言词典,通过useTranslation(msg函数)在自定义组件里读取当前语言的字符串。两者共享同一翻译键空间,从而让自研 UI 与内置菜单在任何语言下都保持一致。底层上,tldraw 提供了“英文默认 + 语言包增量 + 运行时覆盖”的三层合并语义、缺失键英文兜底、RTL 语言方向支持以及基于用户偏好的语言持久化,这些机制共同保证了从单语言品牌化到完整多语言产品的落地都足够简单可靠。
【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考