Skip to content

Commit 35a493b

Browse files
jcfrCarlos Luque
andcommitted
ENH: Support for scripted module internationalization
This commit adds CMake function "SlicerFunctionAddPythonScriptTrFilesTargets" used to ensure input expected by lupdate/lrelease translation tools are generated. The CMake function associates custom commands with each python scripts and also adds a convenience target called "Add<TargetName>PythonScriptTrFiles" to explicitly regenerate the .py.tr files. The custom commands generate the .py.tr files by invoking a python cli (CMake/Rewrite.py) replacing calls to "slicer.util.tr" with "QT_TRANSLATE_NOOP". The python cli "CMake/Rewrite.py" internally uses the astor python package. Updates SlicerConfig to set Slicer_BUILD_I18N_SUPPORT, Slicer_UPDATE_TRANSLATION and Slicer_LANGUAGES. Co-authored-by: Carlos Luque <carlos.luque@ulpgc.es>
1 parent daecb6e commit 35a493b

File tree

9 files changed

+226
-8
lines changed

9 files changed

+226
-8
lines changed

Base/Python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ set(Slicer_PYTHON_SCRIPTS
66
slicer/slicerqt
77
slicer/testing
88
slicer/util
9+
slicer/i18n
910
freesurfer
1011
mrml
1112
vtkAddon

Base/Python/slicer/i18n.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
def tr(context, text):
3+
from slicer import app
4+
return app.translate(context, text)

CMake/RewriteTr.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import ast
2+
import errno
3+
import getopt
4+
import os
5+
import sys
6+
7+
import astor
8+
9+
10+
class RewriteTr(ast.NodeTransformer):
11+
"""Replace tr to QT_TRANSLATE_NOOP
12+
"""
13+
def visit_Call(self, node):
14+
self.generic_visit(node)
15+
# Transform 'tr' into 'QT_TRANSLATE_NOOP' """
16+
if (isinstance(node.func, ast.Name) and \
17+
("tr" == node.func.id) and \
18+
len(node.args) == 2):
19+
call = ast.Call(func=ast.Name(id='QT_TRANSLATE_NOOP', ctx=ast.Load()),
20+
args=node.args,
21+
keywords=[])
22+
ast.copy_location(call, node)
23+
# Add lineno & col_offset to the nodes we created
24+
ast.fix_missing_locations(call)
25+
return call
26+
27+
# Return the original node if we don't want to change it.
28+
return node
29+
30+
31+
def mkdir_p(path):
32+
"""Ensure directory ``path`` exists. If needed, parent directories
33+
are created.
34+
Adapted from http://stackoverflow.com/a/600612/1539918
35+
"""
36+
try:
37+
os.makedirs(path)
38+
except OSError as exc: # Python >2.5
39+
if exc.errno == errno.EEXIST and os.path.isdir(path):
40+
pass
41+
else: # pragma: no cover
42+
raise
43+
44+
45+
def main(argv):
46+
47+
input_file = ''
48+
output_file = ''
49+
try:
50+
opts, args = getopt.getopt(argv, "hi:o:", ["ifile=","ofile="])
51+
except getopt.GetoptError:
52+
print('RewriteTr.py -i <inputfile> -o <outputfile>')
53+
sys.exit(2)
54+
for opt, arg in opts:
55+
if opt == '-h':
56+
print('RewriteTr.py -i <inputfile> -o <outputfile>')
57+
sys.exit()
58+
elif opt in ("-i", "--ifile"):
59+
input_file = arg
60+
elif opt in ("-o", "--ofile"):
61+
output_file = arg
62+
63+
with open(input_file, "r") as source:
64+
tree = ast.parse(source.read())
65+
66+
tree_new = RewriteTr().visit(tree)
67+
all_lines = astor.to_source(tree_new)
68+
69+
# if needed, create output directory
70+
output_dir = os.path.dirname(output_file)
71+
#print("output_dir [%s]" % output_dir)
72+
mkdir_p(output_dir)
73+
74+
# replace the single quotation marks to double quotation marks. It is necessary for lupdate
75+
with open(output_file, "w") as destination:
76+
for line in all_lines:
77+
linenew = line.replace('\'', "\"")
78+
destination.write(linenew)
79+
80+
81+
if __name__ == "__main__":
82+
main(sys.argv[1:])

CMake/SlicerConfig.cmake.in

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ set(Slicer_BUILD_EXTENSIONMANAGER_SUPPORT "@Slicer_BUILD_EXTENSIONMANAGER_SUPPOR
126126
set(Slicer_BUILD_PARAMETERSERIALIZER_SUPPORT "@Slicer_BUILD_PARAMETERSERIALIZER_SUPPORT@")
127127
set(Slicer_BUILD_TESTING "@BUILD_TESTING@")
128128
set(Slicer_BUILD_WEBENGINE_SUPPORT "@Slicer_BUILD_WEBENGINE_SUPPORT@")
129+
set(Slicer_BUILD_I18N_SUPPORT "@Slicer_BUILD_I18N_SUPPORT@")
130+
set(Slicer_UPDATE_TRANSLATION "@Slicer_UPDATE_TRANSLATION@")
131+
132+
if(Slicer_BUILD_I18N_SUPPORT)
133+
set(Slicer_LANGUAGES "@Slicer_LANGUAGES@")
134+
endif()
129135

130136
set(Slicer_REQUIRED_QT_VERSION "@Slicer_REQUIRED_QT_VERSION@")
131137
set(Slicer_REQUIRED_QT_MODULES "@Slicer_REQUIRED_QT_MODULES@")
@@ -348,14 +354,15 @@ if(Slicer_USE_PYTHONQT)
348354
set(Slicer_INSTALL_QTSCRIPTEDMODULES_SHARE_DIR "@Slicer_INSTALL_QTSCRIPTEDMODULES_SHARE_DIR@")
349355
endif()
350356

357+
set(Slicer_INSTALL_QM_DIR "@Slicer_INSTALL_QM_DIR@")
358+
351359
set(Slicer_INSTALL_THIRDPARTY_BIN_DIR "${Slicer_INSTALL_ROOT}${Slicer_BUNDLE_EXTENSIONS_LOCATION}${Slicer_THIRDPARTY_BIN_DIR}")
352360
set(Slicer_INSTALL_THIRDPARTY_LIB_DIR "${Slicer_INSTALL_ROOT}${Slicer_BUNDLE_EXTENSIONS_LOCATION}${Slicer_THIRDPARTY_LIB_DIR}")
353361
set(Slicer_INSTALL_THIRDPARTY_SHARE_DIR "${Slicer_INSTALL_ROOT}${Slicer_BUNDLE_EXTENSIONS_LOCATION}${Slicer_THIRDPARTY_SHARE_DIR}")
354362

355363
# The Slicer install prefix (*not* defined in the install tree)
356364
set(Slicer_INSTALL_PREFIX "@CMAKE_INSTALL_PREFIX@")
357365

358-
359366
# --------------------------------------------------------------------------
360367
# Testing
361368
# --------------------------------------------------------------------------

CMake/SlicerMacroBuildScriptedModule.cmake

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ macro(slicerMacroBuildScriptedModule)
9797
set(_no_install_subdir_option "")
9898
endif()
9999

100+
# --------------------------------------------------------------------------
101+
# Copy and/or compile scripts and associated resources
102+
# --------------------------------------------------------------------------
100103
ctkMacroCompilePythonScript(
101104
TARGET_NAME ${MY_SLICER_NAME}
102105
SCRIPTS "${MY_SLICER_SCRIPTS}"
@@ -106,6 +109,16 @@ macro(slicerMacroBuildScriptedModule)
106109
${_no_install_subdir_option}
107110
)
108111

112+
# --------------------------------------------------------------------------
113+
# Translations
114+
# --------------------------------------------------------------------------
115+
if("${CTK_COMPILE_PYTHON_SCRIPTS_GLOBAL_TARGET_NAME}" STREQUAL "")
116+
SlicerFunctionAddPythonScriptTrFilesTargets(${MY_SLICER_NAME})
117+
endif()
118+
119+
# --------------------------------------------------------------------------
120+
# Tests
121+
# --------------------------------------------------------------------------
109122
if(BUILD_TESTING AND MY_SLICER_WITH_GENERIC_TESTS)
110123
set(_generic_unitest_scripts)
111124
SlicerMacroConfigureGenericPythonModuleTests("${MY_SLICER_NAME}" _generic_unitest_scripts)
@@ -122,3 +135,56 @@ macro(slicerMacroBuildScriptedModule)
122135

123136
endmacro()
124137

138+
function(SlicerFunctionAddPythonScriptTrFilesTargets target)
139+
140+
set(rewrite_script "${Slicer_CMAKE_DIR}/RewriteTr.py")
141+
if(NOT EXISTS ${rewrite_script})
142+
message(FATAL_ERROR "Rewrite script does not exist [${rewrite_script}]")
143+
endif()
144+
145+
set(TS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/Resources/Translations/")
146+
147+
#get_property(Slicer_LANGUAGES GLOBAL PROPERTY Slicer_LANGUAGES)
148+
149+
get_property(_CTK_${target}_PYTHON_SCRIPTS GLOBAL PROPERTY _CTK_${target}_PYTHON_SCRIPTS)
150+
151+
set(rewritten_srcs)
152+
foreach(entry IN LISTS _CTK_${target}_PYTHON_SCRIPTS)
153+
string(REPLACE "|" ";" tuple "${entry}")
154+
list(GET tuple 0 src)
155+
list(GET tuple 1 tgt_file)
156+
list(GET tuple 2 dest_dir)
157+
158+
set(rewritten_src_file "${tgt_file}.tr")
159+
set(rewritten_src "${dest_dir}/${rewritten_src_file}")
160+
161+
add_custom_command(DEPENDS ${src}
162+
COMMAND ${PYTHON_EXECUTABLE}
163+
${rewrite_script} -i ${src} -o ${rewritten_src}
164+
OUTPUT ${rewritten_src}
165+
COMMENT "Generating .py.tr file into binary directory: ${rewritten_src_file}")
166+
167+
list(APPEND rewritten_srcs ${rewritten_src})
168+
endforeach()
169+
170+
include(SlicerMacroTranslation)
171+
SlicerMacroTranslation(
172+
SRCS ${rewritten_srcs}
173+
TS_DIR ${TS_DIR}
174+
TS_BASEFILENAME ${target}
175+
TS_LANGUAGES ${Slicer_LANGUAGES}
176+
QM_OUTPUT_DIR_VAR QM_OUTPUT_DIR
177+
QM_OUTPUT_FILES_VAR QM_OUTPUT_FILES
178+
)
179+
180+
# store the paths where the qm files are located
181+
set_property(GLOBAL APPEND PROPERTY Slicer_QM_OUTPUT_DIRS ${QM_OUTPUT_DIR})
182+
183+
# store the qm files associated with scripted modules
184+
set_property(GLOBAL APPEND PROPERTY QM_SCRIPTED_MODULE_FILES ${QM_OUTPUT_FILES})
185+
186+
set(target_name Add${target}PythonScriptTrFiles)
187+
if(NOT TARGET ${target_name})
188+
add_custom_target(${target_name} DEPENDS ${rewritten_srcs} ${ARGN})
189+
endif()
190+
endfunction()

CMakeLists.txt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,8 @@ include(SlicerFunctionAddPythonQtResources)
417417
#-----------------------------------------------------------------------------
418418
if(Slicer_BUILD_I18N_SUPPORT)
419419
set(Slicer_LANGUAGES
420-
"fr"
420+
"es_Es"
421+
"fr_FR"
421422
)
422423
set_property(GLOBAL PROPERTY Slicer_LANGUAGES ${Slicer_LANGUAGES})
423424
endif()
@@ -1151,10 +1152,16 @@ ExternalData_Add_Target(${Slicer_ExternalData_DATA_MANAGEMENT_TARGET})
11511152
#-----------------------------------------------------------------------------
11521153
# Create targets CopySlicerPython{Resource, Script}Files, CompileSlicerPythonFiles
11531154
if(Slicer_USE_PYTHONQT)
1155+
11541156
slicerFunctionAddPythonQtResourcesTargets(SlicerPythonResources)
11551157
ctkFunctionAddCompilePythonScriptTargets(
11561158
${CTK_COMPILE_PYTHON_SCRIPTS_GLOBAL_TARGET_NAME}
1157-
DEPENDS SlicerPythonResources
1159+
DEPENDS
1160+
SlicerPythonResources
1161+
)
1162+
1163+
SlicerFunctionAddPythonScriptTrFilesTargets(
1164+
${CTK_COMPILE_PYTHON_SCRIPTS_GLOBAL_TARGET_NAME}
11581165
)
11591166
endif()
11601167

Modules/Scripted/Endoscopy/Endoscopy.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from slicer.ScriptedLoadableModule import *
66
import logging
77

8+
from slicer.i18n import tr
9+
810
#
911
# Endoscopy
1012
#
@@ -64,7 +66,7 @@ def setup(self):
6466

6567
# Path collapsible button
6668
pathCollapsibleButton = ctk.ctkCollapsibleButton()
67-
pathCollapsibleButton.text = "Path"
69+
pathCollapsibleButton.text = tr("EndoscopyWidget", "Path")
6870
self.layout.addWidget(pathCollapsibleButton)
6971

7072
# Layout within the path collapsible button
@@ -80,7 +82,7 @@ def setup(self):
8082
cameraNodeSelector.removeEnabled = False
8183
cameraNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton)
8284
cameraNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.setCameraNode)
83-
pathFormLayout.addRow("Camera:", cameraNodeSelector)
85+
pathFormLayout.addRow(tr("EndoscopyWidget", "Camera:"), cameraNodeSelector)
8486
self.parent.connect('mrmlSceneChanged(vtkMRMLScene*)',
8587
cameraNodeSelector, 'setMRMLScene(vtkMRMLScene*)')
8688

@@ -94,12 +96,12 @@ def setup(self):
9496
inputFiducialsNodeSelector.addEnabled = False
9597
inputFiducialsNodeSelector.removeEnabled = False
9698
inputFiducialsNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton)
97-
pathFormLayout.addRow("Input Fiducials:", inputFiducialsNodeSelector)
99+
pathFormLayout.addRow(tr("EndoscopyWidget", "Input Fiducials:"), inputFiducialsNodeSelector)
98100
self.parent.connect('mrmlSceneChanged(vtkMRMLScene*)',
99101
inputFiducialsNodeSelector, 'setMRMLScene(vtkMRMLScene*)')
100102

