Интернационализация

Текст справа налево и со смешанным направлением, формообразование арабских букв и перевод панели фокуса.

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) выбирает базовое направление по первому сильному символу, как это делает dir="auto" в HTML. Абзацы справа налево по умолчанию выравниваются по правому краю.

<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 изолирует span подобно <bdi> в HTML: его содержимое размещается независимо и не может менять порядок окружающего текста. Используйте его для имён, заголовков и другого текста с неизвестным направлением, введённого пользователями.

<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. Фиксация порядка. Теперь ячейки содержат глифы в порядке отображения. Браузеры и многие терминалы применили бы к ним bidi повторно, поэтому драйверы фиксируют порядок:
ВыводСпособ фиксации порядка
DevTools (CssConsoleDriver)Span с символами справа налево оборачиваются в LRO … PDF (U+202D / U+202C). Фильтр %c в DevTools удаляет direction и unicode-bidi.
Терминалы (AnsiDriver, text run)Строки с текстом справа налево оборачиваются в LRO … PDF для терминалов, которые применяют bidi построчно. 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: 7,2012 px на глиф при размере 12 px против 7,2246 px у Menlo. Он на 0,3 % уже, чего достаточно, чтобы полная строка на иврите сместилась примерно на половину ячейки.

CssConsoleDriver измеряет каждый span справа налево с помощью canvas со шрифтом консоли (с тем же формированием глифов и подбором запасных шрифтов Blink, что и в DevTools), затем добавляет отступ, равный точной разнице в единицах LayoutUnit (1/64 px). В итоге ширина span совпадает с шириной занимаемых им ячеек сетки (широкая графема, например эмодзи, считается за две). Span сжимается не более чем на половину ячейки, поэтому пропорциональный запасной шрифт (Times New Roman рядом с Cascadia Mono в Windows) выходит за границы, не позволяя следующему span перекрыть его глифы. Измерено в настоящем интерфейсе DevTools (Chromium 151) на кадре 64×21 со строками на иврите, арабском, со смешанным текстом, с эмодзи внутри иврита и с RTL-заголовками блоков (bun bench/devtools/bidi-shot.ts):

КадрСмещение правого края между строками
Ячейки выведены без фиксации порядка14,5 px (DevTools меняет порядок и заново формирует глифы)
Порядок зафиксирован, без подгонки ширины2,22 px
Драйвер из поставки0,156 px
Контроль: те же ячейки, каждый RTL-глиф заменён на x1,67 px

Кадр из поставки совпадает с полностью латинским контрольным кадром на каждой строке, кроме строки с эмодзи, где результат лучше: эмодзи подгоняются вместе с окружающим span справа налево (0,03 px), а в контрольной строке слева направо с эмодзи подгонка не выполняется (1,66 px).

DevTools рисует каждый span %c с contain: paint и отдельно привязывает его к пикселям устройства, поэтому на стыке двух фонов с дробной координатой пиксель между ними может не достаться ни одному из них, оставляя щель шириной 1 px. В Chrome и Edge span с фоном продлевает его на 1 px под непрозрачного соседа справа, а span с подогнанной шириной добавляет этот пиксель к своему отступу. Ни в одном из четырёх кадров выше такой щели нет.

Передайте measureText: false, чтобы отключить подгонку, или задайте собственную функцию измерения (text, bold, italic) => px.

Ввод и редактирование

<Input> и <Textarea> сохраняют логический курсор. Клавиши со стрелками перемещают его по тексту в порядке хранения, поэтому в тексте справа налево ← визуально перемещает его вправо. Блочный курсор находится на покрываемом им логическом символе, куда бы его ни поместила перестановка. Внутри участка справа налево это ячейка слева от предыдущего символа. Выделение тоже задаётся логическим диапазоном, поэтому его подсветка может покрывать несмежные ячейки в строках со смешанным направлением. Горизонтальная прокрутка однострочного <Input> следует за логическим столбцом, поэтому длинное значение справа налево может прокрутить курсор за пределы видимой области. Курсор на пробеле или в конце текста рисуется как изолированный неразрывный пробел, поэтому он остаётся нейтральным и никогда не присоединяется к числам по обе стороны.

Ограничения

  • Разделители абзацев. Строки разделяются по \n. U+2029 и другие разделители абзацев завершают bidi-абзац, но не начинают новую строку.
  • Табуляция. Символы табуляции разворачиваются в пробелы до запуска bidi, поэтому они действуют как пробельные символы (WS), а не разделители сегментов (S).
  • Формирование глифов. Формируются только буквы с контекстными формами: арабские, персидские, урду и другие буквы в U+FB50–U+FDFF и U+FE70–U+FEFF. Лам-алиф остаётся двумя соединёнными буквами в двух ячейках, а не лигатурой из одного глифа. Сирийское письмо, нко и другие связные письменности сохраняют номинальные формы.
  • Копирование. Текст, скопированный из консоли, следует порядку отображения (с метками LRO/PDF), а не логическому порядку.
  • Терминалы. Терминалы, которые игнорируют и BDSM, и LRO и применяют собственный bidi (например, iTerm2 с включённым bidi), могут повторно переставить строки справа налево.
  • DOM-зеркала. <ConsoleMirror> не подгоняет ширину, поэтому участки справа налево могут смещаться на величину до 0,3 % своей ширины.

Перевод панели фокуса

<FocusPad> из @textjs/react/dom (также отображается компонентом <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 или текст defaultAction, если action не задано. 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.

Изменить эту страницу на GitHub