12

考虑编写一个 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 解决方案,也不想强迫用户专门选择一个来满足我的代码需求。

那么,我如何构建我的代码以允许针对不同方面进行最佳和最灵活的本地化:字符串翻译、数字和货币格式、日期和时间……?假设其中一个或另一个显示为我的库的输出。消费开发者可以在哪个位置或界面插入她的本地化解决方案?

4

3 回答 3

8

The most often used solution is a strings file. E.g. like following:

# 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!'
);

You can, to avoid the $this->message assignment, also work with magic getters:

# library again
class Foo {
  # … code from above

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

    return null;
  }
}

You can even add a loadStrings method which takes an array of strings from the user and merge it with your internal strings table.

Edit 1: To achieve more flexibility I would change the above approach a little bit. I would add a translation function as object attribute and always call this when I want to localize a string. The default function just looks up the string in the strings table and returns the value itself if it can't find a localized string, just like gettext. The developer using your library could then change the function to his own provided to do a completely different approach of localization.

Date localization is not a problem. Setting the locale is a matter of the software your library is used in. The format itself is a localized string, e.g. $this->translate('%Y-%m-%d') would return a localized version of the date format string.

Number localization is done by setting the right locale and using functions like sprintf().

Currency localization is a problem, though. I think the best approach would be to add a currency translation function (and, maybe for better flexibility, another number formatting function, too) which a developer could overwrite if he wants to change the currency format. Alternatively you could implement format strings for currencies, too. For example %CUR %.02f – in this example you would replace %CUR with the currency symbol. Currency symbols itself are localized strings, too.

Edit 2: If you don't want to use setlocale you have to do a lot of work… basically you have to rewrite strftime() and sprintf() to achieve localized dates and numbers. Of course possible, but a lot of work.

于 2013-12-13T08:17:39.527 回答
2

这里有一个主要问题。您不想在国际化问题中制作现在的代码。

让我解释。主要翻译人员可能是程序员。第二个和第三个可能是,但是你想把它翻译成任何语言,即使是非程序员。这对非程序员来说应该很容易。为非程序员寻找类、函数等是绝对不行的。

所以我建议:将你的源语句(英文)保持在不可知的格式中,这样每个人都很容易理解。这可能是 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 的荒谬大小以满足我的简单要求)。

  • 缺点:这是一个老班。那时我知道的不多。现在我要更改几件事:创建一个language字段,而不是en,es等,在这里和那里抛出一些异常并上传我所做的一些测试。

于 2013-12-14T20:05:24.003 回答
2

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

例如,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 文件……事实上,无论您喜欢什么格式。


回答您以后的编辑/评论

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

  • 翻译只能通过字符串替换来实现(映射可能有无数种形式)
  • 格式化数字和日期与您无关。这是表示层的责任,您应该只返回原始数字(或DateTime或时间戳),(除非您的库的目的是本地化;)
于 2013-12-13T18:00:49.773 回答