Skip to content

Commit e9f85ff

Browse files
Implement po_create()/po_update() for creating/updating translations (#235)
* Implement tr_add() for adding new translations * Add or update as necessary * Only add previous for tr_add() * Fix typo * Split tr_add() into po_create() and po_update() * lang->languages * lang->languages * Mark .pot file as UTF-8 * Split po_create() and po_update() into pieces And fundamentally change approach * Fix broken tests * Add test for po_create() Bringing in system2() code from #257 * WS * Extract out local_test_package() helper * Extract & test po_language_files() * Add tests for create and update And fix the bugs thus revealed * Add missing line * might as well use fifelse * Add links to solaris docs * Revert unintentional change * Move local_test_package() to better home * Revert CHARSET -> UTF-8 change * More docs about updating * Standardise number of dots * Improve docs * Tweak messaging * Revert accidental doc changes * add TODO * another TODO * typo * clarify fuzzy description * comment need for standardise_dots & americanize 🇺🇸 Co-authored-by: Michael Chirico <michaelchirico4@gmail.com>
1 parent ff188d4 commit e9f85ff

15 files changed

Lines changed: 321 additions & 31 deletions

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ export(check_untranslated_cat)
1010
export(check_untranslated_src)
1111
export(get_message_data)
1212
export(po_compile)
13+
export(po_create)
1314
export(po_extract)
1415
export(po_metadata)
16+
export(po_update)
1517
export(translate_package)
1618
export(write_po_file)
1719
importFrom(data.table,"%chin%")

R/msgmerge.R

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
# split off from tools::update_pkg_po() to only run the msgmerge & checkPoFile steps
2-
run_msgmerge = function(po_file, pot_file) {
3-
if (system(sprintf("msgmerge --update %s %s", po_file, shQuote(pot_file))) != 0L) {
2+
3+
# https://www.gnu.org/software/gettext/manual/html_node/msgmerge-Invocation.html
4+
# https://docs.oracle.com/cd/E36784_01/html/E36870/msgmerge-1.html#scrolltoc
5+
run_msgmerge <- function(po_file, pot_file, previous = FALSE, verbose = TRUE) {
6+
args <- c(
7+
"--update", shQuote(path.expand(po_file)),
8+
if (previous) "--previous", #show previous match for fuzzy matches
9+
shQuote(path.expand(pot_file))
10+
)
11+
12+
val <- system2("msgmerge", args, stdout = TRUE, stderr = TRUE)
13+
if (!identical(attr(val, "status", exact = TRUE), NULL)) {
414
# nocov these warnings? i don't know how to trigger them as of this writing.
5-
warningf("Running msgmerge on '%s' failed.", po_file)
15+
warningf("Running msgmerge on './po/%s' failed:\n %s", basename(po_file), paste(val, collapse = "\n"))
16+
} else if (verbose) {
17+
messagef(paste(val, collapse = "\n"))
618
}
719

820
res <- tools::checkPoFile(po_file, strictPlural = TRUE)
@@ -54,3 +66,23 @@ update_en_quot_mo_files <- function(dir, verbose) {
5466
}
5567
return(invisible())
5668
}
69+
70+
# https://www.gnu.org/software/gettext/manual/html_node/msginit-Invocation.html
71+
# https://docs.oracle.com/cd/E36784_01/html/E36870/msginit-1.html#scrolltoc
72+
run_msginit <- function(po_path, pot_path, locale, width = 80, verbose = TRUE) {
73+
args <- c(
74+
"-i", shQuote(path.expand(pot_path)),
75+
"-o", shQuote(path.expand(po_path)),
76+
"-l", shQuote(locale),
77+
"-w", width,
78+
"--no-translator" # don't consult user-email etc
79+
)
80+
val <- system2("msginit", args, stdout = TRUE, stderr = TRUE)
81+
if (!identical(attr(val, "status", exact = TRUE), NULL)) {
82+
stopf("Running msginit on '%s' failed", pot_path)
83+
} else if (verbose) {
84+
messagef(paste(val, collapse = "\n"))
85+
}
86+
return(invisible())
87+
}
88+

R/po_compile.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,13 @@ get_po_metadata <- function(dir = ".", package = NULL) {
5555

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

5960
data.table(
6061
language = languages,
6162
type = type,
6263
po = po_paths,
64+
pot = pot_paths,
6365
mo = mo_paths
6466
)
6567
}

R/po_create.R

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#' Create a new `.po` file
2+
#'
3+
#' @description
4+
#' `po_create()` creates a new `po/{languages}.po` containing the messages to be
5+
#' translated.
6+
#'
7+
#' Generally, we expect you to use `po_create()` to create new `.po` files
8+
#' but if you call it with an existing translation, it will update it with any
9+
#' changes from the `.pot`. See [po_update()] for details.
10+
#'
11+
#' @param languages Language identifiers. These are typically two letters (e.g.
12+
#' "en" = English, "fr" = French, "es" = Spanish, "zh" = Chinese), but
13+
#' can include an additional suffix for languages that have regional
14+
#' variations (e.g. "fr_CN" = French Canadian, "zh_CN" = simplified
15+
#' characters as used in mainland China, "zh_TW" = traditional characters
16+
#' as used in Taiwan.)
17+
#' @inheritParams po_extract
18+
#' @export
19+
po_create <- function(languages, dir = ".", verbose = !is_testing()) {
20+
package <- get_desc_data(dir, "Package")
21+
po_files <- po_language_files(languages, dir)
22+
23+
for (ii in seq_len(nrow(po_files))) {
24+
row <- po_files[ii]
25+
if (file.exists(row$po_path)) {
26+
if (verbose) messagef("Updating '%s' %s translation", row$language, row$type)
27+
run_msgmerge(row$po_path, row$pot_path, previous = TRUE, verbose = verbose)
28+
} else {
29+
if (verbose) messagef("Creating '%s' %s translation", row$language, row$type)
30+
run_msginit(row$po_path, row$pot_path, locale = row$language, verbose = verbose)
31+
}
32+
}
33+
34+
invisible(po_files)
35+
}
36+
37+
# TODO: make sure this works with translating/updating base, which
38+
# has the anti-pattern that src translations are in R.pot, not base.pot.
39+
po_language_files <- function(languages, dir = ".") {
40+
po_files <- data.table::CJ(type = pot_types(dir), language = languages)
41+
po_files[, "po_path" := file.path(dir, "po", paste0(po_prefix(po_files$type), po_files$language, ".po"))]
42+
po_files[, "pot_path" := pot_paths(dir, po_files$type)]
43+
po_files[]
44+
}
45+
46+
# TODO: should this be po_paths, with a template=TRUE/FALSE argument?
47+
pot_paths <- function(dir, type, package = NULL) {
48+
if (is.null(package)) {
49+
package <- get_desc_data(dir, "Package")
50+
}
51+
if (length(type) == 0) {
52+
character()
53+
} else {
54+
file.path(dir, "po", paste0(po_prefix(type), package, ".pot"))
55+
}
56+
57+
}
58+
po_prefix <- function(type = c("R", "src")) {
59+
data.table::fifelse(type == "R", "R-", "")
60+
}
61+
pot_types <- function(dir = ".") {
62+
types <- c("R", "src")
63+
types[file.exists(pot_paths(dir, types))]
64+
}

R/po_extract.R

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,17 @@
11
#' Extract messages for translation into a `.pot` file
22
#'
3+
#' @description
34
#' `po_extract()` scans your package for strings to be translated and
45
#' saves them into a `.pot` template file (in the package's `po`
56
#' directory). You should never modify this file by hand; instead modify the
67
#' underlying source code and re-run `po_extract()`.
78
#'
9+
#' If you have existing translations, call [po_update()] after [po_extract()]
10+
#' to update them with the changes.
811
#'
9-
#' @param dir Character, default the present directory; a directory in which an
10-
#' R package is stored.
11-
#' @param custom_translation_functions A `list` with either/both of two
12-
#' components, `R` and `src`, together governing how to extract any
13-
#' non-standard strings from the package.
14-
#'
15-
#' See Details in [`translate_package()`][translate_package].
16-
#' @param verbose Logical, default `TRUE` (except during testing). Should
17-
#' extra information about progress, etc. be reported?
18-
#' @param style Translation style, either `"base"` or `"explict"`.
19-
#' The default, `NULL`, reads from the `DESCRIPTION` field
20-
#' `Config/potools/style` so you can specify the style once for your
21-
#' package.
22-
#'
23-
#' Both styles extract strings explicitly flagged for translation with
24-
#' `gettext()` or `ngettext()`. The base style additionally extracts
25-
#' strings in calls to `stop()`, `warning()`, and `message()`,
26-
#' and to `stopf()`, `warningf()`, and `messagef()` if you have
27-
#' added those helpers to your package. The explicit style also accepts
28-
#' `tr_()` as a short hand for `gettext()`. See
29-
#' `vignette("developer")` for more details.
30-
#' @return The extracted messages as computed by
31-
#' [`get_message_data()`][get_message_data], invisibly.
12+
#' @returns The extracted messages as computed by [get_message_data()],
13+
#' invisibly.
14+
#' @inheritParams get_message_data
3215
#' @export
3316
po_extract <- function(
3417
dir = ".",

R/po_update.R

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#' Update all `.po` files with changes in `.pot`
2+
#'
3+
#' @description
4+
#' `po_update()` updates existing `.po` file after the `.pot` file has changed.
5+
#' There are four cases:
6+
#'
7+
#' * New messages: added with blank `msgstr`.
8+
#'
9+
#' * Deleted messages: marked as deprecated and moved to the bottom of the file.
10+
#'
11+
#' * Major changes to existing messages: appear as an addition and a deletion.
12+
#'
13+
#' * Minor changes to existing messages: will be flagged as fuzzy.
14+
#'
15+
#' ```
16+
#' #, fuzzy, c-format
17+
#' #| msgid "Generating en@quot translations"
18+
#' msgid "Updating '%s' %s translation"
19+
#' msgstr "en@quot翻訳生成中。。。"
20+
#' ```
21+
#'
22+
#' The previous message is given in comments starting with `#|`.
23+
#' Translators need to update the actual (uncommented) `msgstr` manually,
24+
#' using the old `msgid` as a potential reference, then
25+
#' delete the old translation and the `fuzzy` comment (c-format should
26+
#' remain, if present).
27+
#'
28+
#' @inheritParams po_extract
29+
#' @param lazy If `TRUE`, only `.po` files that are older than their
30+
#' corresponding `.pot` file will be updated.
31+
#' @export
32+
po_update <- function(dir = ".", lazy = TRUE, verbose = !is_testing()) {
33+
meta <- get_po_metadata(dir)
34+
if (lazy) {
35+
meta <- meta[is_outdated(meta$po, meta$pot)]
36+
}
37+
38+
for (ii in seq_len(nrow(meta))) {
39+
row <- meta[ii]
40+
if (verbose) messagef("Updating '%s' %s translation", row$language, row$type)
41+
run_msgmerge(row$po, row$pot, previous = TRUE, verbose = verbose)
42+
}
43+
44+
invisible(meta)
45+
}

man/po_create.Rd

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/po_extract.Rd

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/po_update.Rd

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/po_create.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# the user is told what's happening
2+
3+
Code
4+
po_create("jp", verbose = TRUE)
5+
Message <simpleMessage>
6+
Creating 'jp' R translation
7+
Created ./po/R-jp.po.
8+
9+
---
10+
11+
Code
12+
po_create("jp", verbose = TRUE)
13+
Message <simpleMessage>
14+
Updating 'jp' R translation
15+
. done.
16+

0 commit comments

Comments
 (0)