Mercurial > projects > mde
diff mde/lookup/Translation.d @ 63:66d555da083e
Moved many modules/packages to better reflect usage.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Fri, 27 Jun 2008 18:35:33 +0100 |
parents | mde/i18n/I18nTranslation.d@611f7b9063c6 |
children | 7fc0a8295c83 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/lookup/Translation.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,242 @@ +/* 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.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 Translation : 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 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 ("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.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") { + char[][] fields = split (stripBrackets (dt)); + + if (fields.length < 1) { + // This tag is invalid, but this fact doesn't need to be reported elsewhere: + logger.error ("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 { + /* 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 ("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 + miscOpts.L10n = currentL10n; + + logger.info ("Unittest complete."); + } +}