Forum: Multi-Language Add-In for Visual Studio

Additional info (hope you find it useful):

Above was introduced to abstract away the ResourceManager (which MLString is currently based on) and not to replace ResourceManager. See following from docs.asp.net:

Make the app's content localizable

  • Introduced in ASP.NET Core, IStringLocalizer and IStringLocalizer were architected to improve productivity when developing localized apps. IStringLocalizer uses the ResourceManager and ResourceReader to provide culture-specific resources at run time.
Germany

Hi Nick,

I will certainly look into it. Normally, it makes sense for me to use the methods which Microsoft provide.

First I want to look into supporting Visual Studio 2017.

Phil


Hey Phil,

How is it looking for Visual Studio 2017 support? :-)

Definitely something on our list as well ;)

Thanks in advance.

Germany

Hi Nick,

I am sure that you know a lot more about MVC applications than I do. Maybe you can help my understanding of how to use the IStringLocalizer interface.

I understand how to use it in a class that derives from Controller:

  • add an IStringLocalizer parameter to the constructor
    (it will be passed in via dependency injection)
  • store a reference to it in a member _localizer
  • get the localized string using _localizer["original string"]


What I don't understand, is how to handle strings in classes which do not derive from Controller. Sure, I could add an IStringLocalizer (or maybe IStringLocalizerFactory) parameter to the constructor, but then whoever creates the class will have to provide it.

I can't find any examples about how to the new MVC localization method in other classes.

Because I am not really an MVC programmer, I think I am missing something. Is it possible that MVC applications just don't contain other classes (not deriving from Controller)? That is hard to believe.

At present, it looks like I will be able to support IStringLocalizer in Controller classes, but fall back to ml_string or named resources in other classes.

Phil

Germany

I have now understood a lot more about working with dependency injection, after asking (and then answering) a question on Stack Overflow.

I am still thinking about what to do if the user selects a string for translation which is a class which is not registered with the DI container. There are two basic options:

  • always use IStringLocalizer
  • fall back to named resources (or ml_string())


Consistently using IStringLocalizer is a better solution, but it requires changes to the code. I can do some of these automatically:

  • define a _localizer variable
  • add a parameter to the constructer
  • even register the class as a service in the startup class


I don't think I can automatically find where the class is instantiated with the new operator and replace it with Dependency Injection. (Actually, that would be quite a clever refactoring.)

Phil

Hi Phil,

Sorry if I'm pushing it, but have you been thinking more about how you would like to support .NET Core in the future?


Germany

I have finally uploaded an 'official' version with support for IStringLocalizer, 6.02.0006. I still have some open questions, but this is how it works at present.

When you add Multi-Language support to an ASP.NET Core project, there is a new option for localizing strings in the source code: Use IStringLocalizer

Image

This option is also available in the project options (5th button on the toolbar).

Image

IStringLocalizer support applies to Controllers classes and to Views. In principle you can use IStringLocalizer in other classes, but this is not yet supported.

Lets look first at a controller class. Here is the code from the HomeController class in a new project:

HomeController - Original version
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace WebApplication4.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        public IActionResult About()
        {
            ViewData["Message"] = "Your application description page.";

            return View();
        }

        public IActionResult Contact()
        {
            ViewData["Message"] = "Your contact page.";

            return View();
        }

        public IActionResult Error()
        {
            return View();
        }
    }
}


If I select the string "Your application description page." for translation, the code is changed as shown below:

HomeController - Modified version
using Microsoft.Extensions.Localization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace WebApplication4.Controllers
{
    public class HomeController : Controller
    {
        private readonly IStringLocalizer<HomeController> _localizer;

        public HomeController(IStringLocalizer<HomeController> localizer)
        {
          _localizer = localizer;
        }

        public IActionResult Index()
        {
            return View();
        }

        public IActionResult About()
        {
            ViewData["Message"] = _localizer["Your application description page."];

            return View();
        }

        public IActionResult Contact()
        {
            ViewData["Message"] = "Your contact page.";

            return View();
        }

        public IActionResult Error()
        {
            return View();
        }
    }
}


Four changes have been made to the file:

  • a using statement for the namespace Microsoft.Extensions.Localization has been added
  • a member variable _localizer has been added to the class
  • a constructor has been added to the class
  • the original text has been replaced with _localizer["Your application description page."]


Obviously, the first three steps are only required when the first text is selected.

The constructor receives the localizer object as a parameter, and stores it in the member variable _localizer. If the class already has a constructor, then the additional parameter and the assignment statement are added to the existing constructor.

