Localizzazione in .NET

La localizzazione è il processo di conversione delle risorse di un'applicazione nelle versioni localizzate per tutte le impostazioni cultura supportate dall'applicazione. È consigliabile passare alla fase di localizzazione solo dopo aver completato il passaggio di Revisione della localizzabilità, per verificare che l'applicazione globalizzata sia pronta per la localizzazione.

Un'applicazione pronta per la localizzazione è suddivisa in due blocchi concettuali: un blocco che contiene tutti gli elementi dell'interfaccia utente e un blocco che contiene il codice eseguibile. Il blocco dell'interfaccia utente contiene solo gli elementi dell'interfaccia utente localizzabili, come stringhe, messaggi di errore, finestre di dialogo, menu, risorse oggetto incorporate e così via, per le impostazioni cultura neutre. Il blocco di codice contiene solo il codice dell'applicazione che deve essere usato da tutte le impostazioni cultura supportate. Common Language Runtime supporta un modello di risorse assembly satellite che separa il codice eseguibile dell'applicazione dalle relative risorse. Per altre informazioni sull'implementazione di questo modello, vedere Risorse nelle app .NET.

Per ogni versione localizzata dell'applicazione, aggiungere un nuovo assembly satellite che contiene il blocco dell'interfaccia utente localizzata tradotto nella lingua appropriata per le impostazioni cultura di destinazione. Il blocco di codice per tutte le impostazioni cultura deve rimanere invariato. La combinazione di una versione localizzata del blocco dell'interfaccia utente con il blocco di codice produce una versione localizzata dell'applicazione.

In questo articolo si apprenderà come usare le implementazioni IStringLocalizer<T> e IStringLocalizerFactory. L'intero codice sorgente di esempio in questo articolo si basa sui pacchetti NuGet Microsoft.Extensions.Localization e Microsoft.Extensions.Hosting. Per altre informazioni sull'hosting, vedere Host generico .NET.

File di risorse

Il meccanismo principale per isolare le stringhe localizzabili è costituito dai file di risorse. Un file di risorse è un file XML con estensione .resx. I file di risorse vengono convertiti prima dell'esecuzione dell'applicazione che utilizza, ovvero rappresentano il contenuto tradotto inattivo. Un nome di file di risorse contiene in genere un identificatore delle impostazioni locali e assume il formato seguente:

<FullTypeName><.Locale>.resx

Dove:

  • <FullTypeName> rappresenta le risorse localizzabili per un tipo specifico.
  • Il facoltativo <.Locale> rappresenta le impostazioni locali del contenuto del file di risorse.

Specifica delle impostazioni locali

Le impostazioni locali devono definire la lingua, come minimo, ma possono anche definire la cultura (lingua regionale) e anche il paese o l'area geografica. Questi segmenti sono comunemente delimitati dal carattere -. Con la specificità aggiunta della cultura, vengono applicate le regole di "fallback cultura", in cui le corrispondenze migliori sono classificate in ordine di priorità. Le impostazioni locali devono essere mappate a un tag di lingua noto. Per ulteriori informazioni, vedere CultureInfo.Name.

Scenari di fallback della cultura

Si supponga che l'app localizzata supporti varie impostazioni locali serbe e disponga dei file di risorse seguenti per MessageService:

file Lingua regionale Codice paese
MessageService.sr-Cyrl-RS.resx (Cirillico, Serbia) RS
MessageService.sr-Cyrl.resx Cirillico
MessageService.sr-Latn-BA.resx (Latino, Bosnia ed Erzegovina) BA
MessageService.sr-Latn-ME.resx (Latino, Montenegro) ME
MessageService.sr-Latn-RS.resx (Latino, Serbia) RS
MessageService.sr-Latn.resx Latino
MessageService.sr.resx Latino
MessageService.resx

Lingua regionale predefinita per la lingua.

Quando l'app è in esecuzione con l'opzione CultureInfo.CurrentCulture impostata sulla cultura di "sr-Cyrl-RS", la localizzazione tenta di risolvere i file nell'ordine seguente:

  1. MessageService.sr-Cyrl-RS.resx
  2. MessageService.sr-Cyrl.resx
  3. MessageService.sr.resx
  4. MessageService.resx

Tuttavia, se l'app è in esecuzione con l'impostazione CultureInfo.CurrentCulture impostata su impostazioni cultura di "sr-Latn-BA" localizzazione tenta di risolvere i file nell'ordine seguente:

  1. MessageService.sr-Latn-BA.resx
  2. MessageService.sr-Latn.resx
  3. MessageService.sr.resx
  4. MessageService.resx

