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>
[](https://rubygems.org/gems/localer) [](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
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
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.