【问题标题】:C#6.0 string interpolation localizationC#6.0 字符串插值本地化
【发布时间】:2015-05-18 01:23:17
【问题描述】:

C#6.0 有一个 string interpolation - 一个很好的功能来格式化字符串,例如:

 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 答案等)。这个问题是关于“本地化”上下文中“字符串插值”的 Roslyn 扩展(两者都是 MS .NET 词汇表中的术语),或者像 Dylan 提出的动态用法。

【问题讨论】:

  • 你能澄清你的问题吗?您是在问如何在 C# 6.0 中执行此操作,还是在早期版本的 C# 中执行此操作?
  • 我不会将这些字符串存储在文件中以供以后读取。您仍然应该创建一个存储这些字段的类。将它们读入。然后在本地通过字符串插值传递它们。
  • @nintendojunkie 我在问 C# 6.0。关于是否可以使用 C# 6.0 字符串插值进行本地化以及如果可能的话如何做。
  • 还没有回应,只是我正在研究 Roslyn 的一个分支以提供 TranslatableStrings

标签: c# .net c#-6.0


【解决方案1】:

内插字符串将花括号之间的块计算为 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();
    }
}

回答你的问题:

框架目前没有提供在运行时评估内插字符串的机制。因此,您不能开箱即用地存储字符串和插值。

存在处理字符串运行时插值的库。

【讨论】:

  • 我同意插值与许多库所做的 format("...{name}") 不同。你的帖子只是说它是在另一个层面上完成的。是真的。但是 .NET、C#、Roslyn 为您提供了如此丰富的可能性,以至于我不同意“您不能存储字符串和动态插值”。你绝对可以。问题是:如何去做,是否有人实施过,是否有人有想法,是否值得花时间在某种程度上实施。等等。
  • 我相信你是正确的,因为可以为运行时字符串插值编写一个 roslyn 扩展。我已更新我的答案以反映该框架不提供支持。
  • 插值库:我最喜欢的是 HenriFormatter,在 haacked.com/archive/2009/01/14/named-formats-redux.aspx 上进行了概述。它将允许您编写 WriteLine("My name is {name}.".HenriFormat(new {name = "fred"})).
【解决方案2】:

根据 Roslyn codeplex 网站上的this discussion,字符串插值可能与资源文件不兼容(强调我的):

字符串插值可能比 String.Format 或串联更简洁、更易于调试...

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

但是,这个例子很可疑。大多数专业程序员不会写作 代码中面向用户的字符串。 出于本地化原因,他们会将这些字符串存储在资源(.resw、.resx 或 .xlf)中。所以这里的字符串插值似乎没有多大用处。

【讨论】:

  • 我看过这个帖子。它看起来更像是讨论,而不是规范或开发人员的愿景。是时候,如果你阅读了这篇文章,字符串插值格式还没有被定义。您可以在同一个论坛上找到其他帖子,讨论本地化如何从字符串插值中受益
【解决方案3】:

正如在之前的答案中所说:您目前无法在运行时(例如从资源文件)加载格式字符串以进行字符串插值,因为它是在编译时使用的。

如果您不关心编译时功能而只想使用命名占位符,您可以使用类似以下扩展方法:

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,
            });

【讨论】:

  • 感谢您的回答。但它没有回答这个问题。不幸的是,您在没有花时间仔细和深思熟虑地阅读的情况下投入了时间来回答这个问题。
  • @MajesticRa 他回答了你的问题,甚至举了一个例子来说明你如何实现它。如果您想要不同的答案,请提出更准确的问题。您问是否有一种方法可以使用字符串插值进行本地化。答案是否定的,特别是因为它似乎是编译器功能而不是语言功能(即语法糖)。字符串是否应该像“{name}”一样存储并动态插值?是的。如何? stb 在上面提出了一个合理的方法。
  • 因为“字符串插值”和“本地化”都是 MS .NET 词汇表中含义明确的术语。但即便如此,格式化扩展库和其他人已经以一种或另一种方式提出了这个问题。所有讨论都描述了为什么这个问题与它无关。 SO上有大量的“如何进行字符串格式扩展”质量检查。所以这显然是断章取义的。但是好的,正如你所说,我更新了最初的帖子,澄清了......
