i18n 0.4.0 → 1.14.4
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +0 -0
- data/README.md +127 -0
- data/lib/i18n/backend/base.rb +189 -111
- data/lib/i18n/backend/cache.rb +58 -22
- data/lib/i18n/backend/cache_file.rb +36 -0
- data/lib/i18n/backend/cascade.rb +9 -10
- data/lib/i18n/backend/chain.rb +95 -42
- data/lib/i18n/backend/fallbacks.rb +68 -22
- data/lib/i18n/backend/flatten.rb +13 -8
- data/lib/i18n/backend/gettext.rb +33 -25
- data/lib/i18n/backend/interpolation_compiler.rb +12 -14
- data/lib/i18n/backend/key_value.rb +112 -10
- data/lib/i18n/backend/lazy_loadable.rb +184 -0
- data/lib/i18n/backend/memoize.rb +13 -7
- data/lib/i18n/backend/metadata.rb +12 -6
- data/lib/i18n/backend/pluralization.rb +64 -25
- data/lib/i18n/backend/simple.rb +44 -18
- data/lib/i18n/backend/transliterator.rb +43 -33
- data/lib/i18n/backend.rb +5 -3
- data/lib/i18n/config.rb +91 -10
- data/lib/i18n/exceptions.rb +118 -22
- data/lib/i18n/gettext/helpers.rb +14 -4
- data/lib/i18n/gettext/po_parser.rb +7 -7
- data/lib/i18n/gettext.rb +6 -5
- data/lib/i18n/interpolate/ruby.rb +53 -0
- data/lib/i18n/locale/fallbacks.rb +33 -26
- data/lib/i18n/locale/tag/parents.rb +8 -8
- data/lib/i18n/locale/tag/rfc4646.rb +0 -2
- data/lib/i18n/locale/tag/simple.rb +2 -4
- data/lib/i18n/locale.rb +2 -0
- data/lib/i18n/middleware.rb +17 -0
- data/lib/i18n/tests/basics.rb +58 -0
- data/lib/i18n/tests/defaults.rb +52 -0
- data/lib/i18n/tests/interpolation.rb +167 -0
- data/lib/i18n/tests/link.rb +66 -0
- data/lib/i18n/tests/localization/date.rb +122 -0
- data/lib/i18n/tests/localization/date_time.rb +103 -0
- data/lib/i18n/tests/localization/procs.rb +118 -0
- data/lib/i18n/tests/localization/time.rb +103 -0
- data/lib/i18n/tests/localization.rb +19 -0
- data/lib/i18n/tests/lookup.rb +81 -0
- data/lib/i18n/tests/pluralization.rb +35 -0
- data/lib/i18n/tests/procs.rb +66 -0
- data/lib/i18n/tests.rb +14 -0
- data/lib/i18n/utils.rb +55 -0
- data/lib/i18n/version.rb +3 -1
- data/lib/i18n.rb +200 -87
- metadata +64 -56
- data/CHANGELOG.textile +0 -135
- data/README.textile +0 -93
- data/lib/i18n/backend/active_record/missing.rb +0 -65
- data/lib/i18n/backend/active_record/store_procs.rb +0 -38
- data/lib/i18n/backend/active_record/translation.rb +0 -93
- data/lib/i18n/backend/active_record.rb +0 -61
- data/lib/i18n/backend/cldr.rb +0 -100
- data/lib/i18n/core_ext/hash.rb +0 -29
- data/lib/i18n/core_ext/string/interpolate.rb +0 -98
@@ -1,9 +1,26 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'i18n/backend/base'
|
4
|
-
require 'active_support/json'
|
5
4
|
|
6
5
|
module I18n
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'oj'
|
9
|
+
class JSON
|
10
|
+
class << self
|
11
|
+
def encode(value)
|
12
|
+
Oj::Rails.encode(value)
|
13
|
+
end
|
14
|
+
def decode(value)
|
15
|
+
Oj.load(value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
require 'active_support/json'
|
21
|
+
JSON = ActiveSupport::JSON
|
22
|
+
end
|
23
|
+
|
7
24
|
module Backend
|
8
25
|
# This is a basic backend for key value stores. It receives on
|
9
26
|
# initialization the store, which should respond to three methods:
|
@@ -59,7 +76,11 @@ module I18n
|
|
59
76
|
@store, @subtrees = store, subtrees
|
60
77
|
end
|
61
78
|
|
62
|
-
def
|
79
|
+
def initialized?
|
80
|
+
!@store.nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
def store_translations(locale, data, options = EMPTY_HASH)
|
63
84
|
escape = options.fetch(:escape, true)
|
64
85
|
flatten_translations(locale, data, escape, @subtrees).each do |key, value|
|
65
86
|
key = "#{locale}.#{key}"
|
@@ -67,14 +88,14 @@ module I18n
|
|
67
88
|
case value
|
68
89
|
when Hash
|
69
90
|
if @subtrees && (old_value = @store[key])
|
70
|
-
old_value =
|
71
|
-
value =
|
91
|
+
old_value = JSON.decode(old_value)
|
92
|
+
value = Utils.deep_merge!(Utils.deep_symbolize_keys(old_value), value) if old_value.is_a?(Hash)
|
72
93
|
end
|
73
94
|
when Proc
|
74
95
|
raise "Key-value stores cannot handle procs"
|
75
96
|
end
|
76
97
|
|
77
|
-
@store[key] =
|
98
|
+
@store[key] = JSON.encode(value) unless value.is_a?(Symbol)
|
78
99
|
end
|
79
100
|
end
|
80
101
|
|
@@ -88,15 +109,96 @@ module I18n
|
|
88
109
|
|
89
110
|
protected
|
90
111
|
|
91
|
-
|
112
|
+
# Queries the translations from the key-value store and converts
|
113
|
+
# them into a hash such as the one returned from loading the
|
114
|
+
# haml files
|
115
|
+
def translations
|
116
|
+
@translations = Utils.deep_symbolize_keys(@store.keys.clone.map do |main_key|
|
117
|
+
main_value = JSON.decode(@store[main_key])
|
118
|
+
main_key.to_s.split(".").reverse.inject(main_value) do |value, key|
|
119
|
+
{key.to_sym => value}
|
120
|
+
end
|
121
|
+
end.inject{|hash, elem| Utils.deep_merge!(hash, elem)})
|
122
|
+
end
|
123
|
+
|
124
|
+
def init_translations
|
125
|
+
# NO OP
|
126
|
+
# This call made also inside Simple Backend and accessed by
|
127
|
+
# other plugins like I18n-js and babilu and
|
128
|
+
# to use it along with the Chain backend we need to
|
129
|
+
# provide a uniform API even for protected methods :S
|
130
|
+
end
|
131
|
+
|
132
|
+
def subtrees?
|
133
|
+
@subtrees
|
134
|
+
end
|
135
|
+
|
136
|
+
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
92
137
|
key = normalize_flat_keys(locale, key, scope, options[:separator])
|
93
138
|
value = @store["#{locale}.#{key}"]
|
94
|
-
value =
|
95
|
-
|
139
|
+
value = JSON.decode(value) if value
|
140
|
+
|
141
|
+
if value.is_a?(Hash)
|
142
|
+
Utils.deep_symbolize_keys(value)
|
143
|
+
elsif !value.nil?
|
144
|
+
value
|
145
|
+
elsif !@subtrees
|
146
|
+
SubtreeProxy.new("#{locale}.#{key}", @store)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def pluralize(locale, entry, count)
|
151
|
+
if subtrees?
|
152
|
+
super
|
153
|
+
else
|
154
|
+
return entry unless entry.is_a?(Hash)
|
155
|
+
key = pluralization_key(entry, count)
|
156
|
+
entry[key]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class SubtreeProxy
|
162
|
+
def initialize(master_key, store)
|
163
|
+
@master_key = master_key
|
164
|
+
@store = store
|
165
|
+
@subtree = nil
|
166
|
+
end
|
167
|
+
|
168
|
+
def has_key?(key)
|
169
|
+
@subtree && @subtree.has_key?(key) || self[key]
|
170
|
+
end
|
171
|
+
|
172
|
+
def [](key)
|
173
|
+
unless @subtree && value = @subtree[key]
|
174
|
+
value = @store["#{@master_key}.#{key}"]
|
175
|
+
if value
|
176
|
+
value = JSON.decode(value)
|
177
|
+
(@subtree ||= {})[key] = value
|
178
|
+
end
|
179
|
+
end
|
180
|
+
value
|
181
|
+
end
|
182
|
+
|
183
|
+
def is_a?(klass)
|
184
|
+
Hash == klass || super
|
185
|
+
end
|
186
|
+
alias :kind_of? :is_a?
|
187
|
+
|
188
|
+
def instance_of?(klass)
|
189
|
+
Hash == klass || super
|
190
|
+
end
|
191
|
+
|
192
|
+
def nil?
|
193
|
+
@subtree.nil?
|
194
|
+
end
|
195
|
+
|
196
|
+
def inspect
|
197
|
+
@subtree.inspect
|
96
198
|
end
|
97
199
|
end
|
98
200
|
|
99
201
|
include Implementation
|
100
202
|
end
|
101
203
|
end
|
102
|
-
end
|
204
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18n
|
4
|
+
module Backend
|
5
|
+
# Backend that lazy loads translations based on the current locale. This
|
6
|
+
# implementation avoids loading all translations up front. Instead, it only
|
7
|
+
# loads the translations that belong to the current locale. This offers a
|
8
|
+
# performance incentive in local development and test environments for
|
9
|
+
# applications with many translations for many different locales. It's
|
10
|
+
# particularly useful when the application only refers to a single locales'
|
11
|
+
# translations at a time (ex. A Rails workload). The implementation
|
12
|
+
# identifies which translation files from the load path belong to the
|
13
|
+
# current locale by pattern matching against their path name.
|
14
|
+
#
|
15
|
+
# Specifically, a translation file is considered to belong to a locale if:
|
16
|
+
# a) the filename is in the I18n load path
|
17
|
+
# b) the filename ends in a supported extension (ie. .yml, .json, .po, .rb)
|
18
|
+
# c) the filename starts with the locale identifier
|
19
|
+
# d) the locale identifier and optional proceeding text is separated by an underscore, ie. "_".
|
20
|
+
#
|
21
|
+
# Examples:
|
22
|
+
# Valid files that will be selected by this backend:
|
23
|
+
#
|
24
|
+
# "files/locales/en_translation.yml" (Selected for locale "en")
|
25
|
+
# "files/locales/fr.po" (Selected for locale "fr")
|
26
|
+
#
|
27
|
+
# Invalid files that won't be selected by this backend:
|
28
|
+
#
|
29
|
+
# "files/locales/translation-file"
|
30
|
+
# "files/locales/en-translation.unsupported"
|
31
|
+
# "files/locales/french/translation.yml"
|
32
|
+
# "files/locales/fr/translation.yml"
|
33
|
+
#
|
34
|
+
# The implementation uses this assumption to defer the loading of
|
35
|
+
# translation files until the current locale actually requires them.
|
36
|
+
#
|
37
|
+
# The backend has two working modes: lazy_load and eager_load.
|
38
|
+
#
|
39
|
+
# Note: This backend should only be enabled in test environments!
|
40
|
+
# When the mode is set to false, the backend behaves exactly like the
|
41
|
+
# Simple backend, with an additional check that the paths being loaded
|
42
|
+
# abide by the format. If paths can't be matched to the format, an error is raised.
|
43
|
+
#
|
44
|
+
# You can configure lazy loaded backends through the initializer or backends
|
45
|
+
# accessor:
|
46
|
+
#
|
47
|
+
# # In test environments
|
48
|
+
#
|
49
|
+
# I18n.backend = I18n::Backend::LazyLoadable.new(lazy_load: true)
|
50
|
+
#
|
51
|
+
# # In other environments, such as production and CI
|
52
|
+
#
|
53
|
+
# I18n.backend = I18n::Backend::LazyLoadable.new(lazy_load: false) # default
|
54
|
+
#
|
55
|
+
class LocaleExtractor
|
56
|
+
class << self
|
57
|
+
def locale_from_path(path)
|
58
|
+
name = File.basename(path, ".*")
|
59
|
+
locale = name.split("_").first
|
60
|
+
locale.to_sym unless locale.nil?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class LazyLoadable < Simple
|
66
|
+
def initialize(lazy_load: false)
|
67
|
+
@lazy_load = lazy_load
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns whether the current locale is initialized.
|
71
|
+
def initialized?
|
72
|
+
if lazy_load?
|
73
|
+
initialized_locales[I18n.locale]
|
74
|
+
else
|
75
|
+
super
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Clean up translations and uninitialize all locales.
|
80
|
+
def reload!
|
81
|
+
if lazy_load?
|
82
|
+
@initialized_locales = nil
|
83
|
+
@translations = nil
|
84
|
+
else
|
85
|
+
super
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Eager loading is not supported in the lazy context.
|
90
|
+
def eager_load!
|
91
|
+
if lazy_load?
|
92
|
+
raise UnsupportedMethod.new(__method__, self.class, "Cannot eager load translations because backend was configured with lazy_load: true.")
|
93
|
+
else
|
94
|
+
super
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Parse the load path and extract all locales.
|
99
|
+
def available_locales
|
100
|
+
if lazy_load?
|
101
|
+
I18n.load_path.map { |path| LocaleExtractor.locale_from_path(path) }.uniq
|
102
|
+
else
|
103
|
+
super
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
108
|
+
if lazy_load?
|
109
|
+
I18n.with_locale(locale) do
|
110
|
+
super
|
111
|
+
end
|
112
|
+
else
|
113
|
+
super
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
protected
|
118
|
+
|
119
|
+
|
120
|
+
# Load translations from files that belong to the current locale.
|
121
|
+
def init_translations
|
122
|
+
file_errors = if lazy_load?
|
123
|
+
initialized_locales[I18n.locale] = true
|
124
|
+
load_translations_and_collect_file_errors(filenames_for_current_locale)
|
125
|
+
else
|
126
|
+
@initialized = true
|
127
|
+
load_translations_and_collect_file_errors(I18n.load_path)
|
128
|
+
end
|
129
|
+
|
130
|
+
raise InvalidFilenames.new(file_errors) unless file_errors.empty?
|
131
|
+
end
|
132
|
+
|
133
|
+
def initialized_locales
|
134
|
+
@initialized_locales ||= Hash.new(false)
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def lazy_load?
|
140
|
+
@lazy_load
|
141
|
+
end
|
142
|
+
|
143
|
+
class FilenameIncorrect < StandardError
|
144
|
+
def initialize(file, expected_locale, unexpected_locales)
|
145
|
+
super "#{file} can only load translations for \"#{expected_locale}\". Found translations for: #{unexpected_locales}."
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Loads each file supplied and asserts that the file only loads
|
150
|
+
# translations as expected by the name. The method returns a list of
|
151
|
+
# errors corresponding to offending files.
|
152
|
+
def load_translations_and_collect_file_errors(files)
|
153
|
+
errors = []
|
154
|
+
|
155
|
+
load_translations(files) do |file, loaded_translations|
|
156
|
+
assert_file_named_correctly!(file, loaded_translations)
|
157
|
+
rescue FilenameIncorrect => e
|
158
|
+
errors << e
|
159
|
+
end
|
160
|
+
|
161
|
+
errors
|
162
|
+
end
|
163
|
+
|
164
|
+
# Select all files from I18n load path that belong to current locale.
|
165
|
+
# These files must start with the locale identifier (ie. "en", "pt-BR"),
|
166
|
+
# followed by an "_" demarcation to separate proceeding text.
|
167
|
+
def filenames_for_current_locale
|
168
|
+
I18n.load_path.flatten.select do |path|
|
169
|
+
LocaleExtractor.locale_from_path(path) == I18n.locale
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Checks if a filename is named in correspondence to the translations it loaded.
|
174
|
+
# The locale extracted from the path must be the single locale loaded in the translations.
|
175
|
+
def assert_file_named_correctly!(file, translations)
|
176
|
+
loaded_locales = translations.keys.map(&:to_sym)
|
177
|
+
expected_locale = LocaleExtractor.locale_from_path(file)
|
178
|
+
unexpected_locales = loaded_locales.reject { |locale| locale == expected_locale }
|
179
|
+
|
180
|
+
raise FilenameIncorrect.new(file, expected_locale, unexpected_locales) unless unexpected_locales.empty?
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
data/lib/i18n/backend/memoize.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
3
|
# Memoize module simply memoizes the values returned by lookup using
|
4
4
|
# a flat hash and can tremendously speed up the lookup process in a backend.
|
5
5
|
#
|
6
6
|
# To enable it you can simply include the Memoize module to your backend:
|
7
7
|
#
|
8
|
-
# I18n::Backend::Simple.
|
8
|
+
# I18n::Backend::Simple.include(I18n::Backend::Memoize)
|
9
9
|
#
|
10
10
|
# Notice that it's the responsibility of the backend to define whenever the
|
11
11
|
# cache should be cleaned.
|
@@ -16,7 +16,7 @@ module I18n
|
|
16
16
|
@memoized_locales ||= super
|
17
17
|
end
|
18
18
|
|
19
|
-
def store_translations(locale, data, options =
|
19
|
+
def store_translations(locale, data, options = EMPTY_HASH)
|
20
20
|
reset_memoizations!(locale)
|
21
21
|
super
|
22
22
|
end
|
@@ -26,9 +26,15 @@ module I18n
|
|
26
26
|
super
|
27
27
|
end
|
28
28
|
|
29
|
+
def eager_load!
|
30
|
+
memoized_lookup
|
31
|
+
available_locales
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
29
35
|
protected
|
30
36
|
|
31
|
-
def lookup(locale, key, scope = nil, options =
|
37
|
+
def lookup(locale, key, scope = nil, options = EMPTY_HASH)
|
32
38
|
flat_key = I18n::Backend::Flatten.normalize_flat_keys(locale,
|
33
39
|
key, scope, options[:separator]).to_sym
|
34
40
|
flat_hash = memoized_lookup[locale.to_sym]
|
@@ -36,7 +42,7 @@ module I18n
|
|
36
42
|
end
|
37
43
|
|
38
44
|
def memoized_lookup
|
39
|
-
@memoized_lookup ||=
|
45
|
+
@memoized_lookup ||= I18n.new_double_nested_cache
|
40
46
|
end
|
41
47
|
|
42
48
|
def reset_memoizations!(locale=nil)
|
@@ -45,4 +51,4 @@ module I18n
|
|
45
51
|
end
|
46
52
|
end
|
47
53
|
end
|
48
|
-
end
|
54
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# I18n translation metadata is useful when you want to access information
|
2
4
|
# about how a translation was looked up, pluralized or interpolated in
|
3
5
|
# your application.
|
@@ -12,7 +14,7 @@
|
|
12
14
|
# To enable translation metadata you can simply include the Metadata module
|
13
15
|
# into the Simple backend class - or whatever other backend you are using:
|
14
16
|
#
|
15
|
-
# I18n::Backend::Simple.
|
17
|
+
# I18n::Backend::Simple.include(I18n::Backend::Metadata)
|
16
18
|
#
|
17
19
|
module I18n
|
18
20
|
module Backend
|
@@ -21,29 +23,33 @@ module I18n
|
|
21
23
|
def included(base)
|
22
24
|
Object.class_eval do
|
23
25
|
def translation_metadata
|
24
|
-
|
26
|
+
unless self.frozen?
|
27
|
+
@translation_metadata ||= {}
|
28
|
+
else
|
29
|
+
{}
|
30
|
+
end
|
25
31
|
end
|
26
32
|
|
27
33
|
def translation_metadata=(translation_metadata)
|
28
|
-
@translation_metadata = translation_metadata
|
34
|
+
@translation_metadata = translation_metadata unless self.frozen?
|
29
35
|
end
|
30
36
|
end unless Object.method_defined?(:translation_metadata)
|
31
37
|
end
|
32
38
|
end
|
33
39
|
|
34
|
-
def translate(locale, key, options =
|
40
|
+
def translate(locale, key, options = EMPTY_HASH)
|
35
41
|
metadata = {
|
36
42
|
:locale => locale,
|
37
43
|
:key => key,
|
38
44
|
:scope => options[:scope],
|
39
45
|
:default => options[:default],
|
40
46
|
:separator => options[:separator],
|
41
|
-
:values => options.reject { |name,
|
47
|
+
:values => options.reject { |name, _value| RESERVED_KEYS.include?(name) }
|
42
48
|
}
|
43
49
|
with_metadata(metadata) { super }
|
44
50
|
end
|
45
51
|
|
46
|
-
def interpolate(locale, entry, values =
|
52
|
+
def interpolate(locale, entry, values = EMPTY_HASH)
|
47
53
|
metadata = entry.translation_metadata.merge(:original => entry)
|
48
54
|
with_metadata(metadata) { super }
|
49
55
|
end
|
@@ -1,15 +1,13 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# I18n
|
4
|
-
#
|
5
|
-
# missing. E.g. you might want to use :en translations when translations in
|
6
|
-
# your applications main locale :de are missing.
|
3
|
+
# I18n Pluralization are useful when you want your application to
|
4
|
+
# customize pluralization rules.
|
7
5
|
#
|
8
6
|
# To enable locale specific pluralizations you can simply include the
|
9
7
|
# Pluralization module to the Simple backend - or whatever other backend you
|
10
8
|
# are using.
|
11
9
|
#
|
12
|
-
# I18n::Backend::Simple.
|
10
|
+
# I18n::Backend::Simple.include(I18n::Backend::Pluralization)
|
13
11
|
#
|
14
12
|
# You also need to make sure to provide pluralization algorithms to the
|
15
13
|
# backend, i.e. include them to your I18n.load_path accordingly.
|
@@ -18,26 +16,57 @@ module I18n
|
|
18
16
|
module Pluralization
|
19
17
|
# Overwrites the Base backend translate method so that it will check the
|
20
18
|
# translation meta data space (:i18n) for a locale specific pluralization
|
21
|
-
# rule and use it to pluralize the given entry. I.e
|
19
|
+
# rule and use it to pluralize the given entry. I.e., the library expects
|
22
20
|
# pluralization rules to be stored at I18n.t(:'i18n.plural.rule')
|
23
21
|
#
|
24
|
-
# Pluralization rules are expected to respond to #call(
|
25
|
-
# return a pluralization key. Valid keys depend on the
|
26
|
-
#
|
27
|
-
#
|
22
|
+
# Pluralization rules are expected to respond to #call(count) and
|
23
|
+
# return a pluralization key. Valid keys depend on the pluralization
|
24
|
+
# rules for the locale, as defined in the CLDR.
|
25
|
+
# As of v41, 6 locale-specific plural categories are defined:
|
26
|
+
# :few, :many, :one, :other, :two, :zero
|
28
27
|
#
|
29
|
-
# The :
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
28
|
+
# n.b., The :one plural category does not imply the number 1.
|
29
|
+
# Instead, :one is a category for any number that behaves like 1 in
|
30
|
+
# that locale. For example, in some locales, :one is used for numbers
|
31
|
+
# that end in "1" (like 1, 21, 151) but that don't end in
|
32
|
+
# 11 (like 11, 111, 10311).
|
33
|
+
# Similar notes apply to the :two, and :zero plural categories.
|
34
|
+
#
|
35
|
+
# If you want to have different strings for the categories of count == 0
|
36
|
+
# (e.g. "I don't have any cars") or count == 1 (e.g. "I have a single car")
|
37
|
+
# use the explicit `"0"` and `"1"` keys.
|
38
|
+
# https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
|
33
39
|
def pluralize(locale, entry, count)
|
34
|
-
return entry unless entry.is_a?(Hash)
|
40
|
+
return entry unless entry.is_a?(Hash) && count
|
35
41
|
|
36
42
|
pluralizer = pluralizer(locale)
|
37
43
|
if pluralizer.respond_to?(:call)
|
38
|
-
|
39
|
-
|
40
|
-
|
44
|
+
# Deprecation: The use of the `zero` key in this way is incorrect.
|
45
|
+
# Users that want a different string for the case of `count == 0` should use the explicit "0" key instead.
|
46
|
+
# We keep this incorrect behaviour for now for backwards compatibility until we can remove it.
|
47
|
+
# Ref: https://github.com/ruby-i18n/i18n/issues/629
|
48
|
+
return entry[:zero] if count == 0 && entry.has_key?(:zero)
|
49
|
+
|
50
|
+
# "0" and "1" are special cases
|
51
|
+
# https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
|
52
|
+
if count == 0 || count == 1
|
53
|
+
value = entry[symbolic_count(count)]
|
54
|
+
return value if value
|
55
|
+
end
|
56
|
+
|
57
|
+
# Lateral Inheritance of "count" attribute (http://www.unicode.org/reports/tr35/#Lateral_Inheritance):
|
58
|
+
# > If there is no value for a path, and that path has a [@count="x"] attribute and value, then:
|
59
|
+
# > 1. If "x" is numeric, the path falls back to the path with [@count=«the plural rules category for x for that locale»], within that the same locale.
|
60
|
+
# > 2. If "x" is anything but "other", it falls back to a path [@count="other"], within that the same locale.
|
61
|
+
# > 3. If "x" is "other", it falls back to the path that is completely missing the count item, within that the same locale.
|
62
|
+
# Note: We don't yet implement #3 above, since we haven't decided how lateral inheritance attributes should be represented.
|
63
|
+
plural_rule_category = pluralizer.call(count)
|
64
|
+
|
65
|
+
value = if entry.has_key?(plural_rule_category) || entry.has_key?(:other)
|
66
|
+
entry[plural_rule_category] || entry[:other]
|
67
|
+
else
|
68
|
+
raise InvalidPluralizationData.new(entry, count, plural_rule_category)
|
69
|
+
end
|
41
70
|
else
|
42
71
|
super
|
43
72
|
end
|
@@ -45,13 +74,23 @@ module I18n
|
|
45
74
|
|
46
75
|
protected
|
47
76
|
|
48
|
-
|
49
|
-
|
50
|
-
|
77
|
+
def pluralizers
|
78
|
+
@pluralizers ||= {}
|
79
|
+
end
|
51
80
|
|
52
|
-
|
53
|
-
|
54
|
-
|
81
|
+
def pluralizer(locale)
|
82
|
+
pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Normalizes categories of 0.0 and 1.0
|
88
|
+
# and returns the symbolic version
|
89
|
+
def symbolic_count(count)
|
90
|
+
count = 0 if count == 0
|
91
|
+
count = 1 if count == 1
|
92
|
+
count.to_s.to_sym
|
93
|
+
end
|
55
94
|
end
|
56
95
|
end
|
57
96
|
end
|
data/lib/i18n/backend/simple.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n/backend/base'
|
2
4
|
|
3
5
|
module I18n
|
4
6
|
module Backend
|
@@ -15,10 +17,13 @@ module I18n
|
|
15
17
|
# end
|
16
18
|
# end
|
17
19
|
#
|
18
|
-
# I18n::Backend::Simple.
|
20
|
+
# I18n::Backend::Simple.include(I18n::Backend::Pluralization)
|
19
21
|
class Simple
|
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,18 +33,23 @@ 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)
|
37
|
+
if I18n.enforce_available_locales &&
|
38
|
+
I18n.available_locales_initialized? &&
|
39
|
+
!I18n.locale_available?(locale)
|
40
|
+
return data
|
41
|
+
end
|
32
42
|
locale = locale.to_sym
|
33
|
-
translations[locale] ||=
|
34
|
-
data = data.
|
35
|
-
translations[locale]
|
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)
|
36
46
|
end
|
37
47
|
|
38
48
|
# Get available locales from the translations hash
|
39
49
|
def available_locales
|
40
50
|
init_translations unless initialized?
|
41
51
|
translations.inject([]) do |locales, (locale, data)|
|
42
|
-
locales << locale unless (data.
|
52
|
+
locales << locale unless data.size <= 1 && (data.empty? || data.has_key?(:i18n))
|
43
53
|
locales
|
44
54
|
end
|
45
55
|
end
|
@@ -51,6 +61,23 @@ module I18n
|
|
51
61
|
super
|
52
62
|
end
|
53
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
|
+
|
54
81
|
protected
|
55
82
|
|
56
83
|
def init_translations
|
@@ -58,24 +85,23 @@ module I18n
|
|
58
85
|
@initialized = true
|
59
86
|
end
|
60
87
|
|
61
|
-
def translations
|
62
|
-
@translations ||= {}
|
63
|
-
end
|
64
|
-
|
65
88
|
# Looks up a translation from the translations hash. Returns nil if
|
66
|
-
#
|
89
|
+
# either key is nil, or locale, scope or key do not exist as a key in the
|
67
90
|
# nested translations hash. Splits keys or scopes containing dots
|
68
91
|
# into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
|
69
92
|
# <tt>%w(currency format)</tt>.
|
70
|
-
def lookup(locale, key, scope = [], options =
|
93
|
+
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
71
94
|
init_translations unless initialized?
|
72
95
|
keys = I18n.normalize_keys(locale, key, scope, options[:separator])
|
73
96
|
|
74
|
-
keys.inject(translations) do |result,
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
97
|
+
keys.inject(translations) do |result, _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
|
103
|
+
result = result[_key]
|
104
|
+
result = resolve_entry(locale, _key, result, Utils.except(options.merge(:scope => nil), :count)) if result.is_a?(Symbol)
|
79
105
|
result
|
80
106
|
end
|
81
107
|
end
|