最佳实践多语言网站

Jos*_*ndo 166 php mysql multilingual localization internationalization

我已经在这个问题上苦苦挣扎了好几个月,但我还没有遇到过需要探索所有可能的选择的情况.现在,我觉得是时候了解可能性并创建我自己的个人偏好,以便在我即将开展的项目中使用.

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

我即将升级/重新开发我已经使用了很长一段时间的内容管理系统.但是,我觉得多语言是对这个系统的一个很大的改进.在我没有使用任何框架之前,我将使用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.数据库表将使用字段将这些字段设置为long value.在模板内部,我们可以使用类似的排序方法echo __('Controller.View.welcome', array('name', 'Joshua')),参数包含Welcome, :name.因此结果是Welcome, Joshua.这似乎是一个很好的方法,因为参数如:name很容易被编辑器理解.
  3. 低数据库负载:当然,如果在运行中加载这些字符串,上述系统将导致数据库负载的负载.因此,我需要一个缓存系统,一旦在管理环境中编辑/保存语言文件,就会重新呈现语言文件.由于生成了文件,因此还需要良好的文件系统布局.我想我们可以使用languages/en_EN/Controller/View.php或.ini,最适合你的.也许.ini甚至可以在最后解析得更快.这个应该包含的数据format parameter=value; .我想这是执行此操作的最佳方式,因为呈现的每个View都可以包含它自己的语言文件(如果存在).然后,语言参数应加载到特定视图而不是全局范围,以防止参数相互覆盖.
  4. 数据库表翻译:这实际上是我最担心的事情.我正在寻找一种方法来创建新闻/页面/等的翻译.尽快.每个模块有两个表(例如NewsNews_translations)是一个选项,但是为了获得一个好的系统感觉很多.我想出的一件事是基于data versioning我写的一个系统:有一个数据库表名Translations,这个表有一个独特的组合language,tablenameprimarykey.例如:en_En/News/1(参考ID = 1的新闻项目的英文版本).但是这个方法存在两个巨大的缺点:首先,这个表在数据库中有大量数据需要很长时间,其次使用这个设置搜索表是一件很麻烦的工作.例如,搜索项目的SEO slug将是一个全文搜索,这是相当愚蠢的.但另一方面:它是一种快速的方式,可以非常快速地在每个表格中创建可翻译内容,但我不相信这个专业人士会超过这个内容.
  5. 前端工作:前端也需要一些思考.当然,我们会将可用语言存储在数据库中,并(de)激活我们需要的语言.通过这种方式,脚本可以生成下拉列表以选择语言,后端可以自动决定使用CMS可以进行哪些翻译.然后,在获取视图的语言文件或获取网站上内容项的正确翻译时,将使用所选语言(例如en_EN).

所以,他们就是.我的想法到目前为止.他们甚至不包括日期等的本地化选项,但是因为我的服务器支持PHP5.3.2 +,最好的选择是使用intl扩展,如下所述:http://devzone.zend.com/1500/internationalization-in -php-53 / - 但这将在任何后期的开发体育场中使用.目前,主要问题是如何在网站上获得最佳的内容翻译实践.

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

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

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

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

第三种选择是两种选择的组合:http://www.domain.com/about-us对主要语言使用"language-identification-less"-URL().并使用带有翻译的SEO slug的URL用于子语言:http://www.domain.com/nl/over-ons&http://www.domain.com/de/uber-uns

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

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

// Edit #1:

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

ter*_*ško 104

主题的前提

多语言网站有三个不同的方面:

  • 界面翻译
  • 内容
  • 网址路由

虽然它们都以不同的方式互连,但从CMS的角度来看,它们使用不同的UI元素进行管理并以不同方式存储.您似乎对实施和理解前两个有信心.问题是关于后一方面 - "URL翻译?我们应该这样做还是不做?以什么方式?"

URL可以由什么组成?

一个非常重要的事情是,不要喜欢IDN.而是赞成音译(也就是:转录和罗马化).虽然乍一看IDN似乎是国际URL的可行选择,但它实际上并不像宣传那样有两个原因:

  • 有些浏览器会打开非ASCII字符状'?''ž''%D1%87''%C5%BE'
  • 如果用户具有自定义主题,则主题的字体很可能没有这些字母的符号

几年前,我在基于Yii的项目(可怕的框架,恕我直言)中尝试过IDN方法.在刮取该解决方案之前,我遇到了上述两个问题.此外,我怀疑它可能是一个攻击媒介.

可用选项......正如我所见.

基本上你有两个选择,可以抽象为:

  • http://site.tld/[:query]:where [:query]决定语言和内容的选择

  • http://site.tld/[:language]/[:query]:[:language]URL的一部分定义语言的选择,[:query]仅用于标识内容

查询是Α和Ω..

让我们说你选择http://site.tld/[:query].

在这种情况下,您有一个主要的语言来源:[:query]段的内容; 还有两个来源:

  • $_COOKIE['lang']该特定浏览器的值
  • HTTP Accept-Language (1),(2)标题中的语言列表

