tml 4.3.1

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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +243 -0
  4. data/Rakefile +9 -0
  5. data/lib/tml.rb +56 -0
  6. data/lib/tml/api/client.rb +206 -0
  7. data/lib/tml/api/post_office.rb +71 -0
  8. data/lib/tml/application.rb +254 -0
  9. data/lib/tml/base.rb +116 -0
  10. data/lib/tml/cache.rb +143 -0
  11. data/lib/tml/cache_adapters/file.rb +89 -0
  12. data/lib/tml/cache_adapters/memcache.rb +104 -0
  13. data/lib/tml/cache_adapters/memory.rb +85 -0
  14. data/lib/tml/cache_adapters/redis.rb +108 -0
  15. data/lib/tml/config.rb +410 -0
  16. data/lib/tml/decorators/base.rb +52 -0
  17. data/lib/tml/decorators/default.rb +43 -0
  18. data/lib/tml/decorators/html.rb +102 -0
  19. data/lib/tml/exception.rb +35 -0
  20. data/lib/tml/ext/array.rb +86 -0
  21. data/lib/tml/ext/date.rb +99 -0
  22. data/lib/tml/ext/fixnum.rb +47 -0
  23. data/lib/tml/ext/hash.rb +99 -0
  24. data/lib/tml/ext/string.rb +56 -0
  25. data/lib/tml/ext/time.rb +89 -0
  26. data/lib/tml/generators/cache/base.rb +117 -0
  27. data/lib/tml/generators/cache/file.rb +159 -0
  28. data/lib/tml/language.rb +175 -0
  29. data/lib/tml/language_case.rb +105 -0
  30. data/lib/tml/language_case_rule.rb +76 -0
  31. data/lib/tml/language_context.rb +117 -0
  32. data/lib/tml/language_context_rule.rb +56 -0
  33. data/lib/tml/languages/en.json +1363 -0
  34. data/lib/tml/logger.rb +109 -0
  35. data/lib/tml/rules_engine/evaluator.rb +162 -0
  36. data/lib/tml/rules_engine/parser.rb +65 -0
  37. data/lib/tml/session.rb +199 -0
  38. data/lib/tml/source.rb +106 -0
  39. data/lib/tml/tokenizers/data.rb +96 -0
  40. data/lib/tml/tokenizers/decoration.rb +204 -0
  41. data/lib/tml/tokenizers/dom.rb +346 -0
  42. data/lib/tml/tokens/data.rb +403 -0
  43. data/lib/tml/tokens/method.rb +61 -0
  44. data/lib/tml/tokens/transform.rb +223 -0
  45. data/lib/tml/translation.rb +67 -0
  46. data/lib/tml/translation_key.rb +178 -0
  47. data/lib/tml/translator.rb +47 -0
  48. data/lib/tml/utils.rb +130 -0
  49. data/lib/tml/version.rb +34 -0
  50. metadata +121 -0
@@ -0,0 +1,403 @@
1
+ # encoding: UTF-8
2
+ #--
3
+ # Copyright (c) 2015 Translation Exchange, Inc
4
+ #
5
+ # _______ _ _ _ ______ _
6
+ # |__ __| | | | | (_) | ____| | |
7
+ # | |_ __ __ _ _ __ ___| | __ _| |_ _ ___ _ __ | |__ __ _____| |__ __ _ _ __ __ _ ___
8
+ # | | '__/ _` | '_ \/ __| |/ _` | __| |/ _ \| '_ \| __| \ \/ / __| '_ \ / _` | '_ \ / _` |/ _ \
9
+ # | | | | (_| | | | \__ \ | (_| | |_| | (_) | | | | |____ > < (__| | | | (_| | | | | (_| | __/
10
+ # |_|_| \__,_|_| |_|___/_|\__,_|\__|_|\___/|_| |_|______/_/\_\___|_| |_|\__,_|_| |_|\__, |\___|
11
+ # __/ |
12
+ # |___/
13
+ # Permission is hereby granted, free of charge, to any person obtaining
14
+ # a copy of this software and associated documentation files (the
15
+ # "Software"), to deal in the Software without restriction, including
16
+ # without limitation the rights to use, copy, modify, merge, publish,
17
+ # distribute, sublicense, and/or sell copies of the Software, and to
18
+ # permit persons to whom the Software is furnished to do so, subject to
19
+ # the following conditions:
20
+ #
21
+ # The above copyright notice and this permission notice shall be
22
+ # included in all copies or substantial portions of the Software.
23
+ #
24
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
28
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
29
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
30
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31
+ #++
32
+
33
+ module Tml
34
+ module Tokens
35
+ class Data
36
+
37
+ attr_reader :label, :full_name, :short_name, :case_keys, :context_keys
38
+
39
+ def self.expression
40
+ /(\{[^_:][\w]*(:[\w]+)*(::[\w]+)*\})/
41
+ end
42
+
43
+ def self.parse(label, opts = {})
44
+ tokens = []
45
+ label.scan(expression).uniq.each do |token_array|
46
+ tokens << self.new(label, token_array.first)
47
+ end
48
+ tokens
49
+ end
50
+
51
+ def initialize(label, token)
52
+ @label = label
53
+ @full_name = token
54
+ parse_elements
55
+ end
56
+
57
+ def parse_elements
58
+ name_without_parens = @full_name[1..-2]
59
+ name_without_case_keys = name_without_parens.split('::').first.strip
60
+
61
+ @short_name = name_without_parens.split(':').first.strip
62
+ @case_keys = name_without_parens.scan(/(::\w+)/).flatten.uniq.collect{|c| c.gsub('::', '')}
63
+ @context_keys = name_without_case_keys.scan(/(:\w+)/).flatten.uniq.collect{|c| c.gsub(':', '')}
64
+ end
65
+
66
+ def name(opts = {})
67
+ val = short_name
68
+ val = "#{val}:#{context_keys.join(':')}" if opts[:context_keys] and context_keys.any?
69
+ val = "#{val}::#{case_keys.join('::')}" if opts[:case_keys] and case_keys.any?
70
+ val = "{#{val}}" if opts[:parens]
71
+ val
72
+ end
73
+
74
+ def key
75
+ short_name.to_sym
76
+ end
77
+
78
+ # used by the translator submit dialog
79
+ def name_for_case_keys(keys)
80
+ keys = [keys] unless keys.is_a?(Array)
81
+ "#{name}::#{keys.join('::')}"
82
+ end
83
+
84
+ def context_for_language(language)
85
+ if context_keys.any?
86
+ language.context_by_keyword(context_keys.first)
87
+ else
88
+ language.context_by_token_name(short_name)
89
+ end
90
+ end
91
+
92
+ # Utility method for errors
93
+ def error(msg, return_token = true)
94
+ Tml.logger.error(msg)
95
+ return_token ? full_name : label
96
+ end
97
+
98
+ ##############################################################################
99
+ #
100
+ # returns token object from tokens param
101
+ #
102
+ ##############################################################################
103
+
104
+ def self.token_object(token_values, token_name)
105
+ return nil if token_values.nil?
106
+ token_object = Tml::Utils.hash_value(token_values, token_name)
107
+ return token_object.first if token_object.is_a?(Array)
108
+ if token_object.is_a?(Hash)
109
+ object = Tml::Utils.hash_value(token_object, :object)
110
+ return object if object
111
+ end
112
+ token_object
113
+ end
114
+
115
+ ##############################################################################
116
+ #
117
+ # tr("Hello {user_list}!", "", {:user_list => [[user1, user2, user3], :name]}}
118
+ #
119
+ # first element is an array, the rest of the elements are similar to the
120
+ # regular tokens lambda, symbol, string, with parameters that follow
121
+ #
122
+ # if you want to pass options, then make the second parameter an array as well
123
+ #
124
+ # tr("{users} joined the site", {:users => [[user1, user2, user3], :name]})
125
+ #
126
+ # tr("{users} joined the site", {:users => [[user1, user2, user3], lambda{|user| user.name}]})
127
+ #
128
+ # tr("{users} joined the site", {:users => [[user1, user2, user3], {:attribute => :name})
129
+ #
130
+ # tr("{users} joined the site", {:users => [[user1, user2, user3], {:attribute => :name, :value => "<strong>{$0}</strong>"})
131
+ #
132
+ # tr("{users} joined the site", {:users => [[user1, user2, user3], "<strong>{$0}</strong>")
133
+ #
134
+ # tr("{users} joined the site", {:users => [[user1, user2, user3], :name, {
135
+ # :limit => 4,
136
+ # :separator => ', ',
137
+ # :joiner => 'and',
138
+ # :remainder => lambda{|elements| tr("#{count||other}", :count => elements.size)},
139
+ # :expandable => true,
140
+ # :collapsable => true
141
+ # })
142
+ #
143
+ #
144
+ ##############################################################################
145
+ def token_values_from_array(params, language, options)
146
+ list_options = {
147
+ :description => "List joiner",
148
+ :limit => 4,
149
+ :separator => ", ",
150
+ :joiner => 'and',
151
+ :less => '{laquo} less',
152
+ :expandable => true,
153
+ :collapsable => true
154
+ }
155
+
156
+ objects = params[0]
157
+ method = params[1]
158
+ list_options.merge!(params[2]) if params.size > 2
159
+ list_options[:expandable] = false if options[:skip_decorations]
160
+
161
+ values = objects.collect do |obj|
162
+ if method.is_a?(String)
163
+ method.gsub("{$0}", sanitize(obj.to_s, obj, language, options.merge(:safe => false)))
164
+ elsif method.is_a?(Symbol)
165
+ if obj.is_a?(Hash)
166
+ value = Tml::Utils.hash_value(obj, method)
167
+ else
168
+ value = obj.send(method)
169
+ end
170
+ sanitize(value, obj, language, options.merge(:safe => false))
171
+ elsif method.is_a?(Hash)
172
+ attr = Tml::Utils.hash_value(method, :attribute) || Tml::Utils.hash_value(method, :property)
173
+ if obj.is_a?(Hash)
174
+ value = Tml::Utils.hash_value(obj, attr)
175
+ else
176
+ value = obj.send(method)
177
+ end
178
+
179
+ hash_value = Tml::Utils.hash_value(method, :value)
180
+ if hash_value
181
+ hash_value.gsub("{$0}", sanitize(value, obj, language, options.merge(:safe => false)))
182
+ else
183
+ sanitize(value, obj, language, options.merge(:safe => false))
184
+ end
185
+ elsif method.is_a?(Proc)
186
+ sanitize(method.call(obj), obj, language, options.merge(:safe => true))
187
+ end
188
+ end
189
+
190
+ return values.first if objects.size == 1
191
+ return values.join(list_options[:separator]) if list_options[:joiner].nil? || list_options[:joiner] == ""
192
+
193
+ joiner = language.translate(list_options[:joiner], list_options[:description], {}, options)
194
+ if values.size <= list_options[:limit]
195
+ return "#{values[0..-2].join(list_options[:separator])} #{joiner} #{values.last}"
196
+ end
197
+
198
+ display_ary = values[0..(list_options[:limit]-1)]
199
+ remaining_ary = values[list_options[:limit]..-1]
200
+ result = "#{display_ary.join(list_options[:separator])}"
201
+
202
+ unless list_options[:expandable]
203
+ result << " " << joiner << " "
204
+ if list_options[:remainder] and list_options[:remainder].is_a?(Proc)
205
+ result << list_options[:remainder].call(remaining_ary)
206
+ else
207
+ result << language.translate("{count||other}", list_options[:description], {:count => remaining_ary.size}, options)
208
+ end
209
+ return result
210
+ end
211
+
212
+ uniq_id = Tml::TranslationKey.generate_key(label, values.join(","))
213
+ result << "<span id=\"tml_other_link_#{uniq_id}\"> #{joiner} "
214
+
215
+ result << "<a href='#' onClick=\"Tml.Utils.Effects.hide('tml_other_link_#{uniq_id}'); Tml.Utils.Effects.show('tml_other_elements_#{uniq_id}'); return false;\">"
216
+ if list_options[:remainder] and list_options[:remainder].is_a?(Proc)
217
+ result << list_options[:remainder].call(remaining_ary)
218
+ else
219
+ result << language.translate("{count||other}", list_options[:description], {:count => remaining_ary.size}, options)
220
+ end
221
+ result << "</a></span>"
222
+
223
+ result << "<span id=\"tml_other_elements_#{uniq_id}\" style='display:none'>"
224
+ result << list_options[:separator] << " "
225
+ result << remaining_ary[0..-2].join(list_options[:separator])
226
+ result << " #{joiner} "
227
+ result << remaining_ary.last
228
+
229
+ if list_options[:collapsable]
230
+ result << "<a href='#' style='font-size:smaller;white-space:nowrap' onClick=\"Tml.Utils.Effects.show('tml_other_link_#{uniq_id}'); Tml.Utils.Effects.hide('tml_other_elements_#{uniq_id}'); return false;\"> "
231
+ result << language.translate(list_options[:less], list_options[:description], {}, options)
232
+ result << "</a>"
233
+ end
234
+
235
+ result << "</span>"
236
+ end
237
+
238
+ ##############################################################################
239
+ #
240
+ # gets the value based on various evaluation methods
241
+ #
242
+ # examples:
243
+ #
244
+ # tr("Hello {user}", {:user => [current_user, current_user.name]}}
245
+ # tr("Hello {user}", {:user => [current_user, :name]}}
246
+ #
247
+ # tr("Hello {user}", {:user => [{:name => "Michael", :gender => :male}, current_user.name]}}
248
+ # tr("Hello {user}", {:user => [{:name => "Michael", :gender => :male}, :name]}}
249
+ #
250
+ ##############################################################################
251
+
252
+ def token_value_from_array_param(array, language, options)
253
+ # if you provided an array, it better have some values
254
+ if array.size < 2
255
+ return error("Invalid value for array token #{full_name} in #{label}")
256
+ end
257
+
258
+ # if the first value of an array is an array handle it here
259
+ if array[0].is_a?(Array)
260
+ return token_values_from_array(array, language, options)
261
+ end
262
+
263
+ if array[1].is_a?(String)
264
+ return sanitize(array[1], array[0], language, options.merge(:safe => true))
265
+ end
266
+
267
+ if array[0].is_a?(Hash)
268
+ if array[1].is_a?(Symbol)
269
+ return sanitize(Tml::Utils.hash_value(array[0], array[1]), array[0], language, options.merge(:safe => false))
270
+ end
271
+
272
+ return error("Invalid value for array token #{full_name} in #{label}")
273
+ end
274
+
275
+ # if second param is symbol, invoke the method on the object with the remaining values
276
+ if array[1].is_a?(Symbol)
277
+ return sanitize(array[0].send(array[1]), array[0], language, options.merge(:safe => false))
278
+ end
279
+
280
+ error("Invalid value for array token #{full_name} in #{label}")
281
+ end
282
+
283
+ ##############################################################################
284
+ #
285
+ # examples:
286
+ #
287
+ # tr("Hello {user}", {:user => {:value => "Michael", :gender => :male}}}
288
+ #
289
+ # tr("Hello {user}", {:user => {:object => {:gender => :male}, :value => "Michael"}}}
290
+ # tr("Hello {user}", {:user => {:object => {:name => "Michael", :gender => :male}, :property => :name}}}
291
+ # tr("Hello {user}", {:user => {:object => {:name => "Michael", :gender => :male}, :attribute => :name}}}
292
+ #
293
+ # tr("Hello {user}", {:user => {:object => user, :value => "Michael"}}}
294
+ # tr("Hello {user}", {:user => {:object => user, :property => :name}}}
295
+ # tr("Hello {user}", {:user => {:object => user, :attribute => :name}}}
296
+ #
297
+ ##############################################################################
298
+
299
+ def token_value_from_hash_param(hash, language, options)
300
+ value = Tml::Utils.hash_value(hash, :value)
301
+ object = Tml::Utils.hash_value(hash, :object)
302
+
303
+ unless value.nil?
304
+ return sanitize(value, object || hash, language, options.merge(:safe => true))
305
+ end
306
+
307
+ if object.nil?
308
+ return error("Missing value for hash token #{full_name} in #{label}")
309
+ end
310
+
311
+ attr = Tml::Utils.hash_value(hash, :attribute) || Tml::Utils.hash_value(hash, :property)
312
+
313
+ if object.is_a?(Hash)
314
+ unless attr.nil?
315
+ return sanitize(Tml::Utils.hash_value(object, attr), object, language, options.merge(:safe => false))
316
+ end
317
+
318
+ return error("Missing value for hash token #{full_name} in #{label}")
319
+ end
320
+
321
+ sanitize(object.send(attr), object, language, options.merge(:safe => false))
322
+ end
323
+
324
+ # evaluate all possible methods for the token value and return sanitized result
325
+ def token_value(object, language, options = {})
326
+ return token_value_from_array_param(object, language, options) if object.is_a?(Array)
327
+ return token_value_from_hash_param(object, language, options) if object.is_a?(Hash)
328
+ sanitize(object, object, language, options)
329
+ end
330
+
331
+ def sanitize(value, object, language, options)
332
+ value = value.to_s
333
+
334
+ unless Tml.session.block_options[:skip_html_escaping]
335
+ if options[:safe] == false
336
+ value = ERB::Util.html_escape(value)
337
+ end
338
+ end
339
+
340
+ return value unless language_cases_enabled?
341
+ apply_language_cases(value, object, language, options)
342
+ end
343
+
344
+ def language_cases_enabled?
345
+ Tml.session.application and Tml.session.application.feature_enabled?(:language_cases)
346
+ end
347
+
348
+ ##############################################################################
349
+ #
350
+ # chooses the appropriate case for the token value. case is identified with ::
351
+ #
352
+ # examples:
353
+ #
354
+ # tr("Hello {user::nom}", "", :user => current_user)
355
+ # tr("{actor} gave {target::dat} a present", "", :actor => user1, :target => user2)
356
+ # tr("This is {user::pos} toy", "", :user => current_user)
357
+ #
358
+ ##############################################################################
359
+ def apply_case(key, value, object, language, options)
360
+ lcase = language.case_by_keyword(key)
361
+ return value unless lcase
362
+ lcase.apply(value, object, options)
363
+ end
364
+
365
+ def apply_language_cases(value, object, language, options)
366
+ case_keys.each do |key|
367
+ value = apply_case(key, value, object, language, options)
368
+ end
369
+
370
+ value
371
+ end
372
+
373
+ def substitute(label, context, language, options = {})
374
+ # get the object from the values
375
+ object = Tml::Utils.hash_value(context, key)
376
+
377
+ # see if the token is a default html token
378
+ object = Tml.config.default_token_value(key) if object.nil?
379
+
380
+ if object.nil? and not context.key?(key)
381
+ return error("Missing value for #{full_name} in #{label}", false)
382
+ end
383
+
384
+ if object.nil? and not Tml::Config.allow_nil_token_values?
385
+ return error("Token value is nil for #{full_name} in #{label}", false)
386
+ end
387
+
388
+ return label.gsub(full_name, "") if object.nil?
389
+
390
+ value = token_value(object, language, options)
391
+ label.gsub(full_name, value)
392
+ end
393
+
394
+ def sanitized_name
395
+ name(:parens => true)
396
+ end
397
+
398
+ def to_s
399
+ full_name
400
+ end
401
+ end
402
+ end
403
+ end
@@ -0,0 +1,61 @@
1
+ # encoding: UTF-8
2
+ #--
3
+ # Copyright (c) 2015 Translation Exchange, Inc
4
+ #
5
+ # _______ _ _ _ ______ _
6
+ # |__ __| | | | | (_) | ____| | |
7
+ # | |_ __ __ _ _ __ ___| | __ _| |_ _ ___ _ __ | |__ __ _____| |__ __ _ _ __ __ _ ___
8
+ # | | '__/ _` | '_ \/ __| |/ _` | __| |/ _ \| '_ \| __| \ \/ / __| '_ \ / _` | '_ \ / _` |/ _ \
9
+ # | | | | (_| | | | \__ \ | (_| | |_| | (_) | | | | |____ > < (__| | | | (_| | | | | (_| | __/
10
+ # |_|_| \__,_|_| |_|___/_|\__,_|\__|_|\___/|_| |_|______/_/\_\___|_| |_|\__,_|_| |_|\__, |\___|
11
+ # __/ |
12
+ # |___/
13
+ # Permission is hereby granted, free of charge, to any person obtaining
14
+ # a copy of this software and associated documentation files (the
15
+ # "Software"), to deal in the Software without restriction, including
16
+ # without limitation the rights to use, copy, modify, merge, publish,
17
+ # distribute, sublicense, and/or sell copies of the Software, and to
18
+ # permit persons to whom the Software is furnished to do so, subject to
19
+ # the following conditions:
20
+ #
21
+ # The above copyright notice and this permission notice shall be
22
+ # included in all copies or substantial portions of the Software.
23
+ #
24
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
28
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
29
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
30
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31
+ #++
32
+
33
+ #######################################################################
34
+ #
35
+ # Method Token Forms
36
+ #
37
+ # {user.name}
38
+ # {user.name:gender}
39
+ #
40
+ #######################################################################
41
+
42
+ class Tml::Tokens::Method < Tml::Tokens::Data
43
+ def self.expression
44
+ /(\{[^_:.][\w]*(\.[\w]+)(:[\w]+)*(::[\w]+)*\})/
45
+ end
46
+
47
+ def object_name
48
+ @object_name ||= short_name.split(".").first
49
+ end
50
+
51
+ def object_method_name
52
+ @object_method_name ||= short_name.split(".").last
53
+ end
54
+
55
+ def substitute(label, context, language, options = {})
56
+ object = Tml::Utils.hash_value(context, object_name)
57
+ raise Tml::Exception.new("Missing value for a token: #{full_name}") unless object
58
+ object_value = sanitize(object.send(object_method_name), object, language, options.merge(:safe => false))
59
+ label.gsub(full_name, object_value)
60
+ end
61
+ end