Retour au Centre de connaissances
Empreinte

Intl.Collator : l’ordre de locale face aux unités de code

Une sonde sur listes fixes : en quoi une comparaison de locale diffère d’un tri par unités de code, comment les options changent le résultat et ce que rapporte resolvedOptions.

BotBrowser Team

Documentation

Vous voulez la documentation structurée pour Empreinte ?

Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.

BotBrowser prend en charge la relecture d’un fixture autorisé du même origine sous un profil déclaré et conserve un relevé borné de ce qu’une page a trié et dans quelle locale, mais il ne peut pas changer les données de collation qu’un environnement embarque, ne peut pas faire tomber deux locales d’accord sur un même ordre, et ne peut pas reconstituer après coup la collation qu’une comparaison a utilisée. Cette revue exécute une sonde sur dix listes fixes et consigne en quoi une comparaison de locale diffère d’un simple tri par unités de code, comment numeric, sensitivity, caseFirst et ignorePunctuation changent chacun le résultat, ce que resolvedOptions() rapporte réellement, vers quoi retombe une locale non prise en charge, et pourquoi passer le collateur plutôt que sa fonction compare lève une erreur.

Résumé

  • Un tri par unités de code place Ápple en dernier, alors qu’un tri de locale le place en troisième, donc les deux ordres divergent sur les cinq mêmes mots.
  • La locale change l’ordre de la même paire : le suédois compare ä après z, l’allemand et l’anglais le comparent avant, donc la sonde imprime pair-sv -> z,ä et pair-de -> ä,z.
  • numeric: true ordonne item2 avant item10, alors que le comportement par défaut les traite comme du texte et place item10 avant item2.
  • sensitivity décide quelles différences comptent : base traite a, A et á comme égaux, accent sépare la forme accentuée, et case sépare la majuscule.
  • ignorePunctuation: true fait comparer ab et a-b comme égaux, alors que le comportement par défaut les compare comme distincts.
  • caseFirst ne se manifeste qu’une fois la casse prise en compte, et il inverse AaBb en aAbB.
  • resolvedOptions() rapporte la collation que l’environnement a accordée, y compris une collation nommée dans l’étiquette de locale, et une locale non prise en charge retombe sur en-US plutôt que de lever une erreur.
  • Array.prototype.sort(collator) lève une TypeError ; seul sort(collator.compare) fonctionne.

Sommaire

Quatre panneaux : cinq mots triés par unité de code, mot accentué en dernier, contre un collateur anglais qui le met troisième ; la même paire en suédois (z puis ä) et en allemand (ä puis z) ; numeric désactivé donnant item1, item10, item2 et activé donnant item1, item2, item10 ; les quatre niveaux de sensitivity et ce que chacun tient pour égal.

Ce qu’un collateur compare et qu’un tri par unités de code ignore

Un Array.prototype.sort() par défaut compare des unités de code UTF-16. Un collateur compare le texte comme une langue l’ordonne : les accents se replient sur leur lettre de base, la casse est une différence secondaire, et chaque locale décide où se place une lettre. La sonde trie les cinq mêmes mots des deux façons :

node=v26.9.0
icu=78.3 unicode=17.0
code-unit -> Apple,apple,banana,zebra,Ápple
locale-en -> apple,Apple,Ápple,banana,zebra
pair-sv -> z,ä
pair-de -> ä,z
pair-en -> ä,z
numeric-off -> item1,item10,item2
numeric-on -> item1,item2,item10
sensitivity-base -> a-vs-A=0 a-vs-á=0 a-vs-b=-1
sensitivity-accent -> a-vs-A=0 a-vs-á=-1 a-vs-b=-1
sensitivity-case -> a-vs-A=-1 a-vs-á=0 a-vs-b=-1
sensitivity-variant -> a-vs-A=-1 a-vs-á=-1 a-vs-b=-1
punct-kept -> 1
punct-ignored -> 0
caseFirst-upper -> AaBb
caseFirst-lower -> aAbB
caseFirst-upper-base -> Aa vs aA
resolved -> locale=de-u-co-phonebk numeric=true sensitivity=accent collation=phonebk
fallback-locale -> en-US
passthrough -> sort(collator)=TypeError sort(collator.compare)=ab
tie-check -> sv(ä vs z)=1 de(ä vs z)=-1

La différence avec un tri simple est celle que Tri de tableau et comparateurs trace pour le comparateur lui-même. Les deux premières lignes sont tout le propos en miniature. Ápple a une unité de code supérieure à z, donc le tri par unités de code le pousse à la fin ; une langue voit Á comme un A accentué, donc le tri de locale le place avec les autres mots en a. Aucun des deux ordres n’est faux, et une page qui compare une liste entre deux environnements sans nommer le second a consigné un résultat qui dépend du tri employé.

