├── _config.yml ├── .rspec ├── public ├── localer-full.png ├── localer-full2.png └── localer-logo.png ├── lib ├── localer │ ├── version.rb │ ├── config │ │ └── locale.rb │ ├── data │ │ ├── checker.rb │ │ ├── missing_translations.rb │ │ ├── service.rb │ │ └── processor.rb │ ├── rails.rb │ ├── rake_task.rb │ ├── data.rb │ ├── config.rb │ └── ext │ │ ├── string.rb │ │ └── hash.rb └── localer.rb ├── gemfiles ├── rails50.gemfile ├── rails51.gemfile ├── rails52.gemfile ├── rails60.gemfile ├── rails61.gemfile ├── rails50.gemfile.lock ├── rails51.gemfile.lock ├── rails52.gemfile.lock ├── rails60.gemfile.lock └── rails61.gemfile.lock ├── spec ├── dummy_app │ ├── config │ │ ├── application.rb │ │ ├── environment.rb │ │ └── locales │ │ │ ├── ru.rails.rb │ │ │ ├── en.rails.rb │ │ │ ├── us.rails.rb │ │ │ ├── ru.rails.yml │ │ │ ├── us.rails.yml │ │ │ └── en.rails.yml │ └── config.ru ├── localer_spec.rb └── spec_helper.rb ├── Gemfile ├── .gitignore ├── features ├── real_locales.feature ├── support │ └── env.rb ├── missing_app.feature ├── global_exclude.feature ├── locale_option.feature ├── simple.feature └── step_definitions │ └── additional_cli_steps.rb ├── Appraisals ├── Rakefile ├── .github └── workflows │ ├── rubocop.yml │ ├── bundle-audit.yml │ └── test.yml ├── LICENSE.txt ├── bin └── localer ├── localer.gemspec ├── .rubocop.yml ├── CODE_OF_CONDUCT.md ├── Gemfile.lock └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-leap-day -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /public/localer-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aderyabin/localer/HEAD/public/localer-full.png -------------------------------------------------------------------------------- /public/localer-full2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aderyabin/localer/HEAD/public/localer-full2.png -------------------------------------------------------------------------------- /public/localer-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aderyabin/localer/HEAD/public/localer-logo.png -------------------------------------------------------------------------------- /lib/localer/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Localer 4 | VERSION = "0.2.0" 5 | end 6 | -------------------------------------------------------------------------------- /gemfiles/rails50.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 5.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails51.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 5.1" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails52.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 5.2" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails60.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 6.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails61.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 6.1" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /spec/dummy_app/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails" 4 | 5 | class DummyApp < Rails::Application 6 | config.eager_load = false 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in localer.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /spec/localer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Localer do 4 | it "has a version number" do 5 | expect(Localer::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy_app/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require ::File.expand_path("../config/environment", __FILE__) 4 | 5 | Rails.application.eager_load! 6 | 7 | run Rails.application 8 | -------------------------------------------------------------------------------- /spec/dummy_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require File.expand_path('application', __dir__) 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | Gemfile.lock 11 | erl_crash.dump 12 | .ruby-version 13 | .vscode 14 | spec/dummy_app/log/ 15 | vendor/ 16 | .devcontainer/ 17 | -------------------------------------------------------------------------------- /spec/dummy_app/config/locales/ru.rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | { 4 | ru: { 5 | number: { 6 | nth: { 7 | ordinals: ->(_key, _options) {}, 8 | ordinalized: ->(_key, options) {} 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/localer/config/locale.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Localer 4 | class Config 5 | # Provide config for locale 6 | class Locale 7 | extend Dry::Initializer 8 | option :exclude, default: -> { [] } 9 | option :enabled, default: -> { true } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /features/real_locales.feature: -------------------------------------------------------------------------------- 1 | Feature: Localer 2 | 3 | Scenario: Real locales does not pass 4 | Given a real locales 5 | When I run checker 6 | Then the checker should returns 4 missing translations: 7 | | ru.population.italy | 8 | | us.population.italy | 9 | | en.population.france | 10 | | en.countries.france.population | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "localer" 5 | 6 | RSpec.configure do |config| 7 | # Disable RSpec exposing methods globally on `Module` and `main` 8 | config.disable_monkey_patching! 9 | 10 | config.expect_with :rspec do |c| 11 | c.syntax = :expect 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'rails50' do 2 | gem 'rails', '~> 5.0' 3 | end 4 | 5 | appraise 'rails51' do 6 | gem 'rails', '~> 5.1' 7 | end 8 | 9 | appraise 'rails52' do 10 | gem 'rails', '~> 5.2' 11 | end 12 | 13 | appraise 'rails60' do 14 | gem 'rails', '~> 6.0' 15 | end 16 | 17 | appraise 'rails61' do 18 | gem 'rails', '~> 6.1' 19 | end 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rubocop/rake_task" 3 | require 'cucumber/rake/task' 4 | 5 | RuboCop::RakeTask.new 6 | 7 | Cucumber::Rake::Task.new(:features) do |t| 8 | t.cucumber_opts = "features --format pretty" 9 | end 10 | 11 | task :default do 12 | if ENV["RUBOCOP"] 13 | Rake::Task["rubocop"].invoke 14 | else 15 | Rake::Task["features"].invoke 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | name: Lint Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | rubocop: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: 2.7 17 | - name: Lint Ruby code with RuboCop 18 | run: | 19 | gem install rubocop 20 | rubocop 21 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'aruba/cucumber' 4 | 5 | DUMMY_APP_DIR = "../../spec/dummy_app" 6 | LOCALE_DIR = "#{DUMMY_APP_DIR}/config/locales" 7 | CONFIG_PATH = "#{DUMMY_APP_DIR}/.localer.yml" 8 | 9 | After do |_| 10 | %w[ru en us].each do |locale| 11 | path = "#{LOCALE_DIR}/#{locale}.yml" 12 | remove(path) if exist?(path) 13 | end 14 | 15 | remove(CONFIG_PATH) if exist?(CONFIG_PATH) 16 | end 17 | -------------------------------------------------------------------------------- /lib/localer/data/checker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Localer 4 | class Data 5 | # Check missing translations 6 | # Returns true if no missing translations found, otherwise false 7 | class Checker < Service 8 | param :data 9 | 10 | def call 11 | data.each do |_locale, _key, value| 12 | return false if value.nil? 13 | end 14 | true 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/localer/data/missing_translations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Localer 4 | class Data 5 | # A service that returns array of missing translations 6 | class MissingTranslations < Service 7 | param :data 8 | 9 | def call 10 | missing = [] 11 | data.each do |locale, key, value| 12 | missing.push("#{locale}#{key}") if value.nil? 13 | end 14 | missing 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/localer/data/service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry-initializer' 4 | module Localer 5 | class Data 6 | # Core service object 7 | class Service 8 | extend Dry::Initializer # use `param` and `option` for dependencies 9 | 10 | class << self 11 | # Instantiates and calls the service at once 12 | def call(*args, &block) 13 | new(*args).call(&block) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /features/missing_app.feature: -------------------------------------------------------------------------------- 1 | Feature: Localer 2 | 3 | Scenario: No rails application 4 | When I run `localer check` 5 | Then the checker should not found rails application 6 | 7 | Scenario: No rails application at existed paths 8 | When I run `localer check` 9 | Then the checker should not found rails application 10 | 11 | Scenario: No rails application at not non-existed 12 | When I run `localer check non-existed_path` 13 | Then the checker should not found rails application 14 | -------------------------------------------------------------------------------- /.github/workflows/bundle-audit.yml: -------------------------------------------------------------------------------- 1 | name: Bundle Audit 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | schedule: 9 | - cron: "0 0 * * *" 10 | 11 | jobs: 12 | rubocop: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: 2.7 19 | - name: Patch-level verification for Bundler 20 | run: | 21 | gem install bundle-audit 22 | bundle-audit check --update 23 | -------------------------------------------------------------------------------- /lib/localer/rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Localer 4 | module Rails # :nodoc: 5 | class << self 6 | def connect! 7 | require File.expand_path("config/environment", Localer.config.app_path) 8 | true 9 | rescue LoadError 10 | false 11 | end 12 | 13 | def translations 14 | return {} unless connect! 15 | 16 | I18n.backend.send(:init_translations) 17 | I18n.backend.send(:translations) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/localer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry-initializer" 4 | require_relative "localer/version" 5 | require_relative "localer/rails" 6 | require_relative "localer/config" 7 | require_relative "localer/data" 8 | 9 | module Localer # :nodoc: 10 | using Localer::Ext::Hash 11 | # using Localer::Ext::String 12 | 13 | class << self 14 | def data 15 | @data ||= load_data 16 | end 17 | 18 | def config 19 | @config ||= configure 20 | end 21 | 22 | def configure(options = {}) 23 | @config = Config.load(options) 24 | end 25 | 26 | def load_data(source = Localer::Rails.translations) 27 | @data = Data.new(source) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/localer/rake_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rake' 4 | require 'rake/tasklib' 5 | require 'localer' 6 | 7 | module Localer 8 | # Defines a Rake task for running Localer. 9 | # The simplest use of it goes something like: 10 | # 11 | # Localer::Rakeask.new 12 | # This will define a task named localer described as 'Run Localer'. 13 | class RakeTask < Rake::TaskLib 14 | def initialize(name = :localer, *args) # rubocop:disable Lint/MissingSuper 15 | @name = name 16 | desc 'Run Localer' 17 | task(name, *args) do |_, _task_args| 18 | sh('localer check') do |ok, res| 19 | exit res.exitstatus unless ok 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /features/global_exclude.feature: -------------------------------------------------------------------------------- 1 | Feature: Localer 2 | Scenario: Exclude strings 3 | Given a real locales 4 | Given a config file with: 5 | """ 6 | Exclude: 7 | - .countries.france 8 | - .population 9 | """ 10 | When I run checker 11 | Then the checker should pass 12 | 13 | Scenario: Exclude regexp 14 | Given a real locales 15 | Given a config file with: 16 | """ 17 | Exclude: 18 | - /population\z/ 19 | """ 20 | When I run checker 21 | Then the checker should pass 22 | 23 | Scenario: Exclude strict regexp 24 | Given a real locales 25 | Given a config file with: 26 | """ 27 | Exclude: 28 | - /^.population/ 29 | """ 30 | When I run checker 31 | Then the checker should returns 1 missing translations: 32 | | en.countries.france.population | 33 | -------------------------------------------------------------------------------- /spec/dummy_app/config/locales/en.rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | { 4 | en: { 5 | number: { 6 | nth: { 7 | ordinals: lambda do |_key, options| 8 | number = options[:number] 9 | case number 10 | when 1 then "st" 11 | when 2 then "nd" 12 | when 3 then "rd" 13 | when 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 then "th" 14 | else 15 | num_modulo = number.to_i.abs % 100 16 | num_modulo %= 10 if num_modulo > 13 17 | case num_modulo 18 | when 1 then "st" 19 | when 2 then "nd" 20 | when 3 then "rd" 21 | else "th" 22 | end 23 | end 24 | end, 25 | 26 | ordinalized: lambda do |_key, options| 27 | number = options[:number] 28 | "#{number}#{ActiveSupport::Inflector.ordinal(number)}" 29 | end 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spec/dummy_app/config/locales/us.rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | { 4 | us: { 5 | number: { 6 | nth: { 7 | ordinals: lambda do |_key, options| 8 | number = options[:number] 9 | case number 10 | when 1 then "st" 11 | when 2 then "nd" 12 | when 3 then "rd" 13 | when 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 then "th" 14 | else 15 | num_modulo = number.to_i.abs % 100 16 | num_modulo %= 10 if num_modulo > 13 17 | case num_modulo 18 | when 1 then "st" 19 | when 2 then "nd" 20 | when 3 then "rd" 21 | else "th" 22 | end 23 | end 24 | end, 25 | 26 | ordinalized: lambda do |_key, options| 27 | number = options[:number] 28 | "#{number}#{ActiveSupport::Inflector.ordinal(number)}" 29 | end 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/localer/data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "data/service" 4 | require_relative "data/checker" 5 | require_relative "data/processor" 6 | require_relative "data/missing_translations" 7 | 8 | module Localer 9 | # Stores translations and provides 10 | # check methods 11 | class Data 12 | extend Dry::Initializer 13 | param :source, default: -> { {} } 14 | param :config, default: -> { Localer.config } 15 | 16 | attr_reader :translations, :locales 17 | 18 | def initialize(*args) 19 | super 20 | @locales, @translations = Processor.call(source, config) 21 | end 22 | 23 | def complete? 24 | Checker.call(self) 25 | end 26 | 27 | def missing_translations 28 | MissingTranslations.call(self) 29 | end 30 | 31 | def each 32 | @translations.each do |key, value| 33 | @locales.each do |locale| 34 | yield locale, key, value[locale] 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Andrey Deryabin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/localer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "thor" 4 | require "irb" 5 | require_relative "../lib/localer" 6 | 7 | module Localer 8 | class CLI < Thor 9 | desc "version", "Print Localer version" 10 | def version 11 | say Localer::VERSION 12 | end 13 | 14 | desc "check [/path/to/rails/application]", "Check missing translations" 15 | def check(app_path = Localer::Config::APP_PATH) 16 | Localer.configure(options.dup.merge(app_path: app_path)) 17 | 18 | connect_to_rails 19 | 20 | if Localer.data.complete? 21 | say "\xE2\x9C\x94 No missing translations found.", :green 22 | else 23 | missing_translations = Localer.data.missing_translations 24 | say "\xE2\x9C\x96 Missing translations found (#{missing_translations.count}):", :red 25 | missing_translations.each do |tr| 26 | say "* #{tr}" 27 | end 28 | 29 | exit 1 30 | end 31 | end 32 | 33 | default_task :check 34 | 35 | private 36 | 37 | def connect_to_rails 38 | return if Localer::Rails.connect! 39 | say "No Rails application found" 40 | exit 1 41 | end 42 | end 43 | end 44 | 45 | 46 | Localer::CLI.start(ARGV) 47 | -------------------------------------------------------------------------------- /features/locale_option.feature: -------------------------------------------------------------------------------- 1 | Feature: Localer 2 | Scenario: Disable en locale 3 | Given a real locales 4 | Given a config file with: 5 | """ 6 | Locale: 7 | en: 8 | Enabled: false 9 | """ 10 | When I run checker 11 | Then the checker should pass 12 | 13 | Scenario: Disable case-insensitive EN locale 14 | Given a real locales 15 | Given a config file with: 16 | """ 17 | Locale: 18 | EN: 19 | Enabled: false 20 | """ 21 | When I run checker 22 | Then the checker should pass 23 | 24 | Scenario: Disable en.population 25 | Given a real locales 26 | Given a config file with: 27 | """ 28 | Locale: 29 | en: 30 | Exclude: 31 | - .population.italy 32 | """ 33 | When I run checker 34 | Then the checker should returns 2 missing translations: 35 | | en.countries.france.population | 36 | | en.population.france | 37 | 38 | Scenario: With empty config file 39 | Given a real locales 40 | Given a config file with: 41 | """ 42 | """ 43 | When I run checker 44 | Then the checker should returns 4 missing translations: 45 | | ru.population.italy | 46 | | us.population.italy | 47 | | en.countries.france.population | 48 | | en.population.france | 49 | -------------------------------------------------------------------------------- /localer.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "localer/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "localer" 8 | spec.version = Localer::VERSION 9 | spec.authors = ["Andrey Deryabin"] 10 | spec.email = ["deriabin@gmail.com"] 11 | 12 | spec.summary = %q{Automatic detecting missing I18n translations tool.} 13 | spec.description = %q{Automatic detecting missing I18n translations tool.} 14 | spec.homepage = "https://github.com/aderyabin/localer" 15 | spec.license = "MIT" 16 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 17 | f.match(%r{^(test|spec|features|gemfiles)/}) 18 | end 19 | spec.bindir = "bin" 20 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 21 | spec.require_paths = ["lib"] 22 | 23 | spec.add_development_dependency "appraisal" 24 | spec.add_development_dependency "bundler", "~> 1.17" 25 | spec.add_development_dependency "rake", ">= 12.3.3" 26 | spec.add_development_dependency "rspec", "~> 3.0" 27 | spec.add_development_dependency "rubocop", "~> 0.50" 28 | spec.add_development_dependency "cucumber" 29 | spec.add_development_dependency "aruba" 30 | 31 | spec.add_dependency "thor", ">= 0.19" 32 | spec.add_dependency "dry-initializer", ">= 2.0" 33 | end 34 | -------------------------------------------------------------------------------- /features/simple.feature: -------------------------------------------------------------------------------- 1 | Feature: Localer 2 | 3 | Scenario: No locales files 4 | When I run checker 5 | Then the checker should pass 6 | 7 | Scenario: Empty en locale 8 | Given a "en" locale file with: 9 | """ 10 | en: 11 | """ 12 | When I run checker 13 | Then the checker should pass 14 | 15 | Scenario: Empty ru locale 16 | Given a "ru" locale file with: 17 | """ 18 | ru: 19 | """ 20 | When I run checker 21 | Then the checker should pass 22 | 23 | Scenario: Complete locales 24 | Given a "en" locale file with: 25 | """ 26 | en: 27 | one: one 28 | """ 29 | Given a "ru" locale file with: 30 | """ 31 | ru: 32 | one: один 33 | """ 34 | Given a "us" locale file with: 35 | """ 36 | us: 37 | one: one 38 | """ 39 | When I run checker 40 | Then the checker should pass 41 | 42 | Scenario: Empty en locale 43 | Given a "ru" locale file with: 44 | """ 45 | ru: 46 | one: один 47 | """ 48 | When I run checker 49 | Then the checker should fail 50 | 51 | Scenario: Incorrect structure 52 | Given a "en" locale file with: 53 | """ 54 | en: 55 | too_long: "Too Long" 56 | """ 57 | Given a "ru" locale file with: 58 | """ 59 | ru: 60 | too_long: 61 | one: слишком большой длины (не может быть больше чем %{count} символ) 62 | other: слишком большой длины (не может быть больше чем %{count} символа) 63 | """ 64 | When I run checker 65 | Then the checker should fail 66 | -------------------------------------------------------------------------------- /lib/localer/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | require_relative '../localer/ext/hash' 5 | require_relative 'config/locale' 6 | 7 | module Localer # :nodoc: 8 | using Localer::Ext::Hash 9 | 10 | # Loads and parse Localer config file `.localer.yml` 11 | class Config 12 | extend Dry::Initializer 13 | 14 | APP_PATH = Dir.pwd 15 | CONFIG_FILENAME = ".localer.yml" 16 | 17 | option :exclude, default: -> { [] } 18 | option :locale, proc { |hash| parse_locales(hash) }, default: -> { Hash.new(Locale.new) } 19 | option :app_path, default: -> { APP_PATH } 20 | 21 | class << self 22 | def load(options = {}) 23 | opts = options.deep_symbolize_keys 24 | app_path = opts.fetch(:app_path, APP_PATH) 25 | file_options = file_config(CONFIG_FILENAME, app_path) 26 | new(file_options.deep_merge(opts).deep_symbolize_keys) 27 | end 28 | 29 | def file_config(filename, path) 30 | filename = File.expand_path(filename, path) 31 | return {} unless File.exist?(filename) 32 | return {} if File.zero?(filename) 33 | 34 | YAML 35 | .load_file(filename) 36 | .deep_downcase_keys 37 | .deep_symbolize_keys 38 | end 39 | 40 | def parse_locales(hash) 41 | hash.each_with_object(Hash.new(Locale.new)) do |(l, v), h| 42 | h[l] = Locale.new(v) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | # Include gemspec and Rakefile 3 | Include: 4 | - "lib/**/*.rb" 5 | - "lib/**/*.rake" 6 | - "spec/**/*.rb" 7 | Exclude: 8 | - "bin/**/*" 9 | - "lib/localer/ext/*.rb" 10 | - "Appraisals" 11 | - "Gemfile" 12 | - "Rakefile" 13 | - "*.gemspec" 14 | - "spec/dummy_app/" 15 | DisplayCopNames: true 16 | NewCops: enable 17 | StyleGuideCopsOnly: false 18 | 19 | Naming/AccessorMethodName: 20 | Enabled: false 21 | 22 | Style/PercentLiteralDelimiters: 23 | Enabled: false 24 | 25 | Style/TrivialAccessors: 26 | Enabled: false 27 | 28 | Style/Documentation: 29 | Exclude: 30 | - "spec/**/*.rb" 31 | 32 | Style/StringLiterals: 33 | Enabled: false 34 | 35 | Style/BlockDelimiters: 36 | Exclude: 37 | - "spec/**/*.rb" 38 | 39 | Style/DoubleNegation: 40 | Enabled: false 41 | 42 | Style/HashEachMethods: 43 | Enabled: true 44 | 45 | Style/HashTransformKeys: 46 | Enabled: true 47 | 48 | Style/HashTransformValues: 49 | Enabled: true 50 | 51 | Layout/SpaceInsideStringInterpolation: 52 | EnforcedStyle: no_space 53 | 54 | Lint/AmbiguousRegexpLiteral: 55 | Enabled: false 56 | 57 | Lint/AmbiguousBlockAssociation: 58 | Enabled: false 59 | 60 | Metrics/MethodLength: 61 | Exclude: 62 | - "spec/**/*.rb" 63 | 64 | Layout/LineLength: 65 | Max: 120 66 | Exclude: 67 | - "spec/**/*.rb" 68 | 69 | Metrics/BlockLength: 70 | Exclude: 71 | - "spec/**/*.rb" 72 | 73 | Security/YAMLLoad: 74 | Enabled: false 75 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | env: 13 | BUNDLE_JOBS: 4 14 | BUNDLE_RETRY: 3 15 | VERIFY_RESERVED: 1 16 | CI: true 17 | CUCUMBER_PUBLISH_QUIET: true 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | ruby: [2.7, 2.6, 2.5, 2.4] 22 | gemfile: [ 23 | 'gemfiles/rails50.gemfile', 24 | 'gemfiles/rails51.gemfile', 25 | 'gemfiles/rails52.gemfile', 26 | 'gemfiles/rails60.gemfile', 27 | 'gemfiles/rails61.gemfile' 28 | ] 29 | exclude: 30 | - ruby: 2.4 31 | gemfile: gemfiles/rails52.gemfile 32 | - ruby: 2.4 33 | gemfile: gemfiles/rails60.gemfile 34 | - ruby: 2.4 35 | gemfile: gemfiles/rails61.gemfile 36 | 37 | 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/cache@v1 41 | with: 42 | path: /home/runner/bundle 43 | key: bundle-${{ matrix.ruby }}-${{ matrix.gemfile }}-${{ hashFiles(matrix.gemfile) }}-${{ hashFiles('**/*.gemspec') }} 44 | restore-keys: | 45 | bundle-${{ matrix.ruby }}-${{ matrix.gemfile }}- 46 | - uses: ruby/setup-ruby@v1 47 | with: 48 | ruby-version: ${{ matrix.ruby }} 49 | - name: Bundle install 50 | run: | 51 | bundle config path /home/runner/bundle 52 | bundle config --global gemfile ${{ matrix.gemfile }} 53 | bundle install 54 | bundle update 55 | - name: Run tests 56 | run: bundle exec rake 57 | -------------------------------------------------------------------------------- /lib/localer/data/processor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../ext/string' 4 | module Localer # :nodoc: 5 | using Localer::Ext::String 6 | 7 | class Data 8 | # Parse translations into hash: 9 | # key: translation key 10 | # value: hash of locale values 11 | class Processor < Service 12 | param :translations 13 | param :config, default: -> { Localer.config } 14 | 15 | attr_reader :data, :locales 16 | 17 | def call 18 | @data = Hash.new { |hsh, key| hsh[key] = {} } 19 | @locales = [] 20 | translations.each do |(locale, translation)| 21 | next unless config.locale[locale.downcase].enabled 22 | 23 | @locales.push locale 24 | prepare(locale, translation) 25 | end 26 | [@locales, @data] 27 | end 28 | 29 | private 30 | 31 | def prepare(locale, translation, prefix = "") 32 | if translation.is_a?(Hash) 33 | translation.each do |(key, value)| 34 | full_key = prefix + ".#{key}" 35 | next if exclude?(full_key, locale) 36 | 37 | prepare(locale, value, full_key) 38 | end 39 | else 40 | # @data[prefix] ||= {} 41 | @data[prefix][locale] = translation 42 | end 43 | end 44 | 45 | def exclude?(key, locale) 46 | (config.exclude + config.locale[locale.downcase].exclude).any? do |pattern| 47 | match?(key, pattern) 48 | end 49 | end 50 | 51 | def match?(key, pattern) 52 | if (regex = pattern.to_regexp) 53 | key =~ regex 54 | else 55 | key.start_with?(pattern) 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /features/step_definitions/additional_cli_steps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Given /^a "(.*)" locale file with:$/ do |locale, file_content| 4 | write_file("#{LOCALE_DIR}/#{locale}.yml", file_content) 5 | end 6 | 7 | Given /^a config file with:$/ do |file_content| 8 | write_file(CONFIG_PATH, file_content) 9 | end 10 | 11 | Given /^a real locales$/ do # rubocop:disable Metrics/BlockLength 12 | steps %{ 13 | Given a "en" locale file with: 14 | """ 15 | en: 16 | population: 17 | italy: 60.6 18 | countries: 19 | italy: 20 | city: Rome 21 | spain: 22 | city: Madrid 23 | france: 24 | city: Paris 25 | """ 26 | Given a "ru" locale file with: 27 | """ 28 | ru: 29 | population: 30 | france: 66.9 31 | countries: 32 | italy: 33 | city: Рим 34 | spain: 35 | city: Мадрид 36 | france: 37 | city: Париж 38 | population: 66.9 39 | """ 40 | Given a "us" locale file with: 41 | """ 42 | us: 43 | population: 44 | france: 66.9 45 | countries: 46 | italy: 47 | city: Rome 48 | spain: 49 | city: Madrid 50 | france: 51 | city: Paris 52 | population: 66.9 53 | """ 54 | } 55 | end 56 | 57 | Then /^the checker should pass$/ do 58 | step 'the output should contain "✔ No missing translations found"' 59 | step 'the exit status should be 0' 60 | end 61 | 62 | Then /^the checker should fail$/ do 63 | step 'the output should contain "✖ Missing translations found"' 64 | step 'the exit status should be 1' 65 | end 66 | 67 | Then /^the checker should returns (.*) missing translations:$/ do |int, translations| 68 | step %{the output should contain "✖ Missing translations found (#{int})"} 69 | translations.raw.each do |tr| 70 | step %{the output should match /^#{Regexp.escape('* ' + tr[0])}$/} 71 | end 72 | 73 | step 'the exit status should be 1' 74 | end 75 | 76 | Then /^the checker should not found rails application$/ do 77 | step 'the output should contain "No Rails application found"' 78 | step 'the exit status should be 1' 79 | end 80 | 81 | When /^I run checker$/ do 82 | if defined?(run) 83 | run("localer check ../../spec/dummy_app") 84 | else 85 | run_command("localer check ../../spec/dummy_app") 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/localer/ext/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Localer 4 | module Ext 5 | # Extend Hash through refinements 6 | # taken from https://github.com/seamusabshere/to_regexp 7 | module String 8 | INLINE_OPTIONS = /[imxnesu]*/ 9 | REGEXP_DELIMITERS = { 10 | '%r{' => '}', 11 | '/' => '/' 12 | }.freeze 13 | 14 | refine ::String do 15 | def literal? 16 | REGEXP_DELIMITERS.none? { |s, e| start_with?(s) && self =~ /#{e}#{INLINE_OPTIONS}\z/ } 17 | end 18 | 19 | def to_regexp(options = {}) 20 | if args = as_regexp(options) 21 | ::Regexp.new(*args) 22 | end 23 | end 24 | 25 | def as_regexp(options = {}) 26 | raise ::ArgumentError, "[to_regexp] Options must be a Hash" unless options.is_a?(::Hash) 27 | str = self 28 | 29 | return if options[:detect] && (str == '') 30 | 31 | if options[:literal] || (options[:detect] && str.literal?) 32 | content = ::Regexp.escape str 33 | elsif delim_set = REGEXP_DELIMITERS.detect { |k, _| str.start_with?(k) } 34 | delim_start, delim_end = delim_set 35 | /\A#{delim_start}(.*)#{delim_end}(#{INLINE_OPTIONS})\z/u =~ str 36 | content = Regexp.last_match(1) 37 | inline_options = Regexp.last_match(2) 38 | return unless content.is_a?(::String) 39 | content.gsub! '\\/', '/' 40 | if inline_options 41 | options[:ignore_case] = true if inline_options.include?('i') 42 | options[:multiline] = true if inline_options.include?('m') 43 | options[:extended] = true if inline_options.include?('x') 44 | # 'n', 'N' = none, 'e', 'E' = EUC, 's', 'S' = SJIS, 'u', 'U' = UTF-8 45 | options[:lang] = inline_options.scan(/[nesu]/i).join.downcase 46 | end 47 | else 48 | return 49 | end 50 | 51 | ignore_case = options[:ignore_case] ? ::Regexp::IGNORECASE : 0 52 | multiline = options[:multiline] ? ::Regexp::MULTILINE : 0 53 | extended = options[:extended] ? ::Regexp::EXTENDED : 0 54 | lang = options[:lang] || '' 55 | lang = lang.delete 'u' if (::RUBY_VERSION > '1.9') && lang.include?('u') 56 | 57 | if lang.empty? 58 | [content, (ignore_case | multiline | extended)] 59 | else 60 | [content, (ignore_case | multiline | extended), lang] 61 | end 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/localer/ext/hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Localer 4 | module Ext 5 | # Extend Hash through refinements 6 | module Hash 7 | refine ::Hash do 8 | # From ActiveSupport http://api.rubyonrails.org/classes/Hash.html#metho 9 | def deep_merge!(other_hash) 10 | other_hash.each_pair do |current_key, other_value| 11 | this_value = self[current_key] 12 | 13 | if this_value.is_a?(::Hash) && other_value.is_a?(::Hash) 14 | this_value.deep_merge!(other_value) 15 | this_value 16 | else 17 | self[current_key] = other_value 18 | end 19 | end 20 | 21 | self 22 | end 23 | 24 | def deep_merge(other_hash, &block) 25 | dup.deep_merge!(other_hash, &block) 26 | end 27 | 28 | def deep_symbolize_keys 29 | deep_transform_keys do |key| 30 | begin 31 | key.to_sym 32 | rescue StandardError 33 | key 34 | end 35 | end 36 | end 37 | 38 | def deep_downcase_keys 39 | deep_transform_keys do |key| 40 | begin 41 | key.downcase 42 | rescue StandardError 43 | key 44 | end 45 | end 46 | end 47 | 48 | def deep_transform_keys(&block) 49 | _deep_transform_keys_in_object(self, &block) 50 | end 51 | 52 | def deep_transform_keys!(&block) 53 | _deep_transform_keys_in_object!(self, &block) 54 | end 55 | 56 | private 57 | 58 | def _deep_transform_keys_in_object!(object, &block) 59 | case object 60 | when ::Hash 61 | object.keys.each do |key| 62 | value = object.delete(key) 63 | object[yield(key)] = _deep_transform_keys_in_object!(value, &block) 64 | end 65 | object 66 | when Array 67 | object.map! { |e| _deep_transform_keys_in_object!(e, &block) } 68 | else 69 | object 70 | end 71 | end 72 | 73 | # support methods for deep transforming nested hashes and arrays 74 | def _deep_transform_keys_in_object(object, &block) 75 | case object 76 | when ::Hash 77 | object.each_with_object({}) do |(key, value), result| 78 | result[yield(key)] = _deep_transform_keys_in_object(value, &block) 79 | end 80 | when Array 81 | object.map { |e| _deep_transform_keys_in_object(e, &block) } 82 | else 83 | object 84 | end 85 | end 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at deriabin@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /spec/dummy_app/config/locales/ru.rails.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | date: 3 | abbr_day_names: 4 | - 5 | abbr_month_names: 6 | - 7 | day_names: 8 | - 9 | month_names: 10 | - 11 | order: 12 | - 13 | formats: 14 | default: "%d.%m.%Y" 15 | long: "%-d %B %Y" 16 | short: "%-d %b" 17 | time: 18 | am: утра 19 | formats: 20 | default: "%a, %d %b %Y, %H:%M:%S %z" 21 | long: "%d %B %Y, %H:%M" 22 | short: "%d %b, %H:%M" 23 | pm: вечера 24 | datetime: 25 | prompts: 26 | day: День 27 | hour: Часов 28 | minute: Минут 29 | month: Месяц 30 | second: Секунд 31 | year: Год 32 | distance_in_words: 33 | about_x_hours: 34 | one: около %{count} часа 35 | other: около %{count} часа 36 | about_x_months: 37 | one: около %{count} месяца 38 | other: около %{count} месяца 39 | about_x_years: 40 | one: около %{count} года 41 | other: около %{count} лет 42 | almost_x_years: 43 | one: почти %{count} год 44 | other: почти %{count} лет 45 | half_a_minute: меньше минуты 46 | less_than_x_minutes: 47 | one: меньше %{count} минуты 48 | other: меньше %{count} минуты 49 | less_than_x_seconds: 50 | one: меньше %{count} секунды 51 | other: меньше %{count} секунды 52 | over_x_years: 53 | one: больше %{count} года 54 | other: больше %{count} лет 55 | x_days: 56 | one: "%{count} день" 57 | other: "%{count} дня" 58 | x_minutes: 59 | one: "%{count} минуту" 60 | other: "%{count} минуты" 61 | x_months: 62 | one: "%{count} месяц" 63 | other: "%{count} месяца" 64 | x_seconds: 65 | one: "%{count} секунду" 66 | other: "%{count} секунды" 67 | number: 68 | precision: 69 | format: 70 | delimiter: '' 71 | format: 72 | delimiter: " " 73 | precision: 3 74 | separator: "," 75 | round_mode: default 76 | significant: false 77 | strip_insignificant_zeros: false 78 | percentage: 79 | format: 80 | delimiter: '' 81 | format: "%n%" 82 | human: 83 | format: 84 | delimiter: '' 85 | precision: 1 86 | significant: false 87 | strip_insignificant_zeros: false 88 | decimal_units: 89 | format: "%n %u" 90 | units: 91 | unit: '' 92 | billion: миллиард 93 | million: миллион 94 | quadrillion: квадриллион 95 | thousand: тысяча 96 | trillion: триллион 97 | storage_units: 98 | format: "%n %u" 99 | units: 100 | byte: 101 | one: байт 102 | other: байта 103 | gb: ГБ 104 | kb: КБ 105 | mb: МБ 106 | tb: ТБ 107 | pb: "" 108 | eb: "" 109 | 110 | currency: 111 | format: 112 | delimiter: " " 113 | format: "%n %u" 114 | precision: 2 115 | separator: "," 116 | significant: false 117 | strip_insignificant_zeros: false 118 | unit: руб. 119 | helpers: 120 | select: 121 | prompt: 'Выберите: ' 122 | submit: 123 | create: Создать %{model} 124 | submit: Сохранить %{model} 125 | update: Сохранить %{model} 126 | support: 127 | array: 128 | last_word_connector: " и " 129 | two_words_connector: " и " 130 | words_connector: ", " 131 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | localer (0.2.0) 5 | dry-initializer (>= 2.0) 6 | thor (>= 0.19) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | activesupport (6.1.4) 12 | concurrent-ruby (~> 1.0, >= 1.0.2) 13 | i18n (>= 1.6, < 2) 14 | minitest (>= 5.1) 15 | tzinfo (~> 2.0) 16 | zeitwerk (~> 2.3) 17 | appraisal (2.4.0) 18 | bundler 19 | rake 20 | thor (>= 0.14.0) 21 | aruba (1.1.2) 22 | bundler (>= 1.17, < 3.0) 23 | childprocess (>= 2.0, < 5.0) 24 | contracts (>= 0.16.0, < 0.18.0) 25 | cucumber (>= 2.4, < 7.0) 26 | rspec-expectations (~> 3.4) 27 | thor (~> 1.0) 28 | ast (2.4.2) 29 | builder (3.2.4) 30 | childprocess (4.1.0) 31 | concurrent-ruby (1.1.9) 32 | contracts (0.16.1) 33 | cucumber (6.1.0) 34 | builder (~> 3.2, >= 3.2.4) 35 | cucumber-core (~> 9.0, >= 9.0.1) 36 | cucumber-create-meta (~> 4.0, >= 4.0.0) 37 | cucumber-cucumber-expressions (~> 12.1, >= 12.1.1) 38 | cucumber-gherkin (~> 18.1, >= 18.1.0) 39 | cucumber-html-formatter (~> 13.0, >= 13.0.0) 40 | cucumber-messages (~> 15.0, >= 15.0.0) 41 | cucumber-wire (~> 5.0, >= 5.0.1) 42 | diff-lcs (~> 1.4, >= 1.4.4) 43 | mime-types (~> 3.3, >= 3.3.1) 44 | multi_test (~> 0.1, >= 0.1.2) 45 | sys-uname (~> 1.2, >= 1.2.2) 46 | cucumber-core (9.0.1) 47 | cucumber-gherkin (~> 18.1, >= 18.1.0) 48 | cucumber-messages (~> 15.0, >= 15.0.0) 49 | cucumber-tag-expressions (~> 3.0, >= 3.0.1) 50 | cucumber-create-meta (4.0.0) 51 | cucumber-messages (~> 15.0, >= 15.0.0) 52 | sys-uname (~> 1.2, >= 1.2.2) 53 | cucumber-cucumber-expressions (12.1.1) 54 | cucumber-gherkin (18.1.1) 55 | cucumber-messages (~> 15.0, >= 15.0.0) 56 | cucumber-html-formatter (13.0.0) 57 | cucumber-messages (~> 15.0, >= 15.0.0) 58 | cucumber-messages (15.0.0) 59 | protobuf-cucumber (~> 3.10, >= 3.10.8) 60 | cucumber-tag-expressions (3.0.1) 61 | cucumber-wire (5.0.1) 62 | cucumber-core (~> 9.0, >= 9.0.1) 63 | cucumber-cucumber-expressions (~> 12.1, >= 12.1.1) 64 | cucumber-messages (~> 15.0, >= 15.0.0) 65 | diff-lcs (1.4.4) 66 | dry-initializer (3.0.4) 67 | ffi (1.15.3) 68 | i18n (1.8.10) 69 | concurrent-ruby (~> 1.0) 70 | middleware (0.1.0) 71 | mime-types (3.3.1) 72 | mime-types-data (~> 3.2015) 73 | mime-types-data (3.2021.0225) 74 | minitest (5.14.4) 75 | multi_test (0.1.2) 76 | parallel (1.20.1) 77 | parser (3.0.1.1) 78 | ast (~> 2.4.1) 79 | protobuf-cucumber (3.10.8) 80 | activesupport (>= 3.2) 81 | middleware 82 | thor 83 | thread_safe 84 | rainbow (3.0.0) 85 | rake (13.0.3) 86 | regexp_parser (2.1.1) 87 | rexml (3.2.5) 88 | rspec (3.10.0) 89 | rspec-core (~> 3.10.0) 90 | rspec-expectations (~> 3.10.0) 91 | rspec-mocks (~> 3.10.0) 92 | rspec-core (3.10.1) 93 | rspec-support (~> 3.10.0) 94 | rspec-expectations (3.10.1) 95 | diff-lcs (>= 1.2.0, < 2.0) 96 | rspec-support (~> 3.10.0) 97 | rspec-mocks (3.10.2) 98 | diff-lcs (>= 1.2.0, < 2.0) 99 | rspec-support (~> 3.10.0) 100 | rspec-support (3.10.2) 101 | rubocop (0.93.1) 102 | parallel (~> 1.10) 103 | parser (>= 2.7.1.5) 104 | rainbow (>= 2.2.2, < 4.0) 105 | regexp_parser (>= 1.8) 106 | rexml 107 | rubocop-ast (>= 0.6.0) 108 | ruby-progressbar (~> 1.7) 109 | unicode-display_width (>= 1.4.0, < 2.0) 110 | rubocop-ast (1.7.0) 111 | parser (>= 3.0.1.1) 112 | ruby-progressbar (1.11.0) 113 | sys-uname (1.2.2) 114 | ffi (~> 1.1) 115 | thor (1.1.0) 116 | thread_safe (0.3.6) 117 | tzinfo (2.0.4) 118 | concurrent-ruby (~> 1.0) 119 | unicode-display_width (1.7.0) 120 | zeitwerk (2.4.2) 121 | 122 | PLATFORMS 123 | ruby 124 | 125 | DEPENDENCIES 126 | appraisal 127 | aruba 128 | bundler (~> 1.17) 129 | cucumber 130 | localer! 131 | rake (>= 12.3.3) 132 | rspec (~> 3.0) 133 | rubocop (~> 0.50) 134 | 135 | BUNDLED WITH 136 | 1.17.3 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
13 |
14 |