howdoc 0.1.0 → 0.2.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: 02cc27d9848b0932cc7ba10c66b8e5682c955071f2cde03f27e6c9061c52d82c
4
- data.tar.gz: 1bed4d6da3860c0ea8ac1f3f680218f24941f45fbb7d8e3b0699085e70588811
3
+ metadata.gz: 3c4cdda2ccd66b3c800f8711f0ea76b5220bee90496b337bccb15d1478b078b0
4
+ data.tar.gz: 7c2252c5adce29342f4483f3cf6529d8a9fd3f009e6b6e92747e408110d173ba
5
5
  SHA512:
6
- metadata.gz: 72e52519fec7d24abdb8765aa146702ef053a627fd87e4b40e1ff77775ba69ed626db5250accbb307719bbfee68a7f12e8d1fac6d966a0ab05df9859a1b38326
7
- data.tar.gz: 70208bdc3b1557985b69bbdceb70ade90a91c39b9063412ed922df7cd1cae26e5629875dd1f66a16277364af39d49eff10ede8df80695c678a1792675f0f0b42
6
+ metadata.gz: fa0d4d38e5ee59ad0b05acd8e9db25f87e99024b9c62fd7eff1b8139a1b83712a75c68e3f540c82393658fd3febd451bf420c33bea5e69e7e6c446079574e3cf
7
+ data.tar.gz: 7e5147574f8c072a49e31d6f2ce9c001606f63d90604f1e7e9bc9a51257ce08ce2ae943449df76d0dbc84413766ecd58869c507a3f8c11ad32259b9a526748bf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ Guides are now written in every language the application publishes, from a
6
+ single test run. Previously a run produced guides in the language the suite
7
+ happened to be running in, and the other languages were silently left behind.
8
+
9
+ - `config.locales` names the languages every guide is written in, not only the
10
+ languages an index is built for.
11
+ - A step is recorded as an action and its values rather than as a finished
12
+ sentence, and narrated once per language when the guide is written.
13
+ - Screenshots are taken once and copied into each language's guide.
14
+ - A guide's own heading and introduction can be translated under
15
+ `howdoc.documents.<permalink>`, since they come from the test rather than
16
+ from the engine.
17
+ - `Howdoc::Narrator.field` defers a field label until the language is known.
18
+ `Howdoc.start(locale:)` still pins a guide to one language.
19
+ - Untranslated keys are reported with the language they are missing from.
20
+
3
21
  ## 0.1.0
4
22
 
5
23
  First release. Extracted from a working in-house documentation helper that had
data/README.md CHANGED
@@ -45,6 +45,7 @@ Howdoc.configure do |config|
45
45
  config.enabled = ENV['DOC'].present? # off unless asked for
46
46
  config.host = 'example.com' # the address a reader should open
47
47
  config.formats = %i[html]
48
+ config.locales = %i[en et] # languages every guide is written in
48
49
 
49
50
  config.register_locale_path 'test/howdoc/locales/*.yml'
50
51
  config.register_template_path 'test/howdoc/templates'
@@ -70,8 +71,7 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
70
71
  id: metadata[:doc],
71
72
  permalink: metadata[:permalink],
72
73
  intro: metadata[:intro],
73
- title: "How to #{metadata[:description]}?",
74
- locale: I18n.locale
74
+ title: "How to #{metadata[:description]}?"
75
75
  )
76
76
  end
77
77
 
@@ -137,6 +137,52 @@ Three actions come translated even though no wrapper records them, because
137
137
  almost every suite ends up writing a helper that needs one: `menu` (a `path`),
138
138
  `fill_in_editor` (a `value`) and `sign_in_required` (a `text`).
139
139
 
