RosaeNLG Tutorial for German

Our goal

This tutorial will guide you through a basic use of RosaeNLG. You will apply RosaeNLG to a basic usecase which is generating texts (Natural Language texts) to describe OnePlus smartphones.

You should read a basic Pug tutorial as a prerequisite.

Our OnePlus phone data will come from this site.

Node environment setup

This tutorial focuses on server-side rendering using node.js. But you can run your templates client side in the browser.

INFO: With the integrated editor you will also be able to run directly the tutoriel in your browser.

You can skip this part if you are familiar with node.js environment setup as it’s completely standard.

  • install node.js and npm in your environment

  • create a tutorial folder somewhere

  • npm init and just accept whatever it says/asks

  • npm install rosaenlg will download rosaenlg and end up with something like + rosaenlg@x.x.x

  • create an tuto.js file, just put console.log("hello NLG"); inside of it

  • node tuto.js should output hello NLG (PS that’s not really Natural Language Generation yet)

Initial data

Our initial data. Put it in your tuto.js file.

let phones = [
  {
    name: 'OnePlus 5T',
    colors: ['Black', 'Red', 'White'],
    displaySize: 6,
    screenRatio: 80.43,
    battery: 3300,
  },
  {
    name: 'OnePlus 5',
    colors: ['Gold', 'Gray'],
    displaySize: 5.5,
    screenRatio: 72.93,
    battery: 3300,
  },
  {
    name: 'OnePlus 3T',
    colors: ['Black', 'Gold', 'Gray'],
    displaySize: 5.5,
    screenRatio: 73.15,
    battery: 3400,
  },
];

Plumbing & first texts

You need the rosaenlg lib, thus, add this at the beginning of your tuto.js file:

const rosaenlgPug = require('rosaenlg');

In the same file, call a pug template (we will create the template just after):

let res = rosaenlgPug.renderFile('tuto.pug', {
    language: 'de_DE',
    phone: phones[0]
});
console.log( res );

This will render the tuto.pug template. Parameters:

  • choosing a language (here language: 'de_DE') is mandatory.

  • cache: true tells Pug that it does not need to recompile the template at each call (in practice it is faster).

  • for the other properties you can organize them as you want; here we just put a phone property with our first phone.

Create a tuto.pug file with this content:

p #{phone.name}

This first template is just standard Pug syntax: we output the name of the phone.

When you render the template (using node tuto.js) you should get:
<p>OnePlus 5T</p>

(ok, it’s not really NLG yet)

List elements with the eachz structure

Let’s talk about the colors of the phone: we want to output Die verfügbaren Töne des Telefons sind aaa, bbb und ccc.

Create a mixin dedicated to listing colors (in your tuto.pug file):

mixin colors
  | die verfügbaren Töne des Telefons sind
  eachz color in phone.colors with { separator:',', last_separator:'und', end:'.' }
    | #{color}
  • eachz is a RosaeNLG structure. It’s like a foreach loop, with additionnal NLG features.

  • { separator:',', last_separator:'und', end:'.' } tells eachz that:

    • the standard separator is the comma

    • und should be used between the two last colors

    • we should end with a dot

Call the mixin:

p #{phone.name} . #[+colors]

Run it. Output should be: OnePlus 5T. Die verfügbaren Töne des Telefons sind Black, Red und White.

See how RosaeNLG managed the spacing between words and the automatic capitalization. This is called "Surface Realization" in NLG.

Now we are doing Natural Language Generation 🚀

It is better to have the names of the colors in German. Define the mapping:

mixin colors
  | die verfügbaren Töne des Telefons sind
  eachz color in phone.colors with { separator:',', last_separator:'und', end:'.' }
    -
      var colorMapping = {
        'Black': 'Schwarz',
        'Red': 'Rot',
        'White': 'Weiß',
        'Gold': 'Gold',
        'Gray': 'Grau'
      }
    | #{colorMapping[color]}

The result must now be OnePlus 5T. Die verfügbaren Töne des Telefons sind Schwarz, Rot und Weiß..

Looping on all the phones

Let’s generate some text for each phone. In your tuto.js file:

const res = rosaenlgPug.renderFile('tuto.pug', {
  language: 'de_DE',
  phones: phones,
  cache: true,
});
console.log(res);

In tuto.pug:

