xref: /netbsd-src/external/gpl2/gettext/dist/gettext-runtime/intl-csharp/intl.cs (revision 946379e7b37692fc43f68eb0d1c10daa0a7f3b6c)
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., 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>
GettextResourceManager(String baseName)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>
GettextResourceManager(String baseName, Assembly assembly)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.
GetSatelliteAssembly(Assembly assembly, String resourceName, CultureInfo culture)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>
MySatelliteAssembly(CultureInfo culture)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>
ConstructClassName(String resourceName)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>
InstantiateResourceSet(Assembly satelliteAssembly, String resourceName, CultureInfo culture)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>
GetResourceSetsFor(CultureInfo culture)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>
GetString(String msgid, CultureInfo culture)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>
GetPluralString(String msgid, String msgidPlural, long n, CultureInfo culture)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>
GetString(String msgid)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>
GetPluralString(String msgid, String msgidPlural, long n)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>
GettextResourceSet()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>
GettextResourceSet(IResourceReader reader)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>
GettextResourceSet(Stream stream)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>
GettextResourceSet(String fileName)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.
GetString(String msgid)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.
GetString(String msgid, bool ignoreCase)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>
GetPluralString(String msgid, String msgidPlural, long n)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>
PluralEval(long n)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.
System.IDisposable.Dispose()481     void System.IDisposable.Dispose () {
482     }
483 
484     // Implementation of IEnumerable.
System.Collections.IEnumerable.GetEnumerator()485     IEnumerator System.Collections.IEnumerable.GetEnumerator () {
486       return null;
487     }
488 
489     // Implementation of IResourceReader.
System.Resources.IResourceReader.Close()490     void System.Resources.IResourceReader.Close () {
491     }
System.Resources.IResourceReader.GetEnumerator()492     IDictionaryEnumerator System.Resources.IResourceReader.GetEnumerator () {
493       return null;
494     }
495 
496   }
497 
498 }
499