最佳实践多语言网站

时间:2013-10-08 13:33:26

标签: php mysql localization internationalization multilingual

我已经在这几个月里一直在努力解决这个问题,但我还没有遇到过以前需要探索所有可能选择的情况。现在,我觉得是时候了解可能性并创建我个人的偏好,以便在我即将开展的项目中使用。

让我先描绘一下我正在寻找的情况

我即将升级/重新开发我已经使用了很长一段时间的内容管理系统。但是,我觉得多语言是对这个系统的一个很大的改进。在我没有使用任何框架之前,我将使用Laraval4来完成即将到来的项目。 Laravel似乎是更简洁的PHP编码方式的最佳选择。 Sidenote: Laraval4 should be no factor in your answer。我正在寻找与平台/框架无关的一般翻译方式。

应该翻译什么

由于我所寻找的系统需要尽可能方便用户,因此管理翻译的方法应该在CMS内部。应该没有必要启动FTP连接来修改翻译文件或任何html / php解析模板。

此外,我正在寻找最简单的方法来翻译多个数据库表,而不需要制作额外的表。

我自己想出了什么

我自己一直在寻找,阅读和尝试。我有几个选择。但我仍然觉得我没有达到我真正想要的最佳实践方法。现在,这是我提出的,但这种方法也有副作用。

  1. PHP Parsed Templates :模板系统应该由PHP解析。通过这种方式,我可以将已翻译的参数插入到HTML中,而无需打开模板并进行修改。除此之外,PHP解析模板使我能够为完整的网站提供1个模板,而不是每种语言都有一个子文件夹(我之前已经拥有过)。达到此目标的方法可以是Smarty,TemplatePower,Laravel的Blade或任何其他模板解析器。正如我所说,这应该独立于书面解决方案。
  2. 数据库驱动:也许我不需要再提这个。但解决方案应该是数据库驱动的。 CMS旨在面向对象和MVC,因此我需要考虑字符串的逻辑数据结构。由于我的模板是结构化的:templates / Controller / View.php也许这个结构最有意义:Controller.View.parameter。数据库表将使这些字段长value字段。在模板内部,我们可以使用某种排序方法,例如echo __('Controller.View.welcome', array('name', 'Joshua')),参数包含Welcome, :name。因此结果为Welcome, Joshua。这似乎是一个很好的方法,因为参数如:名称很容易被编辑理解。
  3. 低数据库负载:当然,如果在旅途中加载这些字符串,上述系统会导致数据库负载加载。因此,我需要一个缓存系统,一旦在管理环境中编辑/保存语言文件,就会重新呈现语言文件。由于生成了文件,因此还需要良好的文件系统布局。我想我们可以使用languages/en_EN/Controller/View.php或.ini,最适合你的。也许.ini甚至可以在最后解析得更快。这个fould应该包含format parameter=value;中的数据 。我想这是执行此操作的最佳方式,因为呈现的每个View都可以包含它自己的语言文件(如果存在)。然后应将语言参数加载到特定视图而不是全局范围,以防止参数相互覆盖。
  4. 数据库表翻译:这实际上是我最担心的事情。我正在寻找一种创建新闻/页面/等翻译的方法。尽快。每个模块都有两个表(例如NewsNews_translations)是一个选项,但是为了获得一个好的系统,感觉还有很多工作要做。我提出的一件事是基于我写的data versioning系统:有一个数据库表名Translations,这个表有language,{{1}的唯一组合}和tablename。例如:en_En / News / 1(参考ID = 1的新闻项目的英文版本)。但是这个方法存在两个巨大的缺点:首先,这个表在数据库中有大量数据需要很长时间,其次使用这个设置搜索表是一件很麻烦的工作。例如。搜索该项目的SEO slug将是一个全文搜索,这是非常愚蠢的。但另一方面:它是一种快速的方式,可以非常快速地在每个表格中创建可翻译的内容,但我不相信这个专业人士会超过这些内容。
  5. 前端工作:前端也需要一些思考。当然,我们会将可用语言存储在数据库中,并(de)激活我们需要的语言。通过这种方式,脚本可以生成下拉列表以选择语言,后端可以自动决定使用CMS可以进行哪些翻译。在获取视图的语言文件或获取网站上内容项的正确翻译时,将使用所选语言(例如en_EN)。
  6. 所以,他们就是。我的想法到目前为止。它们甚至还没有包含日期​​等的本地化选项,但是由于我的服务器支持PHP5.3.2 +,最好的选择是使用intl扩展,如下所述:http://devzone.zend.com/1500/internationalization-in-php-53/ - 但这可用于任何后来的发展体育场。目前,主要问题是如何在网站上获得最佳的内容翻译实践。

    除了我在这里解释的所有内容之外,我还有另外一件我尚未决定的事情,它看起来像一个简单的问题,但事实上它让我很头疼:

    网址翻译?我们应该这样做吗?以什么方式?

    所以..如果我有这个网址:primarykey,英语是我的默认语言。当我选择荷兰语作为我的语言时,是否应将此网址翻译为http://www.domain.com/about-us?或者我们应该走简单的路,只需更改http://www.domain.com/over-ons处可见的页面内容。最后一件事似乎不是一个有效的选项,因为这会生成相同网址的多个版本,这会使内容索引失败。

    另一种选择是改用/about。这会为每个内容生成至少一个唯一的URL。此外,更容易使用其他语言,例如http://www.domain.com/nl/about-us,并且Google和Google访问者都可以更轻松地了解所提供的网址。使用此选项,我们如何处理默认语言?默认语言是否应删除默认选择的语言?因此,将http://www.domain.com/en/about-us重定向到http://www.domain.com/en/about-us ...在我看来,这是最佳解决方案,因为当CMS仅针对一种语言设置时,不需要在URL中使用此语言标识。

    第三个选项是两个选项的组合:使用"语言识别少" -URL(http://www.domain.com/about-us)作为主要语言。并使用带有翻译的SEO slug的URL用于子语言:http://www.domain.com/about-us& http://www.domain.com/nl/over-ons

    我希望我的问题让你的头脑开裂,他们肯定会破解我的!它确实帮助我在这里解决问题。让我有机会回顾一下我之前使用的方法以及我即将推出的CMS的想法。

    我想感谢你花时间阅读这一堆文字!

    http://www.domain.com/de/uber-uns

    我忘了提及:__()函数是翻译给定字符串的别名。在这种方法中,显然应该有某种回退方法,当没有可用的翻译时,加载默认文本。如果缺少翻译,则应插入翻译文件或重新生成翻译文件。

11 个答案:

答案 0 :(得分:50)

使用Thomas Bley建议的预处理器实现没有性能命令的i18n

在工作中,我们最近在我们的几个属性上执行了i18n,我们一直在努力解决的问题之一是处理即时翻译的性能问题,然后我发现了{{3}这激发了我们使用i18n以最小的性能问题处理大流量负载的方式。

而不是为每个翻译操作调用函数,我们在PHP中知道这是昂贵的,我们用占位符定义我们的基本文件,然后使用预处理器来缓存这些文件(我们存储文件修改时间以确保我们“随时提供最新内容。”

翻译标签

Thomas使用{tr}{/tr}标记来定义翻译的开始和结束位置。由于我们正在使用TWIG,因此我们不希望使用{来避免混淆,因此我们使用[%tr%][%/tr%]代替。基本上,这看起来像这样:

`return [%tr%]formatted_value[%/tr%];`

请注意,Thomas建议在文件中使用基础英语。我们不这样做是因为如果我们更改英文值,我们不想修改所有翻译文件。

INI文件

然后,我们为每种语言创建一个INI文件,格式为placeholder = translated

// lang/fr.ini
formatted_value = number_format($value * Model_Exchange::getEurRate(), 2, ',', ' ') . '€'

// lang/en_gb.ini
formatted_value = '£' . number_format($value * Model_Exchange::getStgRate())

// lang/en_us.ini
formatted_value = '$' . number_format($value)

允许用户在CMS中修改这些内容是非常简单的,只需通过preg_split\n上的=获取密钥对,并使CMS能够写入INI档案。

预处理器组件

基本上,Thomas建议使用即时“编译器”(事实上,它是一个预处理器)这样的函数来获取您的翻译文件并在磁盘上创建静态PHP文件。这样,我们实质上缓存了我们翻译的文件,而不是为文件中的每个字符串调用翻译函数:

// This function was written by Thomas Bley, not by me
function translate($file) {
  $cache_file = 'cache/'.LANG.'_'.basename($file).'_'.filemtime($file).'.php';
  // (re)build translation?
  if (!file_exists($cache_file)) {
    $lang_file = 'lang/'.LANG.'.ini';
    $lang_file_php = 'cache/'.LANG.'_'.filemtime($lang_file).'.php';

    // convert .ini file into .php file
    if (!file_exists($lang_file_php)) {
      file_put_contents($lang_file_php, '<?php $strings='.
        var_export(parse_ini_file($lang_file), true).';', LOCK_EX);
    }
    // translate .php into localized .php file
    $tr = function($match) use (&$lang_file_php) {
      static $strings = null;
      if ($strings===null) require($lang_file_php);
      return isset($strings[ $match[1] ]) ? $strings[ $match[1] ] : $match[1];
    };
    // replace all {t}abc{/t} by tr()
    file_put_contents($cache_file, preg_replace_callback(
      '/\[%tr%\](.*?)\[%\/tr%\]/', $tr, file_get_contents($file)), LOCK_EX);
  }
  return $cache_file;
}

注意:我没有验证正则表达式是否有效,我没有从公司服务器上复制它,但你可以看到操作是如何工作的。

如何调用

同样,这个例子来自Thomas Bley,而不是来自我:

// instead of
require("core/example.php");
echo (new example())->now();

// we write
define('LANG', 'en_us');
require(translate('core/example.php'));
echo (new example())->now();

我们将语言存储在cookie中(如果我们无法获取cookie,则将会话变量存储),然后在每次请求时检索它。您可以将其与可选的$_GET参数结合使用以覆盖该语言,但我不建议使用每个语言的子域名或每个语言的页面,因为这样会更难以查看哪些页面很受欢迎,并且会减少入站链接的价值,因为它们几乎不会传播它们。

为什么要使用这种方法?

我们喜欢这种预处理方法有三个原因:

  1. 由于没有为很少更改的内容调用大量功能而获得巨大的性能提升(使用此系统,法语中的100k访问者仍然只会运行一次翻译)。
  2. 它不会向我们的数据库添加任何负载,因为它使用简单的平面文件并且是纯PHP解决方案。
  3. 在翻译中使用PHP表达式的能力。
  4. 获取翻译的数据库内容

    我们只在我们的数据库中添加一个名为language的内容列,然后我们使用我们之前定义的LANG常量的访问器方法,所以我们的SQL调用(遗憾地使用ZF1)看起来像这样:

    $query = select()->from($this->_name)
                     ->where('language = ?', User::getLang())
                     ->where('id       = ?', $articleId)
                     ->limit(1);
    

    我们的文章有一个超过idlanguage的复合主键,所以文章54可以存在于所有语言中。如果未指定,我们的LANG默认为en_US

    URL Slug Translation

    我在这里结合了两件事,一件是你的bootstrap中的一个函数,它接受语言的$_GET参数并覆盖cookie变量,另一个是接受多个slug的路由。然后你可以在你的路由中做这样的事情:

    "/wilkommen" => "/welcome/lang/de"
    ... etc ...
    

    这些可以存储在一个平面文件中,可以从管理面板轻松写入。 JSON或XML可以提供支持它们的良好结构。

    关于其他几个选项的说明

    基于PHP的即时翻译

    我看不出这些优于预处理翻译的优势。

    基于前端的翻译

    我很久以前发现这些很有趣,但有一些警告。例如,您必须向用户提供您计划翻译的网站上的完整短语列表,如果您隐藏或不允许他们访问该网站的某些区域,则可能会出现问题。

    您还必须假设您的所有用户都愿意并且能够在您的网站上使用Javascript,但是从我的统计数据来看,大约2.5%的用户在没有它的情况下运行(或使用Noscript阻止我们的网站用它)。

    数据库驱动的翻译

    PHP的数据库连接速度无需写回家,这增加了在每个要翻译的短语上调用函数的高额开销。表演&amp;这种方法似乎压倒了可扩展性问题。

答案 1 :(得分:14)

我建议你不要发明一个轮子并使用gettext和ISO语言的缩写列表。你有没有看到i18n / l10n如何在流行的CMS或框架中实现?

使用gettext,您将拥有一个功能强大的工具,其中许多案例已经实现为多种形式的数字。在英语中,您只有两个选项:单数和复数。但在俄语中有3种形式,并不像英语那么简单。

许多翻译人员也有使用gettext的经验。

查看CakePHPDrupal。两种多语言都启用了。 CakePHP作为界面本地化的例子,Drupal作为内容翻译的例子。

对于使用数据库的l10n根本不是这种情况。对于查询,它将是吨。标准方法是在早期阶段(或者如果您更喜欢延迟加载,在第一次调用i10n函数期间)将所有l10n数据存入内存。它可以一次从.po文件或DB中读取所有数据。而不只是从数组中读取请求的字符串。

如果您需要实现在线工具来翻译界面,您可以将所有数据保存在数据库中,但仍然将所有数据保存到文件中以便使用它。为了减少内存中的数据量,您可以将所有已翻译的消息/字符串拆分成组,而不是只加载您需要的组。

所以你完全正确的#3。有一个例外:通常它是一个大文件而不是每个控制器文件。因为打开一个文件最好是性能。您可能知道一些高负载的Web应用程序在一个文件中编译所有PHP代码,以避免在include / require调用时进行文件操作。

关于网址。 Google indirectly suggest使用翻译:

  

清楚地表明法语内容:   http://example.ca/fr/vélo-de-montagne.html

此外,我认为您需要将用户重定向到默认语言前缀,例如http://examlpe.com/about-us会重定向到http://examlpe.com/en/about-us 但是,如果您的网站只使用一种语言,那么根本不需要前缀。

退房: http://www.audiomicro.com/trailer-hit-impact-psychodrama-sound-effects-836925 http://nl.audiomicro.com/aanhangwagen-hit-effect-psychodrama-geluidseffecten-836925 http://de.audiomicro.com/anhanger-hit-auswirkungen-psychodrama-sound-effekte-836925

翻译内容是一项更艰巨的任务。我认为这将与不同类型的内容有所不同,例如文章,菜单项等。但在#4你是正确的方式。看看Drupal有更多的想法。它具有足够清晰的DB模式和足够好的翻译界面。就像你创建文章并为它选择语言一样。而且你以后可以将它翻译成其他语言。

Drupal translation interface

我认为这不是URL slugs的问题。您可以为slug创建单独的表,这将是正确的决定。即使使用大量数据,使用正确的索引也无法查询表。 它不是全文搜索,而是字符串匹配,如果将使用varchar数据类型为slug,你也可以在该字段上有索引。

PS抱歉,我的英语远非完美。

答案 2 :(得分:10)

这取决于您的网站有多少内容。起初,我在这里使用了像所有其他人一样的数据库,但编写数据库的所有工作脚本可能非常耗时。我不是说这是一种理想的方法,特别是如果你有很多文字,但如果你想在不使用数据库的情况下快速完成,这种方法可行,但是,你不能允许用户输入将用作翻译文件的数据。但如果您自己添加翻译,它将起作用:

让我们说你有这样的文字:

Welcome!

您可以在包含翻译的数据库中输入,但您也可以这样做:

$welcome = array(
"English"=>"Welcome!",
"German"=>"Willkommen!",
"French"=>"Bienvenue!",
"Turkish"=>"Hoşgeldiniz!",
"Russian"=>"Добро пожаловать!",
"Dutch"=>"Welkom!",
"Swedish"=>"Välkommen!",
"Basque"=>"Ongietorri!",
"Spanish"=>"Bienvenito!"
"Welsh"=>"Croeso!");

现在,如果您的网站使用Cookie,您可以使用以下代码:

$_COOKIE['language'];

为了方便起见,我们将其转换为易于使用的代码:

$language=$_COOKIE['language'];

如果你的cookie语言是威尔士语并且你有这段代码:

echo $welcome[$language];

结果将是:

Croeso!

如果您需要为您的网站添加大量翻译并且数据库过于消耗,那么使用阵列可能是理想的解决方案。

答案 3 :(得分:7)

我建议你不要真正依赖数据库进行翻译,这可能是一个非常混乱的任务,在数据编码的情况下可能是一个极端的问题。

我在前一段时间遇到过类似的问题,并在课后写下来解决我的问题

对象:Locale \ Locale

<?php

  namespace Locale;

  class Locale{

// Following array stolen from Zend Framework
public $country_to_locale = array(
    'AD' => 'ca_AD',
    'AE' => 'ar_AE',
    'AF' => 'fa_AF',
    'AG' => 'en_AG',
    'AI' => 'en_AI',
    'AL' => 'sq_AL',
    'AM' => 'hy_AM',
    'AN' => 'pap_AN',
    'AO' => 'pt_AO',
    'AQ' => 'und_AQ',
    'AR' => 'es_AR',
    'AS' => 'sm_AS',
    'AT' => 'de_AT',
    'AU' => 'en_AU',
    'AW' => 'nl_AW',
    'AX' => 'sv_AX',
    'AZ' => 'az_Latn_AZ',
    'BA' => 'bs_BA',
    'BB' => 'en_BB',
    'BD' => 'bn_BD',
    'BE' => 'nl_BE',
    'BF' => 'mos_BF',
    'BG' => 'bg_BG',
    'BH' => 'ar_BH',
    'BI' => 'rn_BI',
    'BJ' => 'fr_BJ',
    'BL' => 'fr_BL',
    'BM' => 'en_BM',
    'BN' => 'ms_BN',
    'BO' => 'es_BO',
    'BR' => 'pt_BR',
    'BS' => 'en_BS',
    'BT' => 'dz_BT',
    'BV' => 'und_BV',
    'BW' => 'en_BW',
    'BY' => 'be_BY',
    'BZ' => 'en_BZ',
    'CA' => 'en_CA',
    'CC' => 'ms_CC',
    'CD' => 'sw_CD',
    'CF' => 'fr_CF',
    'CG' => 'fr_CG',
    'CH' => 'de_CH',
    'CI' => 'fr_CI',
    'CK' => 'en_CK',
    'CL' => 'es_CL',
    'CM' => 'fr_CM',
    'CN' => 'zh_Hans_CN',
    'CO' => 'es_CO',
    'CR' => 'es_CR',
    'CU' => 'es_CU',
    'CV' => 'kea_CV',
    'CX' => 'en_CX',
    'CY' => 'el_CY',
    'CZ' => 'cs_CZ',
    'DE' => 'de_DE',
    'DJ' => 'aa_DJ',
    'DK' => 'da_DK',
    'DM' => 'en_DM',
    'DO' => 'es_DO',
    'DZ' => 'ar_DZ',
    'EC' => 'es_EC',
    'EE' => 'et_EE',
    'EG' => 'ar_EG',
    'EH' => 'ar_EH',
    'ER' => 'ti_ER',
    'ES' => 'es_ES',
    'ET' => 'en_ET',
    'FI' => 'fi_FI',
    'FJ' => 'hi_FJ',
    'FK' => 'en_FK',
    'FM' => 'chk_FM',
    'FO' => 'fo_FO',
    'FR' => 'fr_FR',
    'GA' => 'fr_GA',
    'GB' => 'en_GB',
    'GD' => 'en_GD',
    'GE' => 'ka_GE',
    'GF' => 'fr_GF',
    'GG' => 'en_GG',
    'GH' => 'ak_GH',
    'GI' => 'en_GI',
    'GL' => 'iu_GL',
    'GM' => 'en_GM',
    'GN' => 'fr_GN',
    'GP' => 'fr_GP',
    'GQ' => 'fan_GQ',
    'GR' => 'el_GR',
    'GS' => 'und_GS',
    'GT' => 'es_GT',
    'GU' => 'en_GU',
    'GW' => 'pt_GW',
    'GY' => 'en_GY',
    'HK' => 'zh_Hant_HK',
    'HM' => 'und_HM',
    'HN' => 'es_HN',
    'HR' => 'hr_HR',
    'HT' => 'ht_HT',
    'HU' => 'hu_HU',
    'ID' => 'id_ID',
    'IE' => 'en_IE',
    'IL' => 'he_IL',
    'IM' => 'en_IM',
    'IN' => 'hi_IN',
    'IO' => 'und_IO',
    'IQ' => 'ar_IQ',
    'IR' => 'fa_IR',
    'IS' => 'is_IS',
    'IT' => 'it_IT',
    'JE' => 'en_JE',
    'JM' => 'en_JM',
    'JO' => 'ar_JO',
    'JP' => 'ja_JP',
    'KE' => 'en_KE',
    'KG' => 'ky_Cyrl_KG',
    'KH' => 'km_KH',
    'KI' => 'en_KI',
    'KM' => 'ar_KM',
    'KN' => 'en_KN',
    'KP' => 'ko_KP',
    'KR' => 'ko_KR',
    'KW' => 'ar_KW',
    'KY' => 'en_KY',
    'KZ' => 'ru_KZ',
    'LA' => 'lo_LA',
    'LB' => 'ar_LB',
    'LC' => 'en_LC',
    'LI' => 'de_LI',
    'LK' => 'si_LK',
    'LR' => 'en_LR',
    'LS' => 'st_LS',
    'LT' => 'lt_LT',
    'LU' => 'fr_LU',
    'LV' => 'lv_LV',
    'LY' => 'ar_LY',
    'MA' => 'ar_MA',
    'MC' => 'fr_MC',
    'MD' => 'ro_MD',
    'ME' => 'sr_Latn_ME',
    'MF' => 'fr_MF',
    'MG' => 'mg_MG',
    'MH' => 'mh_MH',
    'MK' => 'mk_MK',
    'ML' => 'bm_ML',
    'MM' => 'my_MM',
    'MN' => 'mn_Cyrl_MN',
    'MO' => 'zh_Hant_MO',
    'MP' => 'en_MP',
    'MQ' => 'fr_MQ',
    'MR' => 'ar_MR',
    'MS' => 'en_MS',
    'MT' => 'mt_MT',
    'MU' => 'mfe_MU',
    'MV' => 'dv_MV',
    'MW' => 'ny_MW',
    'MX' => 'es_MX',
    'MY' => 'ms_MY',
    'MZ' => 'pt_MZ',
    'NA' => 'kj_NA',
    'NC' => 'fr_NC',
    'NE' => 'ha_Latn_NE',
    'NF' => 'en_NF',
    'NG' => 'en_NG',
    'NI' => 'es_NI',
    'NL' => 'nl_NL',
    'NO' => 'nb_NO',
    'NP' => 'ne_NP',
    'NR' => 'en_NR',
    'NU' => 'niu_NU',
    'NZ' => 'en_NZ',
    'OM' => 'ar_OM',
    'PA' => 'es_PA',
    'PE' => 'es_PE',
    'PF' => 'fr_PF',
    'PG' => 'tpi_PG',
    'PH' => 'fil_PH',
    'PK' => 'ur_PK',
    'PL' => 'pl_PL',
    'PM' => 'fr_PM',
    'PN' => 'en_PN',
    'PR' => 'es_PR',
    'PS' => 'ar_PS',
    'PT' => 'pt_PT',
    'PW' => 'pau_PW',
    'PY' => 'gn_PY',
    'QA' => 'ar_QA',
    'RE' => 'fr_RE',
    'RO' => 'ro_RO',
    'RS' => 'sr_Cyrl_RS',
    'RU' => 'ru_RU',
    'RW' => 'rw_RW',
    'SA' => 'ar_SA',
    'SB' => 'en_SB',
    'SC' => 'crs_SC',
    'SD' => 'ar_SD',
    'SE' => 'sv_SE',
    'SG' => 'en_SG',
    'SH' => 'en_SH',
    'SI' => 'sl_SI',
    'SJ' => 'nb_SJ',
    'SK' => 'sk_SK',
    'SL' => 'kri_SL',
    'SM' => 'it_SM',
    'SN' => 'fr_SN',
    'SO' => 'sw_SO',
    'SR' => 'srn_SR',
    'ST' => 'pt_ST',
    'SV' => 'es_SV',
    'SY' => 'ar_SY',
    'SZ' => 'en_SZ',
    'TC' => 'en_TC',
    'TD' => 'fr_TD',
    'TF' => 'und_TF',
    'TG' => 'fr_TG',
    'TH' => 'th_TH',
    'TJ' => 'tg_Cyrl_TJ',
    'TK' => 'tkl_TK',
    'TL' => 'pt_TL',
    'TM' => 'tk_TM',
    'TN' => 'ar_TN',
    'TO' => 'to_TO',
    'TR' => 'tr_TR',
    'TT' => 'en_TT',
    'TV' => 'tvl_TV',
    'TW' => 'zh_Hant_TW',
    'TZ' => 'sw_TZ',
    'UA' => 'uk_UA',
    'UG' => 'sw_UG',
    'UM' => 'en_UM',
    'US' => 'en_US',
    'UY' => 'es_UY',
    'UZ' => 'uz_Cyrl_UZ',
    'VA' => 'it_VA',
    'VC' => 'en_VC',
    'VE' => 'es_VE',
    'VG' => 'en_VG',
    'VI' => 'en_VI',
    'VN' => 'vn_VN',
    'VU' => 'bi_VU',
    'WF' => 'wls_WF',
    'WS' => 'sm_WS',
    'YE' => 'ar_YE',
    'YT' => 'swb_YT',
    'ZA' => 'en_ZA',
    'ZM' => 'en_ZM',
    'ZW' => 'sn_ZW'
);

/**
 * Store the transaltion for specific languages
 *
 * @var array
 */
protected $translation = array();

/**
 * Current locale
 *
 * @var string
 */
protected $locale;

/**
 * Default locale
 *
 * @var string
 */
protected $default_locale;

/**
 *
 * @var string
 */
protected $locale_dir;

/**
 * Construct.
 *
 *
 * @param string $locale_dir            
 */
public function __construct($locale_dir)
{
    $this->locale_dir = $locale_dir;
}

/**
 * Set the user define localte
 *
 * @param string $locale            
 */
public function setLocale($locale = null)
{
    $this->locale = $locale;

    return $this;
}

/**
 * Get the user define locale
 *
 * @return string
 */
public function getLocale()
{
    return $this->locale;
}

/**
 * Get the Default locale
 *
 * @return string
 */
public function getDefaultLocale()
{
    return $this->default_locale;
}

/**
 * Set the default locale
 *
 * @param string $locale            
 */
public function setDefaultLocale($locale)
{
    $this->default_locale = $locale;

    return $this;
}

/**
 * Determine if transltion exist or translation key exist
 *
 * @param string $locale            
 * @param string $key            
 * @return boolean
 */
public function hasTranslation($locale, $key = null)
{
    if (null == $key && isset($this->translation[$locale])) {
        return true;
    } elseif (isset($this->translation[$locale][$key])) {
        return true;
    }

    return false;
}

/**
 * Get the transltion for required locale or transtion for key
 *
 * @param string $locale            
 * @param string $key            
 * @return array
 */
public function getTranslation($locale, $key = null)
{
    if (null == $key && $this->hasTranslation($locale)) {
        return $this->translation[$locale];
    } elseif ($this->hasTranslation($locale, $key)) {
        return $this->translation[$locale][$key];
    }

    return array();
}

/**
 * Set the transtion for required locale
 *
 * @param string $locale
 *            Language code
 * @param string $trans
 *            translations array
 */
public function setTranslation($locale, $trans = array())
{
    $this->translation[$locale] = $trans;
}

/**
 * Remove transltions for required locale
 *
 * @param string $locale            
 */
public function removeTranslation($locale = null)
{
    if (null === $locale) {
        unset($this->translation);
    } else {
        unset($this->translation[$locale]);
    }
}

/**
 * Initialize locale
 *
 * @param string $locale            
 */
public function init($locale = null, $default_locale = null)
{
    // check if previously set locale exist or not
    $this->init_locale();
    if ($this->locale != null) {
        return;
    }

    if ($locale == null || (! preg_match('#^[a-z]+_[a-zA-Z_]+$#', $locale) && ! preg_match('#^[a-z]+_[a-zA-Z]+_[a-zA-Z_]+$#', $locale))) {
        $this->detectLocale();
    } else {
        $this->locale = $locale;
    }

    $this->init_locale();
}

/**
 * Attempt to autodetect locale
 *
 * @return void
 */
private function detectLocale()
{
    $locale = false;

    // GeoIP
    if (function_exists('geoip_country_code_by_name') && isset($_SERVER['REMOTE_ADDR'])) {

        $country = geoip_country_code_by_name($_SERVER['REMOTE_ADDR']);

        if ($country) {

            $locale = isset($this->country_to_locale[$country]) ? $this->country_to_locale[$country] : false;
        }
    }

    // Try detecting locale from browser headers
    if (! $locale) {

        if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {

            $languages = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);

            foreach ($languages as $lang) {

                $lang = str_replace('-', '_', trim($lang));

                if (strpos($lang, '_') === false) {

                    if (isset($this->country_to_locale[strtoupper($lang)])) {

                        $locale = $this->country_to_locale[strtoupper($lang)];
                    }
                } else {

                    $lang = explode('_', $lang);

                    if (count($lang) == 3) {
                        // language_Encoding_COUNTRY
                        $this->locale = strtolower($lang[0]) . ucfirst($lang[1]) . strtoupper($lang[2]);
                    } else {
                        // language_COUNTRY
                        $this->locale = strtolower($lang[0]) . strtoupper($lang[1]);
                    }

                    return;
                }
            }
        }
    }

    // Resort to default locale specified in config file
    if (! $locale) {
        $this->locale = $this->default_locale;
    }
}

/**
 * Check if config for selected locale exists
 *
 * @return void
 */
private function init_locale()
{
    if (! file_exists(sprintf('%s/%s.php', $this->locale_dir, $this->locale))) {
        $this->locale = $this->default_locale;
    }
}

/**
 * Load a Transtion into array
 *
 * @return void
 */
private function loadTranslation($locale = null, $force = false)
{
    if ($locale == null)
        $locale = $this->locale;

    if (! $this->hasTranslation($locale)) {
        $this->setTranslation($locale, include (sprintf('%s/%s.php', $this->locale_dir, $locale)));
    }
}

/**
 * Translate a key
 *
 * @param
 *            string Key to be translated
 * @param
 *            string optional arguments
 * @return string
 */
public function translate($key)
{
    $this->init();
    $this->loadTranslation($this->locale);

    if (! $this->hasTranslation($this->locale, $key)) {

        if ($this->locale !== $this->default_locale) {

            $this->loadTranslation($this->default_locale);

            if ($this->hasTranslation($this->default_locale, $key)) {

                $translation = $this->getTranslation($this->default_locale, $key);
            } else {
                // return key as it is or log error here
                return $key;
            }
        } else {
            return $key;
        }
    } else {
        $translation = $this->getTranslation($this->locale, $key);
    }
    // Replace arguments
    if (false !== strpos($translation, '{a:')) {
        $replace = array();
        $args = func_get_args();
        for ($i = 1, $max = count($args); $i < $max; $i ++) {
            $replace['{a:' . $i . '}'] = $args[$i];
        }
        // interpolate replacement values into the messsage then return
        return strtr($translation, $replace);
    }

    return $translation;
  }
}

用法

 <?php
    ## /locale/en.php

    return array(
       'name' => 'Hello {a:1}'
       'name_full' => 'Hello {a:1} {a:2}'
   );

$locale = new Locale(__DIR__ . '/locale');
$locale->setLocale('en');// load en.php from locale dir
//want to work with auto detection comment $locale->setLocale('en');

echo $locale->translate('name', 'Foo');
echo $locale->translate('name', 'Foo', 'Bar');

工作原理

传递给方法{a:1}的第一个参数替换

Locale::translate('key_name','arg1') 传递给方法{a:2}

的第二个参数替换Locale::translate('key_name','arg1','arg2')

如何检测

  • 默认情况下,如果安装了geoip,则会按geoip_country_code_by_name返回国家/地区代码,如果未安装geoip,则回退到HTTP_ACCEPT_LANGUAGE标题

答案 4 :(得分:5)

只是一个小答案: 绝对使用带有语言标识符的翻译网址:http://www.domain.com/nl/over-ons
Hybride解决方案往往变得复杂,所以我会坚持下去。为什么?导致网址对SEO至关重要。

关于数据库翻译:语言数量是或多或少是固定的?或者说不可预测和动态?如果它已修复,我只会添加新列,否则使用多个表。

但一般来说,为什么不使用Drupal?我知道每个人都想建立自己的CMS,因为它更快,更精简等等。但这只是一个坏主意!

答案 5 :(得分:4)

在开始使用Symfony框架之前,我之前有过相同的探测。

  1. 只需使用函数__(),它具有参数pageId(或objectId,#2中描述的objectTable),目标语言和后备(默认)语言的可选参数。可以在某些全局配置中设置默认语言,以便以后更容易地进行更改。

  2. 为了在数据库中存储我使用以下结构的内容:( pageId,语言,内容,变量)。

    • pageId将是您要翻译的页面的FK。如果你有其他对象,比如新闻,画廊或其他什么,只需将它分成2个字段objectId,objectTable。

    • 语言 - 显然它会存储ISO语言字符串EN_en,LT_lt,EN_us等。

    • content - 要与变量替换的通配符一起翻译的文本。示例“Hello mr。%% name %%。您的帐户余额为%% balance %%。”

    • 变量 - json编码变量。 PHP提供了快速解析这些功能的函数。例如“姓名:Laurynas,余额:15.23”。

    • 你提到了slug字段。你可以自由地将它添加到这个表中,只是为了快速搜索它。

  3. 通过缓存翻译,您的数据库调用必须减少到最少。它必须存储在PHP数组中,因为它是PHP语言中最快的结构。你将如何进行这种缓存取决于你。根据我的经验,您应该为每种语言提供一个文件夹,并为每个pageId提供一个数组。更新翻译后应重建缓存。只应重新生成更改的数组。

  4. 我想我在#2

  5. 中回答了这个问题
  6. 你的想法完全符合逻辑。这个很简单,我认为不会让你有任何问题。

  7. 应使用转换表中存储的slugs翻译URL。

    最后的话

    研究最佳实践总是好的,但不要重新发明轮子。只需使用和使用众所周知的框架中的组件并使用它们。

    看看Symfony translation component。它可能是一个很好的代码库。

答案 6 :(得分:4)

我不会试图改进已经给出的答案。相反,我会告诉你我自己的OOP PHP框架处理翻译的方式。

在内部,我的框架使用en,fr,es,cn等代码。数组包含网站支持的语言:array('en','fr','es','cn') 语言代码通过$ _GET(lang = fr)传递,如果未传递或无效,则将其设置为数组中的第一种语言。因此,在程序执行期间的任何时候,从一开始就知道当前语言。

了解需要在典型应用程序中翻译的内容类型非常有用:

1)来自类(或程序代码)的错误消息 2)来自类(或程序代码)的非错误消息 3)页面内容(通常存储在数据库中) 4)站点范围的字符串(如网站名称) 5)特定于脚本的字符串

第一种类型很容易理解。基本上,我们正在谈论诸如“无法连接到数据库......”之类的消息。发生错误时,只需加载这些消息。我的经理类接收来自其他类的调用,并使用作为参数传递的信息简单地转到相关的类文件夹并检索错误文件。

第二种类型的错误消息更像是表单验证错误时获得的消息。 (“你不能留下......空白”或“请选择一个超过5个字符的密码”)。在课程运行之前需要加载字符串。我知道什么是

对于实际的页面内容,我使用每种语言一个表,每个表都以该语言的代码为前缀。所以en_content是包含英语内容的表,es_content用于西班牙,cn_content用于中国,fr_content是法语的东西。

第四种字符串与您的网站相关。这是通过使用该语言代码命名的配置文件加载的,即en_lang.php,es_lang.php等。在全局语言文件中,您需要在英语全局文件和数组中加载翻译的语言,如数组('英语','中文','西班牙语','法语')('Anglais','Chinois',' Espagnol','Francais')在法国文件中。因此,当您填写语言选择的下拉列表时,它使用正确的语言;)

最后,您拥有特定于脚本的字符串。因此,如果你写一个烹饪应用程序,它可能是“你的烤箱不够热”。

在我的应用程序周期中,首先加载全局语言文件。在那里,您不仅可以找到全局字符串(如“Jack的网站”),还可以找到某些类的设置。基本上任何语言或文化依赖的东西。其中的一些字符串包括日期掩码(MMDDYYYY或DDMMYYYY)或ISO语言代码。在主语言文件中,我包含了各个类的字符串,因为它们很少。

从磁盘读取的第二个和最后一个语言文件是脚本语言文件。 lang_en_home_welcome.php是主页/欢迎脚本的语言文件。脚本由模式(主页)和操作(欢迎)定义。每个脚本都有自己的包含config和lang文件的文件夹。

该脚本从数据库中提取命名内容表的内容,如上所述。

如果出现问题,经理知道从何处获取与语言相关的错误文件。该文件仅在出错时加载。

所以结论很明显。在开始开发应用程序或框架之前,请考虑翻译问题。您还需要一个包含翻译的开发工作流程。使用我的框架,我用英语开发整个网站,然后翻译所有相关文件。

关于翻译字符串实现方式的最后一句话。我的框架有一个全局的$ manager,它运行可用于任何其他服务的服务。因此,例如表单服务获取html服务并使用它来编写html。我系统上的一项服务是翻译服务。 $ translator-&gt; set($ service,$ code,$ string)为当前语言设置字符串。语言文件是此类语句的列表。 $ translator-&gt; get($ service,$ code)检索翻译字符串。 $ code可以是1之类的数字,也可以是'no_connection'之类的字符串。服务之间不存在冲突,因为每个服务在转换器的数据区域中都有自己的命名空间。

我在这里张贴这篇文章,希望它能像挽救我几年前那样重新发明轮子的任务。

答案 7 :(得分:-1)

数据库工作:

创建语言表'语言':

字段:

language_id(主要和自动递增)

语言名称

created_at

CREATED_BY

的updated_at

updated_by

在数据库'content'中创建一个表:

字段:

content_id(主要和自动递增)

main_content

header_content

footer_content

leftsidebar_content

rightsidebar_content

language_id(外键:引用语言表)

created_at

CREATED_BY

的updated_at

updated_by

前端工作:

当用户从下拉列表或任何区域选择任何语言时,请在会话中保存所选语言ID,如

$ _ SESSION [ '语言'] = 1;

现在根据存储在会话中的语言ID从数据库表'content'中获取数据。

可在此处找到详细信息http://skillrow.com/multilingual-website-in-php-2/

答案 8 :(得分:-2)

作为一个居住在魁北克的人,几乎所有的网站都是法语和英语...我已经为WP尝试了很多(如果不是大多数)多语言插件...这是一个唯一有用的解决方案,可以与我的所有网站一起工作是mQtranslate ......我生活和死亡!

https://wordpress.org/plugins/mqtranslate/

答案 9 :(得分:-3)

WORDPRESS + MULTI-LANGUAGE SITE BASIS (插件)怎么样? 该网站将具有结构:

  • example.com/ eng / category1 / ....
  • example.com/ /我的页面....
  • example.com/ rus / category1 / ....
  • example.com/ RUS /我的页面....

该插件提供了翻译所有短语的界面,简单的逻辑:

(ENG) my_title - "Hello user"
(SPA) my_title - "Holla usuario"
然后它可以被输出:
echo translate('my_title', LNG); // LNG is auto-detected

P.S。但请检查插件是否仍处于活动状态。

答案 10 :(得分:-5)

一个非常简单的选项适用于您可以上传Javascript的任何网站www.multilingualizer.com

它允许您将所有语言的所有文本放在一个页面上,然后隐藏用户不需要查看的语言。效果很好。