[rkward] packages/rkwarddev: added rk.updatePluginMessages()

m.eik michalke meik.michalke at uni-duesseldorf.de
Sun Nov 15 20:30:07 UTC 2015


Git commit 863e7153bac719f015af16a08f5cae64db8a6f79 by m.eik michalke.
Committed on 15/11/2015 at 20:29.
Pushed by meikm into branch 'master'.

added rk.updatePluginMessages()

  - this is an R wrapper function for update_plugin_messages.py to be able to extrat/merge/install translatable strings from a plugin from within an rkwarddev script
  - to make finding the python script easy, the package now comes with its own copy
  - should this be made callable by rk.plugin.skeleton()?

M  +5    -3    packages/rkwarddev/ChangeLog
M  +4    -3    packages/rkwarddev/DESCRIPTION
M  +1    -0    packages/rkwarddev/NAMESPACE
A  +82   -0    packages/rkwarddev/R/rk.updatePluginMessages.R
M  +1    -1    packages/rkwarddev/R/rkwarddev-package.R
M  +5    -3    packages/rkwarddev/inst/NEWS.Rd
A  +562  -0    packages/rkwarddev/inst/scripts/update_plugin_messages.py
A  +40   -0    packages/rkwarddev/man/rk.updatePluginMessages.Rd
M  +1    -1    packages/rkwarddev/man/rkwarddev-package.Rd

http://commits.kde.org/rkward/863e7153bac719f015af16a08f5cae64db8a6f79

diff --git a/packages/rkwarddev/ChangeLog b/packages/rkwarddev/ChangeLog
index 54461bd..dcad0f4 100644
--- a/packages/rkwarddev/ChangeLog
+++ b/packages/rkwarddev/ChangeLog
@@ -1,6 +1,6 @@
 ChangeLog for package rkwarddev
 
-changes in version 0.07-4 (2015-11-14)
+changes in version 0.07-4 (2015-11-15)
 unreleased:
   - this version is under development
 fixed:
@@ -37,6 +37,8 @@ added:
     the handling of empty "else" clauses in JavaScript
   - in rk.JS.header(), the second value provided by "add" can now be named
     "noquote" to have it nested in the JS noquote() function
+  - new wrapper function rk.updatePluginMessages() for
+    update_plugin_messages.py, the package now comes with its own copy of this core i18n script
 changed:
   - improved error handling in rk.JS.header(), error messages are more
     informative now
@@ -46,8 +48,8 @@ changed:
     useful feedback
   - all functions offering "intent.by" as an option now fetch the default
     value by calling rk.get.indent()
-  - all functions offering "empty.e" as an option now fetch the default
-    value by calling rk.get.empty.e()
+  - all functions offering "empty.e" as an option now fetch the default value
+    by calling rk.get.empty.e()
   - removed trailing newline in rk.JS.header() output
 
 changes in version 0.07-3 (2015-06-29)
diff --git a/packages/rkwarddev/DESCRIPTION b/packages/rkwarddev/DESCRIPTION
index 053bcea..f764cb6 100644
--- a/packages/rkwarddev/DESCRIPTION
+++ b/packages/rkwarddev/DESCRIPTION
@@ -1,7 +1,7 @@
 Package: rkwarddev
 Type: Package
 Title: A Collection of Tools for RKWard Plugin Development
-Author: m.eik michalke [aut, cre]
+Author: m.eik michalke [aut, cre, cph]
 Maintainer: m.eik michalke <meik.michalke at hhu.de>
 Depends:
     R (>= 2.9.0),methods,XiMpLe (>= 0.03-21),rkward (>= 0.5.7)
@@ -14,9 +14,9 @@ License: GPL (>= 3)
 Encoding: UTF-8
 LazyLoad: yes
 URL: https://rkward.kde.org
-Authors at R: c(person(given="m.eik", family="michalke", email="meik.michalke at hhu.de", role=c("aut", "cre")))
+Authors at R: c(person(given="m.eik", family="michalke", email="meik.michalke at hhu.de", role=c("aut", "cre", "cph")))
 Version: 0.07-4
-Date: 2015-11-14
+Date: 2015-11-15
 RoxygenNote: 5.0.0
 Collate:
     '00_class_01_rk.JS.arr.R'
@@ -137,6 +137,7 @@ Collate:
     'rk.set.rkh.prompter.R'
     'rk.testsuite.doc.R'
     'rk.uniqueIDs.R'
+    'rk.updatePluginMessages.R'
     'rkwarddev-desc-internal.R'
     'rkwarddev-package.R'
     'rkwarddev.required.R'
diff --git a/packages/rkwarddev/NAMESPACE b/packages/rkwarddev/NAMESPACE
index df509ce..da30ba1 100644
--- a/packages/rkwarddev/NAMESPACE
+++ b/packages/rkwarddev/NAMESPACE
@@ -108,6 +108,7 @@ export(rk.set.empty.e)
 export(rk.set.indent)
 export(rk.set.rkh.prompter)
 export(rk.testsuite.doc)
+export(rk.updatePluginMessages)
 export(rkwarddev.required)
 export(tf)
 exportClasses(rk.JS.arr)
diff --git a/packages/rkwarddev/R/rk.updatePluginMessages.R b/packages/rkwarddev/R/rk.updatePluginMessages.R
new file mode 100644
index 0000000..b152707
--- /dev/null
+++ b/packages/rkwarddev/R/rk.updatePluginMessages.R
@@ -0,0 +1,82 @@
+# Copyright 2010-2015 Meik Michalke <meik.michalke at hhu.de>
+#
+# This file is part of the R package rkwarddev.
+#
+# rkwarddev is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# rkwarddev is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with rkwarddev.  If not, see <http://www.gnu.org/licenses/>.
+
+#' Update plugin i18n messages
+#'
+#' A wrapper for calling \code{update_plugin_messages.py} to extract translatable
+#' strings from a plugin or update/merge translations.
+#' 
+#' @note For details on the translating process, please refer to the chapter
+#' \href{help:rkwardplugins/i18n.html}{Plugin translations}
+#' of the  \emph{Introduction to Writing Plugins for RKWard}, especially
+#' subsection \href{help:rkwardplugins/i18n_workflow.html}{Translation maintainance}.
+#' 
+#' @param pluginmap Character string, full path to the main pluginmap file of the plugin to translate.
+#' @param extractOnly Logical, should translatable strings only be extracted? If \code{FALSE}, translatable
+#'   strings will be updated and installed.
+#' @param default_po Optional character string, fallback default name for \code{*.pot} file.
+#' @param outdir Optional character string, change the output directory for generated files.
+#' @seealso \href{help:rkwardplugins}{Introduction to Writing Plugins for RKWard}
+#' @export
+#' @examples \dontrun{
+#' rk.updatePluginMessages("~/myPlugins/lifeSaver/rkward/lifeSaver.pluginmap")
+#' }
+
+rk.updatePluginMessages <- function(pluginmap, extractOnly=FALSE, default_po=NULL, outdir=NULL){
+  # --default_po=PO_ID -> rkward_${PO_ID}.pot
+  # --outdir=DIR       -> pluginmap basedir
+  rkdPatch <- installed.packages()["rkwarddev", "LibPath"]
+  upmScript <- file.path(rkdPatch, "rkwarddev", "scripts", "update_plugin_messages.py")
+  python <- Sys.which("python")
+  # check system setup
+  if(!file.exists(upmScript)){
+    stop(simpleError("Can't find 'update_plugin_messages.py' script!"))
+  } else {}
+  if(identical(python[[1]], "")){
+    stop(simpleError("Can't find 'python' executable in search path!"))
+  } else {}
+  if(identical(Sys.which("xgettext")[[1]], "")){
+    stop(simpleError("Can't find 'xgettext' executable in search path!"))
+  } else {}
+  stopifnot(length(pluginmap) == 1 | !is.character(pluginmap))
+  if(!file.exists(pluginmap)){
+    stop(simpleError(paste0("Can't find pluginmap file:\n  ", pluginmap)))
+  } else {}
+
+  upmOptions <- ""
+  if(isTRUE(extractOnly)){
+    upmOptions <- " --extract-only"
+  } else {}
+  if(!is.null(default_po)){
+    stopifnot(length(default_po) == 1 | !is.character(default_po))
+    upmOptions <- paste0(upmOptions, " --default_po=\"", default_po, "\"")
+  } else {}
+  if(!is.null(outdir)){
+    stopifnot(length(outdir) == 1 | !is.character(outdir))
+    upmOptions <- paste0(upmOptions, " --outdir=\"", outdir, "\"")
+  } else {}
+  
+  upmCall <- paste0(python[[1]], " ", upmScript, upmOptions, " \"", pluginmap, "\"")
+  message(upmCall)
+  if(identical(base::.Platform[["OS.type"]], "unix")){
+    system(upmCall, intern=TRUE)
+  } else {
+    shell(upmCall, translate=TRUE, intern=TRUE)
+  }
+
+  return(invisible(NULL))
+}
diff --git a/packages/rkwarddev/R/rkwarddev-package.R b/packages/rkwarddev/R/rkwarddev-package.R
index 2c80923..c3b6e07 100644
--- a/packages/rkwarddev/R/rkwarddev-package.R
+++ b/packages/rkwarddev/R/rkwarddev-package.R
@@ -4,7 +4,7 @@
 #' Package: \tab rkwarddev\cr
 #' Type: \tab Package\cr
 #' Version: \tab 0.07-4\cr
-#' Date: \tab 2015-11-14\cr
+#' Date: \tab 2015-11-15\cr
 #' Depends: \tab R (>= 2.9.0),methods,XiMpLe (>= 0.03-21),rkward (>= 0.5.7)\cr
 #' Enhances: \tab rkward\cr
 #' Encoding: \tab UTF-8\cr
diff --git a/packages/rkwarddev/inst/NEWS.Rd b/packages/rkwarddev/inst/NEWS.Rd
index 6e34a67..1d1a44e 100644
--- a/packages/rkwarddev/inst/NEWS.Rd
+++ b/packages/rkwarddev/inst/NEWS.Rd
@@ -1,7 +1,7 @@
 \name{NEWS}
 \title{News for Package 'rkwarddev'}
 \encoding{UTF-8}
-\section{Changes in rkwarddev version 0.07-4 (2015-11-14)}{
+\section{Changes in rkwarddev version 0.07-4 (2015-11-15)}{
   \subsection{unreleased}{
     \itemize{
       \item this version is under development
@@ -45,6 +45,8 @@
         the handling of empty \code{"else"} clauses in JavaScript
       \item in \code{rk.JS.header()}, the second value provided by \code{"add"} can now be named
         \code{"noquote"} to have it nested in the JS \code{noquote()} function
+      \item new wrapper function \code{rk.updatePluginMessages()} for
+        update_plugin_messages.py, the package now comes with its own copy of this core i18n script
     }
   }
   \subsection{changed}{
@@ -57,8 +59,8 @@
         useful feedback
       \item all functions offering \code{"intent.by"} as an option now fetch the default
         value by calling \code{rk.get.indent()}
-      \item all functions offering \code{"empty.e"} as an option now fetch the default
-        value by calling \code{rk.get.empty.e()}
+      \item all functions offering \code{"empty.e"} as an option now fetch the default value
+        by calling \code{rk.get.empty.e()}
       \item removed trailing newline in \code{rk.JS.header()} output
     }
   }
diff --git a/packages/rkwarddev/inst/scripts/update_plugin_messages.py b/packages/rkwarddev/inst/scripts/update_plugin_messages.py
new file mode 100755
index 0000000..4e469a5
--- /dev/null
+++ b/packages/rkwarddev/inst/scripts/update_plugin_messages.py
@@ -0,0 +1,562 @@
+#! /usr/bin/python
+# ***************************************************************************
+#                          update_plugin_messages  -  description
+#                             -------------------
+#    begin                : Oct 2014
+#    copyright            : (C) 2014 by Thomas Friedrichsmeier
+#    email                : tfry at users.sourceforge.net
+# ***************************************************************************
+#
+# ***************************************************************************
+# *                                                                         *
+# *   This program is free software; you can redistribute it and/or modify  *
+# *   it under the terms of the GNU General Public License as published by  *
+# *   the Free Software Foundation; either version 2 of the License, or     *
+# *   (at your option) any later version.                                   *
+# *                                                                         *
+# ***************************************************************************
+#
+# Extracts messages form RKWard plugin files (.pluginmap, .xml, .rkh, .js).
+# Unless --extract-only is specified on the command line, also merges existing
+# translations with the message template, compiles them, and installs them.
+#
+# Included files are walked, automatically. Thus the typical usage is to specify
+# the topmost .pluginmap file as the only file argument.
+
+import os
+import codecs
+import sys
+import subprocess
+from xml.dom import minidom
+import HTMLParser
+import copy
+import re
+
+# You might want to adjust the following values (can also be overridden from environment variable):
+BUGADDR = "http://p.sf.net/rkward/bugs"     # Technically, this is for bugs _in the translation_
+BUGADDR = os.getenv ('BUGADDR', BUGADDR)
+XGETTEXT_CALL =  "xgettext --from-code=UTF-8 -C -kde -ci18n -ki18n:1 -ki18nc:1c,2 -ki18np:1,2 -ki18ncp:1c,2,3"
+XGETTEXT_CALL += " -ktr2i18n:1 -kI18N_NOOP:1 -kI18N_NOOP2:1c,2 -kaliasLocale -kki18n:1 -kki18nc:1c,2 -kki18np:1,2"
+XGETTEXT_CALL += " -kki18ncp:1c,2,3 --msgid-bugs-address=" + BUGADDR
+XGETTEXT_CALL = os.getenv ('XGETTEXT', XGETTEXT_CALL)
+MSGMERGE = os.getenv ('MSGMERGE', "msgmerge")
+MSGFMT = os.getenv ('MSGFMT', "msgfmt --check")
+# end
+
+# list of tag-names the content of which to extract in full (including, possibly, HTML-tags, within)
+text_containers = ['section', 'text', 'related', 'title', 'summary', 'usage', 'technical', 'setting']
+# (HTML)-elements on which to split translation units within a text_container
+text_splitting_elements = ['p', 'ul', 'ol', 'li']
+# Elements that refer to a different (labelled) element by id
+referring_elements = ['setting', 'caption']
+# Map of elements to attributes to extract, and default context info
+attributes_to_extract_for_tag={
+  '*':      { "attributes" : ['label', 'title', 'shorttitle'], "context": ''},
+  'about':  { "attributes" : ['name', 'shortinfo', 'category'], "context": ''},
+  'author': { "attributes" : ['name', 'given', 'family'], "context": 'Author name'}
+}
+# HACK for preserving line number information in the DOM tree. This string should be unique enough to not clash with the files' contents!
+LINE_DUMMY_ATTR = '_DUMMY_LINE'
+
+def usage ():
+  print ("Usage: " + sys.argv[0] + " [--default_po=PO_ID] [--outdir=DIR] files")
+  exit (1)
+
+# initialize globals, and parse args
+infile = {"infile": "", "file_prefix": "", "caption": "", "id_labels" : {}}
+default_po = ""
+outfile = ""
+outdir = ""
+initialized_pot_files = []
+po_file_install_locations = {}
+current_po_id = ""
+do_merge_install = True
+toplevel_sources = []
+for arg in (list (sys.argv[1:])):
+  if (arg.startswith ("--default_po=")):
+    default_po = arg.split ("=", 1)[1]
+  elif (arg.startswith ("--outdir=")):
+    outdir = arg.split ("=", 1)[1]
+  elif (arg == ("--extract-only")):
+    do_merge_install = False
+  elif (arg.startswith ("--")):
+    usage ()
+  else:
+    toplevel_sources.append (arg)
+if (len (toplevel_sources) < 1):
+  usage ()
+
+# For crying out loud! So we are not strictly using XML, because we allow the use of (X)HTML entities, esp. inside <text>-elements,
+# without formally declaring these entities. Python seems to make a point of making it real hard to deal with this. So what we do is
+# escaping all entities before parsing, then passing all through HTMLParser.unescape () before writing the output.
+#
+# The second thing we do is hacking line number information into the parsed XML tree
+def parseFile (filename):
+  f = codecs.open (filename, 'r', 'utf-8')
+  l = 0
+  enriched = list ()
+  for line in f:
+    l += 1
+    enriched.append (re.sub (r'<(\w+)', r'<\1 ' + LINE_DUMMY_ATTR + '="' + str (l) + '"', line))
+  content = "".join (enriched).replace ("&", "&")
+  f.close ()
+
+  try:
+    return minidom.parseString (content.encode ('utf-8'))
+  except:
+    sys.stderr.write ("ERROR: Failed to parse file " + filename + "\n")
+    raise
+
+# Where available, include the labels of parent elements. Particularly helpful for radio-options
+def getElementShort (element, dot_attribute=""):
+  ret = "<" + element.tagName
+  if (dot_attribute != ""):
+    ret += " " + dot_attribute + "=\"...\""
+  else:
+    for attr in ["label", "title"]:
+      if (element.hasAttribute (attr)):
+        ret += " " + attr + "=" + quote (element.getAttribute (attr))
+  return ret + ">"
+
+# Try to extract helpful file context information
+def getFileContext (element, attribute=""):
+  ret = "i18n: file: " + infile["infile"] + ":" + str (getLineOf (element, 0)) + "\n"
+  ret += "i18n: ectx: "
+  if (infile["caption"] != ""):
+    ret += "(" + infile["caption"] + ") "
+  refer_to = ""
+  if ((element.tagName in referring_elements) and (element.hasAttribute ("id"))):
+    if (not (element.getAttribute ("id") in infile["id_labels"])):
+      sys.stderr.write ("WARNING in " + infile["infile"] + ", line " + str (getLineOf (element)) + ": Reference to unknown (or unnamed) element id '" + element.getAttribute ("id") + "'\n")
+    else:
+      refer_to = " (refers to element labelled " + quote (infile["id_labels"][element.getAttribute ("id")]) + ")"
+  tag_stack = [getElementShort (element, attribute)]
+  while ((element.parentNode.nodeType != element.DOCUMENT_NODE)):
+    element = element.parentNode
+    if (element.tagName in ["document", "row", "column", "frame", "content", "tabbook"]):
+      if (not (element.hasAttribute ("label") or element.hasAttribute ("title"))):
+        continue # Skip over tags that don't really add any meaningful context information. (Note: <frame>s _with_ a label are meaningful, of course)
+    tag_stack.insert (0, getElementShort (element))
+  if (len (tag_stack) > 4):
+    tag_stack = [tag_stack[0], "[...]"] + tag_stack[-2:]
+  return (ret + ' '.join (tag_stack) + refer_to)
+
+def writeouti18n (call):
+  if (call.find ("%") >= 0):
+    outfile.write ("/* xgettext:no-c-format */ ")
+  outfile.write (call + "\n")
+
+def quote (text):
+  try:
+     text = text.decode ('utf-8', 'ignore')
+  except:
+     print ("Python has trouble decoding this text: " + text.encode('utf-8'))
+  text = HTMLParser.HTMLParser ().unescape (text)	# unescape character entities, Qt does so while parsing the xml
+  return "\"" + text.replace ("\\", "\\\\").replace ("\"", "\\\"") + "\""
+
+def stripLineDummy (text):
+  return re.sub (r' ' + LINE_DUMMY_ATTR + '="\d+"', '', text)
+
+def getLineOf (element, default=-1):
+  if element.hasAttribute (LINE_DUMMY_ATTR):
+    return int (element.getAttribute (LINE_DUMMY_ATTR))
+  return default
+
+# Normalizes larger text fragments. TODO: Do we want to protect <pre>-blocks?
+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)
+  
+# get everything inside the element as text. Might include further xml tags.
+def getFullText (element):
+  childnodes = element.childNodes
+  ## Skip over anything containing only a <link href="rkward://"/> and nothing else; a somewhat important special case ("Related"-section)
+  ## NOTE: The second attribute, here, is the LINE_DUMMY_ATTR , hence checking for cn.attributes.length == 2
+  if (childnodes.length == 1):
+    cn = childnodes.item (0)
+    if (not cn.hasChildNodes()) and (cn.nodeType == cn.ELEMENT_NODE) and (cn.tagName == "link") and (cn.attributes.length == 2) and (cn.getAttribute ("href").startswith ("rkward://")):
+      return ""
+
+  rc = []
+  for cn in childnodes:
+    if (cn.nodeType == cn.ELEMENT_NODE) and (cn.tagName in text_splitting_elements):
+      rc.append ("\n\n" + getFullText (cn) + "\n\n")
+    elif cn.nodeType != cn.COMMENT_NODE:
+      rc.append(stripLineDummy (cn.toxml ("utf-8")))
+  return ''.join (rc).strip ().replace ("&", "&")
+
+# get the content of all text nodes inside this node (does not include xml tags)
+def getText (node):
+  rc = []
+  for cn in node.childNodes:
+    if cn.nodeType in [cn.TEXT_NODE, cn.CDATA_SECTION_NODE]:
+      rc.append(stripLineDummy (cn.data))
+  return ''.join (rc).strip ()
+
+# Look for an i18n comment in the given node, and add automatically extracted file context information
+def getI18nComment (node, attribute=""):
+  ret = "/* "
+  for cn in node.childNodes:
+    if cn.nodeType == cn.COMMENT_NODE:
+      comment = normalize (cn.data.strip ())
+      if (comment.lower ().startswith ("i18n:") or comment.lower ().startswith ("translators:")):
+        ret += "i18n: " + stripLineDummy (comment) + "\n"
+  ret += getFileContext (node, attribute) + " */\n"
+  return (ret)
+
+# Main workhorse: Look at given node and recurse into children
+def handleNode (node):
+  if (node.nodeType == node.ELEMENT_NODE):
+    attributes = attributes_to_extract_for_tag['*']['attributes']
+    context = attributes_to_extract_for_tag['*']['context']
+    if (node.tagName in attributes_to_extract_for_tag):
+      attributes = attributes + attributes_to_extract_for_tag[node.tagName]['attributes']
+      context = attributes_to_extract_for_tag[node.tagName]['context']
+    for attr in attributes:
+      if (node.hasAttribute (attr)):
+        attrv = node.getAttribute (attr)
+        if (attrv == ""):
+          continue
+        outfile.write (getI18nComment (node, attr))
+        if (node.hasAttribute ("i18n_context")):
+          context = node.getAttribute ("i18n_context")
+        if (context != ''):
+          writeouti18n ("i18nc (" + quote (context) + ", " + quote (attrv) + ");")
+        else:
+          writeouti18n ("i18n (" + quote (attrv) + ");")
+    if (node.hasAttribute ("file")):
+      filename = node.getAttribute ("file")
+      if (filename.endswith (".js")):
+        filename = os.path.join (os.path.dirname (infile["infile"]), filename)
+        jsfile = codecs.open (filename, 'r', 'utf-8')
+        handleJSChunk (jsfile.read (), filename, 0, getFileCaption (None, infile["caption"]))
+        jsfile.close ()
+      else:
+        handleSubFile (filename, node.tagName == "component", node.tagName == "include")
+    if (node.tagName == "script"):
+      handleJSChunk (getText (node), infile["infile"], getLineOf (node), infile["caption"])
+    elif (node.tagName in text_containers):
+      textchunks = getFullText (node).split ("\n\n")
+      for chunk in textchunks:
+        chunk = chunk.strip ()
+        if (chunk != ""):
+          outfile.write (getI18nComment (node))
+          writeouti18n ("i18n (" + quote (normalize (chunk)) + ");")
+    elif (getText (node) != ""):
+      sys.stderr.write ("WARNING: Found text content where none expected: " + getFileContext (node) + "\n")
+      sys.stderr.write (quote (getText (node)))
+  if (not ((node.nodeType == node.ELEMENT_NODE) and (node.tagName in text_containers))):
+    # Don't go looking into the contents of text containers any further (may contain HTML markup)
+    for child in node.childNodes:
+      handleNode (child)
+
+# Try to determine a caption for the file (will be used as context comment). If none is found in this file use "Loaded from loaded_from"
+def getFileCaption (docelem, loaded_from):
+  if (not docelem is None):
+    elems = docelem.getElementsByTagName ("title")
+    if (elems.length):
+      return normalize (getFullText (elems.item (0)))
+    elems = docelem.getElementsByTagName ("dialog")
+    if (elems.length):
+      return elems.item (0).getAttribute ("label")
+    elems = docelem.getElementsByTagName ("wizard")
+    if (elems.length):
+      return elems.item (0).getAttribute ("label")
+  if (loaded_from != ""):
+    return "Loaded from " + loaded_from
+  return ""
+
+# Gather labels of elements with given id (so <setting id="xyz">text</setting> elements can be labelled)
+def getElementLabelsRecursive (elem):
+  ret = {}
+  for ce in elem.childNodes:
+    if (ce.nodeType == ce.ELEMENT_NODE):
+      if (ce.hasAttribute ("id") and ce.hasAttribute ("label")):
+        ret[ce.getAttribute ("id")] = ce.getAttribute ("label")
+      ret.update (getElementLabelsRecursive (ce))
+  return ret
+
+def getAllElementLabels (xmldoc, filename):
+  ret = getElementLabelsRecursive (xmldoc)
+  includes = xmldoc.getElementsByTagName ("include")
+  for inc in includes:
+    subfile = os.path.join (os.path.dirname (filename), inc.getAttribute ("file"))
+    subdoc = parseFile (subfile)
+    ret.update (getAllElementLabels (subdoc, subfile))
+  return ret
+
+# It really is sort of lame to have to parse JS and extract i18n-calls, when xgettext could do it. But that would not
+# - allow us to add info on which plugin this belongs to
+# - list the i18n strings from the JS file in sequence with the i18n strings from the XML parts of the same plugin
+# - give decent file context for inlines JS script code
+class JSParseBuffer:
+  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 (2)
+      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]
+  # TODO: handle includes, somehow
+  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.seekUntil ("*/")
+      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 handleJSChunk (buf, filename, offset, caption):
+  global outfile
+
+  # 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
+
+  jsbuf = JSParseBuffer (buf)
+  while (True):
+    call = ""
+    junk = jsbuf.nibble_until (("i18n", "comment"))
+    if (jsbuf.atEof ()):
+      break
+    for candidate in ["i18n", "i18nc", "i18np", "i18ncp", "comment"]:
+      if (jsbuf.isAtFunctionCall (candidate)):
+        call = candidate
+        break
+    if (call == ""):
+      # skip over somethingelsei18nsomethingelse identifiers, i.e. those not matched, above
+      jsbuf.advance ()
+      continue
+
+    jsbuf.advance (len (call))
+    comment = normalize (jsbuf.comment)
+    line = jsbuf.nline
+    parameters = jsbuf.nibbleCallParameters ()
+    # Ok, here's another crude hack... Strip "noquote()" from anything inside the i18n-call, as xgettext will ignore strings inside other calls
+    subbuf = JSParseBuffer (parameters)
+    parameters = ""
+    while (not subbuf.atEof ()):
+      parameters += subbuf.nibble_until ("noquote")
+      if (subbuf.isAtFunctionCall ("noquote")):
+        subbuf.advance (len ("noquote"))
+        parameters += subbuf.nibbleCallParameters ().strip ()[1:][:-1]	# strip parentheses, too
+    # Hack end.
+    text = "/* "
+    if (comment.lower ().startswith ("i18n:") or comment.lower ().startswith ("translators:")):
+      text += "i18n: " + comment + "\n"
+    text += "i18n: file: " + filename
+    if (offset >= 0):
+      text += ":" + str (offset + line + 1)
+    text += "\ni18n: ectx: (" + caption + ") */\n"
+    if (call == "comment"):
+      call = "i18nc" # for xgettext
+      parameters = parameters.replace ('(', '("R code comment", ', 1)
+    text += call + normalizeQuotes (parameters) + ";\n"
+    writeouti18n (text)
+
+# When we encounter a "file"-attribute, we generally dive right into parsing that file, i.e. we do depth first
+# Advantage is that strings in all files belonging to one plugin will be in direct succession in the .pot file
+# The exception is if the referenced file declares an own (different) po_id. In this case it will be handled, later.
+def handleSubFile (filename, fetch_ids = False, is_include=False):
+  global toplevel_sources
+  global infile
+  cdir = os.path.dirname (infile["infile"])
+  if (is_include):
+    filename = os.path.join (cdir, filename)
+  else:
+    filename = os.path.join (cdir, infile["file_prefix"], filename)
+  if (not os.path.isfile (filename)):
+    sys.stderr.write (" WARNING: File " + filename + " (referenced from " + infile["infile"] + ") does not exist\n")
+    return
+  xmldoc = parseFile (filename)
+  if (xmldoc.documentElement.hasAttribute ("po_id") and (xmldoc.documentElement.getAttribute ("po_id") != current_po_id)):
+    toplevel_sources.append (filename)
+    #sys.stderr.write ("Added " + filename + " to toplevel\n")
+  else:
+    #sys.stderr.write ("Recursing into " + filename + "\n")
+    oldinfile = copy.deepcopy (infile)
+    infile["infile"] = filename
+    infile["file_prefix"] = xmldoc.documentElement.getAttribute ("base_prefix")
+    infile["caption"] = getFileCaption (xmldoc.documentElement, oldinfile["caption"])
+    if (fetch_ids):
+      infile["id_labels"] = getAllElementLabels (xmldoc.documentElement, filename)
+    handleNode (xmldoc.documentElement)
+    infile = oldinfile
+
+def initialize_pot_file (po_id, po_loc):
+  global outfile
+  global current_po_id
+  current_po_id = po_id
+  if (outfile != ""):
+    outfile.close ()
+  if (current_po_id in initialized_pot_files):
+    if (po_file_install_locations[current_po_id] != po_loc):
+      sys.stderr.write ("WARNING: Conflicting path specs for po id " + po_id)
+    mode = 'a'
+  else:
+    initialized_pot_files.append (current_po_id)
+    po_file_install_locations[current_po_id] = po_loc
+    mode = 'w'
+  p_outdir = outdir
+  if (p_outdir == ""):
+    p_outdir = po_file_install_locations[po_id]
+  if (not os.path.exists (p_outdir)):
+    os.makedirs (p_outdir, 0755)
+  outfile = codecs.open (os.path.join (p_outdir, po_id + '.pot.cpp'), mode, 'utf-8')
+  if (mode == 'w'):	# just created
+    outfile.write ('i18nc("NAME OF TRANSLATORS","Your names");\n');
+    outfile.write ('i18nc("EMAIL OF TRANSLATORS","Your emails");\n');
+
+#######
+# Loop over toplevel_sources (specified on command line, or those that want to be split into separate po) and extract messages
+# NOTE: toplevel_sources may grow, dynamically, but only at the end.
+i = 0
+while i < len (toplevel_sources):
+  xmldoc = parseFile (toplevel_sources[i])
+  po_loc = os.path.join (os.path.dirname (toplevel_sources[i]), "po")
+  po_id = xmldoc.documentElement.getAttribute ("po_id")
+  if (po_id == ""):
+    po_id = default_po
+  elif (xmldoc.documentElement.hasAttribute ("po_path")):
+    po_loc = os.path.join (os.path.dirname (toplevel_sources[i]), xmldoc.documentElement.getAttribute ("po_path"))
+  if (po_id == ""):
+    sys.stderr.write ("WARNING: No po_id attribute on file " + toplevel_sources[i] + " and no default specified. Skipping.\n")
+    continue
+  initialize_pot_file (po_id, po_loc)
+  handleSubFile (toplevel_sources[i])  # Some duplication of parsing, instead of duplication of code
+  i += 1
+
+#######
+# Run xgettext on all generated .pot.cpp files, and - unless --extract-only - merge, compile, install
+for po_id in initialized_pot_files:
+  p_outdir = outdir
+  if (p_outdir == ""):
+    p_outdir = po_file_install_locations[po_id]
+  potcppfile = os.path.join (p_outdir, po_id + ".pot.cpp")
+  templatename = "rkward__" + po_id
+  finalpotfile = os.path.join (p_outdir, templatename + ".pot")
+  # NOTE: using --no-location, as that just adds meaningless references to the temporary .pot.cpp-file.
+  res = subprocess.call (XGETTEXT_CALL.split () + ["--no-location", "-o", finalpotfile, potcppfile])
+  if (res):
+    sys.stderr.write ("calling xgettext failed with exit code " + str (res))
+  os.remove (potcppfile)
+  if (not do_merge_install):
+    continue
+  # merge existing translations
+  transfiles = os.listdir (os.path.join (p_outdir))
+  for trans in transfiles:
+    abstrans = os.path.join (p_outdir, trans)
+    # is it really a translation file?
+    if (trans.startswith (templatename + ".") and trans.endswith (".po") and ((len (templatename) + 6) >= len (trans) <= (len (templatename) + 7))):
+      lang = trans.split ('.')[-2]
+      res = subprocess.call (MSGMERGE.split () + ["-o", abstrans + ".new", abstrans, finalpotfile])
+      if (res):
+        sys.stderr.write ("Failed to merge messages for " + abstrans)
+      else:
+        os.remove (abstrans)
+        os.rename (abstrans + ".new", abstrans)
+      m_outdir = os.path.join (po_file_install_locations[po_id], lang, "LC_MESSAGES")
+      if (not os.path.exists (m_outdir)):
+        os.makedirs (m_outdir, 0755)
+      res = subprocess.call (MSGFMT.split () + [abstrans, "-o", os.path.join (m_outdir, templatename + ".mo")])
+      if (res):
+        sys.stderr.write ("calling msgfmt on " + abstrans + " failed with exit code " + str (res))
diff --git a/packages/rkwarddev/man/rk.updatePluginMessages.Rd b/packages/rkwarddev/man/rk.updatePluginMessages.Rd
new file mode 100644
index 0000000..3067a7c
--- /dev/null
+++ b/packages/rkwarddev/man/rk.updatePluginMessages.Rd
@@ -0,0 +1,40 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/rk.updatePluginMessages.R
+\name{rk.updatePluginMessages}
+\alias{rk.updatePluginMessages}
+\title{Update plugin i18n messages}
+\usage{
+rk.updatePluginMessages(pluginmap, extractOnly = FALSE, default_po = NULL,
+  outdir = NULL)
+}
+\arguments{
+\item{pluginmap}{Character string,
+      full path to the main pluginmap file of the plugin to translate.}
+
+\item{extractOnly}{Logical,
+      should translatable strings only be extracted? If \code{FALSE}, translatable
+strings will be updated and installed.}
+
+\item{default_po}{Optional character string, fallback default name for \code{*.pot} file.}
+
+\item{outdir}{Optional character string, change the output directory for generated files.}
+}
+\description{
+A wrapper for calling \code{update_plugin_messages.py} to extract translatable
+strings from a plugin or update/merge translations.
+}
+\note{
+For details on the translating process, please refer to the chapter
+\href{help:rkwardplugins/i18n.html}{Plugin translations}
+of the  \emph{Introduction to Writing Plugins for RKWard}, especially
+subsection \href{help:rkwardplugins/i18n_workflow.html}{Translation maintainance}.
+}
+\examples{
+\dontrun{
+rk.updatePluginMessages("~/myPlugins/lifeSaver/rkward/lifeSaver.pluginmap")
+}
+}
+\seealso{
+\href{help:rkwardplugins}{Introduction to Writing Plugins for RKWard}
+}
+
diff --git a/packages/rkwarddev/man/rkwarddev-package.Rd b/packages/rkwarddev/man/rkwarddev-package.Rd
index fec5bae..635c81d 100644
--- a/packages/rkwarddev/man/rkwarddev-package.Rd
+++ b/packages/rkwarddev/man/rkwarddev-package.Rd
@@ -12,7 +12,7 @@ A Collection of Tools for RKWard Plugin Development.
 Package: \tab rkwarddev\cr
 Type: \tab Package\cr
 Version: \tab 0.07-4\cr
-Date: \tab 2015-11-14\cr
+Date: \tab 2015-11-15\cr
 Depends: \tab R (>= 2.9.0),methods,XiMpLe (>= 0.03-21),rkward (>= 0.5.7)\cr
 Enhances: \tab rkward\cr
 Encoding: \tab UTF-8\cr



More information about the rkward-tracker mailing list