- let phone;
each phoneElt in phones
  - phone = phoneElt;
  p #{phone.name} . #[+colors]
Here we have put the main loop directly in the Pug template. In real cases, it is better to loop outside (directly in the javascript caller), as this allows an easy reset of RosaeNLG and Pug between each rendering, which is much better for performance.

You should get:
OnePlus 5T. Die verfügbaren Töne des Telefons sind Schwarz, Rot und Weiß.
OnePlus 5. Die verfügbaren Töne des Telefons sind Gold und Grau.
OnePlus 3T. Die verfügbaren Töne des Telefons sind Schwarz, Gold und Grau.

Basic synonyms

Readers love when texts are not repetitive. Let’s add some very basic synonyms: Farben and Farbtöne are synonyms of Töne. Change your colors mixin:

mixin colors
  | die verfügbaren #[+syn('Farben', 'Farbtöne', 'Töne')]
  | des Telefons sind
  ...

Run it multiple times and you should have different outputs.

More synonyms

The syn mixin is perfect for words or part of sentences. But let’s say we want create some introduction texts, and that we want to have diversity.

Let’s put all these different introductions in a dedicated mixin:

mixin intro
  synz
    syn
      | das #{phone.name} ist wirklich ein fantastisches Telefon.
    syn
      | ich liebe das neue #{phone.name}.
    syn
      | das #{phone.name} : ein tolles Telefon !

The synz > syn structure simply lists synonymic alternatives. You can put whatever you want in each alternative (in each syn): conditions, more synonyms etc.

Let’s call this new mixin:

mixin phone
  | #[+intro] .
  | #[+colors] .

- let phone;
each phoneElt in phones
  - phone = phoneElt;
  p #[+phone]

You should get:
Das OnePlus 5T: ein tolles Telefon! Die verfügbaren Farbtöne des Telefons sind Schwarz, Rot und Weiß.
Das OnePlus 5 ist wirklich ein fantastisches Telefon. Die verfügbaren Farbtöne des Telefons sind Gold und Grau.
Ich liebe das neue OnePlus 3T. Die verfügbaren Farben des Telefons sind Schwarz, Gold und Grau.

Intros are chosen randomly so you might have repetitions.

List parts of a sentence

Let’s talk about the display: physical size and screen-to-body ratio. We want to output something like das Telefon verfügt über ein Display mit einer physischen Größe von 6 Zoll und einem Bildschirm-zu-Körper-Verhältnis von 80,43%.. We could build a big static sentence, but structuring the code will give us more flexibility.

Let’s cut our big sentence in chunks, one for each property:

mixin display
  itemz { separator:',', last_separator:'und' }
    item
      | einer physischen Größe von #[+value(phone.displaySize)] Zoll
    item
      | einem Bildschirm-zu-Körper-Verhältnis von #[+value(phone.screenRatio)]%
  • value is a mixin that will output the value respecting the locale.

  • itemz > item is much like synz > syn, except that it will not choose one alternative, but list all the items.

  • The js object after itemz tells RosaeNLG how to assemble elements. It is mandatory. separator and last_separator work exactly the same way as in the eachz structure.

Do not forget to call this mixin:

mixin phone
  | #[+intro] .
  | #[+colors] .
  | #[+display] .

The result is not that bad, but the beginning of the text is missing. Let’s fix that:

mixin display
  itemz { begin_with_general: 'das Telefon verfügt über ein Display mit', separator:',', last_separator:'und' }
    item
      | einer physischen Größe von #[+value(phone.displaySize)] Zoll
    item
      | einem Bildschirm-zu-Körper-Verhältnis von #[+value(phone.screenRatio)]%

begin_with_general tells RosaeNLG what the texts should begin with. You could have put it outside the mixin (just before), but it’s a good practice to put them inside: for instance, when the list of the elements is empty, RosaeNLG will not output the begin_with_general content.

You should get better texts:
Das OnePlus 5T ist wirklich ein fantastisches Telefon. Die verfügbaren Farbtöne des Telefons sind Schwarz, Rot und Weiß. Das Telefon verfügt über ein Display mit einer physischen Größe von 6 Zoll und einem Bildschirm-zu-Körper-Verhältnis von 80,43%.
Das OnePlus 5 ist wirklich ein fantastisches Telefon. Die verfügbaren Farbtöne des Telefons sind Gold und Grau. Das Telefon verfügt über ein Display mit einer physischen Größe von 5,5 Zoll und einem Bildschirm-zu-Körper-Verhältnis von 72,93%.
Ich liebe das neue OnePlus 3T. Die verfügbaren Farbtöne des Telefons sind Schwarz, Gold und Grau. Das Telefon verfügt über ein Display mit einem Bildschirm-zu-Körper-Verhältnis von 73,15% und einer physischen Größe von 5,5 Zoll.

