view mde/i18n/I18nTranslation.d @ 20:838577503598

Reworked much of Init. Moved mde.Init to mde.scheduler.Init and largely cleaned up the code. Implemented mde.scheduler.InitStage to reduce dependancies of modules running Init functions. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 22 Mar 2008 16:22:59 +0000
parents db0b48f02b69
children 32eff0e01c05
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, version 2, as published by the Free Software Foundation.

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, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */

/** I18nTranslation − 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.i18n.I18nTranslation;

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 I18nTranslation : 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 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.misc.L10n];  // start by loading the current locale
        
        I18nTranslation transl = new I18nTranslation (name, Options.misc.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.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 {
        // This gets used before it is normally created (test incase this changes).
        if (Options.misc is null) Options.misc = new OptionsMisc;
        
        /* 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.misc.L10n;
        Options.misc.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.
        
        // Restore
        Options.misc.L10n = currentL10n;
        
        logger.info ("Unittest complete.");
    }
}