1
\$\begingroup\$

Following Nadeem Afana's article about how to include internationalization into an MVC application, I have introduced i18n into one of my MVC applications.

These are my goals:

  1. Use strongly typed resources
  2. Allow to easily change existing resources strings without application redeployment (i.e. at runtime)
  3. Easy to use in the code

Created table to hold resources (database first approach)

CREATE TABLE dbo.Resource
(
    ResourceId INT NOT NULL IDENTITY(1, 1) CONSTRAINT PK_Resource PRIMARY KEY,
    Culture VARCHAR(10) NOT NULL,
    Name VARCHAR(100) NOT NULL,
    Value NVARCHAR(4000) NOT NULL,
    CONSTRAINT UQ_Resource_Culture_Name UNIQUE (Culture, Name)
)
GO

For every culture supported culture, a resource name and value pair is defined.

Created a special Resource project for resource information data fetch.

I have used project Nadeem Afana's NuGet package and customized database fetch:

public interface IDbResourceProvider
{
    object GetResource(string name, string culture);
    object GetResource(string name);
}

public class DbResourceProvider : BaseResourceProvider, IDbResourceProvider
{
    private IUnitOfWork _UnitOfWork { get; set; }

    public DbResourceProvider(IUnitOfWork unitOfWork)
    {
        _UnitOfWork = unitOfWork;
    }

    protected override IList<ResourceEntry> ReadResources()
    {
        var retList = _UnitOfWork.ResourceRepository.AllNoTracking
            .Select(r => new ResourceEntry() { Name = r.Name, Value = r.Value, Culture = r.Culture })
            .ToList();

        return retList;
    }

    protected override ResourceEntry ReadResource(string name, string culture)
    {
        var entry = _UnitOfWork.ResourceRepository.AllNoTracking
            .FirstOrDefault(r => r.Culture == culture && r.Name == name);

        if (entry == null)
            throw new Exception(string.Format("Resource {0} for culture {1} was not found", name, culture));

        return new ResourceEntry() { Name = entry.Name, Value = entry.Value, Culture = entry.Culture };
    }
}

Resource builder is slightly changed to allow provider setting (SetProvider method is added to the generated code):

        public string Create(BaseResourceProvider provider, string namespaceName = "Resources", string className = "Resources", string filePath = null, string summaryCulture = null)
        {
            // Retrieve all resources           
            MethodInfo method = provider.GetType().GetMethod("ReadResources", BindingFlags.Instance | BindingFlags.NonPublic);

            IList<ResourceEntry> resources = method.Invoke(provider, null) as List<ResourceEntry>;

            if (resources == null || resources.Count == 0)
                throw new Exception(string.Format("No resources were found in {0}", provider.GetType().Name));

            // Get a unique list of resource names (keys)
            var keys = resources.Select(r => r.Name).Distinct();

            #region Templates
            const string header =
@"using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Resources.Abstract;
using Resources.Concrete;

namespace {0} {{
        public class {1} {{
            private static IResourceProvider resourceProvider;

            public static void SetProvider(IResourceProvider provider)
            {{
                resourceProvider = provider;
            }}

    {3}
        }}        
}}"; // {0}: namespace {1}:class name   {2}:provider class name   {3}: body  

            const string property =
@"
        {1}
        public static {2} {0} {{
               get {{
                   return ({2}) resourceProvider.GetResource(""{0}"", CultureInfo.CurrentUICulture.Name);
               }}
            }}"; // {0}: key
            #endregion


            // store keys in a string builder
            var sbKeys = new StringBuilder();

              foreach (string key in keys) {
                  var resource = resources.Where(r => (summaryCulture == null ? true : r.Culture.ToLowerInvariant() == summaryCulture.ToLowerInvariant()) && r.Name == key).FirstOrDefault();
                  if (resource == null) {
                      throw new Exception(string.Format("Could not find resource {0}", key));
                  }

                sbKeys.Append(new String(' ', 12)); // indentation
                  sbKeys.AppendFormat(property, key, 
                      summaryCulture == null ? string.Empty: string.Format("/// <summary>{0}</summary>", resource.Value), 
                      resource.Type);
                sbKeys.AppendLine();
            }

              if (filePath == null)
                  filePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Resources.cs");
            // write to file
              using (var writer = File.CreateText(filePath)) {

                  // write header along with keys
                  writer.WriteLine(header, namespaceName, className, provider.GetType().Name, sbKeys.ToString());

                  writer.Flush();
                  writer.Close();
              }

              return filePath;
        }

Resource builder project to create resources file. This is a very simple console application that fetches all resources defined in the database and generates the resources class.

public static void Main(string[] args)
{
    IKernel _Kernel = new StandardKernel();
    _Kernel.Load(Assembly.GetExecutingAssembly());

    _Kernel.Bind<IDbResourceProvider>().To<DbResourceProvider>();

    var provider = _Kernel.Get<IDbResourceProvider>();

    var builder = new Resources.Utility.ResourceBuilder();
    string filePath = builder.Create((BaseResourceProvider) provider, summaryCulture: "en-us");
    Console.WriteLine("Created file {0}", filePath);
}

So, by running the console application, Resources.cs is generated and can be linked (as existing file) into any project that uses internationalization.

Each project that uses generated resources must set the provider:

Resources.Resources.SetProvider((IResourceProvider)kernel.Get<IDbResourceProvider>());

Usage example:

using Res = Resources.Resources;

string title = Res.HomePageTitle;

Development cycle is the following:

  1. Resource table is changed
  2. Console application is run to update Resources.cs which will automatically be updated in all referencing projects
  3. All referencing solutions will gain access to the latest added properties

Can something be improved in the above code (both in terms of architecture and development cycle)?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.