i18n-inflector-3 3.0.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,546 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Author:: Paweł Wilk (mailto:pw@gnu.org)
5
+ # Copyright:: (c) 2011,2012,2013 by Paweł Wilk
6
+ # License:: This program is licensed under the terms of {file:docs/LGPL GNU Lesser General Public License} or {file:docs/COPYING Ruby License}.
7
+ #
8
+ # This file contains I18n::Inflector::Interpolate module,
9
+ # which is included in the API.
10
+
11
+ module I18n
12
+ module Inflector
13
+ # This module contains methods for interpolating
14
+ # inflection patterns.
15
+ module Interpolate
16
+ include I18n::Inflector::Config
17
+
18
+ # Interpolates inflection values in the given +string+
19
+ # using kinds given in +options+ and a matching tokens.
20
+ #
21
+ # @param [String] string the translation string
22
+ # containing patterns to interpolate
23
+ # @param [String,Symbol] locale the locale identifier
24
+ # @param [Hash] options the options
25
+ # ComplexPatternMalformed.new
26
+ # @raise {I18n::InvalidInflectionKind}
27
+ # @raise {I18n::InvalidInflectionOption}
28
+ # @raise {I18n::InvalidInflectionToken}
29
+ # @raise {I18n::MisplacedInflectionToken}
30
+ # @option options [Boolean] :inflector_excluded_defaults (false) local switch
31
+ # that overrides global setting (see: {I18n::Inflector::InflectionOptions#excluded_defaults})
32
+ # @option options [Boolean] :inflector_unknown_defaults (true) local switch
33
+ # that overrides global setting (see: {I18n::Inflector::InflectionOptions#unknown_defaults})
34
+ # @option options [Boolean] :inflector_raises (false) local switch
35
+ # that overrides global setting (see: {I18n::Inflector::InflectionOptions#raises})
36
+ # @option options [Boolean] :inflector_aliased_patterns (false) local switch
37
+ # that overrides global setting (see: {I18n::Inflector::InflectionOptions#aliased_patterns})
38
+ # @option options [Boolean] :inflector_cache_aware (false) local switch
39
+ # that overrides global setting (see: {I18n::Inflector::InflectionOptions#cache_aware})
40
+ # @option options [Boolean] :inflector_traverses (true) local switch
41
+ # that overrides global setting (see: {I18n::Inflector::InflectionOptions#traverses})
42
+ # @option options [Boolean] :inflector_interpolate_symbols (false) local switch
43
+ # that overrides global setting (see: {I18n::Inflector::InflectionOptions#interpolate_symbols})
44
+ # @return [String] the string with interpolated patterns
45
+ def interpolate(string, locale, options = {})
46
+ @inflector_opt_cache = nil
47
+
48
+ case string
49
+
50
+ when String
51
+
52
+ if locale.nil? || !inflected_locale?(locale)
53
+ string.gsub(PATTERN_REGEXP) { Escapes::PATTERN[::Regexp.last_match(1)] ? ::Regexp.last_match(0) : ::Regexp.last_match(1) }
54
+ elsif !string.include?(Markers::PATTERN)
55
+ string
56
+ else
57
+ interpolate_core(string, locale, options)
58
+ end
59
+
60
+ when Hash
61
+
62
+ if options[:inflector_traverses]
63
+ string.merge(string) { |_k, v| interpolate(v, locale, options) }
64
+ else
65
+ string
66
+ end
67
+
68
+ when Array
69
+
70
+ if options[:inflector_traverses]
71
+ string.map { |v| interpolate(v, locale, options) }
72
+ else
73
+ string
74
+ end
75
+
76
+ when Symbol
77
+
78
+ if options[:inflector_interpolate_symbols]
79
+ r = interpolate(string.to_s, locale, options)
80
+ begin
81
+ r.to_sym
82
+ rescue StandardError
83
+ :' '
84
+ end
85
+ else
86
+ string
87
+ end
88
+
89
+ else
90
+
91
+ string
92
+
93
+ end
94
+ end
95
+
96
+ # This method creates an inflection pattern
97
+ # by collecting information contained in a key-based
98
+ # inflection data.
99
+ #
100
+ # @param [Hash] key the given key
101
+ # @return [String] the inflection pattern
102
+ def key_to_pattern(key)
103
+ key = key.dup
104
+ pref = key.delete(:@prefix).to_s
105
+ suff = key.delete(:@suffix).to_s
106
+ kind = key.delete(:@kind).to_s
107
+ free = key.delete(:@free)
108
+ free = free.nil? ? '' : "#{Operators::Tokens::OR}#{free}"
109
+
110
+ "#{pref}#{Markers::PATTERN}#{kind}#{Markers::PATTERN_BEGIN}" <<
111
+ (key.map { |k, v| "#{k}#{Operators::Tokens::ASSIGN}#{v}" }
112
+ .join(Operators::Tokens::OR) + free + Markers::PATTERN_END + suff)
113
+ end
114
+
115
+ private
116
+
117
+ # @private
118
+ def interpolate_core(string, locale, options)
119
+ @inflector_opt_cache ||= options.except(*Reserved::KEYS)
120
+ passed_kinds = @inflector_opt_cache
121
+
122
+ raises = options[:inflector_raises]
123
+ aliased_patterns = options[:inflector_aliased_patterns]
124
+ unknown_defaults = options[:inflector_unknown_defaults]
125
+ excluded_defaults = options[:inflector_excluded_defaults]
126
+
127
+ idb = @idb[locale]
128
+ idb_strict = @idb_strict[locale]
129
+
130
+ string.gsub(PATTERN_REGEXP) do
131
+ pattern_fix = ::Regexp.last_match(1) # character sticked to the left side of a pattern
132
+ strict_kind = ::Regexp.last_match(2) # strict kind(s) if any
133
+ pattern_content = ::Regexp.last_match(3) # content of a pattern
134
+ multipattern = ::Regexp.last_match(4) # another pattern(s) sticked to the right side of a pattern
135
+ ext_pattern = ::Regexp.last_match(0) # the matching string
136
+
137
+ # initialize some defaults
138
+ ext_freetext = ''
139
+ found = nil
140
+ default_value = nil
141
+ tb_raised = nil
142
+ wildcard_value = nil
143
+
144
+ # leave escaped pattern as-is
145
+ unless pattern_fix.empty?
146
+ ext_pattern = ext_pattern[1..]
147
+ next ext_pattern if Escapes::PATTERN[pattern_fix]
148
+ end
149
+
150
+ # handle multiple patterns
151
+ unless multipattern.empty?
152
+ patterns = []
153
+ patterns << pattern_content
154
+ patterns += multipattern.scan(MULTI_REGEXP).flatten
155
+ next pattern_fix +
156
+ patterns.map do |content|
157
+ interpolate_core(Markers::PATTERN + strict_kind +
158
+ Markers::PATTERN_BEGIN + content +
159
+ Markers::PATTERN_END,
160
+ locale, options)
161
+ end.join
162
+ end
163
+
164
+ # set parsed kind if strict kind is given (named pattern is parsed)
165
+ if strict_kind.empty?
166
+ sym_parsed_kind = nil
167
+ strict_kind = nil
168
+ parsed_kind = nil
169
+ default_token = nil
170
+ subdb = idb
171
+ else
172
+ sym_parsed_kind = :"#{Markers::STRICT_KIND}#{strict_kind}"
173
+
174
+ if strict_kind.include?(Operators::Tokens::AND)
175
+
176
+ # Complex markers processing
177
+ begin
178
+ result = interpolate_complex(strict_kind,
179
+ pattern_content,
180
+ locale, options)
181
+ rescue I18n::InflectionPatternException => e
182
+ e.pattern = ext_pattern
183
+ raise
184
+ end
185
+ found = pattern_content = '' # disable further processing
186
+
187
+ else
188
+
189
+ # Strict kinds preparing
190
+ subdb = idb_strict
191
+
192
+ # validate strict kind and set needed variables
193
+ if Reserved::Kinds.invalid?(strict_kind, :PATTERN) ||
194
+ !idb_strict.has_kind?(strict_kind.to_sym)
195
+ raise I18n::InvalidInflectionKind.new(locale, ext_pattern, sym_parsed_kind) if raises
196
+
197
+ # Take a free text for invalid kind and return it
198
+ next '' + pattern_fix + pattern_content.scan(TOKENS_REGEXP).reverse
199
+ .select { |t, _v, f| t.nil? && !f.nil? }
200
+ .map { |_t, _v, f| f.to_s }
201
+ .first.to_s
202
+ else
203
+ strict_kind = strict_kind.to_sym
204
+ parsed_kind = strict_kind
205
+ # inject default token
206
+ default_token = subdb.get_default_token(parsed_kind)
207
+ end
208
+
209
+ end
210
+ end
211
+
212
+ # process pattern content's
213
+ pattern_content.scan(TOKENS_REGEXP) do
214
+ ext_token = ::Regexp.last_match(1).to_s # token(s)
215
+ ext_value = ::Regexp.last_match(2).to_s # value of token(s)
216
+ ext_freetext = ::Regexp.last_match(3).to_s # freetext if any
217
+ ext_tokens = nil
218
+ tokens = Hash.new(false)
219
+ negatives = Hash.new(false)
220
+ kind = nil
221
+ passed_token = nil
222
+ result = nil
223
+
224
+ # TOKEN GROUP PROCESSING
225
+
226
+ # token not found?
227
+ if ext_token.empty?
228
+ # free text not found too? that should never happend.
229
+ raise I18n::InvalidInflectionToken.new(locale, ext_pattern, ext_token) if ext_freetext.empty? && raises
230
+
231
+ next
232
+ end
233
+
234
+ # unroll wildcard token
235
+ if ext_token == Operators::Tokens::WILDCARD
236
+ if parsed_kind.nil?
237
+ # wildcard for a regular kind that we do not know yet
238
+ wildcard_value = ext_value
239
+ else
240
+ # wildcard for a known strict or regular kind
241
+ ext_tokens = subdb.each_true_token(parsed_kind).each_key.map(&:to_s)
242
+ end
243
+ end
244
+
245
+ # split groupped tokens if comma is present and put into fast list
246
+ ext_tokens = ext_token.split(Operators::Token::OR) if ext_tokens.nil?
247
+
248
+ # for each token from group
249
+ ext_tokens.each do |t|
250
+ # token name corrupted
251
+ if t.to_s.empty?
252
+ raise I18n::InvalidInflectionToken.new(locale, ext_pattern, t) if raises
253
+
254
+ next
255
+ end
256
+
257
+ # mark negative-matching token and put it on the negatives fast list
258
+ if t[0..0] == Operators::Token::NOT
259
+ t = t[1..]
260
+ negative = true
261
+ else
262
+ negative = false
263
+ end
264
+
265
+ # is token name corrupted?
266
+ if Reserved::Tokens.invalid?(t, :PATTERN)
267
+ raise I18n::InvalidInflectionToken.new(locale, ext_pattern, t) if raises
268
+
269
+ next
270
+ end
271
+
272
+ t = t.to_sym
273
+ t = subdb.get_true_token(t, strict_kind) if aliased_patterns
274
+ negatives[t] = true if negative
275
+
276
+ # get a kind for that token
277
+ kind = subdb.get_kind(t, strict_kind)
278
+
279
+ if kind.nil?
280
+ if raises
281
+ # regular pattern and token that has a bad kind
282
+ raise I18n::InvalidInflectionToken.new(locale, ext_pattern, t, sym_parsed_kind) if strict_kind.nil?
283
+
284
+ # named pattern (kind validated before, so the only error is misplaced token)
285
+ raise I18n::MisplacedInflectionToken.new(locale, ext_pattern, t, sym_parsed_kind)
286
+
287
+ end
288
+ next
289
+ end
290
+
291
+ # set processed kind after matching first token in a pattern
292
+ if parsed_kind.nil?
293
+ parsed_kind = kind
294
+ sym_parsed_kind = kind.to_sym
295
+ default_token = subdb.get_default_token(parsed_kind)
296
+ elsif parsed_kind != kind
297
+ # tokens of different kinds in one regular (not named) pattern are prohibited
298
+ raise I18n::MisplacedInflectionToken.new(locale, ext_pattern, t, sym_parsed_kind) if raises
299
+
300
+ next
301
+ end
302
+
303
+ # use that token
304
+ unless negatives[t]
305
+ tokens[t] = true
306
+ default_value = ext_value if t == default_token
307
+ end
308
+ end
309
+
310
+ # self-explanatory
311
+ if tokens.empty? && negatives.empty? && raises
312
+ raise I18n::InvalidInflectionToken.new(locale, ext_pattern, ext_token)
313
+ end
314
+
315
+ # INFLECTION OPTION PROCESSING
316
+
317
+ # set up expected_kind depending on type of a kind
318
+ if strict_kind.nil?
319
+ expected_kind = parsed_kind
320
+ else
321
+ expected_kind = sym_parsed_kind
322
+ expected_kind = parsed_kind unless passed_kinds.key?(expected_kind)
323
+ end
324
+
325
+ # get passed token from options or from a default token
326
+ if passed_kinds.key?(expected_kind)
327
+
328
+ passed_token = passed_kinds[expected_kind]
329
+
330
+ if passed_token.is_a?(Method)
331
+
332
+ passed_token = passed_token.call { next expected_kind, locale }
333
+ passed_kinds[expected_kind] = passed_token # cache the result
334
+
335
+ elsif passed_token.is_a?(Proc)
336
+
337
+ passed_token = passed_token.call(expected_kind, locale)
338
+ passed_kinds[expected_kind] = passed_token # cache the result
339
+
340
+ end
341
+
342
+ orig_passed_token = passed_token
343
+
344
+ # validate passed token's name
345
+ if Reserved::Tokens.invalid?(passed_token, :OPTION)
346
+ raise I18n::InvalidInflectionOption.new(locale, ext_pattern, orig_passed_token) if raises
347
+
348
+ passed_token = default_token if unknown_defaults
349
+ end
350
+
351
+ else
352
+ # current inflection option wasn't found
353
+ # but delay this exception because we might use
354
+ # the default token if found somewhere in a pattern
355
+ if raises
356
+ tb_raised = InflectionOptionNotFound.new(locale, ext_pattern, ext_token,
357
+ expected_kind, orig_passed_token)
358
+ end
359
+ passed_token = default_token
360
+ nil
361
+ end
362
+
363
+ # explicit default
364
+ passed_token = default_token if passed_token == Keys::DEFAULT_TOKEN
365
+
366
+ # resolve token from options and check if it's known
367
+ unless passed_token.nil?
368
+ passed_token = subdb.get_true_token(passed_token.to_s.to_sym, parsed_kind)
369
+ passed_token = default_token if passed_token.nil? && unknown_defaults
370
+ end
371
+
372
+ # handle memorized wildcard waiting for a kind
373
+ if !wildcard_value.nil? && !parsed_kind.nil?
374
+ found = passed_token
375
+ result = wildcard_value
376
+ wildcard_value = nil
377
+ break
378
+ end
379
+
380
+ # throw the value if the given option matches one of the tokens from group
381
+ # or negatively matches one of the negated tokens
382
+ case negatives.count
383
+ when 0 then next unless tokens[passed_token]
384
+ when 1 then next if negatives[passed_token]
385
+ end
386
+
387
+ # skip further evaluation of the pattern
388
+ # since the right token has been found
389
+ found = passed_token
390
+ result = ext_value
391
+ break
392
+ end
393
+
394
+ # RESULTS PROCESSING
395
+
396
+ # handle memorized wildcard token
397
+ # when there was no way to deduce a token or a kind
398
+ # it's just for regular kinds
399
+ unless wildcard_value.nil? || passed_kinds.nil?
400
+ parsed_kind = nil
401
+ found = nil
402
+ passed_kinds.each do |k, ot|
403
+ t = subdb.get_true_token(ot, k)
404
+ if Reserved::Tokens.invalid?(t, :OPTION)
405
+ raise I18n::InvalidInflectionOption.new(locale, ext_pattern, ot) if raises
406
+
407
+ next
408
+ end
409
+ next if t.nil?
410
+
411
+ found = t
412
+ parsed_kind = k
413
+ break
414
+ end
415
+ if parsed_kind.nil? || found.nil?
416
+ found = nil
417
+ parsed_kind = nil
418
+ else
419
+ result = wildcard_value
420
+ wildcard_value = nil
421
+ end
422
+ end
423
+
424
+ # if there was no hit for that option
425
+ if result.nil?
426
+ raise tb_raised unless tb_raised.nil?
427
+
428
+ # try to extract default token's value
429
+
430
+ # if there is excluded_defaults switch turned on
431
+ # and a correct token was found in an inflection option but
432
+ # has not been found in a pattern then interpolate
433
+ # the pattern with a value picked for the default
434
+ # token for that kind if a default token was present
435
+ # in a pattern
436
+ if excluded_defaults && !parsed_kind.nil?
437
+ expected_kind = sym_parsed_kind
438
+ expected_kind = parsed_kind unless passed_kinds.key?(expected_kind)
439
+ t = passed_kinds[expected_kind]
440
+ if t.is_a?(Method)
441
+ t = t.call { next expected_kind, locale }
442
+ passed_kinds[expected_kind] = t # cache the result
443
+ elsif t.is_a?(Proc)
444
+ t = t.call(expected_kind, locale)
445
+ passed_kinds[expected_kind] = t # cache the result
446
+ end
447
+ if Reserved::Tokens.invalid?(t, :OPTION) && raises
448
+ raise I18n::InvalidInflectionOption.new(locale, ext_pattern, t)
449
+ end
450
+
451
+ result = subdb.has_token?(t, parsed_kind) ? default_value : nil
452
+ end
453
+
454
+ # interpolate loud tokens
455
+ elsif result == Markers::LOUD_VALUE
456
+
457
+ result = subdb.get_description(found, parsed_kind)
458
+
459
+ # interpolate escaped loud tokens or other escaped strings
460
+ elsif result[0..0] == Escapes::ESCAPE
461
+
462
+ result.sub!(Escapes::ESCAPE_R, '\1')
463
+
464
+ end
465
+
466
+ "#{pattern_fix}#{result || ext_freetext}"
467
+ end
468
+ end
469
+
470
+ # This is a helper that reduces a complex inflection pattern
471
+ # by producing equivalent of regular patterns of it and
472
+ # by interpolating them using {#interpolate} method.
473
+ #
474
+ # @param [String] complex_kind the complex kind (many kinds separated
475
+ # by the {Operators::Tokens::AND})
476
+ # @param [String] content the content of the processed pattern
477
+ # @param [Symbol] locale the locale to use
478
+ # @param [Hash] options the options
479
+ # @return [String] the interpolated pattern
480
+ def interpolate_complex(complex_kind, content, locale, options)
481
+ result = nil
482
+ free_text = ''
483
+ kinds = complex_kind.split(Operators::Tokens::AND)
484
+ .reject { |k| k.nil? || k.empty? }.each
485
+
486
+ begin
487
+ content.scan(TOKENS_REGEXP) do |tokens, value, free|
488
+ if tokens.nil?
489
+ raise IndexError if free.empty?
490
+
491
+ free_text = free
492
+ next
493
+ end
494
+
495
+ kinds.rewind
496
+
497
+ # process each token from set
498
+ results = tokens.split(Operators::Tokens::AND).map do |token|
499
+ raise IndexError if token.empty?
500
+
501
+ if value == Markers::LOUD_VALUE
502
+ r = interpolate_core(Markers::PATTERN.to_s +
503
+ kinds.next.to_s +
504
+ Markers::PATTERN_BEGIN +
505
+ token.to_s +
506
+ Operators::Tokens::ASSIGN +
507
+ value.to_s +
508
+ Operators::Tokens::OR +
509
+ Markers::PATTERN +
510
+ Markers::PATTERN_END,
511
+ locale, options)
512
+ break if r == Markers::PATTERN # using this marker only as a helper to indicate empty result!
513
+ else
514
+ r = interpolate_core(Markers::PATTERN.to_s +
515
+ kinds.next.to_s +
516
+ Markers::PATTERN_BEGIN +
517
+ token.to_s +
518
+ Operators::Tokens::ASSIGN +
519
+ value.to_s +
520
+ Markers::PATTERN_END,
521
+ locale, options)
522
+ break if r != value # stop with this set, because something is not matching
523
+ end
524
+ r
525
+ end
526
+
527
+ # some token didn't matched, try another set
528
+ next if results.nil?
529
+
530
+ # generate result for set or raise error
531
+ raise IndexError unless results.size == kinds.count
532
+
533
+ result = (value == Markers::LOUD_VALUE) ? results.join(' ') : value
534
+ break
535
+ end
536
+ rescue IndexError, StopIteration
537
+ raise I18n::ComplexPatternMalformed.new(locale, content, nil, complex_kind) if options[:inflector_raises]
538
+
539
+ result = nil
540
+ end
541
+
542
+ result || free_text
543
+ end
544
+ end
545
+ end
546
+ end