Internacionalización
Texto de derecha a izquierda y de dirección mixta, formas contextuales del árabe y traducción del panel de foco.
Text.js renderiza hebreo, árabe, persa y otras escrituras de derecha a izquierda en el orden correcto, alineadas con la cuadrícula de celdas, en DevTools, terminales y réplicas DOM. Implementa el algoritmo bidireccional de Unicode (UAX #9, Unicode 18.0) y supera las suites oficiales de conformidad: los 770.241 casos de BidiTest.txt (cada entrada en cada dirección de párrafo) y los 91.707 casos de 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> );}Dirección
Cada párrafo (cada línea de un <Text> separada por \n) elige su dirección base a partir de su primer carácter fuerte, como hace dir="auto" en HTML. Los párrafos de derecha a izquierda se alinean a la derecha por defecto.
<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 | Párrafo de izquierda a derecha | Párrafo de derecha a izquierda |
|---|---|---|
"start" (predeterminado) | izquierda | derecha |
"end" | derecha | izquierda |
"left" / "center" / "right" | según lo indicado | según lo indicado |
En un <Text> anidado, dir aísla el span como el <bdi> de HTML: su contenido se distribuye de forma independiente y no puede reordenar el texto que lo rodea. Úsalo para nombres, títulos y otros textos de dirección desconocida proporcionados por los usuarios.
<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)Proceso de renderizado
- Dividir las líneas en orden lógico. Las líneas se dividen exactamente igual que en el texto de izquierda a derecha, según el ancho de las celdas.
- Reordenar cada línea. El algoritmo resuelve los niveles de incrustación de todo el párrafo (incrustaciones explícitas y aislamientos, tipos débiles y neutros, signos de cierre y apertura emparejados); después, cada línea resultante se reordena por separado (reglas L1–L2). Los signos de cierre y apertura y otros caracteres reflejados se intercambian en los niveles de derecha a izquierda (L4), por lo que
(abc)sigue siendo(abc)dentro del hebreo. - Dar forma al árabe. Las letras adoptan sus formas contextuales (aislada, inicial, medial, final) de los bloques Arabic Presentation Forms, calculadas en orden lógico dentro de cada tramo de nivel de la línea (ZWJ y ZWNJ cuentan en la posición que ocupan). Cada letra ocupa una celda, las formas unidas se conservan al cruzar cualquier límite de span (un cursor, un cambio de color) y la consola nunca tiene que darles forma.
- Fijar el orden. Las celdas contienen ahora glifos en orden de visualización. Los navegadores y muchas terminales les aplicarían bidi una segunda vez, así que los controladores fijan el orden:
| Salida | Cómo se fija el orden |
|---|---|
DevTools (CssConsoleDriver) | Los spans que contienen caracteres de derecha a izquierda se envuelven en LRO … PDF (U+202D / U+202C). El sanitizador de %c de DevTools elimina direction y unicode-bidi. |
Terminales (AnsiDriver, text run) | Las filas con texto de derecha a izquierda se envuelven en LRO … PDF para las terminales que aplican bidi por línea. renderToTerminal cambia al modo explícito BDSM de ECMA-48 (CSI 8 l, respetado por las terminales basadas en VTE) una vez al entrar y vuelve al modo implícito (CSI 8 h) al salir; los fotogramas individuales de renderToAnsi que contienen texto de derecha a izquierda empiezan con CSI 8 l. Pasa bidi: "none" para emitir las celdas tal cual. |
Réplicas DOM (<ConsoleMirror>) | La réplica usa direction: ltr (para que una página anfitriona RTL no pueda invertirla), y los spans que contienen texto de derecha a izquierda reciben unicode-bidi: bidi-override. |
<Box title>, <Box bottomTitle> y <Divider title> siguen los mismos pasos (drawLogicalText(buffer, x, y, text, …) dibuja cualquier línea individual de esta manera), por lo que sus etiquetas se muestran en orden y con las letras unidas.
El texto sin caracteres de derecha a izquierda omite todo este proceso. Un modelo de texto exclusivamente de izquierda a derecha se detecta mientras se segmentan sus grafemas (una comparación por grafema) y se dibuja mediante la ruta sin cambios; los fotogramas de la consola son idénticos byte a byte.
Anchos de celda
La pila de fuentes incluida es "SF Mono", Menlo, "Cascadia Mono", Consolas, monospace. Ninguna de estas fuentes tiene glifos hebreos o árabes, por lo que los navegadores recurren a una fuente alternativa para cada carácter. En macOS es Courier New, que es monoespaciada a 0,6 em: 7,2012 px por glifo a 12 px frente a los 7,2246 px de Menlo. Es un 0,3 % más estrecha, lo suficiente para desplazar una fila completa de hebreo aproximadamente media celda.
CssConsoleDriver mide cada span de derecha a izquierda con un canvas que usa la fuente de la consola (con la misma conformación de Blink y sustitución de fuentes que usa DevTools) y luego añade un relleno equivalente a la diferencia exacta en LayoutUnit (1/64 px). El span termina con el mismo ancho que las celdas de la cuadrícula que ocupa (un grafema ancho, como un emoji, cuenta como dos). Un span se contrae como máximo media celda, de modo que una fuente alternativa proporcional (Times New Roman junto a Cascadia Mono en Windows) desborda en vez de permitir que el siguiente span se superponga a sus glifos. Mediciones en la interfaz real de DevTools (Chromium 151) sobre un fotograma de 64×21 con líneas en hebreo, árabe, mixtas y con emojis en hebreo, y títulos de cajas RTL (bun bench/devtools/bidi-shot.ts):
| Fotograma | Desviación del borde derecho entre filas |
|---|---|
| Celdas registradas sin fijar el orden | 14,5 px (DevTools reordena y vuelve a dar forma) |
| Orden fijado, sin ajuste de ancho | 2,22 px |
| Controlador incluido | 0,156 px |
Control: mismas celdas, con cada glifo RTL sustituido por x | 1,67 px |
El fotograma del controlador incluido coincide con el control íntegramente latino en todas las filas salvo la de emojis, donde obtiene un mejor resultado: los emojis se ajustan junto con el span de derecha a izquierda que los rodea (0,03 px), mientras que la fila de emojis de izquierda a derecha del control no se ajusta (1,66 px).
DevTools dibuja cada span %c con contain: paint y ajusta cada uno a los píxeles del dispositivo por separado, por lo que, cuando dos fondos se encuentran en una posición fraccionaria, el píxel entre ambos puede no asignarse a ninguno y dejar una línea de 1 px. En Chrome y Edge, un span con fondo lo extiende 1 px por debajo de su vecino opaco de la derecha, y un span ajustado añade ese píxel a su relleno. Ninguno de los cuatro fotogramas anteriores muestra esa línea.
Pasa measureText: false para desactivar el ajuste, o proporciona tu propia función de medición (text, bold, italic) => px.
Entrada y edición
<Input> y <Textarea> mantienen un cursor lógico. Las teclas de flecha recorren el texto en el orden de almacenamiento, por lo que, en texto de derecha a izquierda, ← se mueve visualmente a la derecha. El cursor de bloque se sitúa sobre el carácter lógico que cubre, dondequiera que lo coloque la reordenación. Dentro de un tramo de derecha a izquierda, es la celda a la izquierda del carácter anterior. La selección también es un intervalo lógico, por lo que su resaltado puede cubrir celdas no contiguas en líneas de dirección mixta. El desplazamiento horizontal de un <Input> de una sola línea sigue la columna lógica, por lo que un valor largo de derecha a izquierda puede desplazar el cursor fuera de la vista. Un cursor sobre un espacio o al final del texto se dibuja como un espacio de no separación aislado, de modo que permanece neutro y nunca se une a los números de ninguno de sus lados.
Limitaciones
- Separadores de párrafo. Las líneas se dividen en
\n. U+2029 y otros separadores de párrafo terminan un párrafo bidi, pero no inician una línea nueva. - Tabulaciones. Las tabulaciones se expanden a espacios antes de que se ejecute bidi, por lo que actúan como espacios en blanco (WS), no como separadores de segmento (S).
- Conformación. Solo se da forma a las letras con formas de presentación: árabe, persa, urdu y las demás letras de U+FB50–U+FDFF y U+FE70–U+FEFF. Lam-alef se mantiene como dos letras unidas en dos celdas, en lugar de la ligadura de un solo glifo. El siríaco, el N'Ko y otras escrituras con letras unidas conservan sus formas nominales.
- Copia. El texto copiado de la consola está en orden de visualización (con las marcas
LRO/PDF), no en orden lógico. - Terminales. Las terminales que ignoran tanto BDSM como
LROy ejecutan su propio bidi (por ejemplo, con bidi activado en iTerm2) pueden volver a reordenar las filas de derecha a izquierda. - Réplicas DOM.
<ConsoleMirror>no ajusta los anchos, por lo que los tramos de derecha a izquierda pueden desviarse hasta un 0,3 % de su ancho.
Traducir el panel de foco
<FocusPad> de @textjs/react/dom (que también renderiza <TextCanvas>) indica a los visitantes que abran la consola, hagan clic en la página y pulsen una tecla. Su texto está en inglés de forma predeterminada. Para traducirlo, pasa messages con las cadenas que quieras reemplazar. Las claves que omitas conservan su texto en inglés, recogido en 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} />;Una cadena que contiene un valor es una función que devuelve la frase completa, de modo que la traducción puede colocar el valor en cualquier posición. step, pressShortcut, then, defaultAction y shortcutOpens son funciones. step recibe el número del paso y el número de pasos. then recibe la prop action, o el texto de defaultAction cuando no hay action. defaultAction recibe las teclas de la primera indicación, o null si no hay indicaciones. pressShortcut recibe las teclas del atajo para los lectores de pantalla, y shortcutOpens recibe el atajo como elementos <kbd>.
messages también incluye las etiquetas que anuncian los lectores de pantalla y el control de tamaño de la consola (sizeControl). <TextCanvas> acepta la misma prop messages. <ViewportControl> también la acepta y usa las claves del control de tamaño. Las representaciones de teclas como ⌘⌥J, Ctrl+Shift+J y Enter no se traducen.
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 y reorderIndices exponen el propio algoritmo: las reglas P1–I2, L1 y L2.