Любая локализация сводится к формированию файлов-переводов. Эти файлы могут иметь различную структуру, но задача у них одна - хранение локализованных сообщений в виде словаря (ключ-значение). При попытке локализации той или иной части системы, требуется выполнить поиск локализованного значения по данному ключу, и если таковое будет найдено, использовать его в качестве замены.
Для начала рассмотрим простой пример GUI с использованием локализации:
<div class="article">
<div class="article__header">
<h1 class="article__title"><?= $article->title ?></h1>
<div class="article__date">
Date create: <?= $article->dateCreate ?>
</div>
</div>
<div class="article__content">
<?= $article->content ?>
</div>
<div class="article__action">
<a href="comments.article.php">Read comments</a>
</div>
</div>
В этом листинге представлена HTML разметка блока статьи (Article), использующая PHP переменные $title
и $content
для вставки конкретных значений заголовка и содержания. Далее будем называть такого рода данные Данными бизнес-модели. Они характеризуются тем, что отличаются для каждого объекта (для каждой статьи).
Блок article__date
использует текст Date create:
(дата создания) для описания своего содержимого. Назовем эти данные Метаданными бизнес-модели, так как они хоть и относятся к объекту (статье), но не разнятся от экземпляра к экземпляру. Другими словами можно сказать, что это данные класса (свойство класса dateCreate
), а не объекта бизнес-модели.
Блок так же дополнен ссылкой на комментарии читателей статьи Read comments
. Текст ссылки не относится к конкретному объекту или классу, а является Статикой интерфейса.
И так мы выделили три основные группы данных, нуждающихся в локализации, теперь рассмотрим подход к переводу и представлению каждой из них.
Локализация статики интерфейса
Статичная часть интерфейса поддается локализации проще всего. Достаточно сформировать файл перевода примерно следующего содержания:
<?php
// locale/ru.php
return [
'Read comments' => 'Читать комментарии',
];
Далее следует обернуть статику интерфейса в функцию-переводчик:
<div class="article">
...
<div class="article__action">
<a href="comments.article.php"><?= tl("Read comments") ?></a>
</div>
</div>
Остается только реализовать саму функцию tl
:
<?php
function tl($message){
$currentLocale = setlocale(LC_MESSAGES, null);
$translateFilePath = 'locale/' . $currentLocale . '.php';
if(!is_file($translateFilePath)){
return $message;
}
$translate = include($translateFilePath);
if(!isset($translate[$message])){
return $message;
}
return $translate[$message];
}
Логика функции довольно проста, она загружает словарь из файла перевода и ищет в нем перевод для данного сообщения. Если файл перевода не найден, или отсутствует перевод сообщения, функция возвращает исходный текст.
Локализация метаданных бизнес-модели
Для локализации метаданных классов, таких как их имена и имена их свойств, применяется контекстный перевод. Реализация его очень схожа с локализацией статики интерфейса с той лишь разницей, что поиск перевода осуществляется в заданном контексте. Для нашего примера контекстом будет являться класс Article
.
Рассмотрим пример:
<div class="article">
<div class="article__header">
...
<div class="article__date">
<?= tl("Date create:", Article::class) ?> ...
</div>
</div>
...
</div>
Здесь Date create:
является локализованным на английский язык именем свойства Article::$dateCreate
. Скорее всего вы уже заметили, что наша функция tl
приняла второй необязательный параметр, содержащий имя класса. Этот параметр и задает контекст поиска локализации, в данном случае класс Article
.
<?php
function tl($message, $context = null){
$currentLocale = setlocale(LC_MESSAGES, null);
$translateFilePath = 'locale/' . $currentLocale . '.php';
if(!is_file($translateFilePath)){
return $message;
}
$translate = include($translateFilePath);
if($context){
$context .= '::';
}
$messageWithContext = $context . $message
if(!isset($translate[$messageWithContext])){
return $message;
}
return $translate[$messageWithContext];
}
Такая реализация позволяет не изменять логику перевода статики интерфейса и в то же время дополнить переводчик контекстным поиском. Достигается это за счет того, что аргумент $context
функции является не обязательным, и в случае передачи ему пустой строки или null
, поиск перевода сообщения будет производиться в глобальном контексте, где размещаются переводы статики интерфейса.
Остается добавить перевод в файл:
<?php
// locale/ru.php
return [
'Read comments' => 'Читать комментарии',
'Article::Date create:' => 'Дата создания:',
];
Локализация данных бизнес-модели
С локализацией данных дела обстоят сложнее. Дело в том, что объектов бизнес-модели множество, и все значения их свойств следует переводить и хранить в отдельных файлах. Одним из решений этой задачи является использование дополнительного, файлового (не обязательно) хранилища переводов. Оно имеет следующую структуру:
locale_store/
ru/ - файлы перевода данных на русский язык
Article/ - файлы перевода данных для всех объектов класса Article
1.php - файл перевода данных для объекта с id = 1
2.php
...
fr/ - файлы перевода данных на французский язык
Article/
1.php
2.php
...
Как видно в хранилище для каждого объекта создается отдельный файл перевода, имя которого совпадает с идентификатором этого объекта. Это упрощает поиск файлов, что ускоряет работу самого переводчика.
Для систем с небольшим количеством объектов, таких как блоги и новостные ленты, созданием файлов локализации может заниматься сам автор, но более крупные системы, с сотнями или даже тысячами объектов, должны включать механизм автоматического создания этих файлов. Делается это за счет предоставления контент-менеджеру (через GUI) возможности переключения полей ввода контента на различные языки. При добавлении перевода, система должна автоматически создавать файл локализации объекта и сохранять в него этот перевод.
После реализации такой логики остается немного изменить нашу HTML разметку:
<div class="article">
<div class="article__header">
<h1 class="article__title"><?= tl("title", $article) ?></h1>
<div class="article__date">
... <?= tl("dateCreate", $article) ?>
</div>
</div>
<div class="article__content">
<?= tl("content", $article) ?>
</div>
...
</div>
Обратите внимание на то, что в качестве контекста функции tl
передается не имя класса, а объект статьи. В этом случае переводчик должен использовать данный объект в качестве контекста и производить поиск перевода в соответствующем файле локализации объекта:
<?php
function tl($message, $context = null){
$currentLocale = setlocale(LC_MESSAGES, null);
// Если в качестве контекста выступает объект, используется объектное хранилище переводов
if(is_object($context)){
$object = $context;
$context = null;
$translateFilePath = 'locale_store/' . $currentLocale . '/' . get_class($object) . '/' . $object->id . '.php'
}
// В противном случае используется обычное хранилище переводов
else{
$translateFilePath = 'locale/' . $currentLocale . '.php';
}
if(!is_file($translateFilePath)){
return $message;
}
$translate = include($translateFilePath);
if($context){
$context .= '::';
}
$messageWithContext = $context . $message
if(!isset($translate[$messageWithContext])){
// Если перевод отсутствует, но контекстом является объект, использются данные из его свойства
if(isset($object)){
return $object->$message;
}
else{
return $message;
}
}
return $translate[$messageWithContext];
}
Функция tl
заметно усложнилась. Это вызвано тем, что ей необходимо подстраиваться под различные хранилища локализации (объектное и обычное). В том случае, если локализовать данные бизнес-модели переводчику не удается, он возвращает хранящиеся в целевом свойстве объекта данные.