Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Implement po_create()/po_update() for creating/updating translations #235

Merged
merged 36 commits into from
Nov 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3affcc0
Implement tr_add() for adding new translations
MichaelChirico Jul 12, 2021
63dcbc5
Add or update as necessary
hadley Nov 1, 2021
cfe1eff
Only add previous for tr_add()
hadley Nov 1, 2021
b205c34
Fix typo
hadley Nov 1, 2021
6124465
Split tr_add() into po_create() and po_update()
hadley Nov 4, 2021
c644cb7
lang->languages
MichaelChirico Nov 6, 2021
a088663
lang->languages
MichaelChirico Nov 6, 2021
28f3e28
Merge commit 'bd9c91a2abc717fe52b56cf6879f83f233a299a1'
hadley Nov 9, 2021
310f2b0
Move to own file
hadley Nov 9, 2021
10e16fd
Mark .pot file as UTF-8
hadley Nov 9, 2021
42885b1
Split po_create() and po_update() into pieces
hadley Nov 9, 2021
205ee8f
Fix broken tests
hadley Nov 9, 2021
0d3391d
Add test for po_create()
hadley Nov 9, 2021
d8880e0
WS
hadley Nov 9, 2021
efa92b6
Extract out local_test_package() helper
hadley Nov 9, 2021
511fad1
Extract & test po_language_files()
hadley Nov 9, 2021
d7c3882
Add tests for create and update
hadley Nov 9, 2021
0daabcd
Add missing line
hadley Nov 9, 2021
b604281
might as well use fifelse
MichaelChirico Nov 10, 2021
ef5449f
Add links to solaris docs
hadley Nov 10, 2021
7322e55
Revert unintentional change
hadley Nov 10, 2021
78afb31
Move local_test_package() to better home
hadley Nov 10, 2021
7133453
Revert CHARSET -> UTF-8 change
hadley Nov 10, 2021
df99ea4
Merge commit '3eaf727e03b1238d01492b99f58f938587b4b73b'
hadley Nov 10, 2021
f081b01
Merged origin/add-translation into add-translation
hadley Nov 10, 2021
6fd0c30
More docs about updating
hadley Nov 10, 2021
0e4371c
Standardise number of dots
hadley Nov 10, 2021
4b3353f
Improve docs
hadley Nov 10, 2021
ae5c5b7
Tweak messaging
hadley Nov 10, 2021
b4e71bd
Merge branch 'master' into add-translation
hadley Nov 10, 2021
b8e17e9
Revert accidental doc changes
hadley Nov 10, 2021
0f93068
add TODO
MichaelChirico Nov 11, 2021
3ff40d5
another TODO
MichaelChirico Nov 11, 2021
5efd88f
typo
MichaelChirico Nov 11, 2021
e6788bd
clarify fuzzy description
MichaelChirico Nov 11, 2021
a36031a
comment need for standardise_dots & americanize 🇺🇸
MichaelChirico Nov 11, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export(check_untranslated_cat)
export(check_untranslated_src)
export(get_message_data)
export(po_compile)
export(po_create)
export(po_extract)
export(po_metadata)
export(po_update)
export(translate_package)
export(write_po_file)
importFrom(data.table,"%chin%")
Expand Down
38 changes: 35 additions & 3 deletions R/msgmerge.R
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
# split off from tools::update_pkg_po() to only run the msgmerge & checkPoFile steps
run_msgmerge = function(po_file, pot_file) {
if (system(sprintf("msgmerge --update %s %s", po_file, shQuote(pot_file))) != 0L) {

# https://www.gnu.org/software/gettext/manual/html_node/msgmerge-Invocation.html
# https://docs.oracle.com/cd/E36784_01/html/E36870/msgmerge-1.html#scrolltoc
run_msgmerge <- function(po_file, pot_file, previous = FALSE, verbose = TRUE) {
args <- c(
"--update", shQuote(path.expand(po_file)),
if (previous) "--previous", #show previous match for fuzzy matches
hadley marked this conversation as resolved.
Show resolved Hide resolved
shQuote(path.expand(pot_file))
)

val <- system2("msgmerge", args, stdout = TRUE, stderr = TRUE)
if (!identical(attr(val, "status", exact = TRUE), NULL)) {
# nocov these warnings? i don't know how to trigger them as of this writing.
warningf("Running msgmerge on '%s' failed.", po_file)
warningf("Running msgmerge on './po/%s' failed:\n %s", basename(po_file), paste(val, collapse = "\n"))
} else if (verbose) {
messagef(paste(val, collapse = "\n"))
}

res <- tools::checkPoFile(po_file, strictPlural = TRUE)
Expand Down Expand Up @@ -54,3 +66,23 @@ update_en_quot_mo_files <- function(dir, verbose) {
}
return(invisible())
}

# https://www.gnu.org/software/gettext/manual/html_node/msginit-Invocation.html
# https://docs.oracle.com/cd/E36784_01/html/E36870/msginit-1.html#scrolltoc
run_msginit <- function(po_path, pot_path, locale, width = 80, verbose = TRUE) {
hadley marked this conversation as resolved.
Show resolved Hide resolved
args <- c(
"-i", shQuote(path.expand(pot_path)),
"-o", shQuote(path.expand(po_path)),
"-l", shQuote(locale),
"-w", width,
"--no-translator" # don't consult user-email etc
)
val <- system2("msginit", args, stdout = TRUE, stderr = TRUE)
if (!identical(attr(val, "status", exact = TRUE), NULL)) {
stopf("Running msginit on '%s' failed", pot_path)
} else if (verbose) {
messagef(paste(val, collapse = "\n"))
}
return(invisible())
}

2 changes: 2 additions & 0 deletions R/po_compile.R
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@ get_po_metadata <- function(dir = ".", package = NULL) {

mo_names <- gsub(lang_regex, sprintf("\\1%s.mo", package), basename(po_paths))
mo_paths <- file.path(dir, "inst", "po", languages, "LC_MESSAGES", mo_names)
pot_paths <- pot_paths(dir, type, package = package)

data.table(
language = languages,
type = type,
po = po_paths,
pot = pot_paths,
mo = mo_paths
)
}
64 changes: 64 additions & 0 deletions R/po_create.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#' Create a new `.po` file
#'
#' @description
#' `po_create()` creates a new `po/{languages}.po` containing the messages to be
#' translated.
#'
#' Generally, we expect you to use `po_create()` to create new `.po` files
#' but if you call it with an existing translation, it will update it with any
#' changes from the `.pot`. See [po_update()] for details.
#'
#' @param languages Language identifiers. These are typically two letters (e.g.
#' "en" = English, "fr" = French, "es" = Spanish, "zh" = Chinese), but
#' can include an additional suffix for languages that have regional
#' variations (e.g. "fr_CN" = French Canadian, "zh_CN" = simplified
#' characters as used in mainland China, "zh_TW" = traditional characters
#' as used in Taiwan.)
#' @inheritParams po_extract
#' @export
po_create <- function(languages, dir = ".", verbose = !is_testing()) {
package <- get_desc_data(dir, "Package")
po_files <- po_language_files(languages, dir)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be nice to warn() here if e.g. R-pkg.pot is present but pkg.pot is not & the package has a src/ directory.

OTOH, if the translation is handled entirely by potools, this should only happen if src/ doesn't have any messages to translate, right? So maybe it will be a warning highly prone to false positives...

skipping for now, but file a follow-up issue if you think the warning is worthwhile.


for (ii in seq_len(nrow(po_files))) {
row <- po_files[ii]
if (file.exists(row$po_path)) {
if (verbose) messagef("Updating '%s' %s translation", row$language, row$type)
run_msgmerge(row$po_path, row$pot_path, previous = TRUE, verbose = verbose)
} else {
if (verbose) messagef("Creating '%s' %s translation", row$language, row$type)
run_msginit(row$po_path, row$pot_path, locale = row$language, verbose = verbose)
}
}

invisible(po_files)
}

# TODO: make sure this works with translating/updating base, which
# has the anti-pattern that src translations are in R.pot, not base.pot.
po_language_files <- function(languages, dir = ".") {
po_files <- data.table::CJ(type = pot_types(dir), language = languages)
po_files[, "po_path" := file.path(dir, "po", paste0(po_prefix(po_files$type), po_files$language, ".po"))]
po_files[, "pot_path" := pot_paths(dir, po_files$type)]
po_files[]
}

# TODO: should this be po_paths, with a template=TRUE/FALSE argument?
pot_paths <- function(dir, type, package = NULL) {
if (is.null(package)) {
package <- get_desc_data(dir, "Package")
}
if (length(type) == 0) {
character()
} else {
file.path(dir, "po", paste0(po_prefix(type), package, ".pot"))
}

}
po_prefix <- function(type = c("R", "src")) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the one place this will break is for base, which uses R-base.pot for R and R.pot for src. I'll just add a TODO for now to make sure that case is handled later.

data.table::fifelse(type == "R", "R-", "")
}
pot_types <- function(dir = ".") {
types <- c("R", "src")
types[file.exists(pot_paths(dir, types))]
}
29 changes: 6 additions & 23 deletions R/po_extract.R
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
#' Extract messages for translation into a `.pot` file
#'
#' @description
#' `po_extract()` scans your package for strings to be translated and
#' saves them into a `.pot` template file (in the package's `po`
#' directory). You should never modify this file by hand; instead modify the
#' underlying source code and re-run `po_extract()`.
#'
#' If you have existing translations, call [po_update()] after [po_extract()]
#' to update them with the changes.
#'
#' @param dir Character, default the present directory; a directory in which an
#' R package is stored.
#' @param custom_translation_functions A `list` with either/both of two
#' components, `R` and `src`, together governing how to extract any
#' non-standard strings from the package.
#'
#' See Details in [`translate_package()`][translate_package].
#' @param verbose Logical, default `TRUE` (except during testing). Should
#' extra information about progress, etc. be reported?
#' @param style Translation style, either `"base"` or `"explict"`.
#' The default, `NULL`, reads from the `DESCRIPTION` field
#' `Config/potools/style` so you can specify the style once for your
#' package.
#'
#' Both styles extract strings explicitly flagged for translation with
#' `gettext()` or `ngettext()`. The base style additionally extracts
#' strings in calls to `stop()`, `warning()`, and `message()`,
#' and to `stopf()`, `warningf()`, and `messagef()` if you have
#' added those helpers to your package. The explicit style also accepts
#' `tr_()` as a short hand for `gettext()`. See
#' `vignette("developer")` for more details.
#' @return The extracted messages as computed by
#' [`get_message_data()`][get_message_data], invisibly.
#' @returns The extracted messages as computed by [get_message_data()],
#' invisibly.
#' @inheritParams get_message_data
#' @export
po_extract <- function(
dir = ".",
Expand Down
45 changes: 45 additions & 0 deletions R/po_update.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#' Update all `.po` files with changes in `.pot`
#'
#' @description
#' `po_update()` updates existing `.po` file after the `.pot` file has changed.
#' There are four cases:
#'
#' * New messages: added with blank `msgstr`.
#'
#' * Deleted messages: marked as deprecated and moved to the bottom of the file.
#'
#' * Major changes to existing messages: appear as an addition and a deletion.
#'
#' * Minor changes to existing messages: will be flagged as fuzzy.
#'
#' ```
#' #, fuzzy, c-format
#' #| msgid "Generating en@quot translations"
#' msgid "Updating '%s' %s translation"
#' msgstr "en@quot翻訳生成中。。。"
#' ```
#'
#' The previous message is given in comments starting with `#|`.
#' Translators need to update the actual (uncommented) `msgstr` manually,
#' using the old `msgid` as a potential reference, then
#' delete the old translation and the `fuzzy` comment (c-format should
#' remain, if present).
#'
#' @inheritParams po_extract
#' @param lazy If `TRUE`, only `.po` files that are older than their
#' corresponding `.pot` file will be updated.
#' @export
po_update <- function(dir = ".", lazy = TRUE, verbose = !is_testing()) {
meta <- get_po_metadata(dir)
if (lazy) {
meta <- meta[is_outdated(meta$po, meta$pot)]
}

for (ii in seq_len(nrow(meta))) {
row <- meta[ii]
if (verbose) messagef("Updating '%s' %s translation", row$language, row$type)
run_msgmerge(row$po, row$pot, previous = TRUE, verbose = verbose)
}

invisible(meta)
}
30 changes: 30 additions & 0 deletions man/po_create.Rd

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

7 changes: 5 additions & 2 deletions man/po_extract.Rd

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

36 changes: 36 additions & 0 deletions man/po_update.Rd

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

16 changes: 16 additions & 0 deletions tests/testthat/_snaps/po_create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# the user is told what's happening

Code
po_create("jp", verbose = TRUE)
Message <simpleMessage>
Creating 'jp' R translation
Created ./po/R-jp.po.

---

Code
po_create("jp", verbose = TRUE)
Message <simpleMessage>
Updating 'jp' R translation
. done.

10 changes: 10 additions & 0 deletions tests/testthat/_snaps/po_update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# user is told what's happening

Code
po_update(verbose = TRUE, lazy = FALSE)
Message <simpleMessage>
Updating 'fr' R translation
. done.
Updating 'ja' R translation
. done.

22 changes: 22 additions & 0 deletions tests/testthat/helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,25 @@ 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))

local_test_package <- function(..., .envir = parent.frame()) {
temp <- withr::local_tempdir(.local_envir = .envir)
writeLines(con = file.path(temp, "DESCRIPTION"), c(
"Package: test",
"Version: 1.0.0"
))
dir_create(file.path(temp, c("po", "R")))

files <- list(...)
for (i in seq_along(files)) {
writeLines(files[[i]], file.path(temp, names(files)[[i]]))
}

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)
}
11 changes: 8 additions & 3 deletions tests/testthat/test-po_compile.R
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# metadata ----------------------------------------------------------------

test_that("can find R and src translations", {
temp <- withr::local_tempdir()
dir.create(file.path(temp, "po"))
temp <- local_test_package()
file.create(file.path(temp, "po", c("R-en.po", "en.po")))

meta <- withr::with_dir(temp, get_po_metadata(package = "test"))
meta <- withr::with_dir(temp, get_po_metadata())
expect_equal(meta$language, c("en", "en"))
expect_setequal(meta$type, c("R", "src"))
})

test_that("get_po_metadata() returns 0 rows if no .po fles", {
temp <- local_test_package()
meta <- get_po_metadata(temp)
expect_equal(nrow(meta), 0)
})
Loading