Как интернационализировать стороннюю библиотеку PHP

Рассмотрите возможность написания библиотеки PHP, которая будет опубликована через Packagist или Pear. Он адресован разработчикам-сверстникам, использующим его в произвольных настройках.

Эта библиотека будет содержать некоторые сообщения о статусе, определенные для клиента. Как я интернационализирую этот код, чтобы разработчики, использующие библиотеку, имели максимально возможную свободу подключать свой собственный метод локализации? Я не хочу ничего принимать, особенно не заставляя разработчика использовать gettext.

Чтобы работать с примером, допустим этот класс:

class Example {

    protected $message = "I'd like to be translated in your client language.";

    public function callMe() {
        return $this->message;
    }

    public function callMeToo($user) {
        return sprintf('Hi %s, nice to meet you!', $user);
    }

}

Здесь есть две проблемы: как пометить приватный $message для перевода и как разрешить разработчику локализовать строку внутри callMeToo()?

Один (очень неудобный) вариант был бы, чтобы спросить о некотором методе i18n в конструкторе, например:

public function __construct($i18n) {
    $this->i18n = $i18n;
    $this->message = $this->i18n($this->message);
}

public function callMeToo($user) {
    return sprintf($this->i18n('Hi %s, nice to meet you!'), $user);
}

но я очень надеюсь на более элегантное решение.

Изменить 1: Помимо простой подстановки строк, поле i18n является широким. Предпосылка заключается в том, что я не хочу упаковывать какое-либо решение i18n в свою библиотеку или принуждать пользователя выбирать его специально для моего кода.

Затем, как я могу структурировать свой код, чтобы обеспечить лучшую и наиболее гибкую локализацию для разных аспектов: строковый перевод, число и форматирование валюты, даты и время,...? Предположим, что один или другой файл выводится из моей библиотеки. В каком месте или интерфейсе может потребляющий разработчик подключиться к ее решению локализации?

Ответ 1

Наиболее часто используемым решением является файл строк. Например. как показано ниже:

# library
class Foo {
  public function __construct($lang = 'en') {
    $this->strings = require('path/to/langfile.' . $lang . '.php');
    $this->message = $this->strings['callMeToo'];
  }

  public function callMeToo($user) {
    return sprintf($this->strings['callMeToo'], $user);
  }
}

# strings file
return Array(
  'callMeToo' => 'Hi %s, nice to meet you!'
);

Вы можете, чтобы избежать назначения $this->message, также работать с магическими геттерами:

# library again
class Foo {
  # … code from above

  function __get($name) {
    if(!empty($this->strings[$name])) {
      return $this->strings[$name];
    }

    return null;
  }
}

Вы даже можете добавить метод loadStrings, который берет массив строк от пользователя и объединяет его со своей внутренней таблицей.

Изменить 1: Чтобы добиться большей гибкости, я немного изменил бы этот подход. Я бы добавил функцию перевода как атрибут объекта и всегда вызываю это, когда хочу локализовать строку. Функция по умолчанию просто ищет строку в таблице строк и возвращает само значение, если не может найти локализованную строку, как gettext. Разработчик, использующий вашу библиотеку, может затем изменить функцию на свой собственный, чтобы сделать совершенно другой подход к локализации.

Локализация даты не является проблемой. Настройка языка - это вопрос программного обеспечения, в котором используется ваша библиотека. Сам формат представляет собой локализованную строку, например. $this->translate('%Y-%m-%d') вернет локализованную версию строки формата даты.

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

Однако локализация валюты является проблемой. Я думаю, что лучшим подходом было бы добавить функцию перевода валюты (и, возможно, для лучшей гибкости, другую функцию форматирования номера), которую разработчик мог бы перезаписать, если он хочет изменить формат валюты. Кроме того, вы можете реализовать строки форматирования для валют. Например, %CUR %.02f - в этом примере вы замените символ %CUR на символ валюты. Символы валют также являются локализованными строками.

Изменить 2: Если вы не хотите использовать setlocale, вам нужно много работать... в основном вы должны переписать strftime() и sprintf() для достижения локализованных дат и чисел. Конечно, возможно, но много работы.

