ASP.NET Core 2.0中RequiredAttribute的本地化

14

我在我的新的.NET Core项目中遇到本地化问题。我有2个项目:

  • DataAccess项目,其中包括模型和数据注释(例如RequiredAttribute)
  • Web项目,其中包括MVC视图等。

我希望可以将所有验证属性在一个单独的地方全局本地化,以获得类似于MVC 5的相似行为。这是可能的吗?

我不想为Models / Views等单独使用语言文件。

微软的文档在使用具有本地化DataAnnotation消息的SharedResources.resx文件方面并不清晰。

在MVC 5中,我没有关心它。 我只需要将区域设置设置为我的语言即可,一切都很好。

我尝试将ErrorMessageResourceName和ErrorMessageResourceType设置为DataAccess项目中共享资源文件名“Strings.resx”和“Strings.de.resx”:

[Required(ErrorMessageResourceName = "RequiredAttribute_ValidationError", ErrorMessageResourceType = typeof(Strings))]

我还尝试将设置名称设为RequiredAttribute_ValidationError,但它并没有起作用。

我已经在Startup.cs中添加了.AddDataAnnotationsLocalization(),但似乎没有起到任何作用。

我已经阅读了几篇文章,但是我找不到原因为什么它不起作用。

编辑: 目前为止我所拥有的:

1.) LocService类

 public class LocService
    {
        private readonly IStringLocalizer _localizer;

        public LocService(IStringLocalizerFactory factory)
        {
            _localizer = factory.Create(typeof(Strings));
        }

        public LocalizedString GetLocalizedHtmlString(string key)
        {
            return _localizer[key];
        }
    }

2.) 添加了“Resources”文件夹,其中包含Strings.cs(空类和虚构构造函数)

3.) 添加了Strings.de-DE.resx文件,其中包含一个项目“RequiredAttribute_ValidationError”

4.) 修改了我的Startup.cs

public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<MessageService>();
            services.AddDbContext<DataContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddSingleton<LocService>();
            services.AddLocalization(options => options.ResourcesPath = "Resources");
            services.AddMvc()
                .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver())
                .AddDataAnnotationsLocalization(
                    options =>
                    {
                        options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(Strings));
                    });

            services.Configure<RequestLocalizationOptions>(
                opts =>
                {
                    var supportedCultures = new List<CultureInfo>
                    {
                        new CultureInfo("de-DE"),
                    };

                    opts.DefaultRequestCulture = new RequestCulture("de-DE");
                    // Formatting numbers, dates, etc.
                    opts.SupportedCultures = supportedCultures;
                    // UI strings that we have localized.
                    opts.SupportedUICultures = supportedCultures;
                });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();

            app.UseRequestLocalization(locOptions.Value);
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }

我按照这里的指示进行了操作,但它不起作用: https://damienbod.com/2017/11/01/shared-localization-in-asp-net-core-mvc/

请记住我的模型保存在一个单独的项目中。


1
你可能想在MS文档上留言或在GitHub上开一个issue来告知他们文档不够清晰。 - NightOwl888
如果您想让我们知道发生了什么,请添加完整的启动类。请阅读如何创建 [mcve]。 - Camilo Terevinto
请仔细查看文档。Resx文件必须具有特殊名称才能正常工作,或更改搜索其名称的位置。 - Tseng
https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/localization#dataannotations-localization (抱歉,没时间提供详细答案,也许在家时可以)。它必须按ViewModel文件命名,或者您可以设置共享资源(两个示例均在文档中)。 - Tseng
2
@Tseng:你指引了我正确的方向。关键在于包含共享资源的resx文件必须与应用程序位于相同的根命名空间中。由于我修改了命名空间,现在一切都正常了。但我仍然想知道本地化是否可以使用简单的[Required]注释。现在我不得不写[Required(ErrorMessage = "RequiredAttribute_ValidationError")] - Sven
显示剩余7条评论
5个回答

17

正如@Sven在他对Tseng的答案的评论中所指出的那样,它仍然需要您指定一个明确的 ErrorMessage ,这变得相当繁琐。

问题来自于ValidationAttributeAdapter<TAttribute>.GetErrorMessage()使用的逻辑,以决定是否使用提供的IStringLocalizer。 我使用以下解决方案来解决这个问题:

  1. 创建一个自定义的IValidationAttributeAdapterProvider实现,该实现使用默认的ValidationAttributeAdapterProvider,如下所示:

    public class LocalizedValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
    {
        private readonly ValidationAttributeAdapterProvider _originalProvider = new ValidationAttributeAdapterProvider();
    
        public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
        {
            attribute.ErrorMessage = attribute.GetType().Name.Replace("Attribute", string.Empty);
            if (attribute is DataTypeAttribute dataTypeAttribute)
                attribute.ErrorMessage += "_" + dataTypeAttribute.DataType;
    
            return _originalProvider.GetAttributeAdapter(attribute, stringLocalizer);
        }
    }
    
  2. 在调用AddMvc()之前,在Startup.ConfigureServices()中注册适配器:

  3. services.AddSingleton<Microsoft.AspNetCore.Mvc.DataAnnotations.IValidationAttributeAdapterProvider, LocalizedValidationAttributeAdapterProvider>();
    

    我更喜欢使用基于实际属性的“严格”资源名称,因此上面的代码将查找类似“Required”和“DataType_Password”之类的资源名称,但这当然可以以许多方式进行自定义。

    如果您喜欢基于属性的默认消息的资源名称,您可以改为编写以下内容:

    attribute.ErrorMessage = attribute.FormatErrorMessage("{0}");
    

这似乎不起作用;我的函数GetAttributeAdapter根本没有被调用,尽管我在MVC之前添加了单例服务。有什么想法吗? - youen
@youen,听起来很奇怪。你是否将AddDataAnnotationsLocalization()调用附加到AddMvc()调用中?此外,MVC框架会进行大量缓存,因此该方法不一定在每个请求上被调用,因此当您期望它们被命中时,断点等可能并不总是被触发。 - Anders
@Anders 我已经尝试了你的解决方案,但是我遇到了一个奇怪的问题: 当我使用[EmailAddress]注释时,自定义GetAttributeAdapter被调用,但是当我使用[Required]或[RequiredAttribute]时它没有被调用。 - Máté Eke
@Anders 看起来是同样的问题:https://dev59.com/KKzka4cB1Zd3GeqP4zba?noredirect=1&lq=1 - Máté Eke
如此简单的任务,却有如此多的东西。微软能否简化这个机制? - ADM-IT
显示剩余2条评论

6

I tried setting the ErrorMessageResourceName and ErrorMessageResourceType to my shared resource file name "Strings.resx" and "Strings.de.resx" in the DataAccess project:

   [Required(ErrorMessageResourceName = "RequiredAttribute_ValidationError", ErrorMessageResourceType = typeof(Strings))]

I also tried the setting name to be RequiredAttribute_ValidationError - but it's not working.

你已经走在正确的道路上,但不一定需要设置ErrorMessageResourceName/ErrorMessageResourceType属性。
如我们在ValidationAttributeAdapter<TAttribute>源代码中所看到的,在使用_stringLocalizer版本的条件是ErrorMessage不为nullErrorMessageResourceName/ErrorMessageResourceTypenull
换句话说,当您不设置任何属性或仅设置ErrorMessage时。因此,一个简单的[Required]应该就可以工作了(参见源代码,其中传递给基类构造函数)。
现在,当我们查看DataAnnotations资源文件时,我们发现名称设置为“RequiredAttribute_ValidationError”,值为“The {0} field is required.”,这是默认的英语翻译。
现在,如果您在您的“Strings.de-DE.resx”(或者作为后备的“Strings.resx”)中使用“RequiredAttribute_ValidationError”和德语翻译,它应该可以与注释中的更正的命名空间一起工作。
因此,使用上述配置和GitHub存储库中的字符串,您应该能够在不使用额外属性的情况下使本地化工作。

