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

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

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

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

Windows 上の ICU

Windows 10 の 2019 年 5 月更新プログラム以降では、OS の一部として icu.dll が含まれるようになり、.NET 5 以降のバージョンでは、既定で ICU が使用されるようになりました。 .NET 5 以降のバージョンを Windows で実行する場合、icu.dll の読み込みが試行され、存在する場合、グローバリゼーションの実装で使用されます。 Windows の古いバージョンを実行している場合などで、ICU ライブラリを見つけたり、読み込んだりすることができない場合、.NET 5 以降のバージョンは NLS ベースの実装に戻ります。

注意

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 がスニペットによって出力されます。
  • Windows 19H1 以降のバージョン上の .NET 5 以降のバージョンでは、スニペットにより 00、および 3 が出力されます (序数検索の場合)。

既定では、String.IndexOf(String) によって、カルチャに対応した言語検索が実行されます。 ICU では、null 文字 \0 を "0 の重み付け文字" と見なします。したがって、.NET 5 以降で言語検索を使用する場合、文字列内のその文字は検索されません。\0 ただし、NLS では null 文字 \0 は 0 の重み付け文字とは見なされず、.NET Core 3.1 以前で言語検索を行うと、その文字は位置 3 で検索されます。 序数検索を行うと、すべての .NET バージョンの位置 3 で該当する文字が検索されます。

コード分析規則「CA1307: 意味を明確にするための StringComparison の指定」および「CA1309: 順序を示す StringComparison を使用します」に従うと、文字列比較が指定されていない、または序数ではないコード内の呼び出しサイトを検索できます。

詳細については、「.NET 5 以降で文字列を比較するときの動作の変更」を参照してください。

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 が実装されるモードをオプトインできます。

  • プロジェクト ファイルで次を実行します。

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • runtimeconfig.json ファイルで次の操作を行います。

    {
      "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 のバージョン。 このバージョンは、バイナリを読み込み、エクスポートされたシンボルを取得するために使用されます。

.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 に組み込む機能がまだありません (deps.jsonを参照してください)。 代わりに、アプリケーションのプロジェクト ファイルに情報を追加して有効にすることができます。 次に例を示します。

<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 の動作

macOSmatch-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) を実行する前に、./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 リポジトリを参照してください。