Le fichier client
Un seul fichier, deux vies. Dans le navigateur, il calcule tout chez la personne qui visite un site. En Node, au moment de construire un site, il lit les mêmes fichiers sur le disque. Les deux vies rendent exactement la même empreinte pour la même chaîne.
Un fichier, deux vies
odersa-translate.js ne change pas de forme selon l’endroit où il tourne. Dans un navigateur, une page y dépose les chaînes qu’elle affiche et récupère aussitôt ce que la mémoire en connaît déjà et ce qui lui manque : rien ne part vers un serveur pour faire ce calcul, tout se passe chez la personne qui a la page ouverte. En Node, au moment de construire un site de la flotte, le même fichier lit les mêmes ressources sur le disque plutôt que par le réseau, et rend un bon de travail : ce qui est déjà traduit, ce qui manque, ce qu’on vient d’éviter de refaire.
Les deux vies s’accordent sur un point qui ne se négocie pas : la même chaîne source, dans le même contexte, doit produire la même empreinte des deux côtés. Ce n’est pas une promesse mais un fait de construction : le générateur qui écrit la mémoire au build importe ce fichier tel quel et appelle sa propre fonction empreinte pour calculer la clé de chaque entrée, plutôt que de recalculer le même algorithme de son côté. Il n’existe donc qu’un seul calcul, jamais deux qui pourraient diverger en silence. Le contrat porte en plus un numéro de version pour la normalisation, et le générateur refuse le build si ce fichier s’en écarte de ce qu’attend la configuration du projet. Le format complet de chaque fichier servi est documenté sur L’API.
L’installation
Rien à installer. Pas de dépendance, pas de npm install, pas d’outil de construction : un seul fichier, qu’on charge ou qu’on copie, et qui fonctionne tout de suite.
Dans une page
Une balise script, puis l’ouverture du client avec l’adresse de l’API.
<script src="https://translate.odersa.org/api/v2/client/odersa-translate.js"></script>
<script>
const T = OdersaTranslate.ouvrir({ base: "https://translate.odersa.org/api/v2" });
</script>En Node
Un require vers le fichier copié dans le projet, puis l’ouverture avec le chemin du dossier où le build a écrit l’API.
const OdersaTranslate = require("./odersa-translate.js");
const T = OdersaTranslate.ouvrir({ racine: "chemin/vers/site/api/v2" });Le même fichier, la même fonction ouvrir, une seule différence : base pour une adresse à interroger, racine pour un dossier à lire.
L’API, fonction par fonction
Onze fonctions, et rien de plus.
| Fonction | Ce qu’elle rend |
|---|---|
ouvrir(options) | le client configuré, prêt à appeler |
modeMemoire() | le mode de lecture réellement retenu, tout ou seau |
normaliser(texte) | le triplet de la normalisation |
empreinte(source, contexte) | { cle, ci, seau } |
manifeste() | le manifeste |
chercher(chaines, options) | un résultat par chaîne, avec son étiquette de correspondance |
bonDeTravail(chaines, options) | le bon de travail |
pack(langue) | le pack de la langue |
glossaire(langue) | le glossaire de la langue |
pieges(langue) | les pièges de la langue |
sources() | le dossier des sources |
Seule normaliser est synchrone : elle ne lit ni le réseau ni le disque, elle ne fait que des calculs sur du texte. Les dix autres rendent une promesse, parce qu’elles lisent une ressource ou calculent une empreinte SHA-256, et crypto.subtle.digest est lui-même asynchrone des deux côtés.
ouvrir
OdersaTranslate.ouvrir(options) configure le client une fois, et rend l’objet qui porte toutes les fonctions suivantes. Une option base pointe vers l’adresse du service, pour un navigateur qui interroge le réseau. Une option racine pointe vers un dossier sur le disque, pour un outil de build en Node qui vient de lire l’API fabriquée à côté de lui.
const T = OdersaTranslate.ouvrir({ base: "https://translate.odersa.org/api/v2" });
const T = OdersaTranslate.ouvrir({ racine: "chemin/vers/site/api/v2" });Elle rend le client T, celui que toutes les fonctions suivantes appellent : T.normaliser(...), T.chercher(...), et ainsi de suite.
Une troisième option décide de la façon dont la mémoire est lue, et c’est la seule qui change ce qui sort de votre appareil.
| Valeur | Ce qu’elle fait |
|---|---|
auto | le défaut : elle suit le conseil que le manifeste publie, mesuré au build sur le poids réel de la mémoire. |
tout | la mémoire entière est prise en une requête, une fois pour toutes ; ensuite, plus aucune adresse ne dépend de ce que vous cherchez. |
seau | une requête par chaîne cherchée, bien plus légère, mais le numéro du seau part avec chacune. |
const T = OdersaTranslate.ouvrir({
base: "https://translate.odersa.org/api/v2",
memoire: "tout"
});
await T.modeMemoire(); // "tout" : ce que le client a fait, pas ce qu'on lui a demandéT.modeMemoire() existe pour une raison précise : qui documente la confidentialité de son propre site doit pouvoir citer ce que son client a RÉELLEMENT fait, et pas ce qu’il croyait avoir demandé. Ce que chaque mode coûte, chiffré, est sur L’API.
normaliser
T.normaliser(texte) applique les sept étapes du contrat à une chaîne, sans toucher ni au réseau ni au disque : la même fonction, le même résultat, dans le navigateur comme en Node.
T.normaliser("Retour :");
// forme "Retour", ponctuation " :", parametres []Elle rend le triplet de la normalisation : la forme canonique, la ponctuation terminale détachée, et la liste des paramètres retrouvés dans la chaîne d’origine. Le détail de chaque étape est plus bas.
empreinte
T.empreinte(source, contexte) calcule les trois valeurs qui identifient une chaîne dans la mémoire, à partir de sa source et de son contexte.
await T.empreinte("Fermer les réglages", "nav");
// { cle: "8d7b606752cf69f6", ci: "a1602e2960e0d418", seau: "d6" }Elle rend { cle, ci, seau } : cle identifie l’entrée exacte, source, contexte et casse confondus ; ci fait la même chose sans tenir compte de la casse ; seau dit lequel des 256 fichiers de mémoire la contient, sous la forme de deux caractères hexadécimaux. Le contexte est facultatif ; omis, il vaut chaîne vide. Le détail du fichier de mémoire est sur La mémoire de traduction.
manifeste
T.manifeste() lit le point d’entrée de l’API : les langues servies, la version de la normalisation, et la taille et l’empreinte de chaque ressource. Une page ou un outil qui a besoin de savoir ce qui est disponible commence ici, plutôt que de coder une adresse en dur.
const m = await T.manifeste();
m.langues; // les langues servies
m.ressources; // taille et empreinte de chaque fichierElle rend le manifeste complet ; le détail de sa forme est sur L’API.
chercher
T.chercher(chaines, options) prend une liste de chaînes, chacune une simple chaîne de caractères ou un objet { source, contexte } quand le contexte compte, et dit pour chacune ce que la mémoire en sait déjà.
const [a, b] = await T.chercher(
["Fermer les réglages", { source: "Fermer les réglages", contexte: "nav" }],
{ langues: ["en", "es"] }
);
a.traductions; // { en: "Close settings", es: "Cerrar los ajustes" }
b.traductions; // { en: "Close the settings", es: "Cerrar los ajustes" }Les deux appels portent sur la même chaîne source ; seul le contexte change, et l’anglais n’écrit pas la même chose au menu qu’ailleurs. C’est exactement ce que le contexte existe pour attraper. Ces valeurs sont celles que la mémoire sert aujourd’hui, relevées sur les fichiers de l’API.
Elle rend un résultat par chaîne d’entrée, dans le même ordre, chacun portant son étiquette de correspondance (voir plus bas) et les traductions trouvées pour les langues demandées dans options.langues.
bonDeTravail
T.bonDeTravail(chaines, options) fait le même travail que chercher, avec les mêmes chaines, puis résume : ce qui est déjà connu, ce qui manque, ce qui est là mais pas encore relu, et ce que cela représente comme travail évité. L’option projet nomme le site ou l’outil pour lequel le bon est établi ; elle se retrouve telle quelle dans la sortie.
const bon = await T.bonDeTravail(mesChaines, {
langues: ["en", "es"],
projet: "mon-site"
});Elle rend le bon de travail complet, détaillé plus bas.
pack
T.pack(langue) télécharge, ou lit sur le disque, le pack complet d’une langue : ses chaînes, prêtes à embarquer dans un site.
const en = await T.pack("en");
en.chaines["013d74a213bf5b5c"]; // "Credits"Elle rend la ressource /api/v2/packs/{langue}.json, détaillée sur Les packs de chaînes. Les codes acceptés sont ceux des sept langues de la maison : fr, en, es, ar, hi, bn, zh ; l’API en sert aujourd’hui 7, et en ajouter une nouvelle coûte un adaptateur, jamais une refonte du client.
glossaire
T.glossaire(langue) rend les termes d’une langue : leurs rendus dans les autres langues, leur nuance d’usage, leur piège.
const fr = await T.glossaire("fr");Elle rend la ressource /api/v2/glossaires/{langue}.json, détaillée sur Les glossaires.
pieges
T.pieges(langue) rend ce qui fait rater une publication dans une langue donnée : sens d’écriture, pluriels, faux amis, nombres, dates.
const ar = await T.pieges("ar");Elle rend la ressource /api/v2/pieges/{langue}.json, détaillée sur Les pièges, langue par langue.
sources
T.sources() rend le dossier des sources tel quel, sans argument : la preuve publique que chaque valeur servie a une origine vérifiable, licence et date comprises.
const dossier = await T.sources();Elle rend la ressource /api/v2/sources.json. C’est ce dossier que citent les identifiants de provenance portés par chaque traduction rendue par chercher ou bonDeTravail (voir plus bas) : source, date et niveau, détaillés sur L’API.
La normalisation, expliquée simplement
Une mémoire qui compare des textes bruts, octet à octet, ne sert pas à grand-chose : une majuscule, une espace différente, une ponctuation de fin, et deux phrases qui disent exactement la même chose deviennent deux entrées à traduire séparément. La normalisation ramène toutes ces variantes à une seule forme avant de comparer quoi que ce soit. Elle applique sept étapes, toujours dans le même ordre, et le contrat porte un numéro de version qui doit rester identique dans le navigateur et en Node : c’est ce qui permet à une chaîne connue de toujours se retrouver, peu importe où elle est cherchée.
Trois de ces étapes font un ménage simple, sans surprise : une entrée qui n’est pas une chaîne de caractères est refusée tout de suite, plutôt que de produire un résultat approximatif ; plusieurs espaces à la suite se réduisent à une seule ; une espace au tout début ou à la toute fin de la chaîne est coupée. Les quatre autres méritent qu’on s’y arrête, avec un avant et un après.
Un mot, puis sa ponctuation détachée
Un bouton s’écrit parfois « Retour », parfois « Retour : », selon qui l’a rédigé et où le bouton vit dans la page. Le sens est le même, le mot est le même : seule change une ponctuation de fin, et le contrat la détache plutôt que de la garder collée.
avant : "Retour"
après : forme "Retour", ponctuation ""
avant : "Retour :"
après : forme "Retour", ponctuation " :"Les deux entrées rejoignent la même forme canonique, donc la même empreinte, donc la même entrée de mémoire. Sans cette étape, ce sont deux chaînes à traduire séparément pour dire exactement la même chose.
Une espace n’est pas toujours une espace
Le français place souvent une espace avant un deux-points, un point d’exclamation ou d’interrogation, et ce n’est pas une espace ordinaire : c’est une espace insécable, un caractère Unicode à part, qui empêche le mot et sa ponctuation de se séparer en fin de ligne. L’anglais n’en met jamais.
avant (fr) : "Retour :" espace insécable avant les deux-points
après (fr) : forme "Retour", ponctuation " :"
avant (en) : "Return:" l'anglais n'a pas cette espace
après (en) : forme "Return", ponctuation ":"Une étape ramène d’abord cette espace insécable, comme toute espace Unicode, à une espace ordinaire ; une autre détache ensuite la ponctuation avec l’espace qui la précède. Sans la première, la seconde ne reconnaîtrait pas l’espace insécable comme une espace du tout, et le français finirait avec une structure différente de l’anglais pour dire exactement la même chose : un mot, suivi ou non d’une ponctuation.
D’autres caractères sont invisibles à l’écran mais bien réels pour l’ordinateur : une espace de largeur nulle laissée par un copier-coller, une marque de sens d’écriture oubliée par un export. Le contrat les supprime, il ne les remplace pas par une espace, parce qu’elles ne séparent rien.
avant : "Retour" une espace de largeur nulle collée après le mot
après : forme "Retour"Rien ne le montre à l’écran : les deux versions de « Retour » semblent identiques. Sans cette étape, ce sont deux chaînes différentes pour la mémoire, et l’une des deux ne serait jamais retrouvée.
{nom} et {name}, le même trou à remplir
Deux interfaces peuvent poser la même phrase avec un paramètre nommé différemment : l’une écrit {nom}, l’autre {name}, pour désigner exactement le même emplacement, le prénom de la personne. Une comparaison lettre à lettre verrait deux chaînes distinctes ; ce n’en est qu’une.
avant : "Bonjour, {nom} !"
après : forme "Bonjour, {1} !", parametres ["nom"]
avant : "Bonjour, {name} !"
après : forme "Bonjour, {1} !", parametres ["name"]Chaque paramètre rencontré est remplacé par un jeton générique, dans l’ordre où il apparaît ; son nom réel est gardé à part, pas perdu. Les deux phrases ci-dessus rejoignent donc la même forme canonique et la même entrée de mémoire, quel que soit le nom donné au paramètre par qui a écrit le code.
Le même signe, écrit de deux façons
Beaucoup de lettres arabes qui portent un hamza existent en deux versions Unicode pour un seul et même signe : un caractère composé, ou la lettre de base suivie du signe du hamza, ajouté à part. Un mot comme إرسال (« envoyer ») commence par un إ qui s’écrit soit en un seul point de code, soit en deux : la lettre ا suivie du signe du hamza placé dessous. Les deux s’affichent à l’identique, à l’écran comme à l’impression.
composé : une seule lettre (U+0625)
décomposé : la lettre de base, puis le hamza séparé (U+0627 + U+0655)Un clavier, un correcteur orthographique ou un simple copier-coller depuis un document peut produire l’une ou l’autre forme sans que personne ne le voie ni ne le choisisse. Sans cette étape, une mémoire qui a enregistré la première graphie ne reconnaîtrait jamais une recherche écrite dans la seconde, pour un mot identique à l’écran et pour qui le lit. C’est pour l’arabe, l’hindi et le bengali que cette étape est la plus indispensable : ce sont des écritures où le même signe s’écrit couramment des deux façons.
La clé de contexte
« Retour » d’un bouton n’est pas « Retour » d’une expérience : dans la plupart des langues, les deux ne se traduisent pas pareil, et aucune relecture ne rattrape une mémoire qui les confond. Le contexte est une chaîne courte et facultative qui règle ce problème, vide par défaut, précisée (bouton, titre, formulaire.aide…) quand le mot change de sens selon l’endroit où il vit.
Ce classement s’appuie sur les deux empreintes que rend empreinte : cle pour une correspondance exacte, ci pour une correspondance à la casse près. Chaque appel qui cherche une chaîne rend l’une de ces sept étiquettes.
| Étiquette | Ce qui a été trouvé | Ce que l’appelant en fait |
|---|---|---|
exacte | la source et le contexte demandés | utiliser la traduction telle quelle |
exacte_neutre | la source, au contexte vide, alors qu’un contexte était demandé | utiliser la traduction, en sachant qu’aucune version propre à ce contexte n’existe encore |
contexte_unique | la source n’existe que sous UN SEUL contexte, et aucun contexte n’était demandé | utiliser la traduction, en regardant le contexte rendu avec elle : c’est l’endroit d’où elle vient, et on peut la refuser s’il ne convient pas |
casse | la source à la casse près, contexte demandé | vérifier si la casse importe ici, par exemple un titre en capitales face à une phrase, avant d’employer la traduction |
casse_neutre | la source à la casse près, contexte vide | même vérification que casse, sachant en plus que la traduction vient du contexte neutre et non du contexte demandé |
ambigu | la source existe sous plusieurs contextes, aucun n’a été demandé | présenter les variantes rendues et laisser la personne choisir, jamais en deviner une |
absente | rien | la chaîne est à traduire ; elle apparaît dans les manquants du bon de travail |
Le contexte est toujours facultatif, pour empreinte, chercher et bonDeTravail. Omis, il vaut chaîne vide : c’est le rendu le plus neutre, celui qui convient partout ailleurs.
Le bon de travail
Ce que rend bonDeTravail : ce qui est déjà connu, ce qui manque, ce qui est là mais pas encore relu, et ce que cela représente comme travail évité. C’est la pièce qui justifie, ou qui chiffre, une vague de traduction.
{
"projet": "mon-site",
"langues": ["en", "es"],
"total": 2,
"connus": 1,
"manquants": 1,
"a_relire": 0,
"entrees": [
{
"cle": "8d7b606752cf69f6",
"source": "Fermer les réglages",
"contexte": "nav",
"correspondance": "exacte",
"etat": "connu",
"traductions": { "en": "Close the settings", "es": "Cerrar los ajustes" },
"manquantes": []
},
{
"cle": "5c9a1e4a3b519520",
"source": "Téléverser une pièce jointe",
"contexte": "",
"correspondance": "absente",
"etat": "manquant",
"traductions": {},
"manquantes": ["en", "es"]
}
]
}Ceci est une sortie réelle, abrégée : deux chaînes déposées, une que la mémoire connaît et une qu’elle ne connaît pas. La sortie complète porte aussi le compte des chaînes ambiguës et des chaînes trouvées sous un contexte unique, une économie détaillée par langue, et la provenance de chaque traduction rendue, source, date et niveau. Chaque entrée porte sa propre étiquette de correspondance, la même liste que plus haut.
La mémoire compte aujourd’hui 674 entrées : chacune de vos chaînes qui s’y trouve déjà est une traduction que vous n’aurez pas à refaire.
Ce que le client ne fait jamais
Aucune requête POST. Aucun de vos textes dans une adresse. Aucun corps de requête. Le client ne fait que des requêtes GET vers des fichiers fixes, et le paragraphe suivant dit exactement ce qui, malgré cela, part de votre appareil.
Vos chaînes sont hachées chez vous, dans votre navigateur ou dans votre outil de build. Le manifeste, un pack, un glossaire : ce sont toujours des adresses de fichiers fixes, jamais une adresse construite à partir de ce que vous avez écrit.
Reste le numéro du seau, et il faut en parler franchement plutôt que de le ranger sous « aucune donnée envoyée ». En mode tout, il ne part pas du tout : le client prend la mémoire d’un bloc, une fois, et la même requête sert à tout le monde quoi qu’on cherche. En mode seau, il part avec chaque recherche. Il ne dit rien du sens de votre texte, une infinité de chaînes tombent dans le même seau, et il ne dépend que de la forme normalisée ; mais il porte 8 bits de son empreinte, et une suite de seaux demandés par le même appelant est une empreinte à part entière. Le compte exact, entrées par seau comprises, est sur L’API.
Cela se vérifie sans nous croire sur parole, en trente secondes.
- Ouvrez les outils de développement de votre navigateur, l’onglet réseau.
- Utilisez la page ou l’outil qui appelle le client.
- Regardez la liste des requêtes : chacune porte une adresse de fichier, un manifeste, un pack, un seau de mémoire, jamais un mot de ce que vous avez écrit.
- Cherchez une requête de méthode
POST, ou un corps de requête : il n’y en a aucun.
Les pièges côté appelant
Deux précautions à prendre chez vous ; le client ne peut pas les prendre à votre place.
Une traduction reste du texte
Ce que rendent pack, chercher ou bonDeTravail est une chaîne de caractères, jamais du balisage vérifié. L’écrire avec innerHTML sans l’échapper transformerait le moindre caractère spécial en HTML actif. Posez-la avec textContent, ou passez-la par votre propre échappement avant un innerHTML.
// à éviter
element.innerHTML = traduction;
// correct
element.textContent = traduction;Une clé __proto__ ne s’invite pas toute seule
Le contrat refuse au build toute clé nommée __proto__, constructor ou prototype dans un fichier servi, et le client refuse pareillement une telle clé à la lecture. Cette garde protège ce que le client vous rend, pas ce que vous en faites ensuite. Si vous fusionnez un pack ou un résultat de chercher dans un objet à vous, avec Object.assign ou une décomposition { ...a, ...b }, gardez votre propre garde contre une clé __proto__ venue d’ailleurs. Ce n’est pas l’aveu d’une faille : c’est la même prudence, des deux côtés de l’appel.