140
+ ## Languages
141
+
142
+ A test runs in one language. Its guide is written in all of them.
143
+
144
+ Name the languages your application publishes and every recorded guide is
145
+ written once per language, from the same run:
146
+
147
+ ```ruby
148
+ config.locales = %i[en et]
149
+ ```
150
+
151
+ ```
152
+ public/docs/en/how_to_add_a_claim.html
153
+ public/docs/et/how_to_add_a_claim.html
154
+ ```
155
+
156
+ Nothing is recorded as a finished sentence, which is what makes this possible:
157
+ a step is kept as the action and the values it happened with, and the sentence
158
+ is composed once per language when the page is written. The screenshot is taken
159
+ once, while the browser is still on the page, and copied into each language's
160
+ guide -- it is a picture of the same application either way.
161
+
162
+ A guide's own heading is the exception, because it comes from the test rather
163
+ than from the engine. Translate `howdoc.documents.<permalink>.title` to give the
164
+ same guide a heading in another language, and `.intro` for its introduction:
165
+
166
+ ```yaml
167
+ et:
168
+ howdoc:
169
+ documents:
170
+ how_to_add_a_claim:
171
+ title: Kuidas lisada nõue?
172
+ ```
173
+
174
+ Left untranslated, every language gets the heading the test gave it. A guide
175
+ that belongs to one language only -- something only that language's users ever
176
+ see -- says so: `Howdoc.start(..., locale: :et)`.
177
+
178
+ A value that reads differently in different languages must not be resolved
179
+ while the test runs, or it freezes into the language the test ran in. Field
180
+ labels are the usual case, and `Howdoc::Narrator.field` defers one:
181
+
182
+ ```ruby
183
+ Howdoc.record(:check, field: Howdoc::Narrator.field(locator))
184
+ ```
185
+
140
186
  ## Wording
141
187
 
142
188
  Every sentence is an I18n key under `howdoc.actions`, so the engine contains no
@@ -161,8 +207,10 @@ own by translating `howdoc.fields.<locator>`, or replace the whole convention:
161
207
  config.field_label { |locator| MyLabels.for(locator) }
