Une appli de CAA pour un enfant qui ne parle pas encore

Quelqu'un de mon entourage a un enfant qui ne parle pas encore, et qui ne lit pas non plus. Le diagnostic est autiste. Ce qui marche pour lui, pour l'instant, c'est une case qu'on touche du doigt et qui dit le mot à sa place. On m'a demandé si je pouvais construire ça.

Les applications de CAA (communication alternative et augmentée) sérieuses existent déjà. Proloquo2Go, TouchChat, LAMP Words for Life : des vocabulaires de plusieurs milliers de mots, construits par des orthophonistes sur des années, entre 150 et 300 dollars sur iOS, jusqu'à 550 livres pour Grid 3 sur ordinateur. Des alternatives libres et gratuites existent aussi, CBoard (soutenue par l'UNICEF) et AsTeRICS Grid (université des sciences appliquées de Vienne), sérieuses et vivantes. Rien de tout ça ne manquait au monde. Ce qui manquait, c'était une grille simple, tout de suite, pour un enfant précis, sans rien à traverser avant de pouvoir appuyer sur une case.

J'ai passé un weekend, puis plusieurs semaines, à construire Mes mots : Vue 3, TypeScript, zéro serveur, zéro compte. Le code est sur GitHub sous licence AGPL. Voici ce que la construire m'a appris, et pourquoi elle ne ressemble pas à une app CAA classique.

Une case qui ne bouge jamais

Le cahier des charges tenait dans une conversation, pas dans un document. Un enfant qui ne lit pas encore apprend par la position de son doigt, pas par le mot écrit sous l'image. Si la case « manger » change de coin d'un jour à l'autre parce qu'on a ajouté un mot ailleurs, l'enfant recommence à zéro. Cette contrainte a dicté toute l'architecture des planches : les cases s'ajoutent, elles ne se réorganisent jamais toutes seules.

Ce n'est pas une promesse en l'air posée dans un README. C'est vérifié par des tests, et par un banc de mutation qui s'assure que ces tests mordent vraiment, j'y reviens plus bas.

Rien ne sort de la tablette

Deuxième contrainte, aussi non négociable que la première : aucune donnée ne doit quitter l'appareil. Les photos ajoutées à une case sont celles de la famille, pas des pictogrammes génériques. Les voix enregistrées sont celles des proches, un enfant reconnaît la voix de sa mère avant de reconnaître un mot. Envoyer ça sur un serveur, même chiffré, même pour un service parfaitement inoffensif, c'était hors de question.

Concrètement, tout vit dans IndexedDB, quatre magasins distincts accédés par un seul dépôt local. Le commentaire du code dit ce que ça engage, texto :

/**
 * Dépôt local de la configuration (exigence T1). Rien ne sort de la tablette.
 *
 * Les magasins d'images et de sons sont clés par identifiant de case : une photo
 * ou un son de la famille, jamais la configuration elle-même, qui ne porte qu'une référence
 * en texte (`perso/<idCase>`, voir `identifiantPersonnalise` dans planche.ts).
 */
interface SchemaMesMots extends DBSchema {
  config: { key: string; value: Configuration | ConfigurationV2 | Planche | number }
  images: { key: string; value: Blob }
  sons: { key: string; value: Blob }
  journal: { key: number; value: EntreeJournal }
}

Pas de compte, pas de synchronisation, pas de sauvegarde automatique dans un cloud. La contrepartie, assumée et rappelée par l'appli elle-même, c'est que la famille doit sauvegarder à la main. Le format de sauvegarde n'est pas propriétaire : c'est un fichier .obz, le standard Open Board Format, lisible par d'autres applications de CAA le jour où il faut changer d'outil.

L'appli tourne en PWA, installable et utilisable en mode avion. Ce sujet mérite un article à lui seul, les pièges d'une appli qui ne demande jamais le réseau, et comment elle se met quand même à jour, c'est le sujet du prochain.

Vue 3 sans une seule librairie d'interface

