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.
@@ -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