Reintegrating Resources


At the beginning of this chapter, I pointed out that there is a time delay between sending the resources to the translator/localizer and getting them back, and that during this time it is unrealistic to stop development. The solution described assumes that the development department has the "master" copy of resources and is free to modify forms as it sees fit. Thus, the incoming resources from the translator/localizer cannot simply be copied back to the master copy. Instead, they must be reintegrated with the original copy.

The process of reintegrating resources is a little involved, so we start with an overview of the process. The essential goal is to add back to the original "master" resources any resources that have been changed by the translator/localizer. It is more complicated than you might imagine because of the fact that Visual Studio form resources use inheritance. Assume that we are using the .NET Framework 2.0. If we send a French resx file (Form1.fr.resx), for example, to the translator, it will contain only those differences from the invariant resx file (Form1.resx). If the translator/localizer makes a change to the French form by translating the Form's title, this new item will be different than the invariant resource, and the French resx file will contain a new entry:

 <data name="$this.Text">   <value>L'Information Personnelle</value> </data> 


Thus, the incoming French resx file contains more entries than the original master French resx file.

Alternatively, if we are using the .NET Framework 1.1, we have to flatten the Form resx files so that WinRes 1.1 can read them. In this case, the incoming French resx file will also contain more entries than the original master French resx file. So regardless of the version of the .NET Framework you are using, the incoming resources will contain more entries than the original master resources. However, to reintegrate these resources, we cannot simply copy all "new" resource entries from the incoming resources to the master resources. If the form has been updated since its release to the translator/localizer and a control has been deleted, adding back all "new" entries would mean that the deleted control would be added back to the project. In addition, if you are using the .NET Framework 1.1, adding back all "new" entries would mean that the master resx files would be converted from Visual Studio File Mode to Single File Mode. This is why the reintegration process is more complicated than you might imagine. In addition to this, you might want to perform some filtering of changes. Recall that you might or might not want to allow your translator/localizer to redesign your forms. If you want to leave form design in the hands of developers, you would reject any changes that are not strings or images. Still further, you might want to reject any incoming change if the incoming invariant resource value is not the same as the current "master" invariant value. This situation would indicate that the resource value has been changed by the development team since it was sent to the translator. In this case, you should reject the incoming translation because it is a translation of a different string and is no longer correct.

So the strategy is this: For each incoming set of resources (where a set is all the files related to a single base name e.g., Form1.resx, Form1.fr.resx, Form1. fr-FR.resx), a flattened ResourceSet for the master resources is created. This contains the complete set of the resources for the given base name. For each incoming resource, we compare the incoming resource entries against this flattened ResourceSet to see if the entry's key already exists. If it does already exist, the entry is one that existed at the time that the resources were sent to the translator/localizer and that still exists in master copy now. If the entry's value is different, the translator/localizer has changed this value, so we need to update our resources. If the incoming resource key exists in the master "unflattened" resource, this is a value that already existed but has now been changed, so we simply change our copy of it. If the resource key does not exist in the master "unflattened" resource, this is a value that had no previous translation but does now, so we add the new entry to the master "unflattened" resource.

With the problems and solutions outlined, let's proceed to the code. The IResourceGovernor has a method called ReintegrateResourceSet that is implemented in the ResourceGovernor base class. This method accepts the flattened master ResourceSet, the master ResourceSet to be updated, and the incoming ResourceSet:

 public bool ReintegrateResourceSet(     ResourceSet flattenedMasterResourceSet,     ResourceSet masterResourceSet,     ResourceSet incomingResourceSet,     AcceptResourceEntry acceptResourceEntry) {     bool changesMade = false;     Hashtable flattenedMasterResourceTable =         GetResourceSetTable(flattenedMasterResourceSet);     Hashtable masterResourceTable =         GetResourceSetTable(masterResourceSet);     IDictionaryEnumerator incomingResourceSetEnumerator =         incomingResourceSet.GetEnumerator();     while (incomingResourceSetEnumerator.MoveNext())     {         DictionaryEntry incomingEntry = (DictionaryEntry)             incomingResourceSetEnumerator.Current;         if (incomingEntry.Value != null &&             flattenedMasterResourceTable.ContainsKey(             incomingEntry.Key) && !             flattenedMasterResourceTable[incomingEntry.Key].Equals(             incomingEntry.Value))         {             if (acceptResourceEntry == null ||                 acceptResourceEntry(incomingEntry,                 flattenedMasterResourceTable[incomingEntry.Key]))             {                 if (masterResourceTable.ContainsKey(                     incomingEntry.Key))                     masterResourceTable[incomingEntry.Key] =                         incomingEntry.Value;                 else                     masterResourceTable.Add(                         incomingEntry.Key, incomingEntry.Value);                 changesMade = true;             }         }     }     return changesMade; } 


