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:
- Use strongly typed resources
- Allow to easily change existing resources strings without application redeployment (i.e. at runtime)
- 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:
- Resource table is changed
- Console application is run to update Resources.cs which will automatically be updated in all referencing projects
- 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)?