Localization using a DI framework - good idea?

2019-05-03 05:13发布

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?

3条回答
The star\"
2楼-- · 2019-05-03 05:47

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.

查看更多
仙女界的扛把子
3楼-- · 2019-05-03 05:48

The only disadvantage I can see is that for any update to "resources", you would have to recompile the assembly containing resources. And depending on your project, this disadvantage may be a good advise to only use a DI framework for resolving a ResourceService of some kind, rather than the values itself.

查看更多
手持菜刀,她持情操
4楼-- · 2019-05-03 05:51

If you cannot use an existing resource framework (like that built into ASP.Net) and would have to build your own, I will assume that you at some point will need to expose services that provide localized resources.

DI frameworks are used to handle service instantiation. Your localization framework will expose services providing localization. Why shouldn't that service be served up by the framework?

Not using DI for its purpose here is like saying, "I'm building a CRM app but cannot use DI because DI is not built for customer relations management".

So yes, if you're already using DI in the rest of your application, IMO it would be wrong to not use it for the services handling localization.

查看更多
登录 后发表回答