65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 XLocalizer 进行 ASP.NET Core 本地化

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2020年11月13日

CPOL

7分钟阅读

viewsIcon

16377

downloadIcon

244

通过在线翻译和自动资源创建本地化 ASP.NET Core...

ASP.NET Core 本地化,通过在线翻译、自动资源创建等功能实现...

引言

开发多语言 Web 应用程序需要构建本地化基础设施来处理请求本地化和视图、错误消息等内容的本地化。另一方面;每种本地化的语言至少需要一个资源文件,其中包含所有本地化的键值对。

构建本地化基础设施和填充资源文件可能需要大量时间和精力。XLocalizer 从头开始开发,以解决这两个问题,让开发者摆脱不必要的工作负担。

XLocalizer 提供什么?

简单的本地化设置:首先,它的目的是帮助开发者轻松创建本地化 Web 应用程序,而无需在开发本地化基础设施上浪费时间。

自动本地化XLocalizer 最具吸引力的两个功能是自动资源创建在线翻译。因此,任何遗漏的键都将被翻译并自动添加到相应的资源文件中。

支持多种资源类型:默认情况下,ASP.NET Core 使用“.resx”资源文件来存储本地化字符串。XLocalizer 打破了限制,提供了内置的本地化存储(XML、RESX、DB)。此外,还可以实现任何其他文件或数据库格式的自定义资源类型。

灵活性XLocalizer 使用标准的本地化接口IStringLocalizerIHtmlLocalizer,因此很容易从默认的 .NET Core 本地化系统切换到XLocalizer,反之亦然。并且借助内置的资源导出功能,所有本地化资源都可以从任何文件/数据库类型导出到“.resx”文件类型。

可定制性XLocalizer 可以进行各种细节的定制,以

  • 使用自定义资源类型(例如,mysql、json、csv 等),
  • 使用自定义资源导出器将资源从任何源导出到任何目标,
  • 使用自定义翻译服务来翻译资源。

集中化:一个单一的位置,可以轻松地以简单的方式自定义所有验证错误、模型绑定错误和身份验证错误。

在本教程中,我将展示如何将XLocalizer 与 XML 资源文件和在线翻译结合使用。要了解其他资源类型的设置,如resx、db 或自定义源,请访问 https://DOCS.Ziyad.info

安装

  • 要充分利用XLocalizer,需要几个 nuget 包,我将首先列出所有包,然后在接下来的步骤中介绍它们的作用。
    // The main package
    PM > Install-Package XLocalizer
    
    // Online translation support
    PM > Install-Package XLocalizer.Translate
    
    // Translation service
    PM > Install-Package XLocalizer.Translate.MyMemoryTranslate
    
    // Use html tags to localize views
    PM > Install-Package XLocalizer.TagHelpers
    
    // Additional taghelper package for language dropdown
    PM > Install-Package LazZiya.TagHelpers
  • Resources 文件夹:在项目根目录下,创建一个名为“LocalizationResources”的新文件夹,然后在其中创建一个名为“LocSource”的空类。此后,该类将用于从代码访问相关的资源文件。
    // Dummy class for grouping and accessing resource files
    public class LocSource { }

无需创建特定语言的资源文件,它们将由XLocalizer 自动创建和填充。

设置 XLocalizer

加速编码的一个小技巧;VS2019 可以自动插入缺失的命名空间(using …)。或者您可以按(Ctrl + .)查看上下文菜单,该菜单将添加缺失的命名空间。

  • 打开 startup 文件并像往常一样配置请求本地化选项
    services.Configure<RequestLocalizationOptions>(ops => 
    {
        var cultures = new CultureInfo[] { 
           new CultureInfo("en"),
           new CultureInfo("tr"),
           ...
        };
        ops.SupportedCultres = cultures;
        ops.SupportedUICultures = cultures;
        ops.DefaultRequestCulture = new RequestCulture("en");
    
        // Optional: add custom provider to support localization 
        // based on route value
        ops.RequestCultureProviders.Insert
            (0, new RouteSegmentRequestCultureProvider(cultures));
    });

    XLocalizer 支持多种资源类型,如 XML、RESX、DB 等。在此示例中,我将使用 XML 文件存储本地化值,因此我们需要注册内置的XmlResourceProvider,此提供程序将帮助我们使用 XML 文件作为资源文件来存储本地化的键值对。

    services.AddSingleton<IXResourceProvider, XmlResourceProvider>();
  • XLocalizer 的主要优点之一是支持在线翻译,因此我们需要在 startup 文件中至少注册一个翻译服务。
    services.AddHttpClient<ITranslator, MyMemoryTranslateService>();
    我在开发XLocalizer 时使用了MyMemoryTranslateService,但您可以自由选择任何可用的翻译服务,甚至实现您自己的翻译服务。
  • 可选地,配置 razor pages 以使用基于路由的本地化提供程序,这样我们可以拥有类似这样的 URL:https://:111/en/Index。然后在同一步骤中配置XLocalizer
    services.AddRazorPages()
        .AddRazorPagesOptions(ops => 
        {
            ops.Conventions.Insert(0, new RouteTemplateModelConventionRazorPages());
        })
        .AddXLocalizer<LocSource, MyMemoryTranslateService>(ops => 
        {
            ops.ResourcesPath = "LocalizationResources";
            ops.AutoAddKeys = true;
            ops.AutoTranslate = true;
            ops.TranslateFromCulture = "en";
        });
  • 配置应用程序使用本地化中间件
    app.UseRequestLocalization();

为翻译服务添加 API 密钥

MyMemory 翻译 API 提供免费的匿名使用,每天最多 1000 个单词(*在撰写本文时*)。因此,基本上,您无需添加任何密钥即可进行测试。无论如何,您可以通过提供电子邮件和免费生成的密钥来增加免费使用量,最高可达每天 30,000 个单词。有关更多详细信息,请参阅MyMemory API 使用限制

使用MyMemory API Keygen 获取密钥,然后像下面一样将密钥和有效的电子邮件地址添加到用户机密文件

{   
  "XLocalizer.Translate": {
    "MyMemory": {
       "Email": "...",
       "Key": "..."
    }
  }
}

不同的翻译服务可能需要不同的设置。有关不同翻译服务设置的详细信息,请参阅翻译服务文档

完整的 XLocalizer Startup 代码

为简化起见,已省略示例 startup 文件中的不必要代码。

public class Startup
{    
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    
    // ...

    public void ConfigureServices(IServiceCollection services)
    {
        // Configure request localization
        services.Configure<RequestLocalizationOptions>(ops =>
        {
            var cultures = new CultureInfo[] { new CultureInfo("en"), 
            new CultureInfo("tr"), new CultureInfo("ar") };
            ops.SupportedCultures = cultures;
            ops.SupportedUICultures = cultures;
            ops.DefaultRequestCulture = 
                new Microsoft.AspNetCore.Localization.RequestCulture("en");
            ops.RequestCultureProviders.Insert
                (0, new RouteSegmentRequestCultureProvider(cultures));
        });

        // Register translation service
        services.AddHttpClient<ITranslator, MyMemoryTranslateService>();

        // Register XmlResourceProvider
        services.AddSingleton<IXResourceProvider, XmlResourceProvider>();

        services.AddRazorPages()
            .AddRazorPagesOptions(ops => 
            { ops.Conventions.Insert(0, new RouteTemplateModelConventionRazorPages()); })
            // Add XLocalizer
            .AddXLocalizer<LocSource, MyMemoryTranslateService>(ops =>
            {
                ops.ResourcesPath = "LocalizationResources";
                ops.AutoAddKeys = true;
                ops.AutoTranslate = true;
                
                // Optional: Just in case you need to change the source translation culture.
                // if not provided, the default culture will be used
                ops.TranslateFromCulture = "en";
                
                // Recommended: turn on caching during production for faster localization
                ops.UseExpressMemoryCache = true; 
            });
    }

