TranslationResourceManager


The resource managers that we have encountered so far all assume that the content of the application is static enough that there is time to have it translated. Whereas this will be true for many applications, it is not true for all applications. Web sites with dynamic, rapidly changing content may need a different approach. Enter the TRanslationResourceManager. The TRanslationResourceManager acts as a proxy for another resource manager. It accepts incoming GetString requests and forwards them to an internal resource manager to fulfill the request. If, however, the internal resource manager is unable to fulfill the request, the translationResourceManager steps in to translate the string on-the-fly. The TRanslationResourceManager can optionally write back the translated string to the source of the resource, ensuring that subsequent requests (from any user) do not suffer the performance hit of translating the string. In this way, the translationResourceMan-ager can "learn" translations. The actual translation process is performed by the translation engine that we wrote in Chapter 9, and its translator classes use hard-coded algorithms or Web services, as necessary, to perform the translations.

Before we get too excited about machine translation, the same caveats about its accuracy compared to the accuracy of human translation that were mentioned in Chapter 9 still apply.


So the important point to grasp here is that the TRanslationResourceManager is only a conduit or a filter. It accepts requests and passes them through to another resource manager to actually do the work. It steps in only when the other resource manager cannot fulfill the request. To implement this, we need some way of getting this "internal" resource manager. You could pass this resource manager as a parameter to the translationResourceManager constructor. This would be a flexible solution and would relieve the translationResourceManager from the responsibility of this problem. I decided against this approach, for two reasons: (1) It seems unlikely to me that a single application would use different resource manager classes within the same application, so the capability to use an internal DbResource Manager class in one instance and a ResourcesResourceManager in the next is not useful, and (2) this would require a nonstandard constructor. For reasons that will become apparent in the section on the ResourceManagerProvider class, using a nonstandard constructor must be avoided. So the implementation uses a public static Type property. The consumer of the translationResourceManager specifies the resource manager Type at the beginning of the application:

 TranslationResourceManager.ResourceManagerType =     typeof(DbResourceManager); 


From here on, the translationResourceManager creates DbResourceManager objects for reading and writing the resources. We also need to provide the translationResourceManager with a TRanslatorCollection (a collection of ITranslator objects). You might like to refer back to Chapter 9 for a recap of the translator Collection class. We provide this collection by assigning the collection to the translationResourceManager's public static TRanslators property:

 TranslationResourceManager.Translators = new TranslatorCollection(); TranslationResourceManager.Translators.Add(     new PseudoTranslator()); TranslationResourceManager.Translators.Add(     new Office2003ResearchServicesTranslator()); TranslationResourceManager.Translators.Add(     new WebServiceXTranslator()); TranslationResourceManager.Translators.Add(     new CloserFarTranslator()); 


This code adds a Pseudo Translator (to provide a pseudo English translation), an Office 2003 Research Services translator (to provide translations to and from numerous languages), a WebServiceX translation (to provide translation for Latin languages, Chinese, Japanese, and Korean), and a CloserFar translation (to provide translation to Arabic).

The translationResourceManager inherits from a SurrogateResource Manager, which provides the functionality of passing requests through to the "workhorse" resource manager:

 public class SurrogateResourceManager: ComponentResourceManager {     private static Type resourceManagerType;     private ResourceManager resourceManager;     private string baseName;     protected virtual void Initialize(         string baseName, Assembly assembly)     {         if (resourceManagerType == null)             throw new ArgumentException(                 "SurrogateResourceManager.ResourceManagerType "+                 "is null");         this.baseName = baseName;         MainAssembly = assembly;         resourceManager = CreateResourceManager(baseName, assembly);     }     public SurrogateResourceManager(         string baseName, Assembly assembly)     {         Initialize(baseName, assembly);     }     public SurrogateResourceManager(string baseName)     {         Initialize(baseName, null);     }     public SurrogateResourceManager(Type resourceType)     {         Initialize(resourceType.FullName, resourceType.Assembly);     }     public static Type ResourceManagerType     {         get {return resourceManagerType;}         set {resourceManagerType = value;}     }     protected ResourceManager ResourceManager     {         get {return resourceManager;}     }     protected virtual ResourceManager CreateResourceManager(         string baseName, Assembly assembly)     {         try         {             return (ResourceManager) Activator.CreateInstance(                 resourceManagerType,                 new object[] {baseName, assembly});         }         catch (TargetInvocationException exception)         {             throw exception.InnerException;         }     }     public override object GetObject(         string name, System.Globalization.CultureInfo culture)     {         return resourceManager.GetObject(name, culture);     }     public override ResourceSet GetResourceSet(         CultureInfo culture, bool createIfNotExists, bool tryParents)     {         return resourceManager.GetResourceSet(             culture, createIfNotExists, tryParents);     }     public override string GetString(         string name, System.Globalization.CultureInfo culture)     {         return resourceManager.GetString(name, culture);     } } 


The Initialize method is called by all of the constructors and calls Create ResourceManager to create a new resource manager from the resourceManager Type. CreateResourceManager makes the assumption that all resource managers support a constructor that accepts a string and an assembly. This is one of the reasons why the DbResourceManager and ResourcesResourceManager classes used static properties to allow nonstandard parameters to be setto ensure that the class constructors could be called generically.

Before we move on, notice the GetObject, GetString, and GetresourceSet methods. These methods simply call the corresponding methods on the internal resource manager. These are the simplest examples of passing requests on to the internal resource manager. Because the translationResourceManager is concerned only with translating strings, the GetObject and GetresourceSet methods can be inherited as-is, and only the GetString method needs to be overridden.

Now that all of the infrastructure is in place, we can take a look at the initial implementation of the translationResourceManager:

 public class TranslationResourceManager: SurrogateResourceManager {     private static TranslatorCollection translators;     private static int translationWriteThreshold = 1;     private CultureInfo neutralResourcesCulture;     private int translationWriteCount = 0;     public TranslationResourceManager(         string baseName, Assembly assembly):         base(baseName, assembly)     {     }     public TranslationResourceManager(string baseName):         base(baseName)     {     }     public TranslationResourceManager(Type resourceType):         base(resourceType)     {     }     public static TranslatorCollection Translators     {         get {return translators;}         set {translators = value;}     }     public static int TranslationWriteThreshold     {         get {return translationWriteThreshold;}         set {translationWriteThreshold = value;}     } } 


Toward the top of the class declaration there is a private static integer field called translationWriteThreshold that is initialized to 1 and a corresponding public static integer property called TRanslationWriteThreshold. The translation write threshold is the number of translations that can occur before the translationResourceManager will attempt to write the translations back to the original resource. The initial value of 1 means that each translation will be written back to the original resource immediately. This is a heavy performance penalty but is immediately beneficial to other users who might need this resource. Setting the value to, say, 5 means that there will be five translations before the values are written back to the original resource. Setting the value to 0 means that values will never be written back to the original resource. This last setting effectively turns off the persistence of translated resources and results in a completely dynamically localized solution.

