i18n 1.0.0 → 1.14.7

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +75 -32
  3. data/lib/i18n/backend/base.rb +93 -34
  4. data/lib/i18n/backend/cache.rb +10 -11
  5. data/lib/i18n/backend/cache_file.rb +36 -0
  6. data/lib/i18n/backend/cascade.rb +3 -1
  7. data/lib/i18n/backend/chain.rb +39 -6
  8. data/lib/i18n/backend/fallbacks.rb +48 -15
  9. data/lib/i18n/backend/flatten.rb +10 -5
  10. data/lib/i18n/backend/gettext.rb +4 -2
  11. data/lib/i18n/backend/interpolation_compiler.rb +8 -8
  12. data/lib/i18n/backend/key_value.rb +31 -4
  13. data/lib/i18n/backend/lazy_loadable.rb +184 -0
  14. data/lib/i18n/backend/memoize.rb +10 -2
  15. data/lib/i18n/backend/metadata.rb +5 -3
  16. data/lib/i18n/backend/pluralization.rb +61 -18
  17. data/lib/i18n/backend/simple.rb +44 -24
  18. data/lib/i18n/backend/transliterator.rb +26 -24
  19. data/lib/i18n/backend.rb +5 -1
  20. data/lib/i18n/config.rb +22 -4
  21. data/lib/i18n/exceptions.rb +71 -18
  22. data/lib/i18n/gettext/helpers.rb +4 -2
  23. data/lib/i18n/gettext/po_parser.rb +7 -7
  24. data/lib/i18n/gettext.rb +2 -0
  25. data/lib/i18n/interpolate/ruby.rb +22 -6
  26. data/lib/i18n/locale/fallbacks.rb +33 -22
  27. data/lib/i18n/locale/tag/parents.rb +8 -6
  28. data/lib/i18n/locale/tag/simple.rb +2 -2
  29. data/lib/i18n/locale.rb +2 -0
  30. data/lib/i18n/middleware.rb +2 -0
  31. data/lib/i18n/tests/basics.rb +5 -7
  32. data/lib/i18n/tests/defaults.rb +8 -1
  33. data/lib/i18n/tests/interpolation.rb +34 -7
  34. data/lib/i18n/tests/link.rb +11 -1
  35. data/lib/i18n/tests/localization/date.rb +37 -10
  36. data/lib/i18n/tests/localization/date_time.rb +28 -7
  37. data/lib/i18n/tests/localization/procs.rb +9 -7
  38. data/lib/i18n/tests/localization/time.rb +27 -5
  39. data/lib/i18n/tests/lookup.rb +11 -5
  40. data/lib/i18n/tests/pluralization.rb +1 -1
  41. data/lib/i18n/tests/procs.rb +23 -7
  42. data/lib/i18n/tests.rb +2 -0
  43. data/lib/i18n/utils.rb +55 -0
  44. data/lib/i18n/version.rb +3 -1
  45. data/lib/i18n.rb +179 -58
  46. metadata +16 -61
  47. data/gemfiles/Gemfile.rails-3.2.x +0 -10
  48. data/gemfiles/Gemfile.rails-4.0.x +0 -10
  49. data/gemfiles/Gemfile.rails-4.1.x +0 -10
  50. data/gemfiles/Gemfile.rails-4.2.x +0 -10
  51. data/gemfiles/Gemfile.rails-5.0.x +0 -10
  52. data/gemfiles/Gemfile.rails-5.1.x +0 -10
  53. data/gemfiles/Gemfile.rails-master +0 -10
  54. data/lib/i18n/core_ext/hash.rb +0 -29
  55. data/lib/i18n/core_ext/kernel/suppress_warnings.rb +0 -8
  56. data/lib/i18n/core_ext/string/interpolate.rb +0 -9
  57. data/test/api/all_features_test.rb +0 -58
  58. data/test/api/cascade_test.rb +0 -28
  59. data/test/api/chain_test.rb +0 -24
  60. data/test/api/fallbacks_test.rb +0 -30
  61. data/test/api/key_value_test.rb +0 -24
  62. data/test/api/memoize_test.rb +0 -56
  63. data/test/api/override_test.rb +0 -42
  64. data/test/api/pluralization_test.rb +0 -30
  65. data/test/api/simple_test.rb +0 -28
  66. data/test/backend/cache_test.rb +0 -109
  67. data/test/backend/cascade_test.rb +0 -86
  68. data/test/backend/chain_test.rb +0 -122
  69. data/test/backend/exceptions_test.rb +0 -36
  70. data/test/backend/fallbacks_test.rb +0 -219
  71. data/test/backend/interpolation_compiler_test.rb +0 -118
  72. data/test/backend/key_value_test.rb +0 -61
  73. data/test/backend/memoize_test.rb +0 -79
  74. data/test/backend/metadata_test.rb +0 -48
  75. data/test/backend/pluralization_test.rb +0 -45
  76. data/test/backend/simple_test.rb +0 -103
  77. data/test/backend/transliterator_test.rb +0 -84
  78. data/test/core_ext/hash_test.rb +0 -36
  79. data/test/gettext/api_test.rb +0 -214
  80. data/test/gettext/backend_test.rb +0 -92
  81. data/test/i18n/exceptions_test.rb +0 -117
  82. data/test/i18n/gettext_plural_keys_test.rb +0 -20
  83. data/test/i18n/interpolate_test.rb +0 -91
  84. data/test/i18n/load_path_test.rb +0 -34
  85. data/test/i18n/middleware_test.rb +0 -24
  86. data/test/i18n_test.rb +0 -462
  87. data/test/locale/fallbacks_test.rb +0 -133
  88. data/test/locale/tag/rfc4646_test.rb +0 -143
  89. data/test/locale/tag/simple_test.rb +0 -32
  90. data/test/run_all.rb +0 -20
  91. data/test/test_data/locales/de.po +0 -82
  92. data/test/test_data/locales/en.rb +0 -3
  93. data/test/test_data/locales/en.yml +0 -3
  94. data/test/test_data/locales/invalid/empty.yml +0 -0
  95. data/test/test_data/locales/invalid/syntax.yml +0 -4
  96. data/test/test_data/locales/plurals.rb +0 -113
  97. data/test/test_helper.rb +0 -61
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/backend/base'
4
+
1
5
  module I18n
