Click here to Skip to main content
15,861,172 members
Articles / Web Development / ASP.NET

Localization in ASP.NET MVC with Griffin.MvcContrib

Rate me:
Please Sign up or sign in to vote.
4.88/5 (10 votes)
25 Mar 2012LGPL310 min read 129.1K   45   58
Griffin.MvcContrib gives you localization features without any code changes.

Introduction

Griffin.MvcContrib is my contribution project for ASP.NET MVC3 which contains several features. This article will go through the localization features that exists in the framework.

The features consist of the following (which I will go through in turn)

  • Validation localization (Localize validation messages without any property attributes)
  • Model localization (no need for the Display Attribute)
  • View localization 

Background

The default localization method for MVC3 is to specify information in your attributes, which produces code like this:

public class UserViewModel
{
    [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
    [DisplayName(ErrorMessageResourceName = "UserId", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
    [Description(ErrorMessageResourceName = "UserIdDescription", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
    public int Id { get; set; }

    [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
    [DisplayName(ErrorMessageResourceName = "UserFirstName", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
    [Description(ErrorMessageResourceName = "UserFirstNameDescription", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
    public string FirstName { get; set; }

    [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
    [DisplayName(ErrorMessageResourceName = "UserLastName", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
    [Description(ErrorMessageResourceName = "UserLastNameDescription", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
    public string LastName { get; set; }
}  

It makes the code hard to read and you have to repeat it for every single class that you would like to localize.

Googling around a bit, you'll find another way which reduces the amount of configuration, and that is to inherit the default attributes and introduce your own:

public class UserViewModel
{
    [LocalizedRequired]
    [LocalizedDisplayName(ErrorMessageResourceName = "UserId")]
    [LocalizedDescription(ErrorMessageResourceName = "UserIdDescription")]
    public int Id { get; set; }
 
    [LocalizedRequired]
    [LocalizedDisplayName(ErrorMessageResourceName = "UserFirstName")]
    [LocalizedDescription(ErrorMessageResourceName = "UserFirstNameDescription")]
    public string FirstName { get; set; }
 
    [LocalizedRequired]
    [LocalizedDisplayName(ErrorMessageResourceName = "UserLastName")]
    [LocalizedDescription(ErrorMessageResourceName = "UserLastNameDescription")]
    public string LastName { get; set; }
} 

The solution is a bit cleaner and the code duplication is reduced. The problem is that custom validation attributes will make the client side validation stop working since the adapters that MVC uses don't recognize your attributes. This can be fixed by creating mappings between the built-in adapters and your custom attributes.

Localization with Griffin.MvcContrib

The model and validation localization in Griffin.MvcContrib makes your models even cleaner:

C#
public class UserViewModel
{
    [Required]
    public int Id { get; set; }
 
    [Required]
    public string FirstName { get; set; }
 
    [Required]
    public string LastName { get; set; }
} 

That's it. The framework takes care of the rest. That was the initial goal with the framework. The localization features have then grown to also include an administration area where you can manage the translations and translation of view strings.

Model/validation localization using a string table

Let's say that we've created a new ASP.NET MVC3 project and would like to localize the models and their validation messages. For that we'll use nuget to install the core Griffin.MvcContrib package by utilizing the Package Manager Console (Tools -> Library Package Manager -> Package Manager Console) .

Which installs the package. It also installs a readme file in App_ReadMe which contains additional instructions. But let's skip that and continue on.

After installing the package we need to configure the framework and add a string table which will be used for the translations.

Create a string table

  1. Right-click on the project file
  2. Select "Add new item"
  3. Scroll down the list and select "Resources File"
  4. Name it "LocalizedStrings"
  5. Click OK

Configure the framework

We need to replace the built in metadata providers with the ones in the framework. The typical place to do this is in global.asax. We'll also specify that we'll use one string table and its name.

var stringProvider = new ResourceStringProvider(Resources.LocalizedStrings.ResourceManager);
ModelMetadataProviders.Current = new LocalizedModelMetadataProvider(stringProvider);
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new LocalizedModelValidatorProvider(stringProvider)); 

Short description of the used classes:

  • ModelValidatorProviders - An ASP.NET MVC which is used to keep track of all validation providers. The default provider is called DataAnnotationsModelValidatorProvider.
  • ModelMetadataProviders - An ASP.NET MVC class is used to provide metadata to the html helpers and the validator providers.
  • ResourceStringProvider - Used to load string translations from resource files / string tables.
  • LocalizedXxxxxProvider - The providers supplied by Griffin.MvcContrib.

String translation

Each string to be translated should follow the following format ClassName_PropertyName. Let's say that one of our view models look like this:

public class UsersViewModel
{
    public int Age { get; set; }
    [Required]
    public string FirstName { get; set; }
    [Required]
    [StringLength(50)]
    public string LastName { get; set; }
}   

Which means that we should enter the following entries into our string table:

String table with our localized strings

Note that the validation attributes are only entered as they are named (without the Attribute suffix).

The property names are however quite common names, and you'll probably have to duplicate the translation for FirstName several times (one per model). There is a built in solution for that. Replace the class name with "CommonPrompts" instead:

Made the translations available for all view models and added a specialized age prompt for one model

As you might noticed we still have a translation left for the UserViewModel. The framework will always pick a specific model translation before a common one.

Detecting missing translations

The framework uses a class call DefaultUICulture to detect if the default language is active or not. The default language will never show tags for missing prompts (it's assumed that the properties are in the default language). Just change the DefaultUICulture to something other to enable the detection.

Showing how missing texts looks like

The langCode:[] is wrapped around all missing translations.

Using dynamic sources

Using a string table is rather stiff. You can't track changes nor get the strings automatically inserted into the data source for you. Neither can you see where the translation is used. You'll probably have a string table with a lot of unused strings after a while. 

The cure for that is to switch to the localization repositories instead. The framework defines a repository for views and a repository for types.

There are three supported sources included in the framework:

  1. Flat files, uses JSON to store the translations
  2. SqlServer (could easily be adapted to other database engines)
  3. RavenDb (NoSQL database)

Using one of those sources will automatically create the strings in the data source (database/flatfile etc) each time you visit a view (or request a model string) that has not yet been translated. But since the translated text is empty, the missing text detection will still work.

Using SqlServer

I've chosen SqlServer as the data source in this article. The wiki at github shows how to use the other sources.

Do note that the SQL source requires an connection to the database, and we cannot keep one open during the applications lifetime. The SQL repositories do therefore require an inversion of control container which will take care of the lifetime for the repositories and their dependencies.

The following example uses Autofac as the container. Configuring it goes outside the scope of this article (it's done in the demo project)

Install the nuget package.

The first thing to do is to install the nuget package for SqlServer. The package is called griffin.mvccontrib.sqlserver .

Create the database tables.

The SQL script can be found here. Run it from within Visual Studio or the SQL Server Management Console.

Configure a connection string in web.config

A typical connection string:

<add name="DemoDb" connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|LocalizationDb.mdf;User Instance=true" providerName="System.Data.SqlClient" /> 

Configure the framework in global.asax

The following code registers the framework classes in the inversion of control container. It's done in Application_Start()

// Register the framework providers  
ModelValidatorProviders.Providers.Clear();
ModelMetadataProviders.Current = new LocalizedModelMetadataProvider();
ModelValidatorProviders.Providers.Add(new LocalizedModelValidatorProvider()); 

// Loads strings from repositories.
builder.RegisterType<RepositoryStringProvider>().AsImplementedInterfaces().InstancePerLifetimeScope();
builder.RegisterType<ViewLocalizer>().AsImplementedInterfaces().InstancePerLifetimeScope();

// Connection factory used by the SQL providers.
builder.RegisterInstance(new AdoNetConnectionFactory("DemoDb")).AsSelf();
builder.RegisterType<LocalizationDbContext>().AsImplementedInterfaces().InstancePerLifetimeScope();

// and the repositories
builder.RegisterType<SqlLocalizedTypesRepository>().AsImplementedInterfaces().InstancePerLifetimeScope();
builder.RegisterType<SqlLocalizedViewsRepository>().AsImplementedInterfaces().InstancePerLifetimeScope();

Short description of the used classes:

  • builder is the object used to build the Autofac container.
  • RepositoryStringProvider is a Griffin.MvcContrib class which uses the repositories to find all translations
  • ViewLocalizer uses ILocalizedStringProvider (which RepositoryStringProvider implements) to find view translations
  • AdoNetConnectionFactory uses a connection string in web.config to build the ADO.NET connection class.
  • LocalizationDbContext keeps the same connection over an HTTP request
  • SqlLocalizedTypesRepository & SqlLocalizedViewsRepository are SQL server implementations of the localization repository classes.

Any missing strings should now be written into your database (so that you can translate them).

View localization

You can also let the framework take care of the view localization. The only thing you need to do to activate the features is to change base class for the views. It's done in the Views\web.config

<system.web.webPages.razor>
  <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
  <pages pageBaseType="Griffin.MvcContrib.GriffinWebViewPage">

To handle translations you just wrap texts with @T(""). Here is a sample view:

@{
    ViewBag.Title = T("About Us");
}

<h2>@ViewBag.Title</h2>
<p>
    @T("Put content here.")
</p>
<p>
    @T("You can also use {0} formatting!", "string")
</p>
<p>
    @T("And format the {0}.", T("Formatters"))
</p>

Administration

Another tedious task is to handle the translations and to translate texts. There is a built in administration area (still somewhat basic / in development) which you can include in your project.

Background

The administration part is a regular ASP.NET MVC Area. The difference is just that it resides in a class library and you do, therefore, need to reconfigure ASP.NET MVC to be able to locate the views for the area.

This is done with the help of a custom VirtualPathProvider. The problem with this approach is that there may only exist one VirtualPathProvider, which can be problamatic. Fortunally the provider supplied in Griffin.MvcContrib is extandable and allows us to use multiple sources to locate files. Not just the file system or embedded resources. You can find the virtual path provider here.

Authorization

The administration area is using role based authorization and the default roles are named as:

  • Admin - Access to the area
  • Translator - Can translate views and types
  • AccountAdmin - Account management (the account management is not completed yet)

The name of the roles can be changed by changing the values of the properties in the class named GriffinAdminRoles.

Configuring

Start by installing the nuget package griffin.mvccontrib.admin. The go to your global.asax and configure as following:

C#
// you can assign a custom WebViewPage or a custom layout in EmbeddedViewFixer.
var fixer = new EmbeddedViewFixer();
var provider = new EmbeddedViewFileProvider(fixer);
provider.Add(new NamespaceMapping(typeof(MvcContrib.Areas.Griffin.GriffinAreaRegistration).Assembly, "Griffin.MvcContrib"));

GriffinVirtualPathProvider.Current.Add(provider);
HostingEnvironment.RegisterVirtualPathProvider(GriffinVirtualPathProvider.Current);

Short description of the used classes:

  • EmbeddedViewFixer transforms embedded views so that they work as regular views. This means that you do not need to do anything special with them just because they are embedded.
  • EmbeddedViewFileProvider are used to handle views which are embedded in assemblies
  • GriffinVirtualPathProvider is the actual virtual path provider
  • HostingEnvironment is class in ASP.NET used to configure the environment ;)

We also need to tell autofac that it should provide the controllers from the Griffin.MvcContrib.Admin dll. That's achieved like this:

C#
builder.RegisterControllers(typeof (GriffinAdminRoles).Assembly); 

That's everything required and basically how you can create a plugin system with the help of Griffin.MvcContrib.

Administration of types

Types are different from view translations in the matter that they got a lot of meta data. MVC3 allows you to specify a description, watermark, null display text and more. These metadata strings are hidden in the administration area by default but can be shown by toggling the checkbox.

Screenshot from translating a prompt: 

Type translation

View translations

The view translation works just like the type translation, except that entire paragraphs are translated at once and that they support string formatting (as in string.Format()) .

View translation

Export translations

You might have a test/dev system which all translations are made and verified in. Then you probably want to get those translations to the production system too. This is possible with the help of the export/import features. 

You start by filtering out the views (or types) that you want to export:

Filtering

And then press the "Preview" button to see which prompts you get:

Preview result

Press "Create" when satisfied and you'll get prompted to download a JSON file with all translations:

Save as

Importing translations

Quite easy. Simply upload the JSON file. All existing prompts will be replaced and all new prompts will be inserted.

Translated prompts

Few tips

The following sections contains a few tips which can help you in the localization process.

Selecting language

The framework has a built in action filter which can select the language for you. The setting is kept in a cookie, so it will work as long as the user allows cookies. The only thing you have to do to change the language is to add a link which includes ?lang=sv-se in the query string. It's picked up by the action filter.

The action filter itself should decorate your base controller:

C#
[Localized]
public class BaseController : Controller
{
}
       

Caching

Caching of the messages is not built into the framework. I do however recommend that you implement caching for sites which high traffic. The easiest way to do it is to subclass RepositoryStringProvider and ViewLocalizer like this:

C#
// Caching view texts
public class CachedViewLocalizer : ViewLocalizer
{
    MyCacheClass _cache;
    
    public override string Translate(RouteData routeData, string text)
    {
        string prompt;
        if (_cache.TryGetValue(routeData, text, out prompt)
            return prompt;
        
        prompt = base.Translate(routeData, text);
        _cache.Insert(routeData, text, prompt);
        return prompt;
    }
}

// caching type translations
public class CachedTypeLocalizer : RepositoryStringProvider 
{
    MyCacheClass _cache;
    
    public override string Translate(Type type, string name)
    {
        var promptName = type.FullName + "." + text;
        
        string prompt;
        if (_cache.TryGetValue(promptName, out prompt)
            return prompt;
        
        prompt = base.Translate(type, name);
        _cache.Insert(promptName, prompt);
        return prompt;
    }
}

Finnally register your implementations in the IoC container. 

Localizing Views/_layout.cshtml

The localization framework uses the controller/action as the base for each translation (so that you can have the same phrase, but with different meanings). This works great for the most time.
However, since the framework can't tell if the text to translate is in your layout or view you'll get the layout prompts for all pages.

The simple solution is to visit one page, then go to the admin area and translate all layout prompts and push them as common prompts.

Feel free to leave a comment if you got a better solution (which also works with areas).

Points of Interest

I stumbled upon an amazing article about the extension points in ASP.NET MVC3 written by Brad Wilson. It's a must read. 

Final words

As you might have noticed, English is not my native language. I do hope that you have enjoyed the article and the framework that it describes.

Griffin.MvcContrib also have a set of HTML Helpers which are extendable and that let you modify the HTML tags before they are outputted into the HTML.

There are also a membership provider which uses inversion of control (service location) to locate it's dependencies. It makes the process of writing a custom membership provider a whole lot easier.

Please post all bugs and feature requests at github instead of leaving them as comments here.

History

  • 2012-03-23 First version of the article

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Founder 1TCompany AB
Sweden Sweden

Comments and Discussions

 
BugError running SQL Server demo Pin
KSig23-Jul-12 23:15
KSig23-Jul-12 23:15 
GeneralRe: Error running SQL Server demo Pin
jgauffin13-Aug-12 19:17
jgauffin13-Aug-12 19:17 
QuestionFluent Validation localization Pin
kelvin199713-Jul-12 6:05
kelvin199713-Jul-12 6:05 
SuggestionLocalization of views Pin
Daniel Schiavini14-Jun-12 23:01
Daniel Schiavini14-Jun-12 23:01 
GeneralRe: Localization of views Pin
Daniel Schiavini15-Jun-12 2:14
Daniel Schiavini15-Jun-12 2:14 
GeneralRe: Localization of views Pin
jgauffin13-Aug-12 3:59
jgauffin13-Aug-12 3:59 
GeneralRe: Localization of views Pin
jgauffin16-Aug-12 21:19
jgauffin16-Aug-12 21:19 
QuestionAutofac really necessary? Pin
Daniel Schiavini14-Jun-12 2:09
Daniel Schiavini14-Jun-12 2:09 
Hello,

This looks like the best solution I could find until now.
However it feels like a lot to install 3 packages (MvcContrib, Sql and Autofac) just for localization.
Is Autofac really necessary? Can't I provide the connection via the entity framework like our other classes?

Thanks
AnswerRe: Autofac really necessary? Pin
jgauffin14-Jun-12 2:15
jgauffin14-Jun-12 2:15 
GeneralRe: Autofac really necessary? Pin
Daniel Schiavini14-Jun-12 3:56
Daniel Schiavini14-Jun-12 3:56 
Questionhow do i use the package? Pin
AceBalasador10-Jun-12 7:29
AceBalasador10-Jun-12 7:29 
AnswerRe: how do i use the package? Pin
jgauffin10-Jun-12 7:36
jgauffin10-Jun-12 7:36 
GeneralRe: how do i use the package? Pin
AceBalasador10-Jun-12 13:27
AceBalasador10-Jun-12 13:27 
GeneralRe: how do i use the package? Pin
AceBalasador10-Jun-12 13:51
AceBalasador10-Jun-12 13:51 
GeneralRe: how do i use the package? Pin
jgauffin10-Jun-12 18:23
jgauffin10-Jun-12 18:23 
QuestionWhat about several languages? Pin
Max Pavlov18-Apr-12 11:07
Max Pavlov18-Apr-12 11:07 
AnswerRe: What about several languages? Pin
jgauffin18-Apr-12 21:27
jgauffin18-Apr-12 21:27 
GeneralGreat! Pin
Gustav Brock26-Mar-12 2:48
professionalGustav Brock26-Mar-12 2:48 
GeneralRe: Great! Pin
KSig23-Jul-12 22:24
KSig23-Jul-12 22:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.