![]() |
General Programming »
Localisation »
General
Beginner
License: The Code Project Open License (CPOL)
LocaleManager - A Practical Tool to Manage Resources Files of Different Locales for Java/Flex and .NETBy Angela HanImplementation of a software tool in C# to help to manage *.resx files for .NET or *.properties files for Java or AS3 of different locales. |
C#, XML, Windows, .NET 2.0, WinForms, Dev, QA
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||



This app allows a user to select a base locale and any other locale or locales to work on. Then, it displays the name and value of the resources in a table. Users can keep track of which entries need to be translated, or simply work on this table to fill all the blanks. The changes will be saved with UTF8 encoding.
It supports two types for resource files:
A problem I have been facing recently during Flex development is that the en_US base locale files change from time to time as new strings are added to the code, and it is hard to keep the files for other locales in sync. I couldn't find any simple and free tool like this on the web, so I decided to develop it by myself.
The assumptions are that users follow the common practices for locale folders: use a separate folder for each locale. So, they normally look like:
locale-
|--- en_US - resource files in English
|--- de - resource files in German
|--- fr - resource files in French
It is very easy to use the locale manager. It is a standalone application that runs on Windows.
To use it, just follow the screen captures.
The target locale file will be reorganized to use the same resource entries as in the base. Entries that exist in the base but are not in the target will be added to the target locale with a blank value. Entries that exist in the target but not in the base will be taken out and saved to a Mismatch_timestamp.log file located in the same folder as the EXE file.
Click Save Changes before closing the window, and changes made will be saved to the target file. The base file will not be touched.
There are two forms in the code: MainForm and WorkSheetForm. MainForm is the one to ask the user to select the base locale directory by file browsing. Then, it displays all the sibling subdirectories of the base directory.
WorkSheetForm will show after the user selects one or more target locales and clicks the Load button. The program will dynamically add columns to the DataGrid depending on how many locales are selected.
This program uses the MVC (Model-View-Controller) pattern to pass data around the UI. Application data is stored in Global.cs, the Model. So, MainForm.cs passes data such as BaseDir, RootDir, and Locales to Global.cs, and WorkSheetForm.cs can go pick it up over there.
FolderBrowserDialog is used instead of an OpenFileDialog so that only folders are shown while user tries to find the base locale directory.
.NET's DirectoryInfo class is very handy and you can retrieve all different information about a directory or a file. The MainForm.cs uses this class to get the base directory and the root locale directory. The fillLocaleList() function finds all the sibling subdirectories and add them to the list box.
private void m_browse_Click(object sender, EventArgs e)
{
m_baseDirBrowserDlg.SelectedPath = Environment.CurrentDirectory;
if (m_baseDirBrowserDlg.ShowDialog(this) != DialogResult.OK)
return;
Global.BaseDir = new DirectoryInfo(m_baseDirBrowserDlg.SelectedPath);
Global.RootDir = Global.BaseDir.Parent;
m_baseLocale.Text = Global.BaseDir.Name;
fillLocaleList();
}
private void fillLocaleList()
{
m_allLocales.Items.Clear();
m_selectedLocale.Items.Clear();
DirectoryInfo[] subs = Global.RootDir.GetDirectories();
foreach (DirectoryInfo sub in subs)
{
String dirName = sub.Name;
if (dirName != Global.BaseDir.Name)
m_allLocales.Items.Add(dirName);
}
if (m_allLocales.Items.Count > 0)
{
m_add.Enabled = true;
m_addAll.Enabled = true;
m_allLocales.SelectedIndex = 0;
}
else
MessageBox.Show("No sibling folders were found for the base locale.");
}
The code will enable buttons only when a user finishes the required steps first. This is very easy to do but will help the user a lot and make the application friendly.
If the datagrid you want to display always has the same number of columns and the headers remain the same, you can just configure the datagrid through property settings. However, sometimes the number of columns and headings depends on user's input, we need to dynamically add columns to the datagrid.
The following code shows how columns are added to the Datagrid on Worksheet form based on which target locales user selects. Column index comes from enumeration of columns:
public enum Columns
{
No = 0, File, Name, Base
};
private void WorkSheetForm_Load(object sender, EventArgs e)
{
FileInfo[] files = Global.BaseDir.GetFiles("*"+ Global.Extension);
...
...
//determine number of columns for the datagrid
m_grid.ColumnCount = Global.Locales.Length + (int)Columns.Base + 1;
_numOfCols = m_grid.ColumnCount;
m_grid.Columns[(int)Columns.No].Name = "no.";
m_grid.Columns[(int)Columns.No].ValueType = typeof(int);
m_grid.Columns[(int)Columns.File].Name = "file";
m_grid.Columns[(int)Columns.Name].Name = "name";
m_grid.Columns[(int)Columns.Base].Name = Global.BaseDir.Name;
int i = (int)Columns.Base + 1;
foreach (string locale in Global.Locales)
m_grid.Columns[i++].Name = locale;
...
}
When the column "no." is added, I set its ValueType to int, otherwise it is going to be string by default. This matters when user clicks the header to do a sort. If the value type is string, the sorted result will be "1, 11, 2, 21, ..." instead of "1, 2, 3, ...".
m_grid.Columns[(int)Columns.No].Name = "no.";
m_grid.Columns[(int)Columns.No].ValueType = typeof(int);
The function AddRowToGrid() shows how to add a new row to the datagrid. The function AddToRow() shows how to fill out cells or access cell values in an existing row.
private void AddRowToGrid(string file, int i, string name, string value)
{
string[] row;
row = new string[_numOfCols];
row[(int)Columns.No] = i.ToString();
row[(int)Columns.File] = file;
row[(int)Columns.Name] = name.Trim();
row[(int)Columns.Base] = value.Trim();
m_grid.Rows.Add(row);
}
private void AddToRow(int i, int localeIndex, string value)
{
if (null!= value && i>=0)
m_grid.Rows[i].Cells[getColumnNo(localeIndex)].Value = value;
}
private int getColumnNo(int localeIndex)
{
return (int)Columns.Base + localeIndex + 1;
}
The *.properties files are loaded by StreamReader, and the resource name/value pairs are read by String.Split.
tokens = line.Split('=');
The *.resx files are loaded by an XmlTextReader. The *.resx file entries look like this:
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema -->
<data name="GoodEvening" xml:space="preserve">
<value>Good Evening</value>
<comment>Evening</comment>
</data>
<data name="GoodMorning" xml:space="preserve">
<value>Good Morning</value>
<comment>Morning</comment>
</data>
<data name="Hello" xml:space="preserve">
<value>Hello</value>
<comment>Greetings</comment>
</data>
</root>
The XML parsing needs to parse element attributes and texts. The parsing code is as follows:
using (XmlTextReader rd = new XmlTextReader(path))
{
XmlParsingSteps step = XmlParsingSteps.ParsingName;
string name = "";
string value = "";
while (rd.Read())
{
switch (rd.NodeType)
{
case XmlNodeType.Element:
if (step == XmlParsingSteps.ParsingName && rd.Name == "data")
{
while (rd.MoveToNextAttribute())
{
if (rd.Name == "name")
{
name = rd.Value;
step = XmlParsingSteps.ParsingValueTag;
break;
}
}
}
else if (step == XmlParsingSteps.ParsingValueTag &&
rd.Name == "value")
{
step = XmlParsingSteps.ParsingValue;
}
break;
case XmlNodeType.Text:
if (step == XmlParsingSteps.ParsingValue)
{
value = rd.Value;
if (isBase)
{
AddRowToGrid(file, rowIndex, name, value);
rowIndex++;
}
else
{
rowIndex = findRowNo(file, name.Trim());
if (rowIndex < 0)
{
//property not found in base
Global.Log(Global.MismatchLog, name + "-" + value);
}
else
AddToRow(rowIndex, localeIndex, value);
}
//reset name and value strings
step = XmlParsingSteps.ParsingName;
name = "";
value = "";
}
break;
default:
break;
}
}
}
The thing I like about this app is that the WorkSheetForm resizes so smoothly. Before, I had the combo box and a Save button above the DataGridView, and it was difficult to set the Anchor and Dock properties to make the DataGrid grow when the user resizes the window. At least, the column width didn't change.
Then later, I just added a menu and added the Save option to the menu. The ComboBox was just dropped over there and it still works fine even though it is sitting on the menu bar. Just set the DataGridView's Dock property to "Fill" and it will fill into the space of the whole app.
The only glitch I see in this application is that after typing anything to a cell in the DataGrid, you have to click somewhere else before the value is updated for that cell, even though on screen, it shows the words you just typed. So, if you continue to edit, you will be fine. Only the last entry may not be saved correctly if you do not click somewhere else before clicking Save. I think it has something to do with the EditMode property of the DataGrid. In my program, the setting is EditOnEnter. I tried to set it to different things, but didn't see it solve this issue. Anyway, I will keep trying to find an answer for this.
| You must Sign In to use this message board. | |||||
|
|||||
|
|||||
|
|||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 2 Aug 2009 Editor: Deeksha Shenoy |
Copyright 2009 by Angela Han Everything else Copyright © CodeProject, 1999-2009 Web19 | Advertise on the Code Project |