Les mots,
séparés de la structure
1681 textes dans un fichier plat, où la clé est le chemin du texte dans le document. Traduire, c'est copier le fichier et changer les valeurs : la structure est partagée, donc elle ne peut pas diverger d'une langue à l'autre.
Un texte par clé, à plat
site/textes/txt_fr.json contient 1681 textes, sans aucune imbrication. Pas d'objets dans des objets : une clé, une chaîne.
Cette platitude est un choix. Une structure imbriquée obligerait le traducteur à reproduire l'arborescence du document — et à la maintenir quand elle change. À plat, un simple diff de clés révèle instantanément ce qui manque dans une traduction.
{
"_langue": "fr",
"_nom": "Français",
"_aide": "Un texte par clé. La clé décrit le CHEMIN…",
"a11y.menu":
"Menu…",
"a11y.nav-principale":
"Navigation principale…",
"a11y.ouvrir-menu":
"Ouvrir le menu…",
…
}
Mesuré. Modifier un texte dans ce fichier change exactement une ligne du HTML produit. La mise en page n'est pas concernée — c'est la garantie que la séparation entre les mots et la structure est réelle.
La clé décrit le chemin dans le document
Une clé n'est pas un identifiant arbitraire : c'est l'adresse du texte dans l'arborescence de la page. Elle se lit de gauche à droite comme un chemin.
Cela a une conséquence pratique : on retrouve un texte sans chercher, et un agent IA peut construire la clé par déduction plutôt que par consultation.
accueil . concept . groupe-1 . etape-2 . titre
│ │ │ │ │
│ │ │ │ └─ la propriété
│ │ │ └────────── le 2ᵉ bloc étape
│ │ └───────────────────── le 1ᵉʳ groupe
│ └─────────────────────────────── la section « concept »
└───────────────────────────────────────── la page
Comment la structure appelle un texte
Les fichiers de site/structure/ ne contiennent aucun mot. Ils nomment des clés, préfixées d'un @.
{
"type": "etape",
"numero": "02",
"etiquette": "@accueil.concept.groupe-1.etape-2.etiquette",
"titre": "@accueil.concept.groupe-1.etape-2.titre",
"texte": "@accueil.concept.groupe-1.etape-2.texte",
"image": "etape-02"
}C'est ce qui permet de compiler la même structure en français et en allemand : seule la table de résolution change. La structure est partagée, donc elle ne peut pas diverger d'une langue à l'autre.
Ajouter une langue : déposer un fichier
Il n'y a pas de réglage à activer, pas de case à cocher, pas de menu d'administration. La présence du fichier est la déclaration.
Déposer txt_de.json puis txt_en.json dans site/textes/ a produit 42 pages en 3 langues, avec leurs URL préfixées, leurs liens hreflang réciproques, le sélecteur de langue et les entrées de sitemap. Aucune autre intervention.
$ cp site/textes/txt_fr.json site/textes/txt_de.json
$ # …traduire les valeurs, garder les clés…
$ ./verifier.sh
✓ de 13 pages 318 Ko
✓ fr 13 pages 315 Ko
26 pages en 0.043 s
Ce que ces balises font — et ne font pas
Trois croyances très répandues n'ont pas résisté à la vérification aux sources primaires. Le détail complet est sur la page Multilingue.
| Ce qu'on lit partout | Ce que dit la source primaire |
|---|---|
hreflang évite une pénalité de contenu dupliqué | Une page traduite n'est pas un doublon, par définition de Google. hreflang sert à choisir l'URL à afficher, pas à éviter une sanction |
<html lang> aide au référencement | Google le classe dans ses Unsupported tags and attributes, à côté de meta keywords. Il reste obligatoire pour l'accessibilité — WCAG 3.1.1, niveau A |
Déclarer hreflang en HTML et en sitemap est plus sûr | « There's no benefit in Search. » Les méthodes sont équivalentes ; il faut en choisir une seule |
Le convertisseur déclare donc hreflang en HTML uniquement, et pose lang pour l'accessibilité — jamais en le présentant comme un levier SEO.
Ce que le convertisseur produit automatiquement : les URL préfixées (/de/…), les liens hreflang réciproques entre toutes les versions, la balise x-default, l'attribut lang et le sens de lecture sur <html>, le sélecteur de langue dans la navigation et le pied de page, et les entrées de sitemap correspondantes.
Comment nommer un fichier de langue
Un seul motif, sans exception : txt_XX.json, où XX est le code de la langue tel qu'il figure au catalogue.
Le code suit la norme BCP 47 — celle qu'attend l'attribut lang du HTML. Deux formes sont admises : xx pour une langue, xx-XX pour une variante régionale.
site/textes/
txt_fr.json français ← langue maîtresse
txt_de.json allemand
txt_en.json anglais
txt_pt.json portugais ← il suffit de le déposer
txt_de-CH.json allemand (Suisse) ← variante régionale
txt_ar.json arabe ← sens de lecture inversé
| Forme | Exemple | Quand l'employer |
|---|---|---|
| txt_xx.json | txt_es.json | une langue, sans distinction de pays — le cas courant |
| txt_xx-XX.json | txt_de-CH.json | une variante régionale, quand le vocabulaire ou les prix diffèrent réellement d'un pays à l'autre |
La langue maîtresse est celle déclarée dans site/donnees/site.json (langue_principale). C'est elle qui sert de référence : toute autre langue est comparée à ses clés, et compilée sous une URL préfixée — /de/, /de-CH/. La maîtresse, elle, reste à la racine.
Ce que le moteur refuse à la lecture du catalogue
Trois champs conditionnent directement le HTML produit : code_html devient l'attribut lang, sens décide de dir, locale alimente og:locale.
Une valeur malformée ne casse rien de visible — elle produit du balisage faux, en silence. Le moteur vérifie donc le catalogue avant toute compilation :
- code non conforme à
xxouxx-XX - champ absent parmi les cinq
sensautre queltrourtllocalehors du formatxx_XX
Ce contrôle a trouvé un vrai défaut : les trois variantes suisses portaient une locale de-CH_DE-CH — produite par un repli automatique. Corrigées en de_CH.
Les 27 langues du catalogue
site/langues/catalogue.json décrit chaque langue disponible : son nom dans sa propre langue, son drapeau, son code HTML et son sens de lecture.
Le nom est un endonyme — « Deutsch », pas « Allemand ». C'est la règle en interface multilingue : un visiteur cherche le nom qu'il reconnaît, pas sa traduction dans une langue qu'il ne lit pas.
| Fichier à déposer | Code | Endonyme | Drapeau | Sens | og:locale |
|---|---|---|---|---|---|
| txt_ar.json | ar | العربية | 🇸🇦 | rtl | ar_AR |
| txt_bg.json | bg | Български | 🇧🇬 | ltr | bg_BG |
| txt_cs.json | cs | Čeština | 🇨🇿 | ltr | cs_CZ |
| txt_da.json | da | Dansk | 🇩🇰 | ltr | da_DK |
| txt_de.json | de | Deutsch | 🇩🇪 | ltr | de_CH |
| txt_de-CH.json | de-CH | Deutsch (Schweiz) | 🇨🇭 | ltr | de_CH |
| txt_el.json | el | Ελληνικά | 🇬🇷 | ltr | el_GR |
| txt_en.json | en | English | 🇬🇧 | ltr | en_GB |
| txt_es.json | es | Español | 🇪🇸 | ltr | es_ES |
| txt_fi.json | fi | Suomi | 🇫🇮 | ltr | fi_FI |
| txt_fr.json | fr | Français | 🇫🇷 | ltr | fr_CH |
| txt_fr-CH.json | fr-CH | Français (Suisse) | 🇨🇭 | ltr | fr_CH |
| txt_he.json | he | עברית | 🇮🇱 | rtl | he_IL |
| txt_hr.json | hr | Hrvatski | 🇭🇷 | ltr | hr_HR |
| txt_hu.json | hu | Magyar | 🇭🇺 | ltr | hu_HU |
| txt_it.json | it | Italiano | 🇮🇹 | ltr | it_CH |
| txt_it-CH.json | it-CH | Italiano (Svizzera) | 🇨🇭 | ltr | it_CH |
| txt_nl.json | nl | Nederlands | 🇳🇱 | ltr | nl_NL |
| txt_no.json | no | Norsk | 🇳🇴 | ltr | nb_NO |
| txt_pl.json | pl | Polski | 🇵🇱 | ltr | pl_PL |
| txt_pt.json | pt | Português | 🇵🇹 | ltr | pt_PT |
| txt_ro.json | ro | Română | 🇷🇴 | ltr | ro_RO |
| txt_ru.json | ru | Русский | 🇷🇺 | ltr | ru_RU |
| txt_sk.json | sk | Slovenčina | 🇸🇰 | ltr | sk_SK |
| txt_sv.json | sv | Svenska | 🇸🇪 | ltr | sv_SE |
| txt_tr.json | tr | Türkçe | 🇹🇷 | ltr | tr_TR |
| txt_uk.json | uk | Українська | 🇺🇦 | ltr | uk_UA |
Le sens de lecture, et sa limite honnête
Le champ sens vaut ltr ou rtl. Le convertisseur le pose sur <html dir>, et la feuille de style utilise des propriétés logiques (margin-inline-start plutôt que margin-left) pour que la mise en page se retourne d'elle-même.
La limite : ce mécanisme existe mais n'a jamais été testé avec une traduction arabe ou hébraïque réelle. Il est correct en théorie ; il n'est pas prouvé. C'est une limite connue, pas une garantie.
Le sélecteur de langue produit
Il est engendré automatiquement dès qu'une seconde langue existe : à droite de la navigation sur ordinateur, dans le pied de page sur mobile.
Trois décisions d'accessibilité y sont prises par construction.
<details class="lang">
<summary aria-label="Choisir la langue">
<span aria-hidden="true">🇫🇷</span>
<span>Français</span>
</summary>
<ul>
<li><a href="/de/" lang="de" hreflang="de">
<span aria-hidden="true">🇩🇪</span> Deutsch</a></li>
</ul>
</details>
Les trois décisions, et leur motif
- Un
<details>natif, pas du JavaScript. Le menu s'ouvre et se ferme au clavier sans une ligne de script, fonctionne si le JavaScript échoue, et est annoncé correctement par les lecteurs d'écran. - Le drapeau porte
aria-hidden. Un drapeau n'est pas une langue — il désigne un pays. Lu à voix haute, « drapeau de l'Allemagne » n'aide pas ; « Deutsch » si. Le drapeau reste visible, mais il est retiré de l'arbre d'accessibilité. - Chaque lien porte
langethreflang. Le premier fait prononcer « Deutsch » avec la phonétique allemande ; le second annonce la correspondance à Google.
Une traduction incomplète est refusée
Si une clé manque dans txt_de.json, le convertisseur ne remplit pas le trou avec le français. Il ne compile pas cette langue — et compile les autres normalement.
Le motif est de référencement autant que d'honnêteté : une page partiellement traduite est un signal de mauvaise qualité, et un mélange de deux langues sur une même page est pire qu'une page absente.
$ ./verifier.sh
⚠ de : traduction incomplète — 2,6 % couvert
1 527 clés manquantes, dont :
accueil.concept.chapo
accueil.concept.groupe-1.etape-1.titre
…
cette langue n'est pas compilée.
✓ fr 13 pages 315 Ko
Vérifié. Avec une traduction allemande couvrant 2,6 % des clés, l'allemand a bien été refusé et le français a compilé normalement. Le refus est par langue, jamais global : une traduction en chantier ne bloque pas la publication du site.