La regola di "fallback cultura" ignorerà le impostazioni locali quando non sono presenti corrispondenze compatibili, vale a dire che viene selezionato il numero quattro del file di risorse, se non è in grado di trovare una corrispondenza. Se la cultura è stata impostata su "fr-FR", la localizzazione finirebbe per utilizzare il file MessageService.resx, il che può essere problematico. Per altre informazioni, vedere Il processo di fallback delle risorse.

Ricerca risorse

I file di risorse vengono risolti automaticamente come parte di una routine di ricerca. Se il nome del file di progetto è diverso dallo spazio dei nomi radice del progetto, il nome dell'assembly potrebbe essere differente. Ciò può impedire che la ricerca delle risorse venga eseguita in caso contrario. Per risolvere questa mancata corrispondenza, usare per RootNamespaceAttribute fornire un suggerimento ai servizi di localizzazione. Se specificato, viene usato durante la ricerca delle risorse.

Il progetto di esempio è denominato example.csproj, che crea un example.dll e example.exe, ma tuttavia viene usato lo spazio dei nomi Localization.Example. Applicare un attributo di livello assembly per correggere questa mancata corrispondenza:

[assembly: RootNamespace("Localization.Example")]

Registrare i servizi di localizzazione

Per registrare i servizi di localizzazione, chiamare uno dei metodi di estensione AddLocalization durante la configurazione dei servizi. Ciò consentirà l'inserimento delle dipendenze dei tipi seguenti:

Configurare le opzioni di localizzazione

L'overload AddLocalization(IServiceCollection, Action<LocalizationOptions>) accetta un parametro setupAction di tipo Action<LocalizationOptions>. In questo modo è possibile configurare le opzioni di localizzazione.

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddLocalization(options =>
{
    options.ResourcesPath = "Resources";
});

// Omitted for brevity.

I file di risorse possono essere collocati ovunque in un progetto, ma esistono procedure comuni che hanno dimostrato di avere successo. Il più delle volte si segue il percorso di minor resistenza. Il codice C# precedente:

In questo, modo i servizi di localizzazione cercherebbero nella directory Risorse i file di risorse.

Usare IStringLocalizer<T> ed IStringLocalizerFactory

Dopo aver registrato (e facoltativamente configurato) i servizi di localizzazione, è possibile usare i tipi seguenti con l'inserimento delle dipendenze:

Per creare un servizio messaggi in grado di restituire stringhe localizzate, considerare quanto segue MessageService:

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Localization;

namespace Localization.Example;

public sealed class MessageService(IStringLocalizer<MessageService> localizer)
{
    [return: NotNullIfNotNull(nameof(localizer))]
    public string? GetGreetingMessage()
    {
        LocalizedString localizedString = localizer["GreetingMessage"];

        return localizedString;
    }
}

Nel codice C# precedente:

  • Viene dichiarato un campo IStringLocalizer<MessageService> localizer.
  • Il costruttore primario definisce un parametro IStringLocalizer<MessageService> e lo acquisisce come argomento localizer.
  • Il metodo GetGreetingMessage richiama IStringLocalizer.Item[String] che passa "GreetingMessage" come argomento.

IStringLocalizer supporta anche le risorse stringa con parametri, considerare quanto segue ParameterizedMessageService:

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Localization;

namespace Localization.Example;

public class ParameterizedMessageService(IStringLocalizerFactory factory)
{
    private readonly IStringLocalizer _localizer =
        factory.Create(typeof(ParameterizedMessageService));

    [return: NotNullIfNotNull(nameof(_localizer))]
    public string? GetFormattedMessage(DateTime dateTime, double dinnerPrice)
    {
        LocalizedString localizedString = _localizer["DinnerPriceFormat", dateTime, dinnerPrice];

        return localizedString;
    }
}

Nel codice C# precedente:

  • Viene dichiarato un campo IStringLocalizer _localizer.
  • Il costruttore primario accetta un parametro IStringLocalizerFactory, che viene usato per creare un IStringLocalizer dal tipo ParameterizedMessageService e lo assegna al campo _localizer.
  • Il metodo GetFormattedMessage richiama IStringLocalizer.Item[String, Object[]], passando come argomenti "DinnerPriceFormat", un oggetto dateTime e dinnerPrice.

Importante

IStringLocalizerFactory non è obbligatorio. È invece preferibile utilizzare i servizi per richiedere IStringLocalizer<T>.

Entrambi gli indicizzatori IStringLocalizer.Item[] restituiscono un LocalizedString, con conversioni implicite in string?.

Combinare tutti gli elementi

Per esemplificare un'app usando entrambi i servizi di messaggistica, insieme ai file di localizzazione e risorse, prendere in considerazione il seguente file di Program.cs:

using System.Globalization;
using Localization.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using static System.Console;
using static System.Text.Encoding;

