pepe-i18n 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 (61) hide show
  1. data/CHANGELOG.textile +57 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.textile +42 -0
  4. data/Rakefile +21 -0
  5. data/VERSION +1 -0
  6. data/lib/i18n.rb +270 -0
  7. data/lib/i18n/backend/base.rb +251 -0
  8. data/lib/i18n/backend/cache.rb +71 -0
  9. data/lib/i18n/backend/chain.rb +64 -0
  10. data/lib/i18n/backend/fallbacks.rb +53 -0
  11. data/lib/i18n/backend/gettext.rb +65 -0
  12. data/lib/i18n/backend/pluralization.rb +56 -0
  13. data/lib/i18n/backend/simple.rb +23 -0
  14. data/lib/i18n/exceptions.rb +61 -0
  15. data/lib/i18n/gettext.rb +25 -0
  16. data/lib/i18n/helpers/gettext.rb +35 -0
  17. data/lib/i18n/locale/fallbacks.rb +100 -0
  18. data/lib/i18n/locale/tag.rb +27 -0
  19. data/lib/i18n/locale/tag/parents.rb +24 -0
  20. data/lib/i18n/locale/tag/rfc4646.rb +78 -0
  21. data/lib/i18n/locale/tag/simple.rb +44 -0
  22. data/lib/i18n/string.rb +95 -0
  23. data/test/all.rb +5 -0
  24. data/test/api/basics.rb +15 -0
  25. data/test/api/interpolation.rb +85 -0
  26. data/test/api/lambda.rb +52 -0
  27. data/test/api/link.rb +47 -0
  28. data/test/api/localization/date.rb +65 -0
  29. data/test/api/localization/date_time.rb +63 -0
  30. data/test/api/localization/lambda.rb +26 -0
  31. data/test/api/localization/time.rb +63 -0
  32. data/test/api/pluralization.rb +37 -0
  33. data/test/api/translation.rb +51 -0
  34. data/test/backend/cache/cache_test.rb +57 -0
  35. data/test/backend/chain/api_test.rb +80 -0
  36. data/test/backend/chain/chain_test.rb +64 -0
  37. data/test/backend/fallbacks/api_test.rb +79 -0
  38. data/test/backend/fallbacks/fallbacks_test.rb +29 -0
  39. data/test/backend/pluralization/api_test.rb +81 -0
  40. data/test/backend/pluralization/pluralization_test.rb +39 -0
  41. data/test/backend/simple/all.rb +5 -0
  42. data/test/backend/simple/api_test.rb +90 -0
  43. data/test/backend/simple/lookup_test.rb +24 -0
  44. data/test/backend/simple/setup.rb +147 -0
  45. data/test/backend/simple/translations_test.rb +89 -0
  46. data/test/fixtures/locales/de.po +61 -0
  47. data/test/fixtures/locales/en.rb +3 -0
  48. data/test/fixtures/locales/en.yml +3 -0
  49. data/test/fixtures/locales/plurals.rb +112 -0
  50. data/test/gettext/api_test.rb +78 -0
  51. data/test/gettext/backend_test.rb +35 -0
  52. data/test/i18n_exceptions_test.rb +97 -0
  53. data/test/i18n_load_path_test.rb +23 -0
  54. data/test/i18n_test.rb +163 -0
  55. data/test/locale/fallbacks_test.rb +128 -0
  56. data/test/locale/tag/rfc4646_test.rb +147 -0
  57. data/test/locale/tag/simple_test.rb +35 -0
  58. data/test/string_test.rb +94 -0
  59. data/test/test_helper.rb +71 -0
  60. data/test/with_options.rb +34 -0
  61. metadata +151 -0
