.NET, MSBuild, Localization

これはあれですね。「メモリは writtenになることができませんでした」(うろ覚え)と 根は同じだろうと思います。 まあメモリは〜の方は、カーネルがらみなのでもっと根が深いんですが。

つまり、このようなメッセージをたとえば日本語として自然な順序で出力できるようにするには

printf("%$2d個のファイルが$1sにありました。", v1, v2);

のような、引数の位置を指定できる仕掛けがどうしても必要になるのですが、 VC++のライブラリはこれサポートしてくれてないんですよね。 まあ思ったより実装が面倒だというのは理解できるんですが (サブセットを自分で作ったことがあるので)、 にしてもこの辺怠慢といいたくなる部分がありますね。 setlocale()の動作とか。 西欧や東欧の言語でも英語と語順が変わる可能性がないわけはないと思うんですが、 そのあたりはどうなんでしょうね。さすがに「writtenになれませんでした」のような 迷めいたメッセージはないんでしょうけど :)

それでいて、C#なんかだと "{1} {3} {2}" のように位置指定ができるので なんだかなあと思うわけですが。

それは言語の問題というよりはフレームワークの問題じゃないかなぁとは思いつつ (つまりここで VC++ とか C# という分け方はちと気持ち悪いかなと),MSBuild のターゲットファイルやリソースだったりしたら修正簡単かも,ということでチェックしてみたり.
まずは targets ファイルから*1.targets ファイルの Message 要素として書かれているのであれば,順序を変えるようなローカライズはまあ余裕なので.

%ProgramFiles%\Program Files 以下の *.targets を全検索
それらしい箇所は見つからず
%Windir%\Microsoft.NET\Framework 以下の *.targets を全検索
それらしい箇所は見つからず

うーむ残念.では次にリソースのダンプでも.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Resources;
using System.Collections;
using System.Reflection;

public static class LinqUtil
{
    public static IEnumerable<TResult>
        UsingSelect<TSource, TDisposable, TResult>(
            this IEnumerable<TSource> sources,
            Func<TSource, TDisposable> initializer,
            Func<TSource, TDisposable, TResult> selector
        ) where TDisposable : IDisposable
    {
        if (sources == null) throw new ArgumentNullException("sources");

        foreach (var source in sources)
        {
            using (var intermediate = initializer(source))
            {
                yield return selector(source, intermediate);
            }
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        var msbuildAsmNames = new[]
        {
            "Microsoft.Build.Engine, Version=2.0.0.0",
            "Microsoft.Build.Engine, Version=3.5.0.0",
            "Microsoft.Build.Tasks, Version=2.0.0.0",
            "Microsoft.Build.Framework, Version=3.5.0.0",
            "Microsoft.Build.Utilities, Version=2.0.0.0",
            "Microsoft.Build.Utilities.v3.5, Version=3.5.0.0",
        }.Select(name => name + ", Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");

        var resourceSets =
            from asmname in msbuildAsmNames
            let asm = Assembly.Load(asmname)
            let resnames = from resname in asm.GetManifestResourceNames()
                           where resname.EndsWith("resources")
                           select resname
            select resnames.UsingSelect
            (
               resname => new ResourceSet(asm.GetManifestResourceStream(resname)),
               (resname, resset) =>
                   new
                   {
                       Name = resname,
                       Items = resset.Cast<DictionaryEntry>().ToDictionary
                       (
                           item => (string)item.Key,
                           item => item.Value
                       )
                   }
            ) into resources
            from resource in resources
            select resource;

        foreach (var resourceSet in resourceSets)
        {
            {
                var fc = Console.ForegroundColor;
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(resourceSet.Name);
                Console.ForegroundColor = fc;
            }

            foreach (var resource in resourceSet.Items)
            {
                Console.WriteLine("   {0}\n      {1}", resource.Key, resource.Value);
            }

            Console.WriteLine();
        }
    }
}

こんな感じのコードで 6 つぐらいのアセンブリのリソースをダンプダンプダンプ.が,こちらもめぼしい文字列は見つからず.というわけで,Visual Studio チームに直して下さいのお願いでやはり正解の模様.
ちなみに上のコードの出力はこんな感じ.

Microsoft.Build.Engine.Resources.Strings.resources
   SolutionParseUpgradeNeeded
      MSB4054: The solution file must be opened in the Visual Studio IDE and converted to the latest version before it can be built by MSBuild.
   InvalidAttributeValueWithException
      MSB4102: The value "{0}" of the "{1}" attribute in element <{2}> is invalid. {3}
   InvalidEventCategory
      "{0}" is not a valid event category. To raise a custom event, use the "{1}" category.
   BuildTargetPartiallyInputNewer
      [{0}: Input={1}, Output={2}] Input file is newer than output file.
   SkipTargetUpToDateOutputs
      Output files: {0}
(以下略)

こんな感じでリソースには .NET のフォーマット文字列が埋め込まれていて,こっちの範囲でも順序を変えるようなローカライズはよゆーと.もっとも,ここにも該当のメッセージはなかったので,Visual Studio のビルドメッセージを何とかしたいというもくろみ自体は外れっすが.

*1:「Microsoft の開発環境って何でも自動で気持ち悪いよね,プログラマなら Makefile 手書きぐらいできないと」って人はここに挙げた場所の *.targets ファイルを全部眺めてみると楽しいかも