wandres.dev
STRINGS Y TEXTO · Unicode bien hecho

Formateo y localización: texto que no se concatena

Convertir un valor en texto es una operación cultural, no técnica, y tratarla como técnica es la causa de la mayoría de los fallos de internacionalización. Esta lección presenta la familia de estilos de formato que sustituyó a los formateadores clásicos, recorre números, medidas, fechas y listas, explica por qué el análisis inverso pertenece al mismo diseño, y argumenta con detalle por qué concatenar fragmentos traducibles produce texto imposible de traducir bien.

⏱ 19 min

Un número no tiene una representación textual: tiene tantas como convenciones existan para escribirlo. El separador decimal cambia de país en país, el de miles a veces es un punto, a veces una coma y a veces un espacio fino, el símbolo de moneda va delante o detrás, las fechas ordenan sus componentes de tres maneras distintas y los plurales no se dividen en dos categorías en la mayoría de los idiomas del mundo. Todo eso está resuelto y tabulado desde hace décadas en el repositorio común de datos de configuración regional, y la única decisión que te queda es si vas a usarlo o si vas a reinventarlo mal con interpolaciones y sumas de cadenas.

🎯 Al terminar esta lección sabrás
  • Usar la familia de estilos de formato para producir texto correcto según la configuración del usuario.
  • Formatear números, monedas, medidas, fechas, rangos y listas con las opciones que más se usan.
  • Separar el texto destinado a personas del destinado a máquinas y elegir la representación estable para el segundo.
  • Explicar por qué la concatenación destruye la traducibilidad y qué mecanismos la sustituyen.

Estilos en lugar de formateadores

Durante años, formatear en las plataformas de Apple significaba crear un objeto formateador, configurarlo con propiedades mutables, guardarlo en algún sitio porque construirlo era caro, y confiar en no tocarlo desde dos hilos. Ese modelo fue reemplazado por otro basado en valores: un estilo de formato es un valor inmutable y componible, se construye en el punto de uso, se combina con métodos encadenados y el compilador comprueba que el estilo corresponde al tipo del dato. La puerta de entrada es un método presente en los tipos formateables.

let n = 1234567.891

print(n.formatted())
print(n.formatted(.number.precision(.fractionLength(2))))
print(n.formatted(.number.grouping(.never)))
print(n.formatted(.percent))
print(n.formatted(.currency(code: "EUR")))

// El estilo es un valor: se puede guardar y reutilizar
let precio = FloatingPointFormatStyle<Double>.Currency(code: "EUR")
  .precision(.fractionLength(2))
print(n.formatted(precio))

Lo que hace correcto a este código no es ninguna de las opciones que ves, sino la que no ves: el estilo consulta la configuración regional vigente para decidir separadores, símbolos y colocación. Cambia el idioma del sistema y el mismo código produce otro texto sin que tú toques nada. Es exactamente el trabajo que no deberías estar haciendo tú.

ℹ️
Interpolar un valor no lo formatea

Escribir un número dentro de una interpolación de cadena invoca su descripción para depuración, que usa punto decimal, no agrupa miles y no conoce ningún idioma. Es la forma correcta de escribir en un registro y la forma incorrecta de escribir en una interfaz. La diferencia entre ambas llamadas es de siete caracteres y de una categoría entera de fallos.

Medidas, fechas, rangos y listas

La familia cubre bastante más que números sueltos, y conocer el catálogo evita que escribas a mano cosas que están resueltas. Las magnitudes con unidad se convierten al sistema del usuario, las fechas se componen campo a campo o por longitud, los rangos y las listas tienen sus propios estilos porque unir con guiones y comas también es una convención cultural.

let ahora = Date.now

print(ahora.formatted(date: .abbreviated, time: .shortened))
print(ahora.formatted(.dateTime.year().month(.wide).day()))
print(ahora.formatted(.relative(presentation: .named)))

let distancia = Measurement(value: 5, unit: UnitLength.kilometers)
print(distancia.formatted(.measurement(width: .abbreviated)))

