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 <- "

{title}{anchor}

" 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 <- '
+
+  shello
+  
+  
+
+
+
+  letters
+  
+  
+
' + + 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")) +}) +``` + + +