/* GNU gettext for C# * Copyright (C) 2003, 2005, 2007, 2015-2016 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 Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ /* * 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. * - In the .NET resource approach, there is no support for context specific * translations. * In the GNU gettext approach, we have the GetParticularString 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 (GettextResourceSet) constructor.Invoke(null); } 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 = (GettextResourceSet[]) Loaded[culture]; if (result == null) { lock(this) { // Look up again - maybe another thread has filled in the entry // while we slept waiting for the lock. result = (GettextResourceSet[]) Loaded[culture]; 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 context /// of a given culture. /// /// the context for the key string, an ASCII /// string /// the key string to be translated, an ASCII /// string /// the translation of , or /// if none is found public String GetParticularString (String msgctxt, String msgid, CultureInfo culture) { String combined = msgctxt + "\u0004" + msgid; foreach (GettextResourceSet rs in GetResourceSetsFor(culture)) { String translation = rs.GetString(combined); if (translation != null) return translation; } // Fallback. return msgid; } /// /// Returns the translation of and /// in the context of /// in a given culture, choosing the right /// plural form depending on the number . /// /// the context for the key string, an ASCII /// string /// 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 GetParticularPluralString (String msgctxt, String msgid, String msgidPlural, long n, CultureInfo culture) { String combined = msgctxt + "\u0004" + msgid; foreach (GettextResourceSet rs in GetResourceSetsFor(culture)) { String translation = rs.GetPluralString(combined, msgidPlural, n); if (translation != null) return translation; } // Fallback: Germanic plural form. return (n == 1 ? msgid : msgidPlural); } /// /// 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); } /// /// Returns the translation of in the context /// of in the current culture. /// /// the context for the key string, an ASCII /// string /// the key string to be translated, an ASCII /// string /// the translation of , or /// if none is found public String GetParticularString (String msgctxt, String msgid) { return GetParticularString(msgctxt, msgid, CultureInfo.CurrentUICulture); } /// /// Returns the translation of and /// in the context of /// in the current culture, choosing the /// right plural form depending on the number . /// /// the context for the key string, an ASCII /// string /// 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 GetParticularPluralString (String msgctxt, String msgid, String msgidPlural, long n) { return GetParticularPluralString(msgctxt, 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 ((String[]) value)[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 ((String[]) value)[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 = (String[]) value; 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; } } }