Flutter, el marco de aplicación multiplataforma de Google, no solo ha ganado popularidad en el ámbito del desarrollo de aplicaciones móviles, sino que también se ha ramificado sin problemas a la web, Linux, macOS y Windows. Más allá de eso, Flutter es increíblemente rápido y trabajar con él es todo un placer.
Cuando se trata de la internacionalización de aplicaciones Flutter, el equipo de Flutter ha creado una sólida solución integrada de localización. En este tutorial, prepararemos y configuraremos las bibliotecas de internacionalización de Flutter, las usaremos para cargar y mostrar traducciones y trabajaremos en el formato de fecha/hora, entre otros temas de localización.
🤿 Profundiza más » El paquete de localización de Flutter nativo se basa en el primer paquete de Dart intl.

Phrase Strings
Toma tu aplicación web o móvil global sin complicaciones
Adapta tu software, sitio web, o videojuego para audiencias globales con la plataforma de localización de software más ágil y confiable.
Nuestra aplicación de demostración
Para no hablar solo de teoría y no aburrirnos, vamos a programar una pequeña aplicación de demostración y la localizaremos: Heroes of Computer Science es una selección de figuras notables en la historia relativamente breve de la computación.

Versiones utilizadas
Estamos utilizando los siguientes idiomas, marcos y versiones de paquetes en este artículo:
- Dart 3.1.1
- Flutter 3.13.3
- DevTools 2.25.0
- flutter_localizations (la versión parece estar vinculada a Flutter): proporciona localizaciones a widgets comunes, como widgets de Material o Cupertino.
- intl 0.18.0 — la columna vertebral del sistema de localización; nos permite crear y usar nuestras propias localizaciones; se utiliza para formatear fechas y números.
Ahora veamos el código de nuestra aplicación de inicio, que es bastante sencillo.
import 'package:flutter/material.dart';
import 'screens/hero_list.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Heroes of Computer Science',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HeroList(title: 'Heroes of Computer Science'),
);
}
}Lenguaje del código: Dart (dart)
Nuestro widget raíz es un MaterialApp básico, con un HeroList en su ruta de home.
import 'package:flutter/material.dart';
import 'package:flutter_i18n_2021/screens/settings.dart';
import 'package:flutter_i18n_2021/widgets/hero_card.dart';
class HeroList extends StatelessWidget {
final String title;
HeroList({this.title = ''});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.settings),
tooltip: 'Open settings',
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Settings()),
);
},
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text('6 Heroes'),
),
Expanded(
child: ListView(
children: <Widget>[
HeroCard(
name: 'Grace Hopper',
born: '9 December 1906',
bio: 'Devised theory of machine...',
),
HeroCard(
name: 'Alan Turing',
born: '23 June 1912',
bio: 'Father of theoretical computer...',
),
// ...
],
),
),
],
),
),
);
}
}Lenguaje del código: Dart (dart)
HeroList alberga principalmente una ListView de HeroCard parametrizados.
import 'package:flutter/material.dart';
class HeroCard extends StatelessWidget {
final String name;
final String born;
final String bio;
final String imagePath;
final String placeholderImagePath = 'assets/images/placeholder.jpg';
const HeroCard({
Key key,
this.name = '',
this.born = '',
this.bio = '',
this.imagePath,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Card(
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: Image.asset(
imagePath ?? placeholderImagePath,
width: 100,
height: 100,
),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
name,
style: theme.textTheme.headline6,
),
),
Padding(
padding: const EdgeInsets.only(top: 2, bottom: 4),
child: Text(
born.isEmpty ? '' : 'Born $born',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w300,
),
),
),
Text(
bio,
style: TextStyle(fontSize: 14),
),
],
),
),
],
),
),
);
}
}
Lenguaje del código: JavaScript (javascript)
HeroCard muestra la imagen dada y los parámetros de cadena en un bonito widget Card de Material y embellece todo. ¡De acuerdo, vamos a localizar este cachorro!
🔗 Recurso » Puedes obtener el código de la aplicación hasta este punto desde la rama de inicio de nuestro repositorio de GitHub. La rama principal también contiene la aplicación completamente localizada.
Instalación y configuración
Podemos instalar nuestros paquetes agregando unas pocas líneas a pubspec.yaml.
version: 1.0.0+1
environment:
sdk: '>=3.1.1 <4.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
# Add the flutter_localizations package
flutter_localizations:
sdk: flutter
# Add the intl package
intl: ^0.18.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
generate: true # Add this line
uses-material-design: trueLenguaje del código: YAML (yaml)
Después de agregar las líneas resaltadas arriba, podemos ejecutar flutter pub get desde la línea de comandos para obtener nuestros paquetes. La generate: true línea es necesaria para la generación automática de código que los paquetes de localización nos proporcionan. Pronto profundizaremos más en el negocio de la generación de código. Por ahora, incluye la línea. Puede ahorrarte bastante tiempo.
Configuración de localización
Con nuestros paquetes instalados, agreguemos un archivo l10n.yaml en la raíz de nuestro proyecto. Este archivo configura dónde estarán nuestros archivos de traducción y los nombres de los archivos dart generados automáticamente.
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dartLenguaje del código: YAML (yaml)
🔗 Recurso » La guía de usuario de internacionalización oficial cubre muchas más opciones que puedes incorporar a l10n.yaml para controlar el generador de código de internacionalización de Flutter.
Agregando archivos de traducción
La localización Flutter utiliza archivos ARB (Application Resource Bundle) para albergar sus traducciones por defecto. Estos son archivos simples escritos en sintaxis JSON. Como mínimo, necesitamos un archivo de plantilla que corresponda a nuestra localización predeterminada (inglés en nuestro caso). Especificamos que nuestro archivo de plantilla será lib/l10n/app_en.arb en nuestra configuración anterior. Así que vamos a crear este directorio de alojamiento y agregar nuestro archivo de traducciones de plantilla a él.
{
"appTitle": "Heroes of Computer Science"
}Lenguaje del código: JSON / JSON con comentarios (json)
Por supuesto, todo este follón no tendría mucho sentido si no pudiéramos proporcionar traducciones para otras localizaciones. Agregaremos un archivo de traducción árabe aquí. Agrega tú cualquier idioma que desees. Hablaremos un poco sobre los diseños de derecha a izquierda (RTL) más adelante, así que si te interesa ese asunto, quizás quieras ceñirte al árabe u otro idioma RTL.
{
"appTitle": "أبطال علوم الكمبيوتر"
}Lenguaje del código: JSON / JSON con comentarios (json)
Podemos agregar tantas traducciones de localización como queramos. Solo necesitamos asegurarnos de que nuestros archivos se ajusten a nuestra convención de nomenclatura configurada: lib/l10n/app_<locale>.arb
Configurando nuestra aplicación
Vamos a contarle a nuestra aplicación lo mucho que nos interesa la internacionalización. Necesitamos configurar nuestro archivo main.dart para usar los paquetes de localización Flutter.
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'screens/hero_list.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Heroes of Computer Science',
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
// 'en' es el código de idioma. Podríamos opcionalmente proporcionar un
// código de país como el segundo parámetro, por ejemplo.
// Locale('en', 'US'). Si hacemos eso, es posible que nos interese
// proporcionar un archivo adicional app_en_US.arb para
// traducciones específicas para la región.
const Locale('en', ''),
const Locale('ar', ''),
],
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HeroList(title: 'Heroes of Computer Science'),
);
}
}
Lenguaje del código: JavaScript (javascript)
Después de importar flutter_localizations.dart, agregamos las propiedades localizationsDelegates y supportedLocales al constructor de MaterialApp. Los localizationsDelegates proporcionan localizaciones a nuestra aplicación. Los que se incluyen arriba proporcionan localizaciones para widgets de Flutter, Material y Cupertino ya sido localizadas por el equipo de Flutter.
Por ejemplo, supongamos que teníamos un MaterialApp y llamamos a la función showDatePicker() en algún lugar dentro de él. Suponiendo también que el idioma de nuestro sistema operativo está configurado en árabe, veríamos algo como lo siguiente.

Ten en cuenta que no tuvimos que traducir nada nosotros mismos para obtener esto. El widget del selector de fecha ya ha sido localizado por el equipo de Flutter. Solo necesitamos conectar los delegados correctos en el constructor de nuestra aplicación, como hicimos arriba. ¡Un gran aplauso para el equipo de Flutter por esto: qué ahorro de tiempo!
🗒️ Nota » En el momento de escribir esto, flutter_localizations admite 78 idiomas.
🔗 Recurso » La documentación oficial de Flutter es muy eficaz a la hora de explicar cómo colaboran las distintas partes, como los delegados y la clase Localizations, para la localización.
La prop supportedLocales que proporcionamos al constructor de MaterialApp enumera los idiomas que nuestra aplicación soporta. Flutter solo reconstruirá widgets en respuesta a un cambio de localización si la localización está en la lista de supportedLocales. Volveremos a supportedLocales en un momento cuando discutamos la resolución de localización. ¡Ahora mismo, generemos algo de código!
Generación automática de código
Para usar las traducciones de los archivos ARB en nuestra aplicación Flutter, necesitamos generar algunos archivos Dart que importamos cada vez que necesitamos las traducciones. Para generar estos archivos, solo asegúrate de haber seguido los pasos de instalación y configuración hasta este punto y ejecuta la aplicación. Así es, solo ejecuta la aplicación. El código se generará automáticamente, y si todo salió bien, deberías ver los siguientes archivos en el directorio de tu proyecto:
.dart_tool/flutter_gen/gen_l10n/app_localizations.dart.dart_tool/flutter_gen/gen_l10n/app_localizations_en.dart.dart_tool/flutter_gen/gen_l10n/app_localizations_ar.dart
🗒️ Nota » Si estos archivos no se generaron, asegúrate de que tu aplicación Flutter no tenga errores de compilación y revisa tu consola de depuración cuando ejecutes la aplicación.
Usando nuestras AppLocalizations
Hagamos uso de los archivos de código recién generados para localizar el título de nuestra aplicación.
import 'package:flutter/material.dart';
import 'package:flutter_i18n_2021/screens/settings.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'screens/hero_list.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateTitle: (context) {
return AppLocalizations.of(context).appTitle;
},
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''),
const Locale('ar', ''),
],
theme: ThemeData(
primarySwatch: Colors.blue,
),
// eliminar home: HeroList(...)
initialRoute: '/',
routes: {
'/': (context) => HeroList(title: AppLocalizations.of(context).appTitle),
'/settings': (context) => Settings(),
},
);
}
}
Lenguaje del código: Dart (dart)
Importamos app_localizations.dart y agregamos el AppLocalizations.delegate autogenerado a nuestra lista de delegados. Esto nos proporciona el widget AppLocalizations, que usamos para traducir el título de la aplicación y el título de HeroList. La propiedad autogenerada appTitle contendrá la traducción que coincide con la localización activa, extraída de nuestro archivo app_<locale>.arb.
✋ Atención » Debido al orden de carga, nuestras traducciones no estarán listas cuando estemos construyendo nuestro MaterialApp. Así que usamos las propiedades onGenerateTitle y routes, y sus funciones de constructor (context) {} para asegurarnos de que nuestras traducciones estén listas cuando establezcamos nuestras cadenas de título.
Ahora, si configuramos el idioma de nuestro sistema operativo en árabe y ejecutamos nuestra aplicación, ¡he aquí!

