Essential WPF

WPF Localization - On-the-fly Language Selection

Download code ver 1.3 from here.

 

Many of my customers asked me:

How WPF supports localization and globalization?

Is there any built-in mechanism for choosing a language on-the-fly, at runtime and without closing any window?

How do I translate formatted text with parameters?

Well..., you know the story,... aren't you!? No? So start by reading this, and this. Still confused? continue with this and also this.

 

As you can see, there is more than one solution. Each has its pros and cons. The WPF official language support provides a complex localization set of API's, and a bizarre SDK tool for translation and creation of localized assemblies. Others provide unofficial great ideas for localizing WPF based on .NET 2.0 resource files, WPF resource dictionary and XML.

 

I decided to share another mechanism for localizing WPF applications.

Download the code from here.

 

Why did I bother to invent another solution, and why would you want to use my solution

  1. It provides an option for replacing languages at runtime, on-the-fly
  2. It performs better than a lame XML, XPath based binding solution
  3. It can be used via Styles, Control Templates and Data Templates
  4. It translates a formatted text with parameters, using default and custom formatters
  5. It provides a Translate custom markup-extension to write an elegant XAML

 

I will start by discussing the markup snippet bellow:

<Window x:Class="Tomers.WPF.Localization.MainWindow" x:Name="Root"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:loc="http://schemas.tomer.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Tomers.WPF.Localization"
xmlns:d="http://schemas.microsoft.com/expression/blend/2006"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"

Background="{StaticResource WindowBrush}"

loc:Translate.Uid="0"

Title="{loc:Translate}"
Width="{loc:Translate}"
Height="{loc:Translate}"
FlowDirection="{loc:Translate}">

<Window.Resources>
<DataTemplate DataType="{x:Type local:Data}">
<TextBlock Margin="8" HorizontalAlignment="Left">
<TextBlock.Text>
<loc:Translate>
<Binding Path
="Uid" />
<Binding Path
="ID" />
</loc:Translate
>
</TextBlock.Text>
<TextBlock.Foreground>
<loc:Translate>
<Binding Path
="Uid" />
</loc:Translate
>
</TextBlock.Foreground>
</TextBlock>
</DataTemplate>
</Window.Resources>

<StackPanel x:Name="_panel">

<Label loc:Translate.Uid="1"
Content="{loc:Translate}"
FontSize="14" />

<TextBlock loc:Translate.Uid="2" FontSize="16">
<TextBlock.Text>
<loc:Translate>
<Binding ElementName="Root" Path
="Width" />
<Binding ElementName="Root" Path
="Height" />
</loc:Translate
>
</TextBlock.Text>
</TextBlock>

<TextBlock loc:Translate.Uid="3"
Text="{loc:Translate}"
Background="{loc:Translate}"
Width="{loc:Translate}"
Height="{loc:Translate}" FontSize="18" />

<TextBlock FontSize="18">
<TextBlock.Text>
<loc:Translate>
<Binding ElementName="Root" Path
="Uid" />
</loc:Translate
>
</TextBlock.Text>
</TextBlock>

<ContentControl x:Name="_elementI" FontSize="20" />
<ContentControl x:Name="_elementII" FontSize="20" />

<Button loc:Translate.Uid="7"
Margin="8"
HorizontalAlignment="Right"
Content="{loc:Translate}"
Width="200"
Click="Button_Click" FontSize="16" />

</StackPanel>
</
Window>

image image   imageimage

As you can see, the markup snippet produces the window above. When clicking the "Click to change language" button, all the elements attached with the "loc:Translate" custom markup-extension, are automatically have new values: In our case the language, alignment, size, position, color and other.

 

So why did I choose to use a custom markup extension, what's wrong with a simple Data Binding markup

Well, this is the magic of my solution. It is possible to use a complex binding expression indeed, for example:

<Label Content="{Binding Source={x:Static loc:LanguageContext.Instance}, Path=Dictionary, Mode=OneWay
Converter={StaticResource languageConverter}, ConverterParameter=2}
" />

But as you can see, it's very long expression. Now try to imagine how long it will be if you pass parameters...

 

What's in the package

There are few types you should know before you start localizing:

  1. LanguageDictionary - This is an abstract class. It provides an abstraction for translating values by exposing two important methods: Load and Translate. The Load method loads the repository with data for each locale, and the Translate method translates a key-value pair into locale value from the repository. For example, I wrote a XmlLanguageDictionary class that loads and stores an XML language file inside a dictionary (you can find it in the LocalizationDemo project).
  2. LanguageContext - This singleton type holds the culture and the dictionary of the active language. It also plays an important role by notifying on culture change.
  3. LanguageConverter - This important type calls the active dictionary to translate values each time a language is replaced. The language converter is instantiated by the TranslateExtesnion during the internal-binding operation (see TranslateExtesnion type bellow). The LanguageConverter implements both IValueConverter and IMultiValueConverter interfaces.
  4. TranslateExtension - This is the magic of this solution. It provides the user an option to bind a localized-property to the language dictionary using a simple and short markup within XAML, as demonstrated in the markup snippet above. By extracting the target element and the dependency property, it binds the LanguageContext.Dictionary to the targets property.

 

How to use it

First, you have to create your own language-dictionary by deriving LanguageDictionary. You can also start by using my XmlLanguageDictionary, provided as a lame example inside the LocalizationDemo project.

Second, you should create and register a dictionary instance for each language and select the default one, before you create any localized UI element. See the markup bellow:

    /// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
LanguageDictionary.RegisterDictionary(
CultureInfo.GetCultureInfo("en-US"),
new XmlLanguageDictionary("Languages/en-US.xml"));

LanguageDictionary.RegisterDictionary(
CultureInfo.GetCultureInfo("he-IL"),
new XmlLanguageDictionary("Languages/he-IL.xml"));

LanguageContext.Instance.Culture = CultureInfo.GetCultureInfo("en-US");

base.OnStartup(e);
}
}

Now, use the Translate markup extension to localize elements as demonstrated above.

 

To translate a simple element with no parameters use the following syntax (The uid value, Content and FontSize should be keys in your dictionary back-storage - see my xml files for an example):

<Label loc:Translate.Uid="1" Content="{loc:Translate}" FontSize="{loc:Translate}" />

 

To translate an element with parameters use the following syntax

<TextBlock loc:Translate.Uid="2" FontSize="16">
<TextBlock.Text>
<loc:Translate>
<Binding ElementName="Root" Path
="Width" />
<Binding ElementName="Root" Path
="Height" />
</loc:Translate
>
</TextBlock.Text>
</TextBlock>

 

To translate an element with a dynamic Uid, use the previous syntax, where the Uid must be the first parameter (this is a great solution for styles and templates):

<TextBlock FontSize="18">
<TextBlock.Text>
<loc:Translate>
<Binding ElementName="Root" Path="Uid" />
</loc:Translate>
</TextBlock.Text>
</TextBlock>

 

There are several more issues that should be considered, also the code should be refactored for production. Use it on your own risk.

Download code ver 1.0 from here.

Download code ver 1.1 from here.

 

Update: On 13-Aug-2008

- Markup extension bug fixed

- Fully supported on both Blend and Cider designers

- Default fallback value was added

- Demo visuals improvements


Download code ver 1.2 from here.

 

Update: On 06-Feb-2009

- Uid's can be provided directly via Translate markup (now works with Setters - see demo)

- Demo upgrade to support new feature


Download code ver 1.3 from here.

 

Comments

C. Marius said:

Hi,

How about support for Blend 2(September CTP). As I lost design support in Blend, it gets pretty impossible to use. I rather use the simple solution of Content={x:Static p:Resources.MyResourceName} which offers design-time support for VS2008 and Blend. Isn't something that could be done for this?

Thank you (c_marius@msn.com)

# November 9, 2007 5:44 PM

Tomer Shamam said:

Hi Marius,

Indead, there is a problem with Blend and Custom Markup Extensions. Blend 2 can deal with it, but not 100% (you will not get the translated values at design time, but at least it will show you the UI).

You can always use a Binding expression with a custom Value Converter instead of my Translate extension, but you should write alot of markup for each element (Path, Source, Converter, Parameter, etc).

Currently, I'm working on another solution for design time, using Data Provider.

# November 11, 2007 2:15 PM

Irfan Baig said:

Hi

What about using resources in code? Can you provide any samples?

# December 6, 2007 9:53 AM

Tomer Shamam said:

Hi,

You should bind your properties to the dictionary from code:

Binding binding = new Binding("Dictionary");

binding.Source = LanguageContext.Instance;

LanguageConverter converter = new LanguageConverter(uid, vid);

binding.Converter = converter;

BindingOperations.SetBinding(yourTargetObject, yourTargetProperty, binding);

You can wrap this code in a class, lets say: LanguageBinder, to prevent writing this code over and over again.

Tomer

# December 6, 2007 6:08 PM

Evgeni said:

Tomer Shalom!

Nice work you did here, I really like this approach. I have one question regarding implementation: when I passed over your code, I saw you using two way binding (when you creating binding inside markup extension). Well, I expected to use one way binding, because I interesting to monitor only source changes. However, when I changed to one way, it stopped to work. In addition, I saw that no one anymore subscribing to "PropertyChanged" event of the dictionary. Could you please explain this point? (why one way binding is not works here).

Thanks,

Evgeni

# March 31, 2008 7:12 AM

Rob Relyea - Xamlified said:

My team has been looking how to make localization support in WPF better.&#160; We hope to improve things

# April 10, 2008 3:46 PM

Xavi said:

There is a bug in your code:

If you are using a DataTemplate in a UserControl, in TranslateExtension.ProvideValue method, you receive a System.Windows.SharedDp object in service.TargetProperty. As you make a cast to DependencyObject type, it returns "this", so translation is not made.

# July 23, 2008 11:30 AM

Tomer Shamam said:

Hi Xavi,

As I already wrote: "the code should be refactored for production. Use it on your own risk"

This means that you may find one or more BUGS in my code.

I only gave the concept.

Cheers

# July 23, 2008 11:43 AM

Xavi said:

Maybe you have missunderstood me. My purpose was improving your great idea and learning how to fix the bug I found.

I didn't know you were only interested in giving th concept, not tracking its feedback.

Thanks anyway.

# July 23, 2008 11:53 AM

Tomer Shamam said:

Thanks for finding a BUG. It will help others to know about it and fix it.

# July 23, 2008 12:14 PM

Benny said:

Tomer,

What if you want to localize an error message that is generated in code?

# July 24, 2008 12:36 PM

Tomer Shamam said:

LanguageDictionary dictionary = LanguageDictionary.GetDictionary(

  LanguageContext.Instance.Culture);

string message = dictionary.Translate(

  "NotEnoughMemory", "Message", typeof(string)) as string;

 

English Entry:

<Value Id="NotEnoughMemory" Message="There is not enough memory" />

Hebrew Entry:

<Value Id="NotEnoughMemory" Message="אין מספיק זכרון" />

Cheers

# July 24, 2008 3:20 PM

Tomer Shamam said:

I have also updated the code to version 1.1. Now you can use a simple form to get any message from code:

string message = LanguageDictionary.Current.Translate<string>("AnyKey", "AnyMessage");

Hope this helps.

# July 24, 2008 3:40 PM

SeriousM said:

hey, please have a look at my engine! i dont have these problems andmore and it works faster tham yours :)

www.codeplex.com/WPFLocalizeExtension

# July 26, 2008 6:52 PM

WPF Application Localization « Roman’s Blog said:

Pingback from  WPF Application Localization &laquo; Roman&#8217;s Blog

# July 29, 2008 10:29 AM

inTagger said:

Try my cool tool and pattern for WPF application localization intagger.blogspot.com/.../wpf-application-localization-pattern_29.html

# July 30, 2008 6:40 AM

inTagger said:

Hail Tomer Shamam! The great miss of your solution (may be of Blend && VS) is impossibility of editing UI in Blend and VS designers (exception is thrown).

Look at my solution intagger.blogspot.com/.../wpf-application-localization-pattern_29.html i think it's simpler and allow seeing all localized values in Blend or VS designers.

# July 30, 2008 10:21 AM

Tomer Shamam said:

Hi inTagger,

Nice solution, but the great miss of your solution may be passing arguments for translation. For example: "You have {0} new message!", where {0} can be retrieved via binding.

Also you didn't provide an option to replace the dictionary source. It should be always XAML based ResourceDictionary.

# July 30, 2008 11:41 AM

Essential WPF said:

