最佳实践多语言网站

我已经在这个问题上挣扎了好几个月了,但是我还没有遇到过需要去探索所有可能的select的情况。 现在,我觉得是时候了解可能性,并创build我自己的个人喜好,在我即将到来的项目中使用。

首先让我来描述一下我正在寻找的情况

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

应该翻译什么

由于我所寻找的系统需要尽可能地方便用户,pipe理翻译的方法应该在CMS内部。 应该没有必要启动一个FTP连接来修改翻译文件或任何HTML / PHP分析模板。

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

我是怎么想出来的

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

  1. PHPparsing模板 :模板系统应该由PHPparsing。 这样我就可以将翻译的参数插入到HTML中,而不必打开模板并修改它们。 除此之外,PHPparsing的模板使我能够为整个网站提供1个模板,而不是每个语言的子文件夹(我以前有过)。 达到此目标的方法可以是Smarty,TemplatePower,Laravel's Blade或任何其他模板parsing器。 正如我所说,这应该是独立的书面解决scheme。
  2. 数据库驱动 :也许我不需要再提这个。 但解决scheme应该是数据库驱动的。 CMS的目标是面向对象和MVC,所以我需要考虑一个逻辑数据结构的string。 由于我的模板将被构造:模板/控制器/ View.php也许这个结构将最有意义: Controller.View.parameter 。 数据库表将具有这些字段与value字段长。 在模板内部,我们可以使用一些sorting方法,如echo __('Controller.View.welcome', array('name', 'Joshua')) ,参数包含Welcome, :name 。 因此,结果是Welcome, Joshua 。 这似乎是一个很好的方法,因为编辑器很容易理解名称这样的参数。
  3. 低数据库负载 :当然,如果这些string正在被加载,上面的系统会导致数据库负载的加载。 因此,我需要一个caching系统,在编辑/保存在pipe理环境中后立即重新渲染语言文件。 因为生成文件,所以还需要一个好的文件系统布局。 我想我们可以用languages/en_EN/Controller/View.php或.ini,无论你最适合。 也许.ini甚至更快地parsing。 这个模块应该包含format parameter=value;的数据format parameter=value; 。 我猜这是这样做的最好方法,因为每个呈现的视图都可以包含它自己的语言文件(如果存在的话)。 那么语言参数应该加载到一个特定的视图,而不是全局范围内,以防止参数互相覆盖。
  4. 数据库表翻译 :这实际上是我最担心的事情。 我正在寻找一种方式来创build新闻/网页/等翻译。 尽快。 每个模块有两个表格(例如NewsNews_translations )是一个选项,但是为了获得一个好的系统感觉News_translations很多工作。 我想到的一件事是基于我写的一个data versioningpipe理系统:有一个数据库表名Translations ,这个表具有languagetablename和主键的独特组合。 例如:en_En / News / 1(参考ID = 1的新闻项目英文版)。 但是这个方法有两个很大的缺点:首先,这个表格往往会在数据库中有很多数据的情况下变得很长,其次是使用这个设置来search表格。 例如search该项目的SEO slu would将是一个全文search,这是非常愚蠢的。 但另一方面:这是一种快速创build每个表格中可翻译内容的快速方法,但我不认为这个专业人员胜过了这个问题的答案。
  5. 前端工作 :前端也需要一些思考。 当然,我们将可用的语言存储在数据库中,然后(de)激活我们需要的语言。 这样脚本可以生成一个下拉菜单来select一种语言,后端可以自动决定使用CMS进行哪些翻译。 所选语言(例如en_EN)将在获取视图的语言文件或获取网站上的内容项目的正确翻译时使用。

所以,他们在那里。 我的想法到目前为止。 他们甚至不包括date等本地化选项,但作为我的服务器支持PHP5.3.2 +最好的select是使用intl扩展,如下所述: http ://devzone.zend.com/1500/internationalization-in -php-53 / – 但是这将在任何后来的发展体育场都有用。 目前主要的问题是如何有最好的网站内容翻译实践。

除了我在这里解释的一切之外,还有一件我还没有确定的东西,看起来像一个简单的问题,但实际上它让我头疼:

url翻译? 我们应该做还是不做? 以什么方式?

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

