Skip to content

Appendix E: Handlebars Usage in Formbird

This document is currently being updated

1 Purpose

Handlebars is a templating language which is compatible with and can be used within a Formbird template.

The pupose of this document is to provide:

  1. An overview of the Handlebars templating language (refer Section 2)
  2. A description of how the Handlebars templating language can be used within a Formbird template (refer Section 3).
  3. Examples of Handlebars usage within a Formbird template (refer Section 4).
  4. Links to resources and further reading on the Handlebars templating language (refer Section 5).

2 Handlebars Templating Language Overview

The basic unit of the Handlebars templating language is the Handlebars expression.

Handlebars expressions are wrapped in double curly braces i.e. {{ some expression }}.

Expressions tell Handlebars to look up and use the value of a field or to execute helper functions.

Helpers allow you to execute simple logic, loop through arrays, manipulate data and more. Handlebars provides a number of built-in helpers such as the if conditional and the each iterator. You can also define your own helpers.

2.1 Basic Expressions

The simplest Handlebars expression is a field name wrapped in double curly braces which tells Handlebars to look up the field in the current context and use its value. When a Handlebars expression is written within a Formbird template, the current context will be the current Formbird document.

The Handlebars expression {{lastName}} means look up the lastName field in the current context and use its value. If the lastName field is not found then no value will be used.

Note: When using Handlebars in a Formbird template, field names within a Handlebars expression should be prefixed with "document." e.g. {{document.lastName}} . Hence in Formbird, the Handlebars expression {{document.lastName}} means look up the lastName field in the current Formbird document and use its value.

How it Works

graph TD A("FORMBIRD TEMPLATE<br/>&quot;summaryNameRule&quot;: &quot;Contact - {{document.firstName}} {{document.lastName}}&quot;") -->|Handlebars<br/>Compiler| B("function()") C("CURRENT FORMBIRD DOCUMENT<br/>&quot;firstName&quot;: &quot;James&quot &quot;<br/>lastName&quot;: &quot;McAllan&quot;") -->|"James<br/>McAllan"| B B-->|On execution|D("CURRENT FORMBIRD DOCUMENT<br/>&quot;summaryName&quot;: &quot;Contact - James McAllan&quot;<br/>&quot;firstName&quot;: &quot;James&quot;<br/>&quot;lastName&quot;: &quot;McAllan&quot;")

In the above diagram:

  1. A Formbird Template contains a "summaryNameRule" field whose value is defined as a concatenation of the text ''contact - '' and two Handlebars expressions separated by a space i.e.
    "summaryNameRule": "Contact - {{document.firstName}} {{document.lastName}}".

  2. The current Formbird document contains the fields
    "firstName": "James",
    "lastName": "McAllan"

  3. Handlebars takes the Handlebars expressions from the Formbird template and compiles them into a function.

  4. This function then executes, setting the values of Handlebars expression fields to the values of the corresponding fields in the current Formbird document.

  5. On execution of the function, the current Formbird document will have the "summaryName" field as shown below:

    "summaryName": "Contact - James McAllan".

2.2 Path Expressions

Handlebars expressions can also be dot-separated paths.

Example:

{{document.address.streetName}} means lookup the address field in the current Formbird document, then look up the streetName field in the result and use its value.

2.3 Block Expressions

