lawrencepit-i18n 0.1.6 → 0.2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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