另一种select是使用http://www.domain.com/nl/about-us 。 这会为每个内容至less生成一个唯一的URL。 而且这样会更容易转到另一种语言,例如http://www.domain.com/en/about-us ,而Google和人类访问者提供的URL更易于理解。 使用这个选项,我们用默认语言做什么? 默认语言是否应该删除默认select的语言? 所以redirecthttp://www.domain.com/en/about-ushttp://www.domain.com/about-us …在我看来,这是最好的解决scheme,因为当CMS设置为只有一种语言不需要在URL中具有该语言标识。

第三个选项是两种select的组合:对主要语言使用“language-identification-less”-URL( http://www.domain.com/about-us )。 并使用翻译的SEO slug为子语言的http://www.domain.com/nl/over-onshttp://www.domain.com/de/uber-uns : http://www.domain.com/nl/over-ons : http://www.domain.com/de/uber-uns

我希望我的问题让你的头脑开裂,他们肯定会破坏我的! 这确实帮助我在这里解决问题。 给我一个可能性,回顾我以前使用的方法,以及我对即将到来的CMS的想法。

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

// Edit #1

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

主题的前提

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

  • 界面翻译
  • 内容
  • url路由

尽pipe它们都以不同的方式相互关联,但从CMS的angular度来看,它们是使用不同的UI元素进行pipe理的,而且存储的方式也不同 你似乎对自己对前两项的执行和理解充满信心。 问题是关于后一个方面 – “URL翻译?我们应该做还是不做?以什么方式?”

什么URL可以被做?

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

  • 一些浏览器会将'ч''ž'这样的非ASCII字符变成'%D1%87''%C5%BE'
  • 如果用户有自定义的主题,主题的字体很可能没有这些字母的符号

几年前,我在一个基于Yii的项目(恐怖的框架,恕我直言)中尝试了IDN方法。 在解决这个问题之前,我遇到了上述两个问题。 另外,我怀疑它可能是一个攻击媒介。

可用的选项…正如我所见。

基本上你有两个select,可以被抽象为:

  • http://site.tld/[:query] :其中[:query]确定语言和内容select

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

查询是Α和Ω..

比方说你selecthttp://site.tld/[:query]

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

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

首先,您需要将查询匹配到定义的路由模式之一(如果您的select是Laravel, 请阅读此处 )。 模式的成功匹配,然后你需要find语言。

你将不得不经过模式的所有部分。 find所有这些细分的潜在翻译,并确定使用了哪种语言。 当(不是“如果”)出现时,另外两个源(cookie和header)将用于解决路由冲突。

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

这是"блог, новинка"的音译,英文意思是近似"blog", "latest"

正如你们已经注意到的那样,在俄文中,“блог”将被译为“博客”。 这意味着对于[:query]的第一部分,您(在最好的情况下 )会以['en', 'ru']列出可能的语言。 然后你把下一部分 – “novinka”。 那可能只有一种语言: ['ru']

当列表中有一个项目时,您已成功find该语言。

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

如果一切都失败了,你select网站的默认语言。

语言作为参数

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

还有一个第二语言来源:cookie值。 但是在这里没有任何关于Accept-Language头的问题,因为在“冷启动”的情况下(当用户第一次用自定义查询打开网站的时候)你没有处理大量的可能的语言。

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

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

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

这不是第三种select吗?

是的,从技术上讲,您可以结合使用这两种方法,但是这会使stream程复杂化,只适应那些想要手动将http://site.tld/en/news URL更改为http://site.tld/de/news期望新闻页面改为德文。

但即使这种情况下,可能会减less使用cookie值(这将包含有关语言先前的select信息),以较less的魔术和希望实施。

使用哪种方法?

正如你可能已经猜到的那样,我会推荐http://site.tld/[:language]/[:query]作为更明智的select。

同样在实际的情况下,你会有第三个主要部分的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,您可能必须扩展核心类 ,因为多语言路由需要访问不同forms的存储(数据库,caching和/或configuration文件)。

它被路由。 现在怎么办?

作为所有的结果,你会得到两个有价值的信息:当前的语言和翻译段的查询。 这些值然后可以用来分派给将产生结果的类。

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

 $parameters = [ 'language' => 'ru', 'classname' => 'blog', 'method' => 'latest', ]; 

你只是用来调度:

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

或者它的一些变化,取决于具体的实现。

如Thomas Bley所build议的那样,使用预处理器来实现i18n而无需执行性能命中

在工作上,我们最近在我们的一些物业上实施了国际化,我们一直在努力的一件事情是处理空中翻译的performance,然后我发现了这篇博客文章Thomas Bley这激发了我们使用i18n来处理大stream量负载时性能问题最less的方式。

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

翻译标签

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

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

请注意,托马斯build议在文件中使用基础英语。 我们不这样做,因为如果我们改变英文的值,我们不想修改所有的翻译文件。

INI文件

然后,我们为每种语言创build一个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内部修改这些内容是很简单的,只需通过\n=上的preg_split获取密钥对,并使CMS能够写入INI文件即可。

预处理器组件

本质上,托马斯build议使用即时“编译器”(但实际上,这是一个预处理器)这样的function,以获取您的翻译文件,并在磁盘上创build静态PHP文件。 这样,我们基本上caching了我们翻译的文件,而不是为文件中的每个string调用翻译函数:

 // 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; } 

注:我没有validation正则expression式的工作原理,我没有从我们公司的服务器上复制它,但是你可以看到这个操作是如何工作的。

如何调用它

再次,这个例子来自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,则会话variables),然后在每个请求中检索它。 你可以将它与一个可选的$_GET参数结合来覆盖语言,但是我不build议每个语言的子域名或每页语言,因为它会使得很难看到哪些页面很受欢迎,并且会降低这个值入境链接,因为你会让他们更难以传播。