2
6
  module Backend
3
7
  # A simple backend that reads translations from YAML files and stores them in
@@ -6,19 +10,20 @@ module I18n
6
10
  # The implementation is provided by a Implementation module allowing to easily
7
11
  # extend Simple backend's behavior by including modules. E.g.:
8
12
  #
9
- # module I18n::Backend::Pluralization
10
- # def pluralize(*args)
11
- # # extended pluralization logic
12
- # super
13
- # end
14
- # end
15
- #
16
- # I18n::Backend::Simple.include(I18n::Backend::Pluralization)
13
+ # module I18n::Backend::Pluralization
14
+ # def pluralize(*args)
15
+ # # extended pluralization logic
16
+ # super
17
+ # end
18
+ # end
19
+ #
20
+ # I18n::Backend::Simple.include(I18n::Backend::Pluralization)
17
21
  class Simple
18
- (class << self; self; end).class_eval { public :include }
19
-
20
22
  module Implementation
21
23
  include Base
24
+
25
+ # Mutex to ensure that concurrent translations loading will be thread-safe
26
+ MUTEX = Mutex.new
22
27
 
23
28
  def initialized?
24
29
  @initialized ||= false
@@ -28,17 +33,16 @@ module I18n
28
33
  # This uses a deep merge for the translations hash, so existing
29
34
  # translations will be overwritten by new ones only at the deepest
30
35
  # level of the hash.
31
- def store_translations(locale, data, options = {})
36
+ def store_translations(locale, data, options = EMPTY_HASH)
32
37
  if I18n.enforce_available_locales &&
33
38
  I18n.available_locales_initialized? &&
34
- !I18n.available_locales.include?(locale.to_sym) &&
35
- !I18n.available_locales.include?(locale.to_s)
39
+ !I18n.locale_available?(locale)
36
40
  return data
37
41
  end
38
42
  locale = locale.to_sym
39
- translations[locale] ||= {}
40
- data = data.deep_symbolize_keys
41
- translations[locale].deep_merge!(data)
43
+ translations[locale] ||= Concurrent::Hash.new
44
+ data = Utils.deep_symbolize_keys(data) unless options.fetch(:skip_symbolize_keys, false)
45
+ Utils.deep_merge!(translations[locale], data)
42
46
  end
43
47
 
44
48
  # Get available locales from the translations hash
@@ -57,6 +61,23 @@ module I18n
57
61
  super
58
62
  end
59
63
 
64
+ def eager_load!
65
+ init_translations unless initialized?
66
+ super
67
+ end
68
+
69
+ def translations(do_init: false)
70
+ # To avoid returning empty translations,
71
+ # call `init_translations`
72
+ init_translations if do_init && !initialized?
73
+
74
+ @translations ||= Concurrent::Hash.new do |h, k|
75
+ MUTEX.synchronize do
76
+ h[k] = Concurrent::Hash.new
77
+ end
78
+ end
79
+ end
80
+
60
81
  protected
61
82
 
62
83
  def init_translations
@@ -64,24 +85,23 @@ module I18n
64
85
  @initialized = true
65
86
  end
66
87
 
67
- def translations
68
- @translations ||= {}
69
- end
70
-
71
88
  # Looks up a translation from the translations hash. Returns nil if
72
89
  # either key is nil, or locale, scope or key do not exist as a key in the
73
90
  # nested translations hash. Splits keys or scopes containing dots
74
91
  # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
75
92
  # <tt>%w(currency format)</tt>.
76
- def lookup(locale, key, scope = [], options = {})
93
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
77
94
  init_translations unless initialized?
78
95
  keys = I18n.normalize_keys(locale, key, scope, options[:separator])
79
96
 
80
97
  keys.inject(translations) do |result, _key|
81
- _key = _key.to_sym
82
- return nil unless result.is_a?(Hash) && result.has_key?(_key)
98
+ return nil unless result.is_a?(Hash)
99
+ unless result.has_key?(_key)
100
+ _key = _key.to_s.to_sym
101
+ return nil unless result.has_key?(_key)
102
+ end
83
103
  result = result[_key]
84
- result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
104
+ result = resolve_entry(locale, _key, result, Utils.except(options.merge(:scope => nil), :count)) if result.is_a?(Symbol)
85
105
  result
86
106
  end
87
107
  end
@@ -1,4 +1,6 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
2
4
  module I18n
3
5
  module Backend
4
6
  module Transliterator
@@ -43,30 +45,30 @@ module I18n
43
45
  "Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I",
44
46
  "Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O",
45
47
  "Õ"=>"O", "Ö"=>"O", "×"=>"x", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U",
46
- "Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "à"=>"a", "á"=>"a", "â"=>"a",
47
- "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c", "è"=>"e", "é"=>"e",
48
- "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i", "ï"=>"i", "ð"=>"d",
49
- "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o", "ö"=>"o", "ø"=>"o",
50
- "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y", "þ"=>"th", "ÿ"=>"y",
51
- "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A", "ą"=>"a", "Ć"=>"C",
52
- "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c", "Č"=>"C", "č"=>"c",
53
- "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E", "ē"=>"e", "Ĕ"=>"E",
54
- "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e", "Ě"=>"E", "ě"=>"e",
55
- "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G", "ġ"=>"g", "Ģ"=>"G",
56
- "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h", "Ĩ"=>"I", "ĩ"=>"i",
57
- "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I", "į"=>"i", "İ"=>"I",
58
- "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j", "Ķ"=>"K", "ķ"=>"k",
59
- "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l", "Ľ"=>"L", "ľ"=>"l",
60
- "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N", "ń"=>"n", "Ņ"=>"N",
61
- "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG", "ŋ"=>"ng",
62
- "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o", "Œ"=>"OE",
63
- "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R", "ř"=>"r",
64
- "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s", "Š"=>"S",
65
- "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T", "ŧ"=>"t",
66
- "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u", "Ů"=>"U",
67
- "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W", "ŵ"=>"w",
68
- "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z", "ż"=>"z",
69
- "Ž"=>"Z", "ž"=>"z"
48
+ "Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", ""=>"SS", "à"=>"a",
49
+ "á"=>"a", "â"=>"a", "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c",
50
+ "è"=>"e", "é"=>"e", "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i",
51
+ "ï"=>"i", "ð"=>"d", "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o",
52
+ "ö"=>"o", "ø"=>"o", "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y",
53
+ "þ"=>"th", "ÿ"=>"y", "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A",
54
+ "ą"=>"a", "Ć"=>"C", "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c",
55
+ "Č"=>"C", "č"=>"c", "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E",
56
+ "ē"=>"e", "Ĕ"=>"E", "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e",
57
+ "Ě"=>"E", "ě"=>"e", "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G",
58
+ "ġ"=>"g", "Ģ"=>"G", "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h",
59
+ "Ĩ"=>"I", "ĩ"=>"i", "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I",
60
+ "į"=>"i", "İ"=>"I", "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j",
61
+ "Ķ"=>"K", "ķ"=>"k", "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l",
62
+ "Ľ"=>"L", "ľ"=>"l", "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N",
63
+ "ń"=>"n", "Ņ"=>"N", "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG",
64
+ "ŋ"=>"ng", "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o",
65
+ "Œ"=>"OE", "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R",
66
+ "ř"=>"r", "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s",
67
+ "Š"=>"S", "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T",
68
+ "ŧ"=>"t", "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u",
69
+ "Ů"=>"U", "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W",
70
+ "ŵ"=>"w", "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z",
71
+ "ż"=>"z", "Ž"=>"Z", "ž"=>"z"
70
72
  }.freeze
71
73
 
72
74
  def initialize(rule = nil)
data/lib/i18n/backend.rb CHANGED
@@ -1,14 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module I18n
2
4
  module Backend
3
5
  autoload :Base, 'i18n/backend/base'
4
- autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
5
6
  autoload :Cache, 'i18n/backend/cache'
7
+ autoload :CacheFile, 'i18n/backend/cache_file'
6
8
  autoload :Cascade, 'i18n/backend/cascade'
7
9
  autoload :Chain, 'i18n/backend/chain'
8
10
  autoload :Fallbacks, 'i18n/backend/fallbacks'
9
11
  autoload :Flatten, 'i18n/backend/flatten'
10
12
  autoload :Gettext, 'i18n/backend/gettext'
13
+ autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
11
14
  autoload :KeyValue, 'i18n/backend/key_value'
15
+ autoload :LazyLoadable, 'i18n/backend/lazy_loadable'
12
16
  autoload :Memoize, 'i18n/backend/memoize'
13
17
  autoload :Metadata, 'i18n/backend/metadata'
14
18
  autoload :Pluralization, 'i18n/backend/pluralization'
data/lib/i18n/config.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
 
3
5
  module I18n
@@ -5,7 +7,7 @@ module I18n
5
7
  # The only configuration value that is not global and scoped to thread is :locale.
6
8
  # It defaults to the default_locale.
7
9
  def locale
8
- defined?(@locale) && @locale ? @locale : default_locale
10
+ defined?(@locale) && @locale != nil ? @locale : default_locale
9
11
  end
10
12
 
11
13
  # Sets the current locale pseudo-globally, i.e. in the Thread.current hash.
@@ -36,7 +38,7 @@ module I18n
36
38
  end
37
39
 
38
40
  # Returns an array of locales for which translations are available.
39
- # Unless you explicitely set these through I18n.available_locales=
41
+ # Unless you explicitly set these through I18n.available_locales=
40
42
  # the call will be delegated to the backend.
41
43
  def available_locales
42
44
  @@available_locales ||= nil
@@ -57,7 +59,7 @@ module I18n
57
59
  @@available_locales = nil if @@available_locales.empty?
58
60
  @@available_locales_set = nil
59
61
  end
60
-
62
+
61
63
  # Returns true if the available_locales have been initialized
62
64
  def available_locales_initialized?
63
65
  ( !!defined?(@@available_locales) && !!@@available_locales )
@@ -104,7 +106,7 @@ module I18n
104
106
  # if you don't care about arity.
105
107
  #
106
108
  # == Example:
107
- # You can supress raising an exception and return string instead:
109
+ # You can suppress raising an exception and return string instead:
108
110
  #
109
111
  # I18n.config.missing_interpolation_argument_handler = Proc.new do |key|
110
112
  # "#{key} is missing"
@@ -143,5 +145,21 @@ module I18n
143
145
  def enforce_available_locales=(enforce_available_locales)
144
146
  @@enforce_available_locales = enforce_available_locales
145
147
  end
148
+
149
+ # Returns the current interpolation patterns. Defaults to
150
+ # I18n::DEFAULT_INTERPOLATION_PATTERNS.
151
+ def interpolation_patterns
152
+ @@interpolation_patterns ||= I18n::DEFAULT_INTERPOLATION_PATTERNS.dup
153
+ end
154
+
155
+ # Sets the current interpolation patterns. Used to set a interpolation
156
+ # patterns.
157
+ #
158
+ # E.g. using {{}} as a placeholder like "{{hello}}, world!":
159
+ #
160
+ # I18n.config.interpolation_patterns << /\{\{(\w+)\}\}/
161
+ def interpolation_patterns=(interpolation_patterns)
162
+ @@interpolation_patterns = interpolation_patterns
163
+ end
146
164
  end
147
165
  end
@@ -1,27 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cgi'
2
4
 
3
5
  module I18n
4
- # Handles exceptions raised in the backend. All exceptions except for
5
- # MissingTranslationData exceptions are re-thrown. When a MissingTranslationData
6
- # was caught the handler returns an error message string containing the key/scope.
7
- # Note that the exception handler is not called when the option :throw was given.
8
6
  class ExceptionHandler
9
- include Module.new {
10
- def call(exception, locale, key, options)
11
- case exception
12
- when MissingTranslation
13
- exception.message
14
- when Exception
15
- raise exception
16
- else
17
- throw :exception, exception
18
- end
7
+ def call(exception, _locale, _key, _options)
8
+ if exception.is_a?(MissingTranslation)
9
+ exception.message
10
+ else
11
+ raise exception
19
12
  end
20
- }
13
+ end
21
14
  end
22
15
 
23
16
  class ArgumentError < ::ArgumentError; end
24
17
 
18
+ class Disabled < ArgumentError
19
+ def initialize(method)
20
+ super(<<~MESSAGE)
21
+ I18n.#{method} is currently disabled, likely because your application is still in its loading phase.
22
+
23
+ This method is meant to display text in the user locale, so calling it before the user locale has
24
+ been set is likely to display text from the wrong locale to some users.
25
+
26
+ If you have a legitimate reason to access i18n data outside of the user flow, you can do so by passing
27
+ the desired locale explicitly with the `locale` argument, e.g. `I18n.#{method}(..., locale: :en)`
28
+ MESSAGE
29
+ end
30
+ end
31
+
25
32
  class InvalidLocale < ArgumentError
26
33
  attr_reader :locale
27
34
  def initialize(locale)
@@ -40,10 +47,12 @@ module I18n
40
47
 
41
48
  class MissingTranslation < ArgumentError
42
49
  module Base
50
+ PERMITTED_KEYS = [:scope, :default].freeze
51
+
43
52
  attr_reader :locale, :key, :options
44
53
 
45
- def initialize(locale, key, options = {})
46
- @key, @locale, @options = key, locale, options.dup
54
+ def initialize(locale, key, options = EMPTY_HASH)
55
+ @key, @locale, @options = key, locale, options.slice(*PERMITTED_KEYS)
47
56
  options.each { |k, v| self.options[k] = v.inspect if v.is_a?(Proc) }
48
57
  end
49
58
 
@@ -54,8 +63,18 @@ module I18n
54
63
  end
55
64
 
56
65
  def message
57
- "translation missing: #{keys.join('.')}"
66
+ if (default = options[:default]).is_a?(Array) && default.any?
67
+ other_options = ([key, *default]).map { |k| normalized_option(k).prepend('- ') }.join("\n")
68
+ "Translation missing. Options considered were:\n#{other_options}"
69
+ else
70
+ "Translation missing: #{keys.join('.')}"
71
+ end
58
72
  end
73
+
74
+ def normalized_option(key)
75
+ I18n.normalize_keys(locale, key, options[:scope]).join('.')
76
+ end
77
+
59
78
  alias :to_s :message
60
79
 
61
80
  def to_exception
@@ -101,4 +120,38 @@ module I18n
101
120
  super "can not load translations from #{filename}, the file type #{type} is not known"
102
121
  end
103
122
  end
123
+
124
+ class UnsupportedMethod < ArgumentError
125
+ attr_reader :method, :backend_klass, :msg
126
+ def initialize(method, backend_klass, msg)
127
+ @method = method
128
+ @backend_klass = backend_klass
129
+ @msg = msg
130
+ super "#{backend_klass} does not support the ##{method} method. #{msg}"
131
+ end
132
+ end
133
+
134
+ class InvalidFilenames < ArgumentError
135
+ NUMBER_OF_ERRORS_SHOWN = 20
136
+ def initialize(file_errors)
137
+ super <<~MSG
138
+ Found #{file_errors.count} error(s).
139
+ The first #{[file_errors.count, NUMBER_OF_ERRORS_SHOWN].min} error(s):
140
+ #{file_errors.map(&:message).first(NUMBER_OF_ERRORS_SHOWN).join("\n")}
141
+
142
+ To use the LazyLoadable backend:
143
+ 1. Filenames must start with the locale.
144
+ 2. An underscore must separate the locale with any optional text that follows.
145
+ 3. The file must only contain translation data for the single locale.
146
+
147
+ Example:
148
+ "/config/locales/fr.yml" which contains:
149
+ ```yml
150
+ fr:
151
+ dog:
152
+ chien
153
+ ```
154
+ MSG
155
+ end
156
+ end
104
157
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'i18n/gettext'
2
4
 
3
5
  module I18n
@@ -16,8 +18,8 @@ module I18n
16
18
  msgsid
17
19
  end
18
20
 
19
- def gettext(msgid, options = {})
20
- I18n.t(msgid, { :default => msgid, :separator => '|' }.merge(options))
21
+ def gettext(msgid, options = EMPTY_HASH)
22
+ I18n.t(msgid, **{:default => msgid, :separator => '|'}.merge(options))
21
23
  end
22
24
  alias _ gettext
23
25
 
@@ -28,7 +28,7 @@ module_eval <<'..end src/poparser.ry modeval..id7a99570e05', 'src/poparser.ry',
28
28
  ret.gsub!(/\\"/, "\"")
29
29
  ret
30
30
  end
31
-
31
+
32
32
  def parse(str, data, ignore_fuzzy = true)
33
33
  @comments = []
34
34
  @data = data
@@ -64,7 +64,7 @@ module_eval <<'..end src/poparser.ry modeval..id7a99570e05', 'src/poparser.ry',
64
64
  str = $'
65
65
  when /\A\#(.*)/
66
66
  @q.push [:COMMENT, $&]
67
- str = $'
67
+ str = $'
68
68
  when /\A\"(.*)\"/
69
69
  @q.push [:STRING, $1]
70
70
  str = $'
@@ -73,7 +73,7 @@ module_eval <<'..end src/poparser.ry modeval..id7a99570e05', 'src/poparser.ry',
73
73
  #@q.push [:STRING, c]
74
74
  str = str[1..-1]
75
75
  end
76
- end
76
+ end
77
77
  @q.push [false, '$end']
78
78
  if $DEBUG
79
79
  @q.each do |a,b|
@@ -88,7 +88,7 @@ module_eval <<'..end src/poparser.ry modeval..id7a99570e05', 'src/poparser.ry',
88
88
  end
89
89
  @data
90
90
  end
91
-
91
+
92
92
  def next_token
93
93
  @q.shift
94
94
  end
@@ -101,11 +101,11 @@ module_eval <<'..end src/poparser.ry modeval..id7a99570e05', 'src/poparser.ry',
101
101
  @comments.clear
102
102
  @msgctxt = ""
103
103
  end
104
-
104
+
105
105
  def on_comment(comment)
106
106
  @fuzzy = true if (/fuzzy/ =~ comment)
107
107
  @comments << comment
108
- end
108
+ end
109
109
 
110
110
 
111
111
  ..end src/poparser.ry modeval..id7a99570e05
@@ -245,7 +245,7 @@ module_eval <<'.,.,', 'src/poparser.ry', 25
245
245
 
246
246
  module_eval <<'.,.,', 'src/poparser.ry', 48
247
247
  def _reduce_8( val, _values, result )
248
- if @fuzzy and $ignore_fuzzy
248
+ if @fuzzy and $ignore_fuzzy
249
249
  if val[1] != ""
250
250
  $stderr.print _("Warning: fuzzy message was ignored.\n")
251
251
  $stderr.print " msgid '#{val[1]}'\n"
data/lib/i18n/gettext.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module I18n
2
4
  module Gettext
3
5
  PLURAL_SEPARATOR = "\001"
@@ -1,24 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # heavily based on Masao Mutoh's gettext String interpolation extension
2
4
  # http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb
3
5
 
4
6
  module I18n
5
- INTERPOLATION_PATTERN = Regexp.union(
7
+ DEFAULT_INTERPOLATION_PATTERNS = [
6
8
  /%%/,
7
- /%\{(\w+)\}/, # matches placeholders like "%{foo}"
8
- /%<(\w+)>(.*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%<foo>.d"
9
- )
9
+ /%\{([\w|]+)\}/, # matches placeholders like "%{foo} or %{foo|word}"
10
+ /%<(\w+)>([^\d]*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%<foo>.d"
11
+ ].freeze
12
+ INTERPOLATION_PATTERN = Regexp.union(DEFAULT_INTERPOLATION_PATTERNS)
13
+ deprecate_constant :INTERPOLATION_PATTERN
14
+
15
+ INTERPOLATION_PATTERNS_CACHE = Hash.new do |hash, patterns|
16
+ hash[patterns] = Regexp.union(patterns)
17
+ end
18
+ private_constant :INTERPOLATION_PATTERNS_CACHE
10
19
 
11
20
  class << self
12
21
  # Return String or raises MissingInterpolationArgument exception.
13
22
  # Missing argument's logic is handled by I18n.config.missing_interpolation_argument_handler.
14
23
  def interpolate(string, values)
15
- raise ReservedInterpolationKey.new($1.to_sym, string) if string =~ RESERVED_KEYS_PATTERN
24
+ raise ReservedInterpolationKey.new($1.to_sym, string) if string =~ I18n.reserved_keys_pattern
16
25
  raise ArgumentError.new('Interpolation values must be a Hash.') unless values.kind_of?(Hash)
17
26
  interpolate_hash(string, values)
18
27
  end
19
28
 
20
29
  def interpolate_hash(string, values)
21
- string.gsub(INTERPOLATION_PATTERN) do |match|
30
+ pattern = INTERPOLATION_PATTERNS_CACHE[config.interpolation_patterns]
31
+ interpolated = false
32
+
33
+ interpolated_string = string.gsub(pattern) do |match|
34
+ interpolated = true
35
+
22
36
  if match == '%%'
23
37
  '%'
24
38
  else
@@ -32,6 +46,8 @@ module I18n
32
46
  $3 ? sprintf("%#{$3}", value) : value
33
47
  end
34
48
  end
49
+
50
+ interpolated ? interpolated_string : string
35
51
  end
36
52
  end
37
53
  end
@@ -15,19 +15,12 @@
15
15
  # * all parent locales of a given locale (e.g. :es for :"es-MX") first,
16
16
  # * the current default locales and all of their parents second
17
17
  #
18
- # The default locales are set to [I18n.default_locale] by default but can be
19
- # set to something else.
18
+ # The default locales are set to [] by default but can be set to something else.
20
19
  #
21
20
  # One can additionally add any number of additional fallback locales manually.
22
21
  # These will be added before the default locales to the fallback chain. For
23
22
  # example:
24
23
  #
25
- # # using the default locale as default fallback locale
26
- #
27
- # I18n.default_locale = :"en-US"
28
- # I18n.fallbacks = I18n::Locale::Fallbacks.new(:"de-AT" => :"de-DE")
29
- # I18n.fallbacks[:"de-AT"] # => [:"de-AT", :"de-DE", :de, :"en-US", :en]
30
- #
31
24
  # # using a custom locale as default fallback locale
32
25
  #
33
26
  # I18n.fallbacks = I18n::Locale::Fallbacks.new(:"en-GB", :"de-AT" => :de, :"de-CH" => :de)
@@ -46,7 +39,7 @@
46
39
  # fallbacks[:"ar-PS"] # => [:"ar-PS", :ar, :"he-IL", :he, :"en-US", :en]
47
40
  # fallbacks[:"ar-EG"] # => [:"ar-EG", :ar, :"en-US", :en]
48
41
  #
49
- # # people speaking Sami as spoken in Finnland also speak Swedish and Finnish as spoken in Finnland
42
+ # # people speaking Sami as spoken in Finland also speak Swedish and Finnish as spoken in Finland
50
43
  # fallbacks.map(:sms => [:"se-FI", :"fi-FI"])
51
44
  # fallbacks[:sms] # => [:sms, :"se-FI", :se, :"fi-FI", :fi, :"en-US", :en]
52
45
 
@@ -56,40 +49,58 @@ module I18n
56
49
  def initialize(*mappings)
57
50
  @map = {}
58
51
  map(mappings.pop) if mappings.last.is_a?(Hash)
59
- self.defaults = mappings.empty? ? [I18n.default_locale.to_sym] : mappings
52
+ self.defaults = mappings.empty? ? [] : mappings
60
53
  end
61
54
 
62
55
  def defaults=(defaults)
63
- @defaults = defaults.map { |default| compute(default, false) }.flatten
56
+ @defaults = defaults.flat_map { |default| compute(default, false) }
64
57
  end
65
58
  attr_reader :defaults
66
59
 
67
60
  def [](locale)
68
61
  raise InvalidLocale.new(locale) if locale.nil?
62
+ raise Disabled.new('fallback#[]') if locale == false
69
63
  locale = locale.to_sym
70
64
  super || store(locale, compute(locale))
71
65
  end
72
66
 
73
- def map(mappings)
74
- mappings.each do |from, to|
75
- from, to = from.to_sym, Array(to)
76
- to.each do |_to|
77
- @map[from] ||= []
78
- @map[from] << _to.to_sym
67
+ def map(*args, &block)
68
+ if args.count == 1 && !block_given?
69
+ mappings = args.first
70
+ mappings.each do |from, to|
71
+ from, to = from.to_sym, Array(to)
72
+ to.each do |_to|
73
+ @map[from] ||= []
74
+ @map[from] << _to.to_sym
75
+ end
79
76
  end
77
+ else
78
+ @map.map(*args, &block)
80
79
  end
81
80
  end
82
81
 
82
+ def empty?
83
+ @map.empty? && @defaults.empty?
84
+ end
85
+
86
+ def inspect
87
+ "#<#{self.class.name} @map=#{@map.inspect} @defaults=#{@defaults.inspect}>"
88
+ end
89
+
83
90
  protected
84
91
 
85
92
  def compute(tags, include_defaults = true, exclude = [])
86
- result = Array(tags).collect do |tag|
93
+ result = []
94
+ Array(tags).each do |tag|
87
95
  tags = I18n::Locale::Tag.tag(tag).self_and_parents.map! { |t| t.to_sym } - exclude
88
- tags.each { |_tag| tags += compute(@map[_tag], false, exclude + tags) if @map[_tag] }
89
- tags
90
- end.flatten
96
+ result += tags
97
+ tags.each { |_tag| result += compute(@map[_tag], false, exclude + result) if @map[_tag] }
98
+ end
99
+
91
100
  result.push(*defaults) if include_defaults
92
- result.uniq.compact
101
+ result.uniq!
102
+ result.compact!
103
+ result
93
104
  end
94
105
  end
95
106
  end