Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

LocaleManager - A Practical Tool to Manage Resources Files of Different Locales for Java/Flex and .NET

0.00/5 (No votes)
2 Aug 2009 2  
Implementation of a software tool in C# to help to manage *.resx files for .NET or *.properties files for Java or AS3 of different locales.

localeManager1.png

localeManager2.png

localeManager3.png

Introduction

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:

  • *.properties files for Java or Flex
  • *.resx files for .NET

Background

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.

How to Use the LocaleManager

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.

  1. Start the program. Then, select the base locale directory, e.g., en_us as in screenshot 1.
  2. After clicking OK, the base locale name will be shown in red color on the dialog. Also, the sibling sub-directories will be displayed in the list box on the left as the existing locale. As I am using Subversion, the svn directory also shows up, and that shouldn't be selected as a target locale. See screenshot 2.
  3. After selecting the locales to work on, click Load and the worksheet will be shown. My test input files only have three lines, so it doesn't look impressive. This window is resizable. This worksheet form shows the resource name, its value in the base locale, and the values in the selected locales. It is very easy to see which ones are not translated yet.

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.

Using the Code

I. Program Structure - Using Model to Store Data

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.

II. Using Directoryinfo and Folderbrowserdialog Classes

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.

III. Dynamically Add Columns to a DataGrid

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);

IV. Dynamically Add Rows to the Datagrid

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;
}

V. Parsing *.resx Files using XMLTextReader

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;
            }
        }
    }

Points of Interest

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.

History

  • 2/20/09
    • Initial version to support *.properties files
  • 2/26/09
    • Added support for *.resx load and view
    • Save hasn't been implemented for *.resx files yet
  • 3/8/09
    • Article updated
  • 7/31/09
    • Added LocaleManager_1.3_Executables.zip

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here