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 >= 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 >= 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 >= 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