thedarkone-i18n 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. data/CHANGELOG.textile +57 -0
  2. data/README.textile +43 -9
  3. data/Rakefile +21 -0
  4. data/VERSION +1 -0
  5. data/lib/i18n.rb +87 -16
  6. data/lib/i18n/backend/base.rb +251 -0
  7. data/lib/i18n/backend/cache.rb +71 -0
  8. data/lib/i18n/backend/chain.rb +64 -0
  9. data/lib/i18n/backend/fallbacks.rb +53 -0
  10. data/lib/i18n/backend/fast.rb +53 -22
  11. data/lib/i18n/backend/fast/interpolation_compiler.rb +84 -0
  12. data/lib/i18n/backend/gettext.rb +65 -0
  13. data/lib/i18n/backend/lazy_reloading.rb +60 -0
  14. data/lib/i18n/backend/pluralization.rb +56 -0
  15. data/lib/i18n/backend/simple.rb +17 -240
  16. data/lib/i18n/exceptions.rb +13 -5
  17. data/lib/i18n/gettext.rb +25 -0
  18. data/lib/i18n/helpers/gettext.rb +35 -0
  19. data/lib/i18n/locale/fallbacks.rb +100 -0
  20. data/lib/i18n/locale/tag.rb +27 -0
  21. data/lib/i18n/locale/tag/parents.rb +24 -0
  22. data/lib/i18n/locale/tag/rfc4646.rb +78 -0
  23. data/lib/i18n/locale/tag/simple.rb +44 -0
  24. data/test/all.rb +5 -7
  25. data/test/api/basics.rb +15 -0
  26. data/test/api/interpolation.rb +85 -0
  27. data/test/api/lambda.rb +52 -0
  28. data/test/api/link.rb +47 -0
  29. data/test/api/localization/date.rb +65 -0
  30. data/test/api/localization/date_time.rb +63 -0
  31. data/test/api/localization/lambda.rb +26 -0
  32. data/test/api/localization/time.rb +63 -0
  33. data/test/api/pluralization.rb +37 -0
  34. data/test/api/translation.rb +51 -0
  35. data/test/backend/cache/cache_test.rb +57 -0
  36. data/test/backend/chain/api_test.rb +80 -0
  37. data/test/backend/chain/chain_test.rb +64 -0
  38. data/test/backend/fallbacks/api_test.rb +79 -0
  39. data/test/backend/fallbacks/fallbacks_test.rb +29 -0
  40. data/test/backend/fast/all.rb +5 -0
  41. data/test/backend/fast/api_test.rb +91 -0
  42. data/test/backend/fast/interpolation_compiler_test.rb +84 -0
  43. data/test/backend/fast/lookup_test.rb +24 -0
  44. data/test/backend/fast/setup.rb +22 -0
  45. data/test/backend/fast/translations_test.rb +89 -0
  46. data/test/backend/lazy_reloading/reloading_test.rb +67 -0
  47. data/test/backend/pluralization/api_test.rb +81 -0
  48. data/test/backend/pluralization/pluralization_test.rb +39 -0
  49. data/test/backend/simple/all.rb +5 -0
  50. data/test/backend/simple/api_test.rb +90 -0
  51. data/test/backend/simple/lookup_test.rb +24 -0
  52. data/test/backend/simple/setup.rb +151 -0
  53. data/test/backend/simple/translations_test.rb +89 -0
  54. data/test/fixtures/locales/de.po +61 -0
  55. data/test/fixtures/locales/en.rb +3 -0
  56. data/test/fixtures/locales/en.yml +3 -0
  57. data/test/fixtures/locales/plurals.rb +112 -0
  58. data/test/gettext/api_test.rb +78 -0
  59. data/test/gettext/backend_test.rb +35 -0
  60. data/test/i18n_exceptions_test.rb +6 -25
  61. data/test/i18n_load_path_test.rb +23 -0
  62. data/test/i18n_test.rb +56 -18
  63. data/test/locale/fallbacks_test.rb +128 -0
  64. data/test/locale/tag/rfc4646_test.rb +147 -0
  65. data/test/locale/tag/simple_test.rb +35 -0
  66. data/test/test_helper.rb +72 -0
  67. data/test/with_options.rb +34 -0
  68. metadata +109 -19
  69. data/i18n.gemspec +0 -31
  70. data/lib/i18n/backend/fast/pluralization_compiler.rb +0 -50
  71. data/test/backend_test.rb +0 -633
  72. data/test/fast_backend_test.rb +0 -34
  73. data/test/locale/en.rb +0 -1
  74. data/test/locale/en.yml +0 -3
  75. data/test/pluralization_compiler_test.rb +0 -35
data/CHANGELOG.textile ADDED
@@ -0,0 +1,57 @@
1
+ h1. Changelog
2
+
3
+ h2. master
4
+
5
+ * (no changes)
6
+
7
+ h2. 0.2.0 (2009-07-12)
8
+
9
+ * "Allow using Ruby 1.9 syntax for string interpolation (API addition)":http://github.com/svenfuchs/i18n/commit/c6e0b06d512f2af57199a843a1d8a40241b32861
10
+ * "Allow configuring the default scope separator, allow to pass a custom scope separator(API addition)":http://github.com/svenfuchs/i18n/commit/5b75bfbc348061adc11e3790187a187275bfd471 (e.g. I18n.t(:'foo|bar', :separator => '|')
11
+ * "Pass :format option to #translate for #localize more useful lambda support":http://github.com/svenfuchs/i18n/commit/e277711b3c844fe7589b8d3f9af0f7d1b969a273
12
+ * "Refactor Simple backend #resolve to #default and #resolve for more consistency. Now allows to pass lambdas as defaults and re-resolve Symbols":http://github.com/svenfuchs/i18n/commit/8c4ce3d923ce5fa73e973fe28217e18165549aba
13
+ * "Add lambda support to #translate (API addition)":http://github.com/svenfuchs/i18n/commit/c90e62d8f7d3d5b78f34cfe328d871b58884f115
14
+ * "Add lambda support to #localize (API addition)":http://github.com/svenfuchs/i18n/commit/9d390afcf33f3f469bb95e6888147152f6cc7442
15
+
16
+ h2. 0.1.3 (2009-02-27)
17
+
18
+ * "Remove unnecessary string encoding handling in the i18n simple backend which made the backend break on Ruby 1.9":http://github.com/svenfuchs/i18n/commit/4c3a970783861a94f2e89f46714fb3434e4f4f8d
19
+
20
+ h2. 0.1.2 (2009-01-09)
21
+
22
+ * "added #available_locales (returns an array of locales for which translations are available)":http://github.com/svenfuchs/i18n/commit/411f8fe7c8f3f89e9b6b921fa62ed66cb92f3af4
23
+ * "flatten load_path before using it so that a nested array of paths won't throw up":http://github.com/svenfuchs/i18n/commit/d473a068a2b90aba98135deb225d6eb6d8104d70
24
+
25
+ h2. 0.1.1 (2008-11-20)
26
+
27
+ * "Use :'en' as a default locale (in favor of :'en-US')":http://github.com/svenfuchs/i18n/commit/c4b10b246aecf7da78cb2568dd0d2ab7e6b8a230
28
+ * "Add #reload! to Simple backend":http://github.com/svenfuchs/i18n/commit/36dd2bd9973b9e1559728749a9daafa44693e964
29
+
30
+ h2. 0.1.0 (2008-10-25)
31
+
32
+ * "Fix Simple backend to distinguish false from nil values":http://github.com/svenfuchs/i18n/commit/39d9a47da14b5f3ba126af48923af8c30e135166
33
+ * "Add #load_path to public api, add initialize to simple backend and remove #load_translations from public api":http://github.com/svenfuchs/i18n/commit/c4c5649e6bc8f020f1aaf5a5470bde048e22c82d
34
+ * "Speed up Backend::Simple#interpolate":http://github.com/svenfuchs/i18n/commit/9e1ac6bf8833304e036323ec9932b9f33c468a35
35
+ * "Remove #populate and #store_translations from public API":http://github.com/svenfuchs/i18n/commit/f4e514a80be7feb509f66824ee311905e2940900
36
+ * "Use :other instead of :many as a plural key":http://github.com/svenfuchs/i18n/commit/0f8f20a2552bf6a2aa758d8fdd62a7154e4a1bf6
37
+ * "Use a class instead of a module for Simple backend":http://github.com/svenfuchs/i18n/commit/08f051aa61320c17debde24a83268bc74e33b995
38
+ * "Make Simple backend #interpolate deal with non-ASCII string encodings":http://github.com/svenfuchs/i18n/commit/d84a3f3f55543c084d5dc5d1fed613b8df148789
39
+ * "Fix default arrays of non-existant keys returning the default array":http://github.com/svenfuchs/i18n/commit/6c04ca86c87f97dc78f07c2a4023644e5ba8b839
40
+
41
+ h2. Initial implementation (June/July 2008)
42
+
43
+ Initial implementation by "Sven Fuchs":http://www.workingwithrails.com/person/9963-sven-fuchs based on previous discussion/consensus of the rails-i18n team (alphabetical order) and many others:
44
+
45
+ * "Matt Aimonetti":http://railsontherun.com
46
+ * "Sven Fuchs":http://www.workingwithrails.com/person/9963-sven-fuchs
47
+ * "Joshua Harvey":http://www.workingwithrails.com/person/759-joshua-harvey
48
+ * "Saimon Moore":http://saimonmoore.net
49
+ * "Stephan Soller":http://www.arkanis-development.de
50
+
51
+ h2. More information
52
+
53
+ * "Homepage":http://rails-i18n.org
54
+ * "Wiki":http://rails-i18n.org/wiki
55
+ * "Mailinglist":http://groups.google.com/group/rails-i18n
56
+ * "About the project/history":http://www.artweb-design.de/2008/7/18/finally-ruby-on-rails-gets-internationalized
57
+ * "Initial API Intro":http://www.artweb-design.de/2008/7/18/the-ruby-on-rails-i18n-core-api
data/README.textile CHANGED
@@ -1,20 +1,54 @@
1
- h1. Ruby I18n gem
1
+ h1. Ruby I18n
2
2
 
