i18n-inflector 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,404 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Author:: Paweł Wilk (mailto:pw@gnu.org)
4
+ # Copyright:: (c) 2011 by Paweł Wilk
5
+ # License:: This program is licensed under the terms of {file:LGPL GNU Lesser General Public License} or {file:COPYING Ruby License}.
6
+ #
7
+ # This file contains I18n::Inflector::Interpolate module,
8
+ # which is included in the API.
9
+
10
+ module I18n
11
+ module Inflector
12
+
13
+ # This module contains methods for interpolating
14
+ # inflection patterns.
15
+ module Interpolate
16
+
17
+ include I18n::Inflector::Config
18
+
19
+ # Interpolates inflection values in the given +string+
20
+ # using kinds given in +options+ and a matching tokens.
21
+ #
22
+ # @param [String] string the translation string
23
+ # containing patterns to interpolate
24
+ # @param [String,Symbol] locale the locale identifier
25
+ # @param [Hash] options the options
26
+ # ComplexPatternMalformed.new
27
+ # @raise {I18n::InvalidInflectionKind}
28
+ # @raise {I18n::InvalidInflectionOption}
29
+ # @raise {I18n::InvalidInflectionToken}
30
+ # @raise {I18n::MisplacedInflectionToken}
31
+ # @option options [Boolean] :inflector_excluded_defaults (false) local switch
32
+ # that overrides global setting (see: {I18n::Inflector::InflectionOptions#excluded_defaults})
33
+ # @option options [Boolean] :inflector_unknown_defaults (true) local switch
34
+ # that overrides global setting (see: {I18n::Inflector::InflectionOptions#unknown_defaults})
35
+ # @option options [Boolean] :inflector_raises (false) local switch
36
+ # that overrides global setting (see: {I18n::Inflector::InflectionOptions#raises})
37
+ # @option options [Boolean] :inflector_aliased_patterns (false) local switch
38
+ # that overrides global setting (see: {I18n::Inflector::InflectionOptions#aliased_patterns})
39
+ # @option options [Boolean] :inflector_cache_aware (false) local switch
40
+ # that overrides global setting (see: {I18n::Inflector::InflectionOptions#cache_aware})
41
+ # @return [String] the string with interpolated patterns
42
+ def interpolate(string, locale, options = {})
43
+ interpolate_core(string, locale, options)
44
+ end
45
+
46
+ # This method creates an inflection pattern
47
+ # by collecting information contained in a key-based
48
+ # inflection data.
49
+ #
50
+ # @param [Hash] key the given key
51
+ # @return [String] the inflection pattern
52
+ def key_to_pattern(key)
53
+ key = key.dup
54
+ pref = key.delete(:@prefix).to_s
55
+ suff = key.delete(:@suffix).to_s
56
+ kind = key.delete(:@kind).to_s
57
+ free = key.delete(:@free)
58
+ free = free.nil? ? "" : (Operators::Tokens::OR + free.to_s)
59
+
60
+ pref + Markers::PATTERN + kind + Markers::PATTERN_BEGIN +
61
+ key.map { |k,v| k.to_s + Operators::Tokens::ASSIGN + v.to_s }.
62
+ join(Operators::Tokens::OR) + free + Markers::PATTERN_END + suff
63
+ end
64
+
65
+ private
66
+
67
+ # @private
68
+ def interpolate_core(string, locale, options)
69
+ passed_kinds = options.except(*Reserved::KEYS)
70
+ raises = options[:inflector_raises]
71
+ aliased_patterns = options[:inflector_aliased_patterns]
72
+ unknown_defaults = options[:inflector_unknown_defaults]
73
+ excluded_defaults = options[:inflector_excluded_defaults]
74
+
75
+ idb = @idb[locale]
76
+ idb_strict = @idb_strict[locale]
77
+
78
+ string.gsub(PATTERN_REGEXP) do
79
+ pattern_fix = $1
80
+ strict_kind = $2
81
+ pattern_content = $3
82
+ multipattern = $4
83
+ ext_pattern = $&
84
+
85
+ # initialize some defaults
86
+ ext_freetext = ''
87
+ found = nil
88
+ default_value = nil
89
+ tb_raised = nil
90
+
91
+ # leave escaped pattern as-is
92
+ unless pattern_fix.empty?
93
+ ext_pattern = ext_pattern[1..-1]
94
+ next ext_pattern if Escapes::PATTERN[pattern_fix]
95
+ end
96
+
97
+ # handle multiple patterns
98
+ unless multipattern.empty?
99
+ patterns = []
100
+ patterns << pattern_content
101
+ patterns += multipattern.scan(MULTI_REGEXP).flatten
102
+ next pattern_fix + patterns.map do |content|
103
+ interpolate_core(Markers::PATTERN + strict_kind +
104
+ Markers::PATTERN_BEGIN + content +
105
+ Markers::PATTERN_END, locale, options)
106
+ end.join
107
+ end
108
+
109
+ # set parsed kind if strict kind is given (named pattern is parsed)
110
+ if strict_kind.empty?
111
+ sym_parsed_kind = nil
112
+ strict_kind = nil
113
+ parsed_kind = nil
114
+ default_token = nil
115
+ subdb = idb
116
+ else
117
+ sym_parsed_kind = (Markers::PATTERN + strict_kind).to_sym
118
+
119
+ if strict_kind.include?(Operators::Tokens::AND)
120
+
121
+ # Complex markers processing
122
+ begin
123
+ result = interpolate_complex(strict_kind,
124
+ pattern_content,
125
+ locale, options)
126
+ rescue I18n::InflectionPatternException => e
127
+ e.pattern = ext_pattern
128
+ raise
129
+ end
130
+ found = pattern_content = "" # disable further processing
131
+
132
+ else
133
+
134
+ # Strict kinds preparing
135
+ subdb = idb_strict
136
+
137
+ # validate strict kind and set needed variables
138
+ if (Reserved::Kinds.invalid?(strict_kind, :PATTERN) ||
139
+ !idb_strict.has_kind?(strict_kind.to_sym))
140
+ raise I18n::InvalidInflectionKind.new(locale, ext_pattern, sym_parsed_kind) if raises
141
+ # Take a free text for invalid kind and return it
142
+ next pattern_fix + pattern_content.scan(TOKENS_REGEXP).reverse.
143
+ select { |t,v,f| t.nil? && !f.nil? }.
144
+ map { |t,v,f| f.to_s }.
145
+ first.to_s
146
+ else
147
+ strict_kind = strict_kind.to_sym
148
+ parsed_kind = strict_kind
149
+ # inject default token
150
+ default_token = subdb.get_default_token(parsed_kind)
151
+ end
152
+
153
+ end
154
+ end
155
+
156
+ # process pattern content's
157
+ pattern_content.scan(TOKENS_REGEXP) do
158
+ ext_token = $1.to_s
159
+ ext_value = $2.to_s
160
+ ext_freetext = $3.to_s
161
+ tokens = Hash.new(false)
162
+ negatives = Hash.new(false)
163
+ kind = nil
164
+ passed_token = nil
165
+ result = nil
166
+
167
+ # TOKEN GROUP PROCESSING
168
+
169
+ # token not found?
170
+ if ext_token.empty?
171
+ # free text not found too? that should never happend.
172
+ if ext_freetext.empty?
173
+ raise I18n::InvalidInflectionToken.new(locale, ext_pattern, ext_token) if raises
174
+ end
175
+ next
176
+ end
177
+
178
+ # split tokens from group if comma is present and put into fast list
179
+ ext_token.split(Operators::Token::OR).each do |t|
180
+ # token name corrupted
181
+ if t.to_s.empty?
182
+ raise I18n::InvalidInflectionToken.new(locale, ext_pattern, t) if raises
183
+ next
184
+ end
185
+
186
+ # mark negative-matching token and put it on the negatives fast list
187
+ if t[0..0] == Operators::Token::NOT
188
+ t = t[1..-1]
189
+ negative = true
190
+ else
191
+ negative = false
192
+ end
193
+
194
+ # is token name corrupted?
195
+ if Reserved::Tokens.invalid?(t, :PATTERN)
196
+ raise I18n::InvalidInflectionToken.new(locale, ext_pattern, t) if raises
197
+ next
198
+ end
199
+
200
+ t = t.to_sym
201
+ t = subdb.get_true_token(t, strict_kind) if aliased_patterns
202
+ negatives[t] = true if negative
203
+
204
+ # get a kind for that token
205
+ kind = subdb.get_kind(t, strict_kind)
206
+
207
+ if kind.nil?
208
+ if raises
209
+ # regular pattern and token that has a bad kind
210
+ if strict_kind.nil?
211
+ raise I18n::InvalidInflectionToken.new(locale, ext_pattern, t, sym_parsed_kind)
212
+ else
213
+ # named pattern (kind validated before, so the only error is misplaced token)
214
+ raise I18n::MisplacedInflectionToken.new(locale, ext_pattern, t, sym_parsed_kind)
215
+ end
216
+ end
217
+ next
218
+ end
219
+
220
+ # set processed kind after matching first token in a pattern
221
+ if parsed_kind.nil?
222
+ parsed_kind = kind
223
+ sym_parsed_kind = kind.to_sym
224
+ default_token = subdb.get_default_token(parsed_kind)
225
+ elsif parsed_kind != kind
226
+ # tokens of different kinds in one regular (not named) pattern are prohibited
227
+ raise I18n::MisplacedInflectionToken.new(locale, ext_pattern, t, sym_parsed_kind) if raises
228
+ next
229
+ end
230
+
231
+ # use that token
232
+ unless negatives[t]
233
+ tokens[t] = true
234
+ default_value = ext_value if t == default_token
235
+ end
236
+ end
237
+
238
+ # self-explanatory
239
+ if (tokens.empty? && negatives.empty?)
240
+ raise I18n::InvalidInflectionToken.new(locale, ext_pattern, ext_token) if raises
241
+ end
242
+
243
+ # INFLECTION OPTION PROCESSING
244
+
245
+ # set up expected_kind depending on type of a kind
246
+ if strict_kind.nil?
247
+ expected_kind = parsed_kind
248
+ else
249
+ expected_kind = sym_parsed_kind
250
+ expected_kind = parsed_kind unless passed_kinds.has_key?(expected_kind)
251
+ end
252
+
253
+ # get passed token from options or from a default token
254
+ if passed_kinds.has_key?(expected_kind)
255
+ passed_token = passed_kinds[expected_kind]
256
+ orig_passed_token = passed_token
257
+ # validate passed token's name
258
+ if Reserved::Tokens.invalid?(passed_token, :OPTION)
259
+ raise I18n::InvalidInflectionOption.new(locale, ext_pattern, orig_passed_token) if raises
260
+ passed_token = default_token if unknown_defaults
261
+ end
262
+ else
263
+ # current inflection option wasn't found
264
+ # but delay this exception because we might use
265
+ # the default token if found somewhere in a pattern
266
+ tb_raised = InflectionOptionNotFound.new(locale, ext_pattern, ext_token,
267
+ expected_kind, orig_passed_token) if raises
268
+ passed_token = default_token
269
+ orig_passed_token = nil
270
+ end
271
+
272
+ # explicit default
273
+ passed_token = default_token if passed_token == Keys::DEFAULT_TOKEN
274
+
275
+ # resolve token from options and check if it's known
276
+ unless passed_token.nil?
277
+ passed_token = subdb.get_true_token(passed_token.to_sym, parsed_kind)
278
+ passed_token = default_token if passed_token.nil? && unknown_defaults
279
+ end
280
+
281
+ # throw the value if the given option matches one of the tokens from group
282
+ # or negatively matches one of the negated tokens
283
+ case negatives.count
284
+ when 0 then next unless tokens[passed_token]
285
+ when 1 then next if negatives[passed_token]
286
+ end
287
+
288
+ # skip further evaluation of the pattern
289
+ # since the right token has been found
290
+ found = passed_token
291
+ result = ext_value
292
+ break
293
+
294
+ end # single token (or a group) processing
295
+
296
+ # RESULTS PROCESSING
297
+
298
+ # if there was no hit for that option
299
+ if result.nil?
300
+ raise tb_raised unless tb_raised.nil?
301
+
302
+ # try to extract default token's value
303
+
304
+ # if there is excluded_defaults switch turned on
305
+ # and a correct token was found in an inflection option but
306
+ # has not been found in a pattern then interpolate
307
+ # the pattern with a value picked for the default
308
+ # token for that kind if a default token was present
309
+ # in a pattern
310
+ result = (excluded_defaults &&
311
+ !parsed_kind.nil? &&
312
+ subdb.has_token?(passed_kinds[parsed_kind], parsed_kind)) ?
313
+ default_value : nil
314
+
315
+ # interpolate loud tokens
316
+ elsif result == Markers::LOUD_VALUE
317
+
318
+ result = subdb.get_description(found, parsed_kind)
319
+
320
+ # interpolate escaped loud tokens or other escaped strings
321
+ elsif result[0..0] == Escapes::ESCAPE
322
+
323
+ result.sub!(Escapes::ESCAPE_R, '\1')
324
+
325
+ end
326
+
327
+ pattern_fix + (result || ext_freetext)
328
+
329
+ end # single pattern processing
330
+
331
+ end # def interpolate
332
+
333
+ # This is a helper that reduces a complex inflection pattern
334
+ # by producing equivalent of regular patterns of it and
335
+ # by interpolating them using {#interpolate} method.
336
+ #
337
+ # @param [String] complex_kind the complex kind (many kinds separated
338
+ # by the {Operators::Tokens::AND})
339
+ # @param [String] content the content of the processed pattern
340
+ # @param [Symbol] locale the locale to use
341
+ # @param [Hash] options the options
342
+ # @return [String] the interpolated pattern
343
+ def interpolate_complex(complex_kind, content, locale, options)
344
+ result = nil
345
+ free_text = ""
346
+ kinds = complex_kind.split(Operators::Tokens::AND).
347
+ reject{ |k| k.nil? || k.empty? }.each
348
+
349
+ begin
350
+
351
+ content.scan(TOKENS_REGEXP) do |tokens, value, free|
352
+ if tokens.nil?
353
+ raise IndexError.new if free.empty?
354
+ free_text = free
355
+ next
356
+ end
357
+
358
+ kinds.rewind
359
+
360
+ # process each token from set
361
+ results = tokens.split(Operators::Tokens::AND).map do |token|
362
+ raise IndexError.new if token.empty?
363
+ if value == Markers::LOUD_VALUE
364
+ r = interpolate_core(Markers::PATTERN + kinds.next.to_s + Markers::PATTERN_BEGIN + token.to_s +
365
+ Operators::Tokens::ASSIGN + value.to_s + Operators::Tokens::OR +
366
+ Markers::PATTERN + Markers::PATTERN_END, locale, options)
367
+ break if r == Markers::PATTERN
368
+ else
369
+ r = interpolate_core(Markers::PATTERN + kinds.next.to_s + Markers::PATTERN_BEGIN + token.to_s +
370
+ Operators::Tokens::ASSIGN + value.to_s + Markers::PATTERN_END, locale, options)
371
+ break if r != value # stop with this set, because something is not matching
372
+ end
373
+ r
374
+ end
375
+
376
+ # some token didn't matched, try another set
377
+ next if results.nil?
378
+
379
+ # generate result for set or raise error
380
+ if results.size == kinds.count
381
+ result = value == Markers::LOUD_VALUE ? results.join(' ') : value
382
+ break
383
+ else
384
+ raise IndexError.new
385
+ end
386
+
387
+ end # scan tokens
388
+
389
+ rescue IndexError, StopIteration
390
+
391
+ if options[:inflector_raises]
392
+ raise I18n::ComplexPatternMalformed.new(locale, content, nil, complex_kind)
393
+ end
394
+ result = nil
395
+
396
+ end
397
+
398
+ result || free_text
399
+
400
+ end # def interpolate_complex
401
+
402
+ end # module Interpolate
403
+ end # module Inflector
404
+ end # module I18n
@@ -12,7 +12,7 @@ module I18n
12
12
  if RUBY_VERSION.gsub(/\D/,'')[0..1].to_i < 19
13
13
  require 'enumerator' rescue nil
14
14
 
15
- class LazyHashEnumerator < Object.const_defined?(:Enumerator) ? Enumerator : Enumerable::Enumerator
15
+ class LazyEnumerator < Object.const_defined?(:Enumerator) ? Enumerator : Enumerable::Enumerator
16
16
 
17
17
  # This class allows to initialize the Enumerator with a block
18
18
  class Yielder
@@ -46,24 +46,62 @@ module I18n
46
46
  alias_method :with_object, :each_with_object
47
47
  end
48
48
 
49
- end # class LazyHashEnumerator for ruby18
49
+ end # class LazyEnumerator for ruby18
50
50
 
51
51
  else # if RUBY_VERSION >= 1.9.0
52
52
 
53
- class LazyHashEnumerator < Enumerator
53
+ class LazyEnumerator < Enumerator
54
+ end
55
+
56
+ end
57
+
58
+ # This class implements simple enumerators for arrays
59
+ # that allow to do lazy operations on them.
60
+ class LazyArrayEnumerator < LazyEnumerator
61
+
62
+ # Mapping enumerator
63
+ # @return [I18n::Inflector::LazyEnumerator] the enumerator
64
+ def map(&block)
65
+ LazyArrayEnumerator.new do |yielder|
66
+ self.each do |v|
67
+ yielder.yield(block.call(v))
68
+ end
69
+ end
70
+ end
71
+
72
+ # Array selecting enumerator
73
+ # @return [I18n::Inflector::LazyHashEnumerator] the enumerator
74
+ def select(&block)
75
+ LazyArrayEnumerator.new do |yielder|
76
+ self.each do |v|
77
+ yielder.yield(v) if block.call(v)
78
+ end
79
+ end
80
+ end
81
+
82
+ # Array rejecting enumerator
83
+ # @return [I18n::Inflector::LazyHashEnumerator] the enumerator
84
+ def reject(&block)
85
+ LazyArrayEnumerator.new do |yielder|
86
+ self.each do |v|
87
+ yielder.yield(v) unless block.call(v)
88
+ end
89
+ end
54
90
  end
55
91
 
56
92
  end
57
93
 
58
94
  # This class implements simple enumerators for hashes
59
95
  # that allow to do lazy operations on them.
60
- class LazyHashEnumerator
96
+ class LazyHashEnumerator < LazyEnumerator
61
97
 
62
98
  # Creates a Hash kind of object by collecting all
63
99
  # data from enumerated collection.
64
100
  # @return [Hash] the resulting hash
65
101
  def to_h
66
- Hash[self.to_a]
102
+ h = Hash.new
103
+ self.each{|k,v| h[k]=v }
104
+ h
67
105
  end
68
106
 
69
107
  # Hash mapping enumerator
@@ -87,13 +125,21 @@ module I18n
87
125
  end
88
126
 
89
127
  # This method converts resulting keys
90
- # to array.
128
+ # to an array.
91
129
  def keys
92
130
  ary = []
93
131
  self.each{ |k,v| ary << k }
94
132
  return ary
95
133
  end
96
134
 
135
+ # This method converts resulting values
136
+ # to an array.
137
+ def values
138
+ ary = []
139
+ self.each{ |k,v| ary << v }
140
+ return ary
141
+ end
142
+
97
143
  # Hash selecting enumerator
98
144
  # @return [I18n::Inflector::LazyHashEnumerator] the enumerator
99
145
  def select(&block)