mova 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 +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +102 -0
- data/Rakefile +7 -0
- data/benchmarks/compare/locale_fallbacks.rb +42 -0
- data/benchmarks/compare/long_scope.rb +48 -0
- data/benchmarks/each_vs_or.rb +42 -0
- data/benchmarks/fallbacks_with_local_storage.rb +21 -0
- data/benchmarks/fallbacks_with_remote_storage.rb +24 -0
- data/lib/mova.rb +36 -0
- data/lib/mova/interpolation/sprintf.rb +73 -0
- data/lib/mova/read_strategy/eager.rb +25 -0
- data/lib/mova/read_strategy/lazy.rb +21 -0
- data/lib/mova/scope.rb +94 -0
- data/lib/mova/storage/chain.rb +124 -0
- data/lib/mova/storage/memory.rb +67 -0
- data/lib/mova/storage/readonly.rb +51 -0
- data/lib/mova/translator.rb +183 -0
- data/mova.gemspec +18 -0
- data/test/acceptance_test.rb +22 -0
- data/test/interpolation/sprintf_test.rb +60 -0
- data/test/mova_test.rb +21 -0
- data/test/read_strategy/eager_test.rb +47 -0
- data/test/read_strategy/lazy_test.rb +46 -0
- data/test/scope_test.rb +41 -0
- data/test/storage/chain_test.rb +100 -0
- data/test/storage/memory_test.rb +52 -0
- data/test/storage/readonly_test.rb +36 -0
- data/test/test_helper.rb +47 -0
- data/test/translator/get_test.rb +55 -0
- data/test/translator/initialize_test.rb +23 -0
- data/test/translator/put_test.rb +27 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dce5a55e194f08af6811f43235c1d05fd61a8fd6
|
4
|
+
data.tar.gz: 0bd231589ce57b0bd8c834efc7fd3f8249104214
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c1295e5cc33f92fce796ef8e96c513a3ce1574c6047d30a9bcd945b7ecb8be77f86bdef3bfb6861c663874f08791e61b051a5229d1fa77cb09f1d05c71e97633
|
7
|
+
data.tar.gz: 99de35a299fc4877a76ce12d937efac57119819a11d38019abfe7a18c893ad89e1ea6156e0feb493fcefdb8aa80c738df7b128be4354014346cf096ce656b80c
|
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 test
|
40
|
+
```
|
41
|
+
|
42
|
+
## Working with documentation
|
43
|
+
|
44
|
+
```
|
45
|
+
yard server -dr
|
46
|
+
open http://localhost:8808
|
47
|
+
```
|
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
gem "minitest", "~> 5.4"
|
6
|
+
gem "rspec-mocks", "~> 3.0"
|
7
|
+
gem "yard", "~> 0.8"
|
8
|
+
gem "pry"
|
9
|
+
|
10
|
+
group :benchmark do
|
11
|
+
gem "benchmark-ips"
|
12
|
+
gem "i18n"
|
13
|
+
gem "r18n-core"
|
14
|
+
gem "activesupport"
|
15
|
+
gem "dalli", ">= 2.6.4"
|
16
|
+
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,102 @@
|
|
1
|
+
# Mova
|
2
|
+
|
3
|
+
**Mova** is a translation and localization library that aims to be simple and fast.
|
4
|
+
|
5
|
+
## Name origin
|
6
|
+
|
7
|
+
"Мова" [['mɔwɑ][mova-pronounce]] in Ukrainian and Belarusian means "language".
|
8
|
+
|
9
|
+
## Why
|
10
|
+
|
11
|
+
Because [I18n][i18n] code is hard to reason about.
|
12
|
+
|
13
|
+
## Status
|
14
|
+
|
15
|
+
Not tested in production. Localization part is yet to be implemented.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile and run `bundle`:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'mova'
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require "mova"
|
29
|
+
|
30
|
+
# instantiate a translator with in-memory storage
|
31
|
+
translator = Mova::Translator.new
|
32
|
+
|
33
|
+
# store translations
|
34
|
+
translator.put(en: {hello: "world!"})
|
35
|
+
|
36
|
+
# retreive translations
|
37
|
+
translator.get("hello", :en) #=> "world!"
|
38
|
+
|
39
|
+
# wrap existing storage
|
40
|
+
require "redis-activesupport"
|
41
|
+
redis = ActiveSupport::Cache::RedisStore.new("localhost:6379/0")
|
42
|
+
translator = Mova::Translator.new(storage: redis)
|
43
|
+
translator.get(:hi_from_redis, :en) #=> "Hi!"
|
44
|
+
```
|
45
|
+
|
46
|
+
## Documentation
|
47
|
+
|
48
|
+
*link to rubydoc*
|
49
|
+
|
50
|
+
## Design principles
|
51
|
+
|
52
|
+
1. **Translation and localization data should be decoupled.**
|
53
|
+
|
54
|
+
Localization info describes how dates, numbers, currencies etc. should be rendered,
|
55
|
+
and what pluralization rules and writing direction should be used. This data is rarely if ever
|
56
|
+
changed during project lifetime.
|
57
|
+
|
58
|
+
On the other hand translation data is always subject to change, because you may need to
|
59
|
+
adjust text length to fit it into new design, update your product title to be more SEO friendly
|
60
|
+
and so on.
|
61
|
+
|
62
|
+
It is more performant to keep localization data in a static Ruby class, rather then
|
63
|
+
fetch it from a translation storage each time when we want to localize a date. This still
|
64
|
+
allows to modify locales on per project level.
|
65
|
+
|
66
|
+
Ruby class is also a natural place for methods and collection data types, while procs and hashes
|
67
|
+
being put into a translation storage feels awkward.
|
68
|
+
|
69
|
+
2. **Translations should be kept in a simple key-value storage.**
|
70
|
+
|
71
|
+
Simple storage means that given a string key it should return only a string or
|
72
|
+
nil if nothing found. No object serialization. No hashes as a return value.
|
73
|
+
|
74
|
+
Such limitation allows to use almost anything as a storage: Ruby hash, file storage that maps
|
75
|
+
to a hash, any RDMBS, any key-value store, or any combination of them.
|
76
|
+
|
77
|
+
This also forces decoupling of translation retrieval and translation management (finding
|
78
|
+
untranslated strings, providing hints to translators etc.) since not much data can be put in a key.
|
79
|
+
|
80
|
+
3. **Translation framework should not be aware of any file format.**
|
81
|
+
|
82
|
+
If we need to import translations from a file, hash should be used as input format. No matter
|
83
|
+
which file format is used to store data on a disk, whether it be YAML or JSON, or TOML, any option
|
84
|
+
should work transparently.
|
85
|
+
|
86
|
+
4. **Exception should be used as a last resort when controlling flow.**
|
87
|
+
|
88
|
+
Raising and catching an exception in Ruby is a very expensive operation and should be avoided
|
89
|
+
whenever possible.
|
90
|
+
|
91
|
+
5. **Instance is better than singleton.**
|
92
|
+
|
93
|
+
You can have separate versions of translator for models and templates. You can have different
|
94
|
+
storages. You can use different interpolation rules.
|
95
|
+
|
96
|
+
## Related projects
|
97
|
+
|
98
|
+
* [mova-i18n][mova-i18n] - integrating with/replacing I18n.
|
99
|
+
|
100
|
+
[mova-pronounce]: http://upload.wikimedia.org/wikipedia/commons/f/ff/Uk-%D0%BC%D0%BE%D0%B2%D0%B0.ogg
|
101
|
+
[mova-i18n]: https://github.com/mova-rb/mova-i18n
|
102
|
+
[i18n]: https://github.com/svenfuchs/i18n
|
data/Rakefile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "benchmark/ips"
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
require "mova"
|
6
|
+
require "i18n"
|
7
|
+
require "r18n-core"
|
8
|
+
|
9
|
+
mova = Mova::Translator.new.tap do |t|
|
10
|
+
def t.locales_to_try(locale)
|
11
|
+
[locale, :ru, :en]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
mova.put(en: {hello: "Hello"}, uk: {hi: "Привіт"})
|
15
|
+
|
16
|
+
I18n.enforce_available_locales = true
|
17
|
+
I18n::Backend::Simple.include(I18n::Backend::Fallbacks)
|
18
|
+
I18n.fallbacks[:uk] = [:uk, :ru, :en]
|
19
|
+
I18n.backend.store_translations(:en, {hello: "Hello"})
|
20
|
+
I18n.backend.store_translations(:uk, {hi: "Привіт"})
|
21
|
+
I18n.locale = :uk
|
22
|
+
|
23
|
+
module R18nHashLoader
|
24
|
+
def self.available; [R18n.locale("uk"), R18n.locale("ru"), R18n.locale("en")] end
|
25
|
+
def self.load(locale)
|
26
|
+
case locale.code
|
27
|
+
when "en" then {"hello" => "Hello"}
|
28
|
+
when "uk" then {"hi" => "Привіт"}
|
29
|
+
when "ru" then {}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
R18n.default_places = R18nHashLoader
|
34
|
+
R18n.set("uk")
|
35
|
+
|
36
|
+
Benchmark.ips do |x|
|
37
|
+
x.report("mova") { mova.get(:hello, :uk) }
|
38
|
+
x.report("i18n") { I18n.t(:hello) }
|
39
|
+
x.report("r18n") { R18n.t.hello }
|
40
|
+
|
41
|
+
x.compare!
|
42
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "benchmark/ips"
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
require "mova"
|
6
|
+
require "i18n"
|
7
|
+
require "r18n-core"
|
8
|
+
|
9
|
+
KEYS = Array.new(1000).map { SecureRandom.hex(8) }
|
10
|
+
DATA = {
|
11
|
+
"this" => {
|
12
|
+
"is" => {
|
13
|
+
"really" => {
|
14
|
+
"deep" => {
|
15
|
+
"nested" => {
|
16
|
+
"key" => KEYS.each_with_object({}) { |key, memo| memo[key] = SecureRandom.hex(30) }
|
17
|
+
}
|
18
|
+
}
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
def random_key
|
25
|
+
KEYS.sample
|
26
|
+
end
|
27
|
+
|
28
|
+
mova = Mova::Translator.new
|
29
|
+
mova.put(en: DATA)
|
30
|
+
|
31
|
+
I18n.enforce_available_locales = true
|
32
|
+
I18n.backend.store_translations(:en, DATA)
|
33
|
+
I18n.locale = :en
|
34
|
+
|
35
|
+
module R18nHashLoader
|
36
|
+
def self.available; [R18n.locale("en")] end
|
37
|
+
def self.load(locale); DATA end
|
38
|
+
end
|
39
|
+
R18n.default_places = R18nHashLoader
|
40
|
+
R18n.set("en")
|
41
|
+
|
42
|
+
Benchmark.ips do |x|
|
43
|
+
x.report("mova") { mova.get("this.is.really.deep.nested.key.#{random_key}", :en) }
|
44
|
+
x.report("i18n") { I18n.t("this.is.really.deep.nested.key.#{random_key}") }
|
45
|
+
x.report("r18n") { R18n.t.this.is.really.deep.nested.key.send(random_key) }
|
46
|
+
|
47
|
+
x.compare!
|
48
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "benchmark/ips"
|
3
|
+
|
4
|
+
storage1 = Object.new.tap do |s|
|
5
|
+
def s.get
|
6
|
+
nil
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
storage2 = Object.new.tap do |s|
|
11
|
+
def s.get
|
12
|
+
"result"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Chain
|
17
|
+
attr_reader :storages
|
18
|
+
|
19
|
+
def initialize(*storages)
|
20
|
+
@storages = storages
|
21
|
+
end
|
22
|
+
|
23
|
+
def with_each
|
24
|
+
storages.each do |s|
|
25
|
+
result = s.get
|
26
|
+
return result if result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def with_or
|
31
|
+
storages[0].get || storages[1].get
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
chain = Chain.new(storage1, storage2)
|
36
|
+
|
37
|
+
Benchmark.ips do |x|
|
38
|
+
x.report("each") { chain.with_each }
|
39
|
+
x.report("or") { chain.with_or }
|
40
|
+
|
41
|
+
x.compare!
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "benchmark/ips"
|
3
|
+
|
4
|
+
require "mova"
|
5
|
+
require "mova/storage/memory"
|
6
|
+
require "mova/read_strategy/eager"
|
7
|
+
|
8
|
+
memory = Mova::Storage::Memory.new
|
9
|
+
memory.write("en.mova_test", "Mova test")
|
10
|
+
|
11
|
+
lazy = Mova::Translator.new(storage: memory)
|
12
|
+
eager = Mova::Translator.new(storage: memory).tap do |t|
|
13
|
+
t.extend Mova::ReadStrategy::Eager
|
14
|
+
end
|
15
|
+
|
16
|
+
Benchmark.ips do |x|
|
17
|
+
x.report("lazy") { lazy.get(:mova_test, [:uk, :ru, :en]) }
|
18
|
+
x.report("eager") { eager.get(:mova_test, [:uk, :ru, :en]) }
|
19
|
+
|
20
|
+
x.compare!
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "benchmark/ips"
|
3
|
+
require "active_support/notifications"
|
4
|
+
require "active_support/cache"
|
5
|
+
require "active_support/cache/strategy/local_cache"
|
6
|
+
require "active_support/cache/dalli_store"
|
7
|
+
|
8
|
+
require "mova"
|
9
|
+
require "mova/read_strategy/eager"
|
10
|
+
|
11
|
+
dalli = ActiveSupport::Cache::DalliStore.new("localhost:11211")
|
12
|
+
dalli.write("en.mova_test", "Mova test")
|
13
|
+
|
14
|
+
lazy = Mova::Translator.new(storage: dalli)
|
15
|
+
eager = Mova::Translator.new(storage: dalli).tap do |t|
|
16
|
+
t.extend Mova::ReadStrategy::Eager
|
17
|
+
end
|
18
|
+
|
19
|
+
Benchmark.ips do |x|
|
20
|
+
x.report("lazy") { lazy.get(:mova_test, [:uk, :ru, :en]) }
|
21
|
+
x.report("eager") { eager.get(:mova_test, [:uk, :ru, :en]) }
|
22
|
+
|
23
|
+
x.compare!
|
24
|
+
end
|
data/lib/mova.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require "mova/scope"
|
2
|
+
require "mova/read_strategy/lazy"
|
3
|
+
require "mova/translator"
|
4
|
+
|
5
|
+
module Mova
|
6
|
+
EMPTY_TRANSLATION = "".freeze
|
7
|
+
|
8
|
+
# @return [String] if translation is non-empty string
|
9
|
+
# @return [nil] if translation is nil or an empty string
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# Mova.presence("hello") #=> "hello"
|
13
|
+
# Mova.presence(nil) #=> nil
|
14
|
+
# Mova.presence("") #=> nil
|
15
|
+
#
|
16
|
+
# @note Unlike ActiveSupport's Object#presence this method doesn't
|
17
|
+
# treat a string made of spaces as blank
|
18
|
+
# " ".presence #=> nil
|
19
|
+
# Mova.presence(" ") #=> " "
|
20
|
+
#
|
21
|
+
# @since 0.1.0
|
22
|
+
def self.presence(translation)
|
23
|
+
return nil if translation == EMPTY_TRANSLATION
|
24
|
+
translation
|
25
|
+
end
|
26
|
+
|
27
|
+
# Classes under this namespace must conform with
|
28
|
+
# {http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html ActiveSupport::Cache::Store} API.
|
29
|
+
#
|
30
|
+
# Instances must respond to at least #read, #read_multi, #write, #exist?, #clear. It is allowed #clear
|
31
|
+
# to be implemented as no-op.
|
32
|
+
module Storage; end
|
33
|
+
|
34
|
+
# Classes under this namespace must implement #call(String, Hash) and #missing_placeholder(Symbol, Hash).
|
35
|
+
module Interpolation; end
|
36
|
+
end
|