Full Code of aderyabin/localer for AI

master 4622d727ab40 cached
51 files
48.9 KB
14.2k tokens
64 symbols
1 requests
Download .txt
Repository: aderyabin/localer
Branch: master
Commit: 4622d727ab40
Files: 51
Total size: 48.9 KB

Directory structure:
gitextract_v_eyauum/

├── .github/
│   └── workflows/
│       ├── bundle-audit.yml
│       ├── rubocop.yml
│       └── test.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Appraisals
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── _config.yml
├── bin/
│   └── localer
├── features/
│   ├── global_exclude.feature
│   ├── locale_option.feature
│   ├── missing_app.feature
│   ├── real_locales.feature
│   ├── simple.feature
│   ├── step_definitions/
│   │   └── additional_cli_steps.rb
│   └── support/
│       └── env.rb
├── gemfiles/
│   ├── rails50.gemfile
│   ├── rails51.gemfile
│   ├── rails52.gemfile
│   ├── rails60.gemfile
│   └── rails61.gemfile
├── lib/
│   ├── localer/
│   │   ├── config/
│   │   │   └── locale.rb
│   │   ├── config.rb
│   │   ├── data/
│   │   │   ├── checker.rb
│   │   │   ├── missing_translations.rb
│   │   │   ├── processor.rb
│   │   │   └── service.rb
│   │   ├── data.rb
│   │   ├── ext/
│   │   │   ├── hash.rb
│   │   │   └── string.rb
│   │   ├── rails.rb
│   │   ├── rake_task.rb
│   │   └── version.rb
│   └── localer.rb
├── localer.gemspec
└── spec/
    ├── dummy_app/
    │   ├── config/
    │   │   ├── application.rb
    │   │   ├── environment.rb
    │   │   └── locales/
    │   │       ├── en.rails.rb
    │   │       ├── en.rails.yml
    │   │       ├── ru.rails.rb
    │   │       ├── ru.rails.yml
    │   │       ├── us.rails.rb
    │   │       └── us.rails.yml
    │   └── config.ru
    ├── localer_spec.rb
    └── spec_helper.rb

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/bundle-audit.yml
================================================
name: Bundle Audit

on:
  push:
    branches:
    - master
  pull_request:
  schedule:
    - cron: "0 0 * * *"

jobs:
  rubocop:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: 2.7
    - name: Patch-level verification for Bundler
      run: |
        gem install bundle-audit
        bundle-audit check --update


================================================
FILE: .github/workflows/rubocop.yml
================================================
name: Lint Ruby

on:
  push:
    branches:
    - master
  pull_request:

jobs:
  rubocop:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: 2.7
    - name: Lint Ruby code with RuboCop
      run: |
        gem install rubocop
        rubocop


================================================
FILE: .github/workflows/test.yml
================================================
name: Build

on:
  push:
    branches:
    - master
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      BUNDLE_JOBS: 4
      BUNDLE_RETRY: 3
      VERIFY_RESERVED: 1
      CI: true
      CUCUMBER_PUBLISH_QUIET: true
    strategy:
      fail-fast: false
      matrix:
        ruby: [2.7, 2.6, 2.5, 2.4]
        gemfile: [
          'gemfiles/rails50.gemfile',
          'gemfiles/rails51.gemfile',
          'gemfiles/rails52.gemfile',
          'gemfiles/rails60.gemfile',
          'gemfiles/rails61.gemfile'
        ]
        exclude:
        - ruby: 2.4
          gemfile: gemfiles/rails52.gemfile
        - ruby: 2.4
          gemfile: gemfiles/rails60.gemfile
        - ruby: 2.4
          gemfile: gemfiles/rails61.gemfile


    steps:
    - uses: actions/checkout@v2
    - uses: actions/cache@v1
      with:
        path: /home/runner/bundle
        key: bundle-${{ matrix.ruby }}-${{ matrix.gemfile }}-${{ hashFiles(matrix.gemfile) }}-${{ hashFiles('**/*.gemspec') }}
        restore-keys: |
          bundle-${{ matrix.ruby }}-${{ matrix.gemfile }}-
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: ${{ matrix.ruby }}
    - name: Bundle install
      run: |
        bundle config path /home/runner/bundle
        bundle config --global gemfile ${{ matrix.gemfile }}
        bundle install
        bundle update
    - name: Run tests
      run: bundle exec rake


================================================
FILE: .gitignore
================================================
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/

Gemfile.lock
erl_crash.dump
.ruby-version
.vscode
spec/dummy_app/log/
vendor/
.devcontainer/


================================================
FILE: .rspec
================================================
--format documentation
--color
--require spec_helper


================================================
FILE: .rubocop.yml
================================================
AllCops:
  # Include gemspec and Rakefile
  Include:
    - "lib/**/*.rb"
    - "lib/**/*.rake"
    - "spec/**/*.rb"
  Exclude:
    - "bin/**/*"
    - "lib/localer/ext/*.rb"
    - "Appraisals"
    - "Gemfile"
    - "Rakefile"
    - "*.gemspec"
    - "spec/dummy_app/"
  DisplayCopNames: true
  NewCops: enable
  StyleGuideCopsOnly: false

Naming/AccessorMethodName:
  Enabled: false

Style/PercentLiteralDelimiters:
  Enabled: false

Style/TrivialAccessors:
  Enabled: false

Style/Documentation:
  Exclude:
    - "spec/**/*.rb"

Style/StringLiterals:
  Enabled: false

Style/BlockDelimiters:
  Exclude:
    - "spec/**/*.rb"

Style/DoubleNegation:
  Enabled: false

Style/HashEachMethods:
  Enabled: true

Style/HashTransformKeys:
  Enabled: true

Style/HashTransformValues:
  Enabled: true

Layout/SpaceInsideStringInterpolation:
  EnforcedStyle: no_space

Lint/AmbiguousRegexpLiteral:
  Enabled: false

Lint/AmbiguousBlockAssociation:
  Enabled: false

Metrics/MethodLength:
  Exclude:
    - "spec/**/*.rb"

Layout/LineLength:
  Max: 120
  Exclude:
    - "spec/**/*.rb"

Metrics/BlockLength:
  Exclude:
    - "spec/**/*.rb"

Security/YAMLLoad:
  Enabled: false


================================================
FILE: Appraisals
================================================
appraise 'rails50' do
  gem 'rails', '~> 5.0'
end

appraise 'rails51' do
  gem 'rails', '~> 5.1'
end

appraise 'rails52' do
  gem 'rails', '~> 5.2'
end

appraise 'rails60' do
  gem 'rails', '~> 6.0'
end

appraise 'rails61' do
  gem 'rails', '~> 6.1'
end


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
  address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at deriabin@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/


================================================
FILE: Gemfile
================================================
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# Specify your gem's dependencies in localer.gemspec
gemspec


================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)

Copyright (c) 2018 Andrey Deryabin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================

<p align="center">
<img title="Localer logo" width="384" height="100" src="https://gist.githubusercontent.com/aderyabin/cb0512cbcd6cb4c79a4d84a4831109a5/raw/localer-logo.png">
</p>