let bytes = 1_536_000
print(bytes.formatted(.byteCount(style: .file)))

let nombres = ["Ada", "Alan", "Grace"]
print(nombres.formatted(.list(type: .and)))

let intervalo = ahora..<ahora.addingTimeInterval(3600)
print(intervalo.formatted(date: .omitted, time: .shortened))
flowchart TD
V[Valor crudo numero fecha medida] --> F[Estilo de formato elegido]
F --> LO[Configuracion regional del usuario]
LO --> S[Texto para personas]
V --> P[Estilo POSIX o ISO 8601]
P --> MQ[Texto para maquinas]
K[Clave del catalogo] --> I[Interpolacion y variantes de plural]
I --> S

El diseño incluye el camino inverso, y esa simetría es la parte que más gente desconoce. Muchos estilos son también estrategias de análisis, de modo que el mismo valor que produce el texto sabe leerlo de vuelta con la misma configuración regional. Un campo donde el usuario teclea una cantidad se analiza con la estrategia de moneda del idioma en curso, y así una coma decimal tecleada en español deja de ser un error de entrada.

let leido = try Decimal("1.234,56 €", strategy: Decimal.FormatStyle.Currency(code: "EUR")
  .locale(Locale(identifier: "es_ES"))
  .parseStrategy)

Lo que leen las personas y lo que leen las máquinas

De la sección anterior se sigue una regla que conviene grabar: la configuración regional es correcta para el usuario y catastrófica para el intercambio de datos. Si escribes en un archivo, en una base de datos o en el cuerpo de una petición un número formateado con la configuración vigente, has creado un archivo que se lee bien en un ordenador y mal en otro. Para ese lado del sistema hay representaciones estables cuya definición no depende de nadie.

// Para personas: sensible a la configuracion regional
let visible = fecha.formatted(date: .long, time: .omitted)

// Para maquinas: estable en todas partes
let intercambio = fecha.formatted(.iso8601)
let numeroEstable = valor.formatted(.number
  .grouping(.never)
  .locale(Locale(identifier: "en_US_POSIX")))
🍊

Elige el destino antes que el estilo

La primera pregunta al formatear no es qué aspecto quieres, sino quién va a leerlo. Persona o máquina. La respuesta determina si el texto debe seguir la configuración del usuario o ignorarla por completo.

🍊

Nunca analices lo que mostraste

Guardar el texto visible y volver a leerlo más tarde es un fallo latente: basta con que el usuario cambie de idioma entre la escritura y la lectura. Guarda el valor, formatea al mostrar.

🍊

Prueba con configuraciones hostiles

Ejecuta la aplicación en un idioma con coma decimal, otro con escritura de derecha a izquierda y otro con calendario no gregoriano. Los tres revelan clases de fallo distintas y ninguno cuesta más de un minuto.

Por qué no se concatena texto traducible

Llegamos al núcleo. Un traductor no recibe tu programa, recibe fragmentos, y un fragmento sin contexto es intraducible. Si construyes una frase sumando trozos, cada trozo llega solo a la hoja de traducción, sin saber qué le precede, qué género tiene el sustantivo que lo acompaña, si el idioma de destino coloca el complemento antes del verbo, o si el número que va delante obliga a declinar. La frase se traduce pieza a pieza y se ensambla en un orden que solo era válido en el idioma original.

// Roto: tres fragmentos sin contexto y un orden fijo
let malo = "Tienes " + String(n) + " mensajes nuevos"

// Correcto: una unidad de traduccion con el valor interpolado
let bueno = String(localized: "Tienes \(n) mensajes nuevos",
                   comment: "Resumen de la bandeja de entrada")

// Diferido: la clave viaja y se resuelve donde se muestra
let recurso = LocalizedStringResource("Tienes \(n) mensajes nuevos")

La segunda forma resuelve además el problema del que casi nadie se acuerda hasta que llega un informe de fallo: los plurales. El español y el inglés distinguen dos formas, singular y plural, pero el árabe distingue seis, el polaco cuatro y el japonés ninguna. Escribir una condición que elija entre dos cadenas codifica la gramática de tu idioma dentro del programa y la impone a todos los demás. La solución no es tuya, es del catálogo: en un catálogo de cadenas declaras las variantes por categoría plural y el sistema elige la que corresponde según las reglas del idioma de destino, que ya están tabuladas. Tu código sigue teniendo una sola línea.

Hay un tercer error que suele acompañar a la concatenación y que conviene desactivar a la vez: dar por hecho que el texto traducido ocupará lo mismo. Una etiqueta que cabe justa en español puede crecer un cuarenta por ciento en alemán y encogerse a la mitad en japonés, y la dirección de escritura puede invertirse por completo. Diseñar el contenedor para el texto más corto que conoces es la versión visual del mismo supuesto que rompe la concatenación.

💡
El comentario no es opcional

El parámetro de comentario existe porque el traductor no ve tu pantalla. Una cadena de dos palabras puede ser un botón, un título o el sujeto de una frase, y en muchos idiomas eso cambia la forma. Escribir para qué sirve y dónde aparece cuesta cinco segundos y es la diferencia entre una traducción correcta y una plausible.

La concatenación asume que la sintaxis de tu idioma es la sintaxis del mundo

Merece la pena entender por qué la suma de cadenas es un error estructural y no una mala costumbre corregible con cuidado, porque el argumento se generaliza mucho más allá de la traducción. Cuando escribes una frase como una suma de partes, estás afirmando dos cosas sin darte cuenta: que el orden de los constituyentes es fijo, y que cada parte es independiente de las demás. Las dos afirmaciones son falsas para el lenguaje natural. El orden de sujeto, verbo y objeto varía entre familias lingüísticas; la posición del número respecto al sustantivo varía; los idiomas con caso gramatical exigen que el sustantivo cambie de forma según su función, de modo que la misma palabra insertada en dos frases distintas no es la misma palabra; y en los idiomas con género, un adjetivo aislado no tiene forma correcta hasta que se sabe a qué acompaña. Una cadena concatenada le pide al traductor que produzca fragmentos que funcionen en cualquier posición y con cualquier vecino, lo cual es sencillamente imposible, y lo que recibes de vuelta es una traducción que funciona por accidente en los casos que alguien probó. La unidad de traducción correcta es la frase completa con huecos marcados, porque es la unidad más pequeña que conserva la sintaxis; y esa es la razón profunda de que la interpolación dentro de una clave sea distinta de la suma de trozos, aunque el resultado en tu idioma sea idéntico carácter a carácter. Hay aquí una lección de diseño que trasciende el texto y que conviene llevarse: cuando compones algo a partir de partes, siempre estás haciendo una suposición sobre qué se conserva bajo esa composición. En texto traducible no se conserva casi nada, y por eso la composición hay que hacerla en el otro lado, en el catálogo, donde cada idioma puede componer a su manera. Reconocer en qué dominios la composición ingenua no preserva el significado, y mover la composición al lugar donde sí lo hace, es una de las intuiciones que más veces vas a reutilizar.

⚔️ Formatea para cada destinatario
  1. Toma cinco valores de tu aplicación, un precio, una distancia, un porcentaje, un tamaño de archivo y una fecha, y formatea los cinco con el estilo adecuado. Ejecuta con dos idiomas y guarda ambas salidas.
  2. Escribe un campo de entrada que acepte una cantidad monetaria tecleada por el usuario, analizándola con la estrategia correspondiente al idioma en curso. Prueba con coma y con punto decimal.
  3. Busca en un proyecto tuyo todos los sitios donde un número entra en una interpolación destinada a la interfaz. Clasifícalos y corrige los que corresponda.
  4. Localiza una frase construida por concatenación, conviértela en una única unidad traducible con interpolación y añade el comentario que necesitaría un traductor sin acceso a la pantalla.
  5. Declara las variantes de plural de esa frase en el catálogo de cadenas y comprueba el resultado con cero, uno y varios elementos en un idioma con más de dos categorías.