i18n 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of i18n might be problematic. Click here for more details.

@@ -1,233 +1,234 @@
1
- require 'yaml'
2
-
3
- module I18n
4
- module Backend
5
- class Simple
6
- RESERVED_KEYS = [:scope, :default, :separator]
7
- INTERPOLATION_SYNTAX_PATTERN = /(\\\\)?\{\{([^\}]+)\}\}/
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 check which format strings are present, then bulk translate them, then replace them
54
- format.gsub!(/%a/, translate(locale, :"date.abbr_day_names", :format => format)[object.wday]) if format.include?('%a')
55
- format.gsub!(/%A/, translate(locale, :"date.day_names", :format => format)[object.wday]) if format.include?('%A')
56
- format.gsub!(/%b/, translate(locale, :"date.abbr_month_names", :format => format)[object.mon]) if format.include?('%b')
57
- format.gsub!(/%B/, translate(locale, :"date.month_names", :format => format)[object.mon]) if format.include?('%B')
58
- format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}", :format => format)) if format.include?('%p') && object.respond_to?(:hour)
59
-
60
- object.strftime(format)
61
- end
62
-
63
- def initialized?
64
- @initialized ||= false
65
- end
66
-
67
- # Returns an array of locales for which translations are available
68
- def available_locales
69
- init_translations unless initialized?
70
- translations.keys
71
- end
72
-
73
- def reload!
74
- @initialized = false
75
- @translations = nil
76
- end
77
-
78
- protected
79
- def init_translations
80
- load_translations(*I18n.load_path.flatten)
81
- @initialized = true
82
- end
83
-
84
- def translations
85
- @translations ||= {}
86
- end
87
-
88
- # Looks up a translation from the translations hash. Returns nil if
89
- # eiher key is nil, or locale, scope or key do not exist as a key in the
90
- # nested translations hash. Splits keys or scopes containing dots
91
- # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
92
- # <tt>%w(currency format)</tt>.
93
- def lookup(locale, key, scope = [], separator = nil)
94
- return unless key
95
- init_translations unless initialized?
96
- keys = I18n.send(:normalize_translation_keys, locale, key, scope, separator)
97
- keys.inject(translations) do |result, k|
98
- if (x = result[k.to_sym]).nil?
99
- return nil
100
- else
101
- x
102
- end
103
- end
104
- end
105
-
106
- # Evaluates defaults.
107
- # If given subject is an Array, it walks the array and returns the
108
- # first translation that can be resolved. Otherwise it tries to resolve
109
- # the translation directly.
110
- def default(locale, object, subject, options = {})
111
- options = options.dup.reject { |key, value| key == :default }
112
- case subject
113
- when Array
114
- subject.each do |subject|
115
- result = resolve(locale, object, subject, options) and return result
116
- end and nil
117
- else
118
- resolve(locale, object, subject, options)
119
- end
120
- end
121
-
122
- # Resolves a translation.
123
- # If the given subject is a Symbol, it will be translated with the
124
- # given options. If it is a Proc then it will be evaluated. All other
125
- # subjects will be returned directly.
126
- def resolve(locale, object, subject, options = {})
127
- case subject
128
- when Symbol
129
- translate(locale, subject, options)
130
- when Proc
131
- resolve(locale, object, subject.call(object, options), options = {})
132
- else
133
- subject
134
- end
135
- rescue MissingTranslationData
136
- nil
137
- end
138
-
139
- # Picks a translation from an array according to English pluralization
140
- # rules. It will pick the first translation if count is not equal to 1
141
- # and the second translation if it is equal to 1. Other backends can
142
- # implement more flexible or complex pluralization rules.
143
- def pluralize(locale, entry, count)
144
- return entry unless entry.is_a?(Hash) and count
145
-
146
- key = :zero if count == 0 && entry.has_key?(:zero)
147
- key ||= count == 1 ? :one : :other
148
- raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
149
- entry[key]
150
- end
151
-
152
- # Interpolates values into a given string.
153
- #
154
- # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
155
- # # => "file test.txt opened by {{user}}"
156
- #
157
- # Note that you have to double escape the <tt>\\</tt> when you want to escape
158
- # the <tt>{{...}}</tt> key in a string (once for the string and once for the
159
- # interpolation).
160
- def interpolate(locale, string, values = {})
161
- return string unless string.is_a?(String) && !values.empty?
162
-
163
- string.gsub(INTERPOLATION_SYNTAX_PATTERN) do
164
- escaped, key = $1, $2.to_sym
165
-
166
- if escaped
167
- key
168
- elsif RESERVED_KEYS.include?(key)
169
- raise ReservedInterpolationKey.new(key, string)
170
- else
171
- "%{#{key}}"
172
- end
173
- end % values
174
-
175
- rescue KeyError => e
176
- raise MissingInterpolationArgument.new(values, string)
177
- end
178
-
179
- # Loads a single translations file by delegating to #load_rb or
180
- # #load_yml depending on the file extension and directly merges the
181
- # data to the existing translations. Raises I18n::UnknownFileType
182
- # for all other file extensions.
183
- def load_file(filename)
184
- type = File.extname(filename).tr('.', '').downcase
185
- raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}")
186
- data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
187
- data.each { |locale, d| merge_translations(locale, d) }
188
- end
189
-
190
- # Loads a plain Ruby translations file. eval'ing the file must yield
191
- # a Hash containing translation data with locales as toplevel keys.
192
- def load_rb(filename)
193
- eval(IO.read(filename), binding, filename)
194
- end
195
-
196
- # Loads a YAML translations file. The data must have locales as
197
- # toplevel keys.
198
- def load_yml(filename)
199
- YAML::load(IO.read(filename))
200
- end
201
-
202
- # Deep merges the given translations hash with the existing translations
203
- # for the given locale
204
- def merge_translations(locale, data)
205
- locale = locale.to_sym
206
- translations[locale] ||= {}
207
- data = deep_symbolize_keys(data)
208
-
209
- # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
210
- merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
211
- translations[locale].merge!(data, &merger)
212
- end
213
-
214
- # Return a new hash with all keys and nested keys converted to symbols.
215
- def deep_symbolize_keys(hash)
216
- hash.inject({}) { |result, (key, value)|
217
- value = deep_symbolize_keys(value) if value.is_a?(Hash)
218
- result[(key.to_sym rescue key) || key] = value
219
- result
220
- }
221
- end
222
-
223
- # Flatten the given array once
224
- def flatten_once(array)
225
- result = []
226
- for element in array # a little faster than each
227
- result.push(*element)
228
- end
229
- result
230
- end
231
- end
232
- end
233
- end
1
+ require 'yaml'
2
+
3
+ module I18n
4
+ module Backend
5
+ class Simple
6
+ RESERVED_KEYS = [:scope, :default, :separator]
7
+ INTERPOLATION_SYNTAX_PATTERN = /(\\\\)?\{\{([^\}]+)\}\}/
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
+ format = format.dup if format
53
+
54
+ # TODO check which format strings are present, then bulk translate them, then replace them
55
+ format.gsub!(/%a/, translate(locale, :"date.abbr_day_names", :format => format)[object.wday]) if format.include?('%a')
56
+ format.gsub!(/%A/, translate(locale, :"date.day_names", :format => format)[object.wday]) if format.include?('%A')
57
+ format.gsub!(/%b/, translate(locale, :"date.abbr_month_names", :format => format)[object.mon]) if format.include?('%b')
58
+ format.gsub!(/%B/, translate(locale, :"date.month_names", :format => format)[object.mon]) if format.include?('%B')
59
+ format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}", :format => format)) if format.include?('%p') && object.respond_to?(:hour)
60
+
61
+ object.strftime(format)
62
+ end
63
+
64
+ def initialized?
65
+ @initialized ||= false
66
+ end
67
+
68
+ # Returns an array of locales for which translations are available
69
+ def available_locales
70
+ init_translations unless initialized?
71
+ translations.keys
72
+ end
73
+
74
+ def reload!
75
+ @initialized = false
76
+ @translations = nil
77
+ end
78
+
79
+ protected
80
+ def init_translations
81
+ load_translations(*I18n.load_path.flatten)
82
+ @initialized = true
83
+ end
84
+
85
+ def translations
86
+ @translations ||= {}
87
+ end
88
+
89
+ # Looks up a translation from the translations hash. Returns nil if
90
+ # eiher key is nil, or locale, scope or key do not exist as a key in the
91
+ # nested translations hash. Splits keys or scopes containing dots
92
+ # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
93
+ # <tt>%w(currency format)</tt>.
94
+ def lookup(locale, key, scope = [], separator = nil)
95
+ return unless key
96
+ init_translations unless initialized?
97
+ keys = I18n.send(:normalize_translation_keys, locale, key, scope, separator)
98
+ keys.inject(translations) do |result, k|
99
+ if (x = result[k.to_sym]).nil?
100
+ return nil
101
+ else
102
+ x
103
+ end
104
+ end
105
+ end
106
+
107
+ # Evaluates defaults.
108
+ # If given subject is an Array, it walks the array and returns the
109
+ # first translation that can be resolved. Otherwise it tries to resolve
110
+ # the translation directly.
111
+ def default(locale, object, subject, options = {})
112
+ options = options.dup.reject { |key, value| key == :default }
113
+ case subject
114
+ when Array
115
+ subject.each do |subject|
116
+ result = resolve(locale, object, subject, options) and return result
117
+ end and nil
118
+ else
119
+ resolve(locale, object, subject, options)
120
+ end
121
+ end
122
+
123
+ # Resolves a translation.
124
+ # If the given subject is a Symbol, it will be translated with the
125
+ # given options. If it is a Proc then it will be evaluated. All other
126
+ # subjects will be returned directly.
127
+ def resolve(locale, object, subject, options = {})
128
+ case subject
129
+ when Symbol
130
+ translate(locale, subject, options)
131
+ when Proc
132
+ resolve(locale, object, subject.call(object, options), options = {})
133
+ else
134
+ subject
135
+ end
136
+ rescue MissingTranslationData
137
+ nil
138
+ end
139
+
140
+ # Picks a translation from an array according to English pluralization
141
+ # rules. It will pick the first translation if count is not equal to 1
142
+ # and the second translation if it is equal to 1. Other backends can
143
+ # implement more flexible or complex pluralization rules.
144
+ def pluralize(locale, entry, count)
145
+ return entry unless entry.is_a?(Hash) and count
146
+
147
+ key = :zero if count == 0 && entry.has_key?(:zero)
148
+ key ||= count == 1 ? :one : :other
149
+ raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
150
+ entry[key]
151
+ end
152
+
153
+ # Interpolates values into a given string.
154
+ #
155
+ # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
156
+ # # => "file test.txt opened by {{user}}"
157
+ #
158
+ # Note that you have to double escape the <tt>\\</tt> when you want to escape
159
+ # the <tt>{{...}}</tt> key in a string (once for the string and once for the
160
+ # interpolation).
161
+ def interpolate(locale, string, values = {})
162
+ return string unless string.is_a?(String) && !values.empty?
163
+
164
+ string.gsub(INTERPOLATION_SYNTAX_PATTERN) do
165
+ escaped, key = $1, $2.to_sym
166
+
167
+ if escaped
168
+ key
169
+ elsif RESERVED_KEYS.include?(key)
170
+ raise ReservedInterpolationKey.new(key, string)
171
+ else
172
+ "%{#{key}}"
173
+ end
174
+ end % values
175
+
176
+ rescue KeyError => e
177
+ raise MissingInterpolationArgument.new(values, string)
178
+ end
179
+
180
+ # Loads a single translations file by delegating to #load_rb or
181
+ # #load_yml depending on the file extension and directly merges the
182
+ # data to the existing translations. Raises I18n::UnknownFileType
183
+ # for all other file extensions.
184
+ def load_file(filename)
185
+ type = File.extname(filename).tr('.', '').downcase
186
+ raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}")
187
+ data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
188
+ data.each { |locale, d| merge_translations(locale, d) }
189
+ end
190
+
191
+ # Loads a plain Ruby translations file. eval'ing the file must yield
192
+ # a Hash containing translation data with locales as toplevel keys.
193
+ def load_rb(filename)
194
+ eval(IO.read(filename), binding, filename)
195
+ end
196
+
197
+ # Loads a YAML translations file. The data must have locales as
198
+ # toplevel keys.
199
+ def load_yml(filename)
200
+ YAML::load(IO.read(filename))
201
+ end
202
+
203
+ # Deep merges the given translations hash with the existing translations
204
+ # for the given locale
205
+ def merge_translations(locale, data)
206
+ locale = locale.to_sym
207
+ translations[locale] ||= {}
208
+ data = deep_symbolize_keys(data)
209
+
210
+ # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
211
+ merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
212
+ translations[locale].merge!(data, &merger)
213
+ end
214
+
215
+ # Return a new hash with all keys and nested keys converted to symbols.
216
+ def deep_symbolize_keys(hash)
217
+ hash.inject({}) { |result, (key, value)|
218
+ value = deep_symbolize_keys(value) if value.is_a?(Hash)
219
+ result[(key.to_sym rescue key) || key] = value
220
+ result
221
+ }
222
+ end
223
+
224
+ # Flatten the given array once
225
+ def flatten_once(array)
226
+ result = []
227
+ for element in array # a little faster than each
228
+ result.push(*element)
229
+ end
230
+ result
231
+ end
232
+ end
233
+ end
234
+ end