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
@@ -0,0 +1,71 @@
1
+ # encoding: utf-8
2
+
3
+ # This module allows you to easily cache all responses from the backend - thus
4
+ # speeding up the I18n aspects of your application quite a bit.
5
+ #
6
+ # To enable caching you can simply include the Cache module to the Simple
7
+ # backend - or whatever other backend you are using:
8
+ #
9
+ # I18n::Backend::Simple.send(:include, I18n::Backend::Cache)
10
+ #
11
+ # You will also need to set a cache store implementation that you want to use:
12
+ #
13
+ # I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
14
+ #
15
+ # You can use any cache implementation you want that provides the same API as
16
+ # ActiveSupport::Cache (only the methods #fetch and #write are being used).
17
+ #
18
+ # The cache_key implementation assumes that you only pass values to
19
+ # I18n.translate that return a valid key from #hash (see
20
+ # http://www.ruby-doc.org/core/classes/Object.html#M000337).
21
+ module I18n
22
+ class << self
23
+ @@cache_store = nil
24
+ @@cache_namespace = nil
25
+
26
+ def cache_store
27
+ @@cache_store
28
+ end
29
+
30
+ def cache_store=(store)
31
+ @@cache_store = store
32
+ end
33
+
34
+ def cache_namespace
35
+ @@cache_namespace
36
+ end
37
+
38
+ def cache_namespace=(namespace)
39
+ @@cache_namespace = namespace
40
+ end
41
+
42
+ def perform_caching?
43
+ !cache_store.nil?
44
+ end
45
+ end
46
+
47
+ module Backend
48
+ module Cache
49
+ def translate(*args)
50
+ I18n.perform_caching? ? fetch(*args) { super } : super
51
+ end
52
+
53
+ protected
54
+
55
+ def fetch(*args, &block)
56
+ result = I18n.cache_store.fetch(cache_key(*args), &block)
57
+ raise result if result.is_a?(Exception)
58
+ result
59
+ rescue MissingTranslationData => exception
60
+ I18n.cache_store.write(cache_key(*args), exception)
61
+ raise exception
62
+ end
63
+
64
+ def cache_key(*args)
65
+ # this assumes that only simple, native Ruby values are passed to I18n.translate
66
+ keys = ['i18n', I18n.cache_namespace, args.hash]
67
+ keys.compact.join('-')
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+
3
+ module I18n
4
+ module Backend
5
+ # Backend that chains multiple other backends and checks each of them when
6
+ # a translation needs to be looked up. This is useful when you want to use
7
+ # standard translations with a Simple backend but store custom application
8
+ # translations in a database or other backends.
9
+ #
10
+ # To use the Chain backend instantiate it and set it to the I18n module.
11
+ # You can add chained backends through the initializer or backends
12
+ # accessor:
13
+ #
14
+ # # preserves the existing Simple backend set to I18n.backend
15
+ # I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
16
+ #
17
+ # The implementation assumes that all backends added to the Chain implement
18
+ # a lookup method with the same API as Simple backend does.
19
+ class Chain < Base
20
+ attr_accessor :backends
21
+
22
+ def initialize(*backends)
23
+ self.backends = backends
24
+ end
25
+
26
+ def reload!
27
+ backends.each { |backend| backend.reload! }
28
+ end
29
+
30
+ def store_translations(locale, data)
31
+ backends.first.store_translations(locale, data)
32
+ end
33
+
34
+ def available_locales
35
+ backends.map { |backend| backend.available_locales }.flatten.uniq
36
+ end
37
+
38
+ def localize(locale, object, format = :default, options = {})
39
+ backends.each do |backend|
40
+ begin
41
+ result = backend.localize(locale, object, format, options) and return result
42
+ rescue MissingTranslationData
43
+ end
44
+ end or nil
45
+ end
46
+
47
+ protected
48
+
49
+ def lookup(locale, key, scope = [], separator = nil)
50
+ return unless key
51
+ result = {}
52
+ backends.each do |backend|
53
+ entry = backend.lookup(locale, key, scope, separator)
54
+ if entry.is_a?(Hash)
55
+ result.merge!(entry)
56
+ elsif !entry.nil?
57
+ return entry
58
+ end
59
+ end
60
+ result.empty? ? nil : result
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'i18n/locale/fallbacks'
4
+
5
+ # I18n locale fallbacks are useful when you want your application to use
6
+ # translations from other locales when translations for the current locale are
7
+ # missing. E.g. you might want to use :en translations when translations in
8
+ # your applications main locale :de are missing.
9
+ #
10
+ # To enable locale fallbacks you can simply include the Fallbacks module to
11
+ # the Simple backend - or whatever other backend you are using:
12
+ #
13
+ # I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
14
+ module I18n
15
+ @@fallbacks = nil
16
+
17
+ class << self
18
+ # Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+.
19
+ def fallbacks
20
+ @@fallbacks ||= I18n::Locale::Fallbacks.new
21
+ end
22
+
23
+ # Sets the current fallbacks implementation. Use this to set a different fallbacks implementation.
24
+ def fallbacks=(fallbacks)
25
+ @@fallbacks = fallbacks
26
+ end
27
+ end
28
+
29
+ module Backend
30
+ module Fallbacks
31
+ # Overwrites the Base backend translate method so that it will try each
32
+ # locale given by I18n.fallbacks for the given locale. E.g. for the
33
+ # locale :"de-DE" it might try the locales :"de-DE", :de and :en
34
+ # (depends on the fallbacks implementation) until it finds a result with
35
+ # the given options. If it does not find any result for any of the
36
+ # locales it will then raise a MissingTranslationData exception as
37
+ # usual.
38
+ #
39
+ # The default option takes precedence over fallback locales, i.e. it
40
+ # will first evaluate a given default option before falling back to
41
+ # another locale.
42
+ def translate(locale, key, options = {})
43
+ I18n.fallbacks[locale].each do |fallback|
44
+ begin
45
+ result = super(fallback, key, options) and return result
46
+ rescue I18n::MissingTranslationData
47
+ end
48
+ end
49
+ raise(I18n::MissingTranslationData.new(locale, key, options))
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,9 +1,9 @@
1
+ require 'i18n/backend/fast/interpolation_compiler'
2
+
1
3
  module I18n
