i18n-inflector 2.1.0 → 2.2.0

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.
@@ -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)