Enum localization
UPDATE 2019
Nowadays its just plain easy, setup your enum:
public enum ContactOptionType
{
[Display(Description = "ContactOption1", ResourceType = typeof(Globalization.Contact))]
Demo = 1,
[Display(Description = "ContactOption2", ResourceType = typeof(Globalization.Contact))]
Callback = 2,
[Display(Description = "ContactOption3", ResourceType = typeof(Globalization.Contact))]
Quotation = 3,
[Display(Description = "ContactOption4", ResourceType = typeof(Globalization.Contact))]
Other = 4
}
Each enum value gets a Display attribute
with a Description
value which is an entry in a resource assembly class called Globalization.Contact
. This resource assembly (project) contains various translations for the different contact option types (Demo, Callback, Quotation, Other). It contains files like these: contact.nl.resx
(for the Netherlands) and contact.resx
(The default which is en-US) in which the different enum values have their localized values (translations).
Now in a static enum helper class we have this method:
public static string GetDisplayDescription(this Enum enumValue)
{
return enumValue.GetType().GetMember(enumValue.ToString())
.FirstOrDefault()?
.GetCustomAttribute<DisplayAttribute>()
.GetDescription() ?? "unknown";
}
This will get the value for the Description
property of the Display
attribute. Which will be the translated value if and only if the CurrentUICulture
is set. This "glues" everything together.
Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
or
Thread.CurrentThread.CurrentUICulture = new CultureInfo("nl-NL");
Now in a simple Unit Test (XUnit) we can see if it works as expected / desired / designed:
[Theory]
[InlineData("nl-NL", "Terugbelverzoek")]
[InlineData("en-US", "Callback")]
public void TestTranslationOfDescriptionAttribute(string culture, string expectedValue)
{
// Arrange
CultureInfo cultureInfo = new CultureInfo(culture);
Thread.CurrentThread.CurrentCulture = cultureInfo;
Thread.CurrentThread.CurrentUICulture = cultureInfo;
ContactOptionType contactOptionType = ContactOptionType.Callback;
// Act
string description = contactOptionType.GetDisplayDescription();
// Assert
Assert.Equal(expectedValue, description);
}
The above will succeed effortlessly ✅ð♂️
So this solution doesn't use a "complex" LocalizedAttribute
anymore only a simple helper that gets the (translated) value of the Description property of the Display attribute. The presence of a ResourceType
value in the Display attribute in combination with setting the CurrentUICulture
does the trick.
I solved the issue by creating a EnumExtension which I use in my view. This extension looks for a resource file called "EnumResources.resx" and looks up the resource by the following naming convention {Name of EnumType}_{Value of enum passed in}. If the resource key is missing it will display the value of the resource encapsulated within double brackets [[EnumValue]]. This way its easy to find a "untranslated" Enum in your view. Also this helps reminding you if you forgot to update the resource file after a rename or such.
public static class EnumExtensions
{
public static string GetDisplayName(this Enum e)
{
var rm = new ResourceManager(typeof (EnumResources));
var resourceDisplayName = rm.GetString(e.GetType().Name + "_" + e);
return string.IsNullOrWhiteSpace(resourceDisplayName) ? string.Format("[[{0}]]", e) : resourceDisplayName;
}
}
The resource file looks like this:
Usage:
<div>@ContractStatus.Created.GetDisplayName()</div>
You can implement a description attribute.
public class LocalizedDescriptionAttribute : DescriptionAttribute
{
private readonly string _resourceKey;
private readonly ResourceManager _resource;
public LocalizedDescriptionAttribute(string resourceKey, Type resourceType)
{
_resource = new ResourceManager(resourceType);
_resourceKey = resourceKey;
}
public override string Description
{
get
{
string displayName = _resource.GetString(_resourceKey);
return string.IsNullOrEmpty(displayName)
? string.Format("[[{0}]]", _resourceKey)
: displayName;
}
}
}
public static class EnumExtensions
{
public static string GetDescription(this Enum enumValue)
{
FieldInfo fi = enumValue.GetType().GetField(enumValue.ToString());
DescriptionAttribute[] attributes =
(DescriptionAttribute[])fi.GetCustomAttributes(
typeof(DescriptionAttribute),
false);
if (attributes != null &&
attributes.Length > 0)
return attributes[0].Description;
else
return enumValue.ToString();
}
}
Define it like this:
public enum Roles
{
[LocalizedDescription("Administrator", typeof(Resource))]
Administrator,
...
}
And use it like this:
var roles = from RoleType role in Enum.GetValues(typeof(RoleType))
select new
{
Id = (int)role,
Name = role.GetDescription()
};
searchModel.roles = new MultiSelectList(roles, "Id", "Name");
There is a way of using attributes to specify a string to use for enums when displaying them, but we found it way too fiddly when you had to handle localization.
So what we usually do for enums that need to be localized is to write an extension class that provides a method to obtain the translated name. You can just use a switch that returns strings from the usual resources. That way, you provide translated strings for enums via the resources just like you do for other strings.
For example:
public enum Role
{
Administrator,
Moderator,
Webmaster,
Guest
}
public static class RoleExt
{
public static string AsDisplayString(this Role role)
{
switch (role)
{
case Role.Administrator: return Resources.RoleAdministrator;
case Role.Moderator: return Resources.RoleModerator;
case Role.Webmaster: return Resources.RoleWebmaster;
case Role.Guest: return Resources.RoleGuest;
default: throw new ArgumentOutOfRangeException("role");
}
}
}
Which you can use like this:
var role = Role.Administrator;
Console.WriteLine(role.AsDisplayString());
If you keep the RoleExt
class implementation next to the enum Role
implementation it will effectively become part of the interface for Role
. Of course you could also add to this class any other useful extensions for the enum .
[EDIT]
If you want to handle multiple flags settings ("Administrator AND Moderator AND Webmaster") then you need to do things a little differently:
[Flags]
public enum Roles
{
None = 0,
Administrator = 1,
Moderator = 2,
Webmaster = 4,
Guest = 8
}
public static class RolesExt
{
public static string AsDisplayString(this Roles roles)
{
if (roles == 0)
return Resources.RoleNone;
var result = new StringBuilder();
if ((roles & Roles.Administrator) != 0)
result.Append(Resources.RoleAdministrator + " ");
if ((roles & Roles.Moderator) != 0)
result.Append(Resources.RoleModerator + " ");
if ((roles & Roles.Webmaster) != 0)
result.Append(Resources.RoleWebmaster + " ");
if ((roles & Roles.Guest) != 0)
result.Append(Resources.RoleGuest + " ");
return result.ToString().TrimEnd();
}
}
Which you might use like this:
Roles roles = Roles.Administrator | Roles.Guest | Roles.Moderator;
Console.WriteLine(roles.AsDisplayString());
Resource Files
Resource files are the way that you internationalize your strings. For more information on how to use them, see here:
http://msdn.microsoft.com/en-us/library/vstudio/aa992030%28v=vs.100%29.aspx http://msdn.microsoft.com/en-us/library/vstudio/756hydy4%28v=vs.100%29.aspx