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

Errore 403 - forbidden dopo aver pubblicato una applicazione ASP.NET MVC su IIS 7: come risolvere

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 /Resources/ .

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

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.

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:

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!

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. Microsoft MVP for Development Technologies dal 2018.

View all posts by Ryan

16 Comments on “Impostare un sito web multi-language con ASP.NET MVC”

  1. Pingback: Cambiare la lingua predefinita di IIS dopo l'installazione
  2. Pingback: Change IIS default language on a working Windows Server
  3. Pingback: ASP.NET - Insert HTML code in Resource Files (.resx)
  4. non si capisce come debbano chiamarsi i file resx, e se ho in mvc un progetto con le viste che ereditano layout?

    1. 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.

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

    1. Ecco qui due esempi funzionanti per la prima opzione, realizzati secondo la logica descritta nell’articolo.

      Html.ActionLink:
      https://www.ryadel.com/estensione-html-actionlink-c-per-gestire-route-multi-language-con-asp-net-mvc/

      Url.Action:
      https://www.ryadel.com/estensione-url-action-in-c-per-gestire-route-multi-language-con-asp-net-mvc/

      Buona lettura!

  6. Pingback: Estensione Html.ActionLink per Route multi-language con ASP.NET MVC
  7. Pingback: Estensione Url.Action in C# per Route multi-language con ASP.NET MVC
  8. Grazie per il tuo lavoro e per la condivisione!
    Nessuno però scrive nulla su come avere i dati del database in più lingue.

    1. Grazie!

      Per quanto riguarda come impostare il DB ti consiglio di dare un’occhiata a questo contributo, che personalmente ho trovato piuttosto interessante (soprattutto per comprendere le differenze tra bad design e good design).

      https://www.codeproject.com/Articles/8084/Creating-Multilingual-Websites-Part-2#databasedesign

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *


Il periodo di verifica reCAPTCHA è scaduto. Ricaricare la pagina.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.