Impostare un sito web multi-language con ASP.NET MVC

Premessa

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.

App_GlobalResources

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  .

Al suo interno potremo creare i file .resx che ci servono, avendo cura di modificare alcune impostazioni presenti nel pannello Proprietà.

Resx.Properties

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   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:

Questa route andrà inserita nel file  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    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.

Questa classe può essere inserita in un qualsiasi punto dell’applicazione dedicato alle classi centralizzate, come ad esempio una directory   oppure . Una volta aggiunta al nostro progetto, assicuriamoci che venga eseguita registrandola come Global Filter all’interno del file   nel seguente modo:

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.

Un’alternativa migliore utilizzando un LocalizedControllerActivator

IMPORTANTE: alcune segnalazioni mi hanno fatto notare che impostare la Culture durante il metodo OnActionExecuting di un ActionFilter potrebbe essere “troppo tardi” in alcune situazioni, per via del funzionamento del lifecycle delle applicazioni MVC. Ad esempio, quando viene inviato un model tramite POST, questo viene processato da ASP.NET MVC prima di eseguire qualsivoglia ActionFilter – incluso il nostro! In conseguenza di questa eventualità, alcune informazioni (come i valori presenti nelle DataAnnotations) vengono gestiti utilizzando la cultura predefinita e non quella indicata nella URL e quindi “impostata” dalla Route.

Fortunatamente, possiamo risolvere il problema creando un LocalizedControllerActivator al posto del LocalizationAttribute nel seguente modo:

E configurandolo nella nostra applicazione aggiungendo la seguente riga al file Global.asax.cs (o Startup.cs, se utilizziamo l’initialization pattern di OWIN):

Fonte: StackOverflow (un grazie a s.ermakovitch per aver condiviso questo workaround).

Localizzare e/o tradurre le Route MVC

Manca soltanto una cosa da fare: trovare un modo per far sì che gli helper di Razor come    ,   et. al. riescano a generare automaticamente una versione localizzata, o addirittura tradotta, delle URL interne alla nostra applicazione basate sulla definizione di Controller e Action. Ci sono due strade che possiamo seguire per raggiungere questo obiettivo, a seconda del tipo di risultato che vogliamo ottenere:

  • Se ci accontentiamo di avere un     davanti alle nostre URL, possiamo cavarcela alla svelta implementando un     personalizzato che accetterà un parametro opzionale di tipo   , in assenza del quale verrà utilizzata la culture del Thread corrente, ovvero    : per generare il prefisso sarà sufficiente utilizzare il valore della proprietà    del suddetto oggetto/parametro.
    EDIT: a grande richiesta, abbiamo pubblicato due esempi completi di codice C# che mostrano come implementare questa opzione mediante due extension method appositi, rispettivamente per Html.ActionLink e Url.Action.
  • Nel caso in cui avessimo bisogno di una flessibilità maggiore, come ad esempio avere URL come     per la versione in inglese e     per quella italiana della stessa pagina, consigliamo di utilizzare l’ottima libreria RouteLocalization sviluppata da Dresel e disponibile gratuitamente su GitHub a questo link, che fa esattamente questo (e anche molto altre cose). E’ possibile installarla anche tramite NuGet utilizzando i seguenti nomi:
    •   (per progetti MVC)
    •   (per progetti WebApi)

Se la prima opzione vi sembra la più adatta alle vostre esigenze ma avete problemi a implementarla, fatelo presente nei commenti a questo articolo: saremo lieti di pubblicare un esempio funzionante. Nel caso in cui preferiate ricorrere alla libreria RouteLocalization , vi suggeriamo di leggere questa ottima guida all’implementazione della stessa:

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!

RELATED POSTS

About Ryan

IT Project Manager, Web Interface Architect e Lead Developer di numerosi siti e servizi web ad alto traffico in Italia e in Europa. Dal 2010 si occupa anche della progettazione di App e giochi per dispositivi Android, iOS e Mobile Phone per conto di numerose società italiane.

View all posts by Ryan
  • Pingback: Cambiare la lingua predefinita di IIS dopo l'installazione()

  • Pingback: Change IIS default language on a working Windows Server()

  • Pingback: ASP.NET - Insert HTML code in Resource Files (.resx)()

  • andrea pulvirenti

    non si capisce come debbano chiamarsi i file resx, e se ho in mvc un progetto con le viste che ereditano layout?

    • Ciao Andrea,

      I file .resx puoi chiamarli con il nome che preferisci, ovviamente questo influenzerà il namespace degli stessi quando li andrai a richiamare. Per siti molto piccoli può anche avere senso farne soltanto uno, mentre in progetti più corposi può avere senso farne uno per Area/Controller o persino Pagina (vedi la seconda screenshot per un esempio).

      Se ti riferisci a come chiamare le versioni per le varie lingue, è spiegato nella premessa, dove trovi anche un link al Walkthrough ufficiale. In sintesi, devi creare delle copie del file di risorse da tradurre inserendo, tra il nomefile e l’estensione, un suffisso di tipo .it, .en, .fr, .de etc. per ciascuna lingua che desideri supportare oltre alla lingua predefinita. All’interno, ovviamente, dovrai tradurre il testo associato a ciascun record.

      Quanto alle viste basate su Layout, la cosa non influenza minimamente il contenuto dell’articolo: ti limiterai a chiamare la risorsa all’interno della vista (e/o del Layout) con il classico @Resources.Home.WelcomeText , e il framework penserà automaticamente a prendere il contenuto dal file corrispondente alla lingua impostata o chiamata dall’utente (Global.en.resx, Global.fr.resx), con fallback sul predefinito Global.resx nel caso in cui non trovi nulla. Questo vale sia nel codice della vista che in quello del Layout eventualmente associato.

      Fammi sapere se è tutto chiaro.

  • marco

    salve si potrebbe avere un esempio funzionante della prima opzione? grazie mille

    • Stefano Sotolongo Marconi

      Sei poi mai riuscito a farlo funzionare?

    • Ho risposto (un pò in ritardo), vedi sopra 🙂

  • Stefano Sotolongo Marconi

    Ciao Ryan, è possibile avere un esempio completo funzionante sia della prima che della seconda opzione?

  • Pingback: Estensione Html.ActionLink per Route multi-language con ASP.NET MVC()

  • Pingback: Estensione Url.Action in C# per Route multi-language con ASP.NET MVC()