--- /dev/null
+/* GNU gettext for C#
+ * Copyright (C) 2003 Free Software Foundation, Inc.
+ * Written by Bruno Haible <bruno@clisp.org>, 2003.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Library General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+/*
+ * Using the GNU gettext approach, compiled message catalogs are assemblies
+ * containing just one class, a subclass of GettextResourceSet. They are thus
+ * interoperable with standard ResourceManager based code.
+ *
+ * The main differences between the common .NET resources approach and the
+ * GNU gettext approach are:
+ * - In the .NET resource approach, the keys are abstract textual shortcuts.
+ * In the GNU gettext approach, the keys are the English/ASCII version
+ * of the messages.
+ * - In the .NET resource approach, the translation files are called
+ * "Resource.locale.resx" and are UTF-8 encoded XML files. In the GNU gettext
+ * approach, the translation files are called "Resource.locale.po" and are
+ * in the encoding the translator has chosen. There are at least three GUI
+ * translating tools (Emacs PO mode, KDE KBabel, GNOME gtranslator).
+ * - In the .NET resource approach, the function ResourceManager.GetString
+ * returns an empty string or throws an InvalidOperationException when no
+ * translation is found. In the GNU gettext approach, the GetString function
+ * returns the (English) message key in that case.
+ * - In the .NET resource approach, there is no support for plural handling.
+ * In the GNU gettext approach, we have the GetPluralString function.
+ *
+ * To compile GNU gettext message catalogs into C# assemblies, the msgfmt
+ * program can be used.
+ */
+
+using System; /* String, InvalidOperationException, Console */
+using System.Globalization; /* CultureInfo */
+using System.Resources; /* ResourceManager, ResourceSet, IResourceReader */
+using System.Reflection; /* Assembly, ConstructorInfo */
+using System.Collections; /* Hashtable, ICollection, IEnumerator, IDictionaryEnumerator */
+using System.IO; /* Path, FileNotFoundException, Stream */
+using System.Text; /* StringBuilder */
+
+namespace GNU.Gettext {
+
+ /// <summary>
+ /// Each instance of this class can be used to lookup translations for a
+ /// given resource name. For each <c>CultureInfo</c>, it performs the lookup
+ /// in several assemblies, from most specific over territory-neutral to
+ /// language-neutral.
+ /// </summary>
+ public class GettextResourceManager : ResourceManager {
+
+ // ======================== Public Constructors ========================
+
+ /// <summary>
+ /// Constructor.
+ /// </summary>
+ /// <param name="baseName">the resource name, also the assembly base
+ /// name</param>
+ public GettextResourceManager (String baseName)
+ : base (baseName, Assembly.GetCallingAssembly(), typeof (GettextResourceSet)) {
+ }
+
+ /// <summary>
+ /// Constructor.
+ /// </summary>
+ /// <param name="baseName">the resource name, also the assembly base
+ /// name</param>
+ public GettextResourceManager (String baseName, Assembly assembly)
+ : base (baseName, assembly, typeof (GettextResourceSet)) {
+ }
+
+ // ======================== Implementation ========================
+
+ /// <summary>
+ /// Loads and returns a satellite assembly.
+ /// </summary>
+ // This is like Assembly.GetSatelliteAssembly, but uses resourceName
+ // instead of assembly.GetName().Name, and works around a bug in
+ // mono-0.28.
+ private static Assembly GetSatelliteAssembly (Assembly assembly, String resourceName, CultureInfo culture) {
+ String satelliteExpectedLocation =
+ Path.GetDirectoryName(assembly.Location)
+ + Path.DirectorySeparatorChar + culture.Name
+ + Path.DirectorySeparatorChar + resourceName + ".resources.dll";
+ return Assembly.LoadFrom(satelliteExpectedLocation);
+ }
+
+ /// <summary>
+ /// Loads and returns the satellite assembly for a given culture.
+ /// </summary>
+ private Assembly MySatelliteAssembly (CultureInfo culture) {
+ return GetSatelliteAssembly(MainAssembly, BaseName, culture);
+ }
+
+ /// <summary>
+ /// Converts a resource name to a class name.
+ /// </summary>
+ /// <returns>a nonempty string consisting of alphanumerics and underscores
+ /// and starting with a letter or underscore</returns>
+ private static String ConstructClassName (String resourceName) {
+ // We could just return an arbitrary fixed class name, like "Messages",
+ // assuming that every assembly will only ever contain one
+ // GettextResourceSet subclass, but this assumption would break the day
+ // we want to support multi-domain PO files in the same format...
+ bool valid = (resourceName.Length > 0);
+ for (int i = 0; valid && i < resourceName.Length; i++) {
+ char c = resourceName[i];
+ if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
+ || (i > 0 && c >= '0' && c <= '9')))
+ valid = false;
+ }
+ if (valid)
+ return resourceName;
+ else {
+ // Use hexadecimal escapes, using the underscore as escape character.
+ String hexdigit = "0123456789abcdef";
+ StringBuilder b = new StringBuilder();
+ b.Append("__UESCAPED__");
+ for (int i = 0; i < resourceName.Length; i++) {
+ char c = resourceName[i];
+ if (c >= 0xd800 && c < 0xdc00
+ && i+1 < resourceName.Length
+ && resourceName[i+1] >= 0xdc00 && resourceName[i+1] < 0xe000) {
+ // Combine two UTF-16 words to a character.
+ char c2 = resourceName[i+1];
+ int uc = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
+ b.Append('_');
+ b.Append('U');
+ b.Append(hexdigit[(uc >> 28) & 0x0f]);
+ b.Append(hexdigit[(uc >> 24) & 0x0f]);
+ b.Append(hexdigit[(uc >> 20) & 0x0f]);
+ b.Append(hexdigit[(uc >> 16) & 0x0f]);
+ b.Append(hexdigit[(uc >> 12) & 0x0f]);
+ b.Append(hexdigit[(uc >> 8) & 0x0f]);
+ b.Append(hexdigit[(uc >> 4) & 0x0f]);
+ b.Append(hexdigit[uc & 0x0f]);
+ i++;
+ } else if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
+ || (c >= '0' && c <= '9'))) {
+ int uc = c;
+ b.Append('_');
+ b.Append('u');
+ b.Append(hexdigit[(uc >> 12) & 0x0f]);
+ b.Append(hexdigit[(uc >> 8) & 0x0f]);
+ b.Append(hexdigit[(uc >> 4) & 0x0f]);
+ b.Append(hexdigit[uc & 0x0f]);
+ } else
+ b.Append(c);
+ }
+ return b.ToString();
+ }
+ }
+
+ /// <summary>
+ /// Instantiates a resource set for a given culture.
+ /// </summary>
+ /// <exception cref="ArgumentException">
+ /// The expected type name is not valid.
+ /// </exception>
+ /// <exception cref="ReflectionTypeLoadException">
+ /// satelliteAssembly does not contain the expected type.
+ /// </exception>
+ /// <exception cref="NullReferenceException">
+ /// The type has no no-arguments constructor.
+ /// </exception>
+ private static GettextResourceSet InstantiateResourceSet (Assembly satelliteAssembly, String resourceName, CultureInfo culture) {
+ // We expect a class with a culture dependent class name.
+ Type clazz = satelliteAssembly.GetType(ConstructClassName(resourceName)+"_"+culture.Name.Replace('-','_'));
+ // We expect it has a no-argument constructor, and invoke it.
+ ConstructorInfo constructor = clazz.GetConstructor(Type.EmptyTypes);
+ return constructor.Invoke(null) as GettextResourceSet;
+ }
+
+ private static GettextResourceSet[] EmptyResourceSetArray = new GettextResourceSet[0];
+
+ // Cache for already loaded GettextResourceSet cascades.
+ private Hashtable /* CultureInfo -> GettextResourceSet[] */ Loaded = new Hashtable();
+
+ /// <summary>
+ /// Returns the array of <c>GettextResourceSet</c>s for a given culture,
+ /// loading them if necessary, and maintaining the cache.
+ /// </summary>
+ private GettextResourceSet[] GetResourceSetsFor (CultureInfo culture) {
+ //Console.WriteLine(">> GetResourceSetsFor "+culture);
+ // Look up in the cache.
+ GettextResourceSet[] result = Loaded[culture] as GettextResourceSet[];
+ if (result == null) {
+ lock(this) {
+ // Look up again - maybe another thread has filled in the entry
+ // while we slept waiting for the lock.
+ result = Loaded[culture] as GettextResourceSet[];
+ if (result == null) {
+ // Determine the GettextResourceSets for the given culture.
+ if (culture.Parent == null || culture.Equals(CultureInfo.InvariantCulture))
+ // Invariant culture.
+ result = EmptyResourceSetArray;
+ else {
+ // Use a satellite assembly as primary GettextResourceSet, and
+ // the result for the parent culture as fallback.
+ GettextResourceSet[] parentResult = GetResourceSetsFor(culture.Parent);
+ Assembly satelliteAssembly;
+ try {
+ satelliteAssembly = MySatelliteAssembly(culture);
+ } catch (FileNotFoundException e) {
+ satelliteAssembly = null;
+ }
+ if (satelliteAssembly != null) {
+ GettextResourceSet satelliteResourceSet;
+ try {
+ satelliteResourceSet = InstantiateResourceSet(satelliteAssembly, BaseName, culture);
+ } catch (Exception e) {
+ Console.Error.WriteLine(e);
+ Console.Error.WriteLine(e.StackTrace);
+ satelliteResourceSet = null;
+ }
+ if (satelliteResourceSet != null) {
+ result = new GettextResourceSet[1+parentResult.Length];
+ result[0] = satelliteResourceSet;
+ Array.Copy(parentResult, 0, result, 1, parentResult.Length);
+ } else
+ result = parentResult;
+ } else
+ result = parentResult;
+ }
+ // Put the result into the cache.
+ Loaded.Add(culture, result);
+ }
+ }
+ }
+ //Console.WriteLine("<< GetResourceSetsFor "+culture);
+ return result;
+ }
+
+ /*
+ /// <summary>
+ /// Releases all loaded <c>GettextResourceSet</c>s and their assemblies.
+ /// </summary>
+ // TODO: No way to release an Assembly?
+ public override void ReleaseAllResources () {
+ ...
+ }
+ */
+
+ /// <summary>
+ /// Returns the translation of <paramref name="msgid"/> in a given culture.
+ /// </summary>
+ /// <param name="msgid">the key string to be translated, an ASCII
+ /// string</param>
+ /// <returns>the translation of <paramref name="msgid"/>, or
+ /// <paramref name="msgid"/> if none is found</returns>
+ public override String GetString (String msgid, CultureInfo culture) {
+ foreach (GettextResourceSet rs in GetResourceSetsFor(culture)) {
+ String translation = rs.GetString(msgid);
+ if (translation != null)
+ return translation;
+ }
+ // Fallback.
+ return msgid;
+ }
+
+ /// <summary>
+ /// Returns the translation of <paramref name="msgid"/> and
+ /// <paramref name="msgidPlural"/> in a given culture, choosing the right
+ /// plural form depending on the number <paramref name="n"/>.
+ /// </summary>
+ /// <param name="msgid">the key string to be translated, an ASCII
+ /// string</param>
+ /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>,
+ /// an ASCII string</param>
+ /// <param name="n">the number, should be >= 0</param>
+ /// <returns>the translation, or <paramref name="msgid"/> or
+ /// <paramref name="msgidPlural"/> if none is found</returns>
+ public virtual String GetPluralString (String msgid, String msgidPlural, long n, CultureInfo culture) {
+ foreach (GettextResourceSet rs in GetResourceSetsFor(culture)) {
+ String translation = rs.GetPluralString(msgid, msgidPlural, n);
+ if (translation != null)
+ return translation;
+ }
+ // Fallback: Germanic plural form.
+ return (n == 1 ? msgid : msgidPlural);
+ }
+
+ // ======================== Public Methods ========================
+
+ /// <summary>
+ /// Returns the translation of <paramref name="msgid"/> in the current
+ /// culture.
+ /// </summary>
+ /// <param name="msgid">the key string to be translated, an ASCII
+ /// string</param>
+ /// <returns>the translation of <paramref name="msgid"/>, or
+ /// <paramref name="msgid"/> if none is found</returns>
+ public override String GetString (String msgid) {
+ return GetString(msgid, CultureInfo.CurrentUICulture);
+ }
+
+ /// <summary>
+ /// Returns the translation of <paramref name="msgid"/> and
+ /// <paramref name="msgidPlural"/> in the current culture, choosing the
+ /// right plural form depending on the number <paramref name="n"/>.
+ /// </summary>
+ /// <param name="msgid">the key string to be translated, an ASCII
+ /// string</param>
+ /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>,
+ /// an ASCII string</param>
+ /// <param name="n">the number, should be >= 0</param>
+ /// <returns>the translation, or <paramref name="msgid"/> or
+ /// <paramref name="msgidPlural"/> if none is found</returns>
+ public virtual String GetPluralString (String msgid, String msgidPlural, long n) {
+ return GetPluralString(msgid, msgidPlural, n, CultureInfo.CurrentUICulture);
+ }
+
+ }
+
+ /// <summary>
+ /// <para>
+ /// Each instance of this class encapsulates a single PO file.
+ /// </para>
+ /// <para>
+ /// This API of this class is not meant to be used directly; use
+ /// <c>GettextResourceManager</c> instead.
+ /// </para>
+ /// </summary>
+ // We need this subclass of ResourceSet, because the plural formula must come
+ // from the same ResourceSet as the object containing the plural forms.
+ public class GettextResourceSet : ResourceSet {
+
+ /// <summary>
+ /// Creates a new message catalog. When using this constructor, you
+ /// must override the <c>ReadResources</c> method, in order to initialize
+ /// the <c>Table</c> property. The message catalog will support plural
+ /// forms only if the <c>ReadResources</c> method installs values of type
+ /// <c>String[]</c> and if the <c>PluralEval</c> method is overridden.
+ /// </summary>
+ protected GettextResourceSet ()
+ : base (DummyResourceReader) {
+ }
+
+ /// <summary>
+ /// Creates a new message catalog, by reading the string/value pairs from
+ /// the given <paramref name="reader"/>. The message catalog will support
+ /// plural forms only if the reader can produce values of type
+ /// <c>String[]</c> and if the <c>PluralEval</c> method is overridden.
+ /// </summary>
+ public GettextResourceSet (IResourceReader reader)
+ : base (reader) {
+ }
+
+ /// <summary>
+ /// Creates a new message catalog, by reading the string/value pairs from
+ /// the given <paramref name="stream"/>, which should have the format of
+ /// a <c>.resources</c> file. The message catalog will not support plural
+ /// forms.
+ /// </summary>
+ public GettextResourceSet (Stream stream)
+ : base (stream) {
+ }
+
+ /// <summary>
+ /// Creates a new message catalog, by reading the string/value pairs from
+ /// the file with the given <paramref name="fileName"/>. The file should
+ /// be in the format of a <c>.resources</c> file. The message catalog will
+ /// not support plural forms.
+ /// </summary>
+ public GettextResourceSet (String fileName)
+ : base (fileName) {
+ }
+
+ /// <summary>
+ /// Returns the translation of <paramref name="msgid"/>.
+ /// </summary>
+ /// <param name="msgid">the key string to be translated, an ASCII
+ /// string</param>
+ /// <returns>the translation of <paramref name="msgid"/>, or <c>null</c> if
+ /// none is found</returns>
+ // The default implementation essentially does (String)Table[msgid].
+ // Here we also catch the plural form case.
+ public override String GetString (String msgid) {
+ Object value = GetObject(msgid);
+ if (value == null || value is String)
+ return (String)value;
+ else if (value is String[])
+ // A plural form, but no number is given.
+ // Like the C implementation, return the first plural form.
+ return (value as String[])[0];
+ else
+ throw new InvalidOperationException("resource for \""+msgid+"\" in "+GetType().FullName+" is not a string");
+ }
+
+ /// <summary>
+ /// Returns the translation of <paramref name="msgid"/>, with possibly
+ /// case-insensitive lookup.
+ /// </summary>
+ /// <param name="msgid">the key string to be translated, an ASCII
+ /// string</param>
+ /// <returns>the translation of <paramref name="msgid"/>, or <c>null</c> if
+ /// none is found</returns>
+ // The default implementation essentially does (String)Table[msgid].
+ // Here we also catch the plural form case.
+ public override String GetString (String msgid, bool ignoreCase) {
+ Object value = GetObject(msgid, ignoreCase);
+ if (value == null || value is String)
+ return (String)value;
+ else if (value is String[])
+ // A plural form, but no number is given.
+ // Like the C implementation, return the first plural form.
+ return (value as String[])[0];
+ else
+ throw new InvalidOperationException("resource for \""+msgid+"\" in "+GetType().FullName+" is not a string");
+ }
+
+ /// <summary>
+ /// Returns the translation of <paramref name="msgid"/> and
+ /// <paramref name="msgidPlural"/>, choosing the right plural form
+ /// depending on the number <paramref name="n"/>.
+ /// </summary>
+ /// <param name="msgid">the key string to be translated, an ASCII
+ /// string</param>
+ /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>,
+ /// an ASCII string</param>
+ /// <param name="n">the number, should be >= 0</param>
+ /// <returns>the translation, or <c>null</c> if none is found</returns>
+ public virtual String GetPluralString (String msgid, String msgidPlural, long n) {
+ Object value = GetObject(msgid);
+ if (value == null || value is String)
+ return (String)value;
+ else if (value is String[]) {
+ String[] choices = value as String[];
+ long index = PluralEval(n);
+ return choices[index >= 0 && index < choices.Length ? index : 0];
+ } else
+ throw new InvalidOperationException("resource for \""+msgid+"\" in "+GetType().FullName+" is not a string");
+ }
+
+ /// <summary>
+ /// Returns the index of the plural form to be chosen for a given number.
+ /// The default implementation is the Germanic plural formula:
+ /// zero for <paramref name="n"/> == 1, one for <paramref name="n"/> != 1.
+ /// </summary>
+ protected virtual long PluralEval (long n) {
+ return (n == 1 ? 0 : 1);
+ }
+
+ /// <summary>
+ /// Returns the keys of this resource set, i.e. the strings for which
+ /// <c>GetObject()</c> can return a non-null value.
+ /// </summary>
+ public virtual ICollection Keys {
+ get {
+ return Table.Keys;
+ }
+ }
+
+ /// <summary>
+ /// A trivial instance of <c>IResourceReader</c> that does nothing.
+ /// </summary>
+ // Needed by the no-arguments constructor.
+ private static IResourceReader DummyResourceReader = new DummyIResourceReader();
+
+ }
+
+ /// <summary>
+ /// A trivial <c>IResourceReader</c> implementation.
+ /// </summary>
+ class DummyIResourceReader : IResourceReader {
+
+ // Implementation of IDisposable.
+ void System.IDisposable.Dispose () {
+ }
+
+ // Implementation of IEnumerable.
+ IEnumerator System.Collections.IEnumerable.GetEnumerator () {
+ return null;
+ }
+
+ // Implementation of IResourceReader.
+ void System.Resources.IResourceReader.Close () {
+ }
+ IDictionaryEnumerator System.Resources.IResourceReader.GetEnumerator () {
+ return null;
+ }
+
+ }
+
+}