- 最后登录
- 2017-5-15
- 注册时间
- 2012-3-1
- 阅读权限
- 90
- 积分
- 32973
- 纳金币
- 32806
- 精华
- 12
|
What is it supposed to do?
Basically, I could simply hard-code most of my strings used in the game directly into the code – no one would notice the difference anyway. But obviously, this is not a very good idea, both because editing strings and later translating them becomes a pain.
Creating some data that would allow me to get strings out of an XML file would solve this problem – and, if the code is good enough, be reusable in later games.
It would allow me to edit text independently of the game code and add translations on a later date.
Preliminary format of the XML
The XML file has the following (as of now experimental) format:
<?xml version="1.0" encoding="UTF-8" ?> <localizableStrings> <meta> <gameName>***st in Me</gameName> <version>1.0</version> <author>Kaspar Manz</author> <language>multilingual</language> <grouping>multi</grouping> </meta> <group id="1"> <string id="1"> <text lang="de">Das ist der Text auf Deutsch.</text> <text lang="en">This is the same string in English.</text> <audio lang="de">audio/de/1/1.ogg</audio> <audio lang="en">audio/en/1/1.ogg</audio> </string> <string id="2"> <text lang="de">Willkommen zu ***st in Me.</text> <text lang="en">Welcome to ***st in Me.</text> </string> </group> </localizableStrings>
In order to allow for different distribution formats, parts of this file can change, as reflected within the <meta> tag.
The file above could contain all strings in all languages for all levels and screens. The format allows for splitting these files up, i.e. into single level files (one file for each level) or single language files (one file for each available language) or a combination thereof.
A a mono-grouped, monolingual file would then look like this:
<?xml version="1.0" encoding="UTF-8" ?> <localizableStrings> <meta> <gameName>***st in Me</gameName> <version>1.0</version> <author>Kaspar Manz</author> <language>en</language> <grouping>menu</grouping> </meta> <group> <string id="button-load"> <text>Load Game</text> </string> <string id="button-save"> <text>Save Game</text> </string> <string id="button-delete"> <text>Delete Game</text> </string> </group> </localizableStrings>
Comparing both examples, you can see certain features:
The XML format does no refer to "levels", but to "groups", in order to take into account that also menu and option screens can have their own groups.
Whenever either language or grouping have been defined within the meta tag, the attributes aren't necessary anymore later on.
All ids are strings, in order to allow for more understandable coding and to reduce human error while matching the ID and the actual string in the code.
The format should also allow for audio files to be matched with the strings. This function is not yet implemented, though.
The format has yet to be formalised in a RelaxNG schema or a DTD.
How to get the strings out of the file
Again, first I thought I could do stuff with a XMLReader – before I realised that I didn't want to program a glorified typewriter. An anonymous commenter pointed me to LINQ, which sounded great, but isn't available in Unity, since it uses an older version of the framework.
What's available is XmlDocument, though – and how comfortable it is. Using simple XPath queries, the document can be easily traversed and the necessary strings can be pulled from it.
The following code prepares the necessary variables:
using System; using UnityEngine; using System.IO; using System.Xml; public class XMLStringReader : MonoBehaviour { public string localizedStringsFile; string language; string grouping; XmlDocument root; void Start () { // Get the file into the document. root = new XmlDocument (); root.Load (localizedStringsFile); language = root.SelectSingleNode ("localizableStrings/meta/language").InnerText; grouping = root.SelectSingleNode ("localizableStrings/meta/grouping").InnerText; }
The following code is used to retrieve the strings.
/**
* <summary>Retrieves the string by its ID. Only works when the referenced file
* is both monolingual and monogrouped. Will throw a NullReference Exception
* when the file doesn't meet the requirements.</summary>
* <param name="id">The ID of the requested string.</param>
* <returns>A string with the required text.</returns>
*/ public string GetText (string id) { try { if (language == "multilingual" || grouping == "multi") { throw new NullReferenceException ("The referenced file is" + "multilangual and/or multigrouped."); } else { string n = root.SelectSingleNode ("localizableStrings" + "/group/string[@id='" + id + "']/text").InnerText; return n; } } catch (NullReferenceException ex) { string s = "Missing string (" + ex.ToString () + ")"; return s; } } /**
* <summary>Retrieves the string by its ID and its level. Assumes the second
* parameter to be the level string, but uses it as a language string if it
* fails the first time.</summary>
* <param name="id">The ID of the requested string.</param>
* <param name="level">The name of the group in which the string is placed.</param>
* <returns>A string with the required text.</returns>
*/ public string GetText (string id, string level) { try { /*TODO Write a routine that silently ignores the level string
* when it's the same of the grouping.
*/ string n = root.SelectSingleNode ("localizableStrings" + "/group[@id='" + level + "']" + "/string[@id='" + id + "']" + "/text").InnerText; return n; } catch (NullReferenceException exa) { // Apparently, there was nothing there ... well, so // we simply try and see whether we can use the last string as // a language code. try { string n = root.SelectSingleNode ("localizableStrings" + "/group/string[@id='" + id + "']" + "/text[@lang='" + level + "']").InnerText; return n; } catch (NullReferenceException exb) { string s = "Missing string (" + exb.ToString () + ")"; return s; } } } /**
* <summary>Retrieves the string by its ID, its level and its language.</summary>
* <param name="id">The ID of the requested string.</param>
* <param name="level">The name of the group in which the string is placed.</param>
* <param name="lang">The language the string is required in.</param>
* <returns>A string with the required text.</returns>
*/ public string GetText (string id, string level, string lang) { try { string xPath = "localizableStrings"; xPath += "/group[@id='" + level + "']"; xPath += "/string[@id='" + id + "']"; xPath += "/text[@lang='" + lang + "']"; string n = root.SelectSingleNode (xPath).InnerText; return n; } catch (NullReferenceException ex) { string s = "[This string has either not been implemented or needs " + "to be translated.]"; return s; } }
The code may not be really beautiful right now and could be written more clearly and compact. It was more an***rcise in overloading methods and using try/catch s***ctures ...
The method overloading allows for coding, since only the necessary parameters have to be given (at least, that's the idea).
Ideas that got trashed along the way
For a brief moment I considered changing that all to a ***pal-style translation system, in which the actual strings in the original language would be string IDs, in order to allow coding along the lines of Button(rect, t(Load Game)); where the ID string could be used as a fallback when the language wasn't available.
I decided against it, because of the following reasons:
Text is, again, distributed over actual game code and an XML file: whenever the wording of the original has to be changed, it has to be changed in the code. This defeats the original purpose of the code.
Whenever the original wording is changed in the game code, the translations break (this is also a problem in ***pal, even though strings are handled in a database).
Unsanitised input – depending on the text, it could wreak havoc both in the code as well as the XML.
What still has to be done
The strings aren't sanitised – bad things could happen if someone tried to sneak in some strings on his own, I guess.
The method using two parameters has to be rewritten – it seems to be too clumsy right now.
|
|