Nuestro título ahora está en árabe. Además, observa cómo Flutter ha dispuesto muchos de sus widgets en una dirección de derecha a izquierda automáticamente para nosotros. Dado que el árabe es un idioma de derecha a izquierda, ¡esto nos ahorra mucho tiempo! Tendremos que arreglar ese padding a la izquierda de la imagen en los HeroCards, y lo haremos cuando abordemos la direccionalidad un poco más adelante.
🤿 Profundiza más » Los lectores más avispados os habréis fijado en que AppLocalizations.of(context) se parece mucho a llamar a un InheritedWidget. Eso es porque los objetos de localización funcionan mucho como InheritedWidgets.
Eso es todo para la configuración. Ahora tenemos la base para localizar nuestra aplicación. Una pregunta que podrías tener en este punto es: «¿cómo decide Flutter qué localización usar?» Hablemos de eso.
Resolución de localización
Las localizaciones que proporcionamos a MaterialApp(supportedLocales: [...]) son las únicas que Flutter usará para determinar la localización activa cuando la aplicación se ejecute. Para hacer esto, Flutter utiliza tres propiedades de una localización:
- El código de idioma, por ejemplo,
'en'para inglés - El código de país (opcional), por ejemplo, la parte
USenen_US - El código de script (opcional): el conjunto de letras utilizado, por ejemplo, chino tradicional (
Hant) o chino simplificado (Hans)
Por defecto, Flutter leerá las localizaciones del sistema preferidas del usuario y:
- Intentará hacer coincidir el
languageCode,scriptCodeycountryCodecon uno ensupportedLocales. Si eso falla, - Intentará hacer coincidir el
languageCodeyscriptCodecon uno ensupportedLocales. Si eso falla, - Intentará hacer coincidir el
languageCodeycountryCodecon uno ensupportedLocales. Si eso falla, - Intenta hacer coincidir el
códigoDeIdiomacon uno enlocalesSoportados. Si eso falla, - Intenta hacer coincidir el
códigoDePaíscon uno enlocalesSoportadossolo cuando todos los locales preferidos no coincidan. Si eso falla, - Devuelve el primer elemento de
localesSoportadoscomo una opción de respaldo.
Así que en nuestra aplicación, si el idioma de iOS del usuario está configurado en ar_SA, verían nuestras localizaciones ar (4. arriba). Si el idioma de iOS del usuario está configurado en fr (francés), verían nuestras localizaciones en (6. arriba). En Android, un usuario puede tener una lista de locales preferidos, no solo uno. Esto lo cubre Flutter en el algoritmo de resolución anterior.
🔗 Recurso » El algoritmo anterior es una paráfrasis de la documentación oficial de la propiedad localesSoportados.
✋ Atención » Si tu aplicación soporta un locale con un código de país, como fr_CA (francés canadiense), deberías proporcionar un respaldo sin el código de país, como fr.
Actualizando el proyecto de iOS
La documentación oficial de Flutter menciona la necesidad de actualizar la Info.plist directamente en el paquete de la aplicación iOS, añadiendo nuestras regiones admitidas a él. Si Info.plist no se actualiza, nuestra aplicación de iOS podría no funcionar como se espera. Para hacer la actualización solo necesitamos abrir ios/Runner/Info.plist en cualquier editor de texto y asegurarnos de que las siguientes entradas estén allí.
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ar</string>
</array>Lenguaje del código: texto plano (plaintext)
Obtener la región activa
A veces necesitamos saber cuál es la región en tiempo de ejecución en nuestro código. Podemos hacer esto con el siguiente fragmento.
Locale activeLocale = Localizations.localeOf(context);
// Suponiendo que nuestra localización activa es fr_CA...
debugPrint(activeLocale.languageCode);
// => fr
debugPrint(activeLocale.countryCode);
// => CALenguaje del código: Dart (dart)
✋ Atención » Observa que estamos usando Localizations, un widget integrado en Flutter, y no el AppLocalizations auto-generado.
Mensajes de traducción básicos
Ya cubrimos mensajes de traducción básicos cuando agregamos nuestro appTitle mensaje. Sin embargo, repasemos rápidamente el flujo de trabajo para agregar mensajes. Traduciremos el tooltip del botón de icono de la barra de aplicaciones de nuestro HeroList a continuación. Ya que no estamos realmente construyendo una pantalla de configuración, podríamos hacer algo con ello 😅.
// ...
class HeroList extends StatelessWidget {
final String title;
HeroList({this.title = ''});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.settings),
tooltip: 'Open settings',
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Settings()),
);
},
)
],
),
body: ...
);
}
Lenguaje del código: Dart (dart)
¿Vamos a localizar ese tooltip, te parece? Primero, agregaremos las entradas relevantes a nuestros archivos ARB.
// English
{
"appTitle": "Heroes of Computer Science",
"openSettings": "Abrir configuración"
}
// Árabe
{
"appTitle": "أبطال علوم الكمبيوتر",
"openSettings": "إفتح الإعدادات"
}Lenguaje del código: JSON / JSON con comentarios (json)
A continuación, vamos a recargar nuestra aplicación para regenerar nuestros archivos de código. Este paso es realmente importante y olvidarlo puede llevar a frustraciones innecesarias. Observa que este es un reinicio completo de la aplicación (

), no es una recarga en activo.
Ahora podemos actualizar nuestro código para usar nuestro nuevo mensaje localizado.
// ...
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class HeroList extends StatelessWidget {
final String title;
HeroList({this.title = ''});
@override
Widget build(BuildContext context) {
var t = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.settings),
tooltip: t.openSettings,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Settings()),
);
},
)
],
),
body: ...
);
}
}
Lenguaje del código: Dart (dart)
Con este código en su lugar, cuando recarguemos nuestra aplicación deberíamos ver el valor localizado de nuestro tooltip en el Widget Inspector.

