Froze rails gems
[depot.git] / vendor / rails / activesupport / lib / active_support / vendor / i18n-0.0.1 / i18n / backend / simple.rb
1 require 'yaml'
2
3 module I18n
4 module Backend
5 class Simple
6 INTERPOLATION_RESERVED_KEYS = %w(scope default)
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 reserved = :scope, :default
29 count, scope, default = options.values_at(:count, *reserved)
30 options.delete(:default)
31 values = options.reject { |name, value| reserved.include?(name) }
32
33 entry = lookup(locale, key, scope)
34 if entry.nil?
35 entry = default(locale, default, options)
36 if entry.nil?
37 raise(I18n::MissingTranslationData.new(locale, key, options))
38 end
39 end
40 entry = pluralize(locale, entry, count)
41 entry = interpolate(locale, entry, values)
42 entry
43 end
44
45 # Acts the same as +strftime+, but returns a localized version of the
46 # formatted date string. Takes a key from the date/time formats
47 # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
48 def localize(locale, object, format = :default)
49 raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
50
51 type = object.respond_to?(:sec) ? 'time' : 'date'
52 # TODO only translate these if format is a String?
53 formats = translate(locale, :"#{type}.formats")
54 format = formats[format.to_sym] if formats && formats[format.to_sym]
55 # TODO raise exception unless format found?
56 format = format.to_s.dup
57
58 # TODO only translate these if the format string is actually present
59 # TODO check which format strings are present, then bulk translate then, then replace them
60 format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
61 format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
62 format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
63 format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
64 format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
65 object.strftime(format)
66 end
67
68 def initialized?
69 @initialized ||= false
70 end
71
72 def reload!
73 @initialized = false
74 @translations = nil
75 end
76
77 protected
78 def init_translations
79 load_translations(*I18n.load_path)
80 @initialized = true
81 end
82
83 def translations
84 @translations ||= {}
85 end
86
87 # Looks up a translation from the translations hash. Returns nil if
88 # eiher key is nil, or locale, scope or key do not exist as a key in the
89 # nested translations hash. Splits keys or scopes containing dots
90 # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
91 # <tt>%w(currency format)</tt>.
92 def lookup(locale, key, scope = [])
93 return unless key
94 init_translations unless initialized?
95 keys = I18n.send(:normalize_translation_keys, locale, key, scope)
96 keys.inject(translations) do |result, k|
97 if (x = result[k.to_sym]).nil?
98 return nil
99 else
100 x
101 end
102 end
103 end
104
105 # Evaluates a default translation.
106 # If the given default is a String it is used literally. If it is a Symbol
107 # it will be translated with the given options. If it is an Array the first
108 # translation yielded will be returned.
109 #
110 # <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if
111 # <tt>translate(locale, :foo)</tt> does not yield a result.
112 def default(locale, default, options = {})
113 case default
114 when String then default
115 when Symbol then translate locale, default, options
116 when Array then default.each do |obj|
117 result = default(locale, obj, options.dup) and return result
118 end and nil
119 end
120 rescue MissingTranslationData
121 nil
122 end
123
124 # Picks a translation from an array according to English pluralization
125 # rules. It will pick the first translation if count is not equal to 1
126 # and the second translation if it is equal to 1. Other backends can
127 # implement more flexible or complex pluralization rules.
128 def pluralize(locale, entry, count)
129 return entry unless entry.is_a?(Hash) and count
130 # raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
131 key = :zero if count == 0 && entry.has_key?(:zero)
132 key ||= count == 1 ? :one : :other
133 raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
134 entry[key]
135 end
136
137 # Interpolates values into a given string.
138 #
139 # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
140 # # => "file test.txt opened by {{user}}"
141 #
142 # Note that you have to double escape the <tt>\\</tt> when you want to escape
143 # the <tt>{{...}}</tt> key in a string (once for the string and once for the
144 # interpolation).
145 def interpolate(locale, string, values = {})
146 return string unless string.is_a?(String)
147
148 if string.respond_to?(:force_encoding)
149 original_encoding = string.encoding
150 string.force_encoding(Encoding::BINARY)
151 end
152
153 result = string.gsub(MATCH) do
154 escaped, pattern, key = $1, $2, $2.to_sym
155
156 if escaped
157 pattern
158 elsif INTERPOLATION_RESERVED_KEYS.include?(pattern)
159 raise ReservedInterpolationKey.new(pattern, string)
160 elsif !values.include?(key)
161 raise MissingInterpolationArgument.new(pattern, string)
162 else
163 values[key].to_s
164 end
165 end
166
167 result.force_encoding(original_encoding) if original_encoding
168 result
169 end
170
171 # Loads a single translations file by delegating to #load_rb or
172 # #load_yml depending on the file extension and directly merges the
173 # data to the existing translations. Raises I18n::UnknownFileType
174 # for all other file extensions.
175 def load_file(filename)
176 type = File.extname(filename).tr('.', '').downcase
177 raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}")
178 data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
179 data.each { |locale, d| merge_translations(locale, d) }
180 end
181
182 # Loads a plain Ruby translations file. eval'ing the file must yield
183 # a Hash containing translation data with locales as toplevel keys.
184 def load_rb(filename)
185 eval(IO.read(filename), binding, filename)
186 end
187
188 # Loads a YAML translations file. The data must have locales as
189 # toplevel keys.
190 def load_yml(filename)
191 YAML::load(IO.read(filename))
192 end
193
194 # Deep merges the given translations hash with the existing translations
195 # for the given locale
196 def merge_translations(locale, data)
197 locale = locale.to_sym
198 translations[locale] ||= {}
199 data = deep_symbolize_keys(data)
200
201 # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
202 merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
203 translations[locale].merge!(data, &merger)
204 end
205
206 # Return a new hash with all keys and nested keys converted to symbols.
207 def deep_symbolize_keys(hash)
208 hash.inject({}) { |result, (key, value)|
209 value = deep_symbolize_keys(value) if value.is_a? Hash
210 result[(key.to_sym rescue key) || key] = value
211 result
212 }
213 end
214 end
215 end
216 end