2 * Copyright (C) 2003, 2005, 2007 Free Software Foundation, Inc.
3 * Written by Bruno Haible <bruno@clisp.org>, 2003.
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.
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.
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/>.
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.
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
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
42 * In the GNU gettext approach, we have the GetParticularString function.
44 * To compile GNU gettext message catalogs into C# assemblies, the msgfmt
45 * program can be used.
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 */
56 namespace GNU.Gettext {
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
64 public class GettextResourceManager : ResourceManager {
66 // ======================== Public Constructors ========================
71 /// <param name="baseName">the resource name, also the assembly base
73 public GettextResourceManager (String baseName)
74 : base (baseName, Assembly.GetCallingAssembly(), typeof (GettextResourceSet)) {
80 /// <param name="baseName">the resource name, also the assembly base
82 public GettextResourceManager (String baseName, Assembly assembly)
83 : base (baseName, assembly, typeof (GettextResourceSet)) {
86 // ======================== Implementation ========================
89 /// Loads and returns a satellite assembly.
91 // This is like Assembly.GetSatelliteAssembly, but uses resourceName
92 // instead of assembly.GetName().Name, and works around a bug in
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);
103 /// Loads and returns the satellite assembly for a given culture.
105 private Assembly MySatelliteAssembly (CultureInfo culture) {
106 return GetSatelliteAssembly(MainAssembly, BaseName, culture);
110 /// Converts a resource name to a class name.
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')))
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);
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]);
152 } else if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
153 || (c >= '0' && c <= '9'))) {
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]);
169 /// Instantiates a resource set for a given culture.
171 /// <exception cref="ArgumentException">
172 /// The expected type name is not valid.
174 /// <exception cref="ReflectionTypeLoadException">
175 /// satelliteAssembly does not contain the expected type.
177 /// <exception cref="NullReferenceException">
178 /// The type has no no-arguments constructor.
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);
188 private static GettextResourceSet[] EmptyResourceSetArray = new GettextResourceSet[0];
190 // Cache for already loaded GettextResourceSet cascades.
191 private Hashtable /* CultureInfo -> GettextResourceSet[] */ Loaded = new Hashtable();
194 /// Returns the array of <c>GettextResourceSet</c>s for a given culture,
195 /// loading them if necessary, and maintaining the cache.
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) {
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;
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;
217 satelliteAssembly = MySatelliteAssembly(culture);
218 } catch (FileNotFoundException e) {
219 satelliteAssembly = null;
221 if (satelliteAssembly != null) {
222 GettextResourceSet satelliteResourceSet;
224 satelliteResourceSet = InstantiateResourceSet(satelliteAssembly, BaseName, culture);
225 } catch (Exception e) {
226 Console.Error.WriteLine(e);
227 Console.Error.WriteLine(e.StackTrace);
228 satelliteResourceSet = null;
230 if (satelliteResourceSet != null) {
231 result = new GettextResourceSet[1+parentResult.Length];
232 result[0] = satelliteResourceSet;
233 Array.Copy(parentResult, 0, result, 1, parentResult.Length);
235 result = parentResult;
237 result = parentResult;
239 // Put the result into the cache.
240 Loaded.Add(culture, result);
244 //Console.WriteLine("<< GetResourceSetsFor "+culture);
250 /// Releases all loaded <c>GettextResourceSet</c>s and their assemblies.
252 // TODO: No way to release an Assembly?
253 public override void ReleaseAllResources () {
259 /// Returns the translation of <paramref name="msgid"/> in a given culture.
261 /// <param name="msgid">the key string to be translated, an ASCII
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)
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"/>.
280 /// <param name="msgid">the key string to be translated, an ASCII
282 /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>,
283 /// an ASCII string</param>
284 /// <param name="n">the number, should be >= 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)
293 // Fallback: Germanic plural form.
294 return (n == 1 ? msgid : msgidPlural);
297 // ======================== Public Methods ========================
300 /// Returns the translation of <paramref name="msgid"/> in the context
301 /// of <paramref name="msgctxt"/> a given culture.
303 /// <param name="msgctxt">the context for the key string, an ASCII
305 /// <param name="msgid">the key string to be translated, an ASCII
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)
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"/>.
326 /// <param name="msgctxt">the context for the key string, an ASCII
328 /// <param name="msgid">the key string to be translated, an ASCII
330 /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>,
331 /// an ASCII string</param>
332 /// <param name="n">the number, should be >= 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)
342 // Fallback: Germanic plural form.
343 return (n == 1 ? msgid : msgidPlural);
347 /// Returns the translation of <paramref name="msgid"/> in the current
350 /// <param name="msgid">the key string to be translated, an ASCII
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);
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"/>.
363 /// <param name="msgid">the key string to be translated, an ASCII
365 /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>,
366 /// an ASCII string</param>
367 /// <param name="n">the number, should be >= 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);
375 /// Returns the translation of <paramref name="msgid"/> in the context
376 /// of <paramref name="msgctxt"/> in the current culture.
378 /// <param name="msgctxt">the context for the key string, an ASCII
380 /// <param name="msgid">the key string to be translated, an ASCII
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);
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"/>.
394 /// <param name="msgctxt">the context for the key string, an ASCII
396 /// <param name="msgid">the key string to be translated, an ASCII
398 /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>,
399 /// an ASCII string</param>
400 /// <param name="n">the number, should be >= 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);
411 /// Each instance of this class encapsulates a single PO file.
414 /// This API of this class is not meant to be used directly; use
415 /// <c>GettextResourceManager</c> instead.
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 {
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.
429 protected GettextResourceSet ()
430 : base (DummyResourceReader) {
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.
439 public GettextResourceSet (IResourceReader reader)
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
449 public GettextResourceSet (Stream stream)
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.
459 public GettextResourceSet (String fileName)
464 /// Returns the translation of <paramref name="msgid"/>.
466 /// <param name="msgid">the key string to be translated, an ASCII
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];
481 throw new InvalidOperationException("resource for \""+msgid+"\" in "+GetType().FullName+" is not a string");
485 /// Returns the translation of <paramref name="msgid"/>, with possibly
486 /// case-insensitive lookup.
488 /// <param name="msgid">the key string to be translated, an ASCII
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];
503 throw new InvalidOperationException("resource for \""+msgid+"\" in "+GetType().FullName+" is not a string");
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"/>.
511 /// <param name="msgid">the key string to be translated, an ASCII
513 /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>,
514 /// an ASCII string</param>
515 /// <param name="n">the number, should be >= 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];
526 throw new InvalidOperationException("resource for \""+msgid+"\" in "+GetType().FullName+" is not a string");
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.
534 protected virtual long PluralEval (long n) {
535 return (n == 1 ? 0 : 1);
539 /// Returns the keys of this resource set, i.e. the strings for which
540 /// <c>GetObject()</c> can return a non-null value.
542 public virtual ICollection Keys {
549 /// A trivial instance of <c>IResourceReader</c> that does nothing.
551 // Needed by the no-arguments constructor.
552 private static IResourceReader DummyResourceReader = new DummyIResourceReader();
557 /// A trivial <c>IResourceReader</c> implementation.
559 class DummyIResourceReader : IResourceReader {
561 // Implementation of IDisposable.
562 void System.IDisposable.Dispose () {
565 // Implementation of IEnumerable.
566 IEnumerator System.Collections.IEnumerable.GetEnumerator () {
570 // Implementation of IResourceReader.
571 void System.Resources.IResourceReader.Close () {
573 IDictionaryEnumerator System.Resources.IResourceReader.GetEnumerator () {