You can add some diversity by randomly changing the order of the output by adding the mix parameter:

  itemz { begin_with_general: 'das Telefon verfügt über ein Display mit', separator:',', last_separator:'und', mix:true }

Das OnePlus 5T: ein tolles Telefon! Die verfügbaren Töne des Telefons sind Schwarz, Rot und Weiß. Das Telefon verfügt über ein Display mit einem Bildschirm-zu-Körper-Verhältnis von 80,43% und einer physischen Größe von 6 Zoll.
Das OnePlus 5 ist wirklich ein fantastisches Telefon. Die verfügbaren Farben des Telefons sind Gold und Grau. Das Telefon verfügt über ein Display mit einer physischen Größe von 5,5 Zoll und einem Bildschirm-zu-Körper-Verhältnis von 72,93%.
Ich liebe das neue OnePlus 3T. Die verfügbaren Farbtöne des Telefons sind Schwarz, Gold und Grau. Das Telefon verfügt über ein Display mit einem Bildschirm-zu-Körper-Verhältnis von 73,15% und einer physischen Größe von 5,5 Zoll.

Even more variety

First let’s add text some about the battery:

| das Telefon hat einen Akku von #[+value(phone.battery)] mAh

Now we have a decent volume of text. But we would like to have more variability: we always talk about colors, the display, and the battery, in this order, but it could be in any order. Let’s put all our text chunks in an itemz > item structure, and add a mix:

mixin phone_chunks
  itemz {separator: '.', end:'.', mix:true}
    item
      | #[+colors]
    item
      | #[+display]
    item
      | das Telefon hat einen Akku von #[+value(phone.battery)] mAh

mixin phone
  | #[+intro] .
  | #[+phone_chunks]

Referring expressions

There is a hidden structure behind the way we talk about the phone :

  • The first time we talk about it we use the name of the phone.

  • The next times we use das Telefon. But we also could say dieses Telefon, das Handy, or die Gurke (and also use pronouns er sie es).

This is called referring expressions in NLG. The first time we talk about something we use its representant representation and after we use the referring expression representation. We want RosaeNLG to care for that automatically.

Let’s create 2 mixins, one for each kind of representant:

mixin phone_ref(obj, params)
  | das #{obj.name}

mixin phone_refexpr(obj, params)
  | #[+syn('das Telefon', 'dieses Telefon', 'das Handy', 'es')]
The first parameter, obj, is the phone itself. {obj.name} is exactly the same as {phone.name}.

We also have to register them:

- let phone;
each phoneElt in phones
  - phone = phoneElt;

  p
    -
      phone.ref = 'phone_ref';
      phone.refexpr = 'phone_refexpr';
    | #[+phone]

Now we can use them (almost) everywhere:

      | #[+value(phone)] ist wirklich ein fantastisches Telefon.

      | #[+value(phone)] : ein tolles Telefon !

      | #[+value(phone)] hat einen Akku von #[+value(phone.battery)] mAh

We have to change the structure for the das Telefon verfügt über ein Display mit, as we cannot put a value directly in the begin_with_general structure. It has to be a string or a mixin:

mixin itHasADisplay
  | #[+value(phone)] verfügt über ein Display mit
...
  itemz { begin_with_general: 'itHasADisplay', separator:',', last_separator:'und', mix:true }

This is what you should get:
Das OnePlus 5T: ein tolles Telefon! Dieses Telefon verfügt über ein Display mit einem Bildschirm-zu-Körper-Verhältnis von 80,43% und einer physischen Größe von 6 Zoll. Es hat einen Akku von 3 300 mAh. Die verfügbaren Töne des Telefons sind Schwarz, Rot und Weiß.
Ich liebe das neue OnePlus 5. Das OnePlus 5 hat einen Akku von 3 300 mAh. Das Handy verfügt über ein Display mit einem Bildschirm-zu-Körper-Verhältnis von 72,93% und einer physischen Größe von 5,5 Zoll. Die verfügbaren Töne des Telefons sind Gold und Grau.
Das OnePlus 3T: ein tolles Telefon! Die verfügbaren Farben des Telefons sind Schwarz, Gold und Grau. Das Telefon verfügt über ein Display mit einer physischen Größe von 5,5 Zoll und einem Bildschirm-zu-Körper-Verhältnis von 73,15%. Es hat einen Akku von 3 400 mAh.

