Localizing enum values
Enum members are localized by naming convention: add one string entry per member to any of your
.resx files, named Enum_<EnumTypeName>_<MemberName>. No attributes on the enum, no extra code.
public enum FileSortOrder { Unsorted, Ascending, Descending }
| Key | AppStrings.resx (English) |
AppStrings.de.resx (German) |
|---|---|---|
Enum_FileSortOrder_Unsorted |
Unsorted |
Unsortiert |
Enum_FileSortOrder_Ascending |
Ascending (A-Z) |
Aufsteigend (A-Z) |
Enum_FileSortOrder_Descending |
Descending (Z-A) |
Absteigend (Z-A) |
Like every other lookup, enum labels update live when the culture changes, and the same three usages work identically in Avalonia and WPF.
In XAML: {l:LocalizeEnum}
{l:LocalizeEnum} localizes the enum value you give it in Value - a binding or a fixed member:
<TextBlock Text="{l:LocalizeEnum Value={Binding SortOrder}}" />
<TextBlock Text="{l:LocalizeEnum {Binding SortOrder}}" /> <!-- the same, shorter -->
<TextBlock Text="{l:LocalizeEnum Value={x:Static m:FileSortOrder.Ascending}}" /> <!-- a fixed member -->
The text follows the bound value: when SortOrder changes, so does the text. A bound value that is
not an enum - for example a null property while the view loads - renders an empty string.
A fixed member is written with {x:Static}. Value=Ascending is the string "Ascending", not the
enum member, and {l:LocalizeEnum} throws InvalidOperationException for it, as it does when Value
is missing.
Value accepts the same bindings as an <l:Argument> Value: in Avalonia any binding, in WPF a plain
{Binding} only - a MultiBinding or a PriorityBinding throws InvalidOperationException. See
Dynamic format arguments.
In item templates
Inside a ComboBox or ListBox item template each item is the enum value - it is the
DataContext - so Value={Binding}, a binding without a path, localizes it:
<ComboBox ItemsSource="{Binding FileSortOrders}"
SelectedItem="{Binding SelectedFileSortOrder}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{l:LocalizeEnum Value={Binding}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
With format arguments
An enum label can have placeholders and take format arguments, as
<l:Argument> children - the same as for {l:Localize}. With a Count_FileSortOrder_Ascending entry
of {0} files, ascending:
<TextBlock>
<TextBlock.Text>
<l:LocalizeEnum Value="{Binding SortOrder}" KeyPrefix="Count_">
<l:Argument Value="{Binding FileCount}" />
</l:LocalizeEnum>
</TextBlock.Text>
</TextBlock>
<!-- "3 files, ascending" -->
The enum value itself is not an argument. A text that needs it, for example under a name, binds it explicitly:
<l:LocalizeEnum Value="{Binding SortOrder}">
<l:Argument Name="Kind" Value="{Binding SortOrder}" />
</l:LocalizeEnum>
Under the default formatter, a text that needs a plural - 1 file and 3 files - needs one enum
member per plural case: the default formatter is string.Format and does not choose between forms.
With the ICU MessageFormat formatter of the ResXLocalization.MessageFormat package, one member and a
plural pattern suffice, for example {0, plural, one {# file} other {# files}}; see
ICU MessageFormat.
Where a markup extension is not accepted: LocalizeEnumConverter
A markup extension does not work everywhere a binding is expected - for example in a property of type
Binding or BindingBase, such as DataGridTextColumn.Binding. A hand-written MultiBinding with
LocalizeEnumConverter does. Its children are, in this order:
- The refresh trigger:
RefreshTriggeringRevisionofLocalizer.Current, which re-runs the conversion whenever a lookup's result can change, such as on a language switch. - The enum value.
- From index 2 on, the format arguments, in order.
<!-- xmlns:core="clr-namespace:RentADeveloper.ResXLocalization;assembly=ResXLocalization.Core" -->
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{x:Static l:LocalizeEnumConverter.Default}">
<Binding Path="RefreshTriggeringRevision" Source="{x:Static core:Localizer.Current}" />
<Binding Path="SelectedFileSortOrder" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
A MultiBinding that puts the enum value first renders an empty string, and nothing reports it; check
the order whenever a converter shows nothing.
With format arguments
Every child from index 2 on is one positional argument: child 2 fills {0}, child 3 fills {1}, and so
on. A converter with a key prefix is declared as a resource; with a Count_FileSortOrder_Ascending entry
of {0} files, ascending:
<Window.Resources>
<l:LocalizeEnumConverter x:Key="CountConverter" KeyPrefix="Count_" />
</Window.Resources>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource CountConverter}">
<Binding Path="RefreshTriggeringRevision" Source="{x:Static core:Localizer.Current}" />
<Binding Path="SortOrder" />
<Binding Path="FileCount" /> <!-- {0} -->
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<!-- "3 files, ascending" -->
Two placeholders take two argument children, in placeholder order. With a Range_FileSortOrder_Ascending
entry of {0} of {1} files, ascending:
<Window.Resources>
<l:LocalizeEnumConverter x:Key="RangeConverter" KeyPrefix="Range_" />
</Window.Resources>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource RangeConverter}">
<Binding Path="RefreshTriggeringRevision" Source="{x:Static core:Localizer.Current}" />
<Binding Path="SortOrder" />
<Binding Path="ShownCount" /> <!-- {0} -->
<Binding Path="FileCount" /> <!-- {1} -->
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<!-- "3 of 10 files, ascending" -->
Named arguments: ArgumentNames
A MultiBinding child has no name. A formatter that understands named arguments, such as ICU
MessageFormat, gets the names from ArgumentNames on the converter: one comma-separated string whose
entry i names the argument at child 2 + i.
With such a formatter assigned to ILocalizer.MessageFormatter, the entry can use names. With a
Range_FileSortOrder_Ascending entry of {Shown} of {Total} files, ascending:
<Window.Resources>
<l:LocalizeEnumConverter x:Key="RangeConverter" KeyPrefix="Range_" ArgumentNames="Shown,Total" />
</Window.Resources>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource RangeConverter}">
<Binding Path="RefreshTriggeringRevision" Source="{x:Static core:Localizer.Current}" />
<Binding Path="SortOrder" />
<Binding Path="ShownCount" /> <!-- {Shown}, also {0} -->
<Binding Path="FileCount" /> <!-- {Total}, also {1} -->
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<!-- "3 of 10 files, ascending" -->
The default formatter, string.Format, ignores names and knows only positions: under it the entry must
use {0} and {1}, as in the previous example. {Shown} is not a valid composite format item, so it
renders the failure text and raises LookupFailed.
Entries are trimmed, and an empty entry leaves that argument unnamed: ",Total" names only the second.
Two entries with the same name throw ArgumentException. Extra names are ignored, and arguments without
an entry are unnamed. Every argument keeps its position whether it is named or not, and the default
formatter ignores names.
In code
Localizer.Current.Get(FileSortOrder.Ascending); // "Ascending (A-Z)" / "Aufsteigend (A-Z)"
With format arguments, and with a resource manager and a key prefix:
Localizer.Current.Get(FileSortOrder.Ascending, MessageArguments.FromValues(count), keyPrefix: "Count_"); // "3 files, ascending"
Localizer.Current.Get(FileSortOrder.Ascending, MessageArguments.FromValues(count), SortingStringsKeys.ResourceManager, "Count_");
The optional parameters after the value are arguments, resourceManager and keyPrefix; name the
ones you pass when you skip an earlier one.
Custom prefix and scoping
The markup extension, the converter and the code API all accept a KeyPrefix (by default
null, which means Enum_ - see Changing the key convention) and an
optional ResourceManager that scopes the lookup to one .resx file. That is what lets
the same enum carry different label sets, or keeps enum labels in a file of their own:
<TextBlock Text="{l:LocalizeEnum Value={Binding SortOrder},
KeyPrefix=Display_,
ResourceManager={x:Static res:SortingStringsKeys.ResourceManager}}" />
Localizer.Current.Get(FileSortOrder.Ascending, resourceManager: SortingStringsKeys.ResourceManager, keyPrefix: "Display_");
LocalizeEnumConverter.Default is read-only, so a converter with a custom prefix, a resource manager or
argument names is declared as its own instance in resources:
<l:LocalizeEnumConverter x:Key="DisplayEnumConverter" KeyPrefix="Display_" />
Changing the key convention
ILocalizer.EnumKeyBuilder is the function that turns an enum value and an optional key prefix into
the resource key. Set it once at startup, and it applies to every enum lookup - Get(Enum, …),
{l:LocalizeEnum} and LocalizeEnumConverter:
Localizer.Current.EnumKeyBuilder = static (value, keyPrefix) =>
(keyPrefix ?? "Text_") + value.GetType().Name + "." + value;
Localizer.Current.Get(FileSortOrder.Ascending); // looks up "Text_FileSortOrder.Ascending"
The function receives two parameters:
- The enum value of the lookup; never
null. - The key prefix the lookup passed -
keyPrefixin code,KeyPrefixin XAML - ornullwhen it passed none.nullmeans "use your own default"; a non-null value is an explicit override.
The default is the convention above, {keyPrefix ?? "Enum_"}{EnumTypeName}_{MemberName}, for example
Enum_FileSortOrder_Ascending. keyPrefix and KeyPrefix therefore override only the prefix: they
are passed to the builder, which decides what to do with them. An application that passes the same
prefix on every enum lookup can make it the builder's default instead.
Assigning a different function later re-resolves bound text, like a language switch; assigning the
current one again does nothing. The builder must not throw or return null or an empty string: an
exception it throws propagates out of the lookup, and a null or empty result makes the lookup throw
InvalidOperationException. Assigning null throws ArgumentNullException.
When a member has no entry
An enum member with no matching resource entry resolves like any other missing key: the
!Enum_FileSortOrder_Ascending! failure text, and a LookupFailed event. See
Lookup modes and culture fallback.