La deuxième paire est le cas plus difficile. ä n’est pas seulement un a accentué en suédois ; c’est une lettre à part entière, et il trie après z. L’allemand le garde près de a. La sonde imprime les deux ordres côte à côte, et une page qui code en dur l’un ou l’autre comme l’ordre unique casse sur l’autre langue. La ligne tie-check montre la même chose comme résultat de comparaison plutôt que comme liste triée : la même paire renvoie 1 en suédois et -1 en allemand.

Comment numeric et sensitivity changent le résultat

numeric: true fait passer une suite de chiffres d’une comparaison textuelle à une comparaison numérique, et la sonde montre la différence sur trois entrées de liste :

numeric-off -> item1,item10,item2
numeric-on -> item1,item2,item10

Le comportement par défaut lit item10 comme les caractères « 1 », « 0 », donc il vient avant item2 comme « 2 ». Avec numeric, l’environnement lit la suite de chiffres comme le nombre dix, donc item2 vient en premier. Une page qui trie des noms de fichiers ou des lignes de tableau avec un collateur par défaut et attend l’ordre naturel obtient la première ligne, pas la seconde.

sensitivity décide quelles différences la comparaison rapporte du tout, et la sonde compare trois paires sous chacun des quatre niveaux :

sensitivity-base -> a-vs-A=0 a-vs-á=0 a-vs-b=-1
sensitivity-accent -> a-vs-A=0 a-vs-á=-1 a-vs-b=-1
sensitivity-case -> a-vs-A=-1 a-vs-á=0 a-vs-b=-1
sensitivity-variant -> a-vs-A=-1 a-vs-á=-1 a-vs-b=-1

base rapporte 0 pour la paire de casse et pour la paire d’accent, donc a, A et á sont tous égaux et seul b trie à part. accent commence à séparer la forme accentuée mais traite encore a et A comme égaux. case fait l’inverse : il sépare la majuscule et replie à nouveau l’accent. variant sépare tout. Une table de correspondance indexée par une chaîne passée en minuscules et privée de ses accents fait à la main ce que sensitivity: 'base' fait en une option, et une table qui veut que A et a se confondent mais que á trie séparément veut accent.

La distinction compte pour une question d’empreinte autant que pour un tri. Une page qui regroupe des chaînes par sensitivity: 'base' et une page qui regroupe par variant produisent des nombres de groupes différents à partir de la même entrée, donc une lecture « nombre de valeurs distinctes » doit nommer la sensitivity, sinon elle n’est pas comparable entre deux pages.

Il y a un piège de second ordre dans la même table. sensitivity: 'base' rapporte 0 pour a contre á, et une page qui traite ce 0 comme « ces chaînes sont la même valeur » fusionnera deux entrées qu’un utilisateur juge distinctes. Les comparaisons rapportent un ordre, pas une égalité : 0 signifie que le collateur n’a pas d’avis sur laquelle vient en premier, et un Map indexé par les chaînes d’origine contient toujours les deux. Un dédoublonnage doit décider séparément si un ex æquo signifie « garder le premier », « garder les deux » ou « fusionner », et le collateur ne répond pas à cette question.

Les quatre niveaux s’emboîtent aussi dans un ordre fixe, et connaître cet ordre prédit chaque ligne. base ignore les accents et la casse ; accent ajoute la distinction d’accent ; case ajoute plutôt la distinction de casse et garde l’accent replié ; variant ajoute les deux. C’est pourquoi accent et case divergent sur des paires différentes au lieu que l’un soit une version plus stricte de l’autre, et pourquoi une page qui veut « même mot, n’importe quelle casse, n’importe quel accent » veut base plutôt qu’une chaîne de normalisations manuelles.

Où la casse, la ponctuation, l’équivalence et la casse liée à la locale entrent dans l’ordre

ignorePunctuation fait ignorer la ponctuation au collateur quand il compare, et la sonde montre l’effet sur une paire :

punct-kept -> 1
punct-ignored -> 0

ab et a-b diffèrent quand la ponctuation compte, et sont égaux quand elle ne compte pas. Une page qui trie une liste de noms avec traits d’union et attend que ab et a-b restent distincts s’appuie sur le comportement par défaut, et une page qui dédoublonne avec un collateur dont la ponctuation est ignorée les fusionnera. L’option est un choix de politique, pas un détail.

La même option change ce qu’une zone de recherche considère comme une correspondance. Un filtre qui passe la requête en minuscules et compare avec un collateur par défaut garde a-b et ab distincts, donc taper ab ne trouve rien dans une liste qui ne contient que a-b. Désactiver la ponctuation fait correspondre la requête, et la désactiver fait aussi correspondre a.b et a-b l’un à l’autre, ce qui est généralement ce qu’un lecteur attend d’un champ de recherche et rarement ce qu’un filtre d’exactitude veut. Une option, deux produits, des comportements par défaut opposés.

caseFirst fixe quelle casse vient en premier une fois que la casse est une différence, et la sonde montre à la fois l’effet et sa limite :

caseFirst-upper -> AaBb
caseFirst-lower -> aAbB
caseFirst-upper-base -> Aa vs aA

Avec la sensitivity par défaut, upper place la majuscule avant la minuscule dans chaque paire et lower l’inverse. La troisième ligne est la limite : sous sensitivity: 'base' les deux casses sont égales, donc caseFirst n’a rien à ordonner, et le résultat imprimé est simplement l’ordre d’entrée dans chaque cas. Une option sans effet tant qu’une autre option n’est pas réglée est facile à lire à tort comme cassée, et la sonde est le moyen le moins coûteux de voir qu’elle ne l’est pas.

Cette dépendance mérite d’être posée comme une règle : caseFirst est un départage à l’intérieur d’une comparaison qui distingue déjà la casse, et il ne fait rien au-dessus de ce niveau. Une page qui règle caseFirst: 'upper' et sensitivity: 'base' et attend les majuscules en premier a réglé deux options qui s’annulent, et le correctif est de retirer la dérogation de sensitivity plutôt que d’ajouter une comparaison manuelle. La dernière ligne de la sonde montre les deux ordres d’entrée survivre intacts, ce qui est la forme visible de l’annulation.

Où l’équivalence canonique et la casse liée à la locale surprennent un tri

Deux des comportements les plus déroutants ne portent pas du tout sur les options ; ils viennent de la façon dont le collateur traite un texte qui se ressemble et un texte dont la casse dépend d’une langue. Une seconde sonde consigne les deux.

Le premier est l’équivalence canonique. La lettre é peut être stockée comme un seul point de code ou comme e suivi d’un accent combinant, et les deux chaînes ne sont pas égales :

normalization-text -> nfc.length=1 nfd.length=2 equal=false
normalization-collate -> base=0 variant=0

Les longueurs diffèrent et une comparaison de chaînes dit qu’elles sont différentes, pourtant le collateur rapporte 0 à la fois en base et même en sensitivity variant, parce que les deux graphies sont canoniquement équivalentes et que les données de collation les replient ensemble. Une page qui compare avec un collateur traite donc deux chaînes différentes en octets comme le même mot, tandis qu’une page qui compare avec === ou un Map ne le fait pas. Cette scission est la source la plus courante de « ça trie bien mais la recherche rate » : le tri a utilisé le collateur, la recherche a utilisé la chaîne brute.

Le premier explique aussi pourquoi une comparaison par collation et une comparaison de texte divergent, ce que Normalisation Unicode traite du côté de la chaîne. Le second est la casse liée à la locale. En turc, I passe en minuscule en ı sans point, et i passe en majuscule en İ pointu, qui est une lettre différente du I anglais :

dotted-i-tr -> i-vs-I=1 i-vs-İ=-1
dotted-i-en -> i-vs-I=-1 i-vs-İ=-1
turkish-lower -> tr=ı en=i

Sous le turc, le collateur place i après I et avant İ, donc i et I ne sont plus la même lettre ni en casse ni en ordre ; sous l’anglais, il place i avant les deux. La dernière ligne montre la même scission au niveau des chaînes : 'I'.toLocaleLowerCase('tr') vaut ı alors que l’appel anglais renvoie i. Une page qui passe en minuscules sans locale puis compare obtient l’ordre anglais dans les deux locales, c’est pourquoi une comparaison sensible à la locale et un repli de casse sensible à la locale doivent être choisis ensemble plutôt qu’un à la fois.

Il y a une troisième forme à consigner parce qu’elle brise l’hypothèse qu’un tri par unités de code est « assez proche ». Les caractères astraux, qui occupent deux unités de code, trient selon leur première unité de code sous un tri simple et selon leur poids de collation sous un collateur :

astral-codeunit -> 61,7a,1f600
astral-collator -> 1f600,61,7a

Le tri simple met l’emoji en dernier parce que son unité de tête est au-dessus de z ; le collateur le met en premier parce que son poids de collation précède les lettres. Une page qui mélange des caractères astraux dans une liste de noms et trie par défaut obtient un ordre qu’aucun lecteur n’attend, et le correctif est le collateur, pas un réordonnancement manuel.

Enfin, le préambule de tout cela est que localeCompare et un collateur sont d’accord, ce qui est le moyen le moins coûteux de confirmer une hypothèse avant de construire un collateur :

localeCompare-vs-collator -> ä-z:-1/-1 a-b:-1/-1
hoisted-vs-per-call -> same=true order=Alpha,bravo,charlie,delta

La première ligne montre 'ä'.localeCompare('z', 'de') et un collateur allemand renvoyant le même -1, donc une sonde peut commencer par la ligne unique et n’atteindre un collateur que lorsqu’elle a besoin d’options. La seconde ligne montre un collateur hissé et un collateur construit par comparaison produisant le même ordre, ce qui confirme l’affirmation antérieure que hisser change le coût plutôt que le résultat.

Ce que resolvedOptions rapporte et vers quoi il retombe

resolvedOptions() renvoie ce que l’environnement a réellement accordé, une fois l’étiquette de locale et les options toutes deux appliquées :

resolved -> locale=de-u-co-phonebk numeric=true sensitivity=accent collation=phonebk

Le collateur a été demandé pour de-u-co-phonebk avec deux options, et la locale résolue garde l’extension -u-co-phonebk et rapporte collation=phonebk séparément. Cette séparation compte parce qu’une page peut lire l’un ou l’autre champ et obtenir une chaîne différente, et qu’un relevé qui ne stocke que locale et abandonne collation a perdu la partie qui a décidé de l’ordre. La collation d’annuaire ordonne ä avec ae en allemand, ce qui est encore un troisième ordre, différent à la fois de l’allemand simple et du suédois que la section précédente a imprimés.

Une locale non prise en charge ne lève pas d’erreur. La sonde demande zz-ZZ :

fallback-locale -> en-US

L’environnement est retombé sur en-US et l’a dit. Une page qui lit la locale depuis un en-tête ou un paramètre de requête et la passe telle quelle dans un collateur obtient donc toujours un collateur, et seul resolvedOptions().locale révèle que la demande n’a pas été honorée. Un relevé qui stocke la locale demandée au lieu de la locale résolue a consigné une intention, pas une mesure.

Pourquoi on ne peut pas passer le collateur directement à sort

Un collateur est un objet doté d’une méthode compare, et Array.prototype.sort() accepte une fonction, pas un objet avec une méthode compatible. La sonde passe le collateur lui-même :

passthrough -> sort(collator)=TypeError sort(collator.compare)=ab

La première moitié lève une TypeError parce que l’argument n’est pas appelable ; la seconde trie correctement parce que collator.compare est une fonction liée. C’est un bogue d’un mot en revue de code et une exception d’exécution en production, et il vaut la peine de le nommer parce que le message de sort ne mentionne pas du tout les collateurs.

La même forme explique une erreur plus subtile. Un collateur créé une fois et réutilisé est plus qu’une fabrique de compare ; il détient la locale et les options, et son compare leur est lié. Une page qui crée un nouveau collateur par comparaison à l’intérieur d’un rappel de tri reconstruit l’état ICU des milliers de fois, ce qui est correct et lent, et une page qui hisse un collateur hors de la boucle obtient le même ordre plus vite. La sonde ne peut pas montrer la différence de chronométrage sur une liste de cinq éléments, et l’ordre est identique dans les deux cas, donc le choix porte uniquement sur le coût.

Consigner une lecture de collation

Une lecture de collation étaye une affirmation étroite : cet environnement, cette version d’ICU, cette locale et ces options, ont produit ces ordres. Cinq champs la gardent vérifiable :

  1. L’environnement et sa version d’ICU, parce que la sonde imprime icu=78.3 unicode=17.0 et qu’un autre ICU embarque d’autres données de collation.
  2. La locale demandée et la locale résolue, parce qu’une étiquette non prise en charge retombe en silence.
  3. La collation résolue, parce qu’une extension -u-co- change l’ordre et est rapportée séparément de la locale.
  4. Les quatre options qui changent le résultat, numeric, sensitivity, caseFirst et ignorePunctuation, parce que deux collateurs avec la même locale et des options différentes divergent.
  5. La comparaison employée, sort() ou collator.compare, parce que passer l’objet lève une erreur.

Sources publiques

Conclusion pratique

Nommez le collateur chaque fois qu’un ordre quitte une page : la locale, la collation résolue et les quatre options, parce que chacune d’elles change le résultat sur la même entrée. Préférez sensitivity à un repli de casse et d’accent bricolé à la main, et traitez ignorePunctuation comme une décision de politique plutôt qu’un détail. Passez collator.compare à sort, jamais le collateur, et hissez un collateur hors de la boucle. Pour la comparaison numérique qu’un tri par défaut rate, lisez Tri de tableau et comparateurs ; pour les données de locale derrière l’ordre, lisez Données de locale de l’API Intl.

#Intl.Collator#localeCompare#ICU#Ordre De Tri#Collation Numérique

Cet article fait partie du guide : Qu'est-ce qu'une empreinte de navigateur ?

Faites passer BotBrowser de la recherche à la production

Utilisez ces guides pour comprendre le modèle, puis passez à la validation multi-plateforme, aux contextes isolés et au déploiement navigateur prêt pour l'échelle.