C#6.0 字符串插值本地化
C#6.0 string interpolation localization
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}”一样存储并以某种方式动态插入?
P.S。这个问题不是关于 "how to make string.FormatIt extension" (有很多这样的库,所以答案等)。这个问题是关于 "localization" 上下文中 "string interpolation" 的 Roslyn 扩展(两者都是 MS .NET 词汇表中的术语),或者像 Dylan 提议的动态用法。
如果格式字符串不在您的 C# 源代码中,C# 6.0 字符串插值将无法为您提供帮助。在这种情况下,您将不得不使用其他解决方案,例如 this library.
内插字符串将大括号之间的块计算为 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();
}
}
回答你的问题:
框架目前没有提供在运行时评估内插字符串的机制。因此,您不能开箱即用地存储字符串和动态插值。
存在处理字符串运行时插值的库。
根据 Roslyn codeplex 站点上的 this discussion,字符串插值可能与资源文件不兼容(强调我的):
String interpolation could be neater and easier to debug than either String.Format or concatenation...
Dim y = $"Robot {name} reporting
{coolant.name} levels are {coolant.level}
{reactor.name} levels are {reactor.level}"
However, this example is fishy. Most professional programmers won't be writing
user-facing strings in code. Instead they'll be storing those strings in resources (.resw, .resx or .xlf) for reasons of localization. So there doesn't seem much use for string interpolation here.
内插字符串无法从其(变量)范围中重构出来,因为在其中使用了嵌入变量。
重新定位字符串文字部分的唯一方法是将范围绑定变量作为参数传递到其他位置,并用特殊占位符标记它们在字符串中的位置。然而,这个解决方案已经 "invented" 并且在那里:
string.Format("literal with placeholers", parameters);
或一些高级库(插值运行时),但使用完全相同的概念(传递参数)。
然后您可以将 "literal with placeholers"
重构为资源。
正如在之前的回答中所说:您目前无法在运行时加载格式字符串(例如从资源文件)以进行字符串插值,因为它在编译时使用。
如果您不关心编译时特性,只想命名占位符,您可以使用类似这样的扩展方法:
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,
});
字符串插值很难与本地化结合,因为编译器更喜欢将其翻译成不支持本地化的string.Format(...)
。但是,有一个技巧可以将本地化和字符串插值结合起来;它在 this article.
的末尾进行了描述
Normally string interpolation is translated to string.Format
, whose behavior cannot be customized. However, in much the same way as lambda methods sometimes become expression trees, the compiler will switch from string.Format
to FormattableStringFactory.Create
(a .NET 4.6 method) if the target method accepts a System.FormattableString
object.
The problem is, the compiler prefers to call string.Format
if possible, so if there were an overload of Localized()
that accepted FormattableString
, it would not work with string interpolation because the C# compiler would simply ignore it [because there is an overload that accepts a plain string]. Actually, it's worse than that: the compiler also refuses to use FormattableString
when calling an extension method.
It can work if you use a non-extension method. For example:
static class Loca
{
public static string lize(this FormattableString message)
{ return message.Format.Localized(message.GetArguments()); }
}
Then you can use it like this:
public class Program
{
public static void Main(string[] args)
{
Localize.UseResourceManager(Resources.ResourceManager);
var name = "Dave";
Console.WriteLine(Loca.lize($"Hello, {name}"));
}
}
It's important to realize that the compiler converts the $"..."
string into an old-fashioned format string. So in this example, Loca.lize
actually receives "Hello, {0}"
as the format string, not "Hello, {name}"
.
假设您的问题更多是关于如何在源代码中本地化内插字符串,而不是如何处理内插字符串资源...
给定示例代码:
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 或资源或类似的方法来定位和 return 一个 suitable 翻译,或者只是 return 要跳过的原始参数翻译.
您仍然需要为翻译人员记录参数数字的含义;原始代码字符串中使用的文本在运行时不存在。
例如,如果在这种情况下 GetTranslation
returned "{2}. {0} {2}, {1}. Don't wear it out."
(嘿,本地化不仅仅是语言!)那么完整程序的输出将是:
Bloggs. John Bloggs, W. Don't wear it out.
话虽如此,虽然使用这种翻译方式很容易开发,但实际翻译却很难,因为字符串隐藏在代码中,只在运行时浮出水面。除非你有一个工具可以静态探索你的代码并提取所有 translatable 字符串(不必在运行时点击该代码路径),否则你最好使用更传统的 resx 文件,因为它们固有地为您提供 table 待翻译的文本。
使用 Microsoft.CodeAnalysis.CSharp.Scripting 包你可以做到这一点。
您需要创建一个对象来存储数据,下面将使用动态对象。您还可以创建具有所有必需属性的特定 class。 here.
中描述的将动态对象包装在 class 中的原因
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 来编译和缓存代码,则可以解决此性能问题。
如果我们使用插值,那么我们是在考虑方法,而不是常量。在那种情况下,我们可以将我们的翻译定义为方法:
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
的实现。这也提供了一种实现回退的方法,因为一个实现可以从另一个实现继承。如果英语是默认语言并且其他实现继承自它,那么在提供翻译之前至少会显示一些内容。
这看起来有点不正统,但有一些好处:
首先,用于插值的字符串始终存储在具有 clearly-specified 个参数的 strongly-typed 方法中。
鉴于此:"Hello, my name is {0} {1}"
我们能否确定占位符按顺序代表名字和姓氏?总会有一种方法可以将值与占位符相匹配,但是当内插字符串与其参数一起存储时,混淆的空间就会减少。
同样,如果我们将翻译字符串存储在一个地方并在另一个地方使用它们,就可以以破坏使用它们的代码的方式修改它们。我们可以将 {2}
添加到将在其他地方使用的字符串,并且该代码将在运行时失败。
使用字符串插值这是不可能的。如果我们的翻译字符串与可用参数不匹配,它甚至不会编译。
尽管我认为维护任何解决方案都存在困难,但仍有缺点。
最大的就是便携性。如果您的翻译是用 C# 编码的并且您切换了,那么导出所有翻译并不是最简单的事情。
这也意味着,如果您希望将翻译包包给不同的人(除非您有一个人会说一切),那么翻译人员必须修改代码。这是简单的代码,但仍然是代码。
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}”一样存储并以某种方式动态插入?
P.S。这个问题不是关于 "how to make string.FormatIt extension" (有很多这样的库,所以答案等)。这个问题是关于 "localization" 上下文中 "string interpolation" 的 Roslyn 扩展(两者都是 MS .NET 词汇表中的术语),或者像 Dylan 提议的动态用法。
如果格式字符串不在您的 C# 源代码中,C# 6.0 字符串插值将无法为您提供帮助。在这种情况下,您将不得不使用其他解决方案,例如 this library.
内插字符串将大括号之间的块计算为 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();
}
}
回答你的问题:
框架目前没有提供在运行时评估内插字符串的机制。因此,您不能开箱即用地存储字符串和动态插值。
存在处理字符串运行时插值的库。
根据 Roslyn codeplex 站点上的 this discussion,字符串插值可能与资源文件不兼容(强调我的):
String interpolation could be neater and easier to debug than either String.Format or concatenation...
Dim y = $"Robot {name} reporting
{coolant.name} levels are {coolant.level}
{reactor.name} levels are {reactor.level}"
However, this example is fishy. Most professional programmers won't be writing user-facing strings in code. Instead they'll be storing those strings in resources (.resw, .resx or .xlf) for reasons of localization. So there doesn't seem much use for string interpolation here.
内插字符串无法从其(变量)范围中重构出来,因为在其中使用了嵌入变量。
重新定位字符串文字部分的唯一方法是将范围绑定变量作为参数传递到其他位置,并用特殊占位符标记它们在字符串中的位置。然而,这个解决方案已经 "invented" 并且在那里:
string.Format("literal with placeholers", parameters);
或一些高级库(插值运行时),但使用完全相同的概念(传递参数)。
然后您可以将 "literal with placeholers"
重构为资源。
正如在之前的回答中所说:您目前无法在运行时加载格式字符串(例如从资源文件)以进行字符串插值,因为它在编译时使用。
如果您不关心编译时特性,只想命名占位符,您可以使用类似这样的扩展方法:
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,
});
字符串插值很难与本地化结合,因为编译器更喜欢将其翻译成不支持本地化的string.Format(...)
。但是,有一个技巧可以将本地化和字符串插值结合起来;它在 this article.
Normally string interpolation is translated to
string.Format
, whose behavior cannot be customized. However, in much the same way as lambda methods sometimes become expression trees, the compiler will switch fromstring.Format
toFormattableStringFactory.Create
(a .NET 4.6 method) if the target method accepts aSystem.FormattableString
object.The problem is, the compiler prefers to call
string.Format
if possible, so if there were an overload ofLocalized()
that acceptedFormattableString
, it would not work with string interpolation because the C# compiler would simply ignore it [because there is an overload that accepts a plain string]. Actually, it's worse than that: the compiler also refuses to useFormattableString
when calling an extension method.It can work if you use a non-extension method. For example:
static class Loca { public static string lize(this FormattableString message) { return message.Format.Localized(message.GetArguments()); } }
Then you can use it like this:
public class Program { public static void Main(string[] args) { Localize.UseResourceManager(Resources.ResourceManager); var name = "Dave"; Console.WriteLine(Loca.lize($"Hello, {name}")); } }
It's important to realize that the compiler converts the
$"..."
string into an old-fashioned format string. So in this example,Loca.lize
actually receives"Hello, {0}"
as the format string, not"Hello, {name}"
.
假设您的问题更多是关于如何在源代码中本地化内插字符串,而不是如何处理内插字符串资源...
给定示例代码:
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 或资源或类似的方法来定位和 return 一个 suitable 翻译,或者只是 return 要跳过的原始参数翻译.
您仍然需要为翻译人员记录参数数字的含义;原始代码字符串中使用的文本在运行时不存在。
例如,如果在这种情况下 GetTranslation
returned "{2}. {0} {2}, {1}. Don't wear it out."
(嘿,本地化不仅仅是语言!)那么完整程序的输出将是:
Bloggs. John Bloggs, W. Don't wear it out.
话虽如此,虽然使用这种翻译方式很容易开发,但实际翻译却很难,因为字符串隐藏在代码中,只在运行时浮出水面。除非你有一个工具可以静态探索你的代码并提取所有 translatable 字符串(不必在运行时点击该代码路径),否则你最好使用更传统的 resx 文件,因为它们固有地为您提供 table 待翻译的文本。
使用 Microsoft.CodeAnalysis.CSharp.Scripting 包你可以做到这一点。
您需要创建一个对象来存储数据,下面将使用动态对象。您还可以创建具有所有必需属性的特定 class。 here.
中描述的将动态对象包装在 class 中的原因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 来编译和缓存代码,则可以解决此性能问题。
如果我们使用插值,那么我们是在考虑方法,而不是常量。在那种情况下,我们可以将我们的翻译定义为方法:
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
的实现。这也提供了一种实现回退的方法,因为一个实现可以从另一个实现继承。如果英语是默认语言并且其他实现继承自它,那么在提供翻译之前至少会显示一些内容。
这看起来有点不正统,但有一些好处:
首先,用于插值的字符串始终存储在具有 clearly-specified 个参数的 strongly-typed 方法中。
鉴于此:"Hello, my name is {0} {1}"
我们能否确定占位符按顺序代表名字和姓氏?总会有一种方法可以将值与占位符相匹配,但是当内插字符串与其参数一起存储时,混淆的空间就会减少。
同样,如果我们将翻译字符串存储在一个地方并在另一个地方使用它们,就可以以破坏使用它们的代码的方式修改它们。我们可以将 {2}
添加到将在其他地方使用的字符串,并且该代码将在运行时失败。
使用字符串插值这是不可能的。如果我们的翻译字符串与可用参数不匹配,它甚至不会编译。
尽管我认为维护任何解决方案都存在困难,但仍有缺点。
最大的就是便携性。如果您的翻译是用 C# 编码的并且您切换了,那么导出所有翻译并不是最简单的事情。
这也意味着,如果您希望将翻译包包给不同的人(除非您有一个人会说一切),那么翻译人员必须修改代码。这是简单的代码,但仍然是代码。