changeset 12:bff0d802cb7d

Implemented some internationalization support. Implemented i18n.I18nTranslation class to load strings and descriptions from files (with unittest). MTUnknownTypeException removed: its pointless since it's always ignored without even any message. A few fixes to mde.mergetag.read.Reader regarding partial reading. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Fri, 22 Feb 2008 11:52:20 +0000
parents b940f267419e
children 914fed025adb
files conf/L10n/i18nUnitTest.mtt conf/L10n/mde.mtt conf/options.mtt doc/jobs doc/policies.txt mde/exception.d mde/i18n.d mde/init.d mde/input/config.d mde/input/input.d mde/mde.d mde/mergetag/dataset.d mde/mergetag/defaultdata.d mde/mergetag/exception.d mde/mergetag/read.d mde/options.d test/mdeTest.d
diffstat 17 files changed, 369 insertions(+), 90 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conf/L10n/i18nUnitTest.mtt	Fri Feb 22 11:52:20 2008 +0000
@@ -0,0 +1,7 @@
+{MT01}
+{test-1}
+<entry|Str1=["Test 1"]>
+<char[][]|depends=["test-2"]>
+{test-2}
+<entry|Str1=["Test 2"]>
+<entry|Str2=["Test 3","Description",bogus,"entries",56]>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conf/L10n/mde.mtt	Fri Feb 22 11:52:20 2008 +0000
@@ -0,0 +1,3 @@
+{MT01}
+{en-GB}
+<entry|greeting=["Hello, and welcome to mde!"]>
--- a/conf/options.mtt	Thu Feb 21 09:05:33 2008 +0000
+++ b/conf/options.mtt	Fri Feb 22 11:52:20 2008 +0000
@@ -1,5 +1,5 @@
 {MT01}
 {Default}
-<bool|useThreads=false>
 <char[]|greeting="Hi − options now work!">
+<char[]|L10n="en-GB">
 
--- a/doc/jobs	Thu Feb 21 09:05:33 2008 +0000
+++ b/doc/jobs	Fri Feb 22 11:52:20 2008 +0000
@@ -1,8 +1,8 @@
 In progress:
 
 To do:
+*   Submit new version of parseTo
 *	Why doesn't input.config filtering via headers "Configs" work?
-*	add options support; in particular for whether or not to use threads (and adjust Init to use this).
 *	OutOfMemoryException is not currently checked for − it should be at least in critical places (use high-level catching of all errors?).
 *	Sensitivity adjustments. From es_a_out:
         /+ FIXME: revise.
@@ -33,6 +33,6 @@
         +/
 
 Done (for git log message):
-*   Options class created (barebones). Loading/saving from Init.
-*   Init no longer runs cleanup functions after initialisation failure.
-*   Improved mergetag exception messages & error reporting.
+*   Implemented i18n.I18nTranslation class to load strings and descriptions from files (with unittest).
+*   MTUnknownTypeException removed: its pointless since it's always ignored without even any message.
+*   A few fixes to mde.mergetag.read.Reader regarding partial reading.
--- a/doc/policies.txt	Thu Feb 21 09:05:33 2008 +0000
+++ b/doc/policies.txt	Fri Feb 22 11:52:20 2008 +0000
@@ -12,7 +12,7 @@
 
 Module/file names: unless you have a good reason, keep names all lower-case. And if you're programming on windows, make sure you always use the correct capitalisation (yeah, of course...).
 
