Localization of DisplayNameAttribute
Asked Answered
O

11

134

I am looking for a way to localize properties names displayed in a PropertyGrid. The property's name may be "overriden" using the DisplayNameAttribute attribute. Unfortunately attributes can not have non constant expressions. So I can not use strongly typed resources such as:

class Foo
{
   [DisplayAttribute(Resources.MyPropertyNameLocalized)]  // do not compile
   string MyProperty {get; set;}
}

I had a look around and found some suggestion to inherit from DisplayNameAttribute to be able to use resource. I would end up up with code like:

class Foo
{
   [MyLocalizedDisplayAttribute("MyPropertyNameLocalized")] // not strongly typed
   string MyProperty {get; set;}
}

However I lose strongly typed resource benefits which is definitely not a good thing. Then I came across DisplayNameResourceAttribute which may be what I'm looking for. But it's supposed to be in Microsoft.VisualStudio.Modeling.Design namespace and I can't find what reference I am supposed to add for this namespace.

Anybody know if there's a easier way to achieve DisplayName localization in a good way ? or if there is as way to use what Microsoft seems to be using for Visual Studio ?

Osbert answered 10/12, 2008 at 15:25 Comment(3)
What about Display(ResourceType=typeof(ResourceStrings),Name="MyProperty") see msdn.microsoft.com/en-us/library/…Egesta
@Egesta read the post carefully, he wants exact opposite, using ResourceStrings and compile time check not hard coded strings...Kolnick
Since C# 6 you can use nameof(Resources.MyPropertyNameLocalized) to keep it strongly typed.Bribe
O
42

Here is the solution I ended up with in a separate assembly (called "Common" in my case):

   [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event)]
   public class DisplayNameLocalizedAttribute : DisplayNameAttribute
   {
      public DisplayNameLocalizedAttribute(Type resourceManagerProvider, string resourceKey)
         : base(Utils.LookupResource(resourceManagerProvider, resourceKey))
      {
      }
   }

with the code to look up the resource:

  internal static string LookupResource(Type resourceManagerProvider, string resourceKey)
  {
     foreach (PropertyInfo staticProperty in  resourceManagerProvider.GetProperties(BindingFlags.Static | BindingFlags.NonPublic))
     {
        if (staticProperty.PropertyType == typeof(System.Resources.ResourceManager))
        {
           System.Resources.ResourceManager resourceManager = (System.Resources.ResourceManager)staticProperty.GetValue(null, null);
           return resourceManager.GetString(resourceKey);
        }
     }

     return resourceKey; // Fallback with the key name
  }

Typical usage would be:

class Foo
{
      [Common.DisplayNameLocalized(typeof(Resources.Resource), "CreationDateDisplayName"),
      Common.DescriptionLocalized(typeof(Resources.Resource), "CreationDateDescription")]
      public DateTime CreationDate
      {
         get;
         set;
      }
}

What is pretty much ugly as I use literal strings for resource key. Using a constant there would mean to modify Resources.Designer.cs which is probably not a good idea.

Conclusion: I am not happy with that, but I am even less happy about Microsoft who can't provide anything useful for such a common task.

