.resx ファイルのバージョン管理

アセンブリは任意の名前付きデータストリームを埋め込みリソースとして保持することができるが、中でも .resources という拡張子をもつ名前で保存されるリソースはマネージドリソースと呼ばれ、System.Resources.ResourceManager などのクラスで手軽に扱えるほか、それらのクラスの備えるサテライトアセンブリの検索とリソースのフォールバック検索機能の恩恵を受けることが出来る。
.resources ファイルは、.NET Framework SDK に付属の WinRes.exe や ResEditor によって編集することができるが、多くの場合ではテキストファイルか .resx 形式のファイルを resgen.exe などによってコンパイルして作成するか、Visual Studio .NET のような開発環境がヴィジュアルデザイナやデータデザイナを通して透過的に書き換え、自動的にコンパイル・リンクされる。
.resx ファイルは XML 文章の形式をとっているので、テキストベースのソース管理システムに手軽に登録ができるし、通常のテキストファイル用の diff プログラムで差分を文章として確認することもできる。しかし、実際に Visual Studio .NET でフォームやコンポーネントをヴィジュアルデザイナを通して操作した場合、出力されている .resx ファイルの差分をとると、その内容の DATA ノードが毎回々々順不同で保存されているため、ほんの少しの変更でもファイル全体が大掛かりに変化してしまい、その差を視認することが非常に困難であることが残念である。
以前紹介した XmlDiff のような XML 文章用の差分ツールを使えば、出現順序の差やノード単位の増減を見ることはできるが、CVSSubversion のような差分ベースの*1リビジョン管理システムに登録して履歴を見たりや通常のベタテキスト用の差分ツールで違いを見たりマージしたりするには都合が悪いので、リビジョン管理システムに追加する前に .resx ファイルの内容をソートしてしまいましょう。ソートするのは簡単で、たとえば

  public ResxEntry : IComparable
  {
    public readonly string Key;
    public readonly object Value;

    public ResxEntry(string key, object value)
    {
      this.Key   = key;
      this.Value = value;
    }

    public int CompareTo(object obj)
    {
      ResxEntry that = obj as ResxEntry;
      if (that == null) throw new ArgumentException();

      return this.Key.CompareTo(that.Key);
    }
  }

  public static ArrayList Load(string filename)
  {
    ArrayList entries = new ArrayList();

    using (ResXResouceReader reader = new ResXResourceReader(filename))
    {
      foreach (DictionaryEntry de in reader)
        entries.Add( new ResxEntry(de.Key as string, de.Value);
    }

    return entries;
  }

  public static void Save(string filename, IEnumerable e)
  {
    using (ResXResourceWriter writer = new ResXResourceWriter(filename))
    {
      foreach (ResxEntry entry in e)
        writer.AddResource(entry.Key, entry.Value);

      writer.Generate();
    }
  }

  static void Main(string[] args)
  {
    foreach (string filename in args)
    {
      ArrayList entries = Load(filename);

      entries.Sort();
      Save(filename, entries);
    }
  }

こんなかんじになる。ソートする側が IComparer にならずに ResxEntry を IComparable にしてるのは手抜きです。ArrayList とか IEnumerable なのは .NET 2.0 の都合です。あとは、ソース管理側…たとえば Subversion の pre-commit フックでソートされていない .resx ファイルの commit を許さないなどの制限を課します。

*1:CVSリポジトリ形式が差分ベースであり、Subversionトランザクションが差分ベースと、同じ差分ベースでも意味が違うが、とりあえずここでは差分をうまく管理しているシステムという意味