How can I make a formatted TextBlock width data binding localizable?
In my WPF application, I would like to display something that looks like this:
User Bob has logged off at 22:17.
Where "Bob" and "22:17" are data-bound values.
The obvious way to do this would be to use a StackPanel
with multiple TextBlock
children, some of them data bound:
<StackPanel Orientation="Horizontal">
<TextBlock Text="The user"/>
<TextBlock Text="{Binding Path=Username}" TextBlock.FontWeight="Bold" />
<TextBlock Text="has logged off at"/>
<TextBlock Text="{Binding Path=LogoffTime}" TextBlock.FontWeight="Bold" />
</StackPanel/>
This works, but it's ugly. The program is supposed to be localized to different languages, and having separate strings for "The user" and "has logged off at" is a recipie for localization disaster.
Ideally, I would like to do something like this:
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}The user <Bold>{0}</Bold> has logged off at <Bold>{1}</Bold>">
<Binding Path="Username" />
<Binding Path="LogoffTime" />
</MultiBinding>
</TextBlock>
So the translator would see a complete sentence The user <Bold>{0}</Bold> has logged 开发者_运维知识库off at <Bold>{1}</Bold>
. But that doesn't work, of course.
This has to be a common problem, what's the right solution for this?
The issue I see is that you want a single String
yet with a different UI presence across the String
.
One option could be to dismiss the need for bold and simply place the single String
within the Resources.resx file. That String
will then be referenced in the property which the TextBlock
is bound to, returning the values as needed; {0} and {1} where applicable.
Another option could be to returning a set of Run
values to be held within the TextBlock
. You can not bind to a Run
out of the box in 3.5 however I believe you can in 4.
<TextBlock>
<Run Text="The user "/><Run FontWeight="Bold" Text="{Binding User}"/><Run Text="has logged at "/><Run FontWeight="Bold" Text="{Binding LogoffTime}"/>
</TextBlock>
The last option can be found here and involves creating a more dynamic approach to the Run
concept, allowing you to bind your values and then tie them back to a String
.
I've never tried to do something like this before, but if I had to I would probably try and use a Converter that takes the MultiBinding and breaks it up and returns a StackPanel of the pieces
For example, the binding would be something like:
<Label>
<Label.Content>
<MultiBinding Converter={StaticResource TextWithBoldParametersConverter}>
<Binding Source="The user {0} has logged off at {1}" />
<Binding Path="Username" />
<Binding Path="LogoffTime" />
</MultiBinding>
</Label.Content>
</Label>
And the Converter would do something like
public class TextWithBoldParametersConverter: IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// Create a StackPanel to hold the content
// Set StackPanel's Orientation to Horizontal
// Take values[0] and split it by the {X} tags
// Go through array of values parts and create a TextBlock object for each part
// If the part is an {X} piece, use values[X+1] for Text and make TextBlock bold
// Add TextBlock to StackPanel
// return StackPanel
}
}
There's unlikely to be a single solution to this problem because there are so many different ways it can manifest itself, and there are so many different ways to solve it.
If you're Microsoft, the solution is to put all of your templates in a resource dictionary, create a project that can use Expression Blend to present them, and then have your translators work with Blend (and probably someone who can help them use it) to translate the text in the resource dictionary, reordering the elements in the template where there are idiomatic differences in word order. This is of course the most expensive solution, but it has the advantage of getting formatting problems (like someone forgot that French text occupies about 20% more space than English text) solved at the same time that the UI is being translated. It also has the advantage that it handles every presentation of text in the UI, not just the presentations that are created by stacking together text blocks.
If you're really only going to need to fix stacks of text blocks, you can create a simple XML representation of marked-up text, and use XSLT to create your XAML from a file in that format. For instance, something like:
<div id="LogoutTime" class="StackPanel">
The user
<strong><span class="User">Bob</span><strong>
logged out at
<strong><span class="Time">22:17</span></strong>
.
</div>
By an amazing coincidence, that markup format is one that can also be viewed in a web browser, so it can be edited with a really wide range of tools and proofread without using, say, Expression Blend. And it's relatively straightforward to translate back into XAML:
<xsl:template match="div[@class='StackPanel']">
<DataTemplate x:Key="{@id}">
<StackPanel Orientation="Horizontal">
<xsl:apply-templates select="node()"/>
</StackPanel>
</DataTemplate>
</xsl:template>
<xsl:template match="div/text()">
<TextBlock Text="{.}"/>
</xsl:template>
<xsl:template match="strong/span">
<TextBlock FontWeight="Bold">
<xsl:attribute name="Text">
<xsl:text>{Binding </xsl:text>
<xsl:value-of select="@class"/>
<xsl:text>}</xsl:text>
</xsl:attribute>
</TextBlock>
</xsl:template>
精彩评论