國際化

從右至左與混合方向文字、阿拉伯文塑形,以及焦點區翻譯。

Text.js 在 DevTools、終端機和 DOM 鏡像中,以正確順序渲染希伯來文、阿拉伯文、波斯文及其他由右至左的文字,並對齊儲存格網格。它實作 Unicode 雙向演算法(UAX #9,Unicode 18.0),並通過官方相容性測試套件:全部 770,241 個 BidiTest.txt 測試案例(每個輸入在每種段落方向下)及全部 91,707 個 BidiCharacterTest.txt 測試案例。

即時48×6 · booting Turbolog…tsx
preview://bidi:1
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)

渲染方式

  1. 依邏輯順序換行。 與由左至右的文字完全相同,依儲存格寬度斷行。
  2. 重新排列每一行。 演算法會解析整個段落的嵌入層級(明確嵌入與隔離、弱型別與中性型別、成對括號),再獨立重新排列換行後的每一行(規則 L1–L2)。括號和其他鏡像字元會在由右至左的層級翻轉(L4),因此希伯來文中的 (abc) 仍維持 (abc)。
  3. 阿拉伯文字形塑形。 字母依上下文採用 Arabic Presentation Forms 區塊中的字形(獨立形、首形、中形、尾形),在行內每個同層級區段中依邏輯順序計算(ZWJ 和 ZWNJ 會在所在位置納入計算)。一個字母佔一個儲存格,連接字形會跨越任何 span 邊界(游標、顏色變更)保持連接,主控台無須再為其塑形。
  4. 固定順序。 此時儲存格已按顯示順序存放字形。瀏覽器和許多終端機會再次對其執行雙向演算法,因此驅動程式會鎖定順序:
輸出固定順序的方式
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 字形皆替換為 x1.67 px

目前發布版本的影格在每一行都與全拉丁字母對照組相符,只有 emoji 行例外,且表現更好:emoji 會隨周圍由右至左的 span 一起調整寬度(0.03 px),而對照組由左至右的 emoji 行則未調整寬度(1.66 px)。

DevTools 以 contain: paint 繪製每個 %c 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 仍是兩個儲存格中的兩個連接字母,不會變成單一字形的連字。敘利亞文、N'Ko 及其他連寫文字會保留標準字形。
  • 複製。 從主控台複製的文字採顯示順序(帶有 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。

在 GitHub 編輯此頁