diff mde/content/Translation.d @ 137:9f035cd139c6

BIG commit. Major change: old Options class is gone, all content values are loaded and saved automatically. All options updated to reflect this, some changed. Content restrutured a lot: New IContent module, Content module includes more functionality. New ContentLoader module to manage content loading/saving/translation. Translation module moved to content dir and cut down to reflect current usage. File format unchanged except renames: FontOptions -> Font, VideoOptions -> Screen. Font render mode and LCD filter options are now enums. GUI loading needs to create content (and set type for enums), but doesn't save/load value. Some setup of mainSchedule moved to mde.mainLoop. Content callbacks are called on content change now. ContentLists are set up implicitly from content symbols. Not as fast but much easier! Bug-fix in the new MTTagReader. Renamed MT *Reader maker functions to avoid confusion in paths.d. New mde.setup.logger module to allow logger setup before any other module's static this().
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 07 Feb 2009 12:46:03 +0000
parents mde/lookup/Translation.d@7ababdf97748
children 0520cc00c0cc
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/content/Translation.d	Sat Feb 07 12:46:03 2009 +0000
@@ -0,0 +1,202 @@
+/* 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
+ *
+ * Loads a set of names and optionally descriptions intended to provide a user-
+ * friendly localised name to symbols defined in code or data files.
+ *
+ * Each locale may specify dependant locales which will be loaded and merged,
+ * so that, for instance, variants of a language need not define common strings
+ * in all variants. Dependencies are loaded in the order specified, including
+ * all dependencies of first locale before any dependencies of other locales.
+ * Circular dependencies are allowed.
+ *
+ * Entries may be extended to include a version number, intended to indicate
+ * translations which may need updating to reflect a changed meaning. A
+ * translation tool is needed to handle this really. */
+module mde.lookup.Translation;
+
+import mde.file.paths;
+import mde.exception;
+
+import mde.file.mergetag.MTTagReader;
+import mde.file.deserialize;
+
+import tango.util.log.Log : Log, Logger;
+
+/** Loads all translation strings for current locale.
+ *
+ * See module description for details.
+ *
+ * Encoding used is UTF-8. */
+struct Translation
+{
+    /** Load the translation for the requested locale.
+    *
+    * Params:
+    *   name = The locale to load strings for.
+    */
+    static {
+	/** Get Translation instance for locale l10n, loading if not already
+	 *  loaded. Only keeps one locale loaded.
+	 * 
+	 * These are loaded from data/L10n/locale.mtt where locale is l10n and
+	 * files for dependant locales given in the header tag
+	 * <char[][]|depends=[...]>.
+	 *
+	 * On errors, a message is logged and the function continues, likely
+	 * resulting in some symbol names being untranslated. */
+	Translation get (char[] l10n) {
+	    if (loadedL10n != l10n) {
+		loadedL10n = l10n;
+		debug logger.trace ("Loading L10n: "~loadedL10n);
+                transl.load (l10n);
+	    }
+            return transl;
+	}
+	private Translation transl;
+        private char[] loadedL10n;
+        private Logger logger;
+        static this () {
+            logger = Log.lookup ("mde.lookup.Translation");
+        }
+    }
+    
+    private void load (char[] l10n) {
+        char[][] files = [loadedL10n];	// initial locale plus dependencies
+        size_t read = 0;	// index in files of next file to read
+        while (read < files.length) {
+            logger.trace ("reading file {}", files[read]);
+            try {
+                MTTagReader reader = dataDir.makeMTTagReader ("L10n/"~files[read], PRIORITY.HIGH_LOW);
+                bool isSecTag;
+                while (reader.readTag (isSecTag)) {
+                    if (isSecTag) break;	// end of header
+                    if (reader.tagID == "depends" && reader.tagType == "char[][]") {
+                        // append, making sure no duplicates are inserted
+                        char[][] deps = deserialize!(char[][]) (reader.tagData);
+                        f1:
+                        foreach (dep; deps) {
+                            foreach (f; files)
+                                if (f == dep)
+                                    continue f1;
+                            files ~= dep;
+                        }
+                    }
+                }
+                char[] symPrefix;
+                do {
+                    if (isSecTag) {
+                        if (reader.section.length == 0) {
+                            symPrefix = "";
+                            continue;
+                        }
+                        symPrefix = reader.section ~ '.';
+                    } else {
+                        if (reader.tagType == "entry") {
+                            char[] sym = symPrefix ~ reader.tagID;
+                            // If the tag already exists, don't replace it
+                            if (sym in entries) continue;
+                            
+                            Entry entry = deserialize!(Entry) (reader.tagData);
+                            if (entry.name is null) {   // This tag is invalid; ignore it
+                                logger.error ("In L10n/{}.mtt: tag {} has no name", files[read], sym);
+                                continue;
+                            }
+                            entries[sym] = entry;
+                        }
+                    }
+                } while (reader.readTag (isSecTag))
+                ++read;
+            } catch (Exception e) {
+                logger.error ("Exception loading translation: {}", e.msg);
+            }
+        }
+    }
+    
+    /+ Getters for entries... not wanted now.
+    alias entry opCall;	    /// Convenience alias
+    
+    /** Get the translation for the given identifier.
+    * If no entry exists, the identifier will be returned.
+    *
+    * Optionally, the description can be returned. */
+    char[] entry (char[] id) {
+        Entry* p = id in entries;
+        if (p) {
+            return p.name;
+        } else {
+            return id;
+        }
+    }
+    /** ditto */
+    char[] entry (char[] id, out char[] description) {
+        Entry* p = id in entries;
+        if (p) {
+            description = p.desc;
+            return p.name;
+        } else {
+            return id;
+        }
+    }
+    
+    /** Alternative usage: return a Translation.Entry struct. */
+    Entry getStruct (char[] id) {
+        Entry* p = id in entries;
+        if (p) {
+            return *p;
+        } else {
+            logger.warn ("Unable to find translation for: {}", id);
+            Entry ret;
+            ret.name = id;
+            return ret;
+        }
+    } +/
+    
+    /** This struct is used to store each translation name and description pair.
+     *
+     * Entries may also have a version field, but this is only needed for
+     * writing/updating translations. */
+    struct Entry {
+        char[] name;        // The translated string
+        char[] desc;        // An optional description
+    }
+    
+    Entry[char[]] entries;  // all entries
+    
+    debug (mdeUnitTest) unittest {
+        // Relies on files in unittest/data/L10n: locale-x.mtt for x in 1..4
+        
+	// Struct tests
+	Translation t = get ("locale-1");
+	Entry e = t.getStruct ("section-2.entry-1");
+	assert (e.name == "Test 1");
+	assert (e.desc == "Description");
+        e = t.getStruct ("section-2.entry-2");
+        assert (e.name == "Test 2");
+	assert (e.desc is null);
+	
+	// Dependency tests. Priority should be 1,2,3,4 (entries should come from first file in list that they appear in).
+	char[] d = "1";
+        assert (t.entry ("section-1.file-1", d) == "locale-1");
+	assert (d is null);
+        assert (t.entry ("section-1.file-2", d) == "locale-2");
+	assert (d == "desc2");
+        assert (t.entry ("section-1.file-3") == "locale-3");
+        assert (t.entry ("section-1.file-4") == "locale-4");
+	
+        logger.info ("Unittest complete.");
+    }
+}