In Handlebars, blocks are expressions that open with {{# }} and close with {{/ }}. Each of the Handlebars built-in helpers described in Section 2.4 below are examples of block expressions.

2.4 Built-In Helper Expressions

Handlebars provides a number of built-in helpers including the if, each, unless, and with block helpers. These built-in helpers provide the ability to execute simple logic.

Most built-in helpers are block expressions i.e. they open with {{# }} and close with {{/ }}. For example the if block helper opens with {{#if}} and closes with {{/if}}.

A Handlebars helper may be followed by parameters (separated by a space), as shown below:

{{#if parameter1 parameter2 ...}} Your content here {{/if}}

2.4.1 if Block Helper

The if block helper is used to conditionally execute a block expression. If the condition returns a truthy value, Handlebars will execute the if block. If the condition returns a falsy value, Handlebars will not execute the if block. The if block helper can also include an {{else}} section in order to define how to execute a falsy value.

Valid Falsy values in Handlebars are false, undefined, null, "", and [] where the latter two are the empty string and empty array.

Example 1:

A Formbird Template contains the Handlebars if block helper shown below:

{{#if document.countries}}Countries are present.{{else}}Countries are not present.{{/if}}

The current Formbird document contains the countries field shown below:

"Countries": [
  "Australia",
  "New Zealand",
  "France"
]

On execution, the if block helper would evaluate the non-empty "countries" array as a truthy value and return the result:

Countries are present.

Example 2:

A Formbird Template contains the Handlebars if block helper shown below:

{{#if document.countries}}Countries are present.{{else}}Countries are not present.{{/if}}

The current Formbird document contains the countries field shown below:

"Countries": []

On execution the if block helper would evaluate the empty "countries" array as a falsy value and return the result:

Countries are not present.

Example 3:

A Formbird Template contains the Handlebars if block helper shown below:

{{#if document.countries}}{{document.countries.[0]}}{{/if}}

The current Formbird document contains the countries field shown below:

"countries": [
  "Australia",
  "New Zealand",
  "France"
]

On execution the if block helper would evaluate the non-empty "countries" array as a truthy value and return the first value of the countries array, i.e. return:

Australia

2.4.2 unless Block Helper

The unlessblock helper is similar to the if block helper in that it is used to conditionally executes a block expression, except it operates on the falsy value i.e. the unless block helper is the inverse of the if block helper. If the condition returns a falsy value, Handlebars will execute the unless block. If the condition returns a truthy value, Handlebars will not execute the unless block. The unless block helper can also include an {{else}} section in order to define how to evaluate a truthy value.

Falsy values in Handlebars are false, undefined, null, "", and [] where the latter two are the empty string and empty array.

Example 1:

A Formbird Template contains the Handlebars unless block helper shown below:

{{#unless document.countries}}Countries are not present.{{else}}Countries are present.{{/unless}}

The current Formbird document contains the countries field shown below:

"countries": []

On execution, the unless block helper would evaluate the empty array as a falsy value and return the result:

Countries are not present.

Example 2:

A Formbird Template contains the Handlebars if block helper shown below:

{{#unless document.countries}}Countries are not present.{{else}}Countries are present.{{/unless}}

The current Formbird document contains the countries field shown below:

"countries": [
  "Australia",
  "New Zealand",
  "France"
]

On execution. the unless block helper would evaluate the non-empty array to a truthy value and return the result:

Countries are present.

2.4.3 each Block Helper

The each block helper is used to iterate over an array of items.

You can use {{this}} inside the each block to reference the array item being iterated over.

The following parameters can be used to provide additional information while iterating:

  • The {{@index}} parameter can be used inside the each block to access the current loop index. This index begins at 0 and increments by 1 with each iteration.
  • The {{@first}} parameter provides a boolean for implementing different logic for the first item when iterating over an array or a list of items.
  • The {{@last}} parameter provides a boolean for implementing different logic for the last item when iterating over an array or a list of items.

Example 1:

A Formbird Template contains the Handlebars each block helper shown below:

{{#each document.countries}}{{@index}}: {{this}};{{/each}}

The current Formbird document contains the countries field shown below:

"countries": [
  "Australia",
  "New Zealand",
  "France"
]

On execution, the each block helper would iterate over each value of the countriesarray and return the result:

0: Australia; 1: New Zealand; 2: France;

Example 2:

A Formbird Template contains a Handlebars each block helper shown below:

{{#each document.names}}
  {{#unless @last}}
    Name: {{document.firstName}} {{document.lastName}};
  {{else}}
    Name: {{document.firstName}} {{documemnt.lastName}}
  {{/unless}}
{{/each}}

The current Formbird document contains the names field shown below:

"names" : [
  {"firstName":"Mary","lastName":"McAllan"},
  {"firstName":"Reginald","lastName":"Katting"},
  {"firstName":"Emily","lastName":"Frousicker"},
  {"firstName":"Lily-Ann","lastName":"Arnott"}
]

On execution, the each block helper will iterate over each item of the names array and return the result:

Name: Mary McAllan; Name: Reginald Katting; Name: Emily Frousicker; Name: Lily-Ann Arnott

Note the unless block helper and the @last parameter were used to suffix each element of the array, except the last, with a semi-colon.

2.4.4 with Block Helper

The {{with}} block helper is similar to the if block helper in that it is used to conditionally executes a block expression , with one key difference.

Normally Handlebars first takes the Handlebars expressions from the Formbird template and compiles them into a function before evaluating the resulting function against the current Formbird document. Whereas the with block helper allows immediate access the current Formbird document.

If the condition returns a truthy value, Handlebars will immediately execute the with block. If the condition returns a falsy value, Handlebars will not execute the with block. The with block helper can also include an {{else}} section in order to define how to evaluate a falsy value.

Example:

A Formbird Template contains the Handlebars with block helper shown below:

{{#with document.author}}Written by {{document.firstName}} {{document.lastName}}{{else}}Unknown Author{{/with}}

The current Formbird document contains the author field shown below:

"author": {
    "firstName": "Lily-Ann",
    "lastName": "Arnott"
  }

The with block helper would immediately access the current Formbird document and return the result:

Written By Lily-Ann Arnott

3 Handlebars Usage in Formbird

Often the best and simplest way of deliveriing a particular requirement is via a Handlebars expression written within a Formbird template. For example:

  1. As shown above, the basic Handlebars expression {{document.lastName}} is an instruction to look up and use the value of the lastName field in the current Formbird document.
  2. The Handlebars built-in helpers described in Section 2.4 or Handlebars helpers defined by you can be used to incorporate logic within a Formbird template.

3.1 Use the value of a field in its current Formbird context

Generally the purpose of a Handlebars expression in a Formbird template is to look up and use the value of a field in its current Formbird context, which can be:

  • A field in the current Formbird document.
  • A field in the current Formbird document template.
  • A field in the current user's Formbird account document.
  • A field in the current Formbird component definition.
  • A field in the currently selected result returned by a Formbird filter query.
  • The total count of results returned by a Formbird filter query.
  • The array index of the currently selected result returned by a Formbird filter query.
  • The full set of results returned by a Formbird filter query.

When used within a Formbird template, the Handlebars expression format for each of the above Formbird contexts is shown in the table below.

Formbird Context Handlebars Expression Description Examples
Formbird document {{document.field name}} Look up and use the value of a field in the current Formbird document. {{document.lastName}}
Formbird document template {{tpl.field name}} Look up and use the value of a field in the current Formbird document template. {{tpl.documentId}}
Formbird component definition {{tplItem.field name}} Look up and use the value of a field in the current Formbird component definition. {{tplItem.name}}
Formbird account document ((account.field name)) Look up and use the value of a field in the current user's Formbird account document. {{account.systemHeader.summaryName}}
Formbird filter query result {{result.field name}} Look up and use the value of a field in the currently selected result returned by a Formbird filter query. {{result.pipe_dia}}
{{result.systemHeader.summaryName}}
Formbird filter query result {{resultsCount}} The total count of results returned by a Formbird filter query. {{resultsCount}}
Formbird filter query result {{resultsIndex}} The array index of the currently selected result returned by a Formbird filter query. {{resultsIndex}}
Formbird filter query result {{results}} The full set of results returned by a Formbird filter query.

For example a sc-selected-asset component with a tooltip field defined as:

"tooltips": "Displaying Pipes of type {{document.filterPipes}} Pipe #{{resultsIndex}} of {{resultsCount}} size {{result.pipe_dia}} mm <a href='/form/{{result.documentId}}' target='_blank'>{{result.systemHeader.summaryName}}</a> Current User {{account.systemHeader.summaryName}} viewing {{tplItem.name}}"

would generate and display a tooltip field as shown below:

4 Examples

Example 1

Requirement

In this example, the Formbird document is required to:

  1. Have its summaryName field value set to the concatenation of "Property - " with the values of each its street address fields i.e. its unit_No, streetNo, street, suburb and postcode fields.
  2. Handle street addresses that don't have a unit_No or a streetNo value so that the unit_No and " - " and streetNo is skipped.

Solution:

Define Formbird template to include the summaryNameRule field shown below:

"summaryNameRule": "Property - {{#with addressGeo.features.[0].properties}}{{#if unit_No}}{{unit_No}} - {{/if}}{{#if streetNo}}{{streetNo}} {{/if}}{{street}}, {{suburb}}, {{postcode}}{{/with}}"


Result

If the current Formbird Document contained the fields show below:

{
    "systemHeader": {
        "systemType": "document",
        "templateId": "cd02a730-6b11-11e9-98c0-e92fbfb6142f",
        "keyIds": [
         ],
        "createdDate": "2019-07-11T07:53:04.028Z",
        "createdBy": "65be85d0-66b8-11e7-b9f2-8fcf6a56c9da",
        "serverUpdatedDate": "2019-07-11T07:53:04.028Z",
        "serverCreatedDate": "2019-07-11T07:53:04.028Z",
        "versionId": "e99a35c0-a3b0-11e9-b25d-1d82576c71d1",
        "currentVersion": true,
    },
    "documentId": "e915ea92-a3b0-11e9-b25d-1d82576c71d1",
    "addressGeo": {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "properties": {
                    "type": "street",
                    "unit_No": "",
                    "streetNo": "600",
                    "street": "SNEYDES ROAD",
                    "suburb": "WERRIBEE",
                    "postcode": "3030",
                    "state": "VIC"
                },
                "geometry": {
                    "type": "Point",
                    "coordinates": [
                        144.68618641755836,
                        -37.897783153444955
                    ]
                }
            }
        ]
    },
    "status": "Active"
}

Then the with block helper would immediately access its street address fields and and include the summaryName field shown below in the current Formbird document:

"summaryName": "Property - 600 SNEYDES ROAD WERRIBEE, 3030"

5 Handlebars in Datatables

5.1 Context Reference in Datatables Filter

The 'filter' query can reference data from the document context by using handlebars. Example:

"{'query':{'bool':{'filter':[{'term':{'foreignField':'{{document.localField}}']}}}"

In the above example document.localField refers to a field present on the document. This field may be saved to the document, or present 'dynamically' by the user entering data in another field 5.3 Watching Fields For Changes
Data available in the context includes
document - data from the document account - data from the current logged in user account * tpl - data from the template

5.1.1 Context Reference in Datatables Columns

Use the 'cellTemplate' definition in the column to display fields. Example:

"cellTemplate":"<div class='boldFont'>{{row.myField}}</div>"

Data available in the context includes
document - data from the document account - data from the current logged in user account tpl - data from the template row - data from the document returned from the datatable filter

An important note for sorting and filtering on columns using cellTemplate: The sort and filter is done on the data defined by the 'field' NOT by the data returned by the cellTemplate function.

5.2 Handlebars functions in Datatables

In addition to simply referencing fields, functions to manipulate data can also be specified and data passed from the context to the function. To achieve this you can use formbird block helper handlebars functions as above or you can specify your own function(s). Example:

"handlebarsHelpers": [
    {
        "name": "setPriority",
        "function": "function(priority) { 
            return priority === 1 ? `<div class='redFont'>Urgent</div>` : `<div class='blueFont'>Normal</div>` 
        }"
    }
]

The example above can of course be achieved inline with block helpers, but you will notice that this is standard javascript / html. The above example would be called from a column definition using 'cellTemplate' where the function is specified followed by function variables. Example:

"cellTemplate":"{{{setPriority row.priority}}}"

Note the triple braces used for handlebars to prevent HTML escaping.
Another example would be concatenating street address fields to display a complete address rather than single data fields per column

"cellTemplate":"{{{getAddress row.myAddressField}}}"
---------------------------------------------------
"handlebarsHelpers": [
    {
        "name": "getAddress",
        "function": "function(arg) { 
            let address = ''; 
            if(arg?.features?.length){ 
                let prop = arg.features[0].properties;
                let unitNo = prop.unitNo ? prop.unitNo : '';
                let streetNo = prop.streetNo ? prop.streetNo : '';
                let street = prop.street ? prop.street : ''; 
                let suburb = prop.suburb ? prop.suburb : ''; 
                address = `${unitNo} ${streetNo} ${street} ${suburb}`;
            } 
            return address; 
        }"
    }
]

Code should be entered inline (no carriage returns).
Data returned from the function must be a string. This can present issues when using a function to alter a filter query. Take the example:

"{'query':{'bool':{'filter':[ {{{checkField document.myField}}} ]}}}"

The checkField function has to return a string, but it's difficult to use strings to manipulate the query, it's easier to use objects. Additionally if the function above doesn't return any value, then the query will return every document which isn't desirable.

"handlebarsHelpers": [
    {
        "name": "checkField",
        "function": "function(arg) { 
            let term = {'term':{'defaultKey':'defaultValue'}}; 
            if(arg){ 
                term = {'term':{'hasMyField': arg }} 
            } 
            return JSON.stringify(term); 
        }"
    }
]

The function above sets up a 'default' query term and if an argument is passed to the function the default term is changed to a query term for the argument passed. The term is then passed back to the filter 'stringified' as a string.

3.3 Watching Fields For Changes

The examples so far have used values saved to the document. We can 'watch' fields in the context for changes and pass these changes to handlebars to be used in the filter.

{
    "componentName": "sc-radio-list",
    "name": "filterGrid",
    "label": "Type",
    "fullWidth": true,
    "radioList": [
        "All",
        "Assets",
        "Components",
        "Work Orders",
        "Requests",
        "Reports",
        "Email Templates",
        "Configuration Import"
    ],
    "disableSave": true
}
"updateWatchFields": [
    "document.filterGrid"
],

This example will watch for changes in the sc-radio-list component 'filterGrid'. When the field changes, the handlebars function 'getType' is executed (the example is shown with line breaks for clarity):

"filter": "{ 'query':{'bool':{'filter':[{{{getType document.filterGrid}}}]}}}"
 "handlebarsHelpers": [
    {
        "name": "getType",
        "function": "function(arg){ 
            var terms = [{'term':{'systemHeader.systemType':'template'}},{'term':{'appTags':'ramFleet'}}]; 
            if(arg){
                var terms = {"All":[{'systemHeader.systemType':'template'},{'appTags':'ramFleet'}],
                            "Assets":[{'appTags':'asset'},{'systemHeader.systemType':'template'},{'appTags':'ramFleet'}],
                            "Components":[{'appTags':'ramFleet'},{'appTags':'assetComponent'},{'systemHeader.systemType':'template'}],
                            "Work Orders":[{'appTags':'workOrder'},{'systemHeader.systemType':'template'},{'appTags':'ramFleet'}],
                            "Requests":[{'appTags':'request'},{'systemHeader.systemType':'template'},{'appTags':'ramFleet'}],
                            "Reports":[{'appTags':'formbirdReports'},{'systemHeader.systemType':'document'}],
                            "Email Templates":[{'appTags':'email_template'},{'systemHeader.systemType':'document'}],
                            "Configuration Import":[{'appTags':'ramFleet'},{'appTags':'csvImport'}]
                };
                terms = terms[arg].map( f => { return {'term':f } } );
            }
            var term = JSON.stringify(terms).replace('[','').replace(']','');
            console.log(term);
            return term;
        }"
    }
]

5.4 Putting it all together

The example below displays a list of templates that can be filtered by a radio list. The filter calls the 'getType' function on the first document load and whenever the 'filterGrid' radio list changes (updateWatchFields). On the first document load the function returns a default list of all templates. When the radio list is changed, the option is passed to the function which uses an object lookup to find the correct terms. The correct terms are then stringified and returned.
The cellTemplate in column definition 3 call constructs a table from the 'appTags' array field.
The href field in column 2 gets the documentId of the row and appends the template editor overlay to the result.

        {
            "componentName": "sc-radio-list",
            "name": "filterGrid",
            "label": "Type",
            "fullWidth": true,
            "radioList": [
                "All",
                "Assets",
                "Components",
                "Work Orders",
                "Requests",
                "Reports",
                "Email Templates",
                "Configuration Import"
            ],
            "disableSave": true
        },
        {
            "componentName": "sc-datatables",
            "detail": "",
            "filter": "{ 'query':{'bool':{'filter':[ {{{getType document.filterGrid}}} ]}}}",
            "fullWidth": true,
            "gridColumns": [
                {
                    "displayName": "Name",
                    "field": "systemHeader.summaryName",
                    "width": 2,
                    "urlOpenIn": "newWindow",
                    "href": "form/{{{row.documentId}}}",
                    "type": "url",
                    "sort": {
                        "direction": "asc",
                        "precedence": 1
                    }
                },
                {
                    "displayName": "Edit Link",
                    "field": "documentId",
                    "width": 2,
                    "urlOpenIn": "newWindow",
                    "href": "form/{{{row.documentId}}}/74746c80-8378-11e6-99b1-71ee944cf59f",
                    "type": "url"
                },
                {
                    "displayName": "appTags",
                    "field": "appTags",
                    "width": 2,
                    "cellTemplate": "{{{makeTable row.appTags}}}"
                }
            ],
            "label": "All Templates",
            "name": "gridCam",
            "rowsPerPage": 500,
            "showReload": true,
            "columnSearch": true,
            "disableSave": true,
            "disableAutoDisplay": true,
            "showHeadings": true,
            "updateWatchFields": [
                "document.filterGrid"
            ],
            "handlebarsHelpers": [
                {
                    "name": "getType",
                    "function": "function(arg){ var terms = [{'term':{'systemHeader.systemType':'template'}},{'term':{'appTags':'ramFleet'}}]; if(arg){ var terms = {"All":[{'systemHeader.systemType':'template'},{'appTags':'ramFleet'}], "Assets":[{'appTags':'asset'},{'systemHeader.systemType':'template'},{'appTags':'ramFleet'}], "Components":[{'appTags':'ramFleet'},{'appTags':'assetComponent'},{'systemHeader.systemType':'template'}], "Work Orders":[{'appTags':'workOrder'},{'systemHeader.systemType':'template'},{'appTags':'ramFleet'}], "Requests":[{'appTags':'request'},{'systemHeader.systemType':'template'},{'appTags':'ramFleet'}], "Reports":[{'appTags':'formbirdReports'},{'systemHeader.systemType':'document'}], "Email Templates":[{'appTags':'email_template'},{'systemHeader.systemType':'document'}], "Configuration Import":[{'appTags':'ramFleet'},{'appTags':'csvImport'}] }; terms = terms[arg].map( f => { return {'term':f } } ); } var term = JSON.stringify(terms).replace('[','').replace(']',''); console.log(term); return term; }"
                },
                {
                    "name":"makeTable",
                    "function": "function(arg){let table=`<table><tbody>`; if(arg?.length){arg.forEach(f => table +=`<tr><td>${f}</td></tr>`)}; table+=`</tbody></table>`;return table }"
                }
            ]
        },

6 Using Handlebars in sc-static-html

Handlebars can be used in the sc-static-html component by using the parameter "useFormbirdHandlebars": true
Execute functions in the html definition and configure handlebarsHelpers array functions as per sc-datatables examples.

{
    "componentName": "sc-static-html",
    "fullWidth": true,
    "html": "<div class='intrayNameClass'>Intrays: {{getAccountNames account.intraysRel}}<div>",
    "name": "intrayNameHtml",
    "useFormbirdHandlebars": true,
    "handlebarsHelpers": [
        {
            "name": "getAccountNames",
            "function": "function(arg) { if(arg?.length){let str = arg.map(function(item){return item.name }); return str.toString().replace(/,/g,' / ') }}"
        }
    ]
}

7 Using Handlebars in Mapping Components

Handlebars functions can be used in sc-address-map, sc-street-address and sc-selected-asset
The setup is largely the same as with sc-datatables in terms of specifying functions (handlebarsHelpers), watching fields (updateWatchFields), passing parameters etc. The main exception is that any query only returns data necessary for the component (not the complete document) - 'additional' fields need to be specified by defining an 'additionalQueryFields' array.

"additionalQueryFields": [
    "type",
    "appTags",
    "systemType"
],

This is defined either in the layer (for that query), or at the root of the component (for the main query).
All filters and most of the formatting options accept handlebars functions. Refer to the 'layers' documentation.

8 Client Script Overlay

Editing handlebars functions inline can be difficult once the function becomes more than a couple of lines long. To assist in editing handlebars functions you can use the Client Script Overlay template by appending /916d66b0-7dda-11ea-a1d9-53ac176796aa to your template. Use the dropdown to choose the field/function to edit the function. Note the function must exist on the template first, so it's recommended to set up the structure first and then edit using the overlay. Example :

"handlebarsHelpers": [
        {
            "name": "myFunction",
            "function": "function(arg) { }"
        }
    ]

IMPORTANT: Do not edit the template using the template editor while editing using the Client Script Overlay (or vice versa) without first reloading the page to ensure the changes have been refreshed.

9 Resources and Further Reading

Listed below are a few more resources and articles if you wish to dig deeper into the Handlebars templating language: