ASP.Net Core 本地化

16

ASP.Net Core特性 新增本地化支持

在我的项目中我只需要一种语言。对于大部分的文本和注释,我可以指定用我的语言,但是对于来自ASP.Net Core本身的文本,它们的语言是英语。

例如:

  • 密码必须至少有一个大写字母('A'-'Z')。
  • 密码必须至少有一个数字('0'-'9')。
  • 用户名 'x@x.com' 已被使用。
  • E-post字段不是有效的电子邮件地址。
  • 值“”无效。

我已经尝试手动设置文化信息,但是语言仍然是英语。

app.UseRequestLocalization(new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("nb-NO"),
    SupportedCultures = new List<CultureInfo> { new CultureInfo("nb-NO") },
    SupportedUICultures = new List<CultureInfo> { new CultureInfo("nb-NO") }
});

我该如何更改ASP.Net Core的语言或覆盖其默认文本?


你的问题似乎与aspnet身份验证错误消息有关,要更改它,请参见https://dev59.com/vZnga4cB1Zd3GeqPeuD3#38968910。 - adem caglin
8个回答

15
列出的错误消息在ASP.NET Core Identity中定义,并由IdentityErrorDescriber提供。到目前为止,我没有找到翻译的资源文件,而且我认为它们没有本地化。在我的系统(德语区域设置)中,它们也没有被翻译,尽管CultureInfo已正确设置。
您可以配置自定义IdentityErrorDescriber以返回您自己的消息以及它们的翻译。例如,在如何更改MVC Core ValidationSummary的默认错误消息?中进行了描述。
Startup.csConfigure方法中,您可以连接从IdentityErrorDescriber继承的错误描述符类,如下所示:
 services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddErrorDescriber<TranslatedIdentityErrorDescriber>();

对于其他默认的模型错误消息(比如无效数字),您可以在ModelBindingMessageProvider中提供自己的访问器函数。这个提供程序被ASP.NET Core MVC使用,也可以在Startup.cs中进行配置。

services.AddMvc(
            options => 
            options.ModelBindingMessageProvider.ValueIsInvalidAccessor = s => $"My not valid text for {s}");

谢谢。这解决了身份部分,但还有更多未翻译的错误。例如,如果数字字段未填写,则会显示“值''无效”。 - Tedd Hansen
@TeddHansen - 我做了一些研究,并在答案中添加了ModelBindingMessageProvider信息。 - Ralf Bönning

11

请让我全面回答这个问题,并描述我是如何在阅读.net core源代码后得出解决方案的。

在进一步描述之前,请先使用NuGet安装此软件包Microsoft.Extensions.Localization

正如大家可能还记得的那样,在asp.net full framework中,切换文化非常简单。

System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");

在.NET Core中,文化(culture)不再与当前线程绑定。ASP.NET Core引擎具有管道(pipeline),我们可以向该管道添加不同的中间件(MiddleWares),因此RequestLocalizationMiddleware是用于处理本地化的中间件类。我们可能会有多个提供程序,因此它将遍历所有的文化提供程序,例如QueryStringRequestCultureProviderCookieRequestCultureProviderAcceptLanguageHeaderRequestCultureProvider等等。
一旦请求本地化中间件从第一个提供程序获取到当前区域设置,则它将忽略其他提供程序并将请求传递给管道中的下一个中间件,因此提供程序列表中的顺序非常重要。
我个人更喜欢将文化存储在浏览器cookie中,因此,由于CookieRequestCultureProvider不是列表中第一个文化提供程序,所以我将其移到列表的顶部。在Startup.cs的ConfigureServices中配置如下:
services.Configure<RequestLocalizationOptions>(options =>
             {
                 var supportedCultures = new[]
                 {
                    new CultureInfo("en-US"),
                    new CultureInfo("fa-IR"),
                    new CultureInfo("de-DE")
                };
                options.DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US");
                options.SupportedCultures = supportedCultures;
                options.SupportedUICultures = supportedCultures;

                var defaultCookieRequestProvider =
                    options.RequestCultureProviders.FirstOrDefault(rcp =>
                        rcp.GetType() == typeof(CookieRequestCultureProvider));
                if (defaultCookieRequestProvider != null)
                    options.RequestCultureProviders.Remove(defaultCookieRequestProvider);

                options.RequestCultureProviders.Insert(0,
                    new CookieRequestCultureProvider()
                    {
                        CookieName = ".AspNetCore.Culture",
                        Options = options
                    });
            });

让我描述一下上面的代码,我们应用程序的默认语言环境是en-US,我们只支持英语、波斯语和德语,因此,如果浏览器的语言环境与这三种语言不同,或者您设置的语言是其他语言,则应用程序必须切换到默认语言环境。在上述代码中,我只是从列表中删除了CookieCultureProvider,并将其作为第一个提供程序添加到列表中(*我已经解释了为什么它必须是第一个*)。默认的CookieName对我有效,如果您想要更改它,可以这样做。

不要忘记在Startup.cs中的Configure(IApplicationBuilder app, IHostingEnvironment env)下面添加以下代码。

   var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
   app.UseRequestLocalization(options.Value);

目前为止还不错。要拥有资源文件,您必须指定资源的路径。我单独在Web项目根路径中为Controller、Views、ViewModels和SharedResources指定资源,层次结构如下所示。

|-Resoures
|---Controllers
|---Views
|---Models
|---SharedResource.resx
|---SharedResource.fa-IR.resx
|---SharedResource.de-DE.resx

请记住,对于SharedResource,在Web项目根路径中创建一个同名的空类,即在其中创建一个带有相同名称的类 SharedResource.cs 。

将以下代码片段添加到 Startup.cs 中以指定资源路径和其他内容。

services.AddLocalization(options => options.ResourcesPath = "Resources");

services.AddMvc()
    AddViewLocalization(
                LanguageViewLocationExpanderFormat.Suffix,
                opts => { opts.ResourcesPath = "Resources/Views"; }) // this line is not required, it is just for more clarification
            .AddDataAnnotationsLocalization();

例如,当您在控制器中注入IStringLocalizer时,需要在Resources>Controllers文件夹中有相应的资源文件(即HomeController.resx、HomeController.fa-IR.resx、HomeController.de-DE.resx)。我们还可以通过 . (点)分隔路径,即文件名Resources/Controllers/HomeController.resx可以是Resource / Controllers.HomeController.resx中的文件,您必须将IViewLocalizer注入视图中,以便在视图中进行本地化,相应地,您必须在Resources / Views文件夹中为Views准备资源文件。对于ViewModels,由于我们命名了Models文件夹,请将所有ViewModels放在Web项目根路径中预先创建的名为Models的文件夹中,如果文件夹具有其他名称或您更喜欢其他名称,请不要忘记在Resource文件夹下重命名Models文件夹。然后,您什么也不需要做,只需注释模型,例如,使用[DisplayName("User Emailaddress")]注释ViewModel属性,然后在模型内对应的资源文件中创建相应的资源(文件名必须与模型类名匹配),并使用相同的键("User Emailaddress")。

让我们完成我们已经开始的内容,我指的是 CookieRequestCultureProvider 。如我之前所说,我更喜欢将其存储在cookie中,但这有点棘手,因为 cookie解析与您预期的有点不同。只需在要更改文化的位置添加下面的代码,只需将preferedCulture替换为您的文化偏好即可。

var preferedCulture = "fa-IR"; // get the culture from user, i just mock it here in a variable
if (HttpContext.Response.Cookies.ContainsKey(".AspNetCore.Culture"))
{
    HttpContext.Response.Cookies.Delete(".AspNetCore.Culture");
}

HttpContext.Response.Cookies.Append(".AspNetCore.Culture",
    $"c={preferedCulture}|uic={preferedCulture}", new CookieOptions {Expires = DateTime.UtcNow.AddYears(1)});

我们开始吧,我们的sp.net核心Web应用现在已本地化 :)


只是给你一个赞。我疯狂地花了两个小时问自己为什么 asp net core 本地化 cookie 在浏览器中存在却不起作用。你的帖子完美地解释了这一点。 - Adelaiglesia
注意两个“陷阱”:1)IStringLocalizer不喜欢一些短的文化名称。尽管当前线程文化和资源名称为“lv”,但它却无法使用lv。当我将它们全部重命名为lv-LV时,它开始工作了。2)如果您将SharedResource文件放在Resources文件夹中,并将其命名空间设置为以“Resources”结尾,则不要设置o.ResourcesPath =“Resources”,因为它会开始查找Resources.Resources文件夹,而这个文件夹不存在。因此,只能选择命名空间或ResourcesPath。 - JustAMartin

5
基于rboe和Tedd Hansen杰出的回答,我编写了一个基于IStringLocalizer的IdentityErrorDescriber来供自己使用,如果有人需要多语言支持,我会在这里分享它 :)。

基本思路是按照通常的方式为默认语言和任何其他语言创建资源文件。(https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization

位于(如果您保留我的类名):

/Resources/yournamespace.LocalizedIdentityErrorDescriber.resx
/Resources/yournamespace.LocalizedIdentityErrorDescriber.fr.resx
等等......

在其中,您将使用错误代码(例如:DefaultError、ConcurrencyError)作为键。

然后添加以下类

LocalizedIdentityErrorDescriber.cs

public class LocalizedIdentityErrorDescriber : IdentityErrorDescriber
{
    /// <summary> 
    /// The <see cref="IStringLocalizer{LocalizedIdentityErrorDescriber}"/>
    /// used to localize the strings
    /// </summary>
    private readonly IStringLocalizer<LocalizedIdentityErrorDescriber> localizer;

    /// <summary>
    /// Initializes a new instance of the <see cref="LocalizedIdentityErrorDescriber"/> class.
    /// </summary>
    /// <param name="localizer">
    /// The <see cref="IStringLocalizer{LocalizedIdentityErrorDescriber}"/>
    /// that we will use to localize the strings
    /// </param>
    public LocalizedIdentityErrorDescriber(IStringLocalizer<LocalizedIdentityErrorDescriber> localizer)
    {
        this.localizer = localizer;
    }

    /// <summary>
    /// Returns the default <see cref="IdentityError" />.
    /// </summary>
    /// <returns>The default <see cref="IdentityError" /></returns>
    public override IdentityError DefaultError()
    {
        return this.GetErrorByCode("DefaultError");
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a concurrency failure.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a concurrency failure.</returns>
    public override IdentityError ConcurrencyFailure()
    {
        return this.GetErrorByCode("ConcurrencyFailure");
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a password mismatch.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a password mismatch.</returns>
    public override IdentityError PasswordMismatch()
    {
        return this.GetErrorByCode("PasswordMismatch");
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating an invalid token.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating an invalid token.</returns>
    public override IdentityError InvalidToken()
    {
        return this.GetErrorByCode("InvalidToken");
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating an external login is already associated with an account.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating an external login is already associated with an account.</returns>
    public override IdentityError LoginAlreadyAssociated()
    {
        return this.GetErrorByCode("LoginAlreadyAssociated");
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating the specified user <paramref name="userName" /> is invalid.
    /// </summary>
    /// <param name="userName">The user name that is invalid.</param>
    /// <returns>An <see cref="IdentityError" /> indicating the specified user <paramref name="userName" /> is invalid.</returns>
    public override IdentityError InvalidUserName(string userName)
    {
        return this.FormatErrorByCode("InvalidUserName", (object)userName);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating the specified <paramref name="email" /> is invalid.
    /// </summary>
    /// <param name="email">The email that is invalid.</param>
    /// <returns>An <see cref="IdentityError" /> indicating the specified <paramref name="email" /> is invalid.</returns>
    public override IdentityError InvalidEmail(string email)
    {
        return this.FormatErrorByCode("InvalidEmail", (object)email);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating the specified <paramref name="userName" /> already exists.
    /// </summary>
    /// <param name="userName">The user name that already exists.</param>
    /// <returns>An <see cref="IdentityError" /> indicating the specified <paramref name="userName" /> already exists.</returns>
    public override IdentityError DuplicateUserName(string userName)
    {
        return this.FormatErrorByCode("DuplicateUserName", (object)userName);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating the specified <paramref name="email" /> is already associated with an account.
    /// </summary>
    /// <param name="email">The email that is already associated with an account.</param>
    /// <returns>An <see cref="IdentityError" /> indicating the specified <paramref name="email" /> is already associated with an account.</returns>
    public override IdentityError DuplicateEmail(string email)
    {
        return this.FormatErrorByCode("DuplicateEmail", (object)email);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating the specified <paramref name="role" /> name is invalid.
    /// </summary>
    /// <param name="role">The invalid role.</param>
    /// <returns>An <see cref="IdentityError" /> indicating the specific role <paramref name="role" /> name is invalid.</returns>
    public override IdentityError InvalidRoleName(string role)
    {
        return this.FormatErrorByCode("InvalidRoleName", (object)role);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating the specified <paramref name="role" /> name already exists.
    /// </summary>
    /// <param name="role">The duplicate role.</param>
    /// <returns>An <see cref="IdentityError" /> indicating the specific role <paramref name="role" /> name already exists.</returns>
    public override IdentityError DuplicateRoleName(string role)
    {
        return this.FormatErrorByCode("DuplicateRoleName", (object)role);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a user already has a password.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a user already has a password.</returns>
    public override IdentityError UserAlreadyHasPassword()
    {
        return this.GetErrorByCode("UserAlreadyHasPassword");
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating user lockout is not enabled.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating user lockout is not enabled..</returns>
    public override IdentityError UserLockoutNotEnabled()
    {
        return this.GetErrorByCode("UserLockoutNotEnabled");
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a user is already in the specified <paramref name="role" />.
    /// </summary>
    /// <param name="role">The duplicate role.</param>
    /// <returns>An <see cref="IdentityError" /> indicating a user is already in the specified <paramref name="role" />.</returns>
    public override IdentityError UserAlreadyInRole(string role)
    {
        return this.FormatErrorByCode("UserAlreadyInRole", (object)role);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a user is not in the specified <paramref name="role" />.
    /// </summary>
    /// <param name="role">The duplicate role.</param>
    /// <returns>An <see cref="IdentityError" /> indicating a user is not in the specified <paramref name="role" />.</returns>
    public override IdentityError UserNotInRole(string role)
    {
        return this.FormatErrorByCode("UserNotInRole", (object)role);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a password of the specified <paramref name="length" /> does not meet the minimum length requirements.
    /// </summary>
    /// <param name="length">The length that is not long enough.</param>
    /// <returns>An <see cref="IdentityError" /> indicating a password of the specified <paramref name="length" /> does not meet the minimum length requirements.</returns>
    public override IdentityError PasswordTooShort(int length)
    {
        return this.FormatErrorByCode("PasswordTooShort", (object)length);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a password entered does not contain a non-alphanumeric character, which is required by the password policy.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a password entered does not contain a non-alphanumeric character.</returns>
    public override IdentityError PasswordRequiresNonAlphanumeric()
    {
        return this.GetErrorByCode("PasswordRequiresNonAlphanumeric");
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a password entered does not contain a numeric character, which is required by the password policy.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a password entered does not contain a numeric character.</returns>
    public override IdentityError PasswordRequiresDigit()
    {
        return this.GetErrorByCode("PasswordRequiresDigit");
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a password entered does not contain a lower case letter, which is required by the password policy.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a password entered does not contain a lower case letter.</returns>
    public override IdentityError PasswordRequiresLower()
    {
        return this.GetErrorByCode("PasswordRequiresLower");
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a password entered does not contain an upper case letter, which is required by the password policy.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a password entered does not contain an upper case letter.</returns>
    public override IdentityError PasswordRequiresUpper()
    {
        return this.GetErrorByCode("PasswordRequiresUpper");
    }

    /// <summary>Returns a localized <see cref="IdentityError"/> for the provided code.</summary>
    /// <param name="code">The error's code.</param>
    /// <returns>A localized <see cref="IdentityError"/>.</returns>
    private IdentityError GetErrorByCode(string code)
    {
        return new IdentityError()
        {
            Code = code,
            Description = this.localizer.GetString(code)
        };
    }

    /// <summary>Formats a localized <see cref="IdentityError"/> for the provided code.</summary>
    /// <param name="code">The error's code.</param>
    /// <param name="parameters">The parameters to format the string with.</param>
    /// <returns>A localized <see cref="IdentityError"/>.</returns>
    private IdentityError FormatErrorByCode(string code, params object[] parameters)
    {
        return new IdentityError
        {
            Code = code,
            Description = string.Format(this.localizer.GetString(code, parameters))
        };
    }
}

初始化所有内容:

Startup.cs

    [...]

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddErrorDescriber<LocalizedIdentityErrorDescriber>();

        services.AddLocalization(options => options.ResourcesPath = "Resources");

        // Your service configuration code

        services.AddMvc()
            .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
            .AddDataAnnotationsLocalization();

        // Your service configuration code cont.
    }
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        // your configuration code

        app.UseRequestLocalization(
            new RequestLocalizationOptions()
                {
                    DefaultRequestCulture = new RequestCulture("fr"),
                    SupportedCultures = SupportedCultures,
                    SupportedUICultures = SupportedCultures
                });

        app.UseStaticFiles();

        app.UseIdentity();

        // your configuration code
    }

    [...]

2
并且提供俄语的 IdentityErrorDescriber,以防有人需要。
public class RussianIdentityErrorDescriber : IdentityErrorDescriber
{

    public override IdentityError ConcurrencyFailure()
    {
        return new IdentityError
        {
            Code = "ConcurrencyFailure",
            Description = "Сбой в параллельных запросах. Возможно объект был изменен."
        };
    }
    public override IdentityError DefaultError()
    {
    return new IdentityError
        {
            Code = "DefaultError",
            Description = "Произошла неизвестная ошибка при авторизации."
        };
    }
    public override IdentityError DuplicateEmail(string email)
    {
        return new IdentityError
        {
            Code = "DuplicateEmail",
            Description = $"E-mail '{email}' уже используется."
        };
    }
    public override IdentityError DuplicateRoleName(string role)
    {
        return new IdentityError
        {
            Code = "DuplicateRoleName",
            Description = $"Роль с именем '{role}' уже существует."
        };
    }
    public override IdentityError DuplicateUserName(string userName)
    {
        return new IdentityError
        {
            Code = "DuplicateUserName",
            Description = $"Пользователь '{userName}' уже зарегистрирован."
        };
    }
    public override IdentityError InvalidEmail(string email)
    {
        return new IdentityError
        {
            Code = "InvalidEmail",
            Description = $"E-mail '{email}' содержит неверный формат."
        };
    }
    public override IdentityError InvalidRoleName(string role)
    {
        return new IdentityError
        {
            Code = "InvalidRoleName",
            Description = $"Имя роли '{role}' задано не верно (содержит не допустимые символы либо длину)."
        };
    }
    public override IdentityError InvalidToken()
    {
        return new IdentityError
        {
            Code = "InvalidToken",
            Description = "Неправильно указан код подтверждения (token)."
        };
    }
    public override IdentityError InvalidUserName(string userName)
    {
        return new IdentityError
        {
            Code = "InvalidUserName",
            Description = $"Имя пользователя '{userName}' указано не верно (содержит не допустимые символы либо длину)."
        };
    }
    public override IdentityError LoginAlreadyAssociated()
    {
        return new IdentityError
        {
            Code = "LoginAlreadyAssociated",
            Description = "Данный пользователь уже привязан к аккаунту."
        };
    }
    public override IdentityError PasswordMismatch()
    {
        return new IdentityError
        {
            Code = "PasswordMismatch",
            Description = "Пароли не совпадают."
        };
    }
    public override IdentityError PasswordRequiresDigit()
    {
        return new IdentityError
        {
            Code = "PasswordRequiresDigit",
            Description = "Пароль должен содержать минимум одну цифру."
        };
    }
    public override IdentityError PasswordRequiresLower()
    {
        return new IdentityError
        {
            Code = "PasswordRequiresLower",
            Description = "Пароль должен содержать минимум одну строчную букву."
        };
    }
    public override IdentityError PasswordRequiresNonAlphanumeric()
    {
        return new IdentityError
        {
            Code = "PasswordRequiresNonAlphanumeric",
            Description = "Пароль должен содержать минимум один специальный символ (не буквенно-цифровой)."
        };
    }
    public override IdentityError PasswordRequiresUniqueChars(int uniqueChars)
    {
        return new IdentityError
        {
            Code = "PasswordRequiresUniqueChars",
            Description = $"Пароль должен содержать минимум '{uniqueChars}' не повторяющихся символов."
        };
    }
    public override IdentityError PasswordRequiresUpper()
    {
        return new IdentityError
        {
            Code = "PasswordRequiresUpper",
            Description = "Пароль должен содержать минимум одну заглавную букву."
        };
    }
    public override IdentityError PasswordTooShort(int length)
    {
        return new IdentityError
        {
            Code = "PasswordTooShort",
            Description = $"Пароль слишком короткий. Минимальное количество символов: '{length}'."
        };
    }
    public override IdentityError RecoveryCodeRedemptionFailed()
    {
        return new IdentityError
        {
            Code = "RecoveryCodeRedemptionFailed",
            Description = "Не удалось использовать код восстановления."
        };
    }
    public override IdentityError UserAlreadyHasPassword()
    {
        return new IdentityError
        {
            Code = "UserAlreadyHasPassword",
            Description = "Пароль для пользователя уже задан."
        };
    }
    public override IdentityError UserAlreadyInRole(string role)
    {
        return new IdentityError
        {
            Code = "UserAlreadyInRole",
            Description = $"Роль '{role}' уже привязана к пользователю."
        };
    }
    public override IdentityError UserLockoutNotEnabled()
    {
        return new IdentityError
        {
            Code = "UserLockoutNotEnabled",
            Description = "Блокировка пользователя отключена."
        };
    }
    public override IdentityError UserNotInRole(string role)
    {
        return new IdentityError
        {
            Code = "UserNotInRole",
            Description = $"Пользователь должен иметь роль: '{role}'"
        };
    }
}

In `Startup.cs` set `RussianIdentityErrorDescriber `

public void ConfigureServices(IServiceCollection services)
{
// ...   
    services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddErrorDescriber<RussianIdentityErrorDescriber>();
    // ...   
}

请编辑您的答案以消除语法错误。第一个错误在方法InvalidEmail中:“содержить”->“содержит”。下一个错误在方法InvalidToken中:“Не правильно”->“Неправильно”。 - Aleks Andreev
已修正。感谢您的评论。我显然在发布时匆忙了! - Иванцов Дмитрий

1
挪威身份验证错误描述器,以防有人需要。
public class NorwegianIdentityErrorDescriber : IdentityErrorDescriber
{

    public override IdentityError DefaultError()
    {
        return new IdentityError()
        {
            Code = "DefaultError",
            Description = "En ukjent feil har oppstått."
        };
    }

    public override IdentityError ConcurrencyFailure()
    {
        return new IdentityError()
        {
            Code = "ConcurrencyFailure",
            Description = "Optimistisk samtidighet feilet, objektet har blitt endret."
        };
    }

    public override IdentityError PasswordMismatch()
    {
        return new IdentityError()
        {
            Code = "PasswordMismatch",
            Description = "Feil passord."
        };
    }

    public override IdentityError InvalidToken()
    {
        return new IdentityError()
        {
            Code = "InvalidToken",
            Description = "Feil token."
        };
    }

    public override IdentityError LoginAlreadyAssociated()
    {
        return new IdentityError()
        {
            Code = "LoginAlreadyAssociated",
            Description = "En bruker med dette brukernavnet finnes allerede."
        };
    }

    public override IdentityError InvalidUserName(string userName)
    {
        IdentityError identityError = new IdentityError();
        identityError.Code = "InvalidUserName";
        string str = $"Brkernavnet '{userName}' er ikke gyldig. Det kan kun inneholde bokstaver og tall.";
        identityError.Description = str;
        return identityError;
    }

    public override IdentityError InvalidEmail(string email)
    {
        IdentityError identityError = new IdentityError();
        identityError.Code = "InvalidEmail";
        string str = $"E-post '{email}' er ugyldig.";
        identityError.Description = str;
        return identityError;
    }

    public override IdentityError DuplicateUserName(string userName)
    {
        IdentityError identityError = new IdentityError();
        identityError.Code = "DuplicateUserName";
        string str = $"Brukernavn '{userName}' er allerede tatt.";
        identityError.Description = str;
        return identityError;
    }

    public override IdentityError DuplicateEmail(string email)
    {
        IdentityError identityError = new IdentityError();
        identityError.Code = "DuplicateEmail";
        string str = $"E-post '{email}' er allerede tatt.";
        identityError.Description = str;
        return identityError;
    }

    public override IdentityError InvalidRoleName(string role)
    {
        IdentityError identityError = new IdentityError();
        identityError.Code = "InvalidRoleName";
        string str = $"Rollenavn '{role}' er ugyldig.";
        identityError.Description = str;
        return identityError;
    }

    public override IdentityError DuplicateRoleName(string role)
    {
        IdentityError identityError = new IdentityError();
        identityError.Code = "DuplicateRoleName";
        string str = $"Rollenavn '{role}' er allerede tatt.";
        identityError.Description = str;
        return identityError;
    }

    public virtual IdentityError UserAlreadyHasPassword()
    {
        return new IdentityError()
        {
            Code = "UserAlreadyHasPassword",
            Description = "Bruker har allerede passord satt."
        };
    }

    public override IdentityError UserLockoutNotEnabled()
    {
        return new IdentityError()
        {
            Code = "UserLockoutNotEnabled",
            Description = "Utestenging er ikke slått på for denne brukeren."
        };
    }

    public override IdentityError UserAlreadyInRole(string role)
    {
        IdentityError identityError = new IdentityError();
        identityError.Code = "UserAlreadyInRole";
        string str = $"Brukeren er allerede i rolle '{role}'.";
        identityError.Description = str;
        return identityError;
    }

    public override IdentityError UserNotInRole(string role)
    {
        IdentityError identityError = new IdentityError();
        identityError.Code = "UserNotInRole";
        string str = $"Bruker er ikke i rolle '{role}'.";
        identityError.Description = str;
        return identityError;
    }

    public override IdentityError PasswordTooShort(int length)
    {
        IdentityError identityError = new IdentityError();
        identityError.Code = "PasswordTooShort";
        string str = $"Passordet må være på minimum {length} tegn.";
        identityError.Description = str;
        return identityError;
    }

    public override IdentityError PasswordRequiresNonAlphanumeric()
    {
        return new IdentityError()
        {
            Code = "PasswordRequiresNonAlphanumeric",
            Description = "Passordet må inneholde minst ett spesialtegn."
        };
    }

    public override IdentityError PasswordRequiresDigit()
    {
        return new IdentityError()
        {
            Code = "PasswordRequiresDigit",
            Description = "Passordet må inneholde minst ett siffer."
        };
    }

    public override IdentityError PasswordRequiresLower()
    {
        return new IdentityError()
        {
            Code = "PasswordRequiresLower",
            Description = "Passordet må inneholde minst en liten bokstav (a-z)."
        };
    }

    public override IdentityError PasswordRequiresUpper()
    {
        return new IdentityError()
        {
            Code = "PasswordRequiresUpper",
            Description = "Passordet må inneholde minst en stor bokstav (A-Z)."
        };
    }
}

1

如果您正在寻找波兰语版本,这里就是:

public class PolishLocalizedIdentityErrorDescriber : IdentityErrorDescriber
{
    public override IdentityError DefaultError()
    {
        return new IdentityError
        {
            Code = nameof(DefaultError),
            Description = "Wystąpił nieznany błąd."
        };
    }
public override IdentityError ConcurrencyFailure() { return new IdentityError { Code = nameof(ConcurrencyFailure), Description = "Błąd współbieżności, obiekt został już zmodyfikowany." }; }
public override IdentityError PasswordMismatch() { return new IdentityError { Code = nameof(PasswordMismatch), Description = "Niepoprawne hasło." }; }
public override IdentityError InvalidToken() { return new IdentityError { Code = nameof(InvalidToken), Description = "Nieprawidłowy token." }; }
public override IdentityError RecoveryCodeRedemptionFailed() { return new IdentityError { Code = nameof(RecoveryCodeRedemptionFailed), Description = "Pobranie kodu odzyskiwania nie powiodło się." }; }
public override IdentityError LoginAlreadyAssociated() { return new IdentityError { Code = nameof(LoginAlreadyAssociated), Description = "Użytkownik o tym loginie już istnieje." }; }
public override IdentityError InvalidUserName(string userName) { return new IdentityError { Code = nameof(InvalidUserName), Description = $"Nazwa użytkownika '{userName}' jest nieprawidłowa, może zawierać tylko litery lub cyfry." }; }
public override IdentityError InvalidEmail(string email) { return new IdentityError { Code = nameof(InvalidEmail), Description = $"Adres e-mail '{email}' jest nieprawidłowy." }; }
public override IdentityError DuplicateUserName(string userName) { return new IdentityError { Code = nameof(DuplicateUserName), Description = $"Nazwa '{userName}' jest już zajęta." }; }
public override IdentityError DuplicateEmail(string email) { return new IdentityError { Code = nameof(DuplicateEmail), Description = $"Adres e-mail '{email}' jest już zajęty." }; }
public override IdentityError InvalidRoleName(string role) { return new IdentityError { Code = nameof(InvalidRoleName), Description = $"Nazwa roli '{role}' już niepoprawna." }; }
public override IdentityError DuplicateRoleName(string role) { return new IdentityError { Code = nameof(DuplicateRoleName), Description = $"Rola '{role}' już istnieje." }; }
public override IdentityError UserAlreadyHasPassword() { return new IdentityError { Code = nameof(UserAlreadyHasPassword), Description = "Użytkownik ma już ustawione hasło." }; }
public override IdentityError UserLockoutNotEnabled() { return new IdentityError { Code = nameof(UserLockoutNotEnabled), Description = "Blokada konta nie jest włączona dla tego użytkownika." }; }
public override IdentityError UserAlreadyInRole(string role) { return new IdentityError { Code = nameof(UserAlreadyInRole), Description = $"Użytkownik korzysta już z roli '{role}'." }; }
public override IdentityError UserNotInRole(string role) { return new IdentityError { Code = nameof(UserNotInRole), Description = $"Użytkownika nie ma w roli '{role}'." }; }
public override IdentityError PasswordTooShort(int length) { return new IdentityError { Code = nameof(PasswordTooShort), Description = $"Hasło musi składać się z co najmniej {length} znaków." }; }
public override IdentityError PasswordRequiresUniqueChars(int uniqueChars) { return new IdentityError { Code = nameof(PasswordRequiresUniqueChars), Description = $"Hasło musi składać się z co najmniej {uniqueChars} unikalnych znaków." }; }
public override IdentityError PasswordRequiresNonAlphanumeric() { return new IdentityError { Code = nameof(PasswordRequiresNonAlphanumeric), Description = "Hasło musi zawierać co

1
对于那些想知道是否可能将当前选择的文化本地化消息的人,是的,这是可能的。在asp.net core 2.2中,您可以创建一个名为LocalizedIdentityErrorDescriber的类,其中IdentityResources是您的虚拟类的名称(在此处描述:全球化和本地化)。
public class LocalizedIdentityErrorDescriber : IdentityErrorDescriber
{
    /// <summary> 
    /// The <see cref="IStringLocalizer{LocalizedIdentityErrorDescriber}"/>
    /// used to localize the strings
    /// </summary>
    private readonly IStringLocalizer<IdentityResources> localizer;

    /// <summary>
    /// Initializes a new instance of the <see cref="LocalizedIdentityErrorDescriber"/> class.
    /// </summary>
    /// <param name="localizer">
    /// The <see cref="IStringLocalizer{LocalizedIdentityErrorDescriber}"/>
    /// that we will use to localize the strings
    /// </param>
    public LocalizedIdentityErrorDescriber(IStringLocalizer<IdentityResources> localizer)
    {
        this.localizer = localizer;
    }

    /// <summary>
    /// Returns the default <see cref="IdentityError" />.
    /// </summary>
    /// <returns>The default <see cref="IdentityError" /></returns>
    public override IdentityError DefaultError()
    {
        return this.GetErrorByCode(nameof(DefaultError));
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a concurrency failure.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a concurrency failure.</returns>
    public override IdentityError ConcurrencyFailure()
    {
        return this.GetErrorByCode(nameof(ConcurrencyFailure));
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a password mismatch.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a password mismatch.</returns>
    public override IdentityError PasswordMismatch()
    {
        return this.GetErrorByCode(nameof(PasswordMismatch));
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating an invalid token.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating an invalid token.</returns>
    public override IdentityError InvalidToken()
    {
        return this.GetErrorByCode(nameof(InvalidToken));
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating an external login is already associated with an account.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating an external login is already associated with an account.</returns>
    public override IdentityError LoginAlreadyAssociated()
    {
        return this.GetErrorByCode(nameof(LoginAlreadyAssociated));
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating the specified user <paramref name="userName" /> is invalid.
    /// </summary>
    /// <param name="userName">The user name that is invalid.</param>
    /// <returns>An <see cref="IdentityError" /> indicating the specified user <paramref name="userName" /> is invalid.</returns>
    public override IdentityError InvalidUserName(string userName)
    {
        return this.FormatErrorByCode(nameof(InvalidUserName), (object)userName);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating the specified <paramref name="email" /> is invalid.
    /// </summary>
    /// <param name="email">The email that is invalid.</param>
    /// <returns>An <see cref="IdentityError" /> indicating the specified <paramref name="email" /> is invalid.</returns>
    public override IdentityError InvalidEmail(string email)
    {
        return this.FormatErrorByCode(nameof(InvalidEmail), (object)email);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating the specified <paramref name="userName" /> already exists.
    /// </summary>
    /// <param name="userName">The user name that already exists.</param>
    /// <returns>An <see cref="IdentityError" /> indicating the specified <paramref name="userName" /> already exists.</returns>
    public override IdentityError DuplicateUserName(string userName)
    {
        return this.FormatErrorByCode(nameof(DuplicateUserName), (object)userName);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating the specified <paramref name="email" /> is already associated with an account.
    /// </summary>
    /// <param name="email">The email that is already associated with an account.</param>
    /// <returns>An <see cref="IdentityError" /> indicating the specified <paramref name="email" /> is already associated with an account.</returns>
    public override IdentityError DuplicateEmail(string email)
    {
        return this.FormatErrorByCode(nameof(DuplicateEmail), (object)email);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating the specified <paramref name="role" /> name is invalid.
    /// </summary>
    /// <param name="role">The invalid role.</param>
    /// <returns>An <see cref="IdentityError" /> indicating the specific role <paramref name="role" /> name is invalid.</returns>
    public override IdentityError InvalidRoleName(string role)
    {
        return this.FormatErrorByCode(nameof(InvalidRoleName), (object)role);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating the specified <paramref name="role" /> name already exists.
    /// </summary>
    /// <param name="role">The duplicate role.</param>
    /// <returns>An <see cref="IdentityError" /> indicating the specific role <paramref name="role" /> name already exists.</returns>
    public override IdentityError DuplicateRoleName(string role)
    {
        return this.FormatErrorByCode(nameof(DuplicateRoleName), (object)role);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a user already has a password.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a user already has a password.</returns>
    public override IdentityError UserAlreadyHasPassword()
    {
        return this.GetErrorByCode(nameof(UserAlreadyHasPassword));
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating user lockout is not enabled.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating user lockout is not enabled..</returns>
    public override IdentityError UserLockoutNotEnabled()
    {
        return this.GetErrorByCode(nameof(UserLockoutNotEnabled));
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a user is already in the specified <paramref name="role" />.
    /// </summary>
    /// <param name="role">The duplicate role.</param>
    /// <returns>An <see cref="IdentityError" /> indicating a user is already in the specified <paramref name="role" />.</returns>
    public override IdentityError UserAlreadyInRole(string role)
    {
        return this.FormatErrorByCode(nameof(UserAlreadyInRole), (object)role);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a user is not in the specified <paramref name="role" />.
    /// </summary>
    /// <param name="role">The duplicate role.</param>
    /// <returns>An <see cref="IdentityError" /> indicating a user is not in the specified <paramref name="role" />.</returns>
    public override IdentityError UserNotInRole(string role)
    {
        return this.FormatErrorByCode(nameof(UserNotInRole), (object)role);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a password of the specified <paramref name="length" /> does not meet the minimum length requirements.
    /// </summary>
    /// <param name="length">The length that is not long enough.</param>
    /// <returns>An <see cref="IdentityError" /> indicating a password of the specified <paramref name="length" /> does not meet the minimum length requirements.</returns>
    public override IdentityError PasswordTooShort(int length)
    {
        return this.FormatErrorByCode(nameof(PasswordTooShort), (object)length);
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a password entered does not contain a non-alphanumeric character, which is required by the password policy.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a password entered does not contain a non-alphanumeric character.</returns>
    public override IdentityError PasswordRequiresNonAlphanumeric()
    {
        return this.GetErrorByCode(nameof(PasswordRequiresNonAlphanumeric));
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a password entered does not contain a numeric character, which is required by the password policy.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a password entered does not contain a numeric character.</returns>
    public override IdentityError PasswordRequiresDigit()
    {
        return this.GetErrorByCode(nameof(PasswordRequiresDigit));
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a password entered does not contain a lower case letter, which is required by the password policy.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a password entered does not contain a lower case letter.</returns>
    public override IdentityError PasswordRequiresLower()
    {
        return this.GetErrorByCode(nameof(PasswordRequiresLower));
    }

    /// <summary>
    /// Returns an <see cref="IdentityError" /> indicating a password entered does not contain an upper case letter, which is required by the password policy.
    /// </summary>
    /// <returns>An <see cref="IdentityError" /> indicating a password entered does not contain an upper case letter.</returns>
    public override IdentityError PasswordRequiresUpper()
    {
        return this.GetErrorByCode(nameof(PasswordRequiresUpper));
    }

    public override IdentityError PasswordRequiresUniqueChars(int uniqueChars)
    {
        return this.FormatErrorByCode(nameof(PasswordRequiresUniqueChars), (object)uniqueChars);
    }

    public override IdentityError RecoveryCodeRedemptionFailed()
    {
        return this.GetErrorByCode(nameof(RecoveryCodeRedemptionFailed));
    }

    /// <summary>Returns a localized <see cref="IdentityError"/> for the provided code.</summary>
    /// <param name="code">The error's code.</param>
    /// <returns>A localized <see cref="IdentityError"/>.</returns>
    private IdentityError GetErrorByCode(string code)
    {
        return new IdentityError()
        {
            Code = code,
            Description = this.localizer.GetString(code)
        };
    }

    /// <summary>Formats a localized <see cref="IdentityError"/> for the provided code.</summary>
    /// <param name="code">The error's code.</param>
    /// <param name="parameters">The parameters to format the string with.</param>
    /// <returns>A localized <see cref="IdentityError"/>.</returns>
    private IdentityError FormatErrorByCode(string code, params object[] parameters)
    {
        return new IdentityError
        {
            Code = code,
            Description = string.Format(this.localizer.GetString(code, parameters))
        };
    }
}

首先,按照以下方式为您的语言创建资源文件:Resources\(上述虚拟类的名称)(.culture).resx。对于默认语言,文化应保留为空白。如果不是默认语言,请勿忘记在文化名称前加上“.”。您的IdentityResources文件应如下所示: enter image description here 其次,在Startup.cs中添加以下行:
 public void ConfigureServices(IServiceCollection services)
 {
      services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; });
      services.AddIdentity<IdentityUser, IdentityRole>()
           .AddEntityFrameworkStores<nameofyourdbcontext>()
           .AddDefaultTokenProviders()
           .AddErrorDescriber<LocalizedIdentityErrorDescriber>(); // this line is important
       services.Configure<RequestLocalizationOptions>(options =>
       {
            var supportedCultures = new[]
            {
                new CultureInfo("en"), // english
                new CultureInfo("tr"), // turkish
                new CultureInfo("ru")  // russian
            };

            options.DefaultRequestCulture = new RequestCulture(culture: "en", uiCulture: "en");
            options.SupportedCultures = supportedCultures;
            options.SupportedUICultures = supportedCultures;
            options.RequestCultureProviders.Insert(0, new CookieRequestCultureProvider()); // type of CultureProvider you want.
        });
 }
 public void Configure(IApplicationBuilder app, IHostingEnvironment env)
 {
        var localizationOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
        // IT IS IMPORTANT TO CALL UseRequestLocalization BEFORE UseMvc
        app.UseRequestLocalization(localizationOptions.Value);
        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}"
            );
        });
 }

到目前为止,您几乎完成了,但您需要设置 Culture cookie,并且此处有一个代码示例:全球化和本地化。希望对某些人有所帮助。


0

瑞典IdentityErrorDescriber,以防有人需要。

public class SwedishIdentityErrorDescriber : IdentityErrorDescriber
{
    public override IdentityError DefaultError() { return new IdentityError { Code = nameof(DefaultError), Description = "Ett okänt fel har inträffat." }; }
    public override IdentityError ConcurrencyFailure() { return new IdentityError { Code = nameof(ConcurrencyFailure), Description = "Objektet har ändrats sedan uppläsning." }; }
    public override IdentityError PasswordMismatch() { return new IdentityError { Code = nameof(PasswordMismatch), Description = "Felaktigt lösenord." }; }
    public override IdentityError InvalidToken() { return new IdentityError { Code = nameof(InvalidToken), Description = "Ogiltigt token." }; }
    public override IdentityError LoginAlreadyAssociated() { return new IdentityError { Code = nameof(LoginAlreadyAssociated), Description = "En användare med den här inloggningen finns redan." }; }
    public override IdentityError InvalidUserName(string userName) { return new IdentityError { Code = nameof(InvalidUserName), Description = $"Användarnamnet '{userName}' är ogiltigt, kan endast innehålla bokstäver eller siffror." }; }
    public override IdentityError InvalidEmail(string email) { return new IdentityError { Code = nameof(InvalidEmail), Description = $"E-postadressen {email} är ogiltig." }; }
    public override IdentityError DuplicateUserName(string userName) { return new IdentityError { Code = nameof(DuplicateUserName), Description = $"Användarnamnet '{userName}' finns redan." }; }
    public override IdentityError DuplicateEmail(string email) { return new IdentityError { Code = nameof(DuplicateEmail), Description = $"E-postadressen {email} finns redan." }; }
    public override IdentityError InvalidRoleName(string role) { return new IdentityError { Code = nameof(InvalidRoleName), Description = $"Rollen '{role}' är ogiltig." }; }
    public override IdentityError DuplicateRoleName(string role) { return new IdentityError { Code = nameof(DuplicateRoleName), Description = $"Rollen '{role}' finns redan." }; }
    public override IdentityError UserAlreadyHasPassword() { return new IdentityError { Code = nameof(UserAlreadyHasPassword), Description = "Användaren har redan angett ett lösenord." }; }
    public override IdentityError UserLockoutNotEnabled() { return new IdentityError { Code = nameof(UserLockoutNotEnabled), Description = "Lockout är inte aktiverat för den här användaren." }; }
    public override IdentityError UserAlreadyInRole(string role) { return new IdentityError { Code = nameof(UserAlreadyInRole), Description = $"Användaren har redan rollen '{role}'." }; }
    public override IdentityError UserNotInRole(string role) { return new IdentityError { Code = nameof(UserNotInRole), Description = $"Användaren har inte rollen '{role}'." }; }
    public override IdentityError PasswordTooShort(int length) { return new IdentityError { Code = nameof(PasswordTooShort), Description = $"Lösenordet måste ha minst {length} tecken." }; }
    public override IdentityError PasswordRequiresNonAlphanumeric() { return new IdentityError { Code = nameof(PasswordRequiresNonAlphanumeric), Description = "Lösenordet måste ha minst ett icke alfanumeriskt tecken." }; }
    public override IdentityError PasswordRequiresDigit() { return new IdentityError { Code = nameof(PasswordRequiresDigit), Description = "Lösenordet måste innehålla minst en siffra ('0'-'9')." }; }
    public override IdentityError PasswordRequiresLower() { return new IdentityError { Code = nameof(PasswordRequiresLower), Description = "Lösenordet måste innehålla minst en gemen ('a'-'z')." }; }
    public override IdentityError PasswordRequiresUpper() { return new IdentityError { Code = nameof(PasswordRequiresUpper), Description = "Lösenordet måste innehålla minst en versal ('A'-'Z')." }; }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接