3
- I18n and localization solution for Ruby.
3
+ Ruby Internationalization and localization solution.
4
4
 
5
- For information please refer to http://rails-i18n.org
5
+ Features:
6
+
7
+ * translation and localization
8
+ * interpolation of values to translations (Ruby 1.9 compatible syntax)
9
+ * pluralization (CLDR compatible)
10
+ * flexible defaults
11
+ * bulk lookup
12
+ * lambdas as translation data
13
+ * custom key/scope separator
14
+ * custom exception handlers
15
+ * extensible architecture with a swappable backend
16
+
17
+ Experimental, pluggable features:
18
+
19
+ * lambda pluralizers stored as translation data
20
+ * RFC4647 compliant locale fallbacks (with optional RFC4646 locale validation)
21
+ * backend cache
22
+
23
+ For more information and lots of resources see: "http://rails-i18n.org/wiki":http://rails-i18n.org/wiki
24
+
25
+ h2. Fast backend
26
+
27
+ This fork includes a @Fast@ backend that optimizes for the most common lookups and makes them about 20x (for @t(:'foo.bar.baz')@) or 6x (for @t(:'foo.bar.baz', :a=>'A', :b=>'B')@) times faster than the original @Simple@ backend. The @Fast@ backend is slightly less memory efficient and is slower to @reload!@, but should be a great choice for production environment.
28
+
29
+ h3. Branches
30
+
31
+ "2-3-stable":http://github.com/thedarkone/i18n/tree/2-3-stable branch is for the old @I18n@ gem shipped with Rails 2.3
32
+ "master":http://github.com/thedarkone/i18n/tree/master is for the current @I18n@ gem version
33
+
34
+ h3. Usage
35
+
36
+ Clone the repo as a plugin into your Rails app.
37
+
38
+ <pre>
39
+ # in initializer or enviroment.rb
40
+ I18n.backend = I18n::Backend::Fast.new
41
+ </pre>
6
42
 
7
43
  h2. Authors
8
44
 
9
- * "Matt Aimonetti":http://railsontherun.com
10
45
  * "Sven Fuchs":http://www.artweb-design.de
11
46
  * "Joshua Harvey":http://www.workingwithrails.com/person/759-joshua-harvey
12
- * "Saimon Moore":http://saimonmoore.net
13
47
  * "Stephan Soller":http://www.arkanis-development.de
48
+ * "Saimon Moore":http://saimonmoore.net
49
+ * "Matt Aimonetti":http://railsontherun.com
50
+ * "thedarkone":http://github.com/thedarkone (@Fast@ backend)
14
51
 
15
52
  h2. License
16
53
 
17
- MIT License. See the included MIT-LICENCE file.
18
-
19
-
20
-
54
+ MIT License. See the included MIT-LICENCE file.
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ task :default => [:test]
2
+
3
+ task :test do
4
+ ruby "test/all.rb"
5
+ end
6
+
7
+ begin
8
+ require 'jeweler'
9
+ Jeweler::Tasks.new do |s|
10
+ s.name = "i18n"
11
+ s.rubyforge_project = "i18n"
12
+ s.summary = "New wave Internationalization support for Ruby"
13
+ s.email = "rails-i18n@googlegroups.com"
14
+ s.homepage = "http://rails-i18n.org"
15
+ s.description = "Add Internationalization support to your Ruby application."
16
+ s.authors = ['Sven Fuchs', 'Joshua Harvey', 'Matt Aimonetti', 'Stephan Soller', 'Saimon Moore']
17
+ s.files = FileList["[A-Z]*", "{lib,test}/**/*"]
18
+ end
19
+ rescue LoadError
20
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
21
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
data/lib/i18n.rb CHANGED
@@ -1,19 +1,23 @@
1
+ # encoding: utf-8
2
+
1
3
  # Authors:: Matt Aimonetti (http://railsontherun.com/),
2
4
  # Sven Fuchs (http://www.artweb-design.de),
3
5
  # Joshua Harvey (http://www.workingwithrails.com/person/759-joshua-harvey),
4
6
  # Saimon Moore (http://saimonmoore.net),
5
- # Stephan Soller (http://www.arkanis-development.de/)
7
+ # Stephan Soller (http://www.arkanis-development.de/),
8
+ # thedarkone (http://github.com/thedarkone)
6
9
  # Copyright:: Copyright (c) 2008 The Ruby i18n Team
7
10
  # License:: MIT
8
11
  require 'i18n/backend/simple'
9
12
  require 'i18n/backend/fast'
10
- require 'i18n/backend/fast/pluralization_compiler'
11
13
  require 'i18n/exceptions'
14
+ require 'i18n/string'
12
15
 
13
16
  module I18n
14
17
  @@backend = nil
15
18
  @@load_path = nil
16
- @@default_locale = :'en'
19
+ @@default_locale = :en
20
+ @@default_separator = '.'
17
21
  @@exception_handler = :default_exception_handler
18
22
 
19
23
  class << self
@@ -34,7 +38,7 @@ module I18n
34
38
 
35
39
  # Sets the current default locale. Used to set a custom default locale.
36
40
  def default_locale=(locale)
37
- @@default_locale = locale
41
+ @@default_locale = locale.to_sym rescue nil
38
42
  end
39
43
 
40
44
  # Returns the current locale. Defaults to I18n.default_locale.
