6 INTERPOLATION_RESERVED_KEYS
= %w(scope default
)
7 MATCH
= /(\\\\)?\{\{([^\}]+)\}\}/
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
12 def load_translations(*filenames
)
13 filenames
.each
{ |filename
| load_file(filename
) }
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
20 def store_translations(locale
, data)
21 merge_translations(locale
, data)
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
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
) }
33 entry
= lookup(locale
, key
, scope
)
35 entry
= default(locale
, default
, options
)
37 raise(I18n
::MissingTranslationData.new(locale
, key
, options
))
40 entry
= pluralize(locale
, entry
, count
)
41 entry
= interpolate(locale
, entry
, values
)
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)
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
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
)
69 @initialized ||= false
79 load_translations(*I18n
.load_path
)
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
= [])
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?
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.
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
= {})
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
120 rescue MissingTranslationData
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
)
137 # Interpolates values into a given string.
139 # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
140 # # => "file test.txt opened by {{user}}"
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
145 def interpolate(locale
, string
, values
= {})
146 return string
unless string
.is_a
?(String
)
148 if string
.respond_to
?(:force_encoding)
149 original_encoding
= string
.encoding
150 string
.force_encoding(Encoding
::BINARY)
153 result
= string
.gsub(MATCH
) do
154 escaped
, pattern
, key
= $1, $2, $2.to_sym
158 elsif INTERPOLATION_RESERVED_KEYS
.include?(pattern
)
159 raise ReservedInterpolationKey
.new(pattern
, string
)
160 elsif !values
.include?(key
)
161 raise MissingInterpolationArgument
.new(pattern
, string
)
167 result
.force_encoding(original_encoding
) if original_encoding
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
) }
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
)
188 # Loads a YAML translations file. The data must have locales as
190 def load_yml(filename
)
191 YAML
::load(IO
.read(filename
))
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)
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
)
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