Статьи

Локализуйте свое веб-приложение для любой страны с помощью Google Translate API

Конечный продукт
Что вы будете создавать

В моем уроке « Локализация с 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 предлагает услуги перевода на 64 языка, включая шведский, но, к сожалению, не шведский шеф-повар :

Вот выборка поддерживаемых языков Google — см. Полный список здесь :

Google Translate Список поддерживаемых языков

Я нашел две библиотеки Composer для работы с Google Translator API в PHP:

Первым я нашел Велиджанашвили, и именно это я использовал в этом уроке. Он использует Google Translate через бесплатный веб-интерфейс RESTful, поэтому вам не нужен ключ API. Однако, если у вас есть большая библиотека ресурсов или вы планируете переводить много языков, вам, вероятно, захочется интегрировать Tillotson, поскольку она полностью интегрирована с платным сервисом Google Translate с помощью ключей.

Для этого урока я использую кодовую базу « Построение стартапа с помощью PHP» . Чтобы установить библиотеку Google Translate Велиджанашвили, просто введите:

Вот пример кода для перевода с английского на испанский:

1
2
use Stichoza\Google\GoogleTranslate;
echo GoogleTranslate::staticTranslate(‘hello world’, «en», «es»).

Это должно вывести:

hola mundo

В настоящее время вы можете просмотреть мое руководство по локализации с 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’,

Вот пример путей к каталогам:

Yii I18n Структура каталогов для файлов сообщений

Я выбрал подход к созданию сценария замены 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 для любой переводимой строки, например той, которая не начинается с левой фигурной скобки. Затем он объединяет эти переводы с токенизированными строками обратно в одну строку.

Вот функция 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 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′) ?> &raquo;</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 или написать мне напрямую.