mova-i18n 0.1.0
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/.gitignore +14 -0
- data/.yardopts +1 -0
- data/CONTRIBUTING.md +47 -0
- data/Gemfile +26 -0
- data/LICENSE.txt +22 -0
- data/README.md +93 -0
- data/Rakefile +24 -0
- data/lib/mova-i18n.rb +77 -0
- data/lib/mova-i18n/bridge.rb +69 -0
- data/lib/mova-i18n/config.rb +36 -0
- data/mova-i18n.gemspec +21 -0
- data/test/exists_test.rb +29 -0
- data/test/locale/en.yml +2 -0
- data/test/localize_test.rb +58 -0
- data/test/mova_config_test.rb +17 -0
- data/test/test_helper.rb +61 -0
- data/test/transfer_translations_test.rb +27 -0
- data/test/translate/default_test.rb +45 -0
- data/test/translate/pluralization_test.rb +62 -0
- data/test/translate/scope_test.rb +26 -0
- data/test/translate_test.rb +61 -0
- data/test/transliterate_test.rb +28 -0
- data/test_rails/dummy/.gitignore +15 -0
- data/test_rails/dummy/app/controllers/hello_controller.rb +2 -0
- data/test_rails/dummy/app/views/hello/html_safe_key.html.erb +1 -0
- data/test_rails/dummy/app/views/hello/html_safe_underscored.html.erb +1 -0
- data/test_rails/dummy/app/views/hello/html_unsafe.html.erb +1 -0
- data/test_rails/dummy/app/views/hello/locale_option.html.erb +1 -0
- data/test_rails/dummy/app/views/hello/missing_translation.html.erb +1 -0
- data/test_rails/dummy/app/views/hello/partial_scope.html.erb +1 -0
- data/test_rails/dummy/app/views/hello/raise_error.html.erb +1 -0
- data/test_rails/dummy/app/views/hello/translate.html.erb +1 -0
- data/test_rails/dummy/config.ru +4 -0
- data/test_rails/dummy/config/application.rb +13 -0
- data/test_rails/dummy/config/environment.rb +5 -0
- data/test_rails/dummy/config/environments/test.rb +10 -0
- data/test_rails/dummy/config/locales/uk.rb +20 -0
- data/test_rails/dummy/config/locales/uk.yml +252 -0
- data/test_rails/dummy/config/locales/views.yml +11 -0
- data/test_rails/helpers_test.rb +37 -0
- data/test_rails/test_helper.rb +11 -0
- data/test_rails/view_test.rb +55 -0
- metadata +154 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7f5ffd7156c6938db949bd1356541156f14cd25f
|
4
|
+
data.tar.gz: c9b80e1787bfa36cad7e9184b8ead9f4a0bd498b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 25d4e0d8c815e7dd6580a72b82d5f1887d466f06e569dd60820cd03e4a9d3dd4128f808c0f9c20ea456ec135d913033d14f73cc930a354719f13160e6fe67de4
|
7
|
+
data.tar.gz: efb39f365dd204519860140b414d38d4cff408d6611c41da1057b065a948b3be382aaec838ba039a1d7c656e64e685162739d2df443b35493108066448de3492
|
data/.gitignore
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
## Adding a feature
|
4
|
+
|
5
|
+
1. Open an issue and explain what you're planning to do. It is better to discuss new idea first,
|
6
|
+
rather when diving into code.
|
7
|
+
2. Add some tests.
|
8
|
+
3. Write the code.
|
9
|
+
4. Make sure all tests pass.
|
10
|
+
5. Commit with detailed explanation what you've done in a message.
|
11
|
+
6. Open pull request.
|
12
|
+
|
13
|
+
## Breaking/removing a feature
|
14
|
+
|
15
|
+
1. Add deprecation warning and fallback to old behaivour if possible.
|
16
|
+
2. Explain how to migrate to the new code in CHANGELOG.
|
17
|
+
3. Update/remove tests.
|
18
|
+
4. Update the code.
|
19
|
+
5. Make sure all tests pass.
|
20
|
+
6. Commit with detailed explanation what you've done in a message.
|
21
|
+
7. Open pull request.
|
22
|
+
|
23
|
+
## Fixing a bug
|
24
|
+
|
25
|
+
1. Add failing test.
|
26
|
+
2. Fix the bug.
|
27
|
+
3. Make sure all tests pass.
|
28
|
+
4. Commit with detailed explanation what you've done in a message.
|
29
|
+
5. Open pull request.
|
30
|
+
|
31
|
+
## Fixing a typo
|
32
|
+
|
33
|
+
1. Commit with a message that include "[ci skip]" remark.
|
34
|
+
2. Open pull request.
|
35
|
+
|
36
|
+
## Running the tests
|
37
|
+
|
38
|
+
```
|
39
|
+
rake bundle
|
40
|
+
rake test
|
41
|
+
```
|
42
|
+
|
43
|
+
## Working with documentation
|
44
|
+
|
45
|
+
```
|
46
|
+
yard server -dr
|
47
|
+
open http://localhost:8808
|
data/Gemfile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
gem "rspec-mocks", "~> 3.0"
|
6
|
+
gem "yard", "~> 0.8"
|
7
|
+
gem "pry"
|
8
|
+
|
9
|
+
case ENV["RAILS"]
|
10
|
+
when "3.2"
|
11
|
+
version = "~> 3.2.19"
|
12
|
+
gem "actionpack", version
|
13
|
+
gem "railties", version
|
14
|
+
gem "tzinfo", "~> 0.3.29"
|
15
|
+
gem "minitest", "~> 4.2"
|
16
|
+
when "4.0"
|
17
|
+
version = '~> 4.0.0'
|
18
|
+
gem 'actionpack', version
|
19
|
+
gem 'railties', version
|
20
|
+
gem "minitest", "~> 4.2"
|
21
|
+
when nil, "4.1"
|
22
|
+
version = '~> 4.1.0'
|
23
|
+
gem 'actionpack', version
|
24
|
+
gem 'railties', version
|
25
|
+
gem "minitest", "~> 5.4"
|
26
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Andrii Malyshko
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# Mova-I18n
|
2
|
+
|
3
|
+
**Mova-I18n** overwrites `translate/t` method of [I18n][i18n] in a way that delegates it to
|
4
|
+
[Mova][mova] internals without major breaking of I18n API. This speeds up translation
|
5
|
+
lookups while staying compatible with libraries that rely on I18n.
|
6
|
+
|
7
|
+
## Status
|
8
|
+
|
9
|
+
Not tested in production.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile and run `bundle`:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'mova-i18n'
|
17
|
+
```
|
18
|
+
|
19
|
+
## Configuration
|
20
|
+
|
21
|
+
No configuration is needed if you have in-memory `I18n::Backend::Simple` backend (used in Rails
|
22
|
+
apps by default). Mova-I18n will use I18n setup (including locale fallbacks) and automatically
|
23
|
+
load translations stored in local files.
|
24
|
+
|
25
|
+
When using other persistent key-value storage, you probably need chain configuration like this:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require "mova/storage/chain"
|
29
|
+
require "mova/storage/readonly"
|
30
|
+
require "mova/storage/memory"
|
31
|
+
redis = ActiveSupport::Cache::RedisStore.new("localhost:6379/0")
|
32
|
+
chain = Mova::Storage::Chain.new(Mova::Storage::Readonly.new(redis), Mova::Storage::Memory.new)
|
33
|
+
I18n.mova.translator = Mova::I18nTranslator.new(storage: chain)
|
34
|
+
```
|
35
|
+
|
36
|
+
Note, that you must pass your storage wrapped in `Readonly`, otherwise `I18n.reload!` will clear
|
37
|
+
all translations stored there when Rails boots up. It will also protect your storage from having
|
38
|
+
incompatible data written to it, such as hashes or procs (those are partially supported and can be
|
39
|
+
handled normally only with in-memory storage). Such configuration allows to override Rails default
|
40
|
+
translations in your backend, since first storage takes precedence over next one.
|
41
|
+
|
42
|
+
## Compatibility
|
43
|
+
|
44
|
+
### Supported
|
45
|
+
|
46
|
+
* the following `I18n.translate` calls:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
I18n.t(:key)
|
50
|
+
I18n.t("key")
|
51
|
+
I18n.t(:key, locale: :fr)
|
52
|
+
I18n.t(:key, raise: true)
|
53
|
+
I18n.t(:key, throw: true)
|
54
|
+
I18n.t(:key, fallback: true) # i.e. disabled locale fallback
|
55
|
+
I18n.t(:key, default: :default_key_lookup)
|
56
|
+
I18n.t(:key, default: "default translation")
|
57
|
+
I18n.t(:key, default: [:default_key_lookup1, :default_key_lookup2])
|
58
|
+
I18n.t(:key, default: [:default_key_lookup1, "default translation"])
|
59
|
+
I18n.t(:key, scope: :key_scope)
|
60
|
+
I18n.t(:key, scope: [:key_scope_part1, :key_scope_part2])
|
61
|
+
I18n.t(:key, interpolation1: "value1", interpolation2: "value2")
|
62
|
+
I18n.t(:key, count: 3)
|
63
|
+
```
|
64
|
+
|
65
|
+
Any combination of `locale`, `raise/throw`, `default`, `scope`, `count`, `fallback` options
|
66
|
+
is also supported.
|
67
|
+
|
68
|
+
* `I18n.localize` and `I18n.transliterate`
|
69
|
+
|
70
|
+
* locale fallbacks
|
71
|
+
|
72
|
+
### Partially supported
|
73
|
+
|
74
|
+
* storing values other than String or nil is possible only with in-memory storage or with chain
|
75
|
+
including in-memory storage (see "Configuration"). Although it violates one of the Mova design principles,
|
76
|
+
Mova-I18n tries to be compatible with Rails localization helpers that require hashes to be
|
77
|
+
returned from `I18n.t`, so it writes to and retrieves a couple of hashes from a storage.
|
78
|
+
|
79
|
+
### Not supported
|
80
|
+
|
81
|
+
* translations as procs, using procs in defaults. Actually, you can store them (see "Partially
|
82
|
+
supported"), but they'll never be called. The only exception are the pluralization and transliteration
|
83
|
+
rules (for compatibility with [Rails-I18n][rails-i18n] project).
|
84
|
+
|
85
|
+
This means that **`default` option in Rails 4.x views won't work**, because Rails wraps it in a proc
|
86
|
+
call. Although using `default` in a template is a bad practice anyway.
|
87
|
+
|
88
|
+
* bulk translation via `I18n.t([:first_key, :second_key])`. Use map instead.
|
89
|
+
* cascade lookup
|
90
|
+
|
91
|
+
[mova]: https://github.com/mova-rb/mova
|
92
|
+
[i18n]: https://github.com/svenfuchs/i18n
|
93
|
+
[rails-i18n]: https://github.com/svenfuchs/rails-i18n
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
rails_versions = %w(3.2 4.0 4.1)
|
4
|
+
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
task :bundle do
|
8
|
+
rails_versions.each do |version|
|
9
|
+
sh "RAILS=#{version} bundle"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
task :test do
|
14
|
+
$LOAD_PATH.unshift("test")
|
15
|
+
Dir.glob("./test/**/*_test.rb").each { |file| require file}
|
16
|
+
|
17
|
+
rails_versions.each do |version|
|
18
|
+
sh "RAILS=#{version} rake test:rails"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
task "test:rails" do
|
23
|
+
Dir.glob("./test_rails/**/*_test.rb").each { |file| require file}
|
24
|
+
end
|
data/lib/mova-i18n.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require "i18n"
|
2
|
+
require "mova"
|
3
|
+
require "mova/interpolation/sprintf"
|
4
|
+
require "mova-i18n/config"
|
5
|
+
require "mova-i18n/bridge"
|
6
|
+
|
7
|
+
module I18n
|
8
|
+
def self.mova
|
9
|
+
Mova::I18nConfig
|
10
|
+
end
|
11
|
+
|
12
|
+
# make sure we can safely call `I18n.fallbacks` even
|
13
|
+
# if `I18n::Backend::Fallbacks` was not required
|
14
|
+
unless respond_to?(:fallbacks)
|
15
|
+
def self.fallbacks
|
16
|
+
@fallbacks ||= Hash.new { |h,k| h[k] = [k] }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def translate(key, options = nil)
|
22
|
+
options = options && options.dup || {}
|
23
|
+
|
24
|
+
locale = options[:locale] || config.locale
|
25
|
+
locale_with_fallbacks =
|
26
|
+
if options[:fallback]
|
27
|
+
# suppress locale fallbacks (inverted due to I18n fallbacks implementation)
|
28
|
+
[locale]
|
29
|
+
else
|
30
|
+
fallbacks[locale]
|
31
|
+
end
|
32
|
+
|
33
|
+
if (default = options[:default]) && !default.is_a?(Hash)
|
34
|
+
defaults = Array(default)
|
35
|
+
options[:default] = defaults.last.is_a?(String) ? defaults.pop : nil
|
36
|
+
key = Array(key).concat(defaults)
|
37
|
+
end
|
38
|
+
|
39
|
+
if (count = options[:count])
|
40
|
+
zero_plural_key = :zero if count == 0
|
41
|
+
plural_key = mova.pluralizer(locale).call(count)
|
42
|
+
key = Array(key).each_with_object([]) do |key, memo|
|
43
|
+
memo << Mova::Scope.join(key, zero_plural_key) if zero_plural_key
|
44
|
+
memo << Mova::Scope.join(key, plural_key)
|
45
|
+
memo << key
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
if (scope = options[:scope])
|
50
|
+
scope = Array(scope)
|
51
|
+
key = Array(key).map do |key|
|
52
|
+
Mova::Scope.join(scope + [key])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
result = mova.translator.get(key, locale_with_fallbacks, options)
|
57
|
+
|
58
|
+
if result.is_a?(String) && !(interpolation_keys = options.keys - RESERVED_KEYS).empty?
|
59
|
+
mova.interpolator.call(result, options)
|
60
|
+
else
|
61
|
+
result
|
62
|
+
end
|
63
|
+
end
|
64
|
+
alias_method :t, :translate
|
65
|
+
|
66
|
+
def exists?(key, locale = config.locale)
|
67
|
+
locale_with_fallbacks = fallbacks[locale]
|
68
|
+
result = mova.translator.get([key], locale_with_fallbacks, default: "")
|
69
|
+
Mova.presence(result)
|
70
|
+
end
|
71
|
+
|
72
|
+
def reload!
|
73
|
+
super
|
74
|
+
mova.transfer_translations!
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Mova
|
2
|
+
# `I18nBridge` name to explicitly avoid collision with root namespace, so we
|
3
|
+
# don't need to type `::I18n`.
|
4
|
+
module I18nBridge
|
5
|
+
module Translator
|
6
|
+
def default(locales, keys, options)
|
7
|
+
origin_locale = locales.first
|
8
|
+
origin_key = keys.first
|
9
|
+
|
10
|
+
if options[:raise]
|
11
|
+
raise I18n::MissingTranslationData.new(origin_locale, origin_key, options)
|
12
|
+
end
|
13
|
+
|
14
|
+
if options[:throw]
|
15
|
+
throw :exception, I18n::MissingTranslation.new(origin_locale, origin_key, options)
|
16
|
+
end
|
17
|
+
|
18
|
+
key_with_locale = Mova::Scope.join(origin_locale, origin_key)
|
19
|
+
"missing translation: #{key_with_locale}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def put(translations)
|
23
|
+
super
|
24
|
+
put_exact_translation(translations, "i18n.transliterate.rule")
|
25
|
+
put_exact_translation(translations, "number.format")
|
26
|
+
put_exact_translation(translations, "number.currency.format")
|
27
|
+
put_exact_translation(translations, "number.human.format")
|
28
|
+
put_exact_translation(translations, "number.human.decimal_units.units")
|
29
|
+
put_exact_translation(translations, "number.percentage.format")
|
30
|
+
put_exact_translation(translations, "number.precision.format")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Stores exact value because {Mova::Translator#put} flattens hashes before writing
|
34
|
+
# to the storage, while `I18n.transliterate` and Rails localization helpers rely on
|
35
|
+
# ability of storing hashes as is.
|
36
|
+
def put_exact_translation(translations, key)
|
37
|
+
scope_path = Mova::Scope.split(key).map &:to_sym
|
38
|
+
locales = translations.keys
|
39
|
+
|
40
|
+
locales.each do |locale|
|
41
|
+
full_path = [locale] + scope_path
|
42
|
+
|
43
|
+
translation = full_path.inject(translations) do |memo, scope|
|
44
|
+
memo[scope] || {}
|
45
|
+
end
|
46
|
+
|
47
|
+
unless translation == {}
|
48
|
+
key_with_locale = Mova::Scope.join(full_path)
|
49
|
+
storage.write(key_with_locale, translation)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module Sprintf
|
56
|
+
def missing_placeholder(placeholder, values, string)
|
57
|
+
raise I18n::MissingInterpolationArgument.new(placeholder, values, string)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class I18nTranslator < Translator
|
63
|
+
include I18nBridge::Translator
|
64
|
+
end
|
65
|
+
|
66
|
+
class I18nInterpolator < Interpolation::Sprintf
|
67
|
+
include I18nBridge::Sprintf
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Mova
|
2
|
+
module I18nConfig
|
3
|
+
class << self
|
4
|
+
attr_writer :translator, :interpolator
|
5
|
+
|
6
|
+
# Transfer translations from `I18n::Backend::Simple`, since we can have enumerate all keys
|
7
|
+
# here. Other key-value storages should be passed to `I18n.mova.translator` directly.
|
8
|
+
#
|
9
|
+
# @note Clears all current translations in `I18n.mova.translator.storage`. Use
|
10
|
+
# {Mova::Storage::Readonly} to protect certain storages if you have a chain of them.
|
11
|
+
def transfer_translations!
|
12
|
+
# calling protected methods
|
13
|
+
I18n.backend.send(:init_translations) if I18n.backend.respond_to?(:init_translations, true)
|
14
|
+
if I18n.backend.respond_to?(:translations, true)
|
15
|
+
translations = I18n.backend.send(:translations)
|
16
|
+
translator.storage.clear
|
17
|
+
translator.put(translations)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def translator
|
22
|
+
@translator ||= I18nTranslator.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def interpolator
|
26
|
+
@interpolator ||= I18nInterpolator.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def pluralizer(locale)
|
30
|
+
@pluralizers ||= {}
|
31
|
+
@pluralizers[locale] ||= translator.storage.read("#{locale}.i18n.plural.rule") ||
|
32
|
+
->(count){ count == 1 ? :one : :other }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/mova-i18n.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = "mova-i18n"
|
3
|
+
spec.version = "0.1.0"
|
4
|
+
spec.authors = ["Andrii Malyshko"]
|
5
|
+
spec.email = ["mail@nashbridges.me"]
|
6
|
+
spec.summary = "Seamless migration from I18n to Mova"
|
7
|
+
spec.description = spec.summary
|
8
|
+
spec.homepage = "https://github.com/mova-rb/mova-i18n"
|
9
|
+
spec.license = "MIT"
|
10
|
+
|
11
|
+
spec.files = `git ls-files -z`.split("\x0")
|
12
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
13
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
14
|
+
spec.require_paths = ["lib"]
|
15
|
+
|
16
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
17
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
18
|
+
|
19
|
+
spec.add_dependency "i18n"
|
20
|
+
spec.add_dependency "mova", "~> 0.1"
|
21
|
+
end
|