1/* GNU gettext for C# 2 * Copyright (C) 2003, 2005, 2007 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 * - In the .NET resource approach, there is no support for context specific 43 * translations. 44 * In the GNU gettext approach, we have the GetParticularString function. 45 * 46 * To compile GNU gettext message catalogs into C# assemblies, the msgfmt 47 * program can be used. 48 */ 49 50using System; /* String, InvalidOperationException, Console */ 51using System.Globalization; /* CultureInfo */ 52using System.Resources; /* ResourceManager, ResourceSet, IResourceReader */ 53using System.Reflection; /* Assembly, ConstructorInfo */ 54using System.Collections; /* Hashtable, ICollection, IEnumerator, IDictionaryEnumerator */ 55using System.IO; /* Path, FileNotFoundException, Stream */ 56using System.Text; /* StringBuilder */ 57 58namespace GNU.Gettext { 59 60 /// <summary> 61 /// Each instance of this class can be used to lookup translations for a 62 /// given resource name. For each <c>CultureInfo</c>, it performs the lookup 63 /// in several assemblies, from most specific over territory-neutral to 64 /// language-neutral. 65 /// </summary> 66 public class GettextResourceManager : ResourceManager { 67 68 // ======================== Public Constructors ======================== 69 70 /// <summary> 71 /// Constructor. 72 /// </summary> 73 /// <param name="baseName">the resource name, also the assembly base 74 /// name</param> 75 public GettextResourceManager (String baseName) 76 : base (baseName, Assembly.GetCallingAssembly(), typeof (GettextResourceSet)) { 77 } 78 79 /// <summary> 80 /// Constructor. 81 /// </summary> 82 /// <param name="baseName">the resource name, also the assembly base 83 /// name</param> 84 public GettextResourceManager (String baseName, Assembly assembly) 85 : base (baseName, assembly, typeof (GettextResourceSet)) { 86 } 87 88 // ======================== Implementation ======================== 89 90 /// <summary> 91 /// Loads and returns a satellite assembly. 92 /// </summary> 93 // This is like Assembly.GetSatelliteAssembly, but uses resourceName 94 // instead of assembly.GetName().Name, and works around a bug in 95 // mono-0.28. 96 private static Assembly GetSatelliteAssembly (Assembly assembly, String resourceName, CultureInfo culture) { 97 String satelliteExpectedLocation = 98 Path.GetDirectoryName(assembly.Location) 99 + Path.DirectorySeparatorChar + culture.Name 100 + Path.DirectorySeparatorChar + resourceName + ".resources.dll"; 101 return Assembly.LoadFrom(satelliteExpectedLocation); 102 } 103 104 /// <summary> 105 /// Loads and returns the satellite assembly for a given culture. 106 /// </summary> 107 private Assembly MySatelliteAssembly (CultureInfo culture) { 108 return GetSatelliteAssembly(MainAssembly, BaseName, culture); 109 } 110 111 /// <summary> 112 /// Converts a resource name to a class name. 113 /// </summary> 114 /// <returns>a nonempty string consisting of alphanumerics and underscores 115 /// and starting with a letter or underscore</returns> 116 private static String ConstructClassName (String resourceName) { 117 // We could just return an arbitrary fixed class name, like "Messages", 118 // assuming that every assembly will only ever contain one 119 // GettextResourceSet subclass, but this assumption would break the day 120 // we want to support multi-domain PO files in the same format... 121 bool valid = (resourceName.Length > 0); 122 for (int i = 0; valid && i < resourceName.Length; i++) { 123 char c = resourceName[i]; 124 if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_') 125 || (i > 0 && c >= '0' && c <= '9'))) 126 valid = false; 127 } 128 if (valid) 129 return resourceName; 130 else { 131 // Use hexadecimal escapes, using the underscore as escape character. 132 String hexdigit = "0123456789abcdef"; 133 StringBuilder b = new StringBuilder(); 134 b.Append("__UESCAPED__"); 135 for (int i = 0; i < resourceName.Length; i++) { 136 char c = resourceName[i]; 137 if (c >= 0xd800 && c < 0xdc00 138 && i+1 < resourceName.Length 139 && resourceName[i+1] >= 0xdc00 && resourceName[i+1] < 0xe000) { 140 // Combine two UTF-16 words to a character. 141 char c2 = resourceName[i+1]; 142 int uc = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00); 143 b.Append('_'); 144 b.Append('U'); 145 b.Append(hexdigit[(uc >> 28) & 0x0f]); 146 b.Append(hexdigit[(uc >> 24) & 0x0f]); 147 b.Append(hexdigit[(uc >> 20) & 0x0f]); 148 b.Append(hexdigit[(uc >> 16) & 0x0f]); 149 b.Append(hexdigit[(uc >> 12) & 0x0f]); 150 b.Append(hexdigit[(uc >> 8) & 0x0f]); 151 b.Append(hexdigit[(uc >> 4) & 0x0f]); 152 b.Append(hexdigit[uc & 0x0f]); 153 i++; 154 } else if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') 155 || (c >= '0' && c <= '9'))) { 156 int uc = c; 157 b.Append('_'); 158 b.Append('u'); 159 b.Append(hexdigit[(uc >> 12) & 0x0f]); 160 b.Append(hexdigit[(uc >> 8) & 0x0f]); 161 b.Append(hexdigit[(uc >> 4) & 0x0f]); 162 b.Append(hexdigit[uc & 0x0f]); 163 } else 164 b.Append(c); 165 } 166 return b.ToString(); 167 } 168 } 169 170 /// <summary> 171 /// Instantiates a resource set for a given culture. 172 /// </summary> 173 /// <exception cref="ArgumentException"> 174 /// The expected type name is not valid. 175 /// </exception> 176 /// <exception cref="ReflectionTypeLoadException"> 177 /// satelliteAssembly does not contain the expected type. 178 /// </exception> 179 /// <exception cref="NullReferenceException"> 180 /// The type has no no-arguments constructor. 181 /// </exception> 182 private static GettextResourceSet InstantiateResourceSet (Assembly satelliteAssembly, String resourceName, CultureInfo culture) { 183 // We expect a class with a culture dependent class name. 184 Type clazz = satelliteAssembly.GetType(ConstructClassName(resourceName)+"_"+culture.Name.Replace('-','_')); 185 // We expect it has a no-argument constructor, and invoke it. 186 ConstructorInfo constructor = clazz.GetConstructor(Type.EmptyTypes); 187 return (GettextResourceSet) constructor.Invoke(null); 188 } 189 190 private static GettextResourceSet[] EmptyResourceSetArray = new GettextResourceSet[0]; 191 192 // Cache for already loaded GettextResourceSet cascades. 193 private Hashtable /* CultureInfo -> GettextResourceSet[] */ Loaded = new Hashtable(); 194 195 /// <summary> 196 /// Returns the array of <c>GettextResourceSet</c>s for a given culture, 197 /// loading them if necessary, and maintaining the cache. 198 /// </summary> 199 private GettextResourceSet[] GetResourceSetsFor (CultureInfo culture) { 200 //Console.WriteLine(">> GetResourceSetsFor "+culture); 201 // Look up in the cache. 202 GettextResourceSet[] result = (GettextResourceSet[]) Loaded[culture]; 203 if (result == null) { 204 lock(this) { 205 // Look up again - maybe another thread has filled in the entry 206 // while we slept waiting for the lock. 207 result = (GettextResourceSet[]) Loaded[culture]; 208 if (result == null) { 209 // Determine the GettextResourceSets for the given culture. 210 if (culture.Parent == null || culture.Equals(CultureInfo.InvariantCulture)) 211 // Invariant culture. 212 result = EmptyResourceSetArray; 213 else { 214 // Use a satellite assembly as primary GettextResourceSet, and 215 // the result for the parent culture as fallback. 216 GettextResourceSet[] parentResult = GetResourceSetsFor(culture.Parent); 217 Assembly satelliteAssembly; 218 try { 219 satelliteAssembly = MySatelliteAssembly(culture); 220 } catch (FileNotFoundException e) { 221 satelliteAssembly = null; 222 } 223 if (satelliteAssembly != null) { 224 GettextResourceSet satelliteResourceSet; 225 try { 226 satelliteResourceSet = InstantiateResourceSet(satelliteAssembly, BaseName, culture); 227 } catch (Exception e) { 228 Console.Error.WriteLine(e); 229 Console.Error.WriteLine(e.StackTrace); 230 satelliteResourceSet = null; 231 } 232 if (satelliteResourceSet != null) { 233 result = new GettextResourceSet[1+parentResult.Length]; 234 result[0] = satelliteResourceSet; 235 Array.Copy(parentResult, 0, result, 1, parentResult.Length); 236 } else 237 result = parentResult; 238 } else 239 result = parentResult; 240 } 241 // Put the result into the cache. 242 Loaded.Add(culture, result); 243 } 244 } 245 } 246 //Console.WriteLine("<< GetResourceSetsFor "+culture); 247 return result; 248 } 249 250 /* 251 /// <summary> 252 /// Releases all loaded <c>GettextResourceSet</c>s and their assemblies. 253 /// </summary> 254 // TODO: No way to release an Assembly? 255 public override void ReleaseAllResources () { 256 ... 257 } 258 */ 259 260 /// <summary> 261 /// Returns the translation of <paramref name="msgid"/> in a given culture. 262 /// </summary> 263 /// <param name="msgid">the key string to be translated, an ASCII 264 /// string</param> 265 /// <returns>the translation of <paramref name="msgid"/>, or 266 /// <paramref name="msgid"/> if none is found</returns> 267 public override String GetString (String msgid, CultureInfo culture) { 268 foreach (GettextResourceSet rs in GetResourceSetsFor(culture)) { 269 String translation = rs.GetString(msgid); 270 if (translation != null) 271 return translation; 272 } 273 // Fallback. 274 return msgid; 275 } 276 277 /// <summary> 278 /// Returns the translation of <paramref name="msgid"/> and 279 /// <paramref name="msgidPlural"/> in a given culture, choosing the right 280 /// plural form depending on the number <paramref name="n"/>. 281 /// </summary> 282 /// <param name="msgid">the key string to be translated, an ASCII 283 /// string</param> 284 /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>, 285 /// an ASCII string</param> 286 /// <param name="n">the number, should be >= 0</param> 287 /// <returns>the translation, or <paramref name="msgid"/> or 288 /// <paramref name="msgidPlural"/> if none is found</returns> 289 public virtual String GetPluralString (String msgid, String msgidPlural, long n, CultureInfo culture) { 290 foreach (GettextResourceSet rs in GetResourceSetsFor(culture)) { 291 String translation = rs.GetPluralString(msgid, msgidPlural, n); 292 if (translation != null) 293 return translation; 294 } 295 // Fallback: Germanic plural form. 296 return (n == 1 ? msgid : msgidPlural); 297 } 298 299 // ======================== Public Methods ======================== 300 301 /// <summary> 302 /// Returns the translation of <paramref name="msgid"/> in the context 303 /// of <paramref name="msgctxt"/> a given culture. 304 /// </summary> 305 /// <param name="msgctxt">the context for the key string, an ASCII 306 /// string</param> 307 /// <param name="msgid">the key string to be translated, an ASCII 308 /// string</param> 309 /// <returns>the translation of <paramref name="msgid"/>, or 310 /// <paramref name="msgid"/> if none is found</returns> 311 public String GetParticularString (String msgctxt, String msgid, CultureInfo culture) { 312 String combined = msgctxt + "\u0004" + msgid; 313 foreach (GettextResourceSet rs in GetResourceSetsFor(culture)) { 314 String translation = rs.GetString(combined); 315 if (translation != null) 316 return translation; 317 } 318 // Fallback. 319 return msgid; 320 } 321 322 /// <summary> 323 /// Returns the translation of <paramref name="msgid"/> and 324 /// <paramref name="msgidPlural"/> in the context of 325 /// <paramref name="msgctxt"/> in a given culture, choosing the right 326 /// plural form depending on the number <paramref name="n"/>. 327 /// </summary> 328 /// <param name="msgctxt">the context for the key string, an ASCII 329 /// string</param> 330 /// <param name="msgid">the key string to be translated, an ASCII 331 /// string</param> 332 /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>, 333 /// an ASCII string</param> 334 /// <param name="n">the number, should be >= 0</param> 335 /// <returns>the translation, or <paramref name="msgid"/> or 336 /// <paramref name="msgidPlural"/> if none is found</returns> 337 public virtual String GetParticularPluralString (String msgctxt, String msgid, String msgidPlural, long n, CultureInfo culture) { 338 String combined = msgctxt + "\u0004" + msgid; 339 foreach (GettextResourceSet rs in GetResourceSetsFor(culture)) { 340 String translation = rs.GetPluralString(combined, msgidPlural, n); 341 if (translation != null) 342 return translation; 343 } 344 // Fallback: Germanic plural form. 345 return (n == 1 ? msgid : msgidPlural); 346 } 347 348 /// <summary> 349 /// Returns the translation of <paramref name="msgid"/> in the current 350 /// culture. 351 /// </summary> 352 /// <param name="msgid">the key string to be translated, an ASCII 353 /// string</param> 354 /// <returns>the translation of <paramref name="msgid"/>, or 355 /// <paramref name="msgid"/> if none is found</returns> 356 public override String GetString (String msgid) { 357 return GetString(msgid, CultureInfo.CurrentUICulture); 358 } 359 360 /// <summary> 361 /// Returns the translation of <paramref name="msgid"/> and 362 /// <paramref name="msgidPlural"/> in the current culture, choosing the 363 /// right plural form depending on the number <paramref name="n"/>. 364 /// </summary> 365 /// <param name="msgid">the key string to be translated, an ASCII 366 /// string</param> 367 /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>, 368 /// an ASCII string</param> 369 /// <param name="n">the number, should be >= 0</param> 370 /// <returns>the translation, or <paramref name="msgid"/> or 371 /// <paramref name="msgidPlural"/> if none is found</returns> 372 public virtual String GetPluralString (String msgid, String msgidPlural, long n) { 373 return GetPluralString(msgid, msgidPlural, n, CultureInfo.CurrentUICulture); 374 } 375 376 /// <summary> 377 /// Returns the translation of <paramref name="msgid"/> in the context 378 /// of <paramref name="msgctxt"/> in the current culture. 379 /// </summary> 380 /// <param name="msgctxt">the context for the key string, an ASCII 381 /// string</param> 382 /// <param name="msgid">the key string to be translated, an ASCII 383 /// string</param> 384 /// <returns>the translation of <paramref name="msgid"/>, or 385 /// <paramref name="msgid"/> if none is found</returns> 386 public String GetParticularString (String msgctxt, String msgid) { 387 return GetParticularString(msgctxt, msgid, CultureInfo.CurrentUICulture); 388 } 389 390 /// <summary> 391 /// Returns the translation of <paramref name="msgid"/> and 392 /// <paramref name="msgidPlural"/> in the context of 393 /// <paramref name="msgctxt"/> in the current culture, choosing the 394 /// right plural form depending on the number <paramref name="n"/>. 395 /// </summary> 396 /// <param name="msgctxt">the context for the key string, an ASCII 397 /// string</param> 398 /// <param name="msgid">the key string to be translated, an ASCII 399 /// string</param> 400 /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>, 401 /// an ASCII string</param> 402 /// <param name="n">the number, should be >= 0</param> 403 /// <returns>the translation, or <paramref name="msgid"/> or 404 /// <paramref name="msgidPlural"/> if none is found</returns> 405 public virtual String GetParticularPluralString (String msgctxt, String msgid, String msgidPlural, long n) { 406 return GetParticularPluralString(msgctxt, msgid, msgidPlural, n, CultureInfo.CurrentUICulture); 407 } 408 409 } 410 411 /// <summary> 412 /// <para> 413 /// Each instance of this class encapsulates a single PO file. 414 /// </para> 415 /// <para> 416 /// This API of this class is not meant to be used directly; use 417 /// <c>GettextResourceManager</c> instead. 418 /// </para> 419 /// </summary> 420 // We need this subclass of ResourceSet, because the plural formula must come 421 // from the same ResourceSet as the object containing the plural forms. 422 public class GettextResourceSet : ResourceSet { 423 424 /// <summary> 425 /// Creates a new message catalog. When using this constructor, you 426 /// must override the <c>ReadResources</c> method, in order to initialize 427 /// the <c>Table</c> property. The message catalog will support plural 428 /// forms only if the <c>ReadResources</c> method installs values of type 429 /// <c>String[]</c> and if the <c>PluralEval</c> method is overridden. 430 /// </summary> 431 protected GettextResourceSet () 432 : base (DummyResourceReader) { 433 } 434 435 /// <summary> 436 /// Creates a new message catalog, by reading the string/value pairs from 437 /// the given <paramref name="reader"/>. The message catalog will support 438 /// plural forms only if the reader can produce values of type 439 /// <c>String[]</c> and if the <c>PluralEval</c> method is overridden. 440 /// </summary> 441 public GettextResourceSet (IResourceReader reader) 442 : base (reader) { 443 } 444 445 /// <summary> 446 /// Creates a new message catalog, by reading the string/value pairs from 447 /// the given <paramref name="stream"/>, which should have the format of 448 /// a <c>.resources</c> file. The message catalog will not support plural 449 /// forms. 450 /// </summary> 451 public GettextResourceSet (Stream stream) 452 : base (stream) { 453 } 454 455 /// <summary> 456 /// Creates a new message catalog, by reading the string/value pairs from 457 /// the file with the given <paramref name="fileName"/>. The file should 458 /// be in the format of a <c>.resources</c> file. The message catalog will 459 /// not support plural forms. 460 /// </summary> 461 public GettextResourceSet (String fileName) 462 : base (fileName) { 463 } 464 465 /// <summary> 466 /// Returns the translation of <paramref name="msgid"/>. 467 /// </summary> 468 /// <param name="msgid">the key string to be translated, an ASCII 469 /// string</param> 470 /// <returns>the translation of <paramref name="msgid"/>, or <c>null</c> if 471 /// none is found</returns> 472 // The default implementation essentially does (String)Table[msgid]. 473 // Here we also catch the plural form case. 474 public override String GetString (String msgid) { 475 Object value = GetObject(msgid); 476 if (value == null || value is String) 477 return (String)value; 478 else if (value is String[]) 479 // A plural form, but no number is given. 480 // Like the C implementation, return the first plural form. 481 return ((String[]) value)[0]; 482 else 483 throw new InvalidOperationException("resource for \""+msgid+"\" in "+GetType().FullName+" is not a string"); 484 } 485 486 /// <summary> 487 /// Returns the translation of <paramref name="msgid"/>, with possibly 488 /// case-insensitive lookup. 489 /// </summary> 490 /// <param name="msgid">the key string to be translated, an ASCII 491 /// string</param> 492 /// <returns>the translation of <paramref name="msgid"/>, or <c>null</c> if 493 /// none is found</returns> 494 // The default implementation essentially does (String)Table[msgid]. 495 // Here we also catch the plural form case. 496 public override String GetString (String msgid, bool ignoreCase) { 497 Object value = GetObject(msgid, ignoreCase); 498 if (value == null || value is String) 499 return (String)value; 500 else if (value is String[]) 501 // A plural form, but no number is given. 502 // Like the C implementation, return the first plural form. 503 return ((String[]) value)[0]; 504 else 505 throw new InvalidOperationException("resource for \""+msgid+"\" in "+GetType().FullName+" is not a string"); 506 } 507 508 /// <summary> 509 /// Returns the translation of <paramref name="msgid"/> and 510 /// <paramref name="msgidPlural"/>, choosing the right plural form 511 /// depending on the number <paramref name="n"/>. 512 /// </summary> 513 /// <param name="msgid">the key string to be translated, an ASCII 514 /// string</param> 515 /// <param name="msgidPlural">the English plural of <paramref name="msgid"/>, 516 /// an ASCII string</param> 517 /// <param name="n">the number, should be >= 0</param> 518 /// <returns>the translation, or <c>null</c> if none is found</returns> 519 public virtual String GetPluralString (String msgid, String msgidPlural, long n) { 520 Object value = GetObject(msgid); 521 if (value == null || value is String) 522 return (String)value; 523 else if (value is String[]) { 524 String[] choices = (String[]) value; 525 long index = PluralEval(n); 526 return choices[index >= 0 && index < choices.Length ? index : 0]; 527 } else 528 throw new InvalidOperationException("resource for \""+msgid+"\" in "+GetType().FullName+" is not a string"); 529 } 530 531 /// <summary> 532 /// Returns the index of the plural form to be chosen for a given number. 533 /// The default implementation is the Germanic plural formula: 534 /// zero for <paramref name="n"/> == 1, one for <paramref name="n"/> != 1. 535 /// </summary> 536 protected virtual long PluralEval (long n) { 537 return (n == 1 ? 0 : 1); 538 } 539 540 /// <summary> 541 /// Returns the keys of this resource set, i.e. the strings for which 542 /// <c>GetObject()</c> can return a non-null value. 543 /// </summary> 544 public virtual ICollection Keys { 545 get { 546 return Table.Keys; 547 } 548 } 549 550 /// <summary> 551 /// A trivial instance of <c>IResourceReader</c> that does nothing. 552 /// </summary> 553 // Needed by the no-arguments constructor. 554 private static IResourceReader DummyResourceReader = new DummyIResourceReader(); 555 556 } 557 558 /// <summary> 559 /// A trivial <c>IResourceReader</c> implementation. 560 /// </summary> 561 class DummyIResourceReader : IResourceReader { 562 563 // Implementation of IDisposable. 564 void System.IDisposable.Dispose () { 565 } 566 567 // Implementation of IEnumerable. 568 IEnumerator System.Collections.IEnumerable.GetEnumerator () { 569 return null; 570 } 571 572 // Implementation of IResourceReader. 573 void System.Resources.IResourceReader.Close () { 574 } 575 IDictionaryEnumerator System.Resources.IResourceReader.GetEnumerator () { 576 return null; 577 } 578 579 } 580 581} 582