For this to work, Multi-Language also makes changes to the Startup class.

Firstly it adds a using statement for the namespace Microsoft.AspNetCore.Mvc.Razor (if not already present).

It them makes changes to the method ConfigureServices as shown below:

ConfigureServices - Before
// 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();
}

ConfigureServices - After
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalization(options => options.ResourcesPath = "Resources");
            
    // Add framework services.
    services.AddMvc()
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix,opts => opts.ResourcesPath = "Resources" )
        .AddDataAnnotationsLocalization();
}


This is basically boilerplate code required to make the new localization mechanism work.

Finally, Multi-Language adds a resource file to the project, as shown in the solution explorer below.

Image

All resource files are generated in the "Resources" directory (which is specified as the path in the ConfigureServices function). The directory and filename is derived from the full class-name of the controller class, without the root namespace.

Now lets look at support in a razor view class. Here is the source code of the about view in a new project:

About.cshtml - Original version
@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>Use this area to provide additional information.</p>


If I select the text "About" for translation, the code is modified as shown below:

About.cshtml - Modified version
@using WebApplication4.App_GlobalResources;
@using Microsoft.AspNetCore.Mvc.Localization;
@inject IViewLocalizer Localizer;
@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>Use this area to provide additional information.</p>


Three changes have been made to the file:

  • a using statement for the class Microsoft.AspNetCore.Mvc.Localization has been added
  • an inject statement has been added, which defines the variable Localizer
  • the original text has been replaced with Localizer["About"]


The first two steps are only performed when the first string is selected.

Multi-Language adds a separate resource file to the project for the view resources, as shown in the solution explorer below.

Image


By the way, the resources for both controllers and views are also shown in the resources tab. They can be edited in the resources tab, but not deselected.

Image

A little known feature of Named-Resource support is that you can edit the resource name in the resources tab (in the name column). That should also be possible with IStringLocalizer support.

That's my initial introduction. I will probably add more details later.

Phil

Germany

In Version 6.02.0008, I have added support for IStringLocalizer in classes which are not Controller classes.

The first time you select a string in a class, you are given a choice whether to localize it using IStringLocalizer or using Named-Resources.

If you choose to use IStringLocalizer, Multi-Language will add a _localizer variable and add a parameter to the constructor. However, you will have to make sure that the object is created via the dependency injection container. If you create the class using the new operator, then the mechanism will not work.

My understanding of how this works is described in this Stack Overflow question.

I am guessing that some programmers will continue create some objects using the new operator, in which case they will have to fall back to the Named-Resources mechanism.

Phil


Hi Phil,

First: Waow this looks great! You are on the right track and I like how you approach this! biggrin

As I understand you now support 3 scenarios - I would like to request that you support 4:

  1. Views (obviously required)
  2. Controllers (not required in theory, but unfortunately it turns out to be required in practice)
  3. Classes (similar to controllers, but also covers Models, Services, ...)
  4. NEW: DataAnnotation Localization (Attributes) idea


Models are commonly using attributes for validation - and DataAnnotation Localization looks very promising compared to how I register named resources in model's attributes today (using your tool and named resources). Sample Model AS IS.