@@ -0,0 +1,100 @@
1
+ # encoding: utf-8
2
+
3
+ require 'i18n/locale/tag'
4
+
5
+ # Locale Fallbacks
6
+ #
7
+ # Extends the I18n module to hold a fallbacks instance which is set to an
8
+ # instance of I18n::Locale::Fallbacks by default but can be swapped with a
9
+ # different implementation.
10
+ #
11
+ # Locale fallbacks will compute a number of fallback locales for a given locale.
12
+ # For example:
13
+ #
14
+ # <pre><code>
15
+ # I18n.fallbacks[:"es-MX"] # => [:"es-MX", :es, :en] </code></pre>
16
+ #
17
+ # Locale fallbacks always fall back to
18
+ #
19
+ # * all parent locales of a given locale (e.g. :es for :"es-MX") first,
20
+ # * the current default locales and all of their parents second
21
+ #
22
+ # The default locales are set to [I18n.default_locale] by default but can be
23
+ # set to something else.
24
+ #
25
+ # One can additionally add any number of additional fallback locales manually.
26
+ # These will be added before the default locales to the fallback chain. For
27
+ # example:
28
+ #
29
+ # # using the default locale as default fallback locale
30
+ #
31
+ # I18n.default_locale = :"en-US"
32
+ # I18n.fallbacks = I18n::Fallbacks.new(:"de-AT" => :"de-DE")
33
+ # I18n.fallbacks[:"de-AT"] # => [:"de-AT", :"de-DE", :de, :"en-US", :en]
34
+ #
35
+ # # using a custom locale as default fallback locale
36
+ #
37
+ # I18n.fallbacks = I18n::Fallbacks.new(:"en-GB", :"de-AT" => :de, :"de-CH" => :de)
38
+ # I18n.fallbacks[:"de-AT"] # => [:"de-AT", :de, :"en-GB", :en]
39
+ # I18n.fallbacks[:"de-CH"] # => [:"de-CH", :de, :"en-GB", :en]
40
+ #
41
+ # # mapping fallbacks to an existing instance
42
+ #
43
+ # # people speaking Catalan also speak Spanish as spoken in Spain
44
+ # fallbacks = I18n.fallbacks
45
+ # fallbacks.map(:ca => :"es-ES")
46
+ # fallbacks[:ca] # => [:ca, :"es-ES", :es, :"en-US", :en]
47
+ #
48
+ # # people speaking Arabian as spoken in Palestine also speak Hebrew as spoken in Israel
49
+ # fallbacks.map(:"ar-PS" => :"he-IL")
50
+ # fallbacks[:"ar-PS"] # => [:"ar-PS", :ar, :"he-IL", :he, :"en-US", :en]
51
+ # fallbacks[:"ar-EG"] # => [:"ar-EG", :ar, :"en-US", :en]
52
+ #
53
+ # # people speaking Sami as spoken in Finnland also speak Swedish and Finnish as spoken in Finnland
54
+ # fallbacks.map(:sms => [:"se-FI", :"fi-FI"])
55
+ # fallbacks[:sms] # => [:sms, :"se-FI", :se, :"fi-FI", :fi, :"en-US", :en]
56
+
57
+ module I18n
58
+ module Locale
59
+ class Fallbacks < Hash
60
+ def initialize(*mappings)
61
+ @map = {}
62
+ map(mappings.pop) if mappings.last.is_a?(Hash)
63
+ self.defaults = mappings.empty? ? [I18n.default_locale.to_sym] : mappings
64
+ end
65
+
66
+ def defaults=(defaults)
67
+ @defaults = defaults.map { |default| compute(default, false) }.flatten
68
+ end
69
+ attr_reader :defaults
70
+
71
+ def [](locale)
72
+ raise InvalidLocale.new(locale) if locale.nil?
73
+ locale = locale.to_sym
74
+ super || store(locale, compute(locale))
75
+ end
76
+
77
+ def map(mappings)
78
+ mappings.each do |from, to|
79
+ from, to = from.to_sym, Array(to)
80
+ to.each do |to|
81
+ @map[from] ||= []
82
+ @map[from] << to.to_sym
83
+ end
84
+ end
85
+ end
86
+
87
+ protected
88
+
89
+ def compute(tags, include_defaults = true)
90
+ result = Array(tags).collect do |tag|
91
+ tags = I18n::Locale::Tag.tag(tag).self_and_parents.map! { |t| t.to_sym }
92
+ tags.each { |tag| tags += compute(@map[tag]) if @map[tag] }
93
+ tags
94
+ end.flatten
95
+ result.push(*defaults) if include_defaults
96
+ result.uniq
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ require 'i18n/locale/tag/simple'
4
+ require 'i18n/locale/tag/rfc4646'
5
+
6
+ module I18n
7
+ module Locale
8
+ module Tag
9
+ class << self
10
+ # Returns the current locale tag implementation. Defaults to +I18n::Locale::Tag::Simple+.
11
+ def implementation
12
+ @@implementation ||= Simple
13
+ end
14
+
15
+ # Sets the current locale tag implementation. Use this to set a different locale tag implementation.
16
+ def implementation=(implementation)
17
+ @@implementation = implementation
18
+ end
19
+
20
+ # Factory method for locale tags. Delegates to the current locale tag implementation.
21
+ def tag(tag)
22
+ implementation.tag(tag)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ 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,78 @@
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
+ require 'i18n/locale/tag/parents'
11
+
12
+ module I18n
13
+ module Locale
14
+ module Tag
15
+ RFC4646_SUBTAGS = [ :language, :script, :region, :variant, :extension, :privateuse, :grandfathered ]
16
+ RFC4646_FORMATS = { :language => :downcase, :script => :capitalize, :region => :upcase, :variant => :downcase }
17
+
18
+ class Rfc4646 < Struct.new(*RFC4646_SUBTAGS)
19
+ class << self
20
+ # Parses the given tag and returns a Tag instance if it is valid.
21
+ # Returns false if the given tag is not valid according to RFC 4646.
22
+ def tag(tag)
23
+ matches = parser.match(tag)
24
+ new(*matches) if matches
25
+ end
26
+
27
+ def parser
28
+ @@parser ||= Rfc4646::Parser
29
+ end
30
+
31
+ def parser=(parser)
32
+ @@parser = parser
33
+ end
34
+ end
35
+
36
+ include Parents
37
+
38
+ RFC4646_FORMATS.each do |name, format|
39
+ define_method(name) { self[name].send(format) unless self[name].nil? }
40
+ end
41
+
42
+ def to_sym
43
+ to_s.to_sym
44
+ end
45
+
46
+ def to_s
47
+ @tag ||= to_a.compact.join("-")
48
+ end
49
+
50
+ def to_a
51
+ members.collect { |attr| self.send(attr) }
52
+ end
53
+
54
+ module Parser
55
+ PATTERN = %r{\A(?:
56
+ ([a-z]{2,3}(?:(?:-[a-z]{3}){0,3})?|[a-z]{4}|[a-z]{5,8}) # language
57
+ (?:-([a-z]{4}))? # script
58
+ (?:-([a-z]{2}|\d{3}))? # region
59
+ (?:-([0-9a-z]{5,8}|\d[0-9a-z]{3}))* # variant
60
+ (?:-([0-9a-wyz](?:-[0-9a-z]{2,8})+))* # extension
61
+ (?:-(x(?:-[0-9a-z]{1,8})+))?| # privateuse subtag
62
+ (x(?:-[0-9a-z]{1,8})+)| # privateuse tag
63
+ /* ([a-z]{1,3}(?:-[0-9a-z]{2,8}){1,2}) */ # grandfathered
64
+ )\z}xi
65
+
66
+ class << self
67
+ def match(tag)
68
+ c = PATTERN.match(tag.to_s).captures
69
+ c[0..4] << (c[5].nil? ? c[6] : c[5]) << c[7] # TODO c[7] is grandfathered, throw a NotImplemented exception here?
70
+ rescue
71
+ false
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+
3
+ require 'i18n/locale/tag/parents'
4
+
5
+ # Simple Locale tag implementation that computes subtags by simply splitting
6
+ # the locale tag at '-' occurences.
7
+
8
+ module I18n
9
+ module Locale
10
+ module Tag
11
+ class Simple
12
+ class << self
13
+ def tag(tag)
14
+ new(tag)
15
+ end
16
+ end
17
+
18
+ include Parents
19
+
20
+ attr_reader :tag
21
+
22
+ def initialize(*tag)
23
+ @tag = tag.join('-').to_sym
24
+ end
25
+
26
+ def subtags
27
+ @subtags = tag.to_s.split('-').map { |subtag| subtag.to_s }
28
+ end
29
+
30
+ def to_sym
31
+ tag
32
+ end
33
+
34
+ def to_s
35
+ tag.to_s
36
+ end
37
+
38
+ def to_a
39
+ subtags
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,95 @@
1
+ # encoding: utf-8
2
+
3
+ =begin
4
+ heavily based on Masao Mutoh's gettext String interpolation extension
5
+ http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb
6
+ Copyright (C) 2005-2009 Masao Mutoh
7
+ You may redistribute it and/or modify it under the same license terms as Ruby.
8
+ =end
9
+
10
+ if RUBY_VERSION < '1.9'
11
+
12
+ # KeyError is raised by String#% when the string contains a named placeholder
13
+ # that is not contained in the given arguments hash. Ruby 1.9 includes and
14
+ # raises this exception natively. We define it to mimic Ruby 1.9's behaviour
15
+ # in Ruby 1.8.x
16
+
17
+ class KeyError < IndexError
18
+ def initialize(message = nil)
19
+ super(message || "key not found")
20
+ end
21
+ end unless defined?(KeyError)
22
+
23
+ # Extension for String class. This feature is included in Ruby 1.9 or later but not occur TypeError.
24
+ #
25
+ # String#% method which accept "named argument". The translator can know
26
+ # the meaning of the msgids using "named argument" instead of %s/%d style.
27
+
28
+ class String
29
+ # For older ruby versions, such as ruby-1.8.5
30
+ alias :bytesize :size unless instance_methods.find {|m| m.to_s == 'bytesize'}
31
+ alias :interpolate_without_ruby_19_syntax :% # :nodoc:
32
+
33
+ INTERPOLATION_PATTERN = Regexp.union(
34
+ /%%/,
35
+ /%\{(\w+)\}/, # matches placeholders like "%{foo}"
36
+ /%<(\w+)>(.*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%<foo>.d"
37
+ )
38
+
39
+ # % uses self (i.e. the String) as a format specification and returns the
40
+ # result of applying it to the given arguments. In other words it interpolates
41
+ # the given arguments to the string according to the formats the string
42
+ # defines.
43
+ #
44
+ # There are three ways to use it:
45
+ #
46
+ # * Using a single argument or Array of arguments.
47
+ #
48
+ # This is the default behaviour of the String class. See Kernel#sprintf for
49
+ # more details about the format string.
50
+ #
51
+ # Example:
52
+ #
53
+ # "%d %s" % [1, "message"]
54
+ # # => "1 message"
55
+ #
56
+ # * Using a Hash as an argument and unformatted, named placeholders.
57
+ #
58
+ # When you pass a Hash as an argument and specify placeholders with %{foo}
59
+ # it will interpret the hash values as named arguments.
60
+ #
61
+ # Example:
62
+ #
63
+ # "%{firstname}, %{lastname}" % {:firstname => "Masao", :lastname => "Mutoh"}
64
+ # # => "Masao Mutoh"
65
+ #
66
+ # * Using a Hash as an argument and formatted, named placeholders.
67
+ #
68
+ # When you pass a Hash as an argument and specify placeholders with %<foo>d
69
+ # it will interpret the hash values as named arguments and format the value
70
+ # according to the formatting instruction appended to the closing >.
71
+ #
72
+ # Example:
73
+ #
74
+ # "%<integer>d, %<float>.1f" % { :integer => 10, :float => 43.4 }
75
+ # # => "10, 43.3"
76
+ def %(args)
77
+ if args.kind_of?(Hash)
78
+ dup.gsub(INTERPOLATION_PATTERN) do |match|
79
+ if match == '%%'
80
+ '%'
81
+ else
82
+ key = ($1 || $2).to_sym
83
+ raise KeyError unless args.has_key?(key)
84
+ $3 ? sprintf("%#{$3}", args[key]) : args[key]
85
+ end
86
+ end
87
+ elsif self =~ INTERPOLATION_PATTERN
88
+ raise ArgumentError.new('one hash required')
89
+ else
90
+ result = gsub(/%([{<])/, '%%\1')
91
+ result.send :'interpolate_without_ruby_19_syntax', args
92
+ end
93
+ end
94
+ end
95
+ end
data/test/all.rb ADDED
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ Dir[File.dirname(__FILE__) + '/**/*_test.rb'].sort.each do |file|
4
+ require file
5
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ module Tests
4
+ module Backend
5
+ module Api
6
+ module Basics
7
+ def test_available_locales
8
+ backend_store_translations 'de', :foo => 'bar'
9
+ backend_store_translations 'en', :foo => 'foo'
10
+ assert_equal ['de', 'en'], I18n.backend.available_locales.map{|locale| locale.to_s }.sort
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,85 @@
1
+ # encoding: utf-8
2
+
3
+ module Tests
4
+ module Backend
5
+ module Api
6
+ module Interpolation
7
+ def interpolate(options)
8
+ I18n.backend.translate('en', nil, options)
9
+ end
10
+
11
+ def test_interpolation_given_no_interpolation_values_it_does_not_alter_the_string
12
+ assert_equal 'Hi {{name}}!', interpolate(:default => 'Hi {{name}}!')
13
+ end
14
+
15
+ def test_interpolation_given_interpolation_values_it_interpolates_the_values_to_the_string
16
+ assert_equal 'Hi David!', interpolate(:default => 'Hi {{name}}!', :name => 'David')
17
+ end
18
+
19
+ def test_interpolation_given_interpolation_values_with_nil_values_it_interpolates_the_values_to_the_string
20
+ assert_equal 'Hi !', interpolate(:default => 'Hi {{name}}!', :name => nil)
21
+ end
22
+
23
+ def test_interpolation_given_a_lambda_as_a_value_it_calls_it_when_the_string_contains_the_key
24
+ assert_equal 'Hi David!', interpolate(:default => 'Hi {{name}}!', :name => lambda { 'David' })
25
+ end
26
+
27
+ def test_interpolation_given_a_lambda_as_a_value_it_does_not_call_it_when_the_string_does_not_contain_the_key
28
+ assert_nothing_raised { interpolate(:default => 'Hi!', :name => lambda { raise 'fail' }) }
29
+ end
30
+
31
+ def test_interpolation_given_interpolation_values_but_missing_a_key_it_raises_a_missing_interpolation_argument_exception
32
+ assert_raises(I18n::MissingInterpolationArgument) do
33
+ interpolate(:default => '{{foo}}', :bar => 'bar')
34
+ end
35
+ end
36
+
37
+ def test_interpolation_does_not_raise_missing_interpolation_argument_exceptions_for_escaped_variables
38
+ assert_nothing_raised(I18n::MissingInterpolationArgument) do
39
+ assert_equal 'Barr {{foo}}', interpolate(:default => '{{bar}} \{{foo}}', :bar => 'Barr')
40
+ end
41
+ end
42
+
43
+ def test_interpolate_with_ruby_1_9_syntax
44
+ assert_equal 'Hi David!', interpolate(:default => 'Hi %{name}!', :name => 'David')
45
+ end
46
+
47
+ def test_interpolate_given_a_value_hash_interpolates_into_unicode_string
48
+ assert_equal 'Häi David!', interpolate(:default => 'Häi {{name}}!', :name => 'David')
49
+ end
50
+
51
+ def test_interpolate_given_a_unicode_value_hash_interpolates_to_the_string
52
+ assert_equal 'Hi ゆきひろ!', interpolate(:default => 'Hi {{name}}!', :name => 'ゆきひろ')
53
+ end
54
+
55
+ def test_interpolate_given_a_unicode_value_hash_interpolates_into_unicode_string
56
+ assert_equal 'こんにちは、ゆきひろさん!', interpolate(:default => 'こんにちは、{{name}}さん!', :name => 'ゆきひろ')
57
+ end
58
+
59
+ if Kernel.const_defined?(:Encoding)
60
+ def test_interpolate_given_a_non_unicode_multibyte_value_hash_interpolates_into_a_string_with_the_same_encoding
61
+ assert_equal euc_jp('Hi ゆきひろ!'), interpolate(:default => 'Hi {{name}}!', :name => euc_jp('ゆきひろ'))
62
+ end
63
+
64
+ def test_interpolate_given_a_unicode_value_hash_into_a_non_unicode_multibyte_string_raises_encoding_compatibility_error
65
+ assert_raises(Encoding::CompatibilityError) do
66
+ interpolate(:default => euc_jp('こんにちは、{{name}}さん!'), :name => 'ゆきひろ')
67
+ end
68
+ end
69
+
70
+ def test_interpolate_given_a_non_unicode_multibyte_value_hash_into_an_unicode_string_raises_encoding_compatibility_error
71
+ assert_raises(Encoding::CompatibilityError) do
72
+ interpolate(:default => 'こんにちは、{{name}}さん!', :name => euc_jp('ゆきひろ'))
73
+ end
74
+ end
75
+ end
76
+
77
+ def test_interpolate_given_a_string_containing_a_reserved_key_raises_reserved_interpolation_key
78
+ assert_raises(I18n::ReservedInterpolationKey) { interpolate(:default => '{{default}}', :foo => :bar) }
79
+ assert_raises(I18n::ReservedInterpolationKey) { interpolate(:default => '{{scope}}', :foo => :bar) }
80
+ assert_raises(I18n::ReservedInterpolationKey) { interpolate(:default => '{{separator}}', :foo => :bar) }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end