├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── i18n-spec.gemspec ├── lib ├── i18n-spec.rb └── i18n-spec │ ├── failure_message.rb │ ├── matchers │ ├── be_a_complete_translation_of_matcher.rb │ ├── be_a_subset_of_matcher.rb │ ├── be_named_like_top_level_namespace_matcher.rb │ ├── be_parseable_matcher.rb │ ├── have_a_valid_locale_matcher.rb │ ├── have_legacy_interpolations.rb │ ├── have_missing_pluralization_keys_matcher.rb │ ├── have_one_top_level_namespace_matcher.rb │ └── have_valid_pluralization_keys_matcher.rb │ ├── models │ └── locale_file.rb │ ├── shared_examples │ └── valid_locale_file.rb │ ├── tasks.rb │ └── version.rb └── spec ├── fixtures ├── en.yml ├── es.yml ├── fr.yml ├── invalid_locale.yml ├── invalid_pluralization_keys.yml ├── legacy_interpolations.yml ├── missing_pluralization_keys.yml ├── multiple_top_levels.yml ├── not_subset.yml └── unparseable.yml ├── lib └── i18n-spec │ ├── matchers_spec.rb │ └── models │ └── locale_file_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # rcov generated 2 | coverage 3 | 4 | # rdoc generated 5 | rdoc 6 | 7 | # yard generated 8 | doc 9 | .yardoc 10 | 11 | # bundler 12 | .bundle 13 | 14 | # rvm 15 | .rvmrc 16 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - gem install bundler:2.0.2 3 | rvm: 4 | - 2.4.9 5 | - 2.5.7 6 | - 2.6.5 7 | - jruby-9.2.9.0 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ### Removed 6 | 7 | * Support for EOL'd rubies 8 | 9 | ## [0.6.0] - 2014-27-10 10 | 11 | ## [0.5.2] - 2014-03-08 12 | 13 | ## [0.5.1] - 2014-23-07 14 | 15 | ## [0.5.0] - 2014-08-07 16 | 17 | ## [0.4.1] - 2014-16-02 18 | 19 | ## [0.4.0] - 2013-04-04 20 | 21 | ## [0.3.0] - 2012-16-09 22 | 23 | ## [0.2.4] - 2012-15-09 24 | 25 | ## [0.2.3] - 2012-14-09 26 | 27 | ## [0.2.2] - 2012-26-04 28 | 29 | ## [0.2.1] - 2012-01-04 30 | 31 | ## [0.2.0] - 2012-28-03 32 | 33 | ## [0.1.3] - 2012-26-03 34 | 35 | ## [0.1.2] - 2012-14-02 36 | 37 | ## [0.1.1] - 2012-14-01 38 | 39 | ## [0.1.0] - 2011-31-12 40 | 41 | ## [0.0.10] - 2011-25-12 42 | 43 | ## [0.0.9] - 2011-21-12 44 | 45 | ## [0.0.8] - 2011-15-12 46 | 47 | ## [0.0.7] - 2011-15-12 48 | 49 | ## [0.0.6] - 2011-14-12 50 | 51 | ## [0.0.5] - 2011-14-12 52 | 53 | ## [0.0.4] - 2011-14-12 54 | 55 | ## [0.0.3] - 2011-14-12 56 | 57 | ## [0.0.2] - 2011-13-12 58 | 59 | ## 0.0.1 - 2011-12-12 60 | 61 | [Unreleased]: https://github.com/tigrish/i18n-spec/compare/v0.6.0..HEAD 62 | [0.6.0]: https://github.com/tigrish/i18n-spec/compare/v0.5.2..v0.6.0 63 | [0.5.2]: https://github.com/tigrish/i18n-spec/compare/v0.5.1..v0.5.2 64 | [0.5.1]: https://github.com/tigrish/i18n-spec/compare/v0.5.0..v0.5.1 65 | [0.5.0]: https://github.com/tigrish/i18n-spec/compare/v0.4.1..v0.5.0 66 | [0.4.1]: https://github.com/tigrish/i18n-spec/compare/v0.4.0..v0.4.1 67 | [0.4.0]: https://github.com/tigrish/i18n-spec/compare/v0.3.0..v0.4.0 68 | [0.3.0]: https://github.com/tigrish/i18n-spec/compare/v0.2.4..v0.3.0 69 | [0.2.4]: https://github.com/tigrish/i18n-spec/compare/v0.2.3..v0.2.4 70 | [0.2.3]: https://github.com/tigrish/i18n-spec/compare/v0.2.2..v0.2.3 71 | [0.2.2]: https://github.com/tigrish/i18n-spec/compare/v0.2.1..v0.2.2 72 | [0.2.1]: https://github.com/tigrish/i18n-spec/compare/v0.2.0..v0.2.1 73 | [0.2.0]: https://github.com/tigrish/i18n-spec/compare/v0.1.3..v0.2.0 74 | [0.1.3]: https://github.com/tigrish/i18n-spec/compare/v0.1.2..v0.1.3 75 | [0.1.2]: https://github.com/tigrish/i18n-spec/compare/v0.1.1..v0.1.2 76 | [0.1.1]: https://github.com/tigrish/i18n-spec/compare/v0.1.0..v0.1.1 77 | [0.1.0]: https://github.com/tigrish/i18n-spec/compare/v0.0.10..v0.1.0 78 | [0.0.10]: https://github.com/tigrish/i18n-spec/compare/v0.0.9..v0.0.10 79 | [0.0.9]: https://github.com/tigrish/i18n-spec/compare/v0.0.8..v0.0.9 80 | [0.0.8]: https://github.com/tigrish/i18n-spec/compare/v0.0.7..v0.0.8 81 | [0.0.7]: https://github.com/tigrish/i18n-spec/compare/v0.0.6..v0.0.7 82 | [0.0.6]: https://github.com/tigrish/i18n-spec/compare/v0.0.5..v0.0.6 83 | [0.0.5]: https://github.com/tigrish/i18n-spec/compare/v0.0.4..v0.0.5 84 | [0.0.4]: https://github.com/tigrish/i18n-spec/compare/v0.0.3..v0.0.4 85 | [0.0.3]: https://github.com/tigrish/i18n-spec/compare/v0.0.2..v0.0.3 86 | [0.0.2]: https://github.com/tigrish/i18n-spec/compare/v0.0.1..v0.0.2 87 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rake' 6 | gem 'rdoc' 7 | gem 'rspec', '~> 3.0' 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | i18n-spec (0.6.0) 5 | iso (~> 0.2) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | concurrent-ruby (1.0.5) 11 | concurrent-ruby (1.0.5-java) 12 | diff-lcs (1.3) 13 | i18n (1.1.0) 14 | concurrent-ruby (~> 1.0) 15 | iso (0.2.2) 16 | i18n 17 | rake (12.3.3) 18 | rdoc (6.0.4) 19 | rspec (3.8.0) 20 | rspec-core (~> 3.8.0) 21 | rspec-expectations (~> 3.8.0) 22 | rspec-mocks (~> 3.8.0) 23 | rspec-core (3.8.0) 24 | rspec-support (~> 3.8.0) 25 | rspec-expectations (3.8.2) 26 | diff-lcs (>= 1.2.0, < 2.0) 27 | rspec-support (~> 3.8.0) 28 | rspec-mocks (3.8.0) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.8.0) 31 | rspec-support (3.8.0) 32 | 33 | PLATFORMS 34 | java 35 | ruby 36 | 37 | DEPENDENCIES 38 | bundler (~> 1.0) 39 | i18n-spec! 40 | rake 41 | rdoc 42 | rspec (~> 3.0) 43 | 44 | BUNDLED WITH 45 | 2.0.2 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Christopher Dell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # i18n-spec 2 | 3 | [![Build Status](https://secure.travis-ci.org/tigrish/i18n-spec.svg)](https://travis-ci.org/tigrish/i18n-spec) 4 | 5 | i18n-spec provides RSpec matchers for testing your locale files and their translations. 6 | 7 | ## Testing the validity of your locale files 8 | 9 | There are a few matchers available; the subject of the spec is always a path to a locale file. 10 | 11 | ```ruby 12 | RSpec.describe "config/locales/en.yml" do 13 | it { is_expected.to be_parseable } 14 | it { is_expected.to have_valid_pluralization_keys } 15 | it { is_expected.to_not have_missing_pluralization_keys } 16 | it { is_expected.to have_one_top_level_namespace } 17 | it { is_expected.to be_named_like_top_level_namespace } 18 | it { is_expected.to_not have_legacy_interpolations } 19 | it { is_expected.to have_a_valid_locale } 20 | end 21 | ``` 22 | 23 | All of these tests can be ran in one line with a shared example: 24 | 25 | ```ruby 26 | RSpec.describe "A locale file" do 27 | it_behaves_like 'a valid locale file', 'config/locales/en.yml' 28 | end 29 | ``` 30 | 31 | Even better, you can run all of these tests for every file in a directory like so: 32 | 33 | ```ruby 34 | Dir.glob('config/locales/*.yml') do |locale_file| 35 | RSpec.describe "a locale file" do 36 | it_behaves_like 'a valid locale file', locale_file 37 | end 38 | end 39 | ``` 40 | 41 | ## Testing the validity of your translation data 42 | 43 | To test that your locale file is a subset of the default locale file 44 | 45 | ```ruby 46 | RSpec.describe "config/locales/fr.yml" do 47 | it { is_expected.to be_a_subset_of 'config/locales/en.yml' } 48 | end 49 | ``` 50 | 51 | If you need to test that all translations have been completed : 52 | 53 | ```ruby 54 | RSpec.describe "config/locales/fr.yml" do 55 | it { is_expected.to be_a_complete_translation_of 'config/locales/en.yml' } 56 | end 57 | ``` 58 | 59 | ## Rake tasks 60 | 61 | Include the tasks in your Rakefile with: 62 | 63 | ```ruby 64 | require 'i18n-spec/tasks' 65 | ``` 66 | 67 | ### Validating locale files 68 | 69 | You can check a locale file with the following task: 70 | 71 | rake i18n-spec:validate FILEPATH 72 | 73 | or check a whole directory : 74 | 75 | rake i18n-spec:validate DIRECTORY 76 | 77 | ### Checking for translation completeness 78 | 79 | You can check a locale file with the following task: 80 | 81 | rake i18n-spec:completeness SOURCE_FILEPATH TRANSLATED_FILEPATH 82 | 83 | or again, check a whole directory: 84 | 85 | rake i18n-spec:completeness SOURCE_FILEPATH DIRECTORY 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | begin 6 | Bundler.setup(:default, :development) 7 | rescue Bundler::BundlerError => e 8 | $stderr.puts e.message 9 | $stderr.puts "Run `bundle install` to install missing gems" 10 | exit e.status_code 11 | end 12 | require 'rake' 13 | 14 | require 'rspec/core' 15 | require 'rspec/core/rake_task' 16 | RSpec::Core::RakeTask.new(:spec) do |spec| 17 | spec.pattern = FileList['spec/**/*_spec.rb'] 18 | end 19 | 20 | RSpec::Core::RakeTask.new(:rcov) do |spec| 21 | spec.pattern = 'spec/**/*_spec.rb' 22 | spec.rcov = true 23 | end 24 | 25 | task :default => :spec 26 | 27 | require 'rdoc/task' 28 | Rake::RDocTask.new do |rdoc| 29 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 30 | 31 | rdoc.rdoc_dir = 'rdoc' 32 | rdoc.title = "i18n-spec #{version}" 33 | rdoc.rdoc_files.include('README*') 34 | rdoc.rdoc_files.include('lib/**/*.rb') 35 | end 36 | 37 | Dir['tasks/**/*.rb'].map { |file| require file } 38 | -------------------------------------------------------------------------------- /i18n-spec.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/i18n-spec/version" 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "i18n-spec" 5 | s.version = I18nSpec::VERSION 6 | 7 | s.required_ruby_version = ">= 2.4.0" 8 | s.require_paths = ["lib"] 9 | s.authors = ["Christopher Dell"] 10 | s.description = "Includes a number of rspec matchers to make specing your locale files easy peasy." 11 | s.email = "chris@tigrish.com" 12 | s.extra_rdoc_files = [ 13 | "LICENSE.txt", 14 | "README.md" 15 | ] 16 | s.files = Dir["lib/**/*.rb"] 17 | s.homepage = "https://github.com/tigrish/i18n-spec" 18 | s.licenses = ["MIT"] 19 | s.summary = "Matchers for specing locale files" 20 | 21 | s.add_runtime_dependency "iso", "~> 0.2" 22 | s.add_development_dependency "bundler", "~> 1.0" 23 | end 24 | 25 | -------------------------------------------------------------------------------- /lib/i18n-spec.rb: -------------------------------------------------------------------------------- 1 | if defined?(Psych) and defined?(Psych::VERSION) and !defined?(YAML::ParseError) 2 | YAML::ParseError = Psych::SyntaxError 3 | end 4 | 5 | require 'rspec/core' 6 | require 'iso' 7 | require 'yaml' 8 | require 'i18n-spec/failure_message' 9 | Dir[File.dirname(__FILE__) + '/i18n-spec/models/*.rb'].each {|file| require file } 10 | Dir[File.dirname(__FILE__) + '/i18n-spec/matchers/*.rb'].each {|file| require file } 11 | Dir[File.dirname(__FILE__) + '/i18n-spec/shared_examples/*.rb'].each {|file| require file } 12 | -------------------------------------------------------------------------------- /lib/i18n-spec/failure_message.rb: -------------------------------------------------------------------------------- 1 | module I18nSpec 2 | module FailureMessage 3 | def failure_for_should(&block) 4 | if respond_to?(:failure_message) 5 | # Rspec 3 6 | failure_message { |f| instance_exec(f, &block) } 7 | else 8 | # Rspec 2 9 | failure_message_for_should { |f| instance_exec(f, &block) } 10 | end 11 | end 12 | 13 | def failure_for_should_not(&block) 14 | if respond_to?(:failure_message_when_negated) 15 | # Rspec 3 16 | failure_message_when_negated { |f| instance_exec(f, &block) } 17 | else 18 | # Rspec 2 19 | failure_message_for_should_not { |f| instance_exec(f, &block) } 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/i18n-spec/matchers/be_a_complete_translation_of_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :be_a_complete_translation_of do |default_locale_filepath| 2 | extend I18nSpec::FailureMessage 3 | 4 | match do |filepath| 5 | locale_file = I18nSpec::LocaleFile.new(filepath) 6 | default_locale = I18nSpec::LocaleFile.new(default_locale_filepath) 7 | @misses = default_locale.flattened_translations.keys - locale_file.flattened_translations.keys 8 | @misses.empty? 9 | end 10 | 11 | failure_for_should do |filepath| 12 | "expected #{filepath} to include :\n- " << @misses.join("\n- ") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/i18n-spec/matchers/be_a_subset_of_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :be_a_subset_of do |default_locale_filepath| 2 | extend I18nSpec::FailureMessage 3 | 4 | match do |filepath| 5 | locale_file = I18nSpec::LocaleFile.new(filepath) 6 | default_locale = I18nSpec::LocaleFile.new(default_locale_filepath) 7 | @superset = locale_file.flattened_translations.keys - default_locale.flattened_translations.keys 8 | @superset.empty? 9 | end 10 | 11 | failure_for_should do |filepath| 12 | "expected #{filepath} to not include :\n- " << @superset.join("\n- ") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/i18n-spec/matchers/be_named_like_top_level_namespace_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :be_named_like_top_level_namespace do 2 | match do |actual| 3 | locale_file = I18nSpec::LocaleFile.new(actual) 4 | locale_file.is_named_like_top_level_namespace? 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/i18n-spec/matchers/be_parseable_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :be_parseable do 2 | extend I18nSpec::FailureMessage 3 | 4 | match do |actual| 5 | @locale_file = I18nSpec::LocaleFile.new(actual) 6 | @locale_file.is_parseable? 7 | end 8 | 9 | failure_for_should do |filepath| 10 | "expected #{filepath} to be parseable but got :\n- #{@locale_file.errors[:unparseable]}" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/i18n-spec/matchers/have_a_valid_locale_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :have_a_valid_locale do 2 | match do |actual| 3 | locale_file = I18nSpec::LocaleFile.new(actual) 4 | locale_file.locale.valid? 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/i18n-spec/matchers/have_legacy_interpolations.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :have_legacy_interpolations do 2 | match do |actual| 3 | locale_file = I18nSpec::LocaleFile.new(actual) 4 | locale_file.content =~ /\{\{.+\}\}/ 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/i18n-spec/matchers/have_missing_pluralization_keys_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :have_missing_pluralization_keys do 2 | extend I18nSpec::FailureMessage 3 | 4 | match do |actual| 5 | @locale_file = I18nSpec::LocaleFile.new(actual) 6 | @locale_file.missing_pluralization_keys.any? 7 | end 8 | 9 | failure_for_should_not do |filepath| 10 | flattened_keys = [] 11 | 12 | @locale_file.errors[:missing_pluralization_keys].each do |parent, subkeys| 13 | subkeys.each do |subkey| 14 | flattened_keys << [parent, subkey].join('.') 15 | end 16 | end 17 | 18 | "expected #{filepath} to contain the following pluralization keys :\n- " << flattened_keys.join("\n- ") 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/i18n-spec/matchers/have_one_top_level_namespace_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :have_one_top_level_namespace do 2 | match do |actual| 3 | locale_file = I18nSpec::LocaleFile.new(actual) 4 | locale_file.has_one_top_level_namespace? 5 | end 6 | end -------------------------------------------------------------------------------- /lib/i18n-spec/matchers/have_valid_pluralization_keys_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :have_valid_pluralization_keys do 2 | extend I18nSpec::FailureMessage 3 | 4 | match do |actual| 5 | @locale_file = I18nSpec::LocaleFile.new(actual) 6 | @locale_file.invalid_pluralization_keys.empty? 7 | end 8 | 9 | failure_for_should do |filepath| 10 | "expected #{filepath} to not contain invalid pluralization keys in the following namespaces :\n- " << @locale_file.errors[:invalid_pluralization_keys].join("\n- ") 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/i18n-spec/models/locale_file.rb: -------------------------------------------------------------------------------- 1 | module I18nSpec 2 | class LocaleFile 3 | PLURALIZATION_KEYS = %w{zero one two few many other} 4 | 5 | attr_accessor :filepath 6 | attr_reader :errors 7 | 8 | def initialize(filepath) 9 | @filepath = filepath 10 | @errors = {} 11 | end 12 | 13 | def content 14 | @content ||= IO.read(@filepath) 15 | end 16 | 17 | def translations 18 | @translations ||= yaml_load_content 19 | end 20 | 21 | def flattened_translations 22 | @flattened_translations ||= flatten_tree(translations.values.first) 23 | end 24 | 25 | def locale 26 | @locale ||= ISO::Tag.new(locale_code) 27 | end 28 | 29 | def locale_code 30 | translations.keys.first 31 | end 32 | 33 | def pluralizations 34 | result = flatten_tree(translations).select do |key, value| 35 | value.is_a?(Hash) 36 | end 37 | 38 | if result.is_a?(Array) 39 | Hash[result] 40 | else 41 | result 42 | end 43 | end 44 | 45 | def invalid_pluralization_keys 46 | invalid = [] 47 | pluralizations.each do |parent, pluralization| 48 | unless pluralization.keys.all? { |key| PLURALIZATION_KEYS.include?(key) } 49 | invalid << parent 50 | end 51 | end 52 | @errors[:invalid_pluralization_keys] = invalid unless invalid.empty? 53 | invalid 54 | end 55 | 56 | def missing_pluralization_keys 57 | return_data = {} 58 | rule_names = locale.language.plural_rule_names 59 | pluralizations.each do |parent, pluralization| 60 | missing_keys = rule_names - pluralization.keys 61 | return_data[parent] = missing_keys if missing_keys.any? 62 | end 63 | @errors[:missing_pluralization_keys] = return_data if return_data.any? 64 | return_data 65 | end 66 | 67 | def is_parseable? 68 | begin 69 | yaml_load_content 70 | true 71 | rescue YAML::SyntaxError => e 72 | @errors[:unparseable] = e.to_s 73 | false 74 | rescue ArgumentError => e 75 | @errors[:unparseable] = e.to_s 76 | false 77 | end 78 | end 79 | 80 | def has_one_top_level_namespace? 81 | translations.keys.size == 1 82 | end 83 | 84 | def is_named_like_top_level_namespace? 85 | locale_code == File.basename(@filepath, File.extname(@filepath)) 86 | end 87 | 88 | protected 89 | 90 | def flatten_tree(data, prefix = '', result = {}) 91 | data.each do |key, value| 92 | current_prefix = prefix.empty? ? key.to_s : "#{prefix}.#{key}" 93 | if !value.is_a?(Hash) || pluralization_data?(value) 94 | result[current_prefix] = value 95 | else 96 | flatten_tree(value, current_prefix, result) 97 | end 98 | end 99 | result 100 | end 101 | 102 | def pluralization_data?(data) 103 | keys = data.keys.map(&:to_s) 104 | keys.any? {|k| PLURALIZATION_KEYS.include?(k) } 105 | end 106 | 107 | def yaml_load_content 108 | if defined?(Psych) and defined?(Psych::VERSION) 109 | Psych.load(content) 110 | else 111 | YAML.load(content) 112 | end 113 | end 114 | 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/i18n-spec/shared_examples/valid_locale_file.rb: -------------------------------------------------------------------------------- 1 | RSpec.shared_examples_for "a valid locale file" do |locale_file| 2 | describe locale_file do 3 | it { is_expected.to be_parseable } 4 | it { is_expected.to have_valid_pluralization_keys } 5 | it { is_expected.to_not have_missing_pluralization_keys } 6 | it { is_expected.to have_one_top_level_namespace } 7 | it { is_expected.to be_named_like_top_level_namespace } 8 | it { is_expected.to_not have_legacy_interpolations } 9 | it { is_expected.to have_a_valid_locale } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/i18n-spec/tasks.rb: -------------------------------------------------------------------------------- 1 | require 'i18n-spec' 2 | 3 | I18nSpec::LOG_DETAIL_PREDICATE = " - " 4 | 5 | namespace :'i18n-spec' do 6 | desc "Checks the validity of a locale file" 7 | task :validate do 8 | if ARGV[1].nil? 9 | puts "You must specifiy a file path or a folder path" 10 | return 11 | elsif File.directory?(ARGV[1]) 12 | filepaths = Dir.glob("#{ARGV[1]}/*.yml") 13 | else 14 | filepaths = [ARGV[1]] 15 | end 16 | 17 | filepaths.each do |filepath| 18 | heading filepath 19 | fatals, errors, warnings = [0, 0, 0] 20 | locale_file = I18nSpec::LocaleFile.new(filepath) 21 | unless locale_file.is_parseable? 22 | log :fatal, 'could not be parsed', format_str(locale_file.errors[:unparseable]) 23 | fatals += 1 24 | break 25 | end 26 | 27 | unless locale_file.invalid_pluralization_keys.empty? 28 | log :error, 'invalid pluralization keys', format_array(locale_file.errors[:invalid_pluralization_keys]) 29 | errors += 1 30 | end 31 | 32 | log :ok if fatals + errors + warnings == 0 33 | end 34 | end 35 | 36 | desc "Checks for missing translations between the default and the translated locale file" 37 | task :completeness do 38 | if ARGV[1].nil? || ARGV[2].nil? 39 | puts "You must specify a default locale file and translated file or a folder of translated files" 40 | elsif File.directory?(ARGV[2]) 41 | translated_filepaths = Dir.glob("#{ARGV[2]}/*.yml") 42 | else 43 | translated_filepaths = [ARGV[2]] 44 | end 45 | default_locale = I18nSpec::LocaleFile.new(ARGV[1]) 46 | translated_filepaths.each do |translated_filepath| 47 | heading translated_filepath 48 | locale_file = I18nSpec::LocaleFile.new(translated_filepath) 49 | misses = default_locale.flattened_translations.keys.reject do |key| 50 | locale_file.flattened_translations.keys.include?(key) 51 | end 52 | if misses.empty? 53 | log :complete 54 | else 55 | misses.each { |miss| log :missing, miss } 56 | end 57 | end 58 | end 59 | 60 | def log(level, msg='', detail=nil) 61 | puts "- *" << level.to_s.upcase << '* ' << msg 62 | puts detail if detail 63 | end 64 | 65 | def heading(str='') 66 | puts "\n### " << str << "\n\n" 67 | end 68 | 69 | def format_array(array) 70 | [I18nSpec::LOG_DETAIL_PREDICATE, array.join(I18nSpec::LOG_DETAIL_PREDICATE)].join 71 | end 72 | 73 | def format_str(str) 74 | [I18nSpec::LOG_DETAIL_PREDICATE, str].join 75 | end 76 | end -------------------------------------------------------------------------------- /lib/i18n-spec/version.rb: -------------------------------------------------------------------------------- 1 | module I18nSpec 2 | VERSION = "0.6.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | hello: 'ohai' 3 | cats: 4 | zero: 'no cats' 5 | one: 'a cat' 6 | other: '%{count} cats' 7 | itteh: 8 | bitteh: 9 | ceiling: 10 | kitteh: 'is watching you' -------------------------------------------------------------------------------- /spec/fixtures/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | cats: 3 | zero: 'no gatos' 4 | one: 'un gato' 5 | other: '%{count} gatos' 6 | itteh: 7 | bitteh: 8 | ceiling: 9 | kitteh: 'te mira' -------------------------------------------------------------------------------- /spec/fixtures/fr.yml: -------------------------------------------------------------------------------- 1 | en: 2 | hello: 'osalu' 3 | cats: 4 | one: 'un chat' 5 | other: '%{count} chats' 6 | itteh: 7 | bitteh: 8 | ceiling: 9 | kitteh: 'te regarde' -------------------------------------------------------------------------------- /spec/fixtures/invalid_locale.yml: -------------------------------------------------------------------------------- 1 | lol: 2 | foo: bar 3 | -------------------------------------------------------------------------------- /spec/fixtures/invalid_pluralization_keys.yml: -------------------------------------------------------------------------------- 1 | en: 2 | cats: 3 | zero: 'no cats' 4 | one: 'a cat' 5 | other: '%{count} cats' 6 | invalid: 'this is not a valid pluralization key' -------------------------------------------------------------------------------- /spec/fixtures/legacy_interpolations.yml: -------------------------------------------------------------------------------- 1 | en: 2 | cats: 3 | one: {{count}} cat 4 | other: {{count}} cats 5 | -------------------------------------------------------------------------------- /spec/fixtures/missing_pluralization_keys.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | cats: 3 | one: 'a cat' 4 | other: '%{count} cats' 5 | -------------------------------------------------------------------------------- /spec/fixtures/multiple_top_levels.yml: -------------------------------------------------------------------------------- 1 | en: 2 | hello: world 3 | fr: 4 | bonjour: monde -------------------------------------------------------------------------------- /spec/fixtures/not_subset.yml: -------------------------------------------------------------------------------- 1 | en: 2 | dogs: 3 | zero: 'no dogs' 4 | one: 'one dog' 5 | other: '%{count} dogs' 6 | itteh: 7 | bitteh: 8 | ceiling: 9 | doggeh: 'is asking for trouble' -------------------------------------------------------------------------------- /spec/fixtures/unparseable.yml: -------------------------------------------------------------------------------- 1 | : foo: bar: -------------------------------------------------------------------------------- /spec/lib/i18n-spec/matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Valid file" do 4 | it_behaves_like "a valid locale file", 'spec/fixtures/en.yml' 5 | end 6 | 7 | describe "Invalid files" do 8 | it { expect('spec/fixtures/unparseable.yml').not_to be_parseable } 9 | it { expect('spec/fixtures/invalid_pluralization_keys.yml').not_to have_valid_pluralization_keys } 10 | it { expect('spec/fixtures/multiple_top_levels.yml').not_to have_one_top_level_namespace } 11 | it { expect('spec/fixtures/multiple_top_levels.yml').not_to be_named_like_top_level_namespace } 12 | it { expect('spec/fixtures/legacy_interpolations.yml').to have_legacy_interpolations } 13 | it { expect('spec/fixtures/invalid_locale.yml').not_to have_a_valid_locale } 14 | it { expect('spec/fixtures/not_subset.yml').not_to be_a_subset_of 'spec/fixtures/en.yml' } 15 | it { expect('spec/fixtures/missing_pluralization_keys.yml').to have_missing_pluralization_keys } 16 | end 17 | 18 | describe "Translated files" do 19 | describe 'spec/fixtures/fr.yml' do 20 | it { is_expected.to be_a_subset_of 'spec/fixtures/en.yml' } 21 | it { is_expected.to be_a_complete_translation_of 'spec/fixtures/en.yml' } 22 | end 23 | 24 | describe 'spec/fixtures/es.yml' do 25 | it { is_expected.to be_a_subset_of 'spec/fixtures/en.yml' } 26 | it { is_expected.not_to be_a_complete_translation_of 'spec/fixtures/en.yml'} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/lib/i18n-spec/models/locale_file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module LocaleFileHelper 4 | def locale_file_with_content(content) 5 | locale_file = I18nSpec::LocaleFile.new('test.yml') 6 | expect(locale_file).to receive(:content).and_return(content) 7 | locale_file 8 | end 9 | end 10 | 11 | describe I18nSpec::LocaleFile do 12 | include LocaleFileHelper 13 | 14 | describe "#locale_code" do 15 | it "returns the first key of the file" do 16 | locale_file = locale_file_with_content("pt-BR:\n hello: world") 17 | expect(locale_file.locale_code).to eq('pt-BR') 18 | end 19 | end 20 | 21 | describe "#locale" do 22 | it "returns an ISO::Tag based on the locale_code" do 23 | locale_file = locale_file_with_content("pt-BR:\n hello: world") 24 | expect(locale_file.locale).to be_a(ISO::Tag) 25 | expect(locale_file.locale.language.code).to eq('pt') 26 | expect(locale_file.locale.region.code).to eq('BR') 27 | end 28 | end 29 | 30 | describe "#is_parseable?" do 31 | context "when YAML is parseable" do 32 | let(:locale_file) { locale_file_with_content("en:\n hello: world") } 33 | 34 | it "returns true" do 35 | expect(locale_file.is_parseable?).to eq(true) 36 | end 37 | end 38 | 39 | context "when YAML is NOT parseable" do 40 | let(:locale_file) { locale_file_with_content(": foo: bar:") } 41 | 42 | it "returns false" do 43 | expect(locale_file.is_parseable?).to eq(false) 44 | end 45 | 46 | it "adds an :unparseable error" do 47 | locale_file.is_parseable? 48 | expect(locale_file.errors[:unparseable]).not_to be_nil 49 | end 50 | end 51 | end 52 | 53 | describe "#pluralizations" do 54 | let(:content) { 55 | "en: 56 | cats: 57 | one: one 58 | two: two 59 | three: three 60 | dogs: 61 | one: one 62 | some: some 63 | birds: 64 | penguins: penguins 65 | doves: doves"} 66 | 67 | it "returns the leaf where one of the keys is a pluralization key" do 68 | locale_file = locale_file_with_content(content) 69 | expect(locale_file.pluralizations).to eq({ 70 | 'en.cats' => {'one' => 'one', 'two' => 'two', 'three' => 'three'}, 71 | 'en.dogs' => {'one' => 'one', 'some' => 'some'}, 72 | }) 73 | end 74 | end 75 | 76 | describe "#invalid_pluralization_keys" do 77 | let(:content) { 78 | "en: 79 | cats: 80 | one: one 81 | two: two 82 | tommy: tommy 83 | tabby: tabby 84 | dogs: 85 | one: one 86 | some: some 87 | birds: 88 | zero: zero 89 | other: other"} 90 | let(:locale_file) { locale_file_with_content(content) } 91 | 92 | it "returns the parent that contains invalid pluralizations" do 93 | expect(locale_file.invalid_pluralization_keys.size).to eq(2) 94 | expect(locale_file.invalid_pluralization_keys).to be_include 'en.cats' 95 | expect(locale_file.invalid_pluralization_keys).to be_include 'en.dogs' 96 | expect(locale_file.invalid_pluralization_keys).not_to be_include 'en.birds' 97 | end 98 | 99 | it "adds a :invalid_pluralization_keys error with each invalid key" do 100 | locale_file.invalid_pluralization_keys 101 | expect(locale_file.invalid_pluralization_keys.size).to eq(2) 102 | expect(locale_file.invalid_pluralization_keys).to be_include 'en.cats' 103 | expect(locale_file.invalid_pluralization_keys).to be_include 'en.dogs' 104 | expect(locale_file.invalid_pluralization_keys).not_to be_include 'en.birds' 105 | end 106 | end 107 | 108 | describe "#missing_pluralization_keys" do 109 | it "returns the parents that containts missing pluralizations in with the english rules" do 110 | content = "en: 111 | cats: 112 | one: one 113 | dogs: 114 | other: other 115 | birds: 116 | one: one 117 | other: other" 118 | locale_file = locale_file_with_content(content) 119 | expect(locale_file.missing_pluralization_keys).to eq({ 120 | 'en.cats' => %w(other), 121 | 'en.dogs' => %w(one) 122 | }) 123 | expect(locale_file.errors[:missing_pluralization_keys]).not_to be_nil 124 | end 125 | 126 | it "returns the parents that containts missing pluralizations in with the russian rules" do 127 | content = "ru: 128 | cats: 129 | one: one 130 | few: few 131 | many: many 132 | other: other 133 | dogs: 134 | one: one 135 | other: some 136 | birds: 137 | zero: zero 138 | one: one 139 | few: few 140 | other: other" 141 | locale_file = locale_file_with_content(content) 142 | expect(locale_file.missing_pluralization_keys).to eq({ 143 | 'ru.dogs' => %w(few many), 144 | 'ru.birds' => %w(many) 145 | }) 146 | expect(locale_file.errors[:missing_pluralization_keys]).not_to be_nil 147 | end 148 | 149 | it "returns the parents that containts missing pluralizations in with the japanese rules" do 150 | content = "ja: 151 | cats: 152 | one: one 153 | dogs: 154 | other: some 155 | birds: not really a pluralization" 156 | locale_file = locale_file_with_content(content) 157 | expect(locale_file.missing_pluralization_keys).to eq({ 'ja.cats' => %w(other) }) 158 | expect(locale_file.errors[:missing_pluralization_keys]).not_to be_nil 159 | end 160 | 161 | it "returns an empty hash when all pluralizations are complete" do 162 | content = "ja: 163 | cats: 164 | other: one 165 | dogs: 166 | other: some 167 | birds: not really a pluralization" 168 | locale_file = locale_file_with_content(content) 169 | expect(locale_file.missing_pluralization_keys).to eq({}) 170 | expect(locale_file.errors[:missing_pluralization_keys]).to be_nil 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | require 'rspec' 4 | require 'i18n-spec' 5 | 6 | # Requires supporting files with custom matchers and macros, etc, 7 | # in ./support/ and its subdirectories. 8 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 9 | 10 | RSpec.configure do |config| 11 | 12 | end 13 | --------------------------------------------------------------------------------