Localizzare un applicazione .NET Core (versione 2.1) è diverso rispetto ad una applicazione ASP.NET standard.
Per prima cosa va aggiunta la configurazione in Startup.cs:
...
using System.Globalization;
...
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Options;

namespace SgartItLocalization
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            //1 - aggiungo i servizi di localizzazione (inject IStringLocalizer)
            services.AddLocalization(options =>
            {
                //definisco la cartella che conterrà i file resx;
                options.ResourcesPath = "Resources";
            });

            //2 - definisco le culture supportate e quella di default (RequestLocalizationMiddleware)
            services.Configure<RequestLocalizationOptions>(options =>
            {
                var defaultCulture = "it-IT";
                // culture supportate dall'applicazione
                var supportedCultures = new List<CultureInfo>
                {
                    new CultureInfo(defaultCulture),
                    new CultureInfo("en-US"),
                    new CultureInfo("en-GB"),
                    new CultureInfo("en"),
                    new CultureInfo("fr-FR"),
                    new CultureInfo("fr"),
                    new CultureInfo("es-MX"),
                    new CultureInfo("de"),
                };
                options.DefaultRequestCulture = new RequestCulture(defaultCulture);
                options.SupportedCultures = supportedCultures;
                options.SupportedUICultures = supportedCultures;
            });

            services.Configure<CookiePolicyOptions>(options =>
            {
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
                //3 - add localization service per le view (inject IViewLocalizer)
                .AddViewLocalization(
                    LanguageViewLocationExpanderFormat.Suffix,
                    opts => { opts.ResourcesPath = "Resources"; })
                .AddDataAnnotationsLocalization();
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            ...
            //4 - add localization, prima di app.UseMvc
            var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
            app.UseRequestLocalization(options.Value);

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}
nella configurazione vengono definite:
  • le dipendence injection (DI) per i servizi di localizzazione (IStringLocalizer)
  • la posizione della cartella che conterrà i file .rexs (Resources)
  • la culture di default (DefaultRequestCulture)
  • le culture supportate dall'applicazione
  • il supporto per la localizzazione delle view (AddViewLocalization)
  • il supporto per la localizzazione delle data annotation (AddDataAnnotationsLocalization)
a questo punto posso creare la classe, SharedResource, che rappresenterà il file di risorse condivise da usare nell'applicazione:
// classe fittizia per le risorse condivise
// non specificare nessun mame space
public class SharedResource
{
  // vuota
}
per uniformità la creo nella cartella Resources.
Attenzione non specificare nessun namespace per questa classe.
La risorsa viene cercata concatenando il nome dell'assembly, la cartella Rsources (specificata in Startup) e il nome classe passato in IStringLocalizer. Nell'esempio MvcMovie.Resources.SharedResource.
il passo successivo è creare un file di risorse comune, con lo stesso nome della classe precedente, SharedResource.resx:
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="Prova" xml:space="preserve">
    <value>Prova (shared)</value>
  </data>
</root>
ed uno o più file localizzati SharedResource.en.resx:
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="Prova" xml:space="preserve">
    <value>Prova (en)</value>
  </data>
</root>
SharedResource.en-US.resx:
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="Prova" xml:space="preserve">
    <value>Prova (en-US)</value>
  </data>
</root>
Se non si creano i file .resx come default viene ritornato come valore la chiave passata, quindi si può lavorare, inizialmente, senza file resx.

Adesso abbiamo l'infrastruttura per gestire le risorse nel codice:
...
using Microsoft.AspNetCore.Localization;
...
using Microsoft.Extensions.Localization;

namespace SgartItLocalization.Controllers
{
  public class HomeController : Controller
  {
    //variabile interna per accedere alla localizzazione (SharedResources)
    private readonly IStringLocalizer<SharedResource> _localizer;

    //viene fatto l'inject di IStringLocalizer nel costruttore
    public HomeController(IStringLocalizer<SharedResource> localizer)
    {
      _localizer = localizer;
    }

    public IActionResult Index()
    {
      // accedo alla risorsa con chiave "Prova"
      ViewData["Prova"] = _localizer["Prova"];
      return View();
    }
  }
}
oppure nella view:
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Localization
@*
  @inject IViewLocalizer Localizer
  IViewLocalizer si aspetta di trovare una risorsa in: SgartItLocalization.Resources.Views.Home.Index
*@

@inject IStringLocalizer<SharedResource> Localizer
@{
    ViewData["ProvaView"] = Localizer["Prova"];
}

<h1>Code: @ViewData["Prova"]</h1>
<h2>View: @ViewData["ProvaView"]</h2>
<h3>Localizer: @Localizer["Prova"]</h3>
Di default il midleware, RequestLocalizationMiddleware, permette di impostare la culture corrente con 3 metodi differenti:
  • tramite query string (Microsoft.AspNetCore.Localization.QueryStringRequestCultureProvider)
  • tramite cookie (Microsoft.AspNetCore.Localization.CookieRequestCultureProvider)
  • tramite le impostazioni di lingua del browser (Microsoft.AspNetCore.Localization.AcceptLanguageHeaderRequestCultureProvider)
il primo che soddisfa la "culture" esce e non esegue i successivi.
Quindi, se ad esempio, se imposto la culture tramite query string, http://localhost/home/index?culture=en-US , non verranno presi in considerazione l'eventuale cookie (.AspNetCore.Culture) ne gli header inviati dal browser.
Attenzione: IViewLocalizer trova il file resx corrispondente componendo il percorso del file di risorse come: [nomeAssembly].Resources.Views.[nomeController].[nomeAction].resx (SgartItLocalization.Resources.Views.Home.Index).
Quindi deve esistere sotto la cartella Resources un file chiamato Views.Home.Index.resx o Views.Home.Index.[culture].resx.
In alternativa è possibile creare una gerarchia di cartelle /Resources/Views/Home/Index.resx.
Esempio di nomenclatura delle risorseEsempio di nomenclatura delle risorse
A questo proposito va evidenziato che se la culture passata non è tra quelle configurate in Startup.cs, come default, verrà presa la lingua del browser (AcceptLanguageHeaderRequestCultureProvider) e non la default culture indicata in Startup.cs.
Quindi a meno che non volgiamo usare le impostazioni del browser, conviene eliminare i provider non necessari per avere sempre il fallback sulla culture di default:
//2 - definisco le culture supportate e quella di default (RequestLocalizationMiddleware)
      services.Configure<RequestLocalizationOptions>(options =>
      {
        ...
        //salvo il provider che voglio tenere, quello con i cookie
        var rcpCookie = options.RequestCultureProviders.FirstOrDefault(x => x is Microsoft.AspNetCore.Localization.CookieRequestCultureProvider);
        //cancello tutti i provider definiti di default
        options.RequestCultureProviders.Clear();
        //aggiungo il provider salvato precedentemente
        if (rcpCookie != null)
          options.RequestCultureProviders.Add(rcpCookie);
      });
Se uso i cookie devo avere un metodo che mi permetta di impostare la nuova culture, ad esempio SetLanguage nel controller Home:
using Microsoft.AspNetCore.Http;
...
[HttpPost]
public IActionResult SetLanguage(string culture, string returnUrl)
{
  Response.Cookies.Append(
      CookieRequestCultureProvider.DefaultCookieName,
      CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
      //scadenza dopo 1 anno
      new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
  );
  if (string.IsNullOrWhiteSpace(returnUrl))
  {
    returnUrl = "/";
  }
  return LocalRedirect(returnUrl);
}
e posso aggiungere nella _Layout.cshtml la scelta della culture:
<form id=form-set-language method="post" action="/home/setlanguage">
  <input type="hidden" id="form-set-language-culture" name="culture">
  <input type="hidden" id="form-set-language-return-url" name="returnUlr" value="@Context.Request.Path">
  <a onclick="setlanguage('en')">set en</a>
  <a onclick="setlanguage('en-US')">set en-US</a>
  <a onclick="setlanguage('en-GB')">set en-GB</a>
  <a onclick="setlanguage('it-IT')">set it-IT</a>
  <a onclick="setlanguage('fr')">set fr</a>
  <a onclick="setlanguage('fr-FR')">set fr-FR</a>
  <a onclick="setlanguage('es')">set es</a>
  <a onclick="setlanguage('es-ES')">set es-ES</a>
  <a onclick="setlanguage('es-MX')">set es-MX</a>
  <a onclick="setlanguage('de')">set de</a>
  <a onclick="setlanguage('de-DE')">set de-DE</a>
</form>
...
@RenderSection("Scripts", required: false)
<script>
  function setlanguage(culture) {
      document.getElementById("form-set-language-culture").value = culture;
      document.getElementById("form-set-language").submit();
  }
</script>
Attenzione, se dopo la prima compilazione vengono aggiunti dei file di risorsa e non sono visibili dall'applicazione, prova a cancellare i file sotto bin/debug o bin/release e ricompilare.
Attenzione 2: se non si riesce ad impostare la lingua tramite cookie e il cookie non viene inviato al browser, vedi Non si riesce ad impostare un cookie in .NET Core.

Per maggiori informazioni vedi Globalizzazione e localizzazione in ASP.NET Core