Skip to content
This repository has been archived by the owner on Jul 16, 2021. It is now read-only.

[Bug/Proposal] Locale missing from queues / queued emails and listeners #394

Closed
steve-rhodes opened this issue Jan 29, 2017 · 52 comments
Closed

Comments

@steve-rhodes
Copy link

steve-rhodes commented Jan 29, 2017

I develop a multi language app. To my disappointment I just noticed that the locale doesn't get passed into queued emails! They launch always with the default "en" locale.

I would have thought that passing on the locale is a default feature. Any queued job should be launched under the original locale that launched it. Kind of common sense, no? Is saw this issue filed by several people as a bug (which I think it is too), but closed by Graham right away.

@steve-rhodes steve-rhodes changed the title Locale missing from queues / queued emails and listeners [Bug/Proposal] Locale missing from queues / queued emails and listeners Jan 29, 2017
@tomschlick
Copy link

I don't think it's the responsibility of the queue to know what the locale of the request that set it was.

Thats something you should pass in as a parameter if needed.

@steve-rhodes
Copy link
Author

Your answer makes no sense. It's like these theoretical discussions about best practices that lack common sense.

This leads to a lot of unnecessary code duplication for no reason, because I have to retrieve the locale, pass it in and set it in every job, when Laravel could do this for me automatically. Especially since even people not needing this feature, it won't hurt them.

It's also something people would assume happens automatically when they make they emails and notifications queueable. But now they get a nasty surprise when all of a sudden their language files work no more! Very "non-Laravel".

@tomschlick
Copy link

tomschlick commented Jan 30, 2017 via email

@steve-rhodes
Copy link
Author

You are saying that 95% of Laravel apps are not using more than one language? Where do you get that number from? Just interested... The memory a 2-byte locale consumes in a queue must be marginal!

Curious to see how other people out there feel about this... but it's Taylor's show the end of the day...

@CyrilMazur
Copy link

I have the same problem. Here's what I did:

In my base Mailable class:

namespace App\Mail;

use Illuminate\Mail\Mailable as NativeMailable;
use Illuminate\Support\Facades\App;

class Mailable extends NativeMailable
{
    /**
     * Locale used for rendering this email.
     *
     * @var string
     */
    public $locale;

    /**
     * Create a new message instance.
     */
    public function __construct()
    {
        $this->locale = App::getLocale();
    }
}

Then in my email view:

<h1>{{ trans('emails.welcome', null, $locale) !!}</h1>

The $locale variable is directly accessible in the view because it's a property of the Mailable object.

That's the best I could come up with. I agree something like this should be embedded in Laravel by default. It's especially cumbersome to have to add the $locale parameter to all the calls to the trans() helper in all the email views (in my case I have 50+ email views and they are all localised).

@barryvdh
Copy link

I agree it would make sense to preserve the locale in the queues.

@dvlpp
Copy link

dvlpp commented Jan 30, 2017

The solution of @CyrilMazur, which is I think the only way to get around it, won't cover the default date format or number format: you'll have to add a setlocale() before the view generation, too...

@sisve
Copy link

sisve commented Jan 30, 2017

You would need to have a listener for the JobProcessing event that calls App::setLocale($job->locale), and a listener for JobProcessed that resets the locale back to default.

@steve-rhodes
Copy link
Author

I know how to do it. But I think this is something that Laravel should do for all queues.

@tomschlick
Copy link

I'd be ok with this if it was optional like the onQueue() method.

For instance you might do it this way:

dispatch((new SendWelcomeEmail($user))->onQueue('emails')->setLocale());

In that case, the setLocale() method would by default pull from the current request if an argument was not provided.

@barryvdh
Copy link

When would you ever not want your jobs to have the current locale?

@RemiCollin
Copy link

Definitely; the Locale should be set in queues by reading the application's configuration/environment variable. If not this should clearly be addressed as a bug.

@steve-rhodes
Copy link
Author

When would you ever not want your jobs to have the current locale?

@barryvdh exactly! Because the answer is "never" it is a bug/oversight.

@steve-rhodes
Copy link
Author

steve-rhodes commented Jan 30, 2017

@RemiCollin It should be set/passed into the queue/job to whatever App::getLocale() returns and not what the environment shows, because I set the locale in a service provider depending on where the user is from.

@RemiCollin
Copy link

RemiCollin commented Jan 30, 2017

@elasticsteve : I agree with you ; that's what I meant by reading from Env/Config ;)

    /**
     * Get the current application locale.
     *
     * @return string
     */
    public function getLocale()
    {
        return $this['config']->get('app.locale');
    }

@steve-rhodes
Copy link
Author

@RemiCollin Yes, of course the Facade is setting app.locale. I wasn't thinking...

@tomschlick
Copy link

When would you ever not want your jobs to have the current locale?

While it makes sense for mail, its useless for other types of jobs like calculating statistics, processing web hooks, etc.

Setting locale on the jobs makes sense if you explicitly set the app locale to something other than the default though.

@barryvdh
Copy link

Yes but it would never hurt to pass it along, even if it's just the default.

@RemiCollin
Copy link

@tomschlick : not everybody has en as default locale in their app, which is the main issue here, regardless of the multilingual mail use case.

@tomschlick
Copy link

@RemiCollin I get that, which is why I don't think it would be a bad thing to do it if the current locale != the default.

@LasseRafn
Copy link

I ended up having different applications for each language and setting language in .env file.

@barryvdh
Copy link

barryvdh commented Feb 9, 2017

And this would be another one to make locales easier with notifications :)
#409

