context: /**
   * gettext-cs-utils
   *
   * Copyright 2011 Manas Technology Solutions 
   * http://www.manas.com.ar/
   * 
   * This library is free software; you can redistribute it and/or
   * modify it under the terms of the GNU Lesser General Public
   * License as published by the Free Software Foundation; either 
   * version 2.1 of the License, or (at your option) any later version.
   * 
   * This library is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   * Lesser General Public License for more details.
   * 
   * You should have received a copy of the GNU Lesser General Public 
   * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
   * 
   **/
   
   
  /* GNU gettext for C#
   * Copyright (C) 2003 Free Software Foundation, Inc.
   * Written by Bruno Haible <bruno@clisp.org>, 2003.
   *
   * This program is free software; you can redistribute it and/or modify it
   * under the terms of the GNU Library General Public License as published
   * by the Free Software Foundation; either version 2, or (at your option)
   * any later version.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   * Library General Public License for more details.
   *
   * You should have received a copy of the GNU Library General Public
   * License along with this program; if not, write to the Free Software
   * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
   * USA.
   */
  
  /*
   * Using the GNU gettext approach, compiled message catalogs are assemblies
   * containing just one class, a subclass of GettextResourceSet. They are thus
   * interoperable with standard ResourceManager based code.
   *
   * The main differences between the common .NET resources approach and the
   * GNU gettext approach are:
   * - In the .NET resource approach, the keys are abstract textual shortcuts.
   *   In the GNU gettext approach, the keys are the English/ASCII version
   *   of the messages.
   * - In the .NET resource approach, the translation files are called
   *   "Resource.locale.resx" and are UTF-8 encoded XML files. In the GNU gettext
   *   approach, the translation files are called "Resource.locale.po" and are
   *   in the encoding the translator has chosen. There are at least three GUI
   *   translating tools (Emacs PO mode, KDE KBabel, GNOME gtranslator).
   * - In the .NET resource approach, the function ResourceManager.GetString
   *   returns an empty string or throws an InvalidOperationException when no
   *   translation is found. In the GNU gettext approach, the GetString function
   *   returns the (English) message key in that case.
   * - In the .NET resource approach, there is no support for plural handling.
   *   In the GNU gettext approach, we have the GetPluralString function.
   *
   * To compile GNU gettext message catalogs into C# assemblies, the msgfmt
   * program can be used.
   */
  
  using System; /* String, InvalidOperationException, Console */
  using System.Globalization; /* CultureInfo */
  using System.Resources; /* ResourceManager, ResourceSet, IResourceReader */
  using System.Reflection; /* Assembly, ConstructorInfo */
  using System.Collections; /* Hashtable, ICollection, IEnumerator, IDictionaryEnumerator */
  using System.IO; /* Path, FileNotFoundException, Stream */
  using System.Text; /* StringBuilder */
  
  namespace GNU.Gettext {
  
    /// <summary>
    /// Each instance of this class can be used to lookup translations for a
    /// given resource name. For each <c>CultureInfo</c>, it performs the lookup
    /// in several assemblies, from most specific over territory-neutral to
    /// language-neutral.
    /// </summary>
    public class GettextResourceManager : ResourceManager {
  
      // ======================== Public Constructors ========================
  
      /// <summary>
      /// Constructor.
      /// </summary>
      /// <param name="baseName">the resource name, also the assembly base
      ///                        name</param>
      public GettextResourceManager (String baseName)
        : base (baseName, Assembly.GetCallingAssembly(), typeof (GettextResourceSet)) {
      }
  
      /// <summary>
      /// Constructor.
      /// </summary>
      /// <param name="baseName">the resource name, also the assembly base
      ///                        name</param>
      public GettextResourceManager (String baseName, Assembly assembly)
        : base (baseName, assembly, typeof (GettextResourceSet)) {
      }
  
      // ======================== Implementation ========================
  
      /// <summary>
      /// Loads and returns a satellite assembly.
      /// </summary>
      // This is like Assembly.GetSatelliteAssembly, but uses resourceName
      // instead of assembly.GetName().Name, and works around a bug in
      // mono-0.28.
      private static Assembly GetSatelliteAssembly (Assembly assembly, String resourceName, CultureInfo culture) {
        String satelliteExpectedLocation =
          Path.GetDirectoryName(assembly.Location)
          + Path.DirectorySeparatorChar + culture.Name
          + Path.DirectorySeparatorChar + resourceName + ".resources.dll";
        return Assembly.LoadFrom(satelliteExpectedLocation);
      }
  
      /// <summary>
      /// Loads and returns the satellite assembly for a given culture.
      /// </summary>
      private Assembly MySatelliteAssembly (CultureInfo culture) {
        return GetSatelliteAssembly(MainAssembly, BaseName, culture);
      }
  
      /// <summary>
      /// Converts a resource name to a class name.
      /// </summary>
      /// <returns>a nonempty string consisting of alphanumerics and underscores
      ///          and starting with a letter or underscore</returns>
      private static String ConstructClassName (String resourceName) {
        // We could just return an arbitrary fixed class name, like "Messages",
        // assuming that every assembly will only ever contain one
        // GettextResourceSet subclass, but this assumption would break the day
        // we want to support multi-domain PO files in the same format...
        bool valid = (resourceName.Length > 0);
        for (int i = 0; valid && i < resourceName.Length; i++) {
          char c = resourceName[i];
          if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
                || (i > 0 && c >= '0' && c <= '9')))
            valid = false;
        }
        if (valid)
          return resourceName;
        else {
          // Use hexadecimal escapes, using the underscore as escape character.
          String hexdigit = "0123456789abcdef";
          StringBuilder b = new StringBuilder();
          b.Append("__UESCAPED__");
          for (int i = 0; i < resourceName.Length; i++) {
            char c = resourceName[i];
            if (c >= 0xd800 && c < 0xdc00
                && i+1 < resourceName.Length
                && resourceName[i+1] >= 0xdc00 && resourceName[i+1] < 0xe000) {
              // Combine two UTF-16 words to a character.
              char c2 = resourceName[i+1];
              int uc = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
              b.Append('_');
              b.Append('U');
              b.Append(hexdigit[(uc >> 28) & 0x0f]);
              b.Append(hexdigit[(uc >> 24) & 0x0f]);
              b.Append(hexdigit[(uc >> 20) & 0x0f]);
              b.Append(hexdigit[(uc >> 16) & 0x0f]);
              b.Append(hexdigit[(uc >> 12) & 0x0f]);
              b.Append(hexdigit[(uc >> 8) & 0x0f]);
              b.Append(hexdigit[(uc >> 4) & 0x0f]);
              b.Append(hexdigit[uc & 0x0f]);
              i++;
            } else if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
                         || (c >= '0' && c <= '9'))) {
              int uc = c;
              b.Append('_');
              b.Append('u');
              b.Append(hexdigit[(uc >> 12) & 0x0f]);
              b.Append(hexdigit[(uc >> 8) & 0x0f]);
              b.Append(hexdigit[(uc >> 4) & 0x0f]);
              b.Append(hexdigit[uc & 0x0f]);
            } else
              b.Append(c);
          }
          return b.ToString();
        }
      }
  
      /// <summary>
      /// Instantiates a resource set for a given culture.
      /// </summary>
      /// <exception cref="ArgumentException">
      ///   The expected type name is not valid.
      /// </exception>
      /// <exception cref="ReflectionTypeLoadException">
      ///   satelliteAssembly does not contain the expected type.
      /// </exception>
      /// <exception cref="NullReferenceException">
      ///   The type has no no-arguments constructor.
      /// </exception>
      private static GettextResourceSet InstantiateResourceSet (Assembly satelliteAssembly, String resourceName, CultureInfo culture) {
        // We expect a class with a culture dependent class name.
        Type clazz = satelliteAssembly.GetType(ConstructClassName(resourceName)+"_"+culture.Name.Replace('-','_'));
        // We expect it has a no-argument constructor, and invoke it.
        ConstructorInfo constructor = clazz.GetConstructor(Type.EmptyTypes);
        return constructor.Invoke(null) as GettextResourceSet;
      }
  
      private static GettextResourceSet[] EmptyResourceSetArray = new GettextResourceSet[0];
  
      // Cache for already loaded GettextResourceSet cascades.
      private Hashtable /* CultureInfo -> GettextResourceSet[] */ Loaded = new Hashtable();
  
      /// <summary>
      /// Returns the array of <c>GettextResourceSet</c>s for a given culture,
      /// loading them if necessary, and maintaining the cache.
      /// </summary>
      private GettextResourceSet[] GetResourceSetsFor (CultureInfo culture) {
        //Console.WriteLine(">> GetResourceSetsFor "+culture);
        // Look up in the cache.
        GettextResourceSet[] result = Loaded[culture] as GettextResourceSet[];
        if (result == null) {
          lock(this) {
            // Look up again - maybe another thread has filled in the entry
            // while we slept waiting for the lock.
            result = Loaded[culture] as GettextResourceSet[];
            if (result == null) {
              // Determine the GettextResourceSets for the given culture.
              if (culture.Parent == null || culture.Equals(CultureInfo.InvariantCulture))
                // Invariant culture.
                result = EmptyResourceSetArray;
              else {
                // Use a satellite assembly as primary GettextResourceSet, and
                // the result for the parent culture as fallback.
                GettextResourceSet[] parentResult = GetResourceSetsFor(culture.Parent);
                Assembly satelliteAssembly;
                try {
                  satelliteAssembly = MySatelliteAssembly(culture);
                } catch (FileNotFoundException e) {
                  satelliteAssembly = null;
                }
                if (satelliteAssembly != null) {
                  GettextResourceSet satelliteResourceSet;
                  try {
                    satelliteResourceSet = InstantiateResourceSet(satelliteAssembly, BaseName, culture);
                  } catch (Exception e) {
                    Console.Error.WriteLine(e);
                    Console.Error.WriteLine(e.StackTrace);
                    satelliteResourceSet = null;
                  }
                  if (satelliteResourceSet != null) {
                    result = new GettextResourceSet[1+parentResult.Length];
                    result[0] = satelliteResourceSet;
                    Array.Copy(parentResult, 0, result, 1, parentResult.Length);
                  } else
                    result = parentResult;
                } else
                  result = parentResult;
              }
              // Put the result into the cache.
              Loaded.Add(culture, result);
            }
          }
        }
        //Console.WriteLine("<< GetResourceSetsFor "+culture);
        return result;
      }
  
      /*
      /// <summary>
      /// Releases all loaded <c>GettextResourceSet</c>s and their assemblies.
      /// </summary>
      // TODO: No way to release an Assembly?
      public override void ReleaseAllResources () {
        ...
      }
      */
  
      /// <summary>
      /// Returns the translation of <paramref name="msgid"/> in a given culture.
      /// </summary>
      /// <param name="msgid">the key string to be translated, an ASCII
      ///                     string</param>
      /// <returns>the translation of <paramref name="msgid"/>, or
      ///          <paramref name="msgid"/> if none is found</returns>
      public override String GetString (String msgid, CultureInfo culture) {
        foreach (GettextResourceSet rs in GetResourceSetsFor(culture)) {
          String translation = rs.GetString(msgid);
  
gt:         if (translation != null)