showoff 0.17.2 → 0.18.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
  SHA1:
3
- metadata.gz: e79f2890ed22c967bf20c13e7086bdd7e6fa441c
4
- data.tar.gz: 5c328c3d8979191809a404b984202d2fca92a8cd
3
+ metadata.gz: fa2018ebcc69b506515c2a88f49b446347256416
4
+ data.tar.gz: 97d7de5896a47cdfc5432315513819fdc060a9c7
5
5
  SHA512:
6
- metadata.gz: 281b88028b291de836db367604aaadead708eb79a34f060e1231defc51b188f5dc0cf01f1ceb8ca17e1662528d6f0d168929410f75a880a7c1ca5b24f95c0071
7
- data.tar.gz: 2408b4d8909d3f84df131a7af5069cbc5029d8f768b67d6508d83899ccafec0d549e863554ef4dbfafe921be89395e2013fe505ee1bd77eb900bba1888d66cbe
6
+ metadata.gz: 55efb2446f06b35a6cf7d0dd28144ce9aaae1dcb95de8897bacf5fd82e896750525a555762305a7f4e47ee9ba7860607b6a1ed3619b21520a77cb4f636f14a50
7
+ data.tar.gz: 420f79aea8433eb3f147600bd774a2e4c9907c3c78c78a4bcd38cb077807d14e1de24fb8ca1b3e4018fbc7ef1243b640224b239989cd03d3011608ffb173c324
data/Rakefile CHANGED
@@ -69,6 +69,43 @@ task :test do
69
69
  sh "turn test/*_test.rb #{suffix}"
70
70
  end
71
71
 
72
+
73
+ desc 'Validate translation files'
74
+ task 'lang:check' do
75
+ require 'yaml'
76
+
77
+ def compare_keys(left, right, name, stack=nil)
78
+ left.each do |key, val|
79
+ inner = stack.nil? ? key : "#{stack}.#{key}"
80
+ compare = right[key]
81
+
82
+ case compare
83
+ when Hash
84
+ compare_keys(val, compare, name, inner)
85
+ when String
86
+ next
87
+ when NilClass
88
+ puts "Error: '#{inner}' is missing from #{name}"
89
+ else
90
+ puts "Error: '#{inner}' in #{name} is a #{compare.class}, not a Hash"
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ canonical = YAML.load_file('locales/en.yml')
97
+ languages = Dir.glob('locales/*.yml').reject {|lang| lang == 'locales/en.yml' }
98
+
99
+ languages.each do |langfile|
100
+ lang = YAML.load_file(langfile)
101
+ code = File.basename(langfile, '.yml')
102
+ key = lang.keys.first
103
+
104
+ puts "Error: #{langfile} has the wrong language code (#{key})" unless code == key
105
+ compare_keys(canonical['en'], lang[key], langfile)
106
+ end
107
+ end
108
+
72
109
  begin
73
110
  require 'mg'
74
111
  MG.new("showoff.gemspec")
data/bin/showoff CHANGED
@@ -304,6 +304,9 @@ module Wrapper
304
304
  c.default_value "showoff.json"
305
305
  c.flag [:f, :file, :pres_file]
306
306
 
307
+ c.desc 'Language code to generate.'
308
+ c.flag [:l, :lang, :language, :locale]
309
+
307
310
  c.action do |global_options,options,args|
308
311
  ShowOff.do_static(args, options)
309
312
  end
data/lib/showoff.rb CHANGED
@@ -9,6 +9,12 @@ require 'htmlentities'
9
9
  require 'sinatra-websocket'
10
10
  require 'tempfile'
11
11
 
12
+ require 'i18n'
13
+ require 'i18n/backend/fallbacks'
14
+ require 'rack'
15
+ require 'rack/contrib'
16
+ require 'iso-639'
17
+
12
18
  here = File.expand_path(File.dirname(__FILE__))
13
19
  require "#{here}/showoff_utils"
14
20
  require "#{here}/commandline_parser"
@@ -53,6 +59,16 @@ class ShowOff < Sinatra::Application
53
59
  set :encoding, nil
54
60
  set :url, nil
55
61
 
62
+ # automatically select the translation based on the user's configured browser language
63
+ use Rack::Locale
64
+
65
+ configure do
66
+ I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
67
+ I18n.load_path += Dir[File.join(settings.root, '..', 'locales', '*.yml')]
68
+ I18n.backend.load_translations
69
+ I18n.enforce_available_locales = false
70
+ end
71
+
56
72
  def initialize(app=nil)
