showoff 0.17.2 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
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)