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