Multilanguage Shiny App - Advanced Cases

Appsilon’s package shiny.i18n is a real gem when it comes to supporting multilanguage UI in Shiny apps or even static reports. The original post with the shiny.i18n introduction is a helpful resource, however it may be too simplistic or even misleading for complex cases.

In this article I provide a summary of my approach for translating modularized Shiny apps and propose several useful techniques. I am using a recent demo app for clinical trials planning. The live demo is hosted via shinyapps and the source code is also available.

1. Add UI “language”

Even though shiny.i18n supports direct translations, I have created an additional ui “language” that contains all possible character strings. This idea is not new at all, however it provides a much better overview of the application text. Also, this approach can effectively handle scenarios when the same text should be translated differently in different contexts. For example, buttons tend to be more restrictive in terms of space compared to text fields. It can happen that the same word in one language should be abbreviated or translated fully in different parts of the application. UI “language” in CSV dictionary is able to solve such issues.

Example of English…

ui|en
ui_nav_page_main|Intro
ui_nav_app_be|BE Sample size
ui_nav_app_rand|Randomisation list
ui_nav_page_help|Help / Feedback
ui_app_common_rcodepanel|Reproducible R code
ui_app_common_envirpanel|Environment version
ui_app_common_outpanel|Output
ui_mainpage_lang|Select UI language
ui_mainpage_loading|Loading...
ui_mainpage_readmefile|Readme_en.html

…and Russian translations

ui|ru
ui_nav_page_main|Главная
ui_nav_app_be|Размер выборки БЭ
ui_nav_app_rand|Рандомизационный лист
ui_nav_page_help|Помощь / Обратная связь
ui_app_common_rcodepanel|Код на R
ui_app_common_envirpanel|Версия рабочей среды
ui_app_common_outpanel|Результат
ui_mainpage_lang|Язык интерфейса
ui_mainpage_loading|Загрузка...
ui_mainpage_readmefile|Readme_ru.html

2. Translate UI

By default UI elements are translated via the i18n$t() function. The only difference from the original Appsilon’s article is that the UI “language” variable is used instead of the human-readable text:

tabPanel(i18n$t("ui_nav_page_main"),
         fluidPage() )

3. update_lang() vs UI redraw

Not all UI components are correctly translated after a language change. For example, RadioButtons or DropdownLists should be redrawn via the respective update functions. Consider the following example:

updateRadioButtons(session, "DESIGN",
                   label = i18n_r()$t("ui_BE_input_design"),

choices = setNames(c("parallel","2x2","2x2x4"),
          i18n_r()$t(c("ui_BE_input_2x1","ui_BE_input_2x2","ui_BE_input_2x2x4"))), selected=input$DESIGN )

Label of the RadioButtons can be easily updated using the default approach: i18n$t(“ui_BE_input_design”). However, individual choices require “manual” update and that’s why the whole UI element should be properly redrawn. Please note that the i18n_r()$t() function supports more than one input parameter. In case several lines of text should be translated, it is possible to pass them all at once: i18n_r()$t(c(“ui_BE_input_2x1”,“ui_BE_input_2x2”,“ui_BE_input_2x2x4”))

4. Pieces of text with dynamic variables

Some text elements may contain dynamic parts. For example, T/R ratio for bioequivalence trials should fall within 80%-125% or 90%-111% range depending on the settings. Hence, if this condition is violated, the app throws a validate error like this one: “Please provide T/R within 0.8 and 1.25”.

It would be impractical to split such text into several smaller pieces. Moreover, in different languages the location of the dynamic parts can be different. That’s why the dictionary should support such nuances. Please consider the following translation settings:

ui|en 
ui_BE_validate_tr|Please provide T/R within %s and %s

here %s parts are dynamic text pieces that can be passed to the whole string via sprintf(). We know that this sentence always has two numbers, but we would like to have some translation flexibility and simplicity at the same time. Translated text can be different, but the R code is the same for all languages:

validate(
need(input$RATIO > theta1_ & input$RATIO < theta2_,
     as.character(sprintf(i18n_r()$t("ui_BE_validate_tr"),
                          as.character(theta1_),
                          as.character(theta2_) ))
))

5. Translating in-app documentation

Finally, some language-specific text can be too complex to be stored in a dictionary. Consider the readme page example: the page is long and the text is formatted. In this case the dictionary can have a link to the documentation

ui|en 
ui_mainpage_readmefile|Readme_en.html

When the user changes the language, the old readme html page is replaced with the new one:

removeUI(selector ="#readmediv", immediate = TRUE)

insertUI(immediate = TRUE, selector = '#readmehere', session=session,
         ui = div(id="readmediv", includeHTML(
              as.character(i18n$get_translations()["ui_mainpage_readmefile",input$lang_pick])
                                             )
                 )
        )

Known issues and limitations

There are still several small issues in my example app.

At First I tried to call the translator only once within the global.R file. After some testing it became obvious, that the global translator object is shared between multiple users that run the same R session. If user A updates the language, User B can experience a nasty surprise when UI elements suddenly switch the language. That’s why I had to define the translator in both ui.R and server.R files causing some duplications.

Second issue popped out when trying to properly redraw UI elements inside a Shiny module. There were several examples online, but for me passing a reactive value with a new language did not work. I had to pass both the translator object plus the language selection.

SERVER_BE_samplesize("BE_samplesize",
                     i18n_r = reactive(i18n),
                     lang = reactive(input$lang_pick)
)

Finally, when updating the documentation from #5, I had to use a peculiar line inside observeEvent(input$lang_pick, { }):

i18n$get_translations()["ui_mainpage_readmefile",input$lang_pick]

The thing is that language picker reactive value is updated earlier than the update_lang() call. So, I had to retrieve the correct filename from the translator object before the language was actually updated.

I hope this article provides useful example of how the shiny.i18n package can be used in real-life applications. Please let me know your thoughts in the comments on LinkedIn :)

Here are some other posts:

A truly static blogdown website
A truly static blogdown website

How to create and deploy a custom static Hugo site with blogdown R package.

2021-05-30 · 8 mins

Moving to Hugo
Moving to Hugo

Finally got some free time to learn blogdown package. Here is a story of converting my website from Wordpress to Hugo.

2021-05-29 · 2 mins

Website TODO list
Website TODO list

A roadmap for writing new posts and developing new features of the stats-consult website.

2021-03-01 · 2 mins