【解决方案4】:

假设您的问题更多是关于如何在源代码中本地化插值字符串,而不是如何处理插值字符串资源...

给出示例代码:

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 文件,因为它们本质上是给你一个要翻译的文本表。

【讨论】:

    【解决方案5】:

    如果格式字符串不在您的 C# 源代码中,C# 6.0 字符串插值将无济于事。在这种情况下,您将不得不使用其他解决方案,例如 this library

    【讨论】:

    • 我知道,有几个......我可以说,很多这样的库和扩展。但正如@Romoku 在他的回答中所说,字符串插值是另一种野兽。如果可以使用这个工具很有趣。
    • 或者我最喜欢的 HenriFormatter,在 haacked.com/archive/2009/01/14/named-formats-redux.aspx 上进行了概述。它将允许您编写 WriteLine("My name is {name}.".HenriFormat(new {name = "fred"})).
    【解决方案6】:

    使用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);
    

    这是编译代码的sn-p并执行,所以是真正的C#字符串插值。尽管您必须考虑它的性能,因为它实际上是在运行时编译和执行您的代码。如果您可以使用 CSharpScript.Create 来编译和缓存代码,那么要避免这种性能损失。

    【讨论】:

    • 我不确定它是否实用,但至少它很有趣并且回答了问题!我希望@Monoman 能够完成 Roslyn 扩展
    【解决方案7】:

    字符串插值很难与本地化结合,因为编译器更喜欢将其翻译成不支持本地化的string.Format(...)。但是,有一个技巧可以将本地化和字符串插值结合起来。它在this article 的末尾附近进行了描述。

    通常字符串插值被转换为string.Format,其行为无法自定义。但是,就像 lambda 方法有时会变成表达式树一样,如果目标方法接受 System.FormattableString 对象,编译器将从 string.Format 切换到 FormattableStringFactory.Create(一种 .NET 4.6 方法)。

    问题是,如果可能的话,编译器更喜欢调用string.Format,所以如果有一个接受FormattableStringLocalized()的重载,它就不能使用字符串插值,因为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}"

    【讨论】:

    • 这需要更多的支持。我喜欢新的 $"" 格式,但对它不能使用资源感到失望。使用您的示例,我能够将 GetFormattedString(FormattableString str) 添加到我们的资源查找代码中,该代码将 str.Format 传递给 GetString(string str),然后返回 String.Format(translatedFormat, str.GetArguments())。现在我可以使用 $"My nicely formatted strings" 调用该函数并返回一个已翻译 + 格式化的字符串。
    • 这是该问题的最佳答案。这是我第一次了解 FormattableString。
    【解决方案8】:

    如果我们使用插值,那么我们考虑的是方法,而不是常量。在这种情况下,我们可以将翻译定义为方法:

    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# 编码的并且您切换了,那么导出所有翻译并不是最简单的事情。

    这也意味着,如果您希望将翻译分包给不同的人(除非您有一个人会说一切),那么翻译人员必须修改代码。这是简单的代码,但仍然是代码。

    【讨论】:

      【解决方案9】:

      内插字符串不能从它们的(变量)范围中重构出来,因为在其中使用了嵌入变量

      重新定位字符串文字部分的唯一方法是将范围绑定变量作为参数传递到其他位置,并用特殊占位符标记它们在字符串中的位置。然而,这个解决方案已经“发明”出来了:

      string.Format("literal with placeholers", parameters);

      或一些高级库(插入运行时),但使用完全相同的概念(传递参数)。

      然后您可以将"literal with placeholers" 重构为资源。

      【讨论】:

        猜你喜欢
        • 2015-10-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-19
        • 1970-01-01
        • 1970-01-01
        • 2018-01-23
        相关资源
        最近更新 更多