@sebastiaanluca
Copy link

So… what's the status of this issue? Running into the same problem.

@tomschlick
Copy link

No one has submitted a PR yet.

I wouldn't personally use it but I can see the need for it. I would suggest including the locale if it != the default set in config/app.php

@sebastiaanluca
Copy link

I wrongfully assumed that if I queue a mail(able) when the locale was set to French, it'd be sent in French and not the fallback locale. So basically you get a different result when opting to use a queue, which is not desired and can lead to weird situation few people are aware of. IMO a bug as previously mentioned.

Currently storing the current application's locale in a public class variable in the (queueable) mailable's constructor and restoring it when it gets triggered. Not sure if there's a better way to do this, seems highly redundant having to call 2 extra methods per queued job.

@taylorotwell
Copy link
Member

Anyone have any ideas on how this would actually even be implemented?

@sebastiaanluca
Copy link

sebastiaanluca commented Mar 15, 2017

Since each queued job is completely isolated from the rest, would it be possible to store the current application's locale in the payload (of course separated from the public class variables) and then re-set the locale before the job is executed? If the queue is running the app as a daemon, you then might have to reset the locale back to what it was before. Perhaps we'd also need to implement a configurable option to keep this functionality disabled by default as to prevent breaking changes.

@LasseRafn
Copy link

LasseRafn commented Mar 15, 2017 via email

@barryvdh
Copy link

Indeed, add the locale to the payload en re-set the locale on fire?

Something like this? (not tested)

Queue/Queue.php
protected function createObjectPayload($job)
{
	return [
		'displayName' => $this->getDisplayName($job),
		'job' => 'Illuminate\Queue\CallQueuedHandler@call',
		'maxTries' => isset($job->tries) ? $job->tries : null,
		'timeout' => isset($job->timeout) ? $job->timeout : null,
		'locale' => isset($job->locale) ? $job->locale : config('app.locale'),
		'data' => [
			'commandName' => get_class($job),
			'command' => serialize(clone $job),
		],
	];
}

Queue/Jobs/Job.php
/**
 * Fire the job.
 *
 * @return void
 */
public function fire()
{
	$payload = $this->payload();
	config(['app.locale' => $payload['locale']]);
	list($class, $method) = JobName::parse($payload['job']);
	with($this->instance = $this->resolve($class))->{$method}($this, $payload['data']);
}

@LasseRafn
Copy link

@barryvdh will config('app.locale') output the same ass \App::getLocale(); — For example if locale has been changed using \App::setLocale()

@sebastiaanluca
Copy link

@LasseRafn Was thinking the same, but double checked and app()->getLocale() uses config()->get(). Of course we could opt to use app()->getLocale() in case the user extended it somehow.

@barryvdh
Copy link

No, you're right, that would be more appropriate. (getLocale does return the same thing, but setLocale also updates the translator and fires an event). See https://github.com/laravel/framework/blob/075220049df5859a39a05df02f74a36f20079d23/src/Illuminate/Foundation/Application.php#L1025-L1048
Updated the snippet

@Christophvh
Copy link

I would love to see a feature like this. But i feel that this would still have issues, What if we have a application where users could set their preferred language . If a French user is logged in the application the App::getLocale() would be French. But if this user triggers emails/notifications to other users , those emails would be in the French language even if the user that receives this notification wants his emails in english.

I would love to have the ability to specify the language to a notification/mailable.

@sebastiaanluca
Copy link

Currently finishing up a medium project that required translated (queued) notifications. Each notification can be sent to different recipients who each have their preferred language, but can also be sent to an entire group (as a thread message) for which it takes the language of the group creator.

So basically I needed to clone each notification per recipient, set the language of the notification for that user, set the application's locale for the duration of it, have everything the recipient receives translated correctly, and then reset the locale so it won't interfere with other translations as part of that job/notification. It was a real hassle to end up with the somewhat hacky solution I have now. Would love localization support in queued jobs, notifications, etc.

@fsasvari
Copy link

fsasvari commented Sep 4, 2017

Still no solution ?

@sylouuu
Copy link

sylouuu commented Sep 4, 2017

@fsasvari Create a $locale in your Mailable class, then pass it.

new AnyMailableClass($arg1, $arg2, app()->getLocale());

@fsasvari
Copy link

fsasvari commented Sep 4, 2017

No other way ? Bugger...

Then I need to use trans('key', [], $locale); ?

@sylouuu
Copy link

sylouuu commented Sep 4, 2017

@fsasvari This solution is OK. Type-hint $locale in your class from your __construct and use trans('key', [], $this->locale); in the build() method.

