Traduce tu aplicación web desde cualquier país con la API del Traductor de Google
Spanish (Español) translation by Andrea Jiménez (you can also view the original English article)



En mi tutorial Localization con I18n para la serie Construyendo tu Startup con PHP, creé un código en español de muestra cortando y pegando cadenas de texto en el Traductor de Google. Comencé a preguntarme si podría integrar la API del Traductor de Google con el script de extracción de recursos I18n de Yii Framework para automatizar la traducción para varios países. Publiqué una solicitud de función en el Foro de Yii y luego decidí ver si podía construir la función yo mismo.
En este tutorial, te guiaré a través de mis extensiones hasta el script de extracción Yii I18n que hace exactamente esto. Y demostraré traducir mi aplicación de inicio, Meeting Planner, a varios idiomas.
Ten en cuenta que el traductor de Google no es perfecto y no atiende los problemas relacionados con los formatos de fecha y hora y las monedas. Pero es una forma rápida y asequible (gratuita) de elaborar traducciones por defecto para tu aplicación web en más de 50 idiomas, por lo tanto es una solución ideal.
Sin embargo, por ejemplo, este es un error muy notorio que encontré en las pruebas; afortunadamente, no son tan comunes:
1 |
'{nFormatted} TB' => '{nFormatted} tuberculosis', |
Si necesitas algo más profesional, un amigo me mostró un servicio de pago para administrar las traducciones dentro de las aplicaciones, Transifex. No lo he usado pero parece interesante.
Trabajando con el traductor de Google
¿Qué idiomas son compatibles?
El traductor de Google ofrece servicios de traducción para 64 idiomas, incluido el sueco, pero lamentablemente no el chef sueco:
Esta es una muestra de los idiomas que admite Google: encuentra la lista completa aquí:



Hablando con la API del traductor de Google
Encontré dos bibliotecas de Composer para trabajar con la API del traductor de Google en PHP:
- Biblioteca del traductor de Google de Levan Velijanashvili
- Cliente de traducción de Google de Travis Tillotson
Encontré a Velijanashvili primero, así que es la que usé para este tutorial. Aprovecha el traductor de Google a través de su interfaz web gratuita RESTful para que no necesites una clave API. Sin embargo, descubrí que si tienes una gran biblioteca de recursos o planeas traducir muchos idiomas, es probable que quieras integrar Tillotson, ya que está completamente integrado con el servicio de pago del traductor de Google mediante claves.
En este tutorial, estoy desarrollando sobre la base del código de la serie Construyendo tu Startup con PHP. Para instalar la biblioteca del traductor de Google de Velijanashvili, simplemente digita:
1 |
composer require stichoza/google-translate-php |
Este es un código de muestra para traducir de inglés a español:
1 |
use Stichoza\Google\GoogleTranslate; |
2 |
echo GoogleTranslate::staticTranslate('hello world', "en", "es"). "\n"; |
Debería dar este resultado:
hola mundo
Ampliación del script de extracción/mensaje I18n de Yii2
Cómo funciona el soporte I18n de Yii2 hoy
En este momento, es posible que quieras revisar mi tutorial sobre la biblioteca Localization con I18n donde explico cómo extraer las cadenas de mensajes necesarias para tus traducciones.
Puedes usar el generador de código Gii de Yii para generar modelos y el código CRUD que integra automáticamente el soporte I18n. Cada cadena en el código se reemplaza por una llamada a la función como esta, Yii::t('category','text string to translate');.
Yii te ofrece un comando de consola mensaje/extracto que encuentra todas estas llamadas de función en tu aplicación y crea un árbol de directorios de archivos por idioma y categoría para la traduccion de todas estas cadenas.
Este es un archivo de cadena de ejemplo para el idioma alemán:
1 |
<?php
|
2 |
/**
|
3 |
* Message translations.
|
4 |
*
|
5 |
* This file is automatically generated by 'yii translate' command.
|
6 |
* It contains the localizable messages extracted from source code.
|
7 |
* You may modify this file by translating the extracted messages.
|
8 |
*
|
9 |
* Each array element represents the translation (value) of a message (key).
|
10 |
* If the value is empty, the message is considered as not translated.
|
11 |
* Messages that no longer need translation will have their translations
|
12 |
* enclosed between a pair of '@@' marks.
|
13 |
*
|
14 |
* Message string can be used with plural forms format. Check i18n section
|
15 |
* of the guide for details.
|
16 |
*
|
17 |
* NOTE: this file must be saved in UTF-8 encoding.
|
18 |
*/
|
19 |
return [ |
20 |
'Get started with Yii' => 'Machen Sie sich mit Yii begonnen', |
21 |
'Heading' => 'Überschrift', |
22 |
'My Yii Application' => 'Meine Yii-Anwendung', |
23 |
'Yii Documentation' => 'Yii Dokumentation', |
24 |
'Yii Extensions' => 'Yü -Erweiterungen', |
25 |
'Yii Forum' => 'Yii Forum', |
26 |
'Are you sure you want to delete this item?' => 'Sind Sie sicher, Sie wollen diesen Inhalt löschen ?', |
27 |
'Congratulations!' => 'Herzlichen Glückwunsch!', |
28 |
'Create' => 'schaffen', |
29 |
'Create {modelClass}' => 'schaffen {modelClass}', |
30 |
'Created At' => 'Erstellt am', |
31 |
'Delete' => 'löschen', |
32 |
'ID' => 'Identifikation', |
Este es un ejemplo de las rutas de acceso del directorio:



Ampliación de mensajes/extractos para el traductor de Google
Elegí el método de crear un script de reemplazo llamado message/google_extract que llamaría al traductor de Google siempre que fuera necesario traducir una cadena.
Evitar que el código roto traduzca tokens
Inmediatamente me encontré con algunos problemas dado que I18n integra tokens de parámetro en llaves para valores de variables. Por ejemplo, estas son algunas cadenas I18n que incluyen tokens y tokens anidados:
1 |
'Create {modelClass}'
|
2 |
'Registered at {0, date, MMMM dd, YYYY HH:mm} from {1}'
|
3 |
'{0, date, MMMM dd, YYYY HH:mm}'
|
4 |
'{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}'
|
La API del traductor de Google no tiene un parámetro para ignorar tokens como estos en este formato. Pero no podemos traducirlos porque corresponden a nombres de variables y cadenas de formato en el código.
No me pareció que una expresión regular pudiera resolver esto cuando las cadenas y los tokens traducibles estuvieran juntos. Es probable que los lectores tengan una solución más eficiente que la que encontré para resolver este problema; si tienes una solución clara, por favor publícala en los comentarios.
Elegí escanear las cadenas por carácter y rastrear la anidación de llaves. Seré el primero en admitir que puede haber una mejor manera. Esta es mi función parse_safe_translate():
1 |
/*
|
2 |
* parses a string into an array
|
3 |
* splitting by any curly bracket segments
|
4 |
* including nested curly brackets
|
5 |
*/
|
6 |
public function parse_safe_translate($s) { |
7 |
$debug = false; |
8 |
$result = array(); |
9 |
$start=0; |
10 |
$nest =0; |
11 |
$ptr_first_curly=0; |
12 |
$total_len = strlen($s); |
13 |
for($i=0; $i<$total_len; $i++) { |
14 |
if ($s[$i]=='{') { |
15 |
// found left curly
|
16 |
if ($nest==0) { |
17 |
// it was the first one, nothing is nested yet
|
18 |
$ptr_first_curly=$i; |
19 |
}
|
20 |
// increment nesting
|
21 |
$nest+=1; |
22 |
} elseif ($s[$i]=='}') { |
23 |
// found right curly
|
24 |
// reduce nesting
|
25 |
$nest-=1; |
26 |
if ($nest==0) { |
27 |
// end of nesting
|
28 |
if ($ptr_first_curly-$start>=0) { |
29 |
// push string leading up to first left curly
|
30 |
$prefix = substr ( $s , $start , $ptr_first_curly-$start); |
31 |
if (strlen($prefix)>0) { |
32 |
array_push($result,$prefix); |
33 |
}
|
34 |
}
|
35 |
// push (possibly nested) curly string
|
36 |
$suffix=substr ( $s , $ptr_first_curly , $i-$ptr_first_curly+1); |
37 |
if (strlen($suffix)>0) { |
38 |
array_push($result,$suffix); |
39 |
}
|
40 |
if ($debug) { |
41 |
echo '|'.substr ( $s , $start , $ptr_first_curly-$start-1)."|\n"; |
42 |
echo '|'.substr ( $s , $ptr_first_curly , $i-$ptr_first_curly+1)."|\n"; |
43 |
}
|
44 |
$start=$i+1; |
45 |
$ptr_first_curly=0; |
46 |
if ($debug) { |
47 |
echo 'next start: '.$start."\n"; |
48 |
}
|
49 |
}
|
50 |
}
|
51 |
}
|
52 |
$suffix = substr ( $s , $start , $total_len-$start); |
53 |
if ($debug) { |
54 |
echo 'Start:'.$start."\n"; |
55 |
echo 'Pfc:'.$ptr_first_curly."\n"; |
56 |
echo $suffix."\n"; |
57 |
}
|
58 |
if (strlen($suffix)>0) { |
59 |
array_push($result,substr ( $s , $start , $total_len-$start)); |
60 |
}
|
61 |
return $result; |
62 |
}
|
Convierte una cadena I18n en una matriz de elementos separados en elementos traducibles e intraducibles. Por ejemplo, este código:
1 |
$message='The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.'; |
2 |
print_r($this->parse_safe_translate($message)); |
Genera esta salida:
1 |
Array
|
2 |
(
|
3 |
[0] => The image " |
4 |
[1] => {file} |
5 |
[2] => " is too large. The height cannot be larger than |
6 |
[3] => {limit, number} |
7 |
[4] => |
8 |
[5] => {limit, plural, one{pixel} other{pixels}} |
9 |
[6] => . |
10 |
)
|
Cada vez que el proceso de extracción identifica una nueva cadena para traducir, divide la cadena en estas partes y llama a la API del traductor de Google para cualquier cadena que se pueda traducir, por ejemplo, una que no comience con una llave izquierda. Luego concatena esas traducciones con las cadenas con token en una sola cadena.
Traducir una cadena tokenizada con el traductor de Google
Esta es la función para una cadena y un idioma de destino getGoogleTranslation() . El idioma de origen está determinado por Yii::$app->language.
1 |
public function getGoogleTranslation($message,$language) { |
2 |
$arr_parts=$this->parse_safe_translate($message); |
3 |
$translation=''; |
4 |
foreach ($arr_parts as $str) { |
5 |
if (!stristr($str,'{')) { |
6 |
if (strlen($translation)>0 and substr($translation,-1)=='}') $translation.=' '; |
7 |
$translation.=GoogleTranslate::staticTranslate($str, Yii::$app->language, $language); |
8 |
} else { |
9 |
// add space prefix unless it's first
|
10 |
if (strlen($translation)>0) |
11 |
$translation.=' '.$str; |
12 |
else
|
13 |
$translation.=$str; |
14 |
}
|
15 |
}
|
16 |
print_r($translation); |
17 |
return $translation; |
18 |
}
|
Descubrí que la combinación de estos métodos funcionaba casi a la perfección en mis pruebas.
Personalización del comando mensaje/extracto de Yii
La implementación I18n de Yii admite la carga de cadenas de recursos desde archivos .PO, archivos .PHP (que uso) y la base de datos. En este tutorial, personalicé el comando Mensaje/Extracto para la generación de archivos PHP.
Copié y amplié el comando message/extract en /console/controllers/TranslateController.php. Debido a las estrictas reglas de PHP 5.6.x, cambié los nombres de las funciones de saveMessagesToPHP a saveMessagesToPHPEnhanced y saveMessagesCategoryToPHP a saveMessagesCategoryToPHPEnhanced.
Esta es la función saveMessagesToPHPEnhanced():
1 |
/**
|
2 |
* Writes messages into PHP files
|
3 |
*
|
4 |
* @param array $messages
|
5 |
* @param string $dirName name of the directory to write to
|
6 |
* @param boolean $overwrite if existing file should be overwritten without backup
|
7 |
* @param boolean $removeUnused if obsolete translations should be removed
|
8 |
* @param boolean $sort if translations should be sorted
|
9 |
*/
|
10 |
protected function saveMessagesToPHPEnhanced($messages, $dirName, $overwrite, $removeUnused, $sort,$language) |
11 |
{
|
12 |
foreach ($messages as $category => $msgs) { |
13 |
$file = str_replace("\\", '/', "$dirName/$category.php"); |
14 |
$path = dirname($file); |
15 |
FileHelper::createDirectory($path); |
16 |
$msgs = array_values(array_unique($msgs)); |
17 |
$coloredFileName = Console::ansiFormat($file, [Console::FG_CYAN]); |
18 |
$this->stdout("Saving messages to $coloredFileName...\n"); |
19 |
$this->saveMessagesCategoryToPHPEnhanced($msgs, $file, $overwrite, $removeUnused, $sort, $category,$language); |
20 |
}
|
21 |
}
|
Llama a la función saveMessagesCategoryToPHP:
1 |
/**
|
2 |
* Writes category messages into PHP file
|
3 |
*
|
4 |
* @param array $messages
|
5 |
* @param string $fileName name of the file to write to
|
6 |
* @param boolean $overwrite if existing file should be overwritten without backup
|
7 |
* @param boolean $removeUnused if obsolete translations should be removed
|
8 |
* @param boolean $sort if translations should be sorted
|
9 |
* @param boolean $language language to translate to
|
10 |
* @param boolean $force google translate
|
11 |
* @param string $category message category
|
12 |
*/
|
13 |
protected function saveMessagesCategoryToPHPEnhanced($messages, $fileName, $overwrite, $removeUnused, $sort, $category,$language,$force=true) |
14 |
{
|
15 |
if (is_file($fileName)) { |
16 |
$existingMessages = require($fileName); |
17 |
sort($messages); |
18 |
ksort($existingMessages); |
19 |
if (!$force) { |
20 |
if (array_keys($existingMessages) == $messages) { |
21 |
$this->stdout("Nothing new in \"$category\" category... Nothing to save.\n\n", Console::FG_GREEN); |
22 |
return; |
23 |
}
|
24 |
}
|
25 |
$merged = []; |
26 |
$untranslated = []; |
27 |
foreach ($messages as $message) { |
28 |
if (array_key_exists($message, $existingMessages) && strlen($existingMessages[$message]) > 0) { |
29 |
$merged[$message] = $existingMessages[$message]; |
30 |
} else { |
31 |
$untranslated[] = $message; |
32 |
}
|
33 |
}
|
34 |
ksort($merged); |
35 |
sort($untranslated); |
36 |
$todo = []; |
37 |
foreach ($untranslated as $message) { |
38 |
$todo[$message] = $this->getGoogleTranslation($message,$language); |
39 |
}
|
40 |
ksort($existingMessages); |
41 |
foreach ($existingMessages as $message => $translation) { |
42 |
if (!isset($merged[$message]) && !isset($todo[$message]) && !$removeUnused) { |
43 |
if (!empty($translation) && strncmp($translation, '@@', 2) === 0 && substr_compare($translation, '@@', -2, 2) === 0) { |
44 |
$todo[$message] = $translation; |
45 |
} else { |
46 |
$todo[$message] = '@@' . $translation . '@@'; |
47 |
}
|
48 |
}
|
49 |
}
|
50 |
|
51 |
$merged = array_merge($todo, $merged); |
52 |
if ($sort) { |
53 |
ksort($merged); |
54 |
}
|
55 |
if (false === $overwrite) { |
56 |
$fileName .= '.merged'; |
57 |
}
|
58 |
$this->stdout("Translation merged.\n"); |
59 |
} else { |
60 |
$merged = []; |
61 |
foreach ($messages as $message) { |
62 |
$merged[$message] = ''; |
63 |
}
|
64 |
ksort($merged); |
65 |
}
|
66 |
|
67 |
|
68 |
$array = VarDumper::export($merged); |
69 |
$content = <<<EOD |
70 |
<?php |
71 |
/**
|
72 |
* Message translations.
|
73 |
*
|
74 |
* This file is automatically generated by 'yii {$this->id}' command.
|
75 |
* It contains the localizable messages extracted from source code.
|
76 |
* You may modify this file by translating the extracted messages.
|
77 |
*
|
78 |
* Each array element represents the translation (value) of a message (key).
|
79 |
* If the value is empty, the message is considered as not translated.
|
80 |
* Messages that no longer need translation will have their translations
|
81 |
* enclosed between a pair of '@@' marks.
|
82 |
*
|
83 |
* Message string can be used with plural forms format. Check i18n section
|
84 |
* of the guide for details.
|
85 |
*
|
86 |
* NOTE: this file must be saved in UTF-8 encoding.
|
87 |
*/
|
88 |
return $array; |
89 |
EOD; |
90 |
|
91 |
file_put_contents($fileName, $content); |
92 |
$this->stdout("Translation saved.\n\n", Console::FG_GREEN); |
93 |
}
|
Desafortunadamente, el código original del comando Mensaje/Extracto no está comentado. Si bien puede haber algunas mejoras adicionales, solamente agregué una llamada a la API del traductor de Google aquí:
1 |
foreach ($untranslated as $message) { |
2 |
$todo[$message] = $this->getGoogleTranslation($message,$language); |
3 |
}
|
Y agregué un parámetro ($force=true) para forzar la recreación de los archivos de mensajes:
1 |
if (!$force) {
|
2 |
if (array_keys($existingMessages) == $messages) {
|
3 |
$this->stdout("Nothing new in \"$category\" category... Nothing to save.\n\n", Console::FG_GREEN);
|
4 |
return; |
5 |
} |
6 |
} |
Traducción del planificador de mensajes
Me entusiasmó traducir Message Planner a más idiomas con la prueba completa. Primero, agregamos las nuevas traducciones de idiomas al archivo /console/config/i18n.php:
1 |
<?php |
2 |
|
3 |
return [ |
4 |
// string, required, root directory of all source files |
5 |
'sourcePath' => __DIR__. DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR, |
6 |
// Root directory containing message translations. |
7 |
'messagePath' => __DIR__ . DIRECTORY_SEPARATOR .'..'. DIRECTORY_SEPARATOR . 'messages', |
8 |
// array, required, list of language codes that the extracted messages |
9 |
// should be translated to. For example, ['zh-CN', 'de']. |
10 |
'languages' => ['ar','es','de','it','iw','ja','yi','zh-CN'], |
Si necesitas un soporte de idiomas más amplio o tienes una mayor cantidad de cadenas por traducir, es posible que te quieras cambiar al Cliente de traducción de Google de Travis Tillotson y al acceso de pago de la API.
Luego, agregué las cadenas de traducción /frontend/views/layouts/main.php y /frontend/views/site/index.php para demostrar la traducción de la página de inicio. Dado que estas páginas no son generadas por el generador de código Gii de Yii, las cadenas de texto se habían dejado en HTML simple. Este es un ejemplo de cómo se ven ahora:
1 |
<div class="row"> |
2 |
<div class="col-lg-4"> |
3 |
<h2><?= Yii::t('frontend','Getting Started') ?></h2> |
4 |
<p><?= Yii::t('frontend','Follow along with our tutorial series at Tuts+ as we build Meeting Planner step by step. In this episode we talk about startups in general and the goals for our application.') ?></p> |
5 |
<p><a class="btn btn-default" href="https://code.tutsplus.com/building-your-startup-with-php-getting-started--cms-21948t"><?= Yii::t('frontend','Episode 1') ?> »</a></p> |
Así es como se ve la página de inicio en inglés:



Luego, ejecuté google_extract:
1 |
./yii translate/google_extract /Users/Jeff/sites/mp/common/config/i18n.php |
Nota: Asegúrate de que al hacerlo, el idioma de la aplicación esté configurado en el idioma predeterminado, por ejemplo, inglés. Esta configuración está en /common/config/main.php:
1 |
<?php |
2 |
return [ |
3 |
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', |
4 |
// available languages |
5 |
// 'ar','de','es','it','iw','ja','yi','zh-CN' |
6 |
'language' => 'en', // english |
7 |
'components' => [ |
Descubrí que era necesario ejecutar la función google_extract una vez para crear la plantilla de archivo de mensaje inicial y una segunda vez para iniciar las llamadas al traductor de Google.
Luego puedo cambiar la configuración del idioma en la función /common/config/main.php para ver cada traducción. Los resultados son bastante increíbles para algo que se puede generar tan rápidamente.
Esta es la página de inicio en chino:



Así se ve la página de inicio en árabe:



Esta es la página de inicio en japonés:



Así se ve la página de inicio en yiddish:



Esta es la página de inicio en alemán:



¿Qué sigue?
Fue divertido escribir algo que tuvo un impacto tan amplio en el alcance potencial de mi aplicación Meeting Planner. Espero que hayas disfrutado este tutorial. Si quieres obtener más información sobre Meeting Planner, consulta los próximos tutoriales en nuestra serie Construyendo tu Startup con PHP. Vienen un montón de características divertidas.
No dudes en dejar tus preguntas y comentarios a continuación; generalmente participo en las discusiones. También puedes contactarme en Twitter como @reifman o enviarme un correo electrónico directamente.