2
4
  module Backend
3
- class Fast < Simple
4
-
5
- # append any your custom pluralization keys to the constant
6
- PLURALIZATION_KEYS = [:zero, :one, :other]
5
+ class Fast < Base
6
+ SEPARATOR_ESCAPE_CHAR = "\001"
7
7
 
8
8
  def reset_flattened_translations!
9
9
  @flattened_translations = nil
@@ -22,28 +22,45 @@ module I18n
22
22
  super
23
23
  reset_flattened_translations!
24
24
  end
25
-
25
+
26
+ def translate(locale, key, opts = nil)
27
+ raise InvalidLocale.new(locale) unless locale
28
+ return key.map { |k| translate(locale, k, opts) } if key.is_a?(Array)
29
+
30
+ if opts
31
+ count = opts[:count]
32
+ scope = opts[:scope]
33
+
34
+ if entry = lookup(locale, key, scope, opts[:separator]) || ((default = opts.delete(:default)) && default(locale, key, default, opts))
35
+ entry = resolve(locale, key, entry, opts)
36
+ entry = pluralize(locale, entry, count) if count
37
+ entry = interpolate(locale, entry, opts)
38
+ entry
39
+ end
40
+ else
41
+ resolve(locale, key, lookup(locale, key), opts)
42
+ end || raise(I18n::MissingTranslationData.new(locale, key, opts))
43
+ end
44
+
26
45
  protected
27
- # {:a=>'a', :b=>{:c=>'c', :d=>'d', :f=>{:x=>'x'}}} => {:"b.c"=>"c", :"b.f.x"=>"x", :"b.d"=>"d", :a=>"a"}
46
+ # flatten_hash({:a=>'a', :b=>{:c=>'c', :d=>'d', :f=>{:x=>'x'}}})
47
+ # # => {:a=>'a', :b=>{:c=>'c', :d=>'d', :f=>{:x=>'x'}}, :"b.f" => {:x=>"x"}, :"b.c"=>"c", :"b.f.x"=>"x", :"b.d"=>"d"}
28
48
  def flatten_hash(h, nested_stack = [], flattened_h = {})
29
49
  h.each_pair do |k, v|