162
208
  ```
163
209
 
164
- Any key that is used but not translated is reported at the end of the run rather
165
- than leaving a silent hole in the guide.
210
+ Any key that is used but not translated is reported at the end of the run,
211
+ naming the language it is missing from, rather than leaving a silent hole in the
212
+ guide. A step no language can describe is not recorded at all; a step one
213
+ language cannot describe is left out of that language's guide only.
166
214
 
167
215
  ## Templates
168
216
 
@@ -29,9 +29,8 @@ module Howdoc
29
29
 
30
30
  def fill_in(locator = nil, with:, currently_with: nil, fill_options: {}, **options)
31
31
  doc = Howdoc.extract_options!(options)
32
- label = howdoc_field_label(locator)
33
32
 
34
- narrate(fill_in_action(label), doc, field: label, value: with)
33
+ narrate(fill_in_action(locator), doc, field: howdoc_field_label(locator), value: with)
35
34
 
36
35
  super(locator, with:, currently_with:, fill_options:, **options)
37
36
  end
@@ -114,16 +113,18 @@ module Howdoc
114
113
 
115
114
  private
116
115
 
116
+ # Not the label itself: a guide is written in every language the
117
+ # application publishes, and what a reader calls a field is a translation,
118
+ # so the label is settled once per language when the page is written.
117
119
  def howdoc_field_label(locator)
118
- Howdoc::Narrator.field_label(locator, locale: howdoc_locale)
120
+ Howdoc::Narrator.field(locator)
119
121
  end
120
122
 
121
- def howdoc_locale
122
- Howdoc.current&.locale || I18n.locale
123
- end
124
-
125
- def fill_in_action(label)
126
- case label.to_s
123
+ # Which sentence a fill_in gets is decided from the field's own name rather
124
+ # than from its label, because the name is the same in every language and
125
+ # the choice must be too.
126
+ def fill_in_action(locator)
127
+ case Howdoc.config.field_label.call(locator).to_s
127
128
  when 'email' then :fill_in_email
128
129
  when 'password' then :fill_in_password
129
130
  else :fill_in
@@ -22,10 +22,13 @@ module Howdoc
22
22
  # Writers to run for every finished document, in order.
23
23
  attr_accessor :formats
24
24
 
25
- # Locales to build an index for. Left nil, only the locales that actually
26
- # produced a guide get one, which means a run in a single language quietly
27
- # drops the other languages' indexes. Naming them keeps every index in
28
- # place, empty ones included.
25
+ # The languages the application publishes its guides in. A test runs in one
26
+ # language, but every guide it records is written in all of these, so a
27
+ # second language costs a translation file rather than a second test run.
28
+ #
29
+ # These are also the locales an index is built for. Left nil, a guide is
30
+ # written only in the language the suite is running in, and only the
31
+ # locales that actually produced a guide get an index.
29
32
  attr_accessor :locales
30
33
 
31
34
  # Screenshots are taken at this height unless the step asked for a full
@@ -4,6 +4,11 @@ module Howdoc
4
4
  # One guide: the identity a test gave it, plus the steps recorded while that
5
5
  # test ran. A document collects everything in memory and is written out once,
6
6
  # so a writer always sees the whole guide rather than a half-built page.
7
+ #
8
+ # A test runs in one language but a guide is published in all of them, so a
9
+ # document holds nothing that a language would change. Every such thing --
10
+ # the heading, the sentences, the file name -- belongs to an Edition, and a
11
+ # document has one per language.
7
12
  class Document
8
13
  # Letters that carry no accent to strip: they are their own character, so
9
14
  # Unicode decomposition leaves them untouched and a filename filter would
@@ -13,24 +18,44 @@ module Howdoc
13
18
  'đ' => 'd', 'ð' => 'd', 'ł' => 'l', 'þ' => 'th'
14
19
  }.freeze
15
20
 
16
- attr_reader :id, :permalink, :title, :intro, :locale, :steps
21
+ attr_reader :id, :permalink, :steps, :locales
17
22
 
18
- def initialize(id:, title:, locale:, permalink: nil, intro: nil)
23
+ # +title+ and +intro+ are whatever the test knew: a string in the language
24
+ # it was written in, or a hash keyed by locale when the test itself knows
25
+ # more than one. Either way a translation under howdoc.documents wins, so a
26
+ # guide gets a heading in a language no test speaks.
27
+ def initialize(id:, title:, locales:, permalink: nil, intro: nil)
19
28
  @id = id
20
29
  @title = title
21
- @locale = locale.to_sym
22
- @permalink = permalink
23
30
  @intro = intro
31
+ @permalink = permalink
32
+ @locales = Array(locales).map(&:to_sym)
24
33
  @steps = []
25
34
  @counter = 0
26
35
  end
27
36
 
28
- def slug
29
- @slug ||= (permalink || slugify(title)).to_s
37
+ def editions
38
+ @editions ||= locales.map { |locale| Edition.new(self, locale) }
39
+ end
40
+
41
+ # The edition a test is recording into. Screenshots are taken while the
42
+ # browser is still on the page, long before the other editions are written,
43
+ # so they are saved where this one keeps its pictures and copied across when
44
+ # the guides are written.
45
+ def primary
46
+ editions.first
47
+ end
48
+
49
+ def locale
50
+ primary.locale
30
51
  end
31
52
 
32
- def heading
33
- id.nil? ? title : "#{id}. #{title}"
53
+ def title_for(locale)
54
+ Narrator.document_text(:title, key: translation_key, locale:, default: in_locale(@title, locale))
55
+ end
56
+
57
+ def intro_for(locale)
58
+ Narrator.document_text(:intro, key: translation_key, locale:, default: in_locale(@intro, locale))
34
59
  end
35
60
 
36
61
  def new_step(**attributes)
@@ -44,23 +69,14 @@ module Howdoc
44
69
  steps.last
45
70
  end
46
71
 
47
- def dir
48
- File.join(Howdoc.config.root, locale.to_s)
49
- end
50
-
51
- def image_dir
52
- File.join(dir, 'images')
53
- end
54
-
55
- def path(extension)
56
- File.join(dir, "#{slug}.#{extension}")
57
- end
58
-
59
- def image_filename(number)
60
- "#{slug}_#{number}.png"
61
- end
62
-
63
- private
72
+ # What the recording edition answers, asked of the document itself, because
73
+ # that is what a recorder has in its hand.
74
+ def slug = primary.slug
75
+ def heading = primary.heading
76
+ def dir = primary.dir
77
+ def image_dir = primary.image_dir
78
+ def path(extension) = primary.path(extension)
79
+ def image_filename(number) = primary.image_filename(number)
64
80
 
65
81
  # Deliberately not ActiveSupport's parameterize: the engine has no business
66
82
  # dragging Rails into a project that only wanted a documentation generator.
@@ -68,7 +84,7 @@ module Howdoc
68
84
  # Accents are decomposed and their marks dropped rather than deleted whole,
69
85
  # so an Estonian title turns into "kuidas_lisada_noue" instead of the
70
86
  # unreadable "kuidas_lisada_n_ue" a plain ASCII filter would leave behind.
71
- def slugify(string)
87
+ def self.slugify(string)
72
88
  string
73
89
  .to_s
74
90
  .downcase
@@ -79,5 +95,20 @@ module Howdoc
79
95
  .gsub(/[^a-z0-9]+/, '_')
80
96
  .gsub(/\A_+|_+\z/, '')
81
97
  end
98
+
99
+ private
100
+
101
+ # The name a translation of this guide's own words is filed under. A
102
+ # permalink is the obvious one; without it the untranslated title has to do,
103
+ # since it is the only stable thing a test gave us.
104
+ def translation_key
105
+ @translation_key ||= (permalink || self.class.slugify(in_locale(@title, locales.first))).to_s
106
+ end
107
+
108
+ def in_locale(value, locale)
109
+ return value unless value.is_a?(Hash)
110
+
111
+ value[locale.to_sym] || value[locale.to_s] || value.values.first
112
+ end
82
113
  end
83
114
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Howdoc
4
+ # One language's version of a guide.
5
+ #
6
+ # A guide is recorded once -- the same clicks, the same screenshots -- and
7
+ # written out once per language. Everything that differs between those
8
+ # versions is here: the heading, the sentences, the directory the page lands
9
+ # in and the names of the pictures in it. A template is handed the edition for
10
+ # the language being written, so nothing in a template has to know that a
11
+ # second language exists.
12
+ class Edition
13
+ attr_reader :document, :locale
14
+
15
+ def initialize(document, locale)
16
+ @document = document
17
+ @locale = locale.to_sym
18
+ end
19
+
20
+ def id
21
+ document.id
22
+ end
23
+
24
+ def permalink
25
+ document.permalink
26
+ end
27
+
28
+ def title
29
+ @title ||= document.title_for(locale)
30
+ end
31
+
32
+ def intro
33
+ @intro ||= document.intro_for(locale)
34
+ end
35
+
36
+ def heading
37
+ id.nil? ? title : "#{id}. #{title}"
38
+ end
39
+
40
+ # A permalink is the same in every language, so the guides sit beside each
41
+ # other under one name. Without one the file is named after the heading,
42
+ # which means a translated heading gives a translated file name.
43
+ def slug
44
+ @slug ||= (permalink || Document.slugify(title)).to_s
45
+ end
46
+
47
+ def steps
48
+ @steps ||= document.steps.map { |step| step.narrate(self) }
49
+ end
50
+
51
+ def dir
52
+ File.join(Howdoc.config.root, locale.to_s)
53
+ end
54
+
55
+ def image_dir
56
+ File.join(dir, 'images')
57
+ end
58
+
59
+ def path(extension)
60
+ File.join(dir, "#{slug}.#{extension}")
61
+ end
62
+
63
+ def image_filename(number)
64
+ "#{slug}_#{number}.png"
65
+ end
66
+ end
67
+ end
@@ -7,12 +7,30 @@ module Howdoc
7
7
  # sentence itself -- every one of them is an I18n key, so the vocabulary is
8
8
  # translatable and an application can reword any instruction without touching
9
9
  # the code that records it.
10
+ #
11
+ # Nothing here is settled while a test runs. A guide is recorded once and
12
+ # written out in every language it is published in, so a sentence is composed
13
+ # at writing time, once per language.
10
14
  module Narrator
11
- SCOPE = 'howdoc.actions'
15
+ ROOT = 'howdoc'
16
+ ACTIONS = :actions
17
+ FIELDS = :fields
18
+ DOCUMENTS = :documents
19
+
20
+ # A value that cannot be settled until the language is known. A form field
21
+ # is the usual case: what a reader calls it is a translation, so recording
22
+ # the resolved label would freeze the guide into the language the test
23
+ # happened to run in.
24
+ Field = Struct.new(:locator) do
25
+ def to_howdoc_text(locale)
26
+ Narrator.field_label(locator, locale:)
27
+ end
28
+ end
12
29
 
13
30
  class << self
14
31
  # Keys that were asked for but not translated, reported once at the end of
15
- # a run rather than silently producing a guide with holes in it.
32
+ # a run rather than silently producing a guide with holes in it. A key is
33
+ # missing in a particular language, so the language is part of it.
16
34
  def missing_keys
17
35
  @missing_keys ||= []
18
36
  end
@@ -21,16 +39,39 @@ module Howdoc
21
39
  @missing_keys = []
22
40
  end
23
41
 
42
+ # Wraps a form field locator so it is named in the language of whichever
43
+ # edition is being written, rather than in the one the test ran in.
44
+ def field(locator)
45
+ Field.new(locator)
46
+ end
47
+
24
48
  def call(action, locale:, **payload)
25
- key = "#{SCOPE}.#{action}"
26
- text = I18n.t(key, locale:, default: nil, **escape(payload))
49
+ sentence = lookup(ACTIONS, action, locale:)
27
50
 
28
- if text.nil?
29
- missing_keys << key unless missing_keys.include?(key)
51
+ if sentence.nil?
52
+ record_missing(action_key(action), locale)
30
53
  return nil
31
54
  end
32
55
 
33
- text
56
+ values = resolve(payload, locale)
57
+ values.empty? ? sentence : I18n.interpolate(sentence, values)
58
+ end
59
+
60
+ # Whether any of the languages a guide is published in has a sentence for
61
+ # this action. A step no language can say anything about is not recorded
62
+ # at all -- it would otherwise leave a numbered instruction with nothing
63
+ # in it, or an illustration of nothing.
64
+ def known?(action, locales)
65
+ return false if action.nil?
66
+
67
+ return true if locales.any? { |locale| translated?(action, locale) }
68
+
69
+ locales.each { |locale| record_missing(action_key(action), locale) }
70
+ false
71
+ end
72
+
73
+ def translated?(action, locale)
74
+ !lookup(ACTIONS, action, locale:).nil?
34
75
  end
35
76
 
36
77
  # The label a reader would recognise for a form field.
@@ -41,10 +82,17 @@ module Howdoc
41
82
  def field_label(locator, locale:)
42
83
  return '' if locator.nil?
43
84
 
44
- translated = I18n.t("howdoc.fields.#{locator}", locale:, default: nil)
45
- return translated if translated
85
+ lookup(FIELDS, locator, locale:) || Howdoc.config.field_label.call(locator)
86
+ end
87
+
88
+ # A guide's own words -- its title, its introduction -- come from the test
89
+ # that recorded it and are therefore written in one language. Translating
90
+ # howdoc.documents.<permalink>.title gives the same guide a heading in
91
+ # another language without the test knowing there is another language.
92
+ def document_text(field, key:, locale:, default: nil)
93
+ return default if key.nil? || key.empty?
46
94
 
47
- Howdoc.config.field_label.call(locator)
95
+ lookup(DOCUMENTS, key, field, locale:) || default
48
96
  end
49
97
 
50
98
  # Rails names fields after the model that owns them, which is noise to
@@ -63,10 +111,56 @@ module Howdoc
63
111
 
64
112
  private
65
113
 
66
- # Translations carry the markup, values do not. Escaping here means a
67
- # label containing an angle bracket cannot break the page it lands on.
68
- def escape(payload)
114
+ # Nothing here asks I18n for a leaf.
115
+ #
116
+ # How a key is resolved belongs to the application, and an application
117
+ # that has switched cascading on answers a missing
118
+ # "howdoc.documents.some_guide.title" with whatever unrelated top-level
119
+ # "title" it happens to have. A guide would then be headed with a word
120
+ # from somewhere else entirely. So the gem's own tree is fetched whole --
121
+ # it is always there, so nothing cascades past it -- and the leaf is dug
122
+ # out of the result, where a key that is missing is simply missing.
123
+ #
124
+ # An application's fallback chain is still honoured, because a sentence
125
+ # in the wrong language reads better than a numbered step with nothing
126
+ # in it.
127
+ def lookup(*path, locale:)
128
+ keys = path.map(&:to_sym)
129
+
130
+ fallback_chain(locale).each do |candidate|
131
+ tree = I18n.t(ROOT, locale: candidate, default: nil)
132
+ next unless tree.is_a?(Hash)
133
+
134
+ value = tree.dig(*keys)
135
+ return value if value.is_a?(String)
136
+ end
137
+
138
+ nil
139
+ end
140
+
141
+ def fallback_chain(locale)
142
+ return [locale] unless I18n.respond_to?(:fallbacks)
143
+
144
+ I18n.fallbacks[locale]
145
+ rescue StandardError
146
+ [locale]
147
+ end
148
+
149
+ def action_key(action)
150
+ "#{ROOT}.#{ACTIONS}.#{action}"
151
+ end
152
+
153
+ def record_missing(key, locale)
154
+ entry = "#{key} (#{locale})"
155
+ missing_keys << entry unless missing_keys.include?(entry)
156
+ end
157
+
158
+ # Values that only a language can settle are settled here, and then
159
+ # escaped like any other: translations carry the markup, values do not, so
160
+ # a label containing an angle bracket cannot break the page it lands on.
161
+ def resolve(payload, locale)
69
162
  payload.transform_values do |value|
163
+ value = value.to_howdoc_text(locale) if value.respond_to?(:to_howdoc_text)
70
164
  value.is_a?(String) || value.is_a?(Symbol) ? ERB::Util.html_escape(value.to_s) : value
71
165
  end
72
166
  end
@@ -37,16 +37,18 @@ module Howdoc
37
37
  end
38
38
 
39
39
  # Records one instruction. +action+ names an I18n key under howdoc.actions;
40
- # the remaining keyword arguments are interpolated into it. Pass
41
- # <tt>html:</tt> instead to supply already-rendered markup, for the rare
42
- # step no sentence describes.
40
+ # the remaining keyword arguments are interpolated into it when the guide is
41
+ # written, once per language. Pass <tt>html:</tt> instead to supply
42
+ # already-rendered markup, for the rare step no sentence describes.
43
+ #
44
+ # Values that read differently in different languages must not be resolved
45
+ # here: pass Howdoc::Narrator.field(locator) and the label is settled by
46
+ # each edition as it is written.
43
47
  def record(action = nil, html: nil, arrival: false, nodoc: false, **payload)
44
48
  return nil if nodoc || !recording?
49
+ return nil if html.nil? && !Narrator.known?(action, current.locales)
45
50
 
46
- markup = html || Narrator.call(action, locale: current.locale, **payload)
47
- return nil if markup.nil?
48
-
49
- current.new_step(html: markup, arrival:)
51
+ current.new_step(action:, payload:, html:, arrival:)
50
52
  end
51
53
 
52
54
  # Illustrates the step recorded most recently. Separate from +record+
data/lib/howdoc/step.rb CHANGED
@@ -1,21 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Howdoc
4
- # One numbered instruction in a guide: a sentence, optionally an illustration,
5
- # and a flag for the steps that mark the reader's arrival on a new page.
4
+ # One numbered instruction in a guide: what happened, optionally an
5
+ # illustration of it, and a flag for the steps that mark the reader's arrival
6
+ # on a new page.
7
+ #
8
+ # What happened is kept as the action and the values it was recorded with,
9
+ # not as a finished sentence, because the same step is told in every language
10
+ # the guide is published in.
6
11
  class Step
7
- attr_reader :number
12
+ # A step as one language tells it: the sentence composed, the illustration
13
+ # named as that language's guide refers to it. This is what a template is
14
+ # handed, so a template never has to know which language it is rendering.
15
+ Narrated = Struct.new(:number, :html, :screenshot, :arrival, keyword_init: true) do
16
+ def empty?
17
+ html.nil? && screenshot.nil?
18
+ end
19
+ end
20
+
21
+ attr_reader :number, :action, :payload
8
22
  attr_accessor :html, :screenshot, :arrival
9
23
 
10
- def initialize(number:, html: nil, screenshot: nil, arrival: false)
24
+ def initialize(number:, action: nil, payload: {}, html: nil, screenshot: nil, arrival: false)
11
25
  @number = number
26
+ @action = action
27
+ @payload = payload
12
28
  @html = html
13
29
  @screenshot = screenshot
14
30
  @arrival = arrival
15
31
  end
16
32
 
17
33
  def empty?
18
- html.nil? && screenshot.nil?
34
+ html.nil? && action.nil? && screenshot.nil?
35
+ end
36
+
37
+ # Markup a caller supplied stands in every language: it was written by the
38
+ # application, which knew what it was doing.
39
+ def html_in(locale)
40
+ return html unless html.nil?
41
+ return nil if action.nil?
42
+
43
+ Narrator.call(action, locale:, **payload)
44
+ end
45
+
46
+ def narrate(edition)
47
+ Narrated.new(
48
+ number:,
49
+ html: html_in(edition.locale),
50
+ screenshot: (edition.image_filename(number) if screenshot),
51
+ arrival:
52
+ )
19
53
  end
20
54
  end
21
55
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Howdoc
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -28,16 +28,43 @@ module Howdoc
28
28
  Howdoc.config.formats.map { |format| fetch(format).call(document) }
29
29
  end
30
30
 
31
- # Writes the illustrated guide a reader actually opens.
31
+ # Writes the illustrated guide a reader actually opens -- one page per
32
+ # language the guide is published in.
32
33
  class Html
33
34
  def self.call(document)
34
- FileUtils.mkdir_p(document.dir)
35
+ document.editions.map { |edition| write(document, edition) }
36
+ end
37
+
38
+ def self.write(document, edition)
39
+ FileUtils.mkdir_p(edition.dir)
40
+ copy_screenshots(document, edition)
41
+
35
42
  markup = Templates.render(
36
43
  'document/html/layout.haml',
37
- document:, config: Howdoc.config
44
+ document: edition, config: Howdoc.config
38
45
  )
39
- File.write(document.path('html'), markup)
40
- document.path('html')
46
+ File.write(edition.path('html'), markup)
47
+ edition.path('html')
48
+ end
49
+
50
+ # A picture of the application is the same picture whatever language the
51
+ # page around it is written in, so it is taken once, while the browser is
52
+ # still on the page, and copied to where each edition looks for it.
53
+ def self.copy_screenshots(document, edition)
54
+ return if edition.equal?(document.primary)
55
+
56
+ document.steps.each do |step|
57
+ next if step.screenshot.nil?
58
+
59
+ copy_screenshot(File.join(document.image_dir, step.screenshot), edition, step.number)
60
+ end
61
+ end
62
+
63
+ def self.copy_screenshot(source, edition, number)
64
+ return unless File.file?(source)
65
+
66
+ FileUtils.mkdir_p(edition.image_dir)
67
+ FileUtils.cp(source, File.join(edition.image_dir, edition.image_filename(number)))
41
68
  end
42
69
  end
43
70
 
data/lib/howdoc.rb CHANGED
@@ -7,6 +7,7 @@ require_relative 'howdoc/version'
7
7
  require_relative 'howdoc/configuration'
8
8
  require_relative 'howdoc/narrator'
9
9
  require_relative 'howdoc/step'
10
+ require_relative 'howdoc/edition'
10
11
  require_relative 'howdoc/document'
11
12
  require_relative 'howdoc/screenshot'
12
13
  require_relative 'howdoc/recorder'
@@ -58,11 +59,36 @@ module Howdoc
58
59
  end
59
60
 
60
61
  # Begins a guide. Everything recorded until #finish belongs to it.
61
- def start(id: nil, title: nil, locale: I18n.locale, permalink: nil, intro: nil)
62
+ #
63
+ # A test runs in one language; the guide it records is written in every
64
+ # language the application publishes, which is config.locales unless this
65
+ # call names them. Pass +locale:+ for the rare guide that belongs to one
66
+ # language only -- a page describing something only that language's users
67
+ # ever see.
68
+ def start(id: nil, title: nil, locale: nil, locales: nil, permalink: nil, intro: nil)
62
69
  return nil unless enabled?
63
- return nil if title.to_s.strip.empty?
70
+ return nil if blank_title?(title)
64
71
 
65
- Recorder.current = Document.new(id:, title:, locale:, permalink:, intro:)
72
+ Recorder.current = Document.new(
73
+ id:, title:, permalink:, intro:,
74
+ locales: document_locales(locale, locales)
75
+ )
76
+ end
77
+
78
+ # Which languages a guide is written in: what the caller asked for, else
79
+ # what the application publishes, else the language the suite is running
80
+ # in, which is all a single-language application ever needs.
81
+ def document_locales(locale, locales)
82
+ chosen = locales || locale || config.locales
83
+ chosen = I18n.locale if chosen.nil? || Array(chosen).empty?
84
+
85
+ Array(chosen).map(&:to_sym)
86
+ end
87
+
88
+ def blank_title?(title)
89
+ return title.values.all? { |value| blank_title?(value) } if title.is_a?(Hash)
90
+
91
+ title.to_s.strip.empty?
66
92
  end
67
93
 
68
94
  # Ends the guide and writes it out. Called even when the test failed, so a
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: howdoc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Priit Tark
@@ -58,6 +58,7 @@ files:
58
58
  - lib/howdoc/capybara_actions.rb
59
59
  - lib/howdoc/configuration.rb
60
60
  - lib/howdoc/document.rb
61
+ - lib/howdoc/edition.rb
61
62
  - lib/howdoc/minitest.rb
62
63
  - lib/howdoc/narrator.rb
63
64
  - lib/howdoc/recorder.rb