1. El número de un carácter no es su secuencia de bytes
Imagine que exporta un documento coreano y, al abrirlo otra vez, el título aparece como símbolos extraños. No ha cambiado el idioma de quien lo escribió: quizá no coincidan las reglas para leer los números guardados. Unicode asigna números, llamados puntos de código, a caracteres y símbolos de muchas escrituras. UTF-8 es una codificación que representa los valores correspondientes mediante secuencias de bytes. Por ejemplo, la sílaba coreana ‘가’ tiene el punto de código U+AC00. La notación después de U+ es hexadecimal; no indica una posición en el archivo ni una dirección de memoria. Tampoco determina la forma de la fuente o los píxeles de la pantalla. Un mismo punto de código puede dibujarse de distintas maneras según la fuente y la composición del texto. La compatibilidad con Unicode, el almacenamiento UTF-8 y la instalación de fuentes coreanas están relacionados, pero se comprueban por separado. La explicación de UTF de Unicode ayuda a distinguir caracteres y formato de almacenamiento.
En UTF-8, ‘가’ se representa con los tres bytes hexadecimales EA B0 80. La letra latina A tiene el punto de código U+0041 y ocupa un único byte, 41. Un byte consta de ocho bits; incluso dentro del mismo documento, distintos caracteres pueden ocupar cantidades diferentes de bytes. La extensión .txt o .csv tampoco permite conocer por sí sola la codificación real. UTF-8 representa los valores escalares Unicode válidos con entre uno y cuatro bytes. Por eso no se calcula exactamente el tamaño del archivo multiplicando solo el número de caracteres visibles. El ejemplo de esta sílaba no debe generalizarse a todos los signos coreanos ni a todos los idiomas. Codificar transforma información textual en bytes: no es comprimir ni cifrar. La información adicional del formato, los saltos de línea y la compresión durante el transporte pueden cambiar el tamaño total de una misma frase. Separar la transformación entre bytes y caracteres definida por el estándar Encoding de WHATWG del tamaño global o la confidencialidad ayuda a acotar el problema.
2. Bytes, unidades de código y caracteres percibidos
La propiedad length de una cadena JavaScript cuenta unidades de código UTF-16. No representa los bytes de un archivo UTF-8 ni necesariamente los caracteres que percibe una persona. La cara sonriente ‘😀’ es un punto de código, U+1F600, pero utiliza dos unidades UTF-16: '😀'.length devuelve 2. Su representación UTF-8 F0 9F 98 80 ocupa cuatro bytes. El decimal 128512 obtenido con codePointAt(0) es el mismo número que U+1F600, no una cantidad de caracteres ni un tamaño de archivo. ‘가’ corresponde, según estas medidas, a un punto de código, una unidad UTF-16 y tres bytes UTF-8. Los resultados difieren porque cambian las unidades. La documentación de MDN sobre length especifica esta unidad. Ese valor tampoco basta para afirmar cuántos bytes de memoria utiliza realmente un motor JavaScript.
Contar los puntos de código uno por uno tampoco coincide siempre con los caracteres visibles. Un carácter base puede llevar marcas combinantes, y varios puntos pueden formar un solo emoji. Las reglas Unicode de segmentación describen los límites de los grupos de grafemas extendidos para tratar esos conjuntos. Incluso Array.from(text), que recorre una cadena por puntos de código, no los cuenta automáticamente como una unidad. En el navegador puede estudiarse Intl.Segmenter con granularity configurada como 'grapheme', después de comprobar su disponibilidad. Sin embargo, un grupo de grafemas no equivale necesariamente a un único dibujo de la fuente, y las expectativas de edición varían según el idioma. Un campo de presentación necesita un contador comprensible para el usuario; el almacenamiento y las comunicaciones pueden requerir además límites en bytes. Si la interfaz anuncia «20 caracteres» pero corta únicamente unidades UTF-16, puede dividir por la mitad un emoji o un carácter combinado.
3. Separar almacenamiento, transmisión y visualización
El texto puede estropearse cuando la codificación usada al escribir difiere de la decodificación aplicada al leer. Interpretar un archivo UTF-8 con otra codificación puede mostrar caracteres equivocados aunque el archivo no haya cambiado. En HTML deben compararse el formato real guardado, el charset de Content-Type en la respuesta HTTP y la declaración meta charset al principio del documento. El atributo HTML lang='ko' informa del idioma del contenido: tiene una función distinta del charset que determina cómo se interpretan los bytes. La guía del W3C sobre declaraciones de codificación explica que no basta con cambiar la declaración: también debe guardarse realmente el archivo en UTF-8 y hacer coincidir las indicaciones del servidor y el documento. Las marcas de codificación pueden afectar al orden de prioridad; una etiqueta meta no siempre anula el resto. Si el resultado cambia entre abrir el archivo directamente y recibirlo del servidor, la transmisión ofrece una pista importante.
Los cuadrados que aparecen porque una fuente no contiene los glifos necesarios pueden tener otra causa que una decodificación incorrecta. Si los datos son correctos y solo falta una forma para dibujarlos, cambiar la codificación de guardado no resuelve nada. Del mismo modo, otra fuente no devuelve los caracteres originales si ya se han interpretado como caracteres diferentes. Conserve una copia del archivo y compare, por este orden, la codificación seleccionada en el editor, las declaraciones de la respuesta, los caracteres interpretados por el navegador y la fuente utilizada. No concluya solo por el aspecto: compruebe también qué puntos de código aparecen al copiar ese mismo fragmento. Anote si solo falla un navegador o si otros editores también leen mal el archivo. Guardar repetidamente hasta que el texto parezca normal puede hacer perder los bytes originales. Si todas las declaraciones son correctas pero solo ciertas publicaciones se ven mal, investigue también en qué etapa se modificó su contenido antes de entrar en la base de datos.
4. Comprobar los valores con TextEncoder y TextDecoder
Empiece un experimento sencillo con una cadena corta creada para la prueba. TextEncoder transforma una cadena JavaScript en un arreglo de bytes UTF-8. El resultado de new TextEncoder().encode('가') es EA B0 80 en hexadecimal, y la length del arreglo es 3. Las herramientas de desarrollo pueden mostrar esos mismos valores como 234, 176, 128 en decimal: no son datos diferentes, sino otra base numérica. Compare esa cifra con la longitud habitual 1 de la cadena para ver el cambio de unidad. new TextDecoder('utf-8').decode(bytes) devuelve ‘가’, donde bytes designa el arreglo generado antes. Este codificador produce UTF-8; no es un conversor universal que permita elegir cualquier codificación antigua de salida. Al investigar un archivo real hay que partir de sus bytes originales preservados. Volver a codificar en UTF-8 un texto que ya se leyó mal en pantalla no demuestra qué bytes tenía el primer archivo.
Si se pasan 'utf-8' y { fatal: true } al constructor TextDecoder, las entradas inválidas pueden provocar un error en lugar de sustituirse silenciosamente. Por ejemplo, un byte FF aislado no es UTF-8 válido y falla su decodificación en este modo. En el modo habitual de sustitución puede aparecer el carácter de reemplazo U+FFFD. No obtener errores, sin embargo, no demuestra que se haya identificado la codificación original ni confirmado la exactitud del contenido. Hay bytes válidos en varias codificaciones, y un texto ya alterado puede haberse guardado de nuevo como UTF-8 válido. La prueba indica si el decodificador elegido acepta la entrada. No verifica automáticamente la procedencia del documento, su coincidencia con el original ni la ausencia de modificaciones durante su recorrido.
5. La normalización alinea representaciones equivalentes
Incluso dos cadenas correctamente leídas pueden contener distintas secuencias internas de puntos de código. ‘가’ puede representarse mediante U+AC00 o mediante el jamo inicial combinante U+1100 seguido del vocálico U+1161. Estos dos últimos no son simplemente los jamos de compatibilidad cotidianos ‘ㄱ’ y ‘ㅏ’ puestos uno tras otro: revise también sus números. Las dos formas suelen dibujarse como la misma sílaba, pero pueden no ser iguales en una comparación directa de cadenas JavaScript. Aplicar NFC a ambos lados conforme a las reglas Unicode de normalización permite comparar con una forma común este caso de equivalencia canónica. NFD emplea descomposición canónica. Aquí la forma NFC ocupa tres bytes UTF-8 y los dos jamos de la forma NFD seis; el mismo aspecto no asegura el mismo espacio de almacenamiento. Esta diferencia puede explicar que una búsqueda y un nombre guardado aparentemente idénticos no coincidan. No es lo mismo que interpretar los bytes con una codificación errónea.
La forma de normalización también depende del propósito. NFC trata equivalencia canónica; NFKC incorpora descomposición de compatibilidad y puede cambiar de manera más amplia representaciones como dígitos de ancho completo o encerrados en un círculo. Esto puede ayudar a crear valores para comparar búsquedas, mientras que en notación matemática o documentos originales las diferencias pueden ser significativas. Conviene conservar el original y decidir qué transformación aplicar a una copia de comparación. Normalizar no sustituye la comparación entre mayúsculas y minúsculas, la ordenación lingüística ni la búsqueda de palabras con igual pronunciación. Convertir masivamente nombres de archivo o identificadores de cuentas puede hacer que elementos distintos produzcan el mismo valor: antes deben revisarse las reglas de nombres y el tratamiento de colisiones del servicio. Guardar bajo el mismo valor todos los signos que se parecen no es el objetivo general de la normalización.
6. Conservar el original y registrar las transformaciones
Antes de sobrescribir el archivo inicial, preserve sus bytes y anote de dónde procede. Pruebe la decodificación prevista en una copia y compruebe si el texto legible se mantiene al volver a guardarlo. La marca inicial UTF-8 descrita en la explicación del BOM del W3C es EF BB BF y no es obligatoria. UTF-8 no necesita elegir un orden de bytes, por lo que esta marca no tiene la misma función que la indicación de orden de UTF-16. Añadir o quitar un BOM tampoco equivale a convertir los bytes del cuerpo a otra codificación. Si valores ilegibles ya se sustituyeron por caracteres de reemplazo o interrogaciones y se guardaron, varios originales pueden haberse fusionado y no puede garantizarse su recuperación automática. Solo con la codificación original y el historial de transformaciones se puede valorar si es posible invertirlas. La normalización no recupera bytes perdidos.
No termine la comprobación de una función multilingüe con una sola línea en inglés. Prepare frases cortas con sílabas coreanas, letras latinas y marcas combinantes, un emoji de un plano suplementario y escrituras de derecha a izquierda como árabe o hebreo; compare entrada, guardado, recuperación y exportación. En cada etapa revise el aspecto, los puntos de código, los bytes UTF-8 y la unidad de conteo fijada por el producto. La dirección de lectura no se soluciona cambiando UTF-8 por otra codificación: deben revisarse por separado el idioma y la dirección de la interfaz. Al modificar límites de entrada o comparaciones de búsqueda, conserve también los originales y explique el nuevo criterio. El contador del navegador y el límite de aceptación del servidor deben usar unidades coherentes; si hacen falta límites distintos, identifique cada uno. Las cifras y ejemplos de código de este artículo muestran transformaciones de cadenas pequeñas, no mediciones de una avería de un servicio concreto. Separar números de carácter, bytes guardados, unidades de cadena, visualización y comparación permite reproducir qué cambió en cada frontera.