Cómo crear una aplicación multilingüe: una demostración con PHP y Gettext
Publicado: 2022-03-11Ya sea que esté creando un sitio web o una aplicación web completa, hacerlo accesible a un público más amplio a menudo requiere que esté disponible en diferentes idiomas y lugares.
Las diferencias fundamentales entre la mayoría de los lenguajes humanos hacen que esto sea todo menos fácil. Las diferencias en las reglas gramaticales, los matices del idioma, los formatos de fecha y más se combinan para hacer de la localización un desafío único y formidable.
Considere este ejemplo simple.
Las reglas de pluralización en inglés son bastante sencillas: puedes tener una forma singular de una palabra o una forma plural de una palabra.
Sin embargo, en otros idiomas, como los idiomas eslavos, hay dos formas plurales además del singular. Incluso puede encontrar idiomas con un total de cuatro, cinco o seis formas plurales, como en esloveno, irlandés o árabe.
La forma en que se organiza su código y cómo se diseñan sus componentes e interfaz juega un papel importante en la determinación de la facilidad con la que puede localizar su aplicación.
La internacionalización (i18n) de su base de código ayuda a garantizar que se pueda adaptar a diferentes idiomas o regiones con relativa facilidad. La internacionalización generalmente se realiza una vez, preferiblemente al comienzo del proyecto para evitar la necesidad de grandes cambios en el código fuente en el futuro.
Una vez que su base de código se ha internacionalizado, la localización (l10n) se convierte en una cuestión de traducir el contenido de su aplicación a un idioma/lugar específico.
La localización debe realizarse cada vez que se necesita admitir un nuevo idioma o región. Además, cada vez que se actualiza una parte de la interfaz (que contiene texto), hay nuevo contenido disponible, que luego debe localizarse (es decir, traducirse) a todas las configuraciones regionales admitidas.
En este artículo, aprenderemos cómo internacionalizar y localizar software escrito en PHP. Revisaremos las diversas opciones de implementación y las diferentes herramientas que están disponibles a nuestra disposición para facilitar el proceso.
Herramientas para la internacionalización
La forma más sencilla de internacionalizar el software PHP es mediante el uso de archivos de matriz. Las matrices se completarán con cadenas traducidas, que luego se pueden buscar desde las plantillas:
<h1><?=$TRANS['title_about_page']?></h1>
Sin embargo, esta no es una forma recomendada para proyectos serios, ya que definitivamente planteará problemas de mantenimiento en el futuro. Algunos problemas pueden incluso aparecer al principio, como la falta de compatibilidad con la interpolación variable o la pluralización de sustantivos, etc.
Una de las herramientas más clásicas (a menudo tomada como referencia para i18n y l10n) es una herramienta de Unix llamada Gettext.
Aunque data de 1995, sigue siendo una herramienta integral para traducir software que también es fácil de usar. Si bien es bastante fácil comenzar, todavía tiene poderosas herramientas de apoyo.
Gettext es lo que usaremos en esta publicación. Presentaremos una excelente aplicación GUI que se puede usar para actualizar fácilmente sus archivos fuente l10n, evitando así la necesidad de lidiar con la línea de comandos.
Bibliotecas para facilitar las cosas
Existen importantes marcos web y bibliotecas de PHP que admiten Gettext y otras implementaciones de i18n. Algunos son más fáciles de instalar que otros, tienen funciones adicionales o admiten diferentes formatos de archivo i18n. Aunque en este documento nos enfocamos en las herramientas provistas con el núcleo de PHP, aquí hay una lista de algunas otras que vale la pena mencionar:
oscarotero/Gettext: soporte Gettext con una interfaz orientada a objetos; incluye funciones auxiliares mejoradas, potentes extractores para varios formatos de archivo (algunos de ellos no son compatibles de forma nativa con el comando
gettext
). También puede exportar a formatos más allá de los archivos .mo/.po, lo que puede ser útil si necesita integrar sus archivos de traducción en otras partes del sistema, como una interfaz de JavaScript.symfony/translation: Admite muchos formatos diferentes, pero recomienda usar XLIFF detallados. No incluye funciones auxiliares ni un extractor integrado, pero admite marcadores de posición mediante
strtr()
internamente.zend/i18n: admite archivos de matriz e INI, o formatos Gettext. Implementa una capa de almacenamiento en caché para evitar tener que leer el sistema de archivos cada vez. También incluye asistentes de visualización y filtros y validadores de entrada que reconocen la configuración regional. Sin embargo, no tiene extractor de mensajes.
Otros marcos también incluyen módulos i18n, pero no están disponibles fuera de sus bases de código:
Laravel: admite archivos de matriz básicos; no tiene extractor automático pero incluye un ayudante
@lang
para archivos de plantilla.Yii: admite la traducción basada en matriz, Gettext y base de datos, e incluye un extractor de mensajes. Respaldado por la extensión
Intl
, disponible desde PHP 5.3, y basado en el proyecto ICU. Esto le permite a Yii ejecutar poderosos reemplazos, como deletrear números, formatear fechas, horas, intervalos, moneda y ordinales.
Si decide optar por una de las bibliotecas que no proporcionan extractores, es posible que desee utilizar los formatos de Gettext, de modo que pueda utilizar la cadena de herramientas original de Gettext (incluido Poedit) como se describe en el resto del capítulo.
Instalando Gettext
Es posible que deba instalar Gettext y la biblioteca PHP relacionada utilizando su administrador de paquetes, como apt-get o yum. Después de instalarlo, habilítelo agregando extension=gettext.so
(Linux/Unix) o extension=php_gettext.dll
(Windows) a su archivo php.ini
.
Aquí también usaremos Poedit para crear archivos de traducción. Probablemente lo encontrará en el administrador de paquetes de su sistema; está disponible para Unix, Mac y Windows y también se puede descargar de forma gratuita en su sitio web.
Tipos de archivos Gettext
Hay tres tipos de archivos con los que normalmente trabaja mientras trabaja con Gettext.
Los principales son archivos PO (Portable Object) y MO (Machine Object), el primero es una lista de "objetos traducidos" legibles y el segundo es el binario correspondiente (para ser interpretado por Gettext al realizar la localización). También hay un archivo POT (plantilla de PO), que simplemente contiene todas las claves existentes de sus archivos de origen y puede usarse como guía para generar y actualizar todos los archivos de PO.
Los archivos de plantilla no son obligatorios; dependiendo de la herramienta que esté usando para hacer l10n, estará bien solo con archivos PO/MO. Tendrá un par de archivos PO/MO por idioma y región, pero solo un POT por dominio.
Separación de dominios
Hay algunos casos, en grandes proyectos, en los que es posible que necesite separar las traducciones cuando las mismas palabras transmiten un significado diferente en diferentes contextos.
En esos casos, deberá dividirlos en diferentes "dominios", que son básicamente grupos con nombres de archivos POT/PO/MO, donde el nombre del archivo es dicho dominio de traducción .
Los proyectos pequeños y medianos generalmente, por simplicidad, usan solo un dominio; su nombre es arbitrario, pero usaremos "principal" para nuestros ejemplos de código.
En los proyectos de Symfony, por ejemplo, los dominios se utilizan para separar la traducción de los mensajes de validación.
Código de configuración regional
Una configuración regional es simplemente un código que identifica una versión de un idioma. Se define siguiendo las especificaciones ISO 639-1 e ISO 3166-1 alpha-2: dos letras minúsculas para el idioma, seguidas opcionalmente de un guión bajo y dos letras mayúsculas que identifican el código de país o región.
Para idiomas raros, se utilizan tres letras.
Para algunos hablantes, la parte del país puede parecer redundante. De hecho, algunos idiomas tienen dialectos en diferentes países, como el alemán austriaco (de_AT) o el portugués brasileño (pt_BR). La segunda parte se usa para distinguir entre esos dialectos: cuando no está presente, se toma como una versión "genérica" o "híbrida" del idioma.
Estructura de directorios
Para usar Gettext, necesitaremos adherirnos a una estructura específica de carpetas.
Primero, deberá seleccionar una raíz arbitraria para sus archivos l10n en su repositorio de origen. En su interior, tendrá una carpeta para cada configuración regional necesaria y una carpeta fija "LC_MESSAGES" que contendrá todos sus pares PO/MO.
Formas plurales
Como dijimos en la introducción, diferentes idiomas pueden tener diferentes reglas de pluralización. Sin embargo, Gettext nos ahorra este problema.
Al crear un nuevo archivo .po, deberá declarar las reglas de pluralización para ese idioma, y las piezas traducidas que son sensibles al plural tendrán una forma diferente para cada una de esas reglas.
Al llamar a Gettext en código, deberá especificar un número relacionado con la oración (por ejemplo, para la frase "Tiene n mensajes", deberá especificar el valor de n), y se resolverá en la forma correcta. usar, incluso usando la sustitución de cadenas si es necesario.
Las reglas plurales se componen del número de reglas necesarias con una prueba booleana para cada regla (se puede omitir la prueba de una regla como máximo). Por ejemplo:
japonés:
nplurals=1; plural=0;
nplurals=1; plural=0;
- una regla: no hay formas pluralesInglés:
nplurals=2; plural=(n != 1);
nplurals=2; plural=(n != 1);
- dos reglas: use la forma plural solo cuando n no es 1, de lo contrario use la forma singular.portugués brasileño:
nplurals=2; plural=(n > 1);
nplurals=2; plural=(n > 1);
- dos reglas, use la forma plural solo cuando n es mayor que 1, de lo contrario use la forma singular.
Para una explicación más profunda, hay un tutorial informativo de LingoHub disponible en línea.
Gettext determinará qué regla usar según el número proporcionado y usará la versión localizada correcta de la cadena. Para las cadenas en las que se debe manejar la pluralización, deberá incluir en el archivo .po una oración diferente para cada regla de plural definida.
Ejemplo de implementación
Después de toda esa teoría, pongámonos un poco prácticos. Aquí hay un extracto de un archivo .po (no se preocupe demasiado por la sintaxis, sino que tenga una idea del contenido general):
msgid "" msgstr "" "Language: pt_BR\n" "Content-Type: text/plain; charset=UTF-8\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" msgid "We're now translating some strings" msgstr "Nos estamos traduzindo algumas strings agora" msgid "Hello %1$s! Your last visit was on %2$s" msgstr "Ola %1$s! Sua ultima visita foi em %2$s" msgid "Only one unread message" msgid_plural "%d unread messages" msgstr[0] "So uma mensagem nao lida" msgstr[1] "%d mensagens nao lidas"
La primera sección funciona como un encabezado, con msgid
y msgstr
vacíos.
Describe la codificación del archivo, las formas plurales y algunas cosas más. La segunda sección traduce una cadena simple del inglés al portugués brasileño, y la tercera hace lo mismo, pero aprovecha el reemplazo de cadenas de sprintf
, lo que permite que la traducción contenga el nombre de usuario y la fecha de visita.
La última sección es una muestra de formas de pluralización, mostrando la versión singular y plural como msgid
en inglés y sus correspondientes traducciones como msgstr
0 y 1 (siguiendo el número dado por la regla del plural).
Allí, también se usa el reemplazo de cadenas, por lo que el número se puede ver directamente en la oración, usando %d
. Las formas plurales siempre tienen dos msgid
(singular y plural), por lo que se recomienda no utilizar un idioma complejo como fuente de traducción.
Claves de Localización
Como habrás notado, estamos usando la oración en inglés real como ID de fuente. Ese msgid
es el mismo que se usa en todos sus archivos .po, lo que significa que otros idiomas tendrán el mismo formato y los mismos campos de msgid
pero líneas de msgstr
traducidas.
Hablando de claves de traducción, aquí hay dos enfoques "filosóficos" estándar:
1. msgstr como oración real
Las principales ventajas de este enfoque son:
Si hay partes del software sin traducir en un idioma determinado, la clave que se muestra seguirá manteniendo algún significado. Por ejemplo, si sabe cómo traducir del inglés al español pero necesita ayuda para traducir al francés, puede publicar la nueva página con oraciones faltantes en francés y, en su lugar, partes del sitio web se mostrarán en inglés.
Es mucho más fácil para el traductor entender lo que está pasando y hacer una traducción adecuada basada en el
msgid
.Le da l10n "gratuito" para un idioma: el de origen.
Por otro lado, la desventaja principal es que, si necesita cambiar el texto real, debe reemplazar el mismo msgid
en varios archivos de idioma.
2. msgid como clave estructurada única
Esto describiría la función de la oración en la aplicación de forma estructurada, incluida la plantilla o parte donde se encuentra la cadena en lugar de su contenido.
Esta es una excelente manera de organizar el código, separando el contenido del texto de la lógica de la plantilla. Sin embargo, eso podría presentar problemas al traductor que perdería el contexto.
Se necesitaría un archivo de idioma de origen como base para otras traducciones. Por ejemplo, lo ideal sería que el desarrollador tuviera un archivo "en.po", que los traductores pudieran leer para entender qué escribir en "fr.po".

Las traducciones faltantes mostrarían teclas sin sentido en la pantalla ("top_menu.welcome" en lugar de "¡Hola, usuario!" en dicha página en francés sin traducir).
Eso es bueno, ya que obligaría a que la traducción se complete antes de la publicación, pero es malo, ya que los problemas de traducción serían realmente terribles en la interfaz. Sin embargo, algunas bibliotecas incluyen una opción para especificar un idioma determinado como "alternativo", con un comportamiento similar al del otro enfoque.
El manual de Gettext favorece el primer enfoque ya que, en general, es más fácil para los traductores y usuarios en caso de problemas. Ese es el enfoque que usaremos aquí también.
Sin embargo, debe tenerse en cuenta que la documentación de Symfony favorece la traducción basada en palabras clave, para permitir cambios independientes de todas las traducciones sin afectar también a las plantillas.
Uso diario
En una aplicación común, usaría algunas funciones Gettext mientras escribe texto estático en sus páginas.
Esas oraciones luego aparecerían en archivos .po, se traducirían, compilarían en archivos .mo y luego Gettext las usaría al representar la interfaz real. Dado eso, unamos lo que hemos discutido hasta ahora en un ejemplo paso a paso:
1. Un archivo de plantilla de muestra, que incluye algunas llamadas gettext diferentes
<?php include 'i18n_setup.php' ?> <div> <h1><?=sprintf(gettext('Welcome, %s!'), $name)?></h1> <!-- code indented this way only for legibility → <?php if ($unread): ?> <h2> <?=sprintf( ngettext('Only one unread message', '%d unread messages', $unread), $unread )?> </h2> <?php endif ?> </div> <h1><?=gettext('Introduction')?></h1> <p><?=gettext('We\'re now translating some strings')?></p>
gettext()
simplemente traduce unmsgid
a sumsgstr
correspondiente para un idioma determinado. También está la función abreviada_()
que funciona de la misma manerangettext()
hace lo mismo pero con reglas en pluralTambién hay
dgettext()
ydngettext()
, que le permiten anular el dominio para una sola llamada (más información sobre la configuración del dominio en el siguiente ejemplo)
2. Un archivo de configuración de muestra (i18n_setup.php como se usó anteriormente), seleccionando la configuración regional correcta y configurando Gettext
El uso de Gettext implica un poco de código repetitivo, pero se trata principalmente de configurar el directorio local y elegir los parámetros apropiados (una configuración regional y un dominio).
<?php /** * Verifies if the given $locale is supported in the project * @param string $locale * @return bool */ function valid($locale) { return in_array($locale, ['en_US', 'en', 'pt_BR', 'pt', 'es_ES', 'es'); } //setting the source/default locale, for informational purposes $lang = 'en_US'; if (isset($_GET['lang']) && valid($_GET['lang'])) { // the locale can be changed through the query-string $lang = $_GET['lang']; //you should sanitize this! setcookie('lang', $lang); //it's stored in a cookie so it can be reused } elseif (isset($_COOKIE['lang']) && valid($_COOKIE['lang'])) { // if the cookie is present instead, let's just keep it $lang = $_COOKIE['lang']; //you should sanitize this! } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // default: look for the languages the browser says the user accepts $langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']); array_walk($langs, function (&$lang) { $lang = strtr(strtok($lang, ';'), ['-' => '_']); }); foreach ($langs as $browser_lang) { if (valid($browser_lang)) { $lang = $browser_lang; break; } } } // here we define the global system locale given the found language putenv("LANG=$lang"); // this might be useful for date functions (LC_TIME) or money formatting (LC_MONETARY), for instance setlocale(LC_ALL, $lang); // this will make Gettext look for ../locales/<lang>/LC_MESSAGES/main.mo bindtextdomain('main', '../locales'); // indicates in what encoding the file should be read bind_textdomain_codeset('main', 'UTF-8'); // if your application has additional domains, as cited before, you should bind them here as well bindtextdomain('forum', '../locales'); bind_textdomain_codeset('forum', 'UTF-8'); // here we indicate the default domain the gettext() calls will respond to textdomain('main'); // this would look for the string in forum.mo instead of main.mo // echo dgettext('forum', 'Welcome back!'); ?>
3. Preparando la traducción para la primera ejecución
Una de las grandes ventajas que tiene Gettext sobre los paquetes personalizados de framework i18n es su extenso y poderoso formato de archivo.
Tal vez estés pensando "Oh hombre, eso es bastante difícil de entender y editar a mano, ¡una matriz simple sería más fácil!" No se equivoque, las aplicaciones como Poedit están aquí para ayudar, mucho. Puede obtener el programa desde su sitio web, es gratuito y está disponible para todas las plataformas. Es una herramienta bastante fácil de usar y muy poderosa al mismo tiempo, ya que utiliza todas las funciones que Gettext tiene disponibles. Estaremos trabajando aquí con la última versión, Poedit 1.8.
En la primera ejecución, debe seleccionar "Archivo> Nuevo..." en el menú. Se le preguntará por el idioma; seleccione/filtre el idioma al que desea traducir, o use el formato que mencionamos anteriormente, como en_US
o pt_BR
.
Ahora, guarde el archivo, utilizando también la estructura de directorios que mencionamos. Luego debe hacer clic en "Extraer de las fuentes", y aquí configurará varias configuraciones para las tareas de extracción y traducción. Podrá encontrarlos todos más adelante a través de “Catálogo > Propiedades”:
Rutas de origen: incluya todas las carpetas del proyecto donde se llama a
gettext()
(y sus hermanos); esta suele ser su(s) carpeta(s) de plantillas/vistas. Esta es la única configuración obligatoria.Propiedades de traducción:
- Nombre y versión del proyecto, Equipo y dirección de correo electrónico del Equipo: Información útil que va en el encabezado del archivo .po.
- Formas plurales: Estas son las reglas que mencionamos antes. Puede dejarlo con la opción predeterminada la mayor parte del tiempo, ya que Poedit ya incluye una práctica base de datos de reglas plurales para muchos idiomas.
- Juegos de caracteres: UTF-8, preferiblemente.
- Juego de caracteres del código fuente: el juego de caracteres utilizado por su base de código, probablemente UTF-8 también, ¿verdad?
Palabras clave de origen: el software subyacente sabe cómo se ven
gettext()
y llamadas a funciones similares en varios lenguajes de programación, pero también puede crear sus propias funciones de traducción. Será aquí donde agregará esos otros métodos. Esto se discutirá más adelante en la sección "Consejos".
Después de configurar esas propiedades, Poedit ejecutará un escaneo a través de sus archivos fuente para encontrar todas las llamadas de localización. Después de cada escaneo, Poedit mostrará un resumen de lo que se encontró y lo que se eliminó de los archivos de origen. Las nuevas entradas estarán vacías en la tabla de traducción, lo que le permitirá ingresar las versiones localizadas de esas cadenas. Guárdelo y un archivo .mo será (re)compilado en la misma carpeta y, ¡presto!, ¡su proyecto está internacionalizado!
Poedit también puede sugerir traducciones comunes de la web y de archivos anteriores. Es útil para que solo tenga que verificar si tienen sentido y aceptarlos. Si no está seguro acerca de una traducción, puede marcarla como Fuzzy y se mostrará en amarillo. Las entradas azules son las que no tienen traducción.
4. Traducir cadenas
Como habrás notado, hay dos tipos principales de cadenas localizadas: las simples y las que tienen formas plurales.
Los simples tienen solo dos casillas: fuente y cadena localizada. La cadena de origen no se puede modificar, ya que Gettext/Poedit no incluye la capacidad de modificar sus archivos de origen; más bien, deberá cambiar la fuente en sí y volver a escanear los archivos. ( Sugerencia: si hace clic con el botón derecho en una línea de traducción, se mostrará una sugerencia con los archivos de origen y las líneas donde se usa esa cadena).
Las cadenas de forma plural incluyen dos cuadros para mostrar las dos cadenas de origen y pestañas para que pueda configurar las diferentes formas finales.
Ejemplo de una cadena con una forma plural en Poedit, mostrando una pestaña de traducción para cada uno.
Cada vez que cambie sus archivos de código fuente y necesite actualizar las traducciones, simplemente presione Actualizar y Poedit volverá a escanear el código, eliminando las entradas inexistentes, fusionando las que cambiaron y agregando otras nuevas.
Poedit también puede tratar de adivinar algunas traducciones, basándose en otras que hiciste. Esas conjeturas y las entradas modificadas recibirán un marcador "Fuzzy", que indica que necesitan revisión, que se muestra en amarillo en la lista.
También es útil si tiene un equipo de traducción y alguien intenta escribir algo de lo que no está seguro: simplemente márquelo como Fuzzy y alguien más lo revisará más tarde.
Finalmente, se recomienda dejar marcado "Ver > Entradas sin traducir primero", ya que le ayudará a evitar olvidar cualquier entrada. Desde ese menú, también puede abrir partes de la interfaz de usuario que le permiten dejar información contextual para los traductores si es necesario.
consejos y trucos
Los servidores web pueden terminar almacenando en caché sus archivos .mo.
Si está ejecutando PHP como un módulo en Apache (mod_php), es posible que tenga problemas con el archivo .mo que se almacena en caché. Ocurre la primera vez que se lee y luego, para actualizarlo, es posible que deba reiniciar el servidor.
En Nginx y PHP5, por lo general, solo se necesitan un par de actualizaciones de página para actualizar el caché de traducción, y en PHP7 rara vez se necesita.
Las bibliotecas proporcionan funciones auxiliares para mantener el código de localización breve.
Como prefieren muchas personas, es más fácil usar _()
en lugar de gettext()
. Muchas bibliotecas i18n personalizadas de marcos también usan algo similar a t()
, para acortar el código traducido. Sin embargo, esa es la única función que tiene un atajo.
Es posible que desee agregar en su proyecto algunos otros, como __()
o _n()
para ngettext()
, o tal vez un elegante _r()
que se uniría a las llamadas gettext()
y sprintf()
. Otras bibliotecas, como Gettext de oscarotero, también proporcionan funciones auxiliares como estas.
En esos casos, deberá indicar a la utilidad Gettext cómo extraer las cadenas de esas nuevas funciones. No tengas miedo, es muy fácil. Es solo un campo en el archivo .po o una pantalla de Configuración en Poedit (en el editor, esa opción está dentro de "Catálogo> Propiedades> Palabras clave de fuentes").
Recuerde: Gettext ya conoce las funciones predeterminadas para muchos idiomas, así que no se preocupe si la lista parece vacía. Debe incluir en esa lista las especificaciones de las nuevas funciones, siguiendo este formato específico:
Si crea algo como
t()
, que simplemente devuelve la traducción de una cadena, puede especificarlo comot
. Gettext sabrá que el único argumento de la función es la cadena a traducir;Si la función tiene más de un argumento, puede especificar cuál es la primera cadena y, si es necesario, también la forma plural. Por ejemplo, si la firma de nuestra función es
__('one user', '%d users', $number)
, la especificación sería__:1,2
, lo que significa que la primera forma es el primer argumento y la segunda forma es el segundo argumento. Si su número aparece como el primer argumento, la especificación sería__:2,3
, lo que indica que la primera forma es el segundo argumento, y así sucesivamente.
Después de incluir esas nuevas reglas en el archivo .po, un nuevo escaneo traerá sus nuevas cadenas con la misma facilidad que antes.
Haga que su aplicación PHP sea multilingüe con Gettext
Gettext es una herramienta muy poderosa para internacionalizar su proyecto PHP. Más allá de su flexibilidad que permite soportar una gran cantidad de lenguajes humanos, su soporte para más de 20 lenguajes de programación te permite transferir fácilmente tus conocimientos de uso con PHP a otros lenguajes como Python, Java o C#.
Además, Poedit puede ayudar a suavizar el camino entre el código y las cadenas traducidas, haciendo que el proceso sea más sencillo y fácil de seguir. También puede optimizar los esfuerzos de traducción compartida con su integración de Crowdin.
Siempre que sea posible, considere otros idiomas que puedan hablar sus usuarios. Esto es principalmente importante para proyectos que no están en inglés: puede aumentar el acceso de sus usuarios si lo publica en inglés además de en su idioma nativo.
Por supuesto, no todos los proyectos tienen una necesidad de internacionalización, pero es mucho más fácil comenzar i18n durante la infancia de un proyecto, incluso si no es necesario inicialmente, que hacerlo más adelante en caso de que luego se convierta en un requisito. Y con herramientas como Gettext y Poedit es más fácil que nunca.