i18n 0.1.0 → 0.2.0

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,193 +1,233 @@
1
- require 'strscan'
2
-
3
- module I18n
4
- module Backend
5
- class Simple
6
- # Allow client libraries to pass a block that populates the translation
7
- # storage. Decoupled for backends like a db backend that persist their
8
- # translations, so the backend can decide whether/when to yield or not.
9
- def populate(&block)
10
- yield
11
- end
12
-
13
- # Accepts a list of paths to translation files. Loads translations from
14
- # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
15
- # for details.
16
- def load_translations(*filenames)
17
- filenames.each {|filename| load_file filename }
18
- end
19
-
20
- # Stores translations for the given locale in memory.
21
- # This uses a deep merge for the translations hash, so existing
22
- # translations will be overwritten by new ones only at the deepest
23
- # level of the hash.
24
- def store_translations(locale, data)
25
- merge_translations(locale, data)
26
- end
27
-
28
- def translate(locale, key, options = {})
29
- raise InvalidLocale.new(locale) if locale.nil?
30
- return key.map{|k| translate locale, k, options } if key.is_a? Array
31
-
32
- reserved = :scope, :default
33
- count, scope, default = options.values_at(:count, *reserved)
34
- options.delete(:default)
35
- values = options.reject{|name, value| reserved.include? name }
36
-
37
- entry = lookup(locale, key, scope) || default(locale, default, options) || raise(I18n::MissingTranslationData.new(locale, key, options))
38
- entry = pluralize locale, entry, count
39
- entry = interpolate locale, entry, values
40
- entry
41
- end
42
-
43
- # Acts the same as +strftime+, but returns a localized version of the
44
- # formatted date string. Takes a key from the date/time formats
45
- # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
46
- def localize(locale, object, format = :default)
47
- raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
48
-
49
- type = object.respond_to?(:sec) ? 'time' : 'date'
50
- formats = translate(locale, :"#{type}.formats")
51
- format = formats[format.to_sym] if formats && formats[format.to_sym]
52
- # TODO raise exception unless format found?
53
- format = format.to_s.dup
54
-
55
- format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
56
- format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
57
- format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
58
- format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
59
- format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
60
- object.strftime(format)
61
- end
62
-
63
- protected
64
-
65
- def translations
66
- @translations ||= {}
67
- end
68
-
69
- # Looks up a translation from the translations hash. Returns nil if
70
- # eiher key is nil, or locale, scope or key do not exist as a key in the
71
- # nested translations hash. Splits keys or scopes containing dots
72
- # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
73
- # <tt>%w(currency format)</tt>.
74
- def lookup(locale, key, scope = [])
75
- return unless key
76
- keys = I18n.send :normalize_translation_keys, locale, key, scope
77
- keys.inject(translations){|result, k| result[k.to_sym] or return nil }
78
- end
79
-
80
- # Evaluates a default translation.
81
- # If the given default is a String it is used literally. If it is a Symbol
82
- # it will be translated with the given options. If it is an Array the first
83
- # translation yielded will be returned.
84
- #
85
- # <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if
86
- # <tt>translate(locale, :foo)</tt> does not yield a result.
87
- def default(locale, default, options = {})
88
- case default
89
- when String then default
90
- when Symbol then translate locale, default, options
91
- when Array then default.each do |obj|
92
- result = default(locale, obj, options.dup) and return result
93
- end and nil
94
- end
95
- rescue MissingTranslationData
96
- nil
97
- end
98
-
99
- # Picks a translation from an array according to English pluralization
100
- # rules. It will pick the first translation if count is not equal to 1
101
- # and the second translation if it is equal to 1. Other backends can
102
- # implement more flexible or complex pluralization rules.
103
- def pluralize(locale, entry, count)
104
- return entry unless entry.is_a?(Hash) and count
105
- # raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
106
- key = :zero if count == 0 && entry.has_key?(:zero)
107
- key ||= count == 1 ? :one : :other
108
- raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
109
- entry[key]
110
- end
111
-
112
- # Interpolates values into a given string.
113
- #
114
- # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
115
- # # => "file test.txt opened by {{user}}"
116
- #
117
- # Note that you have to double escape the <tt>\\</tt> when you want to escape
118
- # the <tt>{{...}}</tt> key in a string (once for the string and once for the
119
- # interpolation).
120
- def interpolate(locale, string, values = {})
121
- return string if !string.is_a?(String)
122
-
123
- string = string.gsub(/%d/, '{{count}}').gsub(/%s/, '{{value}}')
124
- if string.respond_to?(:force_encoding)
125
- original_encoding = string.encoding
126
- string.force_encoding(Encoding::BINARY)
127
- end
128
- s = StringScanner.new(string)
129
-
130
- while s.skip_until(/\{\{/)
131
- s.string[s.pos - 3, 1] = '' and next if s.pre_match[-1, 1] == '\\'
132
- start_pos = s.pos - 2
133
- key = s.scan_until(/\}\}/)[0..-3]
134
- end_pos = s.pos - 1
135
-
136
- raise ReservedInterpolationKey.new(key, string) if %w(scope default).include?(key)
137
- raise MissingInterpolationArgument.new(key, string) unless values.has_key? key.to_sym
138
-
139
- s.string[start_pos..end_pos] = values[key.to_sym].to_s
140
- s.unscan
141
- end
142
-
143
- result = s.string
144
- result.force_encoding(original_encoding) if original_encoding
145
- result
146
- end
147
-
148
- # Loads a single translations file by delegating to #load_rb or
149
- # #load_yml depending on the file extension and directly merges the
150
- # data to the existing translations. Raises I18n::UnknownFileType
151
- # for all other file extensions.
152
- def load_file(filename)
153
- type = File.extname(filename).tr('.', '').downcase
154
- raise UnknownFileType.new(type, filename) unless respond_to? :"load_#{type}"
155
- data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
156
- data.each{|locale, d| merge_translations locale, d }
157
- end
158
-
159
- # Loads a plain Ruby translations file. eval'ing the file must yield
160
- # a Hash containing translation data with locales as toplevel keys.
161
- def load_rb(filename)
162
- eval IO.read(filename), binding, filename
163
- end
164
-
165
- # Loads a YAML translations file. The data must have locales as
166
- # toplevel keys.
167
- def load_yml(filename)
168
- YAML::load IO.read(filename)
169
- end
170
-
171
- # Deep merges the given translations hash with the existing translations
172
- # for the given locale
173
- def merge_translations(locale, data)
174
- locale = locale.to_sym
175
- translations[locale] ||= {}
176
- data = deep_symbolize_keys data
177
-
178
- # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
179
- merger = proc{|key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
180
- translations[locale].merge! data, &merger
181
- end
182
-
183
- # Return a new hash with all keys and nested keys converted to symbols.
184
- def deep_symbolize_keys(hash)
185
- hash.inject({}){|result, (key, value)|
186
- value = deep_symbolize_keys(value) if value.is_a? Hash
187
- result[(key.to_sym rescue key) || key] = value
188
- result
189
- }
190
- end
191
- end
192
- end
193
- 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
+
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,53 +1,59 @@
1
- module I18n
2
- class ArgumentError < ::ArgumentError; end
3
-
4
- class InvalidLocale < ArgumentError
5
- attr_reader :locale
6
- def initialize(locale)
7
- @locale = locale
8
- super "#{locale.inspect} is not a valid locale"
9
- end
10
- end
11
-
12
- class MissingTranslationData < ArgumentError
13
- attr_reader :locale, :key, :options
14
- def initialize(locale, key, options)
15
- @key, @locale, @options = key, locale, options
16
- keys = I18n.send(:normalize_translation_keys, locale, key, options[:scope])
17
- keys << 'no key' if keys.size < 2
18
- super "translation missing: #{keys.join(', ')}"
19
- end
20
- end
21
-
22
- class InvalidPluralizationData < ArgumentError
23
- attr_reader :entry, :count
24
- def initialize(entry, count)
25
- @entry, @count = entry, count
26
- super "translation data #{entry.inspect} can not be used with :count => #{count}"
27
- end
28
- end
29
-
30
- class MissingInterpolationArgument < ArgumentError
31
- attr_reader :key, :string
32
- def initialize(key, string)
33
- @key, @string = key, string
34
- super "interpolation argument #{key} missing in #{string.inspect}"
35
- end
36
- end
37
-
38
- class ReservedInterpolationKey < ArgumentError
39
- attr_reader :key, :string
40
- def initialize(key, string)
41
- @key, @string = key, string
42
- super "reserved key #{key.inspect} used in #{string.inspect}"
43
- end
44
- end
45
-
46
- class UnknownFileType < ArgumentError
47
- attr_reader :type, :filename
48
- def initialize(type, filename)
49
- @type, @filename = type, filename
50
- super "can not load translations from #{filename}, the file type #{type} is not known"
51
- end
52
- end
53
- end
1
+ class KeyError < IndexError
2
+ def initialize(message = nil)
3
+ super(message || "key not found")
4
+ end
5
+ end unless defined?(KeyError)
6
+
7
+ module I18n
8
+ class ArgumentError < ::ArgumentError; end
9
+
10
+ class InvalidLocale < ArgumentError
11
+ attr_reader :locale
12
+ def initialize(locale)
13
+ @locale = locale
14
+ super "#{locale.inspect} is not a valid locale"
15
+ end
16
+ end
17
+
18
+ class MissingTranslationData < ArgumentError
19
+ attr_reader :locale, :key, :options
20
+ def initialize(locale, key, options)
21
+ @key, @locale, @options = key, locale, options
22
+ keys = I18n.send(:normalize_translation_keys, locale, key, options[:scope])
23
+ keys << 'no key' if keys.size < 2
24
+ super "translation missing: #{keys.join(', ')}"
25
+ end
26
+ end
27
+
28
+ class InvalidPluralizationData < ArgumentError
29
+ attr_reader :entry, :count
30
+ def initialize(entry, count)
31
+ @entry, @count = entry, count
32
+ super "translation data #{entry.inspect} can not be used with :count => #{count}"
33
+ end
34
+ end
35
+
36
+ class MissingInterpolationArgument < ArgumentError
37
+ attr_reader :values, :string
38
+ def initialize(values, string)
39
+ @values, @string = values, string
40
+ super "missing interpolation argument in #{string.inspect} (#{values.inspect} given)"
41
+ end
42
+ end
43
+
44
+ class ReservedInterpolationKey < ArgumentError
45
+ attr_reader :key, :string
46
+ def initialize(key, string)
47
+ @key, @string = key, string
48
+ super "reserved key #{key.inspect} used in #{string.inspect}"
49
+ end
50
+ end
51
+
52
+ class UnknownFileType < ArgumentError
53
+ attr_reader :type, :filename
54
+ def initialize(type, filename)
55
+ @type, @filename = type, filename
56
+ super "can not load translations from #{filename}, the file type #{type} is not known"
57
+ end
58
+ end
59
+ end