Любая локализация сводится к формированию файлов-переводов. Эти файлы могут иметь различную структуру, но задача у них одна - хранение локализованных сообщений в виде словаря (ключ-значение). При попытке локализации той или иной части системы, требуется выполнить поиск локализованного значения по данному ключу, и если таковое будет найдено, использовать его в качестве замены.

Для начала рассмотрим простой пример 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 заметно усложнилась. Это вызвано тем, что ей необходимо подстраиваться под различные хранилища локализации (объектное и обычное). В том случае, если локализовать данные бизнес-модели переводчику не удается, он возвращает хранящиеся в целевом свойстве объекта данные.