Jos*_*ndo 166 php mysql multilingual localization internationalization
我已经在这个问题上苦苦挣扎了好几个月,但我还没有遇到过需要探索所有可能的选择的情况.现在,我觉得是时候了解可能性并创建我自己的个人偏好,以便在我即将开展的项目中使用.
让我先描绘一下我正在寻找的情况
我即将升级/重新开发我已经使用了很长一段时间的内容管理系统.但是,我觉得多语言是对这个系统的一个很大的改进.在我没有使用任何框架之前,我将使用Laraval4进行即将到来的项目.Laravel似乎是更简洁的PHP编码方式的最佳选择.Sidenote: Laraval4 should be no factor in your answer
.我正在寻找独立于平台/框架的一般翻译方式.
应该翻译什么
由于我正在寻找的系统需要尽可能用户友好,管理翻译的方法应该在CMS内部.应该没有必要启动FTP连接来修改翻译文件或任何html/php解析模板.
此外,我正在寻找最简单的方法来翻译多个数据库表,而不需要创建额外的表.
我自己想出了什么
正如我一直在寻找,阅读和尝试自己.我有几个选择.但我仍然觉得我没有达到我真正寻求的最佳实践方法.现在,这是我想出来的,但这种方法也有它的副作用.
Controller.View.parameter
.数据库表将使用字段将这些字段设置为long value
.在模板内部,我们可以使用类似的排序方法echo __('Controller.View.welcome', array('name', 'Joshua'))
,参数包含Welcome, :name
.因此结果是Welcome, Joshua
.这似乎是一个很好的方法,因为参数如:name很容易被编辑器理解.languages/en_EN/Controller/View.php
或.ini,最适合你的.也许.ini甚至可以在最后解析得更快.这个应该包含的数据format parameter=value;
.我想这是执行此操作的最佳方式,因为呈现的每个View都可以包含它自己的语言文件(如果存在).然后,语言参数应加载到特定视图而不是全局范围,以防止参数相互覆盖.News
和News_translations
)是一个选项,但是为了获得一个好的系统感觉很多.我想出的一件事是基于data versioning
我写的一个系统:有一个数据库表名Translations
,这个表有一个独特的组合language
,tablename
和primarykey
.例如:en_En/News/1(参考ID = 1的新闻项目的英文版本).但是这个方法存在两个巨大的缺点:首先,这个表在数据库中有大量数据需要很长时间,其次使用这个设置搜索表是一件很麻烦的工作.例如,搜索项目的SEO slug将是一个全文搜索,这是相当愚蠢的.但另一方面:它是一种快速的方式,可以非常快速地在每个表格中创建可翻译内容,但我不相信这个专业人士会超过这个内容.所以,他们就是.我的想法到目前为止.他们甚至不包括日期等的本地化选项,但是因为我的服务器支持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-us
到http://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翻译?我们应该这样做还是不做?以什么方式?"
一个非常重要的事情是,不要喜欢IDN.而是赞成音译(也就是:转录和罗马化).虽然乍一看IDN似乎是国际URL的可行选择,但它实际上并不像宣传那样有两个原因:
'?'
或'ž'
成'%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]
段的内容; 还有两个来源:
首先,您需要将查询与已定义的路由模式之一匹配(如果您的选择是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个简单的优先选项:
[:language]
设置了段,请使用它$_COOKIE['lang']
设置,请使用它如果您使用该语言,则只需尝试翻译查询,如果翻译失败,请使用该特定段的"默认值"(基于路由结果).
是的,理论上你可以结合这两种方法,但这过程复杂且只能容纳谁想要手动更改URL的人http://site.tld/en/news
来http://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使用简单的基于模式的路由机制.要实现多语言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)
..或其中的一些变体,取决于具体实施方式.
Gli*_*ire 50
在工作中,我们最近在我们的几个属性上实现了i18n,我们一直在努力解决的问题之一是处理即时翻译的性能问题,然后我发现了Thomas Bley写的这篇精彩博文这激发了我们使用i18n以最小的性能问题处理大流量负载的方式.
我们知道在PHP中使用占位符来定义我们的基本文件,然后使用预处理器来缓存这些文件(我们存储文件修改时间以确保我们正在服务),而不是为每个翻译操作调用函数最新的内容在任何时候).
Thomas使用{tr}
和{/tr}
标签来定义翻译的开始和结束位置.由于我们使用TWIG这一事实,我们不想使用{
以避免混淆,因此我们使用[%tr%]
而[%/tr%]
不是.基本上,这看起来像这样:
`return [%tr%]formatted_value[%/tr%];`
Run Code Online (Sandbox Code Playgroud)
请注意,Thomas建议在文件中使用基础英语.我们不这样做是因为如果我们更改英文值,我们不想修改所有翻译文件.
然后,我们为每种语言创建一个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_split
on 获取密钥对\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
参数结合使用以覆盖语言,但我不建议使用每个语言的子域名或每个语言的页面,因为它会使得更难以查看哪些页面受欢迎并且会降低入站值链接,因为你会让他们更难以传播.
我们喜欢这种预处理方法有三个原因:
我们只是在我们的数据库中添加一个内容列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
.
我在这里结合了两件事,一件是你的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的经验.
看看CakePHP或Drupal.两种多语言都启用了.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模式和足够好的翻译界面.就像你创建文章并为它选择语言一样.而且您可以在以后将其翻译成其他语言.
我认为这不是URL slugs的问题.您可以为slug创建单独的表,这将是正确的决定.即使使用大量数据,使用正确的索引也无法查询表.它不是全文搜索,而是字符串匹配,如果将使用varchar数据类型为slug,你也可以在该字段上有一个索引.
PS抱歉,我的英语远非完美.
小智 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
我建议你不要真正依赖数据库进行翻译它可能真的是一个混乱的任务,在数据编码的情况下可能是一个极端的问题.
我在前一段时间遇到过类似的问题,并在课后写下来解决我的问题
<?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
标头只是一个简单的答案:绝对使用在其前面带有语言标识符的翻译后的url:http : //www.domain.com/nl/over-ons
Hybride解决方案趋于复杂,因此我会坚持使用。为什么?因为网址对于SEO是必不可少的。
关于数据库翻译:语言的数量是否固定?还是不可预测且动态的?如果它是固定的,我将只添加新的列,否则添加多个表。
但是通常,为什么不使用Drupal?我知道每个人都希望构建自己的CMS,因为它速度更快,更精简,等等。但这确实是一个坏主意!
我不会试图完善已经给出的答案。相反,我将告诉您我自己的 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”这样的字符串。服务之间不会发生冲突,因为每个服务在翻译器的数据区中都有自己的命名空间。
我把这个贴在这里是希望它能像我几年前那样免去重新发明轮子的任务。
归档时间: |
|
查看次数: |
133190 次 |
最近记录: |