In my current project (.NET Windows Forms application) I have a requirement that the .NET windows forms should be localized but the localization elements (just the translations, not the images or control positions) should come from the database in order to enable the end users to modify the control localizable properties (just the caption/text) as they wish. To keep the developers unburdened with localization issues the best solution for me would be to simply mark the form as Localizable in the VS designer. This will place all localizable property values in the forms .resx file.
Now my problem is how to provide translations from the database. The moment that the form is marked as Localizable the VS Forms designer will place everything that can be localized is forms .resx file. The designer will also modify the the standard designer.cs InitializeComponent method so that it instantiates ComponentResourceManager and then use that resource manager the load the localizable properties of objects (controls and components).
I have seen solutions where people have build up their own method of applying the localized properties to Form and its controls. All the solutions that I've seen usually boil down to recursively iterating through the Controls collection of Form and its controls and applying translations. Unfortunately the .NET Forms localization is not so simple and this does not cover all scenarios especially if you have some 3rd party controls. For example:
this.navBarControl.OptionsNavPane.ExpandedWidth = ((int)(resources.GetObject("resource.ExpandedWidth")));
//
// radioGroup1
//
resources.ApplyResources(this.radioGroup1, "radioGroup1");
...
this.radioGroup1.Properties.Items.AddRange(new DevExpress.XtraEditors.Controls.RadioGroupItem[] {
new DevExpress.XtraEditors.Controls.RadioGroupItem(resources.GetString("radioGroup1.Properties.Items"), resources.GetString("radioGroup1.Properties.Items1")),
new DevExpress.XtraEditors.Controls.RadioGroupItem(resources.GetString("radioGroup1.Properties.Items2"), resources.GetString("radioGroup1.Properties.Items3"))});
All the solutions I've seen could not do translations such as required by the above components.
Since the VS has already generated the code which provides translation where needed my ideal solution would be to somehow replace the ComponentResourceManager with my own derived class. If I could just replace the following line in InitializeComponent:
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
with
System.ComponentModel.ComponentResourceManager resources = new MyComponentResourceManager(typeof(Form1));
then I could solve everything else without any problems.
Unfortunately I did not find how I could do something like that so here I am on SO asking a question on how it could be done.
P.S. I would also accept any other localization solution which matches the requirements: 1. Changing translations should be possible without redeploying the application 2. The developer should not take care about translation when creating forms/user controls
Thank you.
EDIT: Larry provided a great reference to a book which has code which partly solves my problem. With that help I was able to create my own component which replaces the default ComponentResourceManager in the InitializeComponent method. Here is the code for a component called Localizer which replaces the ComponentResourceManager with custom MyResourceManager so that someone else can also benefit from it:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.CodeDom;
using System.ComponentModel.Design;
namespace LocalizationTest
{
[Designer(typeof(LocalizerDesigner))]
[DesignerSerializer(typeof(LocalizerSerializer), typeof(CodeDomSerializer))]
public class Localizer : Component
{
public static void GetResourceManager(Type type, out ComponentResourceManager resourceManager)
{
resourceManager = new MyResourceManager(type);
}
}
public class MyResourceManager : ComponentResourceManager
{
public MyResourceManager(Type type) : base(type)
{
}
}
public class LocalizerSerializer : CodeDomSerializer
{
public override object Deserialize(IDesignerSerializationManager manager, object codeDomObject)
{
CodeDomSerializer baseSerializer = (CodeDomSerializer)
manager.GetSerializer(typeof(Component), typeof(CodeDomSerializer));
return baseSerializer.Deserialize(manager, codeDomObject);
}
public override object Serialize(IDesignerSerializationManager manager, object value)
{
CodeDomSerializer baseSerializer =
(CodeDomSerializer)manager.GetSerializer(typeof(Component), typeof(CodeDomSerializer));
object codeObject = baseSerializer.Serialize(manager, value);
if (codeObject is CodeStatementCollection)
{
CodeStatementCollection statements = (CodeStatementCollection)codeObject;
CodeTypeDeclaration classTypeDeclaration =
(CodeTypeDeclaration)manager.GetService(typeof(CodeTypeDeclaration));
CodeExpression typeofExpression = new CodeTypeOfExpression(classTypeDeclaration.Name);
CodeDirectionExpression outResourceExpression = new CodeDirectionExpression(
FieldDirection.Out, new CodeVariableReferenceExpression("resources"));
CodeExpression rightCodeExpression =
new CodeMethodInvokeExpression(new CodeTypeReferenceExpression("LocalizationTest.Localizer"), "GetResourceManager",
new CodeExpression[] { typeofExpression, outResourceExpression });
statements.Insert(0, new CodeExpressionStatement(rightCodeExpression));
}
return codeObject;
}
}
public class LocalizerDesigner : ComponentDesigner
{
public override void Initialize(IComponent c)
{
base.Initialize(c);
var dh = (IDesignerHost)GetService(typeof(IDesignerHost));
if (dh == null)
return;
var innerListProperty = dh.Container.Components.GetType().GetProperty("InnerList", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.FlattenHierarchy);
var innerList = innerListProperty.GetValue(dh.Container.Components, null) as System.Collections.ArrayList;
if (innerList == null)
return;
if (innerList.IndexOf(c) <= 1)
return;
innerList.Remove(c);
innerList.Insert(0, c);
}
}
}
I'm the author of a localization tool for Visual Studio developers (in the interest of full disclosure). I strongly suggest getting hold of a copy of Guy Smith-Ferrier's book, ".NET Internationalization, The Developer's Guide to Building Global Windows and Web Applications". I believe that's the correct book anyway (correct author for sure), but you'll need to check, since it's been a long time since I looked at it (and maybe he's even written something newer since then). Guy is a MSFT MVP and localization guru. He shows you how to do exactly what you're attempting, in his case, by creating a component that you can drag onto the tray area of each of your forms. The component will then allow you to replace the "ComponentResourceManager" with your own (there are several classes involved in his design). You can then read your strings from any source including a DB. IIRC, his own example even uses a DB. You can probably find the code online without having to purchase his book, since I think he may even provide it on his own site. You can also find free passages from his book on reputable book purchasing sites, since the info is invaluable even if you don't use his techniques (very hard to find elsewhere). Note that I tested his code once upon a time (a long time ago), and it works as advertised.