.NET グローバリゼーションと ICU

.NET 5 より前では、.NET グローバリゼーション API は、プラットフォームごとに別々の基になるライブラリを使用していました。 この API は、Unix では、ICU (International Components for Unicode) を使用し、Windows では、各国語サポート (NLS) を使用していました。 これにより、アプリケーションを実行するとき、いくつかのグローバリゼーション API の動作がプラットフォームによって違うことがありました。 次の領域で、動作が違うことがわかっています。

  • カルチャとカルチャ データ
  • 文字の大文字小文字
  • 文字の並べ替えと検索
  • 並べ替えキー
  • 文字の正規化
  • 国際化ドメイン名 (IDN) のサポート
  • Linux のタイム ゾーンの表示名

.NET 5 以降では、プラットフォームによってアプリケーションに違いがでることがないよう、開発者が基になるライブラリをより制御できるようになりました。

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 では、ユーザーが設定している場合は、CurrentCultureCurrentUICulture、および CurrentRegion のメンバーが使用されます。

動作の違い

.NET 5 以降を対象にするようにアプリをアップグレードすると、グローバリゼーション機能を使用していることを認識していない場合でも、アプリでの変更に気付くことがあります。 ここでは、気付く可能性のある動作の変更を 1 つ示しますが、他にもあります。

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 以降のバージョンでは、スニペットは 003 (序数検索用) を出力します。

既定では、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.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'));

重要

Windows 上の ICU テーブルに記載されている Windows バージョンで実行されている .NET 5 以降では、上記のスニペットは次を出力します。

True
True
True
False
False

この動作を回避するには、char パラメーターのオーバーロードまたは StringComparison.Oridinal を使用します。

TimeZoneInfo.FindSystemTimeZoneById

ICU では、アプリケーションが Windows で実行されている場合でも、IANA タイム ゾーン ID を使用して TimeZoneInfo インスタンスを柔軟に作成できます。 同様に、Windows 以外のプラットフォームで実行している場合でも、Windows タイム ゾーン ID を使用して TimeZoneInfo インスタンスを作成できます。 ただし、この機能は NLS モードまたはグローバリゼーション インバリアント モードを使用する場合は、使用できないことに注意することが重要です。

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.RuntimePackageReference をプロジェクトに追加でき、必要なことはこれがすべてです。

代わりに、.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 が libicudatalibicuuc、および 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 はサポートされていません。

次の API は、制限付きでサポートされています。

さらに、サポートされるロケールも少なくなります。 サポートされている一覧は、dotnet/icu リポジトリで確認できます。