The method updates the master ResourceSet with changes from the incoming ResourceSet. To call ReintegrateResourceSet, we iterate through each of the incoming base names (using IResourcesGovernor.GetInvariantCultureBase-Names()):

 IResourcesGovernor incomingResourcesGovernor =     new ResXResourcesGovernor(@"C:\CustomerCare\Incoming"); IResourcesGovernor masterResourcesGovernor =     new ResXResourcesGovernor(@"C:\CustomerCare"); foreach(string baseName in     incomingResourcesGovernor.GetInvariantCultureBaseNames()) {     ResourceSet flattenedMasterResourceSet =         GetFlattenedResourceSet(masterResourcesGovernor, baseName);     foreach(CultureInfo cultureInfo in         incomingResourcesGovernor.GetExistingCultures(baseName))     {         IResourceGovernor incomingResourceGovernor =             incomingResourcesGovernor.GetResourceGovernor(             baseName, cultureInfo);         IResourceGovernor masterResourceGovernor =             masterResourcesGovernor.GetResourceGovernor(             baseName, cultureInfo);         if (incomingResourceGovernor.ResourceExists() &&             masterResourceGovernor.ResourceExists())         {             ResourceSet incomingResourceSet =                 incomingResourceGovernor.ReadResourceSet();             ResourceSet masterResourceSet =                 masterResourceGovernor.ReadResourceSet();             if (masterResourceGovernor.ReintegrateResourceSet(                 flattenedMasterResourceSet, masterResourceSet,                 incomingResourceSet, null))                 masterResourceGovernor.WriteResourceSet(                     masterResourceSet);         }     } } 


So the base names might be "Form1", "Form1Resources", "Form2", and so on. We get the flattened master ResourceSet using GetFlattenedResourceSet, which we return to in a moment. We get an array of CultureInfo objects for which resources exist for the given base name, so if "Form1.resx", "Form1.fr.resx", and "Form1.fr-FR.resx" exist, the array will contain the invariant CultureInfo, the "fr" CultureInfo, and the "fr-FR" CultureInfo. We see if both the incoming resource exists and a corresponding master resource exists (a form could have been deleted in its entirety since the translator/localizer was sent the original files). Finally, we call IResourceGovernor.ReintegrateResourceSet; if this returns TRue, indicating that changes were made, we call IResourceGovernor.WriteResourceSet to make the changes permanent.

Notice that we pass null as the fourth parameter to IResourceGovernor.Rein-tegrateResourceSet. This parameter enables us to control which entries are accepted as valid updates. It is an AcceptResourceEntry delegate:

 public delegate bool AcceptResourceEntry(     DictionaryEntry entry, object originalValue); 


By passing null for this parameter, we accept all updates. If you have taken the approach that your translator/localizer is simply a translator and all other changes (such as moving and resizing controls) should be rejected, you would pass a delegate for this parameter:

 if (masterResourceGovernor.ReintegrateResourceSet(     flattenedMasterResourceSet,     masterResourceSet,     incomingResourceSet,     new AcceptResourceEntry(AcceptStringAndImageResourceEntry))) 


The AcceptStringAndImageResourceEntry method is shown here:

 private bool AcceptStringAndImageResourceEntry(     DictionaryEntry entry, object originalValue) {     return entry.Value is string || entry.Value is Image; } 


This method rejects changes to all entries that are not strings or images. You could also use this approach to strip out hotkeys if you are using an automated runtime solution (see the Hotkeys section of Chapter 8, "Best Practices"). Another possibility is to compare the number of parameters in the original string with the number of parameters in the new string. So "Welcome, {0}" has one parameter, but "Bien-venue" has none, and this could be perceived as a translation error. A variation on the same theme is to check that the type of parameters in the new string is the same as in the original string. So if the original string was "Your goods will be delivered on {0:D}" (where "D" is being used as a long date format) and the parameter was changed to "{0:C}" (where "C" is a currency format), a runtime error would occur and this problem could be caught here.

The final piece is the GetFlattenedResourceSet method. The purpose of this method is to return a completely flattened ResourceSet from all the ResourceSets for a given base name:

 private ResourceSet GetFlattenedResourceSet(     IResourcesGovernor masterResourcesGovernor, string baseName) {     CultureInfo[] masterCultureInfos =         masterResourcesGovernor.GetExistingCultures(baseName);     if (masterCultureInfos.GetLength(0) == 0)         return null;     Array.Sort(masterCultureInfos, new CultureInfoParentComparer());     IResourceGovernor masterResourceGovernor =         masterResourcesGovernor.GetResourceGovernor(         baseName, masterCultureInfos[         masterCultureInfos.GetLength(0) - 1]);     ResourceSet flattenedMasterResourceSet =         masterResourceGovernor.ReadResourceSet();     for(int cultureInfoNumber =         masterCultureInfos.GetLength(0) - 2;         cultureInfoNumber >= 0; cultureInfoNumber--)     {         CultureInfo cultureInfo =             masterCultureInfos[cultureInfoNumber];         masterResourceGovernor =             masterResourcesGovernor.GetResourceGovernor(             baseName, cultureInfo);         masterResourceGovernor.AddResourceSet(             flattenedMasterResourceSet,             masterResourceGovernor.ReadResourceSet());     }     return flattenedMasterResourceSet; } 


This method gets a list of existing cultures (e.g., invariant, "fr", "fr-FR") and sorts them in parental order so that the invariant culture is first, then the neutral culture ("fr"), and then the specific culture ("fr-FR"). It creates a new ResourceSet from the most specific culture (e.g., "fr-FR") and then adds in new resource entries from each of the "parent" cultures using IResourceGovernor.AddResourceSet. The IComparer class used to perform the sorting in Array.Sort is as follows:

 public class CultureInfoParentComparer : IComparer {     int IComparer.Compare(Object x, Object y)     {         CultureInfo cultureInfoX = ((CultureInfo) x);         CultureInfo cultureInfoY = ((CultureInfo) y);         if (cultureInfoX.Equals(CultureInfo.InvariantCulture) &&             cultureInfoY.Equals(CultureInfo.InvariantCulture))             return 0;         else if (cultureInfoX.Equals(CultureInfo.InvariantCulture))             return -1;         else if (cultureInfoY.Equals(CultureInfo.InvariantCulture))             return 1;         else if (cultureInfoX.IsNeutralCulture &&             cultureInfoY.IsNeutralCulture)             return 0;         else if (cultureInfoX.IsNeutralCulture)             return -1;         else if (cultureInfoY.IsNeutralCulture)             return 1;         else if (! cultureInfoX.IsNeutralCulture &&             ! cultureInfoY.IsNeutralCulture)             return 0;         else if (! cultureInfoX.IsNeutralCulture)             return -1;         else             return 1;     } } 


The IComparer.Compare method returns -1 if x is before y, 0 if x is the same as y, and +1 if x is after y.




.NET Internationalization(c) The Developer's Guide to Building Global Windows and Web Applications
.NET Internationalization: The Developers Guide to Building Global Windows and Web Applications
ISBN: 0321341384
EAN: 2147483647
Year: 2006
Pages: 213

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net