Is it possible to set translation default domain on a specified path? - translation

My site is in 2 parts, public side with differents locales is working, admin side is translated in only 1 language.
The problem is that the lang file for public side is the same as admin side. I would like to use a different file for all admin routes
I tried to change the domain with {% trans_default_domain "admin" %} but i need to put this line in all my twig files. I didn't find any solution to change the domain in the controller or elsewhere.
I tried also to use a specific locale for admin but the translations are not found and 'admin' is clearly not a language
Thanks for your help :)

I found an half-solution by replacing the Translator class, this post helps me Replacing the Translator service in Symfony 3
This solution change de translation domain for all the methods in the controller and the templates called in it (even the extended template and the includes)
My translations file is 'admin.fr.yml'
I added in Service folder my new Translator class
<?php
namespace App\Service;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Translation\Translator as BaseTranslator;
class Translator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface
{
private $defaultDomain = 'messages';
private $translator;
public function __construct(BaseTranslator $translator)
{
$this->translator = $translator;
}
public function getDefaultDomain(): string
{
return $this->defaultDomain;
}
public function setDefaultDomain(string $defaultDomain): Translator
{
$this->defaultDomain = $defaultDomain;
return $this;
}
public function trans($id, array $parameters = [], $domain = null, $locale = null)
{
if (null === $domain) {
$domain = $this->defaultDomain;
}
return $this->translator->trans($id, $parameters, $domain, $locale);
}
public function getCatalogue($locale = null)
{
return $this->translator->getCatalogue($locale);
}
public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null)
{
return $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
}
public function setLocale($locale)
{
return $this->translator->setLocale($locale);
}
public function getLocale()
{
return $this->translator->getLocale();
}
}
Then in services.yaml
App\Service\Translator:
decorates: translator
Then in my admin controller folder i created an abstract class BaseAdminController
<?php
namespace App\Controller\Admin;
use App\Service\Translator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Contracts\Translation\TranslatorInterface;
abstract class BaseAdminController extends AbstractController
{
/**
* #var Translator
*/
protected $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
$this->translator->setDefaultDomain('admin');
}
}
Now in each controller where i want admin translations, i just need to replace "extends AbstractController" with "extends BaseAdminController"
If i want to define a construct, i call the parent in it
public function __construct(Translator $translator, EntityManagerInterface $manager)
{
$this->manager = $manager;
parent::__construct($translator);
}

Related

Blazor IStringLocalizer injection to services