Aucune librairie de composants, aucun framework CSS. Chaque bouton, chaque case, chaque transition est écrite à la main, en <style scoped> par composant. Pas par puritanisme : pour une grille d'une quinzaine de cases et deux écrans (enfant, parents), une librairie de composants apporte plus de code à maintenir que de temps gagné, et surtout impose sa propre accessibilité par défaut là où celle-ci doit être pensée pour un usage précis. Le CSS qu'on écrit soi-même, on sait exactement ce qu'il fait et pourquoi.

Un banc de mutation, parce qu'une suite verte ne prouve rien

611 tests unitaires Vitest, 234 tests de bout en bout Playwright, sur deux profils : une tablette 800×1280 et un iPhone 13 sous Safari. Sur le papier, une couverture confortable. Sauf qu'une suite entièrement verte prouve seulement que rien n'a cassé, pas que quelque chose est vraiment vérifié.

Un test unitaire classique appelle une fonction, compare le résultat à ce qu'il attend, et devient vert dès que l'appel passe, même si l'assertion ne vérifie presque rien. Le seul moyen de savoir si un test mord vraiment, c'est de casser volontairement le code qu'il est censé protéger, et de vérifier que le test rougit. C'est ce que fait le mutation testing, et c'est ce que fait mutation.sh dans le projet : une mutation par tâche du backlog, pas une par fichier, motif par motif.

Le commentaire en tête du script raconte pourquoi il existe : « à l'étape E1 l'interruption audio avait pu disparaître entièrement sans qu'un contrôle ne bronche. » Autrement dit, une régression complète sur l'interruption du son, pas un détail, un comportement entier, était passée à travers plus de 600 tests tous verts. Le motif de mutation correspondant, lui, l'a attrapée.

Le script a son lot de pièges pour rester honnête. Le premier : un serveur vite preview oublié sur le port 4173 servirait l'ancien code à chaque mutation, et toutes les mutations « passeraient » pour vues alors que les tests tournent sur du code qui n'a jamais changé.

# Un serveur oublié sur le port servirait l'ancien code et la mutation passerait pour vue.
if ss -lptn 'sport = :4173' 2>/dev/null | grep -q LISTEN; then
  echo "  port 4173 déjà occupé, les mutations de bout en bout seraient faussées"
  exit 1
fi

Le deuxième piège est plus subtil : une mutation qui casse la compilation laisse dist/ intact, donc vite preview continue de servir l'ancien bundle. Sans un vite build avant chaque tentative de bout en bout, le banc accuserait les tests d'avoir raté la mutation, alors que c'est le serveur qui servait du code jamais reconstruit.

Ce qu'elle ne fait pas, et pourquoi ça compte

Pas de commande oculaire ni de contacteur, elle s'utilise au doigt. Pas de vocabulaire de plusieurs milliers de mots construit sur des années par des orthophonistes, comme en portent Proloquo2Go ou LAMP Words for Life. Pas de grammaire, pas de conjugaison : on pose des mots, on ne construit pas des phrases complexes. Pas de synchronisation entre plusieurs appareils, par choix, pas par oubli.

Ce n'est pas non plus un dispositif médical ni un traitement. Une appli de CAA se choisit avec un orthophoniste, qui connaît l'enfant et sait dire si une grille de pictogrammes lui convient, et lesquels. Cette appli a été écrite pour un enfant précis, sur les besoins que sa famille a décrits. Je la publie parce qu'elle peut servir ailleurs, pas parce qu'elle conviendrait à tous.

Conclusion

La partie la plus difficile n'était pas Vue, ni IndexedDB, ni même le banc de mutation. C'était de résister à l'envie d'en faire plus : plus de mots, plus d'options, plus de réglages. Un enfant qui ne lit pas encore n'a rien à gagner à une grille de deux cents cases. Il a besoin de dix, qui ne bougent jamais.

Le code est là si quelqu'un d'autre en a besoin. La démo aussi.

Commentaires (0)