Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More internationalization #456

Merged
merged 35 commits into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6892a6c
PoC internationalization
ColinFay Sep 30, 2020
cc14a86
Added html deps
ColinFay Oct 1, 2020
c3e0bda
libs and example
ColinFay Oct 1, 2020
70852d9
This implement the internationalization of learnr UI
ColinFay Oct 7, 2020
1888d7e
documentation of the new param
ColinFay Oct 7, 2020
f87c89e
more info in the vignette
ColinFay Oct 7, 2020
83d0854
rm language from sandbox.Rmd
ColinFay Oct 7, 2020
10464cd
html dep is written in a tempfile
ColinFay Oct 13, 2020
6463547
bootbox is now internationalized
ColinFay Oct 13, 2020
24af04b
Added a note about bootbox
ColinFay Oct 13, 2020
e4b0a99
Update R/html-dependencies.R
ColinFay Oct 19, 2020
621c9b4
Extend internationalization to expose customization to authors
gadenbuie Dec 23, 2020
8c38979
Rewrite multilang vignette
gadenbuie Dec 23, 2020
4e74ca6
Fix namespacing learnr functions in vignette/multilang
gadenbuie Dec 23, 2020
2a30ba3
Escape all unicode characters
gadenbuie Dec 23, 2020
7af49e9
Backport dQuote() with q argument
gadenbuie Dec 23, 2020
2159e63
Check submit button parent element for data-check attribute
gadenbuie Dec 28, 2020
878f5ae
Fix use of dQuote in multilang vignette
gadenbuie Dec 28, 2020
fc8dc5d
Refactor internationalization
gadenbuie Jan 12, 2021
359fa1f
Add note that only first language is used
gadenbuie Jan 12, 2021
17a2430
Add localize method to window.tutorial
gadenbuie Jan 12, 2021
5d53ccd
Rename and update sandbox demo
gadenbuie Jan 12, 2021
7db49a9
Remove unused backport of dQuote()
gadenbuie Jan 12, 2021
7ef80fc
More cleaning up of sandbox
gadenbuie Jan 12, 2021
be28f46
Add "localize" shiny message handler
gadenbuie Jan 12, 2021
b1a6044
Add translation capability to question buttons
gadenbuie Jan 12, 2021
5167298
Only localize default question button labels
gadenbuie Jan 12, 2021
4571848
Fix mime type of script tag containing json
gadenbuie Jan 13, 2021
622f9ea
Add notes about motivation in comments
gadenbuie Jan 13, 2021
a546490
Explicitly point to vignette() in language param docs
gadenbuie Jan 13, 2021
a46069f
Use arrays in i18next.t() to provide fallback and add translation for…
gadenbuie Jan 13, 2021
649d04a
Add news for #456
gadenbuie Jan 13, 2021
95748fa
Don't use "custom" namespace, instead customizations overwrite defaul…
gadenbuie Jan 15, 2021
d885520
learnr-i18n-init.js -> tutorial-i18n-init.js
gadenbuie Jan 15, 2021
2ac6219
Use ES5 compatible syntax
gadenbuie Jan 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ learnr (development version)
* Added an event handler system, with the functions `event_register_handler()` and `one_time()`. There is also a new event `"section_viewed"`, which is triggered when a new section becomes visible. ([#398](https://github.com/rstudio/learnr/pull/398))
* Previously, when a question submission was reset, it would be recorded as a `"question_submission"` event with the value `reset=TRUE`. Now it a separate event, `"reset_question_submission"`. ([#398](https://github.com/rstudio/learnr/pull/398))
* Added a new `polyglot` tutorial to learnr. This tutorial displays mixing R, python, and sql exercises. See [`run_tutorial("polyglot", "learnr")`](https://learnr-examples.shinyapps.io/polyglot) for a an example. ([#397](https://github.com/rstudio/learnr/pull/397))
* Text throughout the learnr interface can be customized or localized using the new `language` argument of `tutorial()`. Translations for English and French are provided and contributes will be welcomed. Read more about these features in `vignette("multilang", package = "learnr")`. ([#456](https://github.com/rstudio/learnr/pull/456))

## Minor new features and improvements

Expand Down
17 changes: 17 additions & 0 deletions R/html-dependencies.R
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,20 @@ ace_html_dependency <- function() {
script = "ace.js"
)
}

tutorial_i18n_html_dependency <- function(language = NULL) {
htmltools::htmlDependency(
name = "i18n",
version = "1.2.0",
src = system.file("lib/i18n", package = "learnr"),
script = c("i18next.min.js", "jquery-i18next.min.js", "tutorial-i18n-init.js"),
head = format(htmltools::tags$script(
id = "i18n-cstm-trns",
type = "application/json",
jsonlite::toJSON(
i18n_process_language_options(language),
auto_unbox = TRUE
)
))
schloerke marked this conversation as resolved.
Show resolved Hide resolved
)
}
194 changes: 194 additions & 0 deletions R/i18n.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
i18n_process_language_options <- function(language = NULL) {
# Take the language entry from the tutorial options or YAML header and process
# into the list of resources used when initializing the i18next translations

is_path_json <- function(x) {
checkmate::test_string(
x,
na.ok = FALSE,
null.ok = FALSE,
pattern = "[.]json$",
ignore.case = TRUE
)
}

if (is_path_json(language)) {
language <- i18n_read_json(language)
}

custom <- list()
if (is.null(language) || (is.character(language) && length(language) == 1)) {
## language: en
## language: fr
default <- if (!is.null(language)) language else "en"
} else {
## language
## en:
## button:
## continue: Got it!
## es:
## button:
## continue: Continuar
## fr: learnr.fr.json

if (!is.list(language) || is.null(names(language))) {
stop(
"`language` must be a single character language code or ",
"a named list of customizations indexed by language code ",
'as described in `vignette("multilang", package = "learnr")`',
call. = FALSE
)
}

# the first language in this format is the default language
default <- names(language)[1]

for (lng in names(language)) {
if (is_path_json(language[[lng]])) {
language[[lng]] <- i18n_read_json(language[[lng]])
}

language[[lng]] <- i18n_validate_customization(language[[lng]])

if (is.null(language[[lng]])) next
custom[[lng]] <- list(custom = language[[lng]])
}
}

# Get default translations and then merge in customizations
translations <- i18n_translations()
if (length(custom) > 0) {
for (lang in union(names(translations), names(custom))) {
translations[[lang]] <- c(translations[[lang]], custom[[lang]])
}
}

list(language = default, resources = translations)
}

i18n_read_json <- function(path) {
tryCatch(
jsonlite::read_json(path, simplifyDataFrame = FALSE, simplifyMatrix = FALSE),
error = function(e) {
message("Unable to read custom language JSON file at: ", path)
NULL
}
)
}

i18n_validate_customization <- function(lng) {
if (is.null(lng)) {
# NULL language items are okay, esp as the first lang (default)
return(NULL)
}

# returns a valid language customization or NULL
# always throws warnings, not errors
default <- i18n_translations()$en$translation
group_keys <- names(default)

if (!is.list(lng) || is.null(names(lng))) {
warning(
"Custom languages must be lists with entries: ",
paste(group_keys, collapse = ", "),
immediate. = TRUE
)
return(NULL)
}

# Let extra keys through for custom components but warn in case accidental
extra_group_keys <- setdiff(names(lng), group_keys)
if (length(extra_group_keys)) {
warning(
"Ignoring extra customization groups ", paste(extra_group_keys, collapse = ", "),
immediate. = TRUE
)
}

for (group in intersect(names(lng), group_keys)) {
extra_keys <- setdiff(names(lng[[group]]), names(default[[group]]))
if (length(extra_keys)) {
warning(
"Ignoring extra ", group, " language customizations: ",
paste(extra_keys, collapse = ", "),
immediate. = TRUE
)
}
}

lng
}

i18n_translations <- function() {
list(
en = list(
translation = list(
button = list(
runcode = "Run Code",
hints = "Hints",
startover = "Start Over",
continue = "Continue",
submitanswer = "Submit Answer",
previoustopic = "Previous Topic",
nexttopic = "Next Topic",
questionsubmit = "Submit Answer",
questiontryagain = "Try Again"
),
text = list(
startover = "Start Over",
areyousure = "Are you sure you want to start over? (all exercise progress will be reset)",
youmustcomplete = "You must complete the",
exercise = "exercise",
exercise_plural = "exercises",
inthissection = "in this section before continuing."
)
)
),
fr = list(
translation = list(
button = list(
runcode = "Lancer le Code",
hints = "Indice",
startover = "Recommencer",
continue = "Continuer",
submitanswer = "Soumettre",
previoustopic = "Chapitre Pr\u00e9c\u00e9dent",
nexttopic = "Chapitre Suivant",
questionsubmit = "Soumettre",
questiontryagain = "R\u00e9essayer"
),
text = list(
startover = "Recommencer",
areyousure = "\u00cates-vous certains de vouloir recommencer? (La progression sera remise \u00e0 z\u00e9ro)",
youmustcomplete = "Vous devez d'abord compl\u00e9ter",
inthissection = "de cette section avec de continuer.",
exercise = "l'exercice",
exercise_plural = "des exercices"
)
)
),
emo = list(
translation = list(
button = list(
runcode = "\U0001f3c3",
hints = "\U0001f50e",
startover = "\u23ee",
continue = "\u2705",
submitanswer = "\U0001f197",
previoustopic = "\u2b05",
nexttopic = "\u27a1",
questionsubmit = "\U0001f197",
questiontryagain = "\U0001f501"
),
text = list(
startover = "\u23ee",
areyousure = "\U0001f914",
youmustcomplete = "\u26a0 \U0001f449",
exercise = "",
exercise_plural = "",
inthissection = "."
)
)
)
)
}
30 changes: 23 additions & 7 deletions R/quiz.R
Original file line number Diff line number Diff line change
Expand Up @@ -556,34 +556,50 @@ question_module_server_impl <- function(

question_button_label <- function(question, label_type = "submit", is_valid = TRUE) {
label_type <- match.arg(label_type, c("submit", "try_again", "correct", "incorrect"))

if (label_type %in% c("correct", "incorrect")) {
# No button when answer is correct or incorrect (wrong without try again)
return(NULL)
}

button_label <- question$button_labels[[label_type]]
is_valid <- isTRUE(is_valid)

# We don't want to localize button labels that were customized by the user
# If they're default labels, we'll add the `data-i18n` attribute for localization
default_label <- eval(formals("question")[[paste0(label_type, "_button")]])
# At this point, `button_label` has been upgraded to HTML.
# Need to format() for a fair comparison
is_default_label <- identical(format(button_label), default_label)
gadenbuie marked this conversation as resolved.
Show resolved Hide resolved

default_class <- "btn-primary"
warning_class <- "btn-warning"

action_button_id <- NS(question$ids$question)("action_button")

if (label_type == "submit") {
button <- actionButton(action_button_id, button_label, class = default_class)
button <- actionButton(
action_button_id, button_label,
class = default_class,
`data-i18n` = if (is_default_label) "button.questionsubmit"
)
if (!is_valid) {
button <- disable_all_tags(button)
}
button
} else if (label_type == "try_again") {
mutate_tags(
actionButton(action_button_id, button_label, class = warning_class),
actionButton(
action_button_id, button_label,
class = warning_class,
`data-i18n` = if (is_default_label) "button.questiontryagain"
),
paste0("#", action_button_id),
function(ele) {
ele$attribs$class <- str_remove(ele$attribs$class, "\\s+btn-default")
ele
}
)
} else if (
label_type == "correct" ||
label_type == "incorrect"
) {
NULL
}
}

Expand Down
5 changes: 5 additions & 0 deletions R/tutorial-format.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
#' \code{---} to em-dashes, \code{--} to en-dashes, and \code{...} to ellipses.
#' Deprecated in \pkg{rmarkdown} v2.2.0.
#' @param ... Forward parameters to html_document
#' @param language Language or custom text of the UI elements. See
#' `vignette("multilang", package = "learnr")` for more information
#' about available options and formatting
#'
#' @export
#' @importFrom utils getFromNamespace
Expand All @@ -45,6 +48,7 @@ tutorial <- function(fig_width = 6.5,
includes = NULL,
md_extensions = NULL,
pandoc_args = NULL,
language = "en",
...) {

# base pandoc options
Expand Down Expand Up @@ -103,6 +107,7 @@ tutorial <- function(fig_width = 6.5,
tutorial_html_dependency(),
tutorial_autocompletion_html_dependency(),
tutorial_diagnostics_html_dependency(),
tutorial_i18n_html_dependency(language),
htmltools::htmlDependency(
name = "tutorial-format",
version = utils::packageVersion("learnr"),
Expand Down
1 change: 1 addition & 0 deletions inst/lib/i18n/i18n.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions inst/lib/i18n/i18next.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions inst/lib/i18n/jquery-i18next.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading