90

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

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

4

11 回答 11

110

使用带有资源的单独项目

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

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

编辑 2019:与 Xamarin 一起前进,这仍然适用于所有平台。例如,Xamarin.Forms 也建议使用 resx 文件。(我还没有在 Xamarin.Forms 中开发应用程序,但是文档,这是刚刚开始的详细说明,涵盖了它:Xamarin.Forms 文档)。就像将其转换为 JSON 一样,我们也可以将其转换为 Xamarin.Android 的 .xml 文件。

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

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

所以,让我们开始吧。

专业人士

  • 几乎到处都是强类型。
  • 在 WPF 中,您不必处理ResourceDirectories.
  • 据我测试,支持 ASP.NET、类库、WPF、Xamarin、.NET Core、.NET Standard。
  • 不需要额外的第三方库。
  • 支持文化回退:en-US -> en。
  • 不仅后端,还适用于 WPF 的 XAML 和 Xamarin.Forms,适用于 MVC 的 .cshtml。
  • 通过更改Thread.CurrentThread.CurrentCulture
  • 搜索引擎可以抓取不同的语言,用户可以发送或保存特定语言的网址。

骗局

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

设置

在您的解决方案中创建语言项目,将其命名为MyProject.Language。向其中添加一个名为 Resources 的文件夹,并在该文件夹中创建两个资源文件 (.resx)。一个称为Resources.resx,另一个称为Resources.en.resx(或特定的 .en-GB.resx)。在我的实现中,我将 NL(荷兰语)语言作为默认语言,所以它在我的第一个文件中,而英语在我的第二个文件中。

设置应如下所示:

语言设置项目

Resources.resx 的属性必须是: 特性

确保将自定义工具命名空间设置为您的项目命名空间。原因是在 WPF 中,您不能Resources在 XAML 中引用。

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

访问修饰符

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

在另一个项目中使用

对您的项目的引用:右键单击 References -> Add Reference -> Prjects\Solutions。

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

在后端像这样使用它: string someText = Resources.orderGeneralError; 如果还有其他东西称为资源,那么只需放入整个命名空间。

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

本地化属性

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>

或者,如果您不想定义 usings,则只需填写整个命名空间,或者您可以在 /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实现源码教程:Awesome tutorial blog

在类库中使用模型

后端使用也是一样的,只是在属性中使用的一个例子

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

如果您有 reshaper,它将自动检查给定的资源名称是否存在。如果您更喜欢类型安全,您可以使用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填写 aTextBlock的文本。我根本不喜欢那个解决方案,因为我想重用我的资源文件。最终我想出了以下解决方案。我今天(2019 年 9 月 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。当我还在做这个项目时,我注意到我需要做 2 次。我可能会在用户第一次启动时选择语言。但由于这将通过 Windows Store 分发,因此语言通常等于系统语言。

然后在 XAML 中使用:

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

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

现在更常见的是像 Angular 这样的框架结合组件,所以没有 cshtml。翻译存储在 json 文件中,我不打算介绍它是如何工作的,我只是强烈推荐ngx-translate而不是角度多翻译。因此,如果您想将翻译转换为 JSON 文件,这很容易,我使用 T4 模板脚本将资源文件转换为 json 文件。我建议安装T4 编辑器以阅读语法并正确使用它,因为您需要进行一些修改。

只有一件事需要注意:不可能生成数据、复制数据、清理数据并为另一种语言生成数据。因此,您必须复制以下代码的次数与您拥有的语言一样多,并更改“//在此处选择语言”之前的条目。目前没有时间解决这个问题,但可能会稍后更新(如果有兴趣)。

路径: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 模板并更改上面代码中的文件夹即可。

好了,您现在可以为所有项目使用一个资源文件。这使得将所有内容导出到排除文档并让某人翻译并再次导入变得非常容易。

特别感谢这个惊人的 VS 扩展,它对resx文件非常有用。考虑为他的出色工作向他捐款(我与此无关,我只是喜欢扩展)。

于 2016-03-05T11:47:16.440 回答
22

我见过使用许多不同方法实施的项目,每种方法都有其优点和缺点。

  • 一个是在配置文件中完成的(不是我最喜欢的)
  • 一个是使用数据库来做的——这工作得很好,但是你知道要维护什么是一件痛苦的事。
  • 一个按照您建议的方式使用资源文件,我不得不说这是我最喜欢的方法。
  • 最基本的方法是使用一个包含字符串的包含文件 - 丑陋。

我想说您选择的资源方法很有意义。看到其他人的答案也会很有趣,因为我经常想知道是否有更好的方法来做这样的事情。我已经看到很多资源都指向 using resources 方法,包括在 SO 上的一个

于 2008-12-17T01:45:59.363 回答
5

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

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

Microsoft WinForm 和 WPF 专家建议使用针对每个区域设置定制的单独资源程序集。

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

如果您正在考虑 WPF:我建议阅读这篇 msdn 文章 说实话,我发现 WPF 本地化工具:msbuild、locbaml(可能还有一个 excel 电子表格)使用起来很乏味,但它确实有效。

只是稍微相关的事情:我面临的一个常见问题是集成发送错误消息(通常是英文)而不是错误代码的遗留系统。这会强制对遗留系统进行更改,或者将后端字符串映射到我自己的错误代码,然后再映射到本地化字符串……是的。 错误代码是本地化的朋友

于 2008-12-17T04:07:34.727 回答
4

+1 数据库

如果对数据库进行更正,您应用程序中的表单甚至可以即时重新翻译。

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

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

应用程序更多涉及的唯一情况是使用带有插入点的阶段。

数据库翻译软件是您的基本 CRUD 维护屏幕,具有各种工作流程选项,以方便检查丢失的翻译等。

于 2008-12-17T02:47:27.807 回答
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 上的方法可能是相同的。

祝你好运!

于 2010-12-02T08:51:41.360 回答
2

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

于 2008-12-17T08:59:01.067 回答
2

我会使用多个资源文件。配置应该不难。事实上,我最近回答了一个关于设置基于全局语言的资源文件和表单语言资源文件的类似问题。

Visual Studio 2008 中的本地化

我认为至少对于 WinForm 开发来说这是最好的方法。

于 2008-12-17T02:21:47.983 回答
0

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

于 2008-12-17T03:13:16.147 回答
0

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

于 2008-12-17T08:53:39.767 回答
0

标准资源文件更容易。但是,如果您有任何与语言相关的数据,例如查找表,那么您将不得不管理两个资源集。

我还没有这样做,但在我的下一个项目中,我将实现一个数据库资源提供程序。我在 MSDN 上找到了如何做到这一点:

http://msdn.microsoft.com/en-us/library/aa905797.aspx

我还发现了这个实现:

DBResource 提供者

于 2008-12-17T05:57:55.643 回答
0

我强烈推荐使用 ResourceDirectories (resx) 和 {x:static} 标记的ResXManager工具。

ResXManager 用作 Visual Studio 或独立应用程序的插件。

于 2021-08-27T10:42:55.933 回答