Click here to Skip to main content
Click here to Skip to main content

Localization Sync

, 23 Feb 2012
Rate this:
Please Sign up or sign in to vote.
Small utility to keep localized resources synchronized

Contents

Introduction

"During the development of a localized product, the resource files for various languages can get out of sync. In this case, a resource file for one language will contain text that is missing from a resource file for another. This article describes a utility you can use to detect and report these missing entries."

The above introduction (I could not find a good one at the time I was writing the article) was kindly suggested by Tom Clement when he pointed out his observations during initial article submission.

The tool generates CSV files containing differences between outdated resource files and the most complete one that you may have. A common situation is that during development, base language resource will be updated often while the other supported languages only before a software release. Instead of hunting for differences inside each resource file, use the CSV files generated by this simple utility and send them for translation. Loading CSV files into their preferred Excel software, translators will only have to translate the value entries column, save their work and send it back to you. When files are received, load them into Excel and Copy/Paste their content into the corresponding localized translation using Visual Studio resource editor.

The tool is intuitive and easy to use providing detailed information during its short processing time. It is visually fancy (even not being really necessary) making use of UI validation, progress notification, status icons, logs and error reporting. Here is a quick screenshot:

Technical details

As you know, using Resx files is a great technique to provide separate resources for different cultures in your Visual Studio projects. Basically, each resource file contains different values for the same keys. You can store different kind of resources like images, icons, sounds or custom files, but here we will only speak about text resources.

Lets suppose we will create few resource files to localize our application. LocalizationSyncRes.resx will be the default one and it will contain English version of the text and LocalizationSyncRes.fr-FR.resx will contain text in French. Here is a short snapshot of our resources.

LocalizationSyncRes.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
    <!-- ... ado.net/XML headers & schema ... -->
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="LongString"><value>This is a long string</value><comment>this is a comment</comment></data>
    <data name="Title"><value>Localization Sync</value></data>
    <!-- ... other data nodes ... -->
</root>
LocalizationSyncRes.fr-FR.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
    <!-- ... ado.net/XML headers & schema ... -->
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="LongString"><value>C'est une longue chaîne</value><comment>this is a comment</comment></data>
    <data name="Title"><value>Lieu synchronisée</value></data>
    <!-- ... other data nodes ... -->
</root>

Behind the scene, Visual Studio will automatically generate a file named LocalizationSyncRes.Designer.cs containing a resource class with all the key/value pairs from resource files as properties. The most important thing about this class is that it provides easy resource access in code behind, Asp or Xaml.

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class LocalizationSyncRes {
    
    private static global::System.Resources.ResourceManager resourceMan;
    private static global::System.Globalization.CultureInfo resourceCulture;
    
    public static global::System.Resources.ResourceManager ResourceManager {
        get {
            if (object.ReferenceEquals(resourceMan, null)) {
                global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("LocalizationSync.Assets.LocalizationSyncRes", typeof(LocalizationSyncRes).Assembly);
                resourceMan = temp;
            }
            return resourceMan;
        }
    }
    
    public static global::System.Globalization.CultureInfo Culture {
        get {
            return resourceCulture;
        }
        set {
            resourceCulture = value;
        }
    }
    
    /// <summary>
    /// Looks up a localized string similar to Localization sync.
    /// </summary>
    public static string Title {
        get {
            return ResourceManager.GetString("Title", resourceCulture);
        }
    }
    
    /// <summary>
    /// Looks up a localized string similar to This is a long string.
    /// </summary>
    public static string LongString {
        get {
            return ResourceManager.GetString("LongString", resourceCulture);
        }
    }
}

During development, French resource may become outdated and the list of its entries will not match to the ones of default English resource. Here is where the attached utility may help you: to automatically identify missing entries from outdated resource files.

All what we have to do is to parse <data> nodes from localized resource files and compare them with the content of default one (i.e most updated one). The list of missing entries for each localized resources will be saved into corresponding CSV reports. Below you can find core functions needed for our job.