101103
# CreatePath button
102-
createPathButton = qt.QPushButton("Create path")
104+
createPathButton = qt.QPushButton(tr("EndoscopyWidget", "Create path"))
103105
createPathButton.toolTip = "Create the path."
104106
createPathButton.enabled = False
105107
pathFormLayout.addRow(createPathButton)
@@ -108,7 +110,7 @@ def setup(self):
108110

109111
# Flythrough collapsible button
110112
flythroughCollapsibleButton = ctk.ctkCollapsibleButton()
111-
flythroughCollapsibleButton.text = "Flythrough"
113+
flythroughCollapsibleButton.text = tr("EndoscopyWidget", "Flythrough")
112114
flythroughCollapsibleButton.enabled = False
113115
self.layout.addWidget(flythroughCollapsibleButton)
114116

SuperBuild.cmake

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ if(Slicer_USE_PYTHONQT)
155155
endif()
156156
endif()
157157

158+
if(Slicer_BUILD_I18N_SUPPORT AND Slicer_USE_PYTHONQT)
159+
list(APPEND Slicer_DEPENDENCIES python-astor)
160+
endif()
161+
158162
if(Slicer_USE_TBB)
159163
list(APPEND Slicer_DEPENDENCIES tbb)
160164
endif()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
set(proj python-astor)
2+
3+
# Set dependency list
4+
set(${proj}_DEPENDENCIES python python-setuptools)
5+
6+
if(NOT DEFINED Slicer_USE_SYSTEM_${proj})
7+
set(Slicer_USE_SYSTEM_${proj} ${Slicer_USE_SYSTEM_python})
8+
endif()
9+
10+
# Include dependent projects if any
11+
ExternalProject_Include_Dependencies(${proj} PROJECT_VAR proj DEPENDS_VAR ${proj}_DEPENDENCIES)
12+
13+
if(Slicer_USE_SYSTEM_${proj})
14+
ExternalProject_FindPythonPackage(
15+
MODULE_NAME "astor"
16+
REQUIRED
17+
)
18+
endif()
19+
20+
if(NOT Slicer_USE_SYSTEM_${proj})
21+
22+
set(_version "0.7.1")
23+
24+
ExternalProject_Add(${proj}
25+
${${proj}_EP_ARGS}
26+
URL "https://files.pythonhosted.org/packages/99/80/f9482277c919d28bebd85813c0a70117214149a96b08981b72b63240b84c/astor-${_version}.tar.gz"
27+
URL_HASH "SHA256=95c30d87a6c2cf89aa628b87398466840f0ad8652f88eb173125a6df8533fb8d"
28+
DOWNLOAD_DIR ${CMAKE_BINARY_DIR}
29+
SOURCE_DIR ${CMAKE_BINARY_DIR}/${proj}
30+
BUILD_IN_SOURCE 1
31+
CONFIGURE_COMMAND ""
32+
BUILD_COMMAND ""
33+
INSTALL_COMMAND ${PYTHON_EXECUTABLE} setup.py install
34+
LOG_INSTALL 1
35+
DEPENDS
36+
${${proj}_DEPENDENCIES}
37+
)
38+
39+
ExternalProject_GenerateProjectDescription_Step(${proj}
40+
VERSION ${_version}
41+
)
42+
43+
else()
44+
ExternalProject_Add_Empty(${proj} DEPENDS ${${proj}_DEPENDENCIES})
45+
endif()

0 commit comments

Comments
 (0)