1. Code
  2. Coding Fundamentals
  3. Rest API

Localize Your Web Application for Any Country With the Google Translate API

By linking Yii's I18n Message/Extract to the Google Translate API, we can automate translation of our application into 63 languages.
Scroll to top
Final product imageFinal product imageFinal product image
What You'll Be Creating

In my tutorial Localization With I18n for the Building Your Startup With PHP series, I created sample Spanish code by cutting and pasting text strings into Google Translate. I began to wonder if I could integrate the Google Translate API with the Yii Framework's I18n resource extraction script to automate translation for a number of countries. I posted a feature request at the Yii Forum and then decided to see if I could build the feature myself.

In this tutorial, I'll walk you through my extensions to the Yii I18n extract script which do exactly this. And I'll demonstrate translating my startup application, Meeting Planner, into a handful of languages.

Keep in mind, Google Translate isn't perfect and it doesn't address issues related to time and date formats and currencies. But for a quick and affordable (free) way to build default translations for your web application into 50+ languages, this is an ideal solution.

For example, though, here's a more noticeable error I ran into in testing—luckily these are rare:

1
'{nFormatted} TB' => '{nFormatted} tuberculosis',

If you need a more professional approach, a friend pointed me to a paid service for managing localization within apps, Transifex. I haven't checked it out myself but it looks intriguing.

Working With Google Translate

What Languages Does It Support?

Google Translate offers translation services for 64 languages, including Swedish but sadly not Swedish Chef:

Please accept marketing cookies to load this content.

Here's a sampling of Google's supported languages—see the full list here:

Google Translate List of Languages SupportedGoogle Translate List of Languages SupportedGoogle Translate List of Languages Supported

Talking to the Google Translate API

I found two Composer libraries for working with the Google Translator API in PHP:

I found Velijanashvili's first so it's what I used in this tutorial. It leverages Google Translate through its free RESTful web interface so you do not need an API key. However, if you have a large library of resources or plan to translate many languages, you'll likely want to integrate Tillotson's as it is fully integrated with Google Translate's paid service via keys.

For this tutorial, I'm building on the Building Your Startup With PHP series codebase. To install Velijanashvili's Google Translate Library, just type:

1
composer require stichoza/google-translate-php

Here's some sample code to translate from English to Spanish:

1
use Stichoza\Google\GoogleTranslate;
2
echo GoogleTranslate::staticTranslate('hello world', "en", "es"). "\n";         

It should output:

hola mundo

Extending Yii2's I18n Message/Extract Script

How Yii2's I18n Support Works Today

At this time, you may want to review my Localization With I18n tutorial which explains how to extract message strings for your necessary language translations. 

You can use Yii's Gii code generator to generate models and CRUD code which automatically integrates I18n support. Every string in the code is replaced by a function call such as Yii::t('category','text string to translate');.

Yii offers a console command message/extract which finds all these function calls in your application and creates a directory tree of files by language and category for translations of all of these strings.

Here's an example string file for German:

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',

Here's an example of the directory paths:

Yii I18n Directory Structure For Message FilesYii I18n Directory Structure For Message FilesYii I18n Directory Structure For Message Files

Extending Message/Extract for Google Translate

I chose the approach of creating a replacement script called message/google_extract which would call Google Translate whenever it needed to translate a string.

Preventing Broken Code From Translating Tokens

Because I18n integrates parameter tokens in curly braces for variable values, I ran into some problems right away. For example, here are some I18n strings which include tokens and nested tokens:

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}}'

The Google Translate API does not have a parameter for ignoring tokens such as these in this form. But we can't translate these because they correspond to variable names and format strings in code.

It did not appear to me that a regular expression could solve this where translatable strings and tokens were present together. It's likely that readers may have a more efficient solution than I found for solving this problem—if one is clear to you, please post it in the comments.

I chose to scan the strings by character and track the nesting of curly braces. I'll be the first to admit there may be a better way. Here's my function 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
     }    

It converts an I18n string into an array of elements separated into translatable and untranslatable elements. For example, this code:

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));

Generates this output:

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
)

Whenever the extract process identifies a new string to translate, it breaks the string into these parts and calls the Google Translate API for any translatable string, e.g. one that doesn't begin with a left curly brace. Then it concatenates those translations with the tokenized strings back into a single string.

Translating a Tokenized String With Google Translate

Here's the function getGoogleTranslation() for a string and a destination language. The source language is determined by 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
         }

I found that the combination of these approaches worked almost perfectly in my testing.

Customizing Yii's Message/Extract

Yii's I18n implementation supports loading resource strings from .PO files, .PHP files (which I use) and the database. For this tutorial, I've customized Message/Extract for the PHP file generation.

I copied and extended message/extract in /console/controllers/TranslateController.php. Because of PHP 5.6.x's strict rules, I changed the function names for saveMessagesToPHP to saveMessagesToPHPEnhanced and saveMessagesCategoryToPHP to saveMessagesCategoryToPHPEnhanced.

Here's the saveMessagesToPHPEnhanced() function:

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
     }     

It calls the saveMessagesCategoryToPHP function:

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
         }

Unfortunately, the original Message/Extract code is not commented. While there may be some additional improvements that can be made, I simply added a call to the Google Translate API here:

1
foreach ($untranslated as $message) {
2
    $todo[$message] = $this->getGoogleTranslation($message,$language);
3
}

And I added a parameter ($force=true) to force recreation of the message files:

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
}

Translating Message Planner

Testing complete, I was excited to translate Message Planner into more languages. First, we add the new language translations to the /console/config/i18n.php file:

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'],

Again, if you need broader language support or have a larger quantity of strings to translate, you may want to switch to Travis Tillotson's Google Translation Client and paid API access.

Then, I added translation strings /frontend/views/layouts/main.php and /frontend/views/site/index.php in order to demonstrate translating the home page. Since these pages aren't generated by Yii's Gii code generator, the text strings had been left in plain HTML. Here's an example of what they look like now:

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') ?> &raquo;</a></p>

Here's what the home page looks like in English:

Meeting Planner English Home PageMeeting Planner English Home PageMeeting Planner English Home Page

Then, I ran google_extract:

1
./yii translate/google_extract /Users/Jeff/sites/mp/common/config/i18n.php

Note: Be sure when you do this that the application language is set to your default language, e.g. English. This setting is in /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' => [

I found that it was necessary to run google_extract once to create the initial message file template and a second time to initiate the calls to Google Translate.

Then I can change the language setting in /common/config/main.php to see each translation. The results are pretty incredible for something that can be generated so quickly.

Here's the home page in Chinese:

Meeting Planner Chinese Home PageMeeting Planner Chinese Home PageMeeting Planner Chinese Home Page

Here's the home page in Arabic:

Meeting Planner Arabic Home PageMeeting Planner Arabic Home PageMeeting Planner Arabic Home Page

Here's the home page in Japanese:

Meeting Planner Japanese Home PageMeeting Planner Japanese Home PageMeeting Planner Japanese Home Page

Here's the home page in Yiddish:

Meeting Planner Yiddish Home PageMeeting Planner Yiddish Home PageMeeting Planner Yiddish Home Page

Here's the home page in German:

Meeting Planner German Home PageMeeting Planner German Home PageMeeting Planner German Home Page

What's Next?

I hope you enjoyed this tutorial. It was fun to write something that had such a broad impact on the potential reach of my Meeting Planner application. If you'd like to learn more about Meeting Planner, watch for upcoming tutorials in our Building Your Startup With PHP series. There are lots of fun features coming up.

Please feel free to add your questions and comments below; I generally participate in the discussions. You can also reach me on Twitter @reifman or email me directly.

Related Links