スクリプトを使ったXMLドキュメントコメントの多言語対応

# 概要 (TL;DR)

* C# ライブラリの **XML ドキュメントコメントに `xml:lang` 属性を付けて多言語化**し、自作スクリプトで**言語別の XML ファイルを出力**するようにしました。
* Visual Studio の IntelliSense などで、**日本語の開発環境では日本語の、それ以外では英語の XML ドキュメントコメントが、それぞれ優先して表示**されるようになります。
* 出力された XML ファイルは NuGet パッケージにも同梱できます。
* GitHub に[サンプル一式](https://github.com/DTXMania/SeparateDocs)を置いてあります。

# XML ドキュメントコメントとは

自前で .NET ライブラリを作る際には、**XML ドキュメントコメント**を記述しておくと、そのライブラリを参照した時に IntelliSence でヒントが表示されるようになって便利です。
XML ドキュメントコメントとは何ぞ？という方は、「[XMLコメントを含むコードの文書化](https://docs.microsoft.com/ja-jp/dotnet/csharp/codedoc)」（Microsoft）をご参照ください。

# XML ドキュメントコメントの多言語化

* [Step 1. XML ドキュメントコメントファイルを出力するようにしよう](#step-1-xml-ドキュメントコメントファイルを出力するようにしよう)
* [Step 2. XML ドキュメントコメントを書こう](#step-2-xml-ドキュメントコメントを書こう)
* [Step 3. XMLドキュメントコメントを「多言語化」しよう](#step-3-xmlドキュメントコメントを多言語化しよう)
* [Step 4. プロジェクトに分割用スクリプトを仕込もう](#step-4-プロジェクトに分割用スクリプトを仕込もう)
* [Step 5. NuGet パッケージへ取り込もう](#step-5-nuget-パッケージへ取り込もう)

## Step 1. XML ドキュメントコメントファイルを出力するようにしよう

まずは、Visual C# プロジェクトで XML ドキュメントファイルを出力するように設定します。
Visual Studio の C# プロジェクトの `プロパティ` → `ビルド` を表示し、`XMLドキュメントファイル` にチェックを入れます。

![OutputXMLDocumentFile.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185533/58a59440-a653-ff27-4e27-0ecdd6f4946c.png)

これで、プロジェクトをビルドするたびに、`.dll` ファイルと一緒に `.xml` ファイルが出力されるようになります。

![OutputXMLDocumentFile2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185533/c0cd298a-a576-e897-153f-0c815c0f673f.png)

**補足(2021/06/16)**
`.xml` ファイルは、出力フォルダ（`.dll` のあるフォルダ）とは **別に**、`XML ドキュメント ファイル` で指定したパスにも出力されます。上記のように指定した場合はプロジェクトフォルダに `.xml` が出力されてしまうので、それが Visual Studio に拾われてイヤだという場合は、パスとして `bin\SampleLibrary.xml` などのように別の場所を指定してください。

## Step 2. XML ドキュメントコメントを書こう

`.xml` ファイルが出力されるようになったので、C# コードに XML ドキュメントコメントをどんどん書いていきましょう。

以下はサンプルです。

```CSharp:SampleLibrary.cs
using System;
using System.Runtime.InteropServices;

namespace SampleLibrary
{
    /// <summary>
    ///     Converts an array to an array of a different type.
    /// </summary>
    public class ArrayConverter
    {
        /// <summary>
        ///     Converts float[] to byte[].
        /// </summary>
        /// <param name="source">A float array.</param>
        /// <returns>A new byte array.</returns>
        public static byte[] ToByteArray( float[] source )
        {
            var sourceFloatSpan = new Span<float>( source );
            var sourceByteSpan = MemoryMarshal.Cast<float, byte>( sourceFloatSpan );
            return sourceByteSpan.ToArray();
        }
    }
}
```

## Step 3. XMLドキュメントコメントを「多言語化」しよう

Visual Studio の `.xml` ファイル出力機能は、複数の言語には対応していません。

そこで、XML ドキュメントコメントをいろんな言語で記述（**多言語化**）し、IntelliSense やオブジェクトブラウザでは**開発環境で使用している言語のコメントが優先的に表示**されるようにしたいと思います。

一般的な XML の多言語化ルールに基づいて、`xml:lang` 属性を使って言語を指定することにします。（参考：[@IT - XMLを学ぼう（11）多言語対応の問題と解決を考える](https://www.atmarkit.co.jp/ait/articles/0103/24/news003.html)）

```CSharp:SampleLibrary.cs
using System;
using System.Runtime.InteropServices;

namespace SampleLibrary
{
    /// <summary>
    ///     <para xml:lang="en">Converts an array to an array of a different type.</para>
    ///     <para xml:lang="ja">配列を異なる型の配列に変換します。</para>
    /// </summary>
    public class ArrayConverter
    {
        /// <summary>
        ///     <para xml:lang="en">Converts float[] to byte[].</para>
        ///     <para xml:lang="ja">float[] を byte[] に変換します。</para>
        /// </summary>
        /// <param name="source">
        ///     <para xml:lang="en">An array of floats.</para>
        ///     <para xml:lang="ja">float 配列。</para>
        /// </param>
        /// <returns>
        ///     <para xml:lang="en">A new array of bytes.</para>
        ///     <para xml:lang="ja">新しい byte 配列。</para>
        /// </returns>
        public static byte[] ToByteArray( float[] source )
        {
            var sourceFloatSpan = new Span<float>( source );
            var sourceByteSpan = MemoryMarshal.Cast<float, byte>( sourceFloatSpan );
            return sourceByteSpan.ToArray();
        }
    }
}
```

* 読みやすさのために `<para>` タグを使っていますが、`<summary>` や `<param>` や `<return>` などの他のタグでも `xml:lang` 属性を使うことができます。
* コメントのインデントは、あってもなくても構いません。こちらも見た目の問題です。

## Step 4. プロジェクトに分割用スクリプトを仕込もう

Visual Studio は `xml:lang` 属性に対応していないので、このままでは単に1つの `.xml` ファイルにコメントが丸々出力されてしまいます。
そこで、プロジェクトに**分割用スクリプト**を仕込み、ビルド後にその `.xml` ファイルを言語別 XML に自動分割するようにします。スクリプトの言語は PowerShell です。

### 言語別 XML とは？

Visual Studio は XML ドキュメントコメントの「出力」については複数の言語に対応していませんが、「参照」については対応しています。
**言語ごとにフォルダを作ってその中にその言語用の `.xml` ファイルを入れる**と、Visual Studio は**開発環境の言語に対応した `.xml` ファイルを既定の `.xml` ファイルより優先する**ようになります。（既定の `.xml` ファイルとは、`.dll` と同じ場所に置かれた `.xml` ファイルのことです。）

言語別 XML の配置例を以下に示します。

```
SampleLibrary.dll
SampleLibrary.xml
ja\
  SampleLibrary.xml
```

このような形に `.xml` ファイルを配置するだけで、日本語の開発環境の Visual Studio では `ja/SampleLibrary.xml` が優先して参照され、そこに記述がない場合のみ既定の `SampleLibrary.xml` の内容が参照されるようになります。

### プロジェクトに分割用スクリプトを仕込む

分割用スクリプト（`SeparateDocs.ps1`）は、少し長いですが、次の通りです。
GitHub にも[上げてあります](https://github.com/DTXMania/SeparateDocs/blob/main/SampleLibrary/SeparateDocs.ps1)。

```PowerShell:SeparateDocs.ps1
# Separate Docs
# A tool to split XML document comment files based on 'xml:lang' attribute.
# usage: powershell -File separateDocs.ps1 <projectName> <projectOutDir> <xmlOutDir>

Param( [parameter(mandatory)] $projectName,$outdir,$xmldocdir )

Write-Host "Separate Docs (C) Copyright 2020 FROM/Kenji Yamazaki"

# XPath for target documents
$targets = @("//member/*", "//member/*/*")
# target languages
$langs = @("en", "ja")

$script:ErrorActionPreference = 'Stop'

$xmlpath = "${outdir}${projectName}.xml"
Copy-Item ${xmlpath} "${xmlpath}.org"

foreach( $dn in $langs ) {

    $xml = [xml] (Get-Content -Encoding UTF8 "${xmlpath}.org" )

    # namespace for 'xml:lang'
    $ns = New-Object -TypeName System.Xml.XmlNamespaceManager -ArgumentList $xml.NameTable
    $ns.AddNamespace('xml', 'http://www.w3.org/XML/1998/namespace')

    foreach( $target in $targets )
    {
        foreach( $item in $xml.SelectNodes("${target}[@xml:lang!='${dn}']", $ns) )
        {
            $item.ParentNode.RemoveChild( $item ) | Out-Null
        }
    }

    if( ${dn} -eq "en" )
    {
        # en for default
        $docfolder = ${xmldocdir}
    }
    else
    {
        # other into subfolder
        $docfolder = "${xmldocdir}\${dn}"
    }

    if( !( Test-Path $docfolder ) )
    {
        mkdir $docfolder | Out-Null
    }

    $outpath = "${docfolder}\${projectName}.xml"
    $xml.Save( $outpath )
    
    Write-Host "--> xml:lang='${dn}', ${outpath} done."

    # feedback xml from generatedDir to outdir 
    if( ${dn} -eq "en" )
    {
        # en for default
        Copy-Item ${outpath} ${xmlpath}
    }
    else
    {
        $feedbackDir = "${outdir}\${dn}"
        if( !( Test-Path $feedbackDir ) )
        {
            mkdir $feedbackDir | Out-Null
        }
        Copy-Item ${outpath} "${feedbackDir}\${projectName}.xml"
    }
}
```

このスクリプトを実行すると、Visual Studio がビルドで出力する一本の `.xml` ファイルから、既定（en, 英語）の `.xml` ファイルと、日本語（ja）の `.xml` ファイルが抽出されます。日本語の `.xml` ファイルは、`ja\` フォルダ内に出力されます。

このスクリプトの使い方は次の通りです。

```
powershell -File separateDocs.ps1 <プロジェクト名> <入力フォルダ> <出力フォルダ>
```

`<プロジェクト名>` には、Visual C# プロジェクトの名前を指定します。このプロジェクト名は、Visual Studio が出力する XML ファイルの名前でもあります。
`<入力フォルダ>` には、Visual Sutdio が出力する `.xml` ファイルの存在するフォルダを指定します。
`<出力フォルダ>` には、スクリプトが抽出した各 `.xml` ファイルを出力するフォルダを指定します。

例えば、次のようなコマンドラインを C# プロジェクトの「**ビルド後イベント**」に設定すれば、ビルド後にスクリプトが自動実行され、プロジェクトフォルダの `Generated\` フォルダ内に言語別 XML が出力されるようになります。

```:ビルド後イベントスクリプト
powershell -ExecutionPolicy RemoteSigned -File "$(ProjectDir)separateDocs.ps1" $(ProjectName) $(OutDir) $(ProjectDir)Generated
```

### スクリプトの制約とカスタマイズ

前掲した `separateDocs.ps1` スクリプトには、今のところ次のような制約があります。

* 英語（en）と日本語（ja）を抽出します。英語は既定の言語となります。
* `xml:lang` 属性が付与されていないタグは、両方の XML ファイルに出力されます。
* `xml:lang` 属性は、第一階層と第二階層の XML タグで有効です。

言語を増やしたい場合は、スクリプトの最初のほうにある `$langs` 変数に追記してください。（英語（en）は必須です。）
`xml:lang` が有効な XML タグ階層を増やしたい場合は、同じく `$targets` 変数に追記してください。

## Step 5. NuGet パッケージへ取り込もう

分割スクリプトが出力する言語別 XML ファイルを NuGet パッケージに含めるには、次のように、 `<Content>` タグを `.csproj` に追加します。（ここでは、言語別 XML ファイルが `Generated\` フォルダ配下に出力されているものとします。）

```xml:SampleLibrary.csproj
<ItemGroup>
    <Content Include="Generated\*\*.xml" Pack="true" PackagePath="lib\$(TargetFramework)" />
</ItemGroup>
```

例えば、ターゲットフレームワークが .NET 5.0 である場合には、生成される Nuget パッケージのファイル構成は次のようになります。

```
lib\
    net5.0\
        ja\
            SampleLibrary.xml   ... 日本語用 XML
        SampleLibrary.dll
        SampleLibrary.xml       ... 既定の言語（英語）用 XML
```