lawrencepit-i18n 0.1.6 → 0.2.0.1

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,71 @@
1
+ # encoding: utf-8
2
+
3
+ # This module allows you to easily cache all responses from the backend - thus
4
+ # speeding up the I18n aspects of your application quite a bit.
5
+ #
6
+ # To enable caching you can simply include the Cache module to the Simple
7
+ # backend - or whatever other backend you are using:
8
+ #
9
+ # I18n::Backend::Simple.send(:include, I18n::Backend::Cache)
10
+ #
11
+ # You will also need to set a cache store implementation that you want to use:
12
+ #
13
+ # I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
14
+ #
15
+ # You can use any cache implementation you want that provides the same API as
16
+ # ActiveSupport::Cache (only the methods #fetch and #write are being used).
17
+ #
18
+ # The cache_key implementation assumes that you only pass values to
19
+ # I18n.translate that return a valid key from #hash (see
20
+ # http://www.ruby-doc.org/core/classes/Object.html#M000337).
21
+ module I18n
22
+ class << self
23
+ @@cache_store = nil
24
+ @@cache_namespace = nil
25
+
26
+ def cache_store
27
+ @@cache_store
28
+ end
29
+
30
+ def cache_store=(store)
31
+ @@cache_store = store
32
+ end
33
+
34
+ def cache_namespace
35
+ @@cache_namespace
36
+ end
37
+
38
+ def cache_namespace=(namespace)
39
+ @@cache_namespace = namespace
40
+ end
41
+
42
+ def perform_caching?
43
+ !cache_store.nil?
44
+ end
45
+ end
46
+
47
+ module Backend
48
+ module Cache
49
+ def translate(*args)
50
+ I18n.perform_caching? ? fetch(*args) { super } : super
51
+ end
52
+
53
+ protected
54
+
55
+ def fetch(*args, &block)
56
+ result = I18n.cache_store.fetch(cache_key(*args), &block)
57
+ raise result if result.is_a?(Exception)
58
+ result
59
+ rescue MissingTranslationData => exception
60
+ I18n.cache_store.write(cache_key(*args), exception)
61
+ raise exception
62
+ end
63
+
64
+ def cache_key(*args)
65
+ # this assumes that only simple, native Ruby values are passed to I18n.translate
66
+ keys = ['i18n', I18n.cache_namespace, args.hash]
67
+ keys.compact.join('-')
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+
3
+ module I18n
4
+ module Backend
5
+ # Backend that chains multiple other backends and checks each of them when
6
+ # a translation needs to be looked up. This is useful when you want to use
7
+ # standard translations with a Simple backend but store custom application
8
+ # translations in a database or other backends.
9
+ #
10
+ # To use the Chain backend instantiate it and set it to the I18n module.
11
+ # You can add chained backends through the initializer or backends
12
+ # accessor:
13
+ #
14
+ # # preserves the existing Simple backend set to I18n.backend
15
+ # I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
16
+ #
17
+ # The implementation assumes that all backends added to the Chain implement
18
+ # a lookup method with the same API as Simple backend does.
19
+ class Chain < Base
20
+ attr_accessor :backends
21
+
22
+ def initialize(*backends)
23
+ self.backends = backends
24
+ end
25
+
26
+ def reload!
27
+ backends.each { |backend| backend.reload! }
28
+ end
29
+
30
+ def store_translations(locale, data)
31
+ backends.first.store_translations(locale, data)
32
+ end
33
+
34
+ def available_locales
35
+ backends.map { |backend| backend.available_locales }.flatten.uniq
36
+ end
37
+
38
+ def localize(locale, object, format = :default, options = {})
39
+ backends.each do |backend|
40
+ begin
41
+ result = backend.localize(locale, object, format, options) and return result
42
+ rescue MissingTranslationData
43
+ end
44
+ end or nil
45
+ end
46
+
47
+ protected
48
+
49
+ def lookup(locale, key, scope = [], separator = nil)
50
+ return unless key
51
+ result = {}
52
+ backends.each do |backend|
53
+ entry = backend.lookup(locale, key, scope, separator)
54
+ if entry.is_a?(Hash)
55
+ result.merge!(entry)
56
+ elsif !entry.nil?
57
+ return entry
58
+ end
59
+ end
60
+ result.empty? ? nil : result
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'i18n/locale/fallbacks'
4
+
5
+ # I18n locale fallbacks are useful when you want your application to use
6
+ # translations from other locales when translations for the current locale are
7
+ # missing. E.g. you might want to use :en translations when translations in
8
+ # your applications main locale :de are missing.
9
+ #
10
+ # To enable locale fallbacks you can simply include the Fallbacks module to
11
+ # the Simple backend - or whatever other backend you are using:
12
+ #
13
+ # I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
14
+ module I18n
15
+ @@fallbacks = nil
16
+
17
+ class << self
18
+ # Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+.
19
+ def fallbacks
20
+ @@fallbacks ||= I18n::Locale::Fallbacks.new
21
+ end
22
+
23
+ # Sets the current fallbacks implementation. Use this to set a different fallbacks implementation.
24
+ def fallbacks=(fallbacks)
25
+ @@fallbacks = fallbacks
26
+ end
27
+ end
28
+
29
+ module Backend
30
+ module Fallbacks
31
+ # Overwrites the Base backend translate method so that it will try each
32
+ # locale given by I18n.fallbacks for the given locale. E.g. for the
33
+ # locale :"de-DE" it might try the locales :"de-DE", :de and :en
34
+ # (depends on the fallbacks implementation) until it finds a result with
35
+ # the given options. If it does not find any result for any of the
36
+ # locales it will then raise a MissingTranslationData exception as
37
+ # usual.
38
+ #
39
+ # The default option takes precedence over fallback locales, i.e. it
40
+ # will first evaluate a given default option before falling back to
41
+ # another locale.
42
+ def translate(locale, key, options = {})
43
+ I18n.fallbacks[locale].each do |fallback|
44
+ begin
45
+ result = super(fallback, key, options) and return result
46
+ rescue I18n::MissingTranslationData
47
+ end
48
+ end
49
+ raise(I18n::MissingTranslationData.new(locale, key, options))
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,65 @@
1
+ # encoding: utf-8
2
+
3
+ require 'i18n/gettext'
4
+ require File.expand_path(File.dirname(__FILE__) + '/../../../vendor/po_parser.rb')
5
+
6
+ # Experimental support for using Gettext po files to store translations.
7
+ #
8
+ # To use this you can simply include the module to the Simple backend - or
9
+ # whatever other backend you are using.
10
+ #
11
+ # I18n::Backend::Simple.send(:include, I18n::Backend::Gettext)
12
+ #
13
+ # Now you should be able to include your Gettext translation (*.po) files to
14
+ # the I18n.load_path so they're loaded to the backend and you can use them as
15
+ # usual:
16
+ #
17
+ # I18n.load_path += Dir["path/to/locales/*.po"]
18
+ #
19
+ # Following the Gettext convention this implementation expects that your
20
+ # translation files are named by their locales. E.g. the file en.po would
21
+ # contain the translations for the English locale.
22
+ module I18n
23
+ module Backend
24
+ module Gettext
25
+ class PoData < Hash
26
+ def set_comment(msgid_or_sym, comment)
27
+ # ignore
28
+ end
29
+ end
30
+
31
+ protected
32
+ def load_po(filename)
33
+ locale = ::File.basename(filename, '.po').to_sym
34
+ data = normalize(locale, parse(filename))
35
+ { locale => data }
36
+ end
37
+
38
+ def parse(filename)
39
+ GetText::PoParser.new.parse(::File.read(filename), PoData.new)
40
+ end
41
+
42
+ def normalize(locale, data)
43
+ data.inject({}) do |result, (key, value)|
44
+ key, value = normalize_pluralization(locale, key, value) if key.index("\000")
45
+ result[key] = value
46
+ result
47
+ end
48
+ end
49
+
50
+ def normalize_pluralization(locale, key, value)
51
+ # FIXME po_parser includes \000 chars that can not be turned into Symbols
52
+ key = key.dup.gsub("\000", I18n::Gettext::PLURAL_SEPARATOR)
53
+
54
+ keys = I18n::Gettext.plural_keys(locale)
55
+ values = value.split("\000")
56
+ raise "invalid number of plurals: #{values.size}, keys: #{keys.inspect}" if values.size != keys.size
57
+
58
+ result = {}
59
+ values.each_with_index { |value, ix| result[keys[ix]] = value }
60
+ [key, result]
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+
3
+ # I18n locale fallbacks are useful when you want your application to use
4
+ # translations from other locales when translations for the current locale are
5
+ # missing. E.g. you might want to use :en translations when translations in
6
+ # your applications main locale :de are missing.
7
+ #
8
+ # To enable locale specific pluralizations you can simply include the
9
+ # Pluralization module to the Simple backend - or whatever other backend you
10
+ # are using.
11
+ #
12
+ # I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
13
+ #
14
+ # You also need to make sure to provide pluralization algorithms to the
15
+ # backend, i.e. include them to your I18n.load_path accordingly.
16
+ module I18n
17
+ module Backend
18
+ module Pluralization
19
+ # Overwrites the Base backend translate method so that it will check the
20
+ # translation meta data space (:i18n) for locale specific pluralizers
21
+ # and use them to pluralize the given entry.
22
+ #
23
+ # Pluralizers are expected to respond to #call(entry, count) and return
24
+ # a pluralization key. Valid keys depend on the translation data hash
25
+ # (entry) but it is generally recommended to follow CLDR's style, i.e.
26
+ # return one of the keys :zero, :one, :few, :many, :other.
27
+ #
28
+ # The :zero key is always picked directly when count equals 0 AND the
29
+ # translation data has the key :zero. This way translators are free to
30
+ # either pick a special :zero translation even for languages where the
31
+ # pluralizer does not return a :zero key.
32
+ def pluralize(locale, entry, count)
33
+ return entry unless entry.is_a?(Hash) and count
34
+
35
+ pluralizer = pluralizer(locale)
36
+ if pluralizer.respond_to?(:call)
37
+ key = count == 0 && entry.has_key?(:zero) ? :zero : pluralizer.call(count)
38
+ raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
39
+ entry[key]
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ protected
46
+
47
+ def pluralizers
48
+ @pluralizers ||= {}
49
+ end
50
+
51
+ def pluralizer(locale)
52
+ pluralizers[locale] ||= lookup(locale, :"i18n.pluralize")
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,235 +1,23 @@
1
- require 'yaml'
1
+ # encoding: utf-8
2
+
3
+ require 'i18n/backend/base'
4
+
5
+ # Stub class for the Simple backend. The actual implementation is provided by
6
+ # the backend Base class. This makes it easier to extend the Simple backend's
7
+ # behaviour by including modules. E.g.:
8
+ #
9
+ # module I18n::Backend::Pluralization
10
+ # def pluralize(*args)
11
+ # # extended pluralization logic
12
+ # super
13
+ # end
14
+ # end
15
+ #
16
+ # I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
2
17
 
3
18
  module I18n
4
19
  module Backend
5
- class Simple
6
- RESERVED_KEYS = [:scope, :default, :separator]
7
- MATCH = /(\\\\)?\{\{([^\}]+)\}\}/
8
-
9
- # Accepts a list of paths to translation files. Loads translations from
10
- # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
11
- # for details.
12
- def load_translations(*filenames)
13
- filenames.each { |filename| load_file(filename) }
14
- end
15
-
16
- # Stores translations for the given locale in memory.
17
- # This uses a deep merge for the translations hash, so existing
18
- # translations will be overwritten by new ones only at the deepest
19
- # level of the hash.
20
- def store_translations(locale, data)
21
- merge_translations(locale, data)
22
- end
23
-
24
- def translate(locale, key, options = {})
25
- raise InvalidLocale.new(locale) if locale.nil?
26
- return key.map { |k| translate(locale, k, options) } if key.is_a?(Array)
27
-
28
- count, scope, default, separator = options.values_at(:count, *RESERVED_KEYS)
29
- values = options.reject { |name, value| RESERVED_KEYS.include?(name) }
30
-
31
- entry = lookup(locale, key, scope, separator)
32
- entry = entry.nil? ? default(locale, key, default, options) : resolve(locale, key, entry, options)
33
-
34
- raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil?
35
- entry = pluralize(locale, entry, count)
36
- entry = interpolate(locale, entry, values)
37
- entry
38
- end
39
-
40
- # Acts the same as +strftime+, but returns a localized version of the
41
- # formatted date string. Takes a key from the date/time formats
42
- # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
43
- def localize(locale, object, format = :default, options={})
44
- raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
45
-
46
- if Symbol === format
47
- type = object.respond_to?(:sec) ? 'time' : 'date'
48
- format = lookup(locale, :"#{type}.formats.#{format}")
49
- end
50
-
51
- format = resolve(locale, object, format, options.merge(:raise => true))
52
-
53
- # TODO only translate these if the format string is actually present
54
- # TODO check which format strings are present, then bulk translate them, then replace them
55
-
56
- format.gsub!(/%a/, translate(locale, :"date.abbr_day_names", :format => format)[object.wday])
57
- format.gsub!(/%A/, translate(locale, :"date.day_names", :format => format)[object.wday])
58
- format.gsub!(/%b/, translate(locale, :"date.abbr_month_names", :format => format)[object.mon])
59
- format.gsub!(/%B/, translate(locale, :"date.month_names", :format => format)[object.mon])
60
- format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}", :format => format)) if object.respond_to?(:hour)
61
-
62
- object.strftime(format)
63
- end
64
-
65
- def initialized?
66
- @initialized ||= false
67
- end
68
-
69
- # Returns an array of locales for which translations are available
70
- def available_locales
71
- init_translations unless initialized?
72
- translations.keys
73
- end
74
-
75
- def reload!
76
- @initialized = false
77
- @translations = nil
78
- end
79
-
80
- protected
81
- def init_translations
82
- load_translations(*I18n.load_path.flatten)
83
- @initialized = true
84
- end
85
-
86
- def translations
87
- @translations ||= {}
88
- end
89
-
90
- # Looks up a translation from the translations hash. Returns nil if
91
- # eiher key is nil, or locale, scope or key do not exist as a key in the
92
- # nested translations hash. Splits keys or scopes containing dots
93
- # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
94
- # <tt>%w(currency format)</tt>.
95
- def lookup(locale, key, scope = [], separator = nil)
96
- return unless key
97
- init_translations unless initialized?
98
- keys = I18n.send(:normalize_translation_keys, locale, key, scope, separator)
99
- keys.inject(translations) do |result, k|
100
- if (x = result[k.to_sym]).nil?
101
- return nil
102
- else
103
- x
104
- end
105
- end
106
- end
107
-
108
- # Evaluates defaults.
109
- # If given subject is an Array, it walks the array and returns the
110
- # first translation that can be resolved. Otherwise it tries to resolve
111
- # the translation directly.
112
- def default(locale, object, subject, options = {})
113
- options = options.dup.reject { |key, value| key == :default }
114
- case subject
115
- when Array
116
- subject.each do |subject|
117
- result = resolve(locale, object, subject, options) and return result
118
- end and nil
119
- else
120
- resolve(locale, object, subject, options)
121
- end
122
- end
123
-
124
- # Resolves a translation.
125
- # If the given subject is a Symbol, it will be translated with the
126
- # given options. If it is a Proc then it will be evaluated. All other
127
- # subjects will be returned directly.
128
- def resolve(locale, object, subject, options = {})
129
- case subject
130
- when Symbol
131
- translate(locale, subject, options)
132
- when Proc
133
- resolve(locale, object, subject.call(object, options), options = {})
134
- else
135
- subject
136
- end
137
- rescue MissingTranslationData
138
- nil
139
- end
140
-
141
- # Picks a translation from an array according to English pluralization
142
- # rules. It will pick the first translation if count is not equal to 1
143
- # and the second translation if it is equal to 1. Other backends can
144
- # implement more flexible or complex pluralization rules.
145
- def pluralize(locale, entry, count)
146
- return entry unless entry.is_a?(Hash) and count
147
-
148
- key = :zero if count == 0 && entry.has_key?(:zero)
149
- key ||= count == 1 ? :one : :other
150
- raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
151
- entry[key]
152
- end
153
-
154
- # Interpolates values into a given string.
155
- #
156
- # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
157
- # # => "file test.txt opened by {{user}}"
158
- #
159
- # Note that you have to double escape the <tt>\\</tt> when you want to escape
160
- # the <tt>{{...}}</tt> key in a string (once for the string and once for the
161
- # interpolation).
162
- def interpolate(locale, string, values = {})
163
- return string unless string.is_a?(String)
164
-
165
- string.gsub!(MATCH) do
166
- escaped, key = $1, $2.to_sym
167
-
168
- if escaped
169
- key
170
- elsif RESERVED_KEYS.include?(key)
171
- raise ReservedInterpolationKey.new(key, string)
172
- elsif !values.include?(key)
173
- raise MissingInterpolationArgument.new(key, string)
174
- else
175
- values[key].to_s
176
- end
177
- end
178
- string
179
- end
180
-
181
- # Loads a single translations file by delegating to #load_rb or
182
- # #load_yml depending on the file extension and directly merges the
183
- # data to the existing translations. Raises I18n::UnknownFileType
184
- # for all other file extensions.
185
- def load_file(filename)
186
- type = File.extname(filename).tr('.', '').downcase
187
- raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}")
188
- data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
189
- data.each { |locale, d| merge_translations(locale, d) }
190
- end
191
-
192
- # Loads a plain Ruby translations file. eval'ing the file must yield
193
- # a Hash containing translation data with locales as toplevel keys.
194
- def load_rb(filename)
195
- eval(IO.read(filename), binding, filename)
196
- end
197
-
198
- # Loads a YAML translations file. The data must have locales as
199
- # toplevel keys.
200
- def load_yml(filename)
201
- YAML::load(IO.read(filename))
202
- end
203
-
204
- # Deep merges the given translations hash with the existing translations
205
- # for the given locale
206
- def merge_translations(locale, data)
207
- locale = locale.to_sym
208
- translations[locale] ||= {}
209
- data = deep_symbolize_keys(data)
210
-
211
- # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
212
- merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
213
- translations[locale].merge!(data, &merger)
214
- end
215
-
216
- # Return a new hash with all keys and nested keys converted to symbols.
217
- def deep_symbolize_keys(hash)
218
- hash.inject({}) { |result, (key, value)|
219
- value = deep_symbolize_keys(value) if value.is_a?(Hash)
220
- result[(key.to_sym rescue key) || key] = value
221
- result
222
- }
223
- end
224
-
225
- # Flatten the given array once
226
- def flatten_once(array)
227
- result = []
228
- for element in array # a little faster than each
229
- result.push(*element)
230
- end
231
- result
232
- end
20
+ class Simple < Base
233
21
  end
234
22
  end
235
23
  end