Due to the popularity of my WPF Localization tool, I decided to fix several bugs related to Blend and

# August 12, 2008 5:46 PM

Tomer Shamam said:

Hi inTagger again,

"The great miss of your solution (may be of Blend && VS) is impossibility of editing UI in Blend and VS designers (exception is thrown)"

This bug was fixed, so there isn't "a great miss" anymore.

Thanks

# August 12, 2008 5:51 PM

Rob Burke said:

Thanks for this Tomer. With a few additions to this technique for convenience, I've found this really useful on a project.  Great work!  Please do keep updating this if you take any feedback on board.  Do you still use this library, or some riff on this, for your WPF localization needs?

# September 2, 2008 5:08 PM

Celso said:

Hi. Thanks for this solution it´s great. But I will like to if somebody has the following problem. I have some controls on a Grid that are using a dynamic Uid, and I will like to linked its disposition at the Grid with this localization solution. So I used the following code:

<TextBlock FontSize="18">

<TextBlock.Text>

<loc:Translate Default="Text 3">

<Binding ElementName="Root" Path="Uid" />

</loc:Translate>

</TextBlock.Text>

       <Grid.Column>

               <loc:Translate Default="0">

       <Binding ElementName="Root" Path="Uid" />

</loc:Translate>

       </Grid.Column>

       <Grid.Row>

               <loc:Translate Default="3">

<Binding ElementName="Root" Path="Uid" />

</loc:Translate>

       </Grid.Row>

</TextBlock>

When I have the definition for the Row and Colum elements on the Dictionary it works fine. But when the vid property is not contain the default resulting value is not working so all the elements are being positioned at the column and row 0 of the Grid. So I will like to know what I´m doing wrong or why is this happening.

# September 5, 2008 11:32 AM

Tomer Shamam said:

Hi Rob,

Yes I'm using this solution in production. Also I know  others that are using it.

Enjoy.

# September 7, 2008 2:42 PM

Tomer Shamam said:

Hi Celso,

I would really like to help but I didn't understand what you want to do, also why you are using dynamic uid's?

Thanks

# September 7, 2008 2:48 PM

Ben said:

Hi, Tomer

I'm trying to localize a contextmenu without any success. The code that I'm curently using is the following:

<ContextMenu>

       <MenuItem>

           <MenuItem.Header>

               <loc:Translate>

                   <Binding ElementName="Root" Path="Uid" />

               </loc:Translate>

           </MenuItem.Header>

       </MenuItem>  

</ContextMenu>

After making some research I found out that the problem with this code is the fact that a contextmenu lacks of context inheritance, such as explained in this post blogs.msdn.com/.../705116.aspx. So the menuitem doesn't really knows that there is a Root element to associate the uid binding.

Do you have any suggestion in order to implement this?

Thanks

# October 11, 2008 5:57 PM

Tomer Shamam said:

Hi Ben,

Yes you are right. ContextMenu is not part of the visual tree, hence it is not supported by my solution. Maybe I will add this special support in the future. Right now, you can Bind directly to the dictionary. See what the markup extension does.

# October 23, 2008 9:41 PM

Jurmerian said:

It lacks tooltip support.

# November 4, 2008 12:49 PM

Create Work Items Quickly with QuickWit at omnispace said:

Pingback from  Create Work Items Quickly with QuickWit at omnispace

# December 23, 2008 12:47 PM

Mauro Cecili said:

How can i use your solution in a listView - GridViewColumn.CellTemplate with triggers? this is my case:

<GridViewColumn.CellTemplate>

 <DataTemplate>

    <Image Name="ImageTessera"/>

    <DataTemplate.Triggers>

       <DataTrigger Binding="{Binding Path=Icon}" Value="0" Loc:Translate.Uid="ChooseCardStateScaduta">

<Setter  TargetName="ImageTessera" Property="Source" Value="..\Images\TesseraELIMINATA.gif" />

<Setter   TargetName="ImageTessera" Property="ToolTip" Value="{Loc:Translate Scaduta}" />

</DataTrigger>

I try to use Loc:Translate.Uid="ChooseCardStateScaduta" <inside> setter tag but is  worong so i tried in <DataTrigger> XAML is correct but at run time i have only the default value. Can you help me?

Thnaks for your great job!!

# January 21, 2009 4:51 PM

Marcel said:

Hi Tommer, thanks for your code, I'm a .Net developer starting in WPF. I used my own code based on yours so I can use my own implementation of the Dictionary.

I think your code has a great concept, wich I want to discuss with you because your comments will be very helpful.

I assume the on-the-fly update in the UI gets done because the source of the binding (in ProvideValue method of MarkupExtension inheritor) is the instance of the LanguageContext class, wich implements INotifyPropertyChanged so the users of this class be notified once it's Culture property has changed.

So my question is related with having a binding inside the content of some WPF control:

Is that(2nd paragraph) the main point of UI updating on-the-fly when language selection changes??

My question came because I used your code like this:

<Label>

     <Label.Content>

           <Translator>

                 <Binding Path={SomeProperty}/>

           <Translator>

     </Label.Content>

</Label>

and it does not work, I mean, Label does not get notified when LanguageContext.Culture has changed.

But also, like this:

<Label>

     <Label.Content>

           <Translator Key='SomeResourceKey'></Translator>

     </Label.Content>

</Label>

where 'SomeResourceKey' is a key in the resource file, it works perfect.

Why the first sample is not working?

thanks in advance

Marcel

# January 26, 2009 9:25 PM

Nader Alkhatib said:

i think with the following the URL, the original MS solution is complete with no cons (the steps involved in localization can not be thought of as cons):

www.codeproject.com/.../LocBamlClickOnce.aspx

# February 1, 2009 5:55 AM

Tomer Shamam said:

Jurmerian, tooltips are natively supported, you just need to ask for it:

<TextBlock loc:Translate.Uid="3"

ToolTip="{loc:Translate}"

Text="{loc:Translate Default=Text 2}"

Background="{loc:Translate AliceBlue}"

Width="{loc:Translate 300}"

Height="{loc:Translate 30}" FontSize="18" />

# February 6, 2009 2:21 PM

Tomer Shamam said:

Hi Mauro Cecili and Marcel!

Sorry for the late reply, I've upgraded the tool to version 1.3, and thanks to you I've added support for requesting any Uid decleratively.

Now you can provide Uid as part of the Translate markup extension, this will overrides any other attached Uid.

Using this feature, you can now write code like this:

<DataTemplate DataType="{x:Type local:Data}">

  <DataTemplate.Triggers>

     <DataTrigger Binding="{Binding Path=ID}" Value="1">

        <Setter Property="ToolTip"

                Value="{loc:Translate Uid=11, Default=Apple}" />

     </DataTrigger>

     <DataTrigger Binding="{Binding Path=ID}" Value="2">

 <Setter Property="ToolTip"

                 Value="{loc:Translate Uid=12, Default=Bannana}" />

     </DataTrigger>

     <DataTrigger Binding="{Binding Path=ID}" Value="3">

         <Setter Property="ToolTip"

                 Value="{loc:Translate Uid=13, Default=Melon}" />

     </DataTrigger>

  </DataTemplate.Triggers>

</DataTemplate>

Cheers!

# February 6, 2009 2:35 PM

Abraham Mathew said:

Hi,

  Thank you for your code for localization.I am trying to port your demo code to a WPF Project we have to implement localization support for multiple languages,I have defined two xml files:

1.en-US.xml

2.ar-KW.xml

--------------------------------------------------------------------------------

<?xml version="1.0" encoding="utf-8"?>

<Dictionary EnglishId="Arabic" CultureId="Arabic" Culture="ar-KW">

<Value Id="Active" Content="مُفعل"/>

</Dictionary>

--------------------------------------------------------------------------------

<?xml version="1.0" encoding="utf-8" ?>

<Dictionary EnglishId="English" CultureId="English" Culture="en-US">

<Value Id="Active" Content="Active"/>

</Dictionary>

--------------------------------------------------------------------------------

Issue is that when I load the Arabic file from \Languages folder of my app the

app freezes,but when I rename the en-US.xml file to ar-KW.xml file there is

no issues (but english is shown).

I just use the library to read the current value of the title and change

it to the local value and fill a field name of a grid property.

LanguageDictionary.RegisterDictionary(CultureInfo.GetCultureInfo("ar-KW"), new XmlLanguageDictionary("Languages/ar-KW.xml"));

