Skip to content

Commit b28a715

Browse files
committed
ENH: Make JSON files in extensions translatable
A general-purpose mechanism is implemented to detect if a json file is translatable, by looking for "translatable" flags in the json schema file. The schema schema file is searched by name in the folder of the json file or "Schema" or "Schemas" subfolder (this is used for translating VolumeDisplayPresets.json in Slicer core). If schema is not defined in the json file then a myfilename-schema.json file can be placed in the same folder as the myfilename.json file (this is used for translating model description XML in MONAIAuto3DSeg extension). The json schema is obtained from the build tree instead of retrieved from the network to not introduce any dependency on network availability at build time. Also, the schema URL is an identifier and it is not guaranteed to specify a URL where the schema can be downloaded from.
1 parent 4584211 commit b28a715

4 files changed

Lines changed: 204 additions & 7 deletions

File tree

Modules/Loadable/Volumes/Logic/vtkSlicerVolumesLogic.cxx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
namespace
6969
{
7070
const std::string VOLUME_DISPLAY_PRESETS_SCHEMA =
71-
"https://raw.githubusercontent.com/Slicer/Slicer/main/Modules/Loadable/Volumes/Resources/Schema/volumes-display-presets-schema-v1.0.0.json#";
71+
"https://raw.githubusercontent.com/Slicer/Slicer/main/Modules/Loadable/Volumes/Resources/Schema/volumes-display-presets-schema-v1.0.1.json#";
7272
const std::string ACCEPTED_VOLUME_DISPLAY_PRESETS_SCHEMA_REGEX =
7373
"^https://raw\\.githubusercontent\\.com/Slicer/Slicer/main/Modules/Loadable/Volumes/Resources/Schema/"
7474
"volumes-display-presets-schema-v1\\.[0-9]+\\.[0-9]+\\.json#";
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "https://raw.githubusercontent.com/Slicer/Slicer/main/Modules/Loadable/Volumes/Resources/Schema/volumes-display-presets-schema-v1.0.1.json#",
4+
"type": "object",
5+
"title": "Schema for storing presets for displaying image volumes",
6+
"description": "Currently only window and level values are stored.",
7+
"required": ["$schema", "volumeDisplayPresets"],
8+
"additionalProperties": true,
9+
"properties": {
10+
"$schema": {
11+
"$id": "#schema",
12+
"type": "string",
13+
"title": "Schema",
14+
"description": "URL of versioned schema."
15+
},
16+
"volumeDisplayPresets": {
17+
"$id": "#presets",
18+
"type": "array",
19+
"title": "Presets",
20+
"description": "Stores a list of named display presets.",
21+
"additionalItems": true,
22+
"items": {
23+
"$id": "#presetItems",
24+
"anyOf": [
25+
{
26+
"$id": "#preset",
27+
"type": "object",
28+
"title": "Presets",
29+
"description": "Stores a single display preset.",
30+
"default": {},
31+
"required": ["id", "name", "window", "level", "color"],
32+
"additionalProperties": true,
33+
"properties": {
34+
"id": {
35+
"$id": "#preset/id",
36+
"type": "string",
37+
"title": "Id",
38+
"description": "Unique identifier of the preset. Must not start with underscore character (those are reserved for internal use).",
39+
"examples": ["CT_BONE", "CT_ABDOMEN"]
40+
},
41+
"name": {
42+
"$id": "#preset/name",
43+
"type": "string",
44+
"title": "Name",
45+
"description": "Human-readable name of the preset.",
46+
"examples": ["CT-Bone"],
47+
"translatable": true
48+
},
49+
"description": {
50+
"$id": "#preset/description",
51+
"type": "string",
52+
"title": "Description",
53+
"description": "Human-readable description of the preset.",
54+
"examples": ["Emphasize bone in a CT volume."],
55+
"translatable": true
56+
},
57+
"icon": {
58+
"$id": "#preset/icon",
59+
"type": "string",
60+
"title": "Icon",
61+
"description": "Illustrative image filename or resource path.",
62+
"examples": [":/Icons/WindowLevelPreset-CT-bone.png"]
63+
},
64+
"window": {
65+
"$id": "#preset/window",
66+
"type": "number",
67+
"title": "Window",
68+
"description": "Width of the voxel value range that will be mapped to the full dynamic range of the display. Negative value means inverse mapping (higher voxel values appear darker).",
69+
"examples": [1000.0, -50.0]
70+
},
71+
"level": {
72+
"$id": "#preset/level",
73+
"type": "number",
74+
"title": "Level",
75+
"description": "Center of the voxel value range that will be mapped to the full dynamic range of the display.",
76+
"examples": [426.0, -500.0]
77+
},
78+
"color": {
79+
"$id": "#preset/color",
80+
"type": "string",
81+
"title": "Color",
82+
"description": "Color node ID, specifying the lookup table that will be used to map voxel values to colors."
83+
}
84+
}
85+
}
86+
]
87+
}
88+
}
89+
}
90+
}

Modules/Loadable/Volumes/Resources/VolumeDisplayPresets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://raw.githubusercontent.com/Slicer/Slicer/main/Modules/Loadable/Volumes/Resources/Schema/volumes-display-presets-schema-v1.0.0.json#",
2+
"$schema": "https://raw.githubusercontent.com/Slicer/Slicer/main/Modules/Loadable/Volumes/Resources/Schema/volumes-display-presets-schema-v1.0.1.json#",
33
"volumeDisplayPresets": [
44
{
55
"id": "CT_BONE",

Utilities/Scripts/update_translations.py

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import argparse
1818
import glob
19+
import json
1920
import logging
2021
import os
2122
import re
@@ -357,6 +358,115 @@ def extract_translatable_from_cli_modules(input_paths, exclude_filenames=None):
357358
if name.endswith(".xml"):
358359
_generate_translation_header_from_cli_xml(os.path.join(root, name))
359360

361+
def extract_translatable_from_json_files(input_paths):
362+
"""For each JSON file it creates a *_tr.h file that Qt lupdate can consume.
363+
The JSON file is translatable if it has a schema and the schema specifies translatable properties.
364+
Translatable properties are designated in the schema file by the "translatable" flag.
365+
The schema file is searched in the the same folder, `Schema` folder, or `Schemas` folder by the filename
366+
myfile-schema.json ( where myfile is the name of the JSON file without extension) or the filename specified in the
367+
`$schema` property of the JSON file.
368+
:param input_paths: List of folders that may contain JSON files.
369+
"""
370+
371+
for input_path in input_paths:
372+
for root, dirs, files in os.walk(input_path):
373+
for name in files:
374+
if name.endswith(".json"):
375+
json_filepath = os.path.join(root, name)
376+
property_names = _get_translatable_property_names_from_json_file(json_filepath)
377+
if not property_names:
378+
# No translatable properties found in the schema, skip this file
379+
continue
380+
_generate_translation_header_from_json_file(json_filepath, property_names)
381+
382+
def _get_schema_path_from_json_file(filename):
383+
"""Get the schema file path for a JSON file.
384+
The schema file is searched in the the same folder, `Schema` folder, or `Schemas` folder by the filename
385+
myfile-schema.json ( where myfile is the name of the JSON file without extension) or the filename specified in the
386+
`$schema` property of the JSON file.
387+
:param filename: Path to the JSON file.
388+
:return: Path to the schema file or None if not found.
389+
"""
390+
391+
# Try to find myfile-schema.json file
392+
candidate_folders = [os.path.dirname(filename), os.path.join(os.path.dirname(filename), "Schema"), os.path.join(os.path.dirname(filename), "Schemas")]
393+
for folder in candidate_folders:
394+
schema_path = os.path.join(folder, os.path.splitext(os.path.basename(filename))[0] + "-schema.json")
395+
if os.path.isfile(schema_path):
396+
# Schema file is found
397+
return schema_path
398+
399+
# Try to find schema file that is specified in the $schema property of the JSON file
400+
# (network is not accessed, it is searched in local folders only)
401+
schema_filename = None
402+
try:
403+
with open(filename, encoding="utf8") as f:
404+
json_data = json.load(f)
405+
schema_url = json_data["$schema"]
406+
# schema_url example:
407+
# https://raw.githubusercontent.com/Slicer/Slicer/main/Modules/Loadable/Volumes/Resources/Schema/volumes-display-presets-schema-v1.0.0.json#
408+
schema_filename = os.path.basename(schema_url).split("#")[0]
409+
except:
410+
# No schema filename was found
411+
return None
412+
if not schema_filename:
413+
# No schema filename was found
414+
return None
415+
for folder in candidate_folders:
416+
schema_path = os.path.join(folder, schema_filename)
417+
if os.path.isfile(schema_path):
418+
# Schema file is found
419+
return schema_path
420+
421+
# Schema file was not found
422+
return None
423+
424+
def _get_translatable_property_names_from_json_file(filename):
425+
"""Get all translatable property names from a JSON file.
426+
It uses the "translatable" custom flag.
427+
"""
428+
429+
# find the file-schema.json in the same folder as the json file
430+
schema_filename = _get_schema_path_from_json_file(filename)
431+
if not schema_filename:
432+
return []
433+
434+
try:
435+
with open(schema_filename) as f:
436+
schema = json.load(f)
437+
except json.JSONDecodeError as e:
438+
logging.error(f"Failed to decode JSON schema file '{schema_filename}': {e}")
439+
return []
440+
441+
names = set()
442+
443+
def recurse(subschema):
444+
if isinstance(subschema, dict):
445+
446+
# Recurse through object properties
447+
if "properties" in subschema:
448+
for prop, child in subschema["properties"].items():
449+
if child.get("translatable") is True:
450+
names.add(prop)
451+
recurse(child)
452+
453+
# Recurse into array items
454+
if "items" in subschema:
455+
recurse(subschema["items"])
456+
457+
# Recurse into all subschema items
458+
if "anyOf" in subschema:
459+
for item in subschema["anyOf"]:
460+
recurse(item)
461+
if "oneOf" in subschema:
462+
for item in subschema["oneOf"]:
463+
recurse(item)
464+
if "allOf" in subschema:
465+
for item in subschema["allOf"]:
466+
recurse(item)
467+
468+
recurse(schema)
469+
return sorted(names)
360470

361471
def _get_string_values_from_json(data, keys):
362472
"""Get all string values from a JSON object that matches any of the the specified keys"""
@@ -458,14 +568,11 @@ def main(argv):
458568
"DiffusionTensorTest.xml",
459569
]
460570
extract_translatable_from_cli_modules(cli_input_paths, cli_exclude_names)
461-
462-
# Slicer presets files translation
463-
_generate_translation_header_from_json_file(
464-
os.path.join(args.source_code_dir, "Modules/Loadable/Volumes/Resources/VolumeDisplayPresets.json"),
465-
["name", "description"])
466571
else:
467572
extract_translatable_from_cli_modules([args.source_code_dir])
468573

574+
extract_translatable_from_json_files([args.source_code_dir])
575+
469576
update_translations(args.component, args.source_code_dir, args.translations_dir, args.lupdate_path,
470577
args.language, args.remove_obsolete_strings, args.source_filter_regex, args.keep_temporary_files)
471578

0 commit comments

Comments
 (0)