Ce site est un site officiel de l’ODERSA. Voici comment le savoir

Le domaine officiel

L’adresse de ce site se termine par odersa.org. Chaque service de l’association vit sur un sous-domaine de odersa.org et nulle part ailleurs. Si l’adresse dans la barre de votre navigateur ne finit pas par odersa.org, ce site n’est pas le nôtre.

Gratuit, et sans compte

Tout est ouvert tout de suite. Aucune inscription, aucun compte, aucun mot de passe, aucun abonnement, aucune publicité. Rien n’est réservé à qui paie, puisque rien ne se paie.

Aucune donnée collectée

Ce site ne vous suit pas : ni mouchard, ni mesure d’audience, ni cookie de traçage. Vous n’avez pas à nous croire : ouvrez les outils de développement de votre navigateur, onglet Réseau, et rechargez la page. Vous verrez la liste complète de ce que le site demande. Tout vient de odersa.org, rien ne part ailleurs.

Contenus libres

Les contenus sont publiés sous licence CC BY 4.0. Vous pouvez les copier, les traduire, les imprimer et les redistribuer, pour vos cours comme pour vos proches, à la seule condition de citer l’ODERSA.

L’API

Une API de traduction doit traduire. Celle-ci s’importe et s’appelle, dans un navigateur comme en ligne de commande, et elle rend le texte traduit. Elle n’est pas une porte où nous recevrions vos phrases : le moteur et les modèles descendent chez vous comme de simples fichiers, et le processeur qui travaille est le vôtre. C’est la seule raison pour laquelle elle est illimitée et gratuite, et cette raison est mécanique, pas commerciale.

  • Sans compte
  • Sans clé
  • Sans quota
  • Le calcul chez l’appelant
  • Appelable depuis n’importe quel domaine

Les deux appels

Ce qui marche, et rien d’autre

Ces deux exemples ont été exécutés avant d’être écrits ici. Le premier dans un onglet, le second en ligne de commande. Ils ne sont pas les mêmes, et la section suivante dit pourquoi il serait malhonnête de les écrire pareil.

Dans un navigateur

const t = await import(
  "https://translate.odersa.org/api/v2/client/sdk.js"
);

const r = await t.traduire("Bonjour le monde.", "fr", "bn");

r.texte;    // le bengali
r.chemin;   // ["fr", "en", "bn"] : le pivot par l'anglais, nommé
r.moteur;   // qui a calculé, et c'était chez vous

Un module ES, importé par son adresse. Le calcul part dans un worker que le SDK crée lui-même : le moteur est bloquant, et geler l’onglet de qui nous appelle serait impardonnable.

En Node

Le fichier se prend une fois, parce que Node ne charge pas un module par une adresse http : la raison est juste en dessous.

curl -O https://translate.odersa.org/api/v2/client/odersa-translate.js
const T = require("./odersa-translate.js");

const r = await T.traduire("Bonjour le monde.", "fr", "bn", {
  site: "https://translate.odersa.org"
});

r.texte;    // le bengali, calculé sur votre machine

Le même fichier, chargé par require. Aucune dépendance, aucun npm install, aucun compte. Le calcul tient le fil principal pendant qu’il travaille, ce qui convient à un outil de build et ne conviendrait pas à une page.

Information

Deux entrées, une seule implémentation. sdk.js est un module ES et ne fait que republier ce que porte odersa-translate.js, qui vit dans le même dossier. Rien n’est écrit deux fois, donc rien ne peut diverger.

Pourquoi deux entrées

L’appel élégant serait la même ligne des deux côtés. Il ne marche pas, et le publier quand même reviendrait à faire perdre une heure au premier développeur qui le copierait.

Node ne charge pas un module par une adresse http ou https. Il ne s’agit pas d’un réglage à trouver : le drapeau expérimental qui l’autorisait autrefois a été retiré, et le passer aujourd’hui empêche Node de démarrer. Voici ce que Node répond, mot pour mot, quand on essaie :

await import("https://translate.odersa.org/api/v2/client/sdk.js");

// ERR_UNSUPPORTED_ESM_URL_SCHEME
// Only URLs with a scheme in: file and data are supported
// by the default ESM loader. Received protocol 'https:'

D’où la forme retenue. Le navigateur importe sdk.js par son adresse, parce qu’un navigateur sait faire cela et que c’est sa façon normale de charger un module. Node prend odersa-translate.js une fois, puis l’appelle du disque, parce que c’est la façon normale de charger un fichier en Node.

Le message d’erreur nomme lui-même la seconde voie : data: est acceptée. Elle sert quand on ne veut rien poser sur le disque du tout, et elle est mesurée comme le reste.

