如何国际化PHP第三方库


How to internationalize a PHP third-party library

考虑编写一个PHP库,该库将通过Packagist或Pear发布。它面向在任意设置中使用它的对等开发人员。

此库将包含为客户端确定的一些状态消息。我该如何将这些代码国际化,以便使用该库的开发人员能够尽可能自由地插入他们自己的本地化方法?我不想做任何假设,尤其是不要强迫开发人员使用gettext。

为了处理一个例子,让我们来看看这个类:

class Example {
    protected $message = "I'd like to be translated in your client's 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解决方案与我的库打包,也不想强迫用户选择一个专门适合我的代码的解决方案。

那么,我如何构建我的代码,以便在不同方面实现最佳和最灵活的本地化:字符串翻译、数字和货币格式、日期和时间。。。?假设其中一个或另一个作为库的输出出现。在哪个位置或接口上,消费开发人员可以插入她的本地化解决方案?

最常用的解决方案是字符串文件。例如:

# 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的分配,您还可以使用神奇的getters:

# 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(),以实现本地化的日期和数字。当然可能,但要做很多工作。

这里有一个主要问题。你不想让代码像现在这样国际化。

让我解释一下。主要翻译人员可能是一名程序员。第二种和第三种可能是,但您希望将其翻译成任何语言,即使对于非程序员也是如此。这对非程序员来说应该很容易。对于非程序员来说,遍历类、函数等是绝对不好的。

所以我建议:保持你的源句(英语)的不可知格式,这样每个人都很容易理解。这可能是一个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's 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);
  }
}

此外,如果你想使用我做的一个小翻译脚本,你可以进一步简化它。它使用数据库,所以它可能没有你想要的那么多灵活性。您需要注入它,并且在初始化时设置语言。请注意,如果文本不存在,它会自动添加到数据库中。

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's language.");
  }
  public function callMeToo($user) {
    return sprintf($this->translator("Hi %s, nice to meet you!"), $user));
  }
}

它可以很容易地修改为使用xml文件或任何其他源来翻译字符串。

第二种方法的注意事项

  • 这与您提出的解决方案不同,因为它是在输出中进行工作,而不是在初始化中,因此不需要跟踪每个字符串。

  • 你只需要用英语写一次句子。我编写的类将把它放在数据库中,前提是它被正确初始化,使您的代码非常干燥。这正是我开始使用它的原因,而不是仅仅使用gettext(以及针对我的简单需求的gettext的荒谬大小)。

  • Con:这是一门老课。那时我知道的不多。现在我要更改几件事:制作一个language字段,而不是enes等,在这里和那里抛出一些异常,并上传我所做的一些测试。

基本方法是为使用者提供一些定义映射的方法。它可以采用任何形式,只要用户可以定义双射映射即可。

例如,Mantis Bug Tracker使用一个简单的全局文件:

<?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文件。。。事实上,不管你喜欢什么形式。


回答您以后的编辑/评论

是的,上述解决方案与其他解决方案没有太大区别。但我相信没有什么好说的:

  • 翻译只能通过字符串替换来实现(映射可以采用无限多种形式)
  • 格式化数字和日期不关你的事。这是表示层的责任,您应该只返回原始数字(或DateTimes或时间戳),(除非您的库的目的是本地化;)