view mde/lookup/Translation.d @ 103:42e241e7be3e

ContentList content type; getting content items/lists from Options generically via content.Items, and a new addContent widget function. Several improvements to generic handling of content. New button-with-text widget. Some tidy-up. Some name changes, to increase uniformity. Bug-fix: floating widgets of fixed size could previously be made larger than intended from config dimdata.
author Diggory Hardy <diggory.hardy@gmail.com>
date Tue, 25 Nov 2008 18:01:44 +0000
parents 49e7cfed4b34
children 20f7d813bb0f
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
{
    final char[] name;      /// The module/package/... which the instance is for
    final char[] L10n;      /// The localization loaded (e.g. en-GB)
    
    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;
        }
    }
    
    /** 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 (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") {
            // 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 ("For name "~name~", L10n "~L10n~": tag with ID "~cast(char[])id~" has no data");
                return;
            }
            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) {}
    
    /** 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 L10n.
    *
    * Also ensures only load() can create instances. */
    this (char[] n, char[] l) {
        name = n;
        L10n = l;
    }
    
    //BEGIN Data
    static Logger logger;
    
    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.
        TextContent realL10n = miscOpts.L10n;
        miscOpts.L10n = new TextContent ("L10n", "test-1");
        
        Translation transl = load ("unittest/Translation");
        
        // Simple get-string, check dependancy's entry doesn't override
        assert (transl.entry ("Str1") == "Test 1");
        
        // Entry included from dependancy with description
        char[] desc;
        assert (transl.entry ("Str2", desc) == "Test 3");
        assert (desc == "Description");
        
        // No entry: fallback to identifier string
        assert (transl.entry ("Str3") == "Str3");
        
        // No checks for version info since it's not functionality of this module.
        // Only check extra entries are allowed but ignored.
        
        // Restore
	delete miscOpts.L10n;
        miscOpts.L10n = realL10n;
        
        logger.info ("Unittest complete.");
    }
}