Click here to Skip to main content
15,885,182 members
Articles / Programming Languages / C# 4.0

The List Trifecta, Part 2

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
7 Sep 2013LGPL310 min read 28.6K   317   12  
The BDictionary is like a Dictionary mashed up with List<T>. BList and BMultiMap also say hello.
/*
 * Created David on 7/20/2007 at 2:21 PM
 */

using System;
using System.Text;
using NUnit.Framework;
using Loyc.Threading;
using Loyc.Essentials;

namespace Loyc
{
    /// <summary>
    /// Localize is a global hook into which a string-mapping localizer can be
    /// installed. It is designed to make internationalization exceptionally easy
    /// for developers.
    /// </summary><remarks>
    /// All Loyc code should call this hook in order to localize text. Use it like
    /// this:
    /// <code>
    /// string result = Localize.From("Hello, {0}", userName);
    /// </code>. If you use this facility frequently in a given class, you may want
    /// to shorten your typing using a static variable:
    /// <code>
    /// protected static readonly FormatterDelegate L = Localize.From;
    /// </code>
    /// Then you can simply write L("Hello, {0}", userName) instead. Either way,
    /// whatever localizer is installed will look up the text in its database and
    /// return a translation. If no translation to the end user's language is
    /// available, an appropriate default translation should be returned: either the
    /// original text, or a translation to some default language, e.g. English.
	/// <p/>
    /// Alternately, assuming you have the ability to change the table of
    /// translations, you can use a Symbol in your code and call the other overload
    /// of From() to look up the text that should be shown to the end user:
    /// <code>
    /// string result = Localize.From(GSymbol.Get("MY_STRING"));
    /// string result = Localize.From(:MY_STRING); // Loyc syntax
    /// </code>
    /// This is most useful for long strings or paragraphs of text, but I expect
    /// that some projects, as a policy, will use symbols for all localizable text.
	/// <p/>
    /// Localize.Formatter() is then called to make the completed string, unless the
    /// variable argument list is empty. It is possible to perform formatting
    /// separately, for example:
    /// <code>
    /// Console.WriteLine(Localize.From("{0} is {0:X} in hexadecimal"), N);
    /// </code>
    /// Here, writeline performs the formatting instead. However, Localize's
	/// default formatter, Strings.Format, has an extra feature that the standard 
	/// formatter does not: named arguments. Here is an example:
	/// <code>
	/// ...
	/// string verb = Localize.From(IsFileLoaded ? "parse" : "load");
	/// MessageBox.Show(
	///     Localize.From("Not enough memory to {load/parse} '{filename}'."
	///       {Message}", "load/parse", verb, "filename", FileName));
	/// }
    /// </code>
    /// As you can see, named arguments are mentioned in the format string by
	/// specifying an argument name such as {filename} instead of a number like
	/// {0}. The variable argument list contains the same name followed by its
	/// value, e.g. "filename", FileName. This feature gives you, the developer,
	/// the opportunity to indicate to the translator person what a particular 
	/// argument is for.
	/// <p/>
	/// The translator must not change any of the arguments: the word "{filename}"
	/// is not to be translated.
	/// <p/>
	/// At run-time, the format string with named arguments is converted to a
	/// "normal" format string with numbered arguments. The above example would
	/// become "Could not {1} the file: {3}" and then be passed to string.Format.
	/// 
    /// <h3>Design rationale</h3>
    /// Many developers don't want to spend time writing internationalization or
    /// localization code, and are tempted to write code that is only for one
    /// language. It's no wonder, because it's a relative pain in the neck.
    /// Microsoft suggests that code carry around a "ResourceManager" object and
    /// directly request strings from it:
    /// <code>
    /// private ResourceManager rm;
    /// rm = new ResourceManager("MyStrings", this.GetType().Assembly);
    /// Console.Writeline(rm.GetString("HEXER"), N);
    /// </code>
    /// This approach has drawbacks:
    /// * It may be cumbersome to pass around a ResourceManager instance between all
    ///   classes that might contain localizable strings; a global facility is
    ///   much more convenient.
    /// * The programmer has to put all translations in the resource file;
    ///   consequently, writing the code is bothersome because the programmer has
    ///   to switch to the resource file and add the string to it. Someone reading
    ///   the code, in turn, can't tell what the string says and has to load up
    ///   the resource file to find out.
    /// * It is nontrivial to change the localization manager; for instance, what if
    ///   someone wants to store translations in an .ini or .xml file rather than
    ///   inside the assembly? What if the user wants to centralize all
    ///   translations for a set of assemblies, rather than having separate
    ///   resources in each assembly?
    /// * Keeping in mind that the guy in charge of translation is typically
    ///   different than the guys writing most of the code, it makes sense to keep
    ///   translations separate from everything else.
	/// <p/>
    /// The idea of the Localize facility is to convince programmers to support
    /// localization by making it dead-easy to do. By default it is not connected to
    /// any translator (it just passes strings through), so people who are only
    /// writing a program for a one-language market can easily make their code
    /// "multiligual-ready" without doing any extra work, since Localize.From() is
    /// no harder to type than String.Format().
	/// <p/>
	/// The translation system itself is separate, and connected to Localize by a
	/// delegate, for two reasons:
	/// <ol>
	/// <li>Multiple translation systems are possible. This class should be suitable
	///     for any .NET program, and some programs using this utility will want to
	///     plug-in a different localizer. </li>
	/// <li>I personally don't have the time or expertise to write a localizer at 
	///     this time. So until I do, the Localize class will make my code ready for 
	///     translation, although not actually localized.</li>
	/// </ol>
    /// In the open source world, most developers don't have a team of translators
    /// ready make translations for them. The idea of Loyc, for example, is that
    /// many different individuals--not one big team--of programmers will create
	/// and maintain features. By centralizing this translation facility, it should
	/// be straightforward for a single multilingual individual to translate the
	/// text of many Loyc extensions made by many different people.
	/// <p/>
    /// To facilitate this, I propose that in addition to a translator, a Loyc
    /// extension should be made to figure out all the strings/symbols for which
    /// translations are needed. To do this it would scan source code (at compile
    /// time) for calls to methods in this class and generate a list of strings and
    /// symbols needing translation. It would also have to detect certain calls that
    /// perform translation implicity, such as ISimpleMessageSink.Write(). See
	/// <see cref="LocalizableAttribute"/>.
    /// </remarks>
	public static class Localize
	{
		public static ThreadLocalVariable<LocalizerDelegate> _localizer = new ThreadLocalVariable<LocalizerDelegate>(Passthru);
		public static ThreadLocalVariable<FormatterDelegate> _formatter = new ThreadLocalVariable<FormatterDelegate>(StringExt.Format);