30
- new_nested_stack = nested_stack + [k]
31
-
32
- if v.kind_of?(Hash)
33
- # short circuit pluralization hashes
34
- flattened_h[nested_stack_to_flat_key(new_nested_stack)] = v if (v.keys & PLURALIZATION_KEYS).any?
35
-
36
- flatten_hash(v, new_nested_stack, flattened_h)
37
- else
38
- flattened_h[nested_stack_to_flat_key(new_nested_stack)] = PluralizationCompiler.compile_if_an_interpolation(v)
39
- end
50
+ new_nested_stack = nested_stack + [escape_default_separator(k)]
51
+ flattened_h[nested_stack_to_flat_key(new_nested_stack)] = InterpolationCompiler.compile_if_an_interpolation(v)
52
+ flatten_hash(v, new_nested_stack, flattened_h) if v.kind_of?(Hash)
40
53
  end
41
54
 
42
55
  flattened_h
43
56
  end
57
+
58
+ def escape_default_separator(key)
59
+ key.to_s.tr(I18n.default_separator, SEPARATOR_ESCAPE_CHAR)
60
+ end
44
61
 
45
62
  def nested_stack_to_flat_key(nested_stack)
46
- nested_stack.join('.').to_sym
63
+ nested_stack.join(I18n.default_separator).to_sym
47
64
  end
48
65
 
49
66
  def flatten_translations(translations)
@@ -53,14 +70,28 @@ module I18n
53
70
  flattened_h
54
71
  end
55
72
  end
56
-
57
- def interpolate(string, values)
58
- string.respond_to?(:i18n_interpolate) ? string.i18n_interpolate(values) : string
73
+
74
+ def interpolate(locale, string, values)
75
+ if string.respond_to?(:i18n_interpolate)
76
+ string.i18n_interpolate(values)
77
+ elsif values
78
+ super
79
+ else
80
+ string
81
+ end
59
82
  end
60
83
 
61
- def lookup(locale, key, scope = nil)
84
+ def lookup(locale, key, scope = nil, separator = nil)
62
85
  init_translations unless @initialized
63
- flattened_translations[locale.to_sym][(scope ? "#{Array(scope).join('.')}.#{key}" : key).to_sym] rescue nil
86
+ if separator
87
+ key = cleanup_non_standard_separator(key, separator)
88
+ scope = Array(scope).map{|k| cleanup_non_standard_separator(k, separator)} if scope
89
+ end
90
+ flattened_translations[locale.to_sym][(scope ? (Array(scope) + [key]).join(I18n.default_separator) : key).to_sym] rescue nil
91
+ end
92
+
93
+ def cleanup_non_standard_separator(key, user_separator)
94
+ escape_default_separator(key).tr(user_separator, I18n.default_separator)
64
95
  end
65
96
  end
66
97
  end