-Spelling: Use british or american (or other) spellings as you like, but BE CONSISTANT at least for code symbols within packages (i.e. if you write your own package you choose the spelling, and for comments it doesn't matter, unless it's actually refering to a symbol). For text output to the user there need be no convention until internationalisation support is built-in. As far as internationalisation/localisation is concerned, does it make sense to translate log messages or not? (They are going to be seen by end-users, but will largely be used by developers.)
+Spelling (within code): Use whichever spellings you like (so long as it's good English), but use these spellings consistantly, at least for code symbols within packages (i.e. if you write your own package you can choose the spellings, and for comments it doesn't really matter, unless it's actually refering to a symbol).
 
 
 
@@ -26,6 +26,16 @@
 
 
 
+Unittests (last block in the module where multiple unittest blocks are used) should end with:
+    logger.info ("Unittest complete.");
+No more logging should be needed, since if it fails whoever runs the unittest will know about it, and logging messages cannot be used to tell how complete the unittesting is. These messages just confirm that the unittests ran really.
+
+Unittests may be defined in their own modules or other modules. Unittests should be wrapped in
+    debug(mdeUnitTest)
+statements (these may also be used to wrap imports only needed by the unittest). Any modules containing unittests should be imported by test.mdeTest.
+
+
+
 Logging should be handled by tango's Logger class. A logger with the name mde.package.module or mde.package.module.X where X is a symbol within the module should be used for each module. Thrown errors should, where documented, be documented with a log message; an exception message may be used to produce the final log message but must be output via a log message.
 
 In general the levels should be used as follows:
@@ -50,11 +60,10 @@
 Thrown errors should use an exception class specific to at least the package involved to enable specific catching of errors. Exception classes should be defined within a module exception.d in the package directory. Exception classes should generally follow the conventions within mde/exception.d to aid in providing reasonable error messages.
 
 
+Log/exception messages should be in English. They should include a brief description, but do not need a long description since the message can be looked up in the code. Currently there is no plan for translating log messages, however if someone wants to implement I18nTranslation to provide the messages they can.
 
-Unittests (last block in the module where multiple unittest blocks are used) should end with:
-    logger.info ("Unittest complete.");
-No more logging should be needed, since if it fails whoever runs the unittest will know about it, and logging messages cannot be used to tell how complete the unittesting is. These messages just confirm that the unittests ran really.
+
 
-Unittests may be defined in their own modules or other modules. Unittests should be wrapped in
-    debug(mdeUnitTest)
-statements (these may also be used to wrap imports only needed by the unittest). Any modules containing unittests should be imported by test.mdeTest.
+User output (internationalization support): i18n.I18nTranslation is designed to fetch a full string and optionally an associated descripion given an identifier. The identifier may also be a code symbol, and should be brief, in English, and give at least some idea of its meaning, since if no translation is available the identifier will be output. For example:  "example message" or "exampleMessage", not "An example of a message". For any new entry created, at least one full entry should be added to the i18n database for some form of English so that:
+    (a) there is something available to translate to other locales
+    (b) English developers can understand what it means
--- a/mde/exception.d	Thu Feb 21 09:05:33 2008 +0000
+++ b/mde/exception.d	Fri Feb 22 11:52:20 2008 +0000
@@ -25,6 +25,7 @@
     }
 }
 
+/// Thrown when init fails.
 class initException : mdeException {
     // NOTE: if symbol is final, it can't be modified in the static this(), but as const it can
     static const char[] symbol;
@@ -38,6 +39,7 @@
     }
 }
 
+/// Thrown when loading options fails.
 class optionsLoadException : mdeException {
     // NOTE: if symbol is final, it can't be modified in the static this(), but as const it can
     static const char[] symbol;
@@ -51,6 +53,20 @@
     }
 }
 
+/// Thrown when loading strings for the requested name and current locale fails.
+class L10nLoadException : mdeException {
+    // NOTE: if symbol is final, it can't be modified in the static this(), but as const it can
+    static const char[] symbol;
+    static this () {	symbol = super.symbol ~ ".i18n.I18nTranslation";	}
+    char[] getSymbol () {
+        return symbol;
+    }
+    
+    this (char[] msg) {
+        super(msg);
+    }
+}
+
 debug (mdeUnitTest) {
     import tango.util.log.Log : Log, Logger;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/i18n.d	Fri Feb 22 11:52:20 2008 +0000
@@ -0,0 +1,231 @@
+/** i18n − internationalization module
+*
+* The idea behind this module is a class which, when asked to load symbols for a particular module/
+* package/part of the program, will load internationalized names and optional descriptions for each
+* symbol needing translation. No support for non-left-to-right scripts is currently planned, and
+* this module is currently limited to translations, although support for different date formats,
+* etc. could potentially be added later.
+*
+* Code symbols are used as identifiers for each name and its optional description. The code symbol
+* will be used as a fallback in the case no entry exists, however it is not intended to provide the
+* string for the default language (a "translation" should be used for the default language).
+*
+* Each locale may specify dependant locales/sections which will be loaded and merged in to the
+* database, to cover for symbols with a missing entry. Sections are loaded in the order specified,
+* with each section's sub-dependancies loaded before continuing with the next top-level dependancy.
+* A list of loaded sections is used to prevent any locale/section from being loaded twice, and thus
+* allow circular dependancies.
+*
+* In order that translated strings get updated correctly to reflect changes, each entry carries a
+* version number. If, for any entry, a translation exists with a higher version number, that entry
+* is out of date. A tool should be made for checking for out of date entries to take advantage of
+* this feature. Of course, out of date entries are still valid for use.
+*/
+module mde.i18n;
+
+import mde.options;
+import mde.exception;
+
+import mde.mergetag.dataset;
+import mde.mergetag.read;
+import mde.mergetag.exception;
+
+import tango.util.log.Log : Log, Logger;
+import tango.scrapple.text.convert.parseTo;
+
+/** The translation class
+*
+* See module description for details.
+*
+* Encoding used is UTF-8.
+*/
+class I18nTranslation : DataSection
+{
+    final char[] name;      /// The module/package/... which the instance is for
+    final char[] L10n;      /// The localization loaded (e.g. en-GB)
+    
+    /** Get the translation for the given identifier, and optionally the description.
+    * If no entry exists, the identifier will be returned. */
+    char[] getEntry (char[] id, out char[] description) {
+        Entry* p = id in entries;
+        if (p) {    // FIXME: check: a SEGFAULT?
+            description = p.desc;
+            return p.str;
+        } else
+            return id;
+    }
+        /** ditto */
+    char[] getEntry (char[] id) {
+        Entry* p = id in entries;
+        if (p !is null) {    // FIXME: check: a SEGFAULT?
+            return p.str;
+        } else
+            return id;
+    }
+    
+    /** Load the translation for the requested module/package/...
+    *
+    * Options (mde.options) must have been loaded before this is run.
+    *
+    * Params:
+    *   name The module/package/... to load strings for.
+    *
+    * Throws:
+    * If no localization exists for this name and the current locale (or any locale to be chain-
+    * loaded), or an error occurs while loading the database, a L10nLoadException will be thrown.
+    */
+    static I18nTranslation load (char[] name)
+    {
+        bool[ID] loadedSecs;        // set of all locales/sections loaded; used to prevent circular loading
+        ID[] secsToLoad             // locales/sections to load (dependancies may be added)
+        = [cast(ID) options.L10n];  // start by loading the current locale
+        
+        I18nTranslation transl = new I18nTranslation (name, options.L10n);
+        /* Find the file for name and load it.
+        */
+        FilePath filePath = new FilePath ("conf/L10n/"~name~".mtt");
+        // If it's not a file or an empty file stop.
+        if (!filePath.exists || filePath.fileSize == 0u) {
+            throw new L10nLoadException ("No database file conf/L10n/"~name~".mtt exists!");
+        }
+        
+        Reader reader;
+        try {
+            reader = new Reader(filePath);
+            /* Note: we don't want to load every translation section depended on to its own class
+            * instance, since we want to merge them. So make every mergetag section use the same
+            * instance. */
+            reader.dataSecCreator = delegate DataSection(ID) {
+                return transl;
+            };
+        
+            while (secsToLoad.length) {                 // while we have sections left to load
+                ID sec = secsToLoad[0];                 // take current section
+                secsToLoad = secsToLoad[1..$];          // and remove from list
+                                
+                if (sec in loadedSecs) continue;        // skip if it's already been loaded
+                loadedSecs[sec] = true;
+                
+                reader.read ([sec]);                    // Do the actual loading
+                
+                // Add dependancies to front of list:
+                secsToLoad = transl.depends ~ secsToLoad;
+            }
+            // When loop finishes, we're done loading.
+        } catch (MTException e) {
+            logger.error ("Mergetag exception occurred:");
+            logger.error (e.msg);
+            throw new L10nLoadException ("Loading aborted: mergetag exception");
+        }
+        
+        delete transl.depends;      // Free memory
+        transl.depends = [];
+        
+        return transl;
+    }
+    
+    static this() {
+        logger = Log.getLogger ("mde.input.i18n.I18nTranslation");
+    }
+    
+    /* Mergetag functionality.
+    *
+    * Merge tags in to entries, prefering existing values.
+    * Replace depends.
+    *
+    * User-defined type "entry":
+    *   first two element is string and must exist
+    *   second element is description and is optional
+    *   third element is version and is optional
+    *   no limit on number of elements to allow future extensions
+    */
+    void addTag (char[] tp, ID id, char[] dt) {
+        if (tp == "entry") {
+            char[][] fields = split (stripBrackets (dt));
+            
+            if (fields.length < 1) {
+                // This tag is invalid, but since we don't want execution to halt just log a warning:
+                logger.warn ("For name "~name~", L10n "~L10n~": tag with ID "~cast(char[])id~" has no data");
+                return;
+            }
+            // If the tag already exists, don't replace it
+            if (cast(char[]) id in entries) return;
+            
+            Entry entry;
+            entry.str = parseTo!(char[]) (fields[0]);
+            
+            if (fields.length >= 2)
+                entry.desc = parseTo!(char[]) (fields[1]);
+            
+            entries[cast(char[]) id] = entry;
+        } else if (tp == "char[][]") {
+            if (id == cast(ID)"depends") depends = cast(ID[]) parseTo!(char[][]) (dt);
+        }
+    }
+    
+    // This class is read-only and has no need of being saved.
+    void writeAll (ItemDelg) {}
+    
+private:
+    /* Sets name and L10n.
+    *
+    * Also ensures only load() can create instances. */
+    this (char[] n, char[] l) {
+        name = n;
+        L10n = l;
+    }
+    
+    //BEGIN Data
+    static Logger logger;
+    
+    /* This struct is used to store each translation entry.
+    *
+    * Note that although each entry also has a version field, this is not loaded for general use.
+    */
+    struct Entry {
+        char[] str;         // The translated string
+        char[] desc;        // An optional description
+    }
+    
+    Entry[char[]] entries;  // all entries
+    
+    ID[] depends;           // dependancy sections (only used while loading)
+    //END Data
+    
+    debug (mdeUnitTest) unittest {
+        /* Relies on file: conf/L10n/i18nUnitTest.mtt
+        * Contents:
+        *********
+        {MT01}
+        {test-1}
+        <entry|Str1=["Test 1"]>
+        <char[][]|depends=["test-2"]>
+        {test-2}
+        <entry|Str1=["Test 2"]>
+        <entry|Str2=["Test 3","Description",bogus,"entries",56]>
+        *********/
+        
+        // Hack a specific locale...
+        // Also to allow unittest to run without init.
+        char[] currentL10n = options.L10n;
+        options.L10n = "test-1";
+        
+        I18nTranslation transl = load ("i18nUnitTest");
+        
+        // Simple get-string, check dependancy's entry doesn't override
+        assert (transl.getEntry ("Str1") == "Test 1");
+        
+        // Entry included from dependancy with description
+        char[] desc;
+        assert (transl.getEntry ("Str2", desc) == "Test 3");
+        assert (desc == "Description");
+        
+        // No entry: fallback to identifier string
+        assert (transl.getEntry ("Str3") == "Str3");
+        
+        // No checks for version info since it's not functionality of this module.
+        // Only check extra entries are allowed but ignored.
+        
+        logger.info ("Unittest complete.");
+    }
+}
--- a/mde/init.d	Thu Feb 21 09:05:33 2008 +0000
+++ b/mde/init.d	Fri Feb 22 11:52:20 2008 +0000
@@ -31,7 +31,7 @@
  */
 static this()
 {
-    debug(mdeUnitTest) {}   // For unittests, the logger is set up by test.mdeTest
+    version (mdeTest) {}   // test.mdeTest sets up its own root logger
     else {
         // For now, just log to the console:
         Logger root = Log.getRootLogger();
@@ -72,7 +72,7 @@
     {
         /* Initialisation functions.
         *
-        * These should each handle a separate area of initialisation such that these functions could
+        * These should each handle a separate area of initialisation so that these functions can
         * be run simultaneously in separate threads. */
         
         void setFailure () {		// For synchronization, although shouldn't be necessary
@@ -126,25 +126,40 @@
         }
         ];
         
-        // Start all threads
-        ThreadGroup tg = new ThreadGroup;	// can't fail since it does nothing (tango 0.99.4) (unless out of memory − anyway should be safe to throw at this point)
+        /* Call init functions.
+        *
+        * Current method is to try using threads, and on failure assume no threads were actually
+        * created and run functions in a non-threaded mannor.
+        *
+        * Note: can't use options class to control running of threads, since options haven't been
+        * loaded yet.
+        */
+        ThreadGroup tg = new ThreadGroup;	// can't fail since it does nothing (tango 0.99.4)
+        
         try {	// creating/starting threads can fail
-            foreach (func; initFuncs) tg.create(func);
+            foreach (func; initFuncs) tg.create(func);  // Start all threads
         } catch (ThreadException e) {		// to do with threading; try without threads
             logger.warn ("Caught exception while trying to create threads:");
             logger.warn (e.msg);
             logger.warn ("Will continue in a non-threaded manner.");
             
-            foreach (func; initFuncs) func();
+            foreach (func; initFuncs) func();           // Run functions without threads
         }
         
-        // Do some initialisation in the main thread
+        /* Do some initialisation here.
+        *
+        * We might as well make use of this thread rather than create another!
+        */
         global.input = new Input();
         global.input.loadConfig ();		// (may also create instance)
         addEventsSchedule ();
         
-        // Wait for all threads to complete.
-        // If something went wrong, we still need to do this before cleaning up.
+        /* Wait for all threads to complete.
+        *
+        * If something went wrong, we still need to do this before cleaning up.
+        *
+        * For non-threaded usage, tg contains no threads so loop won't run.
+        */
         foreach (t; tg) {
             try {
                 t.join (true);
--- a/mde/input/config.d	Thu Feb 21 09:05:33 2008 +0000
+++ b/mde/input/config.d	Fri Feb 22 11:52:20 2008 +0000
@@ -11,11 +11,6 @@
 import tango.util.log.Log : Log, Logger;
 import tango.util.collection.TreeBag : TreeBag;
 
-private Logger logger;
-static this() {
-    logger = Log.getLogger ("mde.input.config");
-}
-
 /** Class to hold the configuration for the input system. Thus loading and switching between
  *  multiple configurations should be easy.
  *
@@ -107,6 +102,11 @@
     static Config[char[]] configs;	/// All configs loaded by load().
     private static TreeBag!(char[]) loadedFiles;	// all filenames load tried to read
     
+    private static Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.input.config.Config");
+    }
+    
 //BEGIN File loading/saving code
     static this () {
         loadedFiles = new TreeBag!(char[]);
@@ -124,7 +124,7 @@
             // TODO: also load user-config file
             
             file.dataSecCreator =
-            function MT.DataSection (MT.ID) {	return new Config;	};
+            delegate MT.DataSection (MT.ID) {	return new Config;	};
             
             // D2.0: enum MT.ID CONFIGS = "Configs";
             const MT.ID CONFIGS = cast(MT.ID)"Configs";
@@ -174,7 +174,6 @@
             else if (id == QUEUE.MOUSE) relMotion = cast(outQueue[][uint]) parseTo!(uint[][][uint]) (dt);
             else logger.warn ("Unexpected tag encountered with ID " ~ cast(char[])id);
         } // FIXME: add support for name and inheritants.
-        else throw new MT.MTUnknownTypeException ("Input Config: only uint[][][uint] type supported");
     }
     void writeAll (ItemDelg) {
         // FIXME
--- a/mde/input/input.d	Thu Feb 21 09:05:33 2008 +0000
+++ b/mde/input/input.d	Fri Feb 22 11:52:20 2008 +0000
@@ -449,7 +449,8 @@
     *	Callbacks of all types.
     *	Status set from events for all types (button,axis,relMotion).
     *
-    * It relies on config loaded from a file.
+    * It relies on config loaded from a file (dependant on where input bindings are loaded from;
+    * currently conf/input.mtt).
     */
     debug (mdeUnitTest) unittest {
         Input ut = new Input();
--- a/mde/mde.d	Thu Feb 21 09:05:33 2008 +0000
+++ b/mde/mde.d	Fri Feb 22 11:52:20 2008 +0000
@@ -9,7 +9,7 @@
 import global = mde.global;
 import mde.events;
 import mde.scheduler;
-import mde.options;     // greeting message
+import mde.i18n;     // greeting message
 import mde.exception;
 
 import mde.input.input;
@@ -44,7 +44,9 @@
     } );
     //END Initialisation
     
-    logger.info (options.greeting);
+    /* Log a greeting message. Just a little test really, but it can stay until i18n finds a proper use. */
+    I18nTranslation transl = I18nTranslation.load ("mde");
+    logger.info (transl.getEntry ("greeting"));
     
     while (global.run) {
         Scheduler.run (Clock.now());
--- a/mde/mergetag/dataset.d	Thu Feb 21 09:05:33 2008 +0000
+++ b/mde/mergetag/dataset.d	Fri Feb 22 11:52:20 2008 +0000
@@ -37,11 +37,6 @@
     }
 }
 
-/+private Logger logger;
-static this () {
-    logger = Log.getLogger ("mde.mergetag.dataset");
-}+/
-
 /**************************************************************************************************
  * Data class; contains a DataSection class instance for each loaded section of a file.
  *
@@ -75,12 +70,15 @@
  * file.
  *
  * A class implementing this may implement the addTag function to do whatever it likes with the
- * data passed; DefaultData separates this data out into supported types and stores it
- * appropriately, while throwing an error when unsupported types are passed, but a different
- * implementation could filter out the tags desired and use them directly, while ignoring the rest.
- * The parse module provides a useful set of templated functions to
+ * data passed. DefaultData is one implementation which separates this data out into supported
+ * types and stores it appropriately (allowing merging with existing entries by keeping whichever
+ * tag was last loaded), while ignoring unsupported types. A different
+ * implementation could filter out the tags desired and use them directly, and ignore the rest.
+ *
+ * The tango.scrapple.text.convert.parseTo module provides a useful set of templated functions to
  * convert the data accordingly. It is advised to keep the type definitions as defined in the file-
- * format except for user-defined types.
+ * format except for user-defined types, although this isn't necessary for library operation
+ * (addTag and writeAll are solely responsible for using and setting the type, ID and data fields).
  *
  * Another idea for a DataSection class:
  * Use a void*[ID] variable to store all data (may also need a type var for each item).
@@ -89,16 +87,22 @@
  */
 interface DataSection
 {
+    /** Delegate passed to writeAll. */
     typedef void delegate (char[],ID,char[]) ItemDelg;
+    
     /** Handles parsing of data items for all recognised types.
      *
-     * Should throw an MTUnknownTypeException for unsupported types with a short explanation of the
-     * form: "Package ClassName: supported types" or similar.
+     * Should ignore unsupported types/unwanted tags.
      *
-     * Only MTUnknownTypeException and MTaddTagParseException exceptions are caught by a reader
-     * calling addTag. */
+     * TextExceptions (thrown by parseTo/parseFrom) are caught and a warning logged; execution
+     * then continues (so the offending tag gets dropped). */
     void addTag (char[],ID,char[]);
-    void writeAll (ItemDelg);	/// TBD
+    
+    /** Responsible for getting all data tags saved.
+    *
+    * writeAll should call the ItemDelg once for each tag to be saved with parameters in the same
+    * form as received by addTag (char[] type, ID id, char[] data). */
+    void writeAll (ItemDelg);
 }
 
 debug (mdeUnitTest) unittest {	// Only covers DataSet really.
--- a/mde/mergetag/defaultdata.d	Thu Feb 21 09:05:33 2008 +0000
+++ b/mde/mergetag/defaultdata.d	Fri Feb 22 11:52:20 2008 +0000
@@ -86,7 +86,7 @@
                     indent(indents+1) ~ varName(c) ~ "[id] = parseTo!(" ~ c ~ ") (dt);\n" ~
                 indent(indents) ~ "} else ";
             }
-            ret ~= "{\n" ~ indent(indents+1) ~ "throw new MTUnknownTypeException;\n" ~ indent(indents) ~ "}\n";
+            ret = ret[0..$-6];  // remove last else
             return ret;
         }
     }
--- a/mde/mergetag/exception.d	Thu Feb 21 09:05:33 2008 +0000
+++ b/mde/mergetag/exception.d	Fri Feb 22 11:52:20 2008 +0000
@@ -44,17 +44,6 @@
     }
 }
 