Osbert answered 11/12, 2008 at 13:37 Comment(4)
Very useful. Thanks. In the future, I hope Microsoft comes up with a nice solution that offers strongly typed way of referencing the resources.Tericaterina
ya this string stuff sucks really hard :( If you could get the property name of the property that uses the attribute, you could do it in the convention over configuration way, but this seems not to be possible. Caring for the "strongly tpyed" Enumerations, you could use, is also not really maintainable :/Ehlke
That's a good solution. I just wouldn't iterate through the collection of ResourceManager properties. Instead you can simply get the property directly from the type provided in the parameter: PropertyInfo property = resourceManagerProvider.GetProperty(resourceKey, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);Taxidermy
Combine this with @zielu1's T4 template to auto-generate the resource keys, and you have a worthy winner!Impossible
F
126

There is the Display attribute from System.ComponentModel.DataAnnotations in .NET 4. It works on the MVC 3 PropertyGrid.

[Display(ResourceType = typeof(MyResources), Name = "UserName")]
public string UserName { get; set; }

This looks up a resource named UserName in your MyResources.resx file.

Fuji answered 6/10, 2010 at 21:47 Comment(3)
I looked all over before finding this page... this is such a life saver. Thanks! Works good on MVC5 for me.Escent
If the compiler is complaining about typeof(MyResources), you might need to set your resource file access modifier to Public.Jollenta
Note that you can use Name = nameof(MyResources.UserName) to avoid typos, track usage and allow for easier renaming.Photometer
W
83

We are doing this for a number of attributes in order to support multiple language. We have taken a similar approach to Microsoft, where they override their base attributes and pass a resource name rather than the actual string. The resource name is then used to perform a lookup in the DLL resources for the actual string to return.

For example:

class LocalizedDisplayNameAttribute : DisplayNameAttribute
{
    private readonly string resourceName;
    public LocalizedDisplayNameAttribute(string resourceName)
        : base()
    {
      this.resourceName = resourceName;
    }

    public override string DisplayName
    {
        get
        {
            return Resources.ResourceManager.GetString(this.resourceName);
        }
    }
}

You can take this a step further when actually using the attribute, and specify your resource names as constants in a static class. That way, you get declarations like.

[LocalizedDisplayName(ResourceStrings.MyPropertyName)]
public string MyProperty
{
  get
  {
    ...
  }
}

Update
ResourceStrings would look something like (note, each string would refer to the name of a resource that specifies the actual string):

public static class ResourceStrings
{
    public const string ForegroundColorDisplayName="ForegroundColorDisplayName";
    public const string FontSizeDisplayName="FontSizeDisplayName";
}
Woolley answered 10/12, 2008 at 18:3 Comment(14)
When I try this approach, I get an error message saying "An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type". However, passing the value to LocalizedDisplayName as a string works. Wish it would be strongly typed.Overthecounter
@Andy: The values in ResourceStrings must be constants, as indicated in the answer, not properties or readonly values. They must be marked as const and refer to the names of the resources, otherwise you will get an error.Woolley
Answered my own question, was about where you had the Resources.ResourceManager, in my case the resx files are public resx generated so it was [MyNamespace].[MyResourceFile].ResourceManager.GetString("MyString");Specter
says i need an instance of Resources.ResourceManager in order to call get string on itEndocarp
@towpse: You probably don't have a using statement for your namespace so it is default to the using System and calling the actual class rather than the instance off of the autogenerated Resources class.Woolley
@LTR: That's strange. They'd have to work hard to make it do that. I suggest raising a Connect bug, just in case they find time to address this. Can you give more detail on what you mean? What part of the DataGridView is looking for DisplayNameAttribute?Woolley
@Jeff: Sorry, nevermind. I mixed up the Description and DisplayName attributes.Chamkis
@LTR: No problem. I'm glad you got to the bottom of the issue. Happy to help, if I can.Woolley
thanks for the apporach. I am querying from the db instead of using resources file. In my application, there will be a dropdownlist which contains supported locales, and user can choose any one of them. Then how I can I change the language ? By using this approach.Brother
also in this approach how can I pass the locale ?Brother
The locale for resource look-ups comes from CultureInfo.CurrentUICulture. The resource manager already has the culture set on it.Woolley
Ok Thanks @JeffYates for your help. There are some labels and controls like buttons,tooltip etc. The above way discuss about properties of a class. Is there any way to implement the localization for these kind of objects ?Brother
.NET provides built-in support for localizing WinForms. Just set Localizable to true on the form designer and tell it what culture the current layout is for. In WPF, you'll have to look up LocBaml.Woolley
Today with C# could we not use nameof instead of maintaining a ResourceStrings class? It would resemble nameof(Resources.MyResource),Reviewer
O
42

Here is the solution I ended up with in a separate assembly (called "Common" in my case):

   [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event)]
   public class DisplayNameLocalizedAttribute : DisplayNameAttribute
   {
      public DisplayNameLocalizedAttribute(Type resourceManagerProvider, string resourceKey)
         : base(Utils.LookupResource(resourceManagerProvider, resourceKey))
      {
      }
   }

with the code to look up the resource:

  internal static string LookupResource(Type resourceManagerProvider, string resourceKey)
  {
     foreach (PropertyInfo staticProperty in  resourceManagerProvider.GetProperties(BindingFlags.Static | BindingFlags.NonPublic))
     {
        if (staticProperty.PropertyType == typeof(System.Resources.ResourceManager))
        {
           System.Resources.ResourceManager resourceManager = (System.Resources.ResourceManager)staticProperty.GetValue(null, null);
           return resourceManager.GetString(resourceKey);
        }
     }

     return resourceKey; // Fallback with the key name
  }

Typical usage would be:

class Foo
{
      [Common.DisplayNameLocalized(typeof(Resources.Resource), "CreationDateDisplayName"),
      Common.DescriptionLocalized(typeof(Resources.Resource), "CreationDateDescription")]
      public DateTime CreationDate
      {
         get;
         set;
      }
}

What is pretty much ugly as I use literal strings for resource key. Using a constant there would mean to modify Resources.Designer.cs which is probably not a good idea.

Conclusion: I am not happy with that, but I am even less happy about Microsoft who can't provide anything useful for such a common task.

Osbert answered 11/12, 2008 at 13:37 Comment(4)
Very useful. Thanks. In the future, I hope Microsoft comes up with a nice solution that offers strongly typed way of referencing the resources.Tericaterina
ya this string stuff sucks really hard :( If you could get the property name of the property that uses the attribute, you could do it in the convention over configuration way, but this seems not to be possible. Caring for the "strongly tpyed" Enumerations, you could use, is also not really maintainable :/Ehlke
That's a good solution. I just wouldn't iterate through the collection of ResourceManager properties. Instead you can simply get the property directly from the type provided in the parameter: PropertyInfo property = resourceManagerProvider.GetProperty(resourceKey, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);Taxidermy
Combine this with @zielu1's T4 template to auto-generate the resource keys, and you have a worthy winner!Impossible
J
23

Using the Display attribute (from System.ComponentModel.DataAnnotations) and the nameof() expression in C# 6, you'll get a localized and strongly typed solution.

[Display(ResourceType = typeof(MyResources), Name = nameof(MyResources.UserName))]
public string UserName { get; set; }
Juneberry answered 24/9, 2016 at 19:36 Comment(1)
In this example, what is "MyResources"? A strongly-typed resx file? A custom class?Therapy
K
14

You could use T4 to generate constants. I wrote one:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml.dll" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Xml.XPath" #>
using System;
using System.ComponentModel;


namespace Bear.Client
{
 /// <summary>
 /// Localized display name attribute
 /// </summary>
 public class LocalizedDisplayNameAttribute : DisplayNameAttribute
 {
  readonly string _resourceName;

  /// <summary>
  /// Initializes a new instance of the <see cref="LocalizedDisplayNameAttribute"/> class.
  /// </summary>
  /// <param name="resourceName">Name of the resource.</param>
  public LocalizedDisplayNameAttribute(string resourceName)
   : base()
  {
   _resourceName = resourceName;
  }

  /// <summary>
  /// Gets the display name for a property, event, or public void method that takes no arguments stored in this attribute.
  /// </summary>
  /// <value></value>
  /// <returns>
  /// The display name.
  /// </returns>
  public override String DisplayName
  {
   get
   {
    return Resources.ResourceManager.GetString(this._resourceName);
   }
  }
 }

 partial class Constants
 {
  public partial class Resources
  {
  <# 
   var reader = XmlReader.Create(Host.ResolvePath("resources.resx"));
   var document = new XPathDocument(reader);
   var navigator = document.CreateNavigator();
   var dataNav = navigator.Select("/root/data");
   foreach (XPathNavigator item in dataNav)
   {
    var name = item.GetAttribute("name", String.Empty);
  #>
   public const String <#= name#> = "<#= name#>";
  <# } #>
  }
 }
}
Kerstin answered 22/7, 2010 at 16:32 Comment(1)
What would the output be like?Generalist
P
9

This is an old question, but I think this is a very common problem, and here is my solution in MVC 3.

Firstly, a T4 template is needed to generate constants to avoid nasty strings. We have a resource file ‘Labels.resx’ holds all the label strings. Therefore the T4 template uses the resource file directly,

<#@ template debug="True" hostspecific="True" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="C:\Project\trunk\Resources\bin\Development\Resources.dll" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Globalization" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Resources" #>
<#
  var resourceStrings = new List<string>();
  var manager = Resources.Labels.ResourceManager;

  IDictionaryEnumerator enumerator = manager.GetResourceSet(CultureInfo.CurrentCulture,  true, true)
                                             .GetEnumerator();
  while (enumerator.MoveNext())
  {
        resourceStrings.Add(enumerator.Key.ToString());
  }
#>     

// This file is generated automatically. Do NOT modify any content inside.

namespace Lib.Const{
        public static class LabelNames{
<#
            foreach (String label in resourceStrings){
#>                    
              public const string <#=label#> =     "<#=label#>";                    
<#
           }    
#>
    }
}

Then, an extension method get created to localize the ‘DisplayName’,

using System.ComponentModel.DataAnnotations;
using Resources;

namespace Web.Extensions.ValidationAttributes
{
    public static class ValidationAttributeHelper
    {
        public static ValidationContext LocalizeDisplayName(this ValidationContext    context)
        {
            context.DisplayName = Labels.ResourceManager.GetString(context.DisplayName) ?? context.DisplayName;
            return context;
        }
    }
}

‘DisplayName’ attribute is replaced by ‘DisplayLabel’ attribute in order to read from ‘Labels.resx’ automatically,

namespace Web.Extensions.ValidationAttributes
{

    public class DisplayLabelAttribute :System.ComponentModel.DisplayNameAttribute
    {
        private readonly string _propertyLabel;

        public DisplayLabelAttribute(string propertyLabel)
        {
            _propertyLabel = propertyLabel;
        }

        public override string DisplayName
        {
            get
            {
                return _propertyLabel;
            }
        }
    }
}

After all those preparation work, time to touch those default validation attributes. I am using ‘Required’ attribute as an example,

using System.ComponentModel.DataAnnotations;
using Resources;

namespace Web.Extensions.ValidationAttributes
{
    public class RequiredAttribute : System.ComponentModel.DataAnnotations.RequiredAttribute
    {
        public RequiredAttribute()
        {
          ErrorMessageResourceType = typeof (Errors);
          ErrorMessageResourceName = "Required";
        }

        protected override ValidationResult IsValid(object value, ValidationContext  validationContext)
        {
            return base.IsValid(value, validationContext.LocalizeDisplayName());
        }

    }
}

Now, We can apply those attributes in our model,

using Web.Extensions.ValidationAttributes;

namespace Web.Areas.Foo.Models
{
    public class Person
    {
        [DisplayLabel(Lib.Const.LabelNames.HowOldAreYou)]
        public int Age { get; set; }

        [Required]
        public string Name { get; set; }
    }
}

By default, property name is used as the key to look up ‘Label.resx’, but if you set it through ‘DisplayLabel’, it will use that instead.

Pinto answered 4/8, 2011 at 15:30 Comment(0)
G
6

You can subclass DisplayNameAttribute to provide i18n, by overriding one of the methods. Like so. edit: You might have to settle for using a constant for the key.

using System;
using System.ComponentModel;
using System.Windows.Forms;

class Foo {
    [MyDisplayName("bar")] // perhaps use a constant: SomeType.SomeResName
    public string Bar {get; set; }
}

public class MyDisplayNameAttribute : DisplayNameAttribute {
    public MyDisplayNameAttribute(string key) : base(Lookup(key)) {}

    static string Lookup(string key) {
        try {
            // get from your resx or whatever
            return "le bar";
        } catch {
            return key; // fallback
        }
    }
}

class Program {
    [STAThread]
    static void Main() {
        Application.Run(new Form { Controls = {
            new PropertyGrid { SelectedObject =
                new Foo { Bar = "abc" } } } });
    }
}
Grindstone answered 10/12, 2008 at 15:45 Comment(0)
C
2

I use this way solve in my case

[LocalizedDisplayName("Age", NameResourceType = typeof(RegistrationResources))]
 public bool Age { get; set; }

With the code

public sealed class LocalizedDisplayNameAttribute : DisplayNameAttribute
{
    private PropertyInfo _nameProperty;
    private Type _resourceType;


    public LocalizedDisplayNameAttribute(string displayNameKey)
        : base(displayNameKey)
    {

    }

    public Type NameResourceType
    {
        get
        {
            return _resourceType;
        }
        set
        {
            _resourceType = value;
            _nameProperty = _resourceType.GetProperty(base.DisplayName, BindingFlags.Static | BindingFlags.Public);
        }
    }

    public override string DisplayName
    {
        get
        {
            if (_nameProperty == null)
            {
                return base.DisplayName;
            }

            return (string)_nameProperty.GetValue(_nameProperty.DeclaringType, null);
        }
    }

}
Covington answered 12/1, 2015 at 15:4 Comment(0)
F
1

Well, the assembly is Microsoft.VisualStudio.Modeling.Sdk.dll. which comes with the Visual Studio SDK (With Visual Studio Integration Package).

But it would be used in pretty much the same way as your attribute; there is no way to use strongly types resources in attributes simply because they are not constant.

Fallingout answered 10/12, 2008 at 15:36 Comment(0)
O
1

DisplayName:

    public sealed class LocalizedDisplayNameAttribute : DisplayNameAttribute
{
    public string ResourceKey { get; }
    public string BaseName { get; set; }
    public Type ResourceType { get; set; }

    public LocalizedDisplayNameAttribute(string resourceKey)
    {
        ResourceKey = resourceKey;
    }

    public override string DisplayName
    {
        get
        {
            var baseName = BaseName;
            var assembly = ResourceType?.Assembly ?? Assembly.GetEntryAssembly();

            if (baseName.IsNullOrEmpty())
            {
                // ReSharper disable once PossibleNullReferenceException
                baseName = $"{(ResourceType != null ? ResourceType.Namespace : assembly.GetName().Name)}.Resources";
            }

            // ReSharper disable once AssignNullToNotNullAttribute
            var res = new ResourceManager(baseName, assembly);

            var str = res.GetString(ResourceKey);

            return string.IsNullOrEmpty(str)
                ? $"[[{ResourceKey}]]"
                : str;
        }
    }
}

Description:

public sealed class LocalizedDescriptionAttribute : DescriptionAttribute
{
    public string ResourceKey { get; }
    public string BaseName { get; set; }
    public Type ResourceType { get; set; }

    public LocalizedDescriptionAttribute(string resourceKey)
    {
        ResourceKey = resourceKey;
    }

    public override string Description
    {
        get
        {
                var baseName = BaseName;
                var assembly = ResourceType?.Assembly ?? Assembly.GetEntryAssembly();

                if (baseName.IsNullOrEmpty())
                {
                    // ReSharper disable once PossibleNullReferenceException
                    baseName = $"{(ResourceType != null ? ResourceType.Namespace : assembly.GetName().Name)}.Resources";
                }

                // ReSharper disable once AssignNullToNotNullAttribute
                var res = new ResourceManager(baseName, assembly);
                var str = res.GetString(ResourceKey);
                
                return string.IsNullOrEmpty(str)
                    ? $"[[{ResourceKey}]]"
                    : str;
        }
    }
}

Categories (PropertyGrid):

    public sealed class LocalizedCategoryAttribute : CategoryAttribute
{
    public string ResourceKey { get; }
    public string BaseName { get; set; }
    public Type ResourceType { get; set; }

    public LocalizedCategoryAttribute(string resourceKey) 
        : base(resourceKey)
    {
        ResourceKey = resourceKey;
    }

    protected override string GetLocalizedString(string resourceKey)
    {
        var baseName = BaseName;
        var assembly = ResourceType?.Assembly ?? Assembly.GetEntryAssembly();

        if (baseName.IsNullOrEmpty())
        {
            // ReSharper disable once PossibleNullReferenceException
            baseName = $"{(ResourceType != null ? ResourceType.Namespace : assembly.GetName().Name)}.Resources";
        }

        // ReSharper disable once AssignNullToNotNullAttribute
        var res = new ResourceManager(baseName, assembly);
        var str = res.GetString(resourceKey);

        return string.IsNullOrEmpty(str)
            ? $"[[{ResourceKey}]]"
            : str;
    }
}

Example: [LocalizedDisplayName("ResourceKey", ResourceType = typeof(RE))]

Where "RE" is living in the assembly holding your resource files like "Resources.de.resx" or "Resources.en.resx".

Works on enumerations and properties.

Cheers

Osteomyelitis answered 19/12, 2020 at 13:7 Comment(0)
T
0

I apologize for the VB.NET code, my C# is a bit rusty... But you'll get the idea, right?

First of all, create a new class: LocalizedPropertyDescriptor, which inherits PropertyDescriptor. Override the DisplayName property like this:

Public Overrides ReadOnly Property DisplayName() As String
         Get
            Dim BaseValue As String = MyBase.DisplayName
            Dim Translated As String = Some.ResourceManager.GetString(BaseValue)
            If String.IsNullOrEmpty(Translated) Then
               Return MyBase.DisplayName
            Else
               Return Translated
           End If
    End Get
End Property

Some.ResourceManager is the ResourceManager of the resource file that contains your translations.

Next, implement ICustomTypeDescriptor in the class with the localized properties, and override the GetProperties method:

Public Function GetProperties() As PropertyDescriptorCollection Implements System.ComponentModel.ICustomTypeDescriptor.GetProperties
    Dim baseProps As PropertyDescriptorCollection = TypeDescriptor.GetProperties(Me, True)
    Dim LocalizedProps As PropertyDescriptorCollection = New PropertyDescriptorCollection(Nothing)

    Dim oProp As PropertyDescriptor
    For Each oProp In baseProps
        LocalizedProps.Add(New LocalizedPropertyDescriptor(oProp))
    Next
    Return LocalizedProps
End Function

You can now use the 'DisplayName` attribute to store a reference to a value in a resource file...

<DisplayName("prop_description")> _
Public Property Description() As String

prop_description is the key in the resource file.

Twist answered 10/12, 2008 at 15:47 Comment(2)
First part of your solution is what I did... until i had to solve the "what is Some.ResourceManager ?" question. Am I supposed to give a second literal string such as "MyAssembly.Resources.Resource" ? way too dangerous! As for second part (ICustomTypeDescriptor) I don't think it's actually usefulOsbert
Marc Gravell's solution is the way to go if you don't need anything else than a translated DisplayName -- I use the custom descriptor for other stuff too, and this was my solution. There is no way to do this without supplying some sort of key, though.Twist

© 2022 - 2024 — McMap. All rights reserved.