view Lang/Translation.cs @ 42:7e74c7954be9

* Make double parsing from XML use double.parse instead of int.parse no-open-ticket (fixes warfoundry:#185)
author IBBoard <dev@ibboard.co.uk>
date Tue, 29 Sep 2009 19:19:44 +0000
parents cc7fae81afec
children 70d6c2a5d99e
line wrap: on
line source

// This file (Translation.cs) is a part of the IBBoard library and is copyright 2009 IBBoard.
//
// The file and the library/program it is in are licensed under the GNU LGPL license, either version 3 of the License or (at your option) any later version. Please see COPYING.LGPL for more information and the full license.

using System;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Collections.Generic;
using System.Reflection;
using System.ComponentModel;
using IBBoard.IO;
using IBBoard.Logging;
using IBBoard.Xml;

namespace IBBoard.Lang
{
	/// <summary>
	/// A basic string translator that loads a default language and a specified language and returns translated strings that correspond to translation IDs. 
	/// If the string doesn't exist in the specified language then the translator falls back to the default language. If the translation doesn't exist in the default language
	/// then either a supplied value or a "no validation available" message is returned.
	/// </summary>
	public class Translation
	{		
		private static readonly string DEFAULT_LANGUAGE = "en";
		private static readonly string DIVIDER_STRING = "-";
		private static string lang = "";
		private static DirectoryInfo translationDir;
		private static Dictionary<string, string> translationsLocal;
		private static Dictionary<string, string> translationsDefault;
		private static XmlReaderSettings settings;

		/// <summary>
		/// Initialises the translations for the language specified and the default translations so that the Translation class can be used.
		/// Throws a TranslationLoadException if a problem occurred while loading translations. If this occurs then the translation methods can
		/// still be called but no translation will be performed.
		/// </summary>
		/// <param name="appPath">
		/// The full path that the application is running from. Must contain the "translations" folder.
		/// </param>
		/// <param name="language">
		/// The language to use as the load language
		/// </param>
		public static void InitialiseTranslations(string appPath, string language)
		{
			InitialiseDefaults(appPath);
			FileInfo file = GetTranslationFile(DEFAULT_LANGUAGE);
			XmlDocument doc = LoadTranslationDocument(file);
			LoadTranslationsFromDocument(doc, translationsDefault);
			LoadTranslationForLanguage(language);
		}
		
		private static void InitialiseDefaults(string appPath)
		{
			string translationPath = appPath.TrimEnd(Constants.DirectoryChar) + Constants.DirectoryString + "translations";

			if (Directory.Exists(translationPath))
			{
				translationsDefault = new Dictionary<string,string>();
				translationsLocal = new Dictionary<string,string>();
				translationDir = new DirectoryInfo(translationPath);
			}
			else
			{
				throw new TranslationLoadException("Translation path not found ("+translationPath+")");
			}
		}
		
		private static XmlDocument LoadTranslationDocument(FileInfo file)
		{
			XmlDocument doc = new XmlDocument();			
			XmlReader valReader = XmlReader.Create(file.FullName, GetReaderSettings());
			
			try
			{
				doc.Load(valReader);
			}
			catch (DirectoryNotFoundException ex)
			{
				throw new TranslationLoadException("Problem validating schema for translation: " + ex.Message, ex);
			}
			catch (XmlSchemaException ex)
			{
				throw new TranslationLoadException("Problem validating schema for translation: " + ex.Message, ex);
			}
			catch (XmlException ex)
			{
				throw new TranslationLoadException("Problem reading data for translation: " + ex.Message, ex);
			}
			finally
			{
				valReader.Close();
			}
			
			return doc;
		}
		
		/// <summary>
		/// Lazy-getter for XML reader settings. May throw a <see cref="TranslationLoadException"/> if there is a problem with the translation schema.
		/// </summary>
		/// <returns>
		/// A <see cref="XmlReaderSettings"/> with the default values for validating the translation document against the translation schema
		/// </returns>
		private static XmlReaderSettings GetReaderSettings()
		{
			if (settings == null)
			{
				try
				{
					settings = new XmlReaderSettings();
					settings.XmlResolver = new IBBXmlResolver(translationDir.Parent.FullName);
					settings.ValidationType = ValidationType.Schema;
					settings.ValidationFlags = XmlSchemaValidationFlags.ReportValidationWarnings;
					settings.ValidationEventHandler+= new ValidationEventHandler(ValidationEventMethod);
					XmlSchemaSet cache = new XmlSchemaSet();
					cache.Add("http://ibboard.co.uk/translation", translationDir.Parent.FullName + "/dtds/translation.xsd");
					settings.Schemas.Add(cache);
				}
				catch (DirectoryNotFoundException ex)
				{
					throw new TranslationLoadException("Problem validating schema for translation: " + ex.Message, ex);
				}
				catch (XmlSchemaException ex)
				{
					throw new TranslationLoadException("Problem validating schema for translation: " + ex.Message, ex);
				}
				catch (XmlException ex)
				{
					throw new TranslationLoadException("Problem reading data for schema: " + ex.Message, ex);
				}
			}
			
			return settings;
		}
		
		private static FileInfo GetTranslationFile(string language)
		{
			FileInfo file = new FileInfo(translationDir.FullName + Constants.DirectoryString + language + ".translation");

			if (!file.Exists)
			{
				throw new TranslationLoadException(language + ".translation could not be found in "+translationDir.FullName);
			}
			
			return file;
		}
		
		private static void LoadTranslationsFromDocument(XmlDocument doc, Dictionary<string, string> translationTable)
		{
			try
			{
				XmlNodeList translations = doc.GetElementsByTagName("translation");				
				Dictionary<string, string> tempTranslationTable = new Dictionary<string,string>();

				foreach (XmlNode node in translations)
				{
					tempTranslationTable.Add(node.Attributes["id"].Value, node.InnerText);
				}
				
				translationTable.Clear();
				
				foreach (string key in tempTranslationTable.Keys)
				{
					string translation;
					tempTranslationTable.TryGetValue(key, out translation);
					translationTable.Add(key, translation);
				}
			}
			catch(Exception ex)
			{
				throw new TranslationLoadException("Error while parsing " + GetLanguageOfDocument(doc)+" translation: "+ex.Message, ex);
			}	
		}
		
		private static string GetLanguageOfDocument(XmlDocument doc)
		{
			return doc != null ? doc.DocumentElement.GetAttribute("lang") : "";
		}
		
		private static void ValidationEventMethod(object sender, ValidationEventArgs e)
		{
			throw new TranslationLoadException("Problem validating schema for translation: " + e.Exception.Message, e.Exception);
		}

		/// <summary>
		/// Loads translations for a given language and sets them as the local language.
		/// hrows a TranslationLoadException if a problem occurred while loading translations. If this occurs then the translation methods can
		/// still be called but all translations will fall back to the default translation.
		/// </summary>
		/// <param name="translationLang">
		/// The new local language to load
		/// </param>
		public static void LoadTranslation(string translationLanguage)
		{			
			if (translationLanguage == "" || translationLanguage == null)
			{
				throw new ArgumentException("Translation language cannot be null or empty");
			}

			LoadTranslationForLanguage(translationLanguage);
		}
		
		private static void LoadTranslationForLanguage(string translationLanguage)
		{
			CheckInitialisation();
			
			if (translationLanguage != DEFAULT_LANGUAGE && translationLanguage != "" && translationLanguage != null)
			{
				FileInfo file = GetTranslationFile(translationLanguage);
				XmlDocument doc = LoadTranslationDocument(file);
				LoadTranslationsFromDocument(doc, translationsLocal);	
			}
			else
			{
				translationsLocal.Clear();				
			}
			
			lang = translationLanguage;
		}

		/// <summary>
		/// Gets a translation for a given ID, falling back to a "missing translation" message if none can be found. Also optionally replaces any placeholders with the supplied values.
		/// </summary>
		/// <param name="translationID">
		/// The ID to look up the translation for
		/// </param>
		/// <param name="replacements">
		/// A collection of <see cref="System.Object"/>s to replace placeholders with
		/// </param>
		/// <returns>
		/// The translation with the placeholders replaced or a "missing translation" message
		/// </returns>
		public static string GetTranslation(string translationID, params object[] replacements)
		{
			return GetTranslation(translationID, false, replacements);
		}

		/// <summary>
		/// Gets a translation for a given ID, falling back to null or a warning message if a translation cannot be found. Also optionally replaces any placeholders with the supplied values.
		/// </summary>
		/// <param name="translationID">
		/// The ID to look up the translation for
		/// </param>
		/// <param name="returnNullOnFail">
		/// TRUE if null should be returned when no translation can be found, or FALSE if a "missing translation" message should be returned
		/// </param>
		/// <param name="replacements">
		/// A collection of <see cref="System.Object"/>s to replace placeholders with
		/// </param>
		/// <returns>
		/// The translation with the placeholders replaced, or a "missing translation" message or null depending on <param name="returnNullOnFail">
		/// </returns>
		public static string GetTranslation(string translationID, bool returnNullOnFail, params object[] replacements)
		{
			return GetTranslation(translationID, returnNullOnFail ? null : "", replacements);
		}

		/// <summary>
		/// Gets a translation for a given ID, falling back to a supplied default if a translation cannot be found. Also optionally replaces any placeholders with the supplied values.
		/// </summary>
		/// <param name="translationID">
		/// The ID to look up the translation for
		/// </param>
		/// <param name="defaultTranslation">
		/// The string to return if no translation can be found. Can be null or any string.
		/// </param>
		/// <param name="replacements">
		/// A collection of <see cref="System.Object"/>s to replace placeholders with
		/// </param>
		/// <returns>
		/// The translation, if one exists, or the supplied default with the placeholders replaced
		/// </returns>
		public static string GetTranslation(string translationID, string defaultTranslation, params object[] replacements)
		{
			CheckInitialisation();
			string trans = GetTranslationFromTables(translationID);
			
			if (trans == null)
			{
				trans = GetDefaultTranslation(translationID, defaultTranslation);
			}

			trans = AddVariablesToTranslation(trans, replacements);

			return trans;
		}
		
		private static string GetTranslationFromTables(string translationID)
		{
			string translation = null;
			
			if (translationsLocal!=null)
			{
				translationsLocal.TryGetValue(translationID, out translation);
			}
			
			if (translation == null)
			{
				translationsDefault.TryGetValue(translationID, out translation);
			}
			
			return translation;
		}
		
		private static string GetDefaultTranslation(string translationID, string defaultTranslation)
		{
			return (defaultTranslation != "" && defaultTranslation != null) ? defaultTranslation : GetMissingTranslationMessage(translationID);
		}

		private static string GetMissingTranslationMessage(string translationID)
		{
			return  "++ Missing Translation "+translationID+" ++";
		}
		
		private static string AddVariablesToTranslation(string translation, object[] replacements)
		{
			if (translation != null && replacements != null && replacements.Length > 0)
			{
				translation = String.Format(translation, replacements);
			}
			
			return translation;
		}
		
		private static void CheckInitialisation()
		{
			if (translationDir==null)
			{
				throw new InvalidOperationException("Translation class has not been initialised");
			}
		}

		/// <summary>
		/// Translate an <see cref="ITranslatable"/> item, with optional string replacement. If the translation
		/// does not exist then a warning message will be used as the translated text.
		/// </summary>
		/// <param name="item">
		/// A <see cref="ITranslatable"/> to set the text for
		/// </param>
		/// <param name="replacements">
		/// A collection of <see cref="System.Object"/>s that will be used to fill place-holders
		/// </param>
		public static void Translate(ITranslatable item, params object[] replacements)
		{
			Translate(item, (string)null, replacements);
		}

		/// <summary>
		/// Translate an <see cref="ITranslatable"/> item, with optional string replacement. The <code>defaultText</code>
		/// can be used to specify an alternate translation. Passing <code>null</code> will result in a warning message
		/// about a missing translation ID.
		/// </summary>
		/// <param name="item">
		/// A <see cref="ITranslatable"/> to set the text for
		/// </param>
		/// <param name="defaultText">
		/// The default string to display if no translation could be found.
		/// </param>
		/// <param name="replacements">
		/// A collection of <see cref="System.Object"/>s that will be used to fill place-holders
		/// </param>
		public static void Translate(ITranslatable item, string defaultText, params object[] replacements)
		{
			if (item.Text == "" || item.Text == DIVIDER_STRING)
			{
				//it doesn't need translating - either there is no text from the developer or it's a hyphen for a divider
				return;
			}

			item.Text = GetTranslation(item.Name, defaultText, replacements);
		}
		
		/// <summary>
		/// Get the current local translation language. This is an arbitrary string used in the translation file's name and will not necessarily match the ISO language code.
		/// </summary>
		/// <returns>
		/// The string used in the file name of the current local translation
		/// </returns>
		public static string GetTranslationLanguage()
		{
			return lang;
		}
	}
}