Make TranslateBehavior and translatable strings work at the same time - cakephp

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?

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.

After a long discussion and help from #ndm, I took the following measures to make it work:
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.
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.
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!

Related

How can i fetch dynamic data from database based on selected language.?

Hi i am working on a project in laravel 7.0, in back-end i have a table called Posts which contains 2 text language input one in french and the other is arabic added by the back-end application.
what i am trying to do is when the user uses the French Language i want the title_fr to be displayed on the view and same thing in Arabic language the title should be title_ar.
P.S data are stored in French and Arabic
I have tried the similar solution given in an other similar question but none of it worked in my case!
Any idea how i might get this to work ?
Thanks in advance.
You can do something similar to below. We have a model Post, this model has an attribute title. I also assume that you have an attribute that will return user's language from the User model.
class Post extends Model
{
public function getTitleAttribute(): string
{
return Auth::user()->language === 'fr' ? $this->title_fr : $this->title_ar;
}
}
FYI above is just a demo on what can be done. For a full blow solution I would recommend decorator pattern.
Also it might be worth considering using morph for things like that. You can have a service provider that will initiate the morph map for you post model relevant to the language that user has, I.e.
Class ModelProvider {
Protected $models = [
‘fr’ => [
‘post’ => App/Models/Fr/Post::class,
],
‘ar’ => [
‘post’ => App/Models/Ar/Post::class,
]
];
Public function boot() {
$language = Auth::user()->Settings->language;
Relation::morphMap($This->models[$language]);
}
}
Afterwards you just need to call to Relation::getMorphModel(‘post’) to grab Post class that will return correct language.
I.e. App/Models/Fr/Post can have a an attribute title:
Public function getTitleAttribute(): string {
Return $this->title_fr;
}
For example above you would also want to utilise interfaces to make sure that all models follow the same contract, something below would do the trick:
Interface I18nPostInterface {
Public function getTitleAttribute(): string
}
Also, depending on the database you use, to store titles (and other language data) in a JSON format in the database. MySQL 8 has an improve support for JSON data, but there are limitations with that.
So I was Able to fetch data from my database based on the Language selected by the user.
Like i said before I have a table called Posts and has columns id,title_fr and title_ar. I am using laravel Localization.
Inside my PostController in the index function i added this code:
public function index()
{
//
$post = Post::all();
$Frtitle = post::get()->pluck('title_fr');
$Artitle = post::get()->pluck('title_ar');
return view('post.index',compact('post','Frtitle','Artitle'));
}
if anyone has a better way then mine please let me know, i am sure
there is a better way.

Validate associated models in CakePHP2

I'm a noob in CakePHP and I've been trying to do some complex validations here:
I have the following models:
- Fonts (name, file);
- Settings(value1,value2,value3,type_id,script_id);
- Types(name)
Whenever I create a Font I also create a default setting associated to it. Also, this setting has a type associated. After the Font is created I can associate more settings to it (Font hasMany Settings), but I need to make sure that two settings of the same type are not added to that font. I don't know how to handle this case. Any help is appreciated. Thanks.
I'd use a simple beforeSave validation
//in setting.php model
public function beforeSave($options = array()) {
if (isset($this->data[$this->alias]['font_id']) && isset($this->data[$this->alias]['type_id']) {
$otherSettings = $this->find('all', array('conditions'=>
array('type_id'=>$this->data[$this->alias]['type_id'],
'font_id'=>$this->data[$this->alias]['font_id']);
//check if it's insert or update
$updated_id = null;
if ($this->id)
$updated_id = $this->id;
if (isset($this->data[$this->alias][$this->primaryKey]))
$updated_id = $this->data[$this->alias][$this->primaryKey];
if (count($otherSettings) > 0) {
if ($updated_id == null)
return false; //it's not an update and we found other records, so fail
foreach ($otherSettings as $similarSetting)
if ($updated_id != $similarSetting['Setting']['id'])
return false; //found a similar record with other id, fail
}
}
return true; //don't forget this, it won't save otherwise
}
That will prevent inserting new settings to the same font with the same type. Have in mind that this validation will return false if the validation is incorrect, but you have to handle how you want to alert the user of the error. You can throw exceptions from the beforeSave and catch them in the controller to display a flash message to the user. Or you could just not save those settings and let the user figure it out (bad practice).
You could also create a similar function in the model like checkPreviousSettings with a similar logic as the one I wrote above, to check if the settings about to be saved are valid, if not display a message to the user before attempting a save.
The option I prefer is the exception handling error, in that case you'd have to replace the return false with
throw new Exception('Setting of the same type already associated to the font');
and catch it in the controller.
Actually, the better approach is to not even display the settings with the same type and font to the user, so he doesn't even have the option of choosing. But this behind-the-scenes validation would also be needed.

how to set default prefix in controller -> skip defining prefix in links

I have the following idea: I'd like to be able to define the default prefix in any given controller. So let's say the default prefix for the CitiesController implements all actions with the "admin" prefix ("admin_index", "admin_add", etc.), but the ProvincesController implements all actions with the
"superadmin" prefix ("superadmin_index", "superadmin_add", etc.)
The problem with this is, every time I want to link to any "city stuff", I have to specify "admin" => "true". Any time I want to link to any "province stuff", I have to specify
"superadmin" => "true".
That's already quite a bit of work initially, but if I decided I wanted to change the prefix from "admin" to "superadmin" for cities, it would be even more work.
So I was wondering if there's to somehow do something along the lines of:
class CitiesController extends AppController {
var $defaultPrefix = "admin"
}
And then in the HTML helper link function, do something like:
class LinkHelper extends AppHelper {
public $helpers = array('Html');
function myDynamicPrefixLink($title, $options) {
// check whether prefix was set (custom function that checks all known prefixes)
if (! isPrefixSet($options)) {
// no clue how to get the controller here
$controller = functionToGetControllerByName($options['controller']);
// check whether controller has a defined default prefix
$prefix = $controller->defaultPrefix;
if ($prefix) {
// set the given prefix to true
$options[$prefix] = true;
}
// use HTML helper to get link
return $this->Html->link($title, $options);
}
}
I just have no clue how to get from the helper to the controller of the given name dynamically.
Another option would be to store the default prefix somewhere else, but for now I feel that the best place for this would be in any given controller itself.
Another idea would be to even have that look up function dependent on both, the controller and the action, and not just the controller.
You should be able to use the Router::connect to supply defaults (see code and documentation on Github: link) to specify default prefixes for certain controllers and even actions.
However, if you want more flexibility than the current Router provides, you can extend your use of the Router::connect by specifying an alternate Route class to use:
Router::connect(
'/path/to/route',
array('prefix' => 'superadmin'),
array('routeClass' => 'MyCustomRouter')
);
Examples of this can be seen in the CakePHP documentation.

Detect if admin prefix is true from Model

I have made a custom afterFind function in a model, but I just want it to execute it if NOT in admin mode.
public function afterFind($results) {
if(Configure::read('Routing.admin')){
return $results;
}else{
return $this->locale(&$results);
}
}
But it doesn't seems to work. I'm thinking this might not be possible. Any idea?
checking on the core Configure settings doesnt make sense to me.
besides the fact that that 'Routing.admin' is deprecated - its Prefix.admin.
it only stores the prefixes that cake uses.
If you really want to you can store the information in configure::read() in beforeFilter() of your AppController and read it from your model again.
But it would need to something that does not conflict with your settings.
So if you use Prefix you probably could use Routing again:
//beforeFilter - prior to any model find calls!
$isAdmin = !empty($this->params['admin']);
Configure::write('Routing.admin', $isAdmin);
the other option you always have is to pass the information on to the model.
Router::getParam('prefix', true) gives you current request prefix value.
public function afterFind($results, $primary = false) {
return Router::getParam('prefix', true) == 'admin' ? $results : $this->locale(&$results);
}
Tested with Cake 2.4.

Cakephp Localization, Cannot Change language when DEFAULT_LANGUAGE is set

I am confused :)
I'm using the p18n component in cakephp found here:
http://www.palivoda.eu/2008/04/i18n-in-cakephp-12-database-content-translation-part-2/
This component requires me to set in core.php the following constant:
define("DEFAULT_LANGUAGE", 'eng')
However when this is set I cannot change the language using:
Configure::write('Config.language', 'eng');
At the moment, into my knowledge, the only way to change the locale of my static content is the use of the Configure::write. But for the dynamic content to change through the use of the p28n component I must have the DEFINE_LANGUAGE constant set to a value.
This is all very confusing. Any help will be much appreciated.
I'm not familiar with particular component, but I've done this "manually" by setting the same constant in my app/config/bootstrap.php file and then setting the "actual" language to be used in my AppController (copied from the core code to app/app_controller.php). The appropriate snippets of that controller look like this:
uses ( 'L10n' );
class AppController extends Controller {
public function beforeFilter() {
$this->_setLanguage();
/**
* Set the default "domain" for translations. The domain is the
* same as the po file name in a given locale directory. e.g.
* __d ( 'homepage', 'message_id' ) would look for the
* message_id key in homepage.po. Using the __() convenience
* function will always look in default.po.
*/
$this->set ( 'domain', 'default' );
}
private function _setLanguage() {
$this->L10n = new L10n();
# Auto-detect the request language settings
$this->L10n->get();
}
}
Pretty vanilla stuff, but it works great. And breaking out the _setLanguage() method allows for the use of different methodologies to determine locale (e.g subdomain like fr.mydomain.com).

Resources