嗨,Tseng,感谢你的回答,部分正确。但是如果不设置“ErrorMessage”属性,它将无法正常工作。仅使用普通的[RequiredAttribute]无法显示翻译后的验证消息。 - Sven
@Sven:你真的把翻译放在正确的文件里了吗?请记住,在资源文件中,您必须使用“RequiredAttribute_ValidationError”,并且它是特定于语言的模型文件(例如“Models/MyModel.resx”和“Models/MyModel.de-DE.resx”)。 - Tseng

5

事实证明,ValidationAttributeAdapterProvider方法并不像它应该的那样工作,因为它仅用于“客户端验证属性”(这对我来说没有太多意义,因为这些属性是在服务器模型上指定的)。

但我找到了一个解决方案,可以覆盖所有属性的自定义消息。它还能够注入字段名称翻译,而不会在各个地方都出现[Display]。这是惯例优于配置的体现。

此外,作为奖励,此解决方案还覆盖了默认的模型绑定错误文本,即使在验证之前也会使用。一个注意点-如果您收到JSON数据,则Json.Net错误将合并到ModelState错误中,并且不会使用默认绑定错误。我还没有弄清楚如何防止这种情况发生。

所以,这里有三个类你需要:

    public class LocalizableValidationMetadataProvider : IValidationMetadataProvider
    {
        private IStringLocalizer _stringLocalizer;
        private Type _injectableType;

        public LocalizableValidationMetadataProvider(IStringLocalizer stringLocalizer, Type injectableType)
        {
            _stringLocalizer = stringLocalizer;
            _injectableType = injectableType;
        }

        public void CreateValidationMetadata(ValidationMetadataProviderContext context)
        {
            // ignore non-properties and types that do not match some model base type
            if (context.Key.ContainerType == null ||
                !_injectableType.IsAssignableFrom(context.Key.ContainerType))
                return;

            // In the code below I assume that expected use of ErrorMessage will be:
            // 1 - not set when it is ok to fill with the default translation from the resource file
            // 2 - set to a specific key in the resources file to override my defaults
            // 3 - never set to a final text value
            var propertyName = context.Key.Name;
            var modelName = context.Key.ContainerType.Name;

            // sanity check 
            if (string.IsNullOrEmpty(propertyName) || string.IsNullOrEmpty(modelName))
                return;

            foreach (var attribute in context.ValidationMetadata.ValidatorMetadata)
            {
                var tAttr = attribute as ValidationAttribute;
                if (tAttr != null)
                {               
                    // at first, assume the text to be generic error
                    var errorName = tAttr.GetType().Name;
                    var fallbackName = errorName + "_ValidationError";      
                    // Will look for generic widely known resource keys like
                    // MaxLengthAttribute_ValidationError
                    // RangeAttribute_ValidationError
                    // EmailAddressAttribute_ValidationError
                    // RequiredAttribute_ValidationError
                    // etc.

                    // Treat errormessage as resource name, if it's set,
                    // otherwise assume default.
                    var name = tAttr.ErrorMessage ?? fallbackName;

                    // At first, attempt to retrieve model specific text
                    var localized = _stringLocalizer[name];

                    // Some attributes come with texts already preset (breaking the rule 3), 
                    // even if we didn't do that explicitly on the attribute.
                    // For example [EmailAddress] has entire message already filled in by MVC.
                    // Therefore we first check if we could find the value by the given key;
                    // if not, then fall back to default name.

                    // Final attempt - default name from property alone
                    if (localized.ResourceNotFound) // missing key or prefilled text
                        localized = _stringLocalizer[fallbackName];

                    // If not found yet, then give up, leave initially determined name as it is
                    var text = localized.ResourceNotFound ? name : localized;

                    tAttr.ErrorMessage = text;
                }
            }
        }
    }

    public class LocalizableInjectingDisplayNameProvider : IDisplayMetadataProvider
    {
        private IStringLocalizer _stringLocalizer;
        private Type _injectableType;

        public LocalizableInjectingDisplayNameProvider(IStringLocalizer stringLocalizer, Type injectableType)
        {
            _stringLocalizer = stringLocalizer;
            _injectableType = injectableType;
        }

        public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
        {
            // ignore non-properties and types that do not match some model base type
            if (context.Key.ContainerType == null || 
                !_injectableType.IsAssignableFrom(context.Key.ContainerType))
                return;

            // In the code below I assume that expected use of field name will be:
            // 1 - [Display] or Name not set when it is ok to fill with the default translation from the resource file
            // 2 - [Display(Name = x)]set to a specific key in the resources file to override my defaults

            var propertyName = context.Key.Name;
            var modelName = context.Key.ContainerType.Name;

            // sanity check 
            if (string.IsNullOrEmpty(propertyName) || string.IsNullOrEmpty(modelName))
                return;

            var fallbackName = propertyName + "_FieldName";
            // If explicit name is missing, will try to fall back to generic widely known field name,
            // which should exist in resources (such as "Name_FieldName", "Id_FieldName", "Version_FieldName", "DateCreated_FieldName" ...)

            var name = fallbackName;

            // If Display attribute was given, use the last of it
            // to extract the name to use as resource key
            foreach (var attribute in context.PropertyAttributes)
            {
                var tAttr = attribute as DisplayAttribute;
                if (tAttr != null)
                {
                    // Treat Display.Name as resource name, if it's set,
                    // otherwise assume default. 
                    name = tAttr.Name ?? fallbackName;
                }
            }

            // At first, attempt to retrieve model specific text
            var localized = _stringLocalizer[name];

            // Final attempt - default name from property alone
            if (localized.ResourceNotFound)
                localized = _stringLocalizer[fallbackName];

            // If not found yet, then give up, leave initially determined name as it is
            var text = localized.ResourceNotFound ? name : localized;

            context.DisplayMetadata.DisplayName = () => text;
        }

    }

    public static class LocalizedModelBindingMessageExtensions
    {
        public static IMvcBuilder AddModelBindingMessagesLocalizer(this IMvcBuilder mvc,
            IServiceCollection services, Type modelBaseType)
        {
            var factory = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
            var VL = factory.Create(typeof(ValidationMessagesResource));
            var DL = factory.Create(typeof(FieldNamesResource));

            return mvc.AddMvcOptions(o =>
            {
                // for validation error messages
                o.ModelMetadataDetailsProviders.Add(new LocalizableValidationMetadataProvider(VL, modelBaseType));

                // for field names
                o.ModelMetadataDetailsProviders.Add(new LocalizableInjectingDisplayNameProvider(DL, modelBaseType));

                // does not work for JSON models - Json.Net throws its own error messages into ModelState :(
                // ModelBindingMessageProvider is only for FromForm
                // Json works for FromBody and needs a separate format interceptor
                DefaultModelBindingMessageProvider provider = o.ModelBindingMessageProvider;

                provider.SetValueIsInvalidAccessor((v) => VL["FormatHtmlGeneration_ValueIsInvalid", v]);
                provider.SetAttemptedValueIsInvalidAccessor((v, x) => VL["FormatModelState_AttemptedValueIsInvalid", v, x]);
                provider.SetMissingBindRequiredValueAccessor((v) => VL["FormatModelBinding_MissingBindRequiredMember", v]);
                provider.SetMissingKeyOrValueAccessor(() => VL["FormatKeyValuePair_BothKeyAndValueMustBePresent" ]);
                provider.SetMissingRequestBodyRequiredValueAccessor(() => VL["FormatModelBinding_MissingRequestBodyRequiredMember"]);
                provider.SetNonPropertyAttemptedValueIsInvalidAccessor((v) => VL["FormatModelState_NonPropertyAttemptedValueIsInvalid", v]);
                provider.SetNonPropertyUnknownValueIsInvalidAccessor(() => VL["FormatModelState_UnknownValueIsInvalid"]);
                provider.SetUnknownValueIsInvalidAccessor((v) => VL["FormatModelState_NonPropertyUnknownValueIsInvalid", v]);
                provider.SetValueMustNotBeNullAccessor((v) => VL["FormatModelBinding_NullValueNotValid", v]);
                provider.SetValueMustBeANumberAccessor((v) => VL["FormatHtmlGeneration_ValueMustBeNumber", v]);
                provider.SetNonPropertyValueMustBeANumberAccessor(() => VL["FormatHtmlGeneration_NonPropertyValueMustBeNumber"]);
            });
        }
    }

