sudo localize --crossplatform [Free PseudoLocalizer class makes it easy to identify localization issues in WPF, Silverlight, and Windows Phone 7 applications!]
Two posts ago, I explained the benefits of pseudo-localization and showed an easy way to implement it for WPF - then said I'd outline how to do the same for Silverlight and Windows Phone 7. In my previous post, I went off on the seeming diversion of implementing a PNG encoder for Silverlight. With this post, I'll fulfill my original promise and unify the previous two posts! As you'll see, the basic principles of my approach to WPF localization translate fairly directly to Silverlight - though some limitations in the latter platform make achieving the same result more difficult. Even though more code and manual intervention are required for Silverlight and Windows Phone 7, the benefits are the same and pseudo-localization remains a great way to identify potential problems early in the development cycle.
For completeness I'll show examples and techniques for all platforms below...
Please see the original post for an explanation of the changes shown above.
Adding pseudo-localization of RESX resources to a WPF application
-
Add the
PseudoLocalizer.cs
file from the sample download to the project. -
Add
PSEUDOLOCALIZER_ENABLED
to the (semi-colon-delimited) list of conditional compilation symbols for the project (via the Project menu, Properties item, Build tab in Visual Studio). -
Add the following code somewhere it will be run soon after the application starts (for example, add a constructor for the
App
class inApp.xaml.cs
):#if PSEUDOLOCALIZER_ENABLED Delay.PseudoLocalizer.Enable(typeof(ProjectName.Properties.Resources)); #endif
-
If necessary: Add a project reference to
System.Drawing
(via Project menu, Add Reference, .NET tab) if building the project now results in the error"The type or namespace name 'Drawing' does not exist in the namespace 'System' (are you missing an assembly reference?)"
. -
If necessary: Right-click
Resources.resx
and choose Run Custom Tool if running the application under the debugger (F5) throws the exception"No matching constructor found on type 'ProjectName.Properties.Resources'. You can use the Arguments or FactoryMethod directives to construct this type."
.
Adding pseudo-localization of RESX resources to a Silverlight or Windows Phone 7 application
-
Add the
PseudoLocalizer.cs
andPngEncoder.cs
files from the sample download to the project. -
Add
PSEUDOLOCALIZER_ENABLED
to the (semi-colon-delimited) list of conditional compilation symbols for the project (via the Project menu, Properties item, Build tab in Visual Studio). -
Make the following update to the auto-generated resource wrapper class by editing
Resources.Designer.cs
directly (the highlighted portion is the primary change):#if PSEUDOLOCALIZER_ENABLED global::System.Resources.ResourceManager temp = new Delay.PseudoLocalizerResourceManager("PseudoLocalizerSL.Resources", typeof(Resources).Assembly); #else global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PseudoLocalizerSL.Resources", typeof(Resources).Assembly); #endif
In case it's not clear, this change simply duplicates the existing line of code that creates an instance of ResourceManager, modifies it to create an instance of
Delay.PseudoLocalizerResourceManager
instead, and wraps the two versions in an appropriate#if
/#else
/#endif
so pseudo-localization can be completely controlled by whether or notPSEUDOLOCALIZER_ENABLED
is#define
d.Important: This change will be silently overwritten the next time (and every time!) you make a change to
Resources.resx
with the Visual Studio designer. Please see my notes below for more information on this Silverlight-/Windows Phone-specific gotcha.
Adding a pseudo-localizable string (all platforms)
-
Double-click
Resources.resx
to open the resource editor. -
Add the string by name and value.
-
Reference it from code/XAML.
-
Silverlight/Windows Phone 7: Re-apply the
Delay.PseudoLocalizerResourceManager
change toResources.Designer.cs
which was silently undone when the new resource was added.
Adding a pseudo-localizable image (WPF only)
-
Double-click
Resources.resx
to open the resource editor. -
Click the "expand" arrow for Add Resource and choose Add Existing File....
-
Open the desired image file.
-
Reference it from code/XAML (possibly via
BitmapToImageSourceConverter.cs
from the sample ZIP).
Adding a pseudo-localizable image (all platforms)
-
Rename the image file from
Picture.jpg
toPicture.jpg-bin
. -
Double-click
Resources.resx
to open the resource editor. -
Click the "expand" arrow for Add Resource and choose Add Existing File....
-
Open the desired image file.
-
Reference it from code/XAML (probably via
ByteArrayToImageSourceConverter.cs
from the sample ZIP). -
Silverlight/Windows Phone 7: Re-apply the
Delay.PseudoLocalizerResourceManager
change toResources.Designer.cs
which was silently undone when the new resource was added. -
Optionally: Restore the image's original file name in the
Resources
folder of the project and manually update its file name inResources.resx
using a text editor like Notepad. (I've done this for the sample project; it makes things a little clearer and it's easier to edit the image resource without having to rename it each time.)
There you have it - simple text and image pseudo-localization for WPF, Silverlight, and Windows Phone 7 applications is within your grasp! PseudoLocalizer
early (and often) should far outweigh any inconvenience along the way. By finding (and fixing) localization issues early, your application will be more friendly to customers - no matter what language they speak!
Notes
-
For a brief overview of using RESX resources in a WPF, Silverlight, or Windows Phone 7 application, please see the "Notes" section of my original PseudoLocalizer post. You'll want to be sure the basic stuff is all hooked up and working correctly before adding
PseudoLocalizer
into the mix. -
The act of using RESX-style resources in a Silverlight application is more difficult than it is in a WPF application (independent of pseudo-localization). WPF allows you to directly reference the generated resources class directly from XAML:
<Window.Resources> <properties:Resources x:Key="Resources" xmlns:properties="clr-namespace:PseudoLocalizerWPF.Properties"/> </Window.Resources> ... <TextBlock Text="{Binding Path=Message, Source={StaticResource Resources}}"/>
However, that approach doesn't work on Silverlight (or Windows Phone 7) because the generated constructor is internal and Silverlight's XAML parser refuses to create instances of such classes. Therefore, most people create a wrapper class (as Tim Heuer explains here):
/// <summary> /// Class that wraps the generated Resources class (for Resources.resx) in order to provide access from XAML on Silverlight. /// </summary> public class ResourcesWrapper { private Resources _resources = new Resources(); public Resources Resources { get { return _resources; } } }
And reference that instead:
<UserControl.Resources> <local:ResourcesWrapper x:Key="Resources" xmlns:local="clr-namespace:PseudoLocalizerSL"/> </UserControl.Resources> ... <TextBlock Text="{Binding Path=Resources.Message, Source={StaticResource Resources}}"/>
Obviously, the extra level of indirection adds overhead to every place resources are used in XAML - but that's a small price to pay for dodging the platform issue.
:) -
WPF supports private reflection and
PseudoLocalizer
takes advantage of that to enable a simple, seamless, "set it and forget it" hook-up (via the call toEnable
above). Unfortunately, private reflection isn't allowed on Silverlight, so the same trick doesn't work there. I considered a variety of different ways around this, and ultimately settled on editing the generated wrapper class code because it applies exactly the same customization as on WPF. And while it's pretty annoying to have this tweak silently overwritten every time the RESX file is edited, it's simple enough to re-apply and it's easy to spot when reviewing changes before check-in. -
I explained what's wrong with the default behavior of adding an image to a RESX file in my PngEncoder post:
[...] the technique I used for [WPF] (reading the System.Drawing.Bitmap instance from the resources class and manipulating its pixels before handing it off to the application) won't work on Silverlight. You see, the
System.Drawing
namespace/assembly doesn't exist for Silverlight! So although the RESX designer in Visual Studio will happily let you add an image to a Silverlight RESX file, actually doing so results in an immediate compile error [...].Fortunately, the renaming trick I use above works well for Silverlight and Windows Phone - and WPF, too. So if you're looking to standardize on a single technique, this is the one.
:) Even if you're devoted to WPF and don't care about Silverlight, you should still consider the
byte[]
approach: althoughSystem.Drawing.Bitmap
is easier to deal with, it's not the right format. (Recall from the originalPseudoLocalizer
post that I wrote an IValueConverter to convert from it to System.Windows.Media.ImageSource.) Instead of loading images asSystem.Drawing.Bitmap
and converting them withBitmapToImageSourceConverter
, why not load them asbyte[]
and convert them withByteArrayToImageSourceConverter.cs
- and save a few CPU cycles by not bouncing through an unnecessary format? -
In addition to the renaming technique for accessing RESX images from Silverlight, there's a similar approach (courtesy of Justin Van Patten) that renames to
.wav
instead and exposes the resource as a System.IO.Stream. For the purposes of pseudo-localization, the two renaming approaches should be basically equivalent - which led me to go as far as hooking everything up and writingStreamToImageSourceConverter.cs
before I realized why theStream
approach isn't viable...What it comes down to is an unfortunate API definition - the thing that's exposed by the wrapper class isn't a
Stream
, it's an UnmanagedMemoryStream! And while that would be perfectly fine as an implementation detail, it's not: the type of the auto-generated property isUnmanagedMemoryStream
and the type returned by ResourceManager.GetStream is alsoUnmanagedMemoryStream
. ButUnmanagedMemoryStream
can't be created by user code in Silverlight (and requires unsafe code in WPF), so this breaksPseudoLocalizer
's approach of decoding/pseudo-localizing/re-encoding the image because it means the altered bytes can't be wrapped back up in aUnmanagedMemoryStream
to maintain the necessary pass-through behavior!If only the corresponding RESX interfaces had used the
Stream
type (a base class ofUnmanagedMemoryStream
), it would have been possible to wrap the altered image in a MemoryStream and return that - a technique supported by all three platforms. Without digging into this too much more, it seems to me that theStream
type could have been used with no loss of generality - though perhaps there's a subtlety I'm missing.Aside: As a general API design guideline, always seek to expose the most general type that makes sense for a particular scenario. That does not mean everything should expose the Object type and cast everywhere - but it does mean that (for example) APIs exposing a stream should use the
Stream
type and thus automatically work withMemoryStream
,UnmanagedMemoryStream
, NetworkStream, etc.. Only when an API needs something from a specific subclass should it use the more specific subclass.Be that as it may, I didn't see a nice way of wrapping images in a
UnmanagedMemoryStream
, and therefore recommend using thebyte[]
approach instead!