[assembly: RootNamespace("Localization.Example")]

OutputEncoding = Unicode;

if (args is [var cultureName])
{
    CultureInfo.CurrentCulture =
        CultureInfo.CurrentUICulture =
            CultureInfo.GetCultureInfo(cultureName);
}

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddLocalization();
builder.Services.AddTransient<MessageService>();
builder.Services.AddTransient<ParameterizedMessageService>();
builder.Logging.SetMinimumLevel(LogLevel.Warning);

using IHost host = builder.Build();

IServiceProvider services = host.Services;

ILogger logger =
    services.GetRequiredService<ILoggerFactory>()
        .CreateLogger("Localization.Example");

MessageService messageService =
    services.GetRequiredService<MessageService>();
logger.LogWarning(
    "{Msg}",
    messageService.GetGreetingMessage());

ParameterizedMessageService parameterizedMessageService =
    services.GetRequiredService<ParameterizedMessageService>();
logger.LogWarning(
    "{Msg}",
    parameterizedMessageService.GetFormattedMessage(
        DateTime.Today.AddDays(-3), 37.63));

await host.RunAsync();

Nel codice C# precedente:

Ognuna delle *MessageService classi definisce un set di file con estensione .resx, ognuno con una singola voce. Ecco il contenuto di esempio per i file di risorse MessageService, a partire da MessageService.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="GreetingMessage" xml:space="preserve">
    <value>Hi friends, the ".NET" developer community is excited to see you here!</value>
  </data>
</root>

MessageService.sr-Cyrl-RS.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="GreetingMessage" xml:space="preserve">
    <value>Здраво пријатељи, ".NЕТ" девелопер заједница је узбуђена што вас види овде!</value>
  </data>
</root>

MessageService.sr-Latn.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="GreetingMessage" xml:space="preserve">
    <value>Zdravo prijatelji, ".NET" developer zajednica je uzbuđena što vas vidi ovde!</value>
  </data>
</root>

Ecco il contenuto di esempio per i file di risorse ParameterizedMessageService, a partire da ParameterizedMessageService.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="DinnerPriceFormat" xml:space="preserve">
    <value>On {0:D} my dinner cost {1:C}.</value>
  </data>
</root>

ParameterizedMessageService.sr-Cyrl-RS.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="DinnerPriceFormat" xml:space="preserve">
    <value>У {0:D} моја вечера је коштала {1:C}.</value>
  </data>
</root>

ParameterizedMessageService.sr-Latn.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="DinnerPriceFormat" xml:space="preserve">
    <value>U {0:D} moja večera je koštala {1:C}.</value>
  </data>
</root>

Suggerimento

Tutti i commenti, gli schemi e gli elementi <resheader> XML del file di risorse vengono intenzionalmente omessi per brevità.

Esecuzioni di esempio

L'esempio seguente illustra i vari output localizzati, date le impostazioni locali di destinazione.

Considerare "sr-Latn":

dotnet run --project .\example\example.csproj sr-Latn

warn: Localization.Example[0]
      Zdravo prijatelji, ".NET" developer zajednica je uzbuđena što vas vidi ovde!
warn: Localization.Example[0]
      U utorak, 03. avgust 2021. moja večera je koštala 37,63 ¤.

Quando si omette un argomento per l'interfaccia della riga di comando di .NET per eseguire il progetto, viene usata la cultura di sistema predefinita, in questo caso "en-US":

dotnet run --project .\example\example.csproj

warn: Localization.Example[0]
      Hi friends, the ".NET" developer community is excited to see you here!
warn: Localization.Example[0]
      On Tuesday, August 3, 2021 my dinner cost $37.63.

Quando si passa "sr-Cryl-RS", vengono trovati i file di risorse corrispondenti corretti e si applica la localizzazione:

dotnet run --project .\example\example.csproj sr-Cryl-RS

warn: Localization.Example[0]
      Здраво пријатељи, ".NЕТ" девелопер заједница је узбуђена што вас види овде!
warn: Localization.Example[0]
      У уторак, 03. август 2021. моја вечера је коштала 38 RSD.

L'applicazione di esempio non fornisce i file di risorse per "fr-CA", ma quando viene chiamato con tale cultura, vengono usati i file di risorse non localizzati.

Avviso

Poiché la cultura viene trovata ma i file di risorse corretti non sono presenti, quando viene applicata la formattazione, si finisce con la localizzazione parziale:

dotnet run --project .\example\example.csproj fr-CA

warn: Localization.Example[0]
     Hi friends, the ".NET" developer community is excited to see you here!
warn: Localization.Example[0]
     On mardi 3 août 2021 my dinner cost 37,63 $.

Vedi anche