Implementación de la internacionalización(cadenas de idioma) en una aplicación PHP
internationalization globalization (10)
Quiero crear un CMS que pueda manejar la obtención de cadenas de configuración regional para admitir la internacionalización. Planeo almacenar las cadenas en una base de datos y luego colocar un caché clave / valor como memcache entre la base de datos y la aplicación para evitar caídas de rendimiento al golpear la base de datos en cada página para una traducción.
Esto es más complejo que usar archivos PHP con matrices de cadenas, pero ese método es increíblemente ineficiente cuando tienes 2,000 líneas de traducción.
Pensé en usar gettext , pero no estoy seguro de que los usuarios del CMS se sientan cómodos trabajando con los archivos de gettext. Si las cadenas se almacenan en una base de datos, entonces se puede configurar un buen sistema de administración que les permita realizar cambios cuando lo deseen y el almacenamiento en caché en la RAM asegurará que la obtención de esas cadenas sea tan rápida o más rápida que la de gettext. Tampoco me siento seguro usando la extensión PHP, ya que ni siquiera el framework zend la usa .
¿Hay algo malo con este enfoque?
Actualizar
Pensé que tal vez agregaría más alimento para el pensamiento. Uno de los problemas con las traducciones de cadenas es que no admiten fechas, dinero o declaraciones condicionales. Sin embargo, gracias a intl PHP ahora tiene MessageFormatter que es lo que realmente debe usarse de todos modos.
// Load string from gettext file
$string = _("{0} resulted in {1,choice,0#no errors|1#single error|1<{1, number} errors}");
// Format using the current locale
msgfmt_format_message(setlocale(LC_ALL, 0), $string, array(''Update'', 3));
En otra nota, una de las cosas que no me gustan de gettext es que el texto está incrustado en la aplicación en todo el lugar. Eso significa que el equipo responsable de la traducción primaria (generalmente en inglés) debe tener acceso al código fuente del proyecto para realizar cambios en todos los lugares donde se colocan las declaraciones predeterminadas. Es casi tan malo como las aplicaciones que tienen todo el código de spaghetti SQL.
Por lo tanto, tiene sentido usar teclas como _(''error.404_not_found'')
que luego permiten a los escritores y traductores de contenido simplemente preocuparse por los archivos PO / MO sin alterar el código.
Sin embargo, en el caso de que no exista una traducción de obtención de texto para la clave dada, no hay forma de volver a la configuración predeterminada (como podría hacerlo con un controlador personalizado). Esto significa que, o bien, el escritor está desordenando el código, ¡o muestra "error.404_not_found" a los usuarios que no tienen una traducción local!
Además, no tengo conocimiento de ningún proyecto grande que use el gettext de PHP. Apreciaría cualquier enlace a sistemas bien utilizados (y, por lo tanto, probados) que realmente dependen de la extensión nativa de Gettext para PHP.
En otra nota, una de las cosas que no me gustan de gettext es que el texto está incrustado en la aplicación en todo el lugar. Eso significa que el equipo responsable de la traducción primaria (generalmente en inglés) debe tener acceso al código fuente del proyecto para realizar cambios en todos los lugares donde se colocan las declaraciones predeterminadas. Es casi tan malo como las aplicaciones que tienen todo el código de spaghetti SQL.
Esto no es realmente cierto. Puede tener un archivo de encabezado (lo siento, ex programador de C), como:
<?php
define(MSG_404_NOT_FOUND, ''error.404_not_found'')
?>
Luego, cuando quieras un mensaje, usa _(MSG_404_NOT_FOUND)
. Esto es mucho más flexible que requerir que los desarrolladores recuerden la sintaxis exacta del mensaje no localizado cada vez que quieran escupir una versión localizada.
Podría ir un paso más allá y generar el archivo de encabezado en un paso de compilación, tal vez desde CSV o base de datos, y hacer una referencia cruzada con la traducción para detectar las cadenas faltantes.
¿Qué pasa con los archivos csv (que se pueden editar fácilmente en muchas aplicaciones) y el almacenamiento en caché en memcache (wincache, etc.)? Este enfoque funciona bien en Magento. Todas las frases de idiomas en el código están envueltas en la función __()
, por ejemplo
<?php echo $this->__(''Some text'') ?>
Luego, por ejemplo, antes del lanzamiento de la nueva versión, ejecuta un script simple que analiza los archivos de origen, encuentra todo el texto envuelto en __()
y lo coloca en el archivo .csv. Cargas archivos csv y los guardas en caché en memcache. En la función __()
miras en tu memcache donde las traducciones se almacenan en caché.
En un proyecto reciente, consideramos usar gettext, pero resultó ser más fácil simplemente escribir nuestra propia funcionalidad. Realmente es bastante simple: cree un archivo JSON por configuración regional (por ejemplo, strings.en.json, strings.es.json, etc.), y cree una función en algún lugar llamada "translate ()" o algo así, y luego simplemente llame a eso. Esa función determinará la configuración regional actual (desde el URI o una sesión var o algo), y devolverá la cadena localizada.
Lo único que debe recordar es asegurarse de que cualquier HTML que genere esté codificado en UTF-8 y marcado como tal en el marcado (por ejemplo, en el doctype, etc.)
Estoy usando las cosas de la UCI en mi marco y realmente me resulta sencillo y útil de usar. Mi sistema está basado en XML con consultas XPath y no una base de datos, como usted sugiere usar. No he encontrado este enfoque ineficiente. También jugué un poco con los paquetes de recursos cuando investigué técnicas, pero me parecieron bastante complicadas de implementar.
La funcionalidad Locale es un envío de dios. Puedes hacer mucho más fácilmente:
// Available translations
$languages = array(''en'', ''fr'', ''de'');
// The language the user wants
$preference = (isset($_COOKIE[''lang''])) ?
$_COOKIE[''lang''] : ((isset($_SERVER[''HTTP_ACCEPT_LANGUAGE''])) ?
Locale::acceptFromHttp($_SERVER[''HTTP_ACCEPT_LANGUAGE'']) : '''');
// Match preferred language to those available, defaulting to generic English
$locale = Locale::lookup($languages, $preference, false, ''en'');
// Construct path to dictionary file
$file = $dir . ''/'' . $locale . ''.xsl'';
// Check that dictionary file is readable
if (!file_exists($file) || !is_readable($file)) {
throw new RuntimeException(''Dictionary could not be loaded'');
}
// Load and return dictionary file
$dictionary = simplexml_load_file($file);
Luego realizo búsquedas de palabras usando un método como este:
$selector = ''/i18n/text[@label="'' . $word . ''"]'';
$result = $dictionary->xpath($selector);
$text = array_shift($result);
if ($formatted && isset($text)) {
return new MessageFormatter($locale, $text);
}
La ventaja para mi sistema es que el sistema de plantillas está basado en XSL, lo que significa que puedo usar los mismos archivos XML de traducción directamente en mis plantillas para mensajes simples que no necesitan ningún formato i18n.
Gettext usa un protocolo binario que es bastante rápido. Además, la implementación de gettext suele ser más sencilla, ya que solo requiere echo _(''Text to translate'');
. También tiene herramientas existentes para el uso de los traductores y está comprobado que funcionan bien.
Puede almacenarlos en una base de datos, pero creo que sería más lento y un poco excesivo, especialmente porque tendría que crear el sistema para editar las traducciones usted mismo.
Si solo pudieras realmente almacenar en caché las búsquedas en una parte de memoria dedicada en APC, estarías encantado. Lamentablemente, no sé cómo.
Hay una serie de otras preguntas y respuestas de SO similares a esta. Le sugiero que las busque y las lea también.
¿Consejo? Use una solución existente como gettext o xliff, ya que le ahorrará mucha pena cuando llegue a todos los casos de borde de traducción, como el texto de derecha a izquierda, formatos de fecha, diferentes volúmenes de texto, el francés es 30% más detallado que el inglés, por ejemplo, ese tornillo hasta formatear, etc. Incluso mejores consejos No lo hagas. Si los usuarios quieren traducir, harán un clon y lo traducirán. Debido a que la localización tiene más que ver con la apariencia y el uso del lenguaje coloquial, esto suele ser lo que sucede. Una vez más, dando un ejemplo a la cultura anglosajona, le gustan los colores de la web y las caras tipo san-serif. La cultura hispana como los colores brillantes y los tipos Serif / Cursivo. Lo que para atenderlo necesitaría diferentes diseños por idioma.
En realidad, Zend satisface los siguientes adaptadores para Zend_Translate y es una lista útil.
- Array: - Usa arreglos PHP para páginas pequeñas; uso más simple; solo para programadores
- Csv: - Use archivos separados por comas ( .csv / .txt) para el formato de archivo de texto simple; rápido; Posibles problemas con los personajes de Unicode.
- Gettext: - Use archivos binarios de gettext (* .mo) para el estándar GNU para linux; a salvo de amenazas; necesita herramientas para la traducción
- Ini: - Use archivos simples INI (* .ini) para el formato de archivo de texto simple; rápido; Posibles problemas con los personajes de Unicode.
- Tbx: - Use archivos de intercambio de base de datos ( .tbx / .xml) para el estándar de la industria para cadenas de terminología entre aplicaciones; Formato XML
- Tmx: - Use archivos tmx ( .tmx / .xml) para el estándar de la industria para la traducción entre aplicaciones; Formato XML; legible por humanos
- Qt: - Use archivos qt linguist (* .ts) para el marco de la aplicación multiplataforma; Formato XML; legible por humanos
- Xliff: - Use archivos xliff ( .xliff / .xml) para un formato más simple como TMX pero relacionado con él; Formato XML; legible por humanos
- XmlTm: - Use archivos xmltm (* .xml) para el estándar de la industria para la memoria de traducción de documentos XML; Formato XML; legible por humanos
- Otros: - * .sql para otros adaptadores diferentes pueden implementarse en el futuro
Para aquellos que están interesados, parece que el soporte total para las locales y i18n en PHP finalmente comienza a tener lugar.
// Set the current locale to the one the user agent wants
$locale = Locale::acceptFromHttp(getenv(''HTTP_ACCEPT_LANGUAGE''));
// Default Locale
Locale::setDefault($locale);
setlocale(LC_ALL, $locale . ''.UTF-8'');
// Default timezone of server
date_default_timezone_set(''UTC'');
// iconv encoding
iconv_set_encoding("internal_encoding", "UTF-8");
// multibyte encoding
mb_internal_encoding(''UTF-8'');
Hay varias cosas que deben ser asignadas y detectar la zona horaria / locale y luego usarla para analizar y visualizar correctamente la entrada y la salida es importante. Hay una biblioteca PHP I18N que se acaba de lanzar y que contiene tablas de búsqueda para gran parte de esta información.
El procesamiento de la entrada del usuario es importante para asegurarse de que la aplicación tenga cadenas UTF-8 limpias y bien formadas de cualquier entrada que ingrese el usuario. iconv es genial para esto.
/**
* Convert a string from one encoding to another encoding
* and remove invalid bytes sequences.
*
* @param string $string to convert
* @param string $to encoding you want the string in
* @param string $from encoding that string is in
* @return string
*/
function encode($string, $to = ''UTF-8'', $from = ''UTF-8'')
{
// ASCII is already valid UTF-8
if($to == ''UTF-8'' AND is_ascii($string))
{
return $string;
}
// Convert the string
return @iconv($from, $to . ''//TRANSLIT//IGNORE'', $string);
}
/**
* Tests whether a string contains only 7bit ASCII characters.
*
* @param string $string to check
* @return bool
*/
function is_ascii($string)
{
return ! preg_match(''/[^/x00-/x7F]/S'', $string);
}
Luego simplemente ejecuta la entrada a través de estas funciones.
$utf8_string = normalizer_normalize(encode($_POST[''text'']), Normalizer::FORM_C);
Traducciones
Como dijo Andre, parece que gettext es la opción inteligente por defecto para escribir aplicaciones que se pueden traducir.
- Gettext usa un protocolo binario que es bastante rápido.
- La implementación de gettext suele ser más sencilla, ya que solo requiere
_(''Text to translate'')
- Las herramientas existentes para el uso de los traductores están comprobadas y funcionan bien.
Cuando alcanza el tamaño de Facebook, puede trabajar en la implementación de caché RAM, métodos alternativos como el que mencioné en la pregunta. Sin embargo, nada es mejor que "simple, rápido y funciona" para la mayoría de los proyectos.
Sin embargo, también hay cosas adicionales que gettext no puede manejar. Cosas como mostrar fechas, dinero y números. Para aquellos que necesitan la exión INTL .
/**
* Return an IntlDateFormatter object using the current system locale
*
* @param string $locale string
* @param integer $datetype IntlDateFormatter constant
* @param integer $timetype IntlDateFormatter constant
* @param string $timezone Time zone ID, default is system default
* @return IntlDateFormatter
*/
function __date($locale = NULL, $datetype = IntlDateFormatter::MEDIUM, $timetype = IntlDateFormatter::SHORT, $timezone = NULL)
{
return new IntlDateFormatter($locale ?: setlocale(LC_ALL, 0), $datetype, $timetype, $timezone);
}
$now = new DateTime();
print __date()->format($now);
$time = __date()->parse($string);
Además, puede usar strftime para analizar fechas teniendo en cuenta la configuración regional actual.
A veces necesita que los valores de los números y las fechas se inserten correctamente en los mensajes de configuración regional.
/**
* Format the given string using the current system locale
* Basically, it''s sprintf on i18n steroids.
*
* @param string $string to parse
* @param array $params to insert
* @return string
*/
function __($string, array $params = NULL)
{
return msgfmt_format_message(setlocale(LC_ALL, 0), $string, $params);
}
// Multiple choices (can also just use ngettext)
print __(_("{1,choice,0#no errors|1#single error|1<{1, number} errors}"), array(4));
// Show time in the correct way
print __(_("It is now {0,time,medium}), time());
Consulte los detalles del formato de la UCI para obtener más información.
Base de datos
Asegúrese de que su conexión a la base de datos esté utilizando el conjunto de caracteres correcto para que nada se dañe en el almacenamiento.
Funciones de cadena
mb_string comprender la diferencia entre las mb_string string , mb_string y mb_string .
// ''LATIN SMALL LETTER A WITH RING ABOVE'' (U+00E5) normalization form "D"
$char_a_ring_nfd = "a/xCC/x8A";
var_dump(grapheme_strlen($char_a_ring_nfd));
var_dump(mb_strlen($char_a_ring_nfd));
var_dump(strlen($char_a_ring_nfd));
// ''LATIN CAPITAL LETTER A WITH RING ABOVE'' (U+00C5)
$char_A_ring = "/xC3/x85";
var_dump(grapheme_strlen($char_A_ring));
var_dump(mb_strlen($char_A_ring));
var_dump(strlen($char_A_ring));
Nombre de dominio TLD''s
Las funciones de IDN de la biblioteca INTL son una gran ayuda para procesar nombres de dominio no ascii.
Quédate con gettext, no encontrarás una alternativa más rápida en PHP.
Con respecto al cómo , puede utilizar una base de datos para almacenar su catálogo y permitir que otros usuarios traduzcan las cadenas utilizando una interfaz gráfica de usuario amigable. Cuando se revisan / aprueban los nuevos cambios, presione un botón, compile un nuevo archivo .mo
y despliegue.
Algunos recursos para encaminarlo:
Quizás no sea realmente una respuesta a tu pregunta, pero ¿quizás puedas obtener algunas ideas del componente de traducción de Symfony? Me parece muy bien, aunque debo confesar que todavía no lo he usado.
La documentación para el componente se puede encontrar en
http://symfony.com/doc/current/book/translation.html
y el código para el componente se puede encontrar en
https://github.com/symfony/Translation .
Debería ser fácil usar el componente de Traducción, ya que los componentes de Symfony están diseñados para ser utilizados como componentes independientes.
tener un plugin zend que funciona muy bien para esto.
<?php
/** dependencies **/
require ''Zend/Loader/Autoloader.php'';
require ''Zag/Filter/CharConvert.php'';
Zend_Loader_Autoloader::getInstance()->setFallbackAutoloader(true);
//filter
$filter = new Zag_Filter_CharConvert(array(
''replaceWhiteSpace'' => ''-'',
''locale'' => ''en_US'',
''charset''=> ''UTF-8''
));
echo $filter->filter(''ééé ááá 90'');//eee-aaa-90
echo $filter->filter(''óóó 10aáééé'');//ooo-10aaeee
Si no desea utilizar el marco de zend, solo puede usar el complemento.
¡abrazo!