1. Load resource files (simply XML files)
public XDocument LoadResourceFile(string resourceFilePath)
{
    XDocument document = null;

    try
    {
        if (String.IsNullOrEmpty(resourceFilePath) || !File.Exists(resourceFilePath))
        {
            Log.Error("Source file not valid or does not exists.");
            return null;
        }

        document = XDocument.Load(resourceFilePath);
        if (document == null)
            Log.Error("Translation document was not loaded.");
    }
    catch (System.Xml.XmlException ex)
    {
        Log.Error("Invalid XML detected. Translation file can not be loaded.");
        Log.Exception(ex);
    }
    catch (System.Exception ex)
    {
        Log.Error("An error occurred during loading translation file.");
        Log.Exception(ex);
    }

    return document;
}
2. Compare two resource files loaded as XDocument's
Dictionary<string,string> CompareDocs(XDocument defaultDoc, XDocument localizedDoc)
{
    Dictionary<string,> csvDictionary = null;
    try
    {
        if (defaultDoc == null || localizedDoc == null)
            return null;

        foreach (XElement element in defaultDoc.Descendants("data"))
        {
            if (!DocHasKey(localizedDoc, (string)element.Attribute("name")))
            {
                if (csvDictionary == null)
                    csvDictionary = new Dictionary<string,>();

                string key = (string)element.Attribute("name");
                string value = (string)element.Element("value");
                csvDictionary[key] = value;
            }
        }
        return csvDictionary;
    }
    catch (System.Exception ex)
    {
        Log.Error("An error occurred when comparing resource documents");
        Log.Exception(ex);
    }
    return csvDictionary;
}

bool DocHasKey(XDocument document, string key)
{
    if (document != null)
    {
        foreach (XElement element in document.Descendants("data"))
        {
            string name = (string)element.Attribute("name");
            if (key == name)
                return true;
        }
    }
    return false;
}
3. Save differences in CSV format
public bool SaveCSV(Dictionary<string,string> csvDictionary, string filePath)
{
    try
    {
        if (File.Exists(filePath))
            File.Delete(filePath);

        using (StreamWriter sw = File.CreateText(filePath))
        {
            foreach (KeyValuePair<string,> keyValue in csvDictionary)
                sw.WriteLine(String.Format("{0},{1}", keyValue.Key, keyValue.Value));
        }
        return true;
    }
    catch (System.Exception ex)
    {
        Log.Error("An error occurred during saving CSV report.");
        Log.Exception(ex);
    }
    return false;
}

Finally, the whole comparison process is embedded into a BackgroundWorker to separate it from UI and take advantage of progress change reporting.

BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
// Do the actual processing
worker.DoWork += new System.ComponentModel.DoWorkEventHandler(BackgroundWorker_DoWork);
// Report changes after each localized resource is compared with the default one
worker.ProgressChanged += new ProgressChangedEventHandler(Worker_ProgressChanged);
// Notify users that processing was ended and display the result
worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(Worker_RunWorkerCompleted);

The above event handlers can be found into the attached project. Their content depends too much by other source code to easily describe it here. I only wanted to specify that the processing task is run inside a BackgroundWorker.

How to use

  1. Open Localization Sync. Press Browse button for Default translation and navigate to your project's Resources folder where you will find resource files.
  2. Select your default translation as the source of upcoming synchronization. Click Browse button for Localized translations and select all the files you need to synchronize.
  3. Press Update to start processing. At the end we will be notified if the operation was successful or not.
  4. If everything went well, we will end up with a list of .CSV files containing missing entries from default resource.

Send resulting .CSV files to the translators, tell them to import the files in Excel and do the translation into the same document (eventually into a separate column). After receiving back the translations (.XLS or .CSV files), import them into Excel,and COPY/PASTE to the corresponding localized resource file using Visual Studio resource editor.

Alternatives

Zeta Resource Editor is a tool that provides improved localization support. It lets you visually compare translations and use Google or Microsoft translation APIs. If you need an advanced software to deal with localization, it can be a good option for your needs.

Bibliography

Icons used in this tool were created by Mark James and you can download them from www.famfamfam.com.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

rzvdaniel
Software Developer
Romania Romania
I have been a software developer for a while and still find this an interesting job.

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web03 | 2.8.140709.1 | Last Updated 23 Feb 2012
Article Copyright 2012 by rzvdaniel
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid