1
votes
  • What I want

What I want to do is, based on the Accept-Language sent by the user's browser, to translate both the in-code strings (__('translatable string')) and the fields I have configured from tables that have the TranslateBehavior.

  • What I did

Added

DispatcherFactory::add('LocaleSelector', ['locales' => ['en_US', 'el_GR']]);

in bootstrap.php in order to set the locale automatically, using the Accept-Language that is sent by the user's browser. This works just fine and sets the locale to either en_US or el_GR.

I also have setup i18n (following http://book.cakephp.org/3.0/en/orm/behaviors/translate.html) because I want some fields of a table of mine to be translatable.

Of course, I have strings in my code that do not come from the database and need to be translated. I use the __() function for that.

  • The problem

Let's say the user, through Accept-Language, requests Greek (el_GR), then the locale will be set to el_GR. The function __() will work out of the box, because it is exactly what it needs.

But, the TranslateBehavior will not work, because it needs gre, not el_GR.

How can I make those 2 work at the same time, while they expect different locale value for the same language?

2

2 Answers

1
votes

The Translate Behavior doesn't impose any restrictions on the locale identifier format. The possible values are only limited by the database column type/length.

So, just use en_US and el_GR for your translation table records and you should be good.

1
votes

After a long discussion and help from @ndm, I took the following measures to make it work:

  1. After doing the appropriate changes, completely clear your tmp directory and update composer. This solved a problem where LocaleSelector, while it was setting the appropriate locale, my database wasn't being translated.

  2. There is the problem that the Accept-Language header can have many values. You want many of them to be matched with a specific locale. That way you will be able to have the same locale for everything, both for your database and translatable strings. In order to solve this problem, I made my own LocaleSelectorFilter and I placed it in src/Routing/Filter. It overrides the default LocaleSelectorFilter:

    namespace Cake\Routing\Filter;

    use Cake\Event\Event; use Cake\I18n\I18n; use Cake\Routing\DispatcherFilter; use Locale;

    class LocaleSelectorFilter extends DispatcherFilter {

    protected $_locales = [];
    
    public function __construct($config = [])
    {
        parent::__construct($config);
        if (!empty($config['locales'])) {
            $this->_locales = $config['locales'];
        }
    }
    
    private function matchLocaleWithConfigValue($localeFromHeader)
    {
        foreach ($this->_locales as $locale => $acceptableValues) {
            // $acceptableValues is either an array or a single string
            if (!$locale) {
                // single string
                if ($localeFromHeader === $acceptableValues) {
                    return $acceptableValues;
                }
            } else {
                // array
                foreach ($acceptableValues as $acceptableValue) {
                    if ($localeFromHeader === $acceptableValue) {
                        return $locale;
                    }
                }
            }
        }
    
        return false;
    }
    
    public function beforeDispatch(Event $event)
    {
        $request = $event->data['request'];
        $locale = Locale::acceptFromHttp($request->header('Accept-Language'));
    
        if (!$locale) {
            // no locale set in the headers
            return;
        }
    
        if (empty($this->_locales)) {
            // any locale value allowed
            I18n::locale($locale);
            return;
        }
    
        // search whether the requested language is in the accepted ones
        $localeConfigMatch = $this->matchLocaleWithConfigValue($locale);
    
        if (!$localeConfigMatch) {
            // no locale matches the header, leave the default one
            return;
        }
    
        // match was found, switch locale
        I18n::locale($localeConfigMatch);
    }
    

    }

It is used like this, inside bootstrap.php:

DispatcherFactory::add('LocaleSelector', ['locales' => ['el_GR' => ['el', 'el_GR', 'el-GR'], 'en_US']]);

In the above example, all the values el, el_GR and el-GR of the Accept-Language will result to the locale el_GR being set. Also, if Accept-Language has the en_US value, it will be set. So it supports both many different values to be set to one specific locale and it also supports the default LocaleSelector behavior.

  1. Your i18n table's locale column must be set to 'el_GR' (in this example). This is necessary, because it is what the in-code translatable strings expect (the ones using the __() function). So by setting 'el_GR' (instead of 'gre', mentioned in the documentation) you will have both the in-code translatable strings and database translatable fields work out of the box.

That's it!