133

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 ?

serhio
  • 28,010
  • 62
  • 221
  • 374
PowerKiKi
  • 4,539
  • 4
  • 39
  • 47
  • 2
    What about Display(ResourceType=typeof(ResourceStrings),Name="MyProperty") see http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.displayattribute.aspx – Peter Dec 14 '11 at 13:46
  • @Peter read the post carefully, he wants exact opposite, using ResourceStrings and compile time check not hard coded strings... – Marko Dec 20 '13 at 17:13
  • Since C# 6 you can use `nameof(Resources.MyPropertyNameLocalized)` to keep it strongly typed. – Jiří Skála May 10 '22 at 11:32

11 Answers11

124

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.

James Cronen
  • 5,715
  • 2
  • 32
  • 52
RandomEngy
  • 14,931
  • 5
  • 70
  • 113
  • I looked all over before finding this page... this is such a life saver. Thanks! Works good on MVC5 for me. – Kris Apr 20 '18 at 14:42
  • 2
    If the compiler is complaining about `typeof(MyResources)`, you might need to set your resource file access modifier to **Public**. – thatWiseGuy Feb 02 '19 at 15:33
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";
}
serhio
  • 28,010
  • 62
  • 221
  • 374
Jeff Yates
  • 61,417
  • 20
  • 137
  • 189
  • 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. – Andy T Feb 25 '10 at 09:11
  • 1
    @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. – Jeff Yates Mar 01 '10 at 16:05
  • 1
    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");` – Tristan Warner-Smith Aug 17 '10 at 11:37
  • says i need an instance of Resources.ResourceManager in order to call get string on it – topwik Dec 02 '11 at 20:06
  • @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. – Jeff Yates Dec 02 '11 at 21:51
  • @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? – Jeff Yates Jul 24 '12 at 13:30
  • @Jeff: Sorry, nevermind. I mixed up the Description and DisplayName attributes. – LTR Jul 24 '12 at 13:45
  • 1
    @LTR: No problem. I'm glad you got to the bottom of the issue. Happy to help, if I can. – Jeff Yates Jul 24 '12 at 14:34
  • 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. – kbvishnu Oct 19 '12 at 11:51
  • also in this approach how can I pass the locale ? – kbvishnu Oct 19 '12 at 12:54
  • The locale for resource look-ups comes from CultureInfo.CurrentUICulture. The resource manager already has the culture set on it. – Jeff Yates Oct 19 '12 at 12:59
  • 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 ? – kbvishnu Oct 23 '12 at 11:10
  • .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. – Jeff Yates Oct 23 '12 at 14:14
  • Today with C# could we not use `nameof` instead of maintaining a `ResourceStrings` class? It would resemble `nameof(Resources.MyResource)`, – skeletank Aug 24 '18 at 13:18
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.

PowerKiKi
  • 4,539
  • 4
  • 39
  • 47
  • Very useful. Thanks. In the future, I hope Microsoft comes up with a nice solution that offers strongly typed way of referencing the resources. – Johnny Oshika Oct 19 '09 at 05:50
  • 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 :/ – Rookian Jun 30 '10 at 09:18
  • 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);` – Maksymilian Majer Oct 27 '10 at 08:26
  • 1
    Combine this with @zielu1's T4 template to auto-generate the resource keys, and you have a worthy winner! – David Keaveny Jul 01 '11 at 06:04
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; }
dionoid
  • 500
  • 4
  • 9
  • 3
    In this example, what is "MyResources"? A strongly-typed resx file? A custom class? – Greg May 16 '19 at 17:33
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#>";
  <# } #>
  }
 }
}
zielu1
  • 1,308
  • 11
  • 17
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.

YYFish
  • 156
  • 1
  • 3
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" } } } });
    }
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
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);
        }
    }

}
HaikMnatsakanyan
  • 303
  • 2
  • 5
  • 13
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.

configurator
  • 40,828
  • 14
  • 81
  • 115
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

Martin.Martinsson
  • 1,894
  • 21
  • 25
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.

Vincent Van Den Berghe
  • 5,425
  • 2
  • 31
  • 40
  • 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 useful – PowerKiKi Dec 10 '08 at 16:25
  • 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. – Vincent Van Den Berghe Dec 10 '08 at 16:56