---
title: "Texte, Bidi et IME — Docs zenit Zig UI"
description: "Des octets aux clusters de graphèmes, des paragraphes bidirectionnels à la composition IME : zenit traite le texte comme une capacité du système, pas comme…"
url: https://zenit.z.express/fr/docs/guide/text-engine
language: fr
alternate_en: https://zenit.z.express/docs/guide/text-engine.md
alternate_zh: https://zenit.z.express/zh/docs/guide/text-engine.md
alternate_es: https://zenit.z.express/es/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_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
---

# Texte, Bidi et IME

Des octets aux clusters de graphèmes, des paragraphes bidirectionnels à la composition IME : zenit traite le texte comme une capacité du système, pas comme une rangée de glyphes posés point de code par point de code.

## Une ligne, quatre couches

Le stockage, les limites d’édition, la résolution de direction et le shaping de la plateforme ont chacun une seule autorité, et produisent ensemble des lignes visuelles que l’on peut hit-tester et sélectionner. Aucune couche ne décide à la place d’une autre.

TEXT STACK

Les deux premières couches vivent dans src/text\_core, l’algorithme bidi dans src/i18n, et sur macOS le shaping revient à CoreText.

| Couche | Responsabilité | Emplacement |
| --- | --- | --- |
| Coordonnées de document UTF-8 | Offsets d’octets stables, normalisés sur les limites à chaque point d’entrée d’édition | `src/text_core` |
| Graphèmes UAX #29 | Le curseur, la suppression et la sélection ne coupent jamais les emoji ZWJ, les diacritiques combinants ni les drapeaux | `src/text_core/grapheme.zig` |
| Bidi UAX #9 | Direction du paragraphe, isolates, embeddings, paires de crochets, correspondance logique ↔ visuel | `src/i18n/bidi.zig` |
| Shaping CoreText | Fallback de polices, ligatures, glyph runs, placement visuel et caret x | `src/render/text_renderer.zig` |

## Graphèmes, pas points de code

Un emoji famille compte sept scalaires Unicode (quatre personnes et trois ZWJ), `é` peut être `e` suivi d’un accent aigu combinant, et un drapeau se compose de deux indicateurs régionaux. Pour l’utilisateur, chacun est un seul caractère. zenit utilise les clusters de graphèmes étendus comme atome pour le déplacement du curseur, la suppression, la sélection et la troncature à la capacité.

UAX #29

cursor\_pos est un offset d’octets UTF-8 et ne s’arrête que sur une limite de cluster. Même la troncature à la capacité de 2048 octets de l’Input sur une ligne ne laisse jamais un demi-graphème.

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

Les emoji en couleur passent par une page d’atlas BGRA et la branche couleur du shader ; le groupe témoin en niveaux de gris reste gris, et les runs mixtes partagent le pipeline de texte normal.

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

Alt+← / → saute des mots grecs et cyrilliques entiers, le double-clic sélectionne un mot, et l’emoji famille reste un seul cluster.

## Texte bidirectionnel

Le texte est stocké dans l’ordre logique et affiché dans l’ordre visuel. UAX #9 résout d’abord un niveau d’enchâssement pour chaque caractère, puis la règle L2 inverse les runs du niveau le plus élevé vers le bas. Ci-dessous, `abc אבג 123` se trouve dans un paragraphe LTR : l’hébreu est résolu au niveau 1 et les chiffres qui le suivent au niveau 2.

UAX #9

Les chiffres se lisent toujours de gauche à droite, mais tout le segment RTL — chiffres compris — est disposé de droite à gauche comme un bloc.

Le résolveur UAX #9 est une implémentation portable propre à zenit, dans `src/i18n/bidi.zig`, avec des tables de propriétés générées à partir de données Unicode 17.0.0 figées. Sur macOS, le shaping final des glyphes et la liaison RTL restent confiés à CoreText un run entier à la fois — le moteur de rendu de texte ne découpe jamais un run RTL point de code par point de code.