57
73
  super(app)
58
74
  @logger = Logger.new(STDOUT)
@@ -62,10 +78,6 @@ class ShowOff < Sinatra::Application
62
78
  @review = settings.review
63
79
  @execute = settings.execute
64
80
 
65
- dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
66
- @logger.debug(dir)
67
-
68
- showoff_dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
69
81
  settings.pres_dir ||= Dir.pwd
70
82
  @root_path = "."
71
83
 
@@ -139,7 +151,7 @@ class ShowOff < Sinatra::Application
139
151
  @slide_count = 0
140
152
  @section_major = 0
141
153
  @section_minor = 0
142
- @section_title = settings.showoff_config['name'] rescue 'Showoff Presentation'
154
+ @section_title = settings.showoff_config['name'] rescue I18n.t('name')
143
155
  @@slide_titles = [] # a list of generated slide names, used for cross references later.
144
156
 
145
157
  @logger.debug settings.pres_template
@@ -183,8 +195,9 @@ class ShowOff < Sinatra::Application
183
195
 
184
196
  @@downloads = Hash.new # Track downloadable files
185
197
  @@cookie = nil # presenter cookie. Identifies the presenter for control messages
198
+ @@master = nil # this holds the @client_id of the master presenter, for the cases in which multiple presenters are loaded
186
199
  @@current = Hash.new # The current slide that the presenter is viewing
187
- @@cache = nil # Cache slide content for subsequent hits
200
+ @@cache = Hash.new # Cache slide content for subsequent hits
188
201
  @@activity = [] # keep track of completion for activity slides
189
202
 
190
203
  if @interactive
@@ -199,8 +212,11 @@ class ShowOff < Sinatra::Application
199
212
 
200
213
  # Initialize Markdown Configuration
201
214
  MarkdownConfig::setup(settings.pres_dir)
202
- end
203
215
 
216
+ # Process renderer config options
217
+ @engine_options = ShowOffUtils.showoff_renderer_options(settings.pres_dir)
218
+
219
+ end
204
220
  # save stats to disk
205
221
  def self.flush
206
222
  begin
@@ -260,6 +276,81 @@ class ShowOff < Sinatra::Application
260
276
  end
261
277
  end
262
278
 
279
+ # This is just a unified lookup method that takes a full locale name
280
+ # and then resolves it to an available version of the name
281
+ def with_locale(locale)
282
+ locale = locale.to_s
283
+ until (locale.empty?) do
284
+ result = yield(locale)
285
+ return result unless result.nil?
286
+
287
+ # if not found, chop off a section and try again
288
+ locale = locale.rpartition(/[-_]/).first
289
+ end
290
+ end
291
+
292
+ # turns a locale code into a string name
293
+ def get_language_name(locale)
294
+ with_locale(locale) do |str|
295
+ result = ISO_639.find(str)
296
+ result[3] unless result.nil?
297
+ end
298
+ end
299
+
300
+ # This function returns the directory containing translated *content*, defaulting
301
+ # to cwd. This works similarly to I18n fallback, but we cannot reuse that as it's
302
+ # a different translation mechanism.
303
+ def get_locale_dir(prefix, locale)
304
+ return '.' if locale == 'disable'
305
+
306
+ with_locale(locale) do |str|
307
+ path = "#{prefix}/#{str}"
308
+ return path if File.directory?(path)
309
+ end || '.'
310
+ end
311
+
312
+ # return a hash of all language codes available and the long name description of each
313
+ def language_names
314
+ Dir.glob('locales/*').inject({}) do |memo, entry|
315
+ next memo unless File.directory? entry
316
+
317
+ locale = File.basename(entry)
318
+ memo.update(locale => get_language_name(locale))
319
+ end
320
+ end
321
+
322
+ # returns the minimized canonical version of the current selected content locale
323
+ # it assumes that if the user has specified a locale, that it's already minimized
324
+ # note: if the locale doesn't exist on disk, it will just default to no translation
325
+ def locale(user_locale)
326
+ if [nil, '', 'auto'].include? user_locale
327
+ languages = I18n.available_locales
328
+ I18n.fallbacks[I18n.locale].select { |f| languages.include? f }.first
329
+ else
330
+ user_locale
331
+ end
332
+ end
333
+
334
+ # returns a hash of all translations for the current language. This is used
335
+ # for the javascript half of the translations
336
+ def get_translations
337
+ languages = I18n.backend.send(:translations)
338
+ fallback = I18n.fallbacks[I18n.locale].select { |f| languages.keys.include? f }.first
339
+ languages[fallback]
340
+ end
341
+
342
+ # Finds the language key from strings.json and returns the strings hash. This is
343
+ # used for user translations in the presentation, e.g. SVG translations.
344
+ def user_translations
345
+ return {} unless File.file? 'locales/strings.json'
346
+ strings = JSON.parse(File.read('locales/strings.json')) rescue {}
347
+
348
+ with_locale(@locale) do |key|
349
+ return strings[key] if strings.include? key
350
+ end
351
+ {}
352
+ end
353
+
263
354
  # todo: move more behavior into this class