-/** Thrown by addTag (in classes implementing dataset.DataSection) when a tag is read with an
- * unrecognised type field. */
-class MTUnknownTypeException : MTException {
-    this () {
-        super ("Unknown type");
-    }
-    this (char[] msg) {
-        super (msg);
-    }
-}
-
 /** Thrown by addTag (in classes implementing dataset.DataSection) when a data parsing error occurs
 * (really just to make whoever called addTag to log a warning saying where the error occured). */
 class MTaddTagParseException : MTException {
--- a/mde/mergetag/read.d	Thu Feb 21 09:05:33 2008 +0000
+++ b/mde/mergetag/read.d	Fri Feb 22 11:52:20 2008 +0000
@@ -66,15 +66,16 @@
     */
     DataSet dataset;
     
-    /** A function for creating new DataSections within the dataset.
+    /** A delegate for creating new DataSections within the dataset.
     *
-    * Allows a user-made class to be used in the DataSet instead of DefaultData.
+    * Allows a user-made class to be used in the DataSet instead of DefaultData. Also allows an
+    * existing class instance to be used instead of a new one.
     *
     * This works by supplying a function which returns a reference to an instance of a class
     * implementing DataSection. The function is passed the ID of the new section and may use this
     * to use different DataSection classes for different sections.
     */
-    DataSection function (ID) dataSecCreator = null;
+    DataSection delegate (ID) dataSecCreator = null;
     
 private:
     static Logger logger;
@@ -211,7 +212,7 @@
     * later.
     */
     public void read (ID[] secSet) {
-        HashSet!(ID) hs;
+        HashSet!(ID) hs = new HashSet!(ID);
         foreach (id; secSet) hs.add(id);
         read (hs);
     }
@@ -244,7 +245,8 @@
                         DataSection ds = getOrCreateSec (id);
                         pos = parseSection (pos, &ds);
                         secTable[id].read = true;
-                    }
+                    } else
+                        pos = parseSection (pos, null); // skip section
                 }
             }
         } else {
@@ -336,13 +338,9 @@
                         dsec.addTag (type, tagID, data);
                     }
                     catch (TextException e) {
-                        logger.warn ("While reading " ~ ErrFile ~ ":");	// following a parse error
+                        logger.warn ("TextException while reading " ~ ErrFile ~ ":");	// following a parse error
                         logger.warn (e.msg);
                     }
