« Ritorna al blog

Ritorna alla lista completa degli articoli

Globalizzazione e localizzazione in ASP.NET Core

ASP.NET CORE - dicembre 27, 2023

Grazie ad ASP.NET Core possiamo rendere la nostra app multilingua, raggiungendo un pubblico più ampio. ASP.NET Core offre servizi e middleware per la localizzazione in diverse lingue e culture. Vediamo subito come fare.

Scegliamo un progetto ASP.NET Core (Model View Controller)


Diamo un nome al progetto LocalizedWebAPP:

Impostiamo la versione del framework più aggiornata. Al momento la 8.0:

La versione 8.0 del framework ingloba già i seguenti pacchetti. Se lavorate con versioni meno recenti dovrete includerli. Ecco i comandi:

dotnet add package Microsoft.AspNetCore.Localization
dotnet add package Microsoft.Extensions.Localization

Spostiamoci nel file Program.cs e completiamo il seguente codice:

// Add services to the container.
builder.Services.AddControllersWithViews().AddViewLocalization();

AddControllersWithViews() è un metodo di estensione fornito da ASP.NET Core che registra i servizi necessari per l'utilizzo di controller e viste nell'applicazione. Questo metodo, a sua volta, accetta altre configurazioni attraverso una catena di metodi di estensione.

Quando utilizziamo AddViewLocalization(), stiamo dicendo a ASP.NET Core di abilitare la localizzazione delle viste. Ciò è necessario quando si vuole utilizzare le funzionalità di localizzazione, come l'utilizzo di @inject IViewLocalizer o @Localizer["Chiave"] nelle viste Razor.

Dobbiamo finire di configurare la localizzazione in quanto non abbiamo ancora detto quali lingue vogliamo usare e dove prendere le risorse necessarie per visualizzare le stringhe tradotte. ASP.NET utilizza una cartella di default che se non specificato diversamente nel metodo AddLocalization() sottintende la cartella Resources; pertanto creiamo la nostra cartella Resources nella root della nostra app e aggiungiamo questo codice sempre nel file Program.cs 

builder.Services.AddLocalization();
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    var supportedCultures = new[]
    {
            new CultureInfo("it-IT"), // Italiano
            new CultureInfo("en-US"), // Inglese
        };

    options.DefaultRequestCulture = new RequestCulture("it-IT");
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;
});

In questo codice abbiamo specificato che lasciamo intatto il percorso di default della cartella Resources e che la lingua principale è l'italiano e poi l'inglese. Se avessimo voluto specificare un percorso diverso delle risorse, avremo dovuto scrivere:

services.AddLocalization(options => options.ResourcesPath = "MyResources");

Per finire e abilitare la localizzazione dobbiamo applicare le impostazioni che abbiamo usato con questo codice. Se non applichiamo questo codice l'app potrebbe funzionare non correttamente:

app.UseRequestLocalization();

E' importante abilitare localizzazione con app.UseRequestLocalization(); prima di qualsiasi altro middleware. Questo ordine aiuta a garantire una corretta gestione delle lingue prima che avvenga qualsiasi altra manipolazione della richiesta. Questo codice è responsabile di:

  • Recuperare il servizio di configurazione delle opzioni di localizzazione.
  • Utilizzare tali opzioni per abilitare la localizzazione nell'applicazione.

Ecco il codice completo del file Program.cs

using Microsoft.AspNetCore.Localization;
using System.Diagnostics;
using System.Globalization;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews().AddViewLocalization();

// Add Services localization
builder.Services.AddLocalization();
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    var supportedCultures = new[]
    {
        new CultureInfo("it-IT"), // Italiano
        new CultureInfo("en-US"), // Inglese
    };
    options.DefaultRequestCulture = new RequestCulture("it-IT");
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

// Middleware per i file statici
app.UseStaticFiles();

// Middleware di gestione delle preferenze linguistiche del client
// app.Use();

// Middleware per la localizzazione delle richieste
app.UseRequestLocalization();

// Middleware di redirezione HTTPS
app.UseHttpsRedirection();

// Middleware di routing
app.UseRouting();

// Middleware di autorizzazione
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");


app.Run();

L'ordine dei middleware non è per nulla casuale. Vediamolo nel dettaglio:

  1. app.UseStaticFiles(); viene posizionato prima del nostro middleware personalizzato. In questo modo evitiamo che il nostro middleware esamini ogni file statico, migliorando le prestazioni.
  2. app.Use();è il nostro middleware personalizzato che tratteremo più avanti.
  3. app.UseRequestLocalization(); questo middleware è responsabile della localizzazione delle richieste e dovrebbe essere posizionato subito dopo il nostro middleware personalizzato. In questo modo, beneficia delle impostazioni di cultura che abbiamo configurato precedentemente.
  4. app.UseHttpsRedirection(); app.UseRouting(); e app.UseAuthorization(); vengono posizionati alla fine della pipeline perché gestiscono il routing delle richieste e devono essere eseguiti dopo le altre operazioni, come la localizzazione e l'indirizzamento HTTPS.

