Mercurial > projects > mde
view mde/lookup/Translation.d @ 81:d8fccaa45d5f
Moved file IO code from mde/mergetag to mde/file[/mergetag] and changed how some errors are caught.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Fri, 29 Aug 2008 11:59:43 +0100 |
parents | 61ea26abe4dd |
children | 49e7cfed4b34 |
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. char[] currentL10n = miscOpts.L10n; miscOpts.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 miscOpts.L10n = currentL10n; logger.info ("Unittest complete."); } }