Az emoji nem egy karakter: szöveglimitek Intl.Segmenterrel

A JavaScript length értéke UTF-16-kódegységeket számlál, ezért egy emoji vagy kombinált ékezet több helyet foglalhat, mint amennyinek látszik. Az Intl.Segmenter grafémaklaszterekre bontja a szöveget: így a felhasználónak ígért karakterszámot lehet mérni és biztonságosan levágni.

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.

Az UTF-16 kódegységek több különálló elemből egy grafémaklaszterré rendeződnek

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.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük