[education/rkward] /: Add .rk.i18n() and message extraction for rkward R package
Thomas Friedrichsmeier
null at kde.org
Thu Oct 16 20:57:56 BST 2025
Git commit e42b9aed2e5878c85398560d234741d79370d83e by Thomas Friedrichsmeier.
Committed on 14/10/2025 at 16:13.
Pushed by tfry into branch 'master'.
Add .rk.i18n() and message extraction for rkward R package
This also prompted a small update to .rk.cat.output(), to auto-paste
several arguments, obviating the need for extra paste() calls
M +3 -3 Messages.sh
M +8 -1 rkward/rbackend/rkrbackend.cpp
M +1 -1 rkward/rbackend/rpackages/rkward/DESCRIPTION
M +26 -4 rkward/rbackend/rpackages/rkward/R/internal.R
M +2 -2 rkward/rbackend/rpackages/rkward/R/internal_graphics.R
M +7 -11 rkward/rbackend/rpackages/rkward/R/public_graphics.R
M +10 -10 rkward/rbackend/rpackages/rkward/R/rk.download_appimage.R
M +14 -14 rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R
M +4 -4 rkward/rbackend/rpackages/rkward/R/rk.menu.R
M +2 -2 rkward/rbackend/rpackages/rkward/R/rk.output.R
M +4 -4 rkward/rbackend/rpackages/rkward/R/rk.print-functions.R
A +198 -0 scripts/extract_r_message.py
https://invent.kde.org/education/rkward/-/commit/e42b9aed2e5878c85398560d234741d79370d83e
diff --git a/Messages.sh b/Messages.sh
index c3ee467cd..84aa07b47 100755
--- a/Messages.sh
+++ b/Messages.sh
@@ -8,9 +8,9 @@
# invoke the extractrc script on all .ui, .rc, and .kcfg files in the sources
# the results are stored in a pseudo .cpp file to be picked up by xgettext.
$EXTRACTRC `find rkward -name \*.rc -a \! -name rkward_windows_icon.rc -o -name \*.ui -o -name \*.kcfg` >> rc.cpp
-#
-# call xgettext on all source files. If your sources have other filename
-# extensions besides .cc, .cpp, and .h, just add them in the find call.
+# merge messages from .R files into rc.cpp
+python3 scripts/update_plugin_messages.py rkward/rbackend/rpackages/rkward/R/*.R >> rc.cpp
+# call xgettext on all source files in the main app.
$XGETTEXT `find rkward -name \*.cpp -o -name \*.h -name \*.c` rc.cpp -o $podir/rkward.pot
# extract messages from global .rkh pages: >> rkward__pages.pot
diff --git a/rkward/rbackend/rkrbackend.cpp b/rkward/rbackend/rkrbackend.cpp
index fa0541412..d77b19369 100644
--- a/rkward/rbackend/rkrbackend.cpp
+++ b/rkward/rbackend/rkrbackend.cpp
@@ -892,7 +892,13 @@ SEXP doSimpleBackendCall(SEXP _call) {
QStringList list = RKRSupport::SEXPToStringList(_call);
QString call = list[0];
- if (call == QStringLiteral("unused.filename")) {
+ if (call == QStringLiteral("i18n")) {
+ auto msg = ki18n(list.value(1).toUtf8().constData());
+ for (int i = 2; i < list.length(); ++i) {
+ msg = msg.subs(list[i]);
+ }
+ return RKRSupport::StringListToSEXP(QStringList(msg.toString())); // TODO: Avoid wrapping into QSringList
+ } else if (call == QStringLiteral("unused.filename")) {
QString prefix = list.value(1);
QString extension = list.value(2);
QString dirs = list.value(3);
@@ -933,6 +939,7 @@ SEXP doUpdateLocale() {
RK_TRACE(RBACKEND);
RK_DEBUG(RBACKEND, DL_WARNING, "Changing locale");
+ // TODO: properly handle re-initialization of translation, too!
RKTextCodec::reinit();
return ROb(R_NilValue);
diff --git a/rkward/rbackend/rpackages/rkward/DESCRIPTION b/rkward/rbackend/rpackages/rkward/DESCRIPTION
index 9ba922832..2e3464a83 100755
--- a/rkward/rbackend/rpackages/rkward/DESCRIPTION
+++ b/rkward/rbackend/rpackages/rkward/DESCRIPTION
@@ -15,7 +15,7 @@ LazyLoad: yes
Authors at R: c(person(given="Thomas", family="Friedrichsmeier", email="thomas.friedrichsmeier at kdemail.net", role=c("aut")), person(given="the RKWard", family="team",
email="rkward at kde.org", role=c("cre","aut")))
Version: 0.8.3
-Date: 2025-10-10
+Date: 2025-10-11
RoxygenNote: 7.3.3
Collate:
'base_overrides.R'
diff --git a/rkward/rbackend/rpackages/rkward/R/internal.R b/rkward/rbackend/rpackages/rkward/R/internal.R
index 301ed195e..434eacb0a 100755
--- a/rkward/rbackend/rpackages/rkward/R/internal.R
+++ b/rkward/rbackend/rpackages/rkward/R/internal.R
@@ -274,14 +274,14 @@
# hidden, as this is not portable to different output formats
#' @export
-".rk.cat.output" <- function (x) {
- cat (x, file = rk.get.output.html.file(), append = TRUE)
+".rk.cat.output" <- function(...) {
+ cat(..., file = rk.get.output.html.file(), append = TRUE)
}
#' @importFrom utils URLencode
#' @export
-".rk.rerun.plugin.link" <- function (plugin, settings, label) {
- .rk.cat.output (paste ("<a href=\"rkward://runplugin/", plugin, "/", URLencode (settings), "\">", label, "</a>", sep=""))
+".rk.rerun.plugin.link" <- function(plugin, settings, label) {
+ .rk.cat.output("<a href=\"rkward://runplugin/", plugin, "/", URLencode(settings), "\">", label, "</a>", sep="")
}
#' @export
@@ -360,3 +360,25 @@ assign(".rk.shadow.envs", new.env(parent=emptyenv()), envir=.rk.variables)
names(ret) <- c("added", "removed", "changed")
ret
}
+
+# Not exported
+# So why to we use our own i18n mechanism, rather than R's gettext() and friends?
+# For one thing this is easier to integrate into the KDE translation workflow:
+# - we need to extract i18n-calls and provide them to translators. Using R's update_pkg_po would mean to push R as a requirement
+# to the extraction scripts. We'd like to avoid that, meaning, we need a custom extraction script, anyway.
+# - the resulting .pot would either need to be provided separately, or merged with another, and the final translations would have to be
+# moved to the places where R expects them. That means additional scripting complexity that we'd like to avoid
+# - R's translation mechanism induces string puzzles, as it does not support the nifty arg replacement in KDE i18n.
+# While we could trivially implement that on top of R's gettext(), that already implies using a custom wrapper.
+# - This i18n-mechanism concerns only rkward package code, and nothing else. Not relevant to users.
+".rk.i18n" <- function(x, ...) {
+ args <- as.character(...)
+ if (!.rk.inside.rkward.session()) {
+ if (length(args)) {
+ for (i in 1:length(args)) x <- sub(x, paste0("%", i), args[i], fixed=TRUE)
+ }
+ x
+ } else {
+ .rk.call.backend("i18n", c(x, args))
+ }
+}
diff --git a/rkward/rbackend/rpackages/rkward/R/internal_graphics.R b/rkward/rbackend/rpackages/rkward/R/internal_graphics.R
index f69e9a68a..ac708621b 100644
--- a/rkward/rbackend/rpackages/rkward/R/internal_graphics.R
+++ b/rkward/rbackend/rpackages/rkward/R/internal_graphics.R
@@ -15,8 +15,8 @@
#' @importFrom grDevices dev.new
#' @export
"rk.screen.device" <- function (...) {
- warning ("rk.screen.device() is obsolete.\nUse one of dev.new(), RK(), or rk.embed.device(), instead.")
- dev.new (...)
+ warning(.rk.i18n("rk.screen.device() is obsolete.\nUse one of dev.new(), RK(), or rk.embed.device(), instead."))
+ dev.new(...)
}
# Fetch the current size of the given RK() device from the frontend, and redraw
diff --git a/rkward/rbackend/rpackages/rkward/R/public_graphics.R b/rkward/rbackend/rpackages/rkward/R/public_graphics.R
index 8f79dab12..55d657f38 100644
--- a/rkward/rbackend/rpackages/rkward/R/public_graphics.R
+++ b/rkward/rbackend/rpackages/rkward/R/public_graphics.R
@@ -69,17 +69,14 @@
if (device.type == "PNG") {
filename <- rk.get.tempfile.name(prefix = "graph", extension = ".png")
ret <- png(filename = file.path(filename), width = width, height = height, ...)
- .rk.cat.output(paste("<img src=\"", make.url (names (filename)), "\" width=\"", width,
- "\" height=\"", height, "\"><br>", sep = ""))
+ .rk.cat.output("<img src=\"", make.url(names(filename)), "\" width=\"", width, "\" height=\"", height, "\"><br>", sep = "")
} else if (device.type == "JPG") {
if (missing (quality)) {
- quality = getOption ("rk.graphics.jpg.quality") # COMPAT: getOption (x, *default*) not yet available in R 2.9
- if (is.null (quality)) quality = 75
+ quality = getOption("rk.graphics.jpg.quality", 75)
}
filename <- rk.get.tempfile.name(prefix = "graph", extension = ".jpg")
ret <- jpeg(filename = file.path(filename), width = width, height = height, "quality"=quality, ...)
- .rk.cat.output(paste("<img src=\"", make.url (names (filename)), "\" width=\"", width,
- "\" height=\"", height, "\"><br>", sep = ""))
+ .rk.cat.output("<img src=\"", make.url(names(filename)), "\" width=\"", width, "\" height=\"", height, "\"><br>", sep = "")
} else if (device.type == "SVG") {
if (!capabilities ("cairo")) { # cairo support is not always compiled in
requireNamespace ("Cairo")
@@ -89,13 +86,12 @@
# ret <- svg(filename = file.path(filename), ...)
# CairoSVG() uses "file" as first argument, grDevices uses "filename" in svg()
ret <- svg(file.path(filename), ...)
- .rk.cat.output(paste("<object data=\"", make.url (names (filename)), "\" type=\"image/svg+xml\" width=\"", width,
- "\" height=\"", height, "\">\n", sep = ""))
- .rk.cat.output(paste("<param name=\"src\" value=\"", make.url (names (filename)), "\">\n", sep = ""))
- .rk.cat.output(paste("This browser appears incapable of displaying SVG object. The SVG source is at:", filename))
+ .rk.cat.output("<object data=\"", make.url(names(filename)), "\" type=\"image/svg+xml\" width=\"", width,
+ "\" height=\"", height, "\">\n", "<param name=\"src\" value=\"", make.url(names(filename)), "\">\n", sep = "")
+ .rk.cat.output("This browser appears incapable of displaying SVG object. The SVG source is at:", filename)
.rk.cat.output("</object>")
} else {
- stop (paste ("Device type \"", device.type, "\" is unknown to RKWard", sep=""))
+ stop(.rk.i18n("Device type \"%1\" is unknown to RKWard", device.type))
}
invisible (ret)
diff --git a/rkward/rbackend/rpackages/rkward/R/rk.download_appimage.R b/rkward/rbackend/rpackages/rkward/R/rk.download_appimage.R
index 2fbc56a9d..939eb150a 100644
--- a/rkward/rbackend/rpackages/rkward/R/rk.download_appimage.R
+++ b/rkward/rbackend/rpackages/rkward/R/rk.download_appimage.R
@@ -64,7 +64,7 @@ rk.download_appimage <- function(
rkward::require(XiMpLe)
XiMpLe_version <- utils::packageVersion("XiMpLe")
if(isFALSE(XiMpLe_version >= "0.11.3")){
- stop(simpleError(paste0("rk.download_appimage() requires XiMpLe >= 0.11.3, but only found ", XiMpLe_version, ". Please update XiMpLe.")))
+ stop(.rk.i18n("rk.download_appimage() requires XiMpLe >= 0.11.3, but only found %1. Please update XiMpLe.", XiMpLe_version), call.=FALSE)
} else {}
rk_ai_html <- XiMpLe::parseXMLTree(url, drop="empty_attributes")
rk_ai_hrefs <- XiMpLe::XMLScanDeep(rk_ai_html, find="href")
@@ -74,17 +74,17 @@ rk.download_appimage <- function(
if(isTRUE(download)){
if(!dir.exists(dir)){
- stop(simpleError(paste0("Target directory does not exist:\n ", dir)))
+ stop(.rk.i18n("Target directory '%1' does not exist\n", dir), call.=FALSE)
} else {}
have_backup <- FALSE
target_file <- file.path(dir, filename)
if(file.exists(target_file)){
if(isFALSE(overwrite)){
- stop(simpleError(paste0("Target file already exists, use the \"overwrite\" argument to replace it:\n ", target_file)))
+ stop(.rk.i18n("Target file '%1' already exists, use the \"overwrite\" argument to replace it:\n", target_file), call.=FALSE)
} else {}
# create backup of current AppImage as a fallback
rk_ai_backup <- paste0(target_file, ".backup_", format(Sys.time(), "%Y-%m-%d_%H%M%S"))
- message(paste0("Creating backup of current AppImage:\n ", rk_ai_backup))
+ message(.rk.i18n("Creating backup of current AppImage: %1\n ", rk_ai_backup))
file.rename(
from=target_file
, to=rk_ai_backup
@@ -109,35 +109,35 @@ rk.download_appimage <- function(
from=rk_ai_backup
, to=target_file
)
- stop("Something went wrong with the download! Restored previous AppImage from backup.", call.=FALSE)
+ stop(.rk.i18n("Something went wrong with the download! Restored previous AppImage from backup."), call.=FALSE)
} else {
file.remove(target_file)
- stop("Something went wrong with the download!", call.=FALSE)
+ stop(.rk.i18n("Something went wrong with the download!"), call.=FALSE)
}
}
, finally = options(timeout = timeout_orig)
)
if(isTRUE(dl_status == 0)){
- message("Download successful, setting file permissions.")
+ message(.rk.i18n("Download successful, setting file permissions."))
Sys.chmod(
paths=target_file
, mode="0755"
, use_umask=TRUE
)
if(isTRUE(have_backup)){
- message("Removing backup of previous AppImage.")
+ message(.rk.i18n("Removing backup of previous AppImage."))
file.remove(rk_ai_backup)
} else {}
return(invisible(target_file))
} else {
if(isTRUE(have_backup)){
- warning("Something went wrong with the download! Restoring previous AppImage from backup.", call.=FALSE)
+ warning(.rk.i18n("Something went wrong with the download! Restoring previous AppImage from backup."), call.=FALSE)
file.rename(
from=rk_ai_backup
, to=target_file
)
} else {
- warning("Something went wrong with the download!", call.=FALSE)
+ warning(.rk.i18n("Something went wrong with the download!"), call.=FALSE)
file.remove(target_file)
}
}
diff --git a/rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R b/rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R
index e15b8283c..ecfc3be23 100644
--- a/rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R
+++ b/rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R
@@ -137,19 +137,19 @@
if(!is.null(li$codepage)) return(paste0("windows-", li$codepage))
return(tail(strsplit(Sys.getlocale("LC_CTYPE"), ".") ,1))
}
- .rk.cat.output (paste ("<?xml version=\"1.0\" encoding=\"", encoding.name(), "\"?>\n", sep=""))
- .rk.cat.output ("<html><head>\n<title>RKWard Output</title>\n")
+ .rk.cat.output("<?xml version=\"1.0\" encoding=\"", encoding.name(), "\"?>\n", sep="")
+ .rk.cat.output("<html><head>\n<title>RKWard Output</title>\n")
if (!is.null (css)) {
cssfilename <- paste (sub ("\\.[^.]*$", "", basename (x)), ".css", sep="")
- .rk.cat.output (paste ("<link rel=\"StyleSheet\" type=\"text/css\" href=\"", cssfilename, "\"/>\n", sep=""))
+ .rk.cat.output("<link rel=\"StyleSheet\" type=\"text/css\" href=\"", cssfilename, "\"/>\n", sep="")
cssfile <- file.path (dirname (x), cssfilename)
if (!file.copy (css, cssfile, overwrite=TRUE)) {
- warning ("Failed to copy CSS file ", css, " to ", cssfile)
+ warning(.rk.i18n("Failed to copy CSS file '%1' to '%2'", css, cssfile))
}
}
# the next part defines a JavaScript function to add individual results to a global table of contents menu in the document
if (style != "preview") {
- .rk.cat.output (paste ("\t<script type=\"text/javascript\">
+ .rk.cat.output("\t<script type=\"text/javascript\">
<!--
function addToTOC(id, level){
var fullHeader = document.getElementById(id);
@@ -197,18 +197,18 @@
}
}
}
- // -->\n\t</script>\n", sep=""))
+ // -->\n\t</script>\n", sep="")
# positioning of the TOC is done by CSS, default state is hidden
# see $SRC/rkward/pages/rkward_output.css
}
- if (!is.null (additional.header.contents)) .rk.cat.output (as.character (additional.header.contents))
- .rk.cat.output ("</head>\n<body>\n")
+ if (!is.null (additional.header.contents)) .rk.cat.output(as.character(additional.header.contents))
+ .rk.cat.output("</head>\n<body>\n")
if (style != "preview") {
# This initial output mostly to indicate the output is really there, just empty for now
- .rk.cat.output (paste ("<a name=\"top\"></a>\n<pre>RKWard output initialized on", .rk.date (), "</pre>\n"))
+ .rk.cat.output("<a name=\"top\"></a>\n<pre>RKWard output initialized on", .rk.date (), "</pre>\n")
# an empty <div> where the TOC menu gets added to dynamically, and a second one to toggle show/hide
- .rk.cat.output (paste (
+ .rk.cat.output(
"<div id=\"RKWardResultsTOCShown\" class=\"RKTOC\">\n",
"\t<a onclick=\"javascript:switchVisible('RKWardResultsTOCHidden','RKWardResultsTOCShown'); return false;\" href=\"\" class=\"toggleTOC\">Hide TOC</a>\n",
"\t<span class=\"right\"><a href=\"#top\" class=\"toggleTOC\">Go to top</a></span>\n<br />",
@@ -220,7 +220,7 @@
"<div id=\"RKWardResultsTOCHidden\" class=\"RKTOC RKTOChidden\">\n",
"\t<a onclick=\"javascript:switchVisible('RKWardResultsTOCShown','RKWardResultsTOCHidden'); return false;\" href=\"\" class=\"toggleTOC\">Show TOC</a>\n",
"\t<span class=\"right\"><a href=\"#top\" class=\"toggleTOC\">Go to top</a></span>\n",
- "</div>\n", sep=""))
+ "</div>\n", sep="")
}
}
@@ -374,11 +374,11 @@ function registerPlot(element) {
hook <- RK.addHook(
after.create=function(devnum, ...) {
- .rk.cat.output("<div align=\"right\">Plot window created</div>\n");
+ .rk.cat.output("<div align=\"right\">", .rk.i18n("Plot window created"), "</div>\n");
devs[[as.character(devnum)]] <<- RK.revision(devnum)
},
in.close=function(devnum, ...) {
- .rk.cat.output("<div align=\"right\">Plot window closed</div>\n");
+ .rk.cat.output("<div align=\"right\">", .rk.i18n("Plot window closed"), "</div>\n");
devs[[as.character(devnum)]] <<- NULL
}
)
@@ -388,7 +388,7 @@ function registerPlot(element) {
currev <- RK.revision(as.numeric(devnum))
if (devs[[devnum]] < currev) {
cur <- dev.cur()
- .rk.cat.output("<div align=\"right\"><details><script>registerPlot(document.currentScript.parentElement);</script><summary>Plot updated (click to show)</summary><p>\n");
+ .rk.cat.output("<div align=\"right\"><details><script>registerPlot(document.currentScript.parentElement);</script><summary>", .rk.i18n("Plot updated (click to show)"), "</summary><p>\n");
#rk.graph.on(width=200, height=200, pointsize=6)
rk.graph.on()
out <- dev.cur()
diff --git a/rkward/rbackend/rpackages/rkward/R/rk.menu.R b/rkward/rbackend/rpackages/rkward/R/rk.menu.R
index 57c709efb..e3fe4cf41 100644
--- a/rkward/rbackend/rpackages/rkward/R/rk.menu.R
+++ b/rkward/rbackend/rpackages/rkward/R/rk.menu.R
@@ -79,7 +79,7 @@ rk.menu <- setRefClass("rk.menu",
x <- .retrieve(TRUE)
x$label <- label
if (!missing(func)) {
- if (exists("children", envir=x)) stop(paste("Submenu", paste(path, collapse="/"), "cannot be redefined as a leaf item."))
+ if (exists("children", envir=x)) stop(.rk.i18n("Submenu %1 cannot be redefined as a leaf item.", paste(path, collapse="/")))
x$fun <- func
}
.rk.call.async("menuupdate", rk.menu()$.list())
@@ -111,12 +111,12 @@ rk.menu <- setRefClass("rk.menu",
for(p in path) {
walkedpath <- c(walkedpath, p)
if (!exists("children", envir=parent)) {
- if (!create) stop(paste("Menu path", paste(walkedpath, collapse="/"), "is not defined"))
- if (exists("fun", envir=parent)) stop(paste("Leaf menu item", paste(walkedpath, collapse="/"), "cannot be redefined as a submenu."))
+ if (!create) stop(.rk.i18n("Menu path %1 is not defined", paste(walkedpath, collapse="/")))
+ if (exists("fun", envir=parent)) stop(.rk.i18n("Leaf menu item %1 cannot be redefined as a submenu.", paste(walkedpath, collapse="/")))
parent$children <- list()
}
if (is.null(parent$children[[p]])) {
- if (!create) stop(paste("Menu path", paste(walkedpath, collapse="/"), "is not defined"))
+ if (!create) stop(.rk.i18n("Menu path %1 is not defined", paste(walkedpath, collapse="/")))
parent$children[[p]] <- new.env()
}
parent = parent$children[[p]]
diff --git a/rkward/rbackend/rpackages/rkward/R/rk.output.R b/rkward/rbackend/rpackages/rkward/R/rk.output.R
index 3ef6c683b..f1e5769bd 100644
--- a/rkward/rbackend/rpackages/rkward/R/rk.output.R
+++ b/rkward/rbackend/rpackages/rkward/R/rk.output.R
@@ -84,7 +84,7 @@ RK.Output <- setRefClass(Class="RK.Output", fields=list(id="character"),
},
export=function(filename, overwrite=NULL) {
"Save this output, to the specified location, but keep it associated with the previous location (\"save a copy\")."
- if (missing(filename)) stop("No file name specified")
+ if (missing(filename)) stop(.rk.i18n("No file name specified"))
.rk.call.nested("output", c ("export", .checkId(), if(is.null(overwrite)) "ask" else if(isTRUE(overwrite)) "force" else "fail", filename))
},
clear=function(discard=NULL) {
@@ -113,7 +113,7 @@ Do not write anything to the target filename, directly! This is purely for infor
.checkId=function() {
"For internal use: Throws an error, if the id parameter is NULL or too long, returns a length one character vector otherwise."
i <- as.character(id)
- if (length(i) != 1) stop ("Invalid output id. Use rk.output() to obtain a valid output handle.")
+ if (length(i) != 1) stop(.rk.i18n("Invalid output id. Use rk.output() to obtain a valid output handle."))
i
}
))
diff --git a/rkward/rbackend/rpackages/rkward/R/rk.print-functions.R b/rkward/rbackend/rpackages/rkward/R/rk.print-functions.R
index b2643137b..419df501b 100644
--- a/rkward/rbackend/rpackages/rkward/R/rk.print-functions.R
+++ b/rkward/rbackend/rpackages/rkward/R/rk.print-functions.R
@@ -104,7 +104,7 @@
filename <- rk.get.tempfile.name (name, ".html")
dir <- rk.get.tempfile.name (name, "_data")
htmlwidgets::saveWidget (x, filename, selfcontained=FALSE, libdir=dir)
- .rk.cat.output (paste0 ("<object width=\"100%\" height=\"100%\" data=\"file://", filename, "\" onload=\"this.style.height = this.contentWindow.document.body.scrollHeight + 'px';\"></object>"))
+ .rk.cat.output("<object width=\"100%\" height=\"100%\" data=\"file://", filename, "\" onload=\"this.style.height = this.contentWindow.document.body.scrollHeight + 'px';\"></object>", sep="")
} else if (inherits (x, "gvis")) {
requireNamespace ("googleVis", quietly = TRUE)
print (x, file=rk.get.output.html.file(), append=TRUE)
@@ -113,7 +113,7 @@
if(requireNamespace ("R2HTML", quietly = TRUE)) {
R2HTML::HTML(x, file=htmlfile, ...)
} else {
- .rk.cat.output("Please install package R2HTML to enable output!")
+ .rk.cat.output(.rk.i18n("Please install package R2HTML to enable output!"))
}
}
}
@@ -150,7 +150,7 @@
if (length (parameters)) {
# legacy handling: parameter=value used to be passed as parameter, value
if (is.null (names (parameters))) {
- warning ("Unnamed parameter lists are deprecated in rk.header()")
+ warning(.rk.i18n("Unnamed parameter lists are deprecated in rk.header()"))
s <- seq.int (1, length (parameters), by=2)
pnames <- as.character (parameters[s])
parameters <- parameters[s+1]
@@ -243,7 +243,7 @@
cat (x)
cat ("</h3>")
} else {
- stop ("uninmplemented")
+ stop(.rk.i18n("Printing object of class '%1' is not inmplemented", class(x)))
}
}
diff --git a/scripts/extract_r_message.py b/scripts/extract_r_message.py
new file mode 100755
index 000000000..3bc399b28
--- /dev/null
+++ b/scripts/extract_r_message.py
@@ -0,0 +1,198 @@
+#! /usr/bin/env python3
+# This file is part of the RKWard project (https://rkward.kde.org).
+# SPDX-FileCopyrightText: 2025 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
+# SPDX-FileContributor: The RKWard Team <rkward at kde.org>
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Extracts messages form R packages. (Of course R has its own update_pkg_po(), but
+# we don't want to asssume an R installation for the extraction job.
+
+import os
+import codecs
+import sys
+import subprocess
+from xml.dom import minidom
+import html.parser
+import copy
+import re
+
+def usage ():
+ print("Usage: " + sys.argv[0] + " files")
+ exit (1)
+
+# initialize globals, and parse args
+sources = []
+outfile = sys.stdout
+for arg in (list (sys.argv[1:])):
+ if (arg.startswith ("--")):
+ usage ()
+ else:
+ sources.append (arg)
+if (len (sources) < 1):
+ usage ()
+
+def normalize (text):
+ lines = text.split ("\n")
+ nlines = []
+ for line in lines:
+ nlines.append (' '.join (line.strip ().split ())) # remove whitespace at start, end, and simplify whitespace within each line
+ return ' '.join (nlines)
+
+def writeouti18n (call):
+ if (call.find ("%") >= 0):
+ outfile.write ("/* xgettext:no-c-format */ ")
+ outfile.write (call + "\n")
+
+# basic R parsing (comments / strings)
+class RParseBuffer:
+ def __init__ (self, content):
+ self.buf = content
+ self.comment = ""
+ self.nline = 0
+ self.nchar = 0
+ def atEof (self):
+ return (self.nchar >= len (self.buf))
+ def currentChar (self):
+ if (self.atEof ()):
+ return ""
+ return (self.buf[self.nchar])
+ def advance (self, n=1):
+ for step in range (n):
+ self.nchar += 1
+ if (self.nchar >= (len (self.buf) - 1)):
+ return False
+ if (self.buf[self.nchar] == "\n"):
+ self.nline += 1
+ return True
+ def skipWhitespace (self):
+ while (self.buf[self.nchar].isspace ()):
+ if (not self.advance ()):
+ break
+ def seekUntil (self, needle):
+ fromchar = self.nchar
+ while (not self.startswith (needle)):
+ if (not self.advance ()):
+ break
+ return self.buf[fromchar:self.nchar]
+ # returns True, if the given word is found at the current position, _and_ it this is a full keyword (i.e. preceded and followed by some delimiter), and could be a function call
+ def isAtFunctionCall (self, word):
+ if (not self.startswith (word)):
+ return False
+ # return False if previous char is no delimiter
+ if (self.nchar < 0 and self.buf[self.nchar-1].isalnum ()):
+ return False
+ # return False if next char is no delimiter
+ nextchar = self.buf[self.nchar + len (word)]
+ if (not (nextchar.isspace () or nextchar == '(')):
+ return False
+ return True
+ def seek_line_comment_end (self):
+ comment = ""
+ while True:
+ comment += self.seekUntil ("\n")
+ self.skipWhitespace ()
+ if (self.startswith ("#")):
+ self.advance (1)
+ else:
+ break
+ return comment
+ def nibbleCallParameters (self):
+ fromchar = self.nchar
+ self.nibble_until ('(')
+ self.advance ()
+ self.nibble_until (')', True)
+ self.advance ()
+ return self.buf[fromchar:self.nchar]
+
+ def nibble_until (self, string, skip_over_parentheses=False):
+ fromchar = self.nchar
+ while (not self.startswith (string)):
+ if (self.atEof ()):
+ break
+ if (self.buf[self.nchar] in ['"', '\'', '`']):
+ ochar = self.buf[self.nchar]
+ while (self.advance ()):
+ if (self.startswith ('\\')):
+ self.advance () # skip next char
+ elif (self.startswith (ochar)):
+ break
+ elif (self.startswith ("#")):
+ self.comment = self.seek_line_comment_end ()
+ elif (skip_over_parentheses and (self.startswith ("("))): # skip over nested parentheses
+ self.advance ()
+ self.nibble_until (")", True)
+ elif (not self.buf[self.nchar].isspace ()):
+ self.comment = ""
+ if (not self.advance ()):
+ break
+ return (self.buf[fromchar:self.nchar])
+ def startswith (self, string):
+ return self.buf.startswith (string, self.nchar)
+
+def handleRChunk (filename):
+ global outfile
+ keywords = ("gettext", "ngettext", "stop", "warning", "message", ".rk.i18n") # TODO change me!
+
+ # Convert single quoted and backtick quoted strings in the input chunk to double quotes (so xgettext can work in C-style).
+ def normalizeQuotes (chunk):
+ pos = 0
+ current_quote_symbol = ""
+ output = ""
+ strip_closing_parentheses = 0
+ while (pos < len (chunk)):
+ c = chunk[pos]
+ if c == "\\":
+ nc = chunk[pos+1]
+ if ((nc != current_quote_symbol) or (nc == '"')):
+ output += c
+ output += nc
+ pos += 1
+ elif c in ['"', '\'', '`']:
+ if (current_quote_symbol == ""):
+ current_quote_symbol = c
+ output += '"'
+ elif current_quote_symbol == c:
+ current_quote_symbol = ""
+ output += '"'
+ elif c == '"':
+ output += '\\\"'
+ else:
+ output += c
+ else:
+ output += c
+ pos += 1
+ return output
+
+ buf = ""
+ with codecs.open(filename, 'r', 'utf-8') as f:
+ buf = f.read()
+ rbuf = RParseBuffer (buf)
+ while (True):
+ call = ""
+ junk = rbuf.nibble_until (keywords)
+ if (rbuf.atEof ()):
+ break
+ for candidate in keywords:
+ if (rbuf.isAtFunctionCall (candidate)):
+ call = candidate
+ break
+ if (call == ""):
+ # skip over somethingelsei18nsomethingelse identifiers, i.e. those not matched, above
+ rbuf.advance ()
+ continue
+
+ rbuf.advance (len (call))
+ comment = normalize (rbuf.comment)
+ line = rbuf.nline
+ parameters = rbuf.nibbleCallParameters ()
+ text = "/* "
+ if (comment.lower ().startswith ("i18n:") or comment.lower ().startswith ("translators:")):
+ text += "i18n: " + comment + "\n"
+ text += "i18n: file: " + filename + ":" + str(line)
+ text += " */\n" + call + normalizeQuotes (parameters) + ";\n"
+ writeouti18n (text)
+
+#######
+# Loop over toplevel_sources (specified on command line.
+for source in sources:
+ handleRChunk(source)
More information about the rkward-tracker
mailing list