Il prossimo step è di creare la cartella Resources (se non l'avete già fatto), la classe SharedResource  e due file risorse. E' importante specificare nei file lo stesso nome della classe SharedResources per ottenere le stringhe localizzate:

  • SharedResource.en-US.resx
  • SharedResource.it-IT.resx

Apriamo i file SharedResource.en-US.resxSharedResource.it-IT.resx per inserire le stringhe da localizzare. Partiamo dal file in italiano, inseriamo la parola chiave e la rispettiva traduzione. Poi copiamo la riga in italiano e la incolliamo nell'utima riga del file in inglese. Correggiamo la traduzione. In questo modo siamo sicuri di non sbagliare sulle parole chiavi o di creare incongruenze tra i file.

E' importante rispettare la sigla "it-IT", ossia "lingua - regione", in quanto il nostro applicativo funzionerebbe in maniera più corretta. Facciamo un esempio. Immaginiamo di impostare la valuta corrente della Gran Bretagna e degli Stati Uniti d'America. Se usassimo solo la sigla della lingua, senza la regione, non avremo il simbolo corretto della valuta. Pertanto quando impostiamo una lingua dobbiamo impostare anche il codice ISO corretto. 

// Consigliato
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");

// Non consigliato
Thread.CurrentThread.CurrentCulture = new CultureInfo("en");
Thread.CurrentThread.CurrentUICulture = new CultureInfo("en");

Il codice imposta, in maniera forzata, la lingua e la regione per la nostra app. Quando programmiamo un app, i browser impostano automaticamente la lingua principale a seconda delle scelte dell'utente. E' consigliabile non leggere le impostazioni del dispositivo fisico, ma quelle del browser, intercettando le richieste HTTP. Possiamo offrire una scelta della lingua al nostro utente oppure recuperare l'impostazione preferita del browser di navigazione. L'approccio corretto potrebbe essere quello di recuperare automaticamente la lingua preferita o impostata dal browser e al contempo offrire la scelta della lingua all'utente. Per fare ciò dobbiamo aggiungere altro codice al file Program.cs:

// Middleware di gestione delle preferenze linguistiche del client
app.Use(async (context, next) =>
{
    Debug.WriteLine("Prima del blocco di codice");

    // Impostazione della lingua di default del nostro applicativo
    var preferredLanguage = "it-IT";

    // Ottieni le preferenze linguistiche del client dalla richiesta HTTP
    var userLanguages = context.Request.Headers.AcceptLanguage.ToString();

    // Estrai la prima lingua preferita completa
    preferredLanguage = userLanguages.Split(',').FirstOrDefault()?.Trim();

    // Imposta la cultura corrente sulla lingua preferita del client
    if (!string.IsNullOrEmpty(preferredLanguage))
    {
        var culture = new CultureInfo(preferredLanguage);
        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = culture;
    }

    Debug.WriteLine("Dopo il blocco di codice");

    // Passa alla gestione successiva nella pipeline
    await next.Invoke();
});

Il codice recupera la lingua e la regione corrente e se non trova nulla, lascia la lingua che abbiamo impostato di default. E importante posizionare il blocco app.Run() in maniera corretta, onde evitare che venga richiesto inutilmente più volte; infatti, la posizione del middleware nella pipeline di gestione delle richieste è un aspetto cruciale in asp.net core. Il middleware viene eseguito nell'ordine in cui viene aggiunto alla pipeline, e ciò può influire significativamente sul comportamento dell'applicazione e sulle prestazioni. Posizionare il middleware app.Use() nella posizione corretta, ad esempio prima di app.Run(), garantisce che il blocco di codice nel middleware venga eseguito nel momento giusto durante il processo di gestione della richiesta. Se provate, ad esempio, a spostare il blocco app.Run(), prima di app.UseHttpsRedirection(); vedrete che il codice all'interno di esso verrà richiesto più volte inficiando le prestazioni della vostra app. 

Adesso spostiamoci nel controller HomeController.cs e completiamo la pagina con il seguente codice. La parte commentata ci aiuterà a testare ulteriormente la nostra app:

using LocalizeWebApp.Models;
using LocalizeWebApp.Resources;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using System.Diagnostics;
using System.Globalization;

namespace LocalizeWebApp.Controllers
{
    public class HomeController(IStringLocalizer<SharedResource> localizer) : Controller
    {
        private readonly IStringLocalizer<SharedResource> _localizer = localizer;

        public IActionResult Index()
        {
            // Uncomment for testing in english

            // Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
            // Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");

            // Uncomment for testing in italian

            // Thread.CurrentThread.CurrentCulture = new CultureInfo("it-IT");
            // Thread.CurrentThread.CurrentUICulture = new CultureInfo("it-IT");

            var welcomeMessage = _localizer["Benvenuto"];
            ViewBag.welcomeMessage = welcomeMessage;

            return View();
        }

        public IActionResult Privacy()
        {
            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

Abbiamo utilizzato il costruttore primario, una delle novità introdotte e gradite con C# 12. Il IStringLocalizer, invece, è una interfaccia in ASP.NET Core utilizzata per la localizzazione delle stringhe. In particolare, IStringLocalizer<SharedResource> indica che stiamo utilizzando un localizzatore di stringhe specifico per la classe SharedResource.

Spostandoci nella View, aggiorniamo il codice:

@using System.Globalization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Localization
@using LocalizeWebApp.Resources

@inject IStringLocalizer<SharedResource> SharedResource

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">@ViewBag.welcomeMessage</h1>
    <p>@Html.Raw(SharedResource["BenvenutoIntro"].ToString())</p>


    <p class="fw-bold">@SharedResource["DataCorrente"]</p>
    @{
        var currentDate = DateTime.Now;
        var formattedDate = currentDate.ToString("D");
    }

    <p>@formattedDate</p>


    <p class="fw-bold">@SharedResource["PrezzoCorrente"]</p>
    @{
        var price = 1234.56;
        var formattedPrice = price.ToString("C");
    }

    <p>@formattedPrice</p>
</div>

Per testare l'app, abbiamo usato il formato della data estesa e la valuta corrente. Questo codice @inject IStringLocalizer<SharedResource> SharedResource utilizza la dependency injection o iniezione delle dipendenze di ASP.NET Core. In particolare, viene iniettato un servizio chiamato IStringLocalizer<SharedResource> direttamente nella pagina Razor o nella vista. Non sempre questo approccio risulta utile e alle volte abbiamo necessità di richiamare il servizio anche nella parte logica del nostro Controller. Nel codice abbiamo seguito entrambi gli approcci: il primo approccio ha previsto una variabile specificata nel controller @ViewBag.welcomeMessage; il secondo approccio utilizza la pagina Razor e la dependency injection con SharedResource["BenvenutoIntro"].

Avviando l'app possiamo testare il comportamento della localizzazione specificando le due lingue supportate.

Italiano

Inglese

Possiamo testare ulteriormente la nostra app cambiando la lingua al nostro browser. Ad esempio, In Microsoft Edge, impostiamo la lingua Inglese senza specificare Unites States come regione. Avremo un risultato errato della traduzioni in quanto asp.net non troverà il file localizzato correttamente:

Vengono visualizzate solo le parole chiavi dei termini e il simbolo della valuta non viene rappresentato correttamente. Quindi per garantire una visualizzazione corretta dobbiamo rifinire il codice impostando la localizzazione solo per le sigle complete. Nel caso l'utente abbia specificato una lingua generica senza l'area geografica imposteremo la lingua di default del nostro applicativo. Ecco il blocco di codice completo del nostro middleware personalizzato da aggiornare:

// Middleware di gestione delle preferenze linguistiche del client
app.Use(async (context, next) =>
{
    Debug.WriteLine("Prima del blocco di codice");

    // Impostazione della lingua di default del nostro applicativo
    var preferredLanguage = "it-IT";

    // Ottieni le preferenze linguistiche del client dalla richiesta HTTP
    var userLanguages = context.Request.Headers.AcceptLanguage.ToString();

    // Estrai la prima lingua preferita completa
    preferredLanguage = userLanguages.Split(',').FirstOrDefault()?.Trim();

    // Se non è già nel formato completo, aggiungi il codice del paese
    if (!string.IsNullOrEmpty(preferredLanguage) && preferredLanguage.Contains('-'))
    {
        var culture = new CultureInfo(preferredLanguage);
        preferredLanguage = $"{culture.Name}-{culture.TwoLetterISOLanguageName.ToUpper()}";
    }

    // Imposta la cultura corrente sulla lingua preferita del client
    if (!string.IsNullOrEmpty(preferredLanguage))
    {
        var culture = new CultureInfo(preferredLanguage);
        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = culture;
    }

    Debug.WriteLine("Dopo il blocco di codice");

    // Passa alla gestione successiva nella pipeline
    await next.Invoke();
});

Un'altra soluzione, meno consigliabile, era quella di rinominare i file SharedResource.en-US.resx in SharedResource.en.resx, togliendo il codice regione. Il risultato sarebbe stato parzialmente più valido, poiché avremo avuto le stringhe localizzate ad eccezione del simbolo della valuta.

 

Globalizzazione e localizzazione in ASP.NET Core