view mde/content/Translation.d @ 156:36df0ffe34d2

Fix to reload translations.
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 18 Apr 2009 21:51:03 +0200
parents 0520cc00c0cc
children 24d77c52243f
line wrap: on
line source

/* 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) {
	entries = null;		// clear, then re-read
        char[][] files = [loadedL10n];	// initial locale plus dependencies
        size_t read = 0;	// index in files of next file to read
        while (read < files.length) {
            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 = "";
                        else
			    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;
                            
			    try {
			      Entry entry = deserialize!(Entry) (reader.tagData);
			      if (entry.name is null)	// This tag is invalid; ignore it
                                throw new ContentException ("Tag has no name");
			      entries[sym] = entry;
			    } catch (Exception e) {
			      logger.error ("Exception reading L10n/{}.mtt:", files[read]);
			      logger.error ("While parsing tag {}", sym);
			      logger.error (e.msg);
			      continue;
			    }
                        }
                    }
                } while (reader.readTag (isSecTag))
            } catch (Exception e) {
	      logger.error ("Exception reading L10n/{}.mtt:", files[read]);
	      logger.error (e.msg);
            }
	    ++read;	// continue to next file regardless of whether this file was read successfully
	}
    }
    
    /+ 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.");
    }
}