264
355
  class Slide
265
356
  attr_reader :classes, :text, :tpl, :bg
@@ -291,9 +382,8 @@ class ShowOff < Sinatra::Application
291
382
  if settings.encoding and content.respond_to?(:force_encoding)
292
383
  content.force_encoding(settings.encoding)
293
384
  end
294
- engine_options = ShowOffUtils.showoff_renderer_options(settings.pres_dir)
295
385
  @logger.debug "renderer: #{Tilt[:markdown].name}"
296
- @logger.debug "render options: #{engine_options.inspect}"
386
+ @logger.debug "render options: #{@engine_options.inspect}"
297
387
 
298
388
  # if there are no !SLIDE markers, then make every H1 define a new slide
299
389
  unless content =~ /^\<?!SLIDE/m
@@ -407,7 +497,7 @@ class ShowOff < Sinatra::Application
407
497
 
408
498
  # Apply the template to the slide and replace the key to generate the content of the slide
409
499
  sl = process_content_for_replacements(template.gsub(/~~~CONTENT~~~/, slide.text))
410
- sl = Tilt[:markdown].new(nil, nil, engine_options) { sl }.render
500
+ sl = Tilt[:markdown].new(nil, nil, @engine_options) { sl }.render
411
501
  sl = build_forms(sl, content_classes)
412
502
  sl = update_p_classes(sl)
413
503
  sl = process_content_for_section_tags(sl, name, opts)
@@ -418,7 +508,7 @@ class ShowOff < Sinatra::Application
418
508
  content += "</div>\n"
419
509
  if content_classes.include? 'activity'
420
510
  content += '<span class="activityToggle">'
421
- content += " <label for=\"activity-#{ref}\">Activity complete</label>"
511
+ content += " <label for=\"activity-#{ref}\">#{I18n.t('activity_complete')}</label>"
422
512
  content += " <input type=\"checkbox\" class=\"activity\" name=\"activity-#{ref}\" id=\"activity-#{ref}\">"
423
513
  content += '</span>'
424
514
  end
@@ -497,14 +587,11 @@ class ShowOff < Sinatra::Application
497
587
  filename = File.join(settings.pres_dir, '_notes', "#{name}.md")
498
588
  @logger.debug "personal notes filename: #{filename}"
499
589
  if [nil, 'notes'].include? opts[:section] and File.file? filename
500
- # TODO: shouldn't have to reparse config all the time
501
- engine_options = ShowOffUtils.showoff_renderer_options(settings.pres_dir)
502
-
503
590
  # Make sure we've got a notes div to hang personal notes from
504
591
  doc.add_child '<div class="notes-section notes"></div>' if doc.css('div.notes-section.notes').empty?
505
592
  doc.css('div.notes-section.notes').each do |section|
506
- text = Tilt[:markdown].new(nil, nil, engine_options) { File.read(filename) }.render
507
- frag = "<div class=\"personal\"><h1>Personal Notes</h1>#{text}</div>"
593
+ text = Tilt[:markdown].new(nil, nil, @engine_options) { File.read(filename) }.render
594
+ frag = "<div class=\"personal\"><h1>#{I18n.t('presenter.notes.personal')}</h1>#{text}</div>"
508
595
  note = Nokogiri::HTML::DocumentFragment.parse(frag)
509
596
 
510
597
  if section.children.size > 0
@@ -715,8 +802,8 @@ class ShowOff < Sinatra::Application
715
802
 
716
803
  begin
717
804
  tools = '<div class="tools">'
718
- tools << '<input type="button" class="display" value="Display Results">'
719
- tools << '<input type="submit" value="Save" disabled="disabled">'
805
+ tools << "<input type=\"button\" class=\"display\" value=\"#{I18n.t('forms.display')}\">"
806
+ tools << "<input type=\"submit\" value=\"#{I18n.t('forms.save')}\" disabled=\"disabled\">"
720
807
  tools << '</div>'
