在PHP应用程序中实现国际化(语言字符串)

时间:2011-10-19 15:43:21

标签: php internationalization globalization icu

我想构建一个可以处理获取区域设置字符串以支持国际化的CMS。我计划将字符串存储在数据库中,然后在数据库和应用程序之间放置一个键/值缓存(如memcache),以防止性能下降,从而使每个页面的数据库都能进行翻译。

这比使用带有字符串数组的PHP文件更复杂 - 但是当你有2,000个翻译行时,这种方法效率非常低。

我考虑过使用gettext,但我不确定CMS的用户是否愿意使用gettext文件。如果字符串存储在数据库中,那么可以设置一个不错的管理系统,允许它们随时进行更改,RAM中的缓存将确保获取这些字符串的速度比gettext快或快。考虑到not even the zend framework uses it,使用PHP扩展也感觉不安全。

这种方法有什么问题吗?

更新

我想也许我会增加更多的思考。字符串翻译的一个问题是它们不支持日期,金钱或条件语句。但是,感谢intl PHP现在有MessageFormatter,这无论如何都需要使用。

// Load string from gettext file
$string = _("{0} resulted in {1,choice,0#no errors|1#single error|1<{1, number} errors}");

// Format using the current locale
msgfmt_format_message(setlocale(LC_ALL, 0), $string, array('Update', 3));

另一方面,我不喜欢gettext的一个原因是文本被整个地方嵌入到应用程序中。这意味着负责主要翻译的团队(通常是英语)必须能够访问项目源代码,以便在默认语句的所有位置进行更改。它几乎与遍布SQL意大利面条代码的应用程序一样糟糕。

因此,使用像_('error.404_not_found')这样的键是有意义的,这样就可以让内容编写者和翻译者只是担心PO / MO文件,而不会弄乱代码。

但是,如果给定键的gettext转换不存在,则无法回退到默认值(就像使用自定义处理程序一样)。这意味着您要么在您的代码中使用写入器 - 或者向没有语言环境翻译的用户显示“error.404_not_found”!

另外,我不知道有任何大型项目使用PHP的gettext。我很感激任何链接到使用良好的(并因此经过测试)的系统,这些系统实际上依赖于原生的PHP gettext扩展。

10 个答案:

答案 0 :(得分:6)

Gettext使用非常快速的二进制协议。此外,gettext实现通常更简单,因为它只需要echo _('Text to translate');。它还有现有的翻译人员使用工具,并证明它们运作良好。

您可以将它们存储在数据库中,但我觉得它会更慢而且有点矫枉过正,尤其是因为您必须自己构建系统来编辑翻译。

如果只有你可以在APC的专用内存部分中实际缓存查找,那么你就是金色的。可悲的是,我不知道如何。

答案 1 :(得分:5)

对于那些感兴趣的人来说,它似乎完全支持locales,并且PHP中的i18n终于开始了。

// Set the current locale to the one the user agent wants
$locale = Locale::acceptFromHttp(getenv('HTTP_ACCEPT_LANGUAGE'));

// Default Locale
Locale::setDefault($locale);
setlocale(LC_ALL, $locale . '.UTF-8');

// Default timezone of server
date_default_timezone_set('UTC');

// iconv encoding
iconv_set_encoding("internal_encoding", "UTF-8");

// multibyte encoding
mb_internal_encoding('UTF-8');

有几件事情需要解决并检测时区/区域设置,然后使用它来正确解析和显示输入和输出很重要。刚刚发布的PHP I18N library包含大部分信息的查找表。

处理用户输入对于确保应用程序具有来自用户输入的任何输入的干净,格式良好的UTF-8字符串非常重要。 iconv对此非常有用。

/**
 * Convert a string from one encoding to another encoding
 * and remove invalid bytes sequences.
 *
 * @param string $string to convert
 * @param string $to encoding you want the string in
 * @param string $from encoding that string is in
 * @return string
 */
function encode($string, $to = 'UTF-8', $from = 'UTF-8')
{
    // ASCII is already valid UTF-8
    if($to == 'UTF-8' AND is_ascii($string))
    {
        return $string;
    }

    // Convert the string
    return @iconv($from, $to . '//TRANSLIT//IGNORE', $string);
}


/**
 * Tests whether a string contains only 7bit ASCII characters.
 *
 * @param string $string to check
 * @return bool
 */
function is_ascii($string)
{
    return ! preg_match('/[^\x00-\x7F]/S', $string);
}

然后只需通过这些函数运行输入。

$utf8_string = normalizer_normalize(encode($_POST['text']), Normalizer::FORM_C);

翻译

正如Andre所说,似乎gettext是编写可翻译应用程序的明智选择。

  1. Gettext使用非常快的二进制协议。
  2. gettext实现通常更简单,因为它只需要_('Text to translate')
  3. 现有的译员使用工具,并证明它们运作良好。
  4. 当你达到facebook大小时,你就可以开始实现RAM缓存的替代方法,就像我在问题中提到的那样。然而,对于大多数项目来说,没有什么比“简单,快速和有效”更胜一筹。

    但是,还有gettext无法处理的东西。比如显示日期,金钱和数字。对于那些你需要INTL extionsion

    的人
    /**
     * Return an IntlDateFormatter object using the current system locale
     *
     * @param string $locale string
     * @param integer $datetype IntlDateFormatter constant
     * @param integer $timetype IntlDateFormatter constant
     * @param string $timezone Time zone ID, default is system default
     * @return IntlDateFormatter
     */
    function __date($locale = NULL, $datetype = IntlDateFormatter::MEDIUM, $timetype = IntlDateFormatter::SHORT, $timezone = NULL)
    {
        return new IntlDateFormatter($locale ?: setlocale(LC_ALL, 0), $datetype, $timetype, $timezone);
    }
    
    $now = new DateTime();
    print __date()->format($now);
    $time = __date()->parse($string);
    

    此外,您可以使用strftime来解析考虑当前区域设置的日期。

    有时您需要将数字和日期的值正确插入区域设置消息

    /**
     * Format the given string using the current system locale
     * Basically, it's sprintf on i18n steroids.
     *
     * @param string $string to parse
     * @param array $params to insert
     * @return string
     */
    function __($string, array $params = NULL)
    {
        return msgfmt_format_message(setlocale(LC_ALL, 0), $string, $params);
    }
    
    // Multiple choices (can also just use ngettext)
    print __(_("{1,choice,0#no errors|1#single error|1<{1, number} errors}"), array(4));
    
    // Show time in the correct way
    print __(_("It is now {0,time,medium}), time());
    

    有关详细信息,请参阅ICU format details

    数据库

    确保您与数据库的连接使用正确的字符集,以便在存储时不会出现任何问题。

    字符串函数

    您需要了解stringmb_stringgrapheme functions之间的区别。

    // 'LATIN SMALL LETTER A WITH RING ABOVE' (U+00E5) normalization form "D"
    $char_a_ring_nfd = "a\xCC\x8A";
    
    var_dump(grapheme_strlen($char_a_ring_nfd));
    var_dump(mb_strlen($char_a_ring_nfd));
    var_dump(strlen($char_a_ring_nfd));
    
    // 'LATIN CAPITAL LETTER A WITH RING ABOVE' (U+00C5)
    $char_A_ring = "\xC3\x85";
    
    var_dump(grapheme_strlen($char_A_ring));
    var_dump(mb_strlen($char_A_ring));
    var_dump(strlen($char_A_ring));
    

    域名TLD的

    INTL库中的IDN functions对处理非ascii域名有很大帮助。

答案 2 :(得分:3)

还有许多与此类似的其他SO问题和答案。我建议你搜索并阅读它们。

么?使用像gettext或xliff这样的现有解决方案,因为当您点击所有翻译边缘情况(例如从右到左文本,日期格式,不同文本卷)时,它会为您节省很多痛苦,法语比英语更加冗长,例如螺丝格式化等。更好的建议不要这样做。如果用户想要翻译,他们将进行克隆并翻译。因为本地化更多的是外观和使用口语,所以通常会发生这种情况。再给予和示例盎格鲁撒克逊文化喜欢酷网页颜色和san-serif类型的面孔。西班牙文化,如鲜艳的色彩和衬线/草书类型。为了满足您的需要,每种语言需要不同的布局。

Zend实际上为Zend_Translate提供了以下适配器,它是一个有用的列表。

  • 数组: - 将PHP数组用于小页面;最简单的用法;仅适用于程序员
  • Csv: - 使用逗号分隔( .csv / .txt)文件作为简单文本文件格式;快速; unicode字符可能存在的问题
  • Gettext: - 为Linux的GNU标准使用二进制gettext(* .mo)文件;线程安全的;需要翻译工具
  • Ini: - 将简单的INI(* .ini)文件用于简单文本文件格式;快速; unicode字符可能存在的问题
  • Tbx: - 对于应用程序间术语字符串,使用术语库交换( .tbx / .xml)文件作为行业标准; XML格式
  • Tmx: - 使用行业标准的tmx( .tmx / .xml)文件进行应用程序间转换; XML格式;人类可读
  • Qt: - 将qt语言学家(* .ts)文件用于跨平台应用程序框架; XML格式;人类可读
  • Xliff: - 使用xliff( .xliff / .xml)文件作为TMX的简单格式但与之相关; XML格式;人类可读
  • XmlTm: - 使用xmltm(* .xml)文件作为XML文档翻译记忆库的行业标准; XML格式;人类可读
  • 其他: - * .sql for不同的其他适配器可能会在未来实施

答案 3 :(得分:3)

我在我的框架中使用ICU的东西,并且发现它使用简单而有用。我的系统是基于XML的XPath查询,而不是您建议使用的数据库。我没有发现这种方法效率低下。在研究技术时我也使用了资源包,但发现它们很难实现。

Locale功能是神派。你可以更轻松地做到这一点:

// Available translations
$languages = array('en', 'fr', 'de');

// The language the user wants
$preference = (isset($_COOKIE['lang'])) ?
    $_COOKIE['lang'] : ((isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) ?
        Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']) : '');

// Match preferred language to those available, defaulting to generic English
$locale = Locale::lookup($languages, $preference, false, 'en');

// Construct path to dictionary file
$file = $dir . '/' . $locale . '.xsl';

// Check that dictionary file is readable
if (!file_exists($file) || !is_readable($file)) {
    throw new RuntimeException('Dictionary could not be loaded');
}

// Load and return dictionary file
$dictionary = simplexml_load_file($file);

然后我使用这样的方法执行单词查找:

$selector = '/i18n/text[@label="' . $word . '"]';
$result = $dictionary->xpath($selector);
$text = array_shift($result);

if ($formatted && isset($text)) {
    return new MessageFormatter($locale, $text);
 }

我的系统的好处是模板系统是基于XSL的,这意味着我可以直接在我的模板中使用相同的翻译XML文件,用于不需要任何i18n格式化的简单消息。

答案 4 :(得分:1)

坚持使用gettext,你不会在PHP中找到更快的替代方案。

关于如何,您可以使用数据库存储目录,并允许其他用户使用友好的gui翻译字符串。审核/批准新更改后,点击按钮,编译新的.mo文件并进行部署。

让您走上正轨的一些资源:

答案 5 :(得分:1)

csv文件(可以在很多应用程序中轻松编辑)和缓存到memcache(wincache等)怎么样?这种方法在magento中运行良好。代码中的所有语言短语都包含在__()函数中,例如

<?php echo $this->__('Some text') ?>

然后,例如在新版本发布之前,您运行简单脚本来解析源文件,查找包含在__()中的所有文本并放入.csv文件。您加载csv文件并将其缓存到memcache。在__()函数中,您将查看缓存转换的内存缓存。

答案 6 :(得分:0)

在最近的一个项目中,我们考虑使用gettext,但结果却更容易编写我们自己的功能。这非常简单:在每个语言环境中创建一个JSON文件(例如strings.en.json,strings.es.json等),并在某个地方创建一个名为“translate()”的东西,然后调用它。该函数将确定当前的语言环境(来自URI或会话var或其他内容),并返回本地化字符串。

唯一要记住的是确保输出的任何HTML都以UTF-8编码,并在标记中标记为(例如在doctype等中)

答案 7 :(得分:0)

也许不是你问题的真正答案,但也许你可以从Symfony翻译组件中获得一些想法?它看起来对我很好,虽然我必须承认我还没有用过它。

可以在

找到该组件的文档

http://symfony.com/doc/current/book/translation.html

可以在

找到该组件的代码

https://github.com/symfony/Translation

使用Translation组件应该很容易,因为Symfony组件旨在能够用作独立组件。

答案 8 :(得分:0)

  

另一方面,我不喜欢gettext的一个原因是   文本嵌入到整个应用程序中。那   意味着负责主要翻译的团队(通常是   英语)必须有权访问项目源代码才能进行更改   在所有地方放置默认语句。它差不多了   糟糕,因为全都有SQL意大利面条代码的应用程序。

事实并非如此。你可以有一个头文件(对不起,ex C程序员),例如:

<?php
define(MSG_404_NOT_FOUND, 'error.404_not_found')
?>

然后,只要您想要留言,请使用_(MSG_404_NOT_FOUND)。这比要求开发人员每次想要吐出本地化版本时都要记住非本地化消息的确切语法要灵活得多。

您可以更进一步,在构建步骤中生成头文件,可能来自CSV或数据库,并与转换交叉引用以检测缺少的字符串。

答案 9 :(得分:0)

有一个适用于此的zend插件。

<?php
/** dependencies **/
require 'Zend/Loader/Autoloader.php';
require 'Zag/Filter/CharConvert.php';

Zend_Loader_Autoloader::getInstance()->setFallbackAutoloader(true);

//filter
$filter = new Zag_Filter_CharConvert(array(
    'replaceWhiteSpace' => '-',
    'locale' => 'en_US',
    'charset'=> 'UTF-8'
));

echo $filter->filter('ééé ááá 90');//eee-aaa-90
echo $filter->filter('óóó 10aáééé');//ooo-10aaeee

如果你不想使用zend框架,只能使用插件。

拥抱!