[![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)

Le vrai Storybook : arabe, hébreu et mélanges latin + arabic + latin mis en page par un seul pipeline de texte ; le curseur virtuel repose sur la story RTL.

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

Déplacement du curseur et sélection dans des paragraphes RTL et à direction mixte.

## Conformité Unicode 17

Les tables de propriétés d’exécution sont générées à partir de données Unicode 17.0.0 figées dans le dépôt ; les fichiers de test officiels sont vendorisés sans modification sous `vendor/unicode/17.0.0` et figés par SHA-256 dans `SHA256SUMS`. Ici, « pris en charge » ne veut pas dire une poignée de tests unitaires sur des emoji — cela veut dire exécuter les suites officielles complètes.

**490,846**séquences de types BidiTest

**770,241**évaluations de direction de paragraphe

**91,707**séquences BidiCharacterTest

**766**cas 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` vérifie que les tables Zig générées correspondent aux données ; `test-text-core` exécute tous les cas de GraphemeBreakTest ; `test-bidi-conformance` exécute les deux fichiers bidi officiels complets (jusqu’à la règle L2 d’UAX #9) et fait aussi partie de `zig build test-headless`.

## L'IME, citoyen de première classe

L’IME, ce n’est pas les caractères finaux déguisés en frappe clavier. Les événements de la plateforme conservent les phases : `ime_preedit` transporte le texte en composition plus un offset de curseur à l’intérieur, et `ime_commit` transporte le texte final. Pendant la composition, [Input](https://zenit.z.express/fr/components/input) et Textarea affichent un texte marqué souligné, mais le buffer canonique ne change qu’au commit. Il n’y a pas d’événement « annuler » distinct : un preedit vide annule la composition.

IME LIFECYCLE

Preedit et commit sont deux types d’événements, pas des instantanés d’une même chaîne. Après un commit, l’input reste brièvement dans commit\_pending\_end pour absorber un doublon du même texte que la plateforme pourrait envoyer ensuite, puis revient à idle.

| Ce que « première classe » signifie | Contrat concret |
| --- | --- |
| Événements distincts | `ime_preedit` / `ime_commit` conservent la phase ; un preedit vide annule |
| Reconversion | Les deux événements peuvent porter un replacement range (plage d’octets UTF-8) pour ramener du texte validé en composition ou le remplacer sur place |
| Composition visuelle | Le soulignement et la surbrillance du marked range, le curseur de composition et la position de la fenêtre de candidats suivent la ligne visuelle |
| Édition sûre | Curseur, suppression, sélection et troncature à la capacité tombent tous sur des limites de graphèmes étendus |
| Testable | Le harness injecte preedit / commit séparément et relit `ime_phase`, `ime_preedit_len`, `buffer`, `cursor_pos`, `anchor` |
| Validation sur système réel | `scripts/verify_ime.sh` parcourt tout le chemin NSTextInputClient avec les sources de saisie Pinyin / japonais du système |

[![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 : le texte marqué souligné et le I-beam sont visibles ; le buffer canonique n’a pas changé.

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

Commit : 日本語 entre dans le buffer canonique et le preedit est vidé.

## Tester l'IME

Le client E2E expose les deux phases comme des RPC distinctes, et `inputState` relit l’état réel d’un champ par son 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)

Le harness met le focus sur un Input dans une vraie fenêtre Retina, injecte preedit et commit séparément, puis passe à RTL et Checkbox ; le I-beam, le curseur main et l’impulsion de clic sont dessinés dans le drawable par le curseur virtuel.

> TIP
> 
> **L’injection et les vrais IME testent des choses différentes.** L’injection via le harness rend répétables les assertions sur la phase, le buffer et les pixels ; la fenêtre de candidats, le changement de source de saisie et l’intégration NSTextInputClient restent vérifiés par le chemin système réel de `scripts/verify_ime.sh` (nécessite une session graphique et l’autorisation Accessibilité ; ne touchez pas au clavier pendant environ 8 secondes).

Consultez [Harness E2E](https://zenit.z.express/fr/docs/advanced/e2e) pour le workflow complet du harness.