-                    catch (MTUnknownTypeException e) {
-                        logger.warn ("Unsupported type \"" ~ type ~ "\" " ~ ErrInFile /*~ ":"*/);
-                        //logger.warn (e.msg); needless; the above includes enough details.
-                    }
                     catch (Exception e) {
                         logger.error ("Unknown error occured" ~ ErrInFile ~ ':');
                         logger.error (e.msg);
@@ -388,7 +386,9 @@
         for (; pos < fbuf.length; ++pos)
             if (fbuf[pos] == '}' || fbuf[pos] == '{') break;
         
-        if (fbuf[pos] != '}') throwMTErr ("Bad section tag format: not {id}" ~ ErrInFile, new MTSyntaxException);
+        if (pos >= fbuf.length || fbuf[pos] != '}')
+            throwMTErr ("Bad section tag format: not {id}" ~ ErrInFile, new MTSyntaxException);
+        
         ID id = cast(ID) fbuf[start..pos];
         fbufIncrement(pos);
         return id;
--- a/mde/options.d	Thu Feb 21 09:05:33 2008 +0000
+++ b/mde/options.d	Fri Feb 22 11:52:20 2008 +0000
@@ -18,13 +18,12 @@
 import tango.io.FilePath : FilePath;
 import tango.util.log.Log : Log, Logger;
 
-private Logger logger;
+Options options;
 static this() {
-    logger = Log.getLogger ("mde.options");
+    // Since options is used in many places, creating a class of default values now makes its use safer.
+    options = new Options();
 }
 
-Options options;
-
 // Later: add automatic saving/loading for variables, variable lists with GUI name and description.
 
 /** Class instance stores all options.
@@ -35,27 +34,26 @@
 {
     /* The options.
     *
-    * For now, static variables. Later, lists (and static variables?).
+    * For now, named variables. Later, lists (and named variables?).
+    *
+    * They can be read and set directly by other code (so not thread-safe for writing currently).
     */
-    bool useThreads;
     char[] greeting;    // just a testing message
-    
+    char[] L10n;        // locale, e.g. en-GB
     
     /* The code to load/save the values.
     *
     * Uses mergetag.
     */
     void addTag (char[] tp, ID id, char[] dt) {
-        if (tp == "bool") {
-            if (id == cast(ID)"useThreads") useThreads = parseTo!(bool) (dt);
-        } else if (tp == "char[]") {
+        if (tp == "char[]") {
             if (id == cast(ID)"greeting") greeting = parseTo!(char[]) (dt);
-        } else
-            throw new MTUnknownTypeException ("Only bool currently supported.");
+            else if (id == cast(ID)"L10n") L10n = parseTo!(char[]) (dt);
+        }
     }
     void writeAll (ItemDelg dlg) {
-        dlg ("bool", cast(ID)"useThreads", parseFrom!(bool) (useThreads));
         dlg ("char[]", cast(ID)"greeting", parseFrom!(char[]) (greeting));
+        dlg ("char[]", cast(ID)"L10n", parseFrom!(char[]) (L10n));
     }
     
     
@@ -66,19 +64,15 @@
     private static const fileName = "conf/options.mtt";
     private static const secName = cast(ID)"Default";
     static void load () {
-        scope(failure) options = new Options();     // FIXME necessary while options is accessed statically and access is unprotected!
-        
         FilePath filePath = new FilePath (fileName);
-        // If it's not a file, stop. It should still be created on exit (assuming it's not a folder).
-        if (!filePath.exists || filePath.fileSize == 0u) {
-            options = new Options();
-            return;
-        }
+        // If it's not a file or an empty file stop. It should still be created on exit (assuming
+        // it's not a folder).
+        if (!filePath.exists || filePath.fileSize == 0u) return;
         
         Reader reader;
         try {
             reader = new Reader(filePath);
-            reader.dataSecCreator = function DataSection(ID) {
+            reader.dataSecCreator = delegate DataSection(ID) {
                 return new Options;
             };
             reader.read;
@@ -109,4 +103,9 @@
             //FIXME: currently nothing done besides logging the error
         }
     }
+    
+    private static Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.options");
+    }
 }
--- a/test/mdeTest.d	Thu Feb 21 09:05:33 2008 +0000
+++ b/test/mdeTest.d	Fri Feb 22 11:52:20 2008 +0000
@@ -8,9 +8,13 @@
 import mde.mergetag.mtunittest;
 import mde.exception;
 import mde.init;
+import mde.i18n;
 
 import tango.util.log.Log : Log, Logger;
 
+// Set this version to tell mde.init that the root logger is set up here.
+version=mdeTest;
+
 private Logger logger;
 
 static this()