en-GB vs en_GB : trait d’union ou underscore dans les codes de locale ?

La norme dit trait d’union, la moitié de votre chaîne d’outils dit underscore. Voici le format attendu par chaque écosystème, pourquoi cette scission existe, et comment passer de l’un à l’autre sans rien casser.

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.codesetfr_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 Locale et les recherches ResourceBundle utilisent messages_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 ?

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 :

ruby
# 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 :

  1. Le préfixe de région d’Android. fr-CA devient le répertoire values-fr-rCA, pas values-fr-CA. Le r minuscule est un séparateur inventé par Android pour sa propre syntaxe de qualificateur de ressource.
  2. Le suffixe de jeu de caractères. Les noms de locale POSIX peuvent porter un encodage — fr_CA.UTF-8 ou fr_CA@euro. Retirez tout ce qui suit le . ou le @ avant de traiter la valeur comme une balise de langue.
  3. Le repli de casse à l’entrée. FR-ca est 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 :

ruby
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.

Le paramètre de projet pour les balises de langue avec trait d’union

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

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.