@@ -44,12 +48,29 @@ module I18n
44
48
 
45
49
  # Sets the current locale pseudo-globally, i.e. in the Thread.current hash.
46
50
  def locale=(locale)
47
- Thread.current[:locale] = locale
51
+ Thread.current[:locale] = locale.to_sym rescue nil
48
52
  end
49
53
 
50
- # Returns an array of locales for which translations are available
54
+ # Returns an array of locales for which translations are available.
55
+ # Unless you explicitely set the these through I18n.available_locales=
56
+ # the call will be delegated to the backend and memoized on the I18n module.
51
57
  def available_locales
52
- backend.available_locales
58
+ @@available_locales ||= backend.available_locales
59
+ end
60
+
61
+ # Sets the available locales.
62
+ def available_locales=(locales)
63
+ @@available_locales = locales
64
+ end
65
+
66
+ # Returns the current default scope separator. Defaults to '.'
67
+ def default_separator
68
+ @@default_separator
69
+ end
70
+
71
+ # Sets the current default scope separator.
72
+ def default_separator=(separator)
73
+ @@default_separator = separator
53
74
  end
54
75
 
55
76
  # Sets the exception handler.
@@ -152,7 +173,7 @@ module I18n
152
173
  # or <tt>default</tt> if no translations for <tt>:foo</tt> and <tt>:bar</tt> were found.
153
174
  # I18n.t :foo, :default => [:bar, 'default']
154
175
  #
155
- # <b>BULK LOOKUP</b>
176
+ # *BULK LOOKUP*
156
177
  #
157
178
  # This returns an array with the translations for <tt>:foo</tt> and <tt>:bar</tt>.
158
179
  # I18n.t [:foo, :bar]
@@ -162,15 +183,38 @@ module I18n
162
183
  #
163
184
  # Which is the same as using a scope option:
164
185
  # I18n.t [:foo, :bar], :scope => :baz
165
- def translate(key, options = nil)
166
- locale = (options && options.delete(:locale)) || I18n.locale
186
+ #
187
+ # *LAMBDAS*
188
+ #
189
+ # Both translations and defaults can be given as Ruby lambdas. Lambdas will be
190
+ # called and passed the key and options.
191
+ #
192
+ # E.g. assuming the key <tt>:salutation</tt> resolves to:
193
+ # lambda { |key, options| options[:gender] == 'm' ? "Mr. {{options[:name]}}" : "Mrs. {{options[:name]}}" }
194
+ #
195
+ # Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith') will result in "Mrs. Smith".
196
+ #
197
+ # It is recommended to use/implement lambdas in an "idempotent" way. E.g. when
198
+ # a cache layer is put in front of I18n.translate it will generate a cache key
199
+ # from the argument values passed to #translate. Therefor your lambdas should
200
+ # always return the same translations/values per unique combination of argument
201
+ # values.
202
+ def translate(*args)
203
+ options = args.last.is_a?(Hash) ? args.pop : {}
204
+ key = args.shift
205
+ locale = options.delete(:locale) || I18n.locale
167
206
  backend.translate(locale, key, options)
168
- rescue I18n::ArgumentError => e
169
- raise e if options && options[:raise]
170
- send(@@exception_handler, e, locale, key, options || {})
207
+ rescue I18n::ArgumentError => exception
208
+ raise exception if options[:raise]
209
+ handle_exception(exception, locale, key, options)
171
210
  end
172
211
  alias :t :translate
173
212
 
213
+ def translate!(key, options = {})
214
+ translate(key, options.merge( :raise => true ))
215
+ end
216
+ alias :t! :translate!
217
+
174
218
  # Localizes certain objects, such as dates and numbers to local formatting.
175
219
  def localize(object, options = {})
176
220
  locale = options[:locale] || I18n.locale
@@ -180,6 +224,7 @@ module I18n
180
224
  alias :l :localize
181
225
 
182
226
  protected