		/// <summary>Localizer method (thread-local)</summary>
		public static LocalizerDelegate Localizer
		{
			get { return _localizer.Value; }
			set { _localizer.Value = value; }
		}

		/// <summary>Formatting delegate (thread-local), which is set to 
		/// <see cref="StringExt.Format"/> by default.</summary>
		public static FormatterDelegate Formatter
		{
			get { return _formatter.Value; }
			set { _formatter.Value = value; }
		}

		/// <summary>
		/// This is the dummy translator, which is the default value of Localizer. 
		/// It passes strings through untranslated. A msgId symbol cannot be handled 
		/// so it is simply converted to a string.
		/// </summary>
		public static string Passthru(Symbol msgId, string msg)
		{
            return msg ?? (msgId == null ? null : msgId.Name);
		}

		#region Main Localize.From() methods

		/// <summary>
        /// This is the heart of the Localize class, which localizes and formats a
        /// string.
        /// </summary>
        /// <param name="resourceId">Resource ID used to look up a string. If
        /// it is null then message must be provided; otherwise, message is only used 
        /// if no translation is available for the specified ID.</param>
        /// <param name="message">The message to translate, which may include argument 
        /// placeholders (e.g. "{0}").</param>
        /// <param name="args">Arguments given to Formatter to fill in placeholders
        /// after the Localizer is called. If args is null or empty then Formatter
        /// is not called.</param>
        /// <returns>The translated and formatted string.</returns>
		public static string From(Symbol resourceId, [Localizable] string message, params object[] args)
		{
			string localized = Localizer(resourceId, message);
			if (args == null || args.Length == 0)
				return localized;
			else
				return Formatter(localized, args);
		}
		public static string From(Symbol resourceId, params object[] args)
			{ return From(resourceId, null, args); }
		public static string From([Localizable] string message, params object[] args)
			{ return From(null, message, args); }