@@ -0,0 +1,84 @@
1
+ module I18n
2
+ module Backend
3
+ class Fast < Base
4
+ module InterpolationCompiler
5
+ extend self
6
+
7
+ TOKENIZER = /(\\\{\{[^\}]+\}\}|\{\{[^\}]+\}\})/
8
+ INTERPOLATION_SYNTAX_PATTERN = /(\\)?(\{\{([^\}]+)\}\})/
9
+
10
+ def compile_if_an_interpolation(string)
11
+ if interpolated_str?(string)
12
+ string.instance_eval <<-RUBY_EVAL, __FILE__, __LINE__
13
+ def i18n_interpolate(v = {})
14
+ "#{compiled_interpolation_body(string)}"
15
+ end
16
+ RUBY_EVAL
17
+ end
18
+
19
+ string
20
+ end
21
+
22
+ def interpolated_str?(str)
23
+ str.kind_of?(String) && str =~ INTERPOLATION_SYNTAX_PATTERN
24
+ end
25
+
26
+ protected
27
+ # tokenize("foo {{bar}} baz \\{{buz}}") # => ["foo ", "{{bar}}", " baz ", "\\{{buz}}"]
28
+ def tokenize(str)
29
+ str.split(TOKENIZER)
30
+ end
31
+
32
+ def compiled_interpolation_body(str)
33
+ tokenize(str).map do |token|
34
+ (matchdata = token.match(INTERPOLATION_SYNTAX_PATTERN)) ? handle_interpolation_token(token, matchdata) : escape_plain_str(token)
35
+ end.join
36
+ end
37
+
38
+ def handle_interpolation_token(interpolation, matchdata)
39
+ escaped, pattern, key = matchdata.values_at(1, 2, 3)
40
+ escaped ? pattern : compile_interpolation_token(key.to_sym)
41
+ end
42
+
43
+ def compile_interpolation_token(key)
44
+ "\#{#{interpolate_or_raise_missing(key)}}"
45
+ end
46
+
47
+ def interpolate_or_raise_missing(key)
48
+ escaped_key = escape_key_sym(key)
49
+ Base::RESERVED_KEYS.include?(key) ? reserved_key(escaped_key) : interpolate_key(escaped_key)
50
+ end
51
+
52
+ def interpolate_key(key)
53
+ [direct_key(key), nil_key(key), missing_key(key)].join('||')
54
+ end
55
+
56
+ def direct_key(key)
57
+ "((t = v[#{key}]) && t.respond_to?(:call) ? t.call : t)"
58
+ end
59
+
60
+ def nil_key(key)
61
+ "(v.has_key?(#{key}) && '')"
62
+ end
63
+
64
+ def missing_key(key)
65
+ "raise(MissingInterpolationArgument.new(#{key}, self))"
66
+ end
67
+
68
+ def reserved_key(key)
69
+ "raise(ReservedInterpolationKey.new(#{key}, self))"
70
+ end
71
+
72
+ def escape_plain_str(str)
73
+ str.gsub(/"|\\|#/) {|x| "\\#{x}"}
74
+ end
75
+
76
+ def escape_key_sym(key)
77
+ # rely on Ruby to do all the hard work :)
78
+ key.to_sym.inspect
79
+ end
80
+
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,65 @@
1
+ # encoding: utf-8
2
+
3
+ require 'i18n/gettext'
4
+ require File.expand_path(File.dirname(__FILE__) + '/../../../vendor/po_parser.rb')
5
+
6
+ # Experimental support for using Gettext po files to store translations.
7
+ #
8
+ # To use this you can simply include the module to the Simple backend - or
9
+ # whatever other backend you are using.
10
+ #
11
+ # I18n::Backend::Simple.send(:include, I18n::Backend::Gettext)
12
+ #
13
+ # Now you should be able to include your Gettext translation (*.po) files to
14
+ # the I18n.load_path so they're loaded to the backend and you can use them as
15
+ # usual:
16
+ #
17
+ # I18n.load_path += Dir["path/to/locales/*.po"]
18
+ #
19
+ # Following the Gettext convention this implementation expects that your
20
+ # translation files are named by their locales. E.g. the file en.po would
21
+ # contain the translations for the English locale.
22
+ module I18n
23
+ module Backend
24
+ module Gettext
25
+ class PoData < Hash
26
+ def set_comment(msgid_or_sym, comment)
27
+ # ignore
28
+ end
29
+ end
30
+
31
+ protected
32
+ def load_po(filename)
33
+ locale = ::File.basename(filename, '.po').to_sym
34
+ data = normalize(locale, parse(filename))
35
+ { locale => data }
36
+ end
37
+
38
+ def parse(filename)
39
+ GetText::PoParser.new.parse(::File.read(filename), PoData.new)
40
+ end
41
+
42
+ def normalize(locale, data)
43
+ data.inject({}) do |result, (key, value)|
44
+ key, value = normalize_pluralization(locale, key, value) if key.index("\000")
45
+ result[key] = value
46
+ result
47
+ end
48
+ end
49
+
50
+ def normalize_pluralization(locale, key, value)
51
+ # FIXME po_parser includes \000 chars that can not be turned into Symbols
52
+ key = key.dup.gsub("\000", I18n::Gettext::PLURAL_SEPARATOR)
53
+
54
+ keys = I18n::Gettext.plural_keys(locale)
55
+ values = value.split("\000")
56
+ raise "invalid number of plurals: #{values.size}, keys: #{keys.inspect}" if values.size != keys.size
57
+
58
+ result = {}
59
+ values.each_with_index { |value, ix| result[keys[ix]] = value }
60
+ [key, result]
61
+ end
62
+
63
+ end
64
+ end
65
+ end