    // This method gets called by the runtime. 
    // Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ...
        // Use request localization middleware
        app.UseRequestLocalization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

以上是在 startup 文件中所需的所有设置。接下来,我们将配置视图和后端本地化。

本地化视图

我们安装了一个方便的 nuget 包来本地化视图XLocalizer.TagHelpers,这个包使得使用 html 标签和 html 属性轻松本地化视图成为可能,这使得 html 代码干净且易于阅读和维护。

  • _ViewImports.cshtml 文件中添加taghelper
    @addTagHelper *, XLocalizer.TagHelpers
  • 在 html 标签内使用localize-content 属性来本地化内部文本/html
    <h1 localize-content>Welcome</h1>
  • 使用localize html 标签本地化内部文本/html 段落
    <localize>
        <h1>Welcome</h1>
        <p>My contents...</p>
    </localize>
  • 本地化带参数的 html 字符串
    @{
        var args = new object[] { "http://DOCS.Ziyad.info" }
    }
    
    <p localize-args="args">
        Visit <a href="{0}">DOCS</a> for more details.
    </p>
  • 本地化 html 属性,例如 title
    <img src="../picture.jpg" localize-att-title="Nature picture" />

    下面是Register.cshtml 页面的完全本地化示例,请注意,我们只需要将“localize-content”属性添加到相关标签,这使得页面代码保持整洁,易于阅读和更新。

    @page
    @model RegisterModel
    @{
        ViewData["Title"] = "Register";
    }
    
    <h1 localize-content>@ViewData["Title"]</h1>
    
    <div class="row">
        <div class="col-md-4">
            <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
                <h4 localize-content>Create a new account.</h4>
                <hr />
                <div asp-validation-summary="All" class="text-danger"></div>
                <div class="form-group">
                    <label asp-for="Input.Email"></label>
                    <input asp-for="Input.Email" class="form-control" />
                    <span asp-validation-for="Input.Email" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="Input.Password"></label>
                    <input asp-for="Input.Password" class="form-control" />
                    <span asp-validation-for="Input.Password" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="Input.ConfirmPassword"></label>
                    <input asp-for="Input.ConfirmPassword" class="form-control" />
                    <span asp-validation-for="Input.ConfirmPassword" 
                     class="text-danger"></span>
                </div>
                <button type="submit" class="btn btn-primary" localize-content>
                 Register</button>
            </form>
        </div>
    </div>
    @section Scripts {
        <partial name="_ValidationScriptsPartial" />
    }
    使用 XLocalizer.TagHelpers 进行视图本地化示例

本地化验证属性错误、模型绑定错误和身份验证错误消息

使用XLocalizer 本地化所有框架错误消息不需要任何额外的设置,并且不需要在属性标签内提供任何错误消息!所有错误消息都将由 startup 中XLocalizer 的默认设置分配和本地化。

下面是某些验证属性的用法示例

[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }

同样对于模型绑定错误身份验证错误,我们无需进行任何额外的设置,XLocalizer 将默认处理所有错误消息的本地化。

下面是Register.cshtml.cs 文件的后端本地化示例

public class InputModel
{
    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [StringLength(100, MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password")]
    public string ConfirmPassword { get; set; }
}
不定义错误消息的验证属性用法

自定义错误消息

在某些情况下,您可能需要自定义验证属性、模型绑定或身份验证的错误消息。或者您可能希望使用“en”以外的语言提供默认错误消息,这样XLocalizer 就可以从正确的语言进行翻译。

第一个解决方案是在 startup 文件中使用内联选项设置,如下所示提供相关的错误消息

services.AddRazorPages()
        .AddXLocalizer<...>(ops =>
        {
            // ...
            ops.ValidationErrors = new ValidationErrors 
            {
                RequiredAttribute_ValidationError = "The {0} field is required.",
                CompareAttribute_MustMatch = 
                "'{0}' and '{1}' do not match.",
                StringLengthAttribute_ValidationError = 
                "The field {0} must be a string with a maximum length of {1}.",
                // ...
            };
            ops.ModelBindingErrors = new ModelBindingErrors 
            {
                AttemptedValueIsInvalidAccessor = "The value '{0}' is not valid for {1}.",
                MissingBindRequiredValueAccessor = 
                "A value for the '{0}' parameter or property was not provided.",
                MissingKeyOrValueAccessor = "A value is required.",
                // ...
            };
            ops.IdentityErrors = new IdentityErrors 
            {
                DuplicateEmail = "Email '{0}' is already taken.",
                DuplicateUserName = "User name '{0}' is already taken.",
                InvalidEmail = "Email '{0}' is invalid.",
                // ...
            };
        });
在 startup 文件中自定义错误消息

另一个选项是将所有XLocalizer 设置配置在 json 文件中。

JSON 设置

如果您是一位喜欢保持 startup 文件整洁的开发者(就像我一样 :)),那么您会很高兴知道您可以在 json 文件中进行所有这些定制,并且只需使用一行代码在 startup 中读取配置。

  • 将相关配置添加到appsettings.json 或任何您选择的自定义 json 文件
    {
        "XLocalizerOptions" : {
            "AutoAddKeys" : true,
            "AutoTranslate" : true,
            // ...
        }
    }
  • 设置XLocalizer 以读取相关的配置部分
    services.AddRaqzorPages()
            .AddXLocalizer<...>
            (ops => Configuration.GetSection("XLocalizerOptions").Bind(ops));
  • 下面是XLocalizer 选项的示例 json 设置,以及可自定义的错误消息
    {
      "XLocalizerOptions": {
        "ResourcesPath": "LocalizationResources",
        "AutoAddKeys": true,
        "AutoTranslate": true,
        "UseExpressMemoryCache": true,
        "TranslateFromCulture": "en",
        "ValidationErrors": {
          "CompareAttribute_MustMatch": "'{0}' 
                and '{1}' do not match. They should not be different!",
          "CreditCardAttribute_Invalid": 
                "The {0} field is not a valid credit card number.",
          "CustomValidationAttribute_ValidationError": "{0} is not valid.",
          "DataTypeAttribute_EmptyDataTypeString": 
                "The custom DataType string cannot be null or empty.",
          "EmailAddressAttribute_Invalid": 
                "The {0} field is not a valid e-mail address.",
          "FileExtensionsAttribute_Invalid": 
                "The {0} field only accepts files with the following extensions: {1}",
          "MaxLengthAttribute_ValidationError": 
                "The field {0} must be a string or array type 
                 with a maximum length of '{1}'.",
          "MinLengthAttribute_ValidationError": 
                "The field {0} must be a string or array type 
                 with a minimum length of '{1}'.",
          "PhoneAttribute_Invalid": "The {0} field is not a valid phone number.",
          "RangeAttribute_ValidationError": 
                "The field {0} must be between {1} and {2}.",
          "RegexAttribute_ValidationError": 
                "The field {0} must match the regular expression '{1}'.",
          "RequiredAttribute_ValidationError": 
                "The {0} field is required. Don't bypass this field!",
          "StringLengthAttribute_ValidationError": 
                "The field {0} must be a string with a maximum length of {1}.",
          "StringLengthAttribute_ValidationErrorIncludingMinimum": 
                "The field {0} must be a string with a minimum length of {2}
                 and a maximum length of {1}.",
          "UrlAttribute_Invalid": "The {0} field is not a valid fully-qualified http,
                                   https, or ftp URL.",
          "ValidationAttribute_ValidationError": "The field {0} is invalid."
        },
        "IdentityErrors": {
          "DuplicateEmail": "Email '{0}' is already taken.",
          "DuplicateUserName": "User name '{0}' is already taken. 
                                Please try another one.",
          "InvalidEmail": "Email '{0}' is invalid.",
          "DuplicateRoleName": "Role name '{0}' is already taken.",
          "InvalidRoleName": "Role name '{0}' is invalid.",
          "InvalidToken": "Invalid token.",
          "InvalidUserName": 
                  "User name '{0}' is invalid, can only contain letters or digits.",
          "LoginAlreadyAssociated": "A user with this login already exists.",
          "PasswordMismatch": "Incorrect password.",
          "PasswordRequiresDigit": "Passwords must have at least one digit ('0'-'9').",
          "PasswordRequiresLower": 
                   "Passwords must have at least one lowercase ('a'-'z').",
          "PasswordRequiresNonAlphanumeric": 
                   "Passwords must have at least one non alphanumeric character.",
          "PasswordRequiresUniqueChars": 
                   "Passwords must use at least {0} different characters.",
          "PasswordRequiresUpper": 
                   "Passwords must have at least one uppercase ('A'-'Z').",
          "PasswordTooShort": "Passwords must be at least {0} characters.",
          "UserAlreadyHasPassword": "User already has a password set.",
          "UserAlreadyInRole": "User already in role '{0}'.",
          "UserNotInRole": "User is not in role '{0}'.",
          "UserLockoutNotEnabled": "Lockout is not enabled for this user.",
          "RecoveryCodeRedemptionFailed": "Recovery code redemption failed.",
          "ConcurrencyFailure": "Optimistic concurrency failure, 
                                 object has been modified.",
          "DefaultError": "An unknown failure has occurred."
        },
        "ModelBindingErrors": {
          "AttemptedValueIsInvalidAccessor": "The value '{0}' is not valid for {1}.",
          "MissingBindRequiredValueAccessor": 
                "A value for the '{0}' parameter or property was not provided.",
          "MissingKeyOrValueAccessor": "A value is required.",
          "MissingRequestBodyRequiredValueAccessor": 
                         "A non-empty request body is required.",
          "NonPropertyAttemptedValueIsInvalidAccessor": "The value '{0}' is not valid.",
          "NonPropertyUnknownValueIsInvalidAccessor": "The supplied value is invalid.",
          "NonPropertyValueMustBeANumberAccessor": "The field must be a number.",
          "UnknownValueIsInvalidAccessor": "The supplied value is invalid for {0}.",
          "ValueIsInvalidAccessor": 
                  "The value '{0}' is invalid. You entered something weird!",
          "ValueMustBeANumberAccessor": 
                "The field {0} must be a number. 
                 Don't use letters or special characters.",
          "ValueMustNotBeNullAccessor": 
                 "The value '{0}' is invalid. This can't be null."
        }
      }
    }
    在 json 文件中自定义所有 XLocalizer 选项

因此,这是一个单一且简单的方式来定制所有错误消息。这些消息将根据请求的语言由XLocalizer 翻译成其他语言。

添加语言导航

每个多语言 Web 应用程序都必须提供一种切换不同语言的方式。您可能有自己的语言导航实现,但以防您需要轻松添加一个(我们之前安装了LazZiya.TagHelpers

  • taghelpers 添加到_ViewImports 文件
    @addTagHelper *, LazZiya.TagHelpers
  • 打开_layout.cshtml 并将语言导航添加到您需要显示它的位置
    <language-nav></language-nav>

我强烈建议设置语言导航以配置如此处文档页面所述的文化 cookie。这样,文化选择就可以存储在 cookie 中以备将来使用。

运行应用程序

如果您已正确完成所有步骤,并且一旦启动应用程序,请查看 VS 的输出窗口以查看日志,您将看到XLocalizer 已开始自动翻译视图并插入值。此外,所有验证属性、模型绑定和身份验证错误也已本地化。

本地化注册表单的示例屏幕截图
注意:本地化身份验证页面需要将身份验证脚手架到项目中。

您只需添加新语言即可;在 startup 文件中将该语言添加到支持的语言列表中,其余的一切都由XLocalizer 来完成。 :)

支持的 .NET Core 版本

  • 2.x
  • 3.x
  • 5.0

支持的项目类型

  • Razor Pages
  • MVC
  • Blazor Server

参考文献

历史

  • 2020 年 11 月 13 日:初版
© . All rights reserved.