This post was featured on the R Weekly highlights podcast hosted by Eric Nantz and Mike Thomas.
In November I’ll give a talk about multilingualism in R at the Spanish R conference in Barcelona (😍). I can’t wait! Until then, I need to prepare my talk. 😅 I plan to present the rOpenSci “multilingual publishing” project but also other related tools, like potools. In this post, I’ll walk you through a minimal example of using potools to translate messages in an R package!
What is potools?
In R, you can provide messages in different languages by using .po, .pot and .mo files. If you provide messages in English and their translation to Spanish, an user who sets the environment variable LANGUAGE
to es will get to see Spanish messages rather than the default English messages.
The potools package by Michael Chirico is the roxygen2 of .po, .pot and .mo files: as you could well write Rd files by hand1, you could write the translation files by hand… but things are easier with potools.
Create package
I create a package called pockage, with my usual usethis workflow. It initially has a single function, that tries to extract the user’s name via the whoami package, and then says hello using the cli package:
#' Say hello
#'
#' @return Nothing
#' @export
#'
#' @examples
#' speak()
speak <- function() {
name <- whoami::fullname(fallback = "user")
cli::cli_alert_info("Hello {name}!")
}
Let’s try it out:
pockage::speak()
#> ℹ Hello Maëlle Salmon!
Install potools
I install potools from GitHub using pak::pak("MichaelChirico/potools")
.
Register potools style in DESCRIPTION
I shall use potools explicit style, which means it will recognize strings as translatable only if I mark them as so. Therefore I add these lines to DESCRIPTION
:
Config/potools/style: explicit
Create tr_()
function and use it
Following potools’ vignette for developers, I run usethis::use_r("utils-potools")
and paste the definition an internal function in that new file:
tr_ <- function(...) {
enc2utf8(gettext(paste0(...), domain = "R-pockage"))
}
I then modify the speak()
function:
#' Say hello
#'
#' @return Nothing
#' @export
#'
#' @examples
#' speak()
speak <- function() {
name <- whoami::fullname(fallback = "user")
cli::cli_alert_info(tr_("Hello {name}!"))
}
The difference is that the string “Hello {name}!” is now marked as translatable.
Create the general translation file
I run potools::po_extract()
to create the po/R-pockage.pot
file.
Create the translation file for French and fill it
Then I run potools::po_create("fr")
to create the file holding the translation of the string to French.
I obtain this po/R-fr.po
file where I edit two lines (Last-Translator, and msgstr at the bottom):
msgid ""
msgstr ""
"Project-Id-Version: pockage 0.0.0.9000\n"
"POT-Creation-Date: 2023-10-06 10:45+0200\n"
"PO-Revision-Date: 2023-10-06 10:33+0200\n"
"Last-Translator: Malle Salmon\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=ASCII\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: mensaje.R:9
msgid "user"
msgstr "utilisateur·rice"
#: mensaje.R:10
msgid "Hello {name}!"
msgstr "Salut {name} !"
Compile the translation
I compile with potools::po_compile()
which creates the .mo
binary for the French language in inst/po
.
Try out the translation
I load or install&load the package, then I use the following code, magically using withr
to set the LANGUAGE
environment variable locally:
withr::with_language("fr", pockage::speak())
#> ℹ Salut Maëlle Salmon !
# as opposed to
pockage::speak()
#> ℹ Salut Maëlle Salmon !
Quite neat, right?
Add languages
I then add two other languages, unstoppable as I am now, by running potools::po_create(c("es", "ca"))
. I add the Spanish and Catalan translations of the string in respectively po/R-es.po
and po/R-ca.po
. I compile with potools::po_compile()
which creates the .mo
binary for the Spanish and Catalan languages in inst/po
.
After installing the pockage package again, I get:
withr::with_language("fr", pockage::speak())
#> ℹ Salut Maëlle Salmon !
withr::with_language("es", pockage::speak())
#> ℹ Hola Maëlle Salmon!
# I swear it's pronounced differently
withr::with_language("ca", pockage::speak())
#> ℹ Hola Maëlle Salmon!
Translate more strings
Now imagine I also want to translate the fallback of the whoami function call, for the case whoami can’t identify the user.
#' Say hello
#'
#' @return Nothing
#' @export
#'
#' @examples
#' speak()
speak <- function() {
name <- whoami::fullname(fallback = tr_("user"))
cli::cli_alert_info(tr_("Hello {name}!"))
}
I run
potools::po_extract()
again;- then
potools::po_update()
; - after which I need to go add a msgstr under
msgid "user"
inpo/R-fr.po
,po/R-es.po
,po/R-ca.po
; - finally I run
potools::po_compile()
.
Conclusion
Find my final pockage package on GitHub.
To translate messages with potools in the explicit style one needs to:
- register the potools style in
DESCRIPTION
; - create and use a
tr_()
internal function to wrap strings to be translated; - run
potools::po_extract()
at the beginning of the translation efforts and every time strings wrapped intr_()
are changed, deleted, added; - run
potools::po_create()
once per non-default language to be supported; - run
potools::po_update()
afterpotools::po_extract()
every time strings wrapped intr_()
are changed, deleted, added; - run
potools::po_compile()
every time the translation source files are changed.
potools has one vignette for developers and one for translators that I’d recommend reading, because they provide useful advice beyond the basic workflow that I illustrated here.
-
which is what I first learnt to do years and years ago. ↩︎