.NET グローバリゼーションと ICU
.NET 5 より前では、.NET グローバリゼーション API は、プラットフォームごとに別々の基になるライブラリを使用していました。 この API は、Unix では、ICU (International Components for Unicode) を使用し、Windows では、各国語サポート (NLS) を使用していました。 これにより、アプリケーションを実行するとき、いくつかのグローバリゼーション API の動作がプラットフォームによって違うことがありました。 次の領域で、動作が違うことがわかっています。
- カルチャとカルチャ データ
- 文字の大文字小文字
- 文字の並べ替えと検索
- 並べ替えキー
- 文字の正規化
- 国際化ドメイン名 (IDN) のサポート
- Linux のタイム ゾーンの表示名
.NET 5 以降では、プラットフォームによってアプリケーションに違いがでることがないよう、開発者が基になるライブラリをより制御できるようになりました。
Note
ICU ライブラリの動作を制御するカルチャ データは、通常、ランタイムではなく、共通ロケール データ リポジトリ (CLDR) によって保守されます。
Windows 上の ICU
Windows では、グローバリゼーション タスクに自動的に採用される機能の一部として、プリインストールされた icu.dll バージョンが組み込まれました。 この変更により、.NET でこの ICU ライブラリを活用してグローバリゼーションをサポートできるようになりました。 以前の Windows バージョンのように、ICU ライブラリを利用できない、または読み込めない場合、.NET 5 以降のバージョンでは、NLS ベースの実装を使用します。
次の表は、異なる Windows クライアントとサーバーのバージョン間で、ICU ライブラリを読み込める .NET のバージョンを示したものです。
.NET バージョン | Windows バージョン |
---|---|
.NET 5 または .NET 6 | Windows Client 10 バージョン 1903 以降 |
.NET 5 または .NET 6 | Windows Server 2022 以降 |
.NET 7 以降 | Windows Client 10 バージョン 1703 以降 |
.NET 7 以降 | Windows Server 2019 またはそれ以降 |
Note
.NET 7 以降のバージョンには、.NET 6 や .NET 5 とは対照的に、以前の Windows バージョンでも ICU を読み込む機能があります。
Note
ICU が使用されている場合でも、Windows オペレーティング システム API では、ユーザーが設定している場合は、CurrentCulture
、CurrentUICulture
、および CurrentRegion
のメンバーが使用されます。
動作の違い
.NET 5 以降を対象にするようにアプリをアップグレードすると、グローバリゼーション機能を使用していることを認識していない場合でも、アプリでの変更に気付くことがあります。 次のセクションでは、発生する可能性のある動作の変更をいくつか示します。
文字列の並べ替えと System.Globalization.CompareOptions
CompareOptions
は String.Compare
に渡すことができるオプションの列挙型で、2 つの文字列の比較方法に影響を与えます。
等価性についての文字列の比較や、並べ替え順序の決定の方法は、NLS と ICU で異なります。 特に次の点に違いがあります。
- 既定の文字列の並べ替え順序が異なるため、
CompareOptions
を直接使用しない場合でも、この違いは明らかになります。 ICU を使用する場合、None
の既定のオプションはStringSort
と同じように実行されます。StringSort
は、英数字以外の文字を英数字の前に並べ替えます (たとえば、"bill's" は "bills" の前に並べ替えられます)。 以前のNone
機能を復元するには、NLS ベースの実装を使用する必要があります。 - 合字文字の既定の処理は異なります。 NLS では、合字とその非合字 ("oeuf" と "œuf" など) は等しいと見なされますが、.NET の ICU では同じものと見なされません。 これは、2 つの実装間で照合順序の強度が異なるためです。 ICU 使用時に NLS 動作を復元するには、
CompareOptions.IgnoreNonSpace
値を使用します。
String.IndexOf
文字列内の nulll 文字 \0
のインデックスを調べるために String.IndexOf(String) を呼び出す次のコードについて考えます。
const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
- Windows の .NET Core 3.1 以前のバージョンでは、3 行ごとに
3
がスニペットによって出力されます。 - ICU on Windows セクションの表に記載されている Windows バージョンで実行されている .NET 5 以降のバージョンでは、スニペットは
0
、0
、3
(序数検索用) を出力します。
既定では、String.IndexOf(String) によって、カルチャに対応した言語検索が実行されます。 ICU では、null 文字 \0
を "0 の重み付け文字" と見なします。したがって、.NET 5 以降で言語検索を使用する場合、文字列内のその文字は検索されません。 ただし、NLS では null 文字 \0
は 0 の重み付け文字とは見なされず、.NET Core 3.1 以前で言語検索を行うと、その文字は位置 3 で検索されます。 序数検索を行うと、すべての .NET バージョンの位置 3 で該当する文字が検索されます。
コード分析規則「CA1307: 意味を明確にするための StringComparison の指定」および「CA1309: 順序を示す StringComparison を使用します」に従うと、文字列比較が指定されていない、または序数ではないコード内の呼び出しサイトを検索できます。
詳細については、「.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'));
重要
Windows 上の ICU テーブルに記載されている Windows バージョンで実行されている .NET 5 以降では、上記のスニペットは次を出力します。
True
True
True
False
False
この動作を回避するには、char
パラメーターのオーバーロードまたは 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'));
重要
Windows 上の ICU テーブルに記載されている Windows バージョンで実行されている .NET 5 以降では、上記のスニペットは次を出力します。
True
True
True
False
False
この動作を回避するには、char
パラメーターのオーバーロードまたは StringComparison.Ordinal
を使用します。
TimeZoneInfo.FindSystemTimeZoneById
ICU では、アプリケーションが Windows で実行されている場合でも、IANA タイム ゾーン ID を使用して TimeZoneInfo インスタンスを柔軟に作成できます。 同様に、Windows 以外のプラットフォームで実行している場合でも、Windows タイム ゾーン ID を使用して TimeZoneInfo インスタンスを作成できます。 ただし、この機能は NLS モードまたはグローバリゼーション インバリアント モードを使用する場合は、使用できないことに注意することが重要です。
曜日の省略形
DateTimeFormatInfo.GetShortestDayName(DayOfWeek) メソッドは、指定した曜日の最短の省略された曜日名を取得します。
- Windows の .NET Core 3.1 以前のバージョンでは、これらの曜日の省略形は、"Su" のように 2 文字で構成されていました。
- .NET 5 以降のバージョンでは、これらの曜日の省略形は、"S" などの 1 文字のみで構成されます。
ICU 依存の API
.NET では、ICU に依存する API が導入されました。 これらの API は、ICU を使用している場合にのみ成功します。 次に例をいくつか示します。
ICU on Windows セクションの表に記載されている Windows バージョンでは、メンションされている API は成功します。 ただし、古いバージョンの Windows では、これらの API は失敗します。 そのような場合は、アプリローカル ICU 機能を有効にして、これらの API を確実に成功させることができます。 Windows 以外のプラットフォームでは、これらの API はバージョンに関係なく常に成功します。
さらに、これらの API の成功を保証するためには、アプリがグローバリゼーション インバリアント モードまたは NLS モードで実行されていないことを確認することが重要です。
ICU の代わりに NLS を使用する
NLS の代わりに ICU を使用すると、一部のグローバリゼーション関連の操作で動作が違ってしまうことがあります。 NLS を使用するように元に戻すには、ICU 実装をオプトアウトできます。 アプリケーションでは、次のいずれかの方法で NLS モードを有効にできます。
プロジェクト ファイルで次を実行します。
<ItemGroup> <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" /> </ItemGroup>
runtimeconfig.json
ファイルで次の操作を行います。{ "runtimeOptions": { "configProperties": { "System.Globalization.UseNls": true } } }
環境変数
DOTNET_SYSTEM_GLOBALIZATION_USENLS
の値をtrue
または1
に設定します。
注意
プロジェクトまたは runtimeconfig.json
に設定された値が環境変数に優先されます。
詳細については、ランタイムの構成設定に関するページを参照してください。
アプリで ICU を使用するかどうかを確認する
次のコード スニペットは、アプリが (NLS ではなく) ICU ライブラリを使用して実行されているかどうかを判断するのに役立ちます。
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;
}
ファイルのバージョンを調べるには、RuntimeInformation.FrameworkDescription を使用します。
アプリローカル ICU
ICU の各リリースには、バグ修正と、世界の言語が記述された更新された共通ロケール データ リポジトリ (CLDR) データが含まれる場合があります。 ICU のバージョンを変えると、グローバリゼーション関連の操作でアプリの動作がわずかに影響を受ける場合があります。 アプリケーション開発者がすべての展開にわたって整合性を確保できるように、.NET 5 以降のバージョンでは、Windows と Unix の両方のアプリが、独自の ICU のコピーを実行および使用できるようになっています。
アプリケーションは、次のいずれかの方法で、アプリローカル ICU 実装モードにオプトインできます。
プロジェクト ファイルで、次のように適切な
RuntimeHostConfigurationOption
値を設定します。<ItemGroup> <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" /> </ItemGroup>
または、runtimeconfig.json ファイルで、次のように適切な
runtimeOptions.configProperties
値を設定します。{ "runtimeOptions": { "configProperties": { "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>" } } }
または、環境変数
DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU
の値を<suffix>:<version>
または<version>
に設定します。<suffix>
: 公開されている ICU パッケージ規則に従った、長さが 36 文字未満の省略可能なサフィックス。 ICU をカスタマイズして構築する場合、libicuucmyapp
のように、ライブラリ名とエクスポートされたシンボル名にサフィックスが含まれるようにカスタマイズできます。ここでは、myapp
がサフィックスです。<version>
:67.1 などの有効な ICU のバージョン。 このバージョンは、バイナリを読み込み、エクスポートされたシンボルを取得するために使用されます。
これらのオプションのいずれかが設定されている場合、構成されている version
に対応する Microsoft.ICU.ICU4C.Runtime の PackageReference
をプロジェクトに追加でき、必要なのはそれだけです。
代わりに、.NET はアプリローカル スイッチが設定されている場合に ICU を読み込むために NativeLibrary.TryLoad メソッドを使用し、これは複数のパスをプローブします。 このメソッドでは、まず NATIVE_DLL_SEARCH_DIRECTORIES
プロパティからライブラリが検索されます。このプロパティは、アプリの deps.json
ファイルに基づき dotnet ホストが作成します。 詳細については、「既定のプローブ」を参照してください。
自己完結型アプリの場合は、ICU がアプリのディレクトリ内にあることを確認する以外に、ユーザーが特別に行う操作はありません (自己完結型アプリの場合、既定の作業ディレクトリは NATIVE_DLL_SEARCH_DIRECTORIES
です)。
NuGet パッケージの ICU を使用している場合は、フレームワークに依存するアプリケーションでこのようになります。 NuGet はネイティブ資産を解決し、deps.json
ファイルと runtimes
ディレクトリ下のアプリケーションの出力ディレクトリにそれを格納します。 .NET はそこからこれを読み込みます。
ローカル ビルドから ICU が使用されるフレームワーク依存の (自己完結でない) アプリの場合は、追加の手順を実行する必要があります。 .NET SDK には、"ルース" ネイティブ バイナリを deps.json
に組み込む機能がまだありません (この SDK の問題を参照してください)。 代わりに、アプリケーションのプロジェクト ファイルに情報を追加して有効にすることができます。 次に例を示します。
<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>
これは、サポートされているランタイムのすべての ICU バイナリに対して実行する必要があります。 また、RuntimeTargetsCopyLocalItems
項目グループの NuGetPackageId
メタデータが、プロジェクトが実際に参照する NuGet パッケージと一致している必要があります。
macOS の動作
macOS が Mach-O
ファイルで指定した読み込みコマンドから依存しているダイナミック ライブラリを解決する動作は、Linux ローダーの動作とは異なります。 Linux ローダーでは、ICU 依存関係グラフに従い、.NET が libicudata
、libicuuc
、および libicui18n
を (この順序で) 試行します。 ただし、これは macOS では機能しません。 macOS で ICU を構築する場合、ユーザーは既定でこれらの読み込みコマンドで、libicuuc
にダイナミック ライブラリを取得します。 次のスニペットに例を示します。
~/ % 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)
これらのコマンドでは、ICU の他のコンポーネントの依存ライブラリの名前のみを参照します。 ローダーは、dlopen
表記規則に従って検索を実行します。このとき、これらのライブラリはシステム ディレクトリに配置されているか、LD_LIBRARY_PATH
環境変数が設定されているか、アプリレベル ディレクトリに ICU がある必要があります。 LD_LIBRARY_PATH
を設定できない場合、または ICU バイナリがアプリレベルのディレクトリにない可能性がある場合は、作業を追加で行う必要があります。
ローダーには、その読み込みコマンドでバイナリと同じディレクトリから、その依存関係を検索するように指示する、@loader_path
などのディレクティブがいくつかあります。 これを実現する方法は 2 つあります。
install_name_tool -change
次のコマンドを実行します。
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
@loader_path
が使用されたインストール名が作成されるよう、ICU をパッチします。autoconf (
./runConfigureICU
) を実行する前に、これらの行を次のように変更します。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))
WebAssembly での ICU
WebAssembly ワークロード専用の ICU バージョンを利用できます。 このバージョンでは、デスクトップ プロファイルとのグローバリゼーションの互換性が提供されます。 ICU データ ファイルのサイズを 24 MB から 1.4 MB (Brotli で圧縮する場合は約 0.3 MB) に減らすため、このワークロードにはいくつかの制限があります。
次の API はサポートされていません。
- CultureInfo.EnglishName
- CultureInfo.NativeName
- DateTimeFormatInfo.NativeCalendarName
- RegionInfo.NativeName
次の API は、制限付きでサポートされています。
- String.Normalize(NormalizationForm) と String.IsNormalized(NormalizationForm) では、使用頻度の低い FormKC と FormKD 形式はサポートされていません。
- RegionInfo.CurrencyNativeName からは RegionInfo.CurrencyEnglishName と同じ値が返されます。
さらに、サポートされるロケールも少なくなります。 サポートされている一覧は、dotnet/icu リポジトリで確認できます。
.NET