Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Localization using a DI framework - good idea?

I am working on a web application which I need to localize and internationalize. It occurred to me that I could do this using a dependency injection framework. Let's say I declare an interface ILocalResources (using C# for this example but that's not really important):

interface ILocalResources {
    public string OkString { get; }
    public string CancelString { get; }
    public string WrongPasswordString { get; }
    ...
}

and create implementations of this interface, one for each language I need to support. I would then setup my DI framework to instantiate the proper implementation, either statically or dynamically (for example based on the requesting browsers preferred language).

Is there some reason I shouldn't be using a DI framework for this sort of thing? The only objection I could find myself is that it might be a bit overkill, but if I'm using a DI framework in my web app anyway, I might as well use it for internationalization as well?

like image 437
David Nordvall Avatar asked Jan 10 '11 12:01

David Nordvall


1 Answers

A DI framework is built to do dependency injection and localization could just be one of your services, so in that case there's no reason not to use a DI framework IMO. Perhaps we should start discussing the provided ILocalResources interface. While I'm a favor of having compile time support, I'm not sure the supplied interface will help you, because that interface will be probably the type in your system that will change the most. And with that interface the type/types that implement it. Perhaps you should go with a different design.

When we look at most localization frameworks/providers/factories (or whatever), they're all string based. Because of this, think about the following design:

public interface ILocalResources
{
    string GetStringResource(string key);
    string GetStringResource(string key, CultureInfo culture);
}

This would allow you to add keys and cultures to the underlying message data store, without changing the interface. Downside is of course that you should never change a key, because that will probably be a hell.

Another approach could be an abstract base type:

public abstract class LocalResources
{
    public string OkMessage { get { return this.GetString("OK"); } }
    public string CancelMessage { get { return this.GetString("Cancel"); } }
    ...

    protected abstract string GetStringResource(string key, 
        CultureInfo culture);

    private string GetString(string key)
    {
        Culture culture = CultureInfo.CurrentCulture;

        string resource = GetStringResource(key, culture);

        // When the resource is not found, fall back to the neutral culture.
        while (resource == null && culture != CultureInfo.InvariantCulture)
        {
            culture = culture.Parent;
            resource = this.GetStringResource(key, culture);
        }

        if (resource == null) throw new KeyNotFoundException(key);

        return resource;
    }
}

And implementation of this type could look like this:

public sealed class SqlLocalResources : LocalResources
{
    protected override string GetStringResource(string key, 
        CultureInfo culture)
    {
        using (var db = new LocalResourcesContext())
        {
            return (
                from resource in db.StringResources
                where resource.Culture == culture.Name
                where resource.Key == key
                select resource.Value).FirstOrDefault();
        }
    }
}

This approach takes best of both worlds, because the keys won't be scattered through the application and adding new properties just has to be done in one single place. Using your favorite DI library, you can register an implementation like this:

container.RegisterSingleton<LocalResources>(new SqlLocalResources());

And since the LocalResources type has exactly one abstract method that does all the work, it is easy to create a decorator that adds caching to prevent requesting the same data from the database:

public sealed class CachedLocalResources : LocalResources
{
    private readonly Dictionary<CultureInfo, Dictionary<string, string>> cache =
        new Dictionary<CultureInfo, Dictionary<string, string>>();
    private readonly LocalResources decoratee;

    public CachedLocalResources(LocalResources decoratee) { this.decoratee = decoratee; }

    protected override string GetStringResource(string key, CultureInfo culture) {
        lock (this.cache) {
            string res;
            var cultureCache = this.GetCultureCache(culture);
            if (!cultureCache.TryGetValue(key, out res)) {
                cultureCache[key] = res= this.decoratee.GetStringResource(key, culture);
            }                
            return res;
        }
    }

    private Dictionary<string, string> GetCultureCache(CultureInfo culture) {
        Dictionary<string, string> cultureCache;
        if (!this.cache.TryGetValue(culture, out cultureCache)) {
            this.cache[culture] = cultureCache = new Dictionary<string, string>();
        }
        return cultureCache;
    }
}

You can apply the decorator as follows:

container.RegisterSingleton<LocalResources>(
    new CachedLocalResources(new SqlLocalResources()));

Do note that this decorator caches the string resources indefinitely, which might cause memory leaks, so you wish to wrap the strings in WeakReference instances or have some sort of expiration timeout on it. But the idea is that you can apply caching without having to change any existing implementation.

I hope this helps.

like image 89
Steven Avatar answered Oct 13 '22 13:10

Steven