It’s pretty decent, but there are 3 issues (depending on the triggered synonyms combination):

  1. We have not been able to replace ich liebe das neue #{phone.name}, because the article (das) is already present before the neue adjective. It’s not a big issue, but this may issue in these texts: Ich liebe das neue OnePlus 5. Das OnePlus 5 hat einen Akku von 3 300 mAh.

  2. We have not been able to replace des Telefons because it’s genitive case.

There are various more or less sophisticated approaches to solve the first issue. A first direct approach is to use parameters and conditions.

The second issue will be solved quite later in the tutorial.

Conditional texts

We could use different techniques to address the first issue, but a pretty straightforward solution is just to remove das when the representant is called in this specific context.

Let’s add a flag when calling the value of phone: we just don’t want the determiner to be present:

      | ich liebe das neue #[+value(phone, {'NO_DET':true})].

Now we have to catch this flag in our representant mixin:

  if !hasFlag(params,'NO_DET')
    | das
    |
  | #{obj.name}
The empty line containing only a pipe | forces the insertion of a space. If you don’t put it, you will have dasOnePlus. It is difficult to anticipate those cases - simply add a | in an empty line when it happens.

Generate the texts and you should see that the very first issue is solved: Ich liebe das neue OnePlus 5. Die Gurke hat einen Akku von 3 300 mAh.

Fancier sentences and "has said"

Let’s generate a fancier sentence combining display size and battery capacity: Dieses Telefon verfügt über ein Display mit einer physischen Größe von 5,5 Zoll und einem Bildschirm-zu-Körper-Verhältnis von 73,15% sowie einem Akku von 3 400 mAh.

This is quite straightforward:

| #[+display]
| sowie einem Akku von #[+value(phone.battery)] mAh

The problem is, we don’t want to talk about the battery twice. We could just remove the standard battery sentence (Das Handy hat einen Akku von 3 300 mAh), but let’s try to trigger the battery sentence only if we have not talked about the battery before. This is where hasSaid and recordSaid come in.

    item
      | #[+display]
      if !hasSaid('BATTERY')
        | sowie einem Akku von #[+value(phone.battery)] mAh
        recordSaid('BATTERY')

    item
      if !hasSaid('BATTERY')
        | #[+value(phone)] hat einen Akku von #[+value(phone.battery)] mAh
        recordSaid('BATTERY')

The pattern hasSaid/recordSaid pattern, here used twice, is the following: if we haven’t talked about something:

  1. We talk about it

  2. We record that we talked about it

You must use these built-in mechanisms and not rely on your own variables or hashmaps that you would set along text generation, as RosaeNLG goes back and forth in the text rendering.
You also need a deleteSaid('BATTERY') in the main loop, as we must talk of the battery for each phone.

You should get those nice sentences:
Das OnePlus 5T ist wirklich ein fantastisches Telefon. Die verfügbaren Farbtöne des Telefons sind Schwarz, Rot und Weiß. Dieses Telefon hat einen Akku von 3 300 mAh. Das Telefon verfügt über ein Display mit einem Bildschirm-zu-Körper-Verhältnis von 80,43% und einer physischen Größe von 6 Zoll.
Das OnePlus 5 ist wirklich ein fantastisches Telefon. Die verfügbaren Töne des Telefons sind Gold und Grau. Das Handy hat einen Akku von 3 300 mAh. Das Handy verfügt über ein Display mit einer physischen Größe von 5,5 Zoll und einem Bildschirm-zu-Körper-Verhältnis von 72,93%.
Ich liebe das neue OnePlus 3T. Die verfügbaren Farbtöne des Telefons sind Schwarz, Gold und Grau. Die Gurke verfügt über ein Display mit einem Bildschirm-zu-Körper-Verhältnis von 73,15% und einer physischen Größe von 5,5 Zoll sowie einem Akku von 3 400 mAh.

