Cuando una app va a hablar más de un idioma, lo que acaba costando dinero no son las traducciones, sino llegar al segundo idioma y descubrir que hay que reescribir media app para encajarlo. La respuesta corta para evitarlo: separa el texto del código desde el primer día, usa el sistema de localización que ya trae Flutter (gen-l10n) y trata plurales, fechas y moneda como reglas distintas en cada idioma, no como un + entre cadenas.
Esta guía es la versión práctica. Qué montar, en qué orden y las tres o cuatro decisiones donde casi todo el mundo se equivoca.
Antes de nada: ¿te hace falta ya?
Un MVP para un solo mercado y un solo idioma no necesita internacionalización. Montar todo el andamiaje de l10n para un producto que todavía estás validando es trabajo que no te devuelve nada esta semana y te ralentiza cada pantalla nueva.
Dicho esto, hay una trampa. Lo caro no es añadir el segundo idioma, es haber escrito los primeros seis meses de app con los textos incrustados en los widgets. Migrar eso después es ir pantalla por pantalla sacando cadenas a mano, y es el tipo de tarea que nadie estima bien.
La línea que usamos: si hay una probabilidad razonable de un segundo idioma en el primer año, monta la infraestructura de l10n desde el principio aunque arranques con un solo idioma. El coste de tenerla y no usarla es casi cero. El de no tenerla y necesitarla se mide en semanas.
Traducir no es internacionalizar
Dos conceptos que conviene separar, porque el fallo típico es confundirlos.
Internacionalización (i18n) es preparar el código para que admita cualquier idioma y región: nada de texto fijo, formatos de fecha y número que dependen del locale, layouts que aguantan que una palabra alemana ocupe el triple que la inglesa. Lo hace el equipo de desarrollo, una vez.
Localización (l10n) es adaptar la app a un idioma y región concretos: las traducciones, el formato de moneda local, las convenciones de fecha. Se hace una vez por cada mercado.
La internacionalización es la fontanería. La localización es abrir el grifo. Si la fontanería está mal puesta, da igual cuántas traducciones compres.
Montaje base con gen-l10n
Flutter trae su propio generador de localizaciones. No necesitas paquetes de terceros para lo esencial.
En pubspec.yaml:
dependencies:
flutter_localizations:
sdk: flutter
intl: any
flutter:
generate: true
Un archivo l10n.yaml en la raíz del proyecto:
arb-dir: lib/l10n
template-arb-file: app_es.arb
output-localization-file: app_localizations.dart
Los textos viven en archivos ARB, uno por idioma. El de plantilla (app_es.arb) lleva también los metadatos:
{
"saludo": "Hola",
"@saludo": {
"description": "Saludo de bienvenida en la pantalla de inicio"
}
}
Y su equivalente app_en.arb:
{
"saludo": "Hello"
}
Al compilar (o al ejecutar flutter gen-l10n) se genera la clase AppLocalizations. La conectas en el MaterialApp:
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomePage(),
);
Y en cualquier widget:
Text(AppLocalizations.of(context)!.saludo)
A partir de aquí, cada cadena nueva es una entrada en el ARB. La regla: si un Text lleva una cadena entre comillas, es un bug esperando a la siguiente traducción.
Plurales y género sin romper el español
Aquí es donde falla la mayoría, porque en inglés el plural parece trivial y en español no lo es. "1 artículo" frente a "2 artículos" no se resuelve con un if count == 1. Lo hace el formato ICU dentro del propio ARB:
{
"articulos": "{count, plural, =0{No hay artículos} one{{count} artículo} other{{count} artículos}}",
"@articulos": {
"placeholders": {
"count": { "type": "int" }
}
}
}
Lo llamas pasando el número:
Text(AppLocalizations.of(context)!.articulos(carrito.length))
La clave other es obligatoria siempre. Hay idiomas con más formas plurales que el español (el árabe tiene seis), y other es el que los cubre cuando no hay una regla específica. Si la omites, el generador falla.
Lo mismo con el género, con select:
"invitacion": "{genero, select, male{Invitado} female{Invitada} other{Invitada/o}}"
El antipatrón que hay que erradicar es concatenar: 'Tienes ' + count.toString() + ' mensajes'. En cuanto entra un idioma con otro orden de palabras, esa frase se rompe y no hay traductor que la arregle sin tocar código. Todo lo variable va como placeholder dentro de la cadena.
Fechas, números y moneda
El texto es la mitad del trabajo. La otra mitad son los datos que se ven distintos en cada país y que es fácil dejar fijos sin darse cuenta.
1.234,50 € en España es €1,234.50 en Estados Unidos. La coma y el punto cambian de papel. Esto no se formatea a mano, lo hace intl:
import 'package:intl/intl.dart';
final moneda = NumberFormat.currency(locale: 'es_ES', symbol: '€');
moneda.format(1234.5); // 1.234,50 €
final fecha = DateFormat.yMMMMd('es_ES').format(DateTime.now());
// 8 de noviembre de 2026
El error habitual es escribir el formato una vez, con el locale del desarrollador, y olvidarlo. Funciona en tu máquina, falla en el móvil de un usuario en otra región. La regla es la misma que con el texto: ningún formato de fecha o número fijo en el código, todos derivados del locale activo.
RTL de verdad, no solo "poner el árabe"
Si entre tus mercados hay árabe, hebreo o persa, la app se lee de derecha a izquierda, y eso es más que invertir el texto. Flutter gira automáticamente el Row, el texto y la navegación cuando el locale es RTL, pero hay tres sitios donde te deja solo:
- Paddings y márgenes.
EdgeInsets.only(left: 16)se queda a la izquierda siempre. UsaEdgeInsetsDirectional.only(start: 16), que respeta la dirección. - Alineaciones.
Alignment.centerLeftno se gira;AlignmentDirectional.centerStartsí. - Iconos con dirección. Una flecha de "volver" o un icono de "enviar" apuntan al lado contrario en RTL y hay que reflejarlos.
El coste real de RTL no está en el código, está en el diseño. Formularios, flujos de onboarding y pantallas con mucha composición hay que revisarlos uno a uno. Es un trabajo de días, no de horas, y conviene decidir pronto si ese mercado entra en el alcance o no. Añadir árabe "más adelante" sobre una app diseñada solo para izquierda a derecha es rehacer pantallas.
¿Traducciones en el bundle o desde el servidor?
Esta es la decisión de arquitectura que más gente se salta y luego lamenta.
Los ARB se compilan dentro de la app. Son rápidos, funcionan sin conexión y no dependen de nadie. El precio: cambiar una coma en una traducción exige una nueva versión en la tienda y esperar la revisión. Si tu copy es estable, es la opción correcta y la más simple.
La alternativa es servir las traducciones desde el backend y descargarlas en caliente. Ganas poder corregir textos sin pasar por la tienda, hacer pruebas por mercado y que marketing toque el copy sin un despliegue. Pagas con complejidad real: caché, versionado, un idioma de respaldo cuando la descarga falla y la cadena que todavía no ha llegado. Y necesitas igualmente los textos del arranque incrustados, porque la primera pantalla aparece antes de que responda el servidor.
Nuestro criterio: empieza con los ARB en el bundle. Pásate a traducciones en servidor solo cuando tengas una razón concreta: muchos mercados, copy que cambia a menudo o un equipo de contenido que necesita autonomía. No montes el sistema en caliente "por si acaso"; es infraestructura que hay que mantener para siempre.
Los errores que vemos repetidos
Un repaso rápido de lo que aparece una y otra vez en auditorías de apps que llegan a medio internacionalizar:
- Cadenas incrustadas que se colaron después del montaje inicial. Pasa siempre; conviene una regla de lint que las detecte.
- Falta la persistencia del idioma elegido. El usuario cambia a inglés, cierra la app y vuelve a abrirla en el idioma del sistema. Guarda la preferencia y reaplícala al arrancar.
supportedLocalesincompleto. Si no declaras un idioma ahí, Flutter cae al de respaldo aunque tengas el ARB.- Layouts que revientan con textos largos. El alemán y el finés expanden; prueba con la cadena más larga, no con la española.
- Cero tests de localización. Un test de widget que recorra los locales soportados y verifique que no hay textos fijos ni desbordes cuesta poco y evita regresiones.
Cómo lo planteamos nosotros
En quince años construyendo apps para más de veinte países hemos aprendido que la internacionalización se decide al principio del proyecto, no cuando llega el primer cliente de otro mercado. La infraestructura de l10n desde el día uno, aunque arranques monolingüe. Los formatos siempre derivados del locale. Y RTL como decisión consciente de alcance, no como un "ya lo veremos".
No es la parte vistosa del proyecto, y aun así es la que decide si abrir un mercado nuevo te cuesta un sprint o te obliga a un refactor. Si estás a punto de empezar un producto que crecerá fuera de España, o tienes uno que se te ha quedado atascado en un solo idioma, es el tipo de decisión en la que podemos ayudarte a no equivocarte dos veces.
Relacionado en el blog: catálogo y precios en apps multipaís, accesibilidad en apps Flutter y la EAA, testing en Flutter a fondo, reducir el tamaño de una app Flutter.



