Multilingual Quarto Reports: How to Internationalize Quarto Text with {shiny.i18n}
When presenting to global markets, documents, research papers, and other text-based resources need to move beyond being unilingual. It is the age of internationalization or multilingual text. And sure, translation services are dime a dozen but it helps when vetted translations are available from the get-go. Since the release of Quarto, the excitement for making documents using markdown has skyrocketed and we are happy to declare that {shiny.i18n}, the internationalization package from Appsilon, is already compatible with Quarto documents. Through this tutorial, you will learn two things: <ol><li>Integrating {shiny.i18n} with Quarto.</li><li>How to scramble an egg in 10 languages! 🍳</li></ol> Let’s crack a few eggs! Mise en place: <ul><li><a href="#quarto">Firing up Quarto🔥</a></li><li><a href="#shinyi18n">Loading {shiny.i18n}🔖</a></li><li><a href="#prep">Prepping translations🌐</a></li></ul> Cooking with Quarto and {shiny.i18n}: <ul><li><a href="#syntax">Whipping up the syntax for string translations 🥣</a></li><li><a href="#coddle">Coddling the code 🧽</a></li><li><a href="#translate">All set: Let's translate Quarto🪧</a></li><li><a href="#garnish">Garnish and serve: Quarto internationalization with {shiny.i18n}🍽️</a></li></ul> <hr /> <h2 id="quarto">Firing up Quarto🔥</h2> Before we begin, we need to make sure that Quarto is set up and working properly. There is a myriad of ways to use Quarto and more can be found on the <a href="https://quarto.org/docs/get-started/" target="_blank" rel="nofollow noopener">Get Started</a> page by Posit. Once set up, you can continue reading. Also, let’s make sure you have the package installed. Installing it is as easy as install.packages(“shiny.i18n”). Once that is done, you should be able to use the package. In this tutorial, we will be creating a Quarto document that looks like the screenshot below (eggcellent Quarto i18n report). In other words, let’s create a document that helps people scramble an egg in 10 languages! <img class="alignnone size-full wp-image-17648" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01c10051092b3b54180ca_shiny-i18n-Quarto-internationalization-example.webp" alt="Scrambled egg recipe example of a internationalized Quarto report with shiny.i18n" width="1040" height="1262" /> Let’s begin making the document by setting up a .qmd file. The beginning is usually pre-filled but if it isn’t or if it is the default text, you should change it to look like the following: <pre><code> --- title: "How To Scramble Eggs in 10 Languages?" --- <br>Here is how you can scramble an egg in ten languages. </code></pre> Now, for the image. On the internet, we have a lot of resources and so, we can easily choose an image from Pixabay, Unsplash, or Pexels, provided we give credit to the original source. Let’s take <a href="https://pixabay.com/photos/scrambled-eggs-eggs-breakfast-food-6582990/" target="_blank" rel="nofollow noopener">this</a> image from Pixabay and call it <code>eggs.jpg</code>. We have to ensure that the file is in the same directory as our .qmd document. Once it’s there, all we have to do is call it with standard markdown syntax. <pre><code> --- title: "How To Scramble Eggs in 10 Languages?" --- <br>Here is how you can scramble an egg in ten languages. <br>![](eggs.jpg){width=100%} <br>_Image:_ [_Pixabay_](https://pixabay.com/photos/scrambled-eggs-eggs-breakfast-food-6582990/) </code></pre> The document should look something like the first image above. The '_' symbol is used to italicize or emphasize text in markdown. <h2 id="shinyi18n">Loading {shiny.i18n} 🔖</h2> To set up and load the package, you need to call the package and then initialize a Translator object. <pre><code> library(shiny.i18n) i18n <- Translator$new(translation_json_path = "translation.json") </code></pre> To make sure Quarto knows it is R code, you need to either wrap it in an inline r call or an {r} code chunk. With that, the code above should look as follows. <pre><code> ```{r include=FALSE} library(shiny.i18n) <br>i18n <- Translator$new(translation_json_path = "translation.json") ``` </code></pre> The three backticks tell Quarto to execute this as R code. Also, similar to the .Rmd format, you can provide options such as echo and include. You can read more about these in the <a href="https://quarto.org/docs/computations/execution-options.html" target="_blank" rel="nofollow noopener">official documentation</a>. <h2 id="prep">Prepping translations 🌐</h2> The code above sets up a Translator object using a file called translations.json, but it is not set up yet. To set it up, we will need to create a file in the same directory with the following format. There is a key-value mechanism to switch between languages through a Translator object for {shiny.i18n}. The first method is to leverage the first language as both a language and a key by using the following setup in our file. This would look something like the following code: <pre><code> { "cultural_date_format": "%d-%m-%Y", "languages": [ "en", "es", "fr" ], "translation": [ { "en": "ENGLISH_TEXT", "es": "SPANISH_TEXT", "fr": "FRENCH_TEXT" } ] } </code></pre> Here, the English “en” serves both as the default key and a language. The second method is more flexible where we set up a specific key for each string. This looks closer to the following code chunk. <pre><code> { "cultural_date_format": "%d-%m-%Y", "languages": [ "key", "en", "es", "fr" ], "translation": [ { "key": "key_name", "en": "ENGLISH_TEXT", "es": "SPANISH_TEXT", "fr": "FRENCH_TEXT" } ] } </code></pre> Since {shiny.i18n} works by mapping the keys to the values and switching them as per the selected language, the key works as a good, easy-to-remember alternative. Otherwise, there is an ambiguity to remember the correct phrases in the first language, whichever it may be. 👉 A good rule of thumb to follow is “use a key if the number of strings is more than three.” <h3>Adding things in 🧂</h3> If you are within an RStudio environment, you can also access our Addin which is automatically available after installing and activating the package. To access it, go to Tools -> Addins -> Browse Addins... and select the {shiny.i18n} Addin which will detect all places where $t or $translate is used. Note that doing this also essentially reverses the order of steps. In that case, setting up a translation file comes later. <h3>Keeping it simple 👍</h3> For now, however, we are going to set up the translations.json manually. It will look something like the following. <pre><code> { "cultural_date_format": "%d-%m-%Y", "languages": [ "key", "en", "es", "fr", "de", "it", "pt", "ja", "sv", "pl", "hi" ], "translation": [ { "key": "language_name", "en": "English", "es": "Spanish", "fr": "French", "de": "German", "it": "Italian", "pt": "Portuguese", "ja": "Japanese", "sv": "Swedish", "pl": "Polish", "hi": "Hindi" }, { "key": "method", "en": "Crack the egg into a bowl and beat it with a fork. Heat a pan over medium heat and add a small amount of butter or oil. Pour the beaten egg into the pan and stir constantly until the egg is fully cooked and no longer liquid.", "es": "Rompe un huevo en un tazón y bátelo con un tenedor. Calienta una sartén a fuego medio y agrega una pequeña cantidad de mantequilla o aceite. Vierte el huevo batido en la sartén y revuelve constantemente hasta que el huevo esté completamente cocido y ya no sea líquido.", "fr": "Cassez un œuf dans un bol et battez-le avec une fourchette. Faites chauffer une poêle à feu moyen et ajoutez une petite quantité de beurre ou d'huile. Versez l'œuf battu dans la poêle et mélangez constamment jusqu'à ce que l'œuf soit complètement cuit et ne soit plus liquide.", "de": "Schlage ein Ei in einer Schüssel auf und verquirle es mit einer Gabel. Erhitze eine kleine Menge Butter oder ein wenig Öl in einer Pfanne bei mittlerer Hitze und gieße das aufgeschlagene Ei in die Pfanne. Rühre solange bis das Ei stockt wird und nicht mehr flüssig ist.", "it": "Rompi un uovo in una ciotola e sbattilo con una forchetta. Scalda una padella a fuoco medio e aggiungi una piccola quantità di burro o olio. Versa l'uovo sbattuto nella padella e mescola costantemente finché l'uovo non è completamente cotto e non è più liquido.", "pt": "Quebre o ovo em uma tigela e bata com um garfo. Aqueça uma panela em fogo médio e adicione uma pequena quantidade de manteiga ou óleo. Despeje o ovo batido na panela e mexa constantemente até que o ovo esteja totalmente cozido e não mais líquido.", "ja": "ボウルに卵を割り入れ、箸で溶きほぐす。フライパンを中火に熱して、少量バター又はオイルを入れる。卵を入れ、完全に焼けるまで混ぜ続ける。", "sv": "Knäck ägget i en skål och vispa det med en gaffel. Hetta upp en panna på medelvärme och tillsätt en liten mängd smör eller olja. Häll det uppvispade ägget i pannan och rör hela tiden tills ägget är helt genomkokt och inte längre flytande.", "pl": "Rozbij jajko do miski i roztrzep je za pomocą widelca. Rozgrzej patelnię na średnim ogniu i dodaj niewielką ilość masła lub oleju. Wlej roztrzepane jajko na patelnię i mieszaj ciągle aż jajko całkowicie się zetnie i nie będzie już płynne.", "hi": "एक बोल में एक अंडा तोड़कर फोर्क से मिलाएँ। मध्यम ऊर्जा पर पैन गरम करें और थोड़ा मक्खन या तेल डालें। मिलाये हुवे अंडे को पैन में डालें और अंडा पूरी तरह रह से पकने तक हिलाएँ।" } ] } </code></pre> If this is set up correctly and the Translator object can find it, it will be initialized without any errors. <h2 id="syntax">Whipping up the syntax for string translations 🥣</h2> The syntax to translate the strings is important and once we get that down, the rest is just a repetition of steps with a different language each time. <pre><code> translator_object$set_translation_language(LANGUAGE) translator_object$t(KEY) </code></pre> Let’s replace LANGUAGE and KEY with "en" and "method" respectively. Our translator object is called i18n, but you can call it anything when you initialize it. <pre><code> i18n$set_translation_language("en") i18n$t("method") </code></pre> 🦾 <b>Pro tip: </b>In markdown (both .rmd & .qmd), if we want to use inline R code, instead of creating a chunk, we can simply call it within a single pair of backticks. The syntax would be as follows. <pre><code> `r { CODE HERE }` </code></pre> The key is to keep the code inside curly braces and it should work seamlessly. <h2 id="coddle">Coddling the code 🧽</h2> Sometimes a little extra time on one action improves results in the end. If we are to include everything in one line, it makes sense to create a function that does two things: <ol><li>set a translation language</li><li>give a translation</li></ol> The following function is called <code>get_translation()</code>: <pre><code> get_translation <- function(translator_object, language_code, content_key) { translator_object$set_translation_language(language_code) translator_object$t(content_key) } </code></pre> ✅If we take a look at our .qmd file now, it should look as follows. If you’ve followed all the steps, give yourself a pat on the back. <pre><code> --- title: "How To Scramble Eggs in 10 Languages?" --- <br>Here is how you can scramble an egg in ten languages. <br>![](eggs.jpg){width=100%} <br>_Image:_ [_Pixabay_](https://pixabay.com/photos/scrambled-eggs-eggs-breakfast-food-6582990/) <br>```{r include=FALSE} library(shiny.i18n) <br>i18n <- Translator$new(translation_json_path = "translation.json") <br>get_translation <- function(translator_object, language_code, content_key) { translator_object$set_translation_language(language_code) translator_object$t(content_key) } <br>``` </code></pre> The include=FALSE parameter ensures that our code is not visible in the document, yet executes automatically. If an explicit evaluation of the code is desired, one can add the eval=TRUE parameter, though this is typically not required. <h2 id="translate">All set: Let's translate Quarto🪧</h2> The heavy lifting is done. Now, we just need to call the get_translation() function with the correct language name in our document. The syntax for this would look like the following: <pre><code> ## `r { get_translation(i18n, "en", "language_name") }` <br>`r { get_translation(i18n, "en", "method") }` </code></pre> Here, the first line sets up an H2 element, denoted by the two # symbols. The second line is a simple sentence or a p element. Doing this for every language would look like this: <pre><code> ## `r { get_translation(i18n, "en", "language_name") }` <br>`r { get_translation(i18n, "en", "method") }` <br>## `r { get_translation(i18n, "es", "language_name") }` <br>`r { get_translation(i18n, "es", "method") }` <br>## `r { get_translation(i18n, "fr", "language_name") }` <br>`r { get_translation(i18n, "fr", "method") }` <br>## `r { get_translation(i18n, "de", "language_name") }` <br>`r { get_translation(i18n, "de", "method") }` <br>## `r { get_translation(i18n, "it", "language_name") }` <br>`r { get_translation(i18n, "it", "method") }` <br>## `r { get_translation(i18n, "zh", "language_name") }` <br>`r { get_translation(i18n, "zh", "method") }` <br>## `r { get_translation(i18n, "ja", "language_name") }` <br>`r { get_translation(i18n, "ja", "method") }` <br>## `r { get_translation(i18n, "ko", "language_name") }` <br>`r { get_translation(i18n, "ko", "method") }` <br>## `r { get_translation(i18n, "pl", "language_name") }` <br>`r { get_translation(i18n, "pl", "method") }` <br>## `r { get_translation(i18n, "hi", "language_name") }` <br>`r { get_translation(i18n, "hi", "method") }` </code></pre> Now, let’s wrap it up by combining everything, and then we can serve it to the public. 😋 <h2 id="garnish">Garnish and serve: Quarto internationalization with {shiny.i18n}🍽️</h2> The final file, with all the translations, should look exactly like the following. Once you have verified it, you can simply click Render or render the Quarto document through the command line and that’s it! <pre><code> --- title: "How To Scramble Eggs in 10 Languages?" --- <br>Here is how you can scramble an egg in ten languages. <br>![](eggs.jpg){width=100%} <br>_Image:_ [_Pixabay_](https://pixabay.com/photos/scrambled-eggs-eggs-breakfast-food-6582990/) <br>```{r include=FALSE} library(shiny.i18n) <br>i18n <- Translator$new(translation_json_path = "translation.json") <br>get_translation <- function(translator_object, language_code, content_key) { translator_object$set_translation_language(language_code) translator_object$t(content_key) } <br>``` <br>## `r { get_translation(i18n, "en", "language_name") }` <br>`r { get_translation(i18n, "en", "method") }` <br>## `r { get_translation(i18n, "es", "language_name") }` <br>`r { get_translation(i18n, "es", "method") }` <br>## `r { get_translation(i18n, "fr", "language_name") }` <br>`r { get_translation(i18n, "fr", "method") }` <br>## `r { get_translation(i18n, "de", "language_name") }` <br>`r { get_translation(i18n, "de", "method") }` <br>## `r { get_translation(i18n, "it", "language_name") }` <br>`r { get_translation(i18n, "it", "method") }` <br>## `r { get_translation(i18n, "zh", "language_name") }` <br>`r { get_translation(i18n, "zh", "method") }` <br>## `r { get_translation(i18n, "ja", "language_name") }` <br>`r { get_translation(i18n, "ja", "method") }` <br>## `r { get_translation(i18n, "ko", "language_name") }` <br>`r { get_translation(i18n, "ko", "method") }` <br>## `r { get_translation(i18n, "pl", "language_name") }` <br>`r { get_translation(i18n, "pl", "method") }` <br>## `r { get_translation(i18n, "hi", "language_name") }` <br>`r { get_translation(i18n, "hi", "method") }` </code></pre> <h2>Summing up internationalization in Quarto documents with {shiny.i18n}</h2> In this tutorial, we have set up a Quarto document while leveraging {shiny.i18n} for adding internationalization. After completion, the Quarto document should render successfully and appear similar to the article here. Stay tuned to the Appsilon blog for our next Quarto tutorial. In the next blog in this series, we will dive into the interactive side of Quarto and how to set it up with a dropdown for language control. Bon appetit!