Even more referential expressions

We can generate das Telefon, dieses Telefon and das Handy as referential expressions. Let’s add die Gurke or diese Gurke.

It is easy to add synonyms in the list:

  synz
    syn
      | das Telefon
    syn
      | dieses Telefon
    syn
      | das Handy
    syn
      | die Gurke
    syn
      | diese Gurke
    syn
      | es
Replace the +syn function by a synz > syn structur for readibily, and also to add more stuff in each alternative - as we will do.

The issue is, die Gurke is feminine! You will end up with texts like Die Gurke hat einen Akku von 3 400 mAh. Es verfügt über ein Display…​ which are not correct. We need to have sie instead of es. More generally, we need yo be able to follow the current gender of the referential expression.

Explicit gender

A first method is to explicitely indicate the gender with setRefGender. setRefGender indicated RosaeNLG the current gender of the object:

    syn
      | das Telefon
      - setRefGender(phone, 'N');
    syn
      | dieses Telefon
      - setRefGender(phone, 'N');
    syn
      | das Handy
      - setRefGender(phone, 'N');
    syn
      | die Gurke
      - setRefGender(phone, 'F');
    syn
      | diese Gurke
      - setRefGender(phone, 'F');

We can then get the current gender of the referential expression with getRefGender:

    syn
      if getRefGender(phone)=='F'
        | sie
      else
        | es

We should also explicitely set the gender of the representant:

mixin phone_ref(obj, params)
  if !hasFlag(params,'NO_DET')
    | das
    |
  | #{obj.name}
  - setRefGender(phone, 'N');

Now the gender agreements are respected: Diese Gurke hat einen Akku von 3 400 mAh. Sie verfügt über ein Display…​.

The getRefGender structure trigerring er sie es is very classic and there is a shortcut:

    syn
      | #{getMorF(['er', 'sie', 'es'], phone)}

Gender via dictionnary

Setting the gender manually is error prone. It is better to use RosaeNLG’s integrated dictionnary (derived from german-pos-dict):

    syn
      | dieses Telefon
      - setRefGender(phone, 'Telefon');
    syn
      | das Handy
      - setRefGender(phone, 'Handy');

There is a syntactic shortcut to:

  1. write a word

  2. look for its gender in the dictionnary

  3. save its current gender

    syn
      | das #[+value('Telefon', {represents: phone})]
    syn
      | dieses #[+value('Telefon', {represents: phone})]
    syn
      | das #[+value('Handy', {represents: phone})]
    syn
      | die #[+value('Gurke', {represents: phone})]
    syn
      | diese #[+value('Gurke', {represents: phone})]
    syn
      | #{getMorF(['er', 'sie', 'es'], phone)}

Automatic determiner

Let’s generate automatically the determiner. diese/dieses are demonstrative pronous, das/die are definite articles:

    syn
      | #[+value('Telefon', {represents: phone, det: 'DEFINITE'})]
    syn
      | #[+value('Telefon', {represents: phone, det: 'DEMONSTRATIVE'})]
    syn
      | #[+value('Handy', {represents: phone, det: 'DEFINITE'})]
    syn
      | #[+value('Gurke', {represents: phone, det: 'DEFINITE'})]
    syn
      | #[+value('Gurke', {represents: phone, det: 'DEMONSTRATIVE'})]

It is not essential, but we can easily mutualize alternatives using syn_fct. syn_fct is a function that randomly chooses an element from table:

    syn
      | #[+value('Telefon', {represents: phone, det: syn_fct(['DEFINITE','DEMONSTRATIVE'])})]
    syn
      | #[+value('Handy', {represents: phone, det: syn_fct(['DEFINITE','DEMONSTRATIVE'])})]
    syn
      | #[+value('Gurke', {represents: phone, det: syn_fct(['DEFINITE','DEMONSTRATIVE'])})]

Genitive case

At last, let’s solve our second issue. We were not able to replace des Telefons because it’s genitive case. RosaeNLG is able to manage cases in German (well, some of it at least) - but it needs to know both the current gender and the case. Current gender has been coped with just before, let’s adress the case.

First, we must explicitely indicate that the case is genitive:

  | die verfügbaren #[+syn('Farben', 'Farbtöne', 'Töne')]
  | #[+value(phone, {case:'GENITIVE'})]
  | sind

Second, we must convey this case param in the value mixin:

    syn
      | #[+value('Telefon', {represents: phone, det: syn_fct(['DEFINITE','DEMONSTRATIVE']), case: getFlagValue(params, 'case')})]
    syn
      | #[+value('Handy', {represents: phone, det: syn_fct(['DEFINITE','DEMONSTRATIVE']), case: getFlagValue(params, 'case')})]
    syn
      | #[+value('Gurke', {represents: phone, det: syn_fct(['DEFINITE','DEMONSTRATIVE']), case: getFlagValue(params, 'case')})]

getFlagValue is a shortcut to params!=null && params.case!=null ? params.case : null. The syntax is still heavy - we’ll improve that later.

You will not have those kind of texts: Die verfügbaren Farbtöne dieser Gurke, Die verfügbaren Farben des Handys. It is quite good, but you also get Die verfügbaren Farbtöne sie sind Schwarz, Rot und Weiß. We should not trigger the pronoun representation here.

Let’s solve it with a flag as before:

  | #[+value(phone, {'NOT_ES':true, case:'GENITIVE'})]

and

    syn
      if !hasFlag(params, 'NOT_ES')
        | #{getMorF(['er', 'sie', 'es'], phone)}
When an empty synonym is triggered (which can happen here), RosaeNLG will just choose another one.

Syntax improvements

When our phone_ref and phone_refexpr mixins receive information in params, they sometimes use it (like for NOT_ES) or simply transmit it to value (like for case).

Some javascript magic will help. addToParams is a shortcut that completes the params parameter (it has to have that exact name) with more properties:

mixin phone_ref(obj, params)
  - var det = hasFlag(params,'NO_DET') ? null : 'DEFINITE';
  | #[+value(obj.name, addToParams({represents: phone, det: det, gender:'N'}))]

mixin valueProdukt(word, det, params)
  | #[+value(word, addToParams({represents: phone, 'det': det}))]

mixin phone_refexpr(obj, params)
  synz
    syn
      if !hasFlag(params, 'NOT_ES')
        | #{getMorF(['er', 'sie', 'es'], phone)}
    syn
      | #[+valueProdukt('Gurke', syn_fct(['DEFINITE','DEMONSTRATIVE']), params)]
    syn
      | #[+valueProdukt('Handy', syn_fct(['DEFINITE','DEMONSTRATIVE']), params)]
    syn
      | #[+valueProdukt('Telefon', syn_fct(['DEFINITE','DEMONSTRATIVE']), params)]

Simplified syntax

It can be tedious to split nominal groups in determiner, adjective and noun. Use the simplified syntax with <…​>:

    syn
      if !hasFlag(params, 'NOT_ES')
        | #{getMorF(['er', 'sie', 'es'], phone)}
    syn
      | #[+value('<die Gurke>', addToParams({represents: phone}))]
    syn
      | #[+value('<diese Gurke>', addToParams({represents: phone}))]
    syn
      | #[+value('<die Handy>', addToParams({represents: phone}))]
    syn
      | #[+value('<diese Handy>', addToParams({represents: phone}))]
    syn
      | #[+value('<die Telefon>', addToParams({represents: phone}))]
    syn
      | #[+value('<diese Telefon>', addToParams({represents: phone}))]

RosaeNLG will automatically choose the right article.

Do not worry about the gender when using <…​>. <der Handy>, <die Handy>, <das Handy> are equivalent.
The simplified syntax does not work in a browser as it would require too many embedded linguistic resources.

Change synonym mode

We sometimes have this kind of output: Dieses Telefon hat einen Akku von 3 300 mAh. Dieses Telefon verfügt über ein Display mit einem Bildschirm-zu-Körper-Verhältnis von 72,93% und einer physischen Größe von 5,5 Zoll. Die verfügbaren Farbtöne dieses Telefons sind Gold und Grau.

We have 3 times Telefon here which is not perfect. How could we avoid that?

Instead of choosing synonyms randomly, we can just trigger them in sequence. This will avoid close repetitions:

mixin phone_refexpr(obj, params)
  synz {mode:'sequence'}
    syn
      ...

Now we should have less repetitions in our synonyms for the phone.

Congratulations!

Sincere Congratulations! 🎆

Even more

We have gone through some aspects of NLG with this tutorial.

