This tutorial describes how to create a internationalised questionnaire - a questionnaire that can be taken in multiple languages - in psychTestR. Internationalised tests are more complicated to construct than monolingual tests, but they are essential for international studies.


The first step is to put together a data file defining items and answers in the different languages that will be used in your test. This will be used to define a dictionary object (i18n_dict) that psychTestR will use to find a given translation from a given key. For example, the key colour might return “What’s your favourite colour?” if the language is English, or “Quelle est ta couleur préférée?” if the language is French.

The precise form of this data file will depend on the type of questionnaire you want to construct, and what kind of response options are available. Here we will illustrate the creation of a simple questionnaire where each item has a textual cue and two textual response options.

Let’s begin by creating an R file within which to design your questionnaire. If you’re new to writing R code, we recommend you install RStudio, and create a new project (File > New Project), within which to save your file.

At the beginning of your script, write the following to load psychTestR and the tibble package:

If you haven’t got them already, you can install these packages at the R terminal as follows:

install.packages(c("devtools", "tibble"))

Now we’ll define a simple table that defines a questionnaire in our two languages. Ordinarily you would define this table in an external csv file, and read it in using read.csv or readr::read_csv, but here we will define it within our R code. This table takes the form of a tibble object, as created by the tribble function (see ?tibble::tibble for information), but you could also use a data.frame object.

definition <- tribble(
  ~item_number, ~key_type,   ~key,     ~en,                  ~fr,
  1,            "question",  "colour", "Favourite colour?",  "Couleur préférée?",
  1,            "answer_1",  "red",    "Red",                "Rouge",
  1,            "answer_2",  "green",  "Green",              "Vert",
  2,            "question",  "number", "Favourite number?",  "Nombre préféré?",
  2,            "answer_1",  "7",      "Seven",              "Sept",
  2,            "answer_2",  "8",      "Eight",              "Huit"

Each row defines a particular question or answer. The item_number column tells us the current item number; each item has several rows, corresponding to its different textual components. The key_type column tells us whether the current row describes a question ("question"), the first answer ("answer_1"), or the second answer ("answer_2"). The key column gives a succint identifier for that piece of text, which should be unique within the questionnaire. The en column gives the text in English, the fr column in French.

We will first use this table to create a psychTestR dictionary. This is done as follows:

dict <- i18n_dict$new(definition[, c("key", "en", "fr")])

Here we wrote definition[, c("key", "en", "fr")], which takes the original definition tibble and only keeps the three columns key, en, and fr, to match the required input of i18n_dict$new (see i18n for details. In general, i18n_dict$new() requires an input data.frame or tibble where each row corresponds to a text element, the first column is a unique identifier termed key, and the remaining columns provide translations into different languages, specified by capitalised ISO 639-2 country codes.

Next, we will use the definition object to construct our test logic. The general idea is that we will iterate over each item number, collect the rows corresponding to that item number, and define a test page from this information. Here I’m going to use utility functions from the tidyverse packages - if you’re not familiar with these packages, I’d certainly recommend them. If you haven’t already installed the tidyverse, you can install it with install.packages("tidyverse").


questionnaire <- new_timeline(
    definition %>% 
      select(item_number, key_type, key) %>% 
      spread(key_type, key) %>% 
      pmap(function(item_number, answer_1, answer_2, question) {
        NAFC_page(label = question, 
                  prompt = i18n(question),
                  choices = c(answer_1, 
                  labels = c(i18n(answer_1), 
  dict = dict

Here’s a summary of what these operations do:

  • new_timeline is a psychTestR function that creates a timeline, i.e. a series of test elements. Timelines support internationalisation, in that one timeline object can define test elements in multiple languages. When making an internationalised timeline, you must pass a dictionary (such as the one we defined earlier) to the dict argument.

  • The function c combines different timelines or test elements together.

  • begin_module is a psychTestR function signifying that the test is now beginning a new module. Modules are self-contained entities that are identified by a textual label, in this case “my_questionnaire”. Modules are usually only necessary if you intend your test to be incorporated into larger test batteries; in this case, the module label helps to identify the results from your particular test component.

  • The %>% operation takes an R object on one line and ‘pipes’ it forward to the next line as the input of a function. See magrittr for details.

  • select takes the input tibble (definition) and selects the columns item_number, key_type, and key (we could have also used the [ operator, like before).

  • spread turns the tibble from long format to wide format, where each row now represents a different item.

  • pmap iterates over each row of the tibble, and calls a provided function on the elements of this row.

  • NAFC_page is a psychTestR function that defines a multiple-choice page. See the psychTestR documentation for other page types, or define your own using page.

  • i18n gets a translation for a given key from the dictionary. It can only be called within the new_timeline function. Note that we have provided the NAFC_page function with both keys and translations, allowing the test to save data in a consistent format irrespective of the participant’s language.

  • end_module signifies the end of the “my_questionnaire” module.

That’s it! You’ve defined your internationalised questionnaire. You can now run this questionnaire independently, or insert it into a longer test battery. For now, let’s try running it independently. Before we do so, we’ll add two elements to the timeline: an element to save the results to disk (using elt_save_results_to_disk) and a final_page object to terminate the test. We can then run the test using make_test; note that this must be called interactively or else placed within a call to shiny::runApp.

logic <- join(
  elt_save_results_to_disk(complete = TRUE),


When you finish the test it should automatically save results in your working directory. You can access these results by running the test and logging into the admin panel with the default password, “demo”. You can launch the test with different default languages as follows:

# Defaults to English but also supports French
psychTestR::make_test(logic, opt = demo_options(languages = c("en", "fr")))

# Defaults to French but also supports English
psychTestR::make_test(logic, opt = demo_options(languages = c("fr", "en")))

You can also select languages through the URL you use to access the test. Note however that a given participant ID (p_id) is permanently linked with a given language, so to switch languages you must ensure that the p_id component of the URL is omitted.


This internationalisation workflow is somewhat complicated to set up, but it is rather powerful. The combination of i18n_dict and i18n is very flexible, and allows internationalisation to be applied to a great variety of test designs, not just simple questionnaires.


Have you got feedback about this tutorial? Please submit it to the issues tracker.