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.
Nota
I dati delle impostazioni cultura che determinano il comportamento della libreria ICU sono in genere gestiti dal Common Locale Data Repository (CLDR), non dal runtime.
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 usare 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. La sezione seguente elenca alcune modifiche comportamentali che potrebbero verificarsi.
Ordinamento di stringhe e System.Globalization.CompareOptions
CompareOptions
è l'enumerazione delle opzioni che può essere passata a String.Compare
per influenzare il modo in cui vengono confrontate due stringhe.
Il confronto delle stringhe per l'uguaglianza e la determinazione del relativo ordinamento differiscono tra NLS e ICU. In particolare:
- L'ordinamento predefinito delle stringhe è diverso, per cui ciò sarà evidente anche se non si usa direttamente
CompareOptions
. Quando si usa ICU, l'opzione predefinitaNone
funziona comeStringSort
.StringSort
ordina i caratteri non alfanumerici prima di quelli alfanumerici (ad esempio, "bill's" appare nell'ordine prima di "bills"). Per ripristinare la funzionalitàNone
precedente, è necessario usare l'implementazione basata su NLS. - La gestione predefinita dei caratteri di legatura è diversa. In NLS, le legature e le loro controparti non legature (ad esempio, "oeuf" e "œuf") sono considerate uguali, ma ciò non avviene con ICU in .NET. Ciò è dovuto a un peso della regola di confronto diverso tra le due implementazioni. Per ripristinare il comportamento NLS quando si usa ICU, usare il valore
CompareOptions.IgnoreNonSpace
.
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
e3
(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.Ordinal
.
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.Ordinal
.
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.
Abbreviazioni dei giorni della settimana
Il metodo DateTimeFormatInfo.GetShortestDayName(DayOfWeek) ottiene il nome del giorno abbreviato più corto per un giorno della settimana specificato.
- In .NET Core 3.1 e versioni precedenti in Windows, queste abbreviazioni dei giorni della settimana sono formate da due caratteri, ad esempio "Do".
- In .NET 5 e versioni successive, queste abbreviazioni dei giorni della settimana sono formate da un solo carattere, ad esempio "D".
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. Tuttavia, nelle versioni precedenti di Windows queste API avranno esito negativo. 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 hanno 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 potrebbe comportare differenze comportamentali con alcune operazioni correlate alla globalizzazione. Per ripristinare l'uso di NLS, è possibile 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 valoretrue
o1
.
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 potrebbe 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 esempiolibicuucmyapp
, dovemyapp
è 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 Microsoft.ICU.ICU4C.Runtime PackageReference
al progetto che corrisponde a 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:
- CultureInfo.EnglishName
- CultureInfo.NativeName
- DateTimeFormatInfo.NativeCalendarName
- RegionInfo.NativeName
Le API seguenti sono supportate con limitazioni:
- String.Normalize(NormalizationForm) e String.IsNormalized(NormalizationForm) non supportano i moduli FormKC e FormKD usati raramente.
- RegionInfo.CurrencyEnglishName restituisce lo stesso valore di RegionInfo.CurrencyNativeName.
Sono inoltre supportate meno impostazioni locali. L'elenco supportato è disponibile nel repository dotnet/icu.