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