繁体   English   中英

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

[英]Implementing internationalization (language strings) in a PHP application

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

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

我想过使用gettext ,但我不确定CMS的用户是否会习惯使用gettext文件。 如果字符串存储在数据库中,那么可以设置一个不错的管理系统,允许它们随时进行更改,RAM中的缓存将确保获取这些字符串的速度比gettext快或快。 考虑到甚至zend框架都没有使用它,我也觉得使用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扩展。

Gettext使用非常快速的二进制协议。 此外,gettext实现通常更简单,因为它只需要echo _('Text to translate'); 它还有现有的翻译工具,并且已被证明可以很好地运行。

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

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

对于那些感兴趣的人来说,它似乎完全支持语言环境 ,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库包含大部分信息的查找表。

处理用户输入对于确保应用程序具有来自用户输入的任何输入的干净,格式良好的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. 翻译人员使用的现有工具,并证明它们运作良好。

当你达到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格式详细信息。

数据库

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

字符串函数

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

// '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函数是处理非ascii域名的重要帮助。

还有许多与此类似的其他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用于不同的其他适配器将来可能会实现

我在我的框架中使用ICU的东西,并且发现它使用起来非常简单和有用。 我的系统是基于XML的XPath查询,而不是您建议使用的数据库。 我没有发现这种方法效率低下。 在研究技术时我也玩过Resource bundle,但发现它们实现起来相当复杂。

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格式化的简单消息。

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

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

一些资源让您走上正轨:

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

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

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

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

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

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

可以在以下位置找到该组件的文档

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

并且可以在以下位置找到组件的代码

https://github.com/symfony/Translation

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

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

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

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

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

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

有一个适用于此的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框架只能使用插件。

拥抱!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM