views:

103

answers:

3

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.

A: 

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.

EKS
I'm not sure I understand your concerns. The translation is done via satellite assemblies, and specifically does not require the application to be updated to provide new translations, just the provision of a new satellite assembly This is the standard .net method of translation. The problem with your approach (although admittedly this does not apply to my scenario) is that a/ you have to do all translation by hand and b/ what if the text for 'Done' doesn't fit on the button you have created in some language? Using the standard approach, you can provide new button sizes as well as new text.
Sam Holder
EKS, what you're suggesting is primitive. .NET has a bunch of support built into it to use resx files and satellite assemblies which dont require new code to be shipped. For updates/new translations you simply ship the resource binaries, which is NOT code.
psychotik
Primitive but works.Like i tried to make clean in my post, it depends on how often you do updates to the translations. And WHO does them, if you hire someone to do it, my method might not be the one to use. but if you have a community driven translations its good, because "anyone" can update the translations.
EKS
A: 

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 :)

armannvg
+1  A: 

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.

psychotik
This is currently the situation I have, but is what I am trying to get away from. I don't want my model to contain the LocalizedName property, as the fact that there is a localised name is not a concern of the model. What you have might be good encapsulation, but not separation of concerns. Imagine I have ITree representing a tree. It has a name, which is the latin name for the tree. It shouldn't care that when displayed to English users they want to see a different name than the latin name.
Sam Holder
I want to keep the encapsulation (by having the resource live in the dll of the component), and have SOC by having the view access the LocalisedName by querying the component resources using the invariant name as the key.
Sam Holder