Localization is a pain. Anybody who says different is a sadist. It's especially difficult if you are trying to retrofit an app that didn't even try i.e. hard coding all strings in code/views. Even if I'm not developing an application with localization in mind, I still strive to put all strings in a resx file and never hard code them anywhere. The recommended method for localizing a standard ASP.Net application was to have a single resx file per page. Similarly for a WinForms app you would have one resx file per Form. In a WinForms app this is slightly more understandable since you may need to resize fields to accommodate different languages. However in web apps the only thing I ever store in a resx file is strings.
ASP.Net 2.0 introduced the concept of an App_GlobalResource that can be accessed from any page in your site. This was a step in the right direction because you could easily reuse common strings as needed. There are cases however when I need to generate a message in my business layer that will ultimately be displayed to the user. So I end up needing a resx file in my Core assembly, and a resx file in my web app. Kind of defeats the purpose of a centralized location for strings. The problem is further compounded by the fact that VS2005 generates a strongly typed wrapper for your resx file that has all strings marked as internal. So I can't easily put all of my strings in my Core assembly and then reference them in my web app.
I little googling turned up a work around that allows you to generate a wrapper with public properties. This method worked just fine but I get anal about such things and wanted my publicly generated code to match as closely as possible to what VS2005 would have created for me. Here is my adapted code to generate a file called Strings.Designer.cs that contains a public wrapper class called Strings in namespace My.Product.Core for my Strings.resx resource file:
<Compile Include="Strings.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Strings.resx</DependentUpon>
</Compile>
<ItemGroup>
<EmbeddedResource Include="Strings.resx">
<SubType>Designer</SubType>
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<!-- Generate Strings.Designer.cs with Public access -->
<Target Name="BeforeBuild" DependsOnTargets="GenStrongTypeResource" />
<Target Name="GenStrongTypeResource" Inputs="Strings.resx" Outputs="Strings.Designer.cs">
<GetFrameworkSdkPath>
<Output TaskParameter="Path" PropertyName="SdkPath" />
</GetFrameworkSdkPath>
<Exec Command=""$(SdkPath)\bin\resGen.exe" /str:c#,My.Product.Core,Strings,Strings.Designer.cs /publicClass Strings.resx" />
</Target>
The first part of the post is pretty generic, lets see how we can now use our strongly typed Strings class in our MonoRail views.
MonoRail supports Localization in a similar manner to ASP.Net. You specify which resource file you want to read from and then assign it a friendly name you can use in your view. Using my technique described above, you essentially bypass all of that. My view engine of choice is Brail. To be frank, I'm not sure if you will be able to use this method with NVelocity or any of the other engines.
First off, we need to configure Brail with our Core assembly. You do so in your web.config file:
<brail debug="true" saveToDisk="false" saveDirectory="BrailGen" batch="false" commonScriptsDirectory="CommonScripts">
<reference assembly="My.Product.Core" />
</brail>
Next, you add an import statement to your view and then you can use the generated Strings class just like you would in your C# code. Here is the localized version of the login view I used as an example in my last post:
<%
import My.Product.Core
%>
${Form.FormTag({'action':'login'})}
<table>
<tr>
<td>${Form.LabelFor('username', Strings.Username)}</td>
<td>${Form.TextField('username', {'class':'required'})}</td>
<td><div id="advice-username" class="advice" style="display: none;">${Strings.UsernameRequired}</div></td>
</tr>
<tr>
<td>${Form.LabelFor('password', Strings.Password)}</td>
<td>${Form.PasswordField('password', {'class':'required'})}</td>
<td />
</tr>
<tr>
<td />
<td>${Form.Submit(Strings.Login)}</td>
<td />
</tr>
</table>
${Form.EndFormTag()}
Nice and easy. Now all of the strings used in your application are centralized in a single file. This makes translation more straight forward and helps prevent duplication of strings in multiple resx files. You could easily extend this and have a Strings assembly that only contains a resx file that could be reused in multiple applications.
posted @ Saturday, June 23, 2007 3:33 PM