1. Een tekennummer is iets anders dan opgeslagen bytes
Stel dat u een Koreaans document exporteert en opnieuw opent, waarna de titel uit vreemde tekens bestaat. De taal van de schrijver is niet veranderd: mogelijk komen de regels voor het lezen van de opgeslagen getallen niet overeen. Unicode kent aan tekens en symbolen uit verschillende schriften nummers toe, de zogeheten codepunten. UTF-8 is een codering die de bijbehorende waarden als reeksen bytes weergeeft. De Koreaanse lettergreep ‘가’ heeft bijvoorbeeld codepunt U+AC00. De notatie na U+ is hexadecimaal; zij duidt geen positie in een bestand of geheugenadres aan. Het nummer bepaalt ook niet de lettervorm of de pixels op het scherm. Hetzelfde codepunt kan er door lettertype en tekstopmaak verschillend uitzien. Unicode-ondersteuning, opslag als UTF-8 en geïnstalleerde Koreaanse lettertypen hangen samen, maar vragen elk een eigen controle. De Unicode-uitleg over UTF helpt om tekens van opslagformaten te onderscheiden.
In UTF-8 bestaat ‘가’ uit de drie hexadecimale bytes EA B0 80. De Latijnse A heeft codepunt U+0041 en wordt opgeslagen als één byte, 41. Een byte bevat acht bits: zelfs binnen één document kunnen tekens verschillende aantallen bytes gebruiken. Ook de extensie .txt of .csv vertelt op zichzelf niet welke codering werkelijk is toegepast. UTF-8 geeft geldige Unicode-scalaire waarden weer met één tot vier bytes. Daarom kan de precieze bestandsgrootte niet alleen uit een vermenigvuldiging van het aantal zichtbare tekens worden afgeleid. Dit voorbeeld van een Koreaanse lettergreep geldt evenmin voor alle Koreaanse tekens of alle talen. Coderen zet tekeninformatie om in bytes en is geen compressie of versleuteling. Extra informatie van het bestandsformaat, regeleinden en transportcompressie kunnen de totale grootte van dezelfde zin veranderen. Wie de omzetting tussen bytes en tekens uit de WHATWG Encoding Standard onderscheidt van totale bestandsgrootte en geheimhouding, kan het probleem gerichter afbakenen.
2. Bytes, code-eenheden en ervaren tekens tellen
De eigenschap length van een JavaScript-string telt UTF-16-code-eenheden. Dat is niet het aantal bytes van een UTF-8-bestand en ook niet noodzakelijk het aantal tekens dat iemand waarneemt. Het glimlachende gezicht ‘😀’ is één codepunt, U+1F600, maar gebruikt twee UTF-16-code-eenheden: '😀'.length geeft 2. De UTF-8-weergave F0 9F 98 80 beslaat vier bytes. Het decimale getal 128512 uit codePointAt(0) is hetzelfde nummer als U+1F600, geen tekenaantal of bestandsgrootte. ‘가’ bestaat bij deze drie maatstaven uit één codepunt, één UTF-16-code-eenheid en drie UTF-8-bytes. Verschillende uitkomsten voor dezelfde string ontstaan doordat de eenheden verschillen. MDN over stringlengte benoemt de eenheid van length expliciet. Dit getal bepaalt op zichzelf ook niet hoeveel echte geheugenbytes een JavaScript-engine intern gebruikt.
Zelfs het afzonderlijk tellen van codepunten komt niet altijd overeen met zichtbare tekens. Een basisteken kan combinerende tekens meekrijgen en meerdere codepunten kunnen samen één emoji vormen. De Unicode-regels voor tekstsegmentatie beschrijven de grenzen van uitgebreide grafeemclusters om zulke groepen te behandelen. Ook Array.from(text), dat een string per codepunt doorloopt, telt die groepen niet automatisch als één eenheid. In de browser kunt u na controle van de ondersteuning Intl.Segmenter overwegen met granularity op 'grapheme'. Een grafeemcluster correspondeert echter niet gegarandeerd met één getekende lettervorm, en verwachtingen rond tekstbewerking verschillen per taal. Een invoerveld voor een introductie heeft een begrijpelijke tekenteller nodig; opslag en communicatie kunnen daarnaast een afzonderlijke bytegrens vragen. Als de interface ‘20 tekens’ zegt maar intern alleen UTF-16-code-eenheden afsnijdt, kunnen emoji of gecombineerde tekens halverwege worden gesplitst.
3. Opslaan, doorgeven en weergeven afzonderlijk bekijken
Onleesbare tekst kan ontstaan wanneer de codering bij het schrijven niet overeenkomt met het decoderen bij het lezen. Een UTF-8-bestand met een andere codering interpreteren kan verkeerde tekens tonen zonder dat het bestand zelf is veranderd. Controleer bij HTML het werkelijk opgeslagen formaat, charset in de HTTP-responsheader Content-Type en de vroege meta-charset-declaratie samen. Het HTML-attribuut lang='ko' geeft de taal van de inhoud aan en vervult een andere rol dan charset, dat de interpretatie van bytes betreft. De W3C-uitleg over coderingsdeclaraties legt uit dat niet alleen de declaratie moet veranderen: sla het bestand werkelijk als UTF-8 op en houd server en document consistent. Coderingsmarkeringen kunnen bovendien de prioriteitsvolgorde beïnvloeden; een meta-element overschrijft niet altijd alle andere informatie. Verschillen tussen rechtstreeks openen en ophalen via een server wijzen daarom ook op de overdrachtsstap.
Vierkantjes doordat een lettertype de benodigde glyphs mist, kunnen een andere oorzaak hebben dan verkeerd decoderen. Als de tekengegevens juist zijn maar een passende vorm ontbreekt, helpt een andere opslagcodering niet. Omgekeerd herstelt een nieuw lettertype geen oorspronkelijke tekens die al als andere tekens zijn geïnterpreteerd. Bewaar een kopie van het probleembestand en vergelijk achtereenvolgens de in de editor gekozen codering, de responsdeclaraties, de door de browser geïnterpreteerde tekens en het gebruikte lettertype. Oordeel niet alleen op uiterlijk; bekijk ook welke codepunten aanwezig zijn als u dezelfde passage kopieert. Noteer of slechts één browser afwijkt of andere editors hetzelfde bestand eveneens verkeerd lezen. Herhaaldelijk opslaan tot iets aannemelijk lijkt, kan de oorspronkelijke bytes verloren laten gaan. Zijn alle declaraties correct maar slechts enkele berichten beschadigd, onderzoek dan ook waar de inhoud veranderde voordat die in de database terechtkwam.
4. Waarden controleren met TextEncoder en TextDecoder
Begin een eenvoudig experiment met een zelfgemaakte korte string. TextEncoder zet een JavaScript-string om in een UTF-8-bytearray. new TextEncoder().encode('가') levert hexadecimaal EA B0 80 en de length van de array is 3. Ontwikkelhulpmiddelen kunnen dezelfde waarden decimaal tonen als 234, 176, 128: de gegevens verschillen niet, alleen het getalstelsel. Vergelijk dat met de gewone stringlengte 1 om het eenhedenverschil te zien. new TextDecoder('utf-8').decode(bytes) geeft weer ‘가’, waarbij bytes de eerder gemaakte bytearray aanduidt. De encoder produceert UTF-8 en is geen universele converter waarin elke oude uitvoercodering gekozen kan worden. Voor onderzoek van een echt bestand moet u uitgaan van de bewaarde oorspronkelijke bytes. Tekst die op het scherm al verkeerd is gelezen opnieuw als UTF-8 coderen bewijst niet welke bytes het eerste bestand bevatte.
Door 'utf-8' en { fatal: true } aan de TextDecoder-constructor mee te geven, kan ongeldige invoer als fout worden behandeld in plaats van stilzwijgend te worden vervangen. De losse byte FF is bijvoorbeeld geen geldige UTF-8-invoer, zodat decoderen in deze modus mislukt. In de gewone vervangingsmodus kan op de ongeldige plaats het vervangingsteken U+FFFD verschijnen. Geen fout krijgen betekent echter niet dat de oorspronkelijke codering is vastgesteld of de juistheid van de inhoud is gecontroleerd. Sommige bytes zijn in verschillende coderingen geldig; al veranderde tekst kan ook opnieuw als geldige UTF-8 zijn opgeslagen. Deze test controleert of de gekozen decoder de invoer accepteert. Hij bevestigt niet automatisch de herkomst, overeenstemming met het origineel of de afwezigheid van tussentijdse wijzigingen.
5. Normalisatie brengt gelijkwaardige representaties bij elkaar
Ook twee correct gelezen strings kunnen intern verschillende codepuntreeksen bevatten. ‘가’ kan worden weergegeven met U+AC00 alleen of met de combinatie van begin-jamo U+1100 en klinker-jamo U+1161. Die laatste waarden zijn niet gewoon de alledaagse compatibiliteits-jamo ‘ㄱ’ en ‘ㅏ’ naast elkaar: controleer ook de nummers. Beide vormen verschijnen meestal als dezelfde lettergreep maar kunnen bij een ruwe JavaScript-stringvergelijking verschillen. Door op beide kanten NFC toe te passen volgens de Unicode-normalisatieregels kan dit voorbeeld van canonieke gelijkwaardigheid in dezelfde vorm worden vergeleken. NFD gebruikt canonieke ontleding. Hier neemt de NFC-vorm drie UTF-8-bytes in en de twee jamo van de NFD-vorm zes; een gelijk uiterlijk verzekert geen gelijke opslaggrootte. Dit kan verklaren waarom een zoekterm en opgeslagen naam er gelijk uitzien maar niet overeenkomen. Het verschilt van beschadiging door een verkeerde interpretatie van bytes.
De normalisatievorm hangt ook af van het doel. NFC behandelt canonieke gelijkwaardigheid; NFKC omvat compatibiliteitsontleding en kan vormen zoals volle-breedtecijfers of omcirkelde cijfers verder veranderen. Dat kan nuttig zijn voor een vergelijkingswaarde bij zoeken, terwijl zulke verschillen in wiskundige notatie of oorspronkelijke documenten betekenis hebben. Bewaar daarom het origineel en bepaal bewust welke vorm op een vergelijkingskopie wordt toegepast. Normalisatie vervangt geen hoofdlettervergelijking, taalspecifieke sortering of het vinden van gelijk uitgesproken woorden. Bestaande bestandsnamen of accountidentificaties massaal omzetten kan verschillende items dezelfde vergelijkingswaarde geven; onderzoek vooraf de naamregels en afhandeling van botsingen binnen de dienst. Alle visueel gelijkende tekens onder één opgeslagen waarde samenbrengen is geen algemeen doel van normalisatie.
6. Het origineel bewaren en omzettingsgrenzen vastleggen
Bewaar voordat u het oorspronkelijke bestand overschrijft de bytes en noteer hoe u het hebt verkregen. Probeer de verwachte decodering op een kopie en controleer of leesbare inhoud na opnieuw opslaan behouden blijft. De mogelijke UTF-8-markering aan het begin uit de W3C-uitleg over BOM is EF BB BF en is niet verplicht. UTF-8 vereist geen keuze van bytevolgorde; de markering heeft dus niet dezelfde functie als het aanduiden van volgorde in UTF-16. Een BOM toevoegen of verwijderen is ook iets anders dan de tekstbytes naar een andere codering omzetten. Als onleesbare waarden al door vervangingstekens of vraagtekens zijn vervangen en opgeslagen, kunnen verschillende oorspronkelijke waarden zijn samengevallen; automatisch herstel is dan niet gegarandeerd. Alleen met de oorspronkelijke codering en omzettingsgeschiedenis kan een mogelijke omkering worden beoordeeld. Normalisatie haalt verloren bytes niet terug.
Rond de controle van meertalige functies niet af na één Engelse regel. Maak korte voorbeelden met Koreaanse lettergrepen, Latijnse letters met combinerende tekens, een emoji uit een aanvullend Unicode-vlak en rechts-naar-links geschreven tekst zoals Arabisch of Hebreeuws. Vergelijk invoer, opslag, opnieuw ophalen en export. Controleer per stap uiterlijk, codepunten, UTF-8-byteaantal en de door het product gekozen teleenheid. De leesrichting wordt niet opgelost door UTF-8 voor een andere codering te vervangen: taal- en richtinginstellingen van de interface vragen een aparte controle. Behoud ook bestaande originelen wanneer invoergrenzen of zoekvergelijkingen veranderen en licht het nieuwe criterium toe. De browserteller en de serverlimiet moeten samenhangende eenheden gebruiken; als verschillende grenzen nodig zijn, moeten beide worden benoemd. De getallen en codevoorbeelden hier illustreren omzettingen van korte strings, geen metingen van een storing bij een specifieke dienst. Door tekennummer, bytes, stringeenheden, weergave en vergelijking na te lopen, is te reproduceren wat bij elke grens veranderde.