---
title: "Texto, Bidi e IME — Docs de zenit Zig UI"
description: "De los bytes a los clústeres de grafemas, los párrafos bidireccionales y la composición del IME: zenit trata el texto como una capacidad del sistema, no como…"
url: https://zenit.z.express/es/docs/guide/text-engine
language: es
alternate_en: https://zenit.z.express/docs/guide/text-engine.md
alternate_zh: https://zenit.z.express/zh/docs/guide/text-engine.md
alternate_ja: https://zenit.z.express/ja/docs/guide/text-engine.md
alternate_ko: https://zenit.z.express/ko/docs/guide/text-engine.md
alternate_fr: https://zenit.z.express/fr/docs/guide/text-engine.md
alternate_de: https://zenit.z.express/de/docs/guide/text-engine.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Texto, Bidi e IME

De los bytes a los clústeres de grafemas, los párrafos bidireccionales y la composición del IME: zenit trata el texto como una capacidad del sistema, no como una fila de glifos colocados por punto de código.

## Una línea, cuatro capas

El almacenamiento, los límites de edición, la resolución de dirección y el shaping de la plataforma tienen cada uno una única autoridad, y juntos producen líneas visuales en las que puedes hacer hit-test y seleccionar. Ninguna capa decide por otra.

TEXT STACK

Las dos primeras capas viven en src/text\_core, el algoritmo bidi en src/i18n, y en macOS el shaping es cosa de CoreText.

| Capa | Responsable de | Dónde |
| --- | --- | --- |
| Coordenadas de documento UTF-8 | Offsets de bytes estables, normalizados a límites en cada punto de entrada de edición | `src/text_core` |
| Grafemas UAX #29 | El cursor, el borrado y la selección nunca parten emoji ZWJ, marcas combinantes ni banderas | `src/text_core/grapheme.zig` |
| Bidi UAX #9 | Dirección de párrafo, isolates, embeddings, pares de paréntesis, mapeo lógico ↔ visual | `src/i18n/bidi.zig` |
| Shaping con CoreText | Fallback de fuentes, ligaduras, glyph runs, colocación visual y caret x | `src/render/text_renderer.zig` |

## Grafemas, no puntos de código

Un emoji de familia son siete escalares Unicode (cuatro personas y tres ZWJ), `é` puede ser `e` más un acento agudo combinante, y una bandera son dos indicadores regionales. Para el usuario, cada uno es un solo carácter. zenit usa clústeres de grafemas extendidos como átomo para el movimiento del cursor, el borrado, la selección y el truncado por capacidad.

UAX #29

cursor\_pos es un offset de bytes UTF-8 y solo se detiene en límites de clúster. Ni siquiera el truncado a la capacidad de 2048 bytes del Input de una línea deja medio grafema.

[Video](https://zenit.z.express/media/stories/emoji.mp4?v=5f65f98444)

Los emoji en color pasan por una página de atlas BGRA y la rama de color del shader; el grupo de control en escala de grises sigue gris, y los tramos mixtos comparten la pipeline de texto normal.

[Video](https://zenit.z.express/media/stories/wordnav.mp4?v=1fd0aa8709)

Alt+← / → salta palabras griegas y cirílicas completas, el doble clic selecciona una palabra y el emoji de familia sigue siendo un solo clúster.

## Texto bidireccional

El texto se almacena en orden lógico y se muestra en orden visual. UAX #9 primero resuelve un nivel de incrustación para cada carácter y luego la regla L2 invierte los tramos desde el nivel más alto hacia abajo. Abajo, `abc אבג 123` está en un párrafo LTR: el hebreo resuelve al nivel 1 y los dígitos que lo siguen al nivel 2.

UAX #9

Los dígitos se siguen leyendo de izquierda a derecha, pero todo el tramo RTL —dígitos incluidos— se coloca de derecha a izquierda como un bloque.

El resolvedor UAX #9 es una implementación portable propia de zenit en `src/i18n/bidi.zig`, con tablas de propiedades generadas a partir de datos fijados de Unicode 17.0.0. En macOS, el shaping final de glifos y la unión RTL se siguen entregando a CoreText un tramo completo a la vez: el renderizador de texto nunca divide un tramo RTL por punto de código.

[![zenit Storybook showing mixed Arabic, Hebrew and Latin text](https://zenit.z.express/media/unicode-bidi.png?v=d1e0e89e51)](https://zenit.z.express/media/unicode-bidi.png?v=d1e0e89e51)

El Storybook real: árabe, hebreo y mezclas latin + arabic + latin maquetados por una única pipeline de texto; el cursor virtual descansa sobre la story RTL.

[Video](https://zenit.z.express/media/stories/rtl.mp4?v=bc200dee27)

Movimiento del cursor y selección en párrafos RTL y de dirección mixta.

## Conformidad con Unicode 17

Las tablas de propiedades en tiempo de ejecución se generan a partir de datos de Unicode 17.0.0 fijados en el repositorio; los archivos de prueba oficiales se incluyen sin modificar en `vendor/unicode/17.0.0` y se fijan por SHA-256 en `SHA256SUMS`. Aquí, “compatible” no significa un puñado de tests unitarios de emoji: significa ejecutar las suites oficiales completas.

**490,846**secuencias de tipos de BidiTest

**770,241**evaluaciones de dirección de párrafo

**91,707**secuencias de BidiCharacterTest

**766**casos de GraphemeBreakTest

```sh
python3 tools/generate_grapheme_data.py --check
python3 tools/generate_bidi_data.py --check
(cd vendor/unicode/17.0.0 && shasum -a 256 -c SHA256SUMS)
zig build test-text-core
zig build test-bidi-conformance
```

`--check` confirma que las tablas Zig generadas coinciden con los datos; `test-text-core` ejecuta todos los casos de GraphemeBreakTest; `test-bidi-conformance` ejecuta los dos archivos bidi oficiales completos (hasta la regla L2 de UAX #9) y también forma parte de `zig build test-headless`.

## IME como ciudadano de primera clase

El IME no son los caracteres finales disfrazados de pulsación de tecla. Los eventos de la plataforma conservan las fases: `ime_preedit` lleva el texto en composición más un offset del cursor dentro de él, e `ime_commit` lleva el texto final. Durante la composición, [Input](https://zenit.z.express/es/components/input) y Textarea renderizan texto marcado subrayado, pero el buffer canónico solo cambia al hacer commit. No hay un evento “descartar” aparte: un preedit vacío cancela la composición.

IME LIFECYCLE

Preedit y commit son dos tipos de evento, no instantáneas de una misma cadena. Tras un commit, el input permanece brevemente en commit\_pending\_end para absorber un duplicado del mismo texto que la plataforma pueda enviar a continuación, y luego vuelve a idle.

| Qué significa primera clase | Contrato concreto |
| --- | --- |
| Eventos distintos | `ime_preedit` / `ime_commit` conservan la fase; un preedit vacío cancela |
| Reconversión | Ambos eventos pueden llevar un replacement range (intervalo de bytes UTF-8) para devolver texto confirmado a la composición o reemplazarlo en su sitio |
| Composición visual | El subrayado y resaltado del marked range, el cursor de composición y la posición de la ventana de candidatos siguen la línea visual |
| Edición segura | Cursor, borrado, selección y truncado por capacidad caen siempre en límites de grafemas extendidos |
| Testeable | El harness inyecta preedit / commit por separado y lee de vuelta `ime_phase`, `ime_preedit_len`, `buffer`, `cursor_pos`, `anchor` |
| Verificación en sistema real | `scripts/verify_ime.sh` recorre la ruta completa de NSTextInputClient con las fuentes de entrada Pinyin / japonés del sistema |

[![Input showing underlined nihongo preedit text](https://zenit.z.express/media/ime-preedit.png?v=6c12ff351b)](https://zenit.z.express/media/ime-preedit.png?v=6c12ff351b)

Preedit: se ven el texto marcado subrayado y el I-beam; el buffer canónico no ha cambiado.

[![Input showing committed 日本語](https://zenit.z.express/media/ime-commit.png?v=f665179c69)](https://zenit.z.express/media/ime-commit.png?v=f665179c69)

Commit: 日本語 entra en el buffer canónico y el preedit se vacía.

## Probar el IME

El cliente E2E expone ambas fases como RPC separadas, e `inputState` lee de vuelta el estado real de un input por su test id.

`ime.test.ts`

```ts
import { imeCommit, imePreedit, inputState } from "./client";

// The input must already have focus (e.g. click it first).

await imePreedit("nihongo");
let s = await inputState("story.input.name");
// s.ime_phase === "composing", s.ime_preedit_len === 7, s.buffer === ""

await imeCommit("日本語");
s = await inputState("story.input.name");
// s.buffer === "日本語", s.ime_preedit_len === 0

await imePreedit("");   // empty preedit cancels an active composition
```

[Video](https://zenit.z.express/media/e2e-text-ime.mp4?v=efa93e6b3f)

El harness enfoca un Input en una ventana Retina real, inyecta preedit y commit por separado y luego pasa a RTL y Checkbox; el I-beam, el cursor de mano y el pulso de clic los dibuja el cursor virtual en el drawable.

> TIP
> 
> **La inyección y los IME reales prueban cosas distintas.** La inyección del harness hace repetibles las aserciones de fase, buffer y píxeles; la ventana de candidatos, el cambio de fuente de entrada y la integración con NSTextInputClient se siguen verificando por la ruta del sistema real en `scripts/verify_ime.sh` (requiere una sesión GUI y permiso de Accesibilidad; no toques el teclado durante unos 8 segundos).

Consulta [Harness E2E](https://zenit.z.express/es/docs/advanced/e2e) para ver el flujo completo del harness.
