mova 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
@@ -0,0 +1 @@
1
+ --no-private
@@ -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
@@ -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.
@@ -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
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :default => :test
4
+ task :test do
5
+ $LOAD_PATH.unshift("test")
6
+ Dir.glob("./test/**/*_test.rb").each { |file| require file}
7
+ end
@@ -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
@@ -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