Globalizzazione .NET e ICU

Prima di .NET 5, le API di globalizzazione .NET usavano librerie sottostanti diverse su piattaforme diverse. In Unix, le API usavano componenti internazionali per Unicode (ICU)e in Windows usavano il supporto per il linguaggio nazionale (NLS). Ciò ha comportato alcune differenze comportamentali in una manciata di API di globalizzazione durante l'esecuzione di applicazioni su piattaforme diverse. Le differenze di comportamento sono state evidenti in queste aree:

  • Impostazioni cultura e dati relativi alle impostazioni cultura
  • Maiuscole/minuscole di stringa
  • Ordinamento e ricerca di stringhe
  • Chiavi ordinamento
  • Normalizzazione delle stringhe
  • Supporto di IDN (Internationalized Domain Names)
  • Nome visualizzato del fuso orario in Linux

A partire da .NET 5, gli sviluppatori hanno maggiore controllo sulla libreria sottostante usata, consentendo alle applicazioni di evitare differenze tra le piattaforme.

ICU in Windows

Windows ora incorpora una versione di icu.dll preinstallata come parte delle funzionalità usate automaticamente per le attività di globalizzazione. Questa modifica consente a .NET di sfruttare questa libreria di ICU per il supporto della globalizzazione. Nei casi in cui la libreria di ICU non è disponibile o non può essere caricata, come avviene con le versioni precedenti di Windows, .NET 5 e versioni successive ripristinano l'uso dell'implementazione basata su NLS.

La tabella seguente illustra le versioni di .NET in grado di caricare la libreria di ICU in versioni client e server Windows diverse:

Versione di .NET Versione Windows
.NET 5 o .NET 6 Client Windows 10 versione 1903 o successiva
.NET 5 o .NET 6 Windows Server 2022 o versioni successive
.NET 7 o versione successiva Client Windows 10 versione 1703 o successiva
.NET 7 o versione successiva Windows Server 2019 o versione successiva

Nota

.NET 7 e versioni successive hanno la possibilità di caricare l'ICU nelle versioni precedenti di Windows, a differenza di .NET 6 e .NET 5.

Nota

Anche quando si usa l'ICU, i membri CurrentCulture, CurrentUICulture e CurrentRegion usano ancora le API del sistema operativo Windows per rispettare le impostazioni utente.

Differenze di comportamento

Se si aggiorna l'app a .NET 5 o versione successiva, è possibile che vengano visualizzate modifiche nell'app anche se non ci si rende conto che si usano le funzionalità di globalizzazione. Questa sezione elenca una delle modifiche comportamentali che potrebbero essere visualizzate, ma anche altre.

String.IndexOf

Si consideri il codice seguente che chiama String.IndexOf(String) per trovare l'indice del carattere \0 Null in una stringa.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
  • In .NET Core 3.1 e versioni precedenti in Windows il frammento di codice stampa 3 su ognuna delle tre righe.
  • Per .NET 5 e le versioni successive in esecuzione nelle versioni di Windows elencate nella tabella della sezione ICU in Windows, il frammento di codice stampa 0, 0 e 3 (per la ricerca ordinale).

Per impostazione predefinita, String.IndexOf(String) esegue una ricerca linguistica compatibile con le impostazioni cultura. L'ICU considera il carattere \0 Null come carattere di peso zero e quindi il carattere non viene trovato nella stringa quando si usa una ricerca linguistica in .NET 5 e versioni successive. Tuttavia, NLS non considera il carattere \0 Null come carattere zero e una ricerca linguistica in .NET Core 3.1 e versioni precedenti individua il carattere nella posizione 3. Una ricerca ordinale trova il carattere nella posizione 3 in tutte le versioni .NET.

È possibile eseguire regole di analisi del codice CA1307: Specificare StringComparison per maggiore chiarezza e CA1309: Usare lo StringComparison ordinale per trovare i siti di chiamata nel codice in cui il confronto di stringhe non è specificato o non è ordinale.

Per altre informazioni, vedere Modifiche al comportamento durante il confronto di stringhe in .NET 5+.

String.EndsWith

const string foo = "abc";

Console.WriteLine(foo.EndsWith("\0"));
Console.WriteLine(foo.EndsWith("c"));
Console.WriteLine(foo.EndsWith("\0", StringComparison.CurrentCulture));
Console.WriteLine(foo.EndsWith("\0", StringComparison.Ordinal));
Console.WriteLine(foo.EndsWith('\0'));

Importante

In .NET 5+ in esecuzione nelle versioni di Windows elencate nella tabella ICU in Windows, il frammento di codice precedente stampa:

True
True
True
False
False

Per evitare questo comportamento, usare l'overload del parametro char o StringComparison.Oridinal.

String.StartsWith

const string foo = "abc";

Console.WriteLine(foo.StartsWith("\0"));
Console.WriteLine(foo.StartsWith("a"));
Console.WriteLine(foo.StartsWith("\0", StringComparison.CurrentCulture));
Console.WriteLine(foo.StartsWith("\0", StringComparison.Ordinal));
Console.WriteLine(foo.StartsWith('\0'));

Importante

In .NET 5+ in esecuzione nelle versioni di Windows elencate nella tabella ICU in Windows, il frammento di codice precedente stampa:

True
True
True
False
False

Per evitare questo comportamento, usare l'overload del parametro char o StringComparison.Oridinal.

TimeZoneInfo.FindSystemTimeZoneById

L'ICU offre la flessibilità necessaria per creare istanze di TimeZoneInfo usando ID di fuso orario IANA, anche quando l'applicazione è in esecuzione in Windows. Analogamente, è possibile creare istanze TimeZoneInfo con ID fuso orario di Windows, anche se in esecuzione su piattaforme non Windows. Tuttavia, è importante notare che questa funzionalità non è disponibile quando si usa la modalità NLS o globalizzazione in modalità invariante.

API dipendenti dall'ICU

.NET ha introdotto API dipendenti dall'ICU. Queste API possono avere esito positivo solo quando si usa l'ICU. Di seguito sono riportati alcuni esempi.

Nelle versioni di Windows elencate nella tabella della sezione ICU su Windows le API menzionate avranno esito positivo in modo coerente. Tuttavia, nelle versioni precedenti di Windows queste API avranno esito negativo in modo coerente. In questi casi, è possibile abilitare la funzionalità di ICU locale dell'app per garantire il successo di queste API. Nelle piattaforme non Windows queste API avranno sempre esito positivo indipendentemente dalla versione.

Inoltre, è fondamentale per le app assicurarsi che non siano in esecuzione in modalità globalizzazione invariante o modalità NLS per garantire il successo di queste API.

Usare NLS invece di ICU

L'uso di ICU invece di NLS può comportare differenze comportamentali con alcune operazioni correlate alla globalizzazione. Per ripristinare l'uso di NLS, uno sviluppatore può rifiutare esplicitamente l'implementazione dell'ICU. Le applicazioni possono abilitare la modalità NLS in uno dei modi seguenti:

  • Nel file di progetto:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
    </ItemGroup>
    
  • Nel file runtimeconfig.json:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.UseNls": true
          }
      }
    }
    
  • Impostando la variabile di ambiente DOTNET_SYSTEM_GLOBALIZATION_USENLS sul valore true o 1.

Nota

Un valore impostato nel progetto o nel file runtimeconfig.json ha la precedenza sulla variabile di ambiente.

Per altre informazioni, vedere Impostazioni di configurazione del runtime.

Determinare se l'app usa l'ICU

Il frammento di codice seguente consente di determinare se l'app è in esecuzione con librerie di ICU (e non NLS).

public static bool ICUMode()
{
    SortVersion sortVersion = CultureInfo.InvariantCulture.CompareInfo.Version;
    byte[] bytes = sortVersion.SortId.ToByteArray();
    int version = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0];
    return version != 0 && version == sortVersion.FullVersion;
}

Per determinare la versione di .NET, usare RuntimeInformation.FrameworkDescription.

ICU locale dell'app

Ogni versione di ICU può includere correzioni di bug e dati CLDR (Common Locale Data Repository) aggiornati che descrivono le lingue del mondo. Lo spostamento tra versioni di ICU può influire negativamente sul comportamento delle app quando si tratta di operazioni correlate alla globalizzazione. Per aiutare gli sviluppatori di applicazioni a garantire la coerenza tra tutte le distribuzioni, .NET 5 e versioni successive consentono alle app in Windows e Unix di usare la propria copia di ICU.

Le applicazioni possono acconsentire esplicitamente a una modalità di implementazione di ICU locale dell'app in uno dei modi seguenti:

  • Nel file di progetto impostare il valore RuntimeHostConfigurationOption appropriato:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • In alternativa, nel file runtimeconfig.json impostare il valore runtimeOptions.configProperties appropriato:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • In alternativa, impostando la variabile di ambiente DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU sul valore <suffix>:<version> o <version>.

    <suffix>: suffisso facoltativo di lunghezza inferiore a 36 caratteri, seguendo le convenzioni pubbliche di creazione di pacchetti di ICU. Quando si compila un ICU personalizzato, è possibile personalizzarlo per produrre i nomi lib e i nomi dei simboli esportati in modo da contenere un suffisso, ad esempio libicuucmyapp, dove myapp è il suffisso.

    <version>: versione valida dell'ICU, ad esempio 67.1. Questa versione viene usata per caricare i file binari e per ottenere i simboli esportati.

Quando una di queste opzioni è impostata, è possibile aggiungere un Microsoft.ICU.ICU4C.RuntimePackageReference al progetto che corrisponde al version configurato ed è tutto ciò che è necessario.

In alternativa, per caricare l'ICU quando è impostata l'opzione locale dell'app, .NET usa il metodo NativeLibrary.TryLoad, che esegue il probe di più percorsi. Il metodo tenta prima di tutto di trovare la libreria nella proprietà NATIVE_DLL_SEARCH_DIRECTORIES, creata dall'host dotnet in base al file deps.json per l'app. Per altre informazioni, vedere Probe predefinito.

