Translation is an important part of making your website accessible to an international customer base. One way this is accomplished it to use the PHP gettext functions, which are based on the GNU gettext operating system tools installed on the local server. gettext is well documented and well supported, but uses a legacy approach and has distinct disadvantages. Accordingly, in this recipe, we present an alternative approach to translation where you can build your own adapter.
Something important to recognize is that the programmatic translation tools available to PHP are primarily designed to provide limited translation of a word or phrase, referred to as the msgid (message ID). The translated equivalent is referred to as the msgstr (message string). Accordingly, incorporating translation typically only involves relatively unchanging items such as menus, forms, error or success messages, and so on. For the purposes of this recipe, we will assume that you have the actual web page translations stored as blocks of text.
If you need to translate entire pages of content, you might consider using the Google Translate API. This is, however, a paid service. Alternatively, you could outsource the translation to individuals with multi-lingual skills cheaply using Amazon Mechanical Turk. See the See Also section at the end of this recipe for the URLs.
.ini files, .csv files, and databases.namespace Application\I18n\Translate\Adapter;
interface TranslateAdapterInterface
{
public function translate($msgid);
}namespace Application\I18n\Translate\Adapter;
trait TranslateAdapterTrait
{
protected $translation;
public function translate($msgid)
{
return $this->translation[$msgid] ?? $msgid;
}
}.ini file as the source of translations. The first thing you'll notice is that we use the trait defined previously. The constructor method will vary between adapters. In this case, we use parse_ini_file() to produce an array of key/value pairs where the key is the message ID. Notice that we use the $filePattern parameter to substitute the locale, which then allows us to load the appropriate translation file:namespace Application\I18n\Translate\Adapter;
use Exception;
use Application\I18n\Locale;
class Ini implements TranslateAdapterInterface
{
use TranslateAdapterTrait;
const ERROR_NOT_FOUND = 'Translation file not found';
public function __construct(Locale $locale, $filePattern)
{
$translateFileName = sprintf($filePattern, $locale->getLocaleCode());
if (!file_exists($translateFileName)) {
error_log(self::ERROR_NOT_FOUND . ':' . $translateFileName);
throw new Exception(self::ERROR_NOT_FOUND);
} else {
$this->translation = parse_ini_file($translateFileName);
}
}
}Application\I18n\Translate\Adapter\Csv, is identical, except that we open the translation file and loop through using fgetcsv() to retrieve the message ID / message string key pairs. Here we show only the difference in the constructor:public function __construct(Locale $locale, $filePattern)
{
$translateFileName = sprintf($filePattern, $locale->getLocaleCode());
if (!file_exists($translateFileName)) {
error_log(self::ERROR_NOT_FOUND . ':' . $translateFileName);
throw new Exception(self::ERROR_NOT_FOUND);
} else {
$fileObj = new SplFileObject($translateFileName, 'r');
while ($row = $fileObj->fgetcsv()) {
$this->translation[$row[0]] = $row[1];
}
}
}PDO prepared statement which is sent to the database in the beginning, and only one time. We then execute as many times as needed, supplying the message ID as an argument. You will also notice that we needed to override the translate() method defined in the trait. Finally, you might have noticed the use of PDOStatement::fetchColumn() as we only need the one value:namespace Application\I18n\Translate\Adapter;
use Exception;
use Application\Database\Connection;
use Application\I18n\Locale;
class Database implements TranslateAdapterInterface
{
use TranslateAdapterTrait;
protected $connection;
protected $statement;
protected $defaultLocaleCode;
public function __construct(Locale $locale,
Connection $connection,
$tableName)
{
$this->defaultLocaleCode = $locale->getLocaleCode();
$this->connection = $connection;
$sql = 'SELECT msgstr FROM ' . $tableName
. ' WHERE localeCode = ? AND msgid = ?';
$this->statement = $this->connection->pdo->prepare($sql);
}
public function translate($msgid, $localeCode = NULL)
{
if (!$localeCode) $localeCode = $this->defaultLocaleCode;
$this->statement->execute([$localeCode, $msgid]);
return $this->statement->fetchColumn();
}
}Translation class, which is tied to one (or more) adapters. We assign a class constant to represent the default locale, and properties for the locale, adapter, and text file pattern (explained later):namespace Application\I18n\Translate;
use Application\I18n\Locale;
use Application\I18n\Translate\Adapter\TranslateAdapterInterface;
class Translation
{
const DEFAULT_LOCALE_CODE = 'en_GB';
protected $defaultLocaleCode;
protected $adapter = array();
protected $textFilePattern = array();public function __construct(TranslateAdapterInterface $adapter,
$defaultLocaleCode = NULL,
$textFilePattern = NULL)
{
if (!$defaultLocaleCode) {
$this->defaultLocaleCode = self::DEFAULT_LOCALE_CODE;
} else {
$this->defaultLocaleCode = $defaultLocaleCode;
}
$this->adapter[$this->defaultLocaleCode] = $adapter;
$this->textFilePattern[$this->defaultLocaleCode] = $textFilePattern;
}public function setAdapter($localeCode, TranslateAdapterInterface $adapter)
{
$this->adapter[$localeCode] = $adapter;
}
public function setDefaultLocaleCode($localeCode)
{
$this->defaultLocaleCode = $localeCode;
}
public function setTextFilePattern($localeCode, $pattern)
{
$this->textFilePattern[$localeCode] = $pattern;
}__invoke(), which lets us make a direct call to the translator instance, returning the message string given the message ID:public function __invoke($msgid, $locale = NULL)
{
if ($locale === NULL) $locale = $this->defaultLocaleCode;
return $this->adapter[$locale]->translate($msgid);
}public function text($key, $localeCode = NULL)
{
if ($localeCode === NULL) $localeCode = $this->defaultLocaleCode;
$contents = $key;
if (isset($this->textFilePattern[$localeCode])) {
$fn = sprintf($this->textFilePattern[$localeCode], $localeCode, $key);
if (file_exists($fn)) {
$contents = file_get_contents($fn);
}
}
return $contents;
}First you will need to define a directory structure to house the translation files. For the purposes of this illustration, you can make a directory ,/path/to/project/files/data/languages. Under this directory structure, create sub-directories that represent different locales. For this illustration, you could use these: de_DE, fr_FR, en_GB, and es_ES, representing German, French, English, and Spanish.
Next you will need to create the different translation files. As an example, here is a representative data/languages/es_ES/translation.ini file in Spanish:
Welcome=Bienvenido About Us=Sobre Nosotros Contact Us=Contáctenos Find Us=Encontrarnos click=clic para más información
Likewise, to demonstrate the CSV adapter, create the same thing as a CSV file, data/languages/es_ES/translation.csv:
"Welcome","Bienvenido" "About Us","Sobre Nosotros" "Contact Us","Contáctenos" "Find Us","Encontrarnos" "click","clic para más información"
Finally, create a database table, translation, and populate it with the same data. The main difference is that the database table will have three fields: msgid, msgstr, and locale_code:
CREATE TABLE `translation` ( `msgid` varchar(255) NOT NULL, `msgstr` varchar(255) NOT NULL, `locale_code` char(6) NOT NULL DEFAULT '', PRIMARY KEY (`msgid`,`locale_code`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Next, define the classes mentioned previously, using the code shown in this recipe:
Application\I18n\Translate\Adapter\TranslateAdapterInterfaceApplication\I18n\Translate\Adapter\TranslateAdapterTraitApplication\I18n\Translate\Adapter\IniApplication\I18n\Translate\Adapter\CsvApplication\I18n\Translate\Adapter\DatabaseApplication\I18n\Translate\TranslationNow you can create a test file, chap_08_translation_database.php, to test the database translation adapter. It should implement autoloading, use the appropriate classes, and create a Locale and Connection instance. Note that the TEXT_FILE_PATTERN constant is a sprintf() pattern in which the locale code and filename are substituted:
<?php
define('DB_CONFIG_FILE', '/../config/db.config.php');
define('TEXT_FILE_PATTERN', __DIR__ . '/../data/languages/%s/%s.txt');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\I18n\Locale;
use Application\I18n\Translate\ { Translation, Adapter\Database };
use Application\Database\Connection;
$conn = new Connection(include __DIR__ . DB_CONFIG_FILE);
$locale = new Locale('fr_FR');Next, create a translation adapter instance and use that to create a Translation instance:
$adapter = new Database($locale, $conn, 'translation'); $translate = new Translation($adapter, $locale->getLocaleCode(), TEXT_FILE_PATTERN); ?>
Finally, create display logic that uses the $translate instance:
<!DOCTYPE html>
<head>
<title>PHP 7 Cookbook</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<link rel="stylesheet" type="text/css" href="php7cookbook_html_table.css">
</head>
<body>
<table>
<tr>
<th><h1 style="color:white;"><?= $translate('Welcome') ?></h1></th>
<td>
<div style="float:left;width:50%;vertical-align:middle;">
<h3 style="font-size:24pt;"><i>Some Company, Inc.</i></h3>
</div>
<div style="float:right;width:50%;">
<img src="jcartier-city.png" width="300px"/>
</div>
</td>
</tr>
<tr>
<th>
<ul>
<li><?= $translate('About Us') ?></li>
<li><?= $translate('Contact Us') ?></li>
<li><?= $translate('Find Us') ?></li>
</ul>
</th>
<td>
<p>
<?= $translate->text('main_page'); ?>
</p>
<p>
<a href="#"><?= $translate('click') ?></a>
</p>
</td>
</tr>
</table>
</body>
</html>You can then perform additional similar tests, substituting a new locale to get a different language, or using another adapter to test a different data source. Here is an example of output using a locale of fr_FR and the database translation adapter:

gettext, see http://www.gnu.org/software/gettext/manual/gettext.html.