i18n 1.6.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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +125 -0
  4. data/lib/i18n.rb +398 -0
  5. data/lib/i18n/backend.rb +21 -0
  6. data/lib/i18n/backend/base.rb +284 -0
  7. data/lib/i18n/backend/cache.rb +113 -0
  8. data/lib/i18n/backend/cache_file.rb +36 -0
  9. data/lib/i18n/backend/cascade.rb +56 -0
  10. data/lib/i18n/backend/chain.rb +127 -0
  11. data/lib/i18n/backend/fallbacks.rb +84 -0
  12. data/lib/i18n/backend/flatten.rb +115 -0
  13. data/lib/i18n/backend/gettext.rb +85 -0
  14. data/lib/i18n/backend/interpolation_compiler.rb +123 -0
  15. data/lib/i18n/backend/key_value.rb +206 -0
  16. data/lib/i18n/backend/memoize.rb +54 -0
  17. data/lib/i18n/backend/metadata.rb +71 -0
  18. data/lib/i18n/backend/pluralization.rb +55 -0
  19. data/lib/i18n/backend/simple.rb +111 -0
  20. data/lib/i18n/backend/transliterator.rb +108 -0
  21. data/lib/i18n/config.rb +165 -0
  22. data/lib/i18n/core_ext/hash.rb +47 -0
  23. data/lib/i18n/exceptions.rb +111 -0
  24. data/lib/i18n/gettext.rb +28 -0
  25. data/lib/i18n/gettext/helpers.rb +75 -0
  26. data/lib/i18n/gettext/po_parser.rb +329 -0
  27. data/lib/i18n/interpolate/ruby.rb +39 -0
  28. data/lib/i18n/locale.rb +8 -0
  29. data/lib/i18n/locale/fallbacks.rb +96 -0
  30. data/lib/i18n/locale/tag.rb +28 -0
  31. data/lib/i18n/locale/tag/parents.rb +22 -0
  32. data/lib/i18n/locale/tag/rfc4646.rb +74 -0
  33. data/lib/i18n/locale/tag/simple.rb +39 -0
  34. data/lib/i18n/middleware.rb +17 -0
  35. data/lib/i18n/tests.rb +14 -0
  36. data/lib/i18n/tests/basics.rb +60 -0
  37. data/lib/i18n/tests/defaults.rb +52 -0
  38. data/lib/i18n/tests/interpolation.rb +159 -0
  39. data/lib/i18n/tests/link.rb +56 -0
  40. data/lib/i18n/tests/localization.rb +19 -0
  41. data/lib/i18n/tests/localization/date.rb +117 -0
  42. data/lib/i18n/tests/localization/date_time.rb +103 -0
  43. data/lib/i18n/tests/localization/procs.rb +116 -0
  44. data/lib/i18n/tests/localization/time.rb +103 -0
  45. data/lib/i18n/tests/lookup.rb +81 -0
  46. data/lib/i18n/tests/pluralization.rb +35 -0
  47. data/lib/i18n/tests/procs.rb +55 -0
  48. data/lib/i18n/version.rb +5 -0
  49. metadata +124 -0
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The InterpolationCompiler module contains optimizations that can tremendously
4
+ # speed up the interpolation process on the Simple backend.
5
+ #
6
+ # It works by defining a pre-compiled method on stored translation Strings that
7
+ # already bring all the knowledge about contained interpolation variables etc.
8
+ # so that the actual recurring interpolation will be very fast.
9
+ #
10
+ # To enable pre-compiled interpolations you can simply include the
11
+ # InterpolationCompiler module to the Simple backend:
12
+ #
13
+ # I18n::Backend::Simple.include(I18n::Backend::InterpolationCompiler)
14
+ #
15
+ # Note that InterpolationCompiler does not yield meaningful results and consequently
16
+ # should not be used with Ruby 1.9 (YARV) but improves performance everywhere else
17
+ # (jRuby, Rubinius).
18
+ module I18n
19
+ module Backend
20
+ module InterpolationCompiler
21
+ module Compiler
22
+ extend self
23
+
24
+ TOKENIZER = /(%%\{[^\}]+\}|%\{[^\}]+\})/
25
+ INTERPOLATION_SYNTAX_PATTERN = /(%)?(%\{([^\}]+)\})/
26
+
27
+ def compile_if_an_interpolation(string)
28
+ if interpolated_str?(string)
29
+ string.instance_eval <<-RUBY_EVAL, __FILE__, __LINE__
30
+ def i18n_interpolate(v = {})
31
+ "#{compiled_interpolation_body(string)}"
32
+ end
33
+ RUBY_EVAL
34
+ end
35
+
36
+ string
37
+ end
38
+
39
+ def interpolated_str?(str)
40
+ str.kind_of?(::String) && str =~ INTERPOLATION_SYNTAX_PATTERN
41
+ end
42
+
43
+ protected
44
+ # tokenize("foo %{bar} baz %%{buz}") # => ["foo ", "%{bar}", " baz ", "%%{buz}"]
45
+ def tokenize(str)
46
+ str.split(TOKENIZER)
47
+ end
48
+
49
+ def compiled_interpolation_body(str)
50
+ tokenize(str).map do |token|
51
+ (matchdata = token.match(INTERPOLATION_SYNTAX_PATTERN)) ? handle_interpolation_token(token, matchdata) : escape_plain_str(token)
52
+ end.join
53
+ end
54
+
55
+ def handle_interpolation_token(interpolation, matchdata)
56
+ escaped, pattern, key = matchdata.values_at(1, 2, 3)
57
+ escaped ? pattern : compile_interpolation_token(key.to_sym)
58
+ end
59
+
60
+ def compile_interpolation_token(key)
61
+ "\#{#{interpolate_or_raise_missing(key)}}"
62
+ end
63
+
64
+ def interpolate_or_raise_missing(key)
65
+ escaped_key = escape_key_sym(key)
66
+ RESERVED_KEYS.include?(key) ? reserved_key(escaped_key) : interpolate_key(escaped_key)
67
+ end
68
+
69
+ def interpolate_key(key)
70
+ [direct_key(key), nil_key(key), missing_key(key)].join('||')
71
+ end
72
+
73
+ def direct_key(key)
74
+ "((t = v[#{key}]) && t.respond_to?(:call) ? t.call : t)"
75
+ end
76
+
77
+ def nil_key(key)
78
+ "(v.has_key?(#{key}) && '')"
79
+ end
80
+
81
+ def missing_key(key)
82
+ "I18n.config.missing_interpolation_argument_handler.call(#{key}, v, self)"
83
+ end
84
+
85
+ def reserved_key(key)
86
+ "raise(ReservedInterpolationKey.new(#{key}, self))"
87
+ end
88
+
89
+ def escape_plain_str(str)
90
+ str.gsub(/"|\\|#/) {|x| "\\#{x}"}
91
+ end
92
+
93
+ def escape_key_sym(key)
94
+ # rely on Ruby to do all the hard work :)
95
+ key.to_sym.inspect
96
+ end
97
+ end
98
+
99
+ def interpolate(locale, string, values)
100
+ if string.respond_to?(:i18n_interpolate)
101
+ string.i18n_interpolate(values)
102
+ elsif values
103
+ super
104
+ else
105
+ string
106
+ end
107
+ end
108
+
109
+ def store_translations(locale, data, options = EMPTY_HASH)
110
+ compile_all_strings_in(data)
111
+ super
112
+ end
113
+
114
+ protected
115
+ def compile_all_strings_in(data)
116
+ data.each_value do |value|
117
+ Compiler.compile_if_an_interpolation(value)
118
+ compile_all_strings_in(value) if value.kind_of?(Hash)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/backend/base'
4
+
5
+ module I18n
6
+
7
+ begin
8
+ require 'oj'
9
+ class JSON
10
+ class << self
11
+ def encode(value)
12
+ Oj::Rails.encode(value)
13
+ end
14
+ def decode(value)
15
+ Oj.load(value)
16
+ end
17
+ end
18
+ end
19
+ rescue LoadError
20
+ require 'active_support/json'
21
+ JSON = ActiveSupport::JSON
22
+ end
23
+
24
+ module Backend
25
+ # This is a basic backend for key value stores. It receives on
26
+ # initialization the store, which should respond to three methods:
27
+ #
28
+ # * store#[](key) - Used to get a value
29
+ # * store#[]=(key, value) - Used to set a value
30
+ # * store#keys - Used to get all keys
31
+ #
32
+ # Since these stores only supports string, all values are converted
33
+ # to JSON before being stored, allowing it to also store booleans,
34
+ # hashes and arrays. However, this store does not support Procs.
35
+ #
36
+ # As the ActiveRecord backend, Symbols are just supported when loading
37
+ # translations from the filesystem or through explicit store translations.
38
+ #
39
+ # Also, avoid calling I18n.available_locales since it's a somehow
40
+ # expensive operation in most stores.
41
+ #
42
+ # == Example
43
+ #
44
+ # To setup I18n to use TokyoCabinet in memory is quite straightforward:
45
+ #
46
+ # require 'rufus/tokyo/cabinet' # gem install rufus-tokyo
47
+ # I18n.backend = I18n::Backend::KeyValue.new(Rufus::Tokyo::Cabinet.new('*'))
48
+ #
49
+ # == Performance
50
+ #
51
+ # You may make this backend even faster by including the Memoize module.
52
+ # However, notice that you should properly clear the cache if you change
53
+ # values directly in the key-store.
54
+ #
55
+ # == Subtrees
56
+ #
57
+ # In most backends, you are allowed to retrieve part of a translation tree:
58
+ #
59
+ # I18n.backend.store_translations :en, :foo => { :bar => :baz }
60
+ # I18n.t "foo" #=> { :bar => :baz }
61
+ #
62
+ # This backend supports this feature by default, but it slows down the storage
63
+ # of new data considerably and makes hard to delete entries. That said, you are
64
+ # allowed to disable the storage of subtrees on initialization:
65
+ #
66
+ # I18n::Backend::KeyValue.new(@store, false)
67
+ #
68
+ # This is useful if you are using a KeyValue backend chained to a Simple backend.
69
+ class KeyValue
70
+ using I18n::HashRefinements
71
+
72
+ module Implementation
73
+ attr_accessor :store
74
+
75
+ include Base, Flatten
76
+
77
+ def initialize(store, subtrees=true)
78
+ @store, @subtrees = store, subtrees
79
+ end
80
+
81
+ def initialized?
82
+ !@store.nil?
83
+ end
84
+
85
+ def store_translations(locale, data, options = EMPTY_HASH)
86
+ escape = options.fetch(:escape, true)
87
+ flatten_translations(locale, data, escape, @subtrees).each do |key, value|
88
+ key = "#{locale}.#{key}"
89
+
90
+ case value
91
+ when Hash
92
+ if @subtrees && (old_value = @store[key])
93
+ old_value = JSON.decode(old_value)
94
+ value = old_value.deep_symbolize_keys.deep_merge!(value) if old_value.is_a?(Hash)
95
+ end
96
+ when Proc
97
+ raise "Key-value stores cannot handle procs"
98
+ end
99
+
100
+ @store[key] = JSON.encode(value) unless value.is_a?(Symbol)
101
+ end
102
+ end
103
+
104
+ def available_locales
105
+ locales = @store.keys.map { |k| k =~ /\./; $` }
106
+ locales.uniq!
107
+ locales.compact!
108
+ locales.map! { |k| k.to_sym }
109
+ locales
110
+ end
111
+
112
+ protected
113
+
114
+ # Queries the translations from the key-value store and converts
115
+ # them into a hash such as the one returned from loading the
116
+ # haml files
117
+ def translations
118
+ @translations = @store.keys.clone.map do |main_key|
119
+ main_value = JSON.decode(@store[main_key])
120
+ main_key.to_s.split(".").reverse.inject(main_value) do |value, key|
121
+ {key.to_sym => value}
122
+ end
123
+ end.inject{|hash, elem| hash.deep_merge!(elem)}.deep_symbolize_keys
124
+ end
125
+
126
+ def init_translations
127
+ # NO OP
128
+ # This call made also inside Simple Backend and accessed by
129
+ # other plugins like I18n-js and babilu and
130
+ # to use it along with the Chain backend we need to
131
+ # provide a uniform API even for protected methods :S
132
+ end
133
+
134
+ def subtrees?
135
+ @subtrees
136
+ end
137
+
138
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
139
+ key = normalize_flat_keys(locale, key, scope, options[:separator])
140
+ value = @store["#{locale}.#{key}"]
141
+ value = JSON.decode(value) if value
142
+
143
+ if value.is_a?(Hash)
144
+ value.deep_symbolize_keys
145
+ elsif !value.nil?
146
+ value
147
+ elsif !@subtrees
148
+ SubtreeProxy.new("#{locale}.#{key}", @store)
149
+ end
150
+ end
151
+
152
+ def pluralize(locale, entry, count)
153
+ if subtrees?
154
+ super
155
+ else
156
+ return entry unless entry.is_a?(Hash)
157
+ key = pluralization_key(entry, count)
158
+ entry[key]
159
+ end
160
+ end
161
+ end
162
+
163
+ class SubtreeProxy
164
+ def initialize(master_key, store)
165
+ @master_key = master_key
166
+ @store = store
167
+ @subtree = nil
168
+ end
169
+
170
+ def has_key?(key)
171
+ @subtree && @subtree.has_key?(key) || self[key]
172
+ end
173
+
174
+ def [](key)
175
+ unless @subtree && value = @subtree[key]
176
+ value = @store["#{@master_key}.#{key}"]
177
+ if value
178
+ value = JSON.decode(value)
179
+ (@subtree ||= {})[key] = value
180
+ end
181
+ end
182
+ value
183
+ end
184
+
185
+ def is_a?(klass)
186
+ Hash == klass || super
187
+ end
188
+ alias :kind_of? :is_a?
189
+
190
+ def instance_of?(klass)
191
+ Hash == klass || super
192
+ end
193
+
194
+ def nil?
195
+ @subtree.nil?
196
+ end
197
+
198
+ def inspect
199
+ @subtree.inspect
200
+ end
201
+ end
202
+
203
+ include Implementation
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Memoize module simply memoizes the values returned by lookup using
4
+ # a flat hash and can tremendously speed up the lookup process in a backend.
5
+ #
6
+ # To enable it you can simply include the Memoize module to your backend:
7
+ #
8
+ # I18n::Backend::Simple.include(I18n::Backend::Memoize)
9
+ #
10
+ # Notice that it's the responsibility of the backend to define whenever the
11
+ # cache should be cleaned.
12
+ module I18n
13
+ module Backend
14
+ module Memoize
15
+ def available_locales
16
+ @memoized_locales ||= super
17
+ end
18
+
19
+ def store_translations(locale, data, options = EMPTY_HASH)
20
+ reset_memoizations!(locale)
21
+ super
22
+ end
23
+
24
+ def reload!
25
+ reset_memoizations!
26
+ super
27
+ end
28
+
29
+ def eager_load!
30
+ memoized_lookup
31
+ available_locales
32
+ super
33
+ end
34
+
35
+ protected
36
+
37
+ def lookup(locale, key, scope = nil, options = EMPTY_HASH)
38
+ flat_key = I18n::Backend::Flatten.normalize_flat_keys(locale,
39
+ key, scope, options[:separator]).to_sym
40
+ flat_hash = memoized_lookup[locale.to_sym]
41
+ flat_hash.key?(flat_key) ? flat_hash[flat_key] : (flat_hash[flat_key] = super)
42
+ end
43
+
44
+ def memoized_lookup
45
+ @memoized_lookup ||= I18n.new_double_nested_cache
46
+ end
47
+
48
+ def reset_memoizations!(locale=nil)
49
+ @memoized_locales = nil
50
+ (locale ? memoized_lookup[locale.to_sym] : memoized_lookup).clear
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # I18n translation metadata is useful when you want to access information
4
+ # about how a translation was looked up, pluralized or interpolated in
5
+ # your application.
6
+ #
7
+ # msg = I18n.t(:message, :default => 'Hi!', :scope => :foo)
8
+ # msg.translation_metadata
9
+ # # => { :key => :message, :scope => :foo, :default => 'Hi!' }
10
+ #
11
+ # If a :count option was passed to #translate it will be set to the metadata.
12
+ # Likewise, if any interpolation variables were passed they will also be set.
13
+ #
14
+ # To enable translation metadata you can simply include the Metadata module
15
+ # into the Simple backend class - or whatever other backend you are using:
16
+ #
17
+ # I18n::Backend::Simple.include(I18n::Backend::Metadata)
18
+ #
19
+ module I18n
20
+ module Backend
21
+ module Metadata
22
+ class << self
23
+ def included(base)
24
+ Object.class_eval do
25
+ def translation_metadata
26
+ unless self.frozen?
27
+ @translation_metadata ||= {}
28
+ else
29
+ {}
30
+ end
31
+ end
32
+
33
+ def translation_metadata=(translation_metadata)
34
+ @translation_metadata = translation_metadata unless self.frozen?
35
+ end
36
+ end unless Object.method_defined?(:translation_metadata)
37
+ end
38
+ end
39
+
40
+ def translate(locale, key, options = EMPTY_HASH)
41
+ metadata = {
42
+ :locale => locale,
43
+ :key => key,
44
+ :scope => options[:scope],
45
+ :default => options[:default],
46
+ :separator => options[:separator],
47
+ :values => options.reject { |name, _value| RESERVED_KEYS.include?(name) }
48
+ }
49
+ with_metadata(metadata) { super }
50
+ end
51
+
52
+ def interpolate(locale, entry, values = EMPTY_HASH)
53
+ metadata = entry.translation_metadata.merge(:original => entry)
54
+ with_metadata(metadata) { super }
55
+ end
56
+
57
+ def pluralize(locale, entry, count)
58
+ with_metadata(:count => count) { super }
59
+ end
60
+
61
+ protected
62
+
63
+ def with_metadata(metadata, &block)
64
+ result = yield
65
+ result.translation_metadata = result.translation_metadata.merge(metadata) if result
66
+ result
67
+ end
68
+
69
+ end
70
+ end
71
+ end