Per le app autonome, non è richiesta alcuna azione speciale da parte dell'utente, oltre ad assicurarsi che l'ICU si trova nella directory dell'app (per le app autonome, per impostazione predefinita la directory di lavoro è NATIVE_DLL_SEARCH_DIRECTORIES).

Se si usa l'ICU tramite un pacchetto NuGet, questo funziona nelle applicazioni dipendenti dal framework. NuGet risolve gli asset nativi e li include nel file deps.json e nella directory di output per l'applicazione nella directory runtimes. .NET lo carica da questa posizione.

Per le app dipendenti dal framework (non autonome) in cui l'ICU viene usata da una compilazione locale, è necessario eseguire dei passaggi aggiuntivi. .NET SDK non dispone ancora di una funzionalità per i file binari nativi "separati" da incorporare in deps.json (vedere questo problema dell'SDK). È invece possibile abilitare questa operazione aggiungendo informazioni aggiuntive nel file di progetto dell'applicazione. Ad esempio:

<ItemGroup>
  <IcuAssemblies Include="icu\*.so*" />
  <RuntimeTargetsCopyLocalItems Include="@(IcuAssemblies)" AssetType="native" CopyLocal="true"
    DestinationSubDirectory="runtimes/linux-x64/native/" DestinationSubPath="%(FileName)%(Extension)"
    RuntimeIdentifier="linux-x64" NuGetPackageId="System.Private.Runtime.UnicodeData" />
</ItemGroup>

Questa operazione deve essere eseguita per tutti i file binari di ICU per i runtime supportati. Inoltre, i metadati NuGetPackageId nel gruppo di elementi RuntimeTargetsCopyLocalItems devono corrispondere a un pacchetto NuGet a cui fa effettivamente riferimento il progetto.

Comportamento di macOS

macOS ha un comportamento diverso per la risoluzione delle librerie dinamiche dipendenti dai comandi di caricamento specificati nel file Mach-O rispetto al caricatore Linux. Nel caricatore Linux .NET può provare libicudata, libicuuc e libicui18n (in questo ordine) per soddisfare il grafico delle dipendenze di ICU. Tuttavia, in macOS, questo non funziona. Quando si compila l'ICU in macOS, per impostazione predefinita, si ottiene una libreria dinamica con questi comandi di caricamento in libicuuc. Il frammento di codice seguente mostra un esempio.

~/ % otool -L /Users/santifdezm/repos/icu-build/icu/install/lib/libicuuc.67.1.dylib
/Users/santifdezm/repos/icu-build/icu/install/lib/libicuuc.67.1.dylib:
 libicuuc.67.dylib (compatibility version 67.0.0, current version 67.1.0)
 libicudata.67.dylib (compatibility version 67.0.0, current version 67.1.0)
 /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1)
 /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 902.1.0)

Questi comandi fanno riferimento solo al nome delle librerie dipendenti per gli altri componenti dell'ICU. Il caricatore esegue la ricerca seguendo le convenzioni di dlopen, che implica la presenza di queste librerie nelle directory di sistema o l'impostazione delle variabili di ambiente LD_LIBRARY_PATH o l'ICU a livello di app. Se non è possibile impostare LD_LIBRARY_PATH o assicurarsi che i file binari di ICU si trovino nella directory a livello di app, è necessario eseguire alcune operazioni aggiuntive.

Esistono alcune direttive per il caricatore, ad esempio @loader_path, che indicano al caricatore di cercare tale dipendenza nella stessa directory del file binario con tale comando di caricamento. A questo scopo è possibile procedere in due modi:

  • install_name_tool -change

    Eseguire i comandi seguenti:

    install_name_tool -change "libicudata.67.dylib" "@loader_path/libicudata.67.dylib" /path/to/libicuuc.67.1.dylib
    install_name_tool -change "libicudata.67.dylib" "@loader_path/libicudata.67.dylib" /path/to/libicui18n.67.1.dylib
    install_name_tool -change "libicuuc.67.dylib" "@loader_path/libicuuc.67.dylib" /path/to/libicui18n.67.1.dylib
    
  • Patch ICU per produrre i nomi di installazione con @loader_path

    Prima di eseguire autoconf (./runConfigureICU), modificare queste righe in:

    LD_SONAME = -Wl,-compatibility_version -Wl,$(SO_TARGET_VERSION_MAJOR) -Wl,-current_version -Wl,$(SO_TARGET_VERSION) -install_name @loader_path/$(notdir $(MIDDLE_SO_TARGET))
    

ICU in WebAssembly

È disponibile una versione di ICU specifica per i carichi di lavoro WebAssembly. Questa versione offre la compatibilità della globalizzazione con i profili desktop. Per ridurre le dimensioni del file di dati di ICU da 24 MB a 1,4 MB (o ~0,3 MB se compresso con Brotli), questo carico di lavoro presenta alcune limitazioni.

Le API seguenti non sono supportate:

Le API seguenti sono supportate con limitazioni:

Sono inoltre supportate meno impostazioni locali. L'elenco supportato è disponibile nel repository dotnet/icu.