[![Gem Version](https://badge.fury.io/rb/localer.svg)](https://rubygems.org/gems/localer) [![Build Status](https://github.com/aderyabin/localer/workflows/Build/badge.svg)](https://github.com/aderyabin/localer/actions)

Localer is a tool that automatically detects missing I18n translations.

The goal is to preserve the integrity of translations. Localer parses and merges all  application locales’ keys. At the next step, it searches for missing translations among the calculated keys.

<p align="left">
  <img height="500" src="https://gist.githubusercontent.com/aderyabin/cb0512cbcd6cb4c79a4d84a4831109a5/raw/localer2.png">
</p>

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'localer'
```

And then execute:

    $ bundle

Or install it yourself as:

    $ gem install localer

## Usage

At the root directory of a Rails app, run:

    $ localer check .

or for specific Rails path:

    $ localer check /path/to/rails/application

## CI integration

Localer is easy to integrate into your favorite CI workflow:
```yml
# .travis.yml

# other configuration options
script:
  - bundle exec bundle-audit
  - bundle exec rubocop
  - bundle exec rspec
  - bundle exec localer
```

or

```ruby
# Rakefile

# other requirements
require 'localer/rake_task'
Localer::RakeTask.new()

task(:default).clear
task default: [:rubocop, :spec, :localer]
```

## Support

Localer supports
* Ruby: 2.4, 2.5, 2.6, 2.7
* Rails: 5.0, 5.1, 5.2, 6.0, 6.1

## Configuration

The behavior of Localer can be controlled via the `.localer.yml` configuration file. It makes it possible to disable locales and keys. The file can be placed in your project directory.

#### Disable specific locale

By default, Localer enables all locales, but you can disable it:

```yml
Locale:
  EN:
    Enabled: false
```

#### Exclude keys globally
By default, Localer enables all keys, but you can disable keys started with specified string or by regex:

```yml
Exclude:
  - /population\z/
  - .countries.france
```

#### Exclude keys for specific locale
```yml
Locale:
  EN:
    Exclude:
      - /population\z/
      - .countries.france
```

## Using Rake

Localer ships with a rake task. To use Localer's rake task you simply need to require the task file and define a task with it. Below is a rake task that will run `localer`:

```ruby
require 'rubygems'
require 'localer'
require 'localer/rake_task'

Localer::RakeTask.new(:localer)
```

When you now run:

    $ rake -T

you should see

```
rake localer  # Run Localer
```

## Development

After checking out the repo, run `bundle exec appraisal install` to install dependencies for each appraisal. Then, run `bundle exec appraisal rake` to run the tests.

## Built With

* [Thor](https://github.com/erikhuda/thor) - Used for building  command-line interfaces.
* [Appraisal](https://github.com/thoughtbot/appraisal) -  Used for testing against different versions of dependencies
* [Cucumber](https://github.com/cucumber/cucumber) + [Aruba](https://github.com/cucumber/aruba) - Used for testing command-line commands

## Acknowledge
Special thanks to [Roman Shamin](https://www.facebook.com/romanshamin) for the logo.

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/aderyabin/localer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.

## License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

## Code of Conduct

Everyone interacting in the Localer project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/aderyabin/localer/blob/master/CODE_OF_CONDUCT.md).


================================================
FILE: Rakefile
================================================
require "bundler/gem_tasks"
require "rubocop/rake_task"
require 'cucumber/rake/task'

RuboCop::RakeTask.new

Cucumber::Rake::Task.new(:features) do |t|
  t.cucumber_opts = "features --format pretty"
end

task :default do
  if ENV["RUBOCOP"]
    Rake::Task["rubocop"].invoke
  else
    Rake::Task["features"].invoke
  end
end


================================================
FILE: _config.yml
================================================
theme: jekyll-theme-leap-day

================================================
FILE: bin/localer
================================================
#!/usr/bin/env ruby

require "thor"
require "irb"
require_relative "../lib/localer"

module Localer
  class CLI < Thor
    desc "version", "Print Localer version"
    def version
      say Localer::VERSION
    end

    desc "check [/path/to/rails/application]", "Check missing translations"
    def check(app_path = Localer::Config::APP_PATH)
      Localer.configure(options.dup.merge(app_path: app_path))

      connect_to_rails

      if Localer.data.complete?
        say "\xE2\x9C\x94 No missing translations found.", :green
      else
        missing_translations = Localer.data.missing_translations
        say "\xE2\x9C\x96 Missing translations found (#{missing_translations.count}):", :red
        missing_translations.each do |tr|
          say "* #{tr}"
        end

        exit 1
      end
    end

    default_task :check

    private

    def connect_to_rails
      return if Localer::Rails.connect!
      say "No Rails application found"
      exit 1
    end
  end
end


Localer::CLI.start(ARGV)


================================================
FILE: features/global_exclude.feature
================================================
Feature: Localer
Scenario: Exclude strings
  Given a real locales
  Given a config file with:
  """
  Exclude:
    - .countries.france
    - .population
  """
  When I run checker
  Then the checker should pass

Scenario: Exclude regexp
  Given a real locales
  Given a config file with:
  """
  Exclude:
    - /population\z/
  """
  When I run checker
  Then the checker should pass

Scenario: Exclude strict regexp
  Given a real locales
  Given a config file with:
  """
  Exclude:
    - /^.population/
  """
  When I run checker
  Then the checker should returns 1 missing translations:
    | en.countries.france.population |


================================================
FILE: features/locale_option.feature
================================================
Feature: Localer
Scenario: Disable en locale
  Given a real locales
  Given a config file with:
  """
  Locale:
    en:
      Enabled: false
  """
  When I run checker
  Then the checker should pass

Scenario: Disable case-insensitive EN locale
  Given a real locales
  Given a config file with:
  """
  Locale:
    EN:
      Enabled: false
  """
  When I run checker
  Then the checker should pass

Scenario: Disable en.population
  Given a real locales
  Given a config file with:
  """
  Locale:
    en:
      Exclude:
        - .population.italy
  """
  When I run checker
  Then the checker should returns 2 missing translations:
   | en.countries.france.population |
   | en.population.france |

Scenario: With empty config file
  Given a real locales
  Given a config file with:
  """
  """
  When I run checker
  Then the checker should returns 4 missing translations:
   | ru.population.italy |
   | us.population.italy |
   | en.countries.france.population |
   | en.population.france |


================================================
FILE: features/missing_app.feature
================================================
Feature: Localer

Scenario: No rails application
  When I run `localer check`
  Then the checker should not found rails application

Scenario: No rails application at existed paths
  When I run `localer check`
  Then the checker should not found rails application

Scenario: No rails application at not non-existed
  When I run `localer check non-existed_path`
  Then the checker should not found rails application


================================================
FILE: features/real_locales.feature
================================================
Feature: Localer

Scenario: Real locales does not pass
  Given a real locales
  When I run checker
  Then the checker should returns 4 missing translations:
    | ru.population.italy |
    | us.population.italy |
    | en.population.france |
    | en.countries.france.population |

================================================
FILE: features/simple.feature
================================================
Feature: Localer

Scenario: No locales files
  When I run checker
  Then the checker should pass

Scenario: Empty en locale
  Given a "en" locale file with:
  """
  en:
  """
  When I run checker
  Then the checker should pass

Scenario: Empty ru locale
  Given a "ru" locale file with:
  """
  ru:
  """
  When I run checker
  Then the checker should pass

Scenario: Complete locales
  Given a "en" locale file with:
  """
  en:
    one: one
  """
  Given a "ru" locale file with:
  """
  ru:
    one: один
  """
  Given a "us" locale file with:
  """
  us:
    one: one
  """
  When I run checker
  Then the checker should pass

Scenario: Empty en locale
  Given a "ru" locale file with:
  """
  ru:
    one: один
  """
  When I run checker
  Then the checker should fail

Scenario: Incorrect structure
  Given a "en" locale file with:
  """
  en:
    too_long: "Too Long"
  """
  Given a "ru" locale file with:
  """
  ru:
    too_long:
      one: слишком большой длины (не может быть больше чем %{count} символ)
      other: слишком большой длины (не может быть больше чем %{count} символа)
  """
  When I run checker
  Then the checker should fail


================================================
FILE: features/step_definitions/additional_cli_steps.rb
================================================
# frozen_string_literal: true

Given /^a "(.*)" locale file with:$/ do |locale, file_content|
  write_file("#{LOCALE_DIR}/#{locale}.yml", file_content)
end

Given /^a config file with:$/ do |file_content|
  write_file(CONFIG_PATH, file_content)
end

Given /^a real locales$/ do # rubocop:disable Metrics/BlockLength
  steps %{
    Given a "en" locale file with:
    """
    en:
      population:
        italy: 60.6
      countries:
        italy:
          city: Rome
        spain:
          city: Madrid
        france:
          city: Paris
    """
    Given a "ru" locale file with:
    """
    ru:
      population:
        france: 66.9
      countries:
        italy:
          city: Рим
        spain:
          city: Мадрид
        france:
          city: Париж
          population: 66.9
    """
    Given a "us" locale file with:
    """
    us:
      population:
        france: 66.9
      countries:
        italy:
          city: Rome
        spain:
          city: Madrid
        france:
          city: Paris
          population: 66.9
    """
  }
end

Then /^the checker should pass$/ do
  step 'the output should contain "✔ No missing translations found"'
  step 'the exit status should be 0'
end

Then /^the checker should fail$/ do
  step 'the output should contain "✖ Missing translations found"'
  step 'the exit status should be 1'
end

Then /^the checker should returns (.*) missing translations:$/ do |int, translations|
  step %{the output should contain "✖ Missing translations found (#{int})"}
  translations.raw.each do |tr|
    step %{the output should match /^#{Regexp.escape('* ' + tr[0])}$/}
  end

  step 'the exit status should be 1'
end

Then /^the checker should not found rails application$/ do
  step 'the output should contain "No Rails application found"'
  step 'the exit status should be 1'
end

When /^I run checker$/ do
  if defined?(run)
    run("localer check ../../spec/dummy_app")
  else
    run_command("localer check ../../spec/dummy_app")
  end
end


================================================
FILE: features/support/env.rb
================================================
# frozen_string_literal: true

require 'aruba/cucumber'

DUMMY_APP_DIR = "../../spec/dummy_app"
LOCALE_DIR = "#{DUMMY_APP_DIR}/config/locales"
CONFIG_PATH = "#{DUMMY_APP_DIR}/.localer.yml"

After do |_|
  %w[ru en us].each do |locale|
    path = "#{LOCALE_DIR}/#{locale}.yml"
    remove(path) if exist?(path)
  end

  remove(CONFIG_PATH) if exist?(CONFIG_PATH)
end


================================================
FILE: gemfiles/rails50.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "rails", "~> 5.0"

gemspec path: "../"


================================================
FILE: gemfiles/rails51.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "rails", "~> 5.1"

gemspec path: "../"


================================================
FILE: gemfiles/rails52.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "rails", "~> 5.2"

gemspec path: "../"


================================================
FILE: gemfiles/rails60.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "rails", "~> 6.0"

gemspec path: "../"


================================================
FILE: gemfiles/rails61.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "rails", "~> 6.1"

gemspec path: "../"


================================================
FILE: lib/localer/config/locale.rb
================================================
# frozen_string_literal: true

module Localer
  class Config
    # Provide config for locale
    class Locale
      extend Dry::Initializer
      option :exclude, default: -> { [] }
      option :enabled, default: -> { true }
    end
  end
end


================================================
FILE: lib/localer/config.rb
================================================
# frozen_string_literal: true

require 'yaml'
require_relative '../localer/ext/hash'
require_relative 'config/locale'

module Localer # :nodoc:
  using Localer::Ext::Hash

  # Loads and parse Localer config file `.localer.yml`
  class Config
    extend Dry::Initializer

    APP_PATH = Dir.pwd
    CONFIG_FILENAME = ".localer.yml"

    option :exclude, default: -> { [] }
    option :locale, proc { |hash| parse_locales(hash) }, default: -> { Hash.new(Locale.new) }
    option :app_path, default: -> { APP_PATH }

    class << self
      def load(options = {})
        opts = options.deep_symbolize_keys
        app_path = opts.fetch(:app_path, APP_PATH)
        file_options = file_config(CONFIG_FILENAME, app_path)
        new(file_options.deep_merge(opts).deep_symbolize_keys)
      end

      def file_config(filename, path)
        filename = File.expand_path(filename, path)
        return {} unless File.exist?(filename)
        return {} if File.zero?(filename)

        YAML
          .load_file(filename)
          .deep_downcase_keys
          .deep_symbolize_keys
      end

      def parse_locales(hash)
        hash.each_with_object(Hash.new(Locale.new)) do |(l, v), h|
          h[l] = Locale.new(v)
        end
      end
    end
  end
end


================================================
FILE: lib/localer/data/checker.rb
================================================
# frozen_string_literal: true

module Localer
  class Data
    # Check missing translations
    # Returns true if no missing translations found, otherwise false
    class Checker < Service
      param :data

      def call
        data.each do |_locale, _key, value|
          return false if value.nil?
        end
        true
      end
    end
  end
end


================================================
FILE: lib/localer/data/missing_translations.rb
================================================
# frozen_string_literal: true

module Localer
  class Data
    # A service that returns array of missing translations
    class MissingTranslations < Service
      param :data

      def call
        missing = []
        data.each do |locale, key, value|
          missing.push("#{locale}#{key}") if value.nil?
        end
        missing
      end
    end
  end
end


================================================
FILE: lib/localer/data/processor.rb
================================================
# frozen_string_literal: true

require_relative '../ext/string'
module Localer # :nodoc:
  using Localer::Ext::String

  class Data
    # Parse translations into hash:
    # key: translation key
    # value: hash of locale values
    class Processor < Service
      param :translations
      param :config, default: -> { Localer.config }

      attr_reader :data, :locales

      def call
        @data = Hash.new { |hsh, key| hsh[key] = {} }
        @locales = []
        translations.each do |(locale, translation)|
          next unless config.locale[locale.downcase].enabled

          @locales.push locale
          prepare(locale, translation)
        end
        [@locales, @data]
      end

      private

      def prepare(locale, translation, prefix = "")
        if translation.is_a?(Hash)
          translation.each do |(key, value)|
            full_key = prefix + ".#{key}"
            next if exclude?(full_key, locale)

            prepare(locale, value, full_key)
          end
        else
          # @data[prefix] ||= {}
          @data[prefix][locale] = translation
        end
      end

      def exclude?(key, locale)
        (config.exclude + config.locale[locale.downcase].exclude).any? do |pattern|
          match?(key, pattern)
        end
      end

      def match?(key, pattern)
        if (regex = pattern.to_regexp)
          key =~ regex
        else
          key.start_with?(pattern)
        end
      end
    end
  end
end


================================================
FILE: lib/localer/data/service.rb
================================================
# frozen_string_literal: true

require 'dry-initializer'
module Localer
  class Data
    # Core service object
    class Service
      extend Dry::Initializer # use `param` and `option` for dependencies

      class << self
        # Instantiates and calls the service at once
        def call(*args, &block)
          new(*args).call(&block)
        end
      end
    end
  end
end


================================================
FILE: lib/localer/data.rb
================================================
# frozen_string_literal: true

require_relative "data/service"
require_relative "data/checker"
require_relative "data/processor"
require_relative "data/missing_translations"

module Localer
  # Stores translations and provides
  # check methods
  class Data
    extend Dry::Initializer
    param :source, default: -> { {} }
    param :config, default: -> { Localer.config }

    attr_reader :translations, :locales

    def initialize(*args)
      super
      @locales, @translations = Processor.call(source, config)
    end

    def complete?
      Checker.call(self)
    end

    def missing_translations
      MissingTranslations.call(self)
    end

    def each
      @translations.each do |key, value|
        @locales.each do |locale|
          yield locale, key, value[locale]
        end
      end
    end
  end
end


================================================
FILE: lib/localer/ext/hash.rb
================================================
# frozen_string_literal: true

module Localer
  module Ext
    # Extend Hash through refinements
    module Hash
      refine ::Hash do
        # From ActiveSupport http://api.rubyonrails.org/classes/Hash.html#metho
        def deep_merge!(other_hash)
          other_hash.each_pair do |current_key, other_value|
            this_value = self[current_key]

            if this_value.is_a?(::Hash) && other_value.is_a?(::Hash)
              this_value.deep_merge!(other_value)
              this_value
            else
              self[current_key] = other_value
            end
          end

          self
        end

        def deep_merge(other_hash, &block)
          dup.deep_merge!(other_hash, &block)
        end

        def deep_symbolize_keys
          deep_transform_keys do |key|
            begin
              key.to_sym
            rescue StandardError
              key
            end
          end
        end

        def deep_downcase_keys
          deep_transform_keys do |key|
            begin
              key.downcase
            rescue StandardError
              key
            end
          end
        end

        def deep_transform_keys(&block)
          _deep_transform_keys_in_object(self, &block)
        end

        def deep_transform_keys!(&block)
          _deep_transform_keys_in_object!(self, &block)
        end

        private

        def _deep_transform_keys_in_object!(object, &block)
          case object
          when ::Hash
            object.keys.each do |key|
              value = object.delete(key)
              object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
            end
            object
          when Array
            object.map! { |e| _deep_transform_keys_in_object!(e, &block) }
          else
            object
          end
        end

        # support methods for deep transforming nested hashes and arrays
        def _deep_transform_keys_in_object(object, &block)
          case object
          when ::Hash
            object.each_with_object({}) do |(key, value), result|
              result[yield(key)] = _deep_transform_keys_in_object(value, &block)
            end
          when Array
            object.map { |e| _deep_transform_keys_in_object(e, &block) }
          else
            object
          end
        end
      end
    end
  end
end


================================================
FILE: lib/localer/ext/string.rb
================================================
# frozen_string_literal: true

module Localer
  module Ext
    # Extend Hash through refinements
    # taken from https://github.com/seamusabshere/to_regexp
    module String
      INLINE_OPTIONS = /[imxnesu]*/
      REGEXP_DELIMITERS = {
        '%r{' => '}',
        '/' => '/'
      }.freeze

      refine ::String do
        def literal?
          REGEXP_DELIMITERS.none? { |s, e| start_with?(s) && self =~ /#{e}#{INLINE_OPTIONS}\z/ }
        end

        def to_regexp(options = {})
          if args = as_regexp(options)
            ::Regexp.new(*args)
          end
        end

        def as_regexp(options = {})
          raise ::ArgumentError, "[to_regexp] Options must be a Hash" unless options.is_a?(::Hash)
          str = self

          return if options[:detect] && (str == '')

          if options[:literal] || (options[:detect] && str.literal?)
            content = ::Regexp.escape str
          elsif delim_set = REGEXP_DELIMITERS.detect { |k, _| str.start_with?(k) }
            delim_start, delim_end = delim_set
            /\A#{delim_start}(.*)#{delim_end}(#{INLINE_OPTIONS})\z/u =~ str
            content = Regexp.last_match(1)
            inline_options = Regexp.last_match(2)
            return unless content.is_a?(::String)
            content.gsub! '\\/', '/'
            if inline_options
              options[:ignore_case] = true if inline_options.include?('i')
              options[:multiline] = true if inline_options.include?('m')
              options[:extended] = true if inline_options.include?('x')
              # 'n', 'N' = none, 'e', 'E' = EUC, 's', 'S' = SJIS, 'u', 'U' = UTF-8
              options[:lang] = inline_options.scan(/[nesu]/i).join.downcase
            end
          else
            return
          end

          ignore_case = options[:ignore_case] ? ::Regexp::IGNORECASE : 0
          multiline = options[:multiline] ? ::Regexp::MULTILINE : 0
          extended = options[:extended] ? ::Regexp::EXTENDED : 0
          lang = options[:lang] || ''
          lang = lang.delete 'u' if (::RUBY_VERSION > '1.9') && lang.include?('u')

          if lang.empty?
            [content, (ignore_case | multiline | extended)]
          else
            [content, (ignore_case | multiline | extended), lang]
          end
        end
      end
    end
  end
end


================================================
FILE: lib/localer/rails.rb
================================================
# frozen_string_literal: true

module Localer
  module Rails # :nodoc:
    class << self
      def connect!
        require File.expand_path("config/environment", Localer.config.app_path)
        true
      rescue LoadError
        false
      end

      def translations
        return {} unless connect!

        I18n.backend.send(:init_translations)
        I18n.backend.send(:translations)
      end
    end
  end
end


================================================
FILE: lib/localer/rake_task.rb
================================================
# frozen_string_literal: true

require 'rake'
require 'rake/tasklib'
require 'localer'

module Localer
  # Defines a Rake task for running Localer.
  # The simplest use of it goes something like:
  #
  #   Localer::Rakeask.new
  # This will define a task named <tt>localer</tt> described as 'Run Localer'.
  class RakeTask < Rake::TaskLib
    def initialize(name = :localer, *args) # rubocop:disable Lint/MissingSuper
      @name = name
      desc 'Run Localer'
      task(name, *args) do |_, _task_args|
        sh('localer check') do |ok, res|
          exit res.exitstatus unless ok
        end
      end
    end
  end
end


================================================
FILE: lib/localer/version.rb
================================================
# frozen_string_literal: true

module Localer
  VERSION = "0.2.0"
end


================================================
FILE: lib/localer.rb
================================================
# frozen_string_literal: true

require "dry-initializer"
require_relative "localer/version"
require_relative "localer/rails"
require_relative "localer/config"
require_relative "localer/data"

module Localer # :nodoc:
  using Localer::Ext::Hash
  # using Localer::Ext::String

  class << self
    def data
      @data ||= load_data
    end

    def config
      @config ||= configure
    end

    def configure(options = {})
      @config = Config.load(options)
    end

    def load_data(source = Localer::Rails.translations)
      @data = Data.new(source)
    end
  end
end


================================================
FILE: localer.gemspec
================================================

lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "localer/version"

Gem::Specification.new do |spec|
  spec.name          = "localer"
  spec.version       = Localer::VERSION
  spec.authors       = ["Andrey Deryabin"]
  spec.email         = ["deriabin@gmail.com"]

  spec.summary       = %q{Automatic detecting missing I18n translations tool.}
  spec.description   = %q{Automatic detecting missing I18n translations tool.}
  spec.homepage      = "https://github.com/aderyabin/localer"
  spec.license       = "MIT"
  spec.files         = `git ls-files -z`.split("\x0").reject do |f|
    f.match(%r{^(test|spec|features|gemfiles)/})
  end
  spec.bindir        = "bin"
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
  spec.require_paths = ["lib"]

  spec.add_development_dependency "appraisal"
  spec.add_development_dependency "bundler", "~> 1.17"
  spec.add_development_dependency "rake", ">= 12.3.3"
  spec.add_development_dependency "rspec", "~> 3.0"
  spec.add_development_dependency "rubocop", "~> 0.50"
  spec.add_development_dependency "cucumber"
  spec.add_development_dependency "aruba"

  spec.add_dependency "thor", ">= 0.19"
  spec.add_dependency "dry-initializer", ">= 2.0"
end


================================================
FILE: spec/dummy_app/config/application.rb
================================================
# frozen_string_literal: true

require "rails"

class DummyApp < Rails::Application
  config.eager_load = false
end


================================================
FILE: spec/dummy_app/config/environment.rb
================================================
# frozen_string_literal: true

# Load the Rails application.
require File.expand_path('application', __dir__)

# Initialize the Rails application.
Rails.application.initialize!


================================================
FILE: spec/dummy_app/config/locales/en.rails.rb
================================================
# frozen_string_literal: true

{
  en: {
    number: {
      nth: {
        ordinals: lambda do |_key, options|
          number = options[:number]
          case number
          when 1 then "st"
          when 2 then "nd"
          when 3 then "rd"
          when 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 then "th"
          else
            num_modulo = number.to_i.abs % 100
            num_modulo %= 10 if num_modulo > 13
            case num_modulo
            when 1 then "st"
            when 2 then "nd"
            when 3 then "rd"
            else "th"
            end
          end
        end,

        ordinalized: lambda do |_key, options|
          number = options[:number]
          "#{number}#{ActiveSupport::Inflector.ordinal(number)}"
        end
      }
    }
  }
}


================================================
FILE: spec/dummy_app/config/locales/en.rails.yml
================================================
en:
  # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()
  datetime:
    distance_in_words:
      half_a_minute: "half a minute"
      less_than_x_seconds:
        one:   "less than 1 second"
        other: "less than %{count} seconds"
      x_seconds:
        one:   "1 second"
        other: "%{count} seconds"
      less_than_x_minutes:
        one:   "less than a minute"
        other: "less than %{count} minutes"
      x_minutes:
        one:   "1 minute"
        other: "%{count} minutes"
      about_x_hours:
        one:   "about 1 hour"
        other: "about %{count} hours"
      x_days:
        one:   "1 day"
        other: "%{count} days"
      about_x_months:
        one:   "about 1 month"
        other: "about %{count} months"
      x_months:
        one:   "1 month"
        other: "%{count} months"
      about_x_years:
        one:   "about 1 year"
        other: "about %{count} years"
      over_x_years:
        one:   "over 1 year"
        other: "over %{count} years"
      almost_x_years:
        one:   "almost 1 year"
        other: "almost %{count} years"
    prompts:
      year:   "Year"
      month:  "Month"
      day:    "Day"
      hour:   "Hour"
      minute: "Minute"
      second: "Seconds"

  helpers:
    select:
      # Default value for :prompt => true in FormOptionsHelper
      prompt: "Please select"

    # Default translation keys for submit and button FormHelper
    submit:
      create: 'Create %{model}'
      update: 'Update %{model}'
      submit: 'Save %{model}'
  date:
    formats:
      # Use the strftime parameters for formats.
      # When no format has been given, it uses default.
      # You can provide other formats here if you like!
      default: "%Y-%m-%d"
      short: "%b %d"
      long: "%B %d, %Y"

    day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
    abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]

    # Don't forget the nil at the beginning; there's no such thing as a 0th month
    month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
    abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
    # Used in date_select and datetime_select.
    order:
      - year
      - month
      - day

  time:
    formats:
      default: "%a, %d %b %Y %H:%M:%S %z"
      short: "%d %b %H:%M"
      long: "%B %d, %Y %H:%M"
    am: "am"
    pm: "pm"

# Used in array.to_sentence.
  support:
    array:
      words_connector: ", "
      two_words_connector: " and "
      last_word_connector: ", and "
  number:
    # Used in NumberHelper.number_to_delimited()
    # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
    format:
      # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
      separator: "."
      # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three)
      delimiter: ","
      # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
      precision: 3
      # Determine how rounding is performed (see BigDecimal::mode)
      round_mode: default
      # If set to true, precision will mean the number of significant digits instead
      # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2)
      significant: false
      # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2)
      strip_insignificant_zeros: false

    # Used in NumberHelper.number_to_currency()
    currency:
      format:
        # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00)
        format: "%u%n"
        unit: "$"
        # These five are to override number.format and are optional
        separator: "."
        delimiter: ","
        precision: 2
        significant: false
        strip_insignificant_zeros: false

    # Used in NumberHelper.number_to_percentage()
    percentage:
      format:
        # These five are to override number.format and are optional
        # separator:
        delimiter: ""
        # precision:
        # significant: false
        # strip_insignificant_zeros: false
        format: "%n%"

    # Used in NumberHelper.number_to_rounded()
    precision:
      format:
        # These five are to override number.format and are optional
        # separator:
        delimiter: ""
        # precision:
        # significant: false
        # strip_insignificant_zeros: false

    # Used in NumberHelper.number_to_human_size() and NumberHelper.number_to_human()
    human:
      format:
        # These five are to override number.format and are optional
        # separator:
        delimiter: ""
        precision: 3
        significant: true
        strip_insignificant_zeros: true
      # Used in number_to_human_size()
      storage_units:
        # Storage units output formatting.
        # %u is the storage unit, %n is the number (default: 2 MB)
        format: "%n %u"
        units:
          byte:
            one:   "Byte"
            other: "Bytes"
          kb: "KB"
          mb: "MB"
          gb: "GB"
          tb: "TB"
          pb: "PB"
          eb: "EB"
      # Used in NumberHelper.number_to_human()
      decimal_units:
        format: "%n %u"
        # Decimal units output formatting
        # By default we will only quantify some of the exponents
        # but the commented ones might be defined or overridden
        # by the user.
        units:
          # femto: Quadrillionth
          # pico: Trillionth
          # nano: Billionth
          # micro: Millionth
          # mili: Thousandth
          # centi: Hundredth
          # deci: Tenth
          unit: ""
          # ten:
          #   one: Ten
          #   other: Tens
          # hundred: Hundred
          thousand: Thousand
          million: Million
          billion: Billion
          trillion: Trillion
          quadrillion: Quadrillion


================================================
FILE: spec/dummy_app/config/locales/ru.rails.rb
================================================
# frozen_string_literal: true

{
  ru: {
    number: {
      nth: {
        ordinals: ->(_key, _options) {},
        ordinalized: ->(_key, options) {}
      }
    }
  }
}


================================================
FILE: spec/dummy_app/config/locales/ru.rails.yml
================================================
ru:
  date:
    abbr_day_names:
      -
    abbr_month_names:
      -
    day_names:
      -
    month_names:
      -
    order:
      -
    formats:
      default: "%d.%m.%Y"
      long: "%-d %B %Y"
      short: "%-d %b"
  time:
    am: утра
    formats:
      default: "%a, %d %b %Y, %H:%M:%S %z"
      long: "%d %B %Y, %H:%M"
      short: "%d %b, %H:%M"
    pm: вечера
  datetime:
    prompts:
      day: День
      hour: Часов
      minute: Минут
      month: Месяц
      second: Секунд
      year: Год
    distance_in_words:
      about_x_hours:
        one: около %{count} часа
        other: около %{count} часа
      about_x_months:
        one: около %{count} месяца
        other: около %{count} месяца
      about_x_years:
        one: около %{count} года
        other: около %{count} лет
      almost_x_years:
        one: почти %{count} год
        other: почти %{count} лет
      half_a_minute: меньше минуты
      less_than_x_minutes:
        one: меньше %{count} минуты
        other: меньше %{count} минуты
      less_than_x_seconds:
        one: меньше %{count} секунды
        other: меньше %{count} секунды
      over_x_years:
        one: больше %{count} года
        other: больше %{count} лет
      x_days:
        one: "%{count} день"
        other: "%{count} дня"
      x_minutes:
        one: "%{count} минуту"
        other: "%{count} минуты"
      x_months:
        one: "%{count} месяц"
        other: "%{count} месяца"
      x_seconds:
        one: "%{count} секунду"
        other: "%{count} секунды"
  number:
    precision:
      format:
        delimiter: ''
    format:
      delimiter: " "
      precision: 3
      separator: ","
      round_mode: default
      significant: false
      strip_insignificant_zeros: false
    percentage:
      format:
        delimiter: ''
        format: "%n%"
    human:
      format:
        delimiter: ''
        precision: 1
        significant: false
        strip_insignificant_zeros: false
      decimal_units:
        format: "%n %u"
        units:
          unit: ''
          billion: миллиард
          million: миллион
          quadrillion: квадриллион
          thousand: тысяча
          trillion: триллион
      storage_units:
        format: "%n %u"
        units:
          byte:
            one: байт
            other: байта
          gb: ГБ
          kb: КБ
          mb: МБ
          tb: ТБ
          pb: ""
          eb: ""

    currency:
      format:
        delimiter: " "
        format: "%n %u"
        precision: 2
        separator: ","
        significant: false
        strip_insignificant_zeros: false
        unit: руб.
  helpers:
    select:
      prompt: 'Выберите: '
    submit:
      create: Создать %{model}
      submit: Сохранить %{model}
      update: Сохранить %{model}
  support:
    array:
      last_word_connector: " и "
      two_words_connector: " и "
      words_connector: ", "


================================================
FILE: spec/dummy_app/config/locales/us.rails.rb
================================================
# frozen_string_literal: true

{
  us: {
    number: {
      nth: {
        ordinals: lambda do |_key, options|
          number = options[:number]
          case number
          when 1 then "st"
          when 2 then "nd"
          when 3 then "rd"
          when 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 then "th"
          else
            num_modulo = number.to_i.abs % 100
            num_modulo %= 10 if num_modulo > 13
            case num_modulo
            when 1 then "st"
            when 2 then "nd"
            when 3 then "rd"
            else "th"
            end
          end
        end,

        ordinalized: lambda do |_key, options|
          number = options[:number]
          "#{number}#{ActiveSupport::Inflector.ordinal(number)}"
        end
      }
    }
  }
}


================================================
FILE: spec/dummy_app/config/locales/us.rails.yml
================================================
us:
  # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()
  datetime:
    distance_in_words:
      half_a_minute: "half a minute"
      less_than_x_seconds:
        one:   "less than 1 second"
        other: "less than %{count} seconds"
      x_seconds:
        one:   "1 second"
        other: "%{count} seconds"
      less_than_x_minutes:
        one:   "less than a minute"
        other: "less than %{count} minutes"
      x_minutes:
        one:   "1 minute"
        other: "%{count} minutes"
      about_x_hours:
        one:   "about 1 hour"
        other: "about %{count} hours"
      x_days:
        one:   "1 day"
        other: "%{count} days"
      about_x_months:
        one:   "about 1 month"
        other: "about %{count} months"
      x_months:
        one:   "1 month"
        other: "%{count} months"
      about_x_years:
        one:   "about 1 year"
        other: "about %{count} years"
      over_x_years:
        one:   "over 1 year"
        other: "over %{count} years"
      almost_x_years:
        one:   "almost 1 year"
        other: "almost %{count} years"
    prompts:
      year:   "Year"
      month:  "Month"
      day:    "Day"
      hour:   "Hour"
      minute: "Minute"
      second: "Seconds"

  helpers:
    select:
      # Default value for :prompt => true in FormOptionsHelper
      prompt: "Please select"

    # Default translation keys for submit and button FormHelper
    submit:
      create: 'Create %{model}'
      update: 'Update %{model}'
      submit: 'Save %{model}'
  date:
    formats:
      # Use the strftime parameters for formats.
      # When no format has been given, it uses default.
      # You can provide other formats here if you like!
      default: "%Y-%m-%d"
      short: "%b %d"
      long: "%B %d, %Y"

    day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
    abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]

    # Don't forget the nil at the beginning; there's no such thing as a 0th month
    month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
    abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
    # Used in date_select and datetime_select.
    order:
      - year
      - month
      - day

  time:
    formats:
      default: "%a, %d %b %Y %H:%M:%S %z"
      short: "%d %b %H:%M"
      long: "%B %d, %Y %H:%M"
    am: "am"
    pm: "pm"

# Used in array.to_sentence.
  support:
    array:
      words_connector: ", "
      two_words_connector: " and "
      last_word_connector: ", and "
  number:
    # Used in NumberHelper.number_to_delimited()
    # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
    format:
      # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
      separator: "."
      # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three)
      delimiter: ","
      # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
      precision: 3
      round_mode: default
      # If set to true, precision will mean the number of significant digits instead
      # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2)
      significant: false
      # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2)
      strip_insignificant_zeros: false

    # Used in NumberHelper.number_to_currency()
    currency:
      format:
        # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00)
        format: "%u%n"
        unit: "$"
        # These five are to override number.format and are optional
        separator: "."
        delimiter: ","
        precision: 2
        significant: false
        strip_insignificant_zeros: false

    # Used in NumberHelper.number_to_percentage()
    percentage:
      format:
        # These five are to override number.format and are optional
        # separator:
        delimiter: ""
        # precision:
        # significant: false
        # strip_insignificant_zeros: false
        format: "%n%"

    # Used in NumberHelper.number_to_rounded()
    precision:
      format:
        # These five are to override number.format and are optional
        # separator:
        delimiter: ""
        # precision:
        # significant: false
        # strip_insignificant_zeros: false

    # Used in NumberHelper.number_to_human_size() and NumberHelper.number_to_human()
    human:
      format:
        # These five are to override number.format and are optional
        # separator:
        delimiter: ""
        precision: 3
        significant: true
        strip_insignificant_zeros: true
      # Used in number_to_human_size()
      storage_units:
        # Storage units output formatting.
        # %u is the storage unit, %n is the number (default: 2 MB)
        format: "%n %u"
        units:
          byte:
            one:   "Byte"
            other: "Bytes"
          kb: "KB"
          mb: "MB"
          gb: "GB"
          tb: "TB"
          pb: "PB"
          eb: "EB"
      # Used in NumberHelper.number_to_human()
      decimal_units:
        format: "%n %u"
        # Decimal units output formatting
        # By default we will only quantify some of the exponents
        # but the commented ones might be defined or overridden
        # by the user.
        units:
          # femto: Quadrillionth
          # pico: Trillionth
          # nano: Billionth
          # micro: Millionth
          # mili: Thousandth
          # centi: Hundredth
          # deci: Tenth
          unit: ""
          # ten:
          #   one: Ten
          #   other: Tens
          # hundred: Hundred
          thousand: Thousand
          million: Million
          billion: Billion
          trillion: Trillion
          quadrillion: Quadrillion


================================================
FILE: spec/dummy_app/config.ru
================================================
# frozen_string_literal: true

require ::File.expand_path("../config/environment", __FILE__)

Rails.application.eager_load!

run Rails.application


================================================
FILE: spec/localer_spec.rb
================================================
# frozen_string_literal: true

RSpec.describe Localer do
  it "has a version number" do
    expect(Localer::VERSION).not_to be nil
  end
end


================================================
FILE: spec/spec_helper.rb
================================================
# frozen_string_literal: true

require "bundler/setup"
require "localer"

RSpec.configure do |config|
  # Disable RSpec exposing methods globally on `Module` and `main`
  config.disable_monkey_patching!

  config.expect_with :rspec do |c|
    c.syntax = :expect
  end
end
Download .txt
gitextract_v_eyauum/

├── .github/
│   └── workflows/
│       ├── bundle-audit.yml
│       ├── rubocop.yml
│       └── test.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Appraisals
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── _config.yml
├── bin/
│   └── localer
├── features/
│   ├── global_exclude.feature
│   ├── locale_option.feature
│   ├── missing_app.feature
│   ├── real_locales.feature
│   ├── simple.feature
│   ├── step_definitions/
│   │   └── additional_cli_steps.rb
│   └── support/
│       └── env.rb
├── gemfiles/
│   ├── rails50.gemfile
│   ├── rails51.gemfile
│   ├── rails52.gemfile
│   ├── rails60.gemfile
│   └── rails61.gemfile
├── lib/
│   ├── localer/
│   │   ├── config/
│   │   │   └── locale.rb
│   │   ├── config.rb
│   │   ├── data/
│   │   │   ├── checker.rb
│   │   │   ├── missing_translations.rb
│   │   │   ├── processor.rb
│   │   │   └── service.rb
│   │   ├── data.rb
│   │   ├── ext/
│   │   │   ├── hash.rb
│   │   │   └── string.rb
│   │   ├── rails.rb
│   │   ├── rake_task.rb
│   │   └── version.rb
│   └── localer.rb
├── localer.gemspec
└── spec/
    ├── dummy_app/
    │   ├── config/
    │   │   ├── application.rb
    │   │   ├── environment.rb
    │   │   └── locales/
    │   │       ├── en.rails.rb
    │   │       ├── en.rails.yml
    │   │       ├── ru.rails.rb
    │   │       ├── ru.rails.yml
    │   │       ├── us.rails.rb
    │   │       └── us.rails.yml
    │   └── config.ru
    ├── localer_spec.rb
    └── spec_helper.rb
Download .txt
SYMBOL INDEX (64 symbols across 14 files)

FILE: lib/localer.rb
  type Localer (line 9) | module Localer # :nodoc:
    function data (line 14) | def data
    function config (line 18) | def config
    function configure (line 22) | def configure(options = {})
    function load_data (line 26) | def load_data(source = Localer::Rails.translations)

FILE: lib/localer/config.rb
  type Localer (line 7) | module Localer # :nodoc:
    class Config (line 11) | class Config
      method load (line 22) | def load(options = {})
      method file_config (line 29) | def file_config(filename, path)
      method parse_locales (line 40) | def parse_locales(hash)

FILE: lib/localer/config/locale.rb
  type Localer (line 3) | module Localer
    class Config (line 4) | class Config
      class Locale (line 6) | class Locale

FILE: lib/localer/data.rb
  type Localer (line 8) | module Localer
    class Data (line 11) | class Data
      method initialize (line 18) | def initialize(*args)
      method complete? (line 23) | def complete?
      method missing_translations (line 27) | def missing_translations
      method each (line 31) | def each

FILE: lib/localer/data/checker.rb
  type Localer (line 3) | module Localer
    class Data (line 4) | class Data
      class Checker (line 7) | class Checker < Service
        method call (line 10) | def call

FILE: lib/localer/data/missing_translations.rb
  type Localer (line 3) | module Localer
    class Data (line 4) | class Data
      class MissingTranslations (line 6) | class MissingTranslations < Service
        method call (line 9) | def call

FILE: lib/localer/data/processor.rb
  type Localer (line 4) | module Localer # :nodoc:
    class Data (line 7) | class Data
      class Processor (line 11) | class Processor < Service
        method call (line 17) | def call
        method prepare (line 31) | def prepare(locale, translation, prefix = "")
        method exclude? (line 45) | def exclude?(key, locale)
        method match? (line 51) | def match?(key, pattern)

FILE: lib/localer/data/service.rb
  type Localer (line 4) | module Localer
    class Data (line 5) | class Data
      class Service (line 7) | class Service
        method call (line 12) | def call(*args, &block)

FILE: lib/localer/ext/hash.rb
  type Localer (line 3) | module Localer
    type Ext (line 4) | module Ext
      type Hash (line 6) | module Hash
        function deep_merge! (line 9) | def deep_merge!(other_hash)
        function deep_merge (line 24) | def deep_merge(other_hash, &block)
        function deep_symbolize_keys (line 28) | def deep_symbolize_keys
        function deep_downcase_keys (line 38) | def deep_downcase_keys
        function deep_transform_keys (line 48) | def deep_transform_keys(&block)
        function deep_transform_keys! (line 52) | def deep_transform_keys!(&block)
        function _deep_transform_keys_in_object! (line 58) | def _deep_transform_keys_in_object!(object, &block)
        function _deep_transform_keys_in_object (line 74) | def _deep_transform_keys_in_object(object, &block)

FILE: lib/localer/ext/string.rb
  type Localer (line 3) | module Localer
    type Ext (line 4) | module Ext
      type String (line 7) | module String
        function literal? (line 15) | def literal?
        function to_regexp (line 19) | def to_regexp(options = {})
        function as_regexp (line 25) | def as_regexp(options = {})

FILE: lib/localer/rails.rb
  type Localer (line 3) | module Localer
    type Rails (line 4) | module Rails # :nodoc:
      function connect! (line 6) | def connect!
      function translations (line 13) | def translations

FILE: lib/localer/rake_task.rb
  type Localer (line 7) | module Localer
    class RakeTask (line 13) | class RakeTask < Rake::TaskLib
      method initialize (line 14) | def initialize(name = :localer, *args) # rubocop:disable Lint/Missin...

FILE: lib/localer/version.rb
  type Localer (line 3) | module Localer

FILE: spec/dummy_app/config/application.rb
  class DummyApp (line 5) | class DummyApp < Rails::Application
Condensed preview — 51 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (56K chars).
[
  {
    "path": ".github/workflows/bundle-audit.yml",
    "chars": 399,
    "preview": "name: Bundle Audit\n\non:\n  push:\n    branches:\n    - master\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n\njobs:\n  "
  },
  {
    "path": ".github/workflows/rubocop.yml",
    "chars": 326,
    "preview": "name: Lint Ruby\n\non:\n  push:\n    branches:\n    - master\n  pull_request:\n\njobs:\n  rubocop:\n    runs-on: ubuntu-latest\n   "
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1407,
    "preview": "name: Build\n\non:\n  push:\n    branches:\n    - master\n  pull_request:\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    env:\n "
  },
  {
    "path": ".gitignore",
    "chars": 167,
    "preview": "/.bundle/\n/.yardoc\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n\nGemfile.lock\nerl_crash.dump\n.ruby-version\n.vsc"
  },
  {
    "path": ".rspec",
    "chars": 53,
    "preview": "--format documentation\n--color\n--require spec_helper\n"
  },
  {
    "path": ".rubocop.yml",
    "chars": 1162,
    "preview": "AllCops:\n  # Include gemspec and Rakefile\n  Include:\n    - \"lib/**/*.rb\"\n    - \"lib/**/*.rake\"\n    - \"spec/**/*.rb\"\n  Ex"
  },
  {
    "path": "Appraisals",
    "chars": 254,
    "preview": "appraise 'rails50' do\n  gem 'rails', '~> 5.0'\nend\n\nappraise 'rails51' do\n  gem 'rails', '~> 5.1'\nend\n\nappraise 'rails52'"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3226,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "Gemfile",
    "chars": 162,
    "preview": "source \"https://rubygems.org\"\n\ngit_source(:github) {|repo_name| \"https://github.com/#{repo_name}\" }\n\n# Specify your gem'"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1082,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2018 Andrey Deryabin\n\nPermission is hereby granted, free of charge, to any person o"
  },
  {
    "path": "README.md",
    "chars": 4011,
    "preview": "\n<p align=\"center\">\n<img title=\"Localer logo\" width=\"384\" height=\"100\" src=\"https://gist.githubusercontent.com/aderyabin"
  },
  {
    "path": "Rakefile",
    "chars": 325,
    "preview": "require \"bundler/gem_tasks\"\nrequire \"rubocop/rake_task\"\nrequire 'cucumber/rake/task'\n\nRuboCop::RakeTask.new\n\nCucumber::R"
  },
  {
    "path": "_config.yml",
    "chars": 28,
    "preview": "theme: jekyll-theme-leap-day"
  },
  {
    "path": "bin/localer",
    "chars": 1011,
    "preview": "#!/usr/bin/env ruby\n\nrequire \"thor\"\nrequire \"irb\"\nrequire_relative \"../lib/localer\"\n\nmodule Localer\n  class CLI < Thor\n "
  },
  {
    "path": "features/global_exclude.feature",
    "chars": 630,
    "preview": "Feature: Localer\nScenario: Exclude strings\n  Given a real locales\n  Given a config file with:\n  \"\"\"\n  Exclude:\n    - .co"
  },
  {
    "path": "features/locale_option.feature",
    "chars": 997,
    "preview": "Feature: Localer\nScenario: Disable en locale\n  Given a real locales\n  Given a config file with:\n  \"\"\"\n  Locale:\n    en:\n"
  },
  {
    "path": "features/missing_app.feature",
    "chars": 415,
    "preview": "Feature: Localer\n\nScenario: No rails application\n  When I run `localer check`\n  Then the checker should not found rails "
  },
  {
    "path": "features/real_locales.feature",
    "chars": 280,
    "preview": "Feature: Localer\n\nScenario: Real locales does not pass\n  Given a real locales\n  When I run checker\n  Then the checker sh"
  },
  {
    "path": "features/simple.feature",
    "chars": 1153,
    "preview": "Feature: Localer\n\nScenario: No locales files\n  When I run checker\n  Then the checker should pass\n\nScenario: Empty en loc"
  },
  {
    "path": "features/step_definitions/additional_cli_steps.rb",
    "chars": 2001,
    "preview": "# frozen_string_literal: true\n\nGiven /^a \"(.*)\" locale file with:$/ do |locale, file_content|\n  write_file(\"#{LOCALE_DIR"
  },
  {
    "path": "features/support/env.rb",
    "chars": 365,
    "preview": "# frozen_string_literal: true\n\nrequire 'aruba/cucumber'\n\nDUMMY_APP_DIR = \"../../spec/dummy_app\"\nLOCALE_DIR = \"#{DUMMY_AP"
  },
  {
    "path": "gemfiles/rails50.gemfile",
    "chars": 114,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rails\", \"~> 5.0\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/rails51.gemfile",
    "chars": 114,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rails\", \"~> 5.1\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/rails52.gemfile",
    "chars": 114,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rails\", \"~> 5.2\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/rails60.gemfile",
    "chars": 114,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rails\", \"~> 6.0\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/rails61.gemfile",
    "chars": 114,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rails\", \"~> 6.1\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "lib/localer/config/locale.rb",
    "chars": 244,
    "preview": "# frozen_string_literal: true\n\nmodule Localer\n  class Config\n    # Provide config for locale\n    class Locale\n      exte"
  },
  {
    "path": "lib/localer/config.rb",
    "chars": 1255,
    "preview": "# frozen_string_literal: true\n\nrequire 'yaml'\nrequire_relative '../localer/ext/hash'\nrequire_relative 'config/locale'\n\nm"
  },
  {
    "path": "lib/localer/data/checker.rb",
    "chars": 357,
    "preview": "# frozen_string_literal: true\n\nmodule Localer\n  class Data\n    # Check missing translations\n    # Returns true if no mis"
  },
  {
    "path": "lib/localer/data/missing_translations.rb",
    "chars": 367,
    "preview": "# frozen_string_literal: true\n\nmodule Localer\n  class Data\n    # A service that returns array of missing translations\n  "
  },
  {
    "path": "lib/localer/data/processor.rb",
    "chars": 1461,
    "preview": "# frozen_string_literal: true\n\nrequire_relative '../ext/string'\nmodule Localer # :nodoc:\n  using Localer::Ext::String\n\n "
  },
  {
    "path": "lib/localer/data/service.rb",
    "chars": 383,
    "preview": "# frozen_string_literal: true\n\nrequire 'dry-initializer'\nmodule Localer\n  class Data\n    # Core service object\n    class"
  },
  {
    "path": "lib/localer/data.rb",
    "chars": 824,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"data/service\"\nrequire_relative \"data/checker\"\nrequire_relative \"data/pr"
  },
  {
    "path": "lib/localer/ext/hash.rb",
    "chars": 2355,
    "preview": "# frozen_string_literal: true\n\nmodule Localer\n  module Ext\n    # Extend Hash through refinements\n    module Hash\n      r"
  },
  {
    "path": "lib/localer/ext/string.rb",
    "chars": 2315,
    "preview": "# frozen_string_literal: true\n\nmodule Localer\n  module Ext\n    # Extend Hash through refinements\n    # taken from https:"
  },
  {
    "path": "lib/localer/rails.rb",
    "chars": 422,
    "preview": "# frozen_string_literal: true\n\nmodule Localer\n  module Rails # :nodoc:\n    class << self\n      def connect!\n        requ"
  },
  {
    "path": "lib/localer/rake_task.rb",
    "chars": 626,
    "preview": "# frozen_string_literal: true\n\nrequire 'rake'\nrequire 'rake/tasklib'\nrequire 'localer'\n\nmodule Localer\n  # Defines a Rak"
  },
  {
    "path": "lib/localer/version.rb",
    "chars": 70,
    "preview": "# frozen_string_literal: true\n\nmodule Localer\n  VERSION = \"0.2.0\"\nend\n"
  },
  {
    "path": "lib/localer.rb",
    "chars": 575,
    "preview": "# frozen_string_literal: true\n\nrequire \"dry-initializer\"\nrequire_relative \"localer/version\"\nrequire_relative \"localer/ra"
  },
  {
    "path": "localer.gemspec",
    "chars": 1273,
    "preview": "\nlib = File.expand_path(\"../lib\", __FILE__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequire \"localer/ver"
  },
  {
    "path": "spec/dummy_app/config/application.rb",
    "chars": 116,
    "preview": "# frozen_string_literal: true\n\nrequire \"rails\"\n\nclass DummyApp < Rails::Application\n  config.eager_load = false\nend\n"
  },
  {
    "path": "spec/dummy_app/config/environment.rb",
    "chars": 177,
    "preview": "# frozen_string_literal: true\n\n# Load the Rails application.\nrequire File.expand_path('application', __dir__)\n\n# Initial"
  },
  {
    "path": "spec/dummy_app/config/locales/en.rails.rb",
    "chars": 780,
    "preview": "# frozen_string_literal: true\n\n{\n  en: {\n    number: {\n      nth: {\n        ordinals: lambda do |_key, options|\n        "
  },
  {
    "path": "spec/dummy_app/config/locales/en.rails.yml",
    "chars": 6039,
    "preview": "en:\n  # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()\n  datetime:\n    dis"
  },
  {
    "path": "spec/dummy_app/config/locales/ru.rails.rb",
    "chars": 171,
    "preview": "# frozen_string_literal: true\n\n{\n  ru: {\n    number: {\n      nth: {\n        ordinals: ->(_key, _options) {},\n        ord"
  },
  {
    "path": "spec/dummy_app/config/locales/ru.rails.yml",
    "chars": 2899,
    "preview": "ru:\n  date:\n    abbr_day_names:\n      -\n    abbr_month_names:\n      -\n    day_names:\n      -\n    month_names:\n      -\n  "
  },
  {
    "path": "spec/dummy_app/config/locales/us.rails.rb",
    "chars": 780,
    "preview": "# frozen_string_literal: true\n\n{\n  us: {\n    number: {\n      nth: {\n        ordinals: lambda do |_key, options|\n        "
  },
  {
    "path": "spec/dummy_app/config/locales/us.rails.yml",
    "chars": 5972,
    "preview": "us:\n  # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()\n  datetime:\n    dis"
  },
  {
    "path": "spec/dummy_app/config.ru",
    "chars": 147,
    "preview": "# frozen_string_literal: true\n\nrequire ::File.expand_path(\"../config/environment\", __FILE__)\n\nRails.application.eager_lo"
  },
  {
    "path": "spec/localer_spec.rb",
    "chars": 141,
    "preview": "# frozen_string_literal: true\n\nRSpec.describe Localer do\n  it \"has a version number\" do\n    expect(Localer::VERSION).not"
  },
  {
    "path": "spec/spec_helper.rb",
    "chars": 272,
    "preview": "# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nrequire \"localer\"\n\nRSpec.configure do |config|\n  # Disable RSpec "
  }
]

About this extraction

This page contains the full source code of the aderyabin/localer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 51 files (48.9 KB), approximately 14.2k tokens, and a symbol index with 64 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!