问题 如何国际化PHP第三方库


考虑编写一个PHP库,它将通过Packagist或Pear发布。它适用于在任意设置中使用它的同行开发人员。

该库将包含为客户端确定的一些状态消息。如何使此代码国际化,以便使用该库的开发人员可以尽可能自由地插入自己的本地化方法?我不想假设任何东西,特别是不强迫dev使用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解决方案,或强迫用户选择一个专门用于我的代码。

然后,我如何构建我的代码,以便为不同方面提供最佳和最灵活的本地化:字符串翻译,数字和货币格式,日期和时间,......?假设一个或另一个显示为我的库的输出。消费者可以在哪个位置或界面插入本地化解决方案?


4832
2017-12-10 19:32


起源



答案:


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

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


7
2017-12-13 08:17



谢谢,有趣。基本上,建议 loadStrings 似乎很符合我的需求。你知道在野外的任何项目,他们如何处理它? (但Zend是免费的,他们有自己的i18n lib。) - Boldewyn
@Boldewyn我知道有几个闭源项目以这种方式处理它,但我必须认真思考一个开源项目。大多数都没有本地化,如果是,他们使用gettext ... - ckruse
是的,不幸的是,这正是我的问题...... - Boldewyn
@Boldewyn所以,你为什么犹豫不决?有 gettext() 这很糟糕的Web应用程序和轻松部署。还有这种自行开发的简单i18n / l10n解决方案。你为什么不简单地使用其中一个? - ckruse
(我问的不是因为要点,我真的不明白你的问题是什么) - ckruse


答案:


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

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


7
2017-12-13 08:17



谢谢,有趣。基本上,建议 loadStrings 似乎很符合我的需求。你知道在野外的任何项目,他们如何处理它? (但Zend是免费的,他们有自己的i18n lib。) - Boldewyn
@Boldewyn我知道有几个闭源项目以这种方式处理它,但我必须认真思考一个开源项目。大多数都没有本地化,如果是,他们使用gettext ... - ckruse
是的,不幸的是,这正是我的问题...... - Boldewyn
@Boldewyn所以,你为什么犹豫不决?有 gettext() 这很糟糕的Web应用程序和轻松部署。还有这种自行开发的简单i18n / l10n解决方案。你为什么不简单地使用其中一个? - ckruse
(我问的不是因为要点,我真的不明白你的问题是什么) - ckruse


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

让我解释。主译者可能是程序员。第二个和第三个可能是,但是你想将它翻译成任何语言,甚至是 非程序员。这对非程序员来说应该很容易。通过课程,功能等为非程序员进行狩猎绝对不行。

所以我建议这样做:保留你的源句(英语) 不可知 格式,每个人都很容易理解。这可能是一个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等等,在这里和那里抛出一些例外并上传我做过的一些测试。


2
2017-12-14 20:05



感谢你的回答。我也查看了Github上的代码。不幸的是,这个解决方案对我不起作用。我想尽可能地限制自己的要求,同时仍允许消费用户完全本地化我的代码发出的内容。我还更新了问题,包括日期格式等内容。我担心这比输出翻译或初始化更复杂。 - Boldewyn
有了您的更新,我认为实际上它太宽泛了。要么代码需要数百行,要么更多是一个更好的概念问题 程序员。但是,如果你愿意,可以随意扩展(并推回到github)我指出的类可以处理更好的本地化。 - Francisco Presencia
另一个有趣的问题是,你如何处理货币?由于这随时间变化,你需要使用一个外部API,所以你做所有的requeriments变得越来越不方便,它不会是 简单。 - Francisco Presencia
我真的开始质疑,如果拒绝添加第三方库是一种可行的方法。在Python中,我只是投入 pybabel 和 pytz 作为要求,所有数字和货币格式的困境都消失了。 (对于在客户端的代码库中添加依赖项的奖励。)然而,对我来说很明显,它不是 简单 要做到这一点,我当然不想重新发明我的lib中的任何轮子,因此问题:-)它应该简单地了解PHP库中i18n的当前实践。 - Boldewyn


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

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


回答您以后的编辑/评论

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

  • 翻译只能通过字符串替换来实现(映射可能采用无数种形式)
  • 格式编号和日期无关紧要。这是表示层的责任,你应该只返回原始数字(或 约会时间s或时间戳),(除非你的图书馆的目的是本地化;)

2
2017-12-13 18:00



感谢您的回答,但我对我的图书馆用户的后果特别感兴趣。这些解决方案可能会产生哪些陷阱?除此之外,我没有看到你的概念与我在问题中提供的或@ckruse已经提到的不同。我还将更新问题以扩展到更多的i18n问题。 - Boldewyn