web-dev-qa-db-ja.com

C#6.0文字列補間ローカリゼーション

C#6.0には 文字列補間 があります。次のような文字列をフォーマットするための素晴らしい機能です

 var name = "John";
 WriteLine($"My name is {name}");

例は次のように変換されます

 var name = "John";
 WriteLine(String.Format("My name is {0}", name));

ローカリゼーションの観点から、次のような文字列を保存する方がはるかに優れています。

"My name is {name} {middlename} {surname}" 

string.Format表記よりも:

"My name is {0} {1} {2}"

.NETローカライズに文字列補間を使用する方法は?リソースファイルに$ "..."を追加する方法はありますか?または、文字列を「... {name}」のように保存し、その場で補間する必要がありますか?

追伸この質問は、「string.FormatIt拡張の作成方法」に関するものではありません(そのようなライブラリがたくさんありますSO回答など)。この質問は、「string 「ローカリゼーション」コンテキストでの「補間」(両方ともMS .NET語彙の用語)、またはDylanが提案したような動的な使用法。

65
MajesticRa

Microsoft.CodeAnalysis.CSharp.Scripting パッケージを使用すると、これを実現できます。

動的オブジェクトが使用される下に、データを保存するオブジェクトを作成する必要があります。必要なすべてのプロパティを持つ特定のクラスを作成することもできます。記述されている here でクラスに動的オブジェクトをラップする理由。

public class DynamicData
{
    public dynamic Data { get; } = new ExpandoObject();
}

その後、次のように使用できます。

var options = ScriptOptions.Default
    .AddReferences(
        typeof(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException).GetTypeInfo().Assembly,
        typeof(System.Runtime.CompilerServices.DynamicAttribute).GetTypeInfo().Assembly);

var globals = new DynamicData();
globals.Data.Name = "John";
globals.Data.MiddleName = "James";
globals.Data.Surname = "Jamison";

var text = "My name is {Data.Name} {Data.MiddleName} {Data.Surname}";
var result = await CSharpScript.EvaluateAsync<string>($"$\"{text}\"", options, globals);

これはコードのスニペットをコンパイルして実行するため、真のC#文字列補間です。ただし、実行時にコードを実際にコンパイルして実行するため、このパフォーマンスを考慮する必要があります。 CSharpScript.Createを使用してコードをコンパイルおよびキャッシュできる場合、このパフォーマンスヒットを回避します。

3
Dylan

補間された文字列は、中括弧の間のブロックをC#式として評価します(例:{expression}{1 + 1}{person.FirstName})。

つまり、補間された文字列の式は、現在のコンテキストの名前を参照する必要があります。

たとえば、次のステートメントはコンパイルされません。

var nameFormat = $"My name is {name}"; // Cannot use *name*
                                       // before it is declared
var name = "Fred";
WriteLine(nameFormat);

同様に:

class Program
{
    const string interpolated = $"{firstName}"; // Name *firstName* does not exist
                                                // in the current context
    static void Main(string[] args)
    {
        var firstName = "fred";
        Console.WriteLine(interpolated);
        Console.ReadKey();
    }
}

質問に答えるには:

実行時に補間された文字列を評価するためのフレームワークによって提供される現在のメカニズムはありません。したがって、文字列を保存したり、箱から出してすぐに補間したりすることはできません。

文字列の実行時補間を処理するライブラリが存在します。

35
Dustin Kingen

Roslyn codeplexサイトの この説明 によると、文字列の補間はリソースファイルと互換性がない可能性があります(エンファシスマイニング)。

文字列の補間は、String.Formatまたは連結のいずれかよりも簡潔でデバッグが容易です。

Dim y = $"Robot {name} reporting
{coolant.name} levels are {coolant.level}
{reactor.name} levels are {reactor.level}"

ただし、この例は厄介です。ほとんどのプロのプログラマーは、コードでユーザー向けの文字列を作成しません。 代わりに、ローカライズの理由から、それらの文字列をリソース(.resw、.resx、または.xlf)に保存します。そのため、ここでは文字列補間の使用はあまりないようです。

15
BJ Myers

以前の回答ですでに述べたように、現在、コンパイル時に使用されるため、文字列補間のために実行時に(たとえば、リソースファイルから)フォーマット文字列をロードすることはできません。

コンパイル時の機能を気にせず、名前付きのプレースホルダーだけが必要な場合は、この拡張メソッドのようなものを使用できます。

public static string StringFormat(this string input, Dictionary<string, object> elements)
{
    int i = 0;
    var values = new object[elements.Count];
    foreach (var elem in elements)
    {
        input = Regex.Replace(input, "{" + Regex.Escape(elem.Key) + "(?<format>[^}]+)?}", "{" + i + "${format}}");
        values[i++] = elem.Value;
    }
    return string.Format(input, values);
}

ここで{i+1}のようなインライン式を使用することはできず、これは最高のパフォーマンスを発揮するコードではないことに注意してください。

これは、リソースファイルからロードするディクショナリまたは次のようにインラインで使用できます。

var txt = "Hello {name} on {day:yyyy-MM-dd}!".StringFormat(new Dictionary<string, object>
            {
                ["name"] = "Joe",
                ["day"] = DateTime.Now,
            });
5
stb

あなたの質問は、補間された文字列リソースを処理する方法ではなく、ソースコード内の補間された文字列をローカライズする方法に関するものだと仮定しています...

サンプルコードを考えます:

var name = "John";
var middlename = "W";
var surname = "Bloggs";
var text = $"My name is {name} {middlename} {surname}";
Console.WriteLine(text);

出力は明らかに次のとおりです。

My name is John W Bloggs

テキストの割り当てを変更して、代わりに翻訳を取得します。

var text = Translate($"My name is {name} {middlename} {surname}");

Translateは次のように実装されます。

public static string Translate(FormattableString text)
{
    return string.Format(GetTranslation(text.Format),
        text.GetArguments());
}

private static string GetTranslation(string text)
{
    return text; // actually use gettext or whatever
}

GetTranslationの独自の実装を提供する必要があります。 "My name is {0} {1} {2}"のような文字列を受け取り、GetTextまたはリソースなどを使用して適切な翻訳を見つけて返すか、元のパラメーターを返すだけで翻訳をスキップする必要があります。

パラメータ番号の意味を翻訳者に文書化する必要があります。元のコード文字列で使用されているテキストは、実行時には存在しません。

たとえば、この場合、GetTranslation"{2}. {0} {2}, {1}. Don't wear it out."を返した場合(ローカライズは言語だけではありません!)、完全なプログラムの出力は次のようになります。

Bloggs.  John Bloggs, W.  Don't wear it out.

とはいえ、このスタイルの翻訳を使用すると開発が容易ですが、文字列がコードに埋め込まれ、実行時にのみ表示されるため、実際に翻訳するのは困難です。 静的にコードを探索し、すべての翻訳可能な文字列を(実行時にそのコードパスにヒットすることなく)抽出できるツールがない限り、より伝統的なresxファイルを使用する方が良いでしょう。翻訳するテキストの表を提供します。

3
Miral

フォーマット文字列がC#ソースコードにない場合、C#6.0文字列補間は役に立ちません。その場合、 this library のような他のソリューションを使用する必要があります。

3
svick

補間を使用する場合、定数ではなくメソッドの観点で考えています。その場合、翻訳をメソッドとして定義できます。

public abstract class InterpolatedText
{
    public abstract string GreetingWithName(string firstName, string lastName);
}

public class InterpolatedTextEnglish : InterpolatedText
{
    public override string GreetingWithName(string firstName, string lastName) =>
        $"Hello, my name is {firstName} {lastName}.";
}

その後、特定のカルチャのInterpolatedTextの実装をロードできます。これにより、フォールバックを実装する方法も提供されます。これは、ある実装が別の実装を継承できるためです。英語がデフォルト言語であり、他の実装がそれを継承する場合、翻訳が提供されるまで少なくとも表示するものがあります。

これは少し正統ではないように見えますが、いくつかの利点があります。

主に、補間に使用される文字列は、明確に指定された引数を使用して、厳密に型指定されたメソッドに常に格納されます。

これを考えると、"Hello, my name is {0} {1}"は、プレースホルダーが名と姓をこの順序で表していると判断できますか?値をプレースホルダーに一致させるメソッドは常に存在しますが、補間された文字列がその引数とともに保存される場合、混乱の余地は少なくなります。

同様に、翻訳文字列をある場所に保存し、別の場所で使用する場合、それらを使用するコードを壊す方法でそれらを変更することが可能になります。 {2}を他の場所で使用される文字列に追加すると、そのコードは実行時に失敗します。

文字列補間を使用すると、これは不可能です。翻訳文字列が利用可能な引数と一致しない場合、コンパイルもされません。


欠点がありますが、ソリューションを維持するのは困難です。

最大のものは移植性です。翻訳がC#でコーディングされていて、切り替えた場合、すべての翻訳をエクスポートするのは簡単なことではありません。

また、(すべてを話す1人の人がいない限り)別の個人に翻訳を提供したい場合は、翻訳者がコードを変更する必要があります。それは簡単なコードですが、それにもかかわらずコード。

1
Scott Hannen

文字列補間は、コンパイラがローカライズをサポートしないstring.Format(...)に変換することを好むため、ローカライズと組み合わせることが困難です。ただし、ローカライズと文字列補間を組み合わせることを可能にするトリックがあります。 この記事 の終わり近くで説明されています。

通常、文字列補間は_string.Format_に変換され、その動作はカスタマイズできません。ただし、ラムダメソッドが式ツリーになることとほぼ同じ方法で、ターゲットメソッドが_string.Format_オブジェクトを受け入れる場合、コンパイラは_FormattableStringFactory.Create_から_System.FormattableString_(.NET 4.6メソッド)に切り替えます。 。

問題は、コンパイラは可能であれば_string.Format_を呼び出すことを好むため、FormattableStringを受け入れるLocalized()のオーバーロードがあった場合、C#コンパイラは単純にそれを無視します[プレーン文字列を受け入れるオーバーロードがあるため]。実際、それより悪いのは、コンパイラが拡張メソッドを呼び出すときにFormattableStringの使用も拒否することです。

非拡張メソッドを使用すると機能します。例えば:

_static class Loca
{
    public static string lize(this FormattableString message)
        { return message.Format.Localized(message.GetArguments()); }
}
_

その後、次のように使用できます。

_public class Program
{
    public static void Main(string[] args)
    {
        Localize.UseResourceManager(Resources.ResourceManager);

        var name = "Dave";
        Console.WriteLine(Loca.lize($"Hello, {name}"));
    }
}
_

コンパイラが_$"..."_文字列を昔ながらの形式の文字列に変換することを理解することが重要です。したがって、この例では、_Loca.lize_は実際に_"Hello, {0}"_ではなく、フォーマット文字列として_"Hello, {name}"_を受け取ります。

0
Qwertie