Comment diviser correctement un texte en caractères individuels ?
Utilisez Intl.Segmenter pour diviser les chaînes en caractères perçus par l'utilisateur plutôt qu'en unités de code
Introduction
Lorsque vous essayez de diviser l'emoji "👨👩👧👦" en caractères individuels à l'aide des méthodes de chaîne standard de JavaScript, vous obtenez un résultat incorrect. Au lieu d'un emoji de famille, vous voyez des emojis de personnes séparées et des caractères invisibles. Le même problème se produit avec les lettres accentuées comme "é", les emojis de drapeaux comme "🇺🇸" et de nombreux autres éléments de texte qui apparaissent comme des caractères uniques à l'écran.
Cela se produit parce que la division de chaîne intégrée de JavaScript traite les chaînes comme des séquences d'unités de code UTF-16 plutôt que comme des caractères perçus par l'utilisateur. Un seul caractère visible peut être constitué de plusieurs unités de code assemblées. Lorsque vous divisez par unités de code, vous séparez ces caractères.
JavaScript fournit l'API Intl.Segmenter pour gérer cela correctement. Cette leçon explique ce que sont les caractères perçus par l'utilisateur, pourquoi les méthodes de chaîne standard échouent à les diviser correctement, et comment utiliser Intl.Segmenter pour diviser le texte en caractères réels.
Que sont les caractères perçus par l'utilisateur
Un caractère perçu par l'utilisateur est ce qu'une personne reconnaît comme un caractère unique lors de la lecture d'un texte. Ceux-ci sont appelés groupes de graphèmes dans la terminologie Unicode. La plupart du temps, un groupe de graphèmes correspond à ce que vous voyez comme un caractère à l'écran.
La lettre "a" est un groupe de graphèmes constitué d'un point de code Unicode. L'emoji "😀" est un groupe de graphèmes constitué de deux points de code qui forment un seul emoji. L'emoji de famille "👨👩👧👦" est un groupe de graphèmes constitué de sept points de code assemblés avec des caractères invisibles spéciaux.
Lorsque vous comptez les caractères dans un texte, vous souhaitez compter les groupes de graphèmes, et non les points de code ou les unités de code. Lorsque vous divisez un texte en caractères, vous souhaitez diviser aux limites des groupes de graphèmes, et non à des positions arbitraires au sein d'un groupe.
Les chaînes JavaScript sont des séquences d'unités de code UTF-16. Chaque unité de code représente soit un point de code complet, soit une partie d'un point de code. Un groupe de graphèmes peut s'étendre sur plusieurs points de code, et chaque point de code peut s'étendre sur plusieurs unités de code. Cela crée un décalage entre la façon dont JavaScript stocke le texte et la façon dont les utilisateurs perçoivent le texte.
Pourquoi la méthode split échoue avec les caractères complexes
La méthode split('') divise une chaîne à chaque limite d'unité de code. Cela fonctionne correctement pour les caractères ASCII simples où chaque caractère correspond à une unité de code. Cela échoue pour les caractères qui s'étendent sur plusieurs unités de code.
const simple = "hello";
console.log(simple.split(''));
// Output: ["h", "e", "l", "l", "o"]
Le texte ASCII simple se divise correctement car chaque lettre est une unité de code. Cependant, les emojis et autres caractères complexes se fragmentent.
const emoji = "😀";
console.log(emoji.split(''));
// Output: ["\ud83d", "\ude00"]
L'emoji du visage souriant se compose de deux unités de code. La méthode split('') le divise en deux morceaux distincts qui ne sont pas des caractères valides en eux-mêmes. Lorsqu'ils sont affichés, ces morceaux apparaissent comme des caractères de remplacement ou rien du tout.
Les emojis de drapeaux utilisent des symboles d'indicateurs régionaux qui se combinent pour former des drapeaux. Chaque drapeau nécessite deux points de code.
const flag = "🇺🇸";
console.log(flag.split(''));
// Output: ["\ud83c", "\uddfa", "\ud83c", "\uddf8"]
L'emoji du drapeau américain se divise en quatre unités de code représentant deux indicateurs régionaux. Aucun indicateur n'est un caractère valide en lui-même. Vous avez besoin des deux indicateurs ensemble pour former le drapeau.
Les emojis de famille utilisent des caractères de jointure de largeur nulle pour combiner plusieurs emojis de personnes en un seul caractère composite.
const family = "👨👩👧👦";
console.log(family.split(''));
// Output: ["👨", "", "👩", "", "👧", "", "👦"]
L'emoji de famille se divise en emojis de personnes individuelles et en caractères de jointure invisibles. Le caractère composite d'origine est détruit, et vous voyez quatre personnes séparées au lieu d'une famille.
Les lettres accentuées peuvent être représentées de deux manières en Unicode. Certaines lettres accentuées sont des points de code uniques, tandis que d'autres combinent une lettre de base avec un signe diacritique combinant.
const combined = "é"; // e + combining acute accent
console.log(combined.split(''));
// Output: ["e", "́"]
Lorsque la lettre é est représentée par deux points de code (lettre de base plus accent combinant), la division la sépare en morceaux distincts. Le signe d'accent apparaît seul, ce qui ne correspond pas aux attentes des utilisateurs lors de la division du texte en caractères.
Utiliser Intl.Segmenter pour diviser le texte correctement
Le constructeur Intl.Segmenter crée un segmenteur qui divise le texte selon des règles spécifiques à la locale. Passez un identifiant de locale comme premier argument et un objet d'options spécifiant la granularité comme second argument.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
La granularité grapheme indique au segmenteur de diviser le texte aux limites des groupes de graphèmes. Cela respecte la structure des caractères perçus par l'utilisateur et ne les divise pas.
Appelez la méthode segment() avec une chaîne pour obtenir un itérateur de segments. Chaque segment inclut le texte et les informations de position.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const text = "hello";
const segments = segmenter.segment(text);
for (const segment of segments) {
console.log(segment.segment);
}
// Output:
// "h"
// "e"
// "l"
// "l"
// "o"
Chaque objet segment contient une propriété segment avec le texte du caractère et une propriété index avec sa position. Vous pouvez itérer directement sur les segments pour accéder à chaque caractère.
Pour obtenir un tableau de caractères, déployez l'itérateur dans un tableau et mappez vers le texte du segment.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const text = "hello";
const characters = [...segmenter.segment(text)].map(s => s.segment);
console.log(characters);
// Output: ["h", "e", "l", "l", "o"]
Ce modèle convertit l'itérateur en un tableau d'objets de segments, puis extrait uniquement le texte de chaque segment. Le résultat est un tableau de chaînes, une pour chaque groupe de graphèmes.
Diviser correctement les emoji en caractères
L'API Intl.Segmenter gère correctement tous les emojis, y compris les emojis composites qui utilisent plusieurs points de code.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const emoji = "😀";
const characters = [...segmenter.segment(emoji)].map(s => s.segment);
console.log(characters);
// Output: ["😀"]
L'emoji reste intact en tant que groupe de graphèmes unique. Le segmenteur reconnaît que les deux unités de code appartiennent au même caractère et ne les sépare pas.
Les emoji de drapeaux restent des caractères uniques au lieu de se décomposer en indicateurs régionaux.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const flag = "🇺🇸";
const characters = [...segmenter.segment(flag)].map(s => s.segment);
console.log(characters);
// Output: ["🇺🇸"]
Les deux symboles d'indicateurs régionaux forment un groupe de graphèmes représentant le drapeau américain. Le segmenteur les maintient ensemble en tant que caractère unique.
Les emoji de famille et autres emoji composites restent des caractères uniques.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const family = "👨👩👧👦";
const characters = [...segmenter.segment(family)].map(s => s.segment);
console.log(characters);
// Output: ["👨👩👧👦"]
Tous les emoji de personnes et les caractères de jointure de largeur nulle forment un groupe de graphèmes unique. Le segmenteur traite l'emoji de famille entier comme un seul caractère, préservant son apparence et sa signification.
Diviser le texte avec des lettres accentuées
L'API Intl.Segmenter gère correctement les lettres accentuées quelle que soit leur encodage en Unicode.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const precomposed = "café"; // precomposed é
const characters = [...segmenter.segment(precomposed)].map(s => s.segment);
console.log(characters);
// Output: ["c", "a", "f", "é"]
Lorsque la lettre accentuée é est encodée comme un point de code unique, le segmenteur la traite comme un seul caractère. Cela correspond aux attentes des utilisateurs pour la division du mot.
Lorsque la même lettre est encodée comme une lettre de base plus un signe diacritique combinant, le segmenteur la traite toujours comme un seul caractère.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const decomposed = "café"; // e + combining acute accent
const characters = [...segmenter.segment(decomposed)].map(s => s.segment);
console.log(characters);
// Output: ["c", "a", "f", "é"]
Le segmenteur reconnaît que la lettre de base et le signe combinant forment un groupe de graphèmes unique. Le résultat semble identique à la version précomposée, même si l'encodage sous-jacent est différent.
Ce comportement est important pour le traitement de texte dans les langues qui utilisent des signes diacritiques. Les utilisateurs s'attendent à ce que les lettres accentuées soient traitées comme des caractères complets, et non comme des lettres de base et des marques séparées.
Compter les caractères correctement
Un cas d'usage courant pour diviser du texte est de compter le nombre de caractères qu'il contient. La méthode split('') donne des comptages incorrects pour du texte avec des caractères complexes.
const text = "👨👩👧👦";
console.log(text.split('').length);
// Output: 7
L'emoji de famille apparaît comme un seul caractère mais compte pour sept lorsqu'il est divisé par unités de code. Cela ne correspond pas aux attentes des utilisateurs.
L'utilisation de Intl.Segmenter donne des comptages de caractères précis.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const text = "👨👩👧👦";
const count = [...segmenter.segment(text)].length;
console.log(count);
// Output: 1
Le segmenteur reconnaît l'emoji de famille comme un cluster de graphèmes unique, donc le compte est de un. Cela correspond à ce que les utilisateurs voient à l'écran.
Vous pouvez créer une fonction auxiliaire pour compter les clusters de graphèmes dans n'importe quelle chaîne.
function countCharacters(text) {
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
return [...segmenter.segment(text)].length;
}
console.log(countCharacters("hello"));
// Output: 5
console.log(countCharacters("café"));
// Output: 4
console.log(countCharacters("👨👩👧👦"));
// Output: 1
console.log(countCharacters("🇺🇸"));
// Output: 1
Cette fonction fonctionne correctement pour le texte ASCII, les lettres accentuées, les emojis et tout autre caractère Unicode. Le compte correspond toujours au nombre de caractères perçus par l'utilisateur.
Obtenir le caractère à une position spécifique
Lorsque vous devez accéder à un caractère à une position spécifique, vous pouvez d'abord convertir le texte en tableau de clusters de graphèmes.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const text = "Hello 👋";
const characters = [...segmenter.segment(text)].map(s => s.segment);
console.log(characters[6]);
// Output: "👋"
L'emoji de main qui salue se trouve à la position 6 lors du comptage des clusters de graphèmes. Si vous utilisiez l'indexation de tableau standard sur la chaîne, vous obtiendriez un résultat invalide car l'emoji s'étend sur plusieurs unités de code.
Cette approche est utile lors de l'implémentation d'opérations au niveau des caractères comme la sélection de caractères, la mise en évidence de caractères ou les animations caractère par caractère.
Inverser le texte correctement
Inverser une chaîne en inversant son tableau d'unités de code produit des résultats incorrects pour les caractères complexes.
const text = "Hello 👋";
console.log(text.split('').reverse().join(''));
// Output: "�� olleH"
L'emoji se brise car ses unités de code sont inversées séparément. La chaîne résultante contient des séquences de caractères invalides.
L'utilisation de Intl.Segmenter pour inverser du texte préserve l'intégrité des caractères.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const text = "Hello 👋";
const characters = [...segmenter.segment(text)].map(s => s.segment);
const reversed = characters.reverse().join('');
console.log(reversed);
// Output: "👋 olleH"
Chaque cluster de graphèmes reste intact pendant l'inversion. L'emoji reste valide car ses unités de code ne sont pas séparées.
Comprendre le paramètre locale
Le constructeur Intl.Segmenter accepte un paramètre de locale, mais pour la segmentation en graphèmes, la locale a un impact minimal. Les limites des clusters de graphèmes suivent les règles Unicode qui sont en grande partie indépendantes de la langue.
const segmenterEn = new Intl.Segmenter('en', { granularity: 'grapheme' });
const segmenterJa = new Intl.Segmenter('ja', { granularity: 'grapheme' });
const text = "Hello 👋 こんにちは";
const charactersEn = [...segmenterEn.segment(text)].map(s => s.segment);
const charactersJa = [...segmenterJa.segment(text)].map(s => s.segment);
console.log(charactersEn);
console.log(charactersJa);
// Both outputs are identical
Différents identifiants de locale produisent les mêmes résultats de segmentation en graphèmes. Le standard Unicode définit les limites des clusters de graphèmes d'une manière qui fonctionne dans toutes les langues.
Cependant, spécifier une locale reste une bonne pratique pour la cohérence avec les autres API Intl et au cas où de futures versions Unicode introduiraient des règles spécifiques aux locales.
Réutiliser les segmenteurs pour les performances
La création d'une nouvelle instance Intl.Segmenter implique le chargement des données de locale et l'initialisation des structures internes. Lorsque vous devez segmenter plusieurs chaînes avec les mêmes paramètres, créez le segmenteur une fois et réutilisez-le.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const texts = [
"Hello 👋",
"Café ☕",
"World 🌍",
"Family 👨👩👧👦"
];
texts.forEach(text => {
const characters = [...segmenter.segment(text)].map(s => s.segment);
console.log(characters);
});
// Output:
// ["H", "e", "l", "l", "o", " ", "👋"]
// ["C", "a", "f", "é", " ", "☕"]
// ["W", "o", "r", "l", "d", " ", "🌍"]
// ["F", "a", "m", "i", "l", "y", " ", "👨👩👧👦"]
Cette approche est plus efficace que de créer un nouveau segmenteur pour chaque chaîne. La différence de performance devient significative lors du traitement de grandes quantités de texte.
Combiner la segmentation en graphèmes avec d'autres opérations
Vous pouvez combiner la segmentation en graphèmes avec d'autres opérations sur les chaînes pour construire des fonctions de traitement de texte plus complexes.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
function truncateByCharacters(text, maxLength) {
const characters = [...segmenter.segment(text)].map(s => s.segment);
if (characters.length <= maxLength) {
return text;
}
return characters.slice(0, maxLength).join('') + '...';
}
console.log(truncateByCharacters("Hello 👋 World", 7));
// Output: "Hello 👋..."
console.log(truncateByCharacters("Family 👨👩👧👦 Photo", 8));
// Output: "Family 👨👩👧👦..."
Cette fonction de troncature compte les clusters de graphèmes plutôt que les unités de code. Elle préserve les emoji et autres caractères complexes lors de la troncature, de sorte que la sortie ne contient jamais de caractères brisés.
Travailler avec les positions de chaîne
Les objets de segment renvoyés par Intl.Segmenter incluent une propriété index qui indique la position dans la chaîne d'origine. Cette position est mesurée en unités de code, et non en clusters de graphèmes.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const text = "Hello 👋";
for (const segment of segmenter.segment(text)) {
console.log(`Character "${segment.segment}" starts at position ${segment.index}`);
}
// Output:
// Character "H" starts at position 0
// Character "e" starts at position 1
// Character "l" starts at position 2
// Character "l" starts at position 3
// Character "o" starts at position 4
// Character " " starts at position 5
// Character "👋" starts at position 6
L'emoji de la main qui salue commence à la position d'unité de code 6, même s'il occupe les positions 6 et 7 dans la chaîne sous-jacente. Le caractère suivant commencerait à la position 8. Cette information est utile lorsque vous devez mapper entre les positions de graphèmes et les positions de chaîne pour des opérations telles que l'extraction de sous-chaînes.
Gestion des chaînes vides et des cas limites
L'API Intl.Segmenter gère correctement les chaînes vides et autres cas limites.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const empty = "";
const characters = [...segmenter.segment(empty)].map(s => s.segment);
console.log(characters);
// Output: []
Une chaîne vide produit un tableau vide de segments. Aucune gestion spéciale n'est requise.
Les caractères d'espacement sont traités comme des groupes de graphèmes distincts.
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const whitespace = "a b\tc\nd";
const characters = [...segmenter.segment(whitespace)].map(s => s.segment);
console.log(characters);
// Output: ["a", " ", "b", "\t", "c", "\n", "d"]
Les espaces, tabulations et sauts de ligne forment chacun leur propre groupe de graphèmes. Cela correspond aux attentes des utilisateurs pour le traitement de texte au niveau des caractères.