mobility 0.1.20 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +2 -0
  4. data/CHANGELOG.md +17 -0
  5. data/CONTRIBUTING.md +55 -0
  6. data/Gemfile +2 -0
  7. data/Gemfile.lock +2 -56
  8. data/README.md +64 -15
  9. data/Rakefile +17 -17
  10. data/lib/mobility.rb +43 -40
  11. data/lib/mobility/active_model.rb +0 -1
  12. data/lib/mobility/active_model/backend_resetter.rb +1 -1
  13. data/lib/mobility/active_record.rb +8 -15
  14. data/lib/mobility/active_record/backend_resetter.rb +2 -0
  15. data/lib/mobility/active_record/model_translation.rb +1 -1
  16. data/lib/mobility/active_record/string_translation.rb +2 -0
  17. data/lib/mobility/active_record/text_translation.rb +2 -0
  18. data/lib/mobility/attributes.rb +74 -71
  19. data/lib/mobility/backend.rb +64 -25
  20. data/lib/mobility/backend/orm_delegator.rb +17 -7
  21. data/lib/mobility/backend/stringify_locale.rb +18 -0
  22. data/lib/mobility/backend_resetter.rb +8 -5
  23. data/lib/mobility/backends.rb +4 -0
  24. data/lib/mobility/backends/active_record.rb +20 -0
  25. data/lib/mobility/{backend → backends}/active_record/column.rb +25 -13
  26. data/lib/mobility/{backend → backends}/active_record/column/query_methods.rb +5 -3
  27. data/lib/mobility/backends/active_record/hstore.rb +29 -0
  28. data/lib/mobility/{backend → backends}/active_record/hstore/query_methods.rb +2 -2
  29. data/lib/mobility/{backend → backends}/active_record/jsonb.rb +6 -6
  30. data/lib/mobility/{backend → backends}/active_record/jsonb/query_methods.rb +4 -2
  31. data/lib/mobility/{backend → backends}/active_record/key_value.rb +10 -44
  32. data/lib/mobility/{backend → backends}/active_record/key_value/query_methods.rb +4 -2
  33. data/lib/mobility/{backend/active_record/hash_valued.rb → backends/active_record/pg_hash.rb} +13 -21
  34. data/lib/mobility/{backend → backends}/active_record/query_methods.rb +2 -2
  35. data/lib/mobility/{backend → backends}/active_record/serialized.rb +12 -28
  36. data/lib/mobility/{backend → backends}/active_record/serialized/query_methods.rb +5 -8
  37. data/lib/mobility/{backend → backends}/active_record/table.rb +11 -60
  38. data/lib/mobility/{backend → backends}/active_record/table/query_methods.rb +5 -3
  39. data/lib/mobility/{backend → backends}/column.rb +8 -4
  40. data/lib/mobility/backends/hash_valued.rb +29 -0
  41. data/lib/mobility/{backend → backends}/hstore.rb +4 -4
  42. data/lib/mobility/{backend → backends}/jsonb.rb +4 -4
  43. data/lib/mobility/backends/key_value.rb +111 -0
  44. data/lib/mobility/{backend → backends}/null.rb +4 -4
  45. data/lib/mobility/backends/sequel.rb +20 -0
  46. data/lib/mobility/backends/sequel/column.rb +52 -0
  47. data/lib/mobility/{backend → backends}/sequel/column/query_methods.rb +5 -3
  48. data/lib/mobility/backends/sequel/hstore.rb +29 -0
  49. data/lib/mobility/{backend → backends}/sequel/hstore/query_methods.rb +4 -3
  50. data/lib/mobility/{backend → backends}/sequel/jsonb.rb +6 -6
  51. data/lib/mobility/{backend → backends}/sequel/jsonb/query_methods.rb +4 -3
  52. data/lib/mobility/{backend → backends}/sequel/key_value.rb +28 -39
  53. data/lib/mobility/{backend → backends}/sequel/key_value/query_methods.rb +4 -5
  54. data/lib/mobility/backends/sequel/pg_hash.rb +46 -0
  55. data/lib/mobility/{backend → backends}/sequel/postgres_query_methods.rb +1 -2
  56. data/lib/mobility/{backend → backends}/sequel/query_methods.rb +6 -4
  57. data/lib/mobility/{backend → backends}/sequel/serialized.rb +17 -38
  58. data/lib/mobility/backends/sequel/serialized/query_methods.rb +17 -0
  59. data/lib/mobility/{backend → backends}/sequel/table.rb +29 -60
  60. data/lib/mobility/{backend → backends}/sequel/table/query_methods.rb +5 -3
  61. data/lib/mobility/{backend → backends}/serialized.rb +27 -5
  62. data/lib/mobility/{backend → backends}/table.rb +69 -29
  63. data/lib/mobility/configuration.rb +40 -0
  64. data/lib/mobility/{orm.rb → loaded.rb} +0 -0
  65. data/lib/mobility/plugins.rb +35 -0
  66. data/lib/mobility/plugins/active_model.rb +6 -0
  67. data/lib/mobility/plugins/active_model/dirty.rb +81 -0
  68. data/lib/mobility/plugins/active_record.rb +6 -0
  69. data/lib/mobility/plugins/active_record/dirty.rb +59 -0
  70. data/lib/mobility/plugins/cache.rb +54 -0
  71. data/lib/mobility/plugins/cache/translation_cacher.rb +40 -0
  72. data/lib/mobility/plugins/default.rb +73 -0
  73. data/lib/mobility/plugins/dirty.rb +61 -0
  74. data/lib/mobility/{backend → plugins}/fallbacks.rb +36 -31
  75. data/lib/mobility/plugins/fallthrough_accessors.rb +66 -0
  76. data/lib/mobility/plugins/locale_accessors.rb +84 -0
  77. data/lib/mobility/{backend → plugins}/presence.rb +15 -6
  78. data/lib/mobility/plugins/sequel.rb +6 -0
  79. data/lib/mobility/plugins/sequel/dirty.rb +59 -0
  80. data/lib/mobility/sequel.rb +5 -14
  81. data/lib/mobility/sequel/backend_resetter.rb +4 -6
  82. data/lib/mobility/sequel/column_changes.rb +4 -4
  83. data/lib/mobility/sequel/model_translation.rb +1 -1
  84. data/lib/mobility/sequel/string_translation.rb +2 -0
  85. data/lib/mobility/sequel/text_translation.rb +2 -0
  86. data/lib/mobility/translates.rb +1 -5
  87. data/lib/mobility/util.rb +126 -0
  88. data/lib/mobility/version.rb +1 -1
  89. data/lib/mobility/wrapper.rb +1 -1
  90. data/lib/rails/generators/mobility/translations_generator.rb +7 -3
  91. metadata +85 -55
  92. metadata.gz.sig +0 -0
  93. data/lib/mobility/backend/active_model.rb +0 -7
  94. data/lib/mobility/backend/active_model/dirty.rb +0 -95
  95. data/lib/mobility/backend/active_record.rb +0 -29
  96. data/lib/mobility/backend/active_record/dirty.rb +0 -54
  97. data/lib/mobility/backend/active_record/hstore.rb +0 -29
  98. data/lib/mobility/backend/cache.rb +0 -117
  99. data/lib/mobility/backend/dirty.rb +0 -38
  100. data/lib/mobility/backend/key_value.rb +0 -85
  101. data/lib/mobility/backend/sequel.rb +0 -29
  102. data/lib/mobility/backend/sequel/column.rb +0 -39
  103. data/lib/mobility/backend/sequel/dirty.rb +0 -57
  104. data/lib/mobility/backend/sequel/hash_valued.rb +0 -51
  105. data/lib/mobility/backend/sequel/hstore.rb +0 -29
  106. data/lib/mobility/backend/sequel/serialized/query_methods.rb +0 -20
  107. data/lib/mobility/core_ext/object.rb +0 -30
  108. data/lib/mobility/core_ext/string.rb +0 -16
  109. data/lib/mobility/fallthrough_accessors.rb +0 -57
  110. data/lib/mobility/locale_accessors.rb +0 -55