There are some other features you can explore, like verbs (we didn’t need to make them dynamic in the tutorial), or numbers and dates formatting.

Final version of the code

tuto.js

const rosaenlgPug = require('rosaenlg');
let phones = [
  {
    name: 'OnePlus 5T',
    colors: ['Black', 'Red', 'White'],
    displaySize: 6,
    screenRatio: 80.43,
    battery: 3300,
  },
  {
    name: 'OnePlus 5',
    colors: ['Gold', 'Gray'],
    displaySize: 5.5,
    screenRatio: 72.93,
    battery: 3300,
  },
  {
    name: 'OnePlus 3T',
    colors: ['Black', 'Gold', 'Gray'],
    displaySize: 5.5,
    screenRatio: 73.15,
    battery: 3400,
  },
];
const res = rosaenlgPug.renderFile('tuto.pug', {
  language: 'de_DE',
  phones: phones,
  cache: true,
});
console.log(res);

tuto.pug

//- tag::displayMixin[]
mixin display
  itemz { begin_with_general: 'itHasADisplay', separator:',', last_separator:'und', mix:true }
    item
      | einer physischen Größe von #[+value(phone.displaySize)] Zoll
    item
      | einem Bildschirm-zu-Körper-Verhältnis von #[+value(phone.screenRatio)]%
//- end::displayMixin[]


mixin colors
  | die verfügbaren #[+syn('Farben', 'Farbtöne', 'Töne')]
  | #[+value(phone, {'NOT_ES':true, case:'GENITIVE'})]
  | sind

  eachz color in phone.colors with { separator:',', last_separator:'und', end:'.' }
    -
      var colorMapping = {
        'Black': 'Schwarz',
        'Red': 'Rot',
        'White': 'Weiß',
        'Gold': 'Gold',
        'Gray': 'Grau'
      }
    | #{colorMapping[color]}

//- tag::introMixin[]
mixin intro
  synz
    syn
      | #[+value(phone)] ist wirklich ein fantastisches Telefon.
    syn
      | ich liebe das neue #[+value(phone, {'NO_DET':true})].
    syn
      | #[+value(phone)] : ein tolles Telefon !
//- end::introMixin[]

//- tag::mixinItHasADisplay[]
mixin itHasADisplay
  | #[+value(phone)] verfügt über ein Display mit
//- end::mixinItHasADisplay[]

mixin phone_chunks
  itemz {separator: '.', end:'.', mix:true}
    item
      | #[+colors]

    //- tag::hasSaid[]
    item
      | #[+display]
      if !hasSaid('BATTERY')
        | sowie einem Akku von #[+value(phone.battery)] mAh
        recordSaid('BATTERY')

    item
      if !hasSaid('BATTERY')
        | #[+value(phone)] hat einen Akku von #[+value(phone.battery)] mAh
        recordSaid('BATTERY')
    //- end::hasSaid[]

mixin phone_ref(obj, params)
  - var det = hasFlag(params,'NO_DET') ? null : 'DEFINITE';
  | #[+value(obj.name, addToParams({represents: phone, det: det, gender:'N'}))]

mixin valueProdukt(word, det, params)
  | #[+value(word, addToParams({represents: phone, 'det': det}))]

mixin phone_refexpr(obj, params)
  synz {mode:'sequence'}
    syn
      if !hasFlag(params, 'NOT_ES')
        | #{getMorF(['er', 'sie', 'es'], phone)}
    syn
      | #[+value('<die Gurke>', addToParams({represents: phone}))]
    syn
      | #[+value('<diese Gurke>', addToParams({represents: phone}))]
    syn
      | #[+value('<die Handy>', addToParams({represents: phone}))]
    syn
      | #[+value('<diese Handy>', addToParams({represents: phone}))]
    syn
      | #[+value('<die Telefon>', addToParams({represents: phone}))]
    syn
      | #[+value('<diese Telefon>', addToParams({represents: phone}))]

//- tag::phoneMixin[]
mixin phone
  | #[+intro] .
  | #[+phone_chunks]
//- end::phoneMixin[]

//- tag::main[]
- let phone;
each phoneElt in phones
  - phone = phoneElt;

  p
    -
      phone.ref = 'phone_ref';
      phone.refexpr = 'phone_refexpr';
    | #[+phone]
    deleteSaid('BATTERY')
//- end::main[]