4

I have the following View Model:

public class LoginViewModel
    {
        [DisplayName("Email Address")]
        [Required(ErrorMessage = "PleaseEnterYourEmail")]
        public string EmailAddress { get; set; }

    }

I have the following resource file named: DataAnnotation.Localization.de-DE.resx, this is inside the App_LocalResources folder

App_LocalResources

Resource file

With the following properties:

Properties

Now according to the blog post announcing.net 4.6.2 this should just work, being that I should get the localised version of my message returned to the view.

However it's just showing:

View

I've checked my current culture and it's set to: de-DE so the app is aware of the language it needs to show. The Target framework is 4.6.2 as well.

Is there something I'm missing here?

Ryan McDonough
  • 9,732
  • 3
  • 55
  • 76
  • you use an asp.net project? Since the improvements refers to ASP.NET – Jehof Oct 11 '16 at 12:39
  • @Jehof Yes it's an ASP.NET project. – Ryan McDonough Oct 11 '16 at 12:43
  • How are you setting culture? – jle Oct 11 '16 at 12:57
  • @jle setting the culture with either the users current culture or we override it. However when I get the culture whilst on the view it is coming back as de-DE, so it should match the request I'm making. – Ryan McDonough Oct 11 '16 at 13:15
  • Make sure you have a Default.resx and make sure they are in the proper folder. Please post your culture setting code or config (though it sounds like that isn't the issue) – jle Oct 11 '16 at 13:17
  • @jle I've included a screenshot of the App_LocalResources folder, I don't have a default.resx, though I wouldn't expect the Data Annotations to be picking up from there? – Ryan McDonough Oct 11 '16 at 13:25
  • 2
    This feature is added for WebForm in .net framework 4.6.2. You can't use it in asp.net core project or MVC project. – mattfei Nov 15 '16 at 23:01

2 Answers2

2

Finally I found a solution for this problem.

Here I will share my solution for others who are having the same problem.

Add this class to your asp.net core application:

using System;
using Microsoft.Extensions.Localization;

namespace App.Utilities
{
    public static class StringLocalizerFactoryExtensions
    {
        public static IStringLocalizer CreateConventional<T>(this IStringLocalizerFactory factory)
        {
            return factory.CreateConventional(typeof(T));
        }

        public static IStringLocalizer CreateConventional(this IStringLocalizerFactory factory, Type type)
        {
            if (type.Module.ScopeName != "CommonLanguageRuntimeLibrary")
            {
                string[] parts = type.FullName.Split(new[] { type.Assembly.FullName.Split(',')[0] }, StringSplitOptions.None);

                string name = parts[parts.Length - 1].Trim('.');

                return factory.CreateConventional(name);
            }
            else
            {
                return factory.Create(type);
            }
        }

        public static IStringLocalizer CreateConventional(this IStringLocalizerFactory factory, string resourceName)
        {
            return factory.Create(resourceName, null);
        }

        public static IStringLocalizer CreateDataAnnotation(this IStringLocalizerFactory factory)
        {
            if (type.Module.ScopeName != "CommonLanguageRuntimeLibrary")
            {
                return factory.Create("DataAnnotation.Localization", "App_LocalResources");
            }
            else
            {
                return factory.Create(type);
            }
        }
    }
}

... and in your Startup.cs file replace the following part:

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

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

... with this code:

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

services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
//The following part includes the change:
.AddDataAnnotationsLocalization(options => options.DataAnnotationLocalizerProvider = (type, factory) => factory.CreateConventional(type));

The code is treating your view model localization resources like the one used for views or any other place where the default IStringLocalizerFactory can be used.

Therefore, no more DataAnnotation.Localization.de-DE.resx resources and App_LocalResources folder are needed.

  • Just, create a series of resource files with the conventional naming (Models.AccountViewModels.RegisterViewModel.en-US.resx or Models/AccountViewModels/RegisterViewModel.sv-SE.resx in the Resources folder which is set by calling services.AddLocalization(options => options.ResourcesPath = "Resources")) and you are ready to go. The TagHelpers and HtmlHelpers will start working and translating the error messages.

  • Also, this will work for DisplayAttribute.Name out of the box. (v1.1.0-preview1-final + .net v4.6.2)

Update 1: Here is my project.json:

{
  "userSecretsId": "...",

  "dependencies": {
    "Microsoft.NETCore.Platforms": "1.1.0-preview1-*",
    "Microsoft.AspNetCore.Authentication.Cookies": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.Diagnostics": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.DataProtection": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.Mvc": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview3-final",
    "Microsoft.ApplicationInsights.AspNetCore": "1.0.2",
    "Microsoft.AspNetCore.Mvc.Localization": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.Mvc.Razor": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.Mvc.TagHelpers": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.Mvc.DataAnnotations": "1.1.0-preview1-final",
    "Microsoft.Extensions.Configuration.CommandLine": "1.1.0-preview1-final",
    "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0-preview1-final",
    "Microsoft.AspNet.WebApi.Client": "5.2.3",
    "Microsoft.AspNetCore.Routing": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.Server.Kestrel": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.StaticFiles": "1.1.0-preview1-final",
    "Microsoft.EntityFrameworkCore": "1.1.0-preview1-final",
    "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.1.0-preview1-final",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0-preview1-final",
    "Microsoft.Extensions.Configuration.Json": "1.1.0-preview1-final",
    "Microsoft.Extensions.Configuration.UserSecrets": "1.1.0-preview1-final",
    "Microsoft.Extensions.Logging": "1.1.0-preview1-final",
    "Microsoft.Extensions.Logging.Console": "1.1.0-preview1-final",
    "Microsoft.Extensions.Logging.Debug": "1.1.0-preview1-final",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0-preview1-final",
    "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0",
    "Microsoft.VisualStudio.Web.CodeGeneration.Tools": "1.0.0-preview3-final",
    "Microsoft.VisualStudio.Web.CodeGenerators.Mvc": "1.0.0-preview3-final",
    "Microsoft.AspNetCore.Hosting": "1.1.0-preview1-final",
    "Microsoft.AspNetCore.Hosting.WindowsServices": "1.1.0-preview1-final",
    "Loggr.Extensions.Logging": "1.0.0",
    "Microsoft.EntityFrameworkCore.SqlServer": "1.1.0-preview1-final",
    "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview3-final",
    "BundlerMinifier.Core": "2.2.296"
  },
  "tools": {
    "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview3-final",
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview3-final",
    "Microsoft.EntityFrameworkCore.Tools.DotNet": "1.0.0-preview3-final",
    "Microsoft.Extensions.SecretManager.Tools": "1.0.0-preview3-final",
    "Microsoft.VisualStudio.Web.CodeGeneration.Tools": {
      "version": "1.0.0-preview3-final",
      "imports": [
        "portable-net45+win8"
      ]
    }
  },
  "frameworks": {
    "net462": {}
  },
  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },
  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },
  "publishOptions": {
    "include": [
      "wwwroot",
      "**/*.cshtml",
      "appsettings.json",
      "web.config"
    ]
  },
  "scripts": {
    "prepublish": [ "bower install", "dotnet bundle" ],
    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}

Update 2: In case someone wants the code to just work as promised with the DataAnnotation.Localization in the App_LocalResourses folder, I have updated the StringLocalizerFactoryExtensions code. Use the updated class and the following code in Startup.cs class instead and it should work.

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

services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
//The following part includes the change:
.AddDataAnnotationsLocalization(options => options.DataAnnotationLocalizerProvider = (type, factory) => factory.CreateDataAnnotation());
Rojan Gh.
  • 1,062
  • 1
  • 9
  • 32
2

New DataAnnotations localization feature, as described in blog post announcing .NET Framework 4.6.2 works out of the box only for ASP.NET WebForms.

WebForms...

New localization feature in .NET Framework 4.6.2 is implemented in the class System.Web.ModelBinding.DataAnnotationsModelValidator, which uses StringLocalizerProviders.DataAnnotationStringLocalizerProvider.GetLocalizedString to resolve localized string.

System.Web.Mvc.DataAnnotationsModelValidator is set by default to System.Web.Globalization.ResourceFileStringLocalizerProvider. This provider tries to find "App_LocalResources.root" dll in Temporary ASP.NET Files folder for specified website.

However, there is a pre-check if feature should be used at all:

private bool UseStringLocalizerProvider {
    get {
        // if developer already uses existing localization feature,
        // then we don't opt in the new localization feature.
        return (!string.IsNullOrEmpty(Attribute.ErrorMessage) &&
            string.IsNullOrEmpty(Attribute.ErrorMessageResourceName) &&
            Attribute.ErrorMessageResourceType == null);
    }
}

Above check means that new localization feature will work only in case like:

[Required(ErrorMessage = "FirstName is required")]
public string FirstName { get; set; }

where ErrorMessage value is set. For most common use case:

[Required]
public string FirstName { get; set; }

it falls back to legacy name resolution.

In MVC 5 feature does not work because...

MVC 5 uses it's own, specific, implementation, System.Web.Mvc.DataAnnotationsModelValidator. This implementation originates from Microsoft.AspNet.Mvc version 5.x.x and it pre-dates .NET Framework 4.6.2. It does not implement new localization feature.

On top of that, ASP.NET dynamic compilation output for MVC and WebForms differs, so compiled resource used by WebForms (ex: de\App_LocalResources.root.q_wjw-ce.resources.dll") does not even exist for App_LocalResources of ASP.NET MVC application.

This difference of compilation output between MVC and WebForms excludes possibility to write wrapper around WebForms implentation and use it "as is" in MVC application.

MVC 6 works, but slightly differently...

MVC 6 uses 3rd implementation, Microsoft.AspNetCore.Mvc.DataAnnotations.Internal.DataAnnotationsModelValidator. This implementation accepts IStringLocalizer stringLocalizer as constructor parameter.

Default localization configuration can be added in Startup.cs as:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc()
        .AddDataAnnotationsLocalization();
}

And needed request localization in Startup.cs Configure(...) method, for example:

// Configure the localization options
app.UseRequestLocalization(new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture(new CultureInfo("de-AT")),
    SupportedCultures = new List<CultureInfo>
    {
        new CultureInfo("de")
    },
    SupportedUICultures = new List<CultureInfo>
    {
        new CultureInfo("de-AT")
    }
});

If we create ViewModel:

using System.ComponentModel.DataAnnotations;

namespace WebApplication1.Models
{
    public class User
    {
        [Required(ErrorMessage = "First name is required.")]
        public string FirstName { get; set; }
    }
}

We have to add Models.User.{culture}.resx file in the root of WebApplication, with a key "First name is required." and localized validation-error message.

Model.User.resx file.

Even though MVC 6 has different implementation, same condition applies for ValidationAttribute as in WebForms. ErrorMessage has to be defined, while ErrorMessageResourceName and ErrorMessageResourceType should not be used.

Community
  • 1
  • 1
Nenad
  • 24,809
  • 11
  • 75
  • 93
  • I'm just wondering where you discovered that it doesn't work for MVC 5? I can't see that in the blog post? – Ryan McDonough Nov 18 '16 at 15:05
  • @RyanMcDonough, I added more explanation for MVC 5, but simply, if you look at the source code of MVC implementation, parts of code for this new feature are not there at all. – Nenad Nov 19 '16 at 12:29