tests/testthat/test-translate-package.R

test_that("translate_package arg checking errors work", {
  expect_error(translate_package(c("dplyr", "data.table")), "Only one package at a time", fixed=TRUE)
  expect_error(translate_package(1), "'dir' must be a character", fixed=TRUE)
  expect_error(translate_package(languages = 1L), "'languages' must be a character vector", fixed=TRUE)
  expect_error(translate_package(file.path(tempdir(), "abcdefghijklmnopqrstuvwxyz")), "does not exist", fixed=TRUE)

  file.create(tmp <- tempfile())
  on.exit(unlink(tmp))
  expect_error(translate_package(tmp), "not a directory", fixed=TRUE)

  expect_error(translate_package(tempdir()), "not a package (missing DESCRIPTION)", fixed=TRUE)

  file.create(tmp_desc <- file.path(tempdir(), "DESCRIPTION"))
  on.exit(unlink(tmp_desc), add=TRUE)
  expect_error(translate_package(tempdir()), "not a package (missing Package and/or Version", fixed=TRUE)

  # diagnostic argument
  expect_error(translate_package(tempdir(), diagnostics = 1L), "'diagnostics' should be", fixed=TRUE)
  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)
  )
})

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
      )
    }
  )
})

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)
})

# 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
  )
})

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
  )
})

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
      )
    }
  )
})

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(
          paste(
            "Didn't find rSrcMsg.mo; found %s.",
            "**Sysreq paths: %s.",
            "**po/zh_CN contents:",
            "%s",
            "**Direct msgfmt output:",
            "%s**Session info:",
            "%s",
            sep = "\n"
          ),
          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')
        )
      )

      # 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"'
        )
      )
    }
  )

  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
      )
    }
  )
  expect_all_match(
    prompts,
    "Note: a similar message was previously translated as",
    fixed = 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
      )

      # (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"'
        ),
      )
    }
  )
})

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
      )
    }
  )
})

# 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
  )
})

Try the potools package in your browser

Any scripts or data that you put into this service are public.

potools documentation built on Nov. 2, 2023, 5:20 p.m.