Réponse courte : écrivez en-GB avec un trait d’union. BCP 47 — la norme IETF qui définit les balises de langue, et celle que suivent HTML, HTTP et toutes les API des navigateurs — sépare les sous-balises par un trait d’union. L’underscore de en_GB vient de POSIX et de Java, plus anciens que la norme et qui ne l’ont jamais adoptée.
Les deux notations sont bien vivantes et aucune ne va disparaître ; la question utile n’est donc pas « laquelle est correcte » mais « laquelle ce fichier, cette API ou ce framework attend-il ». Ce guide y répond, et montre comment passer de l’une à l’autre sans casser vos fichiers de langue.
De quoi se compose vraiment une balise de langue
Une balise de langue est une séquence de sous-balises, du plus général au plus spécifique :
language[-Script][-REGION][-variant]
| Sous-balise | Norme | Convention | Exemples |
|---|---|---|---|
| Langue | ISO 639-1, sinon 639-2/3 | minuscules | en, fr, zh, fil |
| Script | ISO 15924 | casse de titre, quatre lettres | Latn, Cyrl, Arab, Hans |
| Région | ISO 3166-1 alpha-2, ou UN M.49 | MAJUSCULES | GB, CA, BR, 419 |
| Variante | registre IANA | minuscules | valencia, 1901 |
Ainsi, sr-Latn-RS désigne le serbe, écrit en alphabet latin, tel qu’utilisé en Serbie. es-419 désigne l’espagnol pour l’Amérique latine, avec un code de région ONU plutôt qu’un pays. zh-Hans désigne le chinois simplifié sans aucune région — ce qui est généralement ce que vous voulez, car le script compte plus que le pays.
Le passage pertinent de la RFC 3066, le prédécesseur de BCP 47, est explicite sur le séparateur :
The syntax of this tag in ABNF RFC 2234 is :
Language-Tag = Primary-subtag *( "-" Subtag )The character “-” is HYPHEN-MINUS (ABNF : %x2D).
Elle est tout aussi explicite sur le fait que les balises sont insensibles à la casse, et que les conventions de capitalisation ci-dessus sont des recommandations, pas des règles — ISO 3166 recommande des codes de pays en majuscules, ISO 639 recommande des codes de langue en minuscules. Suivez-les tout de même : beaucoup de logiciels qui lisent vos fichiers sont plus stricts que la spécification qu’ils prétendent implémenter.
Alors, d’où vient l’underscore ?
L’underscore précède BCP 47 de plus d’une décennie. Les locales POSIX sont nommées language_TERRITORY.codeset — fr_CA.UTF-8 — et cette convention de nommage a été reprise en bloc par :
- GNU gettext, dont les catalogues de messages se trouvent dans
locale/fr_CA/LC_MESSAGES/messages.mo - Java, dont la classe
Localeet les recherchesResourceBundleutilisentmessages_fr_CA.properties - Ruby on Rails et Python, dont les écosystèmes ont repris la convention des deux précédents
Rien de tout cela n’est faux, c’est simplement un espace de noms différent. L’underscore n’a jamais été un séparateur de balise ; c’était un séparateur de nom de fichier qui se trouvait contenir une balise. La confusion commence quand un code de locale passe d’un nom de fichier à un en-tête HTTP, ou d’une API JSON à un attribut <html lang>, sans être converti.
Quel format chaque écosystème attend-il ?
| Où | Forme attendue | Exemple |
|---|---|---|
Attribut HTML lang |
trait d’union | <html lang="pt-BR"> |
Accept-Language / Content-Language HTTP |
trait d’union | Accept-Language: fr-CA, fr;q=0.9 |
| URLs et segments de chemin d’URL | trait d’union | /fr-ca/pricing |
Intl JavaScript et toLocaleString |
trait d’union | new Intl.NumberFormat('de-CH') |
| Recherches de données CLDR et ICU | les deux (ICU normalise _ en -) |
fr_CA et fr-CA résolvent tous les deux |
| Noms de fichiers Unicode CLDR | underscore | fr_CA.xml |
Locale / ResourceBundle Java |
underscore | messages_fr_CA.properties |
| Répertoires GNU gettext | underscore | locale/fr_CA/LC_MESSAGES/ |
Fichiers de locale Rails I18n |
les deux, le trait d’union est idiomatique | config/locales/fr-CA.yml |
| Répertoires de ressources Android | trait d’union, avec un r avant la région |
res/values-fr-rCA/strings.xml |
Bundles .lproj iOS / macOS |
trait d’union (l’underscore fonctionne aussi) | fr-CA.lproj/Localizable.strings |
CultureInfo .NET |
trait d’union | new CultureInfo("fr-CA") |
LANGUAGE_CODE Django |
trait d’union, région en minuscules | LANGUAGE_CODE = "fr-ca" |
| Répertoires de locale Django | underscore | locale/fr_CA/LC_MESSAGES/ |
Django est le cas révélateur : le paramètre utilise une balise en minuscules avec trait d’union, et le répertoire utilise une version avec underscore, dans le même projet, et ce par conception. Ce n’est pas un bug de Django ; c’est la couture entre la norme orientée web et le système de fichiers orienté POSIX, à l’endroit où elle se trouve habituellement.
Convertir de l’un à l’autre
Comme le séparateur ne porte aucun sens, convertir la balise elle-même n’est qu’un échange de caractère :
# balise → POSIX
"fr-CA".tr("-", "_") # => "fr_CA"
# POSIX → balise
"fr_CA".tr("_", "-") # => "fr-CA"
Ce que vous ne devez pas faire, c’est supposer que le reste de la convention de nommage se convertit avec elle. Trois pièges :
- Le préfixe de région d’Android.
fr-CAdevient le répertoirevalues-fr-rCA, pasvalues-fr-CA. Lerminuscule est un séparateur inventé par Android pour sa propre syntaxe de qualificateur de ressource. - Le suffixe de jeu de caractères. Les noms de locale POSIX peuvent porter un encodage —
fr_CA.UTF-8oufr_CA@euro. Retirez tout ce qui suit le.ou le@avant de traiter la valeur comme une balise de langue. - Le repli de casse à l’entrée.
FR-caest une balise valide, mais elle échappera à une recherche par hachage sensible à la casse ou à un système de fichiers sensible à la casse. Normalisez vers la casse conventionnelle à la frontière, une seule fois, plutôt que défensivement à chaque point d’appel.
Un normaliseur sûr fait les trois :
def normalize_tag(value)
language, *subtags = value.to_s.split(/[.@]/).first.tr("_", "-").split("-")
[language.downcase, *subtags.map { |subtag|
case subtag.length
when 4 then subtag.capitalize # script: Latn
else subtag.upcase # région: GB, 419
end
}].join("-")
end
normalize_tag("fr_ca.UTF-8") # => "fr-CA"
normalize_tag("sr_latn_rs") # => "sr-Latn-RS"
Cinq codes de locale régulièrement mal utilisés
Le séparateur, c’est la question que tout le monde pose. Voici celles qui leur coûtent vraiment une mise en production.
zh-CN quand ils veulent dire zh-Hans. Le chinois se divise par script, pas par pays : simplifié contre traditionnel. zh-CN signifie « chinois tel qu’utilisé en Chine continentale », ce qui suggère le simplifié sans le dire, et laisse de côté les lecteurs du simplifié à Singapour et en Malaisie. zh-Hans et zh-Hant disent ce que vous voulez dire. N’utilisez la forme régionale que lorsque quelque chose diffère réellement selon le pays, au-delà du script.
pt-BR et pt-PT traités comme interchangeables. Ils ne le sont pas, et le portugais brésilien est un marché plus grand d’un ordre de grandeur. Livrer pt seul en espérant que ça passe est une décision, pas un choix par défaut — décidez vers quelle variante pt doit résoudre.
en-UK. Ce code de région n’existe pas. Le code ISO 3166 du Royaume-Uni est GB ; UK est réservé et ne correspondra à rien. La balise correcte est en-GB.
he contre iw, id contre in. L’ISO a renommé l’hébreu, l’indonésien, le yiddish et le javanais il y a des décennies, mais Java a gelé les anciens codes pour des raisons de compatibilité et renvoie toujours iw depuis Locale("he").getLanguage(). Tout code qui compare des chaînes de locale à travers une frontière JVM doit normaliser explicitement ces alias.
Des codes de région utilisés comme codes de langue. de-AT est une balise de langue ; AT seul est un pays, pas une langue. Cela se manifeste dans des interfaces de sélection de pays branchées directement sur une recherche de locale, et le symptôme, c’est un utilisateur en Autriche qui se retrouve avec l’anglais.
Pourquoi cela compte plus dans un flux de traduction qu’il n’y paraît
À l’intérieur de votre application, le séparateur est cosmétique. À l’intérieur d’un flux de traduction, il détermine si deux choses sont la même locale.
Si votre projet iOS produit fr-CA.lproj, votre backend Rails produit fr_CA.yml, et votre front-end web demande fr-ca, alors un système qui compare les codes de locale comme de simples chaînes de caractères voit trois langues différentes. Vous obtenez trois jeux de traductions, trois mémoires de traduction, et des traducteurs qui font le même travail trois fois.
La solution consiste à normaliser une seule fois, au point où les fichiers entrent dans le système, et à être délibéré sur la forme qui en ressort. Dans WebTranslateIt, c’est un paramètre par projet : les locales sont stockées de façon canonique, et un seul interrupteur décide si les noms de fichiers générés, les URLs d’API et le code de langue écrit à l’intérieur de certains formats de fichiers utilisent des traits d’union ou des underscores. Réglez-le pour correspondre à la chaîne d’outils qui consomme les fichiers, pas à celle qui les produit.

Une règle de base
- Stockez et comparez les locales sous la forme BCP 47 avec trait d’union. C’est la norme, c’est ce que parle la plateforme web, et c’est sans ambiguïté.
- Convertissez vers les underscores à la frontière du système de fichiers, pour les outils spécifiques qui l’exigent.
- Ne laissez jamais une chaîne de locale brute et non normalisée, issue d’une saisie utilisateur, d’un nom de fichier ou d’une API tierce, atteindre vos tables de recherche.
Réglez cela correctement une fois, et la question en-GB contre en_GB cesse d’être un bug récurrent pour devenir ce qu’elle devrait être : un détail de formatage à la frontière de votre système.
Frequently asked questions
- en-GB ou en_GB : lequel est correct ?
- en-GB est correct selon BCP 47, la norme IETF pour les balises de langue, qui impose un trait d’union entre les sous-balises. en_GB est une convention POSIX et Java antérieure à cette norme et qui reste en dehors d’elle. Les deux formes sont largement utilisées ; le trait d’union est celui à écrire partout où la valeur est exposée sur le web, comme un attribut HTML lang, une URL ou un en-tête HTTP.
- La casse a-t-elle une importance dans un code de locale ?
- BCP 47 déclare les balises de langue insensibles à la casse, donc en-gb et EN-GB sont techniquement la même balise. Les conventions restent utiles à suivre : langue en minuscules (en), script en casse de titre (Latn), région en majuscules (GB). En pratique, de nombreux analyseurs, chargeurs de fichiers et recherches basées sur des répertoires sont sensibles à la casse, même si la norme ne l’est pas.
- Puis-je simplement remplacer les underscores par des traits d’union partout ?
- Pas sans réfléchir. Le caractère séparateur ne porte aucun sens, donc la conversion est sans perte pour la balise elle-même, mais le nom du fichier, du répertoire ou de la clé de ressource est souvent, lui, porteur de sens pour le chargeur qui le lit. Java attend fr_CA.properties, Android attend values-fr-rCA, et gettext attend fr_CA/LC_MESSAGES. Convertissez la balise, pas la convention de nommage des fichiers qui l’entoure.
- Quelle est la différence entre un code de langue et un code de locale ?
- Un code de langue identifie la langue seule (fr). Un code de locale ajoute les sous-balises de région, de script ou de variante qui changent la façon dont la langue s’écrit ou se formate (fr-CA, sr-Latn, es-419). Une locale, c’est ce que vous livrez réellement, car les formats de nombres, les formats de date et le vocabulaire varient tous selon la région.
Keep reading
-
Règles de pluriel par langue : le guide CLDR complet
Un tableau de référence des catégories de pluriel CLDR pour 163 langues, plus pourquoi one ne veut pas dire 1 et comment chaque framework i18n attend que vous écriviez les règles.
-
Aide-mémoire des formats de placeholders : %s, %@, %{name}, {{name}}, {0}
Toutes les syntaxes de placeholder que vous rencontrerez, quel langage ou framework les utilise, et ce qui casse quand un traducteur en retape une à la main.
-
Codes de locale, balises de langue et règles de pluriel (documentation)
Comment WebTranslateIt construit les locales, et le paramètre de projet qui bascule les noms de fichiers entre traits d’union et underscores.
Translate your app without the spreadsheet round-trip
WebTranslateIt reads the file formats and placeholder syntax described on this page, validates them as translators work, and syncs the results straight back into your repository.