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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cca63854d9dd69dfe1d36eb0388926fc5e7a7cd297ece21e2fdc431ef9c4c286
|
4
|
+
data.tar.gz: 1ab0f78752eabfd6a03b7c1f4a6f4e45c03689e9fbc0936cd65d6cd1f4103363
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8dd4e1cf010b24d748b4e2713794296964301234b9dbed36c96ae591677fb9ca3d5df7f5aedd86a48a1f470ad81de07997f2103107af32786486892204222d4
|
7
|
+
data.tar.gz: 9e2008459e303dc8e87742231d2468faa3f2edcb61a30c3c26f5240cc5606a8132bccc70ec3e45a9cfcc38eaf04d84c74c6d643ef54f1df2534c66706677180d
|
data/README.md
CHANGED
@@ -1,45 +1,87 @@
|
|
1
1
|
# Ruby I18n
|
2
2
|
|
3
|
-
[](https://badge.fury.io/rb/i18n)
|
4
|
+
[](https://github.com/ruby-i18n/i18n/actions?query=workflow%3ARuby)
|
4
5
|
|
5
|
-
Ruby
|
6
|
+
Ruby internationalization and localization (i18n) solution.
|
6
7
|
|
7
|
-
|
8
|
+
Currently maintained by @radar.
|
8
9
|
|
9
|
-
|
10
|
+
## Usage
|
10
11
|
|
11
|
-
|
12
|
-
* interpolation of values to translations (Ruby 1.9 compatible syntax)
|
13
|
-
* pluralization (CLDR compatible)
|
14
|
-
* customizable transliteration to ASCII
|
15
|
-
* flexible defaults
|
16
|
-
* bulk lookup
|
17
|
-
* lambdas as translation data
|
18
|
-
* custom key/scope separator
|
19
|
-
* custom exception handlers
|
20
|
-
* extensible architecture with a swappable backend
|
12
|
+
### Rails
|
21
13
|
|
22
|
-
|
14
|
+
You will most commonly use this library within a Rails app.
|
15
|
+
|
16
|
+
We support Rails versions from 6.0 and up.
|
17
|
+
|
18
|
+
[See the Rails Guide](https://guides.rubyonrails.org/i18n.html) for an example of its usage.
|
19
|
+
|
20
|
+
### Ruby (without Rails)
|
21
|
+
|
22
|
+
We support Ruby versions from 3.0 and up.
|
23
|
+
|
24
|
+
If you want to use this library without Rails, you can simply add `i18n` to your `Gemfile`:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem 'i18n'
|
28
|
+
```
|
29
|
+
|
30
|
+
Then configure I18n with some translations, and a default locale:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"]
|
34
|
+
I18n.default_locale = :en # (note that `en` is already the default!)
|
35
|
+
```
|
36
|
+
|
37
|
+
A simple translation file in your project might live at `config/locales/en.yml` and look like:
|
38
|
+
|
39
|
+
```yml
|
40
|
+
en:
|
41
|
+
test: "This is a test"
|
42
|
+
```
|
43
|
+
|
44
|
+
You can then access this translation by doing:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
I18n.t(:test)
|
48
|
+
```
|
49
|
+
|
50
|
+
You can switch locales in your project by setting `I18n.locale` to a different value:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
I18n.locale = :de
|
54
|
+
I18n.t(:test) # => "Dies ist ein Test"
|
55
|
+
```
|
56
|
+
|
57
|
+
## Features
|
58
|
+
|
59
|
+
* Translation and localization
|
60
|
+
* Interpolation of values to translations
|
61
|
+
* Pluralization (CLDR compatible)
|
62
|
+
* Customizable transliteration to ASCII
|
63
|
+
* Flexible defaults
|
64
|
+
* Bulk lookup
|
65
|
+
* Lambdas as translation data
|
66
|
+
* Custom key/scope separator
|
67
|
+
* Custom exception handlers
|
68
|
+
* Extensible architecture with a swappable backend
|
69
|
+
|
70
|
+
## Pluggable Features
|
23
71
|
|
24
72
|
* Cache
|
25
73
|
* Pluralization: lambda pluralizers stored as translation data
|
26
74
|
* Locale fallbacks, RFC4647 compliant (optionally: RFC4646 locale validation)
|
27
|
-
* [Gettext support](https://github.com/
|
75
|
+
* [Gettext support](https://github.com/ruby-i18n/i18n/wiki/Gettext)
|
28
76
|
* Translation metadata
|
29
77
|
|
30
|
-
Alternative
|
78
|
+
## Alternative Backend
|
31
79
|
|
32
80
|
* Chain
|
33
81
|
* ActiveRecord (optionally: ActiveRecord::Missing and ActiveRecord::StoreProcs)
|
34
82
|
* KeyValue (uses active_support/json and cannot store procs)
|
35
83
|
|
36
|
-
For more information and lots of resources see [the 'Resources' page on the wiki](https://github.com/
|
37
|
-
|
38
|
-
## Installation
|
39
|
-
|
40
|
-
```
|
41
|
-
gem install i18n
|
42
|
-
```
|
84
|
+
For more information and lots of resources see [the 'Resources' page on the wiki](https://github.com/ruby-i18n/i18n/wiki/Resources).
|
43
85
|
|
44
86
|
## Tests
|
45
87
|
|
@@ -58,7 +100,7 @@ particular tests in different test cases.
|
|
58
100
|
The reason for this is that we need to enforce the I18n API across various
|
59
101
|
combinations of extensions. E.g. the Simple backend alone needs to support
|
60
102
|
the same API as any combination of feature and/or optimization modules included
|
61
|
-
to the Simple backend. We test this by reusing the same API
|
103
|
+
to the Simple backend. We test this by reusing the same API definition (implemented
|
62
104
|
as test methods) in test cases with different setups.
|
63
105
|
|
64
106
|
You can find the test cases that enforce the API in test/api. And you can find
|
@@ -67,17 +109,18 @@ the API definition test methods in test/api/tests.
|
|
67
109
|
All other test cases (e.g. as defined in test/backend, test/core_ext) etc.
|
68
110
|
follow the usual test setup and should be easy to grok.
|
69
111
|
|
70
|
-
##
|
112
|
+
## More Documentation
|
71
113
|
|
72
|
-
|
73
|
-
* [Joshua Harvey](http://www.workingwithrails.com/person/759-joshua-harvey)
|
74
|
-
* [Stephan Soller](http://www.arkanis-development.de)
|
75
|
-
* [Saimon Moore](http://saimonmoore.net)
|
76
|
-
* [Matt Aimonetti](https://matt.aimonetti.net/)
|
114
|
+
Additional documentation can be found here: https://github.com/ruby-i18n/i18n/wiki
|
77
115
|
|
78
116
|
## Contributors
|
79
117
|
|
80
|
-
|
118
|
+
* @radar
|
119
|
+
* @carlosantoniodasilva
|
120
|
+
* @josevalim
|
121
|
+
* @knapo
|
122
|
+
* @tigrish
|
123
|
+
* [and many more](https://github.com/ruby-i18n/i18n/graphs/contributors)
|
81
124
|
|
82
125
|
## License
|
83
126
|
|
data/lib/i18n/backend/base.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'yaml'
|
2
|
-
require '
|
3
|
-
require 'i18n/core_ext/kernel/suppress_warnings'
|
4
|
+
require 'json'
|
4
5
|
|
5
6
|
module I18n
|
6
7
|
module Backend
|
@@ -8,20 +9,23 @@ module I18n
|
|
8
9
|
include I18n::Backend::Transliterator
|
9
10
|
|
10
11
|
# Accepts a list of paths to translation files. Loads translations from
|
11
|
-
# plain Ruby (*.rb)
|
12
|
+
# plain Ruby (*.rb), YAML files (*.yml), or JSON files (*.json). See #load_rb, #load_yml, and #load_json
|
12
13
|
# for details.
|
13
14
|
def load_translations(*filenames)
|
14
15
|
filenames = I18n.load_path if filenames.empty?
|
15
|
-
filenames.flatten.each
|
16
|
+
filenames.flatten.each do |filename|
|
17
|
+
loaded_translations = load_file(filename)
|
18
|
+
yield filename, loaded_translations if block_given?
|
19
|
+
end
|
16
20
|
end
|
17
21
|
|
18
22
|
# This method receives a locale, a data hash and options for storing translations.
|
19
23
|
# Should be implemented
|
20
|
-
def store_translations(locale, data, options =
|
24
|
+
def store_translations(locale, data, options = EMPTY_HASH)
|
21
25
|
raise NotImplementedError
|
22
26
|
end
|
23
27
|
|
24
|
-
def translate(locale, key, options =
|
28
|
+
def translate(locale, key, options = EMPTY_HASH)
|
25
29
|
raise I18n::ArgumentError if (key.is_a?(String) || key.is_a?(Symbol)) && key.empty?
|
26
30
|
raise InvalidLocale.new(locale) unless locale
|
27
31
|
return nil if key.nil? && !options.key?(:default)
|
@@ -31,7 +35,7 @@ module I18n
|
|
31
35
|
if entry.nil? && options.key?(:default)
|
32
36
|
entry = default(locale, key, options[:default], options)
|
33
37
|
else
|
34
|
-
entry =
|
38
|
+
entry = resolve_entry(locale, key, entry, options)
|
35
39
|
end
|
36
40
|
|
37
41
|
count = options[:count]
|
@@ -50,25 +54,28 @@ module I18n
|
|
50
54
|
end
|
51
55
|
|
52
56
|
deep_interpolation = options[:deep_interpolation]
|
53
|
-
|
54
|
-
|
57
|
+
skip_interpolation = options[:skip_interpolation]
|
58
|
+
values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
|
59
|
+
if !skip_interpolation && values && !values.empty?
|
55
60
|
entry = if deep_interpolation
|
56
61
|
deep_interpolate(locale, entry, values)
|
57
62
|
else
|
58
63
|
interpolate(locale, entry, values)
|
59
64
|
end
|
65
|
+
elsif entry.is_a?(String) && entry =~ I18n.reserved_keys_pattern
|
66
|
+
raise ReservedInterpolationKey.new($1.to_sym, entry)
|
60
67
|
end
|
61
68
|
entry
|
62
69
|
end
|
63
70
|
|
64
|
-
def exists?(locale, key)
|
65
|
-
lookup(locale, key) != nil
|
71
|
+
def exists?(locale, key, options = EMPTY_HASH)
|
72
|
+
lookup(locale, key, options[:scope]) != nil
|
66
73
|
end
|
67
74
|
|
68
75
|
# Acts the same as +strftime+, but uses a localized version of the
|
69
76
|
# format string. Takes a key from the date/time formats translations as
|
70
77
|
# a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
|
71
|
-
def localize(locale, object, format = :default, options =
|
78
|
+
def localize(locale, object, format = :default, options = EMPTY_HASH)
|
72
79
|
if object.nil? && options.include?(:default)
|
73
80
|
return options[:default]
|
74
81
|
end
|
@@ -78,7 +85,7 @@ module I18n
|
|
78
85
|
key = format
|
79
86
|
type = object.respond_to?(:sec) ? 'time' : 'date'
|
80
87
|
options = options.merge(:raise => true, :object => object, :locale => locale)
|
81
|
-
format = I18n.t(:"#{type}.formats.#{key}", options)
|
88
|
+
format = I18n.t(:"#{type}.formats.#{key}", **options)
|
82
89
|
end
|
83
90
|
|
84
91
|
format = translate_localization_format(locale, object, format, options)
|
@@ -92,12 +99,21 @@ module I18n
|
|
92
99
|
end
|
93
100
|
|
94
101
|
def reload!
|
102
|
+
eager_load! if eager_loaded?
|
103
|
+
end
|
104
|
+
|
105
|
+
def eager_load!
|
106
|
+
@eager_loaded = true
|
95
107
|
end
|
96
108
|
|
97
109
|
protected
|
98
110
|
|
111
|
+
def eager_loaded?
|
112
|
+
@eager_loaded ||= false
|
113
|
+
end
|
114
|
+
|
99
115
|
# The method which actually looks up for the translation in the store.
|
100
|
-
def lookup(locale, key, scope = [], options =
|
116
|
+
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
101
117
|
raise NotImplementedError
|
102
118
|
end
|
103
119
|
|
@@ -109,8 +125,13 @@ module I18n
|
|
109
125
|
# If given subject is an Array, it walks the array and returns the
|
110
126
|
# first translation that can be resolved. Otherwise it tries to resolve
|
111
127
|
# the translation directly.
|
112
|
-
def default(locale, object, subject, options =
|
113
|
-
|
128
|
+
def default(locale, object, subject, options = EMPTY_HASH)
|
129
|
+
if options.size == 1 && options.has_key?(:default)
|
130
|
+
options = {}
|
131
|
+
else
|
132
|
+
options = Utils.except(options, :default)
|
133
|
+
end
|
134
|
+
|
114
135
|
case subject
|
115
136
|
when Array
|
116
137
|
subject.each do |item|
|
@@ -126,21 +147,29 @@ module I18n
|
|
126
147
|
# If the given subject is a Symbol, it will be translated with the
|
127
148
|
# given options. If it is a Proc then it will be evaluated. All other
|
128
149
|
# subjects will be returned directly.
|
129
|
-
def resolve(locale, object, subject, options =
|
150
|
+
def resolve(locale, object, subject, options = EMPTY_HASH)
|
130
151
|
return subject if options[:resolve] == false
|
131
152
|
result = catch(:exception) do
|
132
153
|
case subject
|
133
154
|
when Symbol
|
134
|
-
I18n.translate(
|
155
|
+
I18n.translate(
|
156
|
+
subject,
|
157
|
+
**options.merge(
|
158
|
+
:locale => locale,
|
159
|
+
:throw => true,
|
160
|
+
:skip_interpolation => true
|
161
|
+
)
|
162
|
+
)
|
135
163
|
when Proc
|
136
164
|
date_or_time = options.delete(:object) || object
|
137
|
-
resolve(locale, object, subject.call(date_or_time, options))
|
165
|
+
resolve(locale, object, subject.call(date_or_time, **options))
|
138
166
|
else
|
139
167
|
subject
|
140
168
|
end
|
141
169
|
end
|
142
170
|
result unless result.is_a?(MissingTranslation)
|
143
171
|
end
|
172
|
+
alias_method :resolve_entry, :resolve
|
144
173
|
|
145
174
|
# Picks a translation from a pluralized mnemonic subkey according to English
|
146
175
|
# pluralization rules :
|
@@ -151,6 +180,7 @@ module I18n
|
|
151
180
|
# not standard with regards to the CLDR pluralization rules.
|
152
181
|
# Other backends can implement more flexible or complex pluralization rules.
|
153
182
|
def pluralize(locale, entry, count)
|
183
|
+
entry = entry.reject { |k, _v| k == :attributes } if entry.is_a?(Hash)
|
154
184
|
return entry unless entry.is_a?(Hash) && count
|
155
185
|
|
156
186
|
key = pluralization_key(entry, count)
|
@@ -166,9 +196,9 @@ module I18n
|
|
166
196
|
#
|
167
197
|
# if the given subject is an array then:
|
168
198
|
# each element of the array is recursively interpolated (until it finds a string)
|
169
|
-
# method interpolates ["yes, %{user}", ["maybe no, %{user}, "no, %{user}"]], :user => "bartuz"
|
170
|
-
# # =>
|
171
|
-
def interpolate(locale, subject, values =
|
199
|
+
# method interpolates ["yes, %{user}", ["maybe no, %{user}", "no, %{user}"]], :user => "bartuz"
|
200
|
+
# # => ["yes, bartuz", ["maybe no, bartuz", "no, bartuz"]]
|
201
|
+
def interpolate(locale, subject, values = EMPTY_HASH)
|
172
202
|
return subject if values.empty?
|
173
203
|
|
174
204
|
case subject
|
@@ -184,7 +214,7 @@ module I18n
|
|
184
214
|
# deep_interpolate { people: { ann: "Ann is %{ann}", john: "John is %{john}" } },
|
185
215
|
# ann: 'good', john: 'big'
|
186
216
|
# #=> { people: { ann: "Ann is good", john: "John is big" } }
|
187
|
-
def deep_interpolate(locale, data, values =
|
217
|
+
def deep_interpolate(locale, data, values = EMPTY_HASH)
|
188
218
|
return data if values.empty?
|
189
219
|
|
190
220
|
case data
|
@@ -210,40 +240,69 @@ module I18n
|
|
210
240
|
def load_file(filename)
|
211
241
|
type = File.extname(filename).tr('.', '').downcase
|
212
242
|
raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
|
213
|
-
data = send(:"load_#{type}", filename)
|
243
|
+
data, keys_symbolized = send(:"load_#{type}", filename)
|
214
244
|
unless data.is_a?(Hash)
|
215
245
|
raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
|
216
246
|
end
|
217
|
-
data.each { |locale, d| store_translations(locale, d || {}) }
|
247
|
+
data.each { |locale, d| store_translations(locale, d || {}, skip_symbolize_keys: keys_symbolized) }
|
248
|
+
|
249
|
+
data
|
218
250
|
end
|
219
251
|
|
220
252
|
# Loads a plain Ruby translations file. eval'ing the file must yield
|
221
253
|
# a Hash containing translation data with locales as toplevel keys.
|
222
254
|
def load_rb(filename)
|
223
|
-
eval(IO.read(filename), binding, filename)
|
255
|
+
translations = eval(IO.read(filename), binding, filename.to_s)
|
256
|
+
[translations, false]
|
224
257
|
end
|
225
258
|
|
226
259
|
# Loads a YAML translations file. The data must have locales as
|
227
260
|
# toplevel keys.
|
228
261
|
def load_yml(filename)
|
229
262
|
begin
|
230
|
-
YAML.
|
263
|
+
if YAML.respond_to?(:unsafe_load_file) # Psych 4.0 way
|
264
|
+
[YAML.unsafe_load_file(filename, symbolize_names: true, freeze: true), true]
|
265
|
+
else
|
266
|
+
[YAML.load_file(filename), false]
|
267
|
+
end
|
231
268
|
rescue TypeError, ScriptError, StandardError => e
|
232
269
|
raise InvalidLocaleData.new(filename, e.inspect)
|
233
270
|
end
|
234
271
|
end
|
272
|
+
alias_method :load_yaml, :load_yml
|
273
|
+
|
274
|
+
# Loads a JSON translations file. The data must have locales as
|
275
|
+
# toplevel keys.
|
276
|
+
def load_json(filename)
|
277
|
+
begin
|
278
|
+
# Use #load_file as a proxy for a version of JSON where symbolize_names and freeze are supported.
|
279
|
+
if ::JSON.respond_to?(:load_file)
|
280
|
+
[::JSON.load_file(filename, symbolize_names: true, freeze: true), true]
|
281
|
+
else
|
282
|
+
[::JSON.parse(File.read(filename)), false]
|
283
|
+
end
|
284
|
+
rescue TypeError, StandardError => e
|
285
|
+
raise InvalidLocaleData.new(filename, e.inspect)
|
286
|
+
end
|
287
|
+
end
|
235
288
|
|
236
289
|
def translate_localization_format(locale, object, format, options)
|
237
|
-
format.to_s.gsub(/%[aAbBpP]/) do |match|
|
290
|
+
format.to_s.gsub(/%(|\^)[aAbBpP]/) do |match|
|
238
291
|
case match
|
239
|
-
when '%a' then I18n.t(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
|
240
|
-
when '
|
241
|
-
when '%
|
242
|
-
when '
|
243
|
-
when '%
|
244
|
-
when '
|
292
|
+
when '%a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
|
293
|
+
when '%^a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday].upcase
|
294
|
+
when '%A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday]
|
295
|
+
when '%^A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday].upcase
|
296
|
+
when '%b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
|
297
|
+
when '%^b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon].upcase
|
298
|
+
when '%B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon]
|
299
|
+
when '%^B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon].upcase
|
300
|
+
when '%p' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).upcase
|
301
|
+
when '%P' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).downcase
|
245
302
|
end
|
246
303
|
end
|
304
|
+
rescue MissingTranslationData => e
|
305
|
+
e.message
|
247
306
|
end
|
248
307
|
|
249
308
|
def pluralization_key(entry, count)
|
data/lib/i18n/backend/cache.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This module allows you to easily cache all responses from the backend - thus
|
2
4
|
# speeding up the I18n aspects of your application quite a bit.
|
3
5
|
#
|
@@ -14,12 +16,12 @@
|
|
14
16
|
# ActiveSupport::Cache (only the methods #fetch and #write are being used).
|
15
17
|
#
|
16
18
|
# The cache_key implementation by default assumes you pass values that return
|
17
|
-
# a valid key from #hash (see
|
18
|
-
#
|
19
|
-
# configure your own digest method via which responds to #hexdigest (see
|
20
|
-
#
|
19
|
+
# a valid key from #hash (see
|
20
|
+
# https://www.ruby-doc.org/core/classes/Object.html#M000337). However, you can
|
21
|
+
# configure your own digest method via which responds to #hexdigest (see
|
22
|
+
# https://ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL/Digest.html):
|
21
23
|
#
|
22
|
-
# I18n.cache_key_digest = Digest::
|
24
|
+
# I18n.cache_key_digest = OpenSSL::Digest::SHA256.new
|
23
25
|
#
|
24
26
|
# If you use a lambda as a default value in your translation like this:
|
25
27
|
#
|
@@ -75,7 +77,7 @@ module I18n
|
|
75
77
|
module Backend
|
76
78
|
# TODO Should the cache be cleared if new translations are stored?
|
77
79
|
module Cache
|
78
|
-
def translate(locale, key, options =
|
80
|
+
def translate(locale, key, options = EMPTY_HASH)
|
79
81
|
I18n.perform_caching? ? fetch(cache_key(locale, key, options)) { super } : super
|
80
82
|
end
|
81
83
|
|
@@ -98,16 +100,13 @@ module I18n
|
|
98
100
|
|
99
101
|
def cache_key(locale, key, options)
|
100
102
|
# This assumes that only simple, native Ruby values are passed to I18n.translate.
|
101
|
-
"i18n/#{I18n.cache_namespace}/#{locale}/#{digest_item(key)}/#{
|
103
|
+
"i18n/#{I18n.cache_namespace}/#{locale}/#{digest_item(key)}/#{digest_item(options)}"
|
102
104
|
end
|
103
105
|
|
104
106
|
private
|
105
|
-
# In Ruby < 1.9 the following is true: { :foo => 1, :bar => 2 }.hash == { :foo => 2, :bar => 1 }.hash
|
106
|
-
# Therefore we must use the hash of the inspect string instead to avoid cache key colisions.
|
107
|
-
USE_INSPECT_HASH = RUBY_VERSION <= "1.9"
|
108
107
|
|
109
108
|
def digest_item(key)
|
110
|
-
I18n.cache_key_digest ? I18n.cache_key_digest.hexdigest(key.to_s) : key.hash
|
109
|
+
I18n.cache_key_digest ? I18n.cache_key_digest.hexdigest(key.to_s) : key.to_s.hash
|
111
110
|
end
|
112
111
|
end
|
113
112
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module I18n
|
6
|
+
module Backend
|
7
|
+
# Overwrites the Base load_file method to cache loaded file contents.
|
8
|
+
module CacheFile
|
9
|
+
# Optionally provide path_roots array to normalize filename paths,
|
10
|
+
# to make the cached i18n data portable across environments.
|
11
|
+
attr_accessor :path_roots
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
# Track loaded translation files in the `i18n.load_file` scope,
|
16
|
+
# and skip loading the file if its contents are still up-to-date.
|
17
|
+
def load_file(filename)
|
18
|
+
initialized = !respond_to?(:initialized?) || initialized?
|
19
|
+
key = I18n::Backend::Flatten.escape_default_separator(normalized_path(filename))
|
20
|
+
old_mtime, old_digest = initialized && lookup(:i18n, key, :load_file)
|
21
|
+
return if (mtime = File.mtime(filename).to_i) == old_mtime ||
|
22
|
+
(digest = OpenSSL::Digest::SHA256.file(filename).hexdigest) == old_digest
|
23
|
+
super
|
24
|
+
store_translations(:i18n, load_file: { key => [mtime, digest] })
|
25
|
+
end
|
26
|
+
|
27
|
+
# Translate absolute filename to relative path for i18n key.
|
28
|
+
def normalized_path(file)
|
29
|
+
return file unless path_roots
|
30
|
+
path = path_roots.find(&file.method(:start_with?)) ||
|
31
|
+
raise(InvalidLocaleData.new(file, 'outside expected path roots'))
|
32
|
+
file.sub(path, path_roots.index(path).to_s)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/i18n/backend/cascade.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# The Cascade module adds the ability to do cascading lookups to backends that
|
2
4
|
# are compatible to the Simple backend.
|
3
5
|
#
|
@@ -31,7 +33,7 @@
|
|
31
33
|
module I18n
|
32
34
|
module Backend
|
33
35
|
module Cascade
|
34
|
-
def lookup(locale, key, scope = [], options =
|
36
|
+
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
35
37
|
return super unless cascade = options[:cascade]
|
36
38
|
|
37
39
|
cascade = { :step => 1 } unless cascade.is_a?(Hash)
|
data/lib/i18n/backend/chain.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module I18n
|
2
4
|
module Backend
|
3
5
|
# Backend that chains multiple other backends and checks each of them when
|
@@ -14,6 +16,8 @@ module I18n
|
|
14
16
|
#
|
15
17
|
# The implementation assumes that all backends added to the Chain implement
|
16
18
|
# a lookup method with the same API as Simple backend does.
|
19
|
+
#
|
20
|
+
# Fallback translations using the :default option are only used by the last backend of a chain.
|
17
21
|
class Chain
|
18
22
|
module Implementation
|
19
23
|
include Base
|
@@ -24,11 +28,24 @@ module I18n
|
|
24
28
|
self.backends = backends
|
25
29
|
end
|
26
30
|
|
31
|
+
def initialized?
|
32
|
+
backends.all? do |backend|
|
33
|
+
backend.instance_eval do
|
34
|
+
return false unless initialized?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
27
40
|
def reload!
|
28
41
|
backends.each { |backend| backend.reload! }
|
29
42
|
end
|
30
43
|
|
31
|
-
def
|
44
|
+
def eager_load!
|
45
|
+
backends.each { |backend| backend.eager_load! }
|
46
|
+
end
|
47
|
+
|
48
|
+
def store_translations(locale, data, options = EMPTY_HASH)
|
32
49
|
backends.first.store_translations(locale, data, options)
|
33
50
|
end
|
34
51
|
|
@@ -36,9 +53,9 @@ module I18n
|
|
36
53
|
backends.map { |backend| backend.available_locales }.flatten.uniq
|
37
54
|
end
|
38
55
|
|
39
|
-
def translate(locale, key, default_options =
|
56
|
+
def translate(locale, key, default_options = EMPTY_HASH)
|
40
57
|
namespace = nil
|
41
|
-
options =
|
58
|
+
options = Utils.except(default_options, :default)
|
42
59
|
|
43
60
|
backends.each do |backend|
|
44
61
|
catch(:exception) do
|
@@ -56,13 +73,13 @@ module I18n
|
|
56
73
|
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
|
57
74
|
end
|
58
75
|
|
59
|
-
def exists?(locale, key)
|
76
|
+
def exists?(locale, key, options = EMPTY_HASH)
|
60
77
|
backends.any? do |backend|
|
61
|
-
backend.exists?(locale, key)
|
78
|
+
backend.exists?(locale, key, options)
|
62
79
|
end
|
63
80
|
end
|
64
81
|
|
65
|
-
def localize(locale, object, format = :default, options =
|
82
|
+
def localize(locale, object, format = :default, options = EMPTY_HASH)
|
66
83
|
backends.each do |backend|
|
67
84
|
catch(:exception) do
|
68
85
|
result = backend.localize(locale, object, format, options) and return result
|
@@ -72,6 +89,22 @@ module I18n
|
|
72
89
|
end
|
73
90
|
|
74
91
|
protected
|
92
|
+
def init_translations
|
93
|
+
backends.each do |backend|
|
94
|
+
backend.send(:init_translations)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def translations
|
99
|
+
backends.reverse.each_with_object({}) do |backend, memo|
|
100
|
+
partial_translations = backend.instance_eval do
|
101
|
+
init_translations unless initialized?
|
102
|
+
translations
|
103
|
+
end
|
104
|
+
Utils.deep_merge!(memo, partial_translations) { |_, a, b| b || a }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
75
108
|
def namespace_lookup?(result, options)
|
76
109
|
result.is_a?(Hash) && !options.has_key?(:count)
|
77
110
|
end
|