		#endregion

		#region Versions of Localize.From() specialized for 0, 1 or 2 arguments

		public static string From(Symbol resourceId)
			{ return From(resourceId, null, null); }
		public static string From([Localizable] string message)
			{ return From(null, message, null); }
		public static string From(Symbol resourceId, [Localizable] string message)
			{ return From(resourceId, message, null); }

		static ScratchBuffer<object[]> _1arg = new ScratchBuffer<object[]>(() => new object[1]);
		static ScratchBuffer<object[]> _2args = new ScratchBuffer<object[]>(() => new object[2]);

		public static string From(Symbol resourceId, object arg1)
		{
			object[] buf = _1arg.Value;
			buf[0] = arg1;
			return From(resourceId, null, buf);
		}
		public static string From([Localizable] string message, object arg1)
		{
			object[] buf = _1arg.Value;
			buf[0] = arg1;
			return From(null, message, buf); 
		}
		public static string From(Symbol resourceId, [Localizable] string message, object arg1)
		{
			object[] buf = _1arg.Value;
			buf[0] = arg1;
			return From(resourceId, message, buf); 
		}

		public static string From(Symbol resourceId, object arg1, object arg2)
		{
			object[] buf = _2args.Value;
			buf[0] = arg1;
			buf[1] = arg2;
			return From(resourceId, null, buf);
		}
		public static string From([Localizable] string message, object arg1, object arg2)
		{
			object[] buf = _2args.Value;
			buf[0] = arg1;
			buf[1] = arg2;
			return From(null, message, buf); 
		}
		public static string From(Symbol resourceId, [Localizable] string message, object arg1, object arg2)
		{
			object[] buf = _2args.Value;
			buf[0] = arg1;
			buf[1] = arg2;
			return From(resourceId, message, buf);
		}

		#endregion
	}
	/// <summary>Contains Localize() extension methods for strings.</summary>
	/// <remarks>This would be part of the <see cref="Localize"/> class except that 
	/// the Localize class is not allowed to have a member that is also named 
	/// Localize.</remarks>
	public static class LocalizeExt
	{
		public static string Localize([Localizable] this string message, params object[] args)
			{ return Loyc.Localize.From(null, message, args); }
		public static string Localize([Localizable] this string message)
			{ return Loyc.Localize.From(message); }
		public static string Localize([Localizable] this string message, object arg1)
			{ return Loyc.Localize.From(message, arg1); }
		public static string Localize([Localizable] this string message, object arg1, object arg2)
			{ return Loyc.Localize.From(message, arg1, arg2); }
	}

	/// <summary>
	/// I plan to use this attribute someday to gather all the localizable strings 
	/// in an application. This attribute should be applied to a string function 
	/// parameter if the method calls Localize() or Localize.From using that 
	/// parameter as the format string.
	/// </summary>
	[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)]
	public class LocalizableAttribute : System.Attribute { }

	public delegate string FormatterDelegate(string format, params object[] args);
	public delegate string LocalizerDelegate(Symbol msgId, string msg);

	[TestFixture]
	public class LocalizeTests
	{
		[Test]
		public void DefaultFormatter()
		{
			//Assert.AreEqual(Localize.EliminateNamedArgs(
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Software Developer None
Canada Canada
Since I started programming when I was 11, I wrote the SNES emulator "SNEqr", the FastNav mapping component, the Enhanced C# programming language (in progress), the parser generator LLLPG, and LES, a syntax to help you start building programming languages, DSLs or build systems.

My overall focus is on the Language of your choice (Loyc) initiative, which is about investigating ways to improve interoperability between programming languages and putting more power in the hands of developers. I'm also seeking employment.

Comments and Discussions