Best practice multi language website
我已经在这个问题上挣扎了好几个月了,但我还没有处于一个我以前需要探索所有可能的选择的境地。现在,我觉得是时候了解可能性并创造自己的个人偏好,以便在即将到来的项目中使用了。
让我先概述一下我要找的情况
我将要升级/重新开发一个内容管理系统,我已经用了很长时间了。但是,我觉得多语言是这个系统的一个很大的改进。在我没有使用任何框架之前,我将在即将到来的项目中使用laraval4。Laravel似乎是一种更简洁的PHP代码编写方法的最佳选择。
应该翻译什么
由于我正在寻找的系统需要尽可能的用户友好,管理翻译的方法应该在CMS内部。不需要启动FTP连接来修改翻译文件或任何HTML/PHP解析的模板。
此外,我正在寻找最简单的方法来翻译多个数据库表,可能不需要制作额外的表。
我自己想出了什么主意
因为我一直在寻找,阅读和尝试自己的东西。我有几个选择。但我仍然不觉得我已经达到了一个最佳实践方法为我真正追求的。现在,这就是我想到的,但是这个方法也有它的副作用。
话题前提
多语言网站有三个不同的方面:好的。
- 接口转换
- 内容
- URL路由
虽然它们都以不同的方式相互连接,但从CMS的角度来看,它们使用不同的UI元素进行管理,并且存储方式也不同。您似乎对前两个问题的实现和理解充满信心。问题是关于后一个方面-"URL翻译?我们是否应该这样做?以什么方式?"好的。URL可以由什么组成?
一件非常重要的事情是,不要对IDN产生幻想。相反,更喜欢音译(也包括:转录和罗马化)。虽然乍一看IDN似乎是国际URL的可行选择,但实际上它并没有像广告中那样工作,原因有两个:好的。
- 有些浏览器会将非ASCII字符(如
'ч' 或'?' 转换为'%D1%87' 和'%C5%BE' 。 - 如果用户有自定义主题,主题的字体很可能没有这些字母的符号。
几年前,我在一个基于yii的项目(可怕的框架,imho)中尝试过IDN方法。在找到解决方案之前,我遇到了上述两个问题。另外,我怀疑它可能是一个攻击向量。好的。可用选项…就像我看到他们一样。
基本上,您有两个选择,可以抽象为:好的。
http://site.tld/[:query] :其中[:query] 决定语言和内容的选择。好的。http://site.tld/[:language]/[:query] :其中,url的[:language] 部分定义了语言的选择,[:query] 仅用于标识内容。好的。
查询是_和Ω..
假设你选择了
在这种情况下,您有一个主要的语言来源:
- 该特定浏览器的值
$_COOKIE['lang'] - HTTP接受语言(1)、(2)头中的语言列表
首先,您需要将查询与一个定义的路由模式相匹配(如果您的选择是laravel,那么在这里阅读)。成功匹配模式后,您需要找到语言。好的。
您将不得不遍历模式的所有部分。找到所有这些片段的潜在翻译,并确定使用了哪种语言。另外两个源(cookie和header)将用于解决路由冲突,当它们出现时(而不是"if")。好的。
例如:
这是
正如你已经注意到的,在俄语中,"бло_"将被音译为"blog"。也就是说,在
当列表有一个项目时,您已成功找到语言。好的。
但如果你最终有2个(例如:俄罗斯和乌克兰)或更多的可能性……或者0种可能性,比如说。您必须使用cookie和/或header来找到正确的选项。好的。
如果所有其他的都失败了,您可以选择站点的默认语言。好的。语言作为参数
另一种方法是使用URL,它可以定义为
还有第二语言来源:cookie值。但是这里没有必要处理accept-language头部,因为在"冷启动"的情况下(当用户第一次使用自定义查询打开站点时),您不会处理未知数量的可能语言。好的。
相反,您有3个简单的优先选项:好的。
当您使用该语言时,只需尝试转换查询,如果转换失败,则使用该特定段的"默认值"(基于路由结果)。好的。这不是第三种选择吗?
是的,从技术上讲,您可以将这两种方法结合起来,但这会使过程复杂化,并且只适合那些希望手动将
但即使是这种情况,也可以通过使用cookie值(其中包含有关先前语言选择的信息)来减少实现的魔力和希望。好的。使用哪种方法?
正如您可能已经猜到的,我建议使用
同样,在实际情况下,URL中还有第三个主要部分:"标题"。如在网上商店或新闻网站上的文章标题中的产品名称。好的。
示例:
在这种情况下,查询将是
有点,但不是默认的。好的。
我不太熟悉它,但是从我所看到的来看,Laravel使用简单的基于模式的路由机制。要实现多语言URL,您可能需要扩展核心类,因为多语言路由需要访问不同形式的存储(数据库、缓存和/或配置文件)。好的。它被路由了。现在怎么办?
结果,您将得到两个有价值的信息:当前语言和已翻译的查询段。然后,这些值可用于分派到将生成结果的类。好的。
基本上,以下URL:
1 2 3 4 5 | $parameters = [ 'language' => 'ru', 'classname' => 'blog', 'method' => 'latest', ]; |
你只是用来调度的:好的。
1 2 | $instance = new {$parameter['classname']}; $instance->{'get'.$parameters['method']}( $parameters ); |
…或者它的一些变体,取决于特定的实现。好的。好啊。
根据Thomas Bley的建议,使用预处理器实现i18n而不影响性能
在工作中,我们最近在我们的一些财产上进行了i18n的实现,我们一直在努力的事情之一是处理即时翻译的性能冲击,然后我发现了Thomas Bley的这篇伟大的博客文章,它启发了我们使用i18n以最低的性能处理大流量负载的方式是起诉。好的。
我们不需要为每个翻译操作调用函数,正如我们在PHP中所知道的那样,我们使用占位符定义基本文件,然后使用预处理器缓存这些文件(我们存储文件修改时间,以确保我们始终提供最新的内容)。好的。翻译标签
托马斯使用
1 | `return [%tr%]formatted_value[%/tr%];` |
注意,托马斯建议在文件中使用基础英语。我们不这样做,因为如果我们更改了英文值,就不需要修改所有翻译文件。好的。ini文件
然后,我们为每种语言创建一个ini文件,格式为
1 2 3 4 5 6 7 8 | // 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中修改这些内容是很简单的,只需在
从本质上讲,托马斯建议使用一个及时的"编译器"(尽管实际上它是一个预处理器)函数来获取翻译文件并在磁盘上创建静态PHP文件。通过这种方式,我们基本上缓存转换后的文件,而不是为文件中的每个字符串调用转换函数:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // 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; } |
注意:我没有验证regex是否有效,我没有从公司服务器复制它,但是您可以看到操作是如何工作的。好的。如何称呼它
同样,这个例子来自托马斯·布莱,而不是我:好的。
1 2 3 4 5 6 7 8 | // 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的话)中,然后在每个请求中检索它。您可以将此参数与可选的
我们喜欢这种预处理方法有三个原因:好的。
获取已翻译的数据库内容
我们只是为数据库中名为
1 2 3 4 | $query = select()->from($this->_name) ->where('language = ?', User::getLang()) ->where('id = ?', $articleId) ->limit(1); |
我们的文章在
我将在这里结合两件事情,一个是引导程序中的函数,它接受一个用于语言的
1 2 | "/wilkommen" =>"/welcome/lang/de" ... etc ... |
这些文件可以存储在一个平面文件中,可以很容易地从管理面板写入。JSON或XML可以提供一个良好的结构来支持它们。好的。其他几个选项的注意事项
基于fly翻译的PHP好的。
我看不出这些比预处理的翻译有任何优势。好的。
基于前端的翻译好的。
我早就发现这些很有趣,但有几个注意事项。例如,您必须向用户提供您计划翻译的网站上的完整短语列表,如果您隐藏或不允许他们访问网站的某些区域,这可能会有问题。好的。
您还必须假设您的所有用户都愿意并且能够在您的站点上使用javascript,但是根据我的统计,大约2.5%的用户没有使用它(或者使用noscript阻止我们的站点使用它)。好的。
数据库驱动的翻译好的。
PHP的数据库连接速度没有什么好写的,这增加了对每个要翻译的短语调用函数的开销。这种方法的性能和可伸缩性问题似乎难以解决。好的。好啊。
我建议你不要发明一个轮子,使用gettext和iso语言缩写列表。您是否看到i18n/l10n是如何在流行的CMSE或框架中实现的?
使用getText,您将拥有一个强大的工具,其中许多情况已经实现,比如复数形式的数字。在英语中,你只有两种选择:单数和复数。但以俄语为例,它有三种形式,而不像英语那么简单。
还有许多翻译人员已经有了使用gettext的经验。
看看Cakephp或Drupal。两种语言都已启用。cakephp作为接口本地化的例子,drupal作为内容翻译的例子。
对于l10n来说,使用数据库一点都不合适。如果有疑问,那将是一大堆。标准的方法是在早期阶段(或者在第一次调用i10n函数时,如果您喜欢延迟加载的话)将所有l10n数据获取到内存中。它可以同时从.po文件或db中读取所有数据。而不仅仅是从数组中读取请求的字符串。
如果您需要实现在线工具来转换接口,那么您可以将所有数据保存在数据库中,但仍然需要将所有数据保存到文件中以便使用它。为了减少内存中的数据量,您可以将所有已翻译的消息/字符串拆分为多个组,如果可能的话,只加载所需的组。
所以你完全正确。有一个例外:通常是一个大文件,而不是每个控制器的文件。因为打开一个文件对性能最好。您可能知道一些高负载的Web应用程序将所有PHP代码编译在一个文件中,以避免在调用include/require时执行文件操作。
关于URL。谷歌间接建议使用翻译:
to clearly indicate French content:
http://example.ca/fr/vélo-de-montagne.html
另外,我认为您需要将用户重定向到默认语言前缀,例如http://examlpe.com/about-us将重定向到http://examlpe.com/en/about-us。但是如果你的网站只使用一种语言,那么你根本不需要前缀。
退房:http://www.audiomico.com/trailer-hit-impact-psychoatracy-sound-effects-836925http://nl.audiomico.com/aanhangwagen-hit-effect-psychoatracy-geluideseffecten-836925http://de.audiomico.com/anhanger-hit-auswirkungen-psychoatracy-sound-effekte-836925
翻译内容更为困难。我认为不同类型的内容会有所不同,例如文章、菜单项等,但在4中,你的方法是正确的。看看Drupal有更多的想法。它有足够清晰的数据库模式和足够好的翻译接口。就像你创建文章并为它选择语言一样。然后你可以把它翻译成其他语言。
我认为这与URL块没有问题。您可以为slugs创建单独的表,这将是正确的决定。另外,使用正确的索引,即使有大量的数据,查询表也没有问题。它不是全文搜索,而是字符串匹配,如果将对slug使用varchar数据类型,您也可以在该字段上有一个索引。
抱歉,我的英语还远远不够好。
这取决于你的网站有多少内容。起初,我像这里的其他人一样使用一个数据库,但编写数据库的所有工作脚本可能很费时。我不认为这是一个理想的方法,尤其是如果你有大量的文本,但是如果你想在不使用数据库的情况下快速完成,这个方法可以工作,但是,你不能允许用户输入数据,这些数据将被用作翻译文件。但是,如果您自己添加翻译,它将起作用:
假设您有以下文本:
1 | Welcome! |
您可以在数据库中输入翻译,但也可以这样做:
1 2 3 4 5 6 7 8 9 10 11 | $welcome = array( "English"=>"Welcome!", "German"=>"Willkommen!", "French"=>"Bienvenue!", "Turkish"=>"Ho?geldiniz!", "Russian"=>"Добро пожаловать!", "Dutch"=>"Welkom!", "Swedish"=>"V?lkommen!", "Basque"=>"Ongietorri!", "Spanish"=>"Bienvenito!" "Welsh"=>"Croeso!"); |
现在,如果您的网站使用cookie,您可以使用它,例如:
1 | $_COOKIE['language']; |
为了方便起见,让我们将其转换为易于使用的代码:
1 | $language=$_COOKIE['language']; |
如果你的曲奇语言是威尔士语,你有这段代码:
4结果是:
1 | Croeso! |
如果您需要为您的网站添加大量翻译,并且数据库过于消耗,那么使用数组可能是一个理想的解决方案。
我建议您不要真正依赖数据库进行翻译,这可能是一项非常棘手的任务,在数据编码的情况下可能是一个极端的问题。
我以前也遇到过类似的问题,为了解决我的问题,我在下课写了一篇文章。
对象:区域设置区域设置1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 | <?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; } } |
用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?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'); |
它是如何工作的
- 默认情况下,如果安装了
geoip ,那么它将通过geoip_country_code_by_name 返回国家代码;如果没有安装geoip,则回退到HTTP_ACCEPT_LANGUAGE 头段。
只是一个次要答案:绝对要使用前面有语言标识符的翻译后的URL:http://www.domain.com/nl/over-ons混合解决方案往往会变得复杂,所以我会坚持下去。为什么?因为URL对于SEO是必不可少的。
关于数据库翻译:语言的数量多少是固定的?或者更不可预测和动态?如果它是固定的,我只需要添加新的列,否则将使用多个表。
但一般来说,为什么不使用Drupal呢?我知道每个人都想建立自己的CMS,因为它更快,更瘦,等等,但这真是一个坏主意!
我不打算试图完善已经给出的答案。相反,我将告诉您我自己的oop-php框架处理翻译的方式。好的。
在内部,我的框架使用诸如en、fr、es、cn等代码。数组包含网站支持的语言:array('en'、'fr'、'es'、'cn')语言代码通过$u get(lang=fr)传递,如果未传递或无效,则将其设置为数组中的第一种语言。所以在程序执行期间的任何时候,从一开始,当前语言就是已知的。好的。
理解在典型应用程序中需要翻译的内容是很有用的:好的。
1)来自类(或过程代码)的错误消息2)来自类(或过程代码)的非错误消息3)页面内容(通常存储在数据库中)4)站点范围字符串(如网站名称)5)脚本特定字符串好的。
第一种类型很容易理解。基本上,我们谈论的是"无法连接到数据库…"之类的消息。只有在出现错误时才需要加载这些消息。我的管理器类接收来自其他类的调用,使用作为参数传递的信息,只需转到相关的类文件夹并检索错误文件。好的。
第二类错误消息更像是表单验证出错时得到的消息。(你不能离开…空白"或"请选择超过5个字符的密码")。在类运行之前需要加载字符串。我知道什么是好的。
对于实际的页面内容,我为每种语言使用一个表,每个表的前缀都是该语言的代码。因此,en-u内容是英语内容的表格,es-u内容是西班牙的表格,cn-u内容是中国的表格,fr-u内容是法国的表格。好的。
第四种字符串在整个网站中都是相关的。这是通过一个使用该语言的代码命名的配置文件加载的,即en-lang.php、es-lang.php等。在全局语言文件中,您需要在英语全局文件中加载array("英语"、"中文"、"西班牙语"、"法语")等翻译语言,并在法语文件中加载array("英语"、"chinois"、"espagnol"、"francais")。因此,当您为语言选择填充下拉列表时,它使用的语言是正确的;)好的。
最后是脚本特定的字符串。所以,如果你写一个烹饪应用程序,它可能是"你的烤箱不够热"。好的。
在我的应用程序周期中,首先加载全局语言文件。在这里,您不仅可以找到全局字符串(如"杰克的网站"),还可以找到一些类的设置。基本上,任何与语言或文化相关的东西。其中的一些字符串包括日期掩码(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'的字符串。服务之间不能有冲突,因为每个服务在转换器的数据区域中都有自己的命名空间。好的。
我把这个贴在这里,希望它能像我多年前做的那样,为别人省去重新发明轮子的任务。好的。好啊。
在开始使用symfony框架之前,我有过同样的问题。
只需使用一个函数uu(),它具有参数pageid(或objectid,objecttable,如2所述)、目标语言和回退(默认)语言的可选参数。可以在一些全局配置中设置默认语言,以便以后更容易地更改它。
为了在数据库中存储内容,我使用了以下结构:(pageid、语言、内容、变量)。
pageid将是一个FK到您想要翻译的页面。如果有其他对象,如新闻、库或其他对象,只需将其拆分为两个字段:objectid、objecttable。
语言-显然,它将存储ISO语言字符串en-en、lt-lt、en-us等。
内容-要与通配符一起转换以替换变量的文本。示例"你好,先生,%%name%"。您的帐户余额是%%balance%%."
变量-JSON编码的变量。PHP提供了快速解析这些内容的函数。示例"name:laurynas,balance:15.23"。
你还提到了史拉格菲尔德。您可以自由地将其添加到此表中,以便快速搜索它。
必须通过缓存翻译将数据库调用减少到最小。它必须存储在PHP数组中,因为它是PHP语言中最快的结构。如何创建这个缓存取决于您自己。根据我的经验,对于每种支持的语言,您应该有一个文件夹,对于每种pageid应该有一个数组。应该在更新转换后重建缓存。只应重新生成已更改的数组。
我想我在2中回答了这个问题
你的想法完全合乎逻辑。这个很简单,我想不会给你带来任何问题。
URL应该使用翻译表中存储的段塞进行翻译。
最后的话
研究最佳实践总是好的,但不要重新发明轮子。只需从已知框架中获取并使用组件,然后使用它们。
看看symfony翻译组件。它可能是一个很好的代码库。
我一遍又一遍地问自己相关的问题,然后就迷上了正规语言……但为了帮你一点忙,我想分享一些发现:好的。
我建议看一下高级CMS好的。
如果你发现2013年的网络应该有所不同,那就从头开始吧。这将意味着组建一个由高技能/经验丰富的人员组成的团队来构建一个新的CMS。也许你想看看聚合物的用途。好的。
如果涉及到编码和多语言网站/本机语言支持,我认为每个程序员都应该了解Unicode。如果你不知道Unicode,你肯定会把你的数据弄乱。不要使用成千上万的ISO代码。他们只会给你留点记忆。但是你可以用UTF-8做任何事情,甚至存储中文字符。但为了实现这一点,您需要存储2或4字节字符,这使得它基本上是一个utf-16或utf-32。好的。
如果是关于URL编码的,那么你也不应该混合编码,并且要知道至少对于域名来说,有一些规则是由不同的大厅定义的,它们提供类似浏览器的应用程序。例如,一个域可能非常类似:好的。
leng ankofamerica.com或bankofamerica.com samesamebutdifferent;)好的。
当然,您需要文件系统来处理所有编码。使用UTF-8文件系统的Unicode的另一个优点。好的。
如果是关于翻译的,考虑一下文档的结构。例如一本书或一篇文章。您有
如果是关于框架的,我所知道的最成熟的框架,去做像MVC这样的一般事情(我真的很讨厌这个词!像"性能"一样,如果你想卖东西,用"性能和特性"这个词,你就卖……他妈的)是
如果是关于缓存…这可能非常复杂/多层。在PHP中,您会想到Accelerator、Opcode,还有HTML、HTTPD、MySQL、XML、CSS、JS……任何类型的缓存。当然,有些部分应该缓存,而动态部分(如博客答案)则不应该缓存。有些应该通过Ajax通过生成的URL请求。JSON、HashBangs等。好的。
然后,您希望网站上的任何小组件只能由特定的用户访问或管理,因此从概念上讲,这起到了很大的作用。好的。
此外,你还想做统计,可能有分布式系统/Facebook的facebook等。任何软件将建立在你的超顶级CMS之上…因此,您需要在内存、bigdata、xml中使用不同类型的数据库。好的。
好吧,我想这就够了。如果您没有听说typo3/plone或提到的框架,那么您就有足够的学习空间。在这条路上,你会找到很多解决你还没有问到的问题的方法。好的。
如果你认为,让我们做一个新的CMS,因为它的2013年和PHP即将死去,那么你R欢迎加入任何其他的开发团队,希望不会迷路。好的。
祝你好运!好的。
顺便说一句,未来人们不会再有任何网站了?我们都会在Google+上?我希望开发人员变得更有创造力,做一些有用的事情(不要被博尔格人同化)好的。
/////////只需考虑一下您现有的应用程序:好的。
如果您有一个php-mysql-cms,并且想要嵌入多语言支持。您可以将表与任何语言的附加列一起使用,也可以在同一个表中插入具有对象ID和语言ID的翻译,或者为任何语言创建相同的表并在其中插入对象,然后在希望所有对象都显示时进行选择联合。对于数据库,使用utf8常规CI,当然在前端/后端使用utf8文本/编码。我已经按照你已经解释过的方式使用了URL路径段好的。
域.org/en/about您可以将语言ID映射到内容表。无论如何,您需要有一个URL的参数映射,这样您就可以定义一个从URL中的路径段映射的参数,例如。好的。
域.org/en/about/employees/it/administrators/好的。
查找配置好的。
页面地址好的。
1/关于/员工/../..好的。
1/…/关于/员工…..。/好的。
将参数映射到URL路径段"好的。
1 2 3 4 5 6 7 8 9 10 11 12 | $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; } |
比如说,上面的帖子已经介绍过了。好的。
不要忘记,您需要"重写"生成php文件的URL,在大多数情况下,该文件就是index.php。好的。好啊。
数据库工作:
创建语言表"语言":
领域:
语言ID(主要和自动增加)
语言名
创造在
被创造的
更新的AT
通过更新
在数据库"content"中创建表:
领域:
内容ID(主要和自动增加)
主要内容
标题内容
足叶内容物
左侧边栏内容
右侧边栏内容
语言u id(外键:引用到语言表)
创造在
被创造的
更新的AT
通过更新
前端工作:
当用户从下拉列表或任何区域中选择任何语言时,将所选语言ID保存在会话中,如下所示:
$会话[‘语言’]=1;
现在根据会话中存储的语言ID从数据库表"content"中提取数据。
详情可在以下网址找到:http://skillrow.com/multilingual-website-in-php-2/
作为一个住在魁北克的人,几乎所有的网站都是法语和英语…我已经尝试了很多,如果不是大多数的多语言插件为wp…唯一有用的解决方案是mqtranslate…我和它一起生活和死亡!
https://wordpress.org/plugins/mqtranslate/
WordPress+EDOCX1(插件)怎么样?现场将具有以下结构:
- 示例.com/eng/category1/…
- 示例.com/eng/my page….
- 示例.com/rus/category1/…
- 示例.com/rus/my page….
该插件提供了翻译所有短语的接口,具有简单的逻辑:
1 2 | (ENG) my_title -"Hello user" (SPA) my_title -"Holla usuario" |
然后可以输出:
但是,请检查插件是否仍处于活动状态。
一个非常简单的选项是www.multilingualizer.com,它适用于任何可以上传javascript的网站。
它允许您将所有语言的所有文本放到一个页面上,然后隐藏用户不需要看到的语言。很好用。