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.
- checksums.yaml +4 -4
- data/README.md +75 -32
- data/lib/i18n/backend/base.rb +93 -34
- data/lib/i18n/backend/cache.rb +10 -11
- data/lib/i18n/backend/cache_file.rb +36 -0
- data/lib/i18n/backend/cascade.rb +3 -1
- data/lib/i18n/backend/chain.rb +39 -6
- data/lib/i18n/backend/fallbacks.rb +48 -15
- data/lib/i18n/backend/flatten.rb +10 -5
- data/lib/i18n/backend/gettext.rb +4 -2
- data/lib/i18n/backend/interpolation_compiler.rb +8 -8
- data/lib/i18n/backend/key_value.rb +31 -4
- data/lib/i18n/backend/lazy_loadable.rb +184 -0
- data/lib/i18n/backend/memoize.rb +10 -2
- data/lib/i18n/backend/metadata.rb +5 -3
- data/lib/i18n/backend/pluralization.rb +61 -18
- data/lib/i18n/backend/simple.rb +44 -24
- data/lib/i18n/backend/transliterator.rb +26 -24
- data/lib/i18n/backend.rb +5 -1
- data/lib/i18n/config.rb +22 -4
- data/lib/i18n/exceptions.rb +71 -18
- data/lib/i18n/gettext/helpers.rb +4 -2
- data/lib/i18n/gettext/po_parser.rb +7 -7
- data/lib/i18n/gettext.rb +2 -0
- data/lib/i18n/interpolate/ruby.rb +22 -6
- data/lib/i18n/locale/fallbacks.rb +33 -22
- data/lib/i18n/locale/tag/parents.rb +8 -6
- data/lib/i18n/locale/tag/simple.rb +2 -2
- data/lib/i18n/locale.rb +2 -0
- data/lib/i18n/middleware.rb +2 -0
- data/lib/i18n/tests/basics.rb +5 -7
- data/lib/i18n/tests/defaults.rb +8 -1
- data/lib/i18n/tests/interpolation.rb +34 -7
- data/lib/i18n/tests/link.rb +11 -1
- data/lib/i18n/tests/localization/date.rb +37 -10
- data/lib/i18n/tests/localization/date_time.rb +28 -7
- data/lib/i18n/tests/localization/procs.rb +9 -7
- data/lib/i18n/tests/localization/time.rb +27 -5
- data/lib/i18n/tests/lookup.rb +11 -5
- data/lib/i18n/tests/pluralization.rb +1 -1
- data/lib/i18n/tests/procs.rb +23 -7
- data/lib/i18n/tests.rb +2 -0
- data/lib/i18n/utils.rb +55 -0
- data/lib/i18n/version.rb +3 -1
- data/lib/i18n.rb +179 -58
- metadata +16 -61
- data/gemfiles/Gemfile.rails-3.2.x +0 -10
- data/gemfiles/Gemfile.rails-4.0.x +0 -10
- data/gemfiles/Gemfile.rails-4.1.x +0 -10
- data/gemfiles/Gemfile.rails-4.2.x +0 -10
- data/gemfiles/Gemfile.rails-5.0.x +0 -10
- data/gemfiles/Gemfile.rails-5.1.x +0 -10
- data/gemfiles/Gemfile.rails-master +0 -10
- data/lib/i18n/core_ext/hash.rb +0 -29
- data/lib/i18n/core_ext/kernel/suppress_warnings.rb +0 -8
- data/lib/i18n/core_ext/string/interpolate.rb +0 -9
- data/test/api/all_features_test.rb +0 -58
- data/test/api/cascade_test.rb +0 -28
- data/test/api/chain_test.rb +0 -24
- data/test/api/fallbacks_test.rb +0 -30
- data/test/api/key_value_test.rb +0 -24
- data/test/api/memoize_test.rb +0 -56
- data/test/api/override_test.rb +0 -42
- data/test/api/pluralization_test.rb +0 -30
- data/test/api/simple_test.rb +0 -28
- data/test/backend/cache_test.rb +0 -109
- data/test/backend/cascade_test.rb +0 -86
- data/test/backend/chain_test.rb +0 -122
- data/test/backend/exceptions_test.rb +0 -36
- data/test/backend/fallbacks_test.rb +0 -219
- data/test/backend/interpolation_compiler_test.rb +0 -118
- data/test/backend/key_value_test.rb +0 -61
- data/test/backend/memoize_test.rb +0 -79
- data/test/backend/metadata_test.rb +0 -48
- data/test/backend/pluralization_test.rb +0 -45
- data/test/backend/simple_test.rb +0 -103
- data/test/backend/transliterator_test.rb +0 -84
- data/test/core_ext/hash_test.rb +0 -36
- data/test/gettext/api_test.rb +0 -214
- data/test/gettext/backend_test.rb +0 -92
- data/test/i18n/exceptions_test.rb +0 -117
- data/test/i18n/gettext_plural_keys_test.rb +0 -20
- data/test/i18n/interpolate_test.rb +0 -91
- data/test/i18n/load_path_test.rb +0 -34
- data/test/i18n/middleware_test.rb +0 -24
- data/test/i18n_test.rb +0 -462
- data/test/locale/fallbacks_test.rb +0 -133
- data/test/locale/tag/rfc4646_test.rb +0 -143
- data/test/locale/tag/simple_test.rb +0 -32
- data/test/run_all.rb +0 -20
- data/test/test_data/locales/de.po +0 -82
- data/test/test_data/locales/en.rb +0 -3
- data/test/test_data/locales/en.yml +0 -3
- data/test/test_data/locales/invalid/empty.yml +0 -0
- data/test/test_data/locales/invalid/syntax.yml +0 -4
- data/test/test_data/locales/plurals.rb +0 -113
- data/test/test_helper.rb +0 -61
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# I18n locale fallbacks are useful when you want your application to use
|
2
4
|
# translations from other locales when translations for the current locale are
|
3
5
|
# missing. E.g. you might want to use :en translations when translations in
|
@@ -14,11 +16,13 @@ module I18n
|
|
14
16
|
# Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+.
|
15
17
|
def fallbacks
|
16
18
|
@@fallbacks ||= I18n::Locale::Fallbacks.new
|
19
|
+
Thread.current[:i18n_fallbacks] || @@fallbacks
|
17
20
|
end
|
18
21
|
|
19
22
|
# Sets the current fallbacks implementation. Use this to set a different fallbacks implementation.
|
20
23
|
def fallbacks=(fallbacks)
|
21
|
-
@@fallbacks = fallbacks
|
24
|
+
@@fallbacks = fallbacks.is_a?(Array) ? I18n::Locale::Fallbacks.new(fallbacks) : fallbacks
|
25
|
+
Thread.current[:i18n_fallbacks] = @@fallbacks
|
22
26
|
end
|
23
27
|
end
|
24
28
|
|
@@ -34,25 +38,24 @@ module I18n
|
|
34
38
|
# The default option takes precedence over fallback locales only when
|
35
39
|
# it's a Symbol. When the default contains a String, Proc or Hash
|
36
40
|
# it is evaluated last after all the fallback locales have been tried.
|
37
|
-
def translate(locale, key, options =
|
41
|
+
def translate(locale, key, options = EMPTY_HASH)
|
38
42
|
return super unless options.fetch(:fallback, true)
|
39
43
|
return super if options[:fallback_in_progress]
|
40
44
|
default = extract_non_symbol_default!(options) if options[:default]
|
41
45
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
46
|
+
fallback_options = options.merge(:fallback_in_progress => true, fallback_original_locale: locale)
|
47
|
+
I18n.fallbacks[locale].each do |fallback|
|
48
|
+
begin
|
49
|
+
catch(:exception) do
|
50
|
+
result = super(fallback, key, fallback_options)
|
51
|
+
unless result.nil?
|
52
|
+
on_fallback(locale, fallback, key, options) if locale.to_s != fallback.to_s
|
53
|
+
return result
|
49
54
|
end
|
50
|
-
rescue I18n::InvalidLocale
|
51
|
-
# we do nothing when the locale is invalid, as this is a fallback anyways.
|
52
55
|
end
|
56
|
+
rescue I18n::InvalidLocale
|
57
|
+
# we do nothing when the locale is invalid, as this is a fallback anyways.
|
53
58
|
end
|
54
|
-
ensure
|
55
|
-
options.delete(:fallback_in_progress)
|
56
59
|
end
|
57
60
|
|
58
61
|
return if options.key?(:default) && options[:default].nil?
|
@@ -61,6 +64,28 @@ module I18n
|
|
61
64
|
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
|
62
65
|
end
|
63
66
|
|
67
|
+
def resolve_entry(locale, object, subject, options = EMPTY_HASH)
|
68
|
+
return subject if options[:resolve] == false
|
69
|
+
result = catch(:exception) do
|
70
|
+
options.delete(:fallback_in_progress) if options.key?(:fallback_in_progress)
|
71
|
+
|
72
|
+
case subject
|
73
|
+
when Symbol
|
74
|
+
I18n.translate(subject, **options.merge(
|
75
|
+
:locale => options[:fallback_original_locale],
|
76
|
+
:throw => true,
|
77
|
+
:skip_interpolation => true
|
78
|
+
))
|
79
|
+
when Proc
|
80
|
+
date_or_time = options.delete(:object) || object
|
81
|
+
resolve_entry(options[:fallback_original_locale], object, subject.call(date_or_time, **options))
|
82
|
+
else
|
83
|
+
subject
|
84
|
+
end
|
85
|
+
end
|
86
|
+
result unless result.is_a?(MissingTranslation)
|
87
|
+
end
|
88
|
+
|
64
89
|
def extract_non_symbol_default!(options)
|
65
90
|
defaults = [options[:default]].flatten
|
66
91
|
first_non_symbol_default = defaults.detect{|default| !default.is_a?(Symbol)}
|
@@ -70,10 +95,11 @@ module I18n
|
|
70
95
|
return first_non_symbol_default
|
71
96
|
end
|
72
97
|
|
73
|
-
def exists?(locale, key)
|
98
|
+
def exists?(locale, key, options = EMPTY_HASH)
|
99
|
+
return super unless options.fetch(:fallback, true)
|
74
100
|
I18n.fallbacks[locale].each do |fallback|
|
75
101
|
begin
|
76
|
-
return true if super(fallback, key)
|
102
|
+
return true if super(fallback, key, options)
|
77
103
|
rescue I18n::InvalidLocale
|
78
104
|
# we do nothing when the locale is invalid, as this is a fallback anyways.
|
79
105
|
end
|
@@ -81,6 +107,13 @@ module I18n
|
|
81
107
|
|
82
108
|
false
|
83
109
|
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# Overwrite on_fallback to add specified logic when the fallback succeeds.
|
114
|
+
def on_fallback(_original_locale, _fallback_locale, _key, _options)
|
115
|
+
nil
|
116
|
+
end
|
84
117
|
end
|
85
118
|
end
|
86
119
|
end
|
data/lib/i18n/backend/flatten.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module I18n
|
2
4
|
module Backend
|
3
5
|
# This module contains several helpers to assist flattening translations.
|
@@ -16,14 +18,17 @@ module I18n
|
|
16
18
|
# and creates way less objects than the one at I18n.normalize_keys.
|
17
19
|
# It also handles escaping the translation keys.
|
18
20
|
def self.normalize_flat_keys(locale, key, scope, separator)
|
19
|
-
keys = [scope, key]
|
21
|
+
keys = [scope, key]
|
22
|
+
keys.flatten!
|
23
|
+
keys.compact!
|
24
|
+
|
20
25
|
separator ||= I18n.default_separator
|
21
26
|
|
22
27
|
if separator != FLATTEN_SEPARATOR
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
28
|
+
from_str = "#{FLATTEN_SEPARATOR}#{separator}"
|
29
|
+
to_str = "#{SEPARATOR_ESCAPE_CHAR}#{FLATTEN_SEPARATOR}"
|
30
|
+
|
31
|
+
keys.map! { |k| k.to_s.tr from_str, to_str }
|
27
32
|
end
|
28
33
|
|
29
34
|
keys.join(".")
|
data/lib/i18n/backend/gettext.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'i18n/gettext'
|
2
4
|
require 'i18n/gettext/po_parser'
|
3
5
|
|
@@ -39,7 +41,7 @@ module I18n
|
|
39
41
|
def load_po(filename)
|
40
42
|
locale = ::File.basename(filename, '.po').to_sym
|
41
43
|
data = normalize(locale, parse(filename))
|
42
|
-
{ locale => data }
|
44
|
+
[{ locale => data }, false]
|
43
45
|
end
|
44
46
|
|
45
47
|
def parse(filename)
|
@@ -57,7 +59,7 @@ module I18n
|
|
57
59
|
{ part => _normalized.empty? ? value : _normalized }
|
58
60
|
end
|
59
61
|
|
60
|
-
|
62
|
+
Utils.deep_merge!(result, normalized)
|
61
63
|
end
|
62
64
|
result
|
63
65
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# The InterpolationCompiler module contains optimizations that can tremendously
|
2
4
|
# speed up the interpolation process on the Simple backend.
|
3
5
|
#
|
@@ -19,8 +21,7 @@ module I18n
|
|
19
21
|
module Compiler
|
20
22
|
extend self
|
21
23
|
|
22
|
-
TOKENIZER
|
23
|
-
INTERPOLATION_SYNTAX_PATTERN = /(%)?(%\{([^\}]+)\})/
|
24
|
+
TOKENIZER = /(%%?\{[^}]+\})/
|
24
25
|
|
25
26
|
def compile_if_an_interpolation(string)
|
26
27
|
if interpolated_str?(string)
|
@@ -35,7 +36,7 @@ module I18n
|
|
35
36
|
end
|
36
37
|
|
37
38
|
def interpolated_str?(str)
|
38
|
-
str.kind_of?(::String) && str =~
|
39
|
+
str.kind_of?(::String) && str =~ TOKENIZER
|
39
40
|
end
|
40
41
|
|
41
42
|
protected
|
@@ -46,13 +47,12 @@ module I18n
|
|
46
47
|
|
47
48
|
def compiled_interpolation_body(str)
|
48
49
|
tokenize(str).map do |token|
|
49
|
-
|
50
|
+
token.match(TOKENIZER) ? handle_interpolation_token(token) : escape_plain_str(token)
|
50
51
|
end.join
|
51
52
|
end
|
52
53
|
|
53
|
-
def handle_interpolation_token(
|
54
|
-
|
55
|
-
escaped ? pattern : compile_interpolation_token(key.to_sym)
|
54
|
+
def handle_interpolation_token(token)
|
55
|
+
token.start_with?('%%') ? token[1..] : compile_interpolation_token(token[2..-2])
|
56
56
|
end
|
57
57
|
|
58
58
|
def compile_interpolation_token(key)
|
@@ -104,7 +104,7 @@ module I18n
|
|
104
104
|
end
|
105
105
|
end
|
106
106
|
|
107
|
-
def store_translations(locale, data, options =
|
107
|
+
def store_translations(locale, data, options = EMPTY_HASH)
|
108
108
|
compile_all_strings_in(data)
|
109
109
|
super
|
110
110
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'i18n/backend/base'
|
2
4
|
|
3
5
|
module I18n
|
@@ -74,7 +76,11 @@ module I18n
|
|
74
76
|
@store, @subtrees = store, subtrees
|
75
77
|
end
|
76
78
|
|
77
|
-
def
|
79
|
+
def initialized?
|
80
|
+
!@store.nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
def store_translations(locale, data, options = EMPTY_HASH)
|
78
84
|
escape = options.fetch(:escape, true)
|
79
85
|
flatten_translations(locale, data, escape, @subtrees).each do |key, value|
|
80
86
|
key = "#{locale}.#{key}"
|
@@ -83,7 +89,7 @@ module I18n
|
|
83
89
|
when Hash
|
84
90
|
if @subtrees && (old_value = @store[key])
|
85
91
|
old_value = JSON.decode(old_value)
|
86
|
-
value =
|
92
|
+
value = Utils.deep_merge!(Utils.deep_symbolize_keys(old_value), value) if old_value.is_a?(Hash)
|
87
93
|
end
|
88
94
|
when Proc
|
89
95
|
raise "Key-value stores cannot handle procs"
|
@@ -103,17 +109,37 @@ module I18n
|
|
103
109
|
|
104
110
|
protected
|
105
111
|
|
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
|
+
|
106
132
|
def subtrees?
|
107
133
|
@subtrees
|
108
134
|
end
|
109
135
|
|
110
|
-
def lookup(locale, key, scope = [], options =
|
136
|
+
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
111
137
|
key = normalize_flat_keys(locale, key, scope, options[:separator])
|
112
138
|
value = @store["#{locale}.#{key}"]
|
113
139
|
value = JSON.decode(value) if value
|
114
140
|
|
115
141
|
if value.is_a?(Hash)
|
116
|
-
|
142
|
+
Utils.deep_symbolize_keys(value)
|
117
143
|
elsif !value.nil?
|
118
144
|
value
|
119
145
|
elsif !@subtrees
|
@@ -125,6 +151,7 @@ module I18n
|
|
125
151
|
if subtrees?
|
126
152
|
super
|
127
153
|
else
|
154
|
+
return entry unless entry.is_a?(Hash)
|
128
155
|
key = pluralization_key(entry, count)
|
129
156
|
entry[key]
|
130
157
|
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,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Memoize module simply memoizes the values returned by lookup using
|
2
4
|
# a flat hash and can tremendously speed up the lookup process in a backend.
|
3
5
|
#
|
@@ -14,7 +16,7 @@ module I18n
|
|
14
16
|
@memoized_locales ||= super
|
15
17
|
end
|
16
18
|
|
17
|
-
def store_translations(locale, data, options =
|
19
|
+
def store_translations(locale, data, options = EMPTY_HASH)
|
18
20
|
reset_memoizations!(locale)
|
19
21
|
super
|
20
22
|
end
|
@@ -24,9 +26,15 @@ module I18n
|
|
24
26
|
super
|
25
27
|
end
|
26
28
|
|
29
|
+
def eager_load!
|
30
|
+
memoized_lookup
|
31
|
+
available_locales
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
27
35
|
protected
|
28
36
|
|
29
|
-
def lookup(locale, key, scope = nil, options =
|
37
|
+
def lookup(locale, key, scope = nil, options = EMPTY_HASH)
|
30
38
|
flat_key = I18n::Backend::Flatten.normalize_flat_keys(locale,
|
31
39
|
key, scope, options[:separator]).to_sym
|
32
40
|
flat_hash = memoized_lookup[locale.to_sym]
|
@@ -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.
|
@@ -35,19 +37,19 @@ module I18n
|
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
38
|
-
def translate(locale, key, options =
|
40
|
+
def translate(locale, key, options = EMPTY_HASH)
|
39
41
|
metadata = {
|
40
42
|
:locale => locale,
|
41
43
|
:key => key,
|
42
44
|
:scope => options[:scope],
|
43
45
|
:default => options[:default],
|
44
46
|
:separator => options[:separator],
|
45
|
-
:values => options.reject { |name,
|
47
|
+
:values => options.reject { |name, _value| RESERVED_KEYS.include?(name) }
|
46
48
|
}
|
47
49
|
with_metadata(metadata) { super }
|
48
50
|
end
|
49
51
|
|
50
|
-
def interpolate(locale, entry, values =
|
52
|
+
def interpolate(locale, entry, values = EMPTY_HASH)
|
51
53
|
metadata = entry.translation_metadata.merge(:original => entry)
|
52
54
|
with_metadata(metadata) { super }
|
53
55
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# I18n Pluralization are useful when you want your application to
|
2
4
|
# customize pluralization rules.
|
3
5
|
#
|
@@ -14,26 +16,57 @@ module I18n
|
|
14
16
|
module Pluralization
|
15
17
|
# Overwrites the Base backend translate method so that it will check the
|
16
18
|
# translation meta data space (:i18n) for a locale specific pluralization
|
17
|
-
# 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
|
18
20
|
# pluralization rules to be stored at I18n.t(:'i18n.plural.rule')
|
19
21
|
#
|
20
22
|
# Pluralization rules are expected to respond to #call(count) and
|
21
|
-
# return a pluralization key. Valid keys depend on the
|
22
|
-
#
|
23
|
-
#
|
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
|
27
|
+
#
|
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.
|
24
34
|
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
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
|
29
39
|
def pluralize(locale, entry, count)
|
30
|
-
return entry unless entry.is_a?(Hash)
|
40
|
+
return entry unless entry.is_a?(Hash) && count
|
31
41
|
|
32
42
|
pluralizer = pluralizer(locale)
|
33
43
|
if pluralizer.respond_to?(:call)
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
37
70
|
else
|
38
71
|
super
|
39
72
|
end
|
@@ -41,13 +74,23 @@ module I18n
|
|
41
74
|
|
42
75
|
protected
|
43
76
|
|
44
|
-
|
45
|
-
|
46
|
-
|
77
|
+
def pluralizers
|
78
|
+
@pluralizers ||= {}
|
79
|
+
end
|
47
80
|
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
51
94
|
end
|
52
95
|
end
|
53
96
|
end
|