diff --git a/.jrubyrc b/.jrubyrc
deleted file mode 100644
index 29cae357..00000000
--- a/.jrubyrc
+++ /dev/null
@@ -1,2 +0,0 @@
-compat.version=2.0
-
diff --git a/.travis.yml b/.travis.yml
index 58b8ec9e..ac9ed8fb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,13 @@
language: ruby
rvm:
-- 2.1.3
-- 2.0.0
+- 2.2.1
- 1.9.3
- jruby
-- rbx-2.2.10
+- rbx
+# travis uses old bundler (https://travis-ci.org/glebm/i18n-tasks/jobs/53485782)
+before_install: gem install bundler
+cache: bundler
+script: bundle exec rspec
env:
global:
- TRAVIS=1
diff --git a/CHANGES.md b/CHANGES.md
index 9f20234e..ba7e0978 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,54 @@
+## 0.8.3
+
+* Fix regression: ActiveSupport < 4 support [#143](https://github.com/glebm/i18n-tasks/issues/143).
+
+## 0.8.2
+
+* Fix failure on nil values in the data config [#142](https://github.com/glebm/i18n-tasks/issues/142).
+
+## 0.8.1
+
+* The default config file now excludes `app/assets/images` and `app/assets/fonts`. Add `*.otf` to ignored extensions.
+* If an error message occurs when scanning, the error message now includes the filename [#141](https://github.com/glebm/i18n-tasks/issues/141).
+
+## 0.8.0
+
+* Parse command line arguments with `optparse`. Remove dependency on Slop.
+ Simplified commands DSL: options are mostly passed directly to optparse.
+* `search.relative_roots` default changed from from `%w(app/views)` to
+ `%w(app/views app/controllers app/helpers app/presenters)`.
+* `add-missing` now adds keys detected in source to all locales (previously just base) [#134](https://github.com/glebm/i18n-tasks/issues/134).
+* The default spec template no long requires `spec_helper` by default [Daniel Levenson](https://github.com/dleve123) [#135](https://github.com/glebm/i18n-tasks/pull/135).
+* `search.exclude` now appends to and not overrides the default exclude list. More extensions excluded by default:
+ *.css, *.sass, *.scss, *.less, *.yml, and *.json. [#137](https://github.com/glebm/i18n-tasks/issues/137).
+
+## 0.7.13
+
+* Fix relative keys when controller name consists of more than one word by [Yuji Nakayama](https://github.com/yujinakayama) [#132](https://github.com/glebm/i18n-tasks/pull/132).
+* Support keys with UTF8 word characters in the name. [#133](https://github.com/glebm/i18n-tasks/issues/133).
+* Change missing report column title from "Details" to "Value in other locales or source", display the locale [#130](https://github.com/glebm/i18n-tasks/issues/130).
+
+## 0.7.12
+
+* Handle relative keys in controllers nested in modules by [Alexander Tipugin](https://github.com/atipugin). [#128](https://github.com/glebm/i18n-tasks/issues/128).
+* Only write files that changed [#125](https://github.com/glebm/i18n-tasks/issues/125).
+* Allow `[]` in the non-strict scanner pattern [#127](https://github.com/glebm/i18n-tasks/issues/127).
+
+## 0.7.11
+
+* Set slop dependency to 3.5 to ensure Ruby 1.9 compatibility ([#121](https://github.com/glebm/i18n-tasks/pull/121)).
+ MRI 1.9 EOL is [February 23, 2015](https://www.ruby-lang.org/en/news/2014/01/10/ruby-1-9-3-will-end-on-2015/).
+ We will support 1.9 until rbx and jruby support 2.0.
+
+## 0.7.10
+
+* Support relative keys in controller action with argument
+
+## 0.7.9
+
+* Support relative keys in Rails controller actions by [Jessie A. Young](https://github.com/jessieay). [#46](https://github.com/glebm/i18n-tasks/issues/46).
+* Minor fixes
+
## 0.7.8
* Fix Google Translate issues with non-string keys [#100](https://github.com/glebm/i18n-tasks/pull/100)
diff --git a/Gemfile b/Gemfile
index 36e8c9ef..5f0c4bd7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,8 +3,11 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in i18n-tasks.gemspec
gemspec
-group :development do
- gem 'byebug', platform: :mri_21, require: false
+unless ENV['TRAVIS']
+ group :development do
+ gem 'byebug', platforms: [:mri_21, :mri_22], require: false
+ gem 'rubinius-debugger', platform: :rbx, require: false
+ end
end
gem 'codeclimate-test-reporter', group: :test, require: nil
diff --git a/README.md b/README.md
index ea28dc09..64ab93d9 100644
--- a/README.md
+++ b/README.md
@@ -17,15 +17,15 @@ Thus addressing the two main problems of [i18n gem][i18n-gem] design:
## Installation
-i18n-tasks can be used with any project using [i18n][i18n-gem] (default in Rails), or similar, even if it isn't ruby.
+i18n-tasks can be used with any project using the ruby [i18n gem][i18n-gem] (default in Rails).
-Add it to the Gemfile:
+Add i18n-tasks to the Gemfile:
```ruby
-gem 'i18n-tasks', '~> 0.7.8'
+gem 'i18n-tasks', '~> 0.8.3'
```
-Copy default [configuration file](#configuration) (optional):
+Copy the default [configuration file](#configuration):
```console
$ cp $(i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/
@@ -146,7 +146,7 @@ See the full list of tasks with `i18n-tasks --help`.
✔ Keys relative to the file path they are used in (see [relative roots configuration](#usage-search)) are supported.
-✘ Keys relative to `controller.action_name` in Rails controllers are not supported.
+✔ Keys relative to `controller.action_name` in Rails controllers are supported. The closest `def` name is used.
#### Plural keys
@@ -177,7 +177,7 @@ For now, you can disable dynamic key inference by passing `-s` or `--strict` to
Configuration is read from `config/i18n-tasks.yml` or `config/i18n-tasks.yml.erb`.
Inspect configuration with `i18n-tasks config`.
-Install the default config file with:
+Install the [default config file][config] with:
```console
$ cp $(i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/
@@ -188,60 +188,16 @@ Settings are compatible with Rails by default.
### Locales
By default, `base_locale` is set to `en` and `locales` are inferred from the paths to data files.
-You can override these in the config:
-
-```yaml
-# config/i18n-tasks.yml
-base_locale: en
-locales: [es, fr] # This includes base_locale by default
-```
-
-`internal_locale` controls the language i18n-tasks reports in. Locales available are `en` and `ru` (pull request to add more!).
-
-```yaml
-internal_locale: en
-```
+You can override these in the [config][config].
### Storage
The default data adapter supports YAML and JSON files.
-```yaml
-# config/i18n-tasks.yml
-data:
- # configure YAML / JSON serializer options
- # passed directly to load / dump / parse / serialize.
- yaml:
- write:
- # do not wrap lines at 80 characters (override default)
- line_width: -1
-```
-
#### Multiple locale files
-Use `data` options to work with locale data spread over multiple files.
-
-`data.read` accepts a list of file globs to read from per-locale:
-
-```
-# config/i18n-tasks.yml
-data:
- read:
- # read from namespaced files, e.g. simple_form.en.yml
- - 'config/locales/*.%{locale}.yml'
- # read from a gem (config is parsed with ERB first, then YAML)
- - "<%= %x[bundle show vagrant].chomp %>/templates/locales/%{locale}.yml"
- # default
- - 'config/locales/%{locale}.yml'
-```
-
-#### Key pattern syntax
-
-| syntax | description |
-|:------------:|:----------------------------------------------------------|
-| `*` | matches everything |
-| `:` | matches a single key |
-| `{a, b.c}` | match any in set, can use `:` and `*`, match is captured |
+i18n-tasks can manage multiple translation files and read translations from other gems.
+To find out more the `data` options in the [config][config].
For writing to locale files i18n-tasks provides 2 options.
@@ -266,7 +222,7 @@ data:
Conservative router keeps the keys where they are found, or infers the path from base locale.
If the key is completely new, conservative router will fall back to pattern router behaviour.
-Conservative router is the default router.
+Conservative router is the **default** router.
```
data:
@@ -276,55 +232,26 @@ data:
- 'config/locales/%{locale}.yml'
```
-#### Custom adapters
-
-If you store data somewhere but in the filesystem, e.g. in the database or mongodb, you can implement a custom adapter.
-Implement [a handful of methods][adapter-example], then set `data.adapter` to the class name; the rest of the `data` config is passed to the initializer.
+##### Key pattern syntax
-```yaml
-# config/i18n-tasks.yml
-data:
- # file_system is the default adapter, you can provide a custom class name here:
- adapter: file_system
-```
+A special syntax similar to file glob patterns is used throughout i18n-tasks to match translation keys:
-### Usage search
+| syntax | description |
+|:------------:|:----------------------------------------------------------|
+| `*` | matches everything |
+| `:` | matches a single key |
+| `{a, b.c}` | match any in set, can use `:` and `*`, match is captured |
-Configure usage search in `config/i18n-tasks.yml`:
+#### Custom adapters
-```yaml
-# config/i18n-tasks.yml
-# i18n usage search in source
-search:
- # search these directories (relative to your Rails.root directory, default: 'app/')
- paths:
- - 'app/'
- - 'vendor/'
- # paths for relative key resolution:
- relative_roots:
- # default:
- - app/views
- # add a custom one:
- - app/views-mobile
- # include only files matching this glob pattern (default: blank = include all files)
- include:
- - '*.rb'
- - '*.html.*'
- - '*.text.*'
- # explicitly exclude files (default: exclude common binary files)
- exclude:
- - '*.js'
- # you can override the default key regex pattern:
- pattern: "\\bt[( ]\\s*(:?\".+?\"|:?'.+?'|:\\w+)"
- # comments are ignored by default
- ignore_lines:
- - "^\\s*[#/](?!\\si18n-tasks-use)"
-```
+If you store data somewhere but in the filesystem, e.g. in the database or mongodb, you can implement a custom adapter.
+If you have implemented a custom adapter please share it on [the wiki][wiki].
-It is also possible to use a custom key usage scanner by setting `search.scanner` to a class name.
-See this basic [pattern scanner](/lib/i18n/tasks/scanners/pattern_scanner.rb) for reference.
+### Usage search
+See the `search` section in the [config file][config] for all available configuration options.
+An example of a custom scanner can be found here: https://github.com/glebm/i18n-tasks/issues/138#issuecomment-87255708.
### Fine-tuning
@@ -335,33 +262,8 @@ Add hints to static analysis with magic comment hints (lines starting with `(#|/
User.model_name.human
```
-You can also explicitly ignore keys appearing in locale files:
-
-```yaml
-# config/i18n-tasks.yml
-# do not report these keys as unused
-ignore_unused:
- - category.*.db_name
-
-# do not report these keys as missing (both on blank value and no key)
-ignore_missing:
- - devise.errors.unauthorized # ignore this key
- - pagination.views.* # ignore the whole pattern
- # E.g to ignore all Rails number / currency keys:
- - 'number.{format, percentage.format, precision.format, human.format, currency.format}.{strip_insignificant_zeros,significant,delimiter}'
- - 'time.{pm,am}'
-
-# do not report these keys when they have the same value as the base locale version
-ignore_eq_base:
- all:
- - common.ok
- es,fr:
- - common.brand
-
-# do not report these keys ever
-ignore:
- - kaminari.*
-```
+You can also explicitly ignore keys appearing in locale files via `ignore*` settings.
+See the [config file][config] to find out more.
### Google Translate
@@ -383,7 +285,7 @@ translation:
api_key:
```
-## Interactive Console
+## Interactive console
`i18n-tasks irb` starts an IRB session in i18n-tasks context. Type `guide` for more information.
@@ -395,12 +297,7 @@ Export missing and unused data to XLSX:
$ i18n-tasks xlsx-report
```
-### HTML
-
-While i18n-tasks does not provide an HTML version of the report, you can add [one like this](https://gist.github.com/glebm/bdd3ab6d12d915f0c81b).
-
-
-## Add New Tasks
+## Add new tasks
Tasks that come with the gem are defined in [lib/i18n/tasks/command/commands](lib/i18n/tasks/command/commands).
@@ -430,6 +327,8 @@ Run with:
$ i18n-tasks my-task
```
+See more examples of custom tasks [on the wiki](https://github.com/glebm/i18n-tasks/wiki#custom-tasks).
+
[MIT license]: /LICENSE.txt
[travis]: https://travis-ci.org/glebm/i18n-tasks
[badge-travis]: http://img.shields.io/travis/glebm/i18n-tasks.svg
@@ -439,6 +338,8 @@ $ i18n-tasks my-task
[badge-gemnasium]: https://gemnasium.com/glebm/i18n-tasks.svg
[code-climate]: https://codeclimate.com/github/glebm/i18n-tasks
[badge-code-climate]: http://img.shields.io/codeclimate/github/glebm/i18n-tasks.svg
+[config]: https://github.com/glebm/i18n-tasks/blob/master/templates/config/i18n-tasks.yml
+[wiki]: https://github.com/glebm/i18n-tasks/wiki "i18n-tasks wiki"
[i18n-gem]: https://github.com/svenfuchs/i18n "svenfuchs/i18n on Github"
[screenshot-find]: https://raw.github.com/glebm/i18n-tasks/master/doc/img/i18n-usages.png "i18n-tasks find output screenshot"
[adapter-example]: https://github.com/glebm/i18n-tasks/blob/master/lib/i18n/tasks/data/file_system_base.rb
diff --git a/bin/i18n-tasks b/bin/i18n-tasks
index 471bb404..d7964860 100755
--- a/bin/i18n-tasks
+++ b/bin/i18n-tasks
@@ -8,49 +8,6 @@ if i18n_gem_config.respond_to?(:enforce_available_locales=) && i18n_gem_config.e
i18n_gem_config.enforce_available_locales = true
end
-require 'i18n/tasks'
-require 'i18n/tasks/commands'
-require 'slop'
+require 'i18n/tasks/cli'
-err = proc { |message, exit_code|
- if $stderr.isatty
- $stderr.puts Term::ANSIColor.yellow('i18n-tasks: ' + message)
- else
- $stderr.puts message
- end
- exit exit_code
-}
-
-begin
- ran = false
- commander = ::I18n::Tasks::Commands
- instance = commander.new
- instance.set_internal_locale!
- slop_adapter = ::I18n::Tasks::SlopCommand
- args = ARGV.dup
- args = ['--help'] if args.empty?
- Slop.parse(args, help: true) do
- on('-v', '--version', 'Print the version') {
- puts I18n::Tasks::VERSION
- exit
- }
- commander.cmds.each do |name, attr|
- slop_dsl = slop_adapter.slop_command(name, attr) { |_name, opts|
- begin
- ran = true
- instance.safe_run name, opts
- rescue Errno::EPIPE
- # ignore Errno::EPIPE which is throw when pipe breaks, e.g.:
- # i18n-tasks missing | head
- exit 1
- end
- }
- instance_exec(&slop_dsl)
- end
- end
-rescue Slop::Error => e
- err.call(e.message, 64)
-end
-
-
-err.call("Command unknown: #{args[0]}", 64) if !ran && args[0]
+I18n::Tasks::CLI.start(ARGV)
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 725418ac..7f0e0b41 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -4,86 +4,19 @@
# You can find the default config here: https://github.com/glebm/i18n-tasks/blob/master/templates/config/i18n-tasks.yml
base_locale: en
-## i18n-tasks detects locales automatically from the existing locale files
-## uncomment to set locales explicitly
-# locales: [en, es, fr]
-
-## i18n-tasks report locale, default: en, available: en, ru
internal_locale: en
-# Read and write locale data
data:
- ## by default, translation data are read from the file system, or you can provide a custom data adapter
- # adapter: I18n::Tasks::Data::FileSystem
-
- # Locale files to read from
read:
- config/locales/%{locale}.yml
- # - config/locales/*.%{locale}.yml
- # - config/locales/**/*.%{locale}.yml
-
- # key => file routes, matched top to bottom
write:
- ## E.g., write devise and simple form keys to their respective files
- # - ['{devise, simple_form}.*', 'config/locales/\1.%{locale}.yml']
- # Catch-all
- config/locales/%{locale}.yml
- # `i18n-tasks normalize -p` will force move the keys according to these rules
-
- # YAML / JSON serializer options, passed to load / dump / parse / serialize
yaml:
write:
## do not wrap lines at 80 characters (override default)
line_width: 96
- json:
- write:
- # pretty print JSON
- indent: ' '
- space: ' '
- object_nl: "\n"
- array_nl: "\n"
# Find translate calls
search:
- ## Default scanner finds t() and I18n.t() calls
- # scanner: I18n::Tasks::Scanners::PatternWithScopeScanner
-
- ## Paths to search in, passed to File.find
paths:
- lib/
-
- ## Root for resolving relative keys (default)
- # relative_roots:
- # - app/views
-
- ## File.fnmatch patterns to exclude from search (default)
- # exclude: ["*.jpg", "*.png", "*.gif", "*.svg", "*.ico", "*.eot", "*.ttf", "*.woff", "*.pdf"]
-
- ## Or, File.fnmatch patterns to include
- # include: ["*.rb", "*.html.slim"]
-
-## Google Translate
-# translation:
-# # Get an API key and set billing info at https://code.google.com/apis/console to use Google Translate
-# api_key: "AbC-dEf5"
-
-## Consider these keys not missing
-# ignore_missing:
-# - pagination.views.*
-
-## Consider these keys used
-# ignore_unused:
-# - 'simple_form.{yes,no}'
-# - 'simple_form.{placeholders,hints,labels}.*'
-# - 'simple_form.{error_notification,required}.:'
-
-## Exclude these keys from `i18n-tasks eq-base' report
-# ignore_eq_base:
-# all:
-# - common.ok
-# fr,es:
-# - common.brand
-
-## Exclude these keys from all of the reports
-# ignore:
-# - kaminari.*
diff --git a/config/locales/en.yml b/config/locales/en.yml
index f010fe10..186c7fd4 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -2,23 +2,24 @@
en:
i18n_tasks:
add_missing:
- added: Added %{count} keys
+ added:
+ one: Added %{count} key
+ other: Added %{count} keys
cmd:
args:
- default_all: 'Default: all'
default_text: 'Default: %{value}'
desc:
confirm: Confirm automatically
- data_format: 'Data format: %{valid_text}. %{default_text}.'
+ data_format: 'Data format: %{valid_text}.'
key_pattern: Filter by key pattern (e.g. 'common.*')
key_pattern_to_rename: Full key (pattern) to rename. Required
- locale: 'Locale. Default: base'
- locale_to_translate_from: 'Locale to translate from (default: base)'
- locales_filter: 'Comma-separated list of locale(s) to process. Default: all. Special: base.'
- missing_types: 'Filter by types: %{valid}. Default: all'
+ locale: Locale
+ locale_to_translate_from: Locale to translate from
+ locales_filter: "Locale(s) to process. Special: base"
+ missing_types: 'Filter by types: %{valid}'
new_key_name: New name, interpolates original name as %{key}. Required
nostdin: Do not read from stdin
- out_format: 'Output format: %{valid_text}. %{default_text}.'
+ out_format: 'Output format: %{valid_text}'
pattern_router: 'Use pattern router: keys moved per config data.write'
strict: Do not infer dynamic key usage such as `t("category.\#{category.name}")`
value: 'Value. Interpolates: %{value}, %{human_key}, %{value_or_human_key}'
@@ -52,22 +53,19 @@ en:
- Well done!
- Perfect!
enum_list_opt:
- desc: 'Comma-separated list of: %{valid_text}. %{default_text}'
invalid: "%{invalid} is not in: %{valid}."
enum_opt:
- desc: "%{valid_text}. %{default_text}"
invalid: "%{invalid} is not one of: %{valid}."
errors:
- invalid_format: 'Unknown format %{invalid}. Valid: %{valid}.'
- invalid_locale: Invalid locale %{invalid}
+ invalid_format: 'invalid format: %{invalid}. valid: %{valid}.'
+ invalid_locale: 'invalid locale: %{invalid}'
invalid_missing_type:
- one: 'Unknown type %{invalid}. Valid: %{valid}.'
- other: 'Unknown types: %{invalid}. Valid: %{valid}.'
- pass_forest: Pass locale forest
+ one: 'invalid type: %{invalid}. valid: %{valid}.'
+ other: 'unknown types: %{invalid}. valid: %{valid}.'
+ pass_forest: pass locale forest
common:
base_value: Base Value
continue_q: Continue?
- details: Details
key: Key
locale: Locale
n_more: "%{count} more"
@@ -88,6 +86,7 @@ en:
health:
no_keys_detected: No keys detected. Check data.read in config/i18n-tasks.yml.
missing:
+ details_title: Value in other locales or source
none: No translations are missing.
remove_unused:
confirm:
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index e103e3bd..f11bc988 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -5,7 +5,6 @@ ru:
added: "Добавлены ключи (%{count})"
cmd:
args:
- default_all: "По умолчанию: все"
default_text: "По умолчанию: %{value}"
desc:
confirm: "Подтвердить автоматом"
@@ -53,10 +52,8 @@ ru:
- "Отлично!"
- "Прекрасно!"
enum_list_opt:
- desc: "Разделенных запятыми список: %{valid_text}. %{default_text}"
invalid: "%{invalid} не в: %{valid}."
enum_opt:
- desc: "%{valid_text}. %{default_text}"
invalid: "%{invalid} не является одним из: %{valid}."
errors:
invalid_format: "Неизвестный формат %{invalid}. Форматы: %{valid}."
@@ -68,7 +65,6 @@ ru:
common:
base_value: "Исходное значение"
continue_q: "Продолжить?"
- details: "Детали"
key: "Ключ"
locale: "Язык"
n_more: "ещё %{count}"
@@ -82,13 +78,14 @@ ru:
title: "Данные (%{locales}):"
google_translate:
errors:
- no_api_key: Задайте ключ API Google через переменную окружения GOOGLE_TRANSLATE_API_KEY
- или translation.api_key в config/i18n-tasks.yml. Получите ключ через https://code.google.com/apis/console.
+ no_api_key: "Задайте ключ API Google через переменную окружения GOOGLE_TRANSLATE_API_KEY или
+ translation.api_key в config/i18n-tasks.yml. Получите ключ через https://code.google.com/apis/console."
no_results: Google Translate не дал результатов. Убедитесь в том, что платежная информация
добавлена в https://code.google.com/apis/console.
health:
no_keys_detected: "Ключи не обнаружены. Проверьте data.read в config/i18n-tasks.yml."
missing:
+ details_title: "На других языках или в коде"
none: "Всё переведено."
remove_unused:
confirm:
diff --git a/i18n-tasks.gemspec b/i18n-tasks.gemspec
index be0b72f1..c5fbd50d 100644
--- a/i18n-tasks.gemspec
+++ b/i18n-tasks.gemspec
@@ -39,7 +39,6 @@ TEXT
s.add_dependency 'term-ansicolor'
s.add_dependency 'terminal-table'
s.add_dependency 'highline'
- s.add_dependency 'slop', '>= 3.5.0'
s.add_dependency 'i18n'
s.add_development_dependency 'axlsx', '~> 2.0'
s.add_development_dependency 'bundler', '~> 1.3'
diff --git a/lib/i18n/tasks.rb b/lib/i18n/tasks.rb
index ad473d9d..ed768c85 100644
--- a/lib/i18n/tasks.rb
+++ b/lib/i18n/tasks.rb
@@ -2,11 +2,22 @@
# define all the modules to be able to use ::
module I18n
module Tasks
+ class << self
+ def gem_path
+ File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
+ end
- def self.gem_path
- File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
+ def verbose?
+ @verbose
+ end
+
+ def verbose=(value)
+ @verbose = value
+ end
end
+ @verbose = !!ENV['VERBOSE']
+
module Data
end
end
@@ -15,6 +26,7 @@ module Data
require 'active_support/core_ext/hash'
require 'active_support/core_ext/string'
+require 'active_support/core_ext/array/access'
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/object/try'
require 'active_support/core_ext/object/blank'
diff --git a/lib/i18n/tasks/cli.rb b/lib/i18n/tasks/cli.rb
new file mode 100644
index 00000000..fdae685c
--- /dev/null
+++ b/lib/i18n/tasks/cli.rb
@@ -0,0 +1,198 @@
+require 'i18n/tasks'
+require 'i18n/tasks/commands'
+require 'optparse'
+
+class I18n::Tasks::CLI
+ include ::I18n::Tasks::Logging
+
+ def self.start(argv)
+ new.start(argv)
+ end
+
+ def initialize
+ end
+
+ def start(argv)
+ auto_output_coloring do
+ begin
+ run(argv)
+ rescue OptionParser::ParseError => e
+ error e.message, 64
+ rescue I18n::Tasks::CommandError => e
+ begin
+ error e.message, 78
+ ensure
+ log_verbose e.backtrace * "\n"
+ end
+ rescue Errno::EPIPE
+ # ignore Errno::EPIPE which is throw when pipe breaks, e.g.:
+ # i18n-tasks missing | head
+ exit 1
+ end
+ end
+ rescue ExecutionError => e
+ exit e.exit_code
+ end
+
+ def run(argv)
+ name, *options = parse!(argv.dup)
+ context.run(name, *options)
+ end
+
+ def context
+ @context ||= ::I18n::Tasks::Commands.new.tap(&:set_internal_locale!)
+ end
+
+ def commands
+ @commands ||= ::I18n::Tasks::Commands.cmds.dup.tap do |cmds|
+ # Hash#transform_keys is only available since activesupport v4.0.0
+ cmds.keys.each { |k| cmds[k.to_s.tr('_', '-')] = cmds.delete(k) }
+ end
+ end
+
+ private
+
+ def parse!(argv)
+ command = parse_command! argv
+ options = optparse! command, argv
+ parse_options! options, command, argv
+ [command.tr('-', '_'), options.update(arguments: argv)]
+ end
+
+ def optparse!(command, argv)
+ if command
+ optparse_command!(command, argv)
+ else
+ optparse_no_command!(argv)
+ end
+ end
+
+ def optparse_command!(command, argv)
+ cmd_conf = commands[command]
+ flags = cmd_conf[:args].dup
+ options = {}
+ OptionParser.new("Usage: #{program_name} #{command} [options] #{cmd_conf[:pos]}".strip) do |op|
+ flags.each do |flag|
+ op.on(*optparse_args(flag)) { |v| options[option_name(flag)] = v }
+ end
+ verbose_option op
+ help_option op
+ end.parse!(argv)
+ options
+ end
+
+ def optparse_no_command!(argv)
+ argv << '--help' if argv.empty?
+ OptionParser.new("Usage: #{program_name} [command] [options]") do |op|
+ op.on('-v', '--version', 'Print the version') do
+ puts I18n::Tasks::VERSION
+ exit
+ end
+ help_option op
+ commands_summary op
+ end.parse!(argv)
+ end
+
+ def allow_help_arg_first!(argv)
+ # allow `i18n-tasks --help command` in addition to `i18n-tasks command --help`
+ argv[0], argv[1] = argv[1], argv[0] if %w(-h --help).include?(argv[0]) && argv[1] && !argv[1].start_with?('-')
+ end
+
+ def parse_command!(argv)
+ allow_help_arg_first! argv
+ if argv[0] && !argv[0].start_with?('-')
+ if commands.keys.include?(argv[0])
+ argv.shift
+ else
+ error "unknown command: #{argv[0]}", 64
+ end
+ end
+ end
+
+ def verbose_option(op)
+ op.on('--verbose', 'Verbose output') {
+ ::I18n::Tasks.verbose = true
+ }
+ end
+
+ def help_option(op)
+ op.on('-h', '--help', 'Show this message') do
+ $stderr.puts op
+ exit
+ end
+ end
+
+ # @param [OptionParser] op
+ def commands_summary(op)
+ op.separator ''
+ op.separator 'Available commands:'
+ op.separator ''
+ commands.each do |cmd, cmd_conf|
+ op.separator " #{cmd.ljust(op.summary_width + 1, ' ')}#{try_call cmd_conf[:desc]}"
+ end
+ op.separator ''
+ op.separator 'See `i18n-tasks --help` for more information on a specific command.'
+ end
+
+ def optparse_args(flag)
+ args = flag.dup
+ args.map! { |v| try_call v }
+ conf = args.extract_options!
+ if conf.key?(:default)
+ args[-1] = "#{args[-1]}. #{I18n.t('i18n_tasks.cmd.args.default_text', value: conf[:default])}"
+ end
+ args
+ end
+
+ def parse_options!(options, command, argv)
+ commands[command][:args].each do |flag|
+ name = option_name flag
+ options[name] = parse_option flag, options[name], argv, self.context
+ end
+ end
+
+ def parse_option(flag, val, argv, context)
+ conf = flag.last.is_a?(Hash) ? flag.last : {}
+ if conf[:consume_positional]
+ val = Array(val) + Array(flag.include?(Array) ? argv.flat_map { |x| x.split(',') } : argv)
+ end
+ val = conf[:default] if val.nil? && conf.key?(:default)
+ val = conf[:parser].call(val, context) if conf.key?(:parser)
+ val
+ end
+
+ def option_name(flag)
+ flag.detect { |f| f.start_with?('--') }.sub(/\A--/, '').sub(/[^\-\w].*\z/, '').to_sym
+ end
+
+ def try_call(v)
+ if v.respond_to? :call
+ v.call
+ else
+ v
+ end
+ end
+
+ def error(message, exit_code)
+ log_error message
+ fail ExecutionError.new(message, exit_code)
+ end
+
+ class ExecutionError < Exception
+ attr_reader :exit_code
+
+ def initialize(message, exit_code)
+ super(message)
+ @exit_code = exit_code
+ end
+ end
+
+ def auto_output_coloring(coloring = ENV['I18N_TASKS_COLOR'] || STDOUT.isatty)
+ coloring_was = Term::ANSIColor.coloring?
+ Term::ANSIColor.coloring = coloring
+ yield
+ ensure
+ Term::ANSIColor.coloring = coloring_was
+ end
+
+end
diff --git a/lib/i18n/tasks/command/collection.rb b/lib/i18n/tasks/command/collection.rb
index 2e6e3b95..b284cf5a 100644
--- a/lib/i18n/tasks/command/collection.rb
+++ b/lib/i18n/tasks/command/collection.rb
@@ -1,6 +1,6 @@
require 'i18n/tasks/command/options/common'
require 'i18n/tasks/command/options/locales'
-require 'i18n/tasks/command/options/trees'
+require 'i18n/tasks/command/options/data'
module I18n::Tasks
module Command
@@ -10,7 +10,7 @@ def self.included(base)
include Command::DSL
include Command::Options::Common
include Command::Options::Locales
- include Command::Options::Trees
+ include Command::Options::Data
end
end
end
diff --git a/lib/i18n/tasks/command/commander.rb b/lib/i18n/tasks/command/commander.rb
index a2b8ad6a..dbba92bf 100644
--- a/lib/i18n/tasks/command/commander.rb
+++ b/lib/i18n/tasks/command/commander.rb
@@ -1,5 +1,5 @@
# coding: utf-8
-require 'i18n/tasks/slop_command'
+require 'i18n/tasks/cli'
require 'i18n/tasks/reports/terminal'
require 'i18n/tasks/reports/spreadsheet'
@@ -12,29 +12,13 @@ def initialize(i18n = nil)
@i18n = i18n
end
- def safe_run(name, opts)
- begin
- coloring_was = Term::ANSIColor.coloring?
- Term::ANSIColor.coloring = ENV['I18N_TASKS_COLOR'] || STDOUT.isatty
- run name, opts
- rescue CommandError => e
- log_error e.message
- log_verbose e.backtrace * "\n"
- exit 78
- ensure
- Term::ANSIColor.coloring = coloring_was
- end
- end
-
def run(name, opts = {})
name = name.to_sym
public_name = name.to_s.tr '_', '-'
- SlopCommand.parse_opts! opts, self.class.cmds[name][:opt], self
- if opts.empty?
- log_verbose "run #{public_name} without arguments"
+ log_verbose "task: #{public_name}(#{opts.map { |k, v| "#{k}: #{v.inspect}" } * ', '})"
+ if opts.empty? || method(name).arity.zero?
send name
else
- log_verbose "run #{public_name} with #{opts.map { |k, v| "#{k}=#{v}" } * ' '}"
send name, opts
end
end
@@ -53,15 +37,11 @@ def spreadsheet_report
@spreadsheet_report ||= I18n::Tasks::Reports::Spreadsheet.new(i18n)
end
- def desc(name)
- self.class.cmds.try(:[], name).try(:desc)
- end
-
def i18n
@i18n ||= I18n::Tasks::BaseTask.new
end
- delegate :base_locale, :t, to: :i18n
+ delegate :base_locale, :locales, :t, to: :i18n
end
end
end
diff --git a/lib/i18n/tasks/command/commands/data.rb b/lib/i18n/tasks/command/commands/data.rb
index c6d45517..18058bac 100644
--- a/lib/i18n/tasks/command/commands/data.rb
+++ b/lib/i18n/tasks/command/commands/data.rb
@@ -4,60 +4,58 @@ module Commands
module Data
include Command::Collection
- cmd_opt :pattern_router, {
- short: :p,
- long: :pattern_router,
- desc: t('i18n_tasks.cmd.args.desc.pattern_router'),
- conf: {argument: false, optional: true}
- }
+ arg :pattern_router,
+ '-p',
+ '--pattern_router',
+ t('i18n_tasks.cmd.args.desc.pattern_router')
cmd :normalize,
- args: '[locale ...]',
+ pos: '[locale ...]',
desc: t('i18n_tasks.cmd.desc.normalize'),
- opt: cmd_opts(:locales, :pattern_router)
+ args: [:locales, :pattern_router]
def normalize(opt = {})
i18n.normalize_store! opt[:locales], opt[:pattern_router]
end
cmd :data,
- args: '[locale ...]',
+ pos: '[locale ...]',
desc: t('i18n_tasks.cmd.desc.data'),
- opt: cmd_opts(:locales, :out_format)
+ args: [:locales, :out_format]
def data(opt = {})
print_forest i18n.data_forest(opt[:locales]), opt
end
cmd :data_merge,
- args: '[tree ...]',
+ pos: '[tree ...]',
desc: t('i18n_tasks.cmd.desc.data_merge'),
- opt: cmd_opts(:data_format, :nostdin)
+ args: [:data_format, :nostdin]
def data_merge(opt = {})
- forest = opt_forests_merged_stdin_args!(opt)
+ forest = merge_forests_stdin_and_pos!(opt)
merged = i18n.data.merge!(forest)
print_forest merged, opt
end
cmd :data_write,
- args: '[tree]',
+ pos: '[tree]',
desc: t('i18n_tasks.cmd.desc.data_write'),
- opt: cmd_opts(:data_format, :nostdin)
+ args: [:data_format, :nostdin]
def data_write(opt = {})
- forest = opt_forest_arg_or_stdin!(opt)
+ forest = forest_pos_or_stdin!(opt)
i18n.data.write forest
print_forest forest, opt
end
cmd :data_remove,
- args: '[tree]',
+ pos: '[tree]',
desc: t('i18n_tasks.cmd.desc.data_remove'),
- opt: cmd_opts(:data_format, :nostdin)
+ args: [:data_format, :nostdin]
def data_remove(opt = {})
- removed = i18n.data.remove_by_key!(opt_forest_arg_or_stdin!(opt))
+ removed = i18n.data.remove_by_key!(forest_pos_or_stdin!(opt))
log_stderr 'Removed:'
print_forest removed, opt
end
diff --git a/lib/i18n/tasks/command/commands/eq_base.rb b/lib/i18n/tasks/command/commands/eq_base.rb
index 6cd00a40..30a8d494 100644
--- a/lib/i18n/tasks/command/commands/eq_base.rb
+++ b/lib/i18n/tasks/command/commands/eq_base.rb
@@ -5,9 +5,9 @@ module EqBase
include Command::Collection
cmd :eq_base,
- args: '[locale ...]',
+ pos: '[locale ...]',
desc: t('i18n_tasks.cmd.desc.eq_base'),
- opt: cmd_opts(:locales, :out_format)
+ args: [:locales, :out_format]
def eq_base(opt = {})
print_forest i18n.eq_base_keys(opt), opt, :eq_base_keys
diff --git a/lib/i18n/tasks/command/commands/health.rb b/lib/i18n/tasks/command/commands/health.rb
index 693cfad3..d4cd60ae 100644
--- a/lib/i18n/tasks/command/commands/health.rb
+++ b/lib/i18n/tasks/command/commands/health.rb
@@ -5,9 +5,9 @@ module Health
include Command::Collection
cmd :health,
- args: '[locale ...]',
+ pos: '[locale ...]',
desc: t('i18n_tasks.cmd.desc.health'),
- opt: cmd_opts(:locales, :out_format)
+ args: [:locales, :out_format]
def health(opt = {})
forest = i18n.data_forest(opt[:locales])
diff --git a/lib/i18n/tasks/command/commands/meta.rb b/lib/i18n/tasks/command/commands/meta.rb
index a789db2e..bdce5c94 100644
--- a/lib/i18n/tasks/command/commands/meta.rb
+++ b/lib/i18n/tasks/command/commands/meta.rb
@@ -5,12 +5,12 @@ module Meta
include Command::Collection
cmd :config,
- args: '[section ...]',
+ pos: '[section ...]',
desc: t('i18n_tasks.cmd.desc.config')
def config(opts = {})
cfg = i18n.config_for_inspect
- cfg = cfg.slice(*opts[:arguments]) if opts[:arguments]
+ cfg = cfg.slice(*opts[:arguments]) if opts[:arguments].present?
cfg = cfg.to_yaml
cfg.sub! /\A---\n/, ''
cfg.gsub! /^([^\s-].+?:)/, Term::ANSIColor.cyan(Term::ANSIColor.bold('\1'))
diff --git a/lib/i18n/tasks/command/commands/missing.rb b/lib/i18n/tasks/command/commands/missing.rb
index fbab1ffd..0b0f219d 100644
--- a/lib/i18n/tasks/command/commands/missing.rb
+++ b/lib/i18n/tasks/command/commands/missing.rb
@@ -4,25 +4,30 @@ module Commands
module Missing
include Command::Collection
- enum_opt :missing_types, I18n::Tasks::MissingKeys.missing_keys_types
- cmd_opt :missing_types, enum_list_opt_attr(
- :t, :types=, enum_opt(:missing_types),
- proc { |valid, default| t('i18n_tasks.cmd.args.desc.missing_types', valid: valid, default: default) },
- proc { |invalid, valid| t('i18n_tasks.cmd.errors.invalid_missing_type', invalid: invalid * ', ', valid: valid * ', ', count: invalid.length) })
+ missing_types = I18n::Tasks::MissingKeys.missing_keys_types
+ arg :missing_types,
+ '-t',
+ "--types #{missing_types * ','}",
+ Array,
+ t('i18n_tasks.cmd.args.desc.missing_types', valid: missing_types * ', '),
+ parser: OptionParsers::Enum::ListParser.new(
+ missing_types,
+ proc { |invalid, valid| I18n.t('i18n_tasks.cmd.errors.invalid_missing_type',
+ invalid: invalid * ', ', valid: valid * ', ', count: invalid.length) })
cmd :missing,
- args: '[locale ...]',
+ pos: '[locale ...]',
desc: t('i18n_tasks.cmd.desc.missing'),
- opt: cmd_opts(:locales, :out_format, :missing_types)
+ args: [:locales, :out_format, :missing_types]
def missing(opt = {})
print_forest i18n.missing_keys(opt), opt, :missing_keys
end
cmd :translate_missing,
- args: '[locale ...]',
+ pos: '[locale ...]',
desc: t('i18n_tasks.cmd.desc.translate_missing'),
- opt: cmd_opts(:locales, :locale_to_translate_from) << cmd_opt(:out_format).except(:short)
+ args: [:locales, :locale_to_translate_from, arg(:out_format).from(1)]
def translate_missing(opt = {})
missing = i18n.missing_diff_forest opt[:locales], opt[:from]
@@ -32,17 +37,19 @@ def translate_missing(opt = {})
print_forest translated, opt
end
- DEFAULT_ADD_MISSING_VALUE = '%{value_or_human_key}'
-
cmd :add_missing,
- args: '[locale ...]',
+ pos: '[locale ...]',
desc: t('i18n_tasks.cmd.desc.add_missing'),
- opt: cmd_opts(:locales, :out_format) <<
- cmd_opt(:value).merge(desc: proc { "#{cmd_opt(:value)[:desc].call}. #{t('i18n_tasks.cmd.args.default_text', value: DEFAULT_ADD_MISSING_VALUE)}" })
+ args: [:locales, :out_format, arg(:value) + [{default: '%{value_or_human_key}'}]]
def add_missing(opt = {})
- forest = i18n.missing_keys(opt).set_each_value!(opt[:value] || DEFAULT_ADD_MISSING_VALUE)
+ forest = i18n.missing_keys(opt).set_each_value!(opt[:value])
i18n.data.merge! forest
+ # missing keys detected in the source are only returned in the base locale tree
+ # merge again in case such keys have been added to add them to other locales
+ forest_2 = i18n.missing_keys(opt).set_each_value!(opt[:value])
+ i18n.data.merge! forest_2
+ forest.merge! forest_2
log_stderr t('i18n_tasks.add_missing.added', count: forest.leaves.count)
print_forest forest, opt
end
diff --git a/lib/i18n/tasks/command/commands/tree.rb b/lib/i18n/tasks/command/commands/tree.rb
index df8e7490..efa08da9 100644
--- a/lib/i18n/tasks/command/commands/tree.rb
+++ b/lib/i18n/tasks/command/commands/tree.rb
@@ -5,55 +5,50 @@ module Tree
include Command::Collection
cmd :tree_translate,
- args: '[tree]',
+ pos: '[tree (or stdin)]',
desc: t('i18n_tasks.cmd.desc.tree_translate'),
- opt: cmd_opts(:locale_to_translate_from) << cmd_opt(:data_format).except(:short)
+ args: [:locale_to_translate_from, arg(:data_format).from(1)]
def tree_translate(opts = {})
- forest = opt_forest_arg_or_stdin!(opts)
- from = opts[:from]
- translated = i18n.google_translate_forest(forest, from)
- print_forest translated, opts
+ forest = forest_pos_or_stdin!(opts)
+ print_forest i18n.google_translate_forest(forest, opts[:from]), opts
end
cmd :tree_merge,
- args: '[tree ...]',
+ pos: '[[tree] [tree] ... (or stdin)]',
desc: t('i18n_tasks.cmd.desc.tree_merge'),
- opt: cmd_opts(:data_format, :nostdin)
+ args: [:data_format, :nostdin]
def tree_merge(opts = {})
- print_forest opt_forests_merged_stdin_args!(opts), opts
+ print_forest merge_forests_stdin_and_pos!(opts), opts
end
cmd :tree_filter,
- args: '[pattern] [tree]',
+ pos: '[pattern] [tree (or stdin)]',
desc: t('i18n_tasks.cmd.desc.tree_filter'),
- opt: cmd_opts(:data_format, :pattern)
+ args: [:data_format, :pattern]
- def tree_filter(opt = {})
- pattern = opt_or_arg! :pattern, opt
- forest = opt_forest_arg_or_stdin!(opt)
+ def tree_filter(opts = {})
+ pattern = arg_or_pos! :pattern, opts
+ forest = forest_pos_or_stdin! opts
unless pattern.blank?
pattern_re = i18n.compile_key_pattern(pattern)
forest = forest.select_keys { |full_key, _node| full_key =~ pattern_re }
end
- print_forest forest, opt
+ print_forest forest, opts
end
cmd :tree_rename_key,
- args: ' [tree]',
+ pos: 'KEY_PATTERN NAME [tree (or stdin)]',
desc: t('i18n_tasks.cmd.desc.tree_rename_key'),
- opt: [
- cmd_opt(:pattern).merge(short: :k, long: :key=, desc: proc {
- t('i18n_tasks.cmd.args.desc.key_pattern_to_rename') }),
- cmd_opt(:pattern).merge(short: :n, long: :name=, desc: proc {
- t('i18n_tasks.cmd.args.desc.new_key_name') })
- ] + cmd_opts(:data_format)
+ args: [['-k', '--key KEY_PATTERN', t('i18n_tasks.cmd.args.desc.key_pattern_to_rename')],
+ ['-n', '--name NAME', t('i18n_tasks.cmd.args.desc.new_key_name')],
+ :data_format]
def tree_rename_key(opt = {})
- key = opt_or_arg! :key, opt
- name = opt_or_arg! :name, opt
- forest = opt_forest_arg_or_stdin! opt
+ key = arg_or_pos! :key, opt
+ name = arg_or_pos! :name, opt
+ forest = forest_pos_or_stdin! opt
raise CommandError.new('pass full key to rename (-k, --key)') if key.blank?
raise CommandError.new('pass new name (-n, --name)') if name.blank?
forest.rename_each_key!(key, name)
@@ -61,24 +56,24 @@ def tree_rename_key(opt = {})
end
cmd :tree_subtract,
- args: '[tree A] [tree B ...]',
+ pos: '[[tree] [tree] ... (or stdin)]',
desc: t('i18n_tasks.cmd.desc.tree_subtract'),
- opt: cmd_opts(:data_format, :nostdin)
+ args: [:data_format, :nostdin]
def tree_subtract(opt = {})
- forests = opt_forests_stdin_args! opt, 2
+ forests = forests_stdin_and_pos! opt, 2
forest = forests.reduce(:subtract_by_key) || empty_forest
print_forest forest, opt
end
cmd :tree_set_value,
- args: '[value] [tree]',
+ pos: '[VALUE] [tree (or stdin)]',
desc: t('i18n_tasks.cmd.desc.tree_set_value'),
- opt: cmd_opts(:value, :data_format, :nostdin, :pattern)
+ args: [:value, :data_format, :nostdin, :pattern]
def tree_set_value(opt = {})
- value = opt_or_arg! :value, opt
- forest = opt_forest_arg_or_stdin!(opt)
+ value = arg_or_pos! :value, opt
+ forest = forest_pos_or_stdin!(opt)
key_pattern = opt[:pattern]
raise CommandError.new('pass value (-v, --value)') if value.blank?
forest.set_each_value!(value, key_pattern)
@@ -86,13 +81,13 @@ def tree_set_value(opt = {})
end
cmd :tree_convert,
- args: '',
+ pos: '[tree (or stdin)]',
desc: t('i18n_tasks.cmd.desc.tree_convert'),
- opt: [cmd_opt(:data_format).merge(short: :f, long: :from=),
- cmd_opt(:out_format).merge(short: :t, long: :to=)]
+ args: [arg(:data_format).dup.tap { |a| a[0..1] = ['-f', '--from FORMAT'] },
+ arg(:out_format).dup.tap { |a| a[0..1] = ['-t', '--to FORMAT'] }]
def tree_convert(opt = {})
- forest = opt_forest_arg_or_stdin! opt.merge(format: opt[:from])
+ forest = forest_pos_or_stdin! opt.merge(format: opt[:from])
print_forest forest, opt.merge(format: opt[:to])
end
end
diff --git a/lib/i18n/tasks/command/commands/usages.rb b/lib/i18n/tasks/command/commands/usages.rb
index 3130a13d..dc707b3f 100644
--- a/lib/i18n/tasks/command/commands/usages.rb
+++ b/lib/i18n/tasks/command/commands/usages.rb
@@ -4,16 +4,15 @@ module Commands
module Usages
include Command::Collection
- cmd_opt :strict, {
- short: :s,
- long: :strict,
- desc: t('i18n_tasks.cmd.args.desc.strict')
- }
+ arg :strict,
+ '-s',
+ '--strict',
+ t('i18n_tasks.cmd.args.desc.strict')
cmd :find,
- args: '[pattern]',
+ pos: '[pattern]',
desc: t('i18n_tasks.cmd.desc.find'),
- opt: cmd_opts(:out_format, :pattern)
+ args: [:out_format, :pattern]
def find(opt = {})
opt[:filter] ||= opt.delete(:pattern) || opt[:arguments].try(:first)
@@ -21,18 +20,18 @@ def find(opt = {})
end
cmd :unused,
- args: '[locale ...]',
+ pos: '[locale ...]',
desc: t('i18n_tasks.cmd.desc.unused'),
- opt: cmd_opts(:locales, :out_format, :strict)
+ args: [:locales, :out_format, :strict]
def unused(opt = {})
print_forest i18n.unused_keys(opt), opt, :unused_keys
end
cmd :remove_unused,
- args: '[locale ...]',
+ pos: '[locale ...]',
desc: t('i18n_tasks.cmd.desc.remove_unused'),
- opt: cmd_opts(:locales, :out_format, :strict, :confirm)
+ args: [:locales, :out_format, :strict, :confirm]
def remove_unused(opt = {})
unused_keys = i18n.unused_keys(opt)
diff --git a/lib/i18n/tasks/command/commands/xlsx.rb b/lib/i18n/tasks/command/commands/xlsx.rb
index 5b50b4b3..36601372 100644
--- a/lib/i18n/tasks/command/commands/xlsx.rb
+++ b/lib/i18n/tasks/command/commands/xlsx.rb
@@ -5,10 +5,10 @@ module XLSX
include Command::Collection
cmd :xlsx_report,
- args: '[locale...]',
+ pos: '[locale...]',
desc: t('i18n_tasks.cmd.desc.xlsx_report'),
- opt: [cmd_opt(:locales),
- {short: :p, long: :path=, desc: 'Destination path', conf: {default: 'tmp/i18n-report.xlsx'}}]
+ args: [:locales,
+ ['-p', '--path PATH', 'Destination path', default: 'tmp/i18n-report.xlsx']]
def xlsx_report(opt = {})
begin
diff --git a/lib/i18n/tasks/command/dsl.rb b/lib/i18n/tasks/command/dsl.rb
index 393f4d7e..05ff267e 100644
--- a/lib/i18n/tasks/command/dsl.rb
+++ b/lib/i18n/tasks/command/dsl.rb
@@ -1,7 +1,3 @@
-require 'i18n/tasks/command/dsl/cmd'
-require 'i18n/tasks/command/dsl/cmd_opt'
-require 'i18n/tasks/command/dsl/enum_opt'
-
module I18n::Tasks
module Command
module DSL
@@ -19,9 +15,28 @@ def t(*args)
end
module ClassMethods
- include DSL::Cmd
- include DSL::CmdOpt
- include DSL::EnumOpt
+ def cmd(name, conf = nil)
+ if conf
+ conf = conf.dup
+ conf[:args] = (args = conf[:args]) ? args.map { |arg| Symbol === arg ? arg(arg) : arg } : []
+
+ dsl(:cmds)[name] = conf
+ else
+ dsl(:cmds)[name]
+ end
+ end
+
+ def arg(ref, *args)
+ if args.present?
+ dsl(:args)[ref] = args
+ else
+ dsl(:args)[ref]
+ end
+ end
+
+ def cmds
+ dsl(:cmds)
+ end
def dsl(key)
@dsl[key]
diff --git a/lib/i18n/tasks/command/dsl/cmd.rb b/lib/i18n/tasks/command/dsl/cmd.rb
deleted file mode 100644
index e33ad9c1..00000000
--- a/lib/i18n/tasks/command/dsl/cmd.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module I18n::Tasks
- module Command
- module DSL
- module Cmd
- def cmd(name, args = nil)
- if args
- dsl(:cmds)[name] = args
- else
- dsl(:cmds)[name]
- end
- end
-
- def cmds
- dsl(:cmds)
- end
- end
- end
- end
-end
diff --git a/lib/i18n/tasks/command/dsl/cmd_opt.rb b/lib/i18n/tasks/command/dsl/cmd_opt.rb
deleted file mode 100644
index 45f6a567..00000000
--- a/lib/i18n/tasks/command/dsl/cmd_opt.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module I18n::Tasks
- module Command
- module DSL
- module CmdOpt
- def cmd_opts(*args)
- dsl(:cmd_opts).values_at(*args)
- end
-
- def cmd_opt(arg, opts = nil)
- if opts
- dsl(:cmd_opts)[arg] = opts
- else
- dsl(:cmd_opts)[arg]
- end
- end
- end
- end
- end
-end
diff --git a/lib/i18n/tasks/command/dsl/enum_opt.rb b/lib/i18n/tasks/command/dsl/enum_opt.rb
deleted file mode 100644
index a3e9587e..00000000
--- a/lib/i18n/tasks/command/dsl/enum_opt.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-module I18n::Tasks
- module Command
- module DSL
- module EnumOpt
- def enum_opt(name, list = nil)
- if list
- dsl(:enum_valid)[name] = list
- else
- dsl(:enum_valid)[name]
- end
- end
-
- DEFAULT_ENUM_OPT_DESC = proc { |valid, default|
- I18n.t('i18n_tasks.cmd.enum_opt.desc', valid_text: valid, default_text: default)
- }
-
- def enum_opt_attr(short, long, valid, desc, error_msg)
- desc ||= DEFAULT_ENUM_OPT_DESC
- desc_proc = proc { desc.call(valid * ', ', I18n.t('i18n_tasks.cmd.args.default_text', value: valid.first)) }
- {short: short, long: long, desc: desc_proc,
- conf: {default: valid.first, argument: true, optional: false},
- parse: enum_parse_proc(:parse_enum_opt, valid, &error_msg)}
- end
-
- DEFAULT_LIST_OPT_DESC = proc { |valid, default|
- I18n.t('i18n_tasks.cmd.enum_list_opt.desc', valid_text: valid, default_text: default)
- }
-
- def enum_list_opt_attr(short, long, valid, desc, error_msg)
- desc ||= DEFAULT_LIST_OPT_DESC
- desc_proc = proc { desc.call(valid * ', ', I18n.t('i18n_tasks.cmd.args.default_all')) }
- {short: short, long: long, desc: desc_proc,
- conf: {as: Array, delimiter: /\s*[+:,]\s*/},
- parse: enum_parse_proc(:parse_enum_list_opt, valid, &error_msg)}
- end
-
- def enum_parse_proc(method, valid, &error)
- proc { |opt, key|
- opt[key] = send(method, opt[key], valid, &error)
- }
- end
- end
- end
- end
-end
diff --git a/lib/i18n/tasks/command/option_parsers/enum.rb b/lib/i18n/tasks/command/option_parsers/enum.rb
new file mode 100644
index 00000000..b53d86ec
--- /dev/null
+++ b/lib/i18n/tasks/command/option_parsers/enum.rb
@@ -0,0 +1,53 @@
+module I18n::Tasks
+ module Command
+ module OptionParsers
+ module Enum
+ class Parser
+ DEFAULT_ERROR = proc { |invalid, valid|
+ I18n.t('i18n_tasks.cmd.enum_opt.invalid', invalid: invalid, valid: valid * ', ')
+ }
+
+ def initialize(valid, error_message = DEFAULT_ERROR)
+ @valid = valid.map(&:to_s)
+ @error_message = error_message
+ end
+
+ def call(value, *)
+ return @valid.first unless value.present?
+ if @valid.include?(value)
+ value
+ else
+ raise CommandError.new @error_message.call(value, @valid)
+ end
+ end
+ end
+
+ class ListParser
+ DEFAULT_ERROR = proc { |invalid, valid|
+ I18n.t('i18n_tasks.cmd.enum_list_opt.invalid', invalid: invalid * ', ', valid: valid * ', ')
+ }
+
+ def initialize(valid, error_message = DEFAULT_ERROR)
+ @valid = valid.map(&:to_s)
+ @error_message = error_message
+ end
+
+ def call(values, *)
+ values = Array(values)
+ return @valid if values == %w(all)
+ invalid = values - @valid
+ if invalid.empty?
+ if values.empty?
+ @valid
+ else
+ values
+ end
+ else
+ raise CommandError.new @error_message.call(invalid, @valid)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/i18n/tasks/command/option_parsers/locale.rb b/lib/i18n/tasks/command/option_parsers/locale.rb
new file mode 100644
index 00000000..ad132746
--- /dev/null
+++ b/lib/i18n/tasks/command/option_parsers/locale.rb
@@ -0,0 +1,48 @@
+module I18n::Tasks
+ module Command
+ module OptionParsers
+ module Locale
+ module Validator
+ VALID_LOCALE_RE = /\A\w[\w\-\.]*\z/i
+
+ def validate!(locale)
+ if VALID_LOCALE_RE !~ locale
+ raise CommandError.new(I18n.t('i18n_tasks.cmd.errors.invalid_locale', invalid: locale))
+ end
+ locale
+ end
+ end
+
+ module Parser
+ module_function
+ extend Validator
+
+ # @param [#base_locale, #locales] context
+ def call(val, context)
+ if val.blank? || val == 'base'
+ context.base_locale
+ else
+ validate! val
+ end
+ end
+ end
+
+ module ListParser
+ module_function
+ extend Validator
+
+ # @param [#base_locale,#locales] context
+ def call(vals, context)
+ if vals == %w(all) || vals.blank?
+ context.locales
+ else
+ vals.map { |v| v == 'base' ? context.base_locale : v }
+ end.tap do |locales|
+ locales.each { |locale| validate! locale }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/i18n/tasks/command/options/common.rb b/lib/i18n/tasks/command/options/common.rb
index 8781e109..57c41c98 100644
--- a/lib/i18n/tasks/command/options/common.rb
+++ b/lib/i18n/tasks/command/options/common.rb
@@ -1,46 +1,36 @@
-require 'i18n/tasks/command/options/enum_opt'
-require 'i18n/tasks/command/options/list_opt'
module I18n::Tasks
module Command
module Options
module Common
include Command::DSL
- include Options::EnumOpt
- include Options::ListOpt
- VALID_LOCALE_RE = /\A\w[\w\-\.]*\z/i
+ arg :nostdin,
+ '-S',
+ '--nostdin',
+ t('i18n_tasks.cmd.args.desc.nostdin')
- cmd_opt :nostdin, {
- short: :S,
- long: :nostdin,
- desc: t('i18n_tasks.cmd.args.desc.nostdin'),
- conf: {default: false}
- }
+ arg :confirm,
+ '-y',
+ '--confirm',
+ desc: t('i18n_tasks.cmd.args.desc.confirm')
- cmd_opt :confirm, {
- short: :y,
- long: :confirm,
- desc: t('i18n_tasks.cmd.args.desc.confirm'),
- conf: {default: false}
- }
+ arg :pattern,
+ '-p',
+ '--pattern PATTERN',
+ t('i18n_tasks.cmd.args.desc.key_pattern')
- cmd_opt :pattern, {
- short: :p,
- long: :pattern=,
- desc: t('i18n_tasks.cmd.args.desc.key_pattern'),
- conf: {argument: true, optional: false}
- }
+ arg :value,
+ '-v',
+ '--value VALUE',
+ t('i18n_tasks.cmd.args.desc.value')
- cmd_opt :value, {
- short: :v,
- long: :value=,
- desc: t('i18n_tasks.cmd.args.desc.value'),
- conf: {argument: true, optional: false}
- }
+ def arg_or_pos!(key, opts)
+ opts[key] ||= opts[:arguments].try(:shift)
+ end
- def opt_or_arg!(key, opt)
- opt[key] ||= opt[:arguments].try(:shift)
+ def pos_or_stdin!(opts)
+ opts[:arguments].try(:shift) || $stdin.read
end
end
end
diff --git a/lib/i18n/tasks/command/options/data.rb b/lib/i18n/tasks/command/options/data.rb
new file mode 100644
index 00000000..2ebc8b99
--- /dev/null
+++ b/lib/i18n/tasks/command/options/data.rb
@@ -0,0 +1,91 @@
+require 'i18n/tasks/command/option_parsers/enum'
+
+module I18n::Tasks
+ module Command
+ module Options
+ module Data
+ include Command::DSL
+
+ DATA_FORMATS = %w(yaml json keys)
+ OUT_FORMATS = ['terminal-table', *DATA_FORMATS, 'inspect']
+
+ format_arg = proc { |type, values|
+ default = values.first
+ arg type,
+ '-f',
+ '--format FORMAT',
+ values,
+ {'yml' => 'yaml'},
+ t("i18n_tasks.cmd.args.desc.#{type}", valid_text: values * ', ', default_text: default),
+ parser: OptionParsers::Enum::Parser.new(values,
+ proc { |value, valid|
+ I18n.t('i18n_tasks.cmd.errors.invalid_format',
+ invalid: value, valid: valid * ', ') })
+ }
+
+ # i18n-tasks-use t('i18n_tasks.cmd.args.desc.data_format')
+ format_arg.call(:data_format, DATA_FORMATS)
+
+ # i18n-tasks-use t('i18n_tasks.cmd.args.desc.out_format')
+ format_arg.call(:out_format, OUT_FORMATS)
+
+ def forest_pos_or_stdin!(opt, format = opt[:format])
+ parse_forest(pos_or_stdin!(opt), format)
+ end
+
+ # @return [Array] trees read from stdin and positional arguments
+ def forests_stdin_and_pos!(opt, num = false, format = opt[:format])
+ args = opt[:arguments] || []
+ if opt[:nostdin]
+ sources = []
+ else
+ sources = [$stdin.read]
+ num -= 1 if num
+ end
+ if num
+ num.times { sources << args.shift }
+ else
+ sources += args
+ args.clear
+ end
+ sources.map { |src| parse_forest(src, format) }
+ end
+
+ def merge_forests_stdin_and_pos!(opt)
+ forests_stdin_and_pos!(opt).inject(i18n.empty_forest) { |result, forest|
+ result.merge! forest
+ }
+ end
+
+ def parse_forest(src, format)
+ if !src
+ raise CommandError.new(I18n.t('i18n_tasks.cmd.errors.pass_forest'))
+ end
+ if format == 'keys'
+ ::I18n::Tasks::Data::Tree::Siblings.from_key_names parse_keys(src)
+ else
+ ::I18n::Tasks::Data::Tree::Siblings.from_nested_hash i18n.data.adapter_parse(src, format)
+ end
+ end
+
+ def parse_keys(src)
+ Array(src).compact.flat_map { |v| v.strip.split(/\s*[,\s\n]\s*/) }.map(&:presence).compact
+ end
+
+ def print_forest(forest, opts, version = :show_tree)
+ format = opts[:format].to_s
+ case format
+ when 'terminal-table'
+ terminal_report.send(version, forest)
+ when 'inspect'
+ puts forest.inspect
+ when 'keys'
+ puts forest.key_names(root: true)
+ when *DATA_FORMATS
+ puts i18n.data.adapter_dump forest.to_hash(true), format
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/i18n/tasks/command/options/enum_opt.rb b/lib/i18n/tasks/command/options/enum_opt.rb
deleted file mode 100644
index 598e75b2..00000000
--- a/lib/i18n/tasks/command/options/enum_opt.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module I18n::Tasks
- module Command
- module Options
- module EnumOpt
- DEFAULT_ENUM_OPT_ERROR = proc { |bad, good|
- I18n.t('i18n_tasks.cmd.enum_opt.invalid', invalid: bad, valid: good * ', ')
- }
-
- def parse_enum_opt(value, valid, &error_msg)
- valid = enum_opt(valid) if Symbol === valid
- return valid.first unless value.present?
- if enum_opt_valid?(valid, value)
- value
- else
- error_msg ||= DEFAULT_ENUM_OPT_ERROR
- raise CommandError.new error_msg.call(value, valid)
- end
- end
-
- def enum_opt_valid?(valid, value)
- valid = enum_opt(valid) if Symbol === valid
- valid.include?(value)
- end
-
- DEFAULT_ENUM_LIST_ERROR = proc { |bad, good|
- I18n.t('i18n_tasks.cmd.enum_list_opt.invalid', invalid: bad * ', ', valid: good * ', ')
- }
-
- def parse_enum_list_opt(values, valid, &error_msg)
- values = explode_list_opt(values)
- invalid = values - valid.map(&:to_s)
- if invalid.empty?
- if values.empty?
- valid
- else
- values
- end
- else
- error_msg ||= DEFAULT_ENUM_LIST_ERROR
- raise CommandError.new error_msg.call(invalid, valid)
- end
- end
-
- def enum_opt(*args)
- self.class.enum_opt(*args)
- end
- end
- end
- end
-end
diff --git a/lib/i18n/tasks/command/options/list_opt.rb b/lib/i18n/tasks/command/options/list_opt.rb
deleted file mode 100644
index 0ffe5950..00000000
--- a/lib/i18n/tasks/command/options/list_opt.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module I18n::Tasks
- module Command
- module Options
- module ListOpt
- def explode_list_opt(list_opt, delim = /\s*[,]\s*/)
- Array(list_opt).compact.map { |v| v.strip.split(delim).compact.presence }.flatten.map(&:presence).compact
- end
- end
- end
- end
-end
diff --git a/lib/i18n/tasks/command/options/locales.rb b/lib/i18n/tasks/command/options/locales.rb
index f524a60c..2cba1533 100644
--- a/lib/i18n/tasks/command/options/locales.rb
+++ b/lib/i18n/tasks/command/options/locales.rb
@@ -1,53 +1,33 @@
+require 'i18n/tasks/command/option_parsers/locale'
+
module I18n::Tasks
module Command
module Options
module Locales
include Command::DSL
- cmd_opt :locales, {
- short: :l,
- long: :locales=,
- desc: t('i18n_tasks.cmd.args.desc.locales_filter'),
- conf: {as: Array, delimiter: /\s*[+:,]\s*/, default: 'all', argument: true, optional: false},
- parse: :parse_locales
- }
-
- cmd_opt :locale, {
- short: :l,
- long: :locale=,
- desc: t('i18n_tasks.cmd.args.desc.locale'),
- conf: {default: 'base', argument: true, optional: false},
- parse: :parse_locale
- }
-
- cmd_opt :locale_to_translate_from, cmd_opt(:locale).merge(
- short: :f,
- long: :from=,
- desc: t('i18n_tasks.cmd.args.desc.locale_to_translate_from'))
-
- def parse_locales(opt, key = :locales)
- argv = Array(opt[:arguments]) + Array(opt[key])
- locales = if argv == ['all'] || argv == 'all' || argv.blank?
- i18n.locales
- else
- explode_list_opt(argv).map { |v| v == 'base' ? base_locale : v }
- end
- locales.each { |locale| validate_locale!(locale) }
- log_verbose "locales for the command are #{locales.inspect}"
- opt[key] = locales
- end
-
- def parse_locale(opt, key = :locale)
- val = opt[key]
- opt[key] = base_locale if val.blank? || val == 'base'
- opt[key]
- end
+ arg :locales,
+ '-l',
+ '--locales en,es,ru',
+ Array,
+ t('i18n_tasks.cmd.args.desc.locales_filter'),
+ parser: OptionParsers::Locale::ListParser,
+ default: 'all',
+ consume_positional: true
- VALID_LOCALE_RE = /\A\w[\w\-\.]*\z/i
+ arg :locale,
+ '-l',
+ '--locale en',
+ t('i18n_tasks.cmd.args.desc.locale'),
+ parser: OptionParsers::Locale::Parser,
+ default: 'base'
- def validate_locale!(locale)
- raise CommandError.new(I18n.t('i18n_tasks.cmd.errors.invalid_locale', invalid: locale)) if VALID_LOCALE_RE !~ locale
- end
+ arg :locale_to_translate_from,
+ '-f',
+ '--from en',
+ t('i18n_tasks.cmd.args.desc.locale_to_translate_from'),
+ parser: OptionParsers::Locale::Parser,
+ default: 'base'
end
end
end
diff --git a/lib/i18n/tasks/command/options/trees.rb b/lib/i18n/tasks/command/options/trees.rb
deleted file mode 100644
index db5ceddf..00000000
--- a/lib/i18n/tasks/command/options/trees.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-module I18n::Tasks
- module Command
- module Options
- module Trees
- include Command::DSL
- format_opt = proc { |type|
- enum_opt_attr :f, :format=, enum_opt(type),
- proc { |valid, default|
- I18n.t("i18n_tasks.cmd.args.desc.#{type}", valid_text: valid, default_text: default) },
- proc { |value, valid|
- I18n.t('i18n_tasks.cmd.errors.invalid_format', invalid: value, valid: valid * ', ') }
- }
-
- enum_opt :data_format, %w(yaml json keys)
- # i18n-tasks-use t('i18n_tasks.cmd.args.desc.data_format')
- cmd_opt :data_format, format_opt.call(:data_format)
-
- enum_opt :out_format, ['terminal-table', *enum_opt(:data_format), 'inspect']
- # i18n-tasks-use t('i18n_tasks.cmd.args.desc.out_format')
- cmd_opt :out_format, format_opt.call(:out_format)
-
- def print_forest(forest, opt, version = :show_tree)
- format = opt[:format].to_s
- case format
- when 'terminal-table'
- terminal_report.send(version, forest)
- when 'inspect'
- puts forest.inspect
- when 'keys'
- puts forest.key_names(root: true)
- when *enum_opt(:data_format)
- puts i18n.data.adapter_dump forest.to_hash(true), format
- end
- end
-
- def opt_forest_arg_or_stdin!(opt, format = opt[:format])
- src = opt[:arguments].try(:shift) || $stdin.read
- parse_forest(src, format)
- end
-
- def opt_forests_stdin_args!(opt, num = false, format = opt[:format])
- args = opt[:arguments] || []
- if opt[:nostdin]
- sources = []
- else
- sources = [$stdin.read]
- num -= 1 if num
- end
- if num
- num.times { sources << args.shift }
- else
- sources += args
- args.clear
- end
- sources.map { |src| parse_forest(src, format) }
- end
-
- def opt_forests_merged_stdin_args!(opt)
- opt_forests_stdin_args!(opt).inject(i18n.empty_forest) { |result, forest|
- result.merge! forest
- }
- end
-
- def parse_forest(src, format)
- if !src
- raise CommandError.new(I18n.t('i18n_tasks.cmd.errors.pass_forest'))
- end
- if format == 'keys'
- Data::Tree::Siblings.from_key_names parse_keys(src)
- else
- Data::Tree::Siblings.from_nested_hash i18n.data.adapter_parse(src, format)
- end
- end
-
- def parse_keys(src)
- explode_list_opt(src, /\s*[,\s\n]\s*/)
- end
- end
- end
- end
-end
diff --git a/lib/i18n/tasks/configuration.rb b/lib/i18n/tasks/configuration.rb
index 0028add1..522e4013 100644
--- a/lib/i18n/tasks/configuration.rb
+++ b/lib/i18n/tasks/configuration.rb
@@ -13,7 +13,7 @@ def config
def file_config
file = CONFIG_FILES.detect { |f| File.exist?(f) }
- config = file && YAML.load(Erubis::Eruby.new(File.read(file)).result)
+ config = file && YAML.load(Erubis::Eruby.new(File.read(file, encoding: 'UTF-8')).result)
if config.present?
config.with_indifferent_access.tap do |c|
if c[:relative_roots]
diff --git a/lib/i18n/tasks/data/file_formats.rb b/lib/i18n/tasks/data/file_formats.rb
index 34c0d536..10109765 100644
--- a/lib/i18n/tasks/data/file_formats.rb
+++ b/lib/i18n/tasks/data/file_formats.rb
@@ -34,14 +34,20 @@ def read_config(format)
end
def load_file(path)
- adapter_parse ::File.read(path), self.class.adapter_name_for_path(path)
+ adapter_parse ::File.read(path, encoding: 'UTF-8'), self.class.adapter_name_for_path(path)
end
def write_tree(path, tree)
+ hash = tree.to_hash(true)
+ adapter = self.class.adapter_name_for_path(path)
+ content = adapter_dump(hash, adapter)
+ # Ignore unchanged data
+ return if File.file?(path) &&
+ # Comparing hashes for equality directly would ignore key order.
+ # Round-trip through the adapter and compare the strings instead:
+ content == adapter_dump(load_file(path), adapter)
::FileUtils.mkpath(File.dirname path)
- ::File.open(path, 'w') { |f|
- f.write adapter_dump(tree.to_hash(true), self.class.adapter_name_for_path(path))
- }
+ ::File.open(path, 'w') { |f| f.write content }
end
module ClassMethods
diff --git a/lib/i18n/tasks/data/file_system_base.rb b/lib/i18n/tasks/data/file_system_base.rb
index e324824f..4ac174fb 100644
--- a/lib/i18n/tasks/data/file_system_base.rb
+++ b/lib/i18n/tasks/data/file_system_base.rb
@@ -25,9 +25,9 @@ def initialize(config = {})
locales = config[:locales].presence
@locales = LocaleList.normalize_locale_list(locales || available_locales, base_locale, true)
if locales.present?
- log_verbose "data locales: #{@locales}"
+ log_verbose "locales read from config #{@locales * ', '}"
else
- log_verbose "data locales (inferred): #{@locales}"
+ log_verbose "locales inferred from data: #{@locales * ', '}"
end
end
@@ -110,7 +110,7 @@ def t(key, locale)
end
def config=(config)
- @config = DEFAULTS.deep_merge((config || {}).with_indifferent_access)
+ @config = DEFAULTS.deep_merge((config || {}).reject { |k, v| v.nil? }.with_indifferent_access)
reload
end
diff --git a/lib/i18n/tasks/data/tree/node.rb b/lib/i18n/tasks/data/tree/node.rb
index be4914dd..5bfeeaee 100644
--- a/lib/i18n/tasks/data/tree/node.rb
+++ b/lib/i18n/tasks/data/tree/node.rb
@@ -4,6 +4,9 @@
require 'i18n/tasks/data/tree/siblings'
module I18n::Tasks::Data::Tree
class Node
+ include Enumerable
+ include Traversal
+
attr_accessor :value
attr_reader :key, :children, :parent
@@ -42,9 +45,6 @@ def each(&block)
self
end
- include Enumerable
- include Traversal
-
def value_or_children_hash
leaf? ? value : children.try(:to_hash)
end
diff --git a/lib/i18n/tasks/data/tree/nodes.rb b/lib/i18n/tasks/data/tree/nodes.rb
index be9d2610..2f607263 100644
--- a/lib/i18n/tasks/data/tree/nodes.rb
+++ b/lib/i18n/tasks/data/tree/nodes.rb
@@ -4,6 +4,9 @@
module I18n::Tasks::Data::Tree
# A list of nodes
class Nodes
+ include Enumerable
+ include Traversal
+
attr_reader :list
def initialize(opts = {})
@@ -11,8 +14,6 @@ def initialize(opts = {})
end
delegate :each, :present?, :empty?, :blank?, :size, :to_a, to: :@list
- include Enumerable
- include Traversal
def to_nodes
self
diff --git a/lib/i18n/tasks/data/tree/siblings.rb b/lib/i18n/tasks/data/tree/siblings.rb
index c20ab235..dac57d5d 100644
--- a/lib/i18n/tasks/data/tree/siblings.rb
+++ b/lib/i18n/tasks/data/tree/siblings.rb
@@ -1,6 +1,5 @@
# coding: utf-8
-require 'i18n/tasks/data/tree/traversal'
require 'i18n/tasks/data/tree/nodes'
module I18n::Tasks::Data::Tree
# Siblings represents a subtree sharing a common parent
diff --git a/lib/i18n/tasks/logging.rb b/lib/i18n/tasks/logging.rb
index e7ae526b..70df3d08 100644
--- a/lib/i18n/tasks/logging.rb
+++ b/lib/i18n/tasks/logging.rb
@@ -3,24 +3,28 @@ module I18n::Tasks::Logging
extend self
def warn_deprecated(message)
- log_stderr Term::ANSIColor.yellow Term::ANSIColor.bold "i18n-tasks: [DEPRECATED] #{message}"
+ log_stderr Term::ANSIColor.yellow Term::ANSIColor.bold "#{program_name}: [DEPRECATED] #{message}"
end
def log_verbose(message)
- if ENV['VERBOSE']
- log_stderr Term::ANSIColor.green "i18n-tasks: #{message}"
+ if ::I18n::Tasks.verbose?
+ log_stderr Term::ANSIColor.bright_blue(message)
end
end
def log_warn(message)
- log_stderr Term::ANSIColor.yellow "i18n-tasks: [WARN] #{message}"
+ log_stderr Term::ANSIColor.yellow "#{program_name}: [WARN] #{message}"
end
def log_error(message)
- log_stderr Term::ANSIColor.red Term::ANSIColor.bold "i18n-tasks: #{message}"
+ log_stderr Term::ANSIColor.red Term::ANSIColor.bold "#{program_name}: #{message}"
end
def log_stderr(*args)
$stderr.puts(*args)
end
+
+ def program_name
+ @program_name ||= File.basename($PROGRAM_NAME)
+ end
end
diff --git a/lib/i18n/tasks/missing_keys.rb b/lib/i18n/tasks/missing_keys.rb
index 85653929..0312fd40 100644
--- a/lib/i18n/tasks/missing_keys.rb
+++ b/lib/i18n/tasks/missing_keys.rb
@@ -69,10 +69,12 @@ def missing_diff_tree(locale, compared_to = base_locale)
data[compared_to].select_keys { |key, _node|
locale_key_missing? locale, depluralize_key(key, compared_to)
}.set_root_key!(locale, type: :missing_diff).keys { |_key, node|
+ # change path and locale to base
+ data = {locale: locale, missing_diff_locale: node.data[:locale]}
if node.data.key?(:path)
- # change path and locale to base
- node.data.update path: LocalePathname.replace_locale(node.data[:path], node.data[:locale], locale), locale: locale
+ data[:path] = LocalePathname.replace_locale(node.data[:path], node.data[:locale], locale)
end
+ node.data.update data
}
end
diff --git a/lib/i18n/tasks/reports/spreadsheet.rb b/lib/i18n/tasks/reports/spreadsheet.rb
index f8b6a18f..8b7f8d93 100644
--- a/lib/i18n/tasks/reports/spreadsheet.rb
+++ b/lib/i18n/tasks/reports/spreadsheet.rb
@@ -11,7 +11,6 @@ def save_report(path, opts)
add_missing_sheet p.workbook
add_unused_sheet p.workbook
add_eq_base_sheet p.workbook
- p.use_shared_strings = true
FileUtils.mkpath(File.dirname(path))
p.serialize(path)
$stderr.puts Term::ANSIColor.green "Saved to #{path}"
diff --git a/lib/i18n/tasks/reports/terminal.rb b/lib/i18n/tasks/reports/terminal.rb
index d899928f..bb288b5c 100644
--- a/lib/i18n/tasks/reports/terminal.rb
+++ b/lib/i18n/tasks/reports/terminal.rb
@@ -11,9 +11,11 @@ def missing_keys(forest = task.missing_keys)
forest = task.collapse_plural_nodes!(forest)
if forest.present?
print_title missing_title(forest)
- print_table headings: [cyan(bold(I18n.t('i18n_tasks.common.locale'))), cyan(bold I18n.t('i18n_tasks.common.key')), I18n.t('i18n_tasks.common.details')] do |t|
+ print_table headings: [cyan(bold(I18n.t('i18n_tasks.common.locale'))),
+ cyan(bold I18n.t('i18n_tasks.common.key')),
+ I18n.t('i18n_tasks.missing.details_title')] do |t|
t.rows = sort_by_attr!(forest_to_attr(forest)).map do |a|
- [{value: cyan(a[:locale]), alignment: :center}, cyan(a[:key]), wrap_string(key_info(a), 60)]
+ [{value: cyan(a[:locale]), alignment: :center}, cyan(a[:key]), missing_key_info(a)]
end
end
else
@@ -74,6 +76,14 @@ def forest_stats(forest, stats = task.forest_stats(forest))
private
+ def missing_key_info(leaf)
+ if leaf[:type] == :missing_used
+ first_occurrence leaf
+ else
+ "#{cyan leaf[:data][:missing_diff_locale]} #{leaf[:value].to_s.strip}"
+ end
+ end
+
def print_occurrences(node, full_key = node.full_key)
occurrences = node.data[:source_occurrences]
puts "#{bold "#{full_key}"} #{green(occurrences.size.to_s) if occurrences.size > 1}"
@@ -84,8 +94,12 @@ def print_occurrences(node, full_key = node.full_key)
def print_locale_key_value_table(locale_key_values)
if locale_key_values.present?
- print_table headings: [bold(cyan(I18n.t('i18n_tasks.common.locale'))), bold(cyan(I18n.t('i18n_tasks.common.key'))), I18n.t('i18n_tasks.common.value')] do |t|
- t.rows = locale_key_values.map { |(locale, k, v)| [{value: cyan(locale), alignment: :center}, cyan(k), v.to_s] }
+ print_table headings: [bold(cyan(I18n.t('i18n_tasks.common.locale'))),
+ bold(cyan(I18n.t('i18n_tasks.common.key'))),
+ I18n.t('i18n_tasks.common.value')] do |t|
+ t.rows = locale_key_values.map { |(locale, k, v)|
+ [{value: cyan(locale), alignment: :center}, cyan(k), v.to_s]
+ }
end
else
puts 'ø'
@@ -124,17 +138,7 @@ def key_occurrence(full_key, info)
end
def highlight_key(full_key, line, range = (0..-1))
- result = line.dup
- result[range] = result[range].sub(full_key) { |m| underline m }
- result
- end
-
- def key_info(leaf)
- if leaf[:type] == :missing_used
- first_occurrence leaf
- else
- leaf[:value].to_s.strip
- end
+ line.dup.tap { |s| s[range] = s[range].sub(full_key) { |m| underline m } }
end
def first_occurrence(leaf)
@@ -143,23 +147,6 @@ def first_occurrence(leaf)
[green("#{first[:src_path]}:#{first[:line_num]}"),
("(#{I18n.t 'i18n_tasks.common.n_more', count: usages.length - 1})" if usages.length > 1)].compact.join(' ')
end
-
- def wrap_string(s, max)
- chars = []
- dist = 0
- s.chars.each do |c|
- chars << c
- dist += 1
- if c == "\n"
- dist = 0
- elsif dist == max
- dist = 0
- chars << "\n"
- end
- end
- chars = chars[0..-2] if chars.last == "\n"
- chars.join
- end
end
end
end
diff --git a/lib/i18n/tasks/scanners/base_scanner.rb b/lib/i18n/tasks/scanners/base_scanner.rb
index 667b164c..1c2122b3 100644
--- a/lib/i18n/tasks/scanners/base_scanner.rb
+++ b/lib/i18n/tasks/scanners/base_scanner.rb
@@ -10,27 +10,27 @@ class BaseScanner
attr_reader :config, :key_filter, :ignore_lines_res
+ ALWAYS_EXCLUDE = %w(*.jpg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf
+ *.css *.sass *.scss *.less *.yml *.json)
+
def initialize(config = {})
@config = config.dup.with_indifferent_access.tap do |conf|
- conf[:relative_roots] = %w(app/views) if conf[:relative_roots].blank?
+ conf[:relative_roots] = %w(app/views app/controllers app/helpers app/presenters) if conf[:relative_roots].blank?
conf[:paths] = %w(app/) if conf[:paths].blank?
conf[:include] = Array(conf[:include]) if conf[:include].present?
- if conf.key?(:exclude)
- conf[:exclude] = Array(conf[:exclude])
- else
- # exclude common binary extensions by default (images and fonts)
- conf[:exclude] = %w(*.jpg *.png *.gif *.svg *.ico *.eot *.ttf *.woff *.pdf)
- end
+ conf[:exclude] = Array(conf[:exclude]) + ALWAYS_EXCLUDE
# Regexps for lines to ignore per extension
if conf[:ignore_lines] && !conf[:ignore_lines].is_a?(Hash)
warn_deprecated "search.ignore_lines must be a Hash, found #{conf[:ignore_lines].class.name}"
conf[:ignore_lines] = nil
end
conf[:ignore_lines] ||= {
- 'rb' => %q(^\s*#(?!\si18n-tasks-use)),
- 'haml' => %q(^\s*-\s*#(?!\si18n-tasks-use)),
- 'slim' => %q(^\s*(?:-#|/)(?!\si18n-tasks-use)),
- 'erb' => %q(^\s*<%\s*#(?!\si18n-tasks-use)),
+ 'rb' => %q(^\s*#(?!\si18n-tasks-use)),
+ 'opal' => %q(^\s*#(?!\si18n-tasks-use)),
+ 'haml' => %q(^\s*-\s*#(?!\si18n-tasks-use)),
+ 'slim' => %q(^\s*(?:-#|/)(?!\si18n-tasks-use)),
+ 'coffee' => %q(^\s*#(?!\si18n-tasks-use)),
+ 'erb' => %q(^\s*<%\s*#(?!\si18n-tasks-use)),
}
@ignore_lines_res = conf[:ignore_lines].inject({}) { |h, (ext, re)| h.update(ext => Regexp.new(re)) }
@key_filter = nil
@@ -59,7 +59,7 @@ def keys(opts = {})
def read_file(path)
result = nil
- File.open(path, 'rb') { |f| result = f.read }
+ File.open(path, 'rb', encoding: 'UTF-8') { |f| result = f.read }
result
end
@@ -126,9 +126,9 @@ def strip_literal(literal)
key
end
- VALID_KEY_CHARS = /[-\w.?!;:]/
+ VALID_KEY_CHARS = /(?:[[:word:]]|[-.?!;À-ž])/
VALID_KEY_RE_STRICT = /^#{VALID_KEY_CHARS}+$/
- VALID_KEY_RE = /^(#{VALID_KEY_CHARS}|[\#{@}])+$/
+ VALID_KEY_RE = /^(#{VALID_KEY_CHARS}|[:\#{@}\[\]])+$/
def valid_key?(key, strict = false)
return false if @key_filter && @key_filter_pattern !~ key
diff --git a/lib/i18n/tasks/scanners/pattern_scanner.rb b/lib/i18n/tasks/scanners/pattern_scanner.rb
index 1ad7ca91..c32c60f3 100644
--- a/lib/i18n/tasks/scanners/pattern_scanner.rb
+++ b/lib/i18n/tasks/scanners/pattern_scanner.rb
@@ -13,15 +13,17 @@ def scan_file(path, opts = {})
text = opts[:text] || read_file(path)
text.scan(pattern) do |match|
src_pos = Regexp.last_match.offset(0).first
- key = match_to_key(match, path)
- next unless valid_key?(key, strict)
- key = key + ':' if key.end_with?('.')
location = src_location(path, text, src_pos)
- unless exclude_line?(location[:line], path)
- keys << [key, data: location]
- end
+ next if exclude_line?(location[:line], path)
+ key = match_to_key(match, path, location)
+ next unless key
+ key = key + ':' if key.end_with?('.')
+ next unless valid_key?(key, strict)
+ keys << [key, data: location]
end
keys
+ rescue Exception => e
+ raise ::I18n::Tasks::CommandError.new("Error scanning #{path}: #{e.message}")
end
def default_pattern
@@ -38,10 +40,31 @@ def default_pattern
# @param [MatchData] match
# @param [String] path
# @return [String] full absolute key name
- def match_to_key(match, path)
+ def match_to_key(match, path, location)
key = strip_literal(match[0])
- key = absolutize_key(key, path) if path && key.start_with?('.')
- key
+ absolute_key(key, path, location)
+ end
+
+ def absolute_key(key, path, location)
+ if key.start_with?('.')
+ if controller_file?(path)
+ absolutize_key(key, path, relative_roots, closest_method(location))
+ else
+ absolutize_key(key, path)
+ end
+ else
+ key
+ end
+ end
+
+ def controller_file?(path)
+ /controllers/.match(path)
+ end
+
+ def closest_method(location)
+ method = File.readlines(location[:src_path], encoding: 'UTF-8').first(location[:line_num] - 1).reverse_each.find { |x| x=~ /\bdef\b/ }
+ method &&= method.strip.sub(/^def\s*/, '').sub(/[\(\s;].*$/, '')
+ method
end
def pattern
diff --git a/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb b/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb
index e80b2eb6..3d3ee83d 100644
--- a/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb
+++ b/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb
@@ -20,7 +20,7 @@ def default_pattern
# @param [MatchData] match
# @param [String] path
# @return [String] full absolute key name with scope resolved if any
- def match_to_key(match, path)
+ def match_to_key(match, path, location)
key = super
scope = match[1]
if scope
diff --git a/lib/i18n/tasks/scanners/relative_keys.rb b/lib/i18n/tasks/scanners/relative_keys.rb
index c7e5faee..69a80f82 100644
--- a/lib/i18n/tasks/scanners/relative_keys.rb
+++ b/lib/i18n/tasks/scanners/relative_keys.rb
@@ -6,14 +6,47 @@ module RelativeKeys
# @param key [String] relative i18n key (starts with a .)
# @param path [String] path to the file containing the key
# @return [String] absolute version of the key
- def absolutize_key(key, path, roots = relative_roots)
- # normalized path
- path = File.expand_path path
- (path_root = roots.map { |path| File.expand_path path }.sort.reverse.detect { |root| path.start_with?(root + '/') }) or
- raise CommandError.new("Error scanning #{path}: cannot resolve relative key \"#{key}\".\nSet relative_roots in config/i18n-tasks.yml (currently #{relative_roots.inspect})")
- # key prefix based on path
- prefix = path.gsub(%r(#{path_root}/|(\.[^/]+)*$), '').tr('/', '.').gsub(%r(\._), '.')
- "#{prefix}#{key}"
+ def absolutize_key(key, path, roots = relative_roots, closest_method = "")
+ normalized_path = File.expand_path(path)
+ path_root(normalized_path, roots) or
+ raise CommandError.new(
+ "Error scanning #{normalized_path}: cannot resolve relative key
+ \"#{key}\".\nSet search.relative_roots in config/i18n-tasks.yml
+ (currently #{relative_roots.inspect})"
+ )
+
+ prefix_key_based_on_path(key, normalized_path, roots, closest_method: closest_method)
+ end
+
+ private
+
+ # Detect the appropriate relative path root
+ # @param [String] path /full/path
+ # @param [Array] roots array of full paths
+ # @return [String] the closest ancestor root for path
+ def path_root(path, roots)
+ expanded_relative_roots(roots).sort.reverse_each.detect do |root|
+ path.start_with?(root + '/')
+ end
+ end
+
+ def expanded_relative_roots(roots)
+ roots.map { |path| File.expand_path(path) }
+ end
+
+ def prefix_key_based_on_path(key, normalized_path, roots, options = {})
+ "#{prefix(normalized_path, roots, options)}#{key}"
+ end
+
+ def prefix(normalized_path, roots, options = {})
+ file_name = normalized_path.gsub(%r(#{path_root(normalized_path, roots)}/|(\.[^/]+)*$), '')
+
+ if options[:closest_method].present?
+ controller_name = file_name.sub(/_controller$/, '')
+ "#{controller_name}.#{options[:closest_method]}".tr('/', '.')
+ else
+ file_name.tr('/', '.').gsub(%r(\._), '.')
+ end
end
end
end
diff --git a/lib/i18n/tasks/slop_command.rb b/lib/i18n/tasks/slop_command.rb
deleted file mode 100644
index 0282de1b..00000000
--- a/lib/i18n/tasks/slop_command.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-module I18n::Tasks::SlopCommand
- extend self
-
- def slop_command(name, attr, &block)
- proc {
- cmd_name = name.tr('_', '-')
- command cmd_name do
- args = attr[:args]
- banner "Usage: i18n-tasks #{cmd_name} [options] #{args}" if args.present?
- desc = attr[:desc]
- desc = desc.call if desc.respond_to?(:call)
- description desc if desc
- attr[:opt].try :each do |opt|
- on *opt.values_at(:short, :long, :desc, :conf).compact.map { |v| v.respond_to?(:call) ? v.call : v }
- end
- run { |slop_opts, slop_args|
- slop_opts = slop_opts.to_hash(true).reject { |k, v| v.nil? }
- slop_opts.merge!(arguments: slop_args) unless slop_args.empty?
- block.call name, slop_opts
- }
- end
- }
- end
-
- def parse_opts!(opts, opts_conf, context)
- return if !opts_conf
- opts_conf.each do |opt_conf|
- parse = opt_conf[:parse]
- if parse
- key = opt_conf[:long].to_s.sub(/=\z/, '').to_sym
- if parse.respond_to?(:call)
- context.instance_exec opts, key, &parse
- elsif Symbol === parse
- context.instance_exec do
- send parse, opts, key
- end
- end
- end
- end
- end
-end
diff --git a/lib/i18n/tasks/used_keys.rb b/lib/i18n/tasks/used_keys.rb
index 7a9e16d7..4a286410 100644
--- a/lib/i18n/tasks/used_keys.rb
+++ b/lib/i18n/tasks/used_keys.rb
@@ -11,11 +11,10 @@ module UsedKeys
# @return [Array]
def used_tree(opts = {})
return scanner.with_key_filter(opts[:key_filter]) { used_tree(opts.except(:key_filter)) } if opts[:key_filter]
- key_attrs = scanner.keys(opts.slice(:strict))
Data::Tree::Node.new(
key: 'used',
data: {key_filter: scanner.key_filter},
- children: Data::Tree::Siblings.from_key_attr(key_attrs)
+ children: Data::Tree::Siblings.from_key_attr(scanner.keys(opts.slice(:strict)))
).to_siblings
end
diff --git a/lib/i18n/tasks/version.rb b/lib/i18n/tasks/version.rb
index a5ca223c..ebedeb8b 100644
--- a/lib/i18n/tasks/version.rb
+++ b/lib/i18n/tasks/version.rb
@@ -1,6 +1,6 @@
# coding: utf-8
module I18n
module Tasks
- VERSION = '0.7.8'
+ VERSION = '0.8.3'
end
end
diff --git a/spec/commands/data_commands_spec.rb b/spec/commands/data_commands_spec.rb
index 5d142b71..00fca2c1 100644
--- a/spec/commands/data_commands_spec.rb
+++ b/spec/commands/data_commands_spec.rb
@@ -20,19 +20,19 @@ def en_data_2
end
it '#data' do
- expect(JSON.parse(run_cmd :data, format: 'json', locales: 'en')).to eq(en_data)
+ expect(JSON.parse(run_cmd 'data', '-fjson', '-len')).to eq(en_data)
end
it '#data-merge' do
- expect(JSON.parse(run_cmd :data_merge, format: 'json', arguments: [en_data_2.to_json], nostdin: true)).to eq(en_data.deep_merge en_data_2)
+ expect(JSON.parse(run_cmd 'data-merge', '-fjson', '-S', en_data_2.to_json)).to eq(en_data.deep_merge en_data_2)
end
it '#data-write' do
- expect(JSON.parse(run_cmd :data_write, format: 'json', arguments: [en_data_2.to_json])).to eq(en_data_2)
+ expect(JSON.parse(run_cmd 'data-write', '-fjson', '-S', en_data_2.to_json)).to eq(en_data_2)
end
it '#data-remove' do
to_remove = {'en' => {'common' => {'hello' => ''}}}
- expect(JSON.parse(run_cmd :data_remove, format: 'json', arguments: [to_remove.to_json])).to eq('en' => {'common' => en_data['en']['common'] })
+ expect(JSON.parse(run_cmd 'data-remove', '-fjson', '-S', to_remove.to_json)).to eq('en' => {'common' => en_data['en']['common'] })
end
end
diff --git a/spec/commands/tree_commands_spec.rb b/spec/commands/tree_commands_spec.rb
index 968fc9b8..7c9327a9 100644
--- a/spec/commands/tree_commands_spec.rb
+++ b/spec/commands/tree_commands_spec.rb
@@ -13,7 +13,7 @@
context 'tree-merge' do
trees = [{'a' => '1', 'b' => '2'}, {'a' => '-1', 'c' => '3'}]
it trees.map(&:to_json).join(', ') do
- merged = JSON.parse run_cmd(:tree_merge, format: 'json', arguments: trees.map(&:to_json), nostdin: true)
+ merged = JSON.parse run_cmd('tree-merge', '-fjson', '-S', *trees.map(&:to_json))
expect(merged).to eq trees.reduce(:merge)
end
end
@@ -22,7 +22,7 @@
forest = {'a' => '1', 'b' => '2', 'c' => {'a' => '3'}}
pattern = '{a,c.*}'
it "-p #{pattern.inspect} #{forest.to_json}" do
- selected = JSON.parse run_cmd(:tree_filter, format: 'json', pattern: pattern, arguments: [forest.to_json])
+ selected = JSON.parse run_cmd('tree-filter', '-fjson', '-p', pattern, forest.to_json)
expect(selected).to eq(forest.except('b'))
end
end
@@ -30,7 +30,7 @@
context 'tree-subtract' do
trees = [{'a' => '1', 'b' => '2'}, {'a' => '-1', 'c' => '3'}]
it trees.map(&:to_json).join(' - ') do
- subtracted = JSON.parse run_cmd(:tree_subtract, format: 'json', arguments: trees.map(&:to_json), nostdin: true)
+ subtracted = JSON.parse run_cmd('tree-subtract', '-fjson', '-S', *trees.map(&:to_json))
expected = {'b' => '2'}
expect(subtracted).to eq expected
end
@@ -41,17 +41,18 @@ def forest
{'a' => {'b' => {'a' => '1'}}}
end
+ def rename_key(from, to)
+ JSON.parse run_cmd('tree-rename-key', '-fjson', '-k', from, '-n', to, forest.to_json)
+ end
+
it 'renames root node' do
- renamed = JSON.parse run_cmd(:tree_rename_key, key: 'a', name: 'x', format: 'json', arguments: [forest.to_json])
- expect(renamed).to eq(forest.tap { |f| f['x'] = f.delete('a') })
+ expect(rename_key('a', 'x')).to eq(forest.tap { |f| f['x'] = f.delete('a') })
end
it 'renames node' do
- renamed = JSON.parse run_cmd(:tree_rename_key, key: 'a.b', name: 'x', format: 'json', arguments: [forest.to_json])
- expect(renamed).to eq(forest.tap { |f| f['a']['x'] = f['a'].delete('b') })
+ expect(rename_key('a.b', 'x')).to eq(forest.tap { |f| f['a']['x'] = f['a'].delete('b') })
end
it 'renames leaf' do
- renamed = JSON.parse run_cmd(:tree_rename_key, key: 'a.b.a', name: 'x', format: 'json', arguments: [forest.to_json])
- expect(renamed).to eq(forest.tap { |f| f['a']['b']['x'] = f['a']['b'].delete('a') })
+ expect(rename_key('a.b.a', 'x')).to eq(forest.tap { |f| f['a']['b']['x'] = f['a']['b'].delete('a') })
end
end
@@ -61,7 +62,7 @@ def forest
end
it 'converts to keys' do
- keys = run_cmd(:tree_convert, from: 'json', to: 'keys', arguments: [forest.to_json]).split("\n")
+ keys = run_cmd('tree-convert', '-fjson', '-tkeys', forest.to_json).split("\n")
expect(keys.sort).to eq(['a.b.a', 'x'].sort)
end
end
diff --git a/spec/file_system_data_spec.rb b/spec/file_system_data_spec.rb
index 66d7be3b..66cd78e1 100644
--- a/spec/file_system_data_spec.rb
+++ b/spec/file_system_data_spec.rb
@@ -95,7 +95,7 @@
data[:en] = data[:en].merge!('en' => locale_data)
files = %w(pizza.en.json sushi.en.json)
expect(Dir['*.json'].sort).to eq(files.sort)
- files.each { |f| expect(JSON.parse(File.read f)['en']).to eq({File.basename(f, '.en.json') => keys}) }
+ files.each { |f| expect(JSON.parse(File.read(f, encoding: 'UTF-8'))['en']).to eq({File.basename(f, '.en.json') => keys}) }
}
end
end
diff --git a/spec/fixtures/app/controllers/events_controller.rb b/spec/fixtures/app/controllers/events_controller.rb
index 8be75c3a..459bc120 100644
--- a/spec/fixtures/app/controllers/events_controller.rb
+++ b/spec/fixtures/app/controllers/events_controller.rb
@@ -1,6 +1,9 @@
# coding: utf-8
class EventsController < ApplicationController
- def show
+ def create
+ end
+
+ def show()
redirect_to :edit, notice: I18n.t('cb.a')
# args are ignored
@@ -26,5 +29,11 @@ def show
# not missing
I18n.t "hash.#{stuff}.a"
+
+ # relative key
+ I18n.t(".success")
+ end
+
+ def update
end
end
diff --git a/spec/fixtures/app/views/index.html.slim b/spec/fixtures/app/views/index.html.slim
index 88e91ce6..d584c6e8 100644
--- a/spec/fixtures/app/views/index.html.slim
+++ b/spec/fixtures/app/views/index.html.slim
@@ -3,6 +3,7 @@
/ t(:fp_comment)
/ i18n-tasks-use t(:fn_comment)
= t 'only_in_es'
+#x = t 'not_a_comment'
p #{t('ca.a')} #{t 'ca.b'} #{t "ca.c"}
p #{t 'ca.d'} #{t 'ca.f', i: 'world'} #{t 'ca.e', i: 'world'}
p #{t 'missing_in_es.a'} #{t 'same_in_es.a'} #{t 'blank_in_es.a'}
@@ -25,3 +26,4 @@ p = t 'devise.a'
p = t :missing_symbol_key
p #{t :"missing_symbol.key_two"} #{t :'missing_symbol.key_three'}
= t 'present_in_es_but_not_en.a'
+= t 'latin_extra.çüéö'
\ No newline at end of file
diff --git a/spec/fixtures/config/i18n-tasks.yml b/spec/fixtures/config/i18n-tasks.yml
index 0fe6ca06..8370869b 100644
--- a/spec/fixtures/config/i18n-tasks.yml
+++ b/spec/fixtures/config/i18n-tasks.yml
@@ -32,7 +32,11 @@ search:
- '*.file'
# explicitly exclude files (default: blank = exclude no files)
exclude: '*.js'
- # search uses grep under the hood
+ # paths for relative key resolution:
+ relative_roots:
+ - app/views
+ - app/controllers
+
# do not report these keys ever
ignore:
diff --git a/spec/google_translate_spec.rb b/spec/google_translate_spec.rb
index 1fba2d32..3fec5080 100644
--- a/spec/google_translate_spec.rb
+++ b/spec/google_translate_spec.rb
@@ -18,7 +18,7 @@
if ENV['GOOGLE_TRANSLATE_API_KEY']
describe 'real world test' do
- delegate :i18n_cmd, :i18n_task, :in_test_app_dir, to: :TestCodebase
+ delegate :i18n_task, :in_test_app_dir, :run_cmd, to: :TestCodebase
context '#google_translate_list' do
it "works with #{tests.map(&:first)}" do
@@ -39,7 +39,6 @@
context 'command' do
let(:task) { i18n_task }
- let(:cmd) { i18n_cmd(task) }
it 'works' do
in_test_app_dir do
@@ -62,7 +61,7 @@
}
})
- cmd.run :translate_missing
+ run_cmd 'translate-missing'
expect(task.t('common.hello', 'es')).to eq(text_test[2])
expect(task.t('common.hello_html', 'es')).to eq(html_test[2])
expect(task.t('common.array_key', 'es')).to eq(array_test[2])
diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb
index c1b7aeef..71211b8a 100644
--- a/spec/i18n_spec.rb
+++ b/spec/i18n_spec.rb
@@ -1,4 +1,3 @@
-require 'spec_helper'
require 'i18n/tasks'
describe 'I18n' do
diff --git a/spec/i18n_tasks_spec.rb b/spec/i18n_tasks_spec.rb
index b3f7af0e..25e8538e 100644
--- a/spec/i18n_tasks_spec.rb
+++ b/spec/i18n_tasks_spec.rb
@@ -1,16 +1,38 @@
# coding: utf-8
require 'spec_helper'
require 'fileutils'
+require 'open3'
+# Integration tests
describe 'i18n-tasks' do
- delegate :run_cmd, :i18n_task, :in_test_app_dir, to: :TestCodebase
+ delegate :run_cmd, :run_cmd_capture_stderr, :i18n_task, :in_test_app_dir, to: :TestCodebase
+
+ describe 'bin/i18n-tasks' do
+ it 'shows help when invoked with no arguments, shows version on --version' do
+ # These bin/i18n-tasks tests are executed in parallel for performance
+ [
+ proc {
+ out, err, status = Open3.capture3('bin/i18n-tasks')
+ expect(status).to be_success
+ expect(out).to be_empty
+ expect(err).to start_with('Usage: i18n-tasks [command] [options]')
+ expect(err).to include('Available commands', 'add-missing')
+ },
+ proc {
+ expect(%x[bin/i18n-tasks --version].chomp).to eq(I18n::Tasks::VERSION)
+ }
+ ].map { |test| Thread.start(&test) }.each(&:join)
+ end
+ end
+
+ # Tests execute i18n-tasks using I18n::Tasks::CLI directly, via #run_cmd(task, *arguments).
+ # This avoid launching a process for each command.
describe 'health' do
it 'outputs stats' do
t = i18n_task
- stats = in_test_app_dir { t.forest_stats(t.data_forest t.locales) }
- out = capture_stderr { run_cmd :health }
- stats.values.each do |v|
+ out = run_cmd_capture_stderr('health')
+ in_test_app_dir { t.forest_stats(t.data_forest t.locales) }.values.each do |v|
expect(out).to include(v.to_s)
end
end
@@ -18,65 +40,72 @@
describe 'missing' do
let (:expected_missing_keys) {
- %w( en.used_but_missing.key en.relative.index.missing
+ %w( en.used_but_missing.key
+ en.relative.index.missing
es.missing_in_es.a
en.present_in_es_but_not_en.a
- en.hash.pattern_missing.a en.hash.pattern_missing.b
- en.missing_symbol_key en.missing_symbol.key_two en.missing_symbol.key_three
- es.missing_in_es_plural_1.a es.missing_in_es_plural_2.a
+ en.hash.pattern_missing.a
+ en.hash.pattern_missing.b
+ en.missing_symbol_key
+ en.missing_symbol.key_two
+ en.missing_symbol.key_three
+ es.missing_in_es_plural_1.a
+ es.missing_in_es_plural_2.a
en.missing-key-with-a-dash.key
en.missing-key-question?.key
- en.fn_comment en.only_in_es
+ en.fn_comment
+ en.only_in_es
+ en.events.show.success
)
}
it 'detects missing' do
- capture_stderr do
- expect(run_cmd :missing).to be_i18n_keys expected_missing_keys
- es_keys = expected_missing_keys.grep(/^es\./)
- # locale argument
- expect(run_cmd :missing, locales: %w(es)).to be_i18n_keys es_keys
- expect(run_cmd :missing, arguments: %w(es)).to be_i18n_keys es_keys
- end
+ es_keys = expected_missing_keys.grep(/^es\./)
+ expect(run_cmd 'missing').to be_i18n_keys expected_missing_keys
+ # locale argument
+ expect(run_cmd 'missing', '-les').to be_i18n_keys es_keys
+ expect(run_cmd 'missing', 'es').to be_i18n_keys es_keys
end
end
describe 'eq_base' do
- it 'detects eq_base' do
- capture_stderr do
- expect(run_cmd :eq_base).to be_i18n_keys %w(es.same_in_es.a)
- end
+ it 'detects eq-base' do
+ expect(run_cmd 'eq-base').to be_i18n_keys %w(es.same_in_es.a)
end
end
- let(:expected_unused_keys) { %w(unused.a unused.numeric unused.plural).map { |k| %w(en es).map { |l| "#{l}.#{k}" } }.reduce(:+) }
- let(:expected_unused_keys_strict) { expected_unused_keys + %w(hash.pattern.a hash.pattern2.a).map { |k| %w(en es).map { |l| "#{l}.#{k}" } }.reduce(:+) }
+ let(:expected_unused_keys) do
+ %w(unused.a unused.numeric unused.plural).map do |k|
+ %w(en es).map { |l| "#{l}.#{k}" }
+ end.reduce(:+)
+ end
+
+ let(:expected_unused_keys_strict) do
+ expected_unused_keys + %w(hash.pattern.a hash.pattern2.a).map do |k|
+ %w(en es).map { |l| "#{l}.#{k}" }
+ end.reduce(:+)
+ end
+
describe 'unused' do
it 'detects unused' do
- capture_stderr do
- expect(run_cmd :unused).to be_i18n_keys expected_unused_keys
- end
+ expect(run_cmd 'unused').to be_i18n_keys expected_unused_keys
end
it 'detects unused (--strict)' do
- capture_stderr do
- expect(run_cmd :unused, strict: true).to be_i18n_keys expected_unused_keys_strict
- end
+ expect(run_cmd 'unused', '--strict').to be_i18n_keys expected_unused_keys_strict
end
end
describe 'remove_unused' do
it 'removes unused' do
in_test_app_dir do
- t = i18n_task
+ t = i18n_task
unused = expected_unused_keys.map { |k| ::I18n::Tasks::SplitKey.split_key(k, 2)[1] }
unused.each do |key|
expect(t.key_value?(key, :en)).to be true
expect(t.key_value?(key, :es)).to be true
end
ENV['CONFIRM'] = '1'
- capture_stderr {
- run_cmd :remove_unused
- }
+ run_cmd 'remove-unused'
t.data.reload
unused.each do |key|
expect(t.key_value?(key, :en)).to be false
@@ -89,7 +118,7 @@
describe 'normalize' do
it 'sorts the keys' do
in_test_app_dir do
- run_cmd :normalize
+ run_cmd 'normalize'
en_yml_data = i18n_task.data.reload['en'].select_keys { |_k, node|
node.data[:path] == 'config/locales/en.yml'
}
@@ -105,7 +134,7 @@
it 'moves keys to the corresponding files as per data.write' do
in_test_app_dir {
expect(File).to_not exist 'config/locales/devise.en.yml'
- run_cmd :normalize, pattern_router: true
+ run_cmd 'normalize', '--pattern_router'
expect(YAML.load_file('config/locales/devise.en.yml')['en']['devise']['a']).to eq 'EN_TEXT'
}
end
@@ -114,7 +143,7 @@
describe 'xlsx_report' do
it 'saves' do
in_test_app_dir {
- capture_stderr { run_cmd :xlsx_report }
+ run_cmd 'xlsx-report'
expect(File).to exist 'tmp/i18n-report.xlsx'
FileUtils.cp('tmp/i18n-report.xlsx', '..')
}
@@ -127,7 +156,7 @@
in_test_app_dir {
expect(YAML.load_file('config/locales/en.yml')['en']['used_but_missing']).to be_nil
}
- run_cmd :add_missing, locales: 'base'
+ run_cmd 'add-missing', 'base'
in_test_app_dir {
expect(YAML.load_file('config/locales/en.yml')['en']['used_but_missing']['key']).to eq I18n.t('i18n_tasks.common.key')
expect(YAML.load_file('config/locales/en.yml')['en']['present_in_es_but_not_en']['a']).to eq 'ES_TEXT'
@@ -138,7 +167,7 @@
in_test_app_dir {
expect(YAML.load_file('config/locales/es.yml')['es']['missing_in_es']).to be_nil
}
- run_cmd :add_missing, locales: 'es'
+ run_cmd 'add-missing', 'es'
in_test_app_dir {
expect(YAML.load_file('config/locales/es.yml')['es']['missing_in_es']['a']).to eq 'EN_TEXT'
expect(YAML.load_file('config/locales/es.yml')['es']['missing_in_es_plural_1']['a']['one']).to eq 'EN_TEXT'
@@ -149,8 +178,8 @@
in_test_app_dir {
expect(YAML.load_file('config/locales/es.yml')['es']['missing_in_es']).to be_nil
}
- run_cmd :normalize, pattern_router: true
- run_cmd :add_missing, locales: 'all', value: 'TRME'
+ run_cmd 'normalize', '--pattern_router'
+ run_cmd 'add-missing', '-v', 'TRME'
in_test_app_dir {
expect(YAML.load_file('config/locales/es.yml')['es']['missing_in_es']['a']).to eq 'TRME'
expect(YAML.load_file('config/locales/devise.es.yml')['es']['devise']['a']).to eq 'ES_TEXT'
@@ -162,7 +191,7 @@
in_test_app_dir {
expect(YAML.load_file('config/locales/es.yml')['es']['missing_in_es']).to be_nil
}
- run_cmd :add_missing, locales: 'all', value: 'TRME %{value}'
+ run_cmd 'add-missing', '-v', 'TRME %{value}'
in_test_app_dir {
expect(YAML.load_file('config/locales/es.yml')['es']['missing_in_es']['a']).to eq 'TRME EN_TEXT'
expect(YAML.load_file('config/locales/en.yml')['en']['present_in_es_but_not_en']['a']).to eq 'TRME ES_TEXT'
@@ -172,7 +201,7 @@
describe 'config' do
it 'prints config' do
- expect(YAML.load(Term::ANSIColor.uncolor(run_cmd :config))).to(
+ expect(YAML.load(Term::ANSIColor.uncolor(run_cmd 'config'))).to(
eq(in_test_app_dir { i18n_task.config_for_inspect })
)
end
@@ -180,20 +209,17 @@
describe 'find' do
it 'prints usages' do
- capture_stderr do
- result = Term::ANSIColor.uncolor(run_cmd :find, arguments: ['used.*'])
- expect(result).to eq(<<-TXT)
+ result = Term::ANSIColor.uncolor(run_cmd 'find', 'used.*')
+ expect(result).to eq(<<-TXT)
used.a 2
app/views/usages.html.slim:1 p = t 'used.a'
app/views/usages.html.slim:2 b = t 'used.a'
- TXT
- end
+ TXT
end
end
-
# --- setup ---
- BENCH_KEYS = 10
+ BENCH_KEYS = ENV['BENCH_KEYS'].to_i
before(:each) do
gen_data = ->(v) {
v_num = v.chars.map(&:ord).join('').to_i
@@ -225,21 +251,25 @@
'devise' => {'a' => v},
'scoped' => {'x' => v},
'very' => {'scoped' => {'x' => v}},
- 'used' => {'a' => v}
+ 'used' => {'a' => v},
+ 'latin_extra' => {'çüéö' => v},
+ 'not_a_comment' => v
}.tap { |r|
- gen = r["bench"] = {}
- BENCH_KEYS.times { |i| gen["key#{i}"] = v }
+ if BENCH_KEYS > 0
+ gen = r['bench'] = {}
+ BENCH_KEYS.times { |i| gen["key#{i}"] = v }
+ end
}
}
- en_data = gen_data.('EN_TEXT')
- es_data = gen_data.('ES_TEXT').except(
- 'missing_in_es', 'missing_in_es_plural_1', 'missing_in_es_plural_2')
- es_data['same_in_es']['a'] = 'EN_TEXT'
- es_data['blank_in_es']['a'] = ''
- es_data['ignore_eq_base_all']['a'] = 'EN_TEXT'
- es_data['ignore_eq_base_es']['a'] = 'EN_TEXT'
- es_data['only_in_es'] = 1
+ en_data = gen_data.('EN_TEXT')
+ es_data = gen_data.('ES_TEXT').except('missing_in_es', 'missing_in_es_plural_1', 'missing_in_es_plural_2')
+
+ es_data['same_in_es']['a'] = 'EN_TEXT'
+ es_data['blank_in_es']['a'] = ''
+ es_data['ignore_eq_base_all']['a'] = 'EN_TEXT'
+ es_data['ignore_eq_base_es']['a'] = 'EN_TEXT'
+ es_data['only_in_es'] = 1
es_data['present_in_es_but_not_en'] = {'a' => 'ES_TEXT'}
fs = fixtures_contents.merge(
diff --git a/spec/locale_tree/siblings_spec.rb b/spec/locale_tree/siblings_spec.rb
index 46492180..684ca1e9 100644
--- a/spec/locale_tree/siblings_spec.rb
+++ b/spec/locale_tree/siblings_spec.rb
@@ -6,12 +6,32 @@
context 'Node' do
it '::new with children' do
children = I18n::Tasks::Data::Tree::Siblings.from_key_attr([['a', value: 1]])
- node = I18n::Tasks::Data::Tree::Node.new(
+ node = new_node(
key: 'fr',
children: children
)
expect(node.to_siblings.first.children.parent.key).to eq 'fr'
end
+
+ it '== (false by value)' do
+ expect(build_node({'a' => {'b' => {'c' => 1}}})).to_not(
+ eq(build_node({'a' => {'b' => {'c' => 2}}})))
+ end
+
+ it '== (false by key)' do
+ expect(build_node({'a' => {'b' => {'c' => 1}}})).to_not(
+ eq(build_node({'a' => {'b' => {'d' => 1}}})))
+ end
+
+ it '== (false by children)' do
+ expect(build_node({'a' => {'b' => {'c' => 1}}})).to_not(
+ eq(build_node({'a' => {'b' => {'c' => 1}, 'x' => 2}})))
+ end
+
+ it '== (true)' do
+ expect(build_node({'a' => {'b' => {'c' => 1}, 'x' => 2}})).to_not(
+ eq(build_node({'a' => {'b' => {'d' => 1}, 'x' => 2}})))
+ end
end
context 'a tree' do
@@ -55,7 +75,7 @@
it '#set conflict value <- scope' do
a = build_tree(a: 1)
- expect { silence_stderr { a.set('a.b', build_node(key: 'b', value: 1)) } }.to_not raise_error
+ expect { silence_stderr { a.set('a.b', new_node(key: 'b', value: 1)) } }.to_not raise_error
end
it '#intersect' do
@@ -72,18 +92,18 @@
end
it '#append!' do
- expect(build_tree({'a' => 1}).append!(build_node(key: 'b', value: 2)).to_hash).to eq('a' => 1, 'b' => 2)
+ expect(build_tree({'a' => 1}).append!(new_node(key: 'b', value: 2)).to_hash).to eq('a' => 1, 'b' => 2)
end
it '#set replace value' do
- expect(build_tree(a: {b: 1}).tap {|t| t['a.b'] = build_node(key: 'b', value: 2) }.to_hash).to(
+ expect(build_tree(a: {b: 1}).tap {|t| t['a.b'] = new_node(key: 'b', value: 2) }.to_hash).to(
eq('a' => {'b' => 2})
)
end
it '#set get' do
t = build_tree(a: {x: 1})
- node = build_node(key: 'd', value: 'e')
+ node = new_node(key: 'd', value: 'e')
t['a.b.c.' + node.key] = node
expect(t['a.b.c.d'].value).to eq('e')
end
diff --git a/spec/pattern_scanner_spec.rb b/spec/pattern_scanner_spec.rb
index 46d7304e..2a0e954e 100644
--- a/spec/pattern_scanner_spec.rb
+++ b/spec/pattern_scanner_spec.rb
@@ -2,21 +2,57 @@
require 'spec_helper'
describe 'Pattern Scanner' do
- describe 'default pattern' do
+ describe 'scan_file' do
+ it 'returns absolute keys from controllers' do
+ file_path = 'spec/fixtures/app/controllers/events_controller.rb'
+ scanner = I18n::Tasks::Scanners::PatternScanner.new
+ allow(scanner).to receive(:relative_roots).and_return(['spec/fixtures/app/controllers'])
+
+ keys = scanner.scan_file(file_path)
+
+ expect(keys).to include(
+ ["events.show.success",
+ {:data=>
+ {
+ :src_path=>"spec/fixtures/app/controllers/events_controller.rb",
+ :pos=>790,
+ :line_num=>34,
+ :line_pos=>10,
+ :line =>" I18n.t(\".success\")"}
+ }
+ ]
+ )
+ end
+ end
+
+ describe 'default_pattern' do
let!(:pattern) { I18n::Tasks::Scanners::PatternScanner.new.default_pattern }
- ['t "a.b"', "t 'a.b'", 't("a.b")', "t('a.b')",
- "t('a.b', :arg => val)", "t('a.b', arg: val)",
- "t :a_b", "t :'a.b'", 't :"a.b"', "t(:ab)", "t(:'a.b')", 't(:"a.b")',
- 'I18n.t("a.b")', 'I18n.translate("a.b")'].each do |s|
- it "matches #{s}" do
- expect(pattern).to match s
+ [
+ 't(".a.b")',
+ 't "a.b"',
+ "t 'a.b'",
+ 't("a.b")',
+ "t('a.b')",
+ "t('a.b', :arg => val)",
+ "t('a.b', arg: val)",
+ "t :a_b",
+ "t :'a.b'",
+ 't :"a.b"',
+ "t(:ab)",
+ "t(:'a.b')",
+ 't(:"a.b")',
+ 'I18n.t("a.b")',
+ 'I18n.translate("a.b")'
+ ].each do |string|
+ it "matches #{string}" do
+ expect(pattern).to match string
end
end
- ["t \"a.b'", "t a.b"].each do |s|
- it "does not match #{s}" do
- expect(pattern).to_not match s
+ ["t \"a.b'", "t a.b"].each do |string|
+ it "does not match #{string}" do
+ expect(pattern).to_not match string
end
end
end
diff --git a/spec/readme_spec.rb b/spec/readme_spec.rb
index 4ecb3fc6..550bdbb2 100644
--- a/spec/readme_spec.rb
+++ b/spec/readme_spec.rb
@@ -1,7 +1,7 @@
# coding: utf-8
-require 'spec_helper'
+
describe 'README.md' do
- let(:readme) { File.read('README.md') }
+ let(:readme) { File.read('README.md', encoding: 'UTF-8') }
it 'has valid YAML in ```yaml blocks' do
readme.scan /```yaml\n(.*)(?=^)\n```/ do |m|
expect { YAML.load(m[0]) }.to_not raise_errors
diff --git a/spec/relative_keys_spec.rb b/spec/relative_keys_spec.rb
index c6fe5f2d..c4b97af1 100644
--- a/spec/relative_keys_spec.rb
+++ b/spec/relative_keys_spec.rb
@@ -17,6 +17,43 @@
end
end
- end
+ context 'relative key in controller' do
+ it 'works' do
+ key = scanner.absolutize_key(
+ '.success',
+ 'app/controllers/users_controller.rb',
+ %w(app/controllers),
+ 'create'
+ )
+
+ expect(key).to eq('users.create.success')
+ end
+
+ context 'multiple words in controller name' do
+ it 'works' do
+ key = scanner.absolutize_key(
+ '.success',
+ 'app/controllers/admin_users_controller.rb',
+ %w(app/controllers),
+ 'create'
+ )
+ expect(key).to eq('admin_users.create.success')
+ end
+ end
+
+ context 'nested in module' do
+ it 'works' do
+ key = scanner.absolutize_key(
+ '.success',
+ 'app/controllers/nested/users_controller.rb',
+ %w(app/controllers),
+ 'create'
+ )
+
+ expect(key).to eq('nested.users.create.success')
+ end
+ end
+ end
+ end
end
diff --git a/spec/support/capture_std.rb b/spec/support/capture_std.rb
index 09502e97..d40193f3 100644
--- a/spec/support/capture_std.rb
+++ b/spec/support/capture_std.rb
@@ -15,4 +15,8 @@ def capture_stdout
ensure
$stdout = out
end
+
+ def silence_stderr(&block)
+ silence_stream($stderr, &block)
+ end
end
diff --git a/spec/support/i18n_tasks_output_matcher.rb b/spec/support/i18n_tasks_output_matcher.rb
index 76940a9b..184df2c3 100644
--- a/spec/support/i18n_tasks_output_matcher.rb
+++ b/spec/support/i18n_tasks_output_matcher.rb
@@ -15,7 +15,6 @@ def extract_keys(actual)
locale_col = 0
key_col = 1
actual.map { |row|
- key =
key = "#{row[locale_col]}.#{row[key_col]}"
key = key[0..-2] if key.end_with?(':')
key
diff --git a/spec/support/test_codebase.rb b/spec/support/test_codebase.rb
index ed9673b3..1fd0a364 100644
--- a/spec/support/test_codebase.rb
+++ b/spec/support/test_codebase.rb
@@ -3,6 +3,7 @@
require 'yaml'
require_relative 'capture_std'
require 'i18n/tasks/commands'
+require 'i18n/tasks/cli'
module TestCodebase
include CaptureStd
@@ -10,23 +11,29 @@ module TestCodebase
AT = 'tmp/test_codebase'
def i18n_task(*args)
- TestCodebase.in_test_app_dir do
+ in_test_app_dir do
::I18n::Tasks::BaseTask.new(*args)
end
end
- def i18n_cmd(*args)
- TestCodebase.in_test_app_dir do
- ::I18n::Tasks::Commands.new(*args)
- end
+ def run_cmd(name, *args)
+ capture_stdout { capture_stderr { in_test_app_dir {
+ run_cli(name, *args)
+ } } }
end
- def run_cmd(name, *args, &block)
- in_test_app_dir do
- silence_stderr {
- capture_stdout { i18n_cmd.run(name, *args, &block) }
- }
- end
+ def run_cmd_capture_stderr(name, *args)
+ capture_stderr { capture_stdout { in_test_app_dir {
+ run_cli(name, *args)
+ } } }
+ end
+
+ def run_cli(name, *args)
+ i18n_cli.run([name, *args])
+ end
+
+ def i18n_cli
+ in_test_app_dir { ::I18n::Tasks::CLI.new }
end
def setup(files = {})
@@ -51,13 +58,13 @@ def rake_result(task, *args)
}
end
- def in_test_app_dir(&block)
- return block.call if @in_dir
+ def in_test_app_dir
+ return yield if @in_dir
begin
pwd = Dir.pwd
Dir.chdir AT
@in_dir = true
- block.call
+ yield
ensure
Dir.chdir pwd
@in_dir = false
diff --git a/spec/support/trees.rb b/spec/support/trees.rb
index 6e1df886..e5887f3d 100644
--- a/spec/support/trees.rb
+++ b/spec/support/trees.rb
@@ -10,6 +10,12 @@ def build_tree(hash)
end
def build_node(attr = {})
+ raise 'invalid node (more than 1 root)' if attr.size > 1
+ key, value = attr.first
+ I18n::Tasks::Data::Tree::Node.from_key_value(key, value)
+ end
+
+ def new_node(attr = {})
I18n::Tasks::Data::Tree::Node.new(attr)
end
end
diff --git a/templates/config/i18n-tasks.yml b/templates/config/i18n-tasks.yml
index 8d97b7da..137483b0 100644
--- a/templates/config/i18n-tasks.yml
+++ b/templates/config/i18n-tasks.yml
@@ -1,75 +1,88 @@
-# i18n-tasks finds and manages missing and unused translations https://github.com/glebm/i18n-tasks
+# i18n-tasks finds and manages missing and unused translations: https://github.com/glebm/i18n-tasks
+# The "main" locale.
base_locale: en
-## i18n-tasks detects locales automatically from the existing locale files
-## uncomment to set locales explicitly
-# locales: [en, es, fr]
+## All available locales are inferred from the data by default. Alternatively, specify them explicitly:
+# locales: [es, fr]
+## Reporting locale, default: en. Available: en, ru.
+# internal_locale: en
-## i18n-tasks report locale, default: en, available: en, ru
-# internal_locale: ru
-
-# Read and write locale data
+# Read and write translations.
data:
- ## by default, translation data are read from the file system, or you can provide a custom data adapter
+ ## Translations are read from the file system. Supported format: YAML, JSON.
+ ## Provide a custom adapter:
# adapter: I18n::Tasks::Data::FileSystem
- # Locale files to read from
+ # Locale files or `File.find` patterns where translations are read from:
read:
- - config/locales/%{locale}.yml
- # - config/locales/*.%{locale}.yml
+ ## Default:
+ # - config/locales/%{locale}.yml
+ ## More files:
# - config/locales/**/*.%{locale}.yml
+ ## Another gem:
+ # - "<%= %x[bundle show vagrant].chomp %>/templates/locales/%{locale}.yml"
- # key => file routes, matched top to bottom
+ # Locale files to write new keys to, based on a list of key pattern => file rules. Matched from top to bottom:
+ # `i18n-tasks normalize -p` will force move the keys according to these rules
write:
- ## E.g., write devise and simple form keys to their respective files
+ ## For example, write devise and simple form keys to their respective files:
# - ['{devise, simple_form}.*', 'config/locales/\1.%{locale}.yml']
- # Catch-all
- - config/locales/%{locale}.yml
- # `i18n-tasks normalize -p` will force move the keys according to these rules
+ ## Catch-all default:
+ # - config/locales/%{locale}.yml
+
+ ## Specify the router (see Readme for details). Valid values: conservative_router, pattern_router, or a custom class.
+ # router: convervative_router
- # YAML / JSON serializer options, passed to load / dump / parse / serialize
yaml:
write:
# do not wrap lines at 80 characters
line_width: -1
- json:
- write:
- # pretty print JSON
- indent: ' '
- space: ' '
- object_nl: "\n"
- array_nl: "\n"
+
+ ## Pretty-print JSON:
+ # json:
+ # write:
+ # indent: ' '
+ # space: ' '
+ # object_nl: "\n"
+ # array_nl: "\n"
# Find translate calls
search:
- ## Default scanner finds t() and I18n.t() calls
- # scanner: I18n::Tasks::Scanners::PatternWithScopeScanner
+ ## Paths or `File.find` patterns to search in:
+ # paths:
+ # - app/
- ## Paths to search in, passed to File.find
- paths:
- - app/
-
- ## Root for resolving relative keys (default)
+ ## Root directories for relative keys resolution.
# relative_roots:
# - app/views
-
- ## File.fnmatch patterns to exclude from search (default)
- # exclude: ["*.jpg", "*.png", "*.gif", "*.svg", "*.ico", "*.eot", "*.ttf", "*.woff", "*.pdf"]
-
- ## Or, File.fnmatch patterns to include
+ # - app/controllers
+ # - app/helpers
+ # - app/presenters
+
+ ## Files or `File.fnmatch` patterns to exclude from search. Some files are always excluded regardless of this setting:
+ ## %w(*.jpg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less *.yml *.json)
+ exclude:
+ - app/assets/images
+ - app/assets/fonts
+
+ ## Alternatively, the only files or `File.fnmatch patterns` to search in `paths`:
+ ## If specified, this settings takes priority over `exclude`, but `exclude` still applies.
# include: ["*.rb", "*.html.slim"]
+ ## Default scanner finds t() and I18n.t() calls.
+ # scanner: I18n::Tasks::Scanners::PatternWithScopeScanner
+
## Google Translate
# translation:
# # Get an API key and set billing info at https://code.google.com/apis/console to use Google Translate
# api_key: "AbC-dEf5"
-## Consider these keys not missing
+## Do not consider these keys missing:
# ignore_missing:
# - 'errors.messages.{accepted,blank,invalid,too_short,too_long}'
# - '{devise,simple_form}.*'
-## Consider these keys used
+## Consider these keys used:
# ignore_unused:
# - 'activerecord.attributes.*'
# - '{devise,kaminari,will_paginate}.*'
@@ -77,13 +90,13 @@ search:
# - 'simple_form.{placeholders,hints,labels}.*'
# - 'simple_form.{error_notification,required}.:'
-## Exclude these keys from `i18n-tasks eq-base' report
+## Exclude these keys from the `i18n-tasks eq-base' report:
# ignore_eq_base:
# all:
# - common.ok
# fr,es:
# - common.brand
-## Exclude these keys from all of the reports
+## Ignore these keys completely:
# ignore:
# - kaminari.*
diff --git a/templates/rspec/i18n_spec.rb b/templates/rspec/i18n_spec.rb
index c1b7aeef..71211b8a 100644
--- a/templates/rspec/i18n_spec.rb
+++ b/templates/rspec/i18n_spec.rb
@@ -1,4 +1,3 @@
-require 'spec_helper'
require 'i18n/tasks'
describe 'I18n' do