tml 4.3.1

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