首先,您需要将查询与已定义的路由模式之一匹配(如果您的选择是Laravel,则在此处阅读).在成功匹配模式后,您需要找到该语言.

你必须经历模式的所有部分.查找所有这些细分的潜在翻译并确定使用的语言.当它们(而不是"if")出现时,将使用另外两个源(cookie和标头)来解决路由冲突.

举个例子:http://site.tld/blog/novinka.

这是音译"????, ???????",用英语表示大约"blog", "latest".

正如您已经注意到的那样,俄语中的"блог"将被音译为"博客".这意味着对于[:query]你的第一部分(在最好的情况下)将最终['en', 'ru']得到可能的语言列表.然后你采取下一部分 - "novinka".在可能性列表中可能只有一种语言:['ru'].

当列表中有一个项目时,您已成功找到该语言.

但是,如果你最终得到2(例如:俄罗斯和乌克兰)或更多的可能性..或0种可能性,视情况而定.您将不得不使用cookie和/或标头来查找正确的选项.

如果一切都失败了,你选择网站的默认语言.

语言作为参数

另一种方法是使用URL,可以定义为http://site.tld/[:language]/[:query].在这种情况下,在翻译查询时,您不需要猜测语言,因为此时您已经知道要使用哪种语言.

还有一个第二语言来源:cookie值.但是这里没有必要弄乱Accept-Language标题,因为在"冷启动"的情况下(当用户第一次使用自定义查询打开网站时),您没有处理未知数量的可能语言.

相反,您有3个简单的优先选项:

  1. 如果[:language]设置了段,请使用它
  2. 如果$_COOKIE['lang']设置,请使用它
  3. 使用默认语言

如果您使用该语言,则只需尝试翻译查询,如果翻译失败,请使用该特定段的"默认值"(基于路由结果).

这不是第三种选择吗?

是的,理论上你可以结合这两种方法,但这过程复杂且只能容纳谁想要手动更改URL的人http://site.tld/en/newshttp://site.tld/de/news,并希望新闻页面更改为德语.

但即使是这种情况也可能使用cookie值(其中包含有关以前语言选择的信息)来减轻,以较少的魔力和希望实现.

使用哪种方法?

正如您可能已经猜到的那样,我建议http://site.tld/[:language]/[:query]作为更明智的选择.

同样在实际情况下,您将在URL中拥有第三个主要部分:"title".如在网上商店的产品名称或新闻网站的文章标题.

例: http://site.tld/en/news/article/121415/EU-as-global-reserve-currency

在这种情况下,'/news/article/121415'将是查询,并且'EU-as-global-reserve-currency'是标题.纯粹用于SEO目的.

可以在Laravel完成吗?

有点,但不是默认.

我不太熟悉它,但从我所看到的,Laravel使用简单的基于模式的路由机制.要实现多语言URL,您可能必须扩展核心类,因为多语言路由需要访问不同形式的存储(数据库,缓存和/或配置文件).

它被击溃了.现在怎么办?

因此,您最终会得到两条有价值的信息:当前语言和已翻译的查询片段.然后可以使用这些值分派到将产生结果的类.

基本上,以下URL :( http://site.tld/ru/blog/novinka或没有的版本'/ru')变成了类似的东西

$parameters = [
   'language' => 'ru',
   'classname' => 'blog',
   'method' => 'latest',
];
Run Code Online (Sandbox Code Playgroud)

您刚才用于调度:

$instance = new {$parameter['classname']};
$instance->{'get'.$parameters['method']}( $parameters );
Run Code Online (Sandbox Code Playgroud)

..或其中的一些变体,取决于具体实施方式.

  • 感谢您的另一个见解!很周到!我也在考虑在 URL 中包含语言参数。这似乎是识别特定语言的最佳方式,不仅对于用户,而且对于 SEO 目的也是如此。如果用户将 /en/news 更改为 /de/news,我的想法是将 301(永久)重定向到 /de/nachrichten 例如。只是为了确保每种语言的每页只有一个唯一的 URL(再次用于 SEO 目的) (2认同)

Gli*_*ire 50

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

在工作中,我们最近在我们的几个属性上实现了i18n,我们一直在努力解决的问题之一是处理即时翻译的性能问题,然后我发现了Thomas Bley写的这篇精彩博文这激发了我们使用i18n以最小的性能问题处理大流量负载的方式.

我们知道在PHP中使用占位符来定义我们的基本文件,然后使用预处理器来缓存这些文件(我们存储文件修改时间以确保我们正在服务),而不是为每个翻译操作调用函数最新的内容在任何时候).

翻译标签

Thomas使用{tr}{/tr}标签来定义翻译的开始和结束位置.由于我们使用TWIG这一事实,我们不想使用{以避免混淆,因此我们使用[%tr%][%/tr%]不是.基本上,这看起来像这样:

`return [%tr%]formatted_value[%/tr%];`
Run Code Online (Sandbox Code Playgroud)

请注意,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)
Run Code Online (Sandbox Code Playgroud)

允许用户在CMS内部修改这些内容,只需通过preg_spliton 获取密钥对\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;
}
Run Code Online (Sandbox Code Playgroud)

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

如何称呼它