@@ -1,5 +1,7 @@
1
+ require "mobility/plugins/cache"
2
+
1
3
  module Mobility
2
- module Backend
4
+ module Backends
3
5
  =begin
4
6
 
5
7
  Stores attribute translation as rows on a model-specific translation table
@@ -30,14 +32,14 @@ another, and Mobility will handle mapping reads/writes to each. The subclass
30
32
  used in this case will be generated from the +association_name+ by
31
33
  singularizing it and converting it to camelcase.
32
34
 
33
- For more details, see examples in {Mobility::Backend::ActiveRecord::Table}.
35
+ For more details, see examples in {Mobility::Backends::ActiveRecord::Table}.
34
36
 
35
37
  ==Backend Options
36
38
 
37
39
  ===+association_name+
38
40
 
39
- Name of association on model. Defaults to +:mobility_model_translations+. If
40
- specified, ensure name does not overlap with other methods on model or with the
41
+ Name of association on model. Defaults to +:translations+. If specified,
42
+ ensure name does not overlap with other methods on model or with the
41
43
  association name used by other backends on model (otherwise one will overwrite
42
44
  the other).
43
45
 
@@ -58,39 +60,77 @@ Subclass to use when dynamically generating translation class for model, by
58
60
  default +:Translation+. Should be a symbol. Generally this does not need to be
59
61
  set.
60
62
 
61
- @see Mobility::Backend::ActiveRecord::Table
62
- @see Mobility::Backend::Sequel::Table
63
+ @see Mobility::Backends::ActiveRecord::Table
64
+ @see Mobility::Backends::Sequel::Table
63
65
  =end
64
66
  module Table
65
- include OrmDelegator
67
+ extend Backend::OrmDelegator
68
+
69
+ # @!macro backend_constructor
70
+ # @option options [Symbol] association_name Name of association
71
+ def initialize(model, attribute, options = {})
72
+ super
73
+ @association_name = options[:association_name]
74
+ end
75
+
76
+ # @!group Backend Accessors
77
+ # @!macro backend_reader
78
+ def read(locale, options = {})
79
+ translation_for(locale, options).send(attribute)
80
+ end
81
+
82
+ # @!macro backend_writer
83
+ def write(locale, value, options = {})
84
+ translation_for(locale, options).tap { |t| t.send("#{attribute}=", value) }.send(attribute)
85
+ end
86
+ # @!endgroup
87
+
88
+ # @!macro backend_iterator
89
+ def each_locale
90
+ translations.each { |t| yield t.locale.to_sym }
91
+ end
92
+
93
+ private
94
+
95
+ def translations
96
+ model.send(association_name)
97
+ end
98
+
99
+ def self.included(backend)
100
+ backend.extend ClassMethods
101
+ end
102
+
103
+ module ClassMethods
104
+ # Apply custom processing for plugin
105
+ # @param (see Backend::Setup#apply_plugin)
106
+ # @return (see Backend::Setup#apply_plugin)
107
+ def apply_plugin(name)
108
+ if name == :cache
109
+ include self::Cache
110
+ true
111
+ else
112
+ super
113
+ end
114
+ end
115
+ end
66
116
 
67
117
  # Simple hash cache to memoize translations as a hash so they can be
68
118
  # fetched quickly.
69
- class TranslationsCache < Hash
70
-
71
- # @yield [locale] Yields locale to block in case attribute is not yet
72
- # cached, expects a new translation for that locale.
73
- # @raise [ArgumentError] if block is not given
74
- def initialize
75
- raise ArgumentError, "missing block" unless block_given?
76
- super() { |hash, locale| hash[locale] = yield(locale) }
77
- end
119
+ module Cache
120
+ include Plugins::Cache::TranslationCacher.new(:translation_for)
78
121
 
79
- # Return wrapper class which reads and writes to only one attribute of this cache.
80
- # @param [String] attribute
81
- # @return [Class] Hash-like wrapper object to be used as attribute cache
82
- def for(attribute)
83
- cache = self
122
+ private
84
123
 
85
- Class.new do
86
- define_singleton_method :[] do |locale|
87
- cache[locale].send(attribute)
88
- end
124
+ def cache
125
+ model_cache || model.instance_variable_set(:"@__mobility_#{association_name}_cache", {})
126
+ end
89
127
 
90
- define_singleton_method :[]= do |locale, value|
91
- cache[locale].send("#{attribute}=", value)
92
- end
93
- end
128
+ def model_cache
129
+ model.instance_variable_get(:"@__mobility_#{association_name}_cache")
130
+ end
131
+
132
+ def clear_cache
133
+ model_cache && model_cache.clear
94
134
  end
