目录
介绍
本地化更长的字符串
设计理由
与ResourceManager结合使用
使用字符串插值进行本地化
源代码
介绍几年来,我一直在构建一个主题为“应该构建到.NET框架中的东西,但不是”的库。但我一直推迟写关于应该内置的东西的文章,但是,你知道,不是。再也不是!它被称为Loyc.Essentials,您可以通过NuGet获取它(它以Loyc命名,但这并不重要。)
Loyc.Essentials有一个Localize类,它是一个全局钩子,可以在其中安装字符串映射本地化器。如果你还在使用Loyc.Essentials,你应该使用它。它准备将您的程序翻译成其他语言,几乎不费吹灰之力。
这个想法是通过让本地化变得非常容易来说服程序员支持它。默认情况下,它没有连接到任何翻译器(它只是通过字符串),所以只为一种语言市场编写程序的人可以轻松地使他们的代码“多语言准备”,而无需做任何额外的工作。
你所要做的就是调用.Localized()扩展方法,这实际上比编写传统string.Format()方法要短。(同时:using Loyc;)
编辑:一般来说,这不适用于C#6的插值字符串($"..."),因为C#6是如何设计的,但本文末尾描述了一种解决方法。
翻译系统本身独立于Localize,并通过委托连接Localized(),以便多语言翻译系统成为可能。此类应该适用于任何.NET程序,并且使用此实用程序的某些程序将希望使用不同的本地化程序。
像这样使用它:
string result = "Hello, {0}".Localized(userName);
或者,为了提高清晰度,请使用命名占位符:
string result = "Hello, {person's name}".Localized("person's name", userName);
无论安装何种本地化程序,都会在其数据库中查找文本并返回翻译。如果没有最终用户语言的翻译,则应返回适当的默认翻译:原始文本或某些默认语言的翻译,例如英语。
本地化程序需要一个外部翻译表,在概念上如下:
关键名称
语言
翻译文本
“Hello,{0}”
“ES”
“Hola,{0}”
“Hello,{0}”
“FR”
“Bonjour,{0}”
“Load
“ES”
“Cargar”
“Load
“FR”
“Charge
“Save
“ES”
“Guardar
“Save
“FR”
“Enregistrer”
许多开发人员使用resx文件来存储翻译。支持这一点,如下所述。
本地化更长的字符串对于较长的消息,最好使用短名称来表示消息,以便在编辑英文文本时,不必更改其他语言的转换表。为此,请使用以下Symbol方法:
// The translation table will be searched for "ConfirmQuitWithoutSaving"
string result = Localize.Symbol("ConfirmQuitWithoutSaving",
"Are you sure you want to quit without saving '{filename}'?", "filename", fileName);
// Enhanced C# syntax with symbol literal
string result = Localize.Symbol(@@ConfirmQuitWithoutSaving,
"Are you sure you want to quit without saving '{filename}'?", "filename", fileName);
这对于长字符串或文本段落最有用,但我希望某些项目,作为策略,使用符号表示所有可本地化的文本。
同样,您可以在不设置任何转换表的情况下调用此方法。但是,允许实际的消息为null。在这种情况下,如果没有设置翻译器或没有可用的翻译,则Localize.Symbol返回符号本身(第一个参数)作为最后的手段。
如果变量参数列表不为空,则Localize.Formatter被调用以从格式字符串构建完成的字符串。可以单独进行格式化——例如:
Console.WriteLine("{0} is {0:X} in hexadecimal".Localized(), N);
在此示例中,WriteLine它本身执行格式化,而不是Localized。
如上所示,Localize默认格式化程序StringExt.FormatCore具有标准格式化程序不具备的额外功能:命名参数。这是一个例子:
...
string verb = (IsFileLoaded ? "parse" : "load").Localized();
MessageBox.Show(
"Not enough memory to {load/parse} '{filename}'.".Localized(
"load/parse", verb, "filename", FileName));
如您所见,通过指定参数名称{filename}而不是像{0}这样的数字,在格式字符串中提到了命名参数。变量参数列表包含相同的名称,后跟其值,例如"filename", FileName。此功能使您(开发人员)有机会告诉写作翻译人员特定参数的目的是什么。
译者不得改变任何论点:这个词{filename}不能被翻译。
在运行时,带有命名参数的格式字符串将转换为带有编号参数的“普通”格式字符串。上面的示例将变为“Could not {1} the file: {3}”,然后传递给string.Format。
设计理由许多开发人员不想花时间编写国际化或本地化代码,并且很想编写仅适用于一种语言的代码。毫无疑问,因为与硬编码字符串相比,这是一个痛苦的问题。Microsoft建议代码携带一个ResourceManager对象并直接从中请求字符串:
private ResourceManager rm;
rm = new ResourceManager("RootNamespace.Resources", this.GetType().Assembly);
Console.Writeline(rm.GetString("StringIdentifier"));
这种方法有缺点:
- 在可能包含可本地化字符串的所有类之间传递ResourceManager实例可能很麻烦; 全局工具更方便。
- 程序员必须将所有翻译放在资源文件中; 因此,编写代码很麻烦,因为程序员必须切换到资源文件并将字符串添加到代码中。反过来,读取代码的人无法分辨出字符串的含义,并且必须加载资源文件才能找到答案。
- 改变本地化管理并不容易; 例如,如果有人想将翻译存储在an.ini,.xml或.les文件中而不是在程序集内部,该怎么办?如果用户想要集中一组程序集的所有翻译,而不是在每个程序集中拥有单独的资源,该怎么办?
- 如果找不到请求的标识符,则GetString返回null,可能导致空白输出或一个NullReferenceException异常 。
Microsoft确实通过提供Visual Studio内置的代码生成器来解决第一个缺点,它为每个字符串提供了一个全局属性; 看这里。
即便如此,您可能会发现此类提供了一种更方便的方法,因为您的本地语言字符串是在代码中编写的,并且如果所需的语言不可用,您可以保证在运行时获取字符串(非空)。
与ResourceManager结合使用该类通过UseResourceManager帮助类方法支持ResourceManager 。例如,在调用Localize.UseResourceManager(resourceManager)之后,如果你编写
"Save As...".Localized()
然后resourceManager.GetString("Save As...") 被调用以获取已翻译的字符串,如果未找到翻译则调用原始字符串(是的,在您的resx文件中,您可以在左侧使用空格和标点符号)。您甚至可以添加“名称计算器”来编码resx文件的命名约定,例如删除空格和标点符号(有关详细信息,请查看UseResourceManager方法。)
在.NET程序中,通常有一个“主”resx文件,例如Resources.resx,它包含默认字符串,以及其他非英语翻译文件(例如,西班牙语的Resources.es.resx)。当使用Localized()时您可能会使用稍微不同的方法:您仍然为项目创建一个Resources.resx文件,但是您将字符串表保留为空(您仍然可以将其用于其他资源,例如图标)。这会导致Visual Studio生成一个具有ResourceManager属性的Resources类,以便您可以轻松获取所需的ResourceManager实例。
- 程序启动时,调用Localize.UseResourceManager(Resources.ResourceManager)。
- 使用Localized()扩展方法获取短字符串的翻译。
- 对于长串,请使用Localize.Symbol("ShortAlias", "Long string", params...)。第一个参数是传递给ResourceManager.GetString()的字符串
可以将本地化与C#6内插字符串结合起来,就像在 $"this string {...}"中一样(并感谢 Florian Rappl 让我注意到这一点。)
不幸的是,Localize() 不能与他们合作。
最初我认为它根本不可能,因为通常字符串插值被转换为 string.Format,其行为无法自定义。但是,与lambda方法有时成为表达式树的方式大致相同,如果目标方法接受System.FormattableString对象,编译器将切换 string.Format 到FormattableStringFactory.Create(.NET 4.6方法)。
问题是,如果可能的话,编译器会更喜欢调用string.Format,所以如果有一个接受FormattableString的Localized()重载 ,它就不能用于字符串插值,因为C#编译器会简单地忽略它(因为Localized()它已经可以接受一个字符串)。实际上,它比这更糟糕:编译器在调用扩展方法时也拒绝使用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}"。
不幸的是,有点令人困惑的是,与普通字符串相比,我们需要一种完全不同的本地化插值字符串方式,如果你忘了——如果你写了$"Hello, {name}".Localized()——你的代码将被破坏,因为格式化将在本地化之前发生,因此没有翻译将被找到。
为了避免这种混淆,我不打算扩展我的库以支持字符串插值,但如果您更喜欢在应用程序中使用字符串插值,您仍然可以通过添加类似Loca.lize 项目的帮助方法来对其进行本地化。
源代码源代码在这里。不幸的是,它使用了一些特定于Loyc.Essentials的类型(Symbol ,ThreadLocalVariable,SavedValue和ScratchBuffer),所以如果你想要在不使用Loyc.Essentials NuGet包的情况下使用Localize,你必须花费一些时间来转换到“plain-old “C#。
原文地址:https://www.codeproject.com/Articles/1165045/Prepare-all-your-apps-for-localization