在您的Startup.cs文件中的ConfigureServices方法:

services.AddMvc( ... )
            .AddModelBindingMessagesLocalizer(services, typeof(IDtoModel));

在这里,我使用了我的自定义空的IDtoModel接口,并将其应用于所有需要自动本地化错误和字段名称的API模型。

创建一个名为Resources的文件夹,并在其中放置空的ValidationMessagesResource和FieldNamesResource类。 创建ValidationMessagesResource.ab-CD.resx和FieldNamesResource.ab-CD.resx文件(将ab-CD替换为您所需的区域设置)。 填写您需要的键的值,例如FormatModelBinding_MissingBindRequiredMemberMaxLengthAttribute_ValidationError等。

在从浏览器启动API时,请确保修改accept-languages头部为您的区域设置名称,否则Core将使用默认值。对于只需要单一语言的API,我更喜欢使用以下代码禁用区域提供程序:

private readonly CultureInfo[] _supportedCultures = new[] {
                            new CultureInfo("ab-CD")
                        };

...
var ci = new CultureInfo("ab-CD");

// can customize decimal separator to match your needs - some customers require to go against culture defaults and, for example, use . instead of , as decimal separator or use different date format
/*
  ci.NumberFormat.NumberDecimalSeparator = ".";
  ci.NumberFormat.CurrencyDecimalSeparator = ".";
*/

_defaultRequestCulture = new RequestCulture(ci, ci);


...

services.Configure<RequestLocalizationOptions>(options =>
            {
                options.DefaultRequestCulture = _defaultRequestCulture;
                options.SupportedCultures = _supportedCultures;
                options.SupportedUICultures = _supportedCultures;
                options.RequestCultureProviders = new List<IRequestCultureProvider>(); // empty list - use default value always
            });



它只适用于一个地区,但一旦我更改了地区,它将显示上一个地区的旧值。似乎这些显示和验证值在应用程序的生命周期内被缓存到某个地方,有没有解决这个问题的方法? - Wajdy Essam
@WajdyEssam,您的自定义文本是否都会出现这种情况,还是只有那些 provider.SetValueIsInvalidAccessor 才会出现?另外,为了支持多语言,您应该跳过最后一个带有 RequestLocalizationOptions 的代码,因为它实际上会禁用多语言支持。 - JustAMartin

4
很遗憾,将所有数据属性的错误消息本地化到一个单一的位置并不那么简单!因为有不同类型的错误消息,
标准数据属性的错误消息:
[Required]
[Range]
[StringLength]
[Compare]
...etc.

ModelBinding 的错误信息:

ValueIsInvalid
ValueMustNotBeNull
PropertyValueMustBeANumber
...etc.

身份验证错误信息:

DuplicateEmail
DuplicateRoleName
InvalidUserName
PasswordRequiresLower
PasswordRequiresUpper
...etc

每个都必须在启动文件中进行配置。此外,还必须考虑客户端验证。

您可以查看以下文章以获取更多详细信息,其中包含GitHub上的实时演示和示例项目:

开发多文化Web应用程序: http://www.ziyad.info/en/articles/10-Developing_Multicultural_Web_Application

本地化数据注释: http://www.ziyad.info/en/articles/16-Localizing_DataAnnotations

本地化ModelBinding错误消息: http://www.ziyad.info/en/articles/18-Localizing_ModelBinding_Error_Messages

本地化身份验证错误消息: http://www.ziyad.info/en/articles/20-Localizing_Identity_Error_Messages

以及客户端验证: http://ziyad.info/en/articles/19-Configuring_Client_Side_Validation

希望这有所帮助 :)


0

    public class RequiredExAttribute : RequiredAttribute
    {
        public override string FormatErrorMessage(string name)
        {
            string Format = GetAFormatStringFromSomewhereAccordingToCurrentCulture();
            return string.Format(Format, name);
        }
    }

    ...

    public class MyModel
    {
       [RequiredEx]
       public string Name { get; set; }
    }


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