diff --git a/DESCRIPTION b/DESCRIPTION index f5dc4429..afec466c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -19,6 +19,7 @@ Imports: Suggests: crayon, knitr, + mockery, rmarkdown, testthat (>= 3.0.0), withr diff --git a/R/find_fuzzy_messages.R b/R/find_fuzzy_messages.R index 6ae96254..c51e5a1f 100644 --- a/R/find_fuzzy_messages.R +++ b/R/find_fuzzy_messages.R @@ -2,7 +2,7 @@ find_fuzzy_messages <- function(message_data, lang_file) { old_message_data = get_po_messages(lang_file) if (any(idx <- old_message_data$fuzzy == 2L)) { - messagef('Found %d translations marked as deprecated in %s.', sum(idx), lang_file) + messagef('Found %d translations marked as deprecated in ./po/%s.', sum(idx), basename(lang_file)) message('Typically, this means the corresponding error messages have been refactored.') message('Reproducing these messages here for your reference since they might still provide some utility.') @@ -10,13 +10,13 @@ find_fuzzy_messages <- function(message_data, lang_file) { old_message_data[idx & type == 'singular', { if (.N > 0L) { message(' ** SINGULAR MESSAGES **') - cat(rbind(dashes, msgid, msgstr), sep='\n') + writeLines(rbind(dashes, msgid, msgstr), useBytes=TRUE) } }] old_message_data[idx & type == 'plural', { if (.N > 0L) { message(' ** PLURAL MESSAGES **') - cat(do.call(rbind, c(list(dashes), msgid_plural, msgstr_plural)), sep='\n') + writeLines(do.call(rbind, c(list(dashes), msgid_plural, msgstr_plural)), useBytes=TRUE) } }] diff --git a/R/get_po_messages.R b/R/get_po_messages.R index c8f4b2a6..9958a15b 100644 --- a/R/get_po_messages.R +++ b/R/get_po_messages.R @@ -123,12 +123,14 @@ get_po_messages <- function(po_file) { msg_j = msg_j + 1L } - # somewhat hacky approach -- strip the comment markers & recurse - writeLines( + # somewhat hacky approach -- strip the comment markers & recurse. + # beware of potential encoding dragons. + + tmp_conn <- write_utf8( gsub("^#~ ", "", grep("^#~ ", po_lines, value = TRUE)), tmp <- tempfile() ) - on.exit(unlink(tmp)) + on.exit({ close(tmp_conn); unlink(tmp) }) deprecated = get_po_messages(tmp) if (nrow(deprecated) > 0L) { set(deprecated, NULL, 'fuzzy', 2L) diff --git a/R/msgmerge.R b/R/msgmerge.R index f4510c04..c7091b90 100644 --- a/R/msgmerge.R +++ b/R/msgmerge.R @@ -26,35 +26,36 @@ run_msgmerge <- function(po_file, pot_file, previous = FALSE, verbose = TRUE) { } run_msgfmt = function(po_file, mo_file, verbose) { - use_stats <- if (verbose) '--statistics' else '' - - po_file <- path.expand(po_file) - mo_file <- path.expand(mo_file) - - # See #218. Solaris msgfmt doesn't support -c or --statistics - if (Sys.info()[["sysname"]] == "SunOS") { - cmd = sprintf("msgfmt -o %s %s", shQuote(mo_file), shQuote(po_file)) # nocov - } else { - cmd = sprintf("msgfmt -c %s -o %s %s", use_stats, shQuote(mo_file), shQuote(po_file)) + # See #218. Solaris msgfmt (non-GNU on CRAN) doesn't support --check or --statistics; + # see also https://bugs.r-project.org/show_bug.cgi?id=18150 + args = character() + if (is_gnu_gettext()) { + args = c("--check", if (verbose) '--statistics') } - if (system(cmd) != 0L) { + val = system2("msgfmt", c(args, "-o", shQuote(mo_file), shQuote(po_file)), stdout = TRUE, stderr = TRUE) + if (!identical(attr(val, "status", exact = TRUE), NULL)) { warningf( - "running msgfmt on %s failed.\nHere is the po file:\n%s", - basename(po_file), paste(readLines(po_file), collapse = "\n"), + "running msgfmt on %s failed; output:\n %s\nHere is the po file:\n%s", + basename(po_file), paste(val, collapse = "\n"), paste(readLines(po_file), collapse = "\n"), immediate. = TRUE ) + } else if (verbose) { + messagef( + "running msgfmt on %s succeeded; output:\n %s", + basename(po_file), paste(val, collapse = "\n") + ) } return(invisible()) } - - update_en_quot_mo_files <- function(dir, verbose) { pot_files <- list.files(file.path(dir, "po"), pattern = "\\.pot$", full.names = TRUE) mo_dir <- file.path(dir, "inst", "po", "en@quot", "LC_MESSAGES") dir.create(mo_dir, recursive = TRUE, showWarnings = FALSE) for (pot_file in pot_files) { - po_file <- tempfile() + # don't use tempfile() -- want a static basename() to keep verbose output non-random + po_file <- file.path(tempdir(), if (startsWith(basename(pot_file), "R-")) "R-en@quot.po" else "en@quot.po") + on.exit(unlink(po_file)) # tools:::en_quote is blocked, but we still need it for now get("en_quote", envir=asNamespace("tools"))(pot_file, po_file) run_msgfmt( @@ -62,7 +63,6 @@ update_en_quot_mo_files <- function(dir, verbose) { mo_file = file.path(mo_dir, gsub("\\.pot$", ".mo", basename(pot_file))), verbose = verbose ) - unlink(po_file) } return(invisible()) } diff --git a/R/onLoad.R b/R/onLoad.R index 5870c072..aba34f02 100644 --- a/R/onLoad.R +++ b/R/onLoad.R @@ -20,20 +20,18 @@ globalVariables( ) .potools = new.env() - -if (requireNamespace('crayon', quietly = TRUE)) { - call_color = getOption('potools.call_color', crayon::green) - file_color = getOption('potools.file_color', crayon::white) - msgid_color = getOption('potools.msgid_color', crayon::red) - language_color = getOption('potools.language_color', crayon::cyan) - replacement_color = getOption('potools.replacement_color', crayon::blue) - plural_range_color = getOption('potools.plural_range_color', crayon::yellow) -} else { - call_color = file_color = msgid_color = language_color = replacement_color = plural_range_color = identity -} +call_color = file_color = msgid_color = language_color = replacement_color = plural_range_color = identity # nocov start .onLoad = function(libname, pkgname) { .potools$base_package_names = get(".get_standard_package_names", envir=asNamespace("tools"), mode="function")()$base + if (getOption('potools.use_colors', requireNamespace('crayon', quietly = TRUE))) { + utils::assignInMyNamespace("call_color", getOption('potools.call_color', crayon::green)) + utils::assignInMyNamespace("file_color", getOption('potools.file_color', crayon::white)) + utils::assignInMyNamespace("msgid_color", getOption('potools.msgid_color', crayon::red)) + utils::assignInMyNamespace("language_color", getOption('potools.language_color', crayon::cyan)) + utils::assignInMyNamespace("replacement_color", getOption('potools.replacement_color', crayon::blue)) + utils::assignInMyNamespace("plural_range_color", getOption('potools.plural_range_color', crayon::yellow)) + } } # nocov end diff --git a/R/translate_package.R b/R/translate_package.R index a96d30fe..f53d6245 100644 --- a/R/translate_package.R +++ b/R/translate_package.R @@ -274,7 +274,7 @@ translate_package = function( messagef( "Updating translation template for package '%s' (last updated %s)", package, - format(file.info(r_potfile)$atime) + get_atime(r_potfile) ) } else { messagef("Starting translations for package '%s'", package) @@ -339,11 +339,11 @@ translate_package = function( if (update && file.exists(lang_file)) { if (verbose) { messagef( - 'Found existing R translations for %s (%s/%s) in %s. Running msgmerge...', - language, metadata$full_name_eng, metadata$full_name_native, lang_file + 'Found existing R translations for %s (%s/%s) in ./po/%s. Running msgmerge...', + language, metadata$full_name_eng, metadata$full_name_native, basename(lang_file) ) } - run_msgmerge(lang_file, r_potfile) + run_msgmerge(lang_file, r_potfile, verbose) find_fuzzy_messages(message_data, lang_file) } else { @@ -354,11 +354,11 @@ translate_package = function( if (update && file.exists(lang_file)) { if (verbose) { messagef( - 'Found existing src translations for %s (%s/%s) in %s. Running msgmerge...', - language, metadata$full_name_eng, metadata$full_name_native, lang_file + 'Found existing src translations for %s (%s/%s) in ./po/%s. Running msgmerge...', + language, metadata$full_name_eng, metadata$full_name_native, basename(lang_file) ) } - run_msgmerge(lang_file, src_potfile) + run_msgmerge(lang_file, src_potfile, verbose) find_fuzzy_messages(message_data, lang_file) } else { @@ -478,39 +478,35 @@ invisible({ gettext("when n is not 1") }) -# take from those present in r-devel: -# ls -1 ~/svn/R-devel/src/library/*/po/*.po | \ -# awk -F"[./]" '{print $10}' | \ -# sed -r 's/^R(Gui)?-//g' | sort -u | \ -# awk '{print " ", $1, " = ,"}' -# alternatively, a more complete list can be found on some websites: -# https://saimana.com/list-of-country-locale-code/ -# nplurals,plural info from https://l10n.gnome.org/teams/ -# NB: looks may be deceiving for right-to-left scripts (e.g. Farsi), where the -# displayed below might not be in the order it is parsed. -# assign to .potools, not a package env, to keep more readily mutable inside update_metadata() -.potools$KNOWN_LANGUAGES = fread(system.file('extdata', 'language_metadata.csv', package='potools'), key='code') - -# the 'plural' column above is designed for computers; -# translate that to something human-legible here. -# NB: 'plural' is 0-based (like in the .po file), but -# 'plural_index' is 1-based (to match the above R-level code). -# assign to .potools, not a package env, to keep more readily mutable inside update_metadata() -.potools$PLURAL_RANGE_STRINGS = fread( - system.file('extdata', 'plurals_metadata.csv', package='potools'), - key = c('plural', 'plural_index') -) - # for testing; unexported -# nocov start reset_language_metadata = function() { + # initially taken from those present in r-devel: + # ls -1 ~/svn/R-devel/src/library/*/po/*.po | \ + # awk -F"[./]" '{print $10}' | \ + # sed -r 's/^R(Gui)?-//g' | sort -u | \ + # awk '{print " ", $1, " = ,"}' + # for extension, a more complete list can be found on some websites: + # https://saimana.com/list-of-country-locale-code/ + # nplurals,plural info from https://l10n.gnome.org/teams/ + # NB: looks may be deceiving for right-to-left scripts (e.g. Farsi), where the + # displayed below might not be in the order it is parsed. + # assign to .potools, not a package env, to keep more readily mutable + # inside update_metadata() & elsewhere .potools$KNOWN_LANGUAGES = fread( system.file('extdata', 'language_metadata.csv', package='potools'), - key='code' + # encoding is required for the full_name_native field to display well on Windows + encoding = "UTF-8", + key = 'code' ) + # the 'plural' column above is designed for computers; + # translate that to something human-legible here. + # NB: 'plural' is 0-based (like in the .po file), but + # 'plural_index' is 1-based (to match the above R-level code). + # assign to .potools, not a package env, to keep more readily mutable inside update_metadata() .potools$PLURAL_RANGE_STRINGS = fread( system.file('extdata', 'plurals_metadata.csv', package='potools'), key = c('plural', 'plural_index') ) } -# nocov end + +reset_language_metadata() diff --git a/R/utils.R b/R/utils.R index 5dc64d20..01a2b73f 100644 --- a/R/utils.R +++ b/R/utils.R @@ -190,3 +190,23 @@ is_outdated <- function(src, dst) { } is_testing = function() identical(Sys.getenv("TESTTHAT"), "true") + +is_gnu_gettext = function() any(grepl("GNU gettext", system('gettext --version', intern=TRUE))) + +# wrapper function to facilitate mocking, else tests --> stochastic output +get_atime <- function(f) format(file.info(f, extra_cols = FALSE)$atime) # nocov. Always mocked in tests. + +# a safer version of writeLines that's better at avoiding encoding hell +# cat seems to fail at writing UTF-8 on Windows; useBytes should do the trick instead: +# https://stackoverflow.com/q/10675360 +# "native.enc" supplied explicitly thanks to: +# https://kevinushey.github.io/blog/2018/02/21/string-encoding-and-r/ +write_utf8 <- function(lines, file, open = "w+") { + if (!inherits(file, "connection")) { + file <- file(file, open, encoding = "native.enc") + } + + writeLines(lines, file, useBytes = TRUE) + # don't necessarily close; let calling env do so + return(file) +} diff --git a/R/write_po_file.R b/R/write_po_file.R index 942d7126..da8ea54f 100644 --- a/R/write_po_file.R +++ b/R/write_po_file.R @@ -158,6 +158,7 @@ write_po_files <- function(message_data, po_dir, params, template = FALSE, use_b #' tmp_po <- tempfile(fileext = '.po'), #' metadata #' ) +#' # NB: in general, beware of encoding in this snippet #' writeLines(readLines(tmp_po)) #' #' # write .pot template @@ -181,18 +182,14 @@ write_po_file <- function( template = endsWith(po_file, ".pot") - # cat seems to fail at writing UTF-8 on Windows; useBytes should do the trick instead: - # https://stackoverflow.com/q/10675360 - po_conn = file(po_file, "wb") - on.exit(close(po_conn)) - po_header = format( metadata, template = template, use_plurals = any(message_data$type == "plural") ) - writeLines(con=po_conn, useBytes=TRUE, po_header) + po_conn <- write_utf8(po_header, po_file, "wb") + on.exit(close(po_conn)) # drop untranslated strings, collapse duplicates, drop unneeded data. # for now, treating R & src separately so they can be treated differently; eventually this should @@ -286,7 +283,7 @@ write_po_file <- function( ) } - writeLines(con=po_conn, useBytes=TRUE, out_lines) + write_utf8(out_lines, po_conn) }] } diff --git a/tests/testthat.R b/tests/testthat.R index 7551cfa4..fd0ed62c 100755 --- a/tests/testthat.R +++ b/tests/testthat.R @@ -1,4 +1,13 @@ +library(withr) library(testthat) +old = options( + potools.use_colors = FALSE, + width = 80L, + warnPartialMatchArgs = TRUE, + warnPartialMatchAttr = TRUE, + warnPartialMatchDollar = TRUE +) +withr::local_envvar("_R_CHECK_LENGTH_1_CONDITION_" = "true") library(potools) # Failed on Solaris because the command-line tools are missing there (which means tools doesn't work there), #186 @@ -8,3 +17,4 @@ if (isTRUE(check_potools_sys_reqs())) { } else { writeLines("Skipping tests on system without gettext installed") } +options(old) diff --git a/tests/testthat/_snaps/translate-package.md b/tests/testthat/_snaps/translate-package.md new file mode 100644 index 00000000..b4102057 --- /dev/null +++ b/tests/testthat/_snaps/translate-package.md @@ -0,0 +1,1274 @@ +# translate_package arg checking errors work + + Code + translate_package() + +--- + + Code + translate_package(verbose = TRUE) + Message + Starting translations for package 'noMsg' + Getting R-level messages. + No messages to translate; finishing + +--- + + Code + translate_package(verbose = TRUE) + Message + Starting translations for package 'rDataPkg' + No messages to translate; finishing + +--- + + Code + translate_package(verbose = TRUE) + Message + Updating translation template for package 'rMsg' (last updated 0000-01-01 00:00:00) + Getting R-level messages. + Running message diagnostics. + Writing R-rMsg.pot + + + No languages provided; finishing + +--- + + Code + translate_package(languages = "zh_CN", verbose = TRUE) + Message + Updating translation template for package 'rMsg' (last updated 0000-01-01 00:00:00) + Getting R-level messages. + Running message diagnostics. + Writing R-rMsg.pot + + + Beginning new translations for zh_CN (Mainland Chinese/普通话); found 6 untranslated messages + (To quit translating, press 'Esc'; progress will be saved) + Output + Thanks! Who should be credited with these translations? + And what is their email? + Message + *************************** + ** BEGINNING TRANSLATION ** + *************************** + + Some helpful reminders: + * You can skip a translation by entering nothing (just press RETURN) + * Special characters (like newlines, \n, or tabs, \t) should be written just like that (with an escape) + * Be sure to match message templates. The count of templates (%s, %d, etc.) must match in all languages, as must initial and terminal newlines (\n) + * While the count of templates must match, the _order_ can be changed by using e.g. %2$s to mean 'use the second input as a string here' + * Whenever templates or escaping is happening in a string, these will be 'highlighted' by carets (^) in the line below + Output + + File: foo.R + Call: base::warning("I warned you!") + Message: I warned you! + + How would you translate this message into Mainland Chinese? + + File: foo.R + Call: stop("Oh no you don't!") + Message: Oh no you don't! + + How would you translate this message into Mainland Chinese? + + File: foo.R + Call: gettext("Hi there") + Message: Hi there + + How would you translate this message into Mainland Chinese? + + File: foo.R + Call: gettextf( fmt = "Avg cat() failures: %.02f; N failures: %d; failure: %s", mean(x), length(x), "don't translate me" ) + Message: Avg cat() failures: %.02f; N failures: %d; failure: %s + ^---^ ^^ ^^ + How would you translate this message into Mainland Chinese? + + File: windows/bar.R + Call: ngettext(length(x), msg1 = "small fail", msg2 = "big fail") + Plural message: small fail + + How would you translate this message into Mainland Chinese independently of n? + Message + Writing R-zh_CN.po + Recompiling 'zh_CN' R translation + running msgfmt on R-zh_CN.po succeeded; output: + 5 translated messages. + +--- + + Code + translate_package(languages = "fa", verbose = TRUE) + Message + Updating translation template for package 'rMsg' (last updated 0000-01-01 00:00:00) + Getting R-level messages. + Running message diagnostics. + Writing R-rMsg.pot + + + Found existing R translations for fa (Farsi/فارسی) in ./po/R-fa.po. Running msgmerge. + . done. + Translations for fa are up to date! Skipping. + Recompiling 'fa' R translation + running msgfmt on R-fa.po succeeded; output: + 5 translated messages. + +--- + + Code + translate_package(languages = "zh_CN", verbose = TRUE) + Message + Updating translation template for package 'rFuzzyMsg' (last updated 0000-01-01 00:00:00) + Getting R-level messages. + Running message diagnostics. + Writing R-rFuzzyMsg.pot + + + Found existing R translations for zh_CN (Mainland Chinese/普通话) in ./po/R-zh_CN.po. Running msgmerge. + . done. + Beginning new translations for zh_CN (Mainland Chinese/普通话); found 2 untranslated messages + (To quit translating, press 'Esc'; progress will be saved) + Output + Thanks! Who should be credited with these translations? + And what is their email? + Message + *************************** + ** BEGINNING TRANSLATION ** + *************************** + + Some helpful reminders: + * You can skip a translation by entering nothing (just press RETURN) + * Special characters (like newlines, \n, or tabs, \t) should be written just like that (with an escape) + * Be sure to match message templates. The count of templates (%s, %d, etc.) must match in all languages, as must initial and terminal newlines (\n) + * While the count of templates must match, the _order_ can be changed by using e.g. %2$s to mean 'use the second input as a string here' + * Whenever templates or escaping is happening in a string, these will be 'highlighted' by carets (^) in the line below + Output + + File: foo.R + Call: stop("I really wish you'd reconsider") + Message: I really wish you'd reconsider + + How would you translate this message into Mainland Chinese? + + File: foo.R + Call: ngettext(length(x), "SOMEWHAT EPIC FAIL", "MAJORLY EPIC FAIL") + Plural message: SOMEWHAT EPIC FAIL + + How would you translate this message into Mainland Chinese independently of n? + Message + Writing R-zh_CN.po + Recompiling 'zh_CN' R translation + running msgfmt on R-zh_CN.po succeeded; output: + 3 translated messages. + +--- + + Code + translate_package(languages = "zh_CN") + Message + Found 4 untranslated messaging calls passed through cat(): + Output + + Problematic call: + base::cat("I warned you!", fill=TRUE, append=TRUE) + < File:foo.R, Line:2 > + Potential replacement: + cat(gettext("I warned you!"), fill=TRUE) + + Problematic call: + cat("Oh no", "you\ndon't!") + < File:foo.R, Line:8 > + Potential replacement: + cat(gettext("Oh no you\ndon't!")) + + Problematic call: + cat("Hi", "boss", sep="xx") + < File:foo.R, Line:15 > + Potential replacement: + cat(gettext("Hixxboss")) + + Problematic call: + cat("This costs", x, "dollars") + < File:foo.R, Line:22 > + Potential replacement: + cat(gettextf("This costs %s dollars", x)) + Exit now to repair any of these? [y/N] + +--- + + Code + translate_package(languages = "cy") + Message + Writing R-rMsg.pot + + 'cy' is not a known language. + Please help supply some metadata about it. You can check https://l10n.gnome.org/teams/ + Output + How would you refer to this language in English? + How would you refer to this language in the language itself? + How many pluralizations are there for this language [nplurals]? + Message + Input must be of type 'integer', but received 'character'. Trying again. + Output + How many pluralizations are there for this language [nplurals]? + What is the rule for deciding which plural applies as a function of n [plural]? + Message + Supplied 'plural': + (n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3 + Did not match any known 'plural's: + (n!=1) + (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2) + (n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2) + (n>1) + 0 + n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5 + Using generic description of cases instead. + Thanks! Please file an issue on GitHub to get this language recognized permanently + Output + Thanks! Who should be credited with these translations? + And what is their email? + + File: foo.R + Call: base::warning("I warned you!") + Message: I warned you! + + How would you translate this message into Welsh? + + File: foo.R + Call: stop("Oh no you don't!") + Message: Oh no you don't! + + How would you translate this message into Welsh? + + File: foo.R + Call: gettext("Hi there") + Message: Hi there + + How would you translate this message into Welsh? + + File: foo.R + Call: gettextf( fmt = "Avg cat() failures: %.02f; N failures: %d; failure: %s", mean(x), length(x), "don't translate me" ) + Message: Avg cat() failures: %.02f; N failures: %d; failure: %s + ^---^ ^^ ^^ + How would you translate this message into Welsh? + + + ** Oops! Invalid translation -- received the same set of templates + bordering newlines, but in incorrect order ([%.02f, %d, %s] vs [%.02f, %s, %d]). Recall that you can use %$N to do redirect, e.g. to swap the order of '%d %s' to be translated more naturally, your translation can use '%1$s %2$d'. Retrying. ** + + File: foo.R + Call: gettextf( fmt = "Avg cat() failures: %.02f; N failures: %d; failure: %s", mean(x), length(x), "don't translate me" ) + Message: Avg cat() failures: %.02f; N failures: %d; failure: %s + ^---^ ^^ ^^ + How would you translate this message into Welsh? + + + ** Oops! Invalid translation -- received templates + bordering newlines not present in the original: %s. Retrying. ** + + File: foo.R + Call: gettextf( fmt = "Avg cat() failures: %.02f; N failures: %d; failure: %s", mean(x), length(x), "don't translate me" ) + Message: Avg cat() failures: %.02f; N failures: %d; failure: %s + ^---^ ^^ ^^ + How would you translate this message into Welsh? + + + ** Oops! Invalid translation -- received the same set of templates + bordering newlines, but in incorrect order ([%.02f, %d, %s] vs [%1$.02f, %2$s, %3$d]). Recall that you can use %$N to do redirect, e.g. to swap the order of '%d %s' to be translated more naturally, your translation can use '%1$s %2$d'. Retrying. ** + + File: foo.R + Call: gettextf( fmt = "Avg cat() failures: %.02f; N failures: %d; failure: %s", mean(x), length(x), "don't translate me" ) + Message: Avg cat() failures: %.02f; N failures: %d; failure: %s + ^---^ ^^ ^^ + How would you translate this message into Welsh? + + + ** Oops! Invalid translation -- received 2 unique templated arguments + bordering newlines but there are 3 in the original. Retrying. ** + + File: foo.R + Call: gettextf( fmt = "Avg cat() failures: %.02f; N failures: %d; failure: %s", mean(x), length(x), "don't translate me" ) + Message: Avg cat() failures: %.02f; N failures: %d; failure: %s + ^---^ ^^ ^^ + How would you translate this message into Welsh? + + + ** Oops! Invalid translation -- received 4 unique templated arguments + bordering newlines but there are 3 in the original. Retrying. ** + + File: foo.R + Call: gettextf( fmt = "Avg cat() failures: %.02f; N failures: %d; failure: %s", mean(x), length(x), "don't translate me" ) + Message: Avg cat() failures: %.02f; N failures: %d; failure: %s + ^---^ ^^ ^^ + How would you translate this message into Welsh? + + + ** Oops! Invalid translation -- received 5 unique templated arguments + bordering newlines but there are 3 in the original. Retrying. ** + + File: foo.R + Call: gettextf( fmt = "Avg cat() failures: %.02f; N failures: %d; failure: %s", mean(x), length(x), "don't translate me" ) + Message: Avg cat() failures: %.02f; N failures: %d; failure: %s + ^---^ ^^ ^^ + How would you translate this message into Welsh? + + File: windows/bar.R + Call: ngettext(length(x), msg1 = "small fail", msg2 = "big fail") + Plural message: small fail + + How would you translate this message into Welsh for n where 'plural' resolves to 0? + + File: windows/bar.R + Call: ngettext(length(x), msg1 = "small fail", msg2 = "big fail") + Plural message: small fail + + How would you translate this message into Welsh for n where 'plural' resolves to 1? + + File: windows/bar.R + Call: ngettext(length(x), msg1 = "small fail", msg2 = "big fail") + Plural message: small fail + + How would you translate this message into Welsh for n where 'plural' resolves to 2? + + File: windows/bar.R + Call: ngettext(length(x), msg1 = "small fail", msg2 = "big fail") + Plural message: small fail + + How would you translate this message into Welsh for n where 'plural' resolves to 3? + Message + Writing R-cy.po + +--- + + Code + translate_package(languages = "ca", diagnostics = NULL) + Message + Writing R-rMsg.pot + + 'ca' is not a known language. + Please help supply some metadata about it. You can check https://l10n.gnome.org/teams/ + Output + How would you refer to this language in English? + How would you refer to this language in the language itself? + How many pluralizations are there for this language [nplurals]? + What is the rule for deciding which plural applies as a function of n [plural]? + Message + Thanks! Please file an issue on GitHub to get this language recognized permanently + Output + Thanks! Who should be credited with these translations? + And what is their email? + + File: foo.R + Call: base::warning("I warned you!") + Message: I warned you! + + How would you translate this message into Catalan? + + File: foo.R + Call: stop("Oh no you don't!") + Message: Oh no you don't! + + How would you translate this message into Catalan? + + File: foo.R + Call: gettext("Hi there") + Message: Hi there + + How would you translate this message into Catalan? + + File: foo.R + Call: gettextf( fmt = "Avg cat() failures: %.02f; N failures: %d; failure: %s", mean(x), length(x), "don't translate me" ) + Message: Avg cat() failures: %.02f; N failures: %d; failure: %s + ^---^ ^^ ^^ + How would you translate this message into Catalan? + + File: windows/bar.R + Call: ngettext(length(x), msg1 = "small fail", msg2 = "big fail") + Plural message: small fail + + How would you translate this message into Catalan when n = 1? + + File: windows/bar.R + Call: ngettext(length(x), msg1 = "small fail", msg2 = "big fail") + Plural message: small fail + + How would you translate this message into Catalan when n is not 1? + Message + Writing R-ca.po + +--- + + Code + translate_package(languages = "zh_CN", diagnostics = NULL) + Message + Writing R-rMsg.pot + + Output + Thanks! Who should be credited with these translations? + And what is their email? + + File: foo.R + Call: base::warning("I warned you!") + Message: I warned you! + + How would you translate this message into Mainland Chinese? + + File: foo.R + Call: stop("Oh no you don't!") + Message: Oh no you don't! + + How would you translate this message into Mainland Chinese? + + File: foo.R + Call: gettext("Hi there") + Message: Hi there + + How would you translate this message into Mainland Chinese? + + File: foo.R + Call: gettextf( fmt = "Avg cat() failures: %.02f; N failures: %d; failure: %s", mean(x), length(x), "don't translate me" ) + Message: Avg cat() failures: %.02f; N failures: %d; failure: %s + ^---^ ^^ ^^ + How would you translate this message into Mainland Chinese? + Error + Invalid templated message. If any %N$ redirects are used, all templates must be redirected. + Redirected tempates: %1$d + Un-redirected templates: %d + Message + Writing R-zh_CN.po + +--- + + Code + translate_package(languages = "zh_CN", diagnostics = NULL) + Message + Writing R-rMsg.pot + + Output + Thanks! Who should be credited with these translations? + And what is their email? + + File: foo.R + Call: base::warning("I warned you!") + Message: I warned you! + + How would you translate this message into Mainland Chinese? + + File: foo.R + Call: stop("Oh no you don't!") + Message: Oh no you don't! + + How would you translate this message into Mainland Chinese? + + File: foo.R + Call: gettext("Hi there") + Message: Hi there + + How would you translate this message into Mainland Chinese? + + File: foo.R + Call: gettextf( fmt = "Avg cat() failures: %.02f; N failures: %d; failure: %s", mean(x), length(x), "don't translate me" ) + Message: Avg cat() failures: %.02f; N failures: %d; failure: %s + ^---^ ^^ ^^ + How would you translate this message into Mainland Chinese? + Error + Invalid templated message string with redirects -- all messages pointing to the same input must have identical formats, but received [%1$s, %1$d] + Message + Writing R-zh_CN.po + +--- + + Code + translate_package(languages = "zh_CN", diagnostics = check_untranslated_src) + Message + Writing R-rSrcMsg.pot + + Output + Thanks! Who should be credited with these translations? + And what is their email? + + File: foo.R + Call: message("a string") + Message: a string + + How would you translate this message into Mainland Chinese? + + File: bar.c + Call: N_("Don't translate me now.") + Message: Don't translate me now. + + How would you translate this message into Mainland Chinese? + + File: bar.c + Call: Rprintf(_("an translated templated string: %" "" "\n"), 10000LL) + Message: an translated templated string: %\n + ^-------^^^ + How would you translate this message into Mainland Chinese? + + File: bar.c + Call: warning(_("a translated "\ + "warning: %s\n"), stardust(z)) + Message: a translated warning: %s\n + ^^^^ + How would you translate this message into Mainland Chinese? + + File: bar.c + Call: snprintf(BUF, 100, _("a simple message")) + Message: a simple message + + How would you translate this message into Mainland Chinese? + + File: bar.c + Call: ngettext("singular", "plural", z) + Plural message: singular + + How would you translate this message into Mainland Chinese independently of n? + + File: bar.c + Call: ngettext("singular %d", "plural %d", z) + Plural message: singular %d + ^^ + How would you translate this message into Mainland Chinese independently of n? + Message + Writing R-zh_CN.po + +--- + + Code + translate_package(languages = "zh_CN", verbose = TRUE) + Message + Updating translation template for package 'rSrcFuzzyMsg' (last updated 0000-01-01 00:00:00) + Getting R-level messages. + Getting src-level messages. + Running message diagnostics. + Writing R-rSrcFuzzyMsg.pot + + + + Found existing R translations for zh_CN (Mainland Chinese/普通话) in ./po/R-zh_CN.po. Running msgmerge. + . done. + Found existing src translations for zh_CN (Mainland Chinese/普通话) in ./po/zh_CN.po. Running msgmerge. + . done. + Translations for zh_CN are up to date! Skipping. + Recompiling 'zh_CN' R translation + running msgfmt on R-zh_CN.po succeeded; output: + 1 translated message. + Recompiling 'zh_CN' src translation + running msgfmt on zh_CN.po succeeded; output: + 0 translated messages, 2 fuzzy translations. + +--- + + Code + translate_package(languages = "es", copyright = "Mata Hari", diagnostics = NULL) + Message + Writing R-rMsgUnusual.pot + + Output + Thanks! Who should be credited with these translations? + And what is their email? + + File: copy1.R + Call: stop("copy one") + Message: copy one + + How would you translate this message into Spanish? + + File: copy2.R + Call: stop("copy two") + Message: copy two + + How would you translate this message into Spanish? + + File: foo.R + Call: base::warning(" I warned you!\n\n") + Message: I warned you! + + How would you translate this message into Spanish? + + File: foo.R + Call: message(r"('abc')") + Message: 'abc' + + How would you translate this message into Spanish? + + File: foo.R + Call: message(R'("def")') + Message: "def" + + How would you translate this message into Spanish? + + File: foo.R + Call: message("R('abc')") + Message: R('abc') + + How would you translate this message into Spanish? + + File: foo.R + Call: message('r("def")') + Message: r("def") + + How would you translate this message into Spanish? + + File: foo.R + Call: message(R'---[ghi]---') + Message: ghi + + How would you translate this message into Spanish? + + File: foo.R + Call: gettext("Hi there") + Message: Hi there + + How would you translate this message into Spanish? + + File: foo.R + Call: gettextf(fmt = "good %s ", "grief") + Message: good %s + ^^ + How would you translate this message into Spanish? + + File: foo.R + Call: warning( '"first"', "second", "third", "fourth", "fifth", "sixth" ) + Message: "first" + + How would you translate this message into Spanish? + + File: foo.R + Call: warning( '"first"', "second", "third", "fourth", "fifth", "sixth" ) + Message: second + + How would you translate this message into Spanish? + + File: foo.R + Call: warning( '"first"', "second", "third", "fourth", "fifth", "sixth" ) + Message: third + + How would you translate this message into Spanish? + + File: foo.R + Call: warning( '"first"', "second", "third", "fourth", "fifth", "sixth" ) + Message: fourth + + How would you translate this message into Spanish? + + File: foo.R + Call: warning( '"first"', "second", "third", "fourth", "fifth", "sixth" ) + Message: fifth + + How would you translate this message into Spanish? + + File: foo.R + Call: warning( '"first"', "second", "third", "fourth", "fifth", "sixth" ) + Message: sixth + + How would you translate this message into Spanish? + + File: foo.R + Call: message("\\n vs \n is OK") + Message: \\n vs \n is OK + + How would you translate this message into Spanish? + + File: foo.R + Call: message("\\t vs \t is OK") + Message: \\t vs \t is OK + + How would you translate this message into Spanish? + + File: foo.R + Call: message('strings with "quotes" are OK') + Message: strings with "quotes" are OK + + How would you translate this message into Spanish? + + File: foo.R + Call: message("strings with escaped \"quotes\" are OK") + Message: strings with escaped "quotes" are OK + + How would you translate this message into Spanish? + + File: foo.R + Call: gettextf( paste("part 1 %s", "part 2"), "input" ) + Message: part 1 %s + ^^ + How would you translate this message into Spanish? + + File: foo.R + Call: gettextf( paste("part 1 %s", "part 2"), "input" ) + Message: part 2 + + How would you translate this message into Spanish? + + File: foo.R + Call: ngettext( 10, "singular ", "plural " ) + Plural message: singular + + How would you translate this message into Spanish when n = 1? + + File: foo.R + Call: ngettext( 10, "singular ", "plural " ) + Plural message: singular + + How would you translate this message into Spanish when n is not 1? + + File: windows/bar.R + Call: ngettext(length(x), msg1 = "small fail ", msg2 = "big fail ") + Plural message: small fail + + How would you translate this message into Spanish when n = 1? + + File: windows/bar.R + Call: ngettext(length(x), msg1 = "small fail ", msg2 = "big fail ") + Plural message: small fail + + How would you translate this message into Spanish when n is not 1? + + File: ABCDEFGHIJKLMNOPQRSTUVWXYZ.c + Call: _("an translated templated string: %" "" "\n") + Message: an translated templated string: %\n + ^-------^^^ + How would you translate this message into Spanish? + + File: MSGs.c + Call: _("any old \ + message") + Message: any old message + + How would you translate this message into Spanish? + + File: msg.c + Call: _("a message in a macro %s") + Message: a message in a macro %s + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: Rprintf(_("that's a mighty big %" """-sized wall over %"""), 100LL, 10L) + Message: that's a mighty big %-sized wall over % + ^-------^ ^-------^ + How would you translate this message into Spanish? + + File: msg.c + Call: Rprintf(_("/* this is what a C comment looks like */ ")) + Message: /* this is what a C comment looks like */ + + How would you translate this message into Spanish? + + File: msg.c + Call: Rprintf(_("// this is what a C comment looks like %s "), "abc") + Message: // this is what a C comment looks like %s + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: Rprintf(_( + "01234567890123456789.01234567890123456789" + "01234567890123456789.01234567890123456789" + "01234567890123456789.01234567890123456789" + "01234567890123456789.01234567890123456789" + )) + Message: 01234567890123456789.0123456789012345678901234567890123456789.0123456789012345678901234567890123456789.0123456789012345678901234567890123456789.01234567890123456789 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("This message\nSpans two lines")) + Message: This message\nSpans two lines + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("This one does not\n")) + Message: This one does not\n + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("Exotic formatters like %I32u, %llx, %li, %ls, %lc")) + Message: Exotic formatters like %I32u, %llx, %li, %ls, %lc + ^---^ ^--^ ^-^ ^-^ ^-^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456\"890")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456"890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("0123456789012345678901234567890123456789012345678901234567890123456789012345(\"890")) + Message: 0123456789012345678901234567890123456789012345678901234567890123456789012345("890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("0123456789012345678901234567890123456789012345678901234567890123456789012345'\"890")) + Message: 0123456789012345678901234567890123456789012345678901234567890123456789012345'"890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("0123456789012345678901234567890123456789012345678901234567890123456789012345a\"890")) + Message: 0123456789012345678901234567890123456789012345678901234567890123456789012345a"890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("0123456789012345678901234567890123456789012345678901234567890123456789012345A\"890")) + Message: 0123456789012345678901234567890123456789012345678901234567890123456789012345A"890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("0123456789012345678901234567890123456789012345678901234567890123456789012345#\"890")) + Message: 0123456789012345678901234567890123456789012345678901234567890123456789012345#"890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("0123456789012345678901234567890123456789012345678901234567890123456789012345@\"890")) + Message: 0123456789012345678901234567890123456789012345678901234567890123456789012345@"890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 .%s.")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 .%s. + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 ?%s?")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 ?%s? + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 ;%s;")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 ;%s; + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 /%s/")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 /%s/ + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 '%s'")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 '%s' + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 [%s]")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 [%s] + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 |%s|")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 |%s| + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 -%s-")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 -%s- + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_(test ? "abc" : "def")) + Message: abc + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_(xxx "abc" "def")) + Message: abcdef + + How would you translate this message into Spanish? + + File: z.c + Call: error(_("You found me!")) + Message: You found me! + + How would you translate this message into Spanish? + + File: cairo/bedfellows.c + Call: _( + "any new message") + Message: any new message + + How would you translate this message into Spanish? + Message + Writing R-es.po + +--- + + Code + translate_package(languages = "es", use_base_rules = TRUE, diagnostics = NULL) + Message + Writing R-rMsgUnusual.pot + + Output + Thanks! Who should be credited with these translations? + And what is their email? + + File: copy1.R + Call: stop("copy one") + Message: copy one + + How would you translate this message into Spanish? + + File: copy2.R + Call: stop("copy two") + Message: copy two + + How would you translate this message into Spanish? + + File: foo.R + Call: base::warning(" I warned you!\n\n") + Message: I warned you! + + How would you translate this message into Spanish? + + File: foo.R + Call: message(r"('abc')") + Message: 'abc' + + How would you translate this message into Spanish? + + File: foo.R + Call: message(R'("def")') + Message: "def" + + How would you translate this message into Spanish? + + File: foo.R + Call: message("R('abc')") + Message: R('abc') + + How would you translate this message into Spanish? + + File: foo.R + Call: message('r("def")') + Message: r("def") + + How would you translate this message into Spanish? + + File: foo.R + Call: message(R'---[ghi]---') + Message: ghi + + How would you translate this message into Spanish? + + File: foo.R + Call: gettext("Hi there") + Message: Hi there + + How would you translate this message into Spanish? + + File: foo.R + Call: gettextf(fmt = "good %s ", "grief") + Message: good %s + ^^ + How would you translate this message into Spanish? + + File: foo.R + Call: warning( '"first"', "second", "third", "fourth", "fifth", "sixth" ) + Message: "first" + + How would you translate this message into Spanish? + + File: foo.R + Call: warning( '"first"', "second", "third", "fourth", "fifth", "sixth" ) + Message: second + + How would you translate this message into Spanish? + + File: foo.R + Call: warning( '"first"', "second", "third", "fourth", "fifth", "sixth" ) + Message: third + + How would you translate this message into Spanish? + + File: foo.R + Call: warning( '"first"', "second", "third", "fourth", "fifth", "sixth" ) + Message: fourth + + How would you translate this message into Spanish? + + File: foo.R + Call: warning( '"first"', "second", "third", "fourth", "fifth", "sixth" ) + Message: fifth + + How would you translate this message into Spanish? + + File: foo.R + Call: warning( '"first"', "second", "third", "fourth", "fifth", "sixth" ) + Message: sixth + + How would you translate this message into Spanish? + + File: foo.R + Call: message("\\n vs \n is OK") + Message: \\n vs \n is OK + + How would you translate this message into Spanish? + + File: foo.R + Call: message("\\t vs \t is OK") + Message: \\t vs \t is OK + + How would you translate this message into Spanish? + + File: foo.R + Call: message('strings with "quotes" are OK') + Message: strings with "quotes" are OK + + How would you translate this message into Spanish? + + File: foo.R + Call: message("strings with escaped \"quotes\" are OK") + Message: strings with escaped "quotes" are OK + + How would you translate this message into Spanish? + + File: foo.R + Call: gettextf( paste("part 1 %s", "part 2"), "input" ) + Message: part 1 %s + ^^ + How would you translate this message into Spanish? + + File: foo.R + Call: gettextf( paste("part 1 %s", "part 2"), "input" ) + Message: part 2 + + How would you translate this message into Spanish? + + File: foo.R + Call: ngettext( 10, "singular ", "plural " ) + Plural message: singular + + How would you translate this message into Spanish when n = 1? + + File: foo.R + Call: ngettext( 10, "singular ", "plural " ) + Plural message: singular + + How would you translate this message into Spanish when n is not 1? + + File: windows/bar.R + Call: ngettext(length(x), msg1 = "small fail ", msg2 = "big fail ") + Plural message: small fail + + How would you translate this message into Spanish when n = 1? + + File: windows/bar.R + Call: ngettext(length(x), msg1 = "small fail ", msg2 = "big fail ") + Plural message: small fail + + How would you translate this message into Spanish when n is not 1? + + File: ABCDEFGHIJKLMNOPQRSTUVWXYZ.c + Call: _("an translated templated string: %" "" "\n") + Message: an translated templated string: %\n + ^-------^^^ + How would you translate this message into Spanish? + + File: MSGs.c + Call: _("any old \ + message") + Message: any old message + + How would you translate this message into Spanish? + + File: msg.c + Call: _("a message in a macro %s") + Message: a message in a macro %s + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: Rprintf(_("that's a mighty big %" """-sized wall over %"""), 100LL, 10L) + Message: that's a mighty big %-sized wall over % + ^-------^ ^-------^ + How would you translate this message into Spanish? + + File: msg.c + Call: Rprintf(_("/* this is what a C comment looks like */ ")) + Message: /* this is what a C comment looks like */ + + How would you translate this message into Spanish? + + File: msg.c + Call: Rprintf(_("// this is what a C comment looks like %s "), "abc") + Message: // this is what a C comment looks like %s + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: Rprintf(_( + "01234567890123456789.01234567890123456789" + "01234567890123456789.01234567890123456789" + "01234567890123456789.01234567890123456789" + "01234567890123456789.01234567890123456789" + )) + Message: 01234567890123456789.0123456789012345678901234567890123456789.0123456789012345678901234567890123456789.0123456789012345678901234567890123456789.01234567890123456789 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("This message\nSpans two lines")) + Message: This message\nSpans two lines + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("This one does not\n")) + Message: This one does not\n + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("Exotic formatters like %I32u, %llx, %li, %ls, %lc")) + Message: Exotic formatters like %I32u, %llx, %li, %ls, %lc + ^---^ ^--^ ^-^ ^-^ ^-^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456\"890")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456"890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("0123456789012345678901234567890123456789012345678901234567890123456789012345(\"890")) + Message: 0123456789012345678901234567890123456789012345678901234567890123456789012345("890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("0123456789012345678901234567890123456789012345678901234567890123456789012345'\"890")) + Message: 0123456789012345678901234567890123456789012345678901234567890123456789012345'"890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("0123456789012345678901234567890123456789012345678901234567890123456789012345a\"890")) + Message: 0123456789012345678901234567890123456789012345678901234567890123456789012345a"890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("0123456789012345678901234567890123456789012345678901234567890123456789012345A\"890")) + Message: 0123456789012345678901234567890123456789012345678901234567890123456789012345A"890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("0123456789012345678901234567890123456789012345678901234567890123456789012345#\"890")) + Message: 0123456789012345678901234567890123456789012345678901234567890123456789012345#"890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("0123456789012345678901234567890123456789012345678901234567890123456789012345@\"890")) + Message: 0123456789012345678901234567890123456789012345678901234567890123456789012345@"890 + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 .%s.")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 .%s. + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 ?%s?")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 ?%s? + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 ;%s;")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 ;%s; + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 /%s/")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 /%s/ + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 '%s'")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 '%s' + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 [%s]")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 [%s] + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 |%s|")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 |%s| + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_("01234567890123456789012345678901234567890123456789012345678901234567890123456 -%s-")) + Message: 01234567890123456789012345678901234567890123456789012345678901234567890123456 -%s- + ^^ + How would you translate this message into Spanish? + + File: msg.c + Call: error(_(test ? "abc" : "def")) + Message: abc + + How would you translate this message into Spanish? + + File: msg.c + Call: error(_(xxx "abc" "def")) + Message: abcdef + + How would you translate this message into Spanish? + + File: z.c + Call: error(_("You found me!")) + Message: You found me! + + How would you translate this message into Spanish? + + File: cairo/bedfellows.c + Call: _( + "any new message") + Message: any new message + + How would you translate this message into Spanish? + Message + Writing R-es.po + +--- + + Code + translate_package(languages = "es", max_translations = 1L, diagnostics = NULL) + Message + Writing R-rMsg.pot + + Output + Thanks! Who should be credited with these translations? + And what is their email? + + File: foo.R + Call: base::warning("I warned you!") + Message: I warned you! + + How would you translate this message into Spanish? + Message + Writing R-es.po + diff --git a/tests/testthat/helpers.R b/tests/testthat/helpers.R index ad61f367..ac76f861 100644 --- a/tests/testthat/helpers.R +++ b/tests/testthat/helpers.R @@ -1,22 +1,20 @@ -# copy a package to tmp, then overwrite any changes on exit -restore_package <- function(dir, expr, tmp_conn) { - # this is way uglier than it should be. i'm missing something. - tdir <- tempdir() - +# copy a package to tmp, deleting on exit +with_package <- function(dir, expr, msg_conn = NULL) { + tdir <- withr::local_tempdir() file.copy(dir, tdir, recursive = TRUE) - on.exit({ - unlink(dir, recursive = TRUE) - dir.create(dir) - file.copy(file.path(tdir, basename(dir)), dirname(dir), recursive = TRUE) - unlink(file.path(tdir, basename(dir)), recursive = TRUE) - }) + withr::local_dir(file.path(tdir, basename(dir))) - if (!missing(tmp_conn)) { - old = options("__potools_testing_prompt_connection__" = tmp_conn) - on.exit(options(old), add = TRUE) + if (!is.null(msg_conn)) { + withr::local_options("__potools_testing_prompt_connection__" = msg_conn) } - invisible(capture.output(expr)) + expr +} + +with_restoration_test_that <- function(desc, pkg, code, conn = NULL) { + pkg <- test_package(pkg) + if (!is.null(conn)) conn <- mock_translation(conn) + with_package(pkg, code, conn) } # TODO: I think this can just be replaced by expect_match and expect_no_match in current testthat dev @@ -47,7 +45,7 @@ expect_messages = function(expr, msgs, ..., invert=FALSE) { } test_package = function(pkg) test_path(file.path("test_packages", pkg)) -mock_translation = function(mocks) test_path(file.path("mock_translations", mocks)) +mock_translation = function(mocks) normalizePath(test_path(file.path("mock_translations", mocks))) local_test_package <- function(..., .envir = parent.frame()) { temp <- withr::local_tempdir(.local_envir = .envir) @@ -65,8 +63,20 @@ local_test_package <- function(..., .envir = parent.frame()) { temp } -# different platforms/installations of gettext apparently -# produce a different number of "." in "progress" output; normalize -standardize_dots <- standardise_dots <- function(x) { - gsub("\\.{2,}", ".", x) +normalize_output <- function(x) { + # different platforms/installations of gettext apparently + # produce a different number of "." in "progress" output; normalize + x <- gsub("\\.{2,}", ".", x) + + # en@quot translations are not produced on Windows (as of now) + x <- grep("Generating en@quot translations", x, fixed = TRUE, invert = TRUE, value = TRUE) + + # this is produced alongside the previous message, but in a different iteration of transform() + idx <- grep("running msgfmt on (?:R-)?en@quot\\.po", x) + if (length(idx)) { + x <- x[-(idx + 0:1)] + } + x } + +expect_normalized_snapshot <- function(...) expect_snapshot(..., transform = normalize_output) diff --git a/tests/testthat/test-po_update.R b/tests/testthat/test-po_update.R index 9f11b4f8..f5c0e4ca 100644 --- a/tests/testthat/test-po_update.R +++ b/tests/testthat/test-po_update.R @@ -5,8 +5,5 @@ test_that("user is told what's happening", { po_extract() po_create(c("ja", "fr")) - expect_snapshot( - po_update(verbose = TRUE, lazy = FALSE), - transform = standardise_dots - ) + expect_normalized_snapshot(po_update(verbose = TRUE, lazy = FALSE)) }) diff --git a/tests/testthat/test-translate-package.R b/tests/testthat/test-translate-package.R index 7c5b1fc0..6475f277 100644 --- a/tests/testthat/test-translate-package.R +++ b/tests/testthat/test-translate-package.R @@ -19,543 +19,430 @@ test_that("translate_package arg checking errors work", { expect_error(translate_package(tempdir(), diagnostics = list(1L)), "'diagnostics' should be", fixed=TRUE) }) -test_that("translate_package handles empty packages", { - restore_package( - pkg <- test_package("no_msg"), - { - expect_invisible(translate_package(pkg)) - - expect_message(translate_package(pkg, verbose=TRUE), "No messages to translate", fixed=TRUE) - } - ) - - # a package with no R directory (e.g. a data package) - restore_package( - pkg <- test_package("r_data_pkg"), - expect_message(translate_package(pkg, verbose=TRUE), "No messages to translate", fixed=TRUE) - ) +with_restoration_test_that("translate_package handles empty packages", "no_msg", { + expect_normalized_snapshot(translate_package()) + expect_normalized_snapshot(translate_package(verbose=TRUE)) }) -test_that("translate_package works on a simple package", { - # simple run-through without doing translations - restore_package( - pkg <- test_package("r_msg"), - { - expect_messages( - translate_package(pkg, verbose=TRUE), - c("No languages provided"), - fixed = TRUE - ) - - pkg_files <- list.files(pkg, recursive=TRUE) - - pot_file <- "po/R-rMsg.pot" - expect_true(pot_file %in% pkg_files) - # testing gettextf's ... arguments are skipped - expect_all_match(readLines(file.path(pkg, pot_file)), "don't translate me", invert=TRUE, fixed=TRUE) - - # Non-UTF-8 machines don't run en@quot translations by default. - # Mostly applies to Windows, but can also apply to Unix - # (e.g. r-devel-linux-x86_64-debian-clang on CRAN), #191 - if (l10n_info()[["UTF-8"]]) { - expect_match(pkg_files, "inst/po/en@quot/LC_MESSAGES/R-rMsg.mo", all = FALSE) - } - } - ) - # do translations with mocked input - prompts <- restore_package( - pkg, - tmp_conn = mock_translation("test-translate-package-r_msg-1.input"), - { - expect_messages( - translate_package(pkg, "zh_CN", verbose=TRUE), - c("Beginning new translations", "BEGINNING TRANSLATION", "Recompiling 'zh_CN' R translation"), - fixed = TRUE - ) - - pkg_files <- list.files(pkg, recursive = TRUE) - - expect_true("po/R-zh_CN.po" %in% pkg_files) - expect_match(pkg_files, "inst/po/zh_CN/LC_MESSAGES/R-rMsg.mo", all = FALSE, fixed = TRUE) - - zh_translations <- readLines(file.path(pkg, "po/R-zh_CN.po"), encoding='UTF-8') - - expect_match(zh_translations, "Last-Translator.*test-user.*test-user@github.com", all = FALSE) - expect_match(zh_translations, "早上好", all = FALSE) - # plural message - expect_match(zh_translations, "该起床了", all = FALSE) - } - ) - expect_all_match(prompts, c("^---^", "^^"), fixed=TRUE) - - # all translations already done - restore_package( - pkg, - { - expect_messages( - translate_package(pkg, "fa", verbose=TRUE), - "Translations for fa are up to date! Skipping", - fixed = TRUE - ) - } - ) +with_restoration_test_that("translate_package handles a data package (no R dir)", "r_data_pkg", { + expect_normalized_snapshot(translate_package(verbose=TRUE)) }) -test_that("translate_package works on package with outdated (fuzzy) translations", { - # simple run-through without doing translations - prompts = restore_package( - pkg <- test_package("r_fuzzy"), - tmp_conn = mock_translation("test-translate-package-r_fuzzy-1.input"), - { - expect_messages( - translate_package(pkg, "zh_CN", verbose=TRUE), - c("translations marked as deprecated", "SINGULAR MESSAGES", "PLURAL MESSAGES"), - fixed = TRUE - ) - } - ) - expect_match(prompts, "a similar message was previously translated as", all=FALSE) -}) +with_restoration_test_that("translate_package works on a simple package w/o translating", "r_msg", { + mockery::stub(translate_package, "get_atime", "0000-01-01 00:00:00") + expect_normalized_snapshot(translate_package(verbose = TRUE)) -# NB: keep this test here (not in test-diagnostics) to keep coverage of the diagnostic flow in translate_package() -test_that("translate_package identifies potential translations in cat() calls", { - prompts = restore_package( - pkg <- test_package("r_cat_msg"), - tmp_conn = mock_translation("test-translate-package-r_cat_message-1.input"), - { - expect_messages( - translate_package(pkg, "zh_CN"), - "Found 4 untranslated messaging calls passed through cat()", - fixed = TRUE - ) - } - ) - expect_all_match( - prompts, - c( - 'cat(gettext("I warned you!"), fill=TRUE)', - 'cat(gettext("Oh no you\\ndon\'t!"))', - "Hixxboss" - ), - fixed=TRUE - ) - expect_all_match( - prompts, - c("shouldn't be translated", "Miss me"), - fixed=TRUE, invert=TRUE - ) -}) + pkg_files <- list.files(recursive=TRUE) -test_that('Unknown language flow works correctly', { - prompts = restore_package( - pkg <- test_package('r_msg'), - tmp_conn = mock_translation('test-translate-package-r_msg-2.input'), - { - expect_messages( - # earlier, did Arabic, but now that's a chosen language. switched two Welsh on the - # (perhaps naive) judgment that it's unlikely to enter our scope anytime soon - # and because there are still several (4) plural forms - translate_package(pkg, 'cy'), - c( - 'not a known language', 'Please file an issue', - # NB: this test will fail if test_that is re-run on the same R session since potools' - # internal state is altered for the remainder of the session... not sure it's worth changing... - "Did not match any known 'plural's" - ), - fixed=TRUE - ) - } - ) - # also include coverage tests of incorrect templating in supplied translations - expect_all_match( - prompts, - c( - 'How would you refer to this language in English?', - 'received the same set of templates', - 'received 2 unique templated arguments', - 'received 4 unique templated arguments', - 'received 5 unique templated arguments' - ), - fixed=TRUE - ) - - # whitespace matching for plural is lenient, #183 - prompts = restore_package( - pkg <- test_package('r_msg'), - tmp_conn = mock_translation('test-translate-package-r_msg-5.input'), - { - expect_messages( - # Catalan -- romance language with >1 plural - translate_package(pkg, 'ca', diagnostics=NULL), - c("Did not match any known 'plural's"), - fixed=TRUE, invert=TRUE - ) - } - ) - expect_all_match( - prompts, - c("when n = 1", "when n is not 1"), - fixed = TRUE - ) -}) + pot_file <- "po/R-rMsg.pot" + expect_true(pot_file %in% pkg_files) + # testing gettextf's ... arguments are skipped + expect_all_match(readLines(file.path(pot_file)), "don't translate me", invert=TRUE, fixed=TRUE) -test_that('Erroneous messages stop get_specials_metadata', { - restore_package( - pkg <- test_package('r_msg'), - tmp_conn = mock_translation('test-translate-package-r_msg-3.input'), - { - expect_error( - translate_package(pkg, 'zh_CN', diagnostics = NULL), - 'Invalid templated message. If any %N$', fixed = TRUE - ) - } - ) - - restore_package( - pkg <- test_package('r_msg'), - tmp_conn = mock_translation('test-translate-package-r_msg-4.input'), - { - expect_error( - translate_package(pkg, 'zh_CN', diagnostics = NULL), - 'all messages pointing to the same input', fixed = TRUE - ) - } - ) + # Non-UTF-8 machines don't run en@quot translations by default. + # Mostly applies to Windows, but can also apply to Unix + # (e.g. r-devel-linux-x86_64-debian-clang on CRAN), #191 + if (l10n_info()[["UTF-8"]]) { + expect_match(pkg_files, "inst/po/en@quot/LC_MESSAGES/R-rMsg.mo", all = FALSE) + } }) -test_that("Packages with src code work correctly", { - prompts = restore_package( - pkg <- test_package('r_src_c'), - tmp_conn = mock_translation('test-translate-package-r_src_c-1.input'), - { - translate_package(pkg, "zh_CN", diagnostics = check_untranslated_src) - - pkg_files <- list.files(pkg, recursive = TRUE) - expect_true("po/R-zh_CN.po" %in% pkg_files) - expect_true("po/zh_CN.po" %in% pkg_files) - expect_true("po/rSrcMsg.pot" %in% pkg_files) - expect( - any(grepl("inst/po/zh_CN/LC_MESSAGES/rSrcMsg.mo", pkg_files, fixed = TRUE)), - sprintf( - "Didn't find rSrcMsg.mo; found %s.\n**Sysreq paths: %s.\n**po/zh_CN contents:\n%s\n**Direct msgfmt output:\n%s**Session info:\n%s", - toString(pkg_files), toString(Sys.which(potools:::SYSTEM_REQUIREMENTS)), - paste(readLines(file.path(pkg, 'po/zh_CN.po')), collapse='\n'), - { - out <- tempfile() - system2( - "msgfmt", - c("-o", tempfile(fileext = '.mo'), file.path(pkg, "po/zh_CN.po")), - stdout = out, stdin = out, stderr = out - ) - paste(readLines(out), collapse='\n') - }, - paste(capture.output(print(sessionInfo())), collapse = '\n') - ) - ) +with_restoration_test_that( + "translate_package works on a simple package w/ translating", + pkg = "r_msg", + conn = "test-translate-package-r_msg-1.input", + { + mockery::stub(translate_package, "get_atime", "0000-01-01 00:00:00") + expect_normalized_snapshot(translate_package(languages="zh_CN", verbose=TRUE)) + + pkg_files <- list.files(recursive = TRUE) + + expect_true("po/R-zh_CN.po" %in% pkg_files) + expect_match(pkg_files, "inst/po/zh_CN/LC_MESSAGES/R-rMsg.mo", all = FALSE, fixed = TRUE) + + zh_translations <- readLines(file.path("po/R-zh_CN.po"), encoding='UTF-8') + + expect_match(zh_translations, "Last-Translator.*test-user.*test-user@github.com", all = FALSE) + expect_match(zh_translations, "早上好", all = FALSE) + # plural message + expect_match(zh_translations, "该起床了", all = FALSE) + } +) + +with_restoration_test_that( + "translate package works for up-to-date translations", + pkg = "r_msg", + code = { + mockery::stub(translate_package, "get_atime", "0000-01-01 00:00:00") + expect_normalized_snapshot(translate_package(languages="fa", verbose=TRUE)) + } +) + +with_restoration_test_that( + "translate_package works on package with outdated (fuzzy) translations", + pkg = "r_fuzzy", + conn = "test-translate-package-r_fuzzy-1.input", + code = { + mockery::stub(translate_package, "get_atime", "0000-01-01 00:00:00") + expect_normalized_snapshot(translate_package(languages="zh_CN", verbose=TRUE)) + } +) - # NB: paste(readLines(), collapse="\n") instead of readChar() for platform robustness - pot_lines <- paste(readLines(file.path(pkg, 'po', 'rSrcMsg.pot')), collapse = "\n") - # (1) test N_-marked messages are included for translation - # (2) test untemplated snprintf() calls get c-format tagged (#137) - # (3)-(4) ngettext() arrays are extracted - expect_all_match( - pot_lines, - c( - '"Don\'t translate me now."', - '#, c-format\nmsgid "a simple message"', - 'msgid "singular"\nmsgid_plural "plural"', - 'msgid "singular %d"\nmsgid_plural "plural %d"' - ) +# NB: keep this test here (not in test-diagnostics) to keep coverage of the diagnostic flow in translate_package() +with_restoration_test_that( + "translate_package identifies potential translations in cat() calls", + pkg = "r_cat_msg", + conn = "test-translate-package-r_cat_message-1.input", + code = expect_normalized_snapshot(translate_package(languages = "zh_CN")) +) + +# NB: this test will fail if test_that is re-run on the same R session since potools' +# internal state is altered for the remainder of the session... not sure it's worth changing... +with_restoration_test_that( + 'Unknown language flow works correctly', + pkg = "r_msg", + conn = 'test-translate-package-r_msg-2.input', + # earlier, did Arabic, but now that's an included language. switched two Welsh on the + # (perhaps naive) judgment that it's unlikely to enter our scope anytime soon + # and because there are still several (4) plural forms + code = expect_normalized_snapshot(translate_package(languages = 'cy')) +) + +# #183 +with_restoration_test_that( + "whitespace matching for plural is lenient", + pkg = "r_msg", + conn = "test-translate-package-r_msg-5.input", + # Catalan -- romance language with >1 plural + code = expect_normalized_snapshot(translate_package(languages='ca', diagnostics=NULL)) +) + +with_restoration_test_that( + 'Erroneous messages stop get_specials_metadata (mixed use of template redirects)', + pkg = "r_msg", + conn = 'test-translate-package-r_msg-3.input', + code = expect_normalized_snapshot(translate_package(languages='zh_CN', diagnostics=NULL), error=TRUE) +) + +with_restoration_test_that( + 'Erroneous messages stop get_specials_metadata (duplicate redirects with different formatters)', + pkg = "r_msg", + conn = 'test-translate-package-r_msg-4.input', + code = expect_normalized_snapshot(translate_package(languages='zh_CN', diagnostics=NULL), error=TRUE) +) + +with_restoration_test_that( + "Packages with src code work correctly", + pkg = "r_src_c", + conn = 'test-translate-package-r_src_c-1.input', + { + expect_normalized_snapshot(translate_package(languages="zh_CN", diagnostics = check_untranslated_src)) + + pkg_files <- list.files(recursive = TRUE) + expect_true("po/R-zh_CN.po" %in% pkg_files) + expect_true("po/zh_CN.po" %in% pkg_files) + expect_true("po/rSrcMsg.pot" %in% pkg_files) + # extended tracing here for hard-to-reproduce issue. + # TODO: refactor this into a custom expectation helper so as not to take up so much space here. + expect( + any(grepl("inst/po/zh_CN/LC_MESSAGES/rSrcMsg.mo", pkg_files, fixed = TRUE)), + sprintf( + "Didn't find rSrcMsg.mo; found %s.\n**Sysreq paths: %s.\n**po/zh_CN contents:\n%s\n**Direct msgfmt output:\n%s**Session info:\n%s", + toString(pkg_files), toString(Sys.which(potools:::SYSTEM_REQUIREMENTS)), + paste(readLines(file.path('po/zh_CN.po')), collapse='\n'), + { + out <- tempfile() + system2( + "msgfmt", + c("-o", tempfile(fileext = '.mo'), file.path("po/zh_CN.po")), + stdout = out, stdin = out, stderr = out + ) + paste(readLines(out), collapse='\n') + }, + paste(capture.output(print(sessionInfo())), collapse = '\n') ) - } - ) - - expect_all_match( - prompts, - c("Rprintf(_(", "warning(_("), - fixed = TRUE - ) - - # error(ngettext(...)) doesn't show error() in check_untranslated_src - expect_all_match( - prompts, - "Problematic call", - invert = TRUE, fixed = TRUE - ) -}) - -test_that("Packages with src code & fuzzy messages work", { - prompts = restore_package( - pkg <- test_package("r_src_fuzzy"), - tmp_conn = mock_translation('test-translate-package-r_src_fuzzy-1.input'), - { - expect_messages( - translate_package(pkg, "zh_CN", verbose = TRUE), - "Found existing src translations", - fixed = TRUE + ) + + # TODO: is there any snapshotting equivalent for .pot/.po files? is writeLines() enough to overcome Windows issues? + # NB: paste(readLines(), collapse="\n") instead of readChar() for platform robustness + pot_lines <- paste(readLines(file.path('po', 'rSrcMsg.pot')), collapse = "\n") + # (1) test N_-marked messages are included for translation + # (2) test untemplated snprintf() calls get c-format tagged (#137) + # (3)-(4) ngettext() arrays are extracted + expect_all_match( + pot_lines, + c( + '"Don\'t translate me now."', + '#, c-format\nmsgid "a simple message"', + 'msgid "singular"\nmsgid_plural "plural"', + 'msgid "singular %d"\nmsgid_plural "plural %d"' ) - } - ) - expect_all_match( - prompts, - "Note: a similar message was previously translated as", - fixed = TRUE - ) -}) + ) + } +) + +with_restoration_test_that( + "Packages with src code & fuzzy messages work", + pkg = "r_src_fuzzy", + conn = 'test-translate-package-r_src_fuzzy-1.input', + code = { + mockery::stub(translate_package, "get_atime", "0000-01-01 00:00:00") + expect_normalized_snapshot(translate_package(languages="zh_CN", verbose=TRUE)) + } +) # TODO: separate get_message_data() tests from write_po_files() tests here -test_that("Various edge cases in retrieving/outputting messages in R files are handled", { - restore_package( - pkg <- test_package("unusual_msg"), - { - translate_package(pkg, diagnostics = NULL) - - r_pot_file <- readLines(file.path(pkg, "po", "R-rMsgUnusual.pot")) - src_pot_file <- readLines(file.path(pkg, "po", "rMsgUnusual.pot")) - - # (1)-(4) raw strings edge cases - # (5)-(6) whitespace trimming behavior (trim for R singular, don't for R plural) - # (7) repeated escapables (#130) - # (8)-(9) gettextf(paste()) gets nested strings (#163) - expect_all_match( - r_pot_file, - c( - 'msgid "\'abc\'"', 'msgid "\\"def\\""', 'msgid "R(\'abc\')"', 'msgid "r(\\"def\\")"', - 'msgid "ghi"', 'good %s', 'msgid "singular "', '"I warned you!"', - 'msgid "part 1 %s"', 'msgid "part 2"' - ), - fixed = TRUE - ) - - # skip empty strings - expect_all_match(paste(r_pot_file, collapse = "\n"), 'msgid ""\nmsgstr ""\n\n', fixed = TRUE, invert = TRUE) - - # don't collapse strings in similar node positions across files - expect_all_match(r_pot_file, c('msgid "copy one"', 'msgid "copy two"')) - - # ordering within the file - # "\\"first\\"" also tests escaping of " on msgid border, #128 - expect_true(which(r_pot_file == 'msgid "\\"first\\""') < which(r_pot_file == 'msgid "second"')) - expect_true(which(r_pot_file == 'msgid "second"') < which(r_pot_file == 'msgid "third"')) - expect_true(which(r_pot_file == 'msgid "third"') < which(r_pot_file == 'msgid "fourth"')) - - # escaping/unescaping - expect_all_match( - r_pot_file, - c('"\\\\n vs \\n', 'msgid "\\\\t vs \\t is OK"', - 'msgid "strings with \\"quotes\\" are OK"', 'msgid "strings with escaped \\"quotes\\" are OK"'), - fixed = TRUE +with_restoration_test_that( + "Various edge cases in retrieving/outputting messages in R files are handled", + pkg = "unusual_msg", + { + translate_package(diagnostics = NULL) + + r_pot_file <- readLines(file.path("po", "R-rMsgUnusual.pot")) + src_pot_file <- readLines(file.path("po", "rMsgUnusual.pot")) + + # (1)-(4) raw strings edge cases + # (5)-(6) whitespace trimming behavior (trim for R singular, don't for R plural) + # (7) repeated escapables (#130) + # (8)-(9) gettextf(paste()) gets nested strings (#163) + expect_all_match( + r_pot_file, + c( + 'msgid "\'abc\'"', 'msgid "\\"def\\""', 'msgid "R(\'abc\')"', 'msgid "r(\\"def\\")"', + 'msgid "ghi"', 'good %s', + 'msgid "singular "', '"I warned you!"', + 'msgid "part 1 %s"', 'msgid "part 2"' + ), + fixed = TRUE + ) + + # skip empty strings + expect_all_match(paste(r_pot_file, collapse = "\n"), 'msgid ""\nmsgstr ""\n\n', fixed = TRUE, invert = TRUE) + + # don't collapse strings in similar node positions across files + expect_all_match(r_pot_file, c('msgid "copy one"', 'msgid "copy two"')) + + # ordering within the file + # "\\"first\\"" also tests escaping of " on msgid border, #128 + expect_true(which(r_pot_file == 'msgid "\\"first\\""') < which(r_pot_file == 'msgid "second"')) + expect_true(which(r_pot_file == 'msgid "second"') < which(r_pot_file == 'msgid "third"')) + expect_true(which(r_pot_file == 'msgid "third"') < which(r_pot_file == 'msgid "fourth"')) + + # escaping/unescaping + expect_all_match( + r_pot_file, + c('"\\\\n vs \\n', + 'msgid "\\\\t vs \\t is OK"', + 'msgid "strings with \\"quotes\\" are OK"', + 'msgid "strings with escaped \\"quotes\\" are OK"'), + fixed = TRUE + ) + + # (1)-(2) whitespace trimming in C + # (3) always split at newlines + # (4) exotic formatters like %lld + # (5) ordering of files within the .pot (#104), and line # when call & array lines differ (#148) + # (6) correct message after removing line continuation (#91) + # (7) a message outside a call (e.g. in a macro) gets a source marker (#133) + # (8) ternary operators return first array; only arrays through first interrupting macro are included (#154) + # (9) initial macro is ignored; arrays through first interrupting macro are included; dgettext() included (#153) + # (10) when a msgid is repeated and is_templated differs, c-format is assumed + expect_all_match( + paste(src_pot_file, collapse = "\n"), # NB: this is a get-out-of-\r\n-jail-free card on Windows, too + c( + 'looks like [*]/ "', 'looks like %s "', + '"This message[\\]n"', + '#, c-format\nmsgid "Exotic formatters', + '#: msg[.]c.*#: cairo/bedfellows[.]c:13', + '"any old message"', + '#: msg[.]c:[0-9]+\n#, c-format\nmsgid "a message in a macro %s"', + '#: msg[.]c:[0-9]+ msg[.]c:[0-9]+\nmsgid "abc"', + '#:( msg[.]c:[0-9]+){3}\nmsgid "abcdef"', + '#: msg[.]c:[0-9]+ msg[.]c:[0-9]+\n#, c-format\nmsgid "This one does not[\\]n"' + ), + ) + } +) + +with_restoration_test_that( + "use_base_rules=FALSE produces our preferred behavior", + pkg = "unusual_msg", + conn = "test-translate-package-unusual_msg-1.input", + { + expect_normalized_snapshot(translate_package(languages="es", copyright="Mata Hari", diagnostics=NULL)) + + r_pot_lines <- readLines(file.path("po", "R-rMsgUnusual.pot")) + src_pot_lines <- readLines(file.path("po", "rMsgUnusual.pot")) + + # (1) default copyright comment in metadata + # (2) default blank Language field in metadata + # (3) testing plural string padding + # (4) source tagging + # (5) splitting at newlines + # (6) msgid quote escaping + # (7) copyright + expect_all_match( + r_pot_lines, + c( + "SOME DESCRIPTIVE TITLE", "Language: \\n", "nplurals=INTEGER", + 'msgid "singular "', '#: foo.R', '"\\\\n vs \\n"', + '"strings with escaped \\"quotes\\"', + 'Copyright (C) YEAR Mata Hari' + ), + fixed = TRUE + ) + + # (1) lack of strwrap despite >79 width + # (2) inclusion of file in "unrecognized" cairo folder + expect_all_match( + src_pot_lines, + c( + "#: [A-Z]{26}[.]c:[0-9] [a-z0-5]{32}[.]c:[0-9] msg[.]c:[0-9]{2} msg[.]c:[0-9]{2}", + '^#: cairo/bedfellows\\.c:' ) - - # (1)-(2) whitespace trimming in C - # (3) always split at newlines - # (4) exotic formatters like %lld - # (5) ordering of files within the .pot (#104), and line # when call & array lines differ (#148) - # (6) correct message after removing line continuation (#91) - # (7) a message outside a call (e.g. in a macro) gets a source marker (#133) - # (8) ternary operators return first array; only arrays through first interrupting macro are included (#154) - # (9) initial macro is ignored; arrays through first interrupting macro are included; dgettext() included (#153) - # (10) when a msgid is repeated and is_templated differs, c-format is assumed - expect_all_match( - paste(src_pot_file, collapse = "\n"), # NB: this is a get-out-of-\r\n-jail-free card on Windows, too - c( - 'looks like [*]/ "', 'looks like %s "', '"This message[\\]n"', - '#, c-format\nmsgid "Exotic formatters', '#: msg[.]c.*#: cairo/bedfellows[.]c:13', - '"any old message"', '#: msg[.]c:[0-9]+\n#, c-format\nmsgid "a message in a macro %s"', - '#: msg[.]c:[0-9]+ msg[.]c:[0-9]+\nmsgid "abc"', - '#:( msg[.]c:[0-9]+){3}\nmsgid "abcdef"', - '#: msg[.]c:[0-9]+ msg[.]c:[0-9]+\n#, c-format\nmsgid "This one does not[\\]n"' - ), + ) + } +) + +with_restoration_test_that( + "use_base_rules=TRUE produces base-aligned behavior", + pkg = "unusual_msg", + conn = "test-translate-package-unusual_msg-1.input", + { + expect_normalized_snapshot(translate_package(languages = "es", use_base_rules = TRUE, diagnostics = NULL)) + r_pot_lines <- readLines(file.path("po", "R-rMsgUnusual.pot")) + src_pot_lines <- readLines(file.path("po", "rMsgUnusual.pot")) + + # (1)-(5) invert the corresponding number in the previous test_that + expect_all_match( + r_pot_lines, + c("SOME DESCRIPTIVE TITLE", "Language: [\\]n", "nplurals=INTEGER", '#: ', '"\\\\n vs \\n is OK"'), + fixed = TRUE, invert = TRUE + ) + expect_all_match(r_pot_lines, 'msgid "small fail "', fixed = TRUE) + + # (1) MSG.c comes before msg.c (sort/collate order) + # (2) c-format tags are produced + # (3) msgid with many duplicates wraps the source markers _exactly_ at width=79 + # (4)-(11) template-adjacent wrapping/non-wrapping (#90, #150) + expect_all_match( + paste(src_pot_lines, collapse='\n'), + c( + 'MSGs\\.c.*msg\\.c', '#, c-format', + '#: [A-Z]{26}[.]c:[0-9] [a-z0-5]{32}[.]c:[0-9] msg[.]c:[0-9]{2}\n#: msg[.]c', + ' [.]"\n"%s[.]"', ' [?]"\n"%s[?]"', ' ;"\n"%s;"', ' /"\n"%s/"', + '"\'%s\'"', '"[[]%s[]]"', '"[|]%s[|]"', '"-%s-"' ) - } - ) -}) - -test_that("use_base_rules=FALSE produces our preferred behavior", { - restore_package( - pkg <- test_package("unusual_msg"), - tmp_conn = mock_translation("test-translate-package-unusual_msg-1.input"), - { - translate_package(pkg, "es", copyright = "Mata Hari", diagnostics = NULL) - r_pot_lines <- readLines(file.path(pkg, "po", "R-rMsgUnusual.pot")) - src_pot_lines <- readLines(file.path(pkg, "po", "rMsgUnusual.pot")) - - # (1) default copyright comment in metadata - # (2) default blank Language field in metadata - # (3) testing plural string padding - # (4) source tagging - # (5) splitting at newlines - # (6) msgid quote escaping - # (7) copyright - expect_all_match( - r_pot_lines, - c( - "SOME DESCRIPTIVE TITLE", "Language: \\n", "nplurals=INTEGER", - 'msgid "singular "', '#: foo.R', '"\\\\n vs \\n"', - '"strings with escaped \\"quotes\\"', - 'Copyright (C) YEAR Mata Hari' - ), - fixed = TRUE - ) - - # (1) lack of strwrap despite >79 width - # (2) inclusion of file in "unrecognized" cairo folder - expect_all_match( - src_pot_lines, - c( - "#: [A-Z]{26}[.]c:[0-9] [a-z0-5]{32}[.]c:[0-9] msg[.]c:[0-9]{2} msg[.]c:[0-9]{2}", - '^#: cairo/bedfellows\\.c:' - ) - ) - } - ) -}) - -test_that("use_base_rules=TRUE produces base-aligned behavior", { - restore_package( - pkg <- test_package("unusual_msg"), - tmp_conn = mock_translation("test-translate-package-unusual_msg-1.input"), - { - translate_package(pkg, "es", use_base_rules = TRUE, diagnostics = NULL) - r_pot_lines <- readLines(file.path(pkg, "po", "R-rMsgUnusual.pot")) - src_pot_lines <- readLines(file.path(pkg, "po", "rMsgUnusual.pot")) - - # (1)-(5) invert the corresponding number in the previous test_that - expect_all_match( - r_pot_lines, - c("SOME DESCRIPTIVE TITLE", "Language: [\\]n", "nplurals=INTEGER", '#: ', '"\\\\n vs \\n is OK"'), - fixed = TRUE, invert = TRUE - ) - expect_all_match(r_pot_lines, 'msgid "small fail "', fixed = TRUE) - - # (1) MSG.c comes before msg.c (sort/collate order) - # (2) c-format tags are produced - # (3) msgid with many duplicates wraps the source markers _exactly_ at width=79 - # (4)-(11) template-adjacent wrapping/non-wrapping (#90, #150) - expect_all_match( - paste(src_pot_lines, collapse='\n'), - c( - 'MSGs\\.c.*msg\\.c', '#, c-format', - '#: [A-Z]{26}[.]c:[0-9] [a-z0-5]{32}[.]c:[0-9] msg[.]c:[0-9]{2}\n#: msg[.]c', - ' [.]"\n"%s[.]"', ' [?]"\n"%s[?]"', ' ;"\n"%s;"', ' /"\n"%s/"', - '"\'%s\'"', '"[[]%s[]]"', '"[|]%s[|]"', '"-%s-"' - ) - ) - - # (1) only src/*.c and src/windows/*.c are included (no other subdirectories), #114 - # (2)-(8) wrapping surrounding \" matches xgettex, #91 - expect_all_match( - src_pot_lines, - c( - '#: bedfellows.c:', - '56\\"890"', '5(\\"890"', '5\'\\"890"', - '345a"', '345A"', '345#"', '345@"' - ), - fixed = TRUE, invert = TRUE - ) - } - ) -}) - -test_that("use_base_rules is auto-detected", { - restore_package( - pkg <- test_package("r-devel/src/library/grDevices"), - { - translate_package(pkg, diagnostics = NULL) - - r_pot_lines <- readLines(file.path(pkg, 'po', 'R-grDevices.pot')) - src_pot_lines <- readLines(file.path(pkg, 'po', 'grDevices.pot')) - - expect_all_match( - r_pot_lines, - c('"Project-Id-Version: R', '"Report-Msgid-Bugs-To: bugs.r-project.org\\n"'), - fixed = TRUE - ) - - # copyright isn't written in the R file, but is in src? A bit strange but done for consistency w base - expect_all_match( - r_pot_lines, - '# Copyright (C) YEAR The R Core Team', - fixed = TRUE, invert = TRUE - ) - - expect_all_match( - src_pot_lines, - '# Copyright (C) YEAR The R Core Team', - fixed = TRUE - ) - - # first argument to dgettext() should be ignored, #184 - expect_all_match( - src_pot_lines, - 'msgid "grDevices"', - fixed = TRUE, invert = TRUE - ) - } - ) -}) + ) + + # (1) only src/*.c and src/windows/*.c are included (no other subdirectories), #114 + # (2)-(8) wrapping surrounding \" matches xgettex, #91 + expect_all_match( + src_pot_lines, + c( + '#: bedfellows.c:', + '56\\"890"', '5(\\"890"', '5\'\\"890"', + '345a"', '345A"', '345#"', '345@"' + ), + fixed = TRUE, invert = TRUE + ) + } +) + +# TODO: extend with_restoration_test_that to handle +# copying a top directory (here, r-devel) but setting to a sub-directory (here, r-devel/src/library/grDevices) +with_restoration_test_that( + "use_base_rules is auto-detected", + pkg = "r-devel", + { + withr::local_dir("src/library/grDevices") + translate_package(diagnostics = NULL) + + r_pot_lines <- readLines(file.path('po', 'R-grDevices.pot')) + src_pot_lines <- readLines(file.path('po', 'grDevices.pot')) + + expect_all_match( + r_pot_lines, + c('"Project-Id-Version: R', '"Report-Msgid-Bugs-To: bugs.r-project.org\\n"'), + fixed = TRUE + ) + + # copyright isn't written in the R file, but is in src? A bit strange but done for consistency w base + expect_all_match( + r_pot_lines, + '# Copyright (C) YEAR The R Core Team', + fixed = TRUE, invert = TRUE + ) + + expect_all_match( + src_pot_lines, + '# Copyright (C) YEAR The R Core Team', + fixed = TRUE + ) + + # first argument to dgettext() should be ignored, #184 + expect_all_match( + src_pot_lines, + 'msgid "grDevices"', + fixed = TRUE, invert = TRUE + ) + } +) # NB: this is _mostly_ about get_message_data(), but we also test the correct R.pot file is created -test_that("translation of 'base' works correctly", { - restore_package( - pkg <- test_package("r-devel/src/library/base"), - { - # NB: it seems file.rename doesn't work for directories on Windows, so we have the - # more cumbersome file.copy() approach here - correct_share <- file.path(pkg, '../../../share') - tmp_share <- file.path(tempdir(), 'share') - dir.create(tmp_share) - on.exit(unlink(tmp_share, recursive=TRUE)) - file.copy(dirname(correct_share), tmp_share, recursive = TRUE) - unlink(correct_share, recursive = TRUE) - expect_error(translate_package(pkg, diagnostics = NULL), "Translation of the 'base' package", fixed = TRUE) - dir.create(correct_share) - file.copy(tmp_share, dirname(correct_share), recursive = TRUE) - - correct_potfiles <- normalizePath(file.path(pkg, '../../../po/POTFILES')) - # tried file.rename, but it fails on some systems (e.g. Debian) as an "Invalid cross-device link" - expect_true(file.copy(correct_potfiles, tmp_potfiles <- tempfile())) - unlink(correct_potfiles) - expect_error(translate_package(pkg, diagnostics = NULL), "Translation of the 'base' package", fixed = TRUE) - file.copy(tmp_potfiles, correct_potfiles) - on.exit(unlink(tmp_potfiles), add = TRUE) - - translate_package(pkg, diagnostics = NULL) - - expect_true(file.exists(file.path(pkg, 'po', 'R-base.pot'))) - expect_true(file.exists(file.path(pkg, 'po', 'R.pot'))) - - r_pot_lines <- readLines(file.path(pkg, 'po', 'R-base.pot')) - src_pot_lines <- readLines(file.path(pkg, 'po', 'R.pot')) - - # confirm share/R messages are included - expect_all_match( - r_pot_lines, - 'msgid "Go clean your room!"', - fixed = TRUE - ) - - # check relative path is recorded correctly - expect_all_match( - src_pot_lines, - "#: src/main/msg.c:", - fixed = TRUE - ) - } - ) -}) - -test_that("max_translations works as expected", { - prompts <- restore_package( - pkg <- test_package("r_msg"), - tmp_conn = mock_translation('test-translate-package-r_msg-1.input'), - { - translate_package(pkg, 'es', max_translations = 1L, diagnostics = NULL) - } - ) - expect_all_match( - prompts, - "Oh no you don't!", - fixed = TRUE, invert = TRUE - ) -}) +with_restoration_test_that( + "translation of 'base' works correctly", + pkg = "r-devel", + { + withr::local_dir("src/library/base") + + # NB: it seems file.rename doesn't work for directories on Windows, so we have the + # more cumbersome file.copy() approach here + correct_share <- file.path('../../../share') + tmp_share <- file.path(tempdir(), 'share') + dir.create(tmp_share) + on.exit(unlink(tmp_share, recursive=TRUE)) + file.copy(dirname(correct_share), tmp_share, recursive = TRUE) + unlink(correct_share, recursive = TRUE) + expect_error(translate_package(diagnostics = NULL), "Translation of the 'base' package", fixed = TRUE) + dir.create(correct_share) + file.copy(tmp_share, dirname(correct_share), recursive = TRUE) + + correct_potfiles <- normalizePath(file.path('../../../po/POTFILES')) + # tried file.rename, but it fails on some systems (e.g. Debian) as an "Invalid cross-device link" + expect_true(file.copy(correct_potfiles, tmp_potfiles <- tempfile())) + unlink(correct_potfiles) + expect_error(translate_package(diagnostics = NULL), "Translation of the 'base' package", fixed = TRUE) + file.copy(tmp_potfiles, correct_potfiles) + on.exit(unlink(tmp_potfiles), add = TRUE) + + translate_package(diagnostics = NULL) + + expect_true(file.exists(file.path('po', 'R-base.pot'))) + expect_true(file.exists(file.path('po', 'R.pot'))) + + r_pot_lines <- readLines(file.path('po', 'R-base.pot')) + src_pot_lines <- readLines(file.path('po', 'R.pot')) + + # confirm share/R messages are included + expect_all_match( + r_pot_lines, + 'msgid "Go clean your room!"', + fixed = TRUE + ) + + # check relative path is recorded correctly + expect_all_match( + src_pot_lines, + "#: src/main/msg.c:", + fixed = TRUE + ) + } +) + +with_restoration_test_that( + "max_translations works as expected", + pkg = "r_msg", + conn = 'test-translate-package-r_msg-1.input', + code = expect_normalized_snapshot(translate_package(languages='es', max_translations = 1L, diagnostics = NULL)) +)