再一次,这个例子来自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();
Run Code Online (Sandbox Code Playgroud)

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

为什么要用这种方法?

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

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

获取翻译的数据库内容

我们只是在我们的数据库中添加一个内容列language,然后我们使用一个访问器方法来处理LANG我们之前定义的常量,所以我们的SQL调用(遗憾地使用ZF1)如下所示:

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

我们的文章有一个复合主键id,language因此文章54可以存在于所有语言中.如果未指定,我们的LANG默认值en_US.

URL Slug翻译

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

"/wilkommen" => "/welcome/lang/de"
... etc ...
Run Code Online (Sandbox Code Playgroud)

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

关于其他一些选择的说明

基于PHP的On-The-Fly翻译

我看不出它们提供了超过预处理翻译的任何优势.

基于前端的翻译

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

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

数据库驱动的翻译

PHP的数据库连接速度无需写回家,这增加了在每个要翻译的短语上调用函数的高额开销.这种方法的性能和可扩展性问题似乎势不可挡.


Yar*_*lav 14

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

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

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

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

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

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

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

关于网址.谷歌间接建议使用翻译:

清楚地表明法语内容: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翻译界面

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

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

  • 没问题.事实上,这个答案对我来说也更加完整和有趣.但我希望你从我的回答中得到一些有用的东西. (2认同)

小智 12

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

假设你有这样的文字:

Welcome!
Run Code Online (Sandbox Code Playgroud)

您可以在具有翻译的数据库中输入此内容,但您也可以这样做:

$welcome = array(
"English"=>"Welcome!",
"German"=>"Willkommen!",
"French"=>"Bienvenue!",
"Turkish"=>"Ho?geldiniz!",
"Russian"=>"????? ??????????!",
"Dutch"=>"Welkom!",
"Swedish"=>"Välkommen!",
"Basque"=>"Ongietorri!",
"Spanish"=>"Bienvenito!"
"Welsh"=>"Croeso!");
Run Code Online (Sandbox Code Playgroud)

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

$_COOKIE['language'];
Run Code Online (Sandbox Code Playgroud)

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

$language=$_COOKIE['language'];
Run Code Online (Sandbox Code Playgroud)

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

echo $welcome[$language];
Run Code Online (Sandbox Code Playgroud)

结果将是:

Croeso!
Run Code Online (Sandbox Code Playgroud)

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


小智 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;
  }
}
Run Code Online (Sandbox Code Playgroud)

用法

 <?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');
Run Code Online (Sandbox Code Playgroud)

这个怎么运作

{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标头


Rem*_*emy 5

只是一个简单的答案:绝对使用在其前面带有语言标识符的翻译后的url:http : //www.domain.com/nl/over-ons
Hybride解决方案趋于复杂,因此我会坚持使用。为什么?因为网址对于SEO是必不可少的。

关于数据库翻译:语言的数量是否固定?还是不可预测且动态的?如果它是固定的,我将只添加新的列,否则添加多个表。

但是通常,为什么不使用Drupal?我知道每个人都希望构建自己的CMS,因为它速度更快,更精简,等等。但这确实是一个坏主意!

  • 我认为所有这些原因在大量文章中都有争议。希望您的客户不会完全选择您,因为您拥有其他人无法维护的专有CMS。但是无论如何,那是完全不同的讨论。 (7认同)
  • 感谢您的回答。我不想使用 Drupal/Joomla 的原因很简单:我想确保我了解我的系统的所有细节、可暂停的缺陷、代码是如何构建的(并且重要的是:不是由 300 名程序员一起构建) 。我有足够多的理由不选择开源。除此之外,我希望我的公司成为我客户的一个重要因素,如果他们可以去找其他开发商而让我一无所有,这是一件坏事。 (2认同)

JG *_*iot 5

我不会试图完善已经给出的答案。相反,我将告诉您我自己的 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 等。在全局语言文件中,您需要在英文全局文件中加载已翻译的语言,例如 array('English','Chinese', 'Spanish','French') 和 array('Anglais','Chinois', ' Espagnol', 'Francais') 在法语文件中。因此,当您为语言选择填充下拉菜单时,它使用的是正确的语言;)

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

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

从磁盘读取的第二个也是最后一个语言文件是脚本语言文件。lang_en_home_welcome.php 是 home/welcome 脚本的语言文件。脚本由模式(home)和动作(welcome)定义。每个脚本都有自己的文件夹,其中包含 config 和 lang 文件。

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

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

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

只是对翻译字符串的实现方式做一个简短的总结。我的框架有一个全局变量 $manager,它运行对任何其他服务可用的服务。例如,表单服务获取 html 服务并使用它来编写 html。我系统上的一项服务是翻译服务。$translator->set($service,$code,$string) 设置当前语言的字符串。语言文件是此类语句的列表。$translator->get($service,$code) 检索翻译字符串。$code 可以是像 1 这样的数字或像“no_connection”这样的字符串。服务之间不会发生冲突,因为每个服务在翻译器的数据区中都有自己的命名空间。

我把这个贴在这里是希望它能像我几年前那样免去重新发明轮子的任务。