i18n 0.4.0 → 1.14.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/i18n.svg)](https://badge.fury.io/rb/i18n)
|
4
|
+
[![Build Status](https://github.com/ruby-i18n/i18n/workflows/Ruby/badge.svg)](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
|