国際化
右から左のテキストと双方向混在テキスト、アラビア文字の字形処理、フォーカスパッドの翻訳。
Text.jsは、ヘブライ語、アラビア語、ペルシア語などの右から左へ書く文字を、DevTools、ターミナル、DOMミラーでセルグリッドに揃えて正しい順序で描画します。Unicode双方向アルゴリズム(UAX #9、Unicode 18.0)を実装し、公式の適合性テストである BidiTest.txt の全770,241ケース(すべての段落方向でのすべての入力)と BidiCharacterTest.txt の全91,707ケースに合格しています。
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> と同様にスパンを分離します。内容は独立してレイアウトされ、周囲のテキストの順序には影響しません。ユーザーが入力する名前、タイトルなど、方向が不明なテキストに使用します。
<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)のままです。 - アラビア文字を字形処理します。 文字にはArabic Presentation Formsブロックの文脈依存の字形(独立形、語頭形、語中形、語末形)を適用し、行内の各レベルランで論理順に計算します(ZWJとZWNJはその位置で考慮されます)。1文字は1セルに対応し、結合した字形はスパンの境界(カーソルや色の変更)をまたいでも維持されるため、コンソール側で字形処理する必要はありません。
- 順序を固定します。 この時点で、セルは表示順にグリフを保持しています。ブラウザーや多くのターミナルはこれらに双方向処理を再適用するため、ドライバーが順序を固定します。
| 出力 | 順序の固定方法 |
|---|---|
DevTools(CssConsoleDriver) | 右から左への文字を含むスパンを 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のホストページによる反転を防ぐため)、右から左へのテキストを含むスパンには unicode-bidi: bidi-override を設定します。 |
<Box title>、<Box bottomTitle>、<Divider title> も同じ手順で処理されるため(drawLogicalText(buffer, x, y, text, …) は任意の単一行をこの方法で描画します)、ラベルは正しい順序で、文字が結合した状態で表示されます。
右から左への文字を含まないテキストは、これらの処理をすべてスキップします。純粋に左から右へのテキストモデルは、書記素の分割時に検出され(書記素ごとに1回比較)、従来と同じ経路で描画されます。コンソールのフレームはバイト単位で一致します。
セル幅
標準のフォントスタックは "SF Mono", Menlo, "Cascadia Mono", Consolas, monospace です。これらのフォントにはヘブライ語やアラビア語のグリフがないため、ブラウザーは文字ごとにフォールバックします。macOSではCourier Newが使われ、その等幅は0.6 emです。12 pxでは1グリフあたり7.2012 pxとなり、Menloの7.2246 pxとは差があります。0.3%狭いため、ヘブライ語が行全体に並ぶと約半セル分ずれます。
CssConsoleDriver は、コンソールのフォントを設定したcanvasで右から左への各スパンを測定し(DevToolsと同じBlinkの字形処理とフォントフォールバックを使用)、LayoutUnit(1/64 px)単位の正確な差分をパディングとして追加します。その結果、スパンの幅は、それが占めるグリッドセルの幅と一致します(絵文字などの幅広い書記素は2セルとして数えます)。スパンの縮小は最大で半セル分に制限されるため、プロポーショナルなフォールバックフォント(WindowsでCascadia Monoと並ぶTimes New Roman)は、次のスパンがグリフに重なる代わりにはみ出します。ヘブライ語、アラビア語、混在テキスト、ヘブライ語内の絵文字を含む行とRTLのボックスタイトルからなる64×21フレームを、実際のDevToolsフロントエンド(Chromium 151)で測定した結果です(bun bench/devtools/bidi-shot.ts)。
| フレーム | 行間の右端のずれ |
|---|---|
| 順序を固定せずにログ出力したセル | 14.5 px(DevToolsが並べ替えと字形処理を再適用) |
| 順序を固定、幅調整なし | 2.22 px |
| 標準ドライバー | 0.156 px |
対照:同じセルで、すべてのRTLグリフを x に置換 | 1.67 px |
標準のフレームは、絵文字の行を除くすべての行で、全文字をラテン文字にした対照と同等であり、絵文字の行ではより良い結果になります。絵文字は周囲の右から左へのスパンとともに幅調整される一方(0.03 px)、対照の左から右への絵文字の行は幅調整されません(1.66 px)。
DevToolsはすべての %c スパンを contain: paint で描画し、それぞれを個別にデバイスピクセルに揃えるため、2つの背景が端数位置で接すると、その間のピクセルがどちらにも属さず、1 pxの細い隙間が残ることがあります。ChromeとEdgeでは、背景を持つスパンの背景を、右隣の不透明なスパンの下へ1 px延ばし、幅調整されたスパンではその1ピクセルをパディングに追加します。上記4つのフレームには、いずれも細い隙間はありません。
幅調整を無効にするには measureText: false を渡し、独自に測定するには (text, bold, italic) => px の測定関数を指定します。
入力と編集
<Input> と <Textarea> は論理カーソルを保持します。矢印キーは保存順にテキスト内を移動するため、右から左へのテキストでは←で見た目上は右へ移動します。ブロックカーソルは、並べ替え後の位置にかかわらず、対象の論理文字上に表示されます。右から左へのラン内では、直前の文字の左隣のセルに相当します。選択も論理的な範囲なので、方向が混在する行では、ハイライトが連続しないセルにまたがる場合があります。単一行の <Input> の水平スクロールは論理列に追従するため、右から左への長い値ではカーソルが表示範囲外へスクロールすることがあります。空白上やテキスト末尾のカーソルは、分離された改行しない空白として描画されるため、中立の方向性を維持し、両側の数字に結合しません。
制限事項
- 段落区切り。 行は
\nで分割されます。U+2029などの段落区切り文字は双方向処理の段落を終了しますが、改行はしません。 - タブ。 タブは双方向処理の前に空白へ展開されるため、セグメント区切り(S)ではなく空白(WS)として扱われます。
- 字形処理。 字形処理の対象は、表示形を持つ文字のみです。アラビア語、ペルシア語、ウルドゥー語、およびU+FB50〜U+FDFFとU+FE70〜U+FEFFに含まれるその他の文字が該当します。Lam-alefは1グリフの合字にはならず、2セル内の結合した2文字のままです。シリア文字、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 プロパティーを受け取り、action がない場合は defaultAction のテキストを受け取ります。defaultAction は最初のヒントのキーを受け取り、ヒントがない場合は null を受け取ります。pressShortcut はスクリーンリーダー用にショートカットのキーを受け取り、shortcutOpens はショートカットを <kbd> 要素として受け取ります。
messages には、スクリーンリーダーが読み上げるラベルとコンソールのサイズコントロール(sizeControl)も含まれます。<TextCanvas> も同じ messages プロパティーを受け取ります。<ViewportControl> もこれを受け取り、サイズコントロールのキーを使用します。⌘⌥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を公開しています。