From: Bruno Haible Date: Thu, 8 Jan 2004 11:06:36 +0000 (+0000) Subject: Support for C#: Source of GNU.Gettext library. X-Git-Tag: v0.14~93 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0efb681bdb5ae16e12cd91677454728b246009b8;p=thirdparty%2Fgettext.git Support for C#: Source of GNU.Gettext library. --- diff --git a/gettext-runtime/intl-csharp/intl.cs b/gettext-runtime/intl-csharp/intl.cs new file mode 100644 index 000000000..802d807a2 --- /dev/null +++ b/gettext-runtime/intl-csharp/intl.cs @@ -0,0 +1,498 @@ +/* GNU gettext for C# + * Copyright (C) 2003 Free Software Foundation, Inc. + * Written by Bruno Haible , 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 { + + /// + /// Each instance of this class can be used to lookup translations for a + /// given resource name. For each CultureInfo, it performs the lookup + /// in several assemblies, from most specific over territory-neutral to + /// language-neutral. + /// + public class GettextResourceManager : ResourceManager { + + // ======================== Public Constructors ======================== + + /// + /// Constructor. + /// + /// the resource name, also the assembly base + /// name + public GettextResourceManager (String baseName) + : base (baseName, Assembly.GetCallingAssembly(), typeof (GettextResourceSet)) { + } + + /// + /// Constructor. + /// + /// the resource name, also the assembly base + /// name + public GettextResourceManager (String baseName, Assembly assembly) + : base (baseName, assembly, typeof (GettextResourceSet)) { + } + + // ======================== Implementation ======================== + + /// + /// Loads and returns a satellite assembly. + /// + // 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); + } + + /// + /// Loads and returns the satellite assembly for a given culture. + /// + private Assembly MySatelliteAssembly (CultureInfo culture) { + return GetSatelliteAssembly(MainAssembly, BaseName, culture); + } + + /// + /// Converts a resource name to a class name. + /// + /// a nonempty string consisting of alphanumerics and underscores + /// and starting with a letter or underscore + 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(); + } + } + + /// + /// Instantiates a resource set for a given culture. + /// + /// + /// The expected type name is not valid. + /// + /// + /// satelliteAssembly does not contain the expected type. + /// + /// + /// The type has no no-arguments constructor. + /// + 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(); + + /// + /// Returns the array of GettextResourceSets for a given culture, + /// loading them if necessary, and maintaining the cache. + /// + 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; + } + + /* + /// + /// Releases all loaded GettextResourceSets and their assemblies. + /// + // TODO: No way to release an Assembly? + public override void ReleaseAllResources () { + ... + } + */ + + /// + /// Returns the translation of in a given culture. + /// + /// the key string to be translated, an ASCII + /// string + /// the translation of , or + /// if none is found + 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; + } + + /// + /// Returns the translation of and + /// in a given culture, choosing the right + /// plural form depending on the number . + /// + /// the key string to be translated, an ASCII + /// string + /// the English plural of , + /// an ASCII string + /// the number, should be >= 0 + /// the translation, or or + /// if none is found + 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 ======================== + + /// + /// Returns the translation of in the current + /// culture. + /// + /// the key string to be translated, an ASCII + /// string + /// the translation of , or + /// if none is found + public override String GetString (String msgid) { + return GetString(msgid, CultureInfo.CurrentUICulture); + } + + /// + /// Returns the translation of and + /// in the current culture, choosing the + /// right plural form depending on the number . + /// + /// the key string to be translated, an ASCII + /// string + /// the English plural of , + /// an ASCII string + /// the number, should be >= 0 + /// the translation, or or + /// if none is found + public virtual String GetPluralString (String msgid, String msgidPlural, long n) { + return GetPluralString(msgid, msgidPlural, n, CultureInfo.CurrentUICulture); + } + + } + + /// + /// + /// Each instance of this class encapsulates a single PO file. + /// + /// + /// This API of this class is not meant to be used directly; use + /// GettextResourceManager instead. + /// + /// + // 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 { + + /// + /// Creates a new message catalog. When using this constructor, you + /// must override the ReadResources method, in order to initialize + /// the Table property. The message catalog will support plural + /// forms only if the ReadResources method installs values of type + /// String[] and if the PluralEval method is overridden. + /// + protected GettextResourceSet () + : base (DummyResourceReader) { + } + + /// + /// Creates a new message catalog, by reading the string/value pairs from + /// the given . The message catalog will support + /// plural forms only if the reader can produce values of type + /// String[] and if the PluralEval method is overridden. + /// + public GettextResourceSet (IResourceReader reader) + : base (reader) { + } + + /// + /// Creates a new message catalog, by reading the string/value pairs from + /// the given , which should have the format of + /// a .resources file. The message catalog will not support plural + /// forms. + /// + public GettextResourceSet (Stream stream) + : base (stream) { + } + + /// + /// Creates a new message catalog, by reading the string/value pairs from + /// the file with the given . The file should + /// be in the format of a .resources file. The message catalog will + /// not support plural forms. + /// + public GettextResourceSet (String fileName) + : base (fileName) { + } + + /// + /// Returns the translation of . + /// + /// the key string to be translated, an ASCII + /// string + /// the translation of , or null if + /// none is found + // 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"); + } + + /// + /// Returns the translation of , with possibly + /// case-insensitive lookup. + /// + /// the key string to be translated, an ASCII + /// string + /// the translation of , or null if + /// none is found + // 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"); + } + + /// + /// Returns the translation of and + /// , choosing the right plural form + /// depending on the number . + /// + /// the key string to be translated, an ASCII + /// string + /// the English plural of , + /// an ASCII string + /// the number, should be >= 0 + /// the translation, or null if none is found + 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"); + } + + /// + /// Returns the index of the plural form to be chosen for a given number. + /// The default implementation is the Germanic plural formula: + /// zero for == 1, one for != 1. + /// + protected virtual long PluralEval (long n) { + return (n == 1 ? 0 : 1); + } + + /// + /// Returns the keys of this resource set, i.e. the strings for which + /// GetObject() can return a non-null value. + /// + public virtual ICollection Keys { + get { + return Table.Keys; + } + } + + /// + /// A trivial instance of IResourceReader that does nothing. + /// + // Needed by the no-arguments constructor. + private static IResourceReader DummyResourceReader = new DummyIResourceReader(); + + } + + /// + /// A trivial IResourceReader implementation. + /// + 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; + } + + } + +}