API Intl.Segmenter
Comment compter les caractères, diviser les mots et segmenter les phrases correctement en JavaScript
Introduction
La propriété string.length de JavaScript compte les unités de code, pas les caractères perçus par l'utilisateur. Lorsque les utilisateurs saisissent des emoji, des caractères accentués ou du texte dans des systèmes d'écriture complexes, string.length renvoie un décompte incorrect. La méthode split() échoue pour les langues qui n'utilisent pas d'espaces entre les mots. Les limites de mots par expressions régulières ne fonctionnent pas pour le texte chinois, japonais ou thaï.
L'API Intl.Segmenter résout ces problèmes. Elle segmente le texte selon les normes Unicode, en respectant les règles linguistiques de chaque langue. Vous pouvez compter les graphèmes (caractères perçus par l'utilisateur), diviser le texte en mots quelle que soit la langue, ou découper le texte en phrases.
Cet article explique pourquoi les opérations de base sur les chaînes échouent pour le texte international, ce que sont les groupes de graphèmes et les limites linguistiques, et comment utiliser Intl.Segmenter pour traiter correctement le texte pour tous les utilisateurs.
Pourquoi string.length échoue pour le comptage de caractères
Les chaînes JavaScript utilisent l'encodage UTF-16. Chaque élément d'une chaîne JavaScript est une unité de code de 16 bits, pas un caractère complet. La propriété string.length compte ces unités de code.
Pour les caractères ASCII de base, une unité de code équivaut à un caractère. La chaîne "hello" a une longueur de 5, ce qui correspond aux attentes de l'utilisateur.
Pour de nombreux autres caractères, cela ne fonctionne plus. Considérez ces exemples :
"😀".length; // 2, not 1
"👨👩👧👦".length; // 11, not 1
"किं".length; // 5, not 2
"🇺🇸".length; // 4, not 1
Les utilisateurs voient un emoji, un emoji de famille, deux syllabes hindi ou un drapeau. JavaScript compte les unités de code sous-jacentes.
Cela importe lorsque vous créez des compteurs de caractères pour les champs de saisie, validez des limites de longueur ou tronquez du texte pour l'affichage. Le décompte rapporté par JavaScript ne correspond pas à ce que voient les utilisateurs.
Que sont les groupes de graphèmes
Un groupe de graphèmes est ce que les utilisateurs perçoivent comme un seul caractère. Il peut être constitué de :
- Un seul point de code comme
"a" - Un caractère de base plus des marques combinées comme
"é"(e + accent aigu combiné) - Plusieurs points de code joints ensemble comme
"👨👩👧👦"(homme + femme + fille + garçon joints avec des jointures de largeur nulle) - Des emoji avec des modificateurs de teinte de peau comme
"👋🏽"(main qui salue + teinte de peau moyenne) - Des séquences d'indicateurs régionaux pour les drapeaux comme
"🇺🇸"(indicateur régional U + indicateur régional S)
Le standard Unicode définit les groupes de graphèmes étendus dans UAX 29. Ces règles déterminent où les utilisateurs s'attendent à trouver des limites entre les caractères. Lorsqu'un utilisateur appuie sur la touche retour arrière, il s'attend à supprimer un groupe de graphèmes. Lorsqu'un curseur se déplace, il devrait se déplacer par groupes de graphèmes.
La propriété string.length de JavaScript ne compte pas les groupes de graphèmes. L'API Intl.Segmenter le fait.
Compter les groupes de graphèmes avec Intl.Segmenter
Créez un segmenteur avec une granularité de graphème pour compter les caractères perçus par l'utilisateur :
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const text = "Hello 👋🏽";
const segments = segmenter.segment(text);
const graphemes = Array.from(segments);
console.log(graphemes.length); // 7
console.log(text.length); // 10
L'utilisateur voit sept caractères : cinq lettres, un espace et un emoji. Le segmenteur de graphèmes renvoie sept segments. La propriété string.length de JavaScript renvoie dix car l'emoji utilise quatre unités de code.
Chaque objet segment contient :
segment: le groupe de graphèmes sous forme de chaîneindex: la position dans la chaîne d'origine où ce segment commenceinput: référence à la chaîne d'origine (pas toujours nécessaire)
Vous pouvez itérer sur les segments avec for...of :
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const text = "café";
for (const { segment } of segmenter.segment(text)) {
console.log(segment);
}
// Logs: "c", "a", "f", "é"
Créer un compteur de caractères qui fonctionne à l'international
Utilisez la segmentation en graphèmes pour créer des compteurs de caractères précis :
function getGraphemeCount(text) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
return Array.from(segmenter.segment(text)).length;
}
// Test with various inputs
getGraphemeCount("hello"); // 5
getGraphemeCount("hello 😀"); // 7
getGraphemeCount("👨👩👧👦"); // 1
getGraphemeCount("किंतु"); // 2
getGraphemeCount("🇺🇸"); // 1
Cette fonction renvoie des décomptes qui correspondent à la perception de l'utilisateur. Un utilisateur qui saisit un emoji de famille voit un caractère, et le compteur affiche un caractère.
Pour la validation des saisies de texte, utilisez les décomptes de graphèmes au lieu de string.length :
function validateInput(text, maxGraphemes) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const count = Array.from(segmenter.segment(text)).length;
return count <= maxGraphemes;
}
Tronquer du texte en toute sécurité avec la segmentation en graphèmes
Lors de la troncature de texte pour l'affichage, vous ne devez pas couper à travers un cluster de graphèmes. Couper à un index d'unité de code arbitraire peut diviser des emoji ou des séquences de caractères combinés, produisant une sortie invalide ou corrompue.
Utilisez la segmentation en graphèmes pour trouver des points de troncature sûrs :
function truncateText(text, maxGraphemes) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const segments = Array.from(segmenter.segment(text));
if (segments.length <= maxGraphemes) {
return text;
}
const truncated = segments
.slice(0, maxGraphemes)
.map(s => s.segment)
.join("");
return truncated + "…";
}
truncateText("Hello 👨👩👧👦 world", 7); // "Hello 👨👩👧👦…"
truncateText("Hello world", 7); // "Hello w…"
Cela préserve les clusters de graphèmes complets et produit une sortie Unicode valide.
Pourquoi split() et les regex échouent pour la segmentation de mots
L'approche courante pour diviser du texte en mots utilise split() avec un espace ou un motif d'espacement :
const text = "Hello world";
const words = text.split(" "); // ["Hello", "world"]
Cela fonctionne pour l'anglais et d'autres langues qui séparent les mots avec des espaces. Cela échoue complètement pour les langues qui n'utilisent pas d'espaces entre les mots.
Le texte chinois, japonais et thaï n'inclut pas d'espaces entre les mots. Diviser sur les espaces renvoie la chaîne entière comme un seul élément :
const text = "你好世界"; // "Hello world" in Chinese
const words = text.split(" "); // ["你好世界"]
L'utilisateur voit quatre mots distincts, mais split() renvoie un élément.
Les limites de mots des expressions régulières (\b) échouent également pour ces langues car le moteur regex ne reconnaît pas les limites de mots dans les systèmes d'écriture sans espaces.
Comment fonctionne la segmentation de mots entre les langues
L'API Intl.Segmenter utilise les règles de limites de mots Unicode définies dans UAX 29. Ces règles comprennent les limites de mots pour tous les systèmes d'écriture, y compris ceux sans espaces.
Créer un segmenteur avec une granularité au niveau des mots :
const segmenter = new Intl.Segmenter("zh", { granularity: "word" });
const text = "你好世界";
const segments = Array.from(segmenter.segment(text));
segments.forEach(({ segment, isWordLike }) => {
console.log(segment, isWordLike);
});
// "你好" true
// "世界" true
Le segmenteur identifie correctement les limites des mots en fonction de la locale et du script. La propriété isWordLike indique si le segment est un mot (lettres, chiffres, idéogrammes) ou un contenu non-mot (espaces, ponctuation).
Pour le texte en anglais, le segmenteur renvoie à la fois les mots et les espaces :
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world!";
const segments = Array.from(segmenter.segment(text));
segments.forEach(({ segment, isWordLike }) => {
console.log(segment, isWordLike);
});
// "Hello" true
// " " false
// "world" true
// "!" false
Utiliser la propriété isWordLike pour filtrer les segments de mots de la ponctuation et des espaces :
function getWords(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
return Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment);
}
getWords("Hello, world!", "en"); // ["Hello", "world"]
getWords("你好世界", "zh"); // ["你好", "世界"]
getWords("สวัสดีครับ", "th"); // ["สวัสดี", "ครับ"] (Thai)
Cette fonction fonctionne pour n'importe quelle langue, gérant à la fois les scripts séparés par des espaces et ceux qui ne le sont pas.
Compter les mots avec précision
Créer un compteur de mots qui fonctionne à l'international :
function countWords(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
return Array.from(segments).filter(s => s.isWordLike).length;
}
countWords("Hello world", "en"); // 2
countWords("你好世界", "zh"); // 2
countWords("Bonjour le monde", "fr"); // 3
Cela produit des comptages de mots précis pour du contenu dans n'importe quelle langue.
Trouver quel mot contient une position de curseur
La méthode containing() trouve le segment qui inclut un index spécifique dans la chaîne. Cela est utile pour déterminer dans quel mot se trouve le curseur ou quel segment contient une position de clic.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world";
const segments = segmenter.segment(text);
const segment = segments.containing(7); // Index 7 is in "world"
console.log(segment);
// { segment: "world", index: 6, isWordLike: true }
Si l'index se trouve dans un espace ou une ponctuation, containing() renvoie ce segment :
const segment = segments.containing(5); // Index 5 is the space
console.log(segment);
// { segment: " ", index: 5, isWordLike: false }
Utiliser cela pour les fonctionnalités d'édition de texte, la mise en évidence de recherche ou les actions contextuelles basées sur la position du curseur.
Segmenter les phrases pour le traitement de texte
La segmentation de phrases divise le texte aux limites de phrases. Cela est utile pour la synthèse, le traitement de synthèse vocale ou la navigation dans de longs documents.
Les approches basiques comme la division sur les points échouent car les points apparaissent dans les abréviations, les nombres et d'autres contextes qui ne sont pas des limites de phrases :
const text = "Dr. Smith bought 100.5 shares. He sold them later.";
text.split(". "); // Incorrect: breaks at "Dr." and "100."
L'API Intl.Segmenter comprend les règles de limites de phrases :
const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Dr. Smith bought 100.5 shares. He sold them later.";
const segments = Array.from(segmenter.segment(text));
segments.forEach(({ segment }) => {
console.log(segment);
});
// "Dr. Smith bought 100.5 shares. "
// "He sold them later."
Le segmenteur traite correctement "Dr." et "100.5" comme faisant partie de la phrase, et non comme des limites de phrases.
Pour le texte multilingue, les limites de phrases varient selon la locale. L'API gère ces différences :
const segmenterEn = new Intl.Segmenter("en", { granularity: "sentence" });
const segmenterJa = new Intl.Segmenter("ja", { granularity: "sentence" });
const textEn = "Hello. How are you?";
const textJa = "こんにちは。お元気ですか。"; // Uses Japanese full stop
Array.from(segmenterEn.segment(textEn)).length; // 2
Array.from(segmenterJa.segment(textJa)).length; // 2
Quand utiliser chaque granularité
Choisissez la granularité en fonction de ce que vous devez compter ou diviser :
-
Graphème : utilisez pour le comptage de caractères, la troncature de texte, le positionnement du curseur, ou toute opération où vous devez correspondre à la perception utilisateur des caractères.
-
Mot : utilisez pour le comptage de mots, la recherche et la mise en évidence, l'analyse de texte, ou toute opération nécessitant des limites de mots linguistiques entre les langues.
-
Phrase : utilisez pour la segmentation de synthèse vocale, la synthèse, la navigation dans les documents, ou toute opération qui traite le texte phrase par phrase.
N'utilisez pas la segmentation par graphème lorsque vous avez besoin de limites de mots, et n'utilisez pas la segmentation par mot lorsque vous avez besoin de comptes de caractères. Chaque granularité sert un objectif distinct.
Créer et réutiliser des segmenteurs
Créer un segmenteur est peu coûteux, mais vous pouvez réutiliser les segmenteurs pour les performances :
const graphemeSegmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const wordSegmenter = new Intl.Segmenter("en", { granularity: "word" });
// Reuse these segmenters for multiple strings
function processTexts(texts) {
return texts.map(text => ({
text,
graphemes: Array.from(graphemeSegmenter.segment(text)).length,
words: Array.from(wordSegmenter.segment(text)).filter(s => s.isWordLike).length
}));
}
Le segmenteur met en cache les données de locale, donc réutiliser la même instance évite une initialisation répétée.
Vérifier la prise en charge du navigateur
L'API Intl.Segmenter a atteint le statut Baseline en avril 2024. Elle fonctionne dans les versions actuelles de Chrome, Firefox, Safari et Edge. Les navigateurs plus anciens ne la prennent pas en charge.
Vérifiez la prise en charge avant utilisation :
if (typeof Intl.Segmenter !== "undefined") {
// Use Intl.Segmenter
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
// ...
} else {
// Fallback for older browsers
const count = text.length; // Not accurate, but available
}
Pour les applications de production ciblant les navigateurs plus anciens, envisagez d'utiliser un polyfill ou de fournir des fonctionnalités dégradées.
Erreurs courantes à éviter
N'utilisez pas string.length pour afficher le nombre de caractères aux utilisateurs. Cela produit des résultats incorrects pour les emoji, les caractères combinés et les scripts complexes.
Ne divisez pas sur les espaces ou n'utilisez pas de limites de mots regex pour la segmentation de mots multilingue. Ces approches ne fonctionnent que pour un sous-ensemble de langues.
Ne supposez pas que les limites de mots ou de phrases sont identiques d'une langue à l'autre. Utilisez une segmentation tenant compte des paramètres régionaux.
N'oubliez pas de vérifier la propriété isWordLike lors du comptage des mots. Inclure la ponctuation et les espaces produit des comptages gonflés.
Ne coupez pas les chaînes à des indices arbitraires lors de la troncature. Coupez toujours aux limites des groupes de graphèmes pour éviter de produire des séquences Unicode invalides.
Quand ne pas utiliser Intl.Segmenter
Pour les opérations simples ASCII uniquement où vous savez que le texte ne contient que des caractères latins de base, les méthodes de chaîne de base sont plus rapides et suffisantes.
Lorsque vous avez besoin de la longueur en octets d'une chaîne pour des opérations réseau ou de stockage, utilisez TextEncoder :
const byteLength = new TextEncoder().encode(text).length;
Lorsque vous avez besoin du nombre réel d'unités de code pour une manipulation de chaîne de bas niveau, string.length est correct. Cela est rare dans le code applicatif.
Pour la plupart des traitements de texte impliquant du contenu destiné aux utilisateurs, en particulier dans les applications internationales, utilisez Intl.Segmenter.
Comment Intl.Segmenter se rapporte aux autres API d'internationalisation
L'API Intl.Segmenter fait partie de l'API d'internationalisation ECMAScript. Les autres API de cette famille incluent :
Intl.DateTimeFormat: formater les dates et heures selon les paramètres régionauxIntl.NumberFormat: formater les nombres, devises et unités selon les paramètres régionauxIntl.Collator: trier et comparer les chaînes selon les paramètres régionauxIntl.PluralRules: déterminer les formes plurielles des nombres dans différentes langues
Ensemble, ces API fournissent les outils nécessaires pour créer des applications qui fonctionnent correctement pour les utilisateurs du monde entier. Utilisez Intl.Segmenter pour la segmentation de texte, et utilisez les autres API Intl pour le formatage et la comparaison.
Exemple pratique : créer un composant de statistiques de texte
Combinez la segmentation de graphèmes et de mots pour créer un composant de statistiques de texte :
function getTextStatistics(text, locale) {
const graphemeSegmenter = new Intl.Segmenter(locale, {
granularity: "grapheme"
});
const wordSegmenter = new Intl.Segmenter(locale, {
granularity: "word"
});
const sentenceSegmenter = new Intl.Segmenter(locale, {
granularity: "sentence"
});
const graphemes = Array.from(graphemeSegmenter.segment(text));
const words = Array.from(wordSegmenter.segment(text))
.filter(s => s.isWordLike);
const sentences = Array.from(sentenceSegmenter.segment(text));
return {
characters: graphemes.length,
words: words.length,
sentences: sentences.length,
averageWordLength: words.length > 0
? graphemes.length / words.length
: 0
};
}
// Works for any language
getTextStatistics("Hello world! How are you?", "en");
// { characters: 24, words: 5, sentences: 2, averageWordLength: 4.8 }
getTextStatistics("你好世界!你好吗?", "zh");
// { characters: 9, words: 5, sentences: 2, averageWordLength: 1.8 }
Cette fonction produit des statistiques significatives pour du texte dans n'importe quelle langue, en utilisant les règles de segmentation appropriées pour chaque locale.