Localization of text that includes subscript and inline images in WPF
I have a small WPF application that I am working on localizing. I have read thru a number of documents but I have not found a good source of information about dealing with 'rich' content - for example:
I have a ui element (TextBlock) that contains a mix of text with formatting, inline images and symbols. I want to localize this element. I believe to localize this element in a way that is natural in another language that the position of the formulas/symbols will need to shift in relation to the surrounding text - and that the best formatting fo开发者_Go百科r the text may be slightly different in other languages as well.
I am looking for suggestions, resources and/or approaches for localizing 'rich' content (content that mixes text, formatting and inline ui elements) in XAML/WPF. Given the emphasis on composition and 'rich' UI in WPF I was surprised not to find any information on the scenario above - what am I missing?
I have thought about storing XAML in the resource file and then parsing it at runtime for inclusion in the UI and about creating a view/usercontrol that is swapped out based on locale - but I am not seeing any mention of these approaches (which makes me wonder if I am on the 'wrong track') and am hoping someone has experience or information to share?
Thanks!
The StaticResource
markup extension works very well for what you are trying to accomplish. You can include almost anything using a StaticResource, even when a DynamicResource doesn't work:
<Window ...>
<Window.Resources>
<Span x:Key="Whatever">
<Bold>Hello</Bold> there<LineBreak/>
A green circle:
<InlineUIContainer>
<Ellipse Width="10" Height="10" Fill="Green" />
</InlineUIContainer>
</Inline>
</Window.Resources>
...
<TextBlock>
<StaticResource ResourceKey="Whatever" />
</TextBlock>
Now localizing this is easy:
- Move the
Span
resource into a separateResourceDictionary
and merge into your Application's resource dictionary. - During application startup and before any windows are created, use code to get the current culture and add an additional language-specific
ResourceDictionary
toapplication.Resources.MergedDictionaries
. The satellite dictionary can be loaded using WPF's built-in localization mechanism.
As long as the dictionares are merged into the application dictionary in the correct order, any named resource that is found in the localized dictioary will take precendence over the one in the main dictionary, for example your Spanish localization dll could have a xaml file containing this:
<ResourceDictionary>
<Span x:Key="Whatever">
El círculo rojo
<InlineUIContainer>
<Ellipse Width="10" Height="10" Fill="Red" />
</InlineUIContainer>
dice <Bold>hola!</Bold>
</Span>
</ResourceDictionary>
Note that the message is similar, but for Spanish the circle is red and the layout of the text is different.
You can take this much further with ControlTemplates if you want to. Using ControlTemplates will allow you to do such things as have buttons laid out in a different order depending on the locale. For example if your generic dictionary contains:
<ResourceDictionary>
<ControlTempate x:Key="Something" TargetType="ContentControl">
<StackPanel>
<TextBlock Text="In English we want the text above the button" />
<ContentPresenter />
</StackPanel>
</ControlTemplate>
</ResourceDictionary>
You can add this to your window or user control:
<ContentControl Template="{StaticResource Something}">
<Button Command="Save">Save File</Button>
</ContentControl>
And then you change the layout for another language, for example:
<ResourceDictionary>
<ControlTempate x:Key="Something" TargetType="ContentControl">
<DockPanel>
<ContentPresenter DockPanel.Dock="Left" />
<TextBlock Text="En español en el botón a la izquierda del texto" />
<!-- In spanish the button is to the left of the text -->
</DockPanel>
</ControlTemplate>
</ResourceDictionary>
Note: If you only use localization in DependencyProperties (eg no InlineCollections, etc) you can get away with using {DynamicResource}
which allows the locale to change at any time with an instant update of the UI. To do this with my first example, instead of including a <Span>
in the ResourceDictionary
and including it inside a TextBlock, you can put the TextBlock in a ControlTemplate inside the ResourceDictionary.
This is only the beginning of the flexibility of localization with WPF. You can go much further.
Let's say you have a text:
"Hello, my <b>dear</b> user!"
And you want to localize that text into some other language:
"Привет, мой <b>дорогой</b> пользователь!!"
So, your localization framework should either store it as it is in one whole string with all formatting and rely on translators to be patient and hard-working and to preserve all formatting tags. Or if formatting is very important, then you should split the string into (3) parts and apply formatting when string is constructed and store each part as individual entry in translations store.
EDIT: If your rich-text contains an image reference, then you should localize the way link to image is produced with something like this:
<img src="$current_locale/logo.jpg" />
or even have a function that will return a default image location if image for current locale is missing:
<img src="$get_current_locale_or_default_locale_image(logo.jpg)" />
I marked the answer by Ray Burns using the StaticResource extension as the accepted answer, I think alot of the details about that approach are perfect. I also wanted to show another idea - using T4 templates to generate 'loose' xaml files that are included in the output and parsed at runtime based on the CurrentCulture.
The code below is the contents of the file base_block.tt. One detail that is important below is the use of encoding="Unicode" (I assumed Utf-8 would work but the XamlParser would error on some characters when the template specified Utf-8, apparently because of the BOM setting).
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".xaml" encoding="Unicode"#>
<UserControl
xml:lang="<#= this.xml_lang #>"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="basic_styles.xaml" />
<ResourceDictionary Source="equations.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<WrapPanel Style="{StaticResource description_wrap_panel_style}">
<TextBlock x:Name="c_textblock"
Style="{StaticResource description_textblock_style}"
AutomationProperties.Name = "<#= this.textblock_automation_name #>">
<#= this.textblock_constant_C_contents #>
</TextBlock>
<TextBlock Style="{StaticResource description_textblock_style}"
KeyboardNavigation.TabIndex="1">
<#= this.hyperlink_textblock_contents #>
</TextBlock>
<TextBox Style="{StaticResource entry_textbox_style}"
AutomationProperties.LabeledBy="{Binding ElementName=c_textblock}"
KeyboardNavigation.TabIndex="0">
</TextBox>
</WrapPanel>
</UserControl>
<#+
private string xml_lang = @"";
private string textblock_constant_C_contents = @"";
private string textblock_automation_name = @"";
private string hyperlink_textblock_contents = @"";
#>
base_block.tt can be used in an include in a .tt file that specifies the needed values - for English here is my en.tt file which will generate en.xaml:
<#
xml_lang = @"en-US";
textblock_constant_C_contents =
@"Enter a constant, <Italic>C</Italic>, that satisfies
<InlineUIContainer Style='{StaticResource image_container_style}'>
<Image x:Name='formula_11' Source='{StaticResource equation_11}' Style='{StaticResource image_style}' Tag='3.0'>
<Image.Height>
<MultiBinding Converter='{StaticResource image_size}'>
<Binding Mode='OneWay' ElementName='formula_11' Path='Tag'/>
<Binding Mode='OneWay' ElementName='c_textblock' Path='FontSize'/>
</MultiBinding>
</Image.Height>
</Image>
</InlineUIContainer>";
textblock_automation_name = @"Enter a Constant, C, that satisfies the following equation: the standard error of the estimate is equal to the constant C over the square root of the sample size";
hyperlink_textblock_contents = @"(<Hyperlink AutomationProperties.Name='More information about the constant C'
x:Name='c_hyperlink'>more info</Hyperlink>)";
#>
<#@ include file="base_block.tt" #>
Or for French - fr.tt -> fr.xaml:
<#
xml_lang = @"fr";
textblock_constant_C_contents =
@"Entrez une constante, <Italic>C</Italic>, pour satisfaire
<InlineUIContainer Style='{StaticResource image_container_style}'>
<Image x:Name='formula_11' Source='{StaticResource equation_11}' Style='{StaticResource image_style}' Tag='3.0'>
<Image.Height>
<MultiBinding Converter='{StaticResource image_size}'>
<Binding Mode='OneWay' ElementName='formula_11' Path='Tag'/>
<Binding Mode='OneWay' ElementName='c_textblock' Path='FontSize'/>
</MultiBinding>
</Image.Height>
</Image>
</InlineUIContainer>";
textblock_automation_name = @"Entrez une constante, C, qui satisfait l'équation suivante: l'erreur-type de l'estimation est égale à la constante C sur la racine carrée de la taille de l'échantillon.";
hyperlink_textblock_contents = @"(<Hyperlink AutomationProperties.Name=""Plus d'informations sur la constante C"">en savoir plus</Hyperlink>)";
#>
<#@ include file="base_block.tt" #>
The French above generates the following .xaml file:
<UserControl
xml:lang="fr"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="basic_styles.xaml" />
<ResourceDictionary Source="equations.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<WrapPanel Style="{StaticResource description_wrap_panel_style}">
<TextBlock x:Name="c_textblock"
Style="{StaticResource description_textblock_style}"
AutomationProperties.Name = "Entrez une constante, C, qui satisfait l'équation suivante: l'erreur-type de l'estimation est égale à la constante C sur la racine carrée de la taille de l'échantillon.">
Entrez une constante, <Italic>C</Italic>, pour satisfaire
<InlineUIContainer Style='{StaticResource image_container_style}'>
<Image x:Name='formula_11' Source='{StaticResource equation_11}' Style='{StaticResource image_style}' Tag='3.0'>
<Image.Height>
<MultiBinding Converter='{StaticResource image_size}'>
<Binding Mode='OneWay' ElementName='formula_11' Path='Tag'/>
<Binding Mode='OneWay' ElementName='c_textblock' Path='FontSize'/>
</MultiBinding>
</Image.Height>
</Image>
</InlineUIContainer>
</TextBlock>
<TextBlock Style="{StaticResource description_textblock_style}"
KeyboardNavigation.TabIndex="1">
(<Hyperlink AutomationProperties.Name="Plus d'informations sur la constante C">en savoir plus</Hyperlink>)
</TextBlock>
<TextBox Style="{StaticResource entry_textbox_style}"
AutomationProperties.LabeledBy="{Binding ElementName=c_textblock}"
KeyboardNavigation.TabIndex="0">
</TextBox>
</WrapPanel>
</UserControl>
At runtime I look at the CurrentCulture, compare that to the generated xaml files that are available, feed the file to XamlReader.Load() and add the resulting Usercontrol in where needed. A small sample app demostrating this is a available here.
精彩评论