The Bug That Cost content: 00K
Eran las 2 de la madrugada. Llegó el correo electrónico del director general: "¿Por qué nuestro sitio en francés muestra un galimatías?"
En lugar de "Café", los usuarios veían "Café". En lugar de "Résumé", veían "Résumé".
Clásico mojibake. El desarrollador había configurado la base de datos en Latin-1, la API en UTF-8 y el frontend en ASCII. Tres codificaciones diferentes, una experiencia de usuario completamente rota.
¿La solución? Cinco minutos. ¿Los daños? Ventas perdidas, usuarios enfadados, 2 semanas de parches de emergencia.
Esta guía lo evita. Explicaremos qué es realmente la codificación de caracteres, por qué es importante y cómo no volver a meter la pata.
¿Qué es la codificación de caracteres?
Los ordenadores no entienden de letras. Entienden números.
La codificación de caracteres es el mapa: carácter → número.
Ejemplo:
- La letra "A" tiene que convertirse en un número que los ordenadores puedan almacenar
- ASCII dice: "A" = 65
- Unicode dice: "A" = U+0041
- UTF-8 dice: "A" = el byte
0x41
Simple, ¿verdad? Excepto que hay como 50 estándares de codificación diferentes, cada uno con reglas distintas.
Los tres que realmente necesitas saber
1. ASCII (El Antiguo)
Qué es: Código Estándar Americano para el Intercambio de Información Inventado: 1963 Caracteres: 128 (0-127)
Qué cubre:
- Letras inglesas (A-Z, a-z)
- Números (0-9)
- Puntuación básica (.,!?)
- Caracteres de control (nueva línea, tabulador)
**Lo que no incluye
- Caracteres acentuados (é, ñ, ü)
- Escrituras no latinas (中文, العربية, हिन्दी)
- Emojis (💩)
- Básicamente cualquier cosa útil para i18n
Cuándo usarlo: Nunca. Estamos en 2026. A menos que estés programando un terminal de los 80.
Ejemplo:
A → 65
B → 66
Z → 90
a → 97
0 → 48
2. Unicode (La Biblioteca)
Qué es: Un catálogo masivo de todos los caracteres de todos los idiomas Versión actual: Unicode 15.1 (2023), 149.813 caracteres Piénsalo como: La agenda, no el teléfono
Importante: Unicode NO es una codificación. Es un conjunto de caracteres.
Unicode asigna a cada carácter un punto de código (un número):
- "A" = U+0041
- "é" = U+00E9
- "中" = U+4E2D
- "🔥" = U+1F525
Pero no dice cómo almacenar esos números. Ahí es donde entra UTF-8.
Planos Unicode:
- BMP (Plano Multilingüe Básico): U+0000 a U+FFFF (caracteres más comunes)
- SMP (Supplementary Multilingual Plane): U+10000 a U+1FFFF (emojis, scripts poco comunes)
- SIP, TIP, SSP: Escrituras antiguas, símbolos matemáticos, notación musical
3. UTF-8 (La única codificación verdadera)
Qué es: Una forma de codificar caracteres Unicode como bytes Inventado: 1992 por Ken Thompson y Rob Pike Cuota de mercado: 98% de todos los sitios web (a partir de 2026)
¿Por qué ganó?
- Compatible con ASCII: Los primeros 128 caracteres son idénticos
- Anchura variable: Utiliza de 1 a 4 bytes dependiendo del carácter
- Autosincronización: Si salta en medio de un flujo UTF-8, puede encontrar el siguiente límite de caracteres
- Eficiente: El texto en inglés tiene el mismo tamaño que ASCII, pero admite todos los idiomas
Cómo funciona:
Character | Code Point | UTF-8 Bytes | Size
----------|-----------|-------------|-----
A | U+0041 | 0x41 | 1 byte
é | U+00E9 | 0xC3 0xA9 | 2 bytes
中 | U+4E2D | 0xE4 0xB8 0xAD | 3 bytes
🔥 | U+1F525 | 0xF0 0x9F 0x94 0xA5 | 4 bytes
Otras codificaciones UTF que verás:
- UTF-16: Utiliza 2 ó 4 bytes. Común en Windows, Java, JavaScript internos
- UTF-32: Siempre 4 bytes. Despilfarrador pero sencillo
- UTF-7: Existe, pero nunca lo usarás
La regla: Usa UTF-8 en todas partes. Punto.
Desastres comunes de codificación
Desastre 1: Mojibake (文字化け)
Síntoma: El texto parece basura aleatoria.
Ejemplo:
Expected: "Café"
Actual: "Café"
Qué ha pasado:
- El texto se codificó como UTF-8:
Café→0x43 0x61 0x66 0xC3 0xA9 - El lector lo interpretó como Latin-1 (ISO-8859-1)
- Latin-1 no entiende caracteres multibyte
- Cada byte se convertía en un carácter independiente: "C", "a", "f", "Ã", "©"
Fix:
JavaScript1// Detect encoding (not 100% reliable, but helps) 2import jschardet from 'jschardet'; 3 4const buffer = fs.readFileSync('file.txt'); 5const detected = jschardet.detect(buffer); 6console.log(detected.encoding); // 'UTF-8', 'ISO-8859-1', etc. 7 8// Convert to UTF-8 9const iconv = require('iconv-lite'); 10const utf8String = iconv.decode(buffer, detected.encoding);
Desastre 2: Desajuste en la codificación de la base de datos
Síntoma: Los datos se ven bien en el código, pero no en la base de datos (o viceversa).
Ejemplo (MySQL):
SQL1-- ❌ Wrong: latin1 database 2CREATE DATABASE mydb CHARACTER SET latin1; 3 4-- ✅ Right: utf8mb4 (supports emojis) 5CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
La trampa UTF-8 de MySQL:
- El conjunto de caracteres
utf8de MySQL NO es UTF-8 real (máximo 3 bytes) - Los emojis necesitan 4 bytes →
utf8no puede almacenarlos - Utilice siempre
utf8mb4(UTF-8, máx. 4 bytes)
Comprueba tu base de datos:
SQL1SHOW VARIABLES LIKE 'character_set_%'; 2SHOW VARIABLES LIKE 'collation_%'; 3 4-- Should all be utf8mb4
La cadena de conexión debe coincidir:
JavaScript1// Node.js MySQL 2const connection = mysql.createConnection({ 3 host: 'localhost', 4 user: 'root', 5 database: 'mydb', 6 charset: 'utf8mb4' // ← CRITICAL 7});
Postgres:
SQL1-- Check encoding 2SHOW SERVER_ENCODING; 3SHOW CLIENT_ENCODING; 4 5-- Set to UTF-8 (usually default) 6SET CLIENT_ENCODING TO 'UTF8';
Desastre 3: El infierno de la codificación JSON
Síntoma: Los caracteres especiales se convierten en secuencias de escape \uXXXX.
Ejemplo:
JavaScriptconst data = { message: "Hello 世界" }; console.log(JSON.stringify(data)); // {"message":"Hello \u4e16\u754c"}
Por qué: JSON.stringify escapa no ASCII por defecto.
Fix:
JavaScript1// Don't escape Unicode 2JSON.stringify(data, null, 2); 3// Still escapes... that's actually correct! 4 5// The real issue: receiving end must parse correctly 6const parsed = JSON.parse('{"message":"Hello \u4e16\u754c"}'); 7console.log(parsed.message); // "Hello 世界" ✅
Problema real normalmente:
JavaScript1// ❌ Wrong: Sending JSON as Latin-1 2res.setHeader('Content-Type', 'application/json; charset=ISO-8859-1'); 3 4// ✅ Right: UTF-8 5res.setHeader('Content-Type', 'application/json; charset=UTF-8');
Desastre 4: Corrupción en la exportación de CSV
Síntoma: Exportar a CSV, abrir en Excel, todos los caracteres especiales rotos.
Por qué: Excel por defecto a la codificación de su sistema (a menudo Windows-1252, no UTF-8).
Fix: Añadir BOM (Byte Order Mark)
JavaScript1// Add UTF-8 BOM so Excel knows it's UTF-8 2const BOM = '\uFEFF'; 3const csv = BOM + 'Name,City\n' + 4 'José,São Paulo\n' + 5 '李明,北京\n'; 6 7fs.writeFileSync('export.csv', csv, 'utf8');
O forzar UTF-8 en la importación: Excel → Datos → Desde texto → Origen del archivo: 65001 (UTF-8)
Desastre 5: Problemas de codificación de URL
Síntoma: Las URL con caracteres no ASCII se rompen.
Ejemplo:
Raw: /search?q=café
Broken: /search?q=caf�
Correct: /search?q=caf%C3%A9
Corrección: Codifique siempre las URL
JavaScript1// ❌ Wrong 2const url = `/search?q=${query}`; 3 4// ✅ Right 5const url = `/search?q=${encodeURIComponent(query)}`; 6 7// Example 8encodeURIComponent('café'); // 'caf%C3%A9' 9encodeURIComponent('中文'); // '%E4%B8%AD%E6%96%87'
Descodificación:
JavaScriptconst query = new URLSearchParams(window.location.search).get('q'); // Automatically decoded ✅
Cómo depurar problemas de codificación
Paso 1: Encontrar dónde se produce el error de codificación
Los problemas de codificación ocurren en los límites:
- Lectura de archivos
- Consultas a bases de datos
- Peticiones/respuestas HTTP
- Concatenación de cadenas de diferentes fuentes
Script de depuración:
JavaScript1function debugEncoding(text) { 2 console.log('Text:', text); 3 console.log('Length:', text.length); 4 console.log('Bytes:', Buffer.from(text, 'utf8')); 5 console.log('Hex:', Buffer.from(text, 'utf8').toString('hex')); 6 7 // Check each character 8 for (let i = 0; i < text.length; i++) { 9 const char = text[i]; 10 const code = text.charCodeAt(i); 11 const unicode = 'U+' + code.toString(16).toUpperCase().padStart(4, '0'); 12 console.log(`[${i}] ${char} → ${code} (${unicode})`); 13 } 14} 15 16debugEncoding('Café'); 17// [0] C → 67 (U+0043) 18// [1] a → 97 (U+0061) 19// [2] f → 102 (U+0066) 20// [3] é → 233 (U+00E9)
Paso 2: Inspeccionar secuencias de bytes
Si ve mojibake, compruebe los bytes:
JavaScript1const broken = 'Café'; 2console.log(Buffer.from(broken, 'utf8').toString('hex')); 3// 43 61 66 c3 83 c2 a9 4 5// Compare to correct: 6const correct = 'Café'; 7console.log(Buffer.from(correct, 'utf8').toString('hex')); 8// 43 61 66 c3 a9
Fíjate en la doble codificación: c3 83 c2 a9 frente a c3 a9.
Lo que pasó:
- "é" = bytes UTF-8:
c3 a9 - Esos bytes se interpretaron como Latin-1 → "é"
- "é" fue recodificado a UTF-8 →
c3 83 c2 a9
Corrección: Doble decodificación
JavaScriptconst broken = 'Café'; const fixed = Buffer.from(broken, 'latin1').toString('utf8'); console.log(fixed); // 'Café' ✅
Paso 3: Comprobar cada capa
Lista de comprobación de la aplicación web:
1. HTML:
HTML<!-- ✅ Add this to every page --> <meta charset="UTF-8">
2. Cabeceras HTTP:
JavaScript// ✅ Server response res.setHeader('Content-Type', 'text/html; charset=UTF-8');
3. Base de datos:
SQL1-- ✅ MySQL 2CREATE TABLE users ( 3 name VARCHAR(255) 4) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 5 6-- ✅ PostgreSQL (usually default) 7CREATE DATABASE mydb ENCODING 'UTF8';
4. Conexión a la base de datos:
JavaScript1// ✅ Specify in connection string 2const pool = new Pool({ 3 connectionString: 'postgres://user:pass@localhost/mydb?client_encoding=UTF8' 4});
5. E/S de archivos:
JavaScript// ✅ Explicitly set encoding fs.writeFileSync('file.txt', content, 'utf8'); fs.readFileSync('file.txt', 'utf8');
6. Solicitudes API:
JavaScript1// ✅ Set Content-Type header 2fetch('/api/data', { 3 method: 'POST', 4 headers: { 5 'Content-Type': 'application/json; charset=UTF-8' 6 }, 7 body: JSON.stringify({ text: 'Café' }) 8});
Buenas prácticas
1. UTF-8 en todas partes
Mantra: UTF-8 desde el almacenamiento hasta la visualización.
Lista de comprobación de configuración:
- ✅ Base de datos: UTF-8 / utf8mb4
- ✅ Conexión de base de datos: charset=utf8mb4
- ✅ Archivos: Guardar como UTF-8 (comprueba la configuración de tu editor)
- ✅ HTML:
<meta charset="UTF-8"> - ✅ HTTP:
Content-Type: ...; charset=UTF-8 - ✅ Código: Leer/escribir archivos con
encoding='utf8'
2. Validar entrada
Rechazar secuencias UTF-8 no válidas:
JavaScript1function isValidUTF8(str) { 2 try { 3 // Try encoding round-trip 4 const encoded = new TextEncoder().encode(str); 5 const decoded = new TextDecoder('utf-8', { fatal: true }).decode(encoded); 6 return decoded === str; 7 } catch (e) { 8 return false; 9 } 10} 11 12// Usage in API 13app.post('/api/comment', (req, res) => { 14 const { text } = req.body; 15 16 if (!isValidUTF8(text)) { 17 return res.status(400).json({ error: 'Invalid UTF-8 encoding' }); 18 } 19 20 // Continue... 21});
3. Normalizar Unicode
Problema: Múltiples formas de codificar el mismo carácter.
Ejemplo:
JavaScript1// "é" can be: 2const composed = 'é'; // Single code point U+00E9 3const decomposed = 'é'; // e (U+0065) + ´ (U+0301) 4 5console.log(composed === decomposed); // false 😱 6console.log(composed.length); // 1 7console.log(decomposed.length); // 2
**Corrección: Normalizar antes de comparar
JavaScript1const a = 'café'.normalize('NFC'); 2const b = 'café'.normalize('NFC'); 3console.log(a === b); // true ✅ 4 5// Forms: 6// NFC (Canonical Composition) - use this for display 7// NFD (Canonical Decomposition) - use for searching 8// NFKC/NFKD (Compatibility) - use for normalization
Normalizar en consultas de base de datos:
JavaScript1// Search ignoring normalization 2const searchTerm = userInput.normalize('NFD'); 3const results = await db.query( 4 'SELECT * FROM products WHERE LOWER(name) LIKE LOWER(content: )', 5 [`%${searchTerm}%`] 6);
4. Límites de longitud
Tenga cuidado con los límites de caracteres:
JavaScript1// ❌ Wrong: Byte length != character length 2const text = '中文测试'; 3console.log(text.length); // 4 characters 4console.log(Buffer.from(text, 'utf8').length); // 12 bytes 5 6// Database varchar(10) in bytes = only 3 Chinese chars!
Fix: Contar caracteres, no bytes
JavaScript1function truncate(str, maxChars) { 2 if (str.length <= maxChars) return str; 3 return str.slice(0, maxChars) + '...'; 4} 5 6// Or use Array.from to handle emojis correctly 7function truncateEmoji(str, maxChars) { 8 const chars = Array.from(str); 9 if (chars.length <= maxChars) return str; 10 return chars.slice(0, maxChars).join('') + '...'; 11} 12 13truncateEmoji('Hello 👨👩👧👦 World', 10); 14// "Hello 👨👩👧👦 Wo..."
5. Manejar correctamente los emojis
Problema: Los emojis son complejos.
JavaScript1const emoji = '👨👩👧👦'; // Family emoji 2console.log(emoji.length); // 11 😱 3 4// Why? It's multiple code points joined with Zero-Width Joiners
Fix: Utilizar la segmentación Unicode adecuada
JavaScript1// Split by grapheme clusters 2const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); 3const segments = Array.from(segmenter.segment('Hello 👨👩👧👦 World')); 4console.log(segments.map(s => s.segment)); 5// ['H', 'e', 'l', 'l', 'o', ' ', '👨👩👧👦', ' ', 'W', 'o', 'r', 'l', 'd'] 6 7// Count "characters" correctly 8const charCount = segments.length; // 13 ✅
Comprobación de problemas de codificación
Datos de prueba
Utilice estas cadenas para probar la codificación:
JavaScript1const testStrings = [ 2 'Hello World', // ASCII baseline 3 'Café résumé naïve', // Latin-1 extensions 4 'Привет мир', // Cyrillic 5 '你好世界', // Chinese 6 'مرحبا بالعالم', // Arabic (RTL) 7 '🔥💯👍', // Emojis 8 '👨👩👧👦', // Complex emoji (ZWJ sequence) 9 '\u0000\u0001\u001F', // Control characters 10 'test\r\nline\rbreaks\n', // Line breaks 11]; 12 13testStrings.forEach(str => { 14 // Send through your system 15 const result = yourFunction(str); 16 assert(result === str, 'Encoding corruption detected'); 17});
Pruebas automatizadas
JavaScript1// Encoding round-trip test 2describe('Encoding', () => { 3 it('should preserve UTF-8 through database', async () => { 4 const testString = 'Café 中文 🔥'; 5 6 await db.insert({ text: testString }); 7 const result = await db.query('SELECT text FROM table'); 8 9 expect(result[0].text).toBe(testString); 10 }); 11 12 it('should handle API round-trip', async () => { 13 const testString = 'Résumé 日本語'; 14 15 const response = await fetch('/api/echo', { 16 method: 'POST', 17 headers: { 'Content-Type': 'application/json; charset=UTF-8' }, 18 body: JSON.stringify({ text: testString }) 19 }); 20 21 const data = await response.json(); 22 expect(data.text).toBe(testString); 23 }); 24});
Problemas específicos de la plataforma
Windows
Problema: Windows utiliza diferentes codificaciones por defecto.
- Símbolo del sistema: Codificación 437 o Windows-1252
- PowerShell: UTF-16 LE
Fix:
POWERSHELL# Set PowerShell to UTF-8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
macOS/Linux
Problema: Normalmente UTF-8 por defecto, pero compruébalo:
Terminal1locale 2# Should show UTF-8 3 4# If not: 5export LC_ALL=en_US.UTF-8 6export LANG=en_US.UTF-8
Python
Python 3:
Python# ✅ Default is UTF-8 (usually) with open('file.txt', 'r', encoding='utf-8') as f: content = f.read()
Python 2 (legacy):
Python1# ❌ Default is ASCII (nightmare) 2# Always specify encoding 3import codecs 4with codecs.open('file.txt', 'r', 'utf-8') as f: 5 content = f.read()
Java
Problema: Java usa UTF-16 internamente.
JAVA1// ✅ Read UTF-8 files 2BufferedReader reader = new BufferedReader( 3 new InputStreamReader( 4 new FileInputStream("file.txt"), 5 StandardCharsets.UTF_8 6 ) 7); 8 9// ✅ Write UTF-8 files 10BufferedWriter writer = new BufferedWriter( 11 new OutputStreamWriter( 12 new FileOutputStream("file.txt"), 13 StandardCharsets.UTF_8 14 ) 15);
PHP
PHP1<?php 2// ✅ Set default encoding 3ini_set('default_charset', 'UTF-8'); 4mb_internal_encoding('UTF-8'); 5 6// ✅ Database connection 7$pdo = new PDO( 8 'mysql:host=localhost;dbname=mydb;charset=utf8mb4', 9 'user', 10 'password' 11);
Validación de codificación de IntlPull
Cuando usted empuja traducciones a IntlPull, nosotros automáticamente:
- ✅ Validamos la codificación UTF-8
- ✅ Comprobación de secuencias de bytes no válidas
- ✅ Normalizar Unicode (forma NFC)
- ✅ Detectar desajustes de codificación
- ✅ Marcar mojibake potencial
Terminal1npx @intlpullhq/cli upload 2 3# Output: 4# ✅ All strings valid UTF-8 5# ⚠️ Warning: String "Café" looks like double-encoded UTF-8 6# 💡 Suggestion: Check database encoding
Esto detecta problemas de codificación antes de que lleguen a producción.
El TL;DR
Reglas para vivir:
- Utiliza UTF-8 en todas partes. Sin excepciones.
- Establezca la codificación explícitamente en cada frontera (archivos, DB, HTTP).
- Probar con cadenas no ASCII (chino, árabe, emojis).
- Normalizar antes de comparar (
.normalize('NFC')). - Contar caracteres correctamente (usar
Array.from()para emojis).
**Errores comunes
- MySQL
utf8(usarutf8mb4) - Olvidar
charset=UTF-8en las cabeceras HTTP - Comparación sin normalización
- Longitud de cadena para límites de caracteres
- Apertura de archivos Windows sin codificación especificada
Cuando vea mojibake:
- Compruebe la codificación de la base de datos
- Compruebe la codificación de la conexión
- Comprobar cabeceras HTTP
- Pruebe la decodificación doble (
latin1 → utf8)
**¿Necesita ayuda para gestionar contenidos multilingües?
Pruebe IntlPull. Valida automáticamente la codificación, detecta mojibake y normaliza Unicode en sus traducciones. Disponible en versión gratuita.
O simplemente recuerde: UTF-8 en todas partes. Todo irá bien.