I am using IStringLocalizer approach to localize my Blazor app as discussed here.
Injecting the IStringLocalizer on razor pages works great. I also need this to localize some services - whether scoped or even singleton services.
Using constructor injection to inject my IStringLocalizer service into the service works. However, when users change the language via UI, the service (whether singleton or scoped) keeps the initial IStringLocalizer - i.e. the one with the original language used when starting the app, not the updated language selected by the user.
What is the suggested approach to retrieve the updated IStringLocalizer from code?
EDIT
To prevent more details, here is some piece of code.
First, I add a Resources folder and create there a default LocaleResources.resx (with public modifiers) and a LocaleResources.fr.resx file, which contain the key-value pairs for each language.
Supported cultures are defined in the appsettings.json file as
"Cultures": {
"en-US": "English",
"fr": "Français (Suisse)",
...
}
In startup, I register the Resources folder and the supported cultures :
public void ConfigureServices(IServiceCollection services {
...
services.AddLocalization(options => options.ResourcesPath = "Resources");
...
services.AddSingleton<MySingletonService>();
services.AddScoped<MyScopedService>();
}
// --- helper method to retrieve the Cultures from appsettings.json
protected RequestLocalizationOptions GetLocalizationOptions() {
var cultures = Configuration.GetSection("Cultures")
.GetChildren().ToDictionary(x => x.Key, x => x.Value);
var supportedCultures = cultures.Keys.ToArray();
var localizationOptions = new RequestLocalizationOptions()
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
return localizationOptions;
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
...
app.UseRequestLocalization(GetLocalizationOptions());
...
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
I created an empty LocaleResources.razor control at the root of the project (this is a trick used to inject a single resource file to all components).
I included a routing controller to change language :
[Route("[controller]/[action]")]
public class CultureController : Controller {
public IActionResult SetCulture(string culture, string redirectUri) {
if (culture != null) {
HttpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(
new RequestCulture(culture)));
}
return LocalRedirect(redirectUri);
}
}
And the language UI switcher looks like this (I use SyncFusion control here, but it could be any lookup actually, that shouldn't really matter)
#inject NavigationManager NavigationManager
#inject IConfiguration Configuration
<SfComboBox TValue="string" TItem="Tuple<string, string>" Placeholder="Select language" DataSource="#Cultures"
#bind-Value="selectedCulture" CssClass="lan-switch" Width="80%">
<ComboBoxFieldSettings Text="Item2" Value="Item1"></ComboBoxFieldSettings>
</SfComboBox>
<style>
.lan-switch {
margin-left: 5%;
}
</style>
#code {
string _activeCulture = System.Threading.Thread.CurrentThread.CurrentCulture.Name;
private string selectedCulture {
get => _activeCulture;
set {
_activeCulture = value;
SelectionChanged(value);
}
}
List<Tuple<string, string>> Cultures;
protected override void OnInitialized() {
var cultures = Configuration.GetSection("Cultures")
.GetChildren().ToDictionary(x => x.Key, x => x.Value);
Cultures = cultures.Select(p => Tuple.Create<string, string>(p.Key, p.Value)).ToList();
}
protected override void OnAfterRender(bool firstRender) {
if (firstRender && selectedCulture != AgendaSettings.SelectedLanguage) {
selectedCulture = AgendaSettings.SelectedLanguage;
}
}
private void SelectionChanged(string culture) {
if (string.IsNullOrWhiteSpace(culture)) {
return;
}
AgendaSettings.SelectedLanguage = culture;
var uri = new Uri(NavigationManager.Uri)
.GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
var query = $"?culture={Uri.EscapeDataString(culture)}&" +
$"redirectUri={Uri.EscapeDataString(uri)}";
NavigationManager.NavigateTo("/Culture/SetCulture" + query, forceLoad: true);
}
}
Finally, to the injection. I inject the IStringLocalizer to pages as follows and it works perfectly fine on razor controls:
#inject IStringLocalizer<LocaleResources> _loc
<h2>#_loc["hello world"]</h2>
Above, when I change language, the page displays the value in the corresponding resource file.
Now, to services: the MySingletonService and MyScopedService are registered at startup. They both have a constructor like
protected IStringLocalizer<LocaleResources> _loc;
public MySingletonService(IStringLocalizer<LocaleResources> loc) {
_loc = loc;
}
public void someMethod() {
Console.WriteLine(_loc["hello world"])
}
I run someMethod on a timer. Strangely, when I break on the above line, the result seems to oscillate : once it returns the default language's value, once the localized one...!
The answer to my question was: your code is correct!
The reason, I found out, is that I use a Scoped service that is started on the default App's start page:
protected async override Task OnAfterRenderAsync(bool firstRender) {
if (firstRender) {
MyScopedService.StartTimer();
}
await base.OnAfterRenderAsync(firstRender);
}
When users change language, the whole page is refreshed and a new instance of the scoped service is created and timer started. As my service did not implement IDisposable, the timer was not actually stopped.
So 2 solutions here:
use singleton services
make servcie disposable and ensure tasks are cancelled when service is disposed of.

zf2 factories for populating controller with domain objects

I have two different ways of loading my controller with it's domain model. I'd be interested in hearing which is better.
First method - traditional.
A controller factory injects the required service into the controller constructor. Within the controller action, the model is loaded based on the request param:
ClientAppointmentsControllerFactory.php
class ClientAppointmentsControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator) {
$serviceManager = $serviceLocator->getServiceLocator();
$controller = new ClientAppointmentsController($serviceManager->get('Service\ClientAppointments'));
return $controller;
}
}
ClientAppointmentsController.php
class ClientAppointmentsController extends AbstractActionController
{
public function __construct(AppointmentFactory $appointmentFactory){
$this->appointmentFactory = $appointmentFactory;
}
public function indexAction() {
$viewModel = $this->acceptableViewModelSelector($this->acceptCriteria);
$appointments = $this->appointmentFactory->getClientAppointments($this->params()->fromRoute('clientId'));
$viewModel->setVariables([
'appointments' => $appointments
]);
return $viewModel;
}
}
Second Method - Accessing request/route parameters in factory
This seems a bit cleaner to me, as now the controller has no dependency on the service layer, and just expects (from whatever source) an array of loaded objects to pass to the view. I think this still fits the definition of a factory, since it is creating the controller with it's required dependencies, although is now actively creating them instead of passing this onto the controller to do:
ClientAppointmentsControllerFactory.php
class ClientAppointmentsControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator) {
$getRequestParam = function($param) use($serviceLocator){
$serviceManager = $serviceLocator->getServiceLocator();
$request = $serviceManager->get('Request');
$router = $serviceManager->get('Router');
$match = $router->match($request); // \Zend\Mvc\Router\RouteMatch
$result = $match->getParam($param);
return $result;
};
$serviceManager = $serviceLocator->getServiceLocator();
$clientService = $serviceManager->get('Service\ClientAppointments');
$appointments = $clientService->fetchByClientId($getRequestParam('clientId));
$controller = new ClientAppointmentsController($appointments);
return $controller;
}
}
ClientAppointmentsController.php
class ClientAppointmentsController extends AbstractActionController
{
/**
* #param Array $appointments Array of Appointment objects
*/
public function __construct(Array $appointments){
$this->appointments = $appointments
}
public function indexAction() {
$viewModel = $this->acceptableViewModelSelector($this->acceptCriteria);
$viewModel->setVariables([
'appointments' => $this->appointments
]);
return $viewModel;
}
Which is better?
(I also have an idea of a mutable factory floating around.)
IMO, the second is not good at all, because it mixes creation logic with business logic. This means a business logic error will preclude a factory from working.
The first one is better, but not good, because you have got now business logic in the controller.
I would suggest moving business logic into a business model OR to a controller plugin.

unable to get route name from url in zend framework 2

I'm using Zend Framework 2, this is my code:
$prevsingletrackurl = $this->getRequest()->getHeader('Referer')->getUri();
From this code which is defined in a controller's method I'm getting previous URL, now what I need to know some information of this from this, it contains which route, controller, method for my own requirement.
You can use the route stack itself to try to match it. It will return you a RouteMatch if the request object matched:
use Zend\Mvc\Router\RouteMatch;
$referer = $this->getRequest()->getHeader('Referer')->getUri();
$request = clone $this->getRequest();
$request->setUri($referer);
$match = $routeStack->match($request);
if ($match instanceof RouteMatch) {
$route = $match->getMatchedRouteName();
}
You can access the route stack ("router") from the service locator; in the root service locator, it is registered as 'Router'. You can inject the route stack in your factory. For instance in a Controller:
use MyModule\Controller\MyController;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class MyControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $sl)
{
$router = $sl->getServiceLocator()->get('Router');
$controller = new MyController($router);
return $controller;
}
}
Use it in your controller as this:
use Zend\Mvc\Controller\AbstractActionController;
use Zend\Mvc\Router\Http\TreeRouteStack;
class MyController extends AbstractActionController
{
protected $router;
public function __construct(TreeRouteStack $router)
{
$this->router = $router;
}
protected function getRouter()
{
return $this->router;
}
}
This code i have used inside a controller action method
$request = $this->getRequest();
$getHeaderReferer=$request->getHeader('Referer');
if (!empty($getHeaderReferer))
{
$prevsingletrackurl = $getHeaderReferer->getUri(); // get previous url
//echo "<br>previous usrl=>".$prevsingletrackurl;
$controller = $this->getEvent()->getRouteMatch()->getParam('controller'); // get controller name
//**** code to get previous url route , controller name , action/method name starts
$request->setUri($prevsingletrackurl);
$router =$this->getServiceLocator()->get('Router');// this gives instance of /Zend/Mv/Router/Http/TreeRouteStack
//echo "<br>router=>".$router->toString();
$routeMatch = $router->match($request); // this gives instance of /Zend/Mv/Router/Http/RouteMatch
$routename="";
if( $routeMatch instanceof RouteMatch )
{
$urlwholeroutedataAr=$routeMatch->getParams();
if(!empty($urlwholeroutedataAr))
{
$controllerdata=$urlwholeroutedataAr['controller'];
$actionmethod=$urlwholeroutedataAr['action'];
if(array_key_exists('trackdata',$urlwholeroutedataAr))
{
$trackdata=$urlwholeroutedataAr['trackdata'];
}
$controllerdataAr=explode("\\",$controllerdata);
if(count($controllerdataAr)>0)
{
$controllername=end($controllerdataAr);
}
}
$routename=$routeMatch->getMatchedRouteName();
}
}

MVC grid sorting - customise links

I am using the Sort method of the MvcContrib Grid to generate sorting links, e.g.
<%= Html.Grid(Model).AutoGenerateColumns().Sort((GridSortOptions)ViewData["sort"]) %>
I have a need to change the default controller/action that’s generated by the sort method. For example,
defaultControllerName/defaultActionName/?Column=ProductId&Direction=Ascending
would change to
customControllerName/customActionName/?Column=ProductId&Direction=Ascending
I haven't been able to find any existing methods in the MVCcontribution classes that would allow me to customise the links. I’d appreciate any pointers on how to go about altering the default links as I’m still very much a MVC/C# newbie.
That's not an easy task. You will need a custom grid renderer to achieve this and override the RenderHeaderText method:
public class MyHtmlTableGridRenderer<T> : HtmlTableGridRenderer<T> where TViewModel : class
{
protected override void RenderHeaderText(GridColumn<TViewModel> column)
{
if (IsSortingEnabled && column.Sortable)
{
// TODO: generate a custom link here based on the sorting options
string text = ...
base.RenderText(text);
}
else
{
RenderText(column.DisplayName);
}
}
}
And then specify that the grid should use this renderer:
.RenderUsing(new MyHtmlTableGridRenderer<Employee>())
I wanted to provide a complete working example:
public class SortableHtmlTableGridRenderer<T> : HtmlTableGridRenderer<T> where T : class
{
readonly string _action;
readonly string _controllerName;
public SortableHtmlTableGridRenderer(string action, string controllerName)
{
_action = action;
_controllerName = controllerName;
}
protected override void RenderHeaderText(GridColumn<T> column)
{
if (IsSortingEnabled && column.Sortable)
{
string sortColumnName = GenerateSortColumnName(column);
bool isSortedByThisColumn = GridModel.SortOptions.Column == sortColumnName;
var sortOptions = new GridSortOptions
{
Column = sortColumnName
};
if (isSortedByThisColumn)
{
sortOptions.Direction = (GridModel.SortOptions.Direction == SortDirection.Ascending)
? SortDirection.Descending
: SortDirection.Ascending;
}
else //default sort order
{
sortOptions.Direction = column.InitialDirection ?? GridModel.SortOptions.Direction;
}
var routeValues = HtmlHelper.AnonymousObjectToHtmlAttributes(new {sortOptions.Column, sortOptions.Direction });
var text = HtmlHelper.GenerateLink(Context.RequestContext, RouteTable.Routes, column.DisplayName, null, _action, _controllerName, routeValues, null);
RenderText(text);
}
else
{
RenderText(column.DisplayName);
}
}
}
Usage:
.RenderUsing(new SortableHtmlTableGridRenderer<YourModelType>("Search", "Search"))

Taking advantage of Doctrine relations in frontend applications in Symfony

Let's consider the following simple schema (in Doctrine, but Propel users are welcome too):
User:
columns:
name: string
Article:
columns:
user_id: integer
content: string
relations:
User:
local: user_id
foreign: id
Now, if you create a route for Article model and generate a module via doctrine:generate-module-for-route frontend #article_route you get a CRUD application that manages all the articles. But in frontend you would normally want to manage objects related to signed-in User, so you have to manually get the id of the User, pass id to the model and write a bunch of methods that would retrieve objects related to this User, for example:
public function executeIndex(sfWebRequest $request)
{
$this->articles = Doctrine::getTable('Articles')
->getUserArticles(this->getUser());
}
public function executeShow(sfWebRequest $request)
{
$this->article = $this->getRoute()->getObject();
if (!$this->article->belongsToUser($this->getUser()))
{
$this->redirect404();
}
}
and model:
class ArticleTable extends Doctrine_Table
{
public function getUserArticles(sfUser $user)
{
$q = $this->createQuery('a')
->where('a.user_id = ?', $user->getId());
return $q->execute();
}
}
class Article extends BaseArticle
{
public function belongsToUser(sfUser $user)
{
return $this->getUserId() == $user->getId();
}
}
This is trivial stuff and yet you have to manually write this code for each new relation. Am I missing some kind of way to take advantage of Doctrine relations? Anyways, how would you do it? Thank you.
I believe you should be able to do this with a custom routing class. I have never done this, but there is a tutorial in the More with Symfony book: Advanced Routing. My guess is that it should look something like this:
class objectWithUserRoute extends sfDoctrineRoute
{
public function matchesUrl($url, $context = array())
{
if (false === $parameters = parent::matchesUrl($url, $context))
{
return false;
}
$parameters['user_id'] = sfContext::getInstance()->getUser()->getId();
return array_merge(array('user_id' => sfContext::getInstance()->getUser()->getId()), $parameters);
}
protected function getRealVariables()
{
return array_merge(array('user_id'), parent::getRealVariables());
}
protected function doConvertObjectToArray($object)
{
$parameters = parent::doConvertObjectToArray($object);
unset($parameters['user_id']);
return $parameters;
}
}
You would then need to set the routing class in routing.yml to use objectWithUserRoute. I haven't tested this, but I think it is the best way to go about solving the problem.

Resources