Best

@barryvdh
Copy link

barryvdh commented Sep 4, 2017

You could set it using app()->setLocale($this->locale) and the translations should work for the rest of the command.

@fsasvari
Copy link

fsasvari commented Sep 4, 2017

Thanks @barryvdh it's working, placed app()->setLocale($this->locale) inside build() method ;)

@renepardon
Copy link

I'm really, really, really pissed.. hours of searching without finding a working solution.
I tried it the "hard" way not passing it; setting it directly (just for testing purpose) but even this one does NOT work for me within a queued email template:

<h2>{{ trans('Password reset instructions', [], 'de') }}</h2>

The corresponding translation is available within resources/lang/de/messages.php as array key:

<?php
return [
    'Password reset instructions' => 'Instruktionen zum Zurücksetzen des Passwortes',
];

I also tried to set locale with app()->setLocale('de') but without any effect. I'm desperate!

@sebastiaanluca
Copy link

sebastiaanluca commented Oct 2, 2017

@renepardon I struggled with that myself. It's important to know where you're setting the locale here. See this gist: https://gist.github.com/sebastiaanluca/acaec76770d13daa3f0845f35195238e. Regular notifications can be wrapped, e-mail notifications cannot as they get constructed after the mail message is returned.

@renepardon
Copy link

@sebastiaanluca I've tried it in the Mailable constructor, in the Event Listener constructor, within the handle method of the Listener, within the build method of a Mailable... works nowhere for me. :(

@renepardon
Copy link

renepardon commented Oct 2, 2017

This is how my Event listener and Mailable looks like. Maybe you see a mistake but I can't :)

Listener:
https://gist.github.com/renepardon/0086ca7a091acd5dffa72e6e229f0a69

Mailable:
https://gist.github.com/renepardon/ac49753590f1922bef6835cb6ea6b79d

This is how it look like now. But I've tried explicit 'de' as well which does not work too.

EDIT: I forgot to mention that setUserLocale() does nothing else than app()->setLocale('de') and Carbon::setLocale('de')

@sebastiaanluca
Copy link

Queued listeners might be a different case, my example works for queued notifications.

I suggest you debug the value of the app locale first. In your mailable's build method, log the value of app()->getLocale() to your laravel.log (using Log::debug()) after setting it to to see what's what. If you set the locale at the right time and it's not being reset, translations should be in that language.

@renepardon
Copy link

Hi @sebastiaanluca yes, I understood this one and already tried debugging the getLocale() which is 'de'. So I think the translations are just not loaded during job execution for some reason. I also tried throwing an exception, catched it and saw that only queue related stuff is executed. Nothing related to the application or translation.
Maybe there is a way to force loading of correct translation files?

I also thought about creating a mail template for every language but this makes no sense as I would like to grow rapidly and then I have templates just for translation?! ;)

Another idea would be to create a custom translation helper which explicitly requires the correct translation files?!

I really don't understand why such a huge framework like laravel does not support something like this.

@renepardon
Copy link

app()->getLocale() returns de as locale which is true.
app('translator') shows me that no translations are loaded. Neither en nor de. So there must be a way to force loading of the translations before handle() get's called or maybe within handle as first command?

@sebastiaanluca
Copy link

I think this might be something related to your app, not Laravel core. Setting the app locale and then calling trans('translation.key') or __('translation.key') should return the translation in that language. Try and follow the call from trans() to when the locale is being used to determine which translation (file) to load. Or maybe first check if a correct translation is returned outside of a queued listeners by setting the app locale and echo'ing or dumping a translated string.

@renepardon
Copy link

renepardon commented Oct 2, 2017

Ok, finally I gave up... I've created a helper trait which loads the messages.php manually based on provided locale and translate by replacing key with value....

Hope this helps someone else:
https://gist.github.com/renepardon/9d3408b1ede334e1c5fde65031463194

I just pass the current class as "translator" to the email view template and can then call:

{{ $translator->translate('translationkey') }}

@Morinohtar
Copy link

Any developtment on this? @barryvdh solution seems to be the best approach so far.

@sebastiaanluca
Copy link

Still using my RemembersLocale trait (https://gist.github.com/sebastiaanluca/acaec76770d13daa3f0845f35195238e), no other way. For a new project, I wrapped the notify method of the Notifiable trait to set the notification's language to the recipient's locale. This way each notification is correctly translated based on whom it's sent to.

I can now understand @taylorotwell's point though and don't see how this can implemented in a universal way. Maybe a basic feature where each queued job, handler, listener, and notification is in the same locale as when it was added to the queue. Then it's mostly the sender's locale that determines the translations to use, not the recipient's. Still better than the default app locale.

@taylorotwell
Copy link
Member

taylorotwell commented Feb 22, 2018

We have a merged implementation of this here: laravel/framework#23178

It is currently on latest dev of 5.6, we would appreciate if anyone could give us feedback before the next 5.6 tag.

Please direct further discussion there.

@laravel laravel locked and limited conversation to collaborators Feb 22, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests