Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

use classes as backends

  • Loading branch information...
commit 08f051aa61320c17debde24a83268bc74e33b995 1 parent 6c04ca8
@svenfuchs authored
View
6 lib/i18n.rb
@@ -9,14 +9,14 @@
require 'i18n/exceptions'
module I18n
- @@backend = Backend::Simple
+ @@backend = nil
@@default_locale = 'en-US'
@@exception_handler = :default_exception_handler
class << self
# Returns the current backend. Defaults to +Backend::Simple+.
def backend
- @@backend
+ @@backend ||= Backend::Simple.new
end
# Sets the current backend. Used to set a custom backend.
@@ -63,7 +63,7 @@ def populate(&block)
# files which are either named *.rb and contain plain Ruby Hashes or are
# named *.yml and contain YAML data.)
def load_translations(*args)
- backend.load_translations *args
+ backend.load_translations(*args)
end
# Stores translations for the given locale in the backend.
View
334 lib/i18n/backend/simple.rb
@@ -2,188 +2,188 @@
module I18n
module Backend
- module Simple
- @@translations = {}
+ class Simple
+ # Allow client libraries to pass a block that populates the translation
+ # storage. Decoupled for backends like a db backend that persist their
+ # translations, so the backend can decide whether/when to yield or not.
+ def populate(&block)
+ yield
+ end
+
+ # Accepts a list of paths to translation files. Loads translations from
+ # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
+ # for details.
+ def load_translations(*filenames)
+ filenames.each {|filename| load_file filename }
+ end
- class << self
- # Allow client libraries to pass a block that populates the translation
- # storage. Decoupled for backends like a db backend that persist their
- # translations, so the backend can decide whether/when to yield or not.
- def populate(&block)
- yield
- end
+ # Stores translations for the given locale in memory.
+ # This uses a deep merge for the translations hash, so existing
+ # translations will be overwritten by new ones only at the deepest
+ # level of the hash.
+ def store_translations(locale, data)
+ merge_translations(locale, data)
+ end
- # Accepts a list of paths to translation files. Loads translations from
- # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
- # for details.
- def load_translations(*filenames)
- filenames.each {|filename| load_file filename }
+ def translate(locale, key, options = {})
+ raise InvalidLocale.new(locale) if locale.nil?
+ return key.map{|key| translate locale, key, options } if key.is_a? Array
+
+ reserved = :scope, :default
+ count, scope, default = options.values_at(:count, *reserved)
+ options.delete(:default)
+ values = options.reject{|name, value| reserved.include? name }
+
+ entry = lookup(locale, key, scope) || default(locale, default, options) || raise(I18n::MissingTranslationData.new(locale, key, options))
+ entry = pluralize locale, entry, count
+ entry = interpolate locale, entry, values
+ entry
+ end
+
+ # Acts the same as +strftime+, but returns a localized version of the
+ # formatted date string. Takes a key from the date/time formats
+ # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
+ def localize(locale, object, format = :default)
+ raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
+
+ type = object.respond_to?(:sec) ? 'time' : 'date'
+ formats = translate(locale, :"#{type}.formats")
+ format = formats[format.to_sym] if formats && formats[format.to_sym]
+ # TODO raise exception unless format found?
+ format = format.to_s.dup
+
+ format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
+ format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
+ format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
+ format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
+ format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
+ object.strftime(format)
+ end
+
+ protected
+
+ def translations
+ @translations ||= {}
end
- # Stores translations for the given locale in memory.
- # This uses a deep merge for the translations hash, so existing
- # translations will be overwritten by new ones only at the deepest
- # level of the hash.
- def store_translations(locale, data)
- merge_translations(locale, data)
+ # Looks up a translation from the translations hash. Returns nil if
+ # eiher key is nil, or locale, scope or key do not exist as a key in the
+ # nested translations hash. Splits keys or scopes containing dots
+ # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
+ # <tt>%w(currency format)</tt>.
+ def lookup(locale, key, scope = [])
+ return unless key
+ keys = I18n.send :normalize_translation_keys, locale, key, scope
+ keys.inject(translations){|result, key| result[key.to_sym] or return nil }
+ end
+
+ # Evaluates a default translation.
+ # If the given default is a String it is used literally. If it is a Symbol
+ # it will be translated with the given options. If it is an Array the first
+ # translation yielded will be returned.
+ #
+ # <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if
+ # <tt>translate(locale, :foo)</tt> does not yield a result.
+ def default(locale, default, options = {})
+ case default
+ when String then default
+ when Symbol then translate locale, default, options
+ when Array then default.each do |obj|
+ result = default(locale, obj, options.dup) and return result
+ end and nil
+ end
+ rescue MissingTranslationData
+ nil
+ end
+
+ # Picks a translation from an array according to English pluralization
+ # rules. It will pick the first translation if count is not equal to 1
+ # and the second translation if it is equal to 1. Other backends can
+ # implement more flexible or complex pluralization rules.
+ def pluralize(locale, entry, count)
+ return entry unless entry.is_a?(Hash) and count
+ # raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
+ key = :zero if count == 0 && entry.has_key?(:zero)
+ key ||= count == 1 ? :one : :many
+ raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
+ entry[key]
end
+
+ # Interpolates values into a given string.
+ #
+ # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
+ # # => "file test.txt opened by {{user}}"
+ #
+ # Note that you have to double escape the <tt>\\</tt> when you want to escape
+ # the <tt>{{...}}</tt> key in a string (once for the string and once for the
+ # interpolation).
+ def interpolate(locale, string, values = {})
+ return string if !string.is_a?(String)
+
+ map = {'%d' => '{{count}}', '%s' => '{{value}}'} # TODO deprecate this?
+ string.gsub!(/#{map.keys.join('|')}/){|key| map[key]}
- def translate(locale, key, options = {})
- raise InvalidLocale.new(locale) if locale.nil?
- return key.map{|key| translate locale, key, options } if key.is_a? Array
+ s = StringScanner.new string.dup
+ while s.skip_until(/\{\{/)
+ s.string[s.pos - 3, 1] = '' and next if s.pre_match[-1, 1] == '\\'
+ start_pos = s.pos - 2
+ key = s.scan_until(/\}\}/)[0..-3]
+ end_pos = s.pos - 1
- reserved = :scope, :default
- count, scope, default = options.values_at(:count, *reserved)
- options.delete(:default)
- values = options.reject{|name, value| reserved.include? name }
+ raise ReservedInterpolationKey.new(key, string) if %w(scope default).include?(key)
+ raise MissingInterpolationArgument.new(key, string) unless values.has_key? key.to_sym
- entry = lookup(locale, key, scope) || default(locale, default, options) || raise(I18n::MissingTranslationData.new(locale, key, options))
- entry = pluralize locale, entry, count
- entry = interpolate locale, entry, values
- entry
+ s.string[start_pos..end_pos] = values[key.to_sym].to_s
+ s.unscan
+ end
+ s.string
end
- # Acts the same as +strftime+, but returns a localized version of the
- # formatted date string. Takes a key from the date/time formats
- # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
- def localize(locale, object, format = :default)
- raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
-
- type = object.respond_to?(:sec) ? 'time' : 'date'
- formats = translate(locale, :"#{type}.formats")
- format = formats[format.to_sym] if formats && formats[format.to_sym]
- # TODO raise exception unless format found?
- format = format.to_s.dup
-
- format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
- format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
- format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
- format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
- format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
- object.strftime(format)
+ # Loads a single translations file by delegating to #load_rb or
+ # #load_yml depending on the file extension and directly merges the
+ # data to the existing translations. Raises I18n::UnknownFileType
+ # for all other file extensions.
+ def load_file(filename)
+ type = File.extname(filename).tr('.', '').downcase
+ raise UnknownFileType.new(type, filename) unless respond_to? :"load_#{type}"
+ data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
+ data.each do |locale, data|
+ merge_translations locale, data
+ end
end
- protected
-
- # Looks up a translation from the translations hash. Returns nil if
- # eiher key is nil, or locale, scope or key do not exist as a key in the
- # nested translations hash. Splits keys or scopes containing dots
- # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
- # <tt>%w(currency format)</tt>.
- def lookup(locale, key, scope = [])
- return unless key
- keys = I18n.send :normalize_translation_keys, locale, key, scope
- keys.inject(@@translations){|result, key| result[key.to_sym] or return nil }
- end
+ # Loads a plain Ruby translations file. eval'ing the file must yield
+ # a Hash containing translation data with locales as toplevel keys.
+ def load_rb(filename)
+ eval IO.read(filename), binding, filename
+ end
- # Evaluates a default translation.
- # If the given default is a String it is used literally. If it is a Symbol
- # it will be translated with the given options. If it is an Array the first
- # translation yielded will be returned.
- #
- # <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if
- # <tt>translate(locale, :foo)</tt> does not yield a result.
- def default(locale, default, options = {})
- case default
- when String then default
- when Symbol then translate locale, default, options
- when Array then default.each do |obj|
- result = default(locale, obj, options.dup) and return result
- end and nil
- end
- rescue MissingTranslationData
- nil
- end
+ # Loads a YAML translations file. The data must have locales as
+ # toplevel keys.
+ def load_yml(filename)
+ YAML::load IO.read(filename)
+ end
- # Picks a translation from an array according to English pluralization
- # rules. It will pick the first translation if count is not equal to 1
- # and the second translation if it is equal to 1. Other backends can
- # implement more flexible or complex pluralization rules.
- def pluralize(locale, entry, count)
- return entry unless entry.is_a?(Hash) and count
- # raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
- key = :zero if count == 0 && entry.has_key?(:zero)
- key ||= count == 1 ? :one : :many
- raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
- entry[key]
- end
-
- # Interpolates values into a given string.
- #
- # interpolate "file {{file}} opened by \\{{user}}", :file => 'test.txt', :user => 'Mr. X'
- # # => "file test.txt opened by {{user}}"
- #
- # Note that you have to double escape the <tt>\\</tt> when you want to escape
- # the <tt>{{...}}</tt> key in a string (once for the string and once for the
- # interpolation).
- def interpolate(locale, string, values = {})
- return string if !string.is_a?(String)
-
- map = {'%d' => '{{count}}', '%s' => '{{value}}'} # TODO deprecate this?
- string.gsub!(/#{map.keys.join('|')}/){|key| map[key]}
-
- s = StringScanner.new string.dup
- while s.skip_until(/\{\{/)
- s.string[s.pos - 3, 1] = '' and next if s.pre_match[-1, 1] == '\\'
- start_pos = s.pos - 2
- key = s.scan_until(/\}\}/)[0..-3]
- end_pos = s.pos - 1
-
- raise ReservedInterpolationKey.new(key, string) if %w(scope default).include?(key)
- raise MissingInterpolationArgument.new(key, string) unless values.has_key? key.to_sym
-
- s.string[start_pos..end_pos] = values[key.to_sym].to_s
- s.unscan
- end
- s.string
- end
-
- # Loads a single translations file by delegating to #load_rb or
- # #load_yml depending on the file extension and directly merges the
- # data to the existing translations. Raises I18n::UnknownFileType
- # for all other file extensions.
- def load_file(filename)
- type = File.extname(filename).tr('.', '').downcase
- raise UnknownFileType.new(type, filename) unless respond_to? :"load_#{type}"
- data = send :"load_#{type}", filename # TODO raise a meaningful exception if this does not yield a Hash
- data.each do |locale, data|
- merge_translations locale, data
- end
- end
-
- # Loads a plain Ruby translations file. eval'ing the file must yield
- # a Hash containing translation data with locales as toplevel keys.
- def load_rb(filename)
- eval IO.read(filename), binding, filename
- end
-
- # Loads a YAML translations file. The data must have locales as
- # toplevel keys.
- def load_yml(filename)
- YAML::load IO.read(filename)
- end
-
- # Deep merges the given translations hash with the existing translations
- # for the given locale
- def merge_translations(locale, data)
- locale = locale.to_sym
- @@translations[locale] ||= {}
- data = deep_symbolize_keys data
+ # Deep merges the given translations hash with the existing translations
+ # for the given locale
+ def merge_translations(locale, data)
+ locale = locale.to_sym
+ translations[locale] ||= {}
+ data = deep_symbolize_keys data
- # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
- merger = proc{|key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
- @@translations[locale].merge! data, &merger
- end
-
- # Return a new hash with all keys and nested keys converted to symbols.
- def deep_symbolize_keys(hash)
- hash.inject({}){|result, (key, value)|
- value = deep_symbolize_keys(value) if value.is_a? Hash
- result[(key.to_sym rescue key) || key] = value
- result
- }
- end
- end
+ # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
+ merger = proc{|key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
+ translations[locale].merge! data, &merger
+ end
+
+ # Return a new hash with all keys and nested keys converted to symbols.
+ def deep_symbolize_keys(hash)
+ hash.inject({}){|result, (key, value)|
+ value = deep_symbolize_keys(value) if value.is_a? Hash
+ result[(key.to_sym rescue key) || key] = value
+ result
+ }
+ end
end
end
end
View
1  test/all.rb
@@ -2,3 +2,4 @@
require dir + '/i18n_test.rb'
require dir + '/simple_backend_test.rb'
require dir + '/i18n_exceptions_test.rb'
+# require dir + '/custom_backend_test.rb'
View
4 test/i18n_test.rb
@@ -19,13 +19,13 @@ def setup
end
def test_uses_simple_backend_set_by_default
- assert_equal I18n::Backend::Simple, I18n.backend
+ assert I18n.backend.is_a?(I18n::Backend::Simple)
end
def test_can_set_backend
assert_nothing_raised{ I18n.backend = self }
assert_equal self, I18n.backend
- I18n.backend = I18n::Backend::Simple
+ I18n.backend = I18n::Backend::Simple.new
end
def test_uses_en_us_as_default_locale_by_default
View
69 test/simple_backend_test.rb
@@ -4,18 +4,27 @@
require 'test/unit'
require 'mocha'
require 'i18n'
-require 'Time'
-require 'Yaml'
+require 'time'
+require 'yaml'
module I18nSimpleBackendTestSetup
def setup_backend
- @backend = I18n::Backend::Simple
- @backend.send :class_variable_set, :@@translations, {}
+ # backend_reset_translations!
+ @backend = I18n::Backend::Simple.new
@backend.store_translations 'en-US', :foo => {:bar => 'bar', :baz => 'baz'}
@locale_dir = File.dirname(__FILE__) + '/locale'
end
alias :setup :setup_backend
+ # def backend_reset_translations!
+ # I18n::Backend::Simple::ClassMethods.send :class_variable_set, :@@translations, {}
+ # end
+
+ def backend_get_translations
+ # I18n::Backend::Simple::ClassMethods.send :class_variable_get, :@@translations
+ @backend.instance_variable_get :@translations
+ end
+
def add_datetime_translations
@backend.store_translations :'de-DE', {
:date => {
@@ -93,25 +102,24 @@ class I18nSimpleBackendTranslationsTest < Test::Unit::TestCase
def test_store_translations_adds_translations # no, really :-)
@backend.store_translations :'en-US', :foo => 'bar'
- assert_equal Hash[:'en-US', {:foo => 'bar'}], @backend.send(:class_variable_get, :@@translations)
+ assert_equal Hash[:'en-US', {:foo => 'bar'}], backend_get_translations
end
def test_store_translations_deep_merges_translations
@backend.store_translations :'en-US', :foo => {:bar => 'bar'}
@backend.store_translations :'en-US', :foo => {:baz => 'baz'}
- assert_equal Hash[:'en-US', {:foo => {:bar => 'bar', :baz => 'baz'}}], @backend.send(:class_variable_get, :@@translations)
+ assert_equal Hash[:'en-US', {:foo => {:bar => 'bar', :baz => 'baz'}}], backend_get_translations
end
def test_store_translations_forces_locale_to_sym
@backend.store_translations 'en-US', :foo => 'bar'
- assert_equal Hash[:'en-US', {:foo => 'bar'}], @backend.send(:class_variable_get, :@@translations)
+ assert_equal Hash[:'en-US', {:foo => 'bar'}], backend_get_translations
end
- def test_store_translations_covert_key_symbols
- @backend.send :class_variable_set, :@@translations, {} # reset translations
- @backend.store_translations :'en-US', 'foo' => {'bar' => 'baz'}
- assert_equal Hash[:'en-US', {:foo => {:bar => 'baz'}}],
- @backend.send(:class_variable_get, :@@translations)
+ def test_store_translations_converts_keys_to_symbols
+ # backend_reset_translations!
+ @backend.store_translations 'en-US', 'foo' => {'bar' => 'bar', 'baz' => 'baz'}
+ assert_equal Hash[:'en-US', {:foo => {:bar => 'bar', :baz => 'baz'}}], backend_get_translations
end
end
@@ -255,7 +263,7 @@ class I18nSimpleBackendLocalizeDateTest < Test::Unit::TestCase
include I18nSimpleBackendTestSetup
def setup
- @backend = I18n::Backend::Simple
+ @backend = I18n::Backend::Simple.new
add_datetime_translations
@date = Date.new 2008, 1, 1
end
@@ -309,7 +317,7 @@ class I18nSimpleBackendLocalizeDateTimeTest < Test::Unit::TestCase
include I18nSimpleBackendTestSetup
def setup
- @backend = I18n::Backend::Simple
+ @backend = I18n::Backend::Simple.new
add_datetime_translations
@morning = DateTime.new 2008, 1, 1, 6
@evening = DateTime.new 2008, 1, 1, 18
@@ -362,7 +370,7 @@ class I18nSimpleBackendLocalizeTimeTest < Test::Unit::TestCase
def setup
@old_timezone, ENV['TZ'] = ENV['TZ'], 'UTC'
- @backend = I18n::Backend::Simple
+ @backend = I18n::Backend::Simple.new
add_datetime_translations
@morning = Time.parse '2008-01-01 6:00 UTC'
@evening = Time.parse '2008-01-01 18:00 UTC'
@@ -417,7 +425,7 @@ def test_translate_given_an_unknown_format_it_does_not_fail
class I18nSimpleBackendHelperMethodsTest < Test::Unit::TestCase
def setup
- @backend = I18n::Backend::Simple
+ @backend = I18n::Backend::Simple.new
end
def test_deep_symbolize_keys_works
@@ -449,35 +457,12 @@ def test_load_rb_loads_data_from_yaml_file
end
def test_load_translations_loads_from_different_file_formats
- @backend.send :class_variable_set, :@@translations, {} # reset translations
+ @backend = I18n::Backend::Simple.new
@backend.load_translations "#{@locale_dir}/en-US.rb", "#{@locale_dir}/en-US.yml"
expected = {
:'en-US-Ruby' => {:foo => {:bar => "baz"}},
:'en-US-Yaml' => {:foo => {:bar => "baz"}}
}
- result = @backend.send :class_variable_get, :@@translations
- assert_equal expected, result
+ assert_equal expected, backend_get_translations
end
-end
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+end
Please sign in to comment.
Something went wrong with that request. Please try again.