Home | History | Annotate | Line # | Download | only in intl-csharp
      1 /* GNU gettext for C#
      2  * Copyright (C) 2003, 2005 Free Software Foundation, Inc.
      3  * Written by Bruno Haible <bruno (at) clisp.org>, 2003.
      4  *
      5  * This program is free software; you can redistribute it and/or modify it
      6  * under the terms of the GNU Library General Public License as published
      7  * by the Free Software Foundation; either version 2, or (at your option)
      8  * any later version.
      9  *
     10  * This program is distributed in the hope that it will be useful,
     11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     13  * Library General Public License for more details.
     14  *
     15  * You should have received a copy of the GNU Library General Public
     16  * License along with this program; if not, write to the Free Software
     17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
     18  * USA.
     19  */
     20 
     21 /*
     22  * Using the GNU gettext approach, compiled message catalogs are assemblies
     23  * containing just one class, a subclass of GettextResourceSet. They are thus
     24  * interoperable with standard ResourceManager based code.
     25  *
     26  * The main differences between the common .NET resources approach and the
     27  * GNU gettext approach are:
     28  * - In the .NET resource approach, the keys are abstract textual shortcuts.
     29  *   In the GNU gettext approach, the keys are the English/ASCII version
     30  *   of the messages.
     31  * - In the .NET resource approach, the translation files are called
     32  *   "Resource.locale.resx" and are UTF-8 encoded XML files. In the GNU gettext
     33  *   approach, the translation files are called "Resource.locale.po" and are
     34  *   in the encoding the translator has chosen. There are at least three GUI
     35  *   translating tools (Emacs PO mode, KDE KBabel, GNOME gtranslator).
     36  * - In the .NET resource approach, the function ResourceManager.GetString
     37  *   returns an empty string or throws an InvalidOperationException when no
     38  *   translation is found. In the GNU gettext approach, the GetString function
     39  *   returns the (English) message key in that case.
     40  * - In the .NET resource approach, there is no support for plural handling.
     41  *   In the GNU gettext approach, we have the GetPluralString function.
     42  *
     43  * To compile GNU gettext message catalogs into C# assemblies, the msgfmt
     44  * program can be used.
     45  */
     46 
     47 using System; /* String, InvalidOperationException, Console */
     48 using System.Globalization; /* CultureInfo */
     49 using System.Resources; /* ResourceManager, ResourceSet, IResourceReader */
     50 using System.Reflection; /* Assembly, ConstructorInfo */
     51 using System.Collections; /* Hashtable, ICollection, IEnumerator, IDictionaryEnumerator */
     52 using System.IO; /* Path, FileNotFoundException, Stream */
     53 using System.Text; /* StringBuilder */
     54 
     55 namespace GNU.Gettext {
     56 
     57   /// <summary>
     58   /// Each instance of this class can be used to lookup translations for a
     59   /// given resource name. For each <c>CultureInfo</c>, it performs the lookup
     60   /// in several assemblies, from most specific over territory-neutral to
     61   /// language-neutral.
     62   /// </summary>
     63   public class GettextResourceManager : ResourceManager {
     64 
     65     // ======================== Public Constructors ========================
     66 
     67     /// <summary>
     68     /// Constructor.
     69     /// </summary>
     70     /// <param name="baseName">the resource name, also the assembly base
     71     ///                        name</param>
     72     public GettextResourceManager (String baseName)
     73       : base (baseName, Assembly.GetCallingAssembly(), typeof (GettextResourceSet)) {
     74     }
     75 
     76     /// <summary>
     77     /// Constructor.
     78     /// </summary>
     79     /// <param name="baseName">the resource name, also the assembly base
     80     ///                        name</param>
     81     public GettextResourceManager (String baseName, Assembly assembly)
     82       : base (baseName, assembly, typeof (GettextResourceSet)) {
     83     }
     84 
     85     // ======================== Implementation ========================
     86 
     87     /// <summary>
     88     /// Loads and returns a satellite assembly.
     89     /// </summary>
     90     // This is like Assembly.GetSatelliteAssembly, but uses resourceName
     91     // instead of assembly.GetName().Name, and works around a bug in
     92     // mono-0.28.
     93     private static Assembly GetSatelliteAssembly (Assembly assembly, String resourceName, CultureInfo culture) {
     94       String satelliteExpectedLocation =
     95         Path.GetDirectoryName(assembly.Location)
     96         + Path.DirectorySeparatorChar + culture.Name
     97         + Path.DirectorySeparatorChar + resourceName + ".resources.dll";
     98       return Assembly.LoadFrom(satelliteExpectedLocation);
     99     }
    100 
    101     /// <summary>
    102     /// Loads and returns the satellite assembly for a given culture.
    103     /// </summary>
    104     private Assembly MySatelliteAssembly (CultureInfo culture) {
    105       return GetSatelliteAssembly(MainAssembly, BaseName, culture);
    106     }
    107 
    108     /// <summary>
    109     /// Converts a resource name to a class name.
    110     /// </summary>
    111     /// <returns>a nonempty string consisting of alphanumerics and underscores
    112     ///          and starting with a letter or underscore</returns>
    113     private static String ConstructClassName (String resourceName) {
    114       // We could just return an arbitrary fixed class name, like "Messages",
    115       // assuming that every assembly will only ever contain one
    116       // GettextResourceSet subclass, but this assumption would break the day
    117       // we want to support multi-domain PO files in the same format...
    118       bool valid = (resourceName.Length > 0);
    119       for (int i = 0; valid && i < resourceName.Length; i++) {
    120         char c = resourceName[i];
    121         if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
    122               || (i > 0 && c >= '0' && c <= '9')))
    123           valid = false;
    124       }
    125       if (valid)
    126         return resourceName;
    127       else {
    128         // Use hexadecimal escapes, using the underscore as escape character.
    129         String hexdigit = "0123456789abcdef";
    130         StringBuilder b = new StringBuilder();
    131         b.Append("__UESCAPED__");
    132         for (int i = 0; i < resourceName.Length; i++) {
    133           char c = resourceName[i];
    134           if (c >= 0xd800 && c < 0xdc00
    135               && i+1 < resourceName.Length
    136               && resourceName[i+1] >= 0xdc00 && resourceName[i+1] < 0xe000) {
    137             // Combine two UTF-16 words to a character.
    138             char c2 = resourceName[i+1];
    139             int uc = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
    140             b.Append('_');
    141             b.Append('U');
    142             b.Append(hexdigit[(uc >> 28) & 0x0f]);
    143             b.Append(hexdigit[(uc >> 24) & 0x0f]);
    144             b.Append(hexdigit[(uc >> 20) & 0x0f]);
    145             b.Append(hexdigit[(uc >> 16) & 0x0f]);
    146             b.Append(hexdigit[(uc >> 12) & 0x0f]);
    147             b.Append(hexdigit[(uc >> 8) & 0x0f]);
    148             b.Append(hexdigit[(uc >> 4) & 0x0f]);
    149             b.Append(hexdigit[uc & 0x0f]);
    150             i++;
    151           } else if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
    152                        || (c >= '0' && c <= '9'))) {
    153             int uc = c;
    154             b.Append('_');
    155             b.Append('u');
    156             b.Append(hexdigit[(uc >> 12) & 0x0f]);
    157             b.Append(hexdigit[(uc >> 8) & 0x0f]);
    158             b.Append(hexdigit[(uc >> 4) & 0x0f]);
    159             b.Append(hexdigit[uc & 0x0f]);
    160           } else
    161             b.Append(c);
    162         }
    163         return b.ToString();
    164       }
    165     }
    166 
    167     /// <summary>
    168     /// Instantiates a resource set for a given culture.
    169     /// </summary>
    170     /// <exception cref="ArgumentException">
    171     ///   The expected type name is not valid.
    172     /// </exception>
    173     /// <exception cref="ReflectionTypeLoadException">
    174     ///   satelliteAssembly does not contain the expected type.
    175     /// </exception>
    176     /// <exception cref="NullReferenceException">
    177     ///   The type has no no-arguments constructor.
    178     /// </exception>
    179     private static GettextResourceSet InstantiateResourceSet (Assembly satelliteAssembly, String resourceName, CultureInfo culture) {
    180       // We expect a class with a culture dependent class name.
    181       Type clazz = satelliteAssembly.GetType(ConstructClassName(resourceName)+"_"+culture.Name.Replace('-','_'));
    182       // We expect it has a no-argument constructor, and invoke it.
    183       ConstructorInfo constructor = clazz.GetConstructor(Type.EmptyTypes);
    184       return (GettextResourceSet) constructor.Invoke(null);
    185     }
    186 
    187     private static GettextResourceSet[] EmptyResourceSetArray = new GettextResourceSet[0];
    188 
    189     // Cache for already loaded GettextResourceSet cascades.
    190     private Hashtable /* CultureInfo -> GettextResourceSet[] */ Loaded = new Hashtable();
    191 
    192     /// <summary>
    193     /// Returns the array of <c>GettextResourceSet</c>s for a given culture,
    194     /// loading them if necessary, and maintaining the cache.
    195     /// </summary>
    196     private GettextResourceSet[] GetResourceSetsFor (CultureInfo culture) {
    197       //Console.WriteLine(">> GetResourceSetsFor "+culture);
    198       // Look up in the cache.
    199       GettextResourceSet[] result = (GettextResourceSet[]) Loaded[culture];
    200       if (result == null) {
    201         lock(this) {
    202           // Look up again - maybe another thread has filled in the entry
    203           // while we slept waiting for the lock.
    204           result = (GettextResourceSet[]) Loaded[culture];
    205           if (result == null) {
    206             // Determine the GettextResourceSets for the given culture.
    207             if (culture.Parent == null || culture.Equals(CultureInfo.InvariantCulture))
    208               // Invariant culture.
    209               result = EmptyResourceSetArray;
    210             else {
    211               // Use a satellite assembly as primary GettextResourceSet, and
    212               // the result for the parent culture as fallback.
    213               GettextResourceSet[] parentResult = GetResourceSetsFor(culture.Parent);
    214               Assembly satelliteAssembly;
    215               try {
    216                 satelliteAssembly = MySatelliteAssembly(culture);
    217               } catch (FileNotFoundException e) {
    218                 satelliteAssembly = null;
    219               }
    220               if (satelliteAssembly != null) {
    221                 GettextResourceSet satelliteResourceSet;
    222                 try {
    223                   satelliteResourceSet = InstantiateResourceSet(satelliteAssembly, BaseName, culture);
    224                 } catch (Exception e) {
    225                   Console.Error.WriteLine(e);
    226                   Console.Error.WriteLine(e.StackTrace);
    227                   satelliteResourceSet = null;
    228                 }
    229                 if (satelliteResourceSet != null) {
    230                   result = new GettextResourceSet[1+parentResult.Length];
    231                   result[0] = satelliteResourceSet;
    232                   Array.Copy(parentResult, 0, result, 1, parentResult.Length);
    233                 } else
    234                   result = parentResult;
    235               } else
    236                 result = parentResult;
    237             }
    238             // Put the result into the cache.
    239             Loaded.Add(culture, result);
    240           }
    241         }
    242       }
    243       //Console.WriteLine("<< GetResourceSetsFor "+culture);
    244       return result;
    245     }
    246 
    247     /*
    248     /// <summary>
    249     /// Releases all loaded <c>GettextResourceSet</c>s and their assemblies.
    250     /// </summary>
    251     // TODO: No way to release an Assembly?
    252     public override void ReleaseAllResources () {
    253       ...
    254     }
    255     */
    256 
    257     /// <summary>
    258     /// Returns the translation of <paramref name="msgid"/> in a given culture.
    259     /// </summary>
    260     /// <param name="msgid">the key string to be translated, an ASCII
    261     ///                     string</param>
    262     /// <returns>the translation of <paramref name="msgid"/>, or
    263     ///          <paramref name="msgid"/> if none is found</returns>
    264     public override String GetString (String msgid, CultureInfo culture) {
    265       foreach (GettextResourceSet rs in GetResourceSetsFor(culture)) {
    266         String translation = rs.GetString(msgid);
    267         if (translation != null)
    268           return translation;
    269       }
    270       // Fallback.
    271       return msgid;
    272     }
    273 
    274     /// <summary>
    275     /// Returns the translation of <paramref name="msgid"/> and
    276     /// <paramref name="msgidPlural"/> in a given culture, choosing the right
    277     /// plural form depending on the number <paramref name="n"/>.
    278     /// </summary>
    279     /// <param name="msgid">the key string to be translated, an ASCII
    280     ///                     string</param>
    281     /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>,
    282     ///                           an ASCII string</param>
    283     /// <param name="n">the number, should be &gt;= 0</param>
    284     /// <returns>the translation, or <paramref name="msgid"/> or
    285     ///          <paramref name="msgidPlural"/> if none is found</returns>
    286     public virtual String GetPluralString (String msgid, String msgidPlural, long n, CultureInfo culture) {
    287       foreach (GettextResourceSet rs in GetResourceSetsFor(culture)) {
    288         String translation = rs.GetPluralString(msgid, msgidPlural, n);
    289         if (translation != null)
    290           return translation;
    291       }
    292       // Fallback: Germanic plural form.
    293       return (n == 1 ? msgid : msgidPlural);
    294     }
    295 
    296     // ======================== Public Methods ========================
    297 
    298     /// <summary>
    299     /// Returns the translation of <paramref name="msgid"/> in the current
    300     /// culture.
    301     /// </summary>
    302     /// <param name="msgid">the key string to be translated, an ASCII
    303     ///                     string</param>
    304     /// <returns>the translation of <paramref name="msgid"/>, or
    305     ///          <paramref name="msgid"/> if none is found</returns>
    306     public override String GetString (String msgid) {
    307       return GetString(msgid, CultureInfo.CurrentUICulture);
    308     }
    309 
    310     /// <summary>
    311     /// Returns the translation of <paramref name="msgid"/> and
    312     /// <paramref name="msgidPlural"/> in the current culture, choosing the
    313     /// right plural form depending on the number <paramref name="n"/>.
    314     /// </summary>
    315     /// <param name="msgid">the key string to be translated, an ASCII
    316     ///                     string</param>
    317     /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>,
    318     ///                           an ASCII string</param>
    319     /// <param name="n">the number, should be &gt;= 0</param>
    320     /// <returns>the translation, or <paramref name="msgid"/> or
    321     ///          <paramref name="msgidPlural"/> if none is found</returns>
    322     public virtual String GetPluralString (String msgid, String msgidPlural, long n) {
    323       return GetPluralString(msgid, msgidPlural, n, CultureInfo.CurrentUICulture);
    324     }
    325 
    326   }
    327 
    328   /// <summary>
    329   /// <para>
    330   /// Each instance of this class encapsulates a single PO file.
    331   /// </para>
    332   /// <para>
    333   /// This API of this class is not meant to be used directly; use
    334   /// <c>GettextResourceManager</c> instead.
    335   /// </para>
    336   /// </summary>
    337   // We need this subclass of ResourceSet, because the plural formula must come
    338   // from the same ResourceSet as the object containing the plural forms.
    339   public class GettextResourceSet : ResourceSet {
    340 
    341     /// <summary>
    342     /// Creates a new message catalog. When using this constructor, you
    343     /// must override the <c>ReadResources</c> method, in order to initialize
    344     /// the <c>Table</c> property. The message catalog will support plural
    345     /// forms only if the <c>ReadResources</c> method installs values of type
    346     /// <c>String[]</c> and if the <c>PluralEval</c> method is overridden.
    347     /// </summary>
    348     protected GettextResourceSet ()
    349       : base (DummyResourceReader) {
    350     }
    351 
    352     /// <summary>
    353     /// Creates a new message catalog, by reading the string/value pairs from
    354     /// the given <paramref name="reader"/>. The message catalog will support
    355     /// plural forms only if the reader can produce values of type
    356     /// <c>String[]</c> and if the <c>PluralEval</c> method is overridden.
    357     /// </summary>
    358     public GettextResourceSet (IResourceReader reader)
    359       : base (reader) {
    360     }
    361 
    362     /// <summary>
    363     /// Creates a new message catalog, by reading the string/value pairs from
    364     /// the given <paramref name="stream"/>, which should have the format of
    365     /// a <c>.resources</c> file. The message catalog will not support plural
    366     /// forms.
    367     /// </summary>
    368     public GettextResourceSet (Stream stream)
    369       : base (stream) {
    370     }
    371 
    372     /// <summary>
    373     /// Creates a new message catalog, by reading the string/value pairs from
    374     /// the file with the given <paramref name="fileName"/>. The file should
    375     /// be in the format of a <c>.resources</c> file. The message catalog will
    376     /// not support plural forms.
    377     /// </summary>
    378     public GettextResourceSet (String fileName)
    379       : base (fileName) {
    380     }
    381 
    382     /// <summary>
    383     /// Returns the translation of <paramref name="msgid"/>.
    384     /// </summary>
    385     /// <param name="msgid">the key string to be translated, an ASCII
    386     ///                     string</param>
    387     /// <returns>the translation of <paramref name="msgid"/>, or <c>null</c> if
    388     ///          none is found</returns>
    389     // The default implementation essentially does (String)Table[msgid].
    390     // Here we also catch the plural form case.
    391     public override String GetString (String msgid) {
    392       Object value = GetObject(msgid);
    393       if (value == null || value is String)
    394         return (String)value;
    395       else if (value is String[])
    396         // A plural form, but no number is given.
    397         // Like the C implementation, return the first plural form.
    398         return ((String[]) value)[0];
    399       else
    400         throw new InvalidOperationException("resource for \""+msgid+"\" in "+GetType().FullName+" is not a string");
    401     }
    402 
    403     /// <summary>
    404     /// Returns the translation of <paramref name="msgid"/>, with possibly
    405     /// case-insensitive lookup.
    406     /// </summary>
    407     /// <param name="msgid">the key string to be translated, an ASCII
    408     ///                     string</param>
    409     /// <returns>the translation of <paramref name="msgid"/>, or <c>null</c> if
    410     ///          none is found</returns>
    411     // The default implementation essentially does (String)Table[msgid].
    412     // Here we also catch the plural form case.
    413     public override String GetString (String msgid, bool ignoreCase) {
    414       Object value = GetObject(msgid, ignoreCase);
    415       if (value == null || value is String)
    416         return (String)value;
    417       else if (value is String[])
    418         // A plural form, but no number is given.
    419         // Like the C implementation, return the first plural form.
    420         return ((String[]) value)[0];
    421       else
    422         throw new InvalidOperationException("resource for \""+msgid+"\" in "+GetType().FullName+" is not a string");
    423     }
    424 
    425     /// <summary>
    426     /// Returns the translation of <paramref name="msgid"/> and
    427     /// <paramref name="msgidPlural"/>, choosing the right plural form
    428     /// depending on the number <paramref name="n"/>.
    429     /// </summary>
    430     /// <param name="msgid">the key string to be translated, an ASCII
    431     ///                     string</param>
    432     /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>,
    433     ///                           an ASCII string</param>
    434     /// <param name="n">the number, should be &gt;= 0</param>
    435     /// <returns>the translation, or <c>null</c> if none is found</returns>
    436     public virtual String GetPluralString (String msgid, String msgidPlural, long n) {
    437       Object value = GetObject(msgid);
    438       if (value == null || value is String)
    439         return (String)value;
    440       else if (value is String[]) {
    441         String[] choices = (String[]) value;
    442         long index = PluralEval(n);
    443         return choices[index >= 0 && index < choices.Length ? index : 0];
    444       } else
    445         throw new InvalidOperationException("resource for \""+msgid+"\" in "+GetType().FullName+" is not a string");
    446     }
    447 
    448     /// <summary>
    449     /// Returns the index of the plural form to be chosen for a given number.
    450     /// The default implementation is the Germanic plural formula:
    451     /// zero for <paramref name="n"/> == 1, one for <paramref name="n"/> != 1.
    452     /// </summary>
    453     protected virtual long PluralEval (long n) {
    454       return (n == 1 ? 0 : 1);
    455     }
    456 
    457     /// <summary>
    458     /// Returns the keys of this resource set, i.e. the strings for which
    459     /// <c>GetObject()</c> can return a non-null value.
    460     /// </summary>
    461     public virtual ICollection Keys {
    462       get {
    463         return Table.Keys;
    464       }
    465     }
    466 
    467     /// <summary>
    468     /// A trivial instance of <c>IResourceReader</c> that does nothing.
    469     /// </summary>
    470     // Needed by the no-arguments constructor.
    471     private static IResourceReader DummyResourceReader = new DummyIResourceReader();
    472 
    473   }
    474 
    475   /// <summary>
    476   /// A trivial <c>IResourceReader</c> implementation.
    477   /// </summary>
    478   class DummyIResourceReader : IResourceReader {
    479 
    480     // Implementation of IDisposable.
    481     void System.IDisposable.Dispose () {
    482     }
    483 
    484     // Implementation of IEnumerable.
    485     IEnumerator System.Collections.IEnumerable.GetEnumerator () {
    486       return null;
    487     }
    488 
    489     // Implementation of IResourceReader.
    490     void System.Resources.IResourceReader.Close () {
    491     }
    492     IDictionaryEnumerator System.Resources.IResourceReader.GetEnumerator () {
    493       return null;
    494     }
    495 
    496   }
    497 
    498 }
    499