RequiredAttribute, StringLengthAttribute etc. all inherit from ValidationAttribute. This is commonly used (by Microsoft) in ASP.NET MVC to handle user inputs validation:

  1. Validating input client side (javascript)
  2. Validating input server side (c#)
  3. Show single validation error next to input
  4. Show summary of all validation errors above/below all inputs


Named resources (AS IS)
To help me manage attribute localization today, I've actually created a shadow cshtml page containing all the validation texts I know of eek (copied a few months back from the open source project). I use this file as following: (oh yes - a manual process that I would love to skip)

  1. Identify need for new validation type
  2. Find validation text in shadow cshtml page
  3. Copy new error text and assign to variable in same file
  4. Scan with your tool (so it becomes a named resource)
  5. Copy the newly created resource name
  6. Navigate to the Model where it's needed
  7. Use the named resource


So with named resources I have to assign the resource to be used in every single Model (even though I literally always use the same string for e.g. "Required").

IStringLocalization (TO BE) vs SharedResource
But with IStringLocalization, your tool might be able to setup those shared strings in Startup.cs once - and then ASP.NET MVC will just work in any Model using the e.g. "Required" attribute without any further setup in the Model.

  1. Sample Model TO BE
  2. Sample Model AS IS (to compare)


With all the default error messages that might be used (and all the other DataAnotation scenarios I properly don't know about yet), your tool might be able to offer a whole lot of value to an ASP.NET MVC Core developer!

Unfortunately, I don't see how this would naturally fit into the tool today. As I understand your tool:

  1. Tool assumes it can scan code and find the actual string to be localized
  2. Then it modifies code where string was found, to enable localization


Suggested way to support "SharedResource"
To fully support a "SharedResource" scenario, the tool might be better off adding a new sheet/tab called "Shared Resources" (like we have "ASP / HTML", "Source code" and "Resources" today) to handle this globally. However to avoid showing a long list of validation strings (including those not in use), this will require your tool to scan for Attribute Type (or ValidationAttribute Type) usage (instead of strings) and from the types found, decide which strings are relevant for the user to translate in "Shared Resources".

I know this is more tricky than the DI approach, but I also think this could again set your tool apart from your competitors.

Please let me know what you think about above. Thanks for reading all of that :-)

Nick


Germany

Hi Nick,

sorry I didn't reply in the last two months, but finally this is something I want to move ahead on.

I knew I still had to do something for DataAnnotations, but I hadn't looked at it in detail.

Aside from you suggestions, it looks fairly straight forward. If an attribute inherits from ValidationAttributes, then the parameters are localizable. Multi-Language should detect them, let you define localized texts and store them in the appropriate resource file (I think in the subdirectory ViewModels).

If I understand correctly, you want to go a step further and use standardized texts, without specifying them for each attribute, e.g. instead of
[Required(ErrorMessage = "Required")]
you would just like to write
[Required]

Couldn't you achieve this by deriving your own attribute, e.g. MyRequred, from the Required attribute class?

I will try to make an example and post it later.

Phil

Germany

Hi Nick,

this is what I meant with a derived class

Attribute classes
using System.ComponentModel.DataAnnotations;

namespace WebApplication2
{
  public class StandardRequiredAttribute : RequiredAttribute
  {
    public StandardRequiredAttribute()
    {
      ErrorMessage = "This attribute is required." ;
    }
  }


  public class StandardStringLengthAttribute : StringLengthAttribute
  {
    public StandardStringLengthAttribute()
      : base ( 45 )
    {
      ErrorMessage = "This field must be a string with a maximum length of 45 characters." ;
    }
  }

}

Usage
namespace WebApplication2
{
  public class User
  {
    public User()
    {
    }

    [StandardRequired]
    [StandardStringLength]
    public string Name { get; set; }
  }
}


Wouldn't that meet your requirements?

Phil


Your suggestion for "MyRequiredAttribute" is valid for sure! And it might work as well (not tested). This is definitely something I will test in the future :-) I'm pretty sure I tried something similar without luck, but I'll try again with your code. You know much more about localization than I do, so maybe I overlooked a barrier when I tried something similar, which you actually fixed in your suggestion. I'm not too sure how it will handle current culture vs the error message defined like you suggest, but again it might work and needs to be tested :-)

The reason for my suggestion would be, to ease localization for the developer using your tool. The idea would be to make it even easier and more intuitive to even notice the standard strings required to be localized. With all due respect, your suggested solution will still require each user to know about the localized strings hidden behind an attribute. User will still have to lookup the english default text in source code (aspnet github) or extract through running their own application and enforcing the error (with the risk of not understanding how parameters should be added etc). It's a fairly long and error prone manual process that you don't know about as a developer (that's why we buy your tool basically ;) because we don't want to deal with such things).


Also I was watching the ASP.NET Live Community Standup meeting yesterday, where they talked about localization.

They are referring to a blog post about how to store localization in json files and why to use json and how to read with speed (part 2).

To be honest I can't evaluate how much of this can be of use to you, but I didn't know about RequestCultureProvider and how to modify it to use the Configuration builder to load (and reload) a json file for localization.

I really buy into the point about compiling resx files as a big drawback. I've experienced resx files to slow down compile process significantly with older ASP.NET projects (with e.g. +20 languages and +100 pages with text). Slow compile times leads to slower testing, releasing etc.

And I also hate the fact that I need to recompile and release my project to change e.g. just a single string in the view. Out of the box without localization it is possible to change "View" strings without recompiling - because that's what the user/designer working with the "View" expects.. A designer shouldn't know too much about C# and compiling..

Well I just found it interesting and relevant to mention in this context. Maybe you could even eventually get mentioned in their weekly show - because you are somewhat an expert in the localization field and trying to make the best .NET Core 2.0 implementation (I assume).