在大型.NET项目中实现多语言/全球化的最佳方法


86

我很快将从事大型c#项目,并希望从一开始就构建多语言支持。我玩过一遍,可以使用每种语言的单独资源文件来使其工作,然后使用资源管理器加载字符串。

我还有什么其他好的方法可以研究吗?

Answers:


88

与资源一起使用单独的项目

我可以从经验中看出这一点,拥有一个包含12 项目的当前解决方案,其中包括API,MVC,项目库(核心功能),WPF,UWP和Xamarin。值得阅读这篇长文章,因为我认为这是最好的方法。借助VS工具,可以轻松导出和导入,以发送给翻译机构或其他人进行审查。

编辑02/2018:仍然很强大,将其转换为.NET Standard库使其甚至可以在.NET Framework和NET Core中使用它。我添加了一个额外的部分来将其转换为JSON,例如angular可以使用它。

EDIT 2019:继续使用Xamarin,这仍然适用于所有平台。例如Xamarin.Forms建议也使用resx文件。(我尚未在Xamarin.Forms中开发应用程序,但是该文档(详细入门的方法)涵盖了它:Xamarin.Forms文档)。就像将其转换为JSON一样,我们也可以将其转换为Xamarin.Android的.xml文件。

EDIT 2019(2):从WPF升级到UWP时,我遇到了他们在UWP中更喜欢使用另一种文件类型.resw,即内容相同但用法不同。我发现这样做的方法不同,我认为它比默认解决方案更好。

编辑2020:更新了一些可能需要多种语言项目的大型(模块化)项目的建议。

因此,让我们开始吧。

专业的

  • 几乎到处都有强类型。
  • 在WPF中,您不必处理ResourceDirectories
  • 据我测试,它支持ASP.NET,类库,WPF,Xamarin,.NET Core,.NET Standard。
  • 无需额外的第三方库。
  • 支持文化后备:zh-CN-> en。
  • 不仅是后端,在WPF的XAML中也适用于XPF和Xamarin.Forms,在MVC的.cshtml中也适用。
  • 更改语言即可轻松操作语言 Thread.CurrentThread.CurrentCulture
  • 搜索引擎可以使用不同的语言进行爬网,用户可以发送或保存特定于语言的网址。

骗子

  • WPF XAML有时是错误的,新添加的字符串不会直接显示。重建是临时修复(vs2015)。
  • UWP XAML在设计时不显示智能建议,也不显示文本。
  • 告诉我。

建立

在您的解决方案中创建语言项目,为其命名,例如MyProject.Language。向其中添加一个名为Resources的文件夹,然后在该文件夹中创建两个Resources文件(.resx)。一个称为Resources.resx,另一个称为Resources.en.resx(或特定于.en-GB.resx)。在我的实现中,我将NL(荷兰语)语言作为默认语言,因此该文件位于我的第一个文件中,而英语则位于第二个文件中。

安装程序应如下所示:

语言设置项目

Resources.resx的属性必须为: 属性

确保将自定义工具名称空间设置为项目名称空间。原因是在WPF中,您不能引用ResourcesXAML内部。

在资源文件中,将访问修饰符设置为Public:

访问修饰符

如果您有这么大的应用程序(比如说不同的模块),则可以考虑创建多个如上所述的项目。在这种情况下,您可以在键和资源类的前面加上特定的模块。使用适用于Visual Studio的最佳语言编辑器将所有文件组合到一个概述中。

在另一个项目中使用

对项目的引用:右键单击引用->添加引用-> Prjects \ Solutions。

在文件中使用名称空间: using MyProject.Language;

在后端中像这样使用它: string someText = Resources.orderGeneralError; 如果还有其他名为Resources的东西,则只需放入整个命名空间即可。

在MVC中使用

在MVC中,您可以设置语言,但是我使用了参数化的url,可以这样设置:

RouteConfig.cs 在其他映射之下

routes.MapRoute(
    name: "Locolized",
    url: "{lang}/{controller}/{action}/{id}",
    constraints: new { lang = @"(\w{2})|(\w{2}-\w{2})" },   // en or en-US
    defaults: new { controller = "shop", action = "index", id = UrlParameter.Optional }
);

FilterConfig.cs(可能需要增加,如果是的话,添加FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);Application_start()在方法Global.asax

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new ErrorHandler.AiHandleErrorAttribute());
        //filters.Add(new HandleErrorAttribute());
        filters.Add(new LocalizationAttribute("nl-NL"), 0);
    }
}

LocalizationAttribute

public class LocalizationAttribute : ActionFilterAttribute
{
    private string _DefaultLanguage = "nl-NL";
    private string[] allowedLanguages = { "nl", "en" };

    public LocalizationAttribute(string defaultLanguage)
    {
        _DefaultLanguage = defaultLanguage;
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        string lang = (string) filterContext.RouteData.Values["lang"] ?? _DefaultLanguage;
        LanguageHelper.SetLanguage(lang);
    }
}

LanguageHelper只是设置区域性信息。

//fixed number and date format for now, this can be improved.
public static void SetLanguage(LanguageEnum language)
{
    string lang = "";
    switch (language)
    {
        case LanguageEnum.NL:
            lang = "nl-NL";
            break;
        case LanguageEnum.EN:
            lang = "en-GB";
            break;
        case LanguageEnum.DE:
            lang = "de-DE";
            break;
    }
    try
    {
        NumberFormatInfo numberInfo = CultureInfo.CreateSpecificCulture("nl-NL").NumberFormat;
        CultureInfo info = new CultureInfo(lang);
        info.NumberFormat = numberInfo;
        //later, we will if-else the language here
        info.DateTimeFormat.DateSeparator = "/";
        info.DateTimeFormat.ShortDatePattern = "dd/MM/yyyy";
        Thread.CurrentThread.CurrentUICulture = info;
        Thread.CurrentThread.CurrentCulture = info;
    }
    catch (Exception)
    {

    }
}

.cshtml中的用法

@using MyProject.Language;
<h3>@Resources.w_home_header</h3>

或者如果您不想定义使用方法,则只需填写整个名称空间,或者可以在/Views/web.config下定义名称空间:

<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
  <namespaces>
    ...
    <add namespace="MyProject.Language" />
  </namespaces>
</pages>
</system.web.webPages.razor>

此mvc实现源教程:很棒的教程博客

在类库中使用模型

后端使用是相同的,只是在in属性中使用的一个示例

using MyProject.Language;
namespace MyProject.Core.Models
{
    public class RegisterViewModel
    {
        [Required(ErrorMessageResourceName = "accountEmailRequired", ErrorMessageResourceType = typeof(Resources))]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }
    }
}

如果您有整形工具,它将自动检查给定的资源名称是否存在。如果您更喜欢类型安全性,则可以使用T4模板生成枚举

在WPF中使用。

当然要添加对MyProject.Language命名空间的引用,我们知道如何在后端使用它。

在XAML中,在Window或UserControl的标题内,添加一个命名空间引用,lang如下所示:

<UserControl x:Class="Babywatcher.App.Windows.Views.LoginView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:MyProject.App.Windows.Views"
              xmlns:lang="clr-namespace:MyProject.Language;assembly=MyProject.Language" <!--this one-->
             mc:Ignorable="d" 
            d:DesignHeight="210" d:DesignWidth="300">

然后,在标签内:

    <Label x:Name="lblHeader" Content="{x:Static lang:Resources.w_home_header}" TextBlock.FontSize="20" HorizontalAlignment="Center"/>

由于它是强类型的,因此您可以确保资源字符串存在。有时您可能需要在安装过程中重新编译项目,WPF有时会在新名称空间中出现问题。

对于WPF,还需要在内设置语言App.xaml.cs。您可以自己执行(在安装过程中选择)或让系统决定。

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        SetLanguageDictionary();
    }

    private void SetLanguageDictionary()
    {
        switch (Thread.CurrentThread.CurrentCulture.ToString())
        {
            case "nl-NL":
                MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("nl-NL");
                break;
            case "en-GB":
                MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("en-GB");
                break;
            default://default english because there can be so many different system language, we rather fallback on english in this case.
                MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("en-GB");
                break;
        }
       
    }
}

在UWP中使用

在UWP中,Microsoft使用此解决方案,这意味着您将需要创建新的资源文件。另外,您也不能重复使用文本,因为它们希望您将x:UidXAML中的控件设置为资源中的键。并且您必须在资源Example.Text中填写TextBlock的文本。我根本不喜欢该解决方案,因为我想重用资源文件。最终,我提出了以下解决方案。我是今天(2019-09-26)才发现的,所以如果发现这不能按预期工作,我可能会再提出其他建议。

将此添加到您的项目:

using Windows.UI.Xaml.Resources;

public class MyXamlResourceLoader : CustomXamlResourceLoader
{
    protected override object GetResource(string resourceId, string objectType, string propertyName, string propertyType)
    {
        return MyProject.Language.Resources.ResourceManager.GetString(resourceId);
    }
}

将此添加到App.xaml.cs构造函数中:

CustomXamlResourceLoader.Current = new MyXamlResourceLoader();

无论您想在应用中的哪个位置使用此语言来更改语言:

ApplicationLanguages.PrimaryLanguageOverride = "nl";
Frame.Navigate(this.GetType());

最后一行需要刷新UI。当我仍在从事此项目时,我注意到我需要这样做两次。在用户第一次启动时,我可能会选择一种语言。但是,由于将通过Windows Store分发该语言,因此该语言通常等于系统语言。

然后在XAML中使用:

<TextBlock Text="{CustomResource ExampleResourceKey}"></TextBlock>

在Angular中使用它(转换为JSON)

如今,将Angular之类的框架与组件结合使用更为普遍,因此无需cshtml。翻译内容存储在json文件中,我将不介绍其工作原理,我强烈建议您使用ngx-translate而不是角度多重翻译。因此,如果您想将翻译转换为JSON文件,这非常简单,我使用了一个T4模板脚本,该脚本将Resources文件转换为json文件。我建议安装T4编辑器以阅读语法并正确使用它,因为您需要进行一些修改。

仅需注意一件事:无法生成数据,将其复制,清理数据并以另一种语言生成它。因此,您必须复制下面的代码与您使用的语言一样多的次数,并在“ // choose language here”之前更改条目。目前没有时间解决此问题,但可能会在以后更新(如果有兴趣)。

路径:MyProject.Language / T4 / CreateLocalizationEN.tt

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Windows.Forms" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Resources" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.ComponentModel.Design" #>
<#@ output extension=".json" #>
<#


var fileNameNl = "../Resources/Resources.resx";
var fileNameEn = "../Resources/Resources.en.resx";
var fileNameDe = "../Resources/Resources.de.resx";
var fileNameTr = "../Resources/Resources.tr.resx";

var fileResultName = "../T4/CreateLocalizationEN.json";//choose language here
var fileResultPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileResultName);
//var fileDestinationPath = "../../MyProject.Web/ClientApp/app/i18n/";

var fileNameDestNl = "nl.json";
var fileNameDestEn = "en.json";
var fileNameDestDe = "de.json";
var fileNameDestTr = "tr.json";

var pathBaseDestination = Directory.GetParent(Directory.GetParent(this.Host.ResolvePath("")).ToString()).ToString();

string[] fileNamesResx = new string[] {fileNameEn }; //choose language here
string[] fileNamesDest = new string[] {fileNameDestEn }; //choose language here

for(int x = 0; x < fileNamesResx.Length; x++)
{
    var currentFileNameResx = fileNamesResx[x];
    var currentFileNameDest = fileNamesDest[x];
    var currentPathResx = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", currentFileNameResx);
    var currentPathDest =pathBaseDestination + "/MyProject.Web/ClientApp/app/i18n/" + currentFileNameDest;
    using(var reader = new ResXResourceReader(currentPathResx))
    {
        reader.UseResXDataNodes = true;
#>
        {
<#
            foreach(DictionaryEntry entry in reader)
            {
                var name = entry.Key;
                var node = (ResXDataNode)entry.Value;
                var value = node.GetValue((ITypeResolutionService) null); 
                 if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\n", "");
                 if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\r", "");
#>
            "<#=name#>": "<#=value#>",
<#

    
            }
#>
        "WEBSHOP_LASTELEMENT": "just ignore this, for testing purpose"
        }
<#
    }
    File.Copy(fileResultPath, currentPathDest, true);
}


#>

如果您有模块化的应用程序,并且按照我的建议创建了多个语言项目,则必须为每个项目创建一个T4文件。确保json文件是在逻辑上定义的,不一定必须是en.json,也可以是example-en.json。要将多个json文件与ngx-translate结合使用,请按照此处的说明进行操作

在Xamarin.Android中使用

如上面的更新所述,我使用与Angular / JSON相同的方法。但是Android使用XML文件,因此我编写了一个T4文件来生成这些XML文件。

路径:MyProject.Language / T4 / CreateAppLocalizationEN.tt

#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Windows.Forms" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Resources" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.ComponentModel.Design" #>
<#@ output extension=".xml" #>
<#
var fileName = "../Resources/Resources.en.resx";
var fileResultName = "../T4/CreateAppLocalizationEN.xml";
var fileResultRexPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileName);
var fileResultPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileResultName);

    var fileNameDest = "strings.xml";

    var pathBaseDestination = Directory.GetParent(Directory.GetParent(this.Host.ResolvePath("")).ToString()).ToString();

    var currentPathDest =pathBaseDestination + "/MyProject.App.AndroidApp/Resources/values-en/" + fileNameDest;

    using(var reader = new ResXResourceReader(fileResultRexPath))
    {
        reader.UseResXDataNodes = true;
        #>
        <resources>
        <#

                foreach(DictionaryEntry entry in reader)
                {
                    var name = entry.Key;
                    //if(!name.ToString().Contains("WEBSHOP_") && !name.ToString().Contains("DASHBOARD_"))//only include keys with these prefixes, or the country ones.
                    //{
                    //  if(name.ToString().Length != 2)
                    //  {
                    //      continue;
                    //  }
                    //}
                    var node = (ResXDataNode)entry.Value;
                    var value = node.GetValue((ITypeResolutionService) null); 
                     if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\n", "");
                     if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\r", "");
                     if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("&", "&amp;");
                     if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("<<", "");
                     //if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("'", "\'");
#>
              <string name="<#=name#>">"<#=value#>"</string>
<#      
                }
#>
            <string name="WEBSHOP_LASTELEMENT">just ignore this</string>
<#
        #>
        </resources>
        <#
        File.Copy(fileResultPath, currentPathDest, true);
    }

#>

Android可以使用values-xx文件夹,因此文件夹中的英文为values-en。但是您还必须生成一个默认值,该默认值将进入该values文件夹。只需复制上面的T4模板并在上面的代码中更改文件夹即可。

到此为止,您现在可以为所有项目使用一个资源文件。这使得将所有内容导出到excl文档非常容易,并可以让他人对其进行翻译并再次导入。

特别感谢这个令人惊叹的VS扩展,它对resx文件的处理非常好。考虑为他的出色工作捐赠给他(我与此无关,我只喜欢扩展名)。


1.部署后如何通过在Mylanguage中添加更多资源并重新编译然后重新部署来添加更多语言?(2)您有用于翻译的IDE或工具吗(我在winform中有40多种表格),可以共享吗?(3)我尝试了一下,但是当我将文化更改为阿拉伯语时,它没有得到翻译,我将提出一个问题,并让您知道
Smith

5
1:只需复制Resources.xx.resx并更改xx,并且在我有switch语句的任何地方添加该语言。2我使用ResxManager(由TomEnglert提供)扩展名,可以通过工具->扩展名进行安装,这是使语言彼此相邻的好方法,但是实际翻译是由其他人完成的(使用ResxManager可以轻松地从excel导出和导入)。3.最后一次添加也遇到了这种情况,请检查上面列出的所有文件,但要检查一些断点。我没有在WinForms中使用它。
CularBytes'Mar

13
我可以支持我自己的答案吗?我只需要愚蠢地检查自己的答案即可解决在WPF中切换语言的错误-.-
CularBytes

1
@TiagoBrenck嗨,仅在这种特定情况下,如果英语是您的默认语言,则可以创建resources.en-gb.resx文件并在其中添加1%的更改。默认情况下,如果选择了语言en-gb并且未翻译单词,则它将使用默认语言(resources.resx)。
CularBytes

1
您如何构造资源文件的键?由于我看到您仅使用一个资源文件,并且据您说,您有一个庞大的应用程序,您如何拆分密钥?像“ Feature.MyMessage”或“ Area.Feature.MyMessage”
Francesco Venturini

21

我已经看到了使用多种不同方法实施的项目,每种方法各有优缺点。

  • 一个是在配置文件中完成的(不是我的最爱)
  • 一个是使用数据库来完成的-效果很好,但是让您知道要维护什么却很痛苦。
  • 一种使用过的资源文件按照您的建议进行,我必须说这是我最喜欢的方法。
  • 最基本的一个使用包含字符串的包含文件来完成它-丑陋。

我想说您选择的资源方法很有意义。看到其他人的答案也很有趣,因为我经常想知道是否有更好的方法来做这样的事情。我已经看到了无数资源,它们都指向使用资源的方法,包括SO上的一种资源。


5

我认为没有“最佳方法”。这实际上取决于您所构建的技术和应用程序类型。

Webapp可以按照其他发布者的建议将信息存储在数据库中,但是我建议使用单独的资源文件。那就是资源文件与您的资源分开。单独的资源文件减少了对相同文件的争用,并且随着项目的增长,您可能会发现本地化将与业务逻辑分开进行。(程序员和翻译者)。

Microsoft WinForm和WPF专家建议使用针对每个语言区域自定义的单独资源程序集。

WPF能够将UI元素调整为内容大小,从而降低了所需的布局工作,例如:(日语单词比英语短得多)。

如果您正在考虑使用WPF:建议您阅读此msdn文章 。老实说,我发现使用Wbuild本地化工具:msbuild,locbaml(以及excel电子表格)非常繁琐,但确实可以使用。

仅有一点相关的东西:我面临的一个常见问题是集成发送错误消息(通常是英语)而不是错误代码的旧系统。这会强制更改旧系统,或者将后端字符串映射到我自己的错误代码,然后再映射到本地字符串... yech。 错误代码是本地化朋友


4

+1数据库

如果对数据库进行了更正,则您的应用程序中的表单甚至可以即时进行自动转换。

我们使用了一个系统,其中所有控件都以XML文件(每种形式一个)映射到语言资源ID,但是所有ID都在数据库中。

基本上,不是让每个控件都保留ID(实现接口,也不是在VB6中使用tag属性),而是使用了一个事实,即在.NET中,控件树很容易通过反射来发现。如果缺少该表单,则加载表单时的过程将生成XML文件。XML文件会将控件映射到它们的资源ID,因此只需填写并映射到数据库即可。这意味着,如果未标记某些东西,或者需要将其拆分为另一个ID,则无需更改已编译的二进制文件(某些可能同时用作名词和动词的英语单词可能需要翻译成两个不同的单词(在字典中),并且不会重复使用,但是在ID的初始分配期间您可能不会发现它)。

只有在使用带有插入点的阶段时,应用程序才会参与其中。

数据库翻译软件是您基本的CRUD维护屏幕,具有各种工作流程选项,可帮助您完成丢失的翻译等。


我喜欢您的方法,您能告诉我如何生成xml文件,然后将其映射到数据库吗?
史密斯

在Form_Load或任何形式中,转换函数是在表单实例上调用的。该表格的XML文件已加载。如果不存在,则创建它。我没有临时可用的架构,但基本上,它已将表单上的控件名称映射到翻译ID。因此,对不在XML文件中的表单的任何控件都将获得一个没有翻译ID的条目。因为可以按需创建它们,所以您只需创建表单,运行应用程序即可为缺少的控件构建或更新XML文件。然后填写项目的翻译ID。
Cade Roux

4

我一直在搜索,发现了这一点:

如果您使用WPF或Silverlight,则出于多种原因可以使用WPF LocalizationExtension

它是开源的,它是免费的(并且将保持免费),处于真正的稳定状态

在Windows应用程序中,您可以执行以下操作:

public partial class App : Application  
{  
     public App()  
     {             
     }  
 
     protected override void OnStartup(StartupEventArgs e)  
     {  
         Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("de-DE"); ;  
         Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("de-DE"); ;  
 
          FrameworkElement.LanguageProperty.OverrideMetadata(  
              typeof(FrameworkElement),  
              new FrameworkPropertyMetadata(  
                  XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));  
          base.OnStartup(e);  
    }  
} 

而且我认为在Wep Page上,方法可能是相同的。

祝好运!


2

我会处理多个资源文件。配置起来并不难。实际上,我最近回答了关于与表单语言资源文件一起设置基于全局语言的资源文件的类似问题。

Visual Studio 2008中的本地化

我认为至少对于WinForm开发而言,这是最好的方法。


资源的缺点是您必须重新启动才能切换语言。对于大多数人来说,这可能是可以接受的,但这是我的宠儿……
Roman Starkov 2010年

2

您可以使用Sisulizer等商业工具。它将为每种语言创建附属程序集。您唯一需要注意的是不要混淆表单类名称(如果使用混淆器)。


0

大多数开源项目为此使用GetText。我不知道以前如何以及是否曾经在.Net项目中使用过它。


0

我们使用自定义提供程序提供多语言支持,并将所有文本放入数据库表中。它运作良好,除了在不更新Web应用程序的情况下更新数据库中的文本时有时会遇到缓存问题。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.