Ответ 2

Основной подход заключается в предоставлении потребителю некоторого метода для определения отображения. Он может принимать любую форму, если пользователь может определить биективное отображение.

Например, Mantis Bug Tracker использует простой файл globals:

<?php
    require_once "strings_$language.txt";
    echo $s_actiongroup_menu_move;

Их метод является базовым, но работает отлично. Оберните его в класс, если хотите:

<?php
    $translator = new Translator(Translator::ENGLISH); // or make it a singleton
    echo $translator->translate('actiongroup_menu_move');

Вместо этого используйте XML файл или INI файл или файл CSV... в любом формате по своему вкусу.


Отвечая на ваши дальнейшие изменения/комментарии

Да, вышесказанное не сильно отличается от других решений. Но я верю, что еще немного можно сказать:

  • перевод может быть достигнут только путем подстановки строки (отображение может принимать бесконечное число форм)
  • номер и даты форматирования никоим образом не беспокоят вас. Это ответственность уровня представления, и вы должны просто возвращать необработанные числа (или DateTime или временные метки) (если ваша библиотека не предназначена для локализации;)

Ответ 3

Здесь есть главная проблема. Вы не хотите делать код так, как сейчас, в своем вопросе о интернационализации.

Позвольте мне объяснить. Главный переводчик, вероятно, программист. Второй и третий могут быть, но тогда вы хотите перевести его на любой язык, даже для не-программистов. Это должно быть легко для не-программистов. Охота через классы, функции и т.д. Для не-программистов определенно не в порядке.

Поэтому я предлагаю следующее: сохраняйте исходные предложения (английский) в агностическом формате, что легко понять для всех. Это может быть файл xml, база данных или любая другая форма, которую вы видите. Затем используйте свои переводы там, где они вам нужны. Вы можете сделать это как:

class Example {
  // Fetch them as you prefer and store them in $messages.
  protected $messages = array(
    'en' => array(
      "message"  => "I'd like to be translated in your client language.",
      "greeting" => "Hi %s, nice to meet you!"
      )
     );

  public function __construct($lang = 'en') {
    $this->lang = $lang;
    }

  protected function get($key, $args = null) {
    // Store the string
    $message = $this->messages[$this->lang][$key];
    if ($args == null)
      return $this->translator($message);
    else {
      $string = $this->translator($message);
      // Merge the two arrays so they can be passed as values
      $sprintf_args = array_merge(array($string), $args);
      return call_user_func_array('sprintf', $sprintf_args);
      }
    }

  public function callMe() {
    return $this->get("message");
  }

  public function callMeToo($user) {
    return $this->get("greeting", $user);
  }
}

Кроме того, если вы хотите использовать небольшой перевод script, который я сделал, вы можете упростить его. Он использует базу данных, поэтому у нее может быть не так много гибкости, как вы ищите. Вам нужно ввести его, и язык будет установлен в инициализации. Обратите внимание, что текст автоматически добавляется в базу данных, если нет.

class Example {
  protected $translator;

  // Translator already knows the language to translate the text to
  public function __construct($Translator) {
    $this->translator = $Translator;
    }

  public function callMe() {
    return $this->translator("I'd like to be translated in your client language.");
  }

  public function callMeToo($user) {
    return sprintf($this->translator("Hi %s, nice to meet you!"), $user));
  }
}

Его можно легко изменить, чтобы использовать файл xml или любой другой источник для переведенных строк.

Примечания для второго метода:

  • Это отличается от предложенного вами решения, поскольку оно выполняет работу на выходе, а не в инициализации, поэтому нет необходимости отслеживать каждую строку.

  • Вам нужно только написать свои предложения один раз, на английском языке. Класс, который я написал, поместит его в базу данных при условии, что он будет правильно инициализирован, что сделает ваш код чрезвычайно сухим. Именно поэтому я начал его, вместо того, чтобы просто использовать gettext (и нелепый размер gettext для моих простых требований).

  • Con: это старый класс. Тогда я многого не знала. Теперь я бы изменил пару вещей: создание поля language, а не en, es и т.д., Бросая некоторые исключения здесь и там и загружая некоторые из проведенных мной тестов.