最佳实践多语言网站

几个月来,我一直在努力解决这个问题,但是我还没有遇到过需要去探索所有可能的选择的情况。 现在,我觉得是时候了解可能性并创建我自己的个人偏好以用于即将到来的项目。

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

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

应该翻译什么

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

此外,我正在寻找翻译多个数据库表的最简单方法,可能无需制作其他表格。

我自己想出了什么

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

  • PHP解析模板 :模板系统应该由PHP解析。 这样我就可以将翻译的参数插入到HTML中,而无需打开模板并修改它们。 除此之外,PHP解析模板使我能够为整个网站提供1个模板,而不是为每种语言设置子文件夹(我曾经使用过)。 达到此目标的方法可以是Smarty,TemplatePower,Laravel's Blade或任何其他模板解析器。 正如我所说,这应该独立于书面解决方案。
  • 数据库驱动 :也许我不需要再提及这一点。 但解决方案应该是数据库驱动的。 CMS的目标是面向对象和MVC,所以我需要为这些字符串设想一个逻辑数据结构。 由于我的模板是结构化的:templates / Controller / View.php,这个结构可能是最有意义的: Controller.View.parameter 。 数据库表将具有这些字段与value字段长。 在模板内部,我们可以使用一些排序方法,如echo __('Controller.View.welcome', array('name', 'Joshua')) ,参数包含Welcome, :name 。 因此, Welcome, Joshua的结果Welcome, Joshua 。 这似乎是一个很好的方法,因为诸如name之类的参数很容易被编辑理解。
  • 低数据库负载 :当然,如果这些字符串正在被加载,上述系统会导致数据库负载的加载。 因此,我需要一个缓存系统,在编辑/保存在管理环境中后立即重新渲染语言文件。 由于生成文件,所以还需要一个好的文件系统布局。 我想我们可以选择languages/en_EN/Controller/View.php或.ini,无论你最适合。 也许.ini甚至更快地解析。 这个模块应该包含format parameter=value;的数据format parameter=value; 。 我猜这是做这件事的最好方法,因为每个渲染的视图如果存在的话可以包含它自己的语言文件。 语言参数应该加载到特定的视图,而不是在全局范围内,以防止参数互相覆盖。
  • 数据库表翻译 :这实际上是我最担心的事情。 我正在寻找一种方法来创建新闻/网页/等的翻译。 尽快。 每个模块都有两个表格(例如NewsNews_translations )是一个选项,但是为了获得一个好的系统感觉News_translations很多工作。 有一个问题我想出了是基于对事物data versioning系统,我写道:有一个数据库表名Translations ,这个表有一个独特的组合languagetablenameprimarykey 。 例如:en_En / News / 1(参考ID = 1的新闻项目英文版)。 但是这种方法有两个巨大的缺点:首先,这个表格往往会在数据库中有很多数据的情况下变得很长,其次,使用这个设置来搜索表格会是一个无聊的工作。 例如搜索该项目的SEO slu would将是一个全文搜索,这是非常愚蠢的。 但另一方面:这是一种在每个表格中快速创建可翻译内容的快速方法,但我不认为这个专业人员胜过了这个con。
  • 前端工作 :前端也需要一些思考。 当然,我们会将可用的语言存储在数据库中,然后激活我们需要的语言。 这样脚本就可以生成一个下拉菜单来选择一种语言,后端可以自动决定使用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/about-us英语是我的默认语言。 当我选择荷兰语作为我的语言时,该网址是否应该翻译成http://www.domain.com/over-ons ? 或者我们应该走简单的路,只需更改/about可见的页面/about 。 最后一件事似乎并不是一个有效的选择,因为这会产生同一个URL的多个版本,这种索引内容的方式将失败。

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

    第三个选项是两种选择的组合:对主要语言使用“无语言标识”-URL( http://www.domain.com/about-us )。 并使用带有翻译的SEO slug的URL用于子语言: http://www.domain.com/nl/over-ons : http://www.domain.com/nl/over-ons : http://www.domain.com/de/uber-uns

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

    我想感谢您花时间阅读这些文本!

    // Edit #1

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


    主题的前提

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

  • 界面翻译
  • 内容
  • 网址路由
  • 虽然它们都以不同的方式相互关联,但从CMS的角度来看,它们使用不同的UI元素进行管理并以不同的方式存储。 您似乎对您对前两项的执行和理解充满信心。 问题是关于后一个方面 - “URL翻译?我们应该做还是不做?以及以什么方式?”

    什么URL可以被做?

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

  • 某些浏览器会将非ASCII字符(如'ч''ž'转换为'%D1%87''%C5%BE'
  • 如果用户具有自定义主题,则该主题的字体很可能没有这些字母的符号
  • 几年前,我在一个基于Yii的项目(恐怖的框架,恕我直言)中实际尝试了IDN方法。 在解决这个问题之前,我遇到了上述两个问题。 另外,我怀疑它可能是一个攻击媒介。

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

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

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

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

  • 查询是Α和Ω..

    比方说你选择http://site.tld/[:query]

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

  • 价值$_COOKIE['lang']为特定的浏览器
  • HTTP Accept-Language(1),(2)头中的语言列表
  • 首先,您需要将查询与某个定义的路由模式进行匹配(如果您的选择是Laravel,请阅读此处)。 模式的成功匹配,然后你需要找到语言。

    你将不得不通过该模式的所有部分。 找到所有这些细分的潜在翻译并确定使用了哪种语言。 当它们出现时(而不是“如果”),将使用另外两个来源(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']已设置,请使用它
  • 使用默认语言
  • 当您拥有语言时,只需尝试翻译查询,如果翻译失败,则使用该特定段的“默认值”(基于路由结果)。

    这不是第三种选择吗?

    是的,从技术上讲,您可以结合使用这两种方法,但这会使流程复杂化,只适应希望手动将http://site.tld/en/news 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完成吗?

    有点儿,但不是默认的。

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

    它被路由。 现在怎么办?

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

    基本上,以下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的建议使用预处理器实现i18n,而无需执行性能命中

    在工作中,我们最近在我们的几个物业上实施了国际化,我们一直在努力解决的问题是处理即时翻译的性能问题,然后我发现了Thomas Bley写的这篇很棒的博客文章这激发了我们使用i18n处理大流量负载时性能问题最少的方式。

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

    翻译标签

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

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

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

    INI文件

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

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

    允许用户在CMS内部修改这些内容是很简单的,只需通过n=上的preg_split获取密钥对,并使CMS能够写入INI文件即可。

    预处理器组件

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

    // This function was written by Thomas Bley, not by me
    function translate($file) {
      $cache_file = 'cache/'.LANG.'_'.basename($file).'_'.filemtime($file).'.php';
      // (re)build translation?
      if (!file_exists($cache_file)) {
        $lang_file = 'lang/'.LANG.'.ini';
        $lang_file_php = 'cache/'.LANG.'_'.filemtime($lang_file).'.php';
    
        // convert .ini file into .php file
        if (!file_exists($lang_file_php)) {
          file_put_contents($lang_file_php, '<?php $strings='.
            var_export(parse_ini_file($lang_file), true).';', LOCK_EX);
        }
        // translate .php into localized .php file
        $tr = function($match) use (&$lang_file_php) {
          static $strings = null;
          if ($strings===null) require($lang_file_php);
          return isset($strings[ $match[1] ]) ? $strings[ $match[1] ] : $match[1];
        };
        // replace all {t}abc{/t} by tr()
        file_put_contents($cache_file, preg_replace_callback(
          '/[%tr%](.*?)[%/tr%]/', $tr, file_get_contents($file)), LOCK_EX);
      }
      return $cache_file;
    }
    

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

    如何调用它

    再次,这个例子来自Thomas Bley,而不是来自我:

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

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

    为什么使用这种方法?

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

  • 由于不会为很少变化的内容调用一大堆函数,因此巨大的性能提升(使用此系统,法语中的10万用户仍然只能运行一次翻译替换)。
  • 它不会为我们的数据库增加任何负载,因为它使用简单的平面文件并且是纯PHP解决方案。
  • 在我们的翻译中使用PHP表达式的能力。
  • 获取翻译的数据库内容

    我们只在数据库中添加一个名为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参数并覆盖cookie变量,另一个是接受多个slug的路由。 然后你可以在你的路由中做这样的事情:

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

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

    有关其他选项的注意事项

    基于PHP的即时翻译

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

    基于前端的翻译

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

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

    数据库驱动的翻译

    PHP的数据库连接速度没有什么可写的,这增加了调用每个要翻译的短语的功能的高昂开销。 这种方法的性能和可伸缩性问题似乎令人难以置信。


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

    使用gettext,你将拥有一个功能强大的工具,其中很多情况已经像复数形式的数字一样实现了。 在英语中,你只有2种选择:单数和复数。 但在俄罗斯,例如有三种形式,并不像英文那么简单。

    还有许多翻译者已经有了使用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有更多的想法。 它有足够清晰的数据库模式和足够好的翻译界面。 就像你创造文章并选择语言一样。 而且你以后可以将它翻译成其他语言。

    Drupal翻译界面

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

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

    链接地址: http://www.djcxy.com/p/6391.html

    上一篇: Best practice multi language website

    下一篇: Defining a 1 to many relationship in Entity Framework