The Language File - ThePix/QuestJS GitHub Wiki

In theory, Quest 6 can be readily converted to another language. That said, I do not speak any other languages anything like fluently enough to test this, so we will see how it goes.

Selecting a language

Once other languages are available, selecting one is done via a line in settings.js.

settings.lang = "lang-en.js"

The language file

The language file must be in the "lang" folder. I suggest it is named "lang-" followed by an identifier for the language, followed by ".js".

The language is contained in a dictionary, "lang", that defines all the setting (it should also define a module export for the editor at the very end of lang-en.js, which does not need changing at all).

The "lang" dictionary need a lot of data, we will take a quick look. There are broadly four things in there.

Regular Expressions for Commands

Every command has a regular expression, associated with the name of the command. Here is an example:

  MetaCredits:/^about$|^credits?$/,

"MetaCredits" is the name of the command (you can find that in commands.js). The regular expression starts and ends with a slash, /, just as a string starts and ends with quote marks. The hat, ^, matches the start of the string, the dollar, $, the end. A vertical bar, |, separates different options. The question mark indicates the preceding character is optional, so the command will therefore match ABOUT, CREDIT or CREDITS.

Brackets are used for capture groups. These are vital because they allow Quest to capture the matched text, match it to an object, and pass that to a command. Brackets also provide a way to group options. Here is an example:

  Examine:/^(?:examine|exam|ex|x) (.+)$/,

So this has two capture groups, the first will match EXAMINE, EXAM, EX or X; the second will match any text - a dot matches any character, a dot with a plus after it will match one or more of them.

Note that the first starts with ?:, indicating it is captured and discarded.

Here is a more complicated example, with four capture groups, two of which are discarded:

  PutIn:/^(?:put|place|drop) (.+) (?:in to|into|in|on to|onto|on) (.+)$/,

The PUT/ON command expects exactly two capture groups, and furthermore it expects them in that order. However, there is an option to reverse or otherwise modify the order, and is discussed here.

Standard Responses

The next section is the "standard responses". These are all strings, most of which use text processor directives to effectively react to the specific situation. There are organised according to templates, just to make it easier to find each one (and if you are not using a template, you can just skip those strings), with more geneal ones at the end.

Complex Responses

These are responses that are too complicated to do in a string, so need a function. There are only a handful, and all but the first two are for NPCs. The first is very important, however.

Meta-command responses

A mix of strings and functions

Language Data

A number of simple phrased or even just words used in various places, mostly strings but also regular expressions.

Language Constructs

Like language data, but the strings are structured, so there is a dictionary of pronouns, a dictionary of display verbs, an array of directions, a dictionary of verb conjugations.

Pronouns

The "pronouns" value is a dictionary of dictionaries. You can add further entries as required by your language, but you need "secondperson" as the PLAYER templates uses it, "thirdperson" as the ITEMS template uses it, and "male" and "female" as the NPC template uses it.

  pronouns:{
    firstperson: {subjective:"I",    objective:"me",   possPro: "mine",   possAdj: "my",    reflexive:"myself",     handleAs:'me'},
    secondperson:{subjective:"you",  objective:"you",  possPro: "yours",  possAdj: "your",  reflexive:"yourself",   handleAs:'you'},
    thirdperson: {subjective:"it",   objective:"it",   possPro: "its",    possAdj: "its",   reflexive:"itself",     handleAs:'it'},
    plural:      {subjective:"they", objective:"them", possPro: "theirs", possAdj: "their", reflexive:"themselves", handleAs:'they'},
    male:        {subjective:"he",   objective:"him",  possPro: "his",    possAdj: "his",   reflexive:"himself",    handleAs:'it'},
    female:      {subjective:"she",  objective:"her",  possPro: "hers",   possAdj: "her",   reflexive:"herself",    handleAs:'it'},
  }

In the sentence, "Mary picks up her ball herself", "Mary" is the subject and could be replaced by the subjective pronoun "she". The "ball" is the object, so could be replaced by the objective pronoun. The word "her" in that context is a possessive adjective (or "possAdj" in the dictionaries above), a describing word indicating who it belongs to. Finally (and possibly gratuitously) "herself" is the reflexive.

In fact, all of this is really up to you. Do you need all these in your language? Do you need more? The only place these are used directly are in he text processor (seach for "tp.handlePronouns" in lib/_text to see where), and you may want to modify or extend that for your language.

The "handleAs" attribute is used when conjugating verbs - a way to group all pronouns that are conjugated the same. In English verbs with "he" and "she" will conjugate the same as with "it".

Display verbs

The "verbs" dictionary is a list of display verbs used in the side pane. For verbs that the parse will understand as just verb-item, a simple string is fine. Sometimes you might want the item in the middle or start of the command. In that case you need to give a dictionary with two attributes, the "name" will get shown in the side pane. The "action" will be used to construct the command. Insert a percentage sign in the string as a stand-in for the item.

For the example, "Carefully examine" will be displayed. If the player selects that for the gold crown, the command will be "Look at gold crown carefully".

  verbs:{
    examine:{name:"Carefully examine", action:'Look at % carefully'},
    use:"Use",
    take:"Take",
...

Exits

The "exit_list" value is an array of dictionaries, one entry for each direction. The dictionaries need a name, this will be the name of the attribute for the room object. The "abbrev" is a one or two letter abbreviation that appears in the compass rose. The "niceDir" string is how it might appear in a string; "Kyle enters the room from the north.". The "key" indicates which key on the number pad can be used. The optional "alt" allows for different phrases to be recognised. If "nocmd" is true, this entry will not be used to generate an exit command (these commands are added elsewhere).

  exit_list:[
    {name:'northwest', abbrev:'NW', niceDir:"the northwest", type:'compass', key:103, x:-1 ,y:1, z:0, opp:'southeast', symbol:'🡬'}, 
    {name:'north', abbrev:'N', niceDir:"the north", type:'compass', key:104, x:0 ,y:1, z:0, opp:'south', symbol:'🡩'}, 
    {name:'northeast', abbrev:'NE', niceDir:"the northeast", type:'compass', key:105, x:1 ,y:1, z:0, opp:'southwest', symbol:'🡭'}, 
    {name:'in', abbrev:'In', alt:'enter|i', niceDir:"inside", type:'inout', key:111, opp:'out', symbol:'↴'}, 
    {name:'up', abbrev:'U', niceDir:"above", type:'vertical', key:109, x:0 ,y:0, z:1, opp:'down', symbol:'↥'},
    
    {name:'west', abbrev:'W', niceDir:"the west", type:'compass', key:100, x:-1 ,y:0, z:0, opp:'east', symbol:'🡨'}, 
    {name:'Look', abbrev:'Lk', type:'nocmd', key:101, symbol:'👁'}, 
    {name:'east', abbrev:'E', niceDir:"the east", type:'compass', key:102, x:1 ,y:0, z:0, opp:'west', symbol:'🡪'}, 
    {name:'out', abbrev:'Out', alt:'exit|o', niceDir:"outside", type:'inout', key:106,opp:'in', symbol:'↱'}, 
    {name:'down', abbrev:'Dn', alt:'d', niceDir:"below", type:'vertical', key:107, x:0 ,y:0, z:-1, opp:'up', symbol:'↧'}, 

    {name:'southwest', abbrev:'SW', niceDir:"the southwest", type:'compass', key:97, x:-1 ,y:-1, z:0, opp:'northeast', symbol:'🡯'}, 
    {name:'south', abbrev:'S', niceDir:"the south", type:'compass', key:98, x:0 ,y:-1, z:0, opp:'north', symbol:'🡫'}, 
    {name:'southeast', abbrev:'SE', niceDir:"the southeast", type:'compass', key:99, x:1 ,y:-1, z:0, opp:'northwest', symbol:'🡮'}, 
    {name:'Wait', abbrev:'Z', type:'nocmd', key:110, symbol:'⏸'}, 
    {name:'Help', abbrev:'?', type:'nocmd', symbol:'🛈'}, 
  ],

Note that lang.exit_list is used to contain a lot of data about directions; make sure you use the most recent version from lang-en.js (rather than the above, which may not be up-to-date!).

Language functions

Then there are some functions that perform language specific duties.

addDefiniteArticle and addIndefiniteArticle both return the item's name, with either "the" or "a" added as appropriate.

The getName function returns he name of the given object. Usually that is the alias, but depending on the options used, it could (in English) have "a" or "the" prepended, or "'s" appended. To be able to handle a COUNTABLE item, it can be quite complex.

The toWords function returns a number as a string, so 5 becomes "five". The toOrdinal function does likewise, but would convert 5 to "fifth". If you are happy with using numerals, a quick solution is this:

  toWords:function(number, noun) {
    return noun ? number.toString()  + " " + noun:number.toString()
  },
  toOrdinal:function(number) {
    return number.toString() + "th"
  },

The convertNumbers function goes the other way, convert a word for a number in input text into digits. If you set settings.convertNumbersInParser to false in settings.js, it is not used.

Conjugating...

The big function is conjugate, which returns a verb is the form modified for the given item (so "go" becomes "goes" for a ball). The best way to do that is very much language dependent. In English we have only one regular verb form, which makes it much easier, but numerous irregular ones, handled via an array called "conjugations".

  conjugate:function(item, verb) {
    let gender = item.pronouns.subjective
    if (gender === "he" || gender === "she") { gender = "it"; }
    const arr = lang.conjugations[gender.toLowerCase()]

    if (!arr) {
      errormsg("No conjugations found: conjugations_" + gender.toLowerCase())
      return verb
    }
    for (let conj of arr) {
      if (conj.name === verb) {
        return conj.value
      }
    }
    
    for (let conj of arr) {
      const name = conj.name
      const value = conj.value
      if (name.startsWith("@") && verb.endsWith(name.substring(1))) {
        return lang.conjugate (item, verb.substring(0, verb.length - name.length + 1)) + value
      }
      else if (name.startsWith("*") && verb.endsWith(name.substring(1))) {
        return item, verb.substring(0, verb.length - name.length + 1) + value
      }
    }
    return verb
  },

Then there are a number of convenience functions that use conjugate to form a pair from a noun or pronoun with the verb, optionally capitalised.

  pronounVerb:function(item, verb, capitalise) {
    let s = item.pronouns.subjective + " " + lang.conjugate (item, verb);
    s = s.replace(/ +\'/, "'");  // yes this is a hack!
    return capitalise ? sentenceCase(s) : s;
  },

The full list is pronounVerb, pronounVerbForGroup, verbPronoun, nounVerb, verbNoun. In English it is usual to have the noun before the verb, but occasionally it is reversed, so there are functions for that. You may not need that. These functions should only be used in the language file (possibly via the text processor) so if you omit some - or add your own - it should be fine.

Notes

Because I am building this from the ground up, the values have been designed from the start to accept the player or an NPC as a second parameter, making it much easier to tell NPCs what to do. This will make translation more complicated, however.

I have no intention of making error and debugging messages translatable (Quest 5 is the same). I think it more useful to have very specific error messages, rather than a limited number that are translated. If errormsg is used, this will give a standard error to screen, which can be translated, and then the details (in English) go to the console, so this seems a good compromise, but I cannot guarantee all errors will use that function!

Recommended Approach

If you want to translate the language file, I would suggest you start by creating a simple game that will test your translations, and either give it unit tests or a walk-though, so you can quickly go through the responses.

Copy the lang-en.js file, and set your game to use the copy.

Now start translating! I suggest starting with the language data and language construct sections (both of which should be pretty straightforward), and then the language functions (which will not!). The most important function is conjugate, and how complex that is will depend on your language. English has a whole bunch of exceptions, but there is just one rule, with only two variations, so relatively easy.

Customising

If you want to just change a few language settings (you still want English, but want to customise the responses), you are better off changing the lang dictionary. If you want to do that in setting.js, it has to be done inside the settings.setup function.

settings.setup = function() {
  // other stuff
  lang.inventoryPreamble = "Lara is carrying"
}