Lokalizace cest
Při lokalizaci webových aplikací se zapomíná no možnost lokalizace cest. Nedivím se, ono je to už trochu složitější a náročnější, ale přeci jen je to něco, co často chybí k úplné lokalizaci aplikace.Pro zjednodušení předpokládám, že příchozí request je zpracováván v kultuře dle klienta, tedy volajícího (lze si nastavit v browseru a dále je uvedeno i jak.
Pro ASP.NET lze takové pravidlo nastavit jednoduše v configu, tedy v souboru v cestě configuration/system.web:
<globalization enableClientBasedCulture="true" culture="auto:en-US" uiCulture="auto:en" resourceProviderFactoryType="MvcSimplyCleverPart3.MvcResourceProviderFactory, MvcSimplyCleverPart3"/>
Výchozí vzor pro cesty
Výchozí vzor používaný v MVC vypadá takto:{controller}/{action}/{id}
a MVC používá následující výchozí hodnoty
- controller - Home
- action - Index
- Id - nepovinny
Takto podobně vypadá standardní definice routování v MVC:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Pokud si chceme lokalizovat i cesty, musíme použít vlastní třídu pro definici cest a vlastní handler, výše uvedený kód se tedy změní na:
public static void RegisterLocalizableRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new LocalizableRoute("{controller}/{action}/{id}", new LocalizableControllerNameMvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = UrlParameter.Optional })
});
}
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new LocalizableRoute("{controller}/{action}/{id}", new LocalizableControllerNameMvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = UrlParameter.Optional })
});
}
Poznámka: důvod pro použíti LocalizableRoute je vysvětlen níže v článku
Aby dával vlastní MvcRouteHandler smysl (všimněte si, že v přizpůsobeném kódě je použit LocalizableControllerNameMvcRouteHandler) , musíme nějak zajistit překlad cest - pro co nejjednoduší implementaci budu tedy předpokládate, že všechny v aplikaci používané cesty budou vycházed z výše uvedeného vzoru {controller}/{action} a aby byl návrh co nejjednodušší, uložím lokalizované názvy controlleru a metod do jazykových resource souborů a pomocí atributu určím každému controlleru a metodě v jakém resource a pod jakým klíčem má vyhledat překlad. Resource soubor tedy vypadá nějak takto:
A nyní popíši jednotlivé atributy, potřebné pro prosazení lokalizovaných názvů.
Atribut pro lokalizace názvů controllerů a metod
Úlohou tohoto atributu je vrátit název lokalizovaný název controleru a nebo jeho metody. Tento lokalizovaný název vyhledá v resource - a to dle jazyka, :
public class LocalizableControllerNameAttribute : Attribute
{
public LocalizableControllerNameAttribute(Type resourceType, string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Cannot be empty or null", "name");
}
if (resourceType == null)
{
throw new ArgumentNullException("resourceType");
}
this.Name = name;
this.ResourceType = resourceType;
}
public virtual string LocalizedControllerName
{
get
{
return GetPropertyValue(this.ResourceType, this.Name);
}
}
public string Name { get; private set; }
public Type ResourceType { get; private set; }
private static string GetPropertyValue(Type type, string propertyName)
{
PropertyInfo propertyInfo = type.GetProperty(propertyName,
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
if (propertyInfo == null)
{
return null;
}
return (string)propertyInfo.GetValue(null, new object[] { });
}
}
{
public LocalizableControllerNameAttribute(Type resourceType, string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Cannot be empty or null", "name");
}
if (resourceType == null)
{
throw new ArgumentNullException("resourceType");
}
this.Name = name;
this.ResourceType = resourceType;
}
public virtual string LocalizedControllerName
{
get
{
return GetPropertyValue(this.ResourceType, this.Name);
}
}
public string Name { get; private set; }
public Type ResourceType { get; private set; }
private static string GetPropertyValue(Type type, string propertyName)
{
PropertyInfo propertyInfo = type.GetProperty(propertyName,
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
if (propertyInfo == null)
{
return null;
}
return (string)propertyInfo.GetValue(null, new object[] { });
}
}
Pro lokalizace metod pak podědíme z atributu ActionNameSelector:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class LocalizableActionNameAttribute : ActionNameSelectorAttribute
{
public LocalizableActionNameAttribute(Type resourceType, string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Cannot be null or empty", "name");
}
if (resourceType == null)
{
throw new ArgumentNullException("resourceType");
}
this.Name = name;
this.ResourceType = resourceType;
}
public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
{
return string.Equals(actionName, this.LocalizedActionName, StringComparison.OrdinalIgnoreCase);
}
public string Name { get; private set; }
public Type ResourceType { get; private set; }
public string LocalizedActionName
{
get
{
return GetPropertyValue(this.ResourceType, this.Name);
}
}
private static string GetPropertyValue(Type type, string propertyName)
{
PropertyInfo propertyInfo = type.GetProperty(propertyName,
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
if (propertyInfo == null)
{
return null;
}
return (string) propertyInfo.GetValue(null, new object[] { });
}
}
public sealed class LocalizableActionNameAttribute : ActionNameSelectorAttribute
{
public LocalizableActionNameAttribute(Type resourceType, string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Cannot be null or empty", "name");
}
if (resourceType == null)
{
throw new ArgumentNullException("resourceType");
}
this.Name = name;
this.ResourceType = resourceType;
}
public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
{
return string.Equals(actionName, this.LocalizedActionName, StringComparison.OrdinalIgnoreCase);
}
public string Name { get; private set; }
public Type ResourceType { get; private set; }
public string LocalizedActionName
{
get
{
return GetPropertyValue(this.ResourceType, this.Name);
}
}
private static string GetPropertyValue(Type type, string propertyName)
{
PropertyInfo propertyInfo = type.GetProperty(propertyName,
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
if (propertyInfo == null)
{
return null;
}
return (string) propertyInfo.GetValue(null, new object[] { });
}
}
Vlastní RouteHandler - LocalizableControllerNameMvcRouteHandler
Ten vychází ze standardního MvcRouteHandleru. Ale před tím, než je zavolán, tak se pokusí vyhledat Controller, který má lokalizovaný název jenž odpovídá přijaté hodnotě z requestu.
V ní se vyhledá controller, u něhož metoda LocalizedControllerName atributu LocalizableControllerName vrátí hodnotu Kontakty.
Název takto nalezeného controlleru pak nahradí původní hodnotu uloženou v requestContext.RouteData.Values["Controller"] - v našem případě je tedy hodnota Kontakty nahrazena hodnotou Contact
Nalezení metody
V jednom z předchozích příspěvků byl zachycen i postup pří výběru metody kontroleru:
ale zde se používalo ActionMethodSelectorAttribute.
Výběr metody ale ve skutečnosti probíhá ve dvou fázích, v té první se vyberou všechny dostupné a možné metody, v druhé se pak vybere vlastní metoda.
V té první fázi se používá mimo jinéi ActionNameSelectorAttribute, respektive jeho metoda IsValidName. Ve druhé fázi se pak vybere jen jedna metoda a tam se uplatní i ten výše zachycený ActionMethodSelectorAttribute.
Zde je tedy použit atribut v první fázi a ten funguje podobně jako v případě controlleru - pokud se lokalizovaný název metody shoduje s hledaným názvem, tak je takto odekorovaná metoda vybrána a dále zpracovávaná.
Kód controlleru tak za uplatnění všeho předchozího vypadá takto:
[LocalizableControllerName(typeof(SimplyCleverResources), "ControllerContact")]
public class ContactController : Controller
{
[SetDefaultValue]
[LocalizableActionName(typeof(SimplyCleverResources), "ControllerContactIndex")]
public ActionResult Index()
{
return View("Index");
}
[HttpPost]
[LocalizableActionName(typeof(SimplyCleverResources), "ControllerContactSave")]
public ActionResult Save(AddressViewModel address)
{
if (!ModelState.IsValid)
return View("Index", address);
CountryProvider.Save(address.Cast());
var list = CountryProvider.GetUserAddresses().Cast();
return View("List", list);
}
[LocalizableActionName(typeof(SimplyCleverResources), "ControllerContactList")]
public ActionResult List()
{
var list = CountryProvider.GetUserAddresses().Cast();
return View("list",list);
}
}
public class ContactController : Controller
{
[SetDefaultValue]
[LocalizableActionName(typeof(SimplyCleverResources), "ControllerContactIndex")]
public ActionResult Index()
{
return View("Index");
}
[HttpPost]
[LocalizableActionName(typeof(SimplyCleverResources), "ControllerContactSave")]
public ActionResult Save(AddressViewModel address)
{
if (!ModelState.IsValid)
return View("Index", address);
CountryProvider.Save(address.Cast());
var list = CountryProvider.GetUserAddresses().Cast();
return View("List", list);
}
[LocalizableActionName(typeof(SimplyCleverResources), "ControllerContactList")]
public ActionResult List()
{
var list = CountryProvider.GetUserAddresses().Cast();
return View("list",list);
}
}
Asi je to vše příliš složitě popsáné, takže tady je stručný seznam změn:
Proto při registraci routy (viz ukázka na začátku stránky) používám vlastní třídu LocalizableRoute - ta dědí od Route, ale přetěžuji její metodu GetVirtualPath - což je právě metoda, která se stará o generování odchozích linků (tedy těch co se generují ve view jako v předchozí ukázce kódu):
Poznámka - další metodu, kterou můžeme přetížit, je GetRouteData - a můžeme si tak zjednodušit routování příchozích požadavků. Je to další způsob, jak řešit, co je popsáno v tomto článku. MVC je prostě hodně otevřený systém, který si lze vhodně přiohýbat.
Pokud se změní nastavení jazyka, tedy například v Chrome se jde do Nastavení, Rozšířených nastavení a klikne se na Nastavení jazyků:
tak musí být adresa zadána anglicky a i odkazy pod linky jsou anglicky (a políčka formuláře pochopitelně také):
- Nastavení web.config tak, aby se request zpracovával dle nastavení prohlížeče (zkrátka pokud je prohlížeč nastaven na češtinu, tak v cs-CZ)
- Vložení položek do resource tak, abychom měli pojmenování pro každý controller a metodu
- Použití atributů pro lokalizované controllery a atributy
- Definice vlastních routů, které budou zpracovávány vlastním handlerem
Výstupní cesty
Takto je ale zpracováno příchozích požadavků, ale každá aplikace si pak vyrábí vlastní linky, typicky takové view může vypadat takto:
<div class="header">
<ul id="menu">
<li>@Html.ActionLink(SimplyCleverResources.ContactLinkAdd, "Index", "Contact")</li>
<li>@Html.ActionLink(SimplyCleverResources.ContactLinkList, "List", "Contact")</li>
</ul>
</div>
<ul id="menu">
<li>@Html.ActionLink(SimplyCleverResources.ContactLinkAdd, "Index", "Contact")</li>
<li>@Html.ActionLink(SimplyCleverResources.ContactLinkList, "List", "Contact")</li>
</ul>
</div>
Takto vytvořené linky by měly být také lokalizovány, tj. asi by nemělo moc cenu vstupovat do aplikace voláním cesty Kontakty/Výchozí, když například první klikatelný link na úvodní stránce by změnil hodnotu na anglické Contact/List.
Proto při registraci routy (viz ukázka na začátku stránky) používám vlastní třídu LocalizableRoute - ta dědí od Route, ale přetěžuji její metodu GetVirtualPath - což je právě metoda, která se stará o generování odchozích linků (tedy těch co se generují ve view jako v předchozí ukázce kódu):
public class LocalizableRoute : Route
{
public LocalizableRoute(string url, IRouteHandler routeHandler)
: base(url, routeHandler)
{
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
if (values.ContainsKey("controller"))
{
string fallbackControllerName = values["controller"].ToString();
string localizedControllerName = this.GetLocalizedControllerName(fallbackControllerName);
if (!String.IsNullOrEmpty(localizedControllerName))
{
values["Controller"] = this.GetLocalizedControllerName(fallbackControllerName);
if (values.ContainsKey("action"))
{
string fallbackActionName = values["action"].ToString();
values["action"] = this.GetLocalizedActionName(fallbackControllerName, fallbackActionName);
}
}
}
return base.GetVirtualPath(requestContext, values);
}
private object GetLocalizedActionName(string fallbackControllerName, string fallbackActionName)
{
Type controllerType = (from t in Assembly.GetExecutingAssembly().GetTypes()
where t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
TypeUtilities.InheritsFrom(t, typeof(Controller)) &&
t.Name == fallbackControllerName + "Controller"
select t).FirstOrDefault();
if (controllerType == null)
{
// There is no controller that matches this fallback name.
return null;
}
// TODO: Find a better approach to matching ambiguous method names (i.e. one for GET
// and one for POST) than simply picking the first match.
MethodInfo methodInfo = (from mi in controllerType.GetMethods()
where mi.Name == fallbackActionName
select mi).FirstOrDefault();
if (methodInfo == null)
{
return null;
}
return GetLocalizedActionName(methodInfo, fallbackActionName);
}
private string GetLocalizedActionName(MethodInfo methodInfo, string fallbackActionName)
{
object[] localizableActionNameAttributes = methodInfo.GetCustomAttributes(typeof(LocalizableActionNameAttribute), true);
if (localizableActionNameAttributes.GetLength(0) == 0)
{
// this method does not have a localizable action name attribute
return fallbackActionName;
}
LocalizableActionNameAttribute localizableActionNameAttribute = (LocalizableActionNameAttribute)localizableActionNameAttributes[0];
return localizableActionNameAttribute.LocalizedActionName;
}
private string GetLocalizedControllerName(string fallbackControllerName)
{
Type controllerType = (from t in Assembly.GetExecutingAssembly().GetTypes()
where t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
TypeUtilities.InheritsFrom(t, typeof(Controller)) &&
t.Name == fallbackControllerName + "Controller"
select t).FirstOrDefault();
if (controllerType == null)
{
// There is no controller that matches this fallback name.
// Proceed as normal using the original controller name.
return fallbackControllerName;
}
return GetLocalizedControllerName(controllerType, fallbackControllerName);
}
private string GetLocalizedControllerName(Type controllerType, string fallbackControllerName)
{
object[] localizableControllerNameAttributes = controllerType.GetCustomAttributes(typeof(LocalizableControllerNameAttribute), true);
if (localizableControllerNameAttributes.GetLength(0) == 0)
{
// this type does not have a localizable controller name attribute
return fallbackControllerName;
}
LocalizableControllerNameAttribute localizableControllerNameAttribute = (LocalizableControllerNameAttribute)localizableControllerNameAttributes[0];
return localizableControllerNameAttribute.LocalizedControllerName;
}
}
{
public LocalizableRoute(string url, IRouteHandler routeHandler)
: base(url, routeHandler)
{
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
if (values.ContainsKey("controller"))
{
string fallbackControllerName = values["controller"].ToString();
string localizedControllerName = this.GetLocalizedControllerName(fallbackControllerName);
if (!String.IsNullOrEmpty(localizedControllerName))
{
values["Controller"] = this.GetLocalizedControllerName(fallbackControllerName);
if (values.ContainsKey("action"))
{
string fallbackActionName = values["action"].ToString();
values["action"] = this.GetLocalizedActionName(fallbackControllerName, fallbackActionName);
}
}
}
return base.GetVirtualPath(requestContext, values);
}
private object GetLocalizedActionName(string fallbackControllerName, string fallbackActionName)
{
Type controllerType = (from t in Assembly.GetExecutingAssembly().GetTypes()
where t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
TypeUtilities.InheritsFrom(t, typeof(Controller)) &&
t.Name == fallbackControllerName + "Controller"
select t).FirstOrDefault();
if (controllerType == null)
{
// There is no controller that matches this fallback name.
return null;
}
// TODO: Find a better approach to matching ambiguous method names (i.e. one for GET
// and one for POST) than simply picking the first match.
MethodInfo methodInfo = (from mi in controllerType.GetMethods()
where mi.Name == fallbackActionName
select mi).FirstOrDefault();
if (methodInfo == null)
{
return null;
}
return GetLocalizedActionName(methodInfo, fallbackActionName);
}
private string GetLocalizedActionName(MethodInfo methodInfo, string fallbackActionName)
{
object[] localizableActionNameAttributes = methodInfo.GetCustomAttributes(typeof(LocalizableActionNameAttribute), true);
if (localizableActionNameAttributes.GetLength(0) == 0)
{
// this method does not have a localizable action name attribute
return fallbackActionName;
}
LocalizableActionNameAttribute localizableActionNameAttribute = (LocalizableActionNameAttribute)localizableActionNameAttributes[0];
return localizableActionNameAttribute.LocalizedActionName;
}
private string GetLocalizedControllerName(string fallbackControllerName)
{
Type controllerType = (from t in Assembly.GetExecutingAssembly().GetTypes()
where t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
TypeUtilities.InheritsFrom(t, typeof(Controller)) &&
t.Name == fallbackControllerName + "Controller"
select t).FirstOrDefault();
if (controllerType == null)
{
// There is no controller that matches this fallback name.
// Proceed as normal using the original controller name.
return fallbackControllerName;
}
return GetLocalizedControllerName(controllerType, fallbackControllerName);
}
private string GetLocalizedControllerName(Type controllerType, string fallbackControllerName)
{
object[] localizableControllerNameAttributes = controllerType.GetCustomAttributes(typeof(LocalizableControllerNameAttribute), true);
if (localizableControllerNameAttributes.GetLength(0) == 0)
{
// this type does not have a localizable controller name attribute
return fallbackControllerName;
}
LocalizableControllerNameAttribute localizableControllerNameAttribute = (LocalizableControllerNameAttribute)localizableControllerNameAttributes[0];
return localizableControllerNameAttribute.LocalizedControllerName;
}
}
Poznámka - další metodu, kterou můžeme přetížit, je GetRouteData - a můžeme si tak zjednodušit routování příchozích požadavků. Je to další způsob, jak řešit, co je popsáno v tomto článku. MVC je prostě hodně otevřený systém, který si lze vhodně přiohýbat.
A jak to funguje?
A je to, aplikace se chová takto - pokud je prohlížeč nastaven na češtinu, tak musí být adresa zadána česky a na oplátku je i formulář zobrazen v češtině a stejně tak i cesty pod odkazy:Pokud se změní nastavení jazyka, tedy například v Chrome se jde do Nastavení, Rozšířených nastavení a klikne se na Nastavení jazyků:
a angličtina se nastaví jako první jazyk:
Žádné komentáře:
Okomentovat