Egy névmezőben a „legfeljebb 20 karakter” ártalmatlan szabálynak tűnik, amíg valaki családi emojit, zászlót vagy kombinált ékezetet ír bele. JavaScriptben a value.length nem azt méri, amit az ember karakternek lát, hanem UTF-16 kódegységeket. A különbségből elutasított űrlap, rosszul levágott profilnév és kliens–szerver validációs vita lesz.
Erre való az Intl.Segmenter. Nem újabb string-helper: a Unicode szövegtördelési szabályait használja, és képes a felhasználó által egyetlen karakternek érzékelt egységekre, úgynevezett grafémaklaszterekre bontani a szöveget.

Háromféle hosszúság ugyanarra a szövegre
Az ECMAScript stringje UTF-16 kódegységek sorozata. A length ezért a kódegységek számát adja. Egy magyar zászló például két Unicode-kódpontból áll, amelyek a UTF-16-ban összesen négy kódegységet foglalnak. A megjelenésben mégis egyetlen zászló.
const graphemeSegmenter = new Intl.Segmenter(undefined, {
granularity: "grapheme",
});
const graphemeCount = (value) =>
Array.from(graphemeSegmenter.segment(value)).length;
for (const value of ["👨👩👧👦", "e\u0301", "🇭🇺"]) {
console.log(JSON.stringify(value), value.length, [...value].length, graphemeCount(value));
}
// "👨👩👧👦" 11 7 1
// "é" 2 2 1
// "🇭🇺" 4 2 1
A középső szám, a [...value].length, már Unicode-kódpontokat számol: a surrogate pair hibáját javítja, de nem érti a több kódpontból összerakott megjelenített jeleket. A családi emoji zero-width joiner karakterekkel fűzi össze a szereplőket, az é pedig egy e és egy kombináló ékezet. Mindkettő egy grafémaklaszter. Az ilyen határokat a Unicode Text Segmentation szabályrendszere írja le; az Intl.Segmenter ennek megfelelő eredményt ad.
Ne slice()-old le a felhasználó szövegét
A slice(0, 20) akár egy surrogate pair közepén is kettévághat egy stringet. A kódpontokra szétszedés ezt az esetet elkerüli, de a családi emojiból még mindig levághat egy embert vagy egy joinert. Ha a limit a látható karakterekről szól, ugyanazzal a szegmentálóval vágjunk is:
const takeGraphemes = (value, limit) =>
Array.from(graphemeSegmenter.segment(value), ({ segment }) => segment)
.slice(0, limit)
.join("");
takeGraphemes("alma👨👩👧👦körte", 5);
// "alma👨👩👧👦"
Rövid címekhez és bemeneti mezőkhöz ez teljesen megfelelő. Nagy szövegen ne alakítsuk automatikusan tömbbé: iteráljunk a segment() eredményén, és álljunk meg a limiten. A szabványos API ECMA-402 specifikációja nemcsak grafémát, hanem word és sentence granularitást is definiál. A word szegmentumoknál az isWordLike jelzi, hogy a rész tényleges szó-e; ez hasznos lehet keresési kiemelésnél vagy egyedi számlálónál.
A HTML maxlength ettől még másképp számol
A natív maxlength korlát is UTF-16 kódegységben értendő. Egy maxlength="20" mező tehát nem húsz emoji- vagy grafémaklaszter-hely. Ez nem böngészőhiba, hanem a HTML előírása. Ha termékoldali szabály szerint húsz látható jel a plafon, a maxlength legfeljebb durva technikai korlát legyen; a saját grafémaszámlálóval adjunk érthető visszajelzést, és ugyanazt az üzleti szabályt a szerveren is hajtsuk végre.
A backendnek nem kell JavaScriptnek lennie, de Unicode-grafémaszegmentálást támogató könyvtárra vagy runtime-ra szüksége lesz. A kliensoldali számláló nem validációs határ. Érdemes azt is külön döntésként kezelni, normalizáljuk-e a tárolt szöveget: a String.prototype.normalize() az azonosan megjelenő, eltérő kódolású alakok egységesítésére való, nem a grafémák helyettesítője. Keresésnél vagy egyedi névnél fontos lehet, de vakon normalizálni adatvesztő termékdöntés is lehet.
Mikor érdemes bevezetni?
Ahol a felhasználó olvasott limitet kap: profilnév, üzenet-előnézet, komment, fájlnév vagy push értesítés. Belső, gép által generált azonosítóknál továbbra is a bájt- vagy kódegység-limit lehet a megfelelő szerződés. A lényeg, hogy a limit egysége legyen kimondva. A „20 karakter” csak akkor egyértelmű, ha eldöntöttük, hogy a gép kódegységét vagy az ember által látott grafémát értjük alatta.