Before we move on to the meat of the class (i.e., the GetString method), let's briefly cover the NeuTRalResourcesCulture protected property:

 protected virtual CultureInfo NeutralResourcesCulture {     get     {         if (neutralResourcesCulture == null)         {             if (MainAssembly == null)                 // We have no main assembly so we cannot get                 // the NeutralResourceLanguageAttribute.                 // We will have to make a guess.                 neutralResourcesCulture = new CultureInfo("en");             else             {                 neutralResourcesCulture = ResourceManager.                     GetNeutralResourcesLanguage(MainAssembly);                 if (neutralResourcesCulture == null ||                     neutralResourcesCulture.Equals(                     CultureInfo.InvariantCulture))                     // we didn't manage to get it from the main                     // assembly or it was the invariant culture                     // so make a guess                     neutralResourcesCulture = new CultureInfo("en");             }         }         return neutralResourcesCulture;     } } 


This property initializes the neuTRalResourcesCulture private field. If the MainAssembly was set in the constructor, the static ResourceManager.Get NeutralResourceLanguage is used to get the value from the assemblies' Neutral ResourcesLanguageAttribute. If there is no such attribute or it is the invariant culture, neuTRalResourcesCulture is set to the English culture. If the MainAssembly was not set, we take a guess at the English culture. Unlike other resource managers, the translationResourceManager needs to know what language the fallback assembly uses; if it can't find out, it has to take a guess. It needs to do this because the translators, not unreasonably, need to know what language they are translating from.

The GetString method is:

 public override string GetString(     string name, System.Globalization.CultureInfo culture) {     if (culture == null)         culture = CultureInfo.CurrentUICulture;     if (culture.Equals(NeutralResourcesCulture) ||         culture.Equals(CultureInfo.InvariantCulture))         // This is the fallback culture          // there is no translation to do.         return ResourceManager.GetString(name, culture);     // get (or create) the resource set for this culture     ResourceSet resourceSet =         ResourceManager.GetResourceSet(culture, true, false);     if (resourceSet != null)     {         // get the string from the resource set         string resourceStringValue =             resourceSet.GetString(name, IgnoreCase);         if (resourceStringValue != null)             // the resource string was found in the resource set             return resourceStringValue;     }     // The string was not found in the resource set or the     // whole resource set was not found and could not be created.     // Get the corresponding string from the invariant culture.     string invariantStringValue =ResourceManager.GetString(         name, CultureInfo.InvariantCulture);     if (invariantStringValue == null ||         invariantStringValue == String.Empty)         // there is no equivalent in the invariant culture or         // the invariant culture string is empty or null         return invariantStringValue;     // the invariant string isn't empty so it     // should be possible to translate it     CultureInfo fallbackCultureInfo = NeutralResourcesCulture;     if (fallbackCultureInfo.TwoLetterISOLanguageName ==         culture.TwoLetterISOLanguageName)         // The languages are the same.         // There is no translation to perform.         return invariantStringValue;     if (! IsOkToTranslate(fallbackCultureInfo, culture, name,         invariantStringValue))         return invariantStringValue;     ITranslator translator = translators.GetTranslator(         fallbackCultureInfo.ToString(), culture.ToString());     if (translator == null)         throw new ApplicationException(String.Format(             "No translator for this language combination "+             "({0}, {1})", fallbackCultureInfo.ToString(),             culture.ToString()));     string translatedResourceStringValue = translator.Translate(         fallbackCultureInfo.ToString(), culture.ToString(),         invariantStringValue);     if (translatedResourceStringValue != String.Empty &&         resourceSet != null)     {         // put the new string back into the resource set         if (resourceSet is CustomResourceSet)             ((CustomResourceSet) resourceSet).Add(                 name, translatedResourceStringValue);         else             ForceResourceSetAdd(resourceSet,                 name, translatedResourceStringValue);         WriteResources(resourceSet);     }     return translatedResourceStringValue; } 


GetString defaults the culture to the CurrentUICulture. If the culture is the invariant culture, it calls the internal resource manager's GetString method and returns that string. (If it is the invariant culture, there is no translation to perform.) Next, we try to get a ResourceSet from the internal resource manager. The internal resource manager returns a null if there is no such resource. For example, if we ask for an Italian resource from a ResourcesResourceManager class and the relevant "it.resources" file does not exist, a null will be returned. If the ResourceSet is not null, we search it for the key that we are looking for; if it is found, we return it. This would happen if it has already been translated or if the translation ResourceManager has previously encountered this key, translated it, and saved it back to the original resource.

If the ResourceSet was null or the key wasn't found, we have to translate it. The first step in the process is to go back to the fallback assembly and find the original string that should be translated. We can get this from the internal resource manager GetString method passing the invariant culture. If this string is empty, we don't bother going to the effort of translating it, as it will be another empty string.

Using the NeutralResourcesLanguage property, we check that the language that we are trying to translate to is different from the language of the fallback assembly by comparing the CultureInfo.TwoLetterISOLanguageName properties. If the languages are different, we can proceed with translation.

Before we perform the translation, we perform a final check to ensure that we really want to translate this key:

 if (! IsOkToTranslate(     fallbackCultureInfo, culture, name, invariantStringValue))     return invariantStringValue; 


The IsOkToTranslate method always returns TRue. I have included it only because you might encounter string properties of components that you do not want translated, and this represents your opportunity to filter out these properties. If this happens, you would modify the IsOkToTranslate method to return false for these properties.

The next line gets the ITranslator, which can handle the translation from the fallback assembly language to the language of the culture we are trying to translate to:

 ITranslator translator = translators.GetTranslator(     fallbackCultureInfo.ToString(), culture.ToString()); 


So an example of this line would be:

 ITranslator translator = translators.GetTranslator("en","it"); 


This line gets a translator to translate from English to Italian. The translator then performs this translation (which might well result in a call to a Web service), and we are left with a translated string. The final piece of the jigsaw before returning the string is to keep a copy of it:

 if (translatedResourceStringValue != String.Empty &&     resourceSet != null) {     // put the new string back into the resource set     if (resourceSet is CustomResourceSet)         ((CustomResourceSet) resourceSet).Add(             name, translatedResourceStringValue);     else         ForceResourceSetAdd(             resourceSet, name, translatedResourceStringValue);     WriteResources(resourceSet); } 


Our first challenge is to put the string back into the resource set. The second challenge is to persist the resource set. The problem with the first challenge is that the ResourceSet class lacks a public facility for adding new entries to the resource set. If you cast your mind back to the CustomResourceSet class that we wrote earlier, you will recall an Add method and a public Table property that I added to allow exactly this. So we check to see that the class is a CustomResourceSet, and if it is, we dutifully call the Add method. If it isn't, we call the rather nasty ForceResourceSetAdd method, which takes the brute-force approach of getting the protected Table field using reflection and calling its Add method. It's not nice, but it works.

The WriteResources method is as follows:

 protected virtual void WriteResources(ResourceSet resourceSet) {     translationWriteCount++;     if (translationWriteThreshold > 0 &&         translationWriteCount >= translationWriteThreshold &&         resourceSet is CustomResourceSet)     {         // the current number of pending writes is greater         // than or equal to the write threshold so         // it is time to write the values back to the source         CustomResourceSet customResourceSet =             ((CustomResourceSet) resourceSet);         IResourceWriter resourceWriter =             customResourceSet.CreateDefaultWriter();         // copy all of the existing resources to the         // resource writer         foreach(DictionaryEntry dictionaryEntry in             customResourceSet.Table)         {             resourceWriter.AddResource(                 dictionaryEntry.Key.ToString(),                 dictionaryEntry.Value);         }         resourceWriter.Generate();         resourceWriter.Close();         translationWriteCount = 0;     } } 


It increments the "translation write count" and compares it with the "translation write threshold" to see if the time has come to update the original resource. If it has and the resource set is a CustomResourceSet, we need to write the resource. If the resource set is not a CustomResourceSet, the original resource doesn't get updated. This would be true if the internal resource manager was a ResourceManager object. The ResourceManager reads resources from an assembly, and I have taken the approach that writing back to the original assembly is not practical in this scenario.

The last obstacle to overcome is that IResourceWriter expects to write the resource in its entirety. This means that we have to load the complete resource set into the resource writer before we generate it. In other words, we can't simply save just the one new item that we have just added.

Congratulations, you are now the proud owner of a TRanslationResource Manager.

There is one last possibility that you might like to consider. The translation ResourceManager performs an "automatic" translation. That is, there is no human intervention in the translation process. An alternative to this approach would be to create a "manual" translation resource manager. This would be a variation of the automatic version, but before each string was returned, a dialog would pop up, offering a translator the original language translation and the machine translation, and allowing the translator to correct the translation and finally save the corrected translation. This manual translation resource manager would only ever be used by the translator and would be considered to be part of the permanent translation process. As such, the translator would receive a new version of the application and run the application; as the translator used the application, it would prompt for every new string that hadn't already been translated in previous translation runs. Seems like a good idea in theory, but I suspect that it would be impractical in practice, as the translator would see all of the strings out of context and would get prompted only for strings encountered during the translator's use of the application. There would be no guarantee that all strings had been encountered and, therefore, translated.




.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