mova-i18n 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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.yardopts +1 -0
  4. data/CONTRIBUTING.md +47 -0
  5. data/Gemfile +26 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +93 -0
  8. data/Rakefile +24 -0
  9. data/lib/mova-i18n.rb +77 -0
  10. data/lib/mova-i18n/bridge.rb +69 -0
  11. data/lib/mova-i18n/config.rb +36 -0
  12. data/mova-i18n.gemspec +21 -0
  13. data/test/exists_test.rb +29 -0
  14. data/test/locale/en.yml +2 -0
  15. data/test/localize_test.rb +58 -0
  16. data/test/mova_config_test.rb +17 -0
  17. data/test/test_helper.rb +61 -0
  18. data/test/transfer_translations_test.rb +27 -0
  19. data/test/translate/default_test.rb +45 -0
  20. data/test/translate/pluralization_test.rb +62 -0
  21. data/test/translate/scope_test.rb +26 -0
  22. data/test/translate_test.rb +61 -0
  23. data/test/transliterate_test.rb +28 -0
  24. data/test_rails/dummy/.gitignore +15 -0
  25. data/test_rails/dummy/app/controllers/hello_controller.rb +2 -0
  26. data/test_rails/dummy/app/views/hello/html_safe_key.html.erb +1 -0
  27. data/test_rails/dummy/app/views/hello/html_safe_underscored.html.erb +1 -0
  28. data/test_rails/dummy/app/views/hello/html_unsafe.html.erb +1 -0
  29. data/test_rails/dummy/app/views/hello/locale_option.html.erb +1 -0
  30. data/test_rails/dummy/app/views/hello/missing_translation.html.erb +1 -0
  31. data/test_rails/dummy/app/views/hello/partial_scope.html.erb +1 -0
  32. data/test_rails/dummy/app/views/hello/raise_error.html.erb +1 -0
  33. data/test_rails/dummy/app/views/hello/translate.html.erb +1 -0
  34. data/test_rails/dummy/config.ru +4 -0
  35. data/test_rails/dummy/config/application.rb +13 -0
  36. data/test_rails/dummy/config/environment.rb +5 -0
  37. data/test_rails/dummy/config/environments/test.rb +10 -0
  38. data/test_rails/dummy/config/locales/uk.rb +20 -0
  39. data/test_rails/dummy/config/locales/uk.yml +252 -0
  40. data/test_rails/dummy/config/locales/views.yml +11 -0
  41. data/test_rails/helpers_test.rb +37 -0
  42. data/test_rails/test_helper.rb +11 -0
  43. data/test_rails/view_test.rb +55 -0
  44. metadata +154 -0
@@ -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
@@ -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 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
@@ -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,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
@@ -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
@@ -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
@@ -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