JMSI18nRoutingBundle可以使用HTTP Accept-Language数组吗?

时间:2014-12-18 00:54:17

标签: symfony internationalization

我正在尝试建立一个国际化网站,我翻译的每种语言都有一个网址前缀(例如/fr/my/page/it/my/page)。

我尝试了JMSI18nRoutingBundle,它几乎没有其他配置,效果很好。但我真的想自动确定用户首选语言。 用户最喜欢的语言被传输到Accept-Language HTTP标头,我想选择我翻译的第一种语言。

这是我的JMSI18nRouting配置:

jms_i18n_routing:
    default_locale: en
    locales: [fr, en]
    strategy: prefix_except_default

我想要这种行为:

http://mywebsite.com/my/page执行自动语言检测,然后重定向到/xx/...(其中xx是用户最喜欢的语言),因为未在URL中指定语言 - 目前默认语言为EN。

http://mywebsite.com/XX/my/page以XX语言显示页面 - 目前,工作正常。

有什么想法吗?配置好吗?

哦,如果有人有解决方案在纯粹的Symfony中做同样的事情(没有JMSI18nRoutingBundle),我的耳朵就会大开。

编辑/找到一种方法,使用JMSI18nRoutingBundle进行智能重定向,以尊重用户最喜欢的语言或让用户强制显示语言。查看我的answer

3 个答案:

答案 0 :(得分:4)

最后,我回答了我的问题。

我开发了一个小“补丁”,使用 JMSI18nRoutingBundle并检测用户的首选语言,并让用户强制使用语言。

创建侦听器YourBundle/EventListener/LocaleListener.php

如果用户的首选语言环境与Symfony或JMSI18nRoutingBundle定义的语言环境不同,则此侦听器将更改URL。通过这种方式,您可以使用两种不同语言的两种不同内容的URL:它是SEO友好的。

您还可以创建一个由 hrefing ?setlang=xx的链接组成的语言选择器,其中xx是用户想要显示的语言。侦听器将检测setlang查询并强制显示xx lang,包括在下一个请求中。

注意$this->translatable = [...数组。它可以让您定义网站的哪些部分可以翻译/翻译。可以从供应商到操作方法定义粒度。 您还可以创建一个配置节点来定义可翻译的供应商/软件包/控制器,由于性能方面的考虑,我没有这样做。

<?php

namespace YourVendor\YourBundle\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class LocaleListener implements EventSubscriberInterface
{
    private $defaultLocale;
    private $acceptedLocales;
    private $translatable;

    public function __construct($router, $defaultLocale, $acceptedLocales)
    {
        $this->router = $router;
        $this->defaultLocale = $defaultLocale;
        $this->acceptedLocales = $acceptedLocales;

        $this->translatable = [
            'Vendor1',
            'Vendor2\Bundle1',
            'Vendor2\Bundle2\Controller1',
            'Vendor2\Bundle2\Controller2::myPageAction',
        ];
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        $request = $event->getRequest();
        $route = $request->get('_route');

        if(!empty($newLocale = $request->query->get('setlang'))) {
            if(in_array($newLocale, $this->acceptedLocales)) {
                $cookie = new Cookie('force_lang', $newLocale, time() + 3600 * 24 * 7);

                $url = $this->router->generate($route, ['_locale' => $newLocale] + $request->attributes->get('_route_params'));

                $response = new RedirectResponse($url);
                $response->headers->setCookie($cookie);
                $event->setResponse($response);
            }
        } else if($this->translatable($request->attributes->get('_controller'))) {
            $preferred = empty($force = $request->cookies->get('force_lang')) ? $request->getPreferredLanguage($this->acceptedLocales) : $force;

            if($preferred && $request->attributes->get('_locale') != $preferred) {
                $url = $this->router->generate($route, ['_locale' => $preferred] + $request->attributes->get('_route_params'));
                $event->setResponse(new RedirectResponse($url));
            }
        }
    }

    private function translatable($str)
    {
        foreach($this->translatable as $t) {
            if(strpos($str, $t) !== false) return true;
        }

        return false;
    }

    public static function getSubscribedEvents()
    {
        return [ KernelEvents::REQUEST => [['onKernelRequest', 200]] ];
    }
}

将您的侦听器绑定在HTTP内核上。

修改您的services.yml文件。

services:
    app.event_listener.locale_listener:
        class: YourVendor\YourBundle\EventListener\LocaleListener
        arguments: ["@router", "%kernel.default_locale%", "%jms_i18n_routing.locales%"]
        tags:
          - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }

JMSI18nRoutingBundle

的配置

你无需改变。 例如:

# JMS i18n Routing Configuration
jms_i18n_routing:
    default_locale: "%locale%"
    locales: [fr, en]
    strategy: prefix_except_default

答案 1 :(得分:1)

这是使用直接Symfony进行此操作的方法。它可能会觉得有点hacky因为它需要为每个动作指定2条路线,所以如果有人能想出更好的方式,我就会全力以赴。

首先,我将为所有可接受的语言环境定义某种配置参数,并将第一个列为默认语言

<强> parameters.yml.dist:

parameters:
    accepted_locales: [en, es, fr]

然后确保您的Controller路由匹配时_locale已设置且未设置。对两者使用相同的路由名称,但没有_locale的后缀除了|之类的分隔符:

/**
 * @Route("/{_locale}/test/{var}", name="test")
 * @Route(          "/test/{var}", name="test|")
 */
public function testAction(Request $request, $var, $_locale = null)
{
    // whatever your controller action does
}

接下来定义一个服务,该服务将侦听Controller事件并将接受的语言环境传递给它:

<service id="kernel.listener.locale" class="My\Bundle\EventListener\LocaleListener">
    <tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />
    <argument>%accepted_locales%</argument>
</service>

现在使用该服务检测路由中是否设置了_locale,如果没有,则根据HTTP_ACCEPT_LANGUAGE标头确定区域设置并重定向到包含它的路由。这是一个示例监听器,它将执行此操作(我添加了注释来解释我在做什么):

namespace NAB\UtilityBundle\EventListener;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;

class ControllerListener
{
    private $acceptedLocales;

    public function __construct(array $acceptedLocales)
    {
        $this->acceptedLocales = $acceptedLocales;
    }

    public function onKernelController(FilterControllerEvent $event)
    {
        if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) {
            return;
        }

        $controller = $event->getController();

        if (!is_array($controller)) {
            return;
        }

        $request = $event->getRequest();
        $params  = $request->attributes->get('_route_params');

        // return if _locale is already set on the route
        if ($request->attributes->get('_locale')) {
            return;
        }

        // if the user has accepted languages set, set the locale on the first match found
        $languages = $request->server->get('HTTP_ACCEPT_LANGUAGE');

        if (!empty($languages))
        {
            foreach (explode(',', $languages) as $language) 
            {
                $splits  = array();
                $pattern = '/^(?P<primarytag>[a-zA-Z]{2,8})(?:-(?P<subtag>[a-zA-Z]{2,8}))?(?:(?:;q=)(?P<quantifier>\d\.\d))?$/';

                // if the user's locale matches the accepted locales, set _locale in the route params
                if (preg_match($pattern, $language, $splits) && in_array($splits['primarytag'], $this->acceptedLocales)) 
                {
                    $params['_locale'] = $splits['primarytag'];

                    // stop checking once the first match is found
                    break;
                }
            }
        }

        // if no locale was found, default to the first accepted locale
        if (!$params['_locale']) {
            $params['_locale'] = $this->acceptedLocales[0];
        }

        // drop the '|' to get the appropriate route name
        list($localeRoute) = explode('|', $request->attributes->get('_route'));

        // attempt get the redirect URL but return if it could not be found
        try {
            $redirectUrl = $controller[0]->generateUrl($localeRoute, $params);
        }
        catch (\Exception $e) {
            return;
        }

        // set the controller response to redirect to the route we just created
        $event->setController(function() use ($redirectUrl) {
            return new RedirectResponse($redirectUrl);
        });
    }
}

有关在Controller上设置前置过滤器的进一步说明,请查看the Symfony documentation here。如果你使用这样的东西,要非常小心,正确定义每个路径名称。

答案 2 :(得分:0)

另一种更有用的解决方案
转到I18nRoutingBundle的供应商和编辑监听器

/www/vendor/jms/i18n-routing-bundle/JMS/I18nRoutingBundle/EventListener

<强>替换

$locale = $this->localeResolver->resolveLocale($request, $this->locales) ?: $this->defaultLocale;

$locale = $this->localeResolver->resolveLocale($request, $this->locales) ?: $request->getPreferredLanguage($this->locales);

(覆盖监听器比直接编辑供应商更清晰)