source: trunk/third/gettext/gettext-runtime/intl-csharp/intl.cs @ 21665

Revision 21665, 20.6 KB checked in by ghudson, 20 years ago (diff)
This commit was generated by cvs2svn to compensate for changes in r21664, which included commits to RCS files with non-trunk default branches.
Line 
1/* GNU gettext for C#
2 * Copyright (C) 2003, 2005 Free Software Foundation, Inc.
3 * Written by Bruno Haible <bruno@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., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
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
47using System; /* String, InvalidOperationException, Console */
48using System.Globalization; /* CultureInfo */
49using System.Resources; /* ResourceManager, ResourceSet, IResourceReader */
50using System.Reflection; /* Assembly, ConstructorInfo */
51using System.Collections; /* Hashtable, ICollection, IEnumerator, IDictionaryEnumerator */
52using System.IO; /* Path, FileNotFoundException, Stream */
53using System.Text; /* StringBuilder */
54
55namespace 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}
Note: See TracBrowser for help on using the repository browser.