В моем уроке « Локализация с I18n для создания своего стартапа с помощью PHP» я создал образец испанского кода, вырезав и вставив текстовые строки в Google Translate . Я начал задаваться вопросом, могу ли я интегрировать API Google Translate со сценарием извлечения ресурсов Iii Framework I18n для автоматизации перевода для ряда стран. Я разместил запрос на функцию на форуме Yii, а затем решил посмотреть, смогу ли я создать эту функцию самостоятельно.
В этом уроке я расскажу вам о моих расширениях сценария извлечения Yii I18n, которые делают именно это. И я покажу, как перевести мое стартап-приложение Meeting Planner на несколько языков.
Помните, что Google Translate не совершенен и не решает проблемы, связанные с форматами времени и даты и валют. Но для быстрого и доступного (бесплатного) способа создания переводов по умолчанию для вашего веб-приложения на более чем 50 языков это идеальное решение.
Например, вот более заметная ошибка, с которой я столкнулся при тестировании — к счастью, это редкость:
1
|
‘{nFormatted} TB’ => ‘{nFormatted} tuberculosis’,
|
Если вам нужен более профессиональный подход, друг указал мне на платный сервис для управления локализацией в приложениях, Transifex . Я не проверял это сам, но это выглядит интригующим.
Работа с Google Translate
Какие языки он поддерживает?
Google Translate предлагает услуги перевода на 64 языка, включая шведский, но, к сожалению, не шведский шеф-повар :
Вот выборка поддерживаемых языков Google — см. Полный список здесь :
Разговор с Google Translate API
Я нашел две библиотеки Composer для работы с Google Translator API в PHP:
- Библиотека Google Translate Левана Велиджанашвили
- Клиент перевода Google Трэвиса Тиллотсона
Первым я нашел Велиджанашвили, и именно это я использовал в этом уроке. Он использует Google Translate через бесплатный веб-интерфейс RESTful, поэтому вам не нужен ключ API. Однако, если у вас есть большая библиотека ресурсов или вы планируете переводить много языков, вам, вероятно, захочется интегрировать Tillotson, поскольку она полностью интегрирована с платным сервисом Google Translate с помощью ключей.
Для этого урока я использую кодовую базу « Построение стартапа с помощью PHP» . Чтобы установить библиотеку Google Translate Велиджанашвили, просто введите:
composer require stichoza/google-translate-php
Вот пример кода для перевода с английского на испанский:
1
2
|
use Stichoza\Google\GoogleTranslate;
echo GoogleTranslate::staticTranslate(‘hello world’, «en», «es»).
|
Это должно вывести:
hola mundo
Расширение сценария сообщения / извлечения Yii2 I18n
Как Yii2 поддерживает I18n сегодня
В настоящее время вы можете просмотреть мое руководство по локализации с I18n, в котором объясняется, как извлечь строки сообщений для необходимых языковых переводов.
Вы можете использовать Yii генератор кода Gii для генерации моделей и кода CRUD, который автоматически интегрирует поддержку I18n. Каждая строка в коде заменяется вызовом функции, таким как Yii::t('category','text string to translate');
,
Yii предлагает консольную команду message / extract, которая находит все эти вызовы функций в вашем приложении и создает дерево каталогов файлов по языкам и категориям для переводов всех этих строк.
Вот пример строкового файла для немецкого языка:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
<?php
/**
* Message translations.
*
* This file is automatically generated by ‘yii translate’ command.
* It contains the localizable messages extracted from source code.
* You may modify this file by translating the extracted messages.
*
* Each array element represents the translation (value) of a message (key).
* If the value is empty, the message is considered as not translated.
* Messages that no longer need translation will have their translations
* enclosed between a pair of ‘@@’ marks.
*
* Message string can be used with plural forms format.
* of the guide for details.
*
* NOTE: this file must be saved in UTF-8 encoding.
*/
return [
‘Get started with Yii’ => ‘Machen Sie sich mit Yii begonnen’,
‘Heading’ => ‘Überschrift’,
‘My Yii Application’ => ‘Meine Yii-Anwendung’,
‘Yii Documentation’ => ‘Yii Dokumentation’,
‘Yii Extensions’ => ‘Yü -Erweiterungen’,
‘Yii Forum’ => ‘Yii Forum’,
‘Are you sure you want to delete this item?’
‘Congratulations!’
‘Create’ => ‘schaffen’,
‘Create {modelClass}’ => ‘schaffen {modelClass}’,
‘Created At’ => ‘Erstellt am’,
‘Delete’ => ‘löschen’,
‘ID’ => ‘Identifikation’,
|
Вот пример путей к каталогам:
Расширение сообщения / выдержка для Google Translate
Я выбрал подход к созданию сценария замены message/google_extract
который будет вызывать Google Translate всякий раз, когда потребуется перевести строку.
Предотвращение взлома кода от перевода токенов
Поскольку I18n интегрирует токены параметров в фигурные скобки для значений переменных, я сразу столкнулся с некоторыми проблемами. Например, вот некоторые строки I18n, которые включают токены и вложенные токены:
1
2
3
4
|
‘Create {modelClass}’
‘Registered at {0, date, MMMM dd, YYYY HH:mm} from {1}’
‘{0, date, MMMM dd, YYYY HH:mm}’
‘{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}’
|
В Google Translate API нет параметра для игнорирования токенов, подобных этим в этой форме. Но мы не можем перевести их, потому что они соответствуют именам переменных и форматным строкам в коде.
Мне не показалось, что регулярное выражение может решить эту проблему, когда переводимые строки и токены присутствуют вместе. Вероятно, у читателей может быть более эффективное решение, чем я нашел, для решения этой проблемы — если оно вам ясно, пожалуйста, опубликуйте его в комментариях.
Я решил сканировать строки по символам и отслеживать вложение фигурных скобок. Я буду первым, кто признает, что может быть лучше. Вот моя функция parse_safe_translate()
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
/*
* parses a string into an array
* splitting by any curly bracket segments
* including nested curly brackets
*/
public function parse_safe_translate($s) {
$debug = false;
$result = array();
$start=0;
$nest =0;
$ptr_first_curly=0;
$total_len = strlen($s);
for($i=0; $i<$total_len; $i++) {
if ($s[$i]=='{‘) {
// found left curly
if ($nest==0) {
// it was the first one, nothing is nested yet
$ptr_first_curly=$i;
}
// increment nesting
$nest+=1;
} elseif ($s[$i]==’}’) {
// found right curly
// reduce nesting
$nest-=1;
if ($nest==0) {
// end of nesting
if ($ptr_first_curly-$start>=0) {
// push string leading up to first left curly
$prefix = substr ( $s , $start , $ptr_first_curly-$start);
if (strlen($prefix)>0) {
array_push($result,$prefix);
}
}
// push (possibly nested) curly string
$suffix=substr ( $s , $ptr_first_curly , $i-$ptr_first_curly+1);
if (strlen($suffix)>0) {
array_push($result,$suffix);
}
if ($debug) {
echo ‘|’.substr ( $s , $start , $ptr_first_curly-$start-1).»|\n»;
echo ‘|’.substr ( $s , $ptr_first_curly , $i-$ptr_first_curly+1).»|\n»;
}
$start=$i+1;
$ptr_first_curly=0;
if ($debug) {
echo ‘next start: ‘.$start.»\n»;
}
}
}
}
$suffix = substr ( $s , $start , $total_len-$start);
if ($debug) {
echo ‘Start:’.$start.»\n»;
echo ‘Pfc:’.$ptr_first_curly.»\n»;
echo $suffix.»\n»;
}
if (strlen($suffix)>0) {
array_push($result,substr ( $s , $start , $total_len-$start));
}
return $result;
}
|
Он преобразует строку I18n в массив элементов, разделенных на переводимые и непереводимые элементы. Например, этот код:
1
2
|
$message=’The image «{file}» is too large.
print_r($this->parse_safe_translate($message));
|
Создает этот вывод:
01
02
03
04
05
06
07
08
09
10
|
Array
(
[0] => The image «
[1] => {file}
[2] => » is too large. The height cannot be larger than
[3] => {limit, number}
[4] =>
[5] => {limit, plural, one{pixel} other{pixels}}
[6] => .
)
|
Всякий раз, когда процесс извлечения идентифицирует новую строку для перевода, он разбивает строку на эти части и вызывает Google Translate API для любой переводимой строки, например той, которая не начинается с левой фигурной скобки. Затем он объединяет эти переводы с токенизированными строками обратно в одну строку.
Перевод токенизированной строки с помощью Google Translate
Вот функция getGoogleTranslation()
для строки и языка назначения. Исходный язык определяется языком Yii::$app->language
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public function getGoogleTranslation($message,$language) {
$arr_parts=$this->parse_safe_translate($message);
$translation=»;
foreach ($arr_parts as $str) {
if (!stristr($str,'{‘)) {
if (strlen($translation)>0 and substr($translation,-1)==’}’) $translation.=’ ‘;
$translation.=GoogleTranslate::staticTranslate($str, Yii::$app->language, $language);
} else {
// add space prefix unless it’s first
if (strlen($translation)>0)
$translation.=’ ‘.$str;
else
$translation.=$str;
}
}
print_r($translation);
return $translation;
}
|
Я обнаружил, что комбинация этих подходов почти идеально работала в моем тестировании.
Настройка сообщения / извлечения Yii
Реализация Yii I18n поддерживает загрузку строк ресурсов из файлов .PO, файлов .PHP (которые я использую) и базы данных. Для этого урока я настроил Message / Extract для генерации PHP-файлов.
Я скопировал и расширил message/extract
в /console/controllers/TranslateController.php
. Из-за строгих правил PHP 5.6.x я изменил имена функций для saveMessagesToPHP
на saveMessagesToPHPEnhanced
и saveMessagesCategoryToPHP
на saveMessagesCategoryToPHPEnhanced
.
Вот saveMessagesToPHPEnhanced()
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
/**
* Writes messages into PHP files
*
* @param array $messages
* @param string $dirName name of the directory to write to
* @param boolean $overwrite if existing file should be overwritten without backup
* @param boolean $removeUnused if obsolete translations should be removed
* @param boolean $sort if translations should be sorted
*/
protected function saveMessagesToPHPEnhanced($messages, $dirName, $overwrite, $removeUnused, $sort,$language)
{
foreach ($messages as $category => $msgs) {
$file = str_replace(«\\», ‘/’, «$dirName/$category.php»);
$path = dirname($file);
FileHelper::createDirectory($path);
$msgs = array_values(array_unique($msgs));
$coloredFileName = Console::ansiFormat($file, [Console::FG_CYAN]);
$this->stdout(«Saving messages to $coloredFileName…\n»);
$this->saveMessagesCategoryToPHPEnhanced($msgs, $file, $overwrite, $removeUnused, $sort, $category,$language);
}
}
|
saveMessagesCategoryToPHP
функцию saveMessagesCategoryToPHP
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
/**
* Writes category messages into PHP file
*
* @param array $messages
* @param string $fileName name of the file to write to
* @param boolean $overwrite if existing file should be overwritten without backup
* @param boolean $removeUnused if obsolete translations should be removed
* @param boolean $sort if translations should be sorted
* @param boolean $language language to translate to
* @param boolean $force google translate
* @param string $category message category
*/
protected function saveMessagesCategoryToPHPEnhanced($messages, $fileName, $overwrite, $removeUnused, $sort, $category,$language,$force=true)
{
if (is_file($fileName)) {
$existingMessages = require($fileName);
sort($messages);
ksort($existingMessages);
if (!$force) {
if (array_keys($existingMessages) == $messages) {
$this->stdout(«Nothing new in \»$category\» category… Nothing to save.\n\n», Console::FG_GREEN);
return;
}
}
$merged = [];
$untranslated = [];
foreach ($messages as $message) {
if (array_key_exists($message, $existingMessages) && strlen($existingMessages[$message]) > 0) {
$merged[$message] = $existingMessages[$message];
} else {
$untranslated[] = $message;
}
}
ksort($merged);
sort($untranslated);
$todo = [];
foreach ($untranslated as $message) {
$todo[$message] = $this->getGoogleTranslation($message,$language);
}
ksort($existingMessages);
foreach ($existingMessages as $message => $translation) {
if (!isset($merged[$message]) && !isset($todo[$message]) && !$removeUnused) {
if (!empty($translation) && strncmp($translation, ‘@@’, 2) === 0 && substr_compare($translation, ‘@@’, -2, 2) === 0) {
$todo[$message] = $translation;
} else {
$todo[$message] = ‘@@’ .
}
}
}
$merged = array_merge($todo, $merged);
if ($sort) {
ksort($merged);
}
if (false === $overwrite) {
$fileName .= ‘.merged’;
}
$this->stdout(«Translation merged.\n»);
} else {
$merged = [];
foreach ($messages as $message) {
$merged[$message] = »;
}
ksort($merged);
}
$array = VarDumper::export($merged);
$content = <<<EOD
<?php
/**
* Message translations.
*
* This file is automatically generated by ‘yii {$this->id}’ command.
* It contains the localizable messages extracted from source code.
* You may modify this file by translating the extracted messages.
*
* Each array element represents the translation (value) of a message (key).
* If the value is empty, the message is considered as not translated.
* Messages that no longer need translation will have their translations
* enclosed between a pair of ‘@@’ marks.
*
* Message string can be used with plural forms format.
* of the guide for details.
*
* NOTE: this file must be saved in UTF-8 encoding.
*/
return $array;
EOD;
file_put_contents($fileName, $content);
$this->stdout(«Translation saved.\n\n», Console::FG_GREEN);
}
|
К сожалению, оригинальный код сообщения / выдержки не комментируется. Хотя могут быть сделаны некоторые дополнительные улучшения, я просто добавил вызов Google Translate API здесь:
1
2
3
|
foreach ($untranslated as $message) {
$todo[$message] = $this->getGoogleTranslation($message,$language);
}
|
И я добавил параметр ($force=true)
чтобы принудительно воссоздать файлы сообщений:
1
2
3
4
5
6
|
if (!$force) {
if (array_keys($existingMessages) == $messages) {
$this->stdout(«Nothing new in \»$category\» category… Nothing to save.\n\n», Console::FG_GREEN);
return;
}
}
|
Переводчик Планировщика сообщений
Завершив тестирование, я был рад перевести Планировщик сообщений на другие языки. Сначала мы добавим новые языковые переводы в файл /console/config/i18n.php
:
01
02
03
04
05
06
07
08
09
10
|
<?php
return [
// string, required, root directory of all source files
‘sourcePath’ => __DIR__.
// Root directory containing message translations.
‘messagePath’ => __DIR__ .
// array, required, list of language codes that the extracted messages
// should be translated to.
‘languages’ => [‘ar’,’es’,’de’,’it’,’iw’,’ja’,’yi’,’zh-CN’],
|
Опять же, если вам нужна более широкая языковая поддержка или для перевода требуется большее количество строк, вы можете переключиться на клиент перевода Google Travis Tillotson и платный доступ к API.
Затем я добавил строки перевода /frontend/views/layouts/main.php
и /frontend/views/site/index.php
, чтобы продемонстрировать перевод домашней страницы. Поскольку эти страницы не генерируются генератором кода Gii Yii, текстовые строки были оставлены в простом HTML. Вот пример того, как они выглядят сейчас:
1
2
3
4
5
|
<div class=»row»>
<div class=»col-lg-4″>
<h2><?= Yii::t(‘frontend’,’Getting Started’) ?></h2>
<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>
<p><a class=»btn btn-default» href=»http://code.tutsplus.com/tutorials/building-your-startup-with-php-getting-started—cms-21948″><?= Yii::t(‘frontend’,’Episode 1′) ?> »</a></p>
|
Вот как выглядит домашняя страница на английском языке:
Затем я запустил google_extract
:
1
|
./yii translate/google_extract /Users/Jeff/sites/mp/common/config/i18n.php
|
Примечание. Убедитесь, что при этом язык приложения установлен на язык по умолчанию, например английский. Этот параметр находится в /common/config/main.php
:
1
2
3
4
5
6
7
|
<?php
return [
‘vendorPath’ => dirname(dirname(__DIR__)) .
// available languages
// ‘ar’,’de’,’es’,’it’,’iw’,’ja’,’yi’,’zh-CN’
‘language’ => ‘en’, // english
‘components’ => [
|
Я обнаружил, что необходимо было запустить google_extract
один раз, чтобы создать исходный шаблон файла сообщений, и второй раз, чтобы инициировать вызовы в Google Translate.
Затем я могу изменить настройку языка в /common/config/main.php
чтобы увидеть каждый перевод. Результаты довольно невероятны для чего-то, что может быть получено так быстро.
Вот домашняя страница на китайском языке:
Вот домашняя страница на арабском языке:
Вот домашняя страница на японском языке:
Вот домашняя страница на идише:
Вот домашняя страница на немецком языке:
Что дальше?
Я надеюсь, вам понравился этот урок. Было весело написать что-то, что оказало такое большое влияние на потенциальную область применения моего приложения Meeting Planner. Если вы хотите узнать больше о Meeting Planner, следите за новыми учебниками в нашей серии «Построение стартапа с помощью PHP» . Есть много интересных функций.
Пожалуйста, не стесняйтесь добавлять свои вопросы и комментарии ниже; Я обычно участвую в обсуждениях. Вы также можете связаться со мной в Twitter @reifman или написать мне напрямую.