// Node, sans rien installer et sans rien écrire sur le disque.
const source = await (await fetch(
  "https://translate.odersa.org/api/v2/client/odersa-translate.js"
)).text();

await import("data:text/javascript," + encodeURIComponent(source));
const T = globalThis.OdersaTranslate;   // l'UMD se pose là, comme sous une balise script

const r = await T.traduire("Bonjour le monde.", "fr", "es");

Le fichier de calcul reste en UMD, et ce n’est pas de la nostalgie : le générateur de cet endpoint et deux de ses verrous le chargent par require pour refaire les empreintes du disque et comparer le contrat au client au caractère près. Un module ES ne se charge pas par require. La porte en module ES est donc un fichier à côté, et non une réécriture de celui-là.

Pourquoi elle est illimitée, et gratuite

Ce n’est pas une générosité, c’est une conséquence. Suivez la chaîne :

  1. Vous demandez une direction de traduction

    Le SDK lit /modeles/registre.json, la liste de ce qui existe réellement chez nous, et calcule le chemin : un saut quand l’anglais est d’un côté, deux sinon.

  2. Le moteur descend chez vous

    Un fichier WebAssembly, servi comme une image ou une feuille de style. Notre serveur ne fait rien d’autre que le tendre.

  3. Les modèles descendent chez vous

    Des fichiers compressés, un par pièce du modèle, servis tels quels. Là encore : aucun calcul de notre côté, aucune session, aucune trace de ce que vous allez traduire.

  4. Votre processeur travaille

    La traduction se calcule chez vous, dans un worker si vous êtes dans un navigateur, sur le fil principal si vous êtes en ligne de commande. Votre texte n’a pas d’adresse à laquelle partir : il n’y a personne au bout.

  5. Il n’y a rien à facturer, donc rien à rationner

    Un service qui calcule doit compter, donc identifier, donc plafonner. Celui-ci ne calcule pas. Pas de compte, pas de clé, pas de quota : il n’y a rien à compter.

Chaque appelant apporte son propre processeur. C’est cela, et rien d’autre, qui rend cette API illimitée.

La contrepartie est réelle et se dit dans le même souffle : ce que nous ne payons pas en calcul, vous le payez en téléchargement et en mémoire. La section suivante le chiffre au lieu de l’adoucir.

Ce que cela vous coûte

Un appelant qui découvre ce coût en production a été mal renseigné. Voici ce qu’il faut savoir avant d’écrire la première ligne.

Ce qu’un appel demande à la machine de l’appelant
Ce qui est en jeuCe qu’il faut prévoir
Le moteur Un fichier WebAssembly, téléchargé une fois par onglet ou par processus, puis gardé.
Une paire de langues En moyenne 20 Mo compressés. Un pivot en demande deux, donc le double avant la première phrase.
La mémoire vive Un modèle vivant occupe une grande part du tas que le binaire du moteur s’alloue, et ce tas est plafonné en dur. Le SDK ne garde donc que deux paires en vie, et évacue la moins récemment servie.
Le premier appel Lent : il télécharge, il décompresse, il construit le modèle. Les suivants sur la même direction sont immédiats.
Le fil d’exécution Le moteur est bloquant. Dans un navigateur, le SDK le met dans un worker. En Node, il tient le fil principal.

Le plafond de deux paires vivantes n’est pas un réglage prudent, c’est une mesure : au-delà, le moteur épuise le tas que son binaire lui alloue et abandonne. Deux est aussi le plancher, puisqu’un pivot demande deux modèles en même temps. Changer de langue d’arrivée ne reconstruit donc que le second saut, et les octets viennent du cache du navigateur, pas du réseau.

Un appel qui télécharge suit son avancement, et le SDK dit honnêtement ce qu’il sait : le poids des modèles est au registre, donc il rend une part réelle ; le poids du moteur n’y est pas, donc il n’en invente pas.

const t = OdersaTranslate.traducteur({ site: "https://translate.odersa.org" });

const r = await t.traduire("Bonjour le monde.", "fr", "bn", {
  avancement: (e) => console.log(e.etape, e.part)
  // "moteur" null   : le poids du moteur n'est pas annoncé, donc aucune part
  // "modele" 7 … 100 : le registre donne le poids des modèles, donc une vraie part
});

await t.fermer();   // rend la mémoire tout de suite au lieu d'attendre l'onglet

Tout ce que le SDK expose

Deux façons d’appeler, et elles ne servent pas la même chose. traduire se suffit quand on veut une phrase et qu’on ne veut rien tenir : il garde un traducteur pour lui, sinon chaque phrase retéléchargerait son modèle. traducteur rend un objet à soi, dont on tient la durée de vie et qu’on ferme.

Le SDK, appel par appel
AppelCe qu’il rend
traduire(texte, de, vers, options)le résultat, avec son chemin et le nom du moteur qui a calculé
traducteur(options)un traducteur à soi : mêmes appels, plus fermer()
langues()les codes utilisables, lus au registre servi
paires()les paires de modèles réellement servies
chemin(de, vers)les sauts, ou un refus nommé quand la direction n’existe pas
fermer()libère les modèles et arrête le worker
Les options d’un appel, et les champs d’un résultat
CléCe qu’elle porte
sitela racine qui sert le moteur et les modèles ; le domaine du service par défaut
htmlconserve les balises à leur place plutôt que de les traduire
avancementune fonction appelée pendant les téléchargements
fil"principal" pour refuser le worker, en sachant que le calcul gèlera la page
textela traduction ; une liste si on a passé une liste
cheminles langues traversées, dans l’ordre
pivotvrai quand la traduction a passé par l’anglais
moteurqui a calculé. Une sortie de machine se dit, elle ne se laisse pas confondre avec une phrase relue par une personne

Un lot se traduit en un appel, et c’est deux fois moins de travail qu’une boucle : le modèle est construit une fois, et le moteur découpe lui-même en phrases. Les balises restent à leur place quand on le demande.

const t = OdersaTranslate.traducteur({ site: "https://translate.odersa.org" });

await t.traduire(["Bonjour.", "Merci beaucoup."], "fr", "en");
// { texte: ["Hello.", "Thank you very much."], chemin: ["fr", "en"], … }

await t.traduire("<p>Bonjour <b>le monde</b>.</p>", "fr", "en", { html: true });
// { texte: "<p>Hello <b>the world</b>.</p>", … }

Les langues, et le pivot par l’anglais

Le SDK sert 112 paires de modèles, soit 59 langues. La liste ne se recopie pas ici : elle se lit au registre, qui est le seul endroit où elle est vraie.

const t = await import("https://translate.odersa.org/api/v2/client/sdk.js");

await t.langues();   // les codes servis, lus au registre
// Ou, sans le SDK du tout :
await (await fetch("https://translate.odersa.org/modeles/registre.json")).json();

L’anglais est le pivot, et il n’y a aucune paire directe entre deux langues tierces. Une traduction du français vers le bengali passe donc par deux modèles, et la qualité s’en ressent : ce qui est perdu au premier saut ne se retrouve pas au second. Le SDK ne le cache pas, il le nomme dans chemin et dans pivot, pour qu’un appelant puisse décider de prévenir ses lecteurs.

Le pivot passe par la fonction que le moteur expose pour cela, jamais par deux appels enchaînés. L’enchaînement manuel repasse par une chaîne intermédiaire et perd le découpage en phrases ; le pivot du moteur garde l’alignement d’un bout à l’autre.

Quatre langues ne vont que vers l’anglais et pas dans l’autre sens. Ce n’est pas un oubli de notre part : c’est ce que publie Mozilla, dont viennent ces modèles, et nous n’inventons pas un modèle qui n’existe pas. Une direction impossible est refusée par une phrase qui dit laquelle, plutôt que par un résultat vide.

await t.chemin("fr", "bn");
// ["fr", "en", "bn"]

await t.chemin("fr", "af");
// ODERSA Translate : il n'existe pas de modèle « en-af ». Cette direction
// n'est pas publiée par Mozilla et nous n'en inventons pas.

Le service de traduction du moteur est synchrone : il tient le fil d’exécution jusqu’au bout du calcul. Appelé depuis le fil d’une page, il gèle l’onglet pendant la construction du modèle comme pendant la traduction. Le SDK met donc le moteur dans un worker, sans exception, et la page ne lui parle que par messages.

Ce worker naît d’une adresse blob:, et ce n’est pas un détour : un worker ne se construit pas directement sur une adresse d’une autre origine. Le blob porte l’origine de votre page, et c’est de là qu’il va chercher notre fichier, notre moteur et nos modèles. Une page qui porte une politique de sécurité de contenu stricte doit donc l’autoriser.

Content-Security-Policy:
  worker-src blob: ;
  script-src 'self' https://translate.odersa.org ;
  connect-src 'self' https://translate.odersa.org

Le wasm-unsafe-eval est réclamé par tout chargement de WebAssembly, quel qu’il soit. Le moteur, lui, est compilé sans exécution dynamique : ni eval, ni new Function. Il n’a pas non plus besoin des en-têtes d’isolation d’origine, parce que sa mémoire n’est pas partagée : il tourne sur de l’hébergement statique nu.

Si votre politique interdit les workers de blob et que vous préférez assumer le gel, { fil: "principal" } le dit explicitement. Le SDK obéit et ne le fait jamais dans votre dos.

Ce qu’il ne sait pas faire

Une page qui ne dit que du bien de son sujet ne se croit pas. Voici ce que ce SDK ne fait pas, et n’a pas l’intention de faire.

Les limites, dites franchement
Ce qui manquePourquoi
Un appel par le réseau Il n’y a aucune adresse à qui envoyer du texte pour recevoir une traduction, et il n’y en aura pas : ce serait la fin de la promesse et la fin de l’illimité.
Un score de confiance Le moteur accepte de calculer une estimation de qualité, mais ce binaire n’expose aucun moyen de la lire. La demander ne servirait à rien, donc nous ne la demandons pas.
Une traduction publiable sans relecture C’est une sortie de machine. Elle est étiquetée comme telle, et ce projet existe précisément contre la traduction automatique publiée sans relecture.
Un poids léger Le premier appel sur une direction télécharge un modèle. Il n’existe pas de version allégée : ce qui rend la promesse vraie est exactement ce qui la rend lourde.
Un très vieux navigateur Le moteur exige les instructions vectorielles de WebAssembly, présentes dans tous les navigateurs courants depuis plusieurs années. Un navigateur trop ancien échoue au chargement, et le dit.

Une panne du moteur ne remonte jamais telle quelle. Ce qui sort du SDK est une phrase que nous avons écrite, avec une porte de sortie : la trace technique va à la console, où un développeur ira la lire, et nulle part ailleurs.

Appelable depuis n’importe quel domaine

Sans en-tête d’accès, une API publique n’est pas publique : un navigateur tiers refuse de lire nos fichiers, et la promesse est fausse. Le SDK, le moteur et les modèles répondent donc Access-Control-Allow-Origin: *, avec GET, HEAD et OPTIONS pour méthodes. Sans cela, le worker créé dans la page d’un tiers ne pourrait charger ni notre fichier ni nos modèles, et le SDK ne fonctionnerait que sur nos propres pages.

Cette ouverture ne donne rien d’autre : il n’y a ni session, ni cookie, ni compte, et le SDK demande explicitement qu’aucune information d’identification ne soit envoyée. Ces en-têtes sont écrits à la source, dans le gabarit du build, et reposés à chaque passage : un fichier d’en-têtes déposé à la main dans un dossier de sortie disparaît à la première régénération, sans que personne le voie partir.

La mémoire de phrases, à côté

Le même fichier ouvre une seconde porte, et il faut la présenter pour ce qu’elle est : ce n’est pas la traduction, c’est une pièce des données ouvertes. La mémoire porte ce que la maison a déjà traduit et relu, avec sa provenance et ses trous nommés. On y vient pour ne pas refaire un travail humain déjà fait, jamais pour traduire.

const M = OdersaTranslate.ouvrir({ base: "https://translate.odersa.org/api/v2" });

const [r] = await M.chercher([{ source: "Menu", contexte: "nav" }], { langues: ["en", "ar"] });
r.correspondance;    // "exacte"
r.contexte_trouve;   // "nav" : d'où vient ce qu'on vous rend
r.traductions;       // { en: "Menu", ar: "…" }

Elle compte 674 entrées en 7 langues, et ces sept langues ne sont pas celles du moteur : le moteur en sert bien plus, la mémoire est celle de l’atelier. Elle ne devine rien et n’invente rien : ce qu’elle ne connaît pas, elle le nomme dans un bon de travail plutôt que de le combler.

Tout ce qui la concerne, la normalisation, les empreintes, les seaux, le format des réponses, ce qu’un appel laisse sortir et les deux exports ouverts, est sur la page de la mémoire et détaillé appel par appel sur le fichier client. Le manifeste /api/v2/manifeste.json reste ce qui fait foi, jamais une page.

La version, et ce qui ne bougera pas

Le v2 dans l’adresse fait partie du contrat, au même titre que le format des fichiers ou le calcul des empreintes : il ne bouge pas en silence. Une seule version est servie, et c’est vérifié plutôt que supposé : cet endpoint n’a jamais été mis en ligne, donc aucun appelant extérieur ne dépend d’une version antérieure.

Ce qui ne bougera pas, quoi qu’il arrive : le calcul reste chez l’appelant. Une porte en HTTP où nous recevrions le texte ne serait pas une fonctionnalité de plus, ce serait un autre projet.

Le fichier de calcul vérifie lui-même son accord avec le manifeste à chaque lecture : si la version d’API ou la version de normalisation qu’il porte diverge de celle que le manifeste annonce, il refuse de répondre plutôt que de rendre des empreintes qui ne trouveraient rien.