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:

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

Right-click on the project file

Select "Add new item"

Scroll down the list and select "Resources File"

Name it "LocalizedStrings"

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.

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

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:

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.

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:

Flat files, uses JSON to store the translations

SqlServer (could easily be adapted to other database engines)

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.

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 customVirtualPathProvider. The problem with
this approach is that there may only exist oneVirtualPathProvider, 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:

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:

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:

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()) .

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:

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

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

Importing translations

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

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:

[Localized]
publicclass 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:

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.

Comments and Discussions

I have used your test project from which to base my implementation. I am still a little confused on how to implement the localization provider in my controlers (I would like to implement it in a base controller and inherit from the base the localization object in the child controllers if it is possible). I really would find this frame work extremely useful if I could just figure this out.

I ran the demo straight out of the box (err.. zip) and got the following error , when I go to the admin section with Swedish selected as the language.

Quote:

one of the constructors found with 'Public binding flags' on type 'Griffin.MvcContrib.Areas.Griffin.Controllers.AccountAdminController' can be invoked with the available services and parameters:
Cannot resolve parameter 'Griffin.MvcContrib.Providers.Membership.IAccountRepository repository' of constructor 'Void .ctor(Griffin.MvcContrib.Providers.Membership.IAccountRepository)'.

Maybe it's supposed to do this, I have not yet worked trough the whole article yet..

My idea is (try) not to validate and spit messages twice at data layer and UI layer. Could you please give me a quick pointer (if it exists) how we can integrate Griffin Localization with Fluent Validation at data layer? I am using minimum if not none of the mvc validation attributes.

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?

The LocalizationDbContext[^] needs to be refactored so that the connection property is virtual (so that the implementation can either fetch the current http request's connection or create a new one)

Edit

Oopps. I was incorrect. You need to implement ILocalizationDbContext[^]. Either return a new connection for each request (inefficient) or store the connection in HttpContext.Current.Items to be able to reuse it for all localization strings.

The down side with not using a container is that the connections won't be disposed until the HttpContext is garbage collected (thus making the ILocalizationDbContext unreferences = available for GC). You can of course implement a IHttpHandler which disposes the connection.

i followed the instructions and installed the package via NUGET. when i try to use the custom annotations/ attributes in my model i get a "type or namespace missing/ required" error.

i tried importing ALL the librarries in the GRIFFIN package and still doenst get to compile

using Griffin;
using Griffin.MvcContrib;
using Griffin.MvcContrib.Localization;
using Griffin.MvcContrib.Localization.FlatFile;
using Griffin.MvcContrib.Localization.Types;
using Griffin.MvcContrib.Localization.Views;
using Griffin.MvcContrib.Caching;
using Griffin.MvcContrib.Html.Generators;
using Griffin.MvcContrib.Json;
using Griffin.MvcContrib.Logging;
using Griffin.MvcContrib.Plugins;
using Griffin.MvcContrib.Providers.Membership.PasswordStrategies;
using Griffin.MvcContrib.Providers.Membership.SqlRepository;
using Griffin.MvcContrib.Providers.Profile;
using Griffin.MvcContrib.Providers.Roles;
using Griffin.MvcContrib.VirtualPathProvider;