mova 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 +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
|