721
808
  form = "<form id='#{title}' action='/form/#{title}' method='POST'>#{content}#{tools}</form>"
722
809
  doc = Nokogiri::HTML::DocumentFragment.parse(form)
@@ -742,7 +829,7 @@ class ShowOff < Sinatra::Application
742
829
  def form_element(id, code, name, required, rhs, text)
743
830
  required = required ? 'required' : ''
744
831
  str = "<div class='form element #{required}' id='#{id}' data-name='#{code}'>"
745
- str << "<label for='#{id}'>#{name}</label>"
832
+ str << "<label class='question' for='#{id}'>#{name}</label>"
746
833
  case rhs
747
834
  when /^\[\s+(\d*)\]$$/ # value = [ 5] (textarea)
748
835
  str << form_element_textarea(id, code, $1)
@@ -873,10 +960,10 @@ class ShowOff < Sinatra::Application
873
960
 
874
961
  def form_classes(modifier)
875
962
  modifier.downcase!
876
- classes = []
963
+ classes = ['response']
877
964
  classes << 'correct' if modifier.include?('=')
878
965
 
879
- classes.join
966
+ classes.join(' ')
880
967
  end
881
968
 
882
969
  def form_checked?(modifier)
@@ -1016,8 +1103,11 @@ class ShowOff < Sinatra::Application
1016
1103
  end
1017
1104
 
1018
1105
  def get_slides_html(opts={:static=>false, :pdf=>false, :toc=>false, :supplemental=>nil, :section=>nil})
1106
+ sections = nil
1107
+ Dir.chdir(get_locale_dir('locales', @locale)) do
1108
+ sections = ShowOffUtils.showoff_sections(settings.pres_dir, settings.showoff_config, @logger)
1109
+ end
1019
1110
 
1020
- sections = ShowOffUtils.showoff_sections(settings.pres_dir, @logger)
1021
1111
  if sections
1022
1112
  data = ''
1023
1113
  sections.each do |section, slides|
@@ -1102,17 +1192,11 @@ class ShowOff < Sinatra::Application
1102
1192
  # Provide a button in the sidebar for interactive editing if configured
1103
1193
  @edit = settings.showoff_config['edit'] if @review
1104
1194
 
1195
+ # translated UI strings, according to the current locale
1196
+ @language = get_translations()
1197
+
1105
1198
  # store a cookie to tell clients apart. More reliable than using IP due to proxies, etc.
1106
- if request.nil? # when running showoff static
1107
- @client_id = guid()
1108
- else
1109
- if request.cookies['client_id']
1110
- @client_id = request.cookies['client_id']
1111
- else
1112
- @client_id = guid()
1113
- response.set_cookie('client_id', @client_id)
1114
- end
1115
- end
1199
+ manage_client_cookies()
1116
1200
 
1117
1201
  erb :index
1118
1202
  end
@@ -1122,8 +1206,10 @@ class ShowOff < Sinatra::Application
1122
1206
  @issues = settings.showoff_config['issues']
1123
1207
  @edit = settings.showoff_config['edit'] if @review
1124
1208
  @feedback = settings.showoff_config['feedback']
1125
- @@cookie ||= guid()
1126
- response.set_cookie('presenter', @@cookie)
1209
+ @language = get_translations()
1210
+
1211
+ manage_client_cookies(true)
1212
+
1127
1213
  erb :presenter
1128
1214
  end
1129
1215
 
@@ -1165,21 +1251,26 @@ class ShowOff < Sinatra::Application
1165
1251
  end
1166
1252
 
1167
1253
  def slides(static=false)
1254
+ @logger.warn "Cached presentations: #{@@cache.keys}"
1255
+
1256
+ # if we have a cache and we're not asking to invalidate it
1257
+ return @@cache[@locale] if (@@cache[@locale] and params['cache'] != 'clear')
1258
+
1259
+ @logger.warn "Generating locale: #{@locale}"
1260
+
1168
1261
  # If we're displaying from a repository, let's update it
1169
1262
  ShowOffUtils.update(settings.verbose) if settings.url
1170
1263
 
1171
- # if we have a cache and we're not asking to invalidate it
1172
- return @@cache if (@@cache and params['cache'] != 'clear')
1173
1264
  @@slide_titles = []