227
+
183
228
  # Handles exceptions raised in the backend. All exceptions except for
184
229
  # MissingTranslationData exceptions are re-raised. When a MissingTranslationData
185
230
  # was caught and the option :raise is not set the handler returns an error
@@ -189,12 +234,38 @@ module I18n
189
234
  raise exception
190
235
  end
191
236
 
237
+ # Any exceptions thrown in translate will be sent to the @@exception_handler
238
+ # which can be a Symbol, a Proc or any other Object.
239
+ #
240
+ # If exception_handler is a Symbol then it will simply be sent to I18n as
241
+ # a method call. A Proc will simply be called. In any other case the
242
+ # method #call will be called on the exception_handler object.
243
+ #
244
+ # Examples:
245
+ #
246
+ # I18n.exception_handler = :default_exception_handler # this is the default
247
+ # I18n.default_exception_handler(exception, locale, key, options) # will be called like this
248
+ #
249
+ # I18n.exception_handler = lambda { |*args| ... } # a lambda
250
+ # I18n.exception_handler.call(exception, locale, key, options) # will be called like this
251
+ #
252
+ # I18n.exception_handler = I18nExceptionHandler.new # an object
253
+ # I18n.exception_handler.call(exception, locale, key, options) # will be called like this
254
+ def handle_exception(exception, locale, key, options)
255
+ case @@exception_handler
256
+ when Symbol
257
+ send(@@exception_handler, exception, locale, key, options)
258
+ else
259
+ @@exception_handler.call(exception, locale, key, options)
260
+ end
261
+ end
262
+
192
263
  # Merges the given locale, key and scope into a single array of keys.
193
264
  # Splits keys that contain dots into multiple keys. Makes sure all
194
265
  # keys are Symbols.
195
- def normalize_translation_keys(locale, key, scope)
196
- keys = [locale] + Array(scope) + [key]
197
- keys = keys.map { |k| k.to_s.split(/\./) }
266
+ def normalize_translation_keys(locale, key, scope, separator = nil)
267
+ keys = [locale] + Array(scope) + Array(key)
268
+ keys = keys.map { |k| k.to_s.split(separator || I18n.default_separator) }
198
269
  keys.flatten.map { |k| k.to_sym }
199
270
  end
200
271
  end
