Indice dei contenuti
Fin dal giorno della sua prima release il .NET Framework dà la possibilità agli sviluppatori di impostare un qualsiasi progetto - sia esso un Website, una Web Application, un client realizzato con Windows Forms o con il più recente approccio WPF/XAML o altro - in modalità multi-language, ovvero con supporto di localization multiple, mediante l'utilizzo dei cosiddetti Resource Files, contraddistinti dall'estensione .resx. Non è intenzione di questo articolo spiegare l'utilizzo dei Resource Files, per i quali rimandiamo all'ottimo walkthrough ufficiale presente sul sito Microsoft: ci limiteremo a ricordare che, come probabilmente già saprete, lo scopo dei Resource Files è quello di immagazzinare in un array di chiavi/valori una serie di elementi di testo e/o immagini per ciascuna lingua supportata dall'applicazione: per ottenere questo risultato lo sviluppatore non deve far altro che creare un Resource File per la lingua predefinita (ad es. l'inglese) e poi un Resource File per ciascuna lingua, utilizzando lo stesso nome del file originario con l'aggiunta del codice ISO 639-1 (two-letters language code) e, se necessario, il codice ISO 3166-1 (two-letters country code) ad essa relativi. Sarà quindi possibile, ad esempio, creare:
- un Global.resx per immagazzinare i testi nella lingua predefinita
- un Global.it.resx contenente le medesime chiavi con i testi tradotti in lingua italiana
- un Global.de.resx contenente le medesime chiavi con i testi tradotti in lingua tedesca
e così via. Una volta fatto questo, sarà sufficiente utilizzare le chiavi impostate in questi file in luogo dei testi veri e propri (per sapere come, leggete il walkthrough di cui sopra): il Framework .NET penserà automaticamente a cercare la chiave nei vari Resource File, partendo da quello con l'estensione più vicina alla Localization impostata sul thread corrente e procedendo a ritroso fino a quello relativo alla lingua predefinita.
A conti fatti, si tratta di una funzionalità davvero niente male. Vediamo come utilizzarla per rendere multi-language una Web Application basata su ASP.NET MVC.
I Resource File in ASP.NET MVC
La prima domanda a cui dobbiamo rispondere è: dove inserire i file .resx? La risposta è tutt'altro che scontata: il primo impulso potrebbe essere quello di aggiungere al nostro progetto l'apposita cartella ASP.NET denominata App_GlobalResources, presente fin dalle primissime versioni del Framework e da sempre presentata come la scelta ideale.
Contrariamente a quanto si potrebbe pensare, questa scelta non è ottimale in un progetto basato su ASP.NET MVC. Se volete i dettagli sul perché, potete approfondire la problematica leggendo l'ottimo articolo di K. Scott Allen sul tema. In estrema sintesi, il motivo è legato al fatto che il Framework compila i file contenuti in quella cartella in un assembly separato, rendendoli così inaccessibili ai nostri Controller, Unit Test, etc.: inutile dire che questo risulta incompatibile con i nostri scopi, motivo per cui la soluzione migliore è quella di creare una directory apposita che, per semplificare, consigliamo di chiamare /Resources/ .
Al suo interno potremo creare i file .resx che ci servono, avendo cura di modificare alcune impostazioni presenti nel pannello Proprietà.
La prima cosa da cambiare è il Custom Tool, ovvero il code-generator che il Framework andrà a utilizzare per creare la strongly-typed class corrispondente al nostro Resource File. L'impostazione predefinita, ResXFileCodeGenerator, creerà una classe privata che non è quello che ci serve. Andremo dunque a sostituirlo con PublicResXFileCodeGenerator, assicurandoci in tal modo una classe e dei metodi pubblici.
La seconda impostazione da modificare è il Custom Tool Namespace, ovvero lo spazio dei nomi all'interno del quale il Custom Tool di cui abbiamo appena parlato andrà a collocare la classe corrispondente al Resource File. Il valore predefinito, una stringa vuota, si tradurrà in una assenza di namespace. Sostituiamo la stringa vuota con Resources, così da assicurarci che il tool generi un codice conforme alla struttura di directory che abbiamo impostato: in questo modo tutte le nostre risorse saranno presenti nella directory /Resources/ e risulteranno accessibili includendo il namespace Resources.
Queste due modifiche dovranno essere applicate a ogni file .resx che aggiungeremo al nostro progetto: per risparmiarsi di ripetere il lavoro più volte consigliamo di duplicare i Resource Files di volta in volta, sfruttando il fatto che la copia di un file .resx viene creata con le medesime proprietà del file di origine.
Come impostare la lingua di ciascuna Request
Come abbiamo detto in precedenza, il .NET Framework seleziona i Resource Files sulla base delle informazioni di localizzazione del thread che avrà il compito di rispondere alla request. Queste informazioni sono contenute nell'oggetto CultureInfo, la cui impostazione predefinita dipende dalla lingua del client che dà origine alla request stessa: in altre parole, dalla lingua impostata sul browser dell'utente, la quale in molti casi - anche se non sempre - coincide con quella del sistema operativo. Questo fa sì che un sito correttamente configurato per utilizzare i Resource Files per visualizzare i contenuti possa presentare, sia pure a utenti diversi, testi scritti in lingue diverse a parità di URL.
La domanda che dobbiamo porci è: si tratta di un comportamento corretto? Da un punto di vista di funzionalità, sicuramente si: è indubbio che leggere una pagina in italiano o in inglese a seconda della localizzazione dell'utente possa essere molto comodo. Il problema è che da un punto di vista SEO si tratta di un fallimento completo. Chiunque abbia un minimo di infarinatura nel campo sa bene che è opportuno fare in modo che ogni traduzione di ciascuna pagina disponga di una URL specifica: se avete dei dubbi in proposito potete chiarirvi le idee su questa breve guida di Google che illustra una serie di best-practices da utilizzare per gestire siti multi-lingua, della quale ci limiteremo a citare la frase più significativa: Keep the content for each language on separate URLs.
Questo, in parole povere, significa che non possiamo permetterci di impostare - o far impostare al .NET Framework - la localizzazione del thread tenendo conto di cose come:
- la lingua impostata sul browser dell'utente
- i valori presenti nei cookie (se presenti)
- i valori presenti nella sessione dell'utente (se presente)
- qualsiasi altro campo presente nella Request (HEADER, POST data, etc.) diverso dalla URL
Bensì nell'unico modo opportuno, ovvero tenendo conto unicamente delle informazioni presenti nella URL stessa. Questo significa che dovremo impostare il nostro sito in modo che possa rispondere efficacemente a URL come le seguenti:
- http://www.example.com/page (presentando i contenuti nella lingua predefinita)
- http://www.example.com/en/page (presentando i contenuti in lingua inglese o, se assenti, nella lingua predefinita)
- http://www.example.com/de/page (presentando i contenuti in lingua tedesca o, se assenti, nella lingua predefinita)
... e così via. Il che, in poche parole, significa impostare la localizzazione del thread che risponde la request sulla base delle informazioni presenti nella URL.
Nei paragrafi successivi vedremo i passaggi da effettuare per fare in modo che la nostra Web Application in ASP.MVC faccia esattamente questo.
Impostare una Route Multi-Language
La prima cosa da fare è impostare una route che possa gestire questo tipo di URL ed acquisire le informazioni necessarie. Se la vostra Web Application segue il route-pattern introdotto con ASP.NET MVC, ovvero {controller}/{action}/{id}, potete utilizzare il seguente esempio:
1 2 3 4 5 6 |
routes.MapRoute( name: "DefaultLocalized", url: "{lang}/{controller}/{action}/{id}", constraints: new { lang = @"(\w{2})|(\w{2}-\w{2})" }, // en or en-US defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); |
Questa route andrà inserita nel file /App_Start/RouteConfig.cs in penultima posizione, subito prima della route predefinita {controller}/{action}/{id}. Nel caso in cui la vostra Web Application segua una logica di routing diversa, dovrete adattare la route alle vostre esigenze e/o ai vostri cambiamenti.
Lo scopo di questa route è quello di isolare, controllare ed eventualmente memorizzare - se presente e valido - una variabile lang corrispondente alla lingua richiesta dall'utente con quella request specifica. Questa informazione, se presente, verrà utilizzata per impostare la localizzazione del thread principale e forzare quindi l'utilizzo dei Resource File corrispondenti.
Gestire le Request multi-language tramite un LocalizationAttribute
Ora che abbiamo impostato la route per intercettare le URL multi-language, non ci resta che creare un LocalizationAttribute che ci consenta di gestirla. Per far questo creiamo una nuova classe LocalizationAttribute.cs che avrà il compito, in conseguenza dell'esecuzione di ciascuna Action, di impostare la Culture e la UICulture del thread principale sulla base della variabile lang impostata dalla route.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class LocalizationAttribute : ActionFilterAttribute { private string _DefaultLanguage = "en"; public LocalizationAttribute(string defaultLanguage) { _DefaultLanguage = defaultLanguage; } public override void OnActionExecuting(ActionExecutingContext filterContext) { string lang = (string)filterContext.RouteData.Values["lang"] ?? _DefaultLanguage; if (lang != _DefaultLanguage) { try { Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo(lang); } catch (Exception e) { throw new NotSupportedException(String.Format("ERROR: Invalid language code '{0}'.", lang)); } } } } |
Questa classe può essere inserita in un qualsiasi punto dell'applicazione dedicato alle classi centralizzate, come ad esempio una directory /Classes/ oppure /AppCode/. Una volta aggiunta al nostro progetto, assicuriamoci che venga eseguita registrandola come Global Filter all'interno del file /App_Start/FilterConfig.cs nel seguente modo:
1 2 3 4 5 6 7 8 |
public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); filters.Add(new LocalizationAttribute("it"), 0); } } |
La linea evidenziata corrisponde alla riga che dobbiamo aggiungere per fare in modo che il nostro LocalizationAttribute entri in gioco in conseguenza di ogni response. Con l'occasione, approfittiamo del fatto che il costruttore che abbiamo impostato consente di specificare una Localization predefinita, specificando "it" (corrispondente alla lingua italiana): in questo modo, in mancanza di una URL che contenga le informazioni relative alla Localization - e quindi della mancata valorizzazione del parametro lang - verrà utilizzato l'italiano, che sarà quindi la lingua predefinita della nostra Web Application.
Conclusioni
Non ci sono altri passaggi da effettuare, a parte ovviamente impostare i .resx file in modo che ogni lingua che abbiamo intenzione di supportare disponga dei suoi testi. Una volta fatto questo, infatti, sarà possibile accedere a qualsiasi pagina della nostra Web Application nel seguente modo:
- http://www.example.com/page , visualizzando i contenuti nella lingua predefinita.
- http://www.example.com/en/page , visualizzando i contenuti in lingua inglese o, se assenti, nella lingua predefinita.
- http://www.example.com/de/page , visualizzando i contenuti in lingua tedesca o, se assenti, nella lingua predefinita.
... e così via.
Per il momento è tutto: felice sviluppo!