1174
1265
  content = get_slides_html(:static=>static)
1175
1266
 
1176
1267
  # allow command line cache disabling
1177
- @@cache = content unless settings.nocache
1268
+ @@cache[@locale] = content unless settings.nocache
1178
1269
  content
1179
1270
  end
1180
1271
 
1181
- def print(static=false, section=nil)
1182
- @slides = get_slides_html(:static=>static, :toc=>true, :print=>true, :section=>section)
1272
+ def print(section=nil)
1273
+ @slides = get_slides_html(:static=>true, :toc=>true, :print=>true, :section=>section)
1183
1274
  @favicon = settings.showoff_config['favicon']
1184
1275
  erb :onepage
1185
1276
  end
@@ -1334,6 +1425,8 @@ class ShowOff < Sinatra::Application
1334
1425
  path = showoff.instance_variable_get(:@root_path)
1335
1426
  logger = showoff.instance_variable_get(:@logger)
1336
1427
 
1428
+ I18n.locale = opts[:language]
1429
+
1337
1430
  case what
1338
1431
  when 'supplemental'
1339
1432
  data = showoff.send(what, opt, true)
@@ -1342,7 +1435,7 @@ class ShowOff < Sinatra::Application
1342
1435
  data = showoff.send(what, opt)
1343
1436
  when 'print'
1344
1437
  opt ||= 'handouts'
1345
- data = showoff.send(what, true, opt)
1438
+ data = showoff.send(what, opt)
1346
1439
  else
1347
1440
  data = showoff.send(what, true)
1348
1441
  end
@@ -1512,6 +1605,33 @@ class ShowOff < Sinatra::Application
1512
1605
  (request.cookies['presenter'] == @@cookie)
1513
1606
  end
1514
1607
 
1608
+ def master_presenter?
1609
+ @@master == @client_id
1610
+ end
1611
+
1612
+ def manage_client_cookies(presenter=false)
1613
+ # store a cookie to tell clients apart. More reliable than using IP due to proxies, etc.
1614
+ if request.nil? # when running showoff static
1615
+ @client_id = guid()
1616
+ else
1617
+ if request.cookies['client_id']
1618
+ @client_id = request.cookies['client_id']
1619
+ else
1620
+ @client_id = guid()
1621
+ response.set_cookie('client_id', @client_id)
1622
+ end
1623
+
1624
+ # if we have no content translations then remove the cookie
1625
+ response.delete_cookie('locale') if language_names.empty?
1626
+ end
1627
+
1628
+ if presenter
1629
+ @@master ||= @client_id
1630
+ @@cookie ||= guid()
1631
+ response.set_cookie('presenter', @@cookie)
1632
+ end
1633
+ end
1634
+
1515
1635
  post '/form/:id' do |id|
1516
1636
  client_id = request.cookies['client_id']
1517
1637
  @logger.warn("Saving form answers from ip:#{request.ip} with ID of #{client_id} for id:##{id}")
@@ -1723,7 +1843,7 @@ class ShowOff < Sinatra::Application
1723
1843
  control['id'] = guid()
1724
1844
  EM.next_tick { settings.presenters.each{|s| s.send(control.to_json) } }
1725
1845
 
1726
- when 'complete'
1846
+ when 'complete', 'answerkey'
1727
1847
  EM.next_tick { settings.sockets.each{|s| s.send(control.to_json) } }
1728
1848
 
1729
1849
  when 'annotation', 'annotationConfig'
@@ -1771,7 +1891,8 @@ class ShowOff < Sinatra::Application
1771
1891
 
1772
1892
  # gawd, this whole routing scheme is bollocks
1773
1893
  get %r{/([^/]*)/?([^/]*)} do
1774
- @title = ShowOffUtils.showoff_title(settings.pres_dir)
1894
+ @locale = locale(request.cookies['locale'])
1895
+ @title = ShowOffUtils.showoff_title(settings.pres_dir)
1775
1896
  @pause_msg = ShowOffUtils.pause_msg
1776
1897
  what = params[:captures].first
1777
1898
  opt = params[:captures][1]
@@ -1787,7 +1908,7 @@ class ShowOff < Sinatra::Application
1787
1908
 
1788
1909
  begin
1789
1910
  if (what != "favicon.ico")
1790
- if what == 'supplemental'
1911
+ if ['supplemental', 'print'].include? what
1791
1912
  data = send(what, opt)
1792
1913
  else
1793
1914
  data = send(what)