@@ -0,0 +1,251 @@
1
+ # encoding: utf-8
2
+
3
+ require 'yaml'
4
+
5
+ module I18n
6
+ module Backend
7
+ class Base
8
+ RESERVED_KEYS = [:scope, :default, :separator]
9
+ INTERPOLATION_SYNTAX_PATTERN = /(\\)?\{\{([^\}]+)\}\}/
10
+
11
+ # Accepts a list of paths to translation files. Loads translations from
12
+ # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
13
+ # for details.
14
+ def load_translations(*filenames)
15
+ filenames.each { |filename| load_file(filename) }
16
+ end
17
+
18
+ # Stores translations for the given locale in memory.
19
+ # This uses a deep merge for the translations hash, so existing
20
+ # translations will be overwritten by new ones only at the deepest
21
+ # level of the hash.
22
+ def store_translations(locale, data)
23
+ merge_translations(locale, data)
24
+ end
25
+
26
+ def translate(locale, key, options = {})
27
+ raise InvalidLocale.new(locale) if locale.nil?
28
+ return key.map { |k| translate(locale, k, options) } if key.is_a?(Array)
29
+
30
+ count, scope, default, separator = options.values_at(:count, *RESERVED_KEYS)
31
+ values = options.reject { |name, value| RESERVED_KEYS.include?(name) }
32
+
33
+ entry = lookup(locale, key, scope, separator)
34
+ entry = entry.nil? ? default(locale, key, default, options) : resolve(locale, key, entry, options)
35
+
36
+ raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil?
37
+ entry = pluralize(locale, entry, count)
38
+ entry = interpolate(locale, entry, values)
39
+ entry
40
+ end
41
+
42
+ # Acts the same as +strftime+, but uses a localized version of the
43
+ # format string. Takes a key from the date/time formats translations as
44
+ # a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
45
+ def localize(locale, object, format = :default, options = {})
46
+ raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
47
+
48
+ if Symbol === format
49
+ key = format
50
+ type = object.respond_to?(:sec) ? 'time' : 'date'
51
+ format = lookup(locale, :"#{type}.formats.#{key}")
52
+ raise(MissingTranslationData.new(locale, key, options)) if format.nil?
53
+ end
54
+
55
+ format = resolve(locale, object, format, options)
56
+ format = format.to_s.gsub(/%[aAbBp]/) do |match|
57
+ case match
58
+ when '%a' then I18n.t(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
59
+ when '%A' then I18n.t(:"date.day_names", :locale => locale, :format => format)[object.wday]
60
+ when '%b' then I18n.t(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
61
+ when '%B' then I18n.t(:"date.month_names", :locale => locale, :format => format)[object.mon]
62
+ when '%p' then I18n.t(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format) if object.respond_to? :hour
63
+ end
64
+ end
65
+
66
+ object.strftime(format)
67
+ end
68
+
69
+ def initialized?
70
+ @initialized ||= false
71
+ end
72
+
73
+ # Returns an array of locales for which translations are available
74
+ # ignoring the reserved translation meta data key :i18n.
75
+ def available_locales
76
+ init_translations unless initialized?
77
+ translations.inject([]) do |locales, (locale, data)|
78
+ locales << locale unless (data.keys - [:i18n]).empty?
79
+ locales
80
+ end
81
+ end
82
+
83
+ def reload!
84
+ @initialized = false
85
+ @translations = nil
86
+ end
87
+
88
+ protected
89
+ def init_translations
90
+ load_translations(*I18n.load_path.flatten)
91
+ @initialized = true
92
+ end
93
+
94
+ def translations
95
+ @translations ||= {}
96
+ end
97
+
98
+ # Looks up a translation from the translations hash. Returns nil if
99
+ # eiher key is nil, or locale, scope or key do not exist as a key in the
100
+ # nested translations hash. Splits keys or scopes containing dots
101
+ # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
102
+ # <tt>%w(currency format)</tt>.
103
+ def lookup(locale, key, scope = [], separator = nil)
104
+ return unless key
105
+ init_translations unless initialized?
106
+ keys = I18n.send(:normalize_translation_keys, locale, key, scope, separator)
107
+ keys.inject(translations) do |result, key|
108
+ key = key.to_sym
109
+ if result.respond_to?(:has_key?) and result.has_key?(key)
110
+ result[key]
111
+ else
112
+ return nil
113
+ end
114
+ end
115
+ end
116
+
117
+ # Evaluates defaults.
118
+ # If given subject is an Array, it walks the array and returns the
119
+ # first translation that can be resolved. Otherwise it tries to resolve
120
+ # the translation directly.
121
+ def default(locale, object, subject, options = {})
122
+ options = options.dup.reject { |key, value| key == :default }
123
+ case subject
124
+ when Array
125
+ subject.each do |subject|
126
+ result = resolve(locale, object, subject, options) and return result
127
+ end and nil
128
+ else
129
+ resolve(locale, object, subject, options)
130
+ end
131
+ end
132
+
133
+ # Resolves a translation.
134
+ # If the given subject is a Symbol, it will be translated with the
135
+ # given options. If it is a Proc then it will be evaluated. All other
136
+ # subjects will be returned directly.
137
+ def resolve(locale, object, subject, options = {})
138
+ case subject
139
+ when Symbol
140
+ translate(locale, subject, options)
141
+ when Proc
142
+ resolve(locale, object, subject.call(object, options), options = {})
143
+ else
144
+ subject
145
+ end
146
+ rescue MissingTranslationData
147
+ nil
148
+ end
149
+
150
+ # Picks a translation from an array according to English pluralization
151
+ # rules. It will pick the first translation if count is not equal to 1
152
+ # and the second translation if it is equal to 1. Other backends can
153
+ # implement more flexible or complex pluralization rules.
154
+ def pluralize(locale, entry, count)
155
+ return entry unless entry.is_a?(Hash) and count
156
+
157
+ key = :zero if count == 0 && entry.has_key?(:zero)
158
+ key ||= count == 1 ? :one : :other
159
+ raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
160
+ entry[key]
161
+ end
162
+
163
+ # Interpolates values into a given string.
164
+ #
165
+ # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
166
+ # # => "file test.txt opened by {{user}}"
167
+ #
168
+ # Note that you have to double escape the <tt>\\</tt> when you want to escape
169
+ # the <tt>{{...}}</tt> key in a string (once for the string and once for the
170
+ # interpolation).
171
+ def interpolate(locale, string, values = {})
172
+ return string unless string.is_a?(String) && !values.empty?
173
+
174
+ s = string.gsub(INTERPOLATION_SYNTAX_PATTERN) do
175
+ escaped, key = $1, $2.to_sym
176
+ if escaped
177
+ "{{#{key}}}"
178
+ elsif RESERVED_KEYS.include?(key)
179
+ raise ReservedInterpolationKey.new(key, string)
180
+ else
181
+ "%{#{key}}"
182
+ end
183
+ end
184
+ values.each { |key, value| values[key] = value.call if interpolate_lambda?(value, s, key) }
185
+ s % values
186
+
187
+ rescue KeyError => e
188
+ raise MissingInterpolationArgument.new(values, string)
189
+ end
190
+
191
+ # returns true when the given value responds to :call and the key is
192
+ # an interpolation placeholder in the given string
193
+ def interpolate_lambda?(object, string, key)
194
+ object.respond_to?(:call) && string =~ /%\{#{key}\}|%\<#{key}>.*?\d*\.?\d*[bBdiouxXeEfgGcps]\}/
195
+ end
196
+
197
+ # Loads a single translations file by delegating to #load_rb or
198
+ # #load_yml depending on the file extension and directly merges the
199
+ # data to the existing translations. Raises I18n::UnknownFileType
200
+ # for all other file extensions.
201
+ def load_file(filename)
202
+ type = File.extname(filename).tr('.', '').downcase
203
+ raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}")
204
+ data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
205
+ data.each { |locale, d| merge_translations(locale, d) }
206
+ end
207
+
208
+ # Loads a plain Ruby translations file. eval'ing the file must yield
209
+ # a Hash containing translation data with locales as toplevel keys.
210
+ def load_rb(filename)
211
+ eval(IO.read(filename), binding, filename)
212
+ end
213
+
214
+ # Loads a YAML translations file. The data must have locales as
215
+ # toplevel keys.
216
+ def load_yml(filename)
217
+ YAML::load(IO.read(filename))
218
+ end
219
+
220
+ # Deep merges the given translations hash with the existing translations
221
+ # for the given locale
222
+ def merge_translations(locale, data)
223
+ locale = locale.to_sym
224
+ translations[locale] ||= {}
225
+ data = deep_symbolize_keys(data)
226
+
227
+ # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
228
+ merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
229
+ translations[locale].merge!(data, &merger)
230
+ end
231
+
232
+ # Return a new hash with all keys and nested keys converted to symbols.
233
+ def deep_symbolize_keys(hash)
234
+ hash.inject({}) { |result, (key, value)|
235
+ value = deep_symbolize_keys(value) if value.is_a?(Hash)
236
+ result[(key.to_sym rescue key) || key] = value
237
+ result
238
+ }
239
+ end
240
+
241
+ # Flatten the given array once
242
+ def flatten_once(array)
243
+ result = []
244
+ for element in array # a little faster than each
245
+ result.push(*element)
246
+ end
247
+ result
248
+ end
249
+ end
250
+ end
251
+ end