mova-i18n 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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