diff mde/lookup/Translation.d @ 63:66d555da083e

Moved many modules/packages to better reflect usage.
author Diggory Hardy <diggory.hardy@gmail.com>
date Fri, 27 Jun 2008 18:35:33 +0100
parents mde/i18n/I18nTranslation.d@611f7b9063c6
children 7fc0a8295c83
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/lookup/Translation.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,242 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+/** Translation − internationalization module for translating strings
+*
+* 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.lookup.Translation;
+
+import mde.options;
+import mde.exception;
+
+import mde.mergetag.DataSet;
+import mde.mergetag.Reader;
+import mde.mergetag.exception;
+import mde.resource.paths;
+
+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 Translation : IDataSection
+{
+    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 Translation 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) miscOpts.L10n];  // start by loading the current locale
+        
+        Translation transl = new Translation (name, miscOpts.L10n);
+        
+        IReader reader;
+        try {
+            reader = dataDir.makeMTReader ("L10n/"~name, PRIORITY.HIGH_LOW);
+            /* 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 IDataSection(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) {
+            // If an error occurs, log a message but continue (strings will just be untranslated)
+            logger.error ("Mergetag exception occurred:");
+            logger.error (e.msg);
+        }
+        
+        delete transl.depends;      // Free memory
+        transl.depends = [];
+        
+        return transl;
+    }
+    
+    static this() {
+        logger = Log.getLogger ("mde.lookup.Translation");
+    }
+    
+    /* 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 this fact doesn't need to be reported elsewhere:
+                logger.error ("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 = miscOpts.L10n;
+        miscOpts.L10n = "test-1";
+        
+        Translation 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.
+        
+        // Restore
+        miscOpts.L10n = currentL10n;
+        
+        logger.info ("Unittest complete.");
+    }
+}