i18n 0.2.1 → 0.3.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.

Files changed (77) hide show
  1. data/README.textile +44 -9
  2. data/Rakefile +2 -1
  3. data/VERSION +1 -1
  4. data/lib/i18n.rb +60 -15
  5. data/lib/i18n/backend.rb +14 -0
  6. data/lib/i18n/backend/active_record.rb +69 -0
  7. data/lib/i18n/backend/active_record/store_procs.rb +37 -0
  8. data/lib/i18n/backend/active_record/translation.rb +82 -0
  9. data/lib/i18n/backend/active_record_missing.rb +55 -0
  10. data/lib/i18n/backend/base.rb +235 -0
  11. data/lib/i18n/backend/cache.rb +71 -0
  12. data/lib/i18n/backend/chain.rb +74 -0
  13. data/lib/i18n/backend/fallbacks.rb +51 -0
  14. data/lib/i18n/backend/gettext.rb +75 -0
  15. data/lib/i18n/backend/helpers.rb +53 -0
  16. data/lib/i18n/backend/metadata.rb +73 -0
  17. data/lib/i18n/backend/pluralization.rb +57 -0
  18. data/lib/i18n/backend/simple.rb +15 -227
  19. data/lib/i18n/core_ext/object/meta_class.rb +5 -0
  20. data/lib/i18n/{string.rb → core_ext/string/interpolate.rb} +2 -0
  21. data/lib/i18n/exceptions.rb +2 -0
  22. data/lib/i18n/gettext.rb +25 -0
  23. data/lib/i18n/helpers.rb +5 -0
  24. data/lib/i18n/helpers/gettext.rb +64 -0
  25. data/lib/i18n/locale.rb +6 -0
  26. data/lib/i18n/locale/fallbacks.rb +98 -0
  27. data/lib/i18n/locale/tag.rb +28 -0
  28. data/lib/i18n/locale/tag/parents.rb +24 -0
  29. data/lib/i18n/locale/tag/rfc4646.rb +76 -0
  30. data/lib/i18n/locale/tag/simple.rb +41 -0
  31. data/test/all.rb +7 -2
  32. data/test/api/basics.rb +3 -1
  33. data/test/api/interpolation.rb +35 -4
  34. data/test/api/lambda.rb +5 -3
  35. data/test/api/link.rb +4 -2
  36. data/test/api/localization/date.rb +2 -0
  37. data/test/api/localization/date_time.rb +3 -1
  38. data/test/api/localization/lambda.rb +4 -2
  39. data/test/api/localization/time.rb +3 -1
  40. data/test/api/pluralization.rb +12 -15
  41. data/test/api/translation.rb +5 -3
  42. data/test/backend/active_record/active_record_test.rb +40 -0
  43. data/test/backend/active_record/all.rb +3 -0
  44. data/test/backend/active_record/api_test.rb +54 -0
  45. data/test/backend/active_record/setup.rb +166 -0
  46. data/test/backend/active_record_missing/active_record_missing_test.rb +63 -0
  47. data/test/backend/all/api_test.rb +88 -0
  48. data/test/backend/cache/cache_test.rb +69 -0
  49. data/test/backend/chain/api_test.rb +80 -0
  50. data/test/backend/chain/chain_test.rb +64 -0
  51. data/test/backend/fallbacks/api_test.rb +84 -0
  52. data/test/backend/fallbacks/fallbacks_test.rb +57 -0
  53. data/test/backend/metadata/metadata_test.rb +65 -0
  54. data/test/backend/pluralization/api_test.rb +86 -0
  55. data/test/backend/pluralization/pluralization_test.rb +43 -0
  56. data/test/backend/simple/all.rb +2 -0
  57. data/test/backend/simple/api_test.rb +27 -20
  58. data/test/backend/simple/helpers_test.rb +26 -0
  59. data/test/backend/simple/lookup_test.rb +2 -1
  60. data/test/backend/simple/{setup/localization.rb → setup.rb} +29 -11
  61. data/test/backend/simple/translations_test.rb +1 -6
  62. data/test/{string_test.rb → core_ext/string/interpolate_test.rb} +4 -2
  63. data/test/fixtures/locales/de.po +67 -0
  64. data/test/fixtures/locales/en.rb +2 -0
  65. data/test/fixtures/locales/plurals.rb +113 -0
  66. data/test/gettext/api_test.rb +204 -0
  67. data/test/gettext/backend_test.rb +84 -0
  68. data/test/i18n_exceptions_test.rb +3 -1
  69. data/test/i18n_load_path_test.rb +8 -1
  70. data/test/i18n_test.rb +30 -7
  71. data/test/locale/fallbacks_test.rb +128 -0
  72. data/test/locale/tag/rfc4646_test.rb +145 -0
  73. data/test/locale/tag/simple_test.rb +35 -0
  74. data/test/test_helper.rb +11 -5
  75. data/test/with_options.rb +2 -0
  76. metadata +75 -11
  77. data/test/backend/simple/setup/base.rb +0 -21
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  class KeyError < IndexError
2
4
  def initialize(message = nil)
