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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e4ea90302f4838a9b7c5f87f6dbd4d27c4966ec54dafc9448168635c32584ec
4
- data.tar.gz: 371f50e287667c40f77897d9edc791367cc55d646efa561ca4cf526d8ca4e056
3
+ metadata.gz: 9aa19b0ae3da964273f613a8705e3b63e699dc14fc9793f67e13b891f5e6f42f
4
+ data.tar.gz: 8b2e41485f4d05711ea4098f1bbc23fdfe6e43fac6764047760434bbc04c41b3
5
5
  SHA512:
6
- metadata.gz: 3a472cbc39f53181e19b6b9385537e3d15efb9f65cd20c33eb28d3bfff16e8fab4361c5d1df511a58ae14d3c7ccf2ff37046145cd0089db13ede18dbe3a59290
7
- data.tar.gz: 9d073a016dc4407b0ef0b39d2ac4691ac18d99c4e37fbfd5a87a5f794db2db82ed9079fe31b78de0930053faf4213eef50ec3c73aca6c81d858f4b9509093bf5
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
- def text(value, options = {})
27
- @panel.items << Entities::Text.new(value.to_s, options)
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
- def html(value)
31
- @panel.items << Entities::Text.new('', html_content: value.to_s)
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
- def raw_html(value)
35
- @panel.items << Entities::Text.new('', raw_content: value.to_s)
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('', raw_content: video_html)
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, options = {}, &block)
187
- panel = PanelBuilder.build(title, **options, **default_panel_options, &block)
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
- question_option = Entities::QuestionOption.new(nil, @question, inner_title: true, description: value)
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
@@ -25,7 +25,7 @@ module Quby
25
25
  end
26
26
 
27
27
  def text(value, **options)
28
- @table.items << Entities::Text.new(value.to_s, options)
28
+ @table.items << Entities::Text.new(value.to_s, key: nil, **options)
29
29
  end
30
30
 
31
31
  def question(key, **options, &block)
@@ -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: {}, &block)
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: 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 { |opt| opt.input_key }.compact
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 = :nl
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."
@@ -56,7 +56,7 @@ module Quby
56
56
 
57
57
  def answer_keys
58
58
  # Some options don't have a key (inner_title), they are stripped.
59
- options.map { |opt| opt.input_key }.compact
59
+ options.reject(&:inner_title?).map { |opt| opt.input_key }
60
60
  end
61
61
  end
62
62
  end
@@ -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
- def initialize(str, options = {})
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
- @str = str
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
- if block # defined in block for tests
12
- questionnaire = DSL.build(key, path: path, &block)
13
- else # sourcecode given as string
14
- tempfile = Tempfile.new([key, '.rb'])
15
- questionnaire = Entities::Questionnaire.new(key)
16
- Thread.current["quby-questionnaire-loading"] = Quby::Compiler::DSL::QuestionnaireBuilder.new(questionnaire, lookup_tables: lookup_tables)
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
- tempfile.puts "Thread.current['quby-questionnaire-loading'].instance_eval do"
19
- tempfile.puts sourcecode
20
- tempfile.puts "end"
21
- tempfile.flush
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).to_json,
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).to_json,
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).to_json,
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&.close
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: key, sourcecode: sourcecode, path: "validating '#{key}'", lookup_tables: lookup_tables)
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: "i-t-#{idx}",
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
@@ -1,5 +1,5 @@
1
1
  module Quby
2
2
  module Compiler
3
- VERSION = "0.5.41"
3
+ VERSION = "0.6.0"
4
4
  end
5
5
  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
- def self.compile(key, sourcecode, path: nil, lookup_tables:, &block)
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: lookup_tables).validate(
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.5.41
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