1. Numer znaku to nie zapisane bajty
Wyobraź sobie, że eksportujesz koreański dokument, a po ponownym otwarciu jego tytuł zawiera dziwne znaki. Język autora się nie zmienił: mogły nie zgadzać się reguły odczytywania zapisanych liczb. Unicode przypisuje znakom i symbolom różnych pism numery zwane punktami kodowymi. UTF-8 to kodowanie przedstawiające odpowiednie wartości jako sekwencje bajtów. Koreańska sylaba ‘가’ ma na przykład punkt kodowy U+AC00. Zapis po U+ jest szesnastkowy; nie wskazuje pozycji w pliku ani adresu pamięci. Numer nie określa również kształtu czcionki czy pikseli ekranu. Ten sam punkt kodowy może wyglądać inaczej zależnie od fontu i składu tekstu. Obsługa Unicode, zapis UTF-8 i zainstalowanie koreańskiej czcionki są powiązane, lecz wymagają osobnego sprawdzenia. Wyjaśnienie UTF autorstwa Unicode pomaga rozdzielić pojęcia znaku i formatu zapisu.
W UTF-8 ‘가’ jest reprezentowane przez trzy bajty szesnastkowe EA B0 80. Łacińska litera A ma punkt kodowy U+0041 i zajmuje jeden bajt, 41. Bajt obejmuje osiem bitów: nawet w jednym dokumencie znaki mogą zajmować różną liczbę bajtów. Samo rozszerzenie .txt lub .csv także nie ujawnia rzeczywistego kodowania. UTF-8 przedstawia poprawne wartości skalarne Unicode za pomocą od jednego do czterech bajtów. Nie da się więc dokładnie wyliczyć wielkości pliku przez pomnożenie liczby widocznych znaków. Przykładu tej sylaby nie należy uogólniać na wszystkie koreańskie znaki ani języki. Kodowanie zamienia informację tekstową na bajty; nie jest kompresją ani szyfrowaniem. Dodatkowe dane formatu, zakończenia wierszy i kompresja przesyłania mogą zmieniać całkowity rozmiar tego samego zdania. Oddzielenie konwersji bajtów i znaków opisanej w standardzie Encoding WHATWG od rozmiaru całego pliku lub poufności pomaga zawęzić problem.
2. Bajty, jednostki kodowe i znaki postrzegane
Właściwość length napisu JavaScript zlicza jednostki kodowe UTF-16. Nie oznacza liczby bajtów pliku UTF-8 ani koniecznie liczby znaków postrzeganych przez człowieka. Uśmiechnięta twarz ‘😀’ to jeden punkt kodowy U+1F600, lecz dwie jednostki UTF-16: '😀'.length daje 2. Jej zapis UTF-8 F0 9F 98 80 zajmuje cztery bajty. Dziesiętna liczba 128512 zwracana przez codePointAt(0) to ten sam numer co U+1F600, a nie liczba znaków czy wielkość pliku. ‘가’ oznacza w tych trzech miarach jeden punkt kodowy, jedną jednostkę UTF-16 i trzy bajty UTF-8. Wyniki różnią się, ponieważ mierzą inne jednostki. Dokumentacja MDN dotycząca długości napisów wyraźnie podaje jednostkę length. Sama ta liczba nie określa także, ile rzeczywistych bajtów pamięci wykorzystuje wewnętrznie silnik JavaScript.
Nawet zliczanie poszczególnych punktów kodowych nie zawsze odpowiada liczbie widocznych znaków. Znak podstawowy może otrzymać znaki łączące, a kilka punktów może utworzyć jedno emoji. Reguły segmentacji tekstu Unicode opisują granice rozszerzonych klastrów grafemowych służących do obsługi takich grup. Nawet Array.from(text), które przechodzi przez napis punktami kodowymi, nie liczy automatycznie tych grup jako jednej jednostki. W przeglądarce można po sprawdzeniu obsługi rozważyć Intl.Segmenter z granularity ustawioną na 'grapheme'. Klaster grafemowy nie musi jednak odpowiadać jednemu rysunkowi czcionki, a oczekiwania dotyczące edycji zależą od języka. Pole krótkiego opisu powinno podawać zrozumiałą liczbę znaków; pamięć i transmisja mogą wymagać osobnych ograniczeń bajtowych. Jeśli interfejs obiecuje „20 znaków”, a ucina tylko jednostki UTF-16, może przeciąć emoji lub znak złożony w środku.
3. Oddzielić zapis, przesyłanie i wyświetlanie
Tekst może stać się nieczytelny, gdy kodowanie przy zapisie różni się od dekodowania przy odczycie. Interpretowanie pliku UTF-8 według innego kodowania może wyświetlać błędne znaki mimo braku zmian w samym pliku. W HTML trzeba wspólnie sprawdzić rzeczywisty format zapisu, charset w nagłówku odpowiedzi HTTP Content-Type i deklarację meta charset na początku dokumentu. Atrybut HTML lang='ko' wskazuje język treści i ma inną rolę niż charset określający interpretację bajtów. Poradnik W3C o deklaracjach kodowania wyjaśnia, że nie wystarczy zmienić deklaracji: plik musi rzeczywiście zostać zapisany jako UTF-8, a informacje serwera i dokumentu muszą być zgodne. Znaczniki kodowania wpływają również na kolejność pierwszeństwa; element meta nie zawsze zastępuje wszystkie inne wskazówki. Różnice między otwarciem lokalnym a pobraniem z serwera wskazują zatem także na etap przesyłania.
Kwadraty wynikające z braku potrzebnych glifów w czcionce mogą mieć inną przyczynę niż błędne dekodowanie. Jeśli dane znaków są poprawne, a brakuje tylko ich kształtu, zmiana kodowania zapisu nie pomoże. Odwrotnie, nowa czcionka nie przywróci oryginału, jeżeli dane zostały już odczytane jako inne znaki. Zachowaj kopię pliku i porównaj kolejno kodowanie wybrane w edytorze, deklaracje odpowiedzi, znaki zinterpretowane przez przeglądarkę i używany font. Nie wnioskuj wyłącznie z wyglądu: sprawdź też punkty kodowe po skopiowaniu tego samego fragmentu. Zanotuj, czy problem występuje tylko w jednej przeglądarce, czy także inne edytory czytają plik błędnie. Powtarzanie zapisu aż do uzyskania wiarygodnego wyglądu może zniszczyć pierwotne bajty. Jeśli deklaracje są poprawne, a uszkodzone są tylko niektóre artykuły, sprawdź również, kiedy treść zmieniła się przed umieszczeniem jej w bazie danych.
4. Sprawdzanie wartości za pomocą TextEncoder i TextDecoder
Proste doświadczenie zacznij od krótkiego napisu utworzonego na potrzeby próby. TextEncoder przekształca napis JavaScript w tablicę bajtów UTF-8. Wynik new TextEncoder().encode('가') to szesnastkowo EA B0 80, a length tablicy wynosi 3. Narzędzia deweloperskie mogą pokazywać te same wartości dziesiętnie jako 234, 176, 128: dane są takie same, inna jest jedynie podstawa zapisu liczby. Porównanie z typową długością napisu równą 1 ujawnia różnicę jednostek. new TextDecoder('utf-8').decode(bytes) zwraca ‘가’, przy czym bytes oznacza wcześniej utworzoną tablicę. Encoder tworzy UTF-8; nie jest uniwersalnym konwerterem z możliwością wyboru dowolnego dawnego kodowania wyjścia. Badając rzeczywisty plik, trzeba korzystać z zachowanych oryginalnych bajtów. Ponowne zakodowanie w UTF-8 tekstu już błędnie odczytanego na ekranie nie dowodzi, jakie bajty znajdowały się w pierwszym pliku.
Przekazanie 'utf-8' i { fatal: true } do konstruktora TextDecoder pozwala zgłaszać błąd dla niepoprawnego wejścia zamiast cicho je zastępować. Pojedynczy bajt FF nie jest na przykład poprawnym UTF-8, więc w tym trybie dekodowanie kończy się błędem. W zwykłym trybie zastępowania może pojawić się znak zastępczy U+FFFD. Brak błędu nie znaczy jednak, że rozpoznano pierwotne kodowanie lub potwierdzono poprawność treści. Niektóre sekwencje bajtów są poprawne w kilku kodowaniach, a wcześniej zmieniony tekst mógł zostać ponownie zapisany jako poprawne UTF-8. Próba sprawdza, czy wybrany dekoder akceptuje wejście. Nie potwierdza automatycznie pochodzenia dokumentu, zgodności z oryginałem ani braku zmian podczas jego przekazywania.
5. Normalizacja ujednolica reprezentacje równoważne
Nawet dwa poprawnie odczytane napisy mogą zawierać różne wewnętrzne sekwencje punktów kodowych. ‘가’ można przedstawić jako samo U+AC00 albo jako łączące początkowe jamo U+1100 oraz samogłoskowe U+1161. Te dwie ostatnie wartości nie są po prostu codziennymi jamo zgodności ‘ㄱ’ i ‘ㅏ’ ustawionymi obok siebie: sprawdzaj również numery. Reprezentacje zwykle wyglądają jak ta sama sylaba, ale mogą różnić się przy bezpośrednim porównaniu napisów JavaScript. Zastosowanie NFC po obu stronach według reguł normalizacji Unicode pozwala porównać ten przykład równoważności kanonicznej w jednej postaci. NFD stosuje dekompozycję kanoniczną. Tutaj postać NFC zajmuje trzy bajty UTF-8, a dwa jamo w NFD sześć: ten sam wygląd nie gwarantuje tego samego rozmiaru. To możliwa przyczyna niedopasowania wyszukiwania do zapisanego imienia lub nazwy wyglądających identycznie. Należy odróżnić ją od błędnej interpretacji bajtów.
Wybór postaci normalizacji również zależy od celu. NFC dotyczy równoważności kanonicznej, natomiast NFKC uwzględnia dekompozycję zgodności i może szerzej zmieniać formy takie jak cyfry pełnej szerokości lub otoczone kółkiem. Bywa to przydatne przy tworzeniu wartości porównawczej do wyszukiwania, ale różnice mogą być znaczące w zapisie matematycznym lub materiale źródłowym. Warto zachować oryginał i określić formę stosowaną do osobnej kopii porównawczej. Normalizacja nie zastępuje porównywania wielkich i małych liter, sortowania językowego czy wyszukiwania słów o tej samej wymowie. Masowa zmiana nazw plików lub identyfikatorów kont może przypisać różnym pozycjom tę samą wartość; najpierw sprawdź reguły nazewnictwa i obsługę kolizji w usłudze. Zapisywanie wszystkich podobnie wyglądających znaków jako jednej wartości nie jest ogólnym celem normalizacji.
6. Zachować oryginał i opisać granice przekształceń
Przed nadpisaniem pliku początkowego zachowaj bajty i zapisz sposób jego otrzymania. Wypróbuj oczekiwane dekodowanie na kopii, a potem sprawdź, czy czytelna treść pozostaje taka sama po ponownym zapisie. Początkowy znacznik UTF-8 opisany w wyjaśnieniu BOM przez W3C ma postać EF BB BF i nie jest obowiązkowy. UTF-8 nie wymaga wyboru kolejności bajtów, więc znacznik nie pełni tej samej roli co wybór kolejności w UTF-16. Dodanie lub usunięcie BOM różni się także od konwersji bajtów treści na inne kodowanie. Jeśli nieczytelne wartości już zastąpiono znakami zastępczymi lub pytajnikami i zapisano, kilka różnych oryginałów mogło zostać scalonych; automatycznego odzyskania nie da się zagwarantować. Możliwość odwrócenia można oceniać dopiero przy znanym pierwotnym kodowaniu i historii zmian. Normalizacja nie przywraca utraconych bajtów.
Nie kończ sprawdzania funkcji wielojęzycznej na jednym angielskim wierszu. Przygotuj krótkie przykłady obejmujące sylaby koreańskie, łacińskie litery ze znakami łączącymi, emoji z płaszczyzny dodatkowej i pisma od prawej do lewej, takie jak arabskie lub hebrajskie; porównaj wpisywanie, zapis, ponowny odczyt i eksport. Na każdym etapie kontroluj wygląd, punkty kodowe, liczbę bajtów UTF-8 i jednostkę liczenia określoną w produkcie. Kierunku czytania nie naprawia się zamianą UTF-8 na inne kodowanie: język i kierunek interfejsu wymagają osobnego sprawdzenia. Przy zmianie limitów wpisywania lub porównań wyszukiwania zachowaj dotychczasowe oryginały i wyjaśnij nowe kryterium. Licznik przeglądarki i limit serwera powinny używać spójnych jednostek; jeśli potrzebne są różne ograniczenia, oznacz każde. Liczby i przykłady kodu tutaj opisują krótkie napisy, a nie pomiar awarii konkretnej usługi. Kolejne sprawdzanie numerów, bajtów, jednostek napisów, wyświetlania i porównywania pozwala odtworzyć zmiany na każdej granicy.