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,175 @@
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
+ class Tml::Language < Tml::Base
34
+ belongs_to :application
35
+ attributes :locale, :name, :english_name, :native_name, :right_to_left, :flag_url
36
+ has_many :contexts, :cases
37
+
38
+ def self.cache_key(locale)
39
+ File.join(locale, 'language')
40
+ end
41
+
42
+ def fetch
43
+ update_attributes(application.api_client.get("language/#{locale}", {}, {:cache_key => self.class.cache_key(locale)}))
44
+ rescue Tml::Exception => ex
45
+ Tml.logger.error("Failed to load language: #{ex}")
46
+ self
47
+ end
48
+
49
+ def update_attributes(attrs)
50
+ super
51
+
52
+ self.attributes[:contexts] = {}
53
+ if hash_value(attrs, :contexts)
54
+ hash_value(attrs, :contexts).each do |key, context|
55
+ self.attributes[:contexts][key] = Tml::LanguageContext.new(context.merge(:keyword => key, :language => self))
56
+ end
57
+ end
58
+
59
+ self.attributes[:cases] = {}
60
+ if hash_value(attrs, :cases)
61
+ hash_value(attrs, :cases).each do |key, lcase|
62
+ self.attributes[:cases][key] = Tml::LanguageCase.new(lcase.merge(:keyword => key, :language => self))
63
+ end
64
+ end
65
+ end
66
+
67
+ def context_by_keyword(keyword)
68
+ hash_value(contexts, keyword)
69
+ end
70
+
71
+ def context_by_token_name(token_name)
72
+ contexts.values.detect{|ctx| ctx.applies_to_token?(token_name)}
73
+ end
74
+
75
+ def case_by_keyword(keyword)
76
+ cases[keyword]
77
+ end
78
+
79
+ def has_definition?
80
+ contexts.any?
81
+ end
82
+
83
+ def default?
84
+ return true unless application
85
+ application.default_locale == locale
86
+ end
87
+
88
+ def dir
89
+ right_to_left? ? 'rtl' : 'ltr'
90
+ end
91
+
92
+ def align(dest)
93
+ return dest unless right_to_left?
94
+ dest.to_s == 'left' ? 'right' : 'left'
95
+ end
96
+
97
+ def full_name
98
+ return english_name if english_name == native_name
99
+ "#{english_name} - #{native_name}"
100
+ end
101
+
102
+ def current_source(options)
103
+ (options[:source] || Tml.session.block_options[:source] || Tml.session.current_source || 'undefined').to_s
104
+ end
105
+
106
+ #######################################################################################################
107
+ # Translation Methods
108
+ #
109
+ # Note - when inline translation mode is enable, cache will not be used and translators will
110
+ # always hit the live service to get the most recent translations
111
+ #
112
+ # Some cache adapters cache by source, others by key. Some are read-only, some are built on the fly.
113
+ #
114
+ # There are three ways to call the tr method
115
+ #
116
+ # tr(label, description = "", tokens = {}, options = {})
117
+ # or
118
+ # tr(label, tokens = {}, options = {})
119
+ # or
120
+ # tr(:label => label, :description => "", :tokens => {}, :options => {})
121
+ ########################################################################################################
122
+
123
+ def translate(label, description = nil, tokens = {}, options = {})
124
+ params = Tml::Utils.normalize_tr_params(label, description, tokens, options)
125
+ return params[:label] if params[:label].tml_translated?
126
+
127
+ translation_key = Tml::TranslationKey.new({
128
+ :application => application,
129
+ :label => params[:label],
130
+ :description => params[:description],
131
+ :locale => hash_value(params[:options], :locale) || hash_value(Tml.session.block_options, :locale) || Tml.config.default_locale,
132
+ :level => hash_value(params[:options], :level) || hash_value(Tml.session.block_options, :level) || Tml.config.default_level,
133
+ :translations => []
134
+ })
135
+
136
+ #Tml.logger.info("Translating " + params[:label] + " from: " + translation_key.locale.inspect + " to " + locale.inspect)
137
+
138
+ params[:tokens] ||= {}
139
+ params[:tokens][:viewing_user] ||= Tml.session.current_user
140
+
141
+ if Tml.config.disabled? or self.locale == translation_key.locale
142
+ return translation_key.substitute_tokens(
143
+ params[:label],
144
+ params[:tokens],
145
+ self,
146
+ params[:options]
147
+ ).tml_translated
148
+ end
149
+
150
+ cached_translations = application.cached_translations(locale, translation_key.key)
151
+ if cached_translations
152
+ translation_key.set_translations(locale, cached_translations)
153
+ return translation_key.translate(self, params[:tokens], params[:options]).tml_translated
154
+ end
155
+
156
+ source_key = current_source(options)
157
+
158
+ if Tml.cache.segmented? or Tml.session.inline_mode?
159
+ source = application.source(source_key, locale)
160
+ cached_translations = source.cached_translations(locale, translation_key.key)
161
+ else
162
+ cached_translations = application.fetch_translations(locale)[translation_key.key]
163
+ end
164
+
165
+ if cached_translations
166
+ translation_key.set_translations(locale, cached_translations)
167
+ else
168
+ application.register_missing_key(source_key, translation_key)
169
+ end
170
+
171
+ translation_key.translate(self, params[:tokens], params[:options]).tml_translated
172
+ end
173
+ alias :tr :translate
174
+
175
+ end
@@ -0,0 +1,105 @@
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
+ class Tml::LanguageCase < Tml::Base
34
+ belongs_to :language
35
+ attributes :id, :keyword, :latin_name, :native_name, :description, :application
36
+ has_many :rules
37
+
38
+ TR8N_HTML_TAGS_REGEX = /<\/?[^>]*>/
39
+
40
+ def initialize(attrs = {})
41
+ super
42
+ self.attributes[:rules] = []
43
+ if hash_value(attrs, :rules)
44
+ self.attributes[:rules] = hash_value(attrs, :rules).collect{ |rule| Tml::LanguageCaseRule.new(rule.merge(:language_case => self)) }
45
+ end
46
+ end
47
+
48
+ def find_matching_rule(value, object = nil)
49
+ rules.each do |rule|
50
+ return rule if rule.evaluate(value, object)
51
+ end
52
+ nil
53
+ end
54
+
55
+ #######################################################################################################
56
+ ## Evaluation Methods
57
+ #######################################################################################################
58
+
59
+ def apply(value, object = nil, options = {})
60
+ value = value.to_s
61
+
62
+ decorator = Tml::Decorators::Base.decorator
63
+
64
+ options = options.merge(:skip_decorations => true) if value.index('not_translated')
65
+
66
+ html_tokens = value.scan(TR8N_HTML_TAGS_REGEX).uniq
67
+ sanitized_value = value.gsub(TR8N_HTML_TAGS_REGEX, '')
68
+
69
+ if application.to_s == 'phrase'
70
+ words = [sanitized_value]
71
+ else
72
+ words = sanitized_value.split(/[\s\/\\]/).uniq
73
+ end
74
+
75
+ # replace html tokens with temporary placeholders {$h1}
76
+ html_tokens.each_with_index do |html_token, index|
77
+ value = value.gsub(html_token, "{$h#{index}}")
78
+ end
79
+
80
+ # replace words with temporary placeholders {$w1}
81
+ words.each_with_index do |word, index|
82
+ value = value.gsub(word, "{$w#{index}}")
83
+ end
84
+
85
+ transformed_words = []
86
+ words.each do |word|
87
+ case_rule = find_matching_rule(word, object)
88
+ case_value = case_rule ? case_rule.apply(word) : word
89
+ transformed_words << decorator.decorate_language_case(self, case_rule, word, case_value, options)
90
+ end
91
+
92
+ # replace back the temporary placeholders with the html tokens
93
+ transformed_words.each_with_index do |word, index|
94
+ value = value.gsub("{$w#{index}}", word)
95
+ end
96
+
97
+ # replace back the temporary placeholders with the html tokens
98
+ html_tokens.each_with_index do |html_token, index|
99
+ value = value.gsub("{$h#{index}}", html_token)
100
+ end
101
+
102
+ value
103
+ end
104
+
105
+ end
@@ -0,0 +1,76 @@
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
+ class Tml::LanguageCaseRule < Tml::Base
34
+ belongs_to :language_case
35
+ attributes :id, :description, :examples, :conditions, :conditions_expression, :operations, :operations_expression
36
+
37
+ def conditions_expression
38
+ self.attributes[:conditions_expression] ||= Tml::RulesEngine::Parser.new(self.conditions).parse
39
+ end
40
+
41
+ def operations_expression
42
+ self.attributes[:operations_expression] ||= Tml::RulesEngine::Parser.new(self.operations).parse
43
+ end
44
+
45
+ def gender_variables(object)
46
+ return {} unless self.conditions.index('@gender')
47
+ return {'@gender' => 'unknown'} unless object
48
+ context = language_case.language.context_by_keyword(:gender)
49
+ return {'@gender' => 'unknown'} unless context
50
+ context.vars(object)
51
+ end
52
+
53
+ def evaluate(value, object = nil)
54
+ return false if conditions.nil?
55
+
56
+ re = Tml::RulesEngine::Evaluator.new
57
+ re.evaluate(['let', '@value', value])
58
+
59
+ gender_variables(object).each do |key, value|
60
+ re.evaluate(['let', key, value])
61
+ end
62
+
63
+ re.evaluate(conditions_expression)
64
+ end
65
+
66
+ def apply(value)
67
+ value = value.to_s
68
+ return value if operations.nil?
69
+
70
+ re = Tml::RulesEngine::Evaluator.new
71
+ re.evaluate(['let', '@value', value])
72
+
73
+ re.evaluate(operations_expression)
74
+ end
75
+
76
+ end
@@ -0,0 +1,117 @@
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
+ class Tml::LanguageContext < Tml::Base
34
+ belongs_to :language
35
+ attributes :keyword, :description, :default_key, :token_expression, :variables, :token_mapping
36
+ has_many :keys, :rules
37
+
38
+ def initialize(attrs = {})
39
+ super
40
+
41
+ self.attributes[:rules] = {}
42
+ if hash_value(attrs, :rules)
43
+ hash_value(attrs, :rules).each do |key, rule|
44
+ self.attributes[:rules][key] = Tml::LanguageContextRule.new(rule.merge(
45
+ :keyword => key,
46
+ :language_context => self
47
+ ))
48
+ end
49
+ end
50
+ end
51
+
52
+ def config
53
+ context_rules = Tml.config.context_rules
54
+ hash_value(context_rules, self.keyword.to_sym) || {}
55
+ end
56
+
57
+ def token_expression
58
+ @token_expression ||= begin
59
+ exp = self.attributes[:token_expression]
60
+ exp = Regexp.new(exp[1..-2])
61
+ exp
62
+ end
63
+ end
64
+
65
+ def applies_to_token?(token)
66
+ token_expression.match(token) != nil
67
+ end
68
+
69
+ def fallback_rule
70
+ @fallback_rule ||= rules.values.detect{|rule| rule.fallback?}
71
+ end
72
+
73
+ # prepare variables for evaluation
74
+ def vars(obj)
75
+ vars = {}
76
+
77
+ variables.each do |key|
78
+ method = hash_value(config, "variables.#{key}")
79
+ unless method
80
+ vars[key] = obj
81
+ next
82
+ end
83
+
84
+ if method.is_a?(String)
85
+ if obj.is_a?(Hash)
86
+ object = hash_value(obj, 'object') || obj
87
+ if object.is_a?(Hash)
88
+ vars[key] = hash_value(object, method, :whole => true)
89
+ else
90
+ vars[key] = object.send(method)
91
+ end
92
+ elsif obj.is_a?(String)
93
+ vars[key] = obj
94
+ else
95
+ vars[key] = obj.send(method)
96
+ end
97
+ elsif method.is_a?(Proc)
98
+ vars[key] = method.call(obj)
99
+ else
100
+ vars[key] = obj
101
+ end
102
+
103
+ vars[key] = vars[key].to_s if vars[key].is_a?(Symbol)
104
+ end
105
+ vars
106
+ end
107
+
108
+ def find_matching_rule(obj)
109
+ token_vars = vars(obj)
110
+ rules.values.each do |rule|
111
+ next if rule.fallback?
112
+ return rule if rule.evaluate(token_vars)
113
+ end
114
+ fallback_rule
115
+ end
116
+
117
+ end