国际化
从右到左与混合方向文本、阿拉伯文塑形及焦点输入区翻译。
Text.js 在 DevTools、终端和 DOM 镜像中按正确顺序渲染希伯来文、阿拉伯文、波斯文等从右到左的文字,并对齐单元格网格。它实现了 Unicode 双向算法(UAX #9,Unicode 18.0),并通过了官方一致性测试套件:全部 770,241 个 BidiTest.txt 用例(每种段落方向下的每个输入)和全部 91,707 个 BidiCharacterTest.txt 用例。
import { Box, Text } from "@textjs/react";export default function Page() { return ( <Box flexDirection="column" padding={1}> <Text>שלום עולם! זהו טקסט בעברית.</Text> <Text>مرحبا بالعالم! هذا نص عربي.</Text> <Text>Order #1234 (<Text color="accent">שלום</Text>) total: 25.50 ₪</Text> <Text>user <Text dir="auto" color="success">שלום!</Text> replied 3 times</Text> </Box> );}方向
每个段落(<Text> 中以 \n 分隔的每一行)都根据首个强方向字符选择自己的基础方向,与 HTML 中的 dir="auto" 相同。从右到左的段落默认右对齐。
<Text>שלום</Text> {/* auto: RTL, right-aligned */}<Text dir="rtl">hello!</Text> {/* "!hello", right-aligned */}<Text dir="ltr">שלום!</Text> {/* LTR paragraph: "םולש!" reads correctly left of the "!" */}<Text align="left">שלום</Text> {/* RTL order, pinned to the left edge */}align | 从左到右的段落 | 从右到左的段落 |
|---|---|---|
"start"(默认) | 左对齐 | 右对齐 |
"end" | 右对齐 | 左对齐 |
"left" / "center" / "right" | 按指定值对齐 | 按指定值对齐 |
在嵌套的 <Text> 上,dir 会像 HTML 的 <bdi> 一样隔离 span:其内容独立布局,不会改变周围文本的顺序。对于用户提供的姓名、标题以及其他方向未知的文本,使用此属性。
<Text>user <Text dir="auto">{name}</Text> replied 3 times</Text>// name = "שלום!" → user !םולש replied 3 times (isolated: "!" ends the name)// without dir → user םולש! replied 3 times ("!" resolves with the Latin text around it)渲染过程
- 按逻辑顺序换行。 与从左到右的文本完全相同,按单元格宽度换行。
- 逐行重排。 算法解析整个段落的嵌入层级(显式嵌入与隔离、弱类型与中性类型、配对括号),然后独立重排换行后的每一行(规则 L1–L2)。在从右到左的层级中,括号和其他镜像字符会翻转(L4),因此希伯来文中的
(abc)仍显示为(abc)。 - 阿拉伯文塑形。 字母从阿拉伯文表现形式区块中取得上下文形式(独立形、首形、中形、尾形),在行内每个同层级片段中按逻辑顺序计算(ZWJ 和 ZWNJ 在各自所在位置参与计算)。一个字母占一个单元格,连接形式跨越任何 span 边界(光标、颜色变化)都能保留,控制台无需再做塑形。
- 固定顺序。 此时单元格中的字形已按显示顺序排列。浏览器和许多终端会再次对其运行双向算法,因此驱动会锁定顺序:
| 输出 | 固定顺序的方式 |
|---|---|
DevTools(CssConsoleDriver) | 包含从右到左字符的 span 用 LRO … PDF(U+202D / U+202C)包裹。DevTools 的 %c 清理器会移除 direction 和 unicode-bidi。 |
终端(AnsiDriver、text run) | 对于逐行运行双向算法的终端,包含从右到左文本的行用 LRO … PDF 包裹。renderToTerminal 在进入时切换一次到 ECMA-48 BDSM 显式模式(CSI 8 l,基于 VTE 的终端支持此模式),退出时切回隐式模式(CSI 8 h);包含从右到左文本的单次 renderToAnsi 帧以 CSI 8 l 开头。传入 bidi: "none" 可原样输出单元格。 |
DOM 镜像(<ConsoleMirror>) | 镜像设为 direction: ltr(因此 RTL 宿主页面无法翻转它),包含从右到左文本的 span 会设置 unicode-bidi: bidi-override。 |
<Box title>、<Box bottomTitle> 和 <Divider title> 经过相同的步骤(drawLogicalText(buffer, x, y, text, …) 以这种方式绘制任意单行文本),因此其标签按顺序显示,并保留字母连接。
不含从右到左字符的文本会跳过以上所有步骤。纯从左到右的文本模型会在字素分段时被识别(每个字素比较一次),并通过未改动的路径绘制;控制台帧逐字节一致。
单元格宽度
当前发布版的字体栈是 "SF Mono", Menlo, "Cascadia Mono", Consolas, monospace。这些字体都没有希伯来文或阿拉伯文字形,因此浏览器会逐字符回退到其他字体。在 macOS 上,回退字体是 Courier New,其等宽字形宽度为 0.6 em:字号为 12 px 时,每个字形宽 7.2012 px,而 Menlo 为 7.2246 px。它窄了 0.3%,足以让一整行希伯来文偏移约半个单元格。
CssConsoleDriver 使用控制台字体在 canvas 上测量每个从右到左的 span(采用与 DevTools 相同的 Blink 塑形和字体回退机制),然后按精确的 LayoutUnit(1/64 px)差值补充内边距。span 的最终宽度与其覆盖的网格单元格相同(emoji 等宽字素占两个单元格)。span 最多收缩半个单元格,因此比例回退字体(Windows 上与 Cascadia Mono 搭配的 Times New Roman)会溢出,而不会让下一个 span 覆盖其字形。在真实 DevTools 前端(Chromium 151)中,测量了一个 64×21 的帧,其中包含希伯来文、阿拉伯文、混合文本、希伯来文中夹有 emoji 的行,以及 RTL 框标题(bun bench/devtools/bidi-shot.ts):
| 帧 | 各行右边缘偏移 |
|---|---|
| 未固定顺序时输出的单元格 | 14.5 px(DevTools 重排并重新塑形) |
| 固定顺序,未适配宽度 | 2.22 px |
| 当前发布版驱动 | 0.156 px |
对照组:相同单元格,将所有 RTL 字形替换为 x | 1.67 px |
当前发布版的帧在除 emoji 行之外的每一行上都与全拉丁文对照组一致,而在 emoji 行上表现更好:emoji 会随周围从右到左的 span 一起适配宽度(0.03 px),而对照组中从左到右的 emoji 行未做宽度适配(1.66 px)。
DevTools 使用 contain: paint 绘制每个 %c span,并将各个 span 分别对齐到设备像素,因此当两个背景在小数位置相接时,中间的像素可能不属于任何一方,留下 1 px 的细缝。在 Chrome 和 Edge 中,带背景的 span 会将背景向右延伸 1 px,伸入不透明的右侧相邻 span 下方,经过宽度适配的 span 则会将这个像素加入内边距。上面的四个帧都没有出现细缝。
传入 measureText: false 可关闭宽度适配,也可以提供自己的 (text, bold, italic) => px 测量函数。
输入与编辑
<Input> 和 <Textarea> 使用逻辑光标。方向键按存储顺序在文本中移动,因此在从右到左的文本中,← 在视觉上向右移动。无论重排将字符放到哪里,块状光标都会位于其覆盖的逻辑字符上。在从右到左的片段中,这个位置是前一个字符左侧的单元格。选区也是逻辑范围,因此在混合方向的行中,高亮可能覆盖不连续的单元格。单行 <Input> 的水平滚动跟随逻辑列,因此较长的从右到左文本值可能使光标滚出可见区域。空格上或文本末尾的光标会绘制为隔离的不换行空格,因此保持中性,不会与任一侧的数字连接。
限制
- 段落分隔符。 文本在
\n处换行。U+2029 和其他段落分隔符会结束双向段落,但不会开始新行。 - 制表符。 制表符在双向算法运行前展开为空格,因此按空白字符(WS)处理,而非分段分隔符(S)。
- 塑形。 仅对具有表现形式的字母塑形:阿拉伯文、波斯文、乌尔都文,以及 U+FB50–U+FDFF 和 U+FE70–U+FEFF 中的其他字母。Lam-alef 保持为占据两个单元格的两个相连字母,而非单字形合字。叙利亚文、西非书面文字及其他连写文字保留其标称形式。
- 复制。 从控制台复制的文本按显示顺序排列(带有
LRO/PDF标记),而非逻辑顺序。 - 终端。 同时忽略 BDSM 和
LRO且自行运行双向算法的终端(例如启用双向文本的 iTerm2)可能会再次重排从右到左的行。 - DOM 镜像。
<ConsoleMirror>不做宽度适配,因此其中从右到左的片段最多可能偏移自身宽度的 0.3%。
翻译焦点区
@textjs/react/dom 中的 <FocusPad>(也由 <TextCanvas> 渲染)提示访客打开控制台、点击页面并按键。文本默认为英语。要翻译文本,传入 messages,其中包含要替换的字符串。未提供的键保留英文文本,这些文本列在 defaultFocusPadMessages 中。
import { FocusPad, type FocusPadMessages } from "@textjs/react/dom";const ja: Partial<FocusPadMessages> = { openConsole: "コンソールを開く", step: (n, total) => `${total}ステップ中${n}ステップ目`, then: (action) => `次にここをクリックして、${action}。`, shortcutOpens: (shortcut) => <>{shortcut}でコンソールが開きます。</>,};<FocusPad root={root} hints={[{ keys: "↑ ↓", label: "件数" }]} action="↑ ↓を押します" messages={ja} />;包含值的字符串采用返回完整句子的函数形式,因此翻译时可以将值放在句子中的任意位置。step、pressShortcut、then、defaultAction 和 shortcutOpens 都是函数。step 接收当前步骤编号和总步骤数。then 接收 action prop,没有 action 时则接收 defaultAction 的文本。defaultAction 接收第一条提示的按键,没有提示时则接收 null。pressShortcut 接收供屏幕阅读器使用的快捷键按键,shortcutOpens 则接收以 <kbd> 元素形式表示的快捷键。
messages 还包含屏幕阅读器朗读的标签和控制台尺寸控件(sizeControl)的文本。<TextCanvas> 接收相同的 messages prop。<ViewportControl> 也接收该 prop,并使用尺寸控件的键。⌘⌥J、Ctrl+Shift+J 和 Enter 等键帽文本不翻译。
API
import { reorderLine, paragraphDirection, resolveLevels, bidiClass, shapeArabicCodePoints } from "@textjs/core";reorderLine("שלום (abc) 123!"); // "!123 (abc) םולש": one line in display orderreorderLine("مرحبا", "auto", { shape: true }); // presentation forms, display orderparagraphDirection("123 שלום"); // "rtl" (rules P2–P3)resolveLevels(types, codePoints, direction)、applyLineRules 和 reorderIndices 暴露了算法本身:规则 P1–I2、L1 和 L2。