LanguageContext.Instance.Culture = CultureInfo.GetCultureInfo("ar-KW");

try {

   column.Title = LanguageDictionary.Current.Translate<string>(fieldLayoutInfo.Title, "Content");

} catch (Exception ex) {

   throw (ex);

}

Is there any encoding that I have to follow when I create my xml file ?

Any suggestions would be helpfull

Thanks

# March 10, 2009 9:49 AM

Tomer Shamam said:

Hi Abraham,

It looks like you have problems with XmlDocument and not WPF.

# March 10, 2009 10:52 PM

Fabrice's weblog said:

Several techniques exist for localizing WPF applications. I have yet to study them before making a choice

# May 12, 2009 1:53 PM

Arthur said:

Hello Tomer,

First I would like to thank you for this usefull piece of code.

Hope you could help me with the following problem;

When I try to use it in a resource dictionary it works, but Blend 2.5 gives me the error "The ':' characher, hexadecimalvalue 0x3A, cannot be included in a name. Line...". Visual studio does not give this error. Also running the app is no problem.

# May 18, 2009 5:54 PM

Tomer Shamam said:

Arthur, did you try it on Blend 2.0 Or 3.0 preview?

2.5 is kind of deprecated.

# May 25, 2009 8:54 AM

Yuval said:

Hi Tomer,

I'm from AVT, and participated in your course on WPF.

As you know, we use your localization module in our software.

It's been modified through time, but the basics are the same.

There is that bug in version 1.0 where you cannot open the designer. You fixed it in version 1.2 and that's great.

We want to fix it in our code also.

Unfortunatelly, I cannot spot the bug fix among all your changes for version 1.2.

Can you help me with that? Can you refer me to the exact code change that fixes the bug?

Thanks.

# June 3, 2009 11:51 AM

realmontanakid said:

Is it possible to pass a vid in xaml? What you did with "NotEnoughMemory" and "Message". Could it work from Xaml code?

Kind regards

# June 3, 2009 10:46 PM

Tomer Shamam said:

Hi Yuval,

Unfortunately I don't remember what the change was so please download version 1.1 and 1.2 and compare both using WinDiff for example.

# June 8, 2009 9:10 AM

Anon said:

This sample project was just what I needed to enable my users to select a language on the fly.  Seems to work well.  Thanks for sharing your expertise, Tomer!

# August 14, 2009 2:33 AM

Tomer Shamam said:

You welcome!

# August 14, 2009 8:42 AM

Arthur said:

Hi Tomer,

I try to have a TextBlock in a datatemplate translated. The TextBlock is not bound to any data. Whatever I try, I cannot get the textblock to translate.

Then I started to modify your sample app to see if it would work there. I modified the DataTemplate (a stackpanel wuth the orgininal contents plus  TextBlock loc:Translate.Uid="20" Foreground="Green" Text="{loc:Translate}"

It al works fine, but when I start your MainWindow from another window (and make that window the startup window) it fails to translate this TextBlock as well.

How to solve this??

Best regards,

Arthur

# August 20, 2009 1:45 PM

Pattern per la localizzazione delle applicazioni WinForm, ASP.NET e WPF « Rainbowbreeze said:

Pingback from  Pattern per la localizzazione delle applicazioni WinForm, ASP.NET e WPF &laquo;  Rainbowbreeze

# September 11, 2009 1:53 AM

John said:

Which license applies to the files?

# September 13, 2009 4:21 PM

Tomer Shamam said:

Hi John,

You may use it freely, just add credits.

Thanks,

Tomer

# September 13, 2009 10:19 PM

Chris said:

Hallo everybody,

I have problems with Blend 3. My Code looks like this:  ....loc:Translate.Uid="EntryWindow_Btn1_DataHandling" Text="{loc:Translate Default=Type}".

This works, but if i try to edit the 'Default = Type1' -Property (in Blend 3), i get an Exception (.. for the Translate-Type, there is no DependencyObjectType (Default)-Element available. In the Code we have an normal Property Default(get;set) maybe this is the problem.

Anybody has an idea.

Thanks,

Chris

# September 20, 2009 1:19 PM
Leave a Comment

(required) 

(required) 

(optional)

(required) 


Enter the numbers above: