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,223 @@
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
+ # Transform Token Form
36
+ #
37
+ # {count:number || one: message, many: messages}
38
+ # {count:number || one: сообщение, few: сообщения, many: сообщений, other: много сообщений} in other case the number is not displayed#
39
+ #
40
+ # {count | message} - will not include {count}, resulting in "messages" with implied {count}
41
+ # {count | message, messages}
42
+ #
43
+ # {count:number | message, messages}
44
+ #
45
+ # {user:gender | he, she, he/she}
46
+ #
47
+ # {user:gender | male: he, female: she, other: he/she}
48
+ #
49
+ # {now:date | did, does, will do}
50
+ # {users:list | all male, all female, mixed genders}
51
+ #
52
+ # {count || message, messages} - will include count: "5 messages"
53
+ #
54
+ #######################################################################
55
+
56
+ class Tml::Tokens::Transform < Tml::Tokens::Data
57
+ attr_reader :pipe_separator, :piped_params
58
+
59
+ def self.expression
60
+ /(\{[^_:|][\w]*(:[\w]+)*(::[\w]+)*\s*\|\|?[^{^}]+\})/
61
+ end
62
+
63
+ def parse_elements
64
+ name_without_parens = @full_name[1..-2]
65
+ name_without_pipes = name_without_parens.split('|').first.strip
66
+ name_without_case_keys = name_without_pipes.split('::').first.strip
67
+
68
+ @short_name = name_without_pipes.split(':').first.strip
69
+ @case_keys = name_without_pipes.scan(/(::\w+)/).flatten.uniq.collect{|c| c.gsub('::', '')}
70
+ @context_keys = name_without_case_keys.scan(/(:\w+)/).flatten.uniq.collect{|c| c.gsub(':', '')}
71
+
72
+ @pipe_separator = (full_name.index("||") ? "||" : "|")
73
+ @piped_params = name_without_parens.split(pipe_separator).last.split(",").collect{|param| param.strip}
74
+ end
75
+
76
+ def displayed_in_translation?
77
+ pipe_separator == "||"
78
+ end
79
+
80
+ def implied?
81
+ not displayed_in_translation?
82
+ end
83
+
84
+ def prepare_label_for_suggestion(label, index, language)
85
+ context = context_for_language(language)
86
+ values = generate_value_map(piped_params, context)
87
+
88
+ label.gsub(full_name, values[context.default_rule] || values.values.first)
89
+ end
90
+
91
+ # token: {count|| one: message, many: messages}
92
+ # results in: {"one": "message", "many": "messages"}
93
+ #
94
+ # token: {count|| message}
95
+ # transform: [{"one": "{$0}", "other": "{$0::plural}"}, {"one": "{$0}", "other": "{$1}"}]
96
+ # results in: {"one": "message", "other": "messages"}
97
+ #
98
+ # token: {count|| message, messages}
99
+ # transform: [{"one": "{$0}", "other": "{$0::plural}"}, {"one": "{$0}", "other": "{$1}"}]
100
+ # results in: {"one": "message", "other": "messages"}
101
+ #
102
+ # token: {user| Dorogoi, Dorogaya}
103
+ # transform: ["unsupported", {"male": "{$0}", "female": "{$1}", "other": "{$0}/{$1}"}]
104
+ # results in: {"male": "Dorogoi", "female": "Dorogaya", "other": "Dorogoi/Dorogaya"}
105
+ #
106
+ # token: {actors:|| likes, like}
107
+ # transform: ["unsupported", {"one": "{$0}", "other": "{$1}"}]
108
+ # results in: {"one": "likes", "other": "like"}
109
+ def generate_value_map(params, context)
110
+ values = {}
111
+
112
+ if params.first.index(':')
113
+ params.each do |p|
114
+ nv = p.split(':')
115
+ values[nv.first.strip] = nv.last.strip
116
+ end
117
+ return values
118
+ end
119
+
120
+ unless context.token_mapping
121
+ error("The token context #{context.keyword} does not support transformation for unnamed params: #{full_name}")
122
+ return nil
123
+ end
124
+
125
+ token_mapping = context.token_mapping
126
+
127
+ # "unsupported"
128
+ if token_mapping.is_a?(String)
129
+ error("The token mapping #{token_mapping} does not support #{params.size} params: #{full_name}")
130
+ return nil
131
+ end
132
+
133
+ # ["unsupported", "unsupported", {}]
134
+ if token_mapping.is_a?(Array)
135
+ if params.size > token_mapping.size
136
+ error("The token mapping #{token_mapping} does not support #{params.size} params: #{full_name}")
137
+ return nil
138
+ end
139
+ token_mapping = token_mapping[params.size-1]
140
+ if token_mapping.is_a?(String)
141
+ error("The token mapping #{token_mapping} does not support #{params.size} params: #{full_name}")
142
+ return nil
143
+ end
144
+ end
145
+
146
+ # {}
147
+ token_mapping.each do |key, value|
148
+ values[key] = value
149
+ value.scan(/({\$\d(::\w+)*})/).each do |matches|
150
+ token = matches.first
151
+ parts = token[1..-2].split('::')
152
+ index = parts.first.gsub('$', '').to_i
153
+
154
+ if params.size < index
155
+ error("The index inside #{context.token_mapping} is out of bound: #{full_name}")
156
+ return nil
157
+ end
158
+
159
+ # apply settings cases
160
+ value = params[index]
161
+ if language_cases_enabled?
162
+ parts[1..-1].each do |case_key|
163
+ lcase = context.language.case_by_keyword(case_key)
164
+ unless lcase
165
+ error("Language case #{case_key} for context #{context.keyword} is not defined: #{full_name}")
166
+ return nil
167
+ end
168
+ value = lcase.apply(value)
169
+ end
170
+ end
171
+ values[key] = values[key].gsub(token, value)
172
+ end
173
+ end
174
+
175
+ values
176
+ end
177
+
178
+ def substitute(label, context, language, options = {})
179
+ object = self.class.token_object(context, key)
180
+
181
+ unless object
182
+ return error("Missing value for a token \"#{key}\" in \"#{label}\"", false)
183
+ end
184
+
185
+ if piped_params.empty?
186
+ return error("Piped params may not be empty for token \"#{key}\" in \"#{label}\"", false)
187
+ end
188
+
189
+ language_context = context_for_language(language)
190
+
191
+ unless language_context
192
+ return error("Unknown context for a token: #{full_name} in #{language.locale}", false)
193
+ end
194
+
195
+ piped_values = generate_value_map(piped_params, language_context)
196
+
197
+ unless piped_values
198
+ return error("Failed to generate value map for: #{full_name} in #{language.locale}", false)
199
+ end
200
+
201
+ rule = language_context.find_matching_rule(object)
202
+ return label unless rule
203
+
204
+ value = piped_values[rule.keyword]
205
+ if value.nil? and language_context.fallback_rule
206
+ value = piped_values[language_context.fallback_rule.keyword]
207
+ end
208
+
209
+ return label unless value
210
+
211
+ substitution_value = []
212
+ if displayed_in_translation?
213
+ substitution_value << token_value(Tml::Utils.hash_value(context, key), language, options)
214
+ substitution_value << ' '
215
+ else
216
+ value = value.gsub("##{short_name}#", token_value(Tml::Utils.hash_value(context, key), language, options))
217
+ end
218
+ substitution_value << value
219
+
220
+ label.gsub(full_name, substitution_value.join(''))
221
+ end
222
+
223
+ end
@@ -0,0 +1,67 @@
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::Translation < Tml::Base
34
+ belongs_to :translation_key, :language
35
+ attributes :locale, :label, :context, :precedence
36
+
37
+ def has_context_rules?
38
+ context and context.any?
39
+ end
40
+
41
+ # checks if the translation is valid for the given tokens
42
+ #{
43
+ # "count" => {"number":"one"},
44
+ # "user" => {"gender":"male"}
45
+ #}
46
+ def matches_rules?(token_values)
47
+ return true unless has_context_rules?
48
+
49
+ context.each do |token_name, rules|
50
+ token_object = Tml::Tokens::Data.token_object(token_values, token_name)
51
+ return false unless token_object
52
+
53
+ rules.each do |context_key, rule_key|
54
+ next if rule_key == 'other'
55
+
56
+ context = language.context_by_keyword(context_key)
57
+ return false unless context
58
+
59
+ rule = context.find_matching_rule(token_object)
60
+ return false if rule.nil? or rule.keyword != rule_key
61
+ end
62
+ end
63
+
64
+ true
65
+ end
66
+
67
+ end
@@ -0,0 +1,178 @@
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
+ require 'digest/md5'
34
+
35
+ class Tml::TranslationKey < Tml::Base
36
+ belongs_to :application, :language
37
+ attributes :id, :key, :label, :description, :locale, :level, :locked
38
+ has_many :translations # hashed by language
39
+
40
+ def initialize(attrs = {})
41
+ super
42
+
43
+ self.attributes[:key] ||= self.class.generate_key(label, description)
44
+ self.attributes[:locale] ||= Tml.session.block_options[:locale] || (application ? application.default_locale : Tml.config.default_locale)
45
+ self.attributes[:language] ||= application ? application.language(locale) : Tml.config.default_language
46
+ self.attributes[:translations] = {}
47
+
48
+ if hash_value(attrs, :translations)
49
+ hash_value(attrs, :translations).each do |locale, translations|
50
+ language = application.language(locale)
51
+
52
+ self.attributes[:translations][locale] ||= []
53
+
54
+ translations.each do |translation_hash|
55
+ translation = Tml::Translation.new(translation_hash.merge(:translation_key => self, :locale => language.locale))
56
+ self.attributes[:translations][locale] << translation
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def self.generate_key(label, desc = '')
63
+ "#{Digest::MD5.hexdigest("#{label};;;#{desc}")}~"[0..-2].to_s
64
+ end
65
+
66
+ def has_translations_for_language?(language)
67
+ translations and translations[language.locale] and translations[language.locale].any?
68
+ end
69
+
70
+ def set_translations(locale, translations)
71
+ translations.each do |translation|
72
+ translation.locale ||= locale
73
+ translation.translation_key = self
74
+ translation.language = self.application.language(translation.locale)
75
+ end
76
+ self.translations[locale] = translations
77
+ end
78
+
79
+ # switches to a new application
80
+ def set_application(app)
81
+ self.application = app
82
+ translations.values.each do |locale_translations|
83
+ locale_translations.each do |translation|
84
+ translation.translation_key = self
85
+ translation.language = self.application.language(translation.locale)
86
+ end
87
+ end
88
+ self
89
+ end
90
+
91
+ ###############################################################
92
+ ## Translation Methods
93
+ ###############################################################
94
+
95
+ def translations_for_language(language)
96
+ return [] unless self.translations
97
+ self.translations[language.locale] || []
98
+ end
99
+
100
+ def find_first_valid_translation(language, token_values)
101
+ translations = translations_for_language(language)
102
+
103
+ translations.sort! { |x,y| x.precedence <=> y.precedence }
104
+
105
+ translations.each do |translation|
106
+ return translation if translation.matches_rules?(token_values)
107
+ end
108
+
109
+ nil
110
+ end
111
+
112
+ def translate(language, token_values = {}, options = {})
113
+ if Tml.config.disabled? or language.locale == self.locale
114
+ return substitute_tokens(label, token_values, language, options.merge(:fallback => false))
115
+ end
116
+
117
+ translation = find_first_valid_translation(language, token_values)
118
+ decorator = Tml::Decorators::Base.decorator
119
+
120
+ if translation
121
+ translated_label = substitute_tokens(translation.label, token_values, translation.language, options)
122
+ return decorator.decorate(translated_label, translation.language, language, self, options)
123
+ end
124
+
125
+ translated_label = substitute_tokens(label, token_values, self.language, options)
126
+ decorator.decorate(translated_label, self.language, language, self, options)
127
+ end
128
+
129
+ ###############################################################
130
+ ## Token Substitution Methods
131
+ ###############################################################
132
+
133
+ # Returns an array of decoration tokens from the translation key
134
+ def decoration_tokens
135
+ @decoration_tokens ||= begin
136
+ dt = Tml::Tokenizers::Decoration.new(label)
137
+ dt.parse
138
+ dt.tokens
139
+ end
140
+ end
141
+
142
+ # Returns an array of data tokens from the translation key
143
+ def data_tokens
144
+ @data_tokens ||= begin
145
+ dt = Tml::Tokenizers::Data.new(label)
146
+ dt.tokens
147
+ end
148
+ end
149
+
150
+ def data_tokens_names_map
151
+ @data_tokens_names_map ||= begin
152
+ map = {}
153
+ data_tokens.each do |token|
154
+ map[token.name] = token
155
+ end
156
+ map
157
+ end
158
+ end
159
+
160
+ # if the translations engine is disabled
161
+ def self.substitute_tokens(label, token_values, language, options = {})
162
+ return label.to_s if options[:skip_substitution]
163
+ Tml::TranslationKey.new(:label => label.to_s).substitute_tokens(label.to_s, token_values, language, options)
164
+ end
165
+
166
+ def substitute_tokens(translated_label, token_values, language, options = {})
167
+ if Tml::Tokenizers::Data.required?(translated_label)
168
+ translated_label = Tml::Tokenizers::Data.new(translated_label, token_values, :allowed_tokens => data_tokens_names_map).substitute(language, options)
169
+ end
170
+
171
+ if Tml::Tokenizers::Decoration.required?(translated_label)
172
+ translated_label = Tml::Tokenizers::Decoration.new(translated_label, token_values, :allowed_tokens => decoration_tokens).substitute
173
+ end
174
+
175
+ translated_label
176
+ end
177
+
178
+ end