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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ef99abbb2ed698cd9dd77ab484720a5ecf68be96a7064f2f8b432a69758ada07
|
4
|
+
data.tar.gz: 0a4ade5577fe636c03d6ef9ca11f7a26b80a5c58078ee87c2f9dcf9fdd6882de
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 21b666c06e7c8ebdb78088ef2ecd8da866ebd414dff3f3a8535b3e950c12bf0f38695a22acac6da7775e8e245c7a99a0d1d9a6bc409e0ea3d552520a0be26f68
|
7
|
+
data.tar.gz: c5f81a0bafbf70daa4a2c4374f3906ef31cea1d8f94d5d5e9b980224d1b6bc75f95cdd3e08709f6cae410c34f4285deb375d7e5b06e19f283743221606e23434
|
data/MIT-LICENSE
CHANGED
File without changes
|
data/README.md
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# Ruby I18n
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/i18n)
|
4
|
+
[](https://github.com/ruby-i18n/i18n/actions?query=workflow%3ARuby)
|
5
|
+
|
6
|
+
Ruby internationalization and localization (i18n) solution.
|
7
|
+
|
8
|
+
Currently maintained by @radar.
|
9
|
+
|
10
|
+
## Usage
|
11
|
+
|
12
|
+
### Rails
|
13
|
+
|
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
|
71
|
+
|
72
|
+
* Cache
|
73
|
+
* Pluralization: lambda pluralizers stored as translation data
|
74
|
+
* Locale fallbacks, RFC4647 compliant (optionally: RFC4646 locale validation)
|
75
|
+
* [Gettext support](https://github.com/ruby-i18n/i18n/wiki/Gettext)
|
76
|
+
* Translation metadata
|
77
|
+
|
78
|
+
## Alternative Backend
|
79
|
+
|
80
|
+
* Chain
|
81
|
+
* ActiveRecord (optionally: ActiveRecord::Missing and ActiveRecord::StoreProcs)
|
82
|
+
* KeyValue (uses active_support/json and cannot store procs)
|
83
|
+
|
84
|
+
For more information and lots of resources see [the 'Resources' page on the wiki](https://github.com/ruby-i18n/i18n/wiki/Resources).
|
85
|
+
|
86
|
+
## Tests
|
87
|
+
|
88
|
+
You can run tests both with
|
89
|
+
|
90
|
+
* `rake test` or just `rake`
|
91
|
+
* run any test file directly, e.g. `ruby -Ilib:test test/api/simple_test.rb`
|
92
|
+
|
93
|
+
You can run all tests against all Gemfiles with
|
94
|
+
|
95
|
+
* `ruby test/run_all.rb`
|
96
|
+
|
97
|
+
The structure of the test suite is a bit unusual as it uses modules to reuse
|
98
|
+
particular tests in different test cases.
|
99
|
+
|
100
|
+
The reason for this is that we need to enforce the I18n API across various
|
101
|
+
combinations of extensions. E.g. the Simple backend alone needs to support
|
102
|
+
the same API as any combination of feature and/or optimization modules included
|
103
|
+
to the Simple backend. We test this by reusing the same API definition (implemented
|
104
|
+
as test methods) in test cases with different setups.
|
105
|
+
|
106
|
+
You can find the test cases that enforce the API in test/api. And you can find
|
107
|
+
the API definition test methods in test/api/tests.
|
108
|
+
|
109
|
+
All other test cases (e.g. as defined in test/backend, test/core_ext) etc.
|
110
|
+
follow the usual test setup and should be easy to grok.
|
111
|
+
|
112
|
+
## More Documentation
|
113
|
+
|
114
|
+
Additional documentation can be found here: https://github.com/ruby-i18n/i18n/wiki
|
115
|
+
|
116
|
+
## Contributors
|
117
|
+
|
118
|
+
* @radar
|
119
|
+
* @carlosantoniodasilva
|
120
|
+
* @josevalim
|
121
|
+
* @knapo
|
122
|
+
* @tigrish
|
123
|
+
* [and many more](https://github.com/ruby-i18n/i18n/graphs/contributors)
|
124
|
+
|
125
|
+
## License
|
126
|
+
|
127
|
+
MIT License. See the included MIT-LICENSE file.
|
data/lib/i18n/backend/base.rb
CHANGED
@@ -1,77 +1,93 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'yaml'
|
4
|
-
require '
|
4
|
+
require 'json'
|
5
5
|
|
6
6
|
module I18n
|
7
7
|
module Backend
|
8
8
|
module Base
|
9
9
|
include I18n::Backend::Transliterator
|
10
10
|
|
11
|
-
RESERVED_KEYS = [:scope, :default, :separator, :resolve]
|
12
|
-
RESERVED_KEYS_PATTERN = /%\{(#{RESERVED_KEYS.join("|")})\}/
|
13
|
-
DEPRECATED_INTERPOLATION_SYNTAX_PATTERN = /(\\)?\{\{([^\}]+)\}\}/
|
14
|
-
|
15
11
|
# Accepts a list of paths to translation files. Loads translations from
|
16
|
-
# plain Ruby (*.rb)
|
12
|
+
# plain Ruby (*.rb), YAML files (*.yml), or JSON files (*.json). See #load_rb, #load_yml, and #load_json
|
17
13
|
# for details.
|
18
14
|
def load_translations(*filenames)
|
19
|
-
filenames = I18n.load_path
|
20
|
-
filenames.each
|
15
|
+
filenames = I18n.load_path if filenames.empty?
|
16
|
+
filenames.flatten.each do |filename|
|
17
|
+
loaded_translations = load_file(filename)
|
18
|
+
yield filename, loaded_translations if block_given?
|
19
|
+
end
|
21
20
|
end
|
22
21
|
|
23
22
|
# This method receives a locale, a data hash and options for storing translations.
|
24
23
|
# Should be implemented
|
25
|
-
def store_translations(locale, data, options =
|
24
|
+
def store_translations(locale, data, options = EMPTY_HASH)
|
26
25
|
raise NotImplementedError
|
27
26
|
end
|
28
27
|
|
29
|
-
def translate(locale, key, options =
|
28
|
+
def translate(locale, key, options = EMPTY_HASH)
|
29
|
+
raise I18n::ArgumentError if (key.is_a?(String) || key.is_a?(Symbol)) && key.empty?
|
30
30
|
raise InvalidLocale.new(locale) unless locale
|
31
|
-
return key.
|
31
|
+
return nil if key.nil? && !options.key?(:default)
|
32
32
|
|
33
|
-
entry =
|
33
|
+
entry = lookup(locale, key, options[:scope], options) unless key.nil?
|
34
34
|
|
35
|
-
if options.
|
36
|
-
entry =
|
35
|
+
if entry.nil? && options.key?(:default)
|
36
|
+
entry = default(locale, key, options[:default], options)
|
37
37
|
else
|
38
|
-
|
39
|
-
values = options.except(*RESERVED_KEYS)
|
40
|
-
entry = entry.nil? && default ?
|
41
|
-
default(locale, key, default, options) : resolve(locale, key, entry, options)
|
38
|
+
entry = resolve_entry(locale, key, entry, options)
|
42
39
|
end
|
43
40
|
|
44
|
-
|
45
|
-
|
41
|
+
count = options[:count]
|
42
|
+
|
43
|
+
if entry.nil? && (subtrees? || !count)
|
44
|
+
if (options.key?(:default) && !options[:default].nil?) || !options.key?(:default)
|
45
|
+
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
|
46
|
+
end
|
47
|
+
end
|
46
48
|
|
49
|
+
entry = entry.dup if entry.is_a?(String)
|
47
50
|
entry = pluralize(locale, entry, count) if count
|
48
|
-
|
51
|
+
|
52
|
+
if entry.nil? && !subtrees?
|
53
|
+
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
|
54
|
+
end
|
55
|
+
|
56
|
+
deep_interpolation = options[:deep_interpolation]
|
57
|
+
values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
|
58
|
+
if values && !values.empty?
|
59
|
+
entry = if deep_interpolation
|
60
|
+
deep_interpolate(locale, entry, values)
|
61
|
+
else
|
62
|
+
interpolate(locale, entry, values)
|
63
|
+
end
|
64
|
+
elsif entry.is_a?(String) && entry =~ I18n.reserved_keys_pattern
|
65
|
+
raise ReservedInterpolationKey.new($1.to_sym, entry)
|
66
|
+
end
|
49
67
|
entry
|
50
68
|
end
|
51
69
|
|
70
|
+
def exists?(locale, key, options = EMPTY_HASH)
|
71
|
+
lookup(locale, key, options[:scope]) != nil
|
72
|
+
end
|
73
|
+
|
52
74
|
# Acts the same as +strftime+, but uses a localized version of the
|
53
75
|
# format string. Takes a key from the date/time formats translations as
|
54
76
|
# a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
|
55
|
-
def localize(locale, object, format = :default, options =
|
77
|
+
def localize(locale, object, format = :default, options = EMPTY_HASH)
|
78
|
+
if object.nil? && options.include?(:default)
|
79
|
+
return options[:default]
|
80
|
+
end
|
56
81
|
raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
|
57
82
|
|
58
83
|
if Symbol === format
|
59
|
-
key
|
84
|
+
key = format
|
60
85
|
type = object.respond_to?(:sec) ? 'time' : 'date'
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
# format = resolve(locale, object, format, options)
|
65
|
-
format = format.to_s.gsub(/%[aAbBp]/) do |match|
|
66
|
-
case match
|
67
|
-
when '%a' then I18n.t(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
|
68
|
-
when '%A' then I18n.t(:"date.day_names", :locale => locale, :format => format)[object.wday]
|
69
|
-
when '%b' then I18n.t(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
|
70
|
-
when '%B' then I18n.t(:"date.month_names", :locale => locale, :format => format)[object.mon]
|
71
|
-
when '%p' then I18n.t(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format) if object.respond_to? :hour
|
72
|
-
end
|
86
|
+
options = options.merge(:raise => true, :object => object, :locale => locale)
|
87
|
+
format = I18n.t(:"#{type}.formats.#{key}", **options)
|
73
88
|
end
|
74
89
|
|
90
|
+
format = translate_localization_format(locale, object, format, options)
|
75
91
|
object.strftime(format)
|
76
92
|
end
|
77
93
|
|
@@ -82,26 +98,44 @@ module I18n
|
|
82
98
|
end
|
83
99
|
|
84
100
|
def reload!
|
85
|
-
|
101
|
+
eager_load! if eager_loaded?
|
102
|
+
end
|
103
|
+
|
104
|
+
def eager_load!
|
105
|
+
@eager_loaded = true
|
86
106
|
end
|
87
107
|
|
88
108
|
protected
|
89
109
|
|
110
|
+
def eager_loaded?
|
111
|
+
@eager_loaded ||= false
|
112
|
+
end
|
113
|
+
|
90
114
|
# The method which actually looks up for the translation in the store.
|
91
|
-
def lookup(locale, key, scope = [], options =
|
115
|
+
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
92
116
|
raise NotImplementedError
|
93
117
|
end
|
94
118
|
|
119
|
+
def subtrees?
|
120
|
+
true
|
121
|
+
end
|
122
|
+
|
95
123
|
# Evaluates defaults.
|
96
124
|
# If given subject is an Array, it walks the array and returns the
|
97
125
|
# first translation that can be resolved. Otherwise it tries to resolve
|
98
126
|
# the translation directly.
|
99
|
-
def default(locale, object, subject, options =
|
100
|
-
|
127
|
+
def default(locale, object, subject, options = EMPTY_HASH)
|
128
|
+
if options.size == 1 && options.has_key?(:default)
|
129
|
+
options = {}
|
130
|
+
else
|
131
|
+
options = Utils.except(options, :default)
|
132
|
+
end
|
133
|
+
|
101
134
|
case subject
|
102
135
|
when Array
|
103
136
|
subject.each do |item|
|
104
|
-
result = resolve(locale, object, item, options)
|
137
|
+
result = resolve(locale, object, item, options)
|
138
|
+
return result unless result.nil?
|
105
139
|
end and nil
|
106
140
|
else
|
107
141
|
resolve(locale, object, subject, options)
|
@@ -112,116 +146,160 @@ module I18n
|
|
112
146
|
# If the given subject is a Symbol, it will be translated with the
|
113
147
|
# given options. If it is a Proc then it will be evaluated. All other
|
114
148
|
# subjects will be returned directly.
|
115
|
-
def resolve(locale, object, subject, options =
|
149
|
+
def resolve(locale, object, subject, options = EMPTY_HASH)
|
116
150
|
return subject if options[:resolve] == false
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
151
|
+
result = catch(:exception) do
|
152
|
+
case subject
|
153
|
+
when Symbol
|
154
|
+
I18n.translate(subject, **options.merge(:locale => locale, :throw => true))
|
155
|
+
when Proc
|
156
|
+
date_or_time = options.delete(:object) || object
|
157
|
+
resolve(locale, object, subject.call(date_or_time, **options))
|
158
|
+
else
|
159
|
+
subject
|
160
|
+
end
|
125
161
|
end
|
126
|
-
|
127
|
-
nil
|
162
|
+
result unless result.is_a?(MissingTranslation)
|
128
163
|
end
|
164
|
+
alias_method :resolve_entry, :resolve
|
129
165
|
|
130
|
-
# Picks a translation from
|
131
|
-
# rules
|
132
|
-
#
|
133
|
-
#
|
166
|
+
# Picks a translation from a pluralized mnemonic subkey according to English
|
167
|
+
# pluralization rules :
|
168
|
+
# - It will pick the :one subkey if count is equal to 1.
|
169
|
+
# - It will pick the :other subkey otherwise.
|
170
|
+
# - It will pick the :zero subkey in the special case where count is
|
171
|
+
# equal to 0 and there is a :zero subkey present. This behaviour is
|
172
|
+
# not standard with regards to the CLDR pluralization rules.
|
173
|
+
# Other backends can implement more flexible or complex pluralization rules.
|
134
174
|
def pluralize(locale, entry, count)
|
175
|
+
entry = entry.reject { |k, _v| k == :attributes } if entry.is_a?(Hash)
|
135
176
|
return entry unless entry.is_a?(Hash) && count
|
136
177
|
|
137
|
-
key =
|
138
|
-
|
139
|
-
raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
|
178
|
+
key = pluralization_key(entry, count)
|
179
|
+
raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
|
140
180
|
entry[key]
|
141
181
|
end
|
142
182
|
|
143
|
-
# Interpolates values into a given
|
183
|
+
# Interpolates values into a given subject.
|
144
184
|
#
|
145
|
-
#
|
185
|
+
# if the given subject is a string then:
|
186
|
+
# method interpolates "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X'
|
146
187
|
# # => "file test.txt opened by %{user}"
|
147
188
|
#
|
148
|
-
#
|
149
|
-
# the
|
150
|
-
#
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
preserve_encoding(string) do
|
155
|
-
string = string.gsub(DEPRECATED_INTERPOLATION_SYNTAX_PATTERN) do
|
156
|
-
escaped, key = $1, $2.to_sym
|
157
|
-
if escaped
|
158
|
-
"{{#{key}}}"
|
159
|
-
else
|
160
|
-
warn_syntax_deprecation!
|
161
|
-
"%{#{key}}"
|
162
|
-
end
|
163
|
-
end
|
189
|
+
# if the given subject is an array then:
|
190
|
+
# each element of the array is recursively interpolated (until it finds a string)
|
191
|
+
# method interpolates ["yes, %{user}", ["maybe no, %{user}, "no, %{user}"]], :user => "bartuz"
|
192
|
+
# # => "["yes, bartuz",["maybe no, bartuz", "no, bartuz"]]"
|
193
|
+
def interpolate(locale, subject, values = EMPTY_HASH)
|
194
|
+
return subject if values.empty?
|
164
195
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
values[key] = value
|
169
|
-
end
|
170
|
-
|
171
|
-
string % values
|
172
|
-
end
|
173
|
-
rescue KeyError => e
|
174
|
-
if string =~ RESERVED_KEYS_PATTERN
|
175
|
-
raise ReservedInterpolationKey.new($1.to_sym, string)
|
196
|
+
case subject
|
197
|
+
when ::String then I18n.interpolate(subject, values)
|
198
|
+
when ::Array then subject.map { |element| interpolate(locale, element, values) }
|
176
199
|
else
|
177
|
-
|
200
|
+
subject
|
178
201
|
end
|
179
202
|
end
|
180
203
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
204
|
+
# Deep interpolation
|
205
|
+
#
|
206
|
+
# deep_interpolate { people: { ann: "Ann is %{ann}", john: "John is %{john}" } },
|
207
|
+
# ann: 'good', john: 'big'
|
208
|
+
# #=> { people: { ann: "Ann is good", john: "John is big" } }
|
209
|
+
def deep_interpolate(locale, data, values = EMPTY_HASH)
|
210
|
+
return data if values.empty?
|
211
|
+
|
212
|
+
case data
|
213
|
+
when ::String
|
214
|
+
I18n.interpolate(data, values)
|
215
|
+
when ::Hash
|
216
|
+
data.each_with_object({}) do |(k, v), result|
|
217
|
+
result[k] = deep_interpolate(locale, v, values)
|
218
|
+
end
|
219
|
+
when ::Array
|
220
|
+
data.map do |v|
|
221
|
+
deep_interpolate(locale, v, values)
|
222
|
+
end
|
187
223
|
else
|
188
|
-
|
224
|
+
data
|
189
225
|
end
|
190
226
|
end
|
191
227
|
|
192
|
-
# returns true when the given value responds to :call and the key is
|
193
|
-
# an interpolation placeholder in the given string
|
194
|
-
def interpolate_lambda?(object, string, key)
|
195
|
-
object.respond_to?(:call) && string =~ /%\{#{key}\}|%\<#{key}>.*?\d*\.?\d*[bBdiouxXeEfgGcps]\}/
|
196
|
-
end
|
197
|
-
|
198
228
|
# Loads a single translations file by delegating to #load_rb or
|
199
229
|
# #load_yml depending on the file extension and directly merges the
|
200
230
|
# data to the existing translations. Raises I18n::UnknownFileType
|
201
231
|
# for all other file extensions.
|
202
232
|
def load_file(filename)
|
203
233
|
type = File.extname(filename).tr('.', '').downcase
|
204
|
-
raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}")
|
205
|
-
data = send(:"load_#{type}", filename)
|
206
|
-
data.
|
234
|
+
raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
|
235
|
+
data, keys_symbolized = send(:"load_#{type}", filename)
|
236
|
+
unless data.is_a?(Hash)
|
237
|
+
raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
|
238
|
+
end
|
239
|
+
data.each { |locale, d| store_translations(locale, d || {}, skip_symbolize_keys: keys_symbolized) }
|
240
|
+
|
241
|
+
data
|
207
242
|
end
|
208
243
|
|
209
244
|
# Loads a plain Ruby translations file. eval'ing the file must yield
|
210
245
|
# a Hash containing translation data with locales as toplevel keys.
|
211
246
|
def load_rb(filename)
|
212
|
-
eval(IO.read(filename), binding, filename)
|
247
|
+
translations = eval(IO.read(filename), binding, filename)
|
248
|
+
[translations, false]
|
213
249
|
end
|
214
250
|
|
215
251
|
# Loads a YAML translations file. The data must have locales as
|
216
252
|
# toplevel keys.
|
217
253
|
def load_yml(filename)
|
218
|
-
|
254
|
+
begin
|
255
|
+
if YAML.respond_to?(:unsafe_load_file) # Psych 4.0 way
|
256
|
+
[YAML.unsafe_load_file(filename, symbolize_names: true, freeze: true), true]
|
257
|
+
else
|
258
|
+
[YAML.load_file(filename), false]
|
259
|
+
end
|
260
|
+
rescue TypeError, ScriptError, StandardError => e
|
261
|
+
raise InvalidLocaleData.new(filename, e.inspect)
|
262
|
+
end
|
219
263
|
end
|
264
|
+
alias_method :load_yaml, :load_yml
|
220
265
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
266
|
+
# Loads a JSON translations file. The data must have locales as
|
267
|
+
# toplevel keys.
|
268
|
+
def load_json(filename)
|
269
|
+
begin
|
270
|
+
# Use #load_file as a proxy for a version of JSON where symbolize_names and freeze are supported.
|
271
|
+
if ::JSON.respond_to?(:load_file)
|
272
|
+
[::JSON.load_file(filename, symbolize_names: true, freeze: true), true]
|
273
|
+
else
|
274
|
+
[::JSON.parse(File.read(filename)), false]
|
275
|
+
end
|
276
|
+
rescue TypeError, StandardError => e
|
277
|
+
raise InvalidLocaleData.new(filename, e.inspect)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def translate_localization_format(locale, object, format, options)
|
282
|
+
format.to_s.gsub(/%(|\^)[aAbBpP]/) do |match|
|
283
|
+
case match
|
284
|
+
when '%a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
|
285
|
+
when '%^a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday].upcase
|
286
|
+
when '%A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday]
|
287
|
+
when '%^A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday].upcase
|
288
|
+
when '%b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
|
289
|
+
when '%^b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon].upcase
|
290
|
+
when '%B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon]
|
291
|
+
when '%^B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon].upcase
|
292
|
+
when '%p' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).upcase
|
293
|
+
when '%P' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).downcase
|
294
|
+
end
|
295
|
+
end
|
296
|
+
rescue MissingTranslationData => e
|
297
|
+
e.message
|
298
|
+
end
|
299
|
+
|
300
|
+
def pluralization_key(entry, count)
|
301
|
+
key = :zero if count == 0 && entry.has_key?(:zero)
|
302
|
+
key ||= count == 1 ? :one : :other
|
225
303
|
end
|
226
304
|
end
|
227
305
|
end
|
data/lib/i18n/backend/cache.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# This module allows you to easily cache all responses from the backend - thus
|
4
4
|
# speeding up the I18n aspects of your application quite a bit.
|
@@ -6,22 +6,44 @@
|
|
6
6
|
# To enable caching you can simply include the Cache module to the Simple
|
7
7
|
# backend - or whatever other backend you are using:
|
8
8
|
#
|
9
|
-
#
|
9
|
+
# I18n::Backend::Simple.send(:include, I18n::Backend::Cache)
|
10
10
|
#
|
11
11
|
# You will also need to set a cache store implementation that you want to use:
|
12
12
|
#
|
13
|
-
#
|
13
|
+
# I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
|
14
14
|
#
|
15
15
|
# You can use any cache implementation you want that provides the same API as
|
16
16
|
# ActiveSupport::Cache (only the methods #fetch and #write are being used).
|
17
17
|
#
|
18
|
-
# The cache_key implementation assumes
|
19
|
-
#
|
20
|
-
#
|
18
|
+
# The cache_key implementation by default assumes you pass values that return
|
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):
|
23
|
+
#
|
24
|
+
# I18n.cache_key_digest = OpenSSL::Digest::SHA256.new
|
25
|
+
#
|
26
|
+
# If you use a lambda as a default value in your translation like this:
|
27
|
+
#
|
28
|
+
# I18n.t(:"date.order", :default => lambda {[:month, :day, :year]})
|
29
|
+
#
|
30
|
+
# Then you will always have a cache miss, because each time this method
|
31
|
+
# is called the lambda will have a different hash value. If you know
|
32
|
+
# the result of the lambda is a constant as in the example above, then
|
33
|
+
# to cache this you can make the lambda a constant, like this:
|
34
|
+
#
|
35
|
+
# DEFAULT_DATE_ORDER = lambda {[:month, :day, :year]}
|
36
|
+
# ...
|
37
|
+
# I18n.t(:"date.order", :default => DEFAULT_DATE_ORDER)
|
38
|
+
#
|
39
|
+
# If the lambda may result in different values for each call then consider
|
40
|
+
# also using the Memoize backend.
|
41
|
+
#
|
21
42
|
module I18n
|
22
43
|
class << self
|
23
44
|
@@cache_store = nil
|
24
45
|
@@cache_namespace = nil
|
46
|
+
@@cache_key_digest = nil
|
25
47
|
|
26
48
|
def cache_store
|
27
49
|
@@cache_store
|
@@ -39,6 +61,14 @@ module I18n
|
|
39
61
|
@@cache_namespace = namespace
|
40
62
|
end
|
41
63
|
|
64
|
+
def cache_key_digest
|
65
|
+
@@cache_key_digest
|
66
|
+
end
|
67
|
+
|
68
|
+
def cache_key_digest=(key_digest)
|
69
|
+
@@cache_key_digest = key_digest
|
70
|
+
end
|
71
|
+
|
42
72
|
def perform_caching?
|
43
73
|
!cache_store.nil?
|
44
74
|
end
|
@@ -47,31 +77,37 @@ module I18n
|
|
47
77
|
module Backend
|
48
78
|
# TODO Should the cache be cleared if new translations are stored?
|
49
79
|
module Cache
|
50
|
-
def translate(
|
51
|
-
I18n.perform_caching? ? fetch(
|
80
|
+
def translate(locale, key, options = EMPTY_HASH)
|
81
|
+
I18n.perform_caching? ? fetch(cache_key(locale, key, options)) { super } : super
|
52
82
|
end
|
53
83
|
|
54
84
|
protected
|
55
85
|
|
56
|
-
def fetch(
|
57
|
-
result =
|
58
|
-
|
86
|
+
def fetch(cache_key, &block)
|
87
|
+
result = _fetch(cache_key, &block)
|
88
|
+
throw(:exception, result) if result.is_a?(MissingTranslation)
|
59
89
|
result = result.dup if result.frozen? rescue result
|
60
90
|
result
|
61
|
-
rescue MissingTranslationData => exception
|
62
|
-
I18n.cache_store.write(cache_key(*args), exception)
|
63
|
-
raise exception
|
64
91
|
end
|
65
92
|
|
66
|
-
def cache_key
|
93
|
+
def _fetch(cache_key, &block)
|
94
|
+
result = I18n.cache_store.read(cache_key)
|
95
|
+
return result unless result.nil?
|
96
|
+
result = catch(:exception, &block)
|
97
|
+
I18n.cache_store.write(cache_key, result) unless result.is_a?(Proc)
|
98
|
+
result
|
99
|
+
end
|
100
|
+
|
101
|
+
def cache_key(locale, key, options)
|
67
102
|
# This assumes that only simple, native Ruby values are passed to I18n.translate.
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
103
|
+
"i18n/#{I18n.cache_namespace}/#{locale}/#{digest_item(key)}/#{digest_item(options)}"
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def digest_item(key)
|
109
|
+
I18n.cache_key_digest ? I18n.cache_key_digest.hexdigest(key.to_s) : key.to_s.hash
|
74
110
|
end
|
75
111
|
end
|
76
112
|
end
|
77
|
-
end
|
113
|
+
end
|