Is this a good solution for localisation of pluggable components?

247 views Asked by At

I asked a question previously which had only a single answer. I've had some time to play around with this now and have a plan, but want some feedback on if it is a good idea.

The problem:

I want a component which has a name (invariant, used to identify the component) to have its name localised in the application that is consuming it, without polluting the model of the component with a DisplayName attribute. The component may exist in a separate dll and be dynamically loaded at runtime.

my feeling is that the component dll should be responsible for providing the localised name (this seems like good encapsulation), but the application consuming the component should be responsible for getting/using the localised name (the fact that the component has a different name for display purposes is not a concern of the component, but of the 'view' using that component)

The solution:

Add a resource to the components dll with the same name as the file the component class is in. Add a string to the resource with a key which is the name of the component.

In the application get the localised name like so:

ExternalObject obj = GetExternalObject ();            
ResourceManager manager = new ResourceManager (obj.GetType ());
string localisedName= manager.GetString (obj.Name);

This code will probably be encapsulated in a Localiser class, but conveys the point. This seems to work, but is it a good idea, or is there a better/more standard way of doing this?

EDIT: I should point out that one thing that I'm not sure about with this solution is that the resources have to be in a .resx file that has the same name as the file that the class is in. This makes it work, as the resource file can be identified from the type name. This is the same as localisation for forms seems to work, and makes visual studio put the .resx as a 'sub-component' of the .cs file, which all seems nice. But visual studio then throws a warning (about editing a resource that is part of another project item) if I try and edit this file, which makes me think that perhaps there is some other way that I'm supposed to be doing this.

3

There are 3 answers

2
psychotik On BEST ANSWER

I think you have the right idea, but there's better way to accomplish this.

Presumably, you have an interface that the pluggable component implements. Say, IPluggable:

interface IPluggable {
    ...
    string LocalizedName {get;}
    ...
}

From your main binary, load the pluggable assembly and create the IPluggable instance using reflection (I assume that's what the GetExternalObject() method you have does) and then access the localized name using the LocalizedName property. Inside the IPluggable implementation, create a ResourceManager and access the LocalizedName from the resx of that pluggable assembly.

What you get by doing is good encapsulation of behavior in the pluggable assembly - it is responsible for providing you the localized name, however it chooses to do it, without your man program assuming that a ResourceManager can be created to access a localized name.

3
EKS On

The problem with the way you have suggested is that its going to be hard to update the translations, and it may even require a programmer. Also how are you going to update translations without updating the entire applications?

I have done alot of translated applications, and what i have done is have a seperate text file with translatations formated something like this:

[English]
Done=Done

[Norwegian]
Done=Ferdig

And i have a function called TranslateForm() that i call inside Form Show event, that will translate all the UI elements. The TranslateForm() function will have things like

buttonDone.Text = Translate.GetTranslation("Done");

The last part with the TranslateForm is not a optimal solution, i think over time i will migrate to a solution that where the control itself calls the Translate class. The advantage of using this system is that its simple for the programer, you can have other ppl add translations without you having to do manual work afterwards ( This is importent to me as i have community driven translations ), so they are updated often and i dont want to spend time on that. I can also update the translations while the application is running, without the application having to restart or be updated.

0
armannvg On

I had a problem some time ago of localizing enum values, I'm not sure if it answers you question, but at least gives you another approach to have in mind.

Started by creating my own Localizing attribute

/// <SUMMARY>
/// Attribute used for localization. Description field should contain a reference to the Resource file for correct localization
/// </SUMMARY>
public class LocalizationAttribute : Attribute
{
    public LocalizationAttribute(string description)
    {
        this._description = description;
    }

    private string _description;
    /// <SUMMARY>
    /// Used to reference a resource key
    /// </SUMMARY>
    public string Description
    {
        get
        {
            return this._description;
        }
    }
}

From there I create the enum itself

[TypeConverter(typeof(EnumToLocalizedString))]
public enum ReviewReason
{
    [LocalizationAttribute("ReviewReasonNewDocument")]
    NewDocument = 1,


    [LocalizationAttribute("ReviewReasonInternalAudit")]
    InternalAudit = 2,


    [LocalizationAttribute("ReviewReasonExternalAudit")]
    ExternalAudit = 3,


    [LocalizationAttribute("ReviewReasonChangedWorkBehaviour")]
    ChangedWorkBehaviour = 4,


    [LocalizationAttribute("ReviewReasonChangedWorkBehaviourBecauseOfComplaints")]
    ChangedWorkBehaviourBecauseOfComplaints = 5,


    [LocalizationAttribute("ReviewReasonMovedFromOlderSystem")]
    MovedFromOlderSystem = 6,


    [LocalizationAttribute("ReviewReasonPeriodicUpdate")]
    PeriodicUpdate = 7,


    [LocalizationAttribute("ReviewReasonDocumentChanged")]
    DocumentChanged = 8
}

Then I created a type converter which will fetch the LocalizationAttribute description key and access the Resource file to get the localization (Attribute description must match the resource key :))

public class EnumToLocalizedString : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return (sourceType.Equals(typeof(Enum)));
        }

        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            return (destinationType.Equals(typeof(String)));
        }

        public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
        {
            return base.ConvertFrom(context, culture, value);
        }

        public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
        {
            if (!destinationType.Equals(typeof(String)))
            {
                throw new ArgumentException("Can only convert to string.", "destinationType");
            }
            if (!value.GetType().BaseType.Equals(typeof(Enum)))
            {
                throw new ArgumentException("Can only convert an instance of enum.", "value");
            }

            string name = value.ToString();
            object[] attrs = value.GetType().GetField(name).GetCustomAttributes(typeof(LocalizationAttribute), false);
            if (attrs.Length != 1  !(attrs[0] is LocalizationAttribute))
            {
                throw new ArgumentException("Invalid enum argument");
            }
            return Handbok.Code.Resources.handbok.ResourceManager.GetString(((LocalizationAttribute)attrs[0]).Description);
        }
    }

Finally I created the client which uses the TypeConverter, which in this case is a collection

public class ReviewReasonCollection
{
    private static Collection<KEYVALUEPAIR<REVIEWREASON,>> _reviewReasons;

    public static Collection<KEYVALUEPAIR<REVIEWREASON,>> AllReviewReasons
    {
        get
        {
            if (_reviewReasons == null)
            {
                _reviewReasons = new Collection<KEYVALUEPAIR<REVIEWREASON,>>();
                TypeConverter t = TypeDescriptor.GetConverter(typeof(ReviewReason));

                foreach (ReviewReason reviewReason in Enum.GetValues(typeof(ReviewReason)))
                {
                    _reviewReasons.Add(new KeyValuePair<REVIEWREASON,>(reviewReason, t.ConvertToString(reviewReason)));
                }
            }
            return _reviewReasons;
        }
    }
}

I originally posted this solution on my blog. Hope it helps you out :)