✋ Atención » A menudo puede que obtengas resaltado de error en tu IDE después de agregar nuevos mensajes de traducción. Si has recargado tu aplicación, el error puede ser incorrecto (puede que estés bien). Si recibes un mensaje que dice que hay errores de construcción, puedes intentar ejecutar la aplicación de todos modos. Mientras la aplicación se construya y se ejecute, y veas tus nuevas traducciones, entonces todo probablemente esté bien. Para hacer que el error desaparezca en el IDE, intenta cerrar completamente tu aplicación y volver a iniciarla.
Interpolación en mensajes
Estamos en camino de traducir nuestra aplicación. ¿Pero qué pasa con la interpolación de valores dinámicos en tiempo de ejecución en nuestros mensajes de traducción? Por ejemplo, la biografía de Steve Wozniak contiene los nombres de los productos Apple I y Apple II.
// ...
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
return Scaffold(
// ...
body: HeroCard(
name: 'Steve Wozniak',
born: '11 August 1950',
bio: 'Designed & developed the Apple I & '
'Apple II.',
imagePath: 'assets/images/steve_wozniak.jpg',
),
// ...
);
}
}Lenguaje del código: Dart (dart)
Cuando localizamos este mensaje, es posible que queramos mantener Apple I y Apple II en su inglés original independientemente de la localización activa. Podemos usar marcadores de posición en nuestros archivos de traducción para lograr esto.
// English
{
// ...
"wozniakBio": "Developed the {appleOne} & {appleTwo} microcomputers.",
"@wozniakBio": {
"placeholders": {
"appleOne": {},
"appleTwo": {}
}
},
// ...
}
// Árabe
{
// ...
"wozniakBio": "طور جهازي كمبيوتر {appleOne} و {appleTwo}",
// ...
}
Lenguaje del código: JSON / JSON con comentarios (json)
Usamos la sintaxis {placeholderName} para establecer los marcadores de posición para nuestros valores dinámicos, y podemos tener tantos marcadores de posición como queramos en un mensaje.
Probablemente también te hayas fijado en la clave @wozniakBio en nuestras traducciones en inglés anteriores. Esta entrada es un complemento del mensaje wozniakBio en el mismo archivo. Las entradas complementarias son opcionales para mensajes básicos pero obligatorias para mensajes con marcadores de posición. De hecho, usamos entradas complementarias para definir los marcadores de posición de los mensajes (entre otras cosas).
🗒️ Nota » La entrada complementaria para un mensaje con la clave foo debe tener una clave de @foo. Solo necesitamos entradas complementarias en nuestro archivo de traducción predeterminado/plantilla (inglés en nuestro caso).
"@wozniakBio": { "placeholders": { "appleOne": {}, "appleTwo": {} } }Lenguaje del código: JavaScript (javascript)
✋ Atención » Los nombres de los marcadores de posición deben ser nombres de parámetros de método Dart válidos.
Podríamos usar el objeto placeholders para especificar el tipo de cada valor, e incluso proporcionar ejemplos como documentación si lo deseamos. También podemos dejar la definición como un {} vacío.
"@wozniakBio": {
"placeholders": {
"appleOne": {
// Tipo explícito
"type": "String",
// Un poco de documentación
"example": "Apple I"
},
// Está bien especificar solo el nombre
"appleTwo": {}
}
}Lenguaje del código: JavaScript (javascript)
El tipo se utiliza en el método que Flutter generará en AppLocalizations para nuestro mensaje wozniakBio.
// ...
abstract class AppLocalizations {
// ...
// Este método se implementa en app_localizations_en.dart
// y app_localizations_ar.dart.
// Tipo explícito para el parámetro appleOne. AppleTwo está implícito
// como parámetro.
String wozniakBio(String appleOne, Object appleTwo);
// ...
}
Lenguaje del código: JavaScript (javascript)
🗒️ Nota » Está perfectamente bien en la mayoría de los casos usar {} vacío para definiciones de marcador de posición. Una definición vacía hará que el parámetro sea de tipo Object. Entre bastidores, Flutter solo usará el valor theParameter.toString() del parámetro dado, por lo que un parámetro Object funcionará perfectamente. Un marcador de posición Object implícito también mantiene nuestros mensajes flexibles para aceptar cualquier tipo, ya que todos los tipos de Dart derivan de Object y tienen un toString().
OK, después de reiniciar la aplicación para actualizar AppLocalizations, podemos localizar el mensaje de la biografía de Woz con el nuevo método.
// ...
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
var t = AppLocalizations.of(context);
return Scaffold(
// ...
body: HeroCard(
name: 'Steve Wozniak',
born: '11 August 1950',
bio: t.wozniakBio('Apple I', 'Apple II'),
imagePath: 'assets/images/steve_wozniak.jpg',
),
// ...
);
}
}
Lenguaje del código: Dart (dart)
Con eso en su lugar, sabemos que nunca podremos ser demandados por ninguna empresa de frutas por tergiversar sus productos en cualquier idioma.

Plurales
A menudo necesitamos manejar plurales dinámicos en nuestra localización. “Has recibido un mensaje” o “Has recibido 3 mensajes”, por ejemplo.
Es importante notar que los diferentes idiomas manejan los plurales de manera diferente. Por ejemplo, el inglés tiene dos formas plurales: uno y otro (otro == cero y >1). El árabe tiene seis formas plurales. Esto puede ser un poco complicado al localizar usando bibliotecas que no soportan reglas plurales complejas. Afortunadamente, la solución de internacionalización directa de Flutter maneja plurales complejos de forma predeterminada, así que estamos cubiertos. Utilicémoslo para localizar el contador de héroes en nuestra aplicación.

// ...
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
return Scaffold(
// ...
body: Padding(
// ...
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
// La cadena localizada debería reemplazar '6 Heroes'
child: Text('6 Heroes'),
),
// ...
],
),
),
);
}
}
// ...
Lenguaje del código: Dart (dart)
Primero, agreguemos el mensaje a nuestro archivo ARB de plantilla en inglés.
{
// ...
"heroCount": "{count,plural, =0{No heroes yet} =1{1 hero} other{{count} heroes}}",
"@heroCount": {
"placeholders": {
"count": {}
}
},
// ...
}
Lenguaje del código: JSON / JSON con comentarios (json)
Especificamos un count marcador de posición en nuestro mensaje, y lo usamos con la sintaxis especial {count,plural,...} para definir las diferentes formas plurales.
✋ Atención » El parámetro count siempre será de tipo int. Si especificas otro tipo para count, Flutter lo ignorará y usará int de todos modos.
🗒️ Nota » Puedes agregar marcadores de posición además de count a un mensaje plural; se especifican como de costumbre (ver Interpolación arriba).
Flutter admite las siguientes formas plurales.
- zero ➞
=0{No heroes} - one ➞
=1{One hero} - two ➞
=2(Two heroes} - few ➞
few{The {count} heroes} - many ➞
many{{count} heroes} - other ➞
other{{count} heroes}
few, many, y other tienen diferentes significados dependiendo del idioma activo. La única forma requerida en cualquier idioma es la forma other.
🗒️ Nota » No necesitábamos usar la forma cero =0 en nuestro mensaje en inglés anterior. Si la hubiéramos omitido, Flutter habría usado nuestra forma other en su lugar.
Muy bien, agreguemos nuestro mensaje en árabe. Como mencionamos anteriormente, el árabe tiene seis formas plurales.
{
// ...
"heroCount": "{count,plural, =0{لا توجد أبطال بعد} =1{بطل واحد} =2{بطلان} few{{count} أبطال} many{{count} بطل} other{{count} بطل}}",
// ...
}Lenguaje del código: JSON / JSON con comentarios (json)
Ahora conectemos todo y usemos nuestro nuevo mensaje en nuestro HeroList widget.
// ...
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
var t = AppLocalizations.of(context);
return Scaffold(
// ...
body: Padding(
// ...
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(t.heroCount(6)),
),
// ...
],
),
),
);
}
}
// ...
Lenguaje del código: Dart (dart)
Por supuesto, en una aplicación de producción, el parámetro de conteo pasado a t.heroCount() sería dinámico. Flutter elige la forma plural correcta de nuestro mensaje dependiendo de la localización activa.


Formato de números
Podemos formatear números en nuestros mensajes localizados usando nuestro amigo el placeholders objeto en las entradas compañeras de nuestro archivo de plantilla ARB (inglés en nuestro caso). No hay un buen lugar para poner el formato de números en nuestra pequeña aplicación de demostración, así que solo pretendamos que tenemos una aplicación de comercio electrónico para demostrar.
// app_en.arb en la aplicación de ejemplo que tiene un carrito de compras
{
"itemTotal": "Your total is: {value}",
"@itemTotal": {
"placeholders": {
"value": {
"type": "double",
"format": "currency"
}
}
}
}
Lenguaje del código: JSON / JSON con comentarios (json)
Nota que especificamos un tipo explícito y un formato para controlar cómo se mostrará el número. Como de costumbre, podemos traducir nuestro mensaje en nuestros otros archivos de localización.
// app_ar.arb en la aplicación de ejemplo que tiene un carrito de compras
{ "itemTotal": "Total: {value}" }Lenguaje del código: JSON / JSON con comentarios (json)
Después de recargar nuestra aplicación, podemos usar nuestro mensaje como de costumbre.
// En un Widget
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// En algún constructor de widget con un contexto
var t = AppLocalizations.of(context);
var message = t.itemTotal(56.12);
// => "Tu total es USD56.12" cuando la localización actual es inglés
// => "إجمالي: EGP56.12" cuando la localización actual es árabe
Lenguaje del código: Dart (dart)
✋ Atención » No puedes anular el número formatos por localización. El formato que especifiques en tu plantilla de localización (inglés en nuestro caso) se utilizará en todas las localizaciones independientemente de cualquier formato que anules en tus otros archivos de localización.
Recuerda que, entre bastidores, Flutter está utilizando la biblioteca Dart intl para la mayor parte de su trabajo de ínternacionalización. El formato de moneda que usamos arriba es uno de varios formatos integrados en el formateador de números intl. Otros formatos incluyen decimales, porcentajes y más.
🔗 Recurso » Consulta la guía del usuario oficial para todos los formatos disponibles.`
Sin embargo, no tenemos que depender de Flutter para pasar nuestros números a intl. Podemos usar intl directamente para obtener más control sobre nuestro formateo de números.
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:intl/intl.dart';
// En algún constructor de Widget con un contexto
var currentLocale = AppLocalizations.of(context).localeName;
var compact = NumberFormat.compact(locale: currentLocale).format(6000000);
// => "6M" cuando la configuración regional actual es inglés de EE. UU.
// => "٦ مليون" cuando la configuración regional actual es árabe egipcio
var simpleCurrency = NumberFormat.simpleCurrency(locale: currentLocale).format(14.24);
// => "$14.24" cuando la configuración regional actual es inglés de EE. UU.
// => "ج.م. ١٤٫٢٤" cuando la configuración regional actual es árabeLenguaje del código: Dart (dart)
🔗 Recurso » No tienes por qué usar los formatos predefinidos como compact y simpleCurrency. El constructor internacional NumberFormat te da control granular sobre tus formatos de número. Lee todo sobre el tema en la documentación oficial.
✋ Atención » La única forma en que pude obtener números árabes orientales (١،٢،٣…) renderizando para árabe es configurando el parámetro locale como "ar_EG" (árabe egipcio). Ni "ar" ni ninguna variante de "ar_XX" que no fuera el egipcio me funcionó.
✋ Atención » Los formatos no me funcionaron con la variable plural count. Parece que Flutter está sobrescribiendo el formato cuando procesa plurales. Si puedes obtener formatos en tus plurales, por favor háznoslo saber cómo lo hiciste en los comentarios a continuación.
Formato de fecha
Nuestros héroes actualmente tienen fechas de nacimiento codificadas que no están localizadas, lo cual no es lo ideal.

Recuerda que estamos renderizando a cada uno de nuestros héroes con un widget HeroCard.
import 'package:flutter/material.dart';
class HeroCard extends StatelessWidget {
final String name;
final String born;
final String bio;
final String imagePath;
// ...
const HeroCard({
Key key,
this.name = '',
this.born = '',
this.bio = '',
this.imagePath,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// ...
return Card(
child: Padding(
// ...
Padding(
padding: const EdgeInsets.only(top: 2, bottom: 4),
child: Text(
born.isEmpty ? '' : 'Born $born',
// ...
),
),
// ...
),
);
}
}Lenguaje del código: Dart (dart)
Para formatear la fecha born para cada región que admite nuestra aplicación, primero agregamos algunos nuevos mensajes localizados con valores de fecha interpolados.
// English
{
// ...
"heroBorn": "Born {date}",
"@heroBorn": {
"placeholders": {
"date": {
"type": "DateTime",
"format": "yMMMd"
}
}
},
// ...
}
// Árabe
{
// ...
"heroBorn": "تاريخ الميلاد {date}",
// ...
}Lenguaje del código: JSON / JSON con comentarios (json)
Cuando definimos nuestro date marcador de posición en nuestro archivo de localización de plantilla, necesitamos darle el tipo DateTime. Luego podemos usar un format para especificar cómo queremos mostrar la fecha. El formato yMMMd que definimos arriba significa “año, mes abreviado, día”, que en inglés estadounidense se vería algo como “Dec 9, 1906”.
🔗 Recurso » De hecho, entre bastidores, Flutter está usando la clase DateFormat de intl, generando código como DateFormat.yMMMd(localeName).format(date). El constructor nombrado yMMMd es un atajo útil llamado “esqueleto”, y hay bastantes que podemos usar. Consúltalos en la documentación oficial de DateFormat.
🗒️ Nota » No tuvimos que llamar a nuestra variable de marcador de posición date. Podríamos haberle dado cualquier nombre, siempre que fuera un nombre de parámetro de función Dart válido.
Bien, conectemos esto en nuestro widget para mostrar nuestros nuevos mensajes.
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class HeroCard extends StatelessWidget {
final String name;
final String born;
final String bio;
final String imagePath;
// ...
final DateTime bornDateTime;
HeroCard({
Key key,
this.name = '',
this.born = '',
this.bio = '',
this.imagePath,
}) : bornDateTime = DateFormat('d MMMM yyyy').parse(born),
super(key: key);
@override
Widget build(BuildContext context) {
// ...
var t = AppLocalizations.of(context);
return Card(
child: Padding(
// ...
Padding(
padding: const EdgeInsets.only(top: 2, bottom: 4),
child: Text(
born.isEmpty ? '' : t.heroBorn(bornDateTime),
// ...
),
),
// ...
),
);
}
}Lenguaje del código: Dart (dart)
Se nos entrega la fecha de nacimiento de un héroe como String, así que necesitamos convertirlo a DateTime primero. Usamos la clase DateFormat de intl para hacer esto en nuestro constructor de widget.
En el método build, simplemente pasamos el DateTime analizado a nuestro mensaje localizado t.heroBorn(). Esto nos da fechas bien localizadas.


¿Qué pasa si no queremos usar ninguno de los esqueletos predefinidos y queremos personalizar completamente nuestros formatos de fecha? Bueno, al igual que el formato de números (ver arriba), necesitaríamos usar la clase intl.DateFormat directamente. Supongamos que queremos mostrar las fechas de nacimiento de nuestro héroes en un formato como “1912-06-23”. Haremos algo como lo siguiente.
import 'package:intl/intl.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// In widget builder with context
var t = AppLocalizations.of(context);
var bornDateTime = DateTime(1912, 6, 23);
var formattedBorn = DateFormat('yyyy-MM-dd', t.localeName).format(bornDateTime);
var message = t.heroBorn(formattedBorn);
// => "1912-06-23" in US English
// => "١٩١٢-٠٦-٢٣" in Egyptian Arabic
Lenguaje del código: JavaScript (javascript)
Nuestros mensajes localizados de heroBorn en nuestros archivos ARB tomarían simplemente parámetros regulares de Object o String, ya que ya hemos hecho el formato para ellos.
Direccionalidad: de izquierda a derecha y de derecha a izquierda
Mientras que el inglés es un idioma de izquierda a derecha (LTR), el árabe va en la otra dirección y se organiza de derecha a izquierda (RTL). Esto está causando actualmente un problema para nosotros cuando nuestra aplicación se usa en un dispositivo con árabe como idioma del sistema.

La imagen y el texto en cada tarjeta están alineados porque estamos usando EdgeInsets.only(right) para definir el padding alrededor de nuestra imagen.
import 'package:intl/intl.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// In widget builder with context
var t = AppLocalizations.of(context);
var bornDateTime = DateTime(1912, 6, 23);
var formattedBorn = DateFormat('yyyy-MM-dd', t.localeName).format(bornDateTime);
var message = t.heroBorn(formattedBorn);
// => "1912-06-23" in US English
// => "١٩١٢-٠٦-٢٣" in Egyptian Arabic
Lenguaje del código: Dart (dart)
Esto funciona en idiomas LTR, donde queremos un margen derecho entre la imagen y el texto. En idiomas RTL, sin embargo, queremos el margen a la izquierda.
Un remedio fácil aquí es usar EdgeInsetsDirectional en lugar de EdgeInsets.
class HeroCard extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
// ...
return Card(
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsetsDirectional.only(end: 8.0),
child: ClipRRect(
// Imagen en retrato...
),
),
Expanded(
// Widgets de texto...
),
],
),
),
);
}
}Lenguaje del código: PHP (php)
Observa que usamos end en lugar de right para establecer el padding entre la imagen y el texto. EdgeInsetsDirectional es uno de los pocos widgets de diseño de Flutter que tienen en cuenta la dirección de la región. Estos widgets usan los parámetros start y end en lugar de left y right. Y lo genial es que estos widgets direccionales harán lo correcto automáticamente para la región activa:
start==leftpara idiomas leídos de izquierda a derechastart==rightpara idiomas leídos de derecha a izquierdaend==rightpara idiomas leídos de izquierda a derechaend==leftpara idiomas leídos de derecha a izquierda
Con este pequeño ajuste al código, nuestro problema de diseño se ha resuelto.

🔗 Recurso » En el momento de escribir, la documentación oficial de Flutter enumera los siguientes widgets direccionales:
- EdgeInsetsDirectional
- AlignmentDirectional
- BorderDirectional
- BorderRadiusDirectional
- PositionedDirectional
- AnimatedPositionedDirectional
Con todo eso en su sitio, nuestra aplicación final tiene un aspecto de lo más internacionalizado.

🔗 Recurso » Obtén el código completo para nuestra aplicación de demostración en nuestro repositorio de GitHub.
Agregando activos localizados
Localizar imágenes en Flutter implica usar diferentes conjuntos de imágenes para diferentes localizaciones o idiomas en tu aplicación. Esto se suele hacer para mostrar imágenes con texto o contenido que coincida con el idioma preferido del usuario.
Vamos a agregar una bandera, que se muestra de manera diferente dependiendo de la región del usuario.

Comenzaremos agregando las imágenes, una para cada localización que planeamos soportar. Deberíamos organizar estas imágenes en una estructura de carpetas basada en localizaciones, por ejemplo:
└── assets/
└── images/
├── eg/
│ └── flag.jpg
├── us/
│ └── flag.jpg
└── flag.jpgLenguaje del código: texto plano (plaintext)
Para este tutorial, agregaremos imágenes de bandera para Egipto y los EE. UU. El archivo directamente en la carpeta de imágenes (sin localización) es una imagen de respaldo para otras regiones.
A continuación, registremos los archivos en el archivo pubspec.yml.
# pubspec.yml
# ...
flutter:
# ...
assets:
# ...
- assets/images/in/flag.jpg
- assets/images/us/flag.jpg
- assets/images/flag.jpg
# ...Lenguaje del código: YAML (yaml)
Ahora podemos cargar las imágenes en nuestro archivo hero_list.dart:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../widgets/hero_card.dart';
class HeroList extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ...
String getImagePath(String imageName) {
String basePath = 'assets/images/';
// Cuando el código de país no es compatible,
// mostramos la imagen de respaldo.
if (locale.countryCode?.isEmpty == true) {
return basePath + 'flag.jpg';
}
String localePath = '${locale.countryCode!.toLowerCase()}/';
return basePath + localePath + imageName;
}
return Scaffold(
// ...
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// ...
Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: Image.asset(
getImagePath('flag.jpg'),
width: 40,
height: 40,
),
),
],
),
),
);
}
}Lenguaje del código: Dart (dart)
Ahora, en el archivo main.dart, haz los siguientes cambios
...
class MyApp extends StatelessWidget {
const MyApp({super.key});
// Este widget es la raíz de tu aplicación.
@override
Widget build(BuildContext context) {
return MaterialApp(
...
supportedLocales: [
const Locale('ar', ''),
const Locale('en', ''),
// Agregar los códigos de región soportados
const Locale('ar', 'EG'),
const Locale('en', 'US'),
],
...
}
}Lenguaje del código: Dart (dart)
Ahora la aplicación muestra el recurso apropiado para la localización del usuario y muestra un recurso alternativo si su localización no es compatible.

Cambiando el idioma en la aplicación
A veces un usuario querrá tener su sistema operativo en un idioma y una aplicación específica en otro. Para acomodar eso, agreguemos un selector de idioma en nuestra aplicación. Debería funcionar para todas las plataformas compatibles con Flutter.

En el archivo main.dart, haremos los siguientes cambios:
void main() {
runApp(const MyApp());
}
// Necesitamos hacer que MyApp sea Stateful porque
// necesita reaccionar cuando la Localización cambia.
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
static void setLocale(BuildContext context, Locale newLocale) {
_MyAppState? state = context.findAncestorStateOfType<_MyAppState>();
state?.setLocale(newLocale);
}
}
class _MyAppState extends State<MyApp> {
Locale? _locale;
setLocale(Locale locale) {
setState(() {
_locale = locale;
});
}
// Este widget es la raíz de tu aplicación.
@override
Widget build(BuildContext context) {
return MaterialApp(
// ...
locale: _locale,
initialRoute: '/',
routes: {
'/': (context) {
return HeroList(title: AppLocalizations.of(context)!.appTitle);
},
},
);
}
}Lenguaje del código: Dart (dart)
Ahora, en el archivo hero_list.dart, hagamos el menú desplegable y llamemos al método setLocale() que definimos arriba.
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../widgets/hero_card.dart';
class HeroList extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
var t = AppLocalizations.of(context)!;
final Locale locale = Localizations.localeOf(context);
// Opciones del menú desplegable
var items = [
'en',
'ar',
];
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: <Widget>[
DropdownButton(
// Icono de flecha hacia abajo
icon: const Icon(Icons.settings, color: Colors.white,),
items: items.map((String items) {
return DropdownMenuItem(
value: items,
child: Text(items),
);
}).toList(),
onChanged: (String? newValue) {
MyApp.setLocale(context, Locale(newValue));
},
),
],
),
// ...
);
}
}Lenguaje del código: Dart (dart)
Con eso, el usuario puede seleccionar el idioma de la aplicación independientemente del idioma del sistema.
🗒️ Nota » Antes de seleccionar una localización, se establecerá en la localización predeterminada del sistema del usuario.

🗒️ Nota » Antes de que el usuario seleccione manualmente una localización en la aplicación, se utiliza la localización del sistema como predeterminada. Sin embargo, la localización seleccionada manualmente en la aplicación por el usuario no persistirá cuando se reinicie la aplicación. Para persistir la preferencia de localización en la aplicación del usuario, necesitas guardar y recuperar la localización usando algo como el plugin de Preferencias Compartidas.
El proyecto final está disponible en GitHub.
Localización Flutter simplificada
Esperamos que hayas disfrutado de nuestro tutorial de localización Flutter y hayas aprendido algunos trucos útiles. Ahora, si estás listo para llevar tu juego de localización a un nivel superior, Phrase Strings es tu solución ideal. Phrase Strings, un asistente de localización para tus aplicaciones Flutter, incluye soporte para archivos ARB, y es muy intuitivo para los desarrolladores gracias a su API y CLI fáciles de usar. También cuenta con un elegante editor de cadenas que facilita la vida para las traducciones.
Además, se sincroniza sin problemas con GitHub, GitLab y Bitbucket, e incluso ofrece traducciones por aire para aplicaciones móviles para manejar la carga pesada de la localización, permitiéndote concentrarte en el código que tanto amas. Consulta todas las características de Phrase para desarrolladores y comprueba por ti mismo cómo pueden ayudarte a llevar tus aplicaciones a nivel global más rápidamente.