为什么使用这种方法?

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

  1. 从很less改变的内容中调用一大堆函数来获得巨大的性能收益(用这个系统,法语版的10万用户仍然只能运行一次翻译replace)。
  2. 它不会为我们的数据库增加任何负载,因为它使用简单的平面文件,是一个纯粹的PHP解决scheme。
  3. 在我们的翻译中使用PHPexpression式的能力。

获取翻译的数据库内容

我们只是在数据库中添加一个名为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

我在这里结合了两件事,一个是引导程序中的一个函数,它接受$_GET参数来覆盖cookievariables,另一个是接受多个slug的路由。 那么你可以在你的路由中做这样的事情:

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

这些可以存储在一个平面文件,可以很容易地从您的pipe理面板写入。 JSON或XML可以提供一个良好的结构来支持它们。

有关其他选项的说明

基于PHP的即时翻译

我看不出这些与预处理的翻译相比有什么优势。

基于前端的翻译

我早就发现这些有趣的事情,但是有一些注意事项。 例如,您必须向用户提供您计划翻译的网站上的整个短语列表,如果您隐藏或禁止访问该网站的某些区域,这可能会有问题。

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

数据库驱动的翻译

PHP的数据库连接速度没有什么值得一提的,而这又增加了调用每个短语翻译函数的开销。 这种方法的性能和可扩展性问题似乎势不可挡。

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

使用gettext,您将拥有一个function强大的工具,其中许多情况已经像复数forms的数字一样实现了。 在英语中,你只有两种select:单数和复数。 但在俄罗斯,例如有三种forms,并不像英文那样简单。

还有许多翻译者已经有了使用gettext的经验。

看看CakePHP或Drupal 。 启用多语言。 CakePHP作为界面本地化的例子,Drupal作为内容翻译的例子。

对于l10n来说,使用数据库根本就不是这种情况。 这将是吨查询。 标准的方法是在早期阶段(或者如果你喜欢延迟加载,首先调用i10n函数的时候)获得内存中的所有l10n数据。 它可以从.po文件读取或从DB读取所有数据。 而不仅仅是从数组中读取请求的string。

如果您需要实现在线工具来翻译界面,您可以将所有数据保存在数据库中,但仍然可以将所有数据保存到文件以使用它。 为了减less内存中的数据量,您可以将所有已翻译的消息/string拆分为组,并且只加载所需的组,如果可能的话。

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

关于url。 谷歌间接build议使用翻译:

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

另外我想你需要redirect用户默认的语言前缀,例如http://examlpe.com/about-us将redirect到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

翻译内容是比较困难的任务。 我认为这将是不同types的内容,如文章,菜单项等的一些差异。但在#4你是在正确的方式。 看看Drupal有更多的想法。 它有足够清晰的数据库模式和足够好的翻译界面。 就像你创build文章并select它的语言一样。 而且,你以后可以把它翻译成其他语言。

Drupal翻译界面

我认为这不是与URL slugs的问题。 你可以为slu create创build单独的表,这将是正确的决定。 即使使用大量数据,也使用正确的索引来查询表是不成问题的。 而不是全文search,但string匹配,如果将使用varchar数据types的slu and,你也可以在该字段上的索引。

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

这取决于你的网站有多less内容。 起初,我在这里使用了一个和所有其他人一样的数据库,但是编写一个数据库的所有工作可能非常耗时。 我并不是说这是一个理想的方法,特别是如果你有很多文本,但是如果你想快速的做,而不使用数据库,这个方法可以工作,但是你不能允许用户input数据这将被用作翻译文件。 但是,如果你自己添加翻译,它将起作用:

假设你有这样的文字:

 Welcome! 

你可以input这个数据库与翻译,但你也可以这样做:

 $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! 

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

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

我以前面对过类似的问题,写了下课后解决了我的问题

对象: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('#^[az]+_[a-zA-Z_]+$#', $locale) && ! preg_match('#^[az]+_[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} is replaced by 1st argument passed to method Locale::translate('key_name','arg1') {a:2} is replaced by 2nd argument passed to method Locale::translate('key_name','arg1','arg2')

How detection works

  • By default if geoip is installed then it will return country code by geoip_country_code_by_name and if geoip is not installed the fallback to HTTP_ACCEPT_LANGUAGE header

Just a sub answer: Absolutely use translated urls with a language identifier in front of them: http://www.domain.com/nl/over-ons
Hybride solutions tend to get complicated, so I would just stick with it. 为什么? Cause the url is essential for SEO.

About the db translation: Is the number of languages more or less fixed? Or rather unpredictable and dynamic? If it is fixed, I would just add new columns, otherwise go with multiple tables.

But generally, why not use Drupal? I know everybody wants to build their own CMS cause it's faster, leaner, etc. etc. But that is just really a bad idea!

I had the same probem a while ago, before starting using Symfony framework.

  1. Just use a function __() which has arameters pageId (or objectId, objectTable described in #2), target language and an optional parameter of fallback (default) language. The default language could be set in some global config in order to have an easier way to change it later.

  2. For storing the content in database i used following structure: (pageId, language, content, variables).

    • pageId would be a FK to your page you want to translate. if you have other objects, like news, galleries or whatever, just split it into 2 fields objectId, objectTable.

    • language – obviously it would store the ISO language string EN_en, LT_lt, EN_us etc.

    • content – the text you want to translate together with the wildcards for variable replacing. Example "Hello mr. %%name%%. Your account balance is %%balance%%."

    • variables – the json encoded variables. PHP provides functions to quickly parse these. Example "name: Laurynas, balance: 15.23".

    • you mentioned also slug field. you could freely add it to this table just to have a quick way to search for it.

  3. Your database calls must be reduced to minimum with caching the translations. It must be stored in PHP array, because it is the fastest structure in PHP language. How you will make this caching is up to you. From my experience you should have a folder for each language supported and an array for each pageId. The cache should be rebuilt after you update the translation. ONLY the changed array should be regenerated.

  4. i think i answered that in #2

  5. your idea is perfectly logical. this one is pretty simple and i think will not make you any problems.

URLs should be translated using the stored slugs in the translation table.

最后的话

it is always good to research the best practices, but do not reinvent the wheel. just take and use the components from well known frameworks and use them.

take a look at Symfony translation component . It could be a good code base for you.

I am not going to attempt to refine the answers already given. Instead I will tell you about the way my own OOP PHP framework handles translations.

Internally, my framework use codes like en, fr, es, cn and so on. An array holds the languages supported by the website: array('en','fr','es','cn') The language code is passed via $_GET (lang=fr) and if not passed or not valid, it is set to the first language in the array. So at any time during program execution and from the very beginning, the current language is known.

It is useful to understand the kind of content that needs to be translated in a typical application:

1) error messages from classes (or procedural code) 2) non-error messages from classes (or procedural code) 3) page content (usually store in a database) 4) site-wide strings (like website name) 5) script-specific strings

