quby-compiler 0.5.41 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/exe/quby-compile +11 -1
- data/lib/quby/compiler/dsl/info_block_builder.rb +10 -2
- data/lib/quby/compiler/dsl/panel_builder.rb +19 -8
- data/lib/quby/compiler/dsl/questionnaire_builder.rb +10 -4
- data/lib/quby/compiler/dsl/questions/base.rb +7 -2
- data/lib/quby/compiler/dsl/table_builder.rb +1 -1
- data/lib/quby/compiler/dsl.rb +2 -2
- data/lib/quby/compiler/entities/definition.rb +3 -2
- data/lib/quby/compiler/entities/question.rb +4 -3
- data/lib/quby/compiler/entities/question_optgroup.rb +3 -1
- data/lib/quby/compiler/entities/question_option.rb +2 -0
- data/lib/quby/compiler/entities/questionnaire.rb +7 -1
- data/lib/quby/compiler/entities/questions/checkbox_question.rb +1 -1
- data/lib/quby/compiler/entities/text.rb +7 -6
- data/lib/quby/compiler/instance.rb +29 -24
- data/lib/quby/compiler/outputs/locale_serializer.rb +153 -0
- data/lib/quby/compiler/outputs/quby_frontend_v1_serializer.rb +1 -1
- data/lib/quby/compiler/outputs/quby_frontend_v2_serializer.rb +22 -3
- data/lib/quby/compiler/outputs.rb +2 -1
- data/lib/quby/compiler/services/definition_validator.rb +21 -0
- data/lib/quby/compiler/version.rb +1 -1
- data/lib/quby/compiler.rb +7 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9aa19b0ae3da964273f613a8705e3b63e699dc14fc9793f67e13b891f5e6f42f
|
|
4
|
+
data.tar.gz: 8b2e41485f4d05711ea4098f1bbc23fdfe6e43fac6764047760434bbc04c41b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 127e6a879195d5ee84994016e53e7dbb99cb5a188023df2156e708f1e7ca57464d589b7c49adbae203acefbd1ad2eaecf943fa069466d82209a624f477f611dd
|
|
7
|
+
data.tar.gz: 5256bfbd80fa05c961cd7cfd0c59c78c35fa6b24fb3581534a7a10d926f58ab5432cdc674ee9ad9abd5262981657f6d164914229a2c28c0a6d836372fff5a08a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# 0.6.0
|
|
2
|
+
|
|
3
|
+
* dsl:
|
|
4
|
+
* Add translate_into
|
|
5
|
+
* Allow translations to be passed to the compiler.
|
|
6
|
+
* Allow keys on panels/text/inner_titles.q
|
|
7
|
+
* Add i18n_key to question_options/slider-labels.
|
|
8
|
+
* Prettify all json files.
|
|
9
|
+
* New export locale_#{lang}.json if translate_into is present.
|
|
10
|
+
* quby2.json:
|
|
11
|
+
* Added translations, also if questionnaire is not translatable yet.
|
|
12
|
+
|
|
1
13
|
# 0.5.41
|
|
2
14
|
|
|
3
15
|
* dsl:
|
data/exe/quby-compile
CHANGED
|
@@ -30,6 +30,16 @@ end
|
|
|
30
30
|
|
|
31
31
|
lookup_tables = Quby::Compiler::Entities::LookupTables.new(lookup_tables_path)
|
|
32
32
|
|
|
33
|
+
def translations_in(path)
|
|
34
|
+
translation_files = Dir.glob(File.join(File.dirname(path), "#{Quby::Compiler::LOCALE_FILE_PREFIX}*.json"))
|
|
35
|
+
|
|
36
|
+
translation_files.to_h { |file_path|
|
|
37
|
+
filename = File.basename(file_path, '.json')
|
|
38
|
+
language_key = filename.delete_prefix(Quby::Compiler::LOCALE_FILE_PREFIX)
|
|
39
|
+
[language_key, JSON.parse(File.read(file_path))]
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
33
43
|
validation_errors_found = false
|
|
34
44
|
paths.each do |path|
|
|
35
45
|
puts "Compiling #{path}"
|
|
@@ -45,7 +55,7 @@ paths.each do |path|
|
|
|
45
55
|
next # let's not create new output files for questionnaires with errors
|
|
46
56
|
end
|
|
47
57
|
|
|
48
|
-
compiled = Quby::Compiler.compile(key, sourcecode, path: path, lookup_tables: lookup_tables)
|
|
58
|
+
compiled = Quby::Compiler.compile(key, sourcecode, path: path, lookup_tables: lookup_tables, translations: translations_in(path))
|
|
49
59
|
|
|
50
60
|
FileUtils.mkdir_p(File.join(output_path, key))
|
|
51
61
|
compiled[:outputs].each do |type, output|
|
|
@@ -25,8 +25,8 @@ module Quby
|
|
|
25
25
|
info_block.html = value
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def html(value)
|
|
29
|
-
info_block.items << Entities::Text.new('', html_content: value.to_s)
|
|
28
|
+
def html(value, key: fallback_key)
|
|
29
|
+
info_block.items << Entities::Text.new('', html_content: value.to_s, key:)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def question(key, **options, &block)
|
|
@@ -47,6 +47,14 @@ module Quby
|
|
|
47
47
|
super
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def fallback_key
|
|
54
|
+
raise "Item without key in #{info_block.key}" if questionnaire.translatable?
|
|
55
|
+
|
|
56
|
+
"#{info_block.key}_item_#{info_block.items.size}"
|
|
57
|
+
end
|
|
50
58
|
end
|
|
51
59
|
end
|
|
52
60
|
end
|
|
@@ -23,21 +23,24 @@ module Quby
|
|
|
23
23
|
@panel.title = value
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
# value deprecated, used named md attr instead.
|
|
27
|
+
def text(value=nil, key: fallback_key, md: value, html: nil, **options)
|
|
28
|
+
@panel.items << Entities::Text.new(key:, md:, html_content: html, **options)
|
|
28
29
|
end
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
# deprecated for translated questionnaires, use `text html: ..` instead.
|
|
32
|
+
def html(value, key: fallback_key)
|
|
33
|
+
@panel.items << Entities::Text.new(key:, html_content: value.to_s)
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
# Deprecated for quby2.
|
|
37
|
+
def raw_html(value="", key: fallback_key)
|
|
38
|
+
@panel.items << Entities::Text.new(key:, raw_content: value.to_s)
|
|
36
39
|
end
|
|
37
40
|
|
|
38
|
-
def video(*urls, **options)
|
|
41
|
+
def video(*urls, key: fallback_key, **options)
|
|
39
42
|
video_html = video_tag(*urls, **options)
|
|
40
|
-
@panel.items << Entities::Text.new(
|
|
43
|
+
@panel.items << Entities::Text.new(key:, raw_content: video_html)
|
|
41
44
|
end
|
|
42
45
|
|
|
43
46
|
def default_question_options(**options)
|
|
@@ -86,6 +89,14 @@ module Quby
|
|
|
86
89
|
def respond_to_missing?(method_name, include_private = false)
|
|
87
90
|
@custom_methods.key?(method_name) || super
|
|
88
91
|
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def fallback_key
|
|
96
|
+
raise "Item without key in #{@panel.key}" if @questionnaire.translatable?
|
|
97
|
+
|
|
98
|
+
"#{@panel.key}_item_#{@panel.items.size}"
|
|
99
|
+
end
|
|
89
100
|
end
|
|
90
101
|
end
|
|
91
102
|
end
|
|
@@ -95,6 +95,11 @@ module Quby
|
|
|
95
95
|
@questionnaire.abortable = true
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
# No args to generate locale file for original language.
|
|
99
|
+
def translatable_into(*languages)
|
|
100
|
+
@questionnaire.translatable_into = languages.map(&:to_s)
|
|
101
|
+
end
|
|
102
|
+
|
|
98
103
|
def allow_hotkeys(type = :all)
|
|
99
104
|
@questionnaire.allow_hotkeys = type
|
|
100
105
|
end
|
|
@@ -105,7 +110,7 @@ module Quby
|
|
|
105
110
|
end
|
|
106
111
|
|
|
107
112
|
def language(language)
|
|
108
|
-
@questionnaire.language = language
|
|
113
|
+
@questionnaire.language = language.to_s
|
|
109
114
|
end
|
|
110
115
|
|
|
111
116
|
def respondent_types(*respondent_types)
|
|
@@ -183,8 +188,9 @@ module Quby
|
|
|
183
188
|
@questionnaire.anonymous_conditions = @questionnaire.anonymous_conditions.new(hide_values_from_professionals: value)
|
|
184
189
|
end
|
|
185
190
|
|
|
186
|
-
def panel(title = nil,
|
|
187
|
-
|
|
191
|
+
def panel(title = nil, key: nil, **options, &block)
|
|
192
|
+
key ||= "p_#{@questionnaire.panels.size}"
|
|
193
|
+
panel = PanelBuilder.build(title, key:, **options, **default_panel_options, &block)
|
|
188
194
|
@questionnaire.add_panel(panel)
|
|
189
195
|
end
|
|
190
196
|
|
|
@@ -209,7 +215,7 @@ module Quby
|
|
|
209
215
|
|
|
210
216
|
# Short-circuit the question command to perform an implicit panel
|
|
211
217
|
def question(key, **options, &block)
|
|
212
|
-
panel(nil, default_panel_options) do
|
|
218
|
+
panel(nil, **default_panel_options) do
|
|
213
219
|
question(key, questionnaire: @questionnaire, **@default_question_options, **options, &block)
|
|
214
220
|
end
|
|
215
221
|
end
|
|
@@ -107,6 +107,10 @@ module Quby
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
module Labeling
|
|
110
|
+
def labels_i18n_key(value)
|
|
111
|
+
@question.labels_i18n_key = value
|
|
112
|
+
end
|
|
113
|
+
|
|
110
114
|
def label(value)
|
|
111
115
|
@question.labels << value
|
|
112
116
|
end
|
|
@@ -172,8 +176,9 @@ module Quby
|
|
|
172
176
|
end
|
|
173
177
|
|
|
174
178
|
module InnerTitles
|
|
175
|
-
def inner_title(value)
|
|
176
|
-
|
|
179
|
+
def inner_title(value, key: nil)
|
|
180
|
+
key ||= "i_t_#{@question.options.size}"
|
|
181
|
+
question_option = Entities::QuestionOption.new(key, @question, inner_title: true, description: value)
|
|
177
182
|
@question.options << question_option
|
|
178
183
|
end
|
|
179
184
|
end
|
data/lib/quby/compiler/dsl.rb
CHANGED
|
@@ -16,9 +16,9 @@ module Quby
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def self.build(key, sourcecode = nil, path: nil, lookup_tables: {}
|
|
19
|
+
def self.build(key, sourcecode = nil, path: nil, lookup_tables: {},&block)
|
|
20
20
|
Entities::Questionnaire.new(key).tap do |questionnaire|
|
|
21
|
-
builder = QuestionnaireBuilder.new(questionnaire, lookup_tables:
|
|
21
|
+
builder = QuestionnaireBuilder.new(questionnaire, lookup_tables:)
|
|
22
22
|
builder.instance_eval(sourcecode, path || key) if sourcecode
|
|
23
23
|
builder.instance_eval(&block) if block
|
|
24
24
|
questionnaire.callback_after_dsl_enhance_on_questions
|
|
@@ -10,14 +10,15 @@ module Quby
|
|
|
10
10
|
extend ActiveModel::Naming
|
|
11
11
|
include ActiveModel::Validations
|
|
12
12
|
|
|
13
|
-
attr_accessor :key, :sourcecode, :timestamp, :path, :lookup_tables
|
|
13
|
+
attr_accessor :key, :sourcecode, :timestamp, :path, :lookup_tables, :translations
|
|
14
14
|
|
|
15
|
-
def initialize(key:, path:, sourcecode: "", timestamp: nil, lookup_tables: {})
|
|
15
|
+
def initialize(key:, path:, sourcecode: "", timestamp: nil, lookup_tables: {}, translations: {})
|
|
16
16
|
@path = path
|
|
17
17
|
@key = key
|
|
18
18
|
@sourcecode = sourcecode
|
|
19
19
|
@timestamp = timestamp
|
|
20
20
|
@lookup_tables = lookup_tables
|
|
21
|
+
@translations = translations
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
validates_with Services::DefinitionValidator
|
|
@@ -20,7 +20,7 @@ module Quby
|
|
|
20
20
|
attr_accessor :description
|
|
21
21
|
attr_accessor :context_description
|
|
22
22
|
|
|
23
|
-
attr_accessor :labels
|
|
23
|
+
attr_accessor :labels, :labels_i18n_key
|
|
24
24
|
|
|
25
25
|
# What kind of question is this?
|
|
26
26
|
attr_accessor :type
|
|
@@ -171,7 +171,8 @@ module Quby
|
|
|
171
171
|
@lines = options[:lines] || 6
|
|
172
172
|
@cols = options[:cols] || 40
|
|
173
173
|
@default_invisible = options[:default_invisible] || false
|
|
174
|
-
@labels
|
|
174
|
+
@labels = []
|
|
175
|
+
@labels_i18n_key = nil
|
|
175
176
|
@css_vars = options[:css_vars]
|
|
176
177
|
@indent = options[:indent]
|
|
177
178
|
|
|
@@ -252,7 +253,7 @@ module Quby
|
|
|
252
253
|
answer_keys
|
|
253
254
|
else
|
|
254
255
|
# Some options don't have a key (inner_title), they are stripped
|
|
255
|
-
all_options.map
|
|
256
|
+
all_options.reject(&:inner_title?).map{ |opt| opt.input_key }
|
|
256
257
|
end
|
|
257
258
|
end
|
|
258
259
|
|
|
@@ -8,12 +8,14 @@ module Quby
|
|
|
8
8
|
include ::Quby::InspectExcept.new(:@question, :@questions)
|
|
9
9
|
|
|
10
10
|
attr_reader :key
|
|
11
|
+
attr_reader :i18n_key
|
|
11
12
|
attr_reader :label
|
|
12
13
|
attr_reader :options
|
|
13
14
|
|
|
14
|
-
def initialize(key, label:)
|
|
15
|
+
def initialize(key, label:, i18n_key: nil)
|
|
15
16
|
@key = key
|
|
16
17
|
@label = label
|
|
18
|
+
@i18n_key = i18n_key
|
|
17
19
|
@options = []
|
|
18
20
|
end
|
|
19
21
|
|
|
@@ -30,6 +30,7 @@ module Quby
|
|
|
30
30
|
attr_reader :question
|
|
31
31
|
attr_reader :view_id
|
|
32
32
|
attr_reader :input_key
|
|
33
|
+
attr_reader :i18n_key
|
|
33
34
|
|
|
34
35
|
def initialize(key, question, options = {})
|
|
35
36
|
@key = key
|
|
@@ -48,6 +49,7 @@ module Quby
|
|
|
48
49
|
|
|
49
50
|
@input_key = (question.type == :check_box ? @key : "#{question.key}_#{key}".to_sym)
|
|
50
51
|
@view_id = "answer_#{input_key}"
|
|
52
|
+
@i18n_key = options[:i18n_key]
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
def inner_title?
|
|
@@ -43,7 +43,8 @@ module Quby
|
|
|
43
43
|
@panels = []
|
|
44
44
|
@flags = {}.with_indifferent_access
|
|
45
45
|
@textvars = {}.with_indifferent_access
|
|
46
|
-
@language =
|
|
46
|
+
@language = 'nl'
|
|
47
|
+
@translatable_into = nil
|
|
47
48
|
@respondent_types = []
|
|
48
49
|
@tags = []
|
|
49
50
|
@check_key_clashes = true
|
|
@@ -87,6 +88,7 @@ module Quby
|
|
|
87
88
|
attr_reader :license
|
|
88
89
|
attr_accessor :licensor
|
|
89
90
|
attr_accessor :language
|
|
91
|
+
attr_accessor :translatable_into
|
|
90
92
|
attr_accessor :respondent_types
|
|
91
93
|
attr_reader :tags # tags= is manually defined below
|
|
92
94
|
attr_accessor :outcome_regeneration_requested_at
|
|
@@ -218,6 +220,10 @@ module Quby
|
|
|
218
220
|
fields.key_in_use?(key) || score_calculations.key?(key)
|
|
219
221
|
end
|
|
220
222
|
|
|
223
|
+
def translatable?
|
|
224
|
+
!translatable_into.nil?
|
|
225
|
+
end
|
|
226
|
+
|
|
221
227
|
def add_score_calculation(builder)
|
|
222
228
|
if score_calculations.key?(builder.key)
|
|
223
229
|
fail InputKeyAlreadyDefined, "Score key `#{builder.key}` already defined."
|
|
@@ -13,13 +13,17 @@ module Quby
|
|
|
13
13
|
# In case of being displayed inside a table, amount of columns/rows to span
|
|
14
14
|
attr_accessor :col_span
|
|
15
15
|
attr_accessor :row_span
|
|
16
|
+
attr_accessor :key
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
# str is deprecated, use named md instead.
|
|
19
|
+
# str fallback to empty string for quby1 compatibility.
|
|
20
|
+
def initialize(str="", key:, md: nil, **options)
|
|
18
21
|
if options[:html_content]
|
|
19
22
|
options[:raw_content] = "<div class='item text'>" + options[:html_content] + "</div>"
|
|
20
23
|
end
|
|
21
24
|
super(options)
|
|
22
|
-
@
|
|
25
|
+
@key = key
|
|
26
|
+
@str = md || str
|
|
23
27
|
@html_content = options[:html_content]
|
|
24
28
|
@display_in = options[:display_in] || [:paged]
|
|
25
29
|
@col_span = options[:col_span] || 1
|
|
@@ -35,13 +39,10 @@ module Quby
|
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
def text
|
|
42
|
+
return "" if str.blank?
|
|
38
43
|
@text ||= Quby::Compiler::MarkdownParser.new(str).to_html
|
|
39
44
|
end
|
|
40
45
|
|
|
41
|
-
def key
|
|
42
|
-
't0'
|
|
43
|
-
end
|
|
44
|
-
|
|
45
46
|
def type
|
|
46
47
|
"text"
|
|
47
48
|
end
|
|
@@ -7,24 +7,25 @@ module Quby
|
|
|
7
7
|
@lookup_tables = lookup_tables
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def compile(key:, sourcecode:, path: nil, &block)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
def compile(key:, sourcecode:, path: nil, translations: {}, &block)
|
|
11
|
+
questionnaire = \
|
|
12
|
+
if block # defined in block for tests
|
|
13
|
+
DSL.build(key, path:, &block)
|
|
14
|
+
elsif path # sourcecode given as string
|
|
15
|
+
definition = Entities::Definition.new(key:, sourcecode:, path: path || "validating '#{key}'", lookup_tables:, translations:)
|
|
16
|
+
DSL.build_from_definition(definition) # this will also validate the definition, and raise if it is invalid. We can rescue this in the caller to return the validation errors in a structured way.
|
|
17
|
+
else
|
|
18
|
+
tempfile = Tempfile.create([key, '.rb'])
|
|
19
|
+
tempfile.write(sourcecode)
|
|
20
|
+
tempfile.close
|
|
21
|
+
definition = Entities::Definition.new(key:, sourcecode:, path: tempfile.path, lookup_tables:, translations:)
|
|
22
|
+
DSL.build_from_definition(definition) # this will also validate the definition, and
|
|
23
|
+
end
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
load tempfile.path
|
|
24
|
-
Thread.current['quby-questionnaire-loading'] = nil
|
|
25
|
-
|
|
26
|
-
questionnaire.callback_after_dsl_enhance_on_questions
|
|
27
|
-
end
|
|
25
|
+
original_locale = Outputs::LocaleSerializer.new(questionnaire).as_json
|
|
26
|
+
all_translations = translations.clone \
|
|
27
|
+
.delete_if { |k, _| !questionnaire.translatable_into&.include?(k) } \
|
|
28
|
+
.merge(questionnaire.language => original_locale)
|
|
28
29
|
|
|
29
30
|
{
|
|
30
31
|
outputs: {
|
|
@@ -36,7 +37,7 @@ module Quby
|
|
|
36
37
|
roqua: Output.new(
|
|
37
38
|
key: :roqua,
|
|
38
39
|
filename: "roqua.json",
|
|
39
|
-
content: Outputs::RoquaSerializer.new(questionnaire).
|
|
40
|
+
content: JSON.pretty_generate(Outputs::RoquaSerializer.new(questionnaire).as_json),
|
|
40
41
|
),
|
|
41
42
|
seeds: Output.new(
|
|
42
43
|
key: :seeds,
|
|
@@ -46,24 +47,28 @@ module Quby
|
|
|
46
47
|
quby_frontend_v1: Output.new(
|
|
47
48
|
key: :quby_frontend_v1,
|
|
48
49
|
filename: "quby-frontend-v1.json",
|
|
49
|
-
content: Outputs::QubyFrontendV1Serializer.new(questionnaire).
|
|
50
|
+
content: JSON.pretty_generate(Outputs::QubyFrontendV1Serializer.new(questionnaire).as_json),
|
|
50
51
|
),
|
|
51
52
|
quby_frontend_v2: Output.new(
|
|
52
53
|
key: :quby_frontend_v2,
|
|
53
54
|
filename: "quby-frontend-v2.json",
|
|
54
|
-
content: Outputs::QubyFrontendV2Serializer.new(questionnaire).
|
|
55
|
+
content: JSON.pretty_generate(Outputs::QubyFrontendV2Serializer.new(questionnaire, translations: all_translations).as_json),
|
|
56
|
+
),
|
|
57
|
+
locale: questionnaire.translatable? && Output.new(
|
|
58
|
+
key: :locale,
|
|
59
|
+
filename: "#{LOCALE_FILE_PREFIX}#{questionnaire.language}.json",
|
|
60
|
+
content: JSON.pretty_generate(original_locale),
|
|
55
61
|
),
|
|
56
62
|
},
|
|
57
63
|
}
|
|
58
64
|
ensure
|
|
59
65
|
# We can only close and remove the file once serializers have finished.
|
|
60
66
|
# The serializers need the file in order to grab the source for score blocks
|
|
61
|
-
tempfile&.
|
|
62
|
-
tempfile&.unlink
|
|
67
|
+
File.unlink(tempfile&.path) if tempfile
|
|
63
68
|
end
|
|
64
69
|
|
|
65
|
-
def validate(key:, sourcecode:)
|
|
66
|
-
definition = Entities::Definition.new(key
|
|
70
|
+
def validate(key:, sourcecode:, translations: {})
|
|
71
|
+
definition = Entities::Definition.new(key:, sourcecode:, path: "validating '#{key}'", lookup_tables:, translations:)
|
|
67
72
|
definition.valid?
|
|
68
73
|
definition
|
|
69
74
|
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
module Quby
|
|
2
|
+
module Compiler
|
|
3
|
+
module Outputs
|
|
4
|
+
# We use Quby2 serialializer for html sanitization, since it's largest set we allow.
|
|
5
|
+
class LocaleSerializer
|
|
6
|
+
def initialize(questionnaire)
|
|
7
|
+
@questionnaire = questionnaire
|
|
8
|
+
@quby_frontend_v2_serializer = QubyFrontendV2Serializer.new(questionnaire)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
delegate_missing_to :@questionnaire
|
|
12
|
+
|
|
13
|
+
def as_json(options = {})
|
|
14
|
+
{
|
|
15
|
+
key: key,
|
|
16
|
+
title: title,
|
|
17
|
+
short_description: short_description,
|
|
18
|
+
footer: @quby_frontend_v2_serializer.footer,
|
|
19
|
+
**panel_locale_values,
|
|
20
|
+
**validations_locale_values,
|
|
21
|
+
}.compact
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def panel_locale_values
|
|
25
|
+
@questionnaire.panels.flat_map { |panel|
|
|
26
|
+
items_locale_values(panel.items, panel: panel)
|
|
27
|
+
}.reduce(:merge)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def items_locale_values(items, panel:)
|
|
31
|
+
items.flat_map { |item|
|
|
32
|
+
case item
|
|
33
|
+
when Quby::Compiler::Entities::Text
|
|
34
|
+
{ "#{panel.key}.#{item.key}" => item.html }
|
|
35
|
+
when Quby::Compiler::Entities::Question
|
|
36
|
+
questions_for_question(item).flat_map { |question|
|
|
37
|
+
question_locale_values(question, panel:)
|
|
38
|
+
}
|
|
39
|
+
when Quby::Compiler::Entities::InfoBlock
|
|
40
|
+
info_block_locale_values(item, panel:)
|
|
41
|
+
when Quby::Compiler::Entities::Table
|
|
42
|
+
{} # quby1 only, doesn't use translations.
|
|
43
|
+
else
|
|
44
|
+
raise "Unknown panel item type #{item.class} for locale serialization"
|
|
45
|
+
end
|
|
46
|
+
}.reduce(:merge) || {} # reduce is nil when no items.
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def info_block_locale_values(info_block, panel:)
|
|
50
|
+
{
|
|
51
|
+
"#{info_block.key}.html" => info_block.html,
|
|
52
|
+
**items_locale_values(info_block.items, panel: panel),
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def question_locale_values(question, panel:)
|
|
57
|
+
v2_question = @quby_frontend_v2_serializer.question(question) or return {} # type hidden
|
|
58
|
+
res = {
|
|
59
|
+
"#{question.key}.title" => v2_question[:title],
|
|
60
|
+
"#{question.key}.context_free_title" => question.context_free_title, # Might be useful if we want to translate in roqua as well.
|
|
61
|
+
"#{question.key}.description" => v2_question[:description],
|
|
62
|
+
"#{question.key}.context_description" => v2_question[:contextDescription], # Might be useful if we want to translate in roqua as well.
|
|
63
|
+
**question_type_specific_values(question),
|
|
64
|
+
}
|
|
65
|
+
rescue
|
|
66
|
+
puts question.key
|
|
67
|
+
raise
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def question_type_specific_values(question)
|
|
71
|
+
as_type = question.as || question.type
|
|
72
|
+
case question.as || question.type
|
|
73
|
+
when :slider
|
|
74
|
+
slider_label_locale_values(question: question, panel: nil)
|
|
75
|
+
when :split_to_units
|
|
76
|
+
split_to_units_question(question)
|
|
77
|
+
when :radio, :check_box, :select, :scale
|
|
78
|
+
options_locale_values(question: question)
|
|
79
|
+
when :integer, :float
|
|
80
|
+
{"#{question.key}.unit" => question.unit}
|
|
81
|
+
when :string, :textarea, :date, :country_select
|
|
82
|
+
{}
|
|
83
|
+
else
|
|
84
|
+
raise "Unknown question as #{as_type} for locale serialization"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def options_locale_values(question:)
|
|
89
|
+
question.options.map { |option, res|
|
|
90
|
+
option_locale_values(option, question:)
|
|
91
|
+
}.reduce(:merge)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def option_locale_values(option, question:)
|
|
95
|
+
base_key = option.i18n_key || "#{question.key}.#{option.key}"
|
|
96
|
+
if option.is_a?(Quby::Compiler::Entities::QuestionOptgroup)
|
|
97
|
+
{
|
|
98
|
+
"#{base_key}.label" => option.label, # no html
|
|
99
|
+
**option.options.map { option_locale_values(_1, question:) }.reduce(:merge)
|
|
100
|
+
}
|
|
101
|
+
elsif option.inner_title
|
|
102
|
+
{
|
|
103
|
+
"#{base_key}.html" => @quby_frontend_v2_serializer.inner_title_as_json(option, 0)[:html]
|
|
104
|
+
}
|
|
105
|
+
elsif option.placeholder
|
|
106
|
+
{"#{question.key}.placeholder" => option.label || option.description}
|
|
107
|
+
else
|
|
108
|
+
v2_option = @quby_frontend_v2_serializer.option_as_json(option)
|
|
109
|
+
{
|
|
110
|
+
"#{base_key}.label" => v2_option[:label],
|
|
111
|
+
"#{base_key}.description" => v2_option[:description],
|
|
112
|
+
"#{base_key}.context_free_description" => option.context_free_description,
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def slider_label_locale_values(question:, panel:)
|
|
118
|
+
return {} unless question.as == :slider && question.labels.present?
|
|
119
|
+
|
|
120
|
+
base_key = question.labels_i18n_key || "#{question.key}.labels"
|
|
121
|
+
question.labels.each_with_index.to_h { |label, idx|
|
|
122
|
+
["#{base_key}.#{idx}", label]
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def split_to_units_question(question)
|
|
127
|
+
return unless question.type != :split_to_units
|
|
128
|
+
return {} unless question.conversions.present? # Units without conversions are translated by quby2.
|
|
129
|
+
|
|
130
|
+
question.units.to_h { |unit|
|
|
131
|
+
["#{question.key}.units.#{unit.to_s.gsub(/[^a-zA-Z]/, "")}", unit.to_s]
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def questions_for_question(question)
|
|
136
|
+
questions = [question]
|
|
137
|
+
questions << question.title_question if question.title_question
|
|
138
|
+
questions + question.options&.flat_map do |option|
|
|
139
|
+
option.questions # We only allow subquestions one level deep.
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def validations_locale_values
|
|
144
|
+
@questionnaire.validations.select { |validation|
|
|
145
|
+
validation.config[:explanation].present?
|
|
146
|
+
}.to_h { |validation|
|
|
147
|
+
["#{validation.config[:field_key]}.validation.#{validation.type}.explanation", validation.config[:explanation]]
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -192,7 +192,7 @@ module Quby
|
|
|
192
192
|
|
|
193
193
|
def option_as_json(option)
|
|
194
194
|
{
|
|
195
|
-
key: option.key,
|
|
195
|
+
key: (option.key unless option.inner_title?),
|
|
196
196
|
value: option.value,
|
|
197
197
|
description: option.label ? "#{option.label} #{option.description}" : option.description,
|
|
198
198
|
context_free_description: option.context_free_description,
|
|
@@ -4,8 +4,9 @@ module Quby
|
|
|
4
4
|
module Compiler
|
|
5
5
|
module Outputs
|
|
6
6
|
class QubyFrontendV2Serializer
|
|
7
|
-
def initialize(questionnaire)
|
|
7
|
+
def initialize(questionnaire, translations: {})
|
|
8
8
|
@questionnaire = questionnaire
|
|
9
|
+
@translations = translations
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
delegate_missing_to :@questionnaire
|
|
@@ -28,6 +29,7 @@ module Quby
|
|
|
28
29
|
sexpVariables: sexp_variables,
|
|
29
30
|
cssVars: css_vars,
|
|
30
31
|
versionNumber: version_number,
|
|
32
|
+
translations: translations,
|
|
31
33
|
}.compact.tap do |json|
|
|
32
34
|
validate_all_questions_in_a_panel_question_keys(json)
|
|
33
35
|
end
|
|
@@ -41,8 +43,13 @@ module Quby
|
|
|
41
43
|
raise "Not all questions are listed in a panel['questionKeys']: #{missings.join(",")}."
|
|
42
44
|
end
|
|
43
45
|
|
|
46
|
+
def footer
|
|
47
|
+
handle_html(@questionnaire.footer, type: :prose, v1_markdown: false)
|
|
48
|
+
end
|
|
49
|
+
|
|
44
50
|
def panel(panel)
|
|
45
51
|
{
|
|
52
|
+
key: panel.key,
|
|
46
53
|
title: panel.title,
|
|
47
54
|
items: panel.items.map { panel_item(_1) }.compact,
|
|
48
55
|
questionKeys: question_keys_for_panel(panel) # added instead of calculated in js, so we can validate completeness.
|
|
@@ -52,7 +59,7 @@ module Quby
|
|
|
52
59
|
def panel_item(item)
|
|
53
60
|
case item
|
|
54
61
|
when Quby::Compiler::Entities::Text
|
|
55
|
-
{ type: 'html', html: handle_html(item.html, type: :prose, v1_markdown: false) }
|
|
62
|
+
{ type: 'html', key: item.key, html: handle_html(item.html, type: :prose, v1_markdown: false) }
|
|
56
63
|
when Quby::Compiler::Entities::Question
|
|
57
64
|
return if item.table # things inside a table are added to the table, AND ALSO to the panel. skip them.
|
|
58
65
|
{ type: 'question', key: item.key }
|
|
@@ -267,7 +274,7 @@ module Quby
|
|
|
267
274
|
def inner_title_as_json(option, idx)
|
|
268
275
|
{
|
|
269
276
|
type: 'html',
|
|
270
|
-
key:
|
|
277
|
+
key: option.key,
|
|
271
278
|
html: handle_html(option.description)
|
|
272
279
|
}
|
|
273
280
|
end
|
|
@@ -316,6 +323,18 @@ module Quby
|
|
|
316
323
|
end
|
|
317
324
|
end
|
|
318
325
|
|
|
326
|
+
def translations
|
|
327
|
+
@translations.clone \
|
|
328
|
+
.transform_values { |translations|
|
|
329
|
+
translations.clone.delete_if { |key, _v|
|
|
330
|
+
key.ends_with?("context_free_title") \
|
|
331
|
+
|| key.ends_with?("context_free_description") \
|
|
332
|
+
|| key == "short_description"
|
|
333
|
+
} \
|
|
334
|
+
.transform_keys! { |k| k.to_s.split('.').tap{ _1[-1] = _1[-1].camelize(:lower) }.join('.') }
|
|
335
|
+
}
|
|
336
|
+
end
|
|
337
|
+
|
|
319
338
|
def depends_on(validation_hsh)
|
|
320
339
|
question = @questionnaire.question_hash[validation_hsh['fieldKey']]
|
|
321
340
|
return unless question.depends_on.present?
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'quby/compiler/outputs/roqua_serializer'
|
|
2
2
|
require 'quby/compiler/outputs/seed_serializer'
|
|
3
3
|
require 'quby/compiler/outputs/quby_frontend_v1_serializer'
|
|
4
|
-
require 'quby/compiler/outputs/quby_frontend_v2_serializer'
|
|
4
|
+
require 'quby/compiler/outputs/quby_frontend_v2_serializer'
|
|
5
|
+
require 'quby/compiler/outputs/locale_serializer'
|
|
@@ -31,6 +31,7 @@ module Quby
|
|
|
31
31
|
validate_checkbox_options(questionnaire)
|
|
32
32
|
validate_visiblity_rules(questionnaire)
|
|
33
33
|
validate_quby2_unsupported_features(questionnaire)
|
|
34
|
+
validate_locales(questionnaire:, definition:)
|
|
34
35
|
# Some compilation errors are Exceptions (pure syntax errors) and some StandardErrors (NameErrors)
|
|
35
36
|
rescue Exception => exception # rubocop:disable Lint/RescueException
|
|
36
37
|
definition.errors.add(:sourcecode, message: "Questionnaire error: #{definition.key}\n" \
|
|
@@ -492,6 +493,26 @@ scores_schema tables to the resulting seed."
|
|
|
492
493
|
end
|
|
493
494
|
end
|
|
494
495
|
end
|
|
496
|
+
|
|
497
|
+
def validate_locales(questionnaire:, definition:)
|
|
498
|
+
return unless ENV['QUBY_VALIDATE_LOCALES'] == 'true' # fail on questionnaire branch, but not in quby admin.
|
|
499
|
+
return unless questionnaire.translatable_into.present?
|
|
500
|
+
|
|
501
|
+
source = Outputs::LocaleSerializer.new(questionnaire).as_json.stringify_keys
|
|
502
|
+
|
|
503
|
+
definition.translations.each do |language, translations|
|
|
504
|
+
next if language == questionnaire.language
|
|
505
|
+
|
|
506
|
+
missing_keys = source.keys - translations.keys
|
|
507
|
+
unless missing_keys.empty?
|
|
508
|
+
fail "Missing translations for keys #{missing_keys.join(', ')} in locale #{language}."
|
|
509
|
+
end
|
|
510
|
+
extra_keys = translations.keys - source.keys
|
|
511
|
+
unless extra_keys.empty?
|
|
512
|
+
fail "Extra translations for keys #{extra_keys.join(', ')} in locale #{language}."
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
end
|
|
495
516
|
end
|
|
496
517
|
end
|
|
497
518
|
end
|
data/lib/quby/compiler.rb
CHANGED
|
@@ -28,19 +28,23 @@ require 'quby/compiler/outputs'
|
|
|
28
28
|
|
|
29
29
|
module Quby
|
|
30
30
|
module Compiler
|
|
31
|
-
|
|
31
|
+
LOCALE_FILE_PREFIX = 'locale-'.freeze
|
|
32
|
+
|
|
33
|
+
def self.compile(key, sourcecode, path: nil, lookup_tables:, translations: {}, &block)
|
|
32
34
|
Quby::Compiler::Instance.new(lookup_tables: lookup_tables).compile(
|
|
33
35
|
key: key,
|
|
34
36
|
sourcecode: sourcecode,
|
|
35
37
|
path: path,
|
|
38
|
+
translations: translations,
|
|
36
39
|
&block
|
|
37
40
|
)
|
|
38
41
|
end
|
|
39
42
|
|
|
40
|
-
def self.validate(key, sourcecode, lookup_tables:)
|
|
41
|
-
Quby::Compiler::Instance.new(lookup_tables:
|
|
43
|
+
def self.validate(key, sourcecode, lookup_tables:, translations: {})
|
|
44
|
+
Quby::Compiler::Instance.new(lookup_tables:).validate(
|
|
42
45
|
key: key,
|
|
43
46
|
sourcecode: sourcecode,
|
|
47
|
+
translations:,
|
|
44
48
|
)
|
|
45
49
|
end
|
|
46
50
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quby-compiler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marten Veldthuis
|
|
@@ -230,6 +230,7 @@ files:
|
|
|
230
230
|
- lib/quby/compiler/markdown_parser.rb
|
|
231
231
|
- lib/quby/compiler/output.rb
|
|
232
232
|
- lib/quby/compiler/outputs.rb
|
|
233
|
+
- lib/quby/compiler/outputs/locale_serializer.rb
|
|
233
234
|
- lib/quby/compiler/outputs/quby_frontend_v1_serializer.rb
|
|
234
235
|
- lib/quby/compiler/outputs/quby_frontend_v2_serializer.rb
|
|
235
236
|
- lib/quby/compiler/outputs/roqua_serializer.rb
|