view mde/lookup/Translation.d @ 127:3328c6fb77ca

2 fixes for ldc - not that I was able to compile anyway (x86_64).
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 08 Jan 2009 20:09:46 +0000
parents 6acd96f8685f
children 41582439a42b
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
*
* 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.lookup.Options;
import mde.setup.paths;
import mde.exception;

import mde.file.mergetag.DataSet;
import mde.file.mergetag.Reader;
import mde.file.mergetag.exception;
import mde.file.deserialize;

import tango.util.log.Log : Log, Logger;

/** The translation class
*
* See module description for details.
*
* Encoding used is UTF-8.
*/
class Translation : IDataSection
{
    /** 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 {
	/** Get Translation instance for _section section.
	 *
	 * On the first call, loads all Translation sections.
	 * 
	 * These are loaded from data/L10n/locale.mtt where locale is the current locale, as well
	 * as any files named for locales specified 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[] section) {
	    if (sections is null || loadedL10n !is miscOpts.L10n()) {
		if (sections.length) {	// Clear entries hash-map, but re-use classes
		    foreach (s; sections)
			s.entries = null;
		}
		loadedL10n = miscOpts.L10n();
		debug logger.trace ("Loading L10n: "~loadedL10n);
		char[][] files = [loadedL10n];	// initial locale plus dependencies
		size_t read = 0;	// index in files of next file to read
		while (read < files.length) {
		    try {
			IReader reader = dataDir.makeMTReader ("L10n/"~files[read++], PRIORITY.HIGH_LOW, null, true);
			assert (reader.dataset.header);
			auto dp = "depends" in reader.dataset.header._charAA;
			if (dp) {	// append, making sure no duplicates are inserted
			    f1: foreach (dep; *dp) {
				foreach (f; files)
				    if (f == dep)
					continue f1;
				files ~= dep;
			    }
			}
			reader.dataSecCreator = delegate IDataSection(ID sec) {
			    auto p = sec in sections;
			    if (p) return *p;
			    return new Translation (sec);
			};
			reader.read;
		    } catch (MTException e) {
			logger.error ("Mergetag exception occurred:");
			logger.error (e.msg);
		    }
		}
	    }
	    auto p = section in sections;
	    if (p is null) {
		logger.warn ("Section "~section ~ " not found in files; strings will not be translated.");
		return new Translation (section);
	    }
	    return *p;
	}
	private Translation[char[]] sections;
	private char[] loadedL10n;	// reload if different
    }
    
    final char[] section;	// ONLY used to log an error message
    
    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 {
            Entry ret;
            ret.name = id;
            return ret;
        }
    }
    
    static this() {
        logger = Log.getLogger ("mde.lookup.Translation");
    }
    
    /* Mergetag functionality.
     *
     * Merge tags in to entries, prefering existing values. */
    void addTag (char[] tp, ID id, char[] dt) {
        if (tp == "entry") {
            // If the tag already exists, don't replace it
            if (cast(char[]) id in entries) return;
            
            Entry entry = deserialize!(Entry) (dt);
            if (entry.name is null) {   // This tag is invalid; ignore it
                logger.error ("In L10n/*.mtt section "~section~": tag with ID "~cast(char[])id~" has no data");
                return;
            }
            entries[cast(char[]) id] = entry;
        }
    }
    
    // This class is read-only and has no need of being saved.
    void writeAll (ItemDelg) {}
    
    /** 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[] name;        // The translated string
        char[] desc;        // An optional description
    }
    
private:
    /* Sets name and ensures only Translation's static functions can create instances. */
    this (char[] n) {
        section = n;
	sections[n] = this;
    }
    
    //BEGIN Data
    static Logger logger;
    
    Entry[char[]] entries;  // all entries
    //END Data
    
    debug (mdeUnitTest) unittest {
        // Relies on files in unittest/data/L10n: locale-x.mtt for x in 1..4
        
        // Hack a specific locale. Also to allow unittest to run without init.
        StringContent realL10n = miscOpts.L10n;
        miscOpts.L10n = new StringContent ("L10n", "locale-1");
        
	// Struct tests
	Translation t = get ("section-2");
	Entry e = t.getStruct ("entry-1");
	assert (e.name == "Test 1");
	assert (e.desc == "Description");
	e = t.getStruct ("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).
	t = get ("section-1");
	char[] d = "1";
	assert (t.entry ("file-1", d) == "locale-1");
	assert (d is null);
	assert (t.entry ("file-2", d) == "locale-2");
	assert (d == "desc2");
	assert (t.entry ("file-3") == "locale-3");
	assert (t.entry ("file-4") == "locale-4");
	
        // Restore
	delete miscOpts.L10n;
        miscOpts.L10n = realL10n;
	sections = null;
        
        logger.info ("Unittest complete.");
    }
}