mova-i18n 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|