The first type is simple to understand. Basically, we are talking about messages like "could not connect to the database …". These messages only need to be loaded when an error occurs. My manager class receives a call from the other classes and using the information passed as parameters simply goes to relevant the class folder and retrieves the error file.

The second type of error message is more like the messages you get when the validation of a form went wrong. ("You cannot leave … blank" or "please choose a password with more than 5 characters"). The strings need to be loaded before the class runs.I know what is

For the actual page content, I use one table per language, each table prefixed by the code for the language. So en_content is the table with English language content, es_content is for spain, cn_content for China and fr_content is the French stuff.

The fourth kind of string is relevant throughout your website. This is loaded via a configuration file named using the code for the language, that is en_lang.php, es_lang.php and so on. In the global language file you will need to load the translated languages such as array('English','Chinese', 'Spanish','French') in the English global file and array('Anglais','Chinois', 'Espagnol', 'Francais') in the French file. So when you populate a dropdown for language selection, it is in the correct language 😉

Finally you have the script-specific strings. So if you write a cooking application, it might be "Your oven was not hot enough".

In my application cycle, the global language file is loaded first. In there you will find not just global strings (like "Jack's Website") but also settings for some of the classes. Basically anything that is language or culture-dependent. Some of the strings in there include masks for dates (MMDDYYYY or DDMMYYYY), or ISO Language Codes. In the main language file, I include strings for individual classes becaue there are so few of them.

The second and last language file that is read from disk is the script language file. lang_en_home_welcome.php is the language file for the home/welcome script. A script is defined by a mode (home) and an action (welcome). Each script has its own folder with config and lang files.

The script pulls the content from the database naming the content table as explained above.

If something goes wrong, the manager knows where to get the language-dependent error file. That file is only loaded in case of an error.

So the conclusion is obvious. Think about the translation issues before you start developing an application or framework. You also need a development workflow that incorporates translations. With my framework, I develop the whole site in English and then translate all the relevant files.

Just a quick final word on the way the translation strings are implemented. My framework has a single global, the $manager, which runs services available to any other service. So for example the form service gets hold of the html service and uses it to write the html. One of the services on my system is the translator service. $translator->set($service,$code,$string) sets a string for the current language. The language file is a list of such statements. $translator->get($service,$code) retrieves a translation string. The $code can be numeric like 1 or a string like 'no_connection'. There can be no clash between services because each has its own namespace in the translator's data area.

I post this here in the hope it will save somebody the task of reinventing the wheel like I had to do a few long years ago.

I've been asking myself related questions over and over again, then got lost in formal languages… but just to help you out a little I'd like to share some findings:

I recommend to give a look at advanced CMS

Typo3 for PHP (I know there is a lot of stuff but thats the one I think is most mature)

Plone in Python

If you find out that the web in 2013 should work different then, start from scratch. That would mean to put together a team of highly skilled/experienced people to build a new CMS. May be you'd like to give a look at polymer for that purpose.

If it comes to coding and multilingual websites / native language support, I think every programmer should have a clue about unicode. If you don't know unicode you'll most certainly mess up your data. Do not go with the thousands of ISO codes. They'll only save you some memory. But you can do literally everything with UTF-8 even store chinese chars. But for that you'd need to store either 2 or 4 byte chars that makes it basically a utf-16 or utf-32.

If it's about URL encoding, again there you shouldn't mix encodings and be aware that at least for the domainname there are rules defined by different lobbies that provide applications like a browser. eg a Domain could be very similar like:

ьankofamerica.com or bankofamerica.com samesamebutdifferent 😉

Of course you need the filesystem to work with all encodings. Another plus for unicode using utf-8 filesystem.

If its about translations, think about the structure of documents. eg a book or an article. You have the docbook specifications to understand about those structures. But in HTML its just about content blocks. So you'd like to have a translation on that level, also on webpage level or domain level. So if a block doesn't exist its just not there, if a webpage doesn't exist you'll get redirected to the upper navigation level. If a domain should be completely different in navigation structure, then.. its a complete different structure to manage. This can already be done with Typo3.

If its about frameworks, the most mature ones I know, to do the general stuff like MVC(buzzword I really hate it! Like "performance" If you want to sell something, use the word performance and featurerich and you sell… what the hell) is Zend . It has proven to be a good thing to bring standards to php chaos coders. But, typo3 also has a Framework besides the CMS. Recently it has been redeveloped and is called flow3 now. The frameworks of course cover database abstraction, templating and concepts for caching, but have individual strengths.

If its about caching… that can be awefully complicated / multilayered. In PHP you'll think about accellerator, opcode, but also html, httpd, mysql, xml, css, js … any kinds of caches. Of course some parts should be cached and dynamic parts like blog answers shouldn't. Some should be requested over AJAX with generated urls. JSON, hashbangs etc.

Then, you'd like to have any little component on your website to be accessed or managed only by certain users , so conceptually that plays a big role.

Also you'd like to make statistics , maybe have distributed system / a facebook of facebooks etc. any software to be built on top of your over the top cms … so you need different type of databases inmemory, bigdata, xml, whatsoever.

well, I think thats enough for now. If you haven't heard of either typo3 / plone or mentioned frameworks, you have enough to study. On that path you'll find a lot of solutions for questions you haven't asked yet.

If then you think, lets make a new CMS because its 2013 and php is about to die anyway, then you r welcome to join any other group of developers hopefully not getting lost.

祝你好运!

And btw. how about people will not having any websites anymore in the future? and we'll all be on google+? I hope developers become a little more creative and do something usefull(to not be assimilated by the borgle)

//// Edit /// Just a little thought for your existing application:

If you have a php mysql CMS and you wanted to embed multilang support. you could either use your table with an aditional column for any language or insert the translation with an object id and a language id in the same table or create an identical table for any language and insert objects there, then make a select union if you want to have them all displayed. For the database use utf8 general ci and of course in the front/backend use utf8 text/encoding. I have used url path segments for urls in the way you already explaned like

domain.org/en/about you can map the lang ID to your content table. anyway you need to have a map of parameters for your urls so you'd like to define a parameter to be mapped from a pathsegment in your URL that would be eg

domain.org/en/about/employees/IT/administrators/

lookup configuration

pageid| url

1 | /about/employees/../..

1 | /../about/employees../../

map parameters to url pathsegment ""

 $parameterlist[lang] = array(0=>"nl",1=>"en"); // default nl if 0 $parameterlist[branch] = array(1=>"IT",2=>"DESIGN"); // default nl if 0 $parameterlist[employertype] = array(1=>"admin",1=>"engineer"); //could be a sql result $websiteconfig[]=$userwhatever; $websiteconfig[]=$parameterlist; $someparameterlist[] = array("branch"=>$someid); $someparameterlist[] = array("employertype"=>$someid); function getURL($someparameterlist){ // todo foreach someparameter lookup pathsegment return path; } 

per say, thats been covered already in upper post.

And to not forget, you'd need to "rewrite" the url to your generating php file that would in most cases be index.php

Database work:

Create Language Table 'languages':

Fields:

language_id(primary and auto increamented)

language_name

created_at

created_by

updated_at

updated_by

Create a table in database 'content':

Fields:

content_id(primary and auto increamented)

main_content

header_content

footer_content

leftsidebar_content

rightsidebar_content

language_id(foreign key: referenced to languages table)

created_at

created_by

updated_at

updated_by

Front End Work:

When user selects any language from dropdown or any area then save selected language id in session like,

$_SESSION['language']=1;

Now fetch data from database table 'content' based on language id stored in session.

Detail may found here http://skillrow.com/multilingual-website-in-php-2/

As a person who live in Quebec where almost all site is french and english… i have try many if not most multilanguage plugin for WP… the one an only usefull solution that work nive with all my site is mQtranslate… i live and die with it !

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

A really simple option that works with any website where you can upload Javascript is http://www.multilingualizer.com

It lets you put all text for all languages onto one page and then hides the languages the user doesn't need to see. 效果很好。

What about WORDPRESS + MULTI-LANGUAGE SITE BASIS (plugin) ? the site will have structure:

  • example.com/ eng /category1/….
  • example.com/ eng /my-page….
  • example.com/ rus /category1/….
  • example.com/ rus /my-page….

The plugin provides Interface for Translation all phrases, with simple logic:

 (ENG) my_title - "Hello user" (SPA) my_title - "Holla usuario" 

then it can be outputed:
echo translate('my_title', LNG); // LNG is auto-detected

ps however, check, if the plugin is still active.