Resource refactoring might be a better title, but I preferred a more bombastic one.
Recently I had to refactor a complete website in order to allow localization of the pages.
The major problem with those kind of things is finding the HTML texts which are not related to anything (i.e. it’s a text that will be parsed on the client side only)
I tried locating a tool for the job. For some unknown reason there ain’t one free, buying one was out of question due to the time we had (2 days). “Build one”, I guess it’s one of the most common answers a programmer (senior software engineer for the ones who really need the title) gets at work.
Build one? OK, I did.
Step one: use whatever is available inside the VS. So we grabbed this piece of Macro to use the refactoring made by the studio:
Imports SystemImports EnvDTE
Imports EnvDTE80
Imports EnvDTE90
Imports EnvDTE90a
Imports EnvDTE100
Imports System.Diagnostics
Imports System.Collections.Generic
Public Module Localization
Dim _oProject As Project
Sub GenerateLocalResources()
Dim _oObject As System.Array = DTE.ActiveSolutionProjects()
Dim _oProject = _oObject(0)
GenerateLocalResourcesForProjectItems(_oProject.ProjectItems)
End Sub
Private Sub GenerateLocalResourcesForProjectItems(ByVal oProjectItems As ProjectItems)
Dim oProjectItem As ProjectItem
Dim fileName As String
For Each oProjectItem In oProjectItems
fileName = oProjectItem.Name.ToLower()
If (fileName.EndsWith(".aspx") OrElse fileName.EndsWith(".ascx")) Then
Dim window As Window = oProjectItem.Open(Constants.vsViewKindDesigner)
window.Activate()
Try
DTE.ExecuteCommand("Tools.GenerateLocalResource")
Catch ex As Exception
End Try
If DTE.ActiveDocument().Name = fileName & ".cs" Then
DTE.ActiveDocument().Close(vsSaveChanges.vsSaveChangesNo)
End If
DTE.ActiveWindow.Close(vsSaveChanges.vsSaveChangesYes)
End If
If Not oProjectItem.ProjectItems Is Nothing AndAlso oProjectItem.ProjectItems.Count > 0 Then
GenerateLocalResourcesForProjectItems(oProjectItem.ProjectItems)
End If
Next
End Sub
End Module
As usual slight editing was required, but it does the job (on VS2010).
Note: it’s safer to exclude the web.config while running it. The “globalization provider” defined in it can be a pain in the ass.
Step two: run a “re-formatter” on the results. The re-formatter consists of a loop on all files (omitted from this article), read file’s resource file, get “extra” resources from the file itself and finally create a new resource file and new file.
Step two, first part: read (and write...) the resource file
private static System.Collections.SortedList ReadResourceFile(string resourceFilename){
System.Resources.ResXResourceReader rr = new System.Resources.ResXResourceReader(resourceFilename);
System.Collections.IDictionaryEnumerator ide = rr.GetEnumerator();
System.Collections.SortedList tags = new System.Collections.SortedList();
while (ide.MoveNext())
{
if ((ide.Key is string) && (ide.Value is string) && (((string)ide.Value).Length > 0))
{
tags.Add(ide.Key, ide.Value);
}
}
return tags;
}
private static void WriteResourceFile(string filename)
{
System.Resources.ResXResourceWriter rw = new System.Resources.ResXResourceWriter(filename);
for (int i = 0; i < tags.Count; i++)
{
rw.AddResource((string)tags.GetKey(i), (string)tags.GetByIndex(i));
}
rw.Close();
}
Step two, second part:
The first thing to do is “dispose of the garbage”, strip the HTML from all the tags etc. I created a function to do that:
private static System.Collections.ArrayList GenerateList(string Filename){
System.Collections.ArrayList res = new System.Collections.ArrayList();
System.IO.StreamReader sr = new System.IO.StreamReader(Filename);
string aspx_text = sr.ReadToEnd();
sr.Close();
aspx_text = RemovePiecesOfJunk(aspx_text, "<script", "/script>");
aspx_text = RemovePiecesOfJunk(aspx_text, "<%", "%>");
aspx_text = RemovePiecesOfJunk(aspx_text, "<!--", "-->");
aspx_text = RemovePiecesOfJunk(aspx_text, "<asp:RegularExpressionValidator", "</asp:RegularExpressionValidator>");
aspx_text = aspx_text.Replace("<br />", "\r\n");
aspx_text = aspx_text.Replace("<br/>", "\r\n");
aspx_text = aspx_text.Replace("<br>", "\r\n");
aspx_text = aspx_text.Replace(" ", " ");
while (aspx_text.Length > 0)
{
int opening = aspx_text.IndexOf('>');
if (opening > -1)
{
aspx_text = aspx_text.Substring(opening + 1);
int closing = aspx_text.IndexOf('<');
if (closing > -1)
{
string tmp = aspx_text.Substring(0, closing).Trim();
aspx_text = aspx_text.Substring(closing + 1);
if (!aspx_text.ToLower().StartsWith("/script>"))
{
if ((tmp.Length > 0) && (!res.Contains(tmp)))
{
res.Add(tmp);
}
}
}
else
{
aspx_text = "";
}
}
else
{
aspx_text = "";
}
}
return res;
}
And the additional piece:
private static string RemovePiecesOfJunk(string orig, string open, string close){
open = open.ToLower();
close = close.ToLower();
while (orig.ToLower().IndexOf(open) > -1)
{
int opening = orig.ToLower().IndexOf(open);
int closing = orig.ToLower().IndexOf(close);
orig = orig.Substring(0, opening) + orig.Substring(closing + close.Length);
}
return orig;
}
Take a note: THIS IS NOT THE BEST METHOD FOR THE JOB! It’s just a quick ‘n dirty solution which hands the needful things in time.
I removed the JS code on purpose, I didn’t need to translate the things in it (luckily…) The regular expressions removal is due to the usage of “unexpected” openings and closings within it. The algorithm just assumes that text between “>” and “<” is a client-side only text.
Step three: do the same to the code behind files.
I did it by locating special “hooks” in the file (i.e. .Text = “XXX”) and replacing them with a reference to the global resource file. The code looks like this (without the loop for getting the files…)
private static string ReplaceSingleInstance(string orig_cs, string hook)
{
string res = orig_cs;
while (res.IndexOf(hook) > -1)
{
int strt = res.IndexOf(hook);
int end = res.IndexOf('"', strt + hook.Length);
res = ExtractPortion(res, strt + hook.Length, end);
}
return res;
}
private static void ChangeFile(string fl)
{
string tmp_cs = System.IO.File.ReadAllText(fl);
string[] hooks = { ".DescritpionAfterDropDown = \"", ".NotDropDownNegateText = \"", ".NotDropDownDefaultText = \"", ".DescritpionBeforeDropDown = \"", ".Text = \"", ".Description = \"", ".HeaderText = \"" };
bool changed = false;
foreach (string hk in hooks)
{
if (tmp_cs.IndexOf(hk) > -1)
{
changed = true;
tmp_cs = ReplaceSingleInstance(tmp_cs, hk);
}
}
if (changed)
{
System.IO.File.SetAttributes(fl, System.IO.FileAttributes.Normal);
System.IO.File.WriteAllText(fl, tmp_cs);
Console.WriteLine("Changed " + fl);
}
}
{
string res = orig_cs;
while (res.IndexOf(hook) > -1)
{
int strt = res.IndexOf(hook);
int end = res.IndexOf('"', strt + hook.Length);
res = ExtractPortion(res, strt + hook.Length, end);
}
return res;
}
private static void ChangeFile(string fl)
{
string tmp_cs = System.IO.File.ReadAllText(fl);
string[] hooks = { ".DescritpionAfterDropDown = \"", ".NotDropDownNegateText = \"", ".NotDropDownDefaultText = \"", ".DescritpionBeforeDropDown = \"", ".Text = \"", ".Description = \"", ".HeaderText = \"" };
bool changed = false;
foreach (string hk in hooks)
{
if (tmp_cs.IndexOf(hk) > -1)
{
changed = true;
tmp_cs = ReplaceSingleInstance(tmp_cs, hk);
}
}
if (changed)
{
System.IO.File.SetAttributes(fl, System.IO.FileAttributes.Normal);
System.IO.File.WriteAllText(fl, tmp_cs);
Console.WriteLine("Changed " + fl);
}
}
Step four: QA!
There’s no way to avoid it. After all the automatic conversions some JS texts remained (I wasn't that lucky after all), a special function in order to avoid the ‘ (common in French) was needed, and last but certainly not least GUI.
Step five: GUI repair.
I’m not joking, just translating from English to French made some sentences freak out. Lines were broken in the mid, images moved out of the frame, and some buttons “disappeared”.