diff --git a/DESCRIPTION b/DESCRIPTION
index 9f0fbad9f..5be150f86 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,6 +1,6 @@
Package: sandpaper
Title: Create and Curate Carpentries Lessons
-Version: 0.15.0
+Version: 0.15.0.9000
Authors@R: c(
person(given = "Zhian N.",
family = "Kamvar",
@@ -44,6 +44,11 @@ Authors@R: c(
role = c("ctb"),
email = "milan.malfait94@gmail.com",
comment = c(ORCID = "0000-0001-9144-3701")),
+ person(given = "Joel H.",
+ family = "Nitta",
+ role = c("aut", "trl"),
+ email = "joelnitta@gmail.com",
+ comment = c(ORCID = "0000-0003-4719-7472")),
person())
Description: We provide tools to build a Carpentries-themed lesson repository
into an accessible standalone static website. These include local tools and
@@ -69,6 +74,7 @@ Imports:
renv (>= 0.14.0),
rprojroot,
usethis (>= 2.0.0),
+ withr,
whisker,
callr,
servr,
@@ -81,7 +87,6 @@ Suggests:
brio,
xml2,
xslt,
- withr,
jsonlite,
sessioninfo,
mockr,
@@ -90,13 +95,14 @@ Additional_repositories: https://carpentries.r-universe.dev/
Remotes:
ropensci/tinkr,
carpentries/pegboard,
- carpentries/varnish
+ carpentries/varnish@add-l10n-support
SystemRequirements: pandoc (>= 2.11.4) - https://pandoc.org
Encoding: UTF-8
LazyData: true
Config/testthat/edition: 3
Config/testthat/parallel: false
Config/Needs/check: rstudio/renv
+Config/potools/style: explicit
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.2.3
URL: https://carpentries.github.io/sandpaper/, https://github.com/carpentries/sandpaper/, https://carpentries.github.io/workbench/
diff --git a/NAMESPACE b/NAMESPACE
index 65c8bbc49..b97363e27 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -19,6 +19,7 @@ export(get_instructors)
export(get_learners)
export(get_profiles)
export(get_syllabus)
+export(known_languages)
export(manage_deps)
export(move_episode)
export(no_package_cache)
diff --git a/NEWS.md b/NEWS.md
index 968cdd6f7..fcb9248fe 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,3 +1,34 @@
+# sandpaper 0.15.0.9000 (unreleased)
+
+## NEW FEATURES
+
+* It is now possible to build lessons in languages other than English so that
+ the website elements are also localised to that language (reported: @zkamvar,
+ #205, @joelnitta, #544; fixed: @joelnitta and @zkamvar, #546).
+* `known_languages()` is a function that will return the language codes that are
+ known by {sandpaper}.
+
+## DOCUMENTATION
+
+* A new vignette `vignette("translation", package = "sandpaper")` describes how
+ translation of template components works and how to submit new/update
+ translations.
+
+## BUG FIX
+
+* The spelling of keypoints is now consistent between the menu item and the
+ callout blocks (reported: @clarallebot,
+ https://github.com/carpentries/workbench/issues/44; fixed: @zkamvar, #546)
+
+## DEPENDENCIES
+
+* The {withr} package has been upgraded to an import from a suggested package.
+
+## MISC
+
+* Added @joelnitta as an author and translator
+
+
# sandpaper 0.15.0 (2023-11-29)
## NEW FEATURES
diff --git a/R/build_404.R b/R/build_404.R
index 9c6038ef1..5975c480d 100644
--- a/R/build_404.R
+++ b/R/build_404.R
@@ -54,7 +54,7 @@ build_404 <- function(pkg, quiet = FALSE) {
this_dat <- list(
this_page = "404.html",
body = html,
- pagetitle = "Page not found"
+ pagetitle = tr_("Page not found")
)
page_globals$instructor$update(this_dat)
page_globals$learner$update(this_dat)
diff --git a/R/build_aio.R b/R/build_aio.R
index 66c082b10..57b017f2e 100644
--- a/R/build_aio.R
+++ b/R/build_aio.R
@@ -3,7 +3,7 @@ build_aio <- function(pkg, pages = NULL, quiet = FALSE) {
build_agg_page(
pkg = pkg,
pages = pages,
- title = "All in One View",
+ title = tr_("All in One View"),
slug = "aio",
aggregate = "*",
prefix = TRUE,
diff --git a/R/build_episode.R b/R/build_episode.R
index 9d690fdbd..8cbdabbf0 100644
--- a/R/build_episode.R
+++ b/R/build_episode.R
@@ -131,7 +131,7 @@ get_nav_data <- function(path_md, path_src = NULL, home = NULL,
pf_title <- NULL
if (!is.null(page_back)) {
- pb_title <- if (page_back == "index.md") "Home" else get_trimmed_title(page_back)
+ pb_title <- if (page_back == "index.md") tr_("Home") else get_trimmed_title(page_back)
page_back <- as_html(page_back)
}
if (!is.null(page_forward)) {
diff --git a/R/build_home.R b/R/build_home.R
index 9876525aa..3f0f33379 100644
--- a/R/build_home.R
+++ b/R/build_home.R
@@ -46,7 +46,7 @@ build_home <- function(pkg, quiet, next_page = NULL) {
needs_title <- nav$pagetitle == ""
if (needs_title) {
- nav$pagetitle <- "Summary and Schedule"
+ nav$pagetitle <- tr_("Summary and Schedule")
}
nav$page_forward <- as_html(nav$page_forward, instructor = TRUE)
page_globals$instructor$update(nav)
@@ -55,7 +55,7 @@ build_home <- function(pkg, quiet, next_page = NULL) {
page_globals$instructor$set("setup", use_instructor(setup))
if (needs_title) {
- nav$pagetitle <- "Summary and Setup"
+ nav$pagetitle <- tr_("Summary and Setup")
}
nav$page_forward <- as_html(nav$page_forward)
page_globals$learner$update(nav)
@@ -97,7 +97,7 @@ format_syllabus <- function(syl, use_col = TRUE) {
)
}
td1 <- glue::glue(td_template, cls = "col-md-2", thing = syl$timings)
- td2 <- glue::glue(td_template, cls = "col-md-3", thing = c(links, "Finish"))
+ td2 <- glue::glue(td_template, cls = "col-md-3", thing = c(links, tr_("Finish")))
td3 <- glue::glue(td_template, cls = "col-md-7", thing = syl$questions)
out <- glue::glue_collapse(glue::glue("
{td1}{td2}{td3}
"), sep = "\n")
tmp <- tempfile(fileext = ".md")
diff --git a/R/build_html.R b/R/build_html.R
index b8f7e3a75..b86a55624 100644
--- a/R/build_html.R
+++ b/R/build_html.R
@@ -60,7 +60,9 @@ build_html <- function(template = "chapter", pkg, nodes, global_data, path_md, q
# Process instructor page ----------------------------------------------------
update_sidebar(global_data$instructor, instructor_nodes, fs::path_file(this_page))
meta$set("url", paste0(base_url, this_page))
+ translated <- fill_translation_vars(global_data$instructor$get())
global_data$instructor$set("json", fill_metadata_template(meta))
+ global_data$instructor$set("translate", translated)
modified <- pkgdown::render_page(pkg,
template,
data = global_data$instructor$get(),
@@ -71,6 +73,7 @@ build_html <- function(template = "chapter", pkg, nodes, global_data, path_md, q
# Process learner page if needed ---------------------------------------------
if (modified) {
+ global_data$learner$set("translate", translated)
this_page <- as_html(this_page)
update_sidebar(global_data$learner, learner_nodes, fs::path_file(this_page))
meta$set("url", paste0(base_url, this_page))
diff --git a/R/build_images.R b/R/build_images.R
index 8ae6367bf..013be2eb5 100644
--- a/R/build_images.R
+++ b/R/build_images.R
@@ -3,7 +3,7 @@ build_images <- function(pkg, pages = NULL, quiet = FALSE) {
build_agg_page(
pkg = pkg,
pages = pages,
- title = "All Images",
+ title = tr_("All Images"),
slug = "images",
aggregate = "/img/..",
prefix = FALSE,
@@ -46,7 +46,7 @@ make_images_section <- function(name, contents, parent) {
content <- contents[[element]]
alt <- xml2::xml_text(xml2::xml_find_all(content, "./img/@alt"))
n <- length(alt)
- xml2::xml_add_child(section, "h3", glue::glue("Figure {element}"),
+ xml2::xml_add_child(section, "h3", glue::glue(tr_("Figure {element}")),
id = glue::glue("{name}-figure-{element}")
)
for (i in seq_along(alt)) {
@@ -57,7 +57,7 @@ make_images_section <- function(name, contents, parent) {
if (txt == "") {
txt <- "[decorative]"
}
- desc <- glue::glue("Image {i} of {n}: {sQuote(txt)}")
+ desc <- glue::glue(tr_("Image {i} of {n}: {sQuote(txt)}"))
xml2::xml_add_child(section, "p", "aria-hidden" = "true", desc)
}
xml2::xml_add_child(section, contents[[element]])
diff --git a/R/build_instructor_notes.R b/R/build_instructor_notes.R
index 119a6a130..d71f4a220 100644
--- a/R/build_instructor_notes.R
+++ b/R/build_instructor_notes.R
@@ -24,7 +24,7 @@ build_instructor_notes <- function(pkg, pages = NULL, built = NULL, quiet) {
this_dat <- list(
this_page = "instructor-notes.html",
body = use_instructor(html),
- pagetitle = "Instructor Notes"
+ pagetitle = tr_("Instructor Notes")
)
page_globals$instructor$update(this_dat)
@@ -135,7 +135,7 @@ make_instructor_note_linkback <- function(node, name) {
title <- trimws(xml2::xml_text(node))
id <- xml2::xml_attr(node, "id")
newid <- glue::glue("{name}-{id}")
- anchor <- glue::glue("")
+ anchor <- glue::glue("")
new <- ""
node <- xml2::read_xml(glue::glue(new))
xml2::xml_set_attr(node, "id", newid)
diff --git a/R/build_keypoints.R b/R/build_keypoints.R
index f6ab0ea02..f2088945a 100644
--- a/R/build_keypoints.R
+++ b/R/build_keypoints.R
@@ -3,7 +3,7 @@ build_keypoints <- function(pkg, pages = NULL, quiet = FALSE) {
build_agg_page(
pkg = pkg,
pages = pages,
- title = "Key Points",
+ title = tr_("Key Points"),
slug = "key-points",
aggregate = "/div[starts-with(@id, 'keypoints')]/div[@class='callout-inner']/div[@class='callout-content']/*",
prefix = FALSE,
diff --git a/R/build_profiles.R b/R/build_profiles.R
index 91dce533e..d6cf438ee 100644
--- a/R/build_profiles.R
+++ b/R/build_profiles.R
@@ -13,7 +13,7 @@ build_profiles <- function(pkg, quiet) {
this_dat <- list(
this_page = "profiles.html",
body = use_instructor(html),
- pagetitle = "Learner Profiles"
+ pagetitle = tr_("Learner Profiles")
)
page_globals$instructor$update(this_dat)
diff --git a/R/build_site.R b/R/build_site.R
index b0e90de5e..b0f9029fb 100644
--- a/R/build_site.R
+++ b/R/build_site.R
@@ -31,6 +31,7 @@ build_site <- function(path = ".", quiet = !interactive(), preview = TRUE, overr
# Here we provision our website using pkgdown and either initialise it if it
# does not exist or update the CSS, HTML, and JS if it does exist.
pkg <- pkgdown::as_pkgdown(path_site(path), override = override)
+ set_language(this_metadata$get()[["lang"]])
built_path <- fs::path(pkg$src_path, "built")
# NOTE: This is a kludge to prevent pkgdown from displaying a bunch of noise
# if the user asks for quiet.
diff --git a/R/set_dropdown.R b/R/set_dropdown.R
index 658c03933..178c57cf1 100644
--- a/R/set_dropdown.R
+++ b/R/set_dropdown.R
@@ -100,6 +100,15 @@ set_dropdown <- function(path = ".", order = NULL, write = FALSE, folder) {
#'
#' The following keypairs are known by sandpaper, but are optional:
#'
+#' - **lang** `[character]` the [language
+#' code](https://www.gnu.org/software/gettext/manual/html_node/Usual-Language-Codes.html)
+#' that matches the language of the lesson content. This defaults to `"en"`,
+#' but can be any language code (e.g. "ja" specifying Japanese) or
+#' combination language code and [country
+#' code](https://www.gnu.org/software/gettext/manual/html_node/Country-Codes.html)
+#' (e.g. "pt_BR" specifies Pourtugese used in Brazil). For more information
+#' on how this is used, see [the Locale Names section of the gettext
+#' manual](https://www.gnu.org/software/gettext/manual/html_node/Locale-Names.html)
#' - **url** `[character]` custom URL if you are deploying to a URL that is not
#' the default github pages io domain.
#' - **fail_on_error** `[boolean]` for R Markdown lessons; fail the build if any
diff --git a/R/utils-cli.R b/R/utils-cli.R
index 2f8c5fa22..1236c41a4 100644
--- a/R/utils-cli.R
+++ b/R/utils-cli.R
@@ -41,6 +41,18 @@ warn_schedule <- function() {
cli::cli_end(thm)
}
+warn_no_language <- function(lang) {
+ thm <- cli::cli_div(theme = sandpaper_cli_theme())
+ wmsg <- "{.code {siQuote(lang)}} is not a language that has been defined in The Workbench."
+ cli::cli_alert_warning(wmsg)
+ amsg1 <- "Use {.code known_languages()} to see a list of known languages"
+ cli::cli_alert_info(cli::style_dim(amsg1), class = "alert-suggestion")
+ amsg2 <- c("To add a new language, consult {.code vignette('translations', package = 'sandpaper')}")
+ cli::cli_alert_info(cli::style_dim(amsg2), class = "alert-suggestion")
+
+ cli::cli_end(thm)
+}
+
show_changed_yaml <- function(sched, order, yaml, what = "episodes") {
# display for the user to distinguish what was added and what was taken
diff --git a/R/utils-potools.R b/R/utils-potools.R
new file mode 100644
index 000000000..dbc628179
--- /dev/null
+++ b/R/utils-potools.R
@@ -0,0 +1,3 @@
+tr_ <- function(...) {
+ enc2utf8(gettext(paste0(...), domain = "R-sandpaper"))
+}
diff --git a/R/utils-translate.R b/R/utils-translate.R
new file mode 100644
index 000000000..ec7a4dc77
--- /dev/null
+++ b/R/utils-translate.R
@@ -0,0 +1,323 @@
+#' Show a list of languages known by {sandpaper}
+#'
+#' @return a character vector of language codes known by {sandpaper}
+#'
+#' @details The known languages are translations of menu and navigational
+#' elements that exist in {sandpaper}. If these elements have not been
+#' translated for a given language and you would like to add translations for
+#' them, please consult `vignette("translations", package = "sandpaper")` for
+#' details of how to do so in the source code for {sandpaper}.
+#'
+#' ## List of Known Languages:
+#'
+#' ```{r, echo = FALSE}
+#' langs <- known_languages()
+#' writeLines(paste("-", langs))
+#' ```
+#'
+#' @export
+#' @examples
+#' known_languages()
+known_languages <- function() {
+ lang_files <- system.file("po", package = "sandpaper")
+ as.character(c("en", fs::path_file(fs::dir_ls(lang_files, type = "dir"))))
+}
+
+is_known_language <- function(lang = NULL, warn = FALSE) {
+ lang <- lang %||% "en"
+ not_known <- lang %nin% known_languages()
+ if (not_known && warn) {
+ warn_no_language(lang)
+ }
+ return(!not_known)
+}
+
+# Translations for static lesson elements happens during the `build_site()`
+# phase. The `set_language()` function is run. It should only ever be run
+# inside of another function to ensure the scope is honoured.
+set_language <- function(lang = NULL, scope = parent.frame()) {
+ lang <- lang %||% "en"
+ known <- is_known_language(lang, warn = TRUE)
+ if (known) {
+ withr::local_envvar(
+ LANGUAGE = lang,
+ .local_envir = scope
+ )
+ }
+ add_varnish_translations()
+}
+
+
+
+
+# These are all the translations that occur in {varnish}
+add_varnish_translations <- function() {
+ menu_translations <- list(
+ # header.html -------------------------------------------------------------
+ SkipToMain = tr_('Skip to main content'),# alt text
+ iPreAlpha = tr_('Pre-Alpha'),
+ PreAlphaNote = tr_('This lesson is in the pre-alpha phase, which means that it is in early development, but has not yet been taught.'),
+ AlphaNote = tr_('This lesson is in the alpha phase, which means that it has been taught once and lesson authors are iterating on feedback.'),
+ iAlpha = tr_('Alpha'),
+ BetaNote = tr_('This lesson is in the beta phase, which means that it is ready for teaching by instructors outside of the original author team.'),
+ iBeta = tr_('Beta'),
+ PeerReview = tr_('This lesson has passed peer review.'),
+ InstructorView = tr_('Instructor View'), # navbar.html
+ LearnerView = tr_('Learner View'), # navbar.html
+ MainNavigation = tr_('Main Navigation'), # alt text
+ ToggleNavigation = tr_('Toggle Navigation'), # alt-text
+ Menu = tr_('Menu'), # footer.html
+ SearchButton = tr_('search button'), # alt text
+ Setup = tr_('Setup'), # navbar.html
+ KeyPoints = tr_("Key Points"), # navbar.html
+ InstructorNotes = tr_('Instructor Notes'), # navbar.html
+ Glossary = tr_('Glossary'), # navbar.html
+ LearnerProfiles = tr_('Learner Profiles'), # navbar.html
+ More = tr_('More'),
+ Search = tr_('Search'),
+ LessonProgress = tr_('Lesson Progress'), # alt text
+
+ # navbar.html -------------------------------------------------------------
+ CloseMenu = tr_("close menu"), # alt text
+ EPISODES = tr_('EPISODES'),
+ Home = tr_('Home'), # content-chapter.html
+ HomePageNav = tr_('Home Page Navigation'), # alt text
+ RESOURCES = tr_('RESOURCES'),
+ ExtractAllImages = tr_('Extract All Images'),
+ AIO = tr_("See all in one page"),
+ DownloadHandout = tr_('Download Lesson Handout'),
+ ExportSlides = tr_('Export Chapter Slides'), # content-chapter.html
+
+ # content-[thing].html ---------------------------------------------------
+ PreviousAndNext = tr_('Previous and Next Chapter'), # alt text
+ Previous = tr_('Previous'),
+ EstimatedTime = tr_('Estimated time: {icons$clock} {minutes} minutes'),
+ Next = tr_('Next'),
+ NextChapter = tr_('Next Chapter'), # alt-text
+ LastUpdate = tr_('Last updated on {updated}'),
+ EditThisPage = tr_('Edit this page'),
+ ExpandAllSolutions = tr_('Expand All Solutions'),
+
+ # content-syllabus.html --------------------------------------------------
+ SetupInstructions = tr_('Setup Instructions'),
+ DownloadFiles = tr_('Download files required for the lesson'),
+ ActualScheduleNote = tr_('The actual schedule may vary slightly depending on the topics and exercises chosen by the instructor.'),
+
+ # footer.html ------------------------------------------------------------
+ BackToTop = tr_('Back To Top'),
+ SpanToTop = tr_('<(Back)> To Top'),
+ ThisLessonCoC = tr_('This lesson is subject to the <(Code of Conduct)>'),
+ CoC = tr_('Code of Conduct'),
+ EditOnGH = tr_('Edit on GitHub'),
+ Contributing = tr_('Contributing'),
+ Source = tr_('Source'),
+ Cite = tr_('Cite'),
+ Contact = tr_('Contact'),
+ About = tr_('About'),
+ MaterialsLicensedUnder = tr_('Materials licensed under {license} by {authors}'),
+ TemplateLicense = tr_('Template licensed under <(CC-BY 4.0)> by {template_authors}'),
+ Carpentries = tr_('The Carpentries'),
+ BuiltWith = tr_('Built with {sandpaper_link}, {pegboard_link}, and {varnish_link}'),
+
+ # javascript --------------------------------------------------------------
+ ExpandAllSolutions = tr_('Expand All Solutions'),
+ CollapseAllSolutions = tr_('Collapse All Solutions'),
+ Collapse = tr_('Collapse'),
+ Episodes = tr_('Episodes'),
+
+ # beta content not used anymore.
+ GiveFeedback = tr_('Give Feedback'),
+ LearnMore = tr_('Learn More')
+ )
+ learner_globals$set("translate", menu_translations)
+ instructor_globals$set("translate", menu_translations)
+ fix_both_sidebars(learner_globals, instructor_globals, menu_translations)
+}
+
+# Apply translations to text assuming that the names of the translations
+# matches the text
+apply_translations <- function(txt, translations) {
+ ntxt <- length(txt)
+ ntranslations <- length(translations)
+ # empty text or empty translations returns the text
+ if (ntxt == 0L || ntranslations == 0L) {
+ return(txt)
+ }
+ # when there are translations, apply them only to the matching elements of
+ # the vector
+ to_translate <- txt %in% names(translations)
+ if (any(to_translate)) {
+ ids <- txt[to_translate]
+ txt[to_translate] <- translations[ids]
+ }
+ return(txt)
+}
+
+# generator of translations for code blocks.
+get_codeblock_translations <- function() {
+ c(
+ OUTPUT = tr_("OUTPUT"),
+ WARNING = tr_("WARNING"),
+ ERROR = tr_("ERROR")
+ )
+}
+
+# generator for translations of callout blocks and accordions
+get_callout_translations <- function() {
+ c(
+ Callout = tr_("Callout"),
+ Challenge = tr_("Challenge"),
+ Prereq = tr_("Prerequisite"),
+ Checklist = tr_("Checklist"),
+ Discussion = tr_("Discussion"),
+ Testimonial = tr_("Testimonial"),
+ Keypoints = tr_("Key Points")
+ )
+}
+get_accordion_translations <- function() {
+ c(
+ "Show me the solution" = tr_("Show me the solution"),
+ "Give me a hint" = tr_("Give me a hint"),
+ "Show details" = tr_("Show details"),
+ "Instructor Note" = tr_("Instructor Note")
+ )
+}
+
+
+# See the `fix_sidebar_translation()` comment. This takes the learner and
+# instructor global data and fixes the titles of the first element
+# representing the home page.
+fix_both_sidebars <- function(learner, instructor, translations) {
+ lside <- learner$get()[["sidebar"]]
+ learn_summary <- tr_("Summary and Setup")
+ learner$set("sidebar", fix_sidebar_translation(lside, learn_summary))
+
+ iside <- instructor$get()[["sidebar"]]
+ instruct_summary <- tr_("Summary and Schedule")
+ instructor$set("sidebar", fix_sidebar_translation(lside, instruct_summary))
+}
+
+# The sidebar construction happens during the first parts of `build_lesson()`
+# when `validate_lesson()` is called and the Lesson object is loaded.
+#
+# Because of this, it will always be in the local of the user, which is not
+# necessarily the locale of the lesson. We should not necessarily impose the
+# lesson locale on the user because the error messages from earlier build
+# stages should be in the user's locale.
+#
+# This is all a really long-winded way to say that we need to set the
+# translation here, where the translation variables are set.
+#
+# This function takes a sidebar list, a translation for either "summary and
+# schedule" or "summary and setup" and applies the translation to the HTML for
+# the first element
+fix_sidebar_translation <- function(sidebar, translation) {
+ first_item <- xml2::read_xml(sidebar[[1]])
+ idx <- xml2::xml_find_first(first_item, ".//a")
+ if (startsWith(xml2::xml_text(idx), "Summary")) {
+ xml2::xml_set_text(idx, translation)
+ } else {
+ return(sidebar)
+ }
+ sidebar[[1]] <- as.character(first_item)
+ return(sidebar)
+}
+
+
+# replace text string with a <(kirby template)> with link text
+# replace_link("this string has a <(kirby template)>", "https://emojicombos.com/kirby")
+replace_link <- function(txt, href) {
+ replace_html(txt, open = paste0(''), close = "")
+}
+
+replace_html <- function(txt, open, close) {
+ txt <- sub("<(", open, txt, fixed = TRUE)
+ return(sub(")>", close, txt, fixed = TRUE))
+}
+
+#' Apply template items to translated strings
+#'
+#' @param the_data a list of global variables (either `learner_globals` or
+#' `instructor_globals`) that also contains a "translate" element containing
+#' a list of translated strings.
+#'
+#' @return the translated list with templated data filled out
+#' @keywords internal
+#' @details There are two kinds of templating we use:
+#'
+#' 1. variable templating indicated by `{key}` where `key` represents a
+#' variable that exists within the global data and is replaced.
+#' 2. link templating indicated by `<(text to wrap)>` where we replace the
+#' `<()>` with a known URL or HTML markup. This allows the translators to
+#' translate text without having to worry about HTML markup.
+#' @examples
+#'
+#' dat <- list(
+#' a = "a barn",
+#' b = "a bee",
+#' minutes = 5,
+#' translate = list(
+#' one = "a normal translated string (pretend it's translated from another language)",
+#' two = "a question: are you (A) {a}, (B) {b}",
+#' EstimatedTime = "Estimated time: {icons$clock} {minutes}",
+#' license = "Licensed under {license} by {authors}",
+#' ThisLessonCoC = "This lesson operates under our <(Code of Conduct)>"
+#' )
+#' )
+#' asNamespace("sandpaper")$fill_translation_vars(dat)
+fill_translation_vars <- function(the_data) {
+ # define icons that we will need to pre-fab insert for the template.
+ icns <- c("clock", "edit")
+ template_icns <- glue::glue(
+ ''
+ )
+
+ # add our templating variables to the data list
+ dat <- c(the_data,
+ list(
+ icons = setNames(as.list(template_icns), icns),
+ template_authors = 'The Carpentries',
+ authors = "the authors",
+ license = the_data$license %||% "CC-BY 4.0",
+ minutes = the_data$minutes %||% NULL,
+ updated = the_data$updated %||% NULL
+ )
+ )
+ # variables that have known fixed URLs can simply have them added in.
+ dat$license <- glue::glue('{dat$license}')
+ translated <- the_data[["translate"]]
+
+ # all translated items need to have variables replaced and the URL templates
+ # filled out.
+ for (key in names(translated)) {
+ the_string <- translated[[key]]
+ is_templated <- grepl("[{][A-z_$.]+?[}]", the_string)
+ if (is_templated) {
+ # if the string has a template variable {key}, it should be replaced
+ # via {glue}.
+ the_string <- glue::glue_data(dat, the_string)
+ }
+ string_exists <- length(the_string) > 0L
+ has_url_template <- string_exists && grepl("<(", the_string, fixed = TRUE)
+ if (has_url_template) {
+ # In this space, we need to replace links present in the URL template
+ # with their URLs
+ # (e.g. going from `<(hello)>` to `hello`)
+ the_string <- switch(key,
+ ThisLessonCoC = replace_link(the_string,
+ href = "CODE_OF_CONDUCT.html"
+ ),
+ TemplateLicense = replace_link(the_string,
+ href = "https://creativecommons.org/licenses/by-sa/4.0/"
+ ),
+ SpanToTop = replace_html(the_string,
+ open = '',
+ close = ''
+ ),
+ the_string
+ )
+ }
+ translated[[key]] <- the_string
+ }
+ return(translated)
+}
diff --git a/R/utils-varnish.R b/R/utils-varnish.R
index ec9ae7a63..1546be519 100644
--- a/R/utils-varnish.R
+++ b/R/utils-varnish.R
@@ -21,7 +21,7 @@ varnish_vars <- function() {
res <- paste0(user, "/", repo, "/tree/", ref)
return(res)
}
- list(
+ res <- list(
sandpaper_version = ver("sandpaper"),
sandpaper_cfg = cfg("sandpaper"),
pegboard_version = ver("pegboard"),
@@ -29,6 +29,18 @@ varnish_vars <- function() {
varnish_version = ver("varnish"),
varnish_cfg = cfg("varnish")
)
+ carpurl <- function(res, pkg) {
+ config <- res[[paste0(pkg, "_cfg")]] %||% paste0("carpentries/", pkg)
+ version <- res[[paste0(pkg, "_version")]]
+ glue::glue('{pkg}{version}')
+ }
+ urls <- list(
+ sandpaper_link = carpurl(res, "sandpaper"),
+ pegboard_link = carpurl(res, "pegboard"),
+ varnish_link = carpurl(res, "varnish")
+ )
+ return(c(res, urls))
+
}
#' Set the necessary common global variables for use in the {varnish} template.
@@ -70,20 +82,26 @@ set_globals <- function(path) {
idx_item <- xml2::read_html(instructor_sidebar[[1]])
idx_link <- xml2::xml_find_first(idx_item, ".//a")
idx_text <- xml2::xml_contents(idx_link)
- if (length(idx_text) == 1 && xml2::xml_text(idx_text) == "0. ") {
- xml2::xml_set_text(idx_link, "Summary and Schedule")
+ no_index_title <- length(idx_text) == 1 && xml2::xml_text(idx_text) == "0. "
+ if (no_index_title) {
+ xml2::xml_set_text(idx_link, tr_("Summary and Schedule"))
} else {
xml2::xml_set_text(idx_text, sub("^0[.] ", "", xml2::xml_text(idx_text)))
}
sindex <- create_sidebar_item(nodes = NULL, as.character(idx_link), 1)
learner_sidebar <- instructor_sidebar
instructor_sidebar[[1]] <- sindex
- learner_sidebar[[1]] <- sub("Schedule", "Setup", sindex)
+ if (no_index_title) {
+ xml2::xml_set_text(idx_link, tr_("Summary and Setup"))
+ sindex <- create_sidebar_item(nodes = NULL, as.character(idx_link), 1)
+ }
+ learner_sidebar[[1]] <- sindex
# Resources
learner <- create_resources_dropdown(these_resources[["learners"]],
"learners")
- instructor <- create_resources_dropdown(these_resources[["instructors"]], "instructors")
+ instructor <- create_resources_dropdown(these_resources[["instructors"]],
+ "instructors")
instructor$extras <- c(instructor$extras, "
", learner$extras)
instructor$resources <- c(instructor$resources, "
", learner$extras)
pkg_versions <- varnish_vars()
diff --git a/R/utils-xml.R b/R/utils-xml.R
index 51270e4ba..496f57a0c 100644
--- a/R/utils-xml.R
+++ b/R/utils-xml.R
@@ -1,6 +1,8 @@
fix_nodes <- function(nodes = NULL) {
if (length(nodes) == 0) return(nodes)
+ translate_overview(nodes)
fix_headings(nodes)
+ fix_accordions(nodes)
fix_callouts(nodes)
fix_codeblocks(nodes)
fix_figures(nodes)
@@ -21,18 +23,64 @@ fix_headings <- function(nodes = NULL) {
invisible(nodes)
}
+#' add codewrapper class and apply code heading to all code blocks
+#'
+#' The syntax highlighte4d code blocks that come out of pandoc have this
+#' structure (where `lang` is the language of the code block):
+#'
+#' ```html
+#'
+#'
+#'
+#' ...
+#'
+#'
+#'
+#' ```
+#'
+#' In The Workbench, we want to have this structure:
+#'
+#' ```html
+#'
+#'
+#' LANG
+#'
+#'
+#'
+#'
+#'
+#' ...
+#'
+#'
+#'
+#' ```
+#'
+#' This allows us to display the language of the code block in the lesson,
+#' which can be helpful when the lesson switches between BASH and another
+#' language.
+#'
+#' @param nodes HTML nodes
+#' @return the modified nodes
+#'
+#' @noRd
fix_codeblocks <- function(nodes = NULL) {
if (length(nodes) == 0) return(nodes)
code <- xml2::xml_find_all(nodes, ".//div[starts-with(@class, 'sourceCode')]")
xml2::xml_set_attr(code, "class", "codewrapper sourceCode")
pre <- xml2::xml_children(code)
- type <- rev(trimws(sub("sourceCode", "", xml2::xml_attr(pre, "class"))))
- add_code_heading(pre, toupper(type))
- outputs <- xml2::xml_find_all(nodes, ".//pre[@class='output' or @class='warning' or @class='error']")
+ # pre-compile these during the transformation so we only have to do it
+ # once per document
+ translations <- get_codeblock_translations()
+ # Extract the language, transform to all caps, and reverse the order.
+ # We need to reverse the order so that we can add
+ type <- toupper(trimws(sub("sourceCode", "", xml2::xml_attr(pre, "class"))))
+ add_code_heading(pre, apply_translations(type, translations))
+ outputs <- xml2::xml_find_all(nodes,
+ ".//pre[@class='output' or @class='warning' or @class='error']")
if (length(outputs)) {
xml2::xml_add_parent(outputs, "div", class = "codewrapper")
- class_headings <- rev(toupper(xml2::xml_attr(outputs, "class")))
- add_code_heading(outputs, class_headings)
+ class_headings <- toupper(trimws(xml2::xml_attr(outputs, "class")))
+ add_code_heading(outputs, apply_translations(class_headings, translations))
}
invisible(nodes)
}
@@ -40,8 +88,11 @@ fix_codeblocks <- function(nodes = NULL) {
add_code_heading <- function(codes = NULL, labels = "OUTPUT") {
if (length(codes) == 0) return(codes)
xml2::xml_set_attr(codes, "tabindex", "0")
- heads <- xml2::xml_add_sibling(codes, "h3", labels, class = "code-label",
- .where = "before")
+ # NOTE: xml_add_sibling adds the siblings from bottom to top, so these labels
+ # need to be in reverse. It's weird.
+ heads <- xml2::xml_add_sibling(codes, "h3", rev(labels),
+ class = "code-label", .where = "before"
+ )
for (head in heads) {
xml2::xml_add_child(head, "i",
"aria-hidden" = "true", "data-feather" = "chevron-left")
@@ -80,7 +131,7 @@ add_class <- function(nodes, new) {
add_anchors <- function(nodes, ids) {
anchor <- paste0(
- ""
+ ""
)
for (i in seq_along(nodes)) {
heading <- nodes[[i]]
@@ -93,10 +144,61 @@ add_anchors <- function(nodes, ids) {
}
}
+# translate the overview cards, which are defined in
+# inst/rmarkdown/lua/lesson.lua
+translate_overview <- function(nodes = NULL) {
+ if (length(nodes) == 0) return(nodes)
+ card <- xml2::xml_find_first(nodes, ".//div[@class='overview card']")
+ if (length(card) == 0) {
+ return(nodes)
+ }
+ overview <- xml2::xml_find_first(card, "./h2[@class='card-header']")
+ qpath <- ".//div[starts-with(@class, 'inner')]/h3[@class='card-title'][text()='Questions']"
+ opath <- ".//div[starts-with(@class, 'inner')]/h3[@class='card-title'][text()='Objectives']"
+ questions <- xml2::xml_find_first(card, qpath)
+ objectives <- xml2::xml_find_first(card, opath)
+
+ xml2::xml_set_text(questions, tr_("Questions"))
+ xml2::xml_set_text(objectives, tr_("Objectives"))
+ xml2::xml_set_text(overview, tr_("Overview"))
+ invisible(nodes)
+}
+
+# translate contents of an XML node list
+# @param nodes an xml node or xml nodelist
+# @param translations a named vector of translated strings whose names are the
+# strings in English
+xml_text_translate <- function(nodes, translations) {
+ txt <- xml2::xml_text(nodes, trim = TRUE)
+ xml2::xml_set_text(nodes, apply_translations(txt, translations))
+ return(invisible(nodes))
+}
+
+fix_accordions <- function(nodes = NULL) {
+ if (length(nodes) == 0) return(nodes)
+ accordions <- xml2::xml_find_all(nodes,
+ ".//div[starts-with(@class, 'accordion ')]"
+ )
+ # NOTE: we need to include `text()` in the call here because of the presence
+ # of the decorative blocks inside the accordion headings.
+ # solution and hint are h4
+ # instructor and spoiler are h3
+ headings <- xml2::xml_find_all(accordions,
+ "./div/button/h3/text() | ./div/button/h4/text()"
+ )
+ translations <- get_accordion_translations()
+ xml_text_translate(headings, translations)
+ # at this point, we would fix headings, but we do not actually have a way to
+ # consistently do this, so it remains as an exercise for the future.
+ return(invisible(nodes))
+}
+
fix_callouts <- function(nodes = NULL) {
if (length(nodes) == 0) return(nodes)
callouts <- xml2::xml_find_all(nodes, ".//div[starts-with(@class, 'callout ')]")
h3 <- xml2::xml_find_all(callouts, "./div/h3")
+ translations <- get_callout_translations()
+ xml_text_translate(h3, translations)
xml2::xml_set_attr(h3, "class", "callout-title")
inner_div <- xml2::xml_parent(h3)
# remove the "section level3 callout-title" attrs
diff --git a/R/utils-yaml.R b/R/utils-yaml.R
index c74f4e3e0..8ab99ca1c 100644
--- a/R/utils-yaml.R
+++ b/R/utils-yaml.R
@@ -171,6 +171,7 @@ create_pkgdown_yaml <- function(path) {
version = siQuote(utils::packageVersion("sandpaper")),
config = siQuote(path_config(path)),
title = siQuote(usr$title),
+ lang = if (is.null(usr$lang)) NULL else siQuote(usr$lang),
time = UTC_timestamp(Sys.time()),
source = siQuote(sub("/$", "", usr$source)),
branch = siQuote(usr$branch),
diff --git a/_pkgdown.yml b/_pkgdown.yml
index e36fb950d..efdc97b84 100644
--- a/_pkgdown.yml
+++ b/_pkgdown.yml
@@ -96,6 +96,7 @@ reference:
of a lesson assuming that the markdown components have been built.
(this is non-exhaustive)
- contents:
+ - known_languages
- render_html
- build_site
- build_episode_html
@@ -139,6 +140,7 @@ articles:
- building-with-renv
- automated-pull-requests
- include-child-documents
+ - translations
- title: "Developer Guides"
contents:
- articles/landscape
diff --git a/inst/po/es/LC_MESSAGES/R-sandpaper.mo b/inst/po/es/LC_MESSAGES/R-sandpaper.mo
new file mode 100644
index 000000000..5552279b1
Binary files /dev/null and b/inst/po/es/LC_MESSAGES/R-sandpaper.mo differ
diff --git a/inst/po/ja/LC_MESSAGES/R-sandpaper.mo b/inst/po/ja/LC_MESSAGES/R-sandpaper.mo
new file mode 100644
index 000000000..167e2ce64
Binary files /dev/null and b/inst/po/ja/LC_MESSAGES/R-sandpaper.mo differ
diff --git a/inst/templates/pkgdown-yaml-template.txt b/inst/templates/pkgdown-yaml-template.txt
index e436b4774..1a313d092 100644
--- a/inst/templates/pkgdown-yaml-template.txt
+++ b/inst/templates/pkgdown-yaml-template.txt
@@ -3,6 +3,7 @@
# If you want to make changes, please edit {{ config }}
# ------------------------------------------------------------------ information
title: {{ title }} # needed to set the site title
+lang: {{ lang }}{{ ^lang }}'en'{{ /lang }}
home:
title: Home
strip_header: true
diff --git a/man/fill_translation_vars.Rd b/man/fill_translation_vars.Rd
new file mode 100644
index 000000000..bf416e453
--- /dev/null
+++ b/man/fill_translation_vars.Rd
@@ -0,0 +1,46 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/utils-translate.R
+\name{fill_translation_vars}
+\alias{fill_translation_vars}
+\title{Apply template items to translated strings}
+\usage{
+fill_translation_vars(the_data)
+}
+\arguments{
+\item{the_data}{a list of global variables (either \code{learner_globals} or
+\code{instructor_globals}) that also contains a "translate" element containing
+a list of translated strings.}
+}
+\value{
+the translated list with templated data filled out
+}
+\description{
+Apply template items to translated strings
+}
+\details{
+There are two kinds of templating we use:
+\enumerate{
+\item variable templating indicated by \verb{\{\% key \%\}} where \code{key} represents a
+variable that exists within the global data and is replaced.
+\item link templating indicated by \verb{<(text to wrap)>} where we replace the
+\verb{<()>} with a known URL or HTML markup. This allows the translators to
+translate text without having to worry about HTML markup.
+}
+}
+\examples{
+
+dat <- list(
+ a = "a barn",
+ b = "a bee",
+ minutes = 5,
+ translate = list(
+ one = "a normal translated string (pretend it's translated from another language)",
+ two = "a question: are you (A) {\% a \%}, (B) {\% b \%}",
+ EstimatedTime = "Estimated time: {\% icons$clock \%} {\% minutes \%}",
+ license = "Licensed under {\% license \%} by {\% authors \%}",
+ ThisLessonCoC = "This lesson operates under our <(Code of Conduct)>"
+ )
+)
+asNamespace("sandpaper")$fill_translation_vars(dat)
+}
+\keyword{internal}
diff --git a/man/known_languages.Rd b/man/known_languages.Rd
new file mode 100644
index 000000000..e81b47a4d
--- /dev/null
+++ b/man/known_languages.Rd
@@ -0,0 +1,30 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/utils-translate.R
+\name{known_languages}
+\alias{known_languages}
+\title{Show a list of languages known by {sandpaper}}
+\usage{
+known_languages()
+}
+\value{
+a character vector of language codes known by {sandpaper}
+}
+\description{
+Show a list of languages known by {sandpaper}
+}
+\details{
+The known languages are translations of menu and navigational
+elements that exist in {sandpaper}. If these elements have not been
+translated for a given language and you would like to add translations for
+them, please consult \code{vignette("translations", package = "sandpaper")} for
+details of how to do so in the source code for {sandpaper}.
+\subsection{List of Known Languages:}{
+
+\if{html}{\out{}}\preformatted{#> - en
+#> - ja
+}\if{html}{\out{
}}
+}
+}
+\examples{
+known_languages()
+}
diff --git a/man/sandpaper-package.Rd b/man/sandpaper-package.Rd
index 5c9f1f14a..e5c10525e 100644
--- a/man/sandpaper-package.Rd
+++ b/man/sandpaper-package.Rd
@@ -23,6 +23,11 @@ Useful links:
\author{
\strong{Maintainer}: Zhian N. Kamvar \email{zkamvar@carpentries.org} (\href{https://orcid.org/0000-0003-1458-7108}{ORCID})
+Authors:
+\itemize{
+ \item Joel H. Nitta \email{joelnitta@gmail.com} (\href{https://orcid.org/0000-0003-4719-7472}{ORCID}) [translator]
+}
+
Other contributors:
\itemize{
\item Julien Colomb (\href{https://orcid.org/0000-0002-3127-5520}{ORCID}) [contributor]
diff --git a/man/set_config.Rd b/man/set_config.Rd
index d80b91d00..9a215bfc9 100644
--- a/man/set_config.Rd
+++ b/man/set_config.Rd
@@ -48,6 +48,12 @@ information about the lesson
The following keypairs are known by sandpaper, but are optional:
\itemize{
+\item \strong{lang} \verb{[character]} the \href{https://www.gnu.org/software/gettext/manual/html_node/Usual-Language-Codes.html}{language code}
+that matches the language of the lesson content. This defaults to \code{"en"},
+but can be any language code (e.g. "ja" specifying Japanese) or
+combination language code and \href{https://www.gnu.org/software/gettext/manual/html_node/Country-Codes.html}{country code}
+(e.g. "pt_BR" specifies Pourtugese used in Brazil). For more information
+on how this is used, see \href{https://www.gnu.org/software/gettext/manual/html_node/Locale-Names.html}{the Locale Names section of the gettext manual}
\item \strong{url} \verb{[character]} custom URL if you are deploying to a URL that is not
the default github pages io domain.
\item \strong{fail_on_error} \verb{[boolean]} for R Markdown lessons; fail the build if any
diff --git a/po/R-es.po b/po/R-es.po
new file mode 100644
index 000000000..7731dafab
--- /dev/null
+++ b/po/R-es.po
@@ -0,0 +1,365 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: sandpaper 0.15.0.9000\n"
+"POT-Creation-Date: 2023-12-06 11:03-0800\n"
+"PO-Revision-Date: 2023-12-06 11:03-0800\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: build_404.R:57
+msgid "Page not found"
+msgstr ""
+
+#: build_aio.R:6
+msgid "All in One View"
+msgstr "Todo en una sola página"
+
+#: build_episode.R:134 utils-translate.R:82
+msgid "Home"
+msgstr ""
+
+#: build_home.R:49 utils-translate.R:195 utils-varnish.R:87
+msgid "Summary and Schedule"
+msgstr ""
+
+#: build_home.R:58 utils-translate.R:191 utils-varnish.R:95
+msgid "Summary and Setup"
+msgstr ""
+
+#: build_home.R:100
+msgid "Finish"
+msgstr "Finale"
+
+#: build_images.R:6
+msgid "All Images"
+msgstr ""
+
+#: build_images.R:49
+msgid "Figure {element}"
+msgstr ""
+
+#: build_images.R:60
+msgid "Image {i} of {n}: {sQuote(txt)}"
+msgstr ""
+
+#: build_instructor_notes.R:27 utils-translate.R:72
+msgid "Instructor Notes"
+msgstr ""
+
+#: build_keypoints.R:6 utils-translate.R:71 utils-translate.R:173
+msgid "Key Points"
+msgstr "Puntos Clave"
+
+#: build_profiles.R:16 utils-translate.R:74
+msgid "Learner Profiles"
+msgstr ""
+
+#: utils-translate.R:56
+msgid "Skip to main content"
+msgstr ""
+
+#: utils-translate.R:57
+msgid "Pre-Alpha"
+msgstr ""
+
+#: utils-translate.R:58
+msgid ""
+"This lesson is in the pre-alpha phase, which means that it is in early "
+"development, but has not yet been taught."
+msgstr ""
+
+#: utils-translate.R:59
+msgid ""
+"This lesson is in the alpha phase, which means that it has been taught once "
+"and lesson authors are iterating on feedback."
+msgstr ""
+
+#: utils-translate.R:60
+msgid "Alpha"
+msgstr ""
+
+#: utils-translate.R:61
+msgid ""
+"This lesson is in the beta phase, which means that it is ready for teaching "
+"by instructors outside of the original author team."
+msgstr ""
+
+#: utils-translate.R:62
+msgid "Beta"
+msgstr ""
+
+#: utils-translate.R:63
+msgid "This lesson has passed peer review."
+msgstr ""
+
+#: utils-translate.R:64
+msgid "Instructor View"
+msgstr ""
+
+#: utils-translate.R:65
+msgid "Learner View"
+msgstr ""
+
+#: utils-translate.R:66
+msgid "Main Navigation"
+msgstr ""
+
+#: utils-translate.R:67
+msgid "Toggle Navigation"
+msgstr ""
+
+#: utils-translate.R:68
+msgid "Menu"
+msgstr ""
+
+#: utils-translate.R:69
+msgid "search button"
+msgstr ""
+
+#: utils-translate.R:70
+msgid "Setup"
+msgstr "Configuración"
+
+#: utils-translate.R:73
+msgid "Glossary"
+msgstr ""
+
+#: utils-translate.R:75
+msgid "More"
+msgstr ""
+
+#: utils-translate.R:76
+msgid "Search"
+msgstr ""
+
+#: utils-translate.R:77
+msgid "Lesson Progress"
+msgstr ""
+
+#: utils-translate.R:80
+msgid "close menu"
+msgstr ""
+
+#: utils-translate.R:81
+msgid "EPISODES"
+msgstr ""
+
+#: utils-translate.R:83
+msgid "Home Page Navigation"
+msgstr ""
+
+#: utils-translate.R:84
+msgid "RESOURCES"
+msgstr ""
+
+#: utils-translate.R:85
+msgid "Extract All Images"
+msgstr ""
+
+#: utils-translate.R:86
+msgid "See all in one page"
+msgstr "Todo en una sola página"
+
+#: utils-translate.R:87
+msgid "Download Lesson Handout"
+msgstr ""
+
+#: utils-translate.R:88
+msgid "Export Chapter Slides"
+msgstr ""
+
+#: utils-translate.R:91
+msgid "Previous and Next Chapter"
+msgstr ""
+
+#: utils-translate.R:92
+msgid "Previous"
+msgstr ""
+
+#: utils-translate.R:93
+msgid "Estimated time: {icons$clock} {minutes} minutes"
+msgstr ""
+
+#: utils-translate.R:94
+msgid "Next"
+msgstr ""
+
+#: utils-translate.R:95
+msgid "Next Chapter"
+msgstr ""
+
+#: utils-translate.R:96
+msgid "Last updated on {updated}"
+msgstr ""
+
+#: utils-translate.R:97
+msgid "Edit this page"
+msgstr "Mejora esta página"
+
+#: utils-translate.R:98 utils-translate.R:122
+msgid "Expand All Solutions"
+msgstr ""
+
+#: utils-translate.R:101
+msgid "Setup Instructions"
+msgstr "Configuración"
+
+#: utils-translate.R:102
+msgid "Download files required for the lesson"
+msgstr "Descargar los archivos necesarios para la lección"
+
+#: utils-translate.R:103
+msgid ""
+"The actual schedule may vary slightly depending on the topics and exercises "
+"chosen by the instructor."
+msgstr "El horario real puede variar ligeramente dependiendo de los temas y "
+"ejercicios elegidos por el instructor."
+
+#: utils-translate.R:106
+msgid "Back To Top"
+msgstr ""
+
+#: utils-translate.R:107
+msgid "<(Back)> To Top"
+msgstr ""
+
+#: utils-translate.R:108
+msgid "This lesson is subject to the <(Code of Conduct)>"
+msgstr ""
+
+#: utils-translate.R:109
+msgid "Code of Conduct"
+msgstr ""
+
+#: utils-translate.R:110
+msgid "Edit on GitHub"
+msgstr "Editar en GitHub"
+
+#: utils-translate.R:111
+msgid "Contributing"
+msgstr "Contribuir"
+
+#: utils-translate.R:112
+msgid "Source"
+msgstr "Fuente"
+
+#: utils-translate.R:113
+msgid "Cite"
+msgstr "Cita"
+
+#: utils-translate.R:114
+msgid "Contact"
+msgstr "Contacto"
+
+#: utils-translate.R:115
+msgid "About"
+msgstr ""
+
+#: utils-translate.R:116
+msgid "Materials licensed under {license} by {authors}"
+msgstr ""
+
+#: utils-translate.R:117
+msgid "Template licensed under <(CC-BY 4.0)> by {template_authors}"
+msgstr ""
+
+#: utils-translate.R:118
+msgid "The Carpentries"
+msgstr ""
+
+#: utils-translate.R:119
+msgid "Built with {sandpaper_link}, {pegboard_link}, and {varnish_link}"
+msgstr ""
+
+#: utils-translate.R:123
+msgid "Collapse All Solutions"
+msgstr ""
+
+#: utils-translate.R:124
+msgid "Collapse"
+msgstr ""
+
+#: utils-translate.R:125
+msgid "Episodes"
+msgstr ""
+
+#: utils-translate.R:128
+msgid "Give Feedback"
+msgstr ""
+
+#: utils-translate.R:129
+msgid "Learn More"
+msgstr ""
+
+#: utils-translate.R:158
+msgid "OUTPUT"
+msgstr ""
+
+#: utils-translate.R:159
+msgid "WARNING"
+msgstr ""
+
+#: utils-translate.R:160
+msgid "ERROR"
+msgstr ""
+
+#: utils-translate.R:167
+msgid "Callout"
+msgstr ""
+
+#: utils-translate.R:168
+msgid "Challenge"
+msgstr ""
+
+#: utils-translate.R:169
+msgid "Prerequisite"
+msgstr ""
+
+#: utils-translate.R:170
+msgid "Checklist"
+msgstr ""
+
+#: utils-translate.R:171
+msgid "Discussion"
+msgstr ""
+
+#: utils-translate.R:172
+msgid "Testimonial"
+msgstr ""
+
+#: utils-translate.R:178
+msgid "Show me the solution"
+msgstr ""
+
+#: utils-translate.R:179
+msgid "Give me a hint"
+msgstr ""
+
+#: utils-translate.R:180
+msgid "Show details"
+msgstr ""
+
+#: utils-translate.R:181
+msgid "Instructor Note"
+msgstr ""
+
+#: utils-xml.R:134
+msgid "anchor"
+msgstr ""
+
+#: utils-xml.R:161
+msgid "Questions"
+msgstr "Preguntas"
+
+#: utils-xml.R:162
+msgid "Objectives"
+msgstr "Objetivos"
+
+#: utils-xml.R:163
+msgid "Overview"
+msgstr "Hoja de ruta"
diff --git a/po/R-ja.po b/po/R-ja.po
new file mode 100644
index 000000000..a66aebab2
--- /dev/null
+++ b/po/R-ja.po
@@ -0,0 +1,376 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: sandpaper 0.14.1.9000\n"
+"POT-Creation-Date: 2023-12-06 11:03-0800\n"
+"PO-Revision-Date: 2023-11-22 09:14+0900\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: ja\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: build_404.R:57
+msgid "Page not found"
+msgstr "ページが見つかりません"
+
+#: build_aio.R:6
+msgid "All in One View"
+msgstr "一つにまとまったページ"
+
+#: build_episode.R:134 utils-translate.R:82
+msgid "Home"
+msgstr "ホーム"
+
+#: build_home.R:49 utils-translate.R:195 utils-varnish.R:87
+msgid "Summary and Schedule"
+msgstr "概要とスケジュール"
+
+#: build_home.R:58 utils-translate.R:191 utils-varnish.R:95
+msgid "Summary and Setup"
+msgstr "概要とセットアップ"
+
+#: build_home.R:100
+msgid "Finish"
+msgstr "終わり"
+
+#: build_images.R:6
+msgid "All Images"
+msgstr "全ての画像"
+
+#: build_images.R:49
+msgid "Figure {element}"
+msgstr "図の{element}"
+
+#: build_images.R:60
+msgid "Image {i} of {n}: {sQuote(txt)}"
+msgstr "画像{i}/{n}: {sQuote(txt)}"
+
+#: build_instructor_notes.R:27 utils-translate.R:72
+msgid "Instructor Notes"
+msgstr "インストラクター用メモ"
+
+#: build_keypoints.R:6 utils-translate.R:71 utils-translate.R:173
+msgid "Key Points"
+msgstr "まとめ"
+
+#: build_profiles.R:16 utils-translate.R:74
+msgid "Learner Profiles"
+msgstr "学習者プロフィール"
+
+#: utils-translate.R:56
+msgid "Skip to main content"
+msgstr "本文へ移動"
+
+#: utils-translate.R:57
+msgid "Pre-Alpha"
+msgstr "プレアルファ"
+
+#: utils-translate.R:58
+msgid ""
+"This lesson is in the pre-alpha phase, which means that it is in early "
+"development, but has not yet been taught."
+msgstr ""
+"このレッスンはプレアルファフェーズ(開発が始まったが、まだ教えられたことがな"
+"い)にある。"
+
+#: utils-translate.R:59
+msgid ""
+"This lesson is in the alpha phase, which means that it has been taught once "
+"and lesson authors are iterating on feedback."
+msgstr ""
+"このレッスンはアルファフェーズ(一度教えられ、レッスン作成者がフィードバック"
+"を反復している)にある。"
+
+#: utils-translate.R:60
+msgid "Alpha"
+msgstr "アルファ"
+
+#: utils-translate.R:61
+msgid ""
+"This lesson is in the beta phase, which means that it is ready for teaching "
+"by instructors outside of the original author team."
+msgstr ""
+"このレッスンはベタフェーズ(原著者チーム以外の指導者にも教える準備が整ってい"
+"る)にある"
+
+#: utils-translate.R:62
+msgid "Beta"
+msgstr "ベタ"
+
+#: utils-translate.R:63
+msgid "This lesson has passed peer review."
+msgstr "このレッスンはピアレビューに合格しました"
+
+#: utils-translate.R:64
+msgid "Instructor View"
+msgstr "インストラクター表示"
+
+#: utils-translate.R:65
+msgid "Learner View"
+msgstr "学習者表示"
+
+#: utils-translate.R:66
+msgid "Main Navigation"
+msgstr "メインナビゲーション"
+
+#: utils-translate.R:67
+msgid "Toggle Navigation"
+msgstr "ナビゲーションの切り替え"
+
+#: utils-translate.R:68
+msgid "Menu"
+msgstr "メニュー"
+
+#: utils-translate.R:69
+msgid "search button"
+msgstr "検索ボタン"
+
+#: utils-translate.R:70
+msgid "Setup"
+msgstr "セットアップ"
+
+#: utils-translate.R:73
+msgid "Glossary"
+msgstr "用語集"
+
+#: utils-translate.R:75
+msgid "More"
+msgstr "その他"
+
+#: utils-translate.R:76
+msgid "Search"
+msgstr "検索"
+
+#: utils-translate.R:77
+msgid "Lesson Progress"
+msgstr "レッスンの進捗状況"
+
+#: utils-translate.R:80
+msgid "close menu"
+msgstr "メニューを閉じる"
+
+#: utils-translate.R:81
+msgid "EPISODES"
+msgstr "エピソード"
+
+#: utils-translate.R:83
+msgid "Home Page Navigation"
+msgstr "ホームページのナビゲーション"
+
+#: utils-translate.R:84
+msgid "RESOURCES"
+msgstr "資料"
+
+#: utils-translate.R:85
+msgid "Extract All Images"
+msgstr "全ての画像"
+
+#: utils-translate.R:86
+msgid "See all in one page"
+msgstr "一つにまとまったページ"
+
+#: utils-translate.R:87
+msgid "Download Lesson Handout"
+msgstr "レッスンのプリントをダウンロード"
+
+#: utils-translate.R:88
+msgid "Export Chapter Slides"
+msgstr "スライドのエキスポート"
+
+#: utils-translate.R:91
+msgid "Previous and Next Chapter"
+msgstr "前の章と次の章"
+
+#: utils-translate.R:92
+msgid "Previous"
+msgstr "前へ"
+
+#: utils-translate.R:93
+msgid "Estimated time: {icons$clock} {minutes} minutes"
+msgstr "所要時間:{icons$clock} {minutes}分"
+
+#: utils-translate.R:94
+msgid "Next"
+msgstr "次へ"
+
+#: utils-translate.R:95
+msgid "Next Chapter"
+msgstr "次のチャプター"
+
+#: utils-translate.R:96
+msgid "Last updated on {updated}"
+msgstr "最終更新日:{updated}"
+
+#: utils-translate.R:97
+msgid "Edit this page"
+msgstr "ページの編集"
+
+#: utils-translate.R:98 utils-translate.R:122
+msgid "Expand All Solutions"
+msgstr "回答を全て表示する"
+
+#: utils-translate.R:101
+msgid "Setup Instructions"
+msgstr "セットアップの手順"
+
+#: utils-translate.R:102
+msgid "Download files required for the lesson"
+msgstr "レッスンに必要なファイルのダウンロード"
+
+#: utils-translate.R:103
+msgid ""
+"The actual schedule may vary slightly depending on the topics and exercises "
+"chosen by the instructor."
+msgstr ""
+"実際のスケジュールは、講師が選んだトピックやエクササイズによって多少異なる場"
+"合があります。"
+
+#: utils-translate.R:106
+msgid "Back To Top"
+msgstr "上へ戻る"
+
+#: utils-translate.R:107
+msgid "<(Back)> To Top"
+msgstr "上へ<(戻る)>"
+
+#: utils-translate.R:108
+msgid "This lesson is subject to the <(Code of Conduct)>"
+msgstr "このレッスンは<(行動規範)>の対象となっている"
+
+#: utils-translate.R:109
+msgid "Code of Conduct"
+msgstr "行動規範"
+
+#: utils-translate.R:110
+msgid "Edit on GitHub"
+msgstr "GitHubで編集"
+
+#: utils-translate.R:111
+msgid "Contributing"
+msgstr "貢献"
+
+#: utils-translate.R:112
+msgid "Source"
+msgstr "ソース"
+
+#: utils-translate.R:113
+msgid "Cite"
+msgstr "引用"
+
+#: utils-translate.R:114
+msgid "Contact"
+msgstr "お問い合わせ"
+
+#: utils-translate.R:115
+msgid "About"
+msgstr "概要"
+
+#: utils-translate.R:116
+msgid "Materials licensed under {license} by {authors}"
+msgstr "材料は{authors}によって{license}のもとに掲載を許諾されている"
+
+#: utils-translate.R:117
+msgid "Template licensed under <(CC-BY 4.0)> by {template_authors}"
+msgstr ""
+"テンプレートは{template_authors}によって<(CC-BY 4.0)>のもとに掲載を許諾されて"
+"いる"
+
+#: utils-translate.R:118
+msgid "The Carpentries"
+msgstr "ザ・カーペントリーズ"
+
+#: utils-translate.R:119
+msgid "Built with {sandpaper_link}, {pegboard_link}, and {varnish_link}"
+msgstr ""
+"このレッスンは{sandpaper_link}、{pegboard_link}、および{varnish_link}によって"
+"作られた"
+
+#: utils-translate.R:123
+msgid "Collapse All Solutions"
+msgstr "回答を全て折りたたむ"
+
+#: utils-translate.R:124
+msgid "Collapse"
+msgstr "折りたたむ"
+
+#: utils-translate.R:125
+msgid "Episodes"
+msgstr "エピソード"
+
+#: utils-translate.R:128
+msgid "Give Feedback"
+msgstr "フィードバック"
+
+#: utils-translate.R:129
+msgid "Learn More"
+msgstr "さらに詳しく"
+
+#: utils-translate.R:158
+msgid "OUTPUT"
+msgstr "出力"
+
+#: utils-translate.R:159
+msgid "WARNING"
+msgstr "警告"
+
+#: utils-translate.R:160
+msgid "ERROR"
+msgstr "エラー"
+
+#: utils-translate.R:167
+msgid "Callout"
+msgstr "コールアウト"
+
+#: utils-translate.R:168
+msgid "Challenge"
+msgstr "チャレンジ"
+
+#: utils-translate.R:169
+msgid "Prerequisite"
+msgstr "必須条件"
+
+#: utils-translate.R:170
+msgid "Checklist"
+msgstr "チェックリスト"
+
+#: utils-translate.R:171
+msgid "Discussion"
+msgstr "ディスカッション"
+
+#: utils-translate.R:172
+msgid "Testimonial"
+msgstr "推薦文"
+
+#: utils-translate.R:178
+msgid "Show me the solution"
+msgstr "回答を表示する"
+
+#: utils-translate.R:179
+msgid "Give me a hint"
+msgstr "ヒントを表示する"
+
+#: utils-translate.R:180
+msgid "Show details"
+msgstr "詳細を表示する"
+
+#: utils-translate.R:181
+msgid "Instructor Note"
+msgstr "インストラクター用メモ"
+
+#: utils-xml.R:134
+msgid "anchor"
+msgstr "アンカー"
+
+#: utils-xml.R:161
+msgid "Questions"
+msgstr "質問"
+
+#: utils-xml.R:162
+msgid "Objectives"
+msgstr "目的"
+
+#: utils-xml.R:163
+msgid "Overview"
+msgstr "概要"
diff --git a/po/R-sandpaper.pot b/po/R-sandpaper.pot
new file mode 100644
index 000000000..b82bc9bb2
--- /dev/null
+++ b/po/R-sandpaper.pot
@@ -0,0 +1,363 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: sandpaper 0.15.0.9000\n"
+"POT-Creation-Date: 2023-12-06 11:03-0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: build_404.R:57
+msgid "Page not found"
+msgstr ""
+
+#: build_aio.R:6
+msgid "All in One View"
+msgstr ""
+
+#: build_episode.R:134 utils-translate.R:82
+msgid "Home"
+msgstr ""
+
+#: build_home.R:49 utils-translate.R:195 utils-varnish.R:87
+msgid "Summary and Schedule"
+msgstr ""
+
+#: build_home.R:58 utils-translate.R:191 utils-varnish.R:95
+msgid "Summary and Setup"
+msgstr ""
+
+#: build_home.R:100
+msgid "Finish"
+msgstr ""
+
+#: build_images.R:6
+msgid "All Images"
+msgstr ""
+
+#: build_images.R:49
+msgid "Figure {element}"
+msgstr ""
+
+#: build_images.R:60
+msgid "Image {i} of {n}: {sQuote(txt)}"
+msgstr ""
+
+#: build_instructor_notes.R:27 utils-translate.R:72
+msgid "Instructor Notes"
+msgstr ""
+
+#: build_keypoints.R:6 utils-translate.R:71 utils-translate.R:173
+msgid "Key Points"
+msgstr ""
+
+#: build_profiles.R:16 utils-translate.R:74
+msgid "Learner Profiles"
+msgstr ""
+
+#: utils-translate.R:56
+msgid "Skip to main content"
+msgstr ""
+
+#: utils-translate.R:57
+msgid "Pre-Alpha"
+msgstr ""
+
+#: utils-translate.R:58
+msgid ""
+"This lesson is in the pre-alpha phase, which means that it is in early "
+"development, but has not yet been taught."
+msgstr ""
+
+#: utils-translate.R:59
+msgid ""
+"This lesson is in the alpha phase, which means that it has been taught once "
+"and lesson authors are iterating on feedback."
+msgstr ""
+
+#: utils-translate.R:60
+msgid "Alpha"
+msgstr ""
+
+#: utils-translate.R:61
+msgid ""
+"This lesson is in the beta phase, which means that it is ready for teaching "
+"by instructors outside of the original author team."
+msgstr ""
+
+#: utils-translate.R:62
+msgid "Beta"
+msgstr ""
+
+#: utils-translate.R:63
+msgid "This lesson has passed peer review."
+msgstr ""
+
+#: utils-translate.R:64
+msgid "Instructor View"
+msgstr ""
+
+#: utils-translate.R:65
+msgid "Learner View"
+msgstr ""
+
+#: utils-translate.R:66
+msgid "Main Navigation"
+msgstr ""
+
+#: utils-translate.R:67
+msgid "Toggle Navigation"
+msgstr ""
+
+#: utils-translate.R:68
+msgid "Menu"
+msgstr ""
+
+#: utils-translate.R:69
+msgid "search button"
+msgstr ""
+
+#: utils-translate.R:70
+msgid "Setup"
+msgstr ""
+
+#: utils-translate.R:73
+msgid "Glossary"
+msgstr ""
+
+#: utils-translate.R:75
+msgid "More"
+msgstr ""
+
+#: utils-translate.R:76
+msgid "Search"
+msgstr ""
+
+#: utils-translate.R:77
+msgid "Lesson Progress"
+msgstr ""
+
+#: utils-translate.R:80
+msgid "close menu"
+msgstr ""
+
+#: utils-translate.R:81
+msgid "EPISODES"
+msgstr ""
+
+#: utils-translate.R:83
+msgid "Home Page Navigation"
+msgstr ""
+
+#: utils-translate.R:84
+msgid "RESOURCES"
+msgstr ""
+
+#: utils-translate.R:85
+msgid "Extract All Images"
+msgstr ""
+
+#: utils-translate.R:86
+msgid "See all in one page"
+msgstr ""
+
+#: utils-translate.R:87
+msgid "Download Lesson Handout"
+msgstr ""
+
+#: utils-translate.R:88
+msgid "Export Chapter Slides"
+msgstr ""
+
+#: utils-translate.R:91
+msgid "Previous and Next Chapter"
+msgstr ""
+
+#: utils-translate.R:92
+msgid "Previous"
+msgstr ""
+
+#: utils-translate.R:93
+msgid "Estimated time: {icons$clock} {minutes} minutes"
+msgstr ""
+
+#: utils-translate.R:94
+msgid "Next"
+msgstr ""
+
+#: utils-translate.R:95
+msgid "Next Chapter"
+msgstr ""
+
+#: utils-translate.R:96
+msgid "Last updated on {updated}"
+msgstr ""
+
+#: utils-translate.R:97
+msgid "Edit this page"
+msgstr ""
+
+#: utils-translate.R:98 utils-translate.R:122
+msgid "Expand All Solutions"
+msgstr ""
+
+#: utils-translate.R:101
+msgid "Setup Instructions"
+msgstr ""
+
+#: utils-translate.R:102
+msgid "Download files required for the lesson"
+msgstr ""
+
+#: utils-translate.R:103
+msgid ""
+"The actual schedule may vary slightly depending on the topics and exercises "
+"chosen by the instructor."
+msgstr ""
+
+#: utils-translate.R:106
+msgid "Back To Top"
+msgstr ""
+
+#: utils-translate.R:107
+msgid "<(Back)> To Top"
+msgstr ""
+
+#: utils-translate.R:108
+msgid "This lesson is subject to the <(Code of Conduct)>"
+msgstr ""
+
+#: utils-translate.R:109
+msgid "Code of Conduct"
+msgstr ""
+
+#: utils-translate.R:110
+msgid "Edit on GitHub"
+msgstr ""
+
+#: utils-translate.R:111
+msgid "Contributing"
+msgstr ""
+
+#: utils-translate.R:112
+msgid "Source"
+msgstr ""
+
+#: utils-translate.R:113
+msgid "Cite"
+msgstr ""
+
+#: utils-translate.R:114
+msgid "Contact"
+msgstr ""
+
+#: utils-translate.R:115
+msgid "About"
+msgstr ""
+
+#: utils-translate.R:116
+msgid "Materials licensed under {license} by {authors}"
+msgstr ""
+
+#: utils-translate.R:117
+msgid "Template licensed under <(CC-BY 4.0)> by {template_authors}"
+msgstr ""
+
+#: utils-translate.R:118
+msgid "The Carpentries"
+msgstr ""
+
+#: utils-translate.R:119
+msgid "Built with {sandpaper_link}, {pegboard_link}, and {varnish_link}"
+msgstr ""
+
+#: utils-translate.R:123
+msgid "Collapse All Solutions"
+msgstr ""
+
+#: utils-translate.R:124
+msgid "Collapse"
+msgstr ""
+
+#: utils-translate.R:125
+msgid "Episodes"
+msgstr ""
+
+#: utils-translate.R:128
+msgid "Give Feedback"
+msgstr ""
+
+#: utils-translate.R:129
+msgid "Learn More"
+msgstr ""
+
+#: utils-translate.R:158
+msgid "OUTPUT"
+msgstr ""
+
+#: utils-translate.R:159
+msgid "WARNING"
+msgstr ""
+
+#: utils-translate.R:160
+msgid "ERROR"
+msgstr ""
+
+#: utils-translate.R:167
+msgid "Callout"
+msgstr ""
+
+#: utils-translate.R:168
+msgid "Challenge"
+msgstr ""
+
+#: utils-translate.R:169
+msgid "Prerequisite"
+msgstr ""
+
+#: utils-translate.R:170
+msgid "Checklist"
+msgstr ""
+
+#: utils-translate.R:171
+msgid "Discussion"
+msgstr ""
+
+#: utils-translate.R:172
+msgid "Testimonial"
+msgstr ""
+
+#: utils-translate.R:178
+msgid "Show me the solution"
+msgstr ""
+
+#: utils-translate.R:179
+msgid "Give me a hint"
+msgstr ""
+
+#: utils-translate.R:180
+msgid "Show details"
+msgstr ""
+
+#: utils-translate.R:181
+msgid "Instructor Note"
+msgstr ""
+
+#: utils-xml.R:134
+msgid "anchor"
+msgstr ""
+
+#: utils-xml.R:161
+msgid "Questions"
+msgstr ""
+
+#: utils-xml.R:162
+msgid "Objectives"
+msgstr ""
+
+#: utils-xml.R:163
+msgid "Overview"
+msgstr ""
diff --git a/tests/testthat/test-build_html.R b/tests/testthat/test-build_html.R
index eb7c62687..654f9a89a 100644
--- a/tests/testthat/test-build_html.R
+++ b/tests/testthat/test-build_html.R
@@ -80,6 +80,9 @@ test_that("(#536) SANDPAPER_SITE envvar works as expected", {
expect_false(fs::file_exists(orig_profiles_learner))
expect_false(fs::file_exists(orig_profiles_instructor))
+ # needed to ensure the global translations are implemented.
+ set_language("en")
+
# after build, only the files constrolled by the SANDPAPER_SITE envvar should
# exist, but the ones in the default site should not exist.
build_profiles(new_pkg, quiet = TRUE)
@@ -102,6 +105,10 @@ test_that("[build_home()] works independently", {
fs::dir_create(built_dir)
fs::file_copy(fs::path(res, "index.md"), built_dir)
fs::file_copy(fs::path(res, "learners", "setup.md"), built_dir)
+
+ # needed to ensure the global translations are implemented.
+ set_language("en")
+
build_home(pkg, quiet = TRUE,
next_page = fs::path(res, "episodes", "introduction.Rmd")
)
@@ -174,6 +181,10 @@ test_that("[build_profiles()] works independently", {
skip_if_not(rmarkdown::pandoc_available("2.11"))
expect_false(fs::file_exists(fs::path(pkg$dst_path, "profiles.html")))
expect_false(fs::file_exists(fs::path(pkg$dst_path, "instructor", "profiles.html")))
+
+ # needed to ensure the global translations are implemented.
+ set_language("en")
+
build_profiles(pkg, quiet = TRUE)
expect_true(fs::file_exists(fs::path(pkg$dst_path, "profiles.html")))
expect_true(fs::file_exists(fs::path(pkg$dst_path, "instructor", "profiles.html")))
diff --git a/tests/testthat/test-translate.R b/tests/testthat/test-translate.R
new file mode 100644
index 000000000..28167c8d9
--- /dev/null
+++ b/tests/testthat/test-translate.R
@@ -0,0 +1,178 @@
+# Generate temporary lesson and set `lang: ja` in config.yaml
+tmp <- res <- restore_fixture()
+config_path <- fs::path(tmp, "config.yaml")
+config <- yaml::read_yaml(config_path)
+config$lang <- "ja"
+yaml::write_yaml(config, config_path)
+sitepath <- fs::path(tmp, "site", "docs")
+
+
+
+test_that("set_language() uses english by default", {
+
+ os <- tolower(Sys.info()[["sysname"]])
+ ver <- getRversion()
+ skip_if(os == "windows" && ver < "4.2")
+
+ # default is english
+ set_language()
+ expect_equal(tr_("OUTPUT"), "OUTPUT")
+
+ # set to japanese and it becomes different
+ set_language("ja")
+ OUTJA <- tr_("OUTPUT")
+ expect_false(identical(OUTJA, "OUTPUT"))
+
+ # unknown language will not switch the current language
+ suppressMessages(expect_message(set_language("xx"), "languages"))
+ expect_equal(tr_("OUTPUT"), OUTJA)
+
+ # set back to english (default)
+ set_language()
+ expect_equal(tr_("OUTPUT"), "OUTPUT")
+
+})
+
+
+test_that("is_known_language returns a warning for an unknown language", {
+
+ os <- tolower(Sys.info()[["sysname"]])
+ ver <- getRversion()
+ skip_if(os == "windows" && ver < "4.2")
+
+ expect_true(is_known_language("ja"))
+ expect_false(is_known_language("xx"))
+ suppressMessages({
+ expect_message({
+ expect_false(is_known_language("xx", warn = TRUE))
+ }, "languages", label = "is_known_language(warn = TRUE)")
+ })
+
+})
+
+
+test_that("Lessons can be translated with lang setting", {
+
+ skip_if_not(rmarkdown::pandoc_available("2.11"))
+
+ os <- tolower(Sys.info()[["sysname"]])
+ ver <- getRversion()
+ skip_if(os == "windows" && ver < "4.2")
+
+ # Build lesson
+ suppressMessages(build_lesson(tmp, preview = FALSE, quiet = TRUE))
+
+ # Extract first header (Summary and Setup) from index
+ xml <- xml2::read_html(fs::path(sitepath, "index.html"))
+ h1_header <- xml2::xml_find_all(xml, "//h1[@class='schedule-heading']")
+
+ # language should be set to japanese
+ expect_equal(xml2::xml_attr(xml, "lang"), "ja")
+
+ # Header should be translated to Japanese
+ expect_true(
+ identical(
+ xml2::xml_text(h1_header),
+ withr::with_language("ja", tr_("Summary and Setup"))
+ )
+ )
+
+ # Header should no longer be in English
+ expect_false(
+ identical(
+ xml2::xml_text(h1_header),
+ withr::with_language("en", tr_("Summary and Setup"))
+ )
+ )
+
+ # aria labels should be translated
+ arias <- c("Main Navigation", "Toggle Navigation", "Search", "search button",
+ "Lesson Progress", "close menu", "Next Chapter", "anchor", "Back To Top")
+ ja_arias <- withr::with_language("ja", vapply(arias, tr_, character(1)))
+
+ expect_false(identical(arias, ja_arias))
+
+ expect_setequal(
+ ja_arias,
+ xml2::xml_text(xml2::xml_find_all(xml, ".//@aria-label"))
+ )
+
+ # Episode elements -------------------------------------------------
+ # We use here the Instructor view because it is more fully featured
+ xml <- xml2::read_html(fs::path(sitepath, "instructor", "introduction.html"))
+
+ # overview, objectives, and questions
+ overview_card <- xml2::xml_find_first(xml, ".//div[@class='overview card']")
+
+ # Overview card
+ overview <- xml2::xml_find_first(overview_card, ".//h2[@class='card-header']")
+ expect_equal(
+ xml2::xml_text(overview, trim = TRUE),
+ withr::with_language("ja", tr_("Overview"))
+ )
+ expect_false(
+ identical(
+ xml2::xml_text(overview, trim = TRUE),
+ withr::with_language("en", tr_("Overview"))
+ )
+ )
+
+ # Questions and Objectives
+ quob <- xml2::xml_find_all(overview_card, ".//h3[@class='card-title']")
+ expect_equal(
+ xml2::xml_text(quob, trim = TRUE),
+ withr::with_language("ja", c(tr_("Questions"), tr_("Objectives")))
+ )
+ expect_false(
+ identical(
+ xml2::xml_text(quob, trim = TRUE),
+ withr::with_language("en", c(tr_("Questions"), tr_("Objectives")))
+ )
+ )
+
+ # Keypoints are always the last block and should be auto-translated
+ xpath_keypoints <- ".//div[@class='callout keypoints']//h3[@class='callout-title']"
+ keypoints <- xml2::xml_find_first(xml, xpath_keypoints)
+ expect_equal(
+ xml2::xml_text(keypoints, trim = TRUE),
+ withr::with_language("ja", tr_("Key Points"))
+ )
+ expect_false(
+ identical(
+ xml2::xml_text(keypoints, trim = TRUE),
+ withr::with_language("en", tr_("Key Points"))
+ )
+ )
+
+ # Instructor note headings should be translated
+ xpath_instructor <- ".//div[@class='accordion-item']/button/h3"
+ instructor_note <- xml2::xml_find_all(xml, xpath_instructor)
+ expect_equal(
+ xml2::xml_text(instructor_note, trim = TRUE),
+ withr::with_language("ja", tr_("Instructor Note"))
+ )
+ expect_false(
+ identical(
+ xml2::xml_text(instructor_note, trim = TRUE),
+ withr::with_language("en", tr_("Instructor Note"))
+ )
+ )
+
+ # solution headings should be translated
+ xpath_solution <- ".//div[@class='accordion-item']/button/h4"
+ solution <- xml2::xml_find_all(xml, xpath_solution)
+ # take the last solution block because that's the one that does not have
+ # a title.
+ solution <- solution[[length(solution)]]
+ expect_equal(
+ xml2::xml_text(solution, trim = TRUE),
+ withr::with_language("ja", tr_("Show me the solution"))
+ )
+ expect_false(
+ identical(
+ xml2::xml_text(solution, trim = TRUE),
+ withr::with_language("en", tr_("Show me the solution"))
+ )
+ )
+
+})
diff --git a/tests/testthat/test-utils-xml.R b/tests/testthat/test-utils-xml.R
index f8e2d64fe..4bd01e112 100644
--- a/tests/testthat/test-utils-xml.R
+++ b/tests/testthat/test-utils-xml.R
@@ -107,6 +107,60 @@ test_that("setup links with anchors are respected", {
})
+test_that("code block languages are in the correct order", {
+
+ # SETUP ---------------------------------------------------
+ html <- '
+ '
+
+ nodes <- xml2::read_html(html)
+
+ # ABSENCE TESTS -------------------------------------------
+ # By default, there are no h3 headings
+ expect_length(xml2::xml_find_all(nodes, ".//h3"), 0L)
+ xpath_codewrap <- ".//div[@class='codewrapper sourceCode']"
+ expect_length(xml2::xml_find_all(nodes, xpath_codewrap), 0L)
+ xpath_pre_tabindex <- ".//pre[@tabindex]"
+ expect_length(xml2::xml_find_all(nodes, xpath_pre_tabindex), 0L)
+
+ fix_codeblocks(nodes)
+
+ # PRESENCE TESTS ------------------------------------------
+ expect_length(xml2::xml_find_all(nodes, ".//h3"), 2L)
+ expect_length(xml2::xml_find_all(nodes, xpath_codewrap), 2L)
+ expect_length(xml2::xml_find_all(nodes, xpath_pre_tabindex), 2L)
+
+ # after fixing, the h3 headings should be in the correct order
+ heading_text <- xml2::xml_text(xml2::xml_find_all(nodes, ".//h3"))
+ expect_equal(heading_text, c("BASH", "R"))
+
+ # divs now have two children
+ first_div <- xml2::xml_find_first(nodes, ".//div")
+ expect_equal(xml2::xml_name(xml2::xml_children(first_div)), c("h3", "pre"))
+
+ # the first pre block is a child of the first div
+ first_pre <- xml2::xml_find_first(nodes, ".//pre")
+ # the parent is _identical_ to the first_div because of how the xml2 package
+ # works
+ expect_identical(xml2::xml_parent(first_pre), first_div)
+ expect_equal(xml2::xml_attr(first_pre, "class"), "sourceCode bash")
+ # a tabindex attribute is added
+ expect_equal(xml2::xml_attr(first_pre, "tabindex"), "0")
+
+})
+
+
+
test_that("empty args result in nothing happening", {
expect_null(fix_nodes())
expect_null(fix_setup_link())
diff --git a/vignettes/translations.Rmd b/vignettes/translations.Rmd
new file mode 100644
index 000000000..ca56118d6
--- /dev/null
+++ b/vignettes/translations.Rmd
@@ -0,0 +1,188 @@
+---
+title: "Translating The Workbench"
+output: rmarkdown::html_vignette
+vignette: >
+ %\VignetteIndexEntry{Translating The Workbench}
+ %\VignetteEngine{knitr::rmarkdown}
+ %\VignetteEncoding{UTF-8}
+---
+
+```{r, include = FALSE}
+knitr::opts_chunk$set(
+ collapse = TRUE,
+ comment = "#>"
+)
+```
+
+## Introduction
+
+The philosophy of The Carpentries Workbench is one of separation between lesson
+content and the tooling needed to transform that content into a website. It is
+possible to write a lesson in any human language that has a syllabary which can
+be represented on a computer. The only catch is: by default the language of the
+_website template_---all the navigational elements of the website---is English,
+so authors need to tell The Workbench what language the website template should
+use.
+
+To write a lesson in a specific language, the lesson author should add `lang:
+'xx'` to the `config.yaml` file where `xx` represents the [language
+code](https://www.gnu.org/software/gettext/manual/html_node/Usual-Language-Codes.html)
+that matches the language of the lesson content. This defaults to `"en"`, but
+can be any language code (e.g. "ja" specifying Japanese) or combination
+language code and [country
+code](https://www.gnu.org/software/gettext/manual/html_node/Country-Codes.html)
+(e.g. "pt_BR" specifies Pourtugese used in Brazil). For more information on how
+this is used, see [the Locale Names section of the gettext
+manual](https://www.gnu.org/software/gettext/manual/html_node/Locale-Names.html).
+
+Setting the `lang:` keyword will allow the lesson navigational elements of the
+website template to be presented in the same language as the lesson content _if
+the language has been added to {sandpaper}_. If not, the menu items will appear
+in English.
+
+This vignette is of interest to those who wish to update translations or add new
+translations. In this vignette I will provide resources for updating and adding
+new languages, the process by which translation happens, and I will outline
+special syntax used in {sandpaper}. This process will use the [{potools}
+package](https://michaelchirico.github.io/potools/), which requires the [GNU
+gettext system](https://www.gnu.org/software/gettext/).
+
+## Resources
+
+The documentation for the {potools} package is a wonderful resource. Use
+`vignette("translators", package = "potools")` to read details about what to
+consider when translating text in a package. Another really good resource is [a
+blog post by Maëlle Salmon](https://masalmon.eu/2023/10/06/potools-mwe/) which
+gives a minimum working example of translating package messages using {potools}.
+
+If you are interested in translating _lesson content_, please consult tools such
+as Joel Nitta's [{dovetail}](https://github.com/joelnitta/dovetail#readme) for
+providing a method for translators to track and deploy translations of
+Carpentries lessons. You can also use rOpenSci's
+[{babeldown}](https://docs.ropensci.org/babeldown/), which uses the DeepL API
+for automated translation that translators can edit afterwards.
+
+## Translating in {sandpaper}
+
+The translations from {sandpaper} are mostly shuffled off to {varnish}, where it
+has template variables written in mustache templating. These variables define
+visible menu text such as "Key Points" and screen-reader accessible text for
+buttons such as "close menu".
+
+Translating in {sandpaper}, you will be working with `.po` files that live in
+[the po/ folder](https://github.com/carpentries/sandpaper/tree/HEAD/po) in the
+source of this package. There is one `.po` file per language translated and the
+syntax looks like this, where the first line shows the file where the
+translation exists, the second line gives the message in English, and the third
+line gives the translation:
+
+```po
+#: build_404.R:57
+msgid "Page not found"
+msgstr "ページが見つかりません"
+```
+
+These po files are compiled into binary `.mo` files that are carried with the
+built package on to the user's computer. These files are used by the R function
+`base::gettext()` to translate messages in a specific context. The context for
+{sandpaper} is known as `R-sandpaper`:
+
+
+```{r gettext}
+library("withr")
+library("sandpaper")
+known_languages()
+with_language("ja", {
+ enc2utf8(gettext("Page not found", domain = "R-sandpaper"))
+})
+with_language("en", {
+ enc2utf8(gettext("Page not found", domain = "R-sandpaper"))
+})
+```
+
+If a language does not exist, it will revert to English:
+
+```{r gettext-2}
+with_language("xx", {
+ enc2utf8(gettext("Page not found", domain = "R-sandpaper"))
+})
+```
+
+To make translation keys easier to detect, a convenience function, `tr_()` has
+been defined, so if you want to find the context for a given translation key,
+you can find it by searching the source code for `tr_`.
+
+### Special syntax for translators
+
+Some content for translation requires variables or markup to be added after
+translation.
+
+Items in `{curly_braces}` are variables and should remain in English:
+
+```po
+#: utils-translate.R:52
+msgid "Estimated time: {icons$clock} {minutes} minutes"
+msgstr "所要時間:{icons$clock} {minutes}分"
+```
+
+Words in `<(kirby quotes)>` will have HTML markup surrounding them and should be
+translated:
+
+```
+#: utils-translate.R:62
+msgid "This lesson is subject to the <(Code of Conduct)>"
+msgstr "このレッスンは<(行動規範)>の対象となります"
+```
+
+### Updates to translations
+
+There may be times in the future where translations will need to be updated
+because text changes or is added. When this happens, the maintainer of
+{sandpaper} will run the following commands to extract the new translation
+strings, update all languages, and recompile the `.mo` files for the built
+package.
+
+```r
+potools::po_extract()
+potools::po_update()
+potools::po_compile()
+```
+
+When the languages are updated, the translation utility will attempt to make
+fuzzy matches or create new strings. For example, if we update the "Page not
+found" translation to be title case, add punctuation and a little whimsy to be
+`"Page? Not Found! -_-;"`, when you go to edit your translation, you
+might see something like this:
+
+```po
+#: build_404.R:57
+#, fuzzy
+#| msgid "Page not found"
+msgid "Page? Not Found! -_-;"
+msgstr "ページが見つかりません"
+```
+
+The old translation will be used until a translator updates it and runs
+`potools::po_compile()` to update the `.mo` files.
+
+When new strings for translations are added, the translation utility does not
+assume to know anything about translation and the will appear like so:
+
+
+```po
+#: build_404.R:57
+msgid "A new translation approaches!"
+msgstr ""
+```
+
+If no translation is available for a given string, it will default to the string
+itself:
+
+```{r default-string}
+with_language("ja", {
+ enc2utf8(gettext("A new translation approaches!", domain = "R-sandpaper"))
+})
+```
+
+
+