1. Số hiệu ký tự khác với các byte được lưu
Giả sử bạn xuất một tài liệu tiếng Hàn rồi mở lại, nhưng tiêu đề hiện thành các ký tự lạ. Ngôn ngữ của người gõ không thay đổi; có thể quy tắc đọc những con số đã lưu không khớp. Unicode là tiêu chuẩn cấp số hiệu gọi là điểm mã cho nhiều chữ và ký hiệu, còn UTF-8 là cách mã hóa biểu diễn giá trị tương ứng bằng một chuỗi byte. Chẳng hạn, âm tiết Hangul ‘가’ có điểm mã U+AC00. Phần sau U+ được viết ở hệ thập lục phân, không chỉ thứ tự trong tệp hay địa chỉ bộ nhớ. Số hiệu này không quy định hình dạng trong phông chữ hoặc vị trí điểm ảnh trên màn hình. Cùng một điểm mã có thể được vẽ khác nhau tùy phông và môi trường dàn trang. Vì vậy, hỗ trợ Unicode, lưu bằng UTF-8 và cài phông Hangul là những việc liên quan nhưng cần kiểm tra riêng. Giải thích UTF của Unicode là điểm khởi đầu để phân biệt ký tự với định dạng lưu trữ.
Trong UTF-8, ‘가’ trở thành ba byte thập lục phân EA B0 80. Ngược lại, chữ A có điểm mã U+0041 và chỉ dùng một byte 41 trong UTF-8. Một byte gồm tám bit, nên số byte của từng ký tự trong cùng tài liệu có thể khác nhau. Đuôi .txt hay .csv cũng không đủ để xác định cách mã hóa thực tế. UTF-8 biểu diễn các giá trị vô hướng Unicode hợp lệ bằng một đến bốn byte; do đó không thể tính chính xác kích thước tệp chỉ bằng cách nhân số ký tự nhìn thấy với một hằng số. Ví dụ về âm tiết Hangul này cũng không áp dụng cho mọi ký hiệu liên quan đến Hangul hay mọi ngôn ngữ. Mã hóa là quy tắc chuyển thông tin ký tự thành byte, không phải nén hay mã hóa bảo mật. Tổng kích thước của cùng một câu còn có thể khác do thông tin bổ sung của định dạng tệp, cách xuống dòng và nén khi truyền. Tách phép chuyển đổi giữa byte và ký tự trong WHATWG Encoding Standard khỏi dung lượng toàn tệp và chức năng giữ bí mật sẽ giúp thu hẹp vấn đề.
2. Số byte, đơn vị mã và số ký tự nhìn thấy
Trong JavaScript, length của một chuỗi trả về số đơn vị mã UTF-16, không phải số byte của tệp UTF-8 hay số ký tự theo cảm nhận con người. Ví dụ, khuôn mặt cười ‘😀’ là một điểm mã U+1F600 nhưng cần hai đơn vị mã UTF-16, nên '😀'.length bằng 2. Khi mã hóa thành UTF-8, kết quả là F0 9F 98 80, gồm bốn byte. Số thập phân 128512 lấy bằng codePointAt(0) là cùng số hiệu với U+1F600, không phải số ký tự hay kích thước tệp. Theo ba thước đo đó, ‘가’ có một điểm mã, một đơn vị mã UTF-16 và ba byte UTF-8. Các con số cho cùng một chuỗi khác nhau vì đơn vị đo khác nhau. Giải thích độ dài chuỗi của MDN nêu rõ đơn vị của length. Chỉ từ số này cũng không thể khẳng định bộ máy JavaScript thực sự dùng bao nhiêu byte bộ nhớ.
Ngay cả đếm từng điểm mã cũng không luôn bằng số ký tự mà người dùng thấy. Một ký tự cơ sở có thể đi cùng dấu kết hợp, hoặc nhiều điểm mã tạo thành một emoji. Quy tắc phân đoạn văn bản Unicode giải thích ranh giới cụm tự vị mở rộng để xử lý các nhóm như vậy. Việc trải chuỗi theo từng điểm mã bằng Array.from(text) cũng không tự động đếm cả tổ hợp nhiều điểm mã thành một nhóm. Để đếm trong trình duyệt, hãy kiểm tra khả năng hỗ trợ trước khi cân nhắc Intl.Segmenter với granularity là 'grapheme'. Tuy nhiên, cụm tự vị không được bảo đảm tương ứng một-một với một hình mà phông vẽ ra; kỳ vọng khi biên tập cũng có thể khác theo ngôn ngữ. Ô giới thiệu nên dùng cách đếm ký tự người dùng hiểu, còn giới hạn lưu trữ hay truyền thông nên áp dụng số byte riêng. Nếu giao diện ghi ‘20 ký tự’ nhưng bên trong chỉ cắt theo đơn vị mã UTF-16, nó có thể cắt đôi emoji hoặc ký tự kết hợp.
3. Khi tiếng Hàn bị lỗi, tách lưu, truyền và hiển thị
Lỗi ký tự có thể xuất hiện khi cách mã hóa dùng để ghi byte khác với cách giải mã dùng để đọc. Tệp UTF-8 bị diễn giải theo mã hóa khác có thể hiện ký tự sai dù chính tệp chưa đổi. Với HTML, cần kiểm tra đồng thời định dạng lưu thực tế, charset trong Content-Type của phản hồi HTTP và khai báo meta charset ở đầu tài liệu. Thuộc tính HTML lang='ko' cho biết ngôn ngữ nội dung; vai trò của nó khác charset chọn cách đọc byte Hangul. Hướng dẫn khai báo mã hóa của W3C giải thích rằng không chỉ sửa khai báo thành UTF-8 mà phải lưu tệp thật bằng UTF-8 và làm cho khai báo của máy chủ khớp với tài liệu. Dấu hiệu mã hóa cũng có thể ảnh hưởng thứ tự ưu tiên xác định, nên không được cho rằng một meta luôn ghi đè mọi thông tin khác. Nếu mở trực tiếp tệp và nhận qua máy chủ cho kết quả khác nhau, cấu hình khâu truyền là một đầu mối quan trọng.
Phông thiếu hình chữ cần dùng khiến văn bản thành ô vuông có thể có nguyên nhân khác với giải mã sai. Nếu dữ liệu ký tự đúng nhưng không tìm được hình để vẽ, đổi mã hóa lưu trữ không giải quyết được. Ngược lại, thay phông cũng không khôi phục ký tự gốc đã bị diễn giải thành ký tự khác. Khi điều tra, giữ một bản sao tệp lỗi rồi lần lượt so sánh mã hóa trình soạn thảo chọn, khai báo phản hồi máy chủ, ký tự trình duyệt đã diễn giải và phông đang dùng. Đừng kết luận chỉ từ vẻ ngoài; hãy xem khi sao chép cùng đoạn thì nhận được những điểm mã nào. Ghi lại lỗi chỉ xảy ra trong một trình duyệt hay tệp cũng bị đọc sai ở trình soạn thảo khác sẽ giúp tách lỗi hiển thị khỏi biến đổi dữ liệu. Lưu đi lưu lại đến khi chữ trông đúng có thể làm mất byte gốc. Nếu mọi khai báo trang đều đúng nhưng chỉ một bài bị lạ, cần lần theo cả những bước biến đổi trước khi nội dung đi vào cơ sở dữ liệu.
4. Đối chiếu cùng giá trị bằng TextEncoder và TextDecoder
Thử nghiệm đơn giản nên bắt đầu với một chuỗi ngắn tự tạo. TextEncoder chuyển chuỗi JavaScript thành mảng byte UTF-8. Kết quả của new TextEncoder().encode('가') khi đọc ở hệ thập lục phân là EA B0 80, và length của mảng là 3. Công cụ phát triển có thể hiển thị cùng giá trị ở dạng thập phân 234, 176, 128; đó không phải dữ liệu khác mà chỉ khác hệ đếm. So với length thông thường bằng 1 của chuỗi ấy, sự khác biệt đơn vị sẽ rõ hơn. Đọc mảng bằng new TextDecoder('utf-8').decode(bytes) sẽ trả lại ‘가’; bytes ở đây là mảng byte đã tạo phía trước. Bộ mã hóa này tạo UTF-8, không phải công cụ chuyển đổi đa năng để lưu theo bất cứ mã hóa cũ nào. Khi kiểm tra tệp thực, hãy dùng byte gốc thu được trước đó. Mã hóa lại câu trên màn hình vốn đã bị đọc sai thành UTF-8 không chứng minh tệp ban đầu chứa những byte nào.
Chỉ định 'utf-8' và { fatal: true } cho hàm khởi tạo TextDecoder khiến đầu vào không đúng quy tắc được xử lý thành lỗi thay vì thay thế âm thầm. Ví dụ, một byte FF đơn lẻ không phải đầu vào UTF-8 hợp lệ, nên giải mã trong chế độ này thất bại. Ở chế độ thay thế mặc định, phần sai có thể thành ký tự thay thế U+FFFD. Tuy nhiên, không có lỗi không có nghĩa là đã tìm đúng mã hóa gốc hoặc xác nhận nội dung chính xác. Một số byte dùng chung được cho nhiều mã hóa, và kết quả đã biến thành ký tự khác cũng có thể được lưu lại thành UTF-8 hợp lệ. Phép kiểm tra này chỉ xác định bộ giải mã đã chọn có chấp nhận đầu vào không. Nó không tự động xác minh nguồn tài liệu, nội dung khớp bản gốc hay tệp có bị sửa giữa đường hay không.
5. Chuẩn hóa là bước riêng để thống nhất cách biểu diễn
Hai chuỗi được đọc đúng vẫn có thể có thứ tự điểm mã khác nhau. ‘가’ có thể được biểu diễn bằng một U+AC00, hoặc bằng chuỗi phụ âm đầu kết hợp U+1100 và nguyên âm U+1161. Hai giá trị sau khác với ví dụ chỉ xếp cạnh nhau các jamo tương thích thông thường ‘ㄱ’ và ‘ㅏ’, nên phải kiểm tra cả số hiệu. Hai cách biểu diễn thường vẽ thành cùng âm tiết, nhưng phép so sánh chuỗi thô của JavaScript có thể coi chúng không bằng nhau. Áp dụng NFC theo quy tắc chuẩn hóa Unicode cho cả hai sẽ đưa ví dụ tương đương chính tắc này về cùng cách biểu diễn để so sánh. NFD dùng phân rã chính tắc. Trong ví dụ này, UTF-8 của NFC có ba byte, còn hai jamo của NFD có sáu byte; hình thức giống nhau không bảo đảm kích thước lưu giống nhau. Khi từ khóa tìm kiếm và tên đã lưu nhìn giống mà không khớp, có thể khảo sát khác biệt biểu diễn này, tách biệt với lỗi ký tự do diễn giải byte sai.
Chọn dạng chuẩn hóa cũng phụ thuộc mục đích. NFC xử lý tương đương chính tắc, trong khi NFKC có phân rã tương thích nên có thể thay đổi rộng hơn những biểu diễn đầu vào vốn phân biệt nhau, như chữ số toàn chiều rộng hoặc số nằm trong vòng tròn. Điều này có thể hữu ích khi tạo giá trị so sánh cho tìm kiếm, nhưng sự khác nhau có thể có ý nghĩa trong ký hiệu toán học hay tư liệu gốc. Vì vậy, nên giữ bản gốc và quyết định áp dụng dạng nào cho bản sao dùng để so sánh khi cần. Chuẩn hóa không thay việc so sánh chữ hoa-thường, sắp xếp theo ngôn ngữ hay tìm từ đồng âm. Chuyển hàng loạt tên tệp hoặc định danh tài khoản sẵn có có thể nhập các mục khác nhau thành cùng giá trị so sánh, nên trước hết cần xem quy tắc đặt tên của dịch vụ và cách xử lý xung đột. Việc lưu mọi ký tự có hình gần giống nhau thành một giá trị không phải mục tiêu chung của chuẩn hóa.
6. Giữ nguyên bản gốc và ghi lại ranh giới chuyển đổi
Khi thu hẹp nguyên nhân hỏng dữ liệu, hãy giữ byte gốc và đường thu nhận trước khi ghi đè tệp ban đầu. Thử đọc bản sao bằng mã hóa dự kiến, rồi kiểm tra khi lưu lại kết quả trông đúng thì nội dung còn nguyên không. Dấu đầu UTF-8 trong giải thích BOM của W3C là EF BB BF, nhưng các byte này không bắt buộc. UTF-8 không cần chọn thứ tự byte, nên không thể hiểu dấu đó có vai trò giống việc chọn thứ tự byte trong UTF-16. Thêm hay xóa BOM cũng khác với đổi byte nội dung sang mã hóa khác. Nếu những giá trị không đọc được đã được lưu thành ký tự thay thế hoặc dấu hỏi, nhiều giá trị gốc có thể đã gộp thành một, nên không thể bảo đảm tự động phục hồi. Chỉ khi biết mã hóa gốc và lịch sử chuyển đổi mới có thể đánh giá khả năng đảo ngược; chuẩn hóa không phải công cụ lấy lại byte đã mất.
Khi xây dựng tính năng đa ngôn ngữ, đừng kết thúc kiểm tra bằng một dòng tiếng Anh. Hãy đưa âm tiết Hangul, chữ Latin có dấu kết hợp, emoji ở mặt phẳng bổ sung và chữ viết phải-sang-trái như Ả Rập hay Do Thái vào cùng nhóm câu ngắn, rồi so sánh nhập, lưu, truy vấn lại và xuất. Ở mỗi bước, cần kiểm tra hình hiển thị, điểm mã, số byte UTF-8 và tiêu chuẩn đếm ký tự của sản phẩm. Hướng viết không phải thuộc tính giải quyết bằng cách đổi UTF-8 sang mã hóa khác; ngôn ngữ và hướng của giao diện cũng cần được xem riêng. Khi sửa giới hạn nhập hay so sánh tìm kiếm, vẫn phải giữ nguyên bản gốc và giải thích tiêu chí thay đổi. Kiểm tra con số hướng dẫn trong trình duyệt và giới hạn chấp nhận của máy chủ có cùng đơn vị không; nếu cần các giới hạn khác nhau, hiển thị từng giới hạn. Số và mã trong bài chỉ minh họa chuyển đổi chuỗi ngắn, không phải kết quả đo sự cố của một dịch vụ cụ thể. Lần lượt xem số hiệu ký tự, byte lưu, đơn vị chuỗi và quy tắc hiển thị, so sánh sẽ giúp tái hiện điều gì thay đổi ở mỗi ranh giới.