3
5
  super(message || "key not found")
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ module I18n
4
+ module Gettext
5
+ PLURAL_SEPARATOR = "\001"
6
+ CONTEXT_SEPARATOR = "\004"
7
+
8
+ @@plural_keys = { :en => [:one, :other] }
9
+
10
+ class << self
11
+ # returns an array of plural keys for the given locale so that we can
12
+ # convert from gettext's integer-index based style
13
+ # TODO move this information to the pluralization module
14
+ def plural_keys(locale)
15
+ @@plural_keys[locale] || @@plural_keys[:en]
16
+ end
17
+
18
+ def extract_scope(msgid, separator)
19
+ scope = msgid.to_s.split(separator)
20
+ msgid = scope.pop
21
+ [scope, msgid]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ module I18n
2
+ module Helpers
3
+ autoload :Gettext, 'i18n/helpers/gettext'
4
+ end
5
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+
3
+ module I18n
4
+ module Helpers
5
+ # Implements classical Gettext style accessors. To use this include the
6
+ # module to the global namespace or wherever you want to use it.
7
+ #
8
+ # include I18n::Helpers::Gettext
9
+ module Gettext
10
+ def gettext(msgid, options = {})
11
+ I18n.t(msgid, { :default => msgid, :separator => '|' }.merge(options))
12
+ end
13
+ alias _ gettext
14
+
15
+ def sgettext(msgid, separator = '|')
16
+ scope, msgid = I18n::Gettext.extract_scope(msgid, separator)
17
+ I18n.t(msgid, :scope => scope, :default => msgid)
18
+ end
19
+ alias s_ sgettext
20
+
21
+ def pgettext(msgctxt, msgid)
22
+ separator = I18n::Gettext::CONTEXT_SEPARATOR
23
+ sgettext([msgctxt, msgid].join(separator), separator)
24
+ end
25
+ alias p_ pgettext
26
+
27
+ def ngettext(msgid, msgid_plural, n = 1)
28
+ nsgettext(msgid, msgid_plural, n)
29
+ end
30
+ alias n_ ngettext
31
+
32
+ # Method signatures:
33
+ # nsgettext('Fruits|apple', 'apples', 2)
34
+ # nsgettext(['Fruits|apple', 'apples'], 2)
35
+ def nsgettext(msgid, msgid_plural, n = 1, separator = '|')
36
+ if msgid.is_a?(Array)
37
+ msgid, msgid_plural, n, separator = msgid[0], msgid[1], msgid_plural, n
38
+ separator = '|' unless separator.is_a?(String)
39
+ end
40
+
41
+ scope, msgid = I18n::Gettext.extract_scope(msgid, separator)
42
+ default = { :one => msgid, :other => msgid_plural }
43
+ I18n.t(msgid, :default => default, :count => n, :scope => scope)
44
+ end
45
+ alias ns_ nsgettext
46
+
47
+ # Method signatures:
48
+ # npgettext('Fruits', 'apple', 'apples', 2)
49
+ # npgettext('Fruits', ['apple', 'apples'], 2)
50
+ def npgettext(msgctxt, msgid, msgid_plural, n = 1)
51
+ separator = I18n::Gettext::CONTEXT_SEPARATOR
52
+
53
+ if msgid.is_a?(Array)
54
+ msgid_plural, msgid, n = msgid[1], [msgctxt, msgid[0]].join(separator), msgid_plural
55
+ else
56
+ msgid = [msgctxt, msgid].join(separator)
57
+ end
58
+
59
+ nsgettext(msgid, msgid_plural, n, separator)
60
+ end
61
+ alias np_ npgettext
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,6 @@
1
+ module I18n
2
+ module Locale
3
+ autoload :Fallbacks, 'i18n/locale/fallbacks'
4
+ autoload :Tag, 'i18n/locale/tag'
5
+ end
6
+ end
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+
3
+ # Locale Fallbacks
4
+ #
5
+ # Extends the I18n module to hold a fallbacks instance which is set to an
6
+ # instance of I18n::Locale::Fallbacks by default but can be swapped with a
7
+ # different implementation.
8
+ #
9
+ # Locale fallbacks will compute a number of fallback locales for a given locale.
10
+ # For example:
11
+ #
12
+ # <pre><code>
13
+ # I18n.fallbacks[:"es-MX"] # => [:"es-MX", :es, :en] </code></pre>
14
+ #
15
+ # Locale fallbacks always fall back to
16
+ #
17
+ # * all parent locales of a given locale (e.g. :es for :"es-MX") first,
18
+ # * the current default locales and all of their parents second
19
+ #
20
+ # The default locales are set to [I18n.default_locale] by default but can be
21
+ # set to something else.
22
+ #
23
+ # One can additionally add any number of additional fallback locales manually.
24
+ # These will be added before the default locales to the fallback chain. For
25
+ # example:
26
+ #
27
+ # # using the default locale as default fallback locale
28
+ #
29
+ # I18n.default_locale = :"en-US"
30
+ # I18n.fallbacks = I18n::Fallbacks.new(:"de-AT" => :"de-DE")
31
+ # I18n.fallbacks[:"de-AT"] # => [:"de-AT", :"de-DE", :de, :"en-US", :en]
32
+ #
33
+ # # using a custom locale as default fallback locale
34
+ #
35
+ # I18n.fallbacks = I18n::Fallbacks.new(:"en-GB", :"de-AT" => :de, :"de-CH" => :de)
36
+ # I18n.fallbacks[:"de-AT"] # => [:"de-AT", :de, :"en-GB", :en]
37
+ # I18n.fallbacks[:"de-CH"] # => [:"de-CH", :de, :"en-GB", :en]
38
+ #
39
+ # # mapping fallbacks to an existing instance
40
+ #
41
+ # # people speaking Catalan also speak Spanish as spoken in Spain
42
+ # fallbacks = I18n.fallbacks
43
+ # fallbacks.map(:ca => :"es-ES")
44
+ # fallbacks[:ca] # => [:ca, :"es-ES", :es, :"en-US", :en]
45
+ #
46
+ # # people speaking Arabian as spoken in Palestine also speak Hebrew as spoken in Israel
47
+ # fallbacks.map(:"ar-PS" => :"he-IL")
48
+ # fallbacks[:"ar-PS"] # => [:"ar-PS", :ar, :"he-IL", :he, :"en-US", :en]
49
+ # fallbacks[:"ar-EG"] # => [:"ar-EG", :ar, :"en-US", :en]
50
+ #
51
+ # # people speaking Sami as spoken in Finnland also speak Swedish and Finnish as spoken in Finnland
52
+ # fallbacks.map(:sms => [:"se-FI", :"fi-FI"])
53
+ # fallbacks[:sms] # => [:sms, :"se-FI", :se, :"fi-FI", :fi, :"en-US", :en]
54
+
55
+ module I18n
56
+ module Locale
57
+ class Fallbacks < Hash
58
+ def initialize(*mappings)
59
+ @map = {}
60
+ map(mappings.pop) if mappings.last.is_a?(Hash)
61
+ self.defaults = mappings.empty? ? [I18n.default_locale.to_sym] : mappings
62
+ end
63
+
64
+ def defaults=(defaults)
65
+ @defaults = defaults.map { |default| compute(default, false) }.flatten
66
+ end
67
+ attr_reader :defaults
68
+
69
+ def [](locale)
70
+ raise InvalidLocale.new(locale) if locale.nil?
71
+ locale = locale.to_sym
72
+ super || store(locale, compute(locale))
73
+ end
74
+
75
+ def map(mappings)
76
+ mappings.each do |from, to|
77
+ from, to = from.to_sym, Array(to)
78
+ to.each do |to|
79
+ @map[from] ||= []
80
+ @map[from] << to.to_sym
81
+ end
82
+ end
83
+ end
84
+
85
+ protected
86
+
87
+ def compute(tags, include_defaults = true)
88
+ result = Array(tags).collect do |tag|
89
+ tags = I18n::Locale::Tag.tag(tag).self_and_parents.map! { |t| t.to_sym }
90
+ tags.each { |tag| tags += compute(@map[tag]) if @map[tag] }
91
+ tags
92
+ end.flatten
93
+ result.push(*defaults) if include_defaults
94
+ result.uniq
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+
3
+ module I18n
4
+ module Locale
5
+ module Tag
6
+ autoload :Parents, 'i18n/locale/tag/parents'
7
+ autoload :Rfc4646, 'i18n/locale/tag/rfc4646'
8
+ autoload :Simple, 'i18n/locale/tag/simple'
9
+
10
+ class << self
11
+ # Returns the current locale tag implementation. Defaults to +I18n::Locale::Tag::Simple+.
12
+ def implementation
13
+ @@implementation ||= Simple
14
+ end
15
+
16
+ # Sets the current locale tag implementation. Use this to set a different locale tag implementation.
17
+ def implementation=(implementation)
18
+ @@implementation = implementation
19
+ end
20
+
21
+ # Factory method for locale tags. Delegates to the current locale tag implementation.
22
+ def tag(tag)
23
+ implementation.tag(tag)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ module I18n
4
+ module Locale
5
+ module Tag
6
+ module Parents
7
+ def parent
8
+ @parent ||= begin
9
+ segs = to_a.compact
10
+ segs.length > 1 ? self.class.tag(*segs[0..(segs.length-2)].join('-')) : nil
11
+ end
12
+ end
13
+
14
+ def self_and_parents
15
+ @self_and_parents ||= [self] + parents
16
+ end
17
+
18
+ def parents
19
+ @parents ||= ([parent] + (parent ? parent.parents : [])).compact
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,76 @@
1
+ # encoding: utf-8
2
+
3
+ # RFC 4646/47 compliant Locale tag implementation that parses locale tags to
4
+ # subtags such as language, script, region, variant etc.
5
+ #
6
+ # For more information see by http://en.wikipedia.org/wiki/IETF_language_tag
7
+ #
8
+ # Rfc4646::Parser does not implement grandfathered tags.
9
+
10
+ module I18n
11
+ module Locale
12
+ module Tag
13
+ RFC4646_SUBTAGS = [ :language, :script, :region, :variant, :extension, :privateuse, :grandfathered ]
14
+ RFC4646_FORMATS = { :language => :downcase, :script => :capitalize, :region => :upcase, :variant => :downcase }
15
+
16
+ class Rfc4646 < Struct.new(*RFC4646_SUBTAGS)
17
+ class << self
18
+ # Parses the given tag and returns a Tag instance if it is valid.
19
+ # Returns false if the given tag is not valid according to RFC 4646.
20
+ def tag(tag)
21
+ matches = parser.match(tag)
22
+ new(*matches) if matches
23
+ end
24
+
25
+ def parser
26
+ @@parser ||= Rfc4646::Parser
27
+ end
28
+
29
+ def parser=(parser)
30
+ @@parser = parser
31
+ end
32
+ end
33
+
34
+ include Parents
35
+
36
+ RFC4646_FORMATS.each do |name, format|
37
+ define_method(name) { self[name].send(format) unless self[name].nil? }
38
+ end
39
+
40
+ def to_sym
41
+ to_s.to_sym
42
+ end
43
+
44
+ def to_s
45
+ @tag ||= to_a.compact.join("-")
46
+ end
47
+
48
+ def to_a
49
+ members.collect { |attr| self.send(attr) }
50
+ end
51
+
52
+ module Parser
53
+ PATTERN = %r{\A(?:
54
+ ([a-z]{2,3}(?:(?:-[a-z]{3}){0,3})?|[a-z]{4}|[a-z]{5,8}) # language
55
+ (?:-([a-z]{4}))? # script
56
+ (?:-([a-z]{2}|\d{3}))? # region
57
+ (?:-([0-9a-z]{5,8}|\d[0-9a-z]{3}))* # variant
58
+ (?:-([0-9a-wyz](?:-[0-9a-z]{2,8})+))* # extension
59
+ (?:-(x(?:-[0-9a-z]{1,8})+))?| # privateuse subtag
60
+ (x(?:-[0-9a-z]{1,8})+)| # privateuse tag
61
+ /* ([a-z]{1,3}(?:-[0-9a-z]{2,8}){1,2}) */ # grandfathered
62
+ )\z}xi
63
+
64
+ class << self
65
+ def match(tag)
66
+ c = PATTERN.match(tag.to_s).captures
67
+ c[0..4] << (c[5].nil? ? c[6] : c[5]) << c[7] # TODO c[7] is grandfathered, throw a NotImplemented exception here?
68
+ rescue
69
+ false
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+
3
+ # Simple Locale tag implementation that computes subtags by simply splitting
4
+ # the locale tag at '-' occurences.
5
+ module I18n
6
+ module Locale
7
+ module Tag
8
+ class Simple
9
+ class << self
10
+ def tag(tag)
11
+ new(tag)
12
+ end
13
+ end
14
+
15
+ include Parents
16
+
17
+ attr_reader :tag
18
+
19
+ def initialize(*tag)
20
+ @tag = tag.join('-').to_sym
21
+ end
22
+
23
+ def subtags
24
+ @subtags = tag.to_s.split('-').map { |subtag| subtag.to_s }
25
+ end
26
+
27
+ def to_sym
28
+ tag
29
+ end
30
+
31
+ def to_s
32
+ tag.to_s
33
+ end
34
+
35
+ def to_a
36
+ subtags
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,3 +1,8 @@
1
- Dir[File.dirname(__FILE__) + '/**/*_test.rb'].each do |file|
2
- require file
1
+ # encoding: utf-8
2
+
3
+ base = File.dirname(__FILE__)
4
+ $LOAD_PATH.unshift base
5
+
6
+ Dir["#{base}/**/*_test.rb"].sort.each do |file|
7
+ require file.sub(/^#{base}\/(.*)\.rb$/, '\1')
3
8
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Tests
2
4
  module Backend
3
5
  module Api
@@ -10,4 +12,4 @@ module Tests
10
12
  end
11
13
  end
12
14
  end
13
- end
15
+ end
@@ -1,11 +1,15 @@
1
+ # encoding: utf-8
2
+
1
3
  module Tests
2
4
  module Backend
3
5
  module Api
4
6
  module Interpolation
5
- def interpolate(options)
6
- I18n.backend.translate('en', nil, options)
7
+ def interpolate(*args)
8
+ options = args.last.is_a?(Hash) ? args.pop : {}
9
+ key = args.pop
10
+ I18n.backend.translate('en', key, options)
7
11
  end
8
-
12
+
9
13
  def test_interpolation_given_no_interpolation_values_it_does_not_alter_the_string
10
14
  assert_equal 'Hi {{name}}!', interpolate(:default => 'Hi {{name}}!')
11
15
  end
@@ -18,6 +22,33 @@ module Tests
18
22
  assert_equal 'Hi !', interpolate(:default => 'Hi {{name}}!', :name => nil)
19
23
  end
20
24
 
25
+ def test_interpolation_given_a_lambda_as_a_value_it_calls_it_when_the_string_contains_the_key
26
+ assert_equal 'Hi David!', interpolate(:default => 'Hi {{name}}!', :name => lambda { 'David' })
27
+ end
28
+
29
+ def test_interpolation_given_a_lambda_as_a_value_it_does_not_call_it_when_the_string_does_not_contain_the_key
30
+ assert_nothing_raised { interpolate(:default => 'Hi!', :name => lambda { raise 'fail' }) }
31
+ end
32
+
33
+ def test_interpolation_given_interpolation_values_but_missing_a_key_it_raises_a_missing_interpolation_argument_exception
34
+ assert_raises(I18n::MissingInterpolationArgument) do
35
+ interpolate(:default => '{{foo}}', :bar => 'bar')
36
+ end
37
+ end
38
+
39
+ def test_interpolation_does_not_raise_missing_interpolation_argument_exceptions_for_escaped_variables
40
+ assert_nothing_raised(I18n::MissingInterpolationArgument) do
41
+ assert_equal 'Barr {{foo}}', interpolate(:default => '{{bar}} \{{foo}}', :bar => 'Barr')
42
+ end
43
+ end
44
+
45
+ def test_interpolation_does_not_change_the_original_stored_translation_string_and_allows_reinterpolation
46
+ I18n.backend.store_translations(:en, :interpolate => 'Hi {{name}}!')
47
+ assert_equal 'Hi David!', interpolate(:interpolate, :name => 'David')
48
+ assert_equal 'Hi Yehuda!', interpolate(:interpolate, :name => 'Yehuda')
49
+ # assert_equal 'Hi {{name}}!', I18n.backend.instance_variable_get(:@translations)[:en][:interpolate]
50
+ end
51
+
21
52
  def test_interpolate_with_ruby_1_9_syntax
22
53
  assert_equal 'Hi David!', interpolate(:default => 'Hi %{name}!', :name => 'David')
23
54
  end
@@ -60,4 +91,4 @@ module Tests
60
91
  end
61
92
  end
62
93
  end
63
- end
94
+ end