95
135
  end
96
136
  end
@@ -5,6 +5,8 @@ Stores shared Mobility configuration referenced by all backends.
5
5
 
6
6
  =end
7
7
  class Configuration
8
+ RESERVED_OPTION_KEYS = %i[backend model_class].freeze
9
+
8
10
  # Alias for mobility_accessor (defaults to +translates+)
9
11
  # @return [Symbol]
10
12
  attr_accessor :accessor_method
@@ -13,6 +15,26 @@ Stores shared Mobility configuration referenced by all backends.
13
15
  # @return [Symbol]
14
16
  attr_accessor :query_method
15
17
 
18
+ # Default set of options. These will be merged with any backend options
19
+ # when defining translated attributes (with +translates+). Default options
20
+ # may not include the keys 'backend' or 'model_class'.
21
+ # @return [Hash]
22
+ attr_reader :default_options
23
+ def default_options=(options)
24
+ if (keys = options.keys & RESERVED_OPTION_KEYS).present?
25
+ raise ReservedOptionKey,
26
+ "Default options may not contain the following reserved keys: #{keys.join(', ')}"
27
+ else
28
+ @default_options = options
29
+ end
30
+ end
31
+
32
+ # Plugins to apply. Order of plugins is important, as this becomes the
33
+ # order in which plugins modules are included into the backend class or
34
+ # attributes instance.
35
+ # @return [Array<Symbol>]
36
+ attr_accessor :plugins
37
+
16
38
  # Default fallbacks instance
17
39
  # @return [I18n::Locale::Fallbacks]
18
40
  def default_fallbacks(fallbacks = {})
@@ -41,6 +63,24 @@ Stores shared Mobility configuration referenced by all backends.
41
63
  @query_method = :i18n
42
64
  @default_fallbacks = lambda { |fallbacks| I18n::Locale::Fallbacks.new(fallbacks) }
43
65
  @default_accessor_locales = lambda { I18n.available_locales }
66
+ @default_options = {
67
+ cache: true,
68
+ dirty: false,
69
+ fallbacks: nil,
70
+ presence: true,
71
+ default: nil
72
+ }
73
+ @plugins = %i[
74
+ cache
75
+ dirty
76
+ fallbacks
77
+ presence
78
+ default
79
+ fallthrough_accessors
80
+ locale_accessors
81
+ ]
44
82
  end
83
+
84
+ class ReservedOptionKey < Exception; end
45
85
  end
46
86
  end
File without changes
@@ -0,0 +1,35 @@
1
+ module Mobility
2
+ =begin
3
+
4
+ Plugins allow modular customization of backends independent of the backend
5
+ itself. They are enabled through the {Configuration.plugins} configuration
6
+ setting, which takes an array of symbols corresponding to plugin names. The
7
+ order of these names is important since it determines the order in which
8
+ plugins will be applied.
9
+
10
+ So if our {Configuration.plugins} is an array +[:foo]+, and we call
11
+ `translates` on our model, +Post+, like this:
12
+
13
+ class Post
14
+ translates :title, foo: true
15
+ end
16
+
17
+ Then the +Foo+ plugin will be applied with the option value +true+. Applying a
18
+ module calls a class method, +apply+ (in this case +Foo.apply+), which takes
19
+ two arguments:
20
+
21
+ - an instance of the {Attributes} class, +attributes+, from which the backend
22
+ can configure the backend class (+attributes.backend_class+) and the model
23
+ (+attributes.model_class+), and the +attributes+ module itself (which
24
+ will be included into the backend).
25
+ - the value of the +option+ passed into the model with +translates+ (in this
26
+ case, +true+).
27
+
28
+ Typically, the plugin will include a module into either
29
+ +attributes.backend_class+ or +attributes+ itself, configured according to the
30
+ option value. For examples, see classes under the {Mobility::Plugins} namespace.
31
+
32
+ =end
33
+ module Plugins
34
+ end
35
+ end
@@ -0,0 +1,6 @@
1
+ module Mobility
2
+ module Plugins
3
+ module ActiveModel
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,81 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Mobility
4
+ module Plugins
5
+ =begin
6
+
7
+ Dirty tracking for models which include the +ActiveModel::Dirty+ module.
8
+
9
+ Assuming we have an attribute +title+, this module will add support for the
10
+ following methods:
11
+ - +title_changed?+
12
+ - +title_change+
13
+ - +title_was+
14
+ - +title_will_change!+
15
+ - +title_previously_changed?+
16
+ - +title_previous_change+
17
+ - +restore_title!+
18
+
19
+ In addition, the private method +restore_attribute!+ will also restore the
20
+ value of the translated attribute if passed to it.
21
+
22
+ @see http://api.rubyonrails.org/classes/ActiveModel/Dirty.html Rails documentation for Active Model Dirty module
23
+
24
+ =end
25
+ module ActiveModel
26
+ module Dirty
27
+ # @!group Backend Accessors
28
+ # @!macro backend_writer
29
+ # @param [Hash] options
30
+ def write(locale, value, options = {})
31
+ locale_accessor = Mobility.normalize_locale_accessor(attribute, locale)
32
+ if model.changed_attributes.has_key?(locale_accessor) && model.changed_attributes[locale_accessor] == value
33
+ model.attributes_changed_by_setter.except!(locale_accessor)
34
+ elsif read(locale, options.merge(fallback: false)) != value
35
+ model.send(:attribute_will_change!, locale_accessor)
36
+ end
37
+ super
38
+ end
39
+ # @!endgroup
40
+
41
+ # Builds module which adds suffix/prefix methods for translated
42
+ # attributes so they act like normal dirty-tracked attributes.
43
+ class MethodsBuilder < Module
44
+ def initialize(*attribute_names)
45
+ attribute_names.each do |name|
46
+ method_suffixes.each do |suffix|
47
+ define_method "#{name}#{suffix}".freeze do
48
+ __send__("attribute#{suffix}".freeze, Mobility.normalize_locale_accessor(name))
49
+ end
50
+ end
51
+
52
+ define_method "restore_#{name}!".freeze do
53
+ locale_accessor = Mobility.normalize_locale_accessor(name)
54
+ if attribute_changed?(locale_accessor)
55
+ __send__("#{name}=".freeze, changed_attributes[locale_accessor])
56
+ end
57
+ end
58
+ end
59
+
60
+ define_method :restore_attribute! do |attr|
61
+ attribute_names.include?(attr.to_s) ? send("restore_#{attr}!".freeze) : super(attr)
62
+ end
63
+ private :restore_attribute!
64
+ end
65
+
66
+ private
67
+
68
+ # Get method suffixes. Creating an object just to get the list of
69
+ # suffixes is not very efficient, but the most reliable way given that
70
+ # they change from Rails version to version.
71
+ def method_suffixes
72
+ @method_suffixes ||=
73
+ Class.new do
74
+ include ::ActiveModel::Dirty
75
+ end.attribute_method_matchers.map(&:suffix).select { |m| m =~ /\A_/ }
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,6 @@
1
+ module Mobility
2
+ module Plugins
3
+ module ActiveRecord
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,59 @@
1
+ # frozen-string-literal: true
2
+ require "mobility/plugins/active_model/dirty"
3
+
4
+ module Mobility
5
+ module Plugins
6
+ =begin
7
+
8
+ Dirty tracking for AR models. See {Mobility::Plugins::ActiveModel::Dirty} for
9
+ details on usage.
10
+
11
+ =end
12
+ module ActiveRecord
13
+ module Dirty
14
+ include ActiveModel::Dirty
15
+
16
+ # Builds module which patches a few AR methods to handle changes to
17
+ # translated attributes just like normal attributes.
18
+ class MethodsBuilder < ActiveModel::Dirty::MethodsBuilder
19
+ def initialize(*attribute_names)
20
+ super
21
+ @attribute_names = attribute_names
22
+
23
+ changes_applied_method = ::ActiveRecord::VERSION::STRING < '5.1' ? :changes_applied : :changes_internally_applied
24
+ define_method changes_applied_method do
25
+ @previously_changed = changes
26
+ super()
27
+ end
28
+
29
+ define_method :clear_changes_information do
30
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
31
+ super()
32
+ end
33
+
34
+ define_method :previous_changes do
35
+ (@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new).merge(super())
36
+ end
37
+ end
38
+
39
+ # Overrides +ActiveRecord::AttributeMethods::ClassMethods#has_attribute+ to treat fallthrough attribute methods
40
+ # just like "real" attribute methods.
41
+ #
42
+ # @note Patching +has_attribute?+ is necessary as of AR 5.1 due to this commit[https://github.com/rails/rails/commit/4fed08fa787a316fa51f14baca9eae11913f5050].
43
+ # (I have voiced my opposition to this change here[https://github.com/rails/rails/pull/27963#issuecomment-310092787]).
44
+ # @param [Attributes] attributes
45
+ def included(model_class)
46
+ names = @attribute_names
47
+ method_name_regex = /\A(#{names.join('|'.freeze)})_([a-z]{2}(_[a-z]{2})?)(=?|\??)\z/.freeze
48
+ has_attribute = Module.new do
49
+ define_method :has_attribute? do |attr_name|
50
+ super(attr_name) || !!method_name_regex.match(attr_name)
51
+ end
52
+ end
53
+ model_class.extend has_attribute
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,54 @@
1
+ require "mobility/plugins/cache/translation_cacher"
2
+
3
+ module Mobility
4
+ module Plugins
5
+ =begin
6
+
7
+ Caches values fetched from the backend so subsequent fetches can be performed
8
+ more quickly. The cache stores cached values in a simple hash, which is not
9
+ optimal for some storage strategies, so some backends (KeyValue, Table) use a
10
+ custom module through the {Mobility::Backend::Setup#apply_plugin} hook. For
11
+ details see the documentation for these backends.
12
+
13
+ The cache is reset when one of a set of events happens (saving, reloading,
14
+ etc.). See {BackendResetter} for details.
15
+
16
+ Values are added to the cache in two ways:
17
+
18
+ 1. first read from backend
19
+ 2. any write to backend
20
+
21
+ =end
22
+ module Cache
23
+ # Applies cache plugin to attributes.
24
+ # @param [Attributes] attributes
25
+ # @param [Boolean] option
26
+ def self.apply(attributes, option)
27
+ if option
28
+ backend_class = attributes.backend_class
29
+ backend_class.include(self) unless backend_class.apply_plugin(:cache)
30
+
31
+ model_class = attributes.model_class
32
+ model_class.include BackendResetter.for(model_class).new(attributes.names) { clear_cache }
33
+ end
34
+ end
35
+
36
+ # @group Backend Accessors
37
+ #
38
+ # @!macro backend_reader
39
+ # @option options [Boolean] cache
40
+ # *false* to disable cache.
41
+ # @!method read(locale, value, options = {})
42
+ include TranslationCacher.new(:read)
43
+
44
+ # @!macro backend_writer
45
+ # @option options [Boolean] cache
46
+ # *false* to disable cache.
47
+ def write(locale, value, **options)
48
+ return super if options.delete(:cache) == false
49
+ cache[locale] = super
50
+ end
51
+ # @!endgroup
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,40 @@
1
+ module Mobility
2
+ module Plugins
3
+ module Cache
4
+ =begin
5
+
6
+ Creates a module to cache a given translation fetch method. The cacher defines
7
+ private methods +cache+ and +clear_cache+ to access and clear, respectively, a
8
+ translations hash.
9
+
10
+ This cacher is used to cache translation values in {Mobility::Plugins::Cache},
11
+ and also to cache translation *records* in {Mobility::Backends::Table} and
12
+ {Mobility::Backends::KeyValue}.
13
+
14
+ =end
15
+ class TranslationCacher < Module
16
+ # @param [Symbol] fetch_method Name of translation fetch method to cache
17
+ def initialize(fetch_method)
18
+ define_method fetch_method do |locale, **options|
19
+ return super(locale, options) if options.delete(:cache) == false
20
+ if cache.has_key?(locale)
21
+ cache[locale]
22
+ else
23
+ cache[locale] = super(locale, options)
24
+ end
25
+ end
26
+
27
+ define_method :cache do
28
+ @cache ||= {}
29
+ end
30
+
31
+ define_method :clear_cache do
32
+ @cache = {}
33
+ end
34
+
35
+ private :cache, :clear_cache
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end