diff --git a/.travis.yml b/.travis.yml index 4bfaa152..58b8ec9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ language: ruby rvm: -- 2.1.1 +- 2.1.3 - 2.0.0 - 1.9.3 -- jruby-1.7.13 +- jruby - rbx-2.2.10 env: global: diff --git a/CHANGES.md b/CHANGES.md index 6187030f..9f20234e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,19 @@ +## 0.7.8 + +* Fix Google Translate issues with non-string keys [#100](https://github.com/glebm/i18n-tasks/pull/100) +* Fix an issue with certain HAML not being parsed [#96](https://github.com/glebm/i18n-tasks/issues/96) [#102](https://github.com/glebm/i18n-tasks/pull/102) +* Fix other minor issues + +## 0.7.7 + +* Fix regression: keys are sorted once again [#92](https://github.com/glebm/i18n-tasks/issues/92). + +## 0.7.6 + +* Add a post-install notice with setup commands +* Fix a small typo in the config template [#91](https://github.com/glebm/i18n-tasks/pull/91). +* Fix `find` crashing on relative keys (regression) + ## 0.7.5 Dynamic key usage inference fixes by [Mikko Koski](https://github.com/rap1ds): diff --git a/README.md b/README.md index 85783186..ea28dc09 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,28 @@ -# i18n-tasks [![Build Status][badge-travis]][travis] [![Coverage Status][badge-coverage]][coverage] [![Code Climate][badge-code-climate]][code-climate] [![Gemnasium][badge-gemnasium]][gemnasium] +# i18n-tasks [![Build Status][badge-travis]][travis] [![Coverage Status][badge-coverage]][coverage] [![Code Climate][badge-code-climate]][code-climate] [![Gemnasium][badge-gemnasium]][gemnasium] [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/glebm/i18n-tasks?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) i18n-tasks helps you find and manage missing and unused translations. -## What? - -i18n-tasks scans calls such as `I18n.t('some.key')` and provides reports on key usage, missing, and unused keys. -i18n-tasks can also can pre-fill missing keys, including from Google Translate, and it can remove unused keys as well. + -## Why? +This gem analyses code statically for key usages, such as `I18n.t('some.key')`, in order to: -The default approach to locale data management with gems such as [i18n][i18n-gem] is flawed. -If you use a key that does not exist, this will only blow up at runtime. Keys left over from removed code accumulate -in the resource files and introduce unnecessary overhead on the translators. Translation files can quickly turn to disarray. +* Report keys that are missing or unused. +* Pre-fill missing keys, optionally from Google Translate. +* Remove unused keys. -i18n-tasks improves this by analysing code statically, without running it. It scans calls such as `I18n.t('some.key')` and provides reports on key usage, missing, and unused keys. -It can also pre-fill missing keys, including from Google Translate, and it can remove unused keys as well. +Thus addressing the two main problems of [i18n gem][i18n-gem] design: -i18n-tasks can be used with any project using [i18n][i18n-gem] (default in Rails), or similar, even if it isn't ruby. - - +* Missing keys only blow up at runtime. +* Keys no longer in use may accumulate and introduce overhead, without you knowing it. ## Installation -Add to Gemfile: +i18n-tasks can be used with any project using [i18n][i18n-gem] (default in Rails), or similar, even if it isn't ruby. + +Add it to the Gemfile: ```ruby -gem 'i18n-tasks', '~> 0.7.5' +gem 'i18n-tasks', '~> 0.7.8' ``` Copy default [configuration file](#configuration) (optional): @@ -117,7 +114,7 @@ Sort the keys: $ i18n-tasks normalize ``` -Sort the keys, and move them to the respective files as defined by (`config.write`)[#multiple-locale-files]: +Sort the keys, and move them to the respective files as defined by [`config.write`](#multiple-locale-files): ```console $ i18n-tasks normalize -p @@ -411,7 +408,7 @@ Add a custom task like the ones defined by the gem: ```ruby # my_commands.rb -class MyCommands +module MyCommands include ::I18n::Tasks::Command::Collection cmd :my_task, desc: 'my custom task' def my_task(opts = {}) @@ -422,7 +419,7 @@ end ```yaml # config/i18n-tasks.yml <% - require 'my_commands' + require './my_commands' I18n::Tasks::Commands.send :include, MyCommands %> ``` diff --git a/bin/i18n-tasks b/bin/i18n-tasks index bc3945fc..471bb404 100755 --- a/bin/i18n-tasks +++ b/bin/i18n-tasks @@ -13,10 +13,10 @@ require 'i18n/tasks/commands' require 'slop' err = proc { |message, exit_code| - if STDERR.isatty - STDERR.puts Term::ANSIColor.yellow('i18n-tasks: ' + message) + if $stderr.isatty + $stderr.puts Term::ANSIColor.yellow('i18n-tasks: ' + message) else - STDERR.puts message + $stderr.puts message end exit exit_code } diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index cfae6019..725418ac 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -1,5 +1,8 @@ # i18n-tasks works on itself! this is the internal config +# This is not the default config for new apps, but is the internal config for i18n-tasks to analyze itself. +# 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 @@ -22,7 +25,7 @@ data: # 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}'] + # - ['{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 @@ -59,10 +62,6 @@ search: ## Or, File.fnmatch patterns to include # include: ["*.rb", "*.html.slim"] - ## Lines starting with # or / are ignored by default - # ignore_lines: - # - "^\\s*[#/](?!\\si18n-tasks-use)" - ## Google Translate # translation: # # Get an API key and set billing info at https://code.google.com/apis/console to use Google Translate diff --git a/config/locales/en.yml b/config/locales/en.yml index 0507de9b..f010fe10 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,102 +1,103 @@ --- en: i18n_tasks: - common: - locale: Locale - type: Type - key: Key - value: Value - base_value: Base Value - details: Details - continue_q: Continue? - n_more: "%{count} more" - google_translate: - errors: - no_results: Google Translate returned no results. Make sure billing information is set at - https://code.google.com/apis/console. - remove_unused: - confirm: - one: One translations will be removed from %{locales}. - other: "%{count} translation will be removed from %{locales}." - removed: Removed %{count} keys - noop: No unused keys to remove - translate_missing: - translated: Translated %{count} keys add_missing: added: Added %{count} keys - unused: - none: Every translation is in use. - missing: - none: No translations are missing. - usages: - none: No key usages found. - health: - no_keys_detected: No keys detected. Check data.read in config/i18n-tasks.yml. - data_stats: - title: Forest (%{locales}) - text: has %{key_count} keys across %{locale_count} locales. On average, values are %{value_chars_avg} - characters long, keys have %{key_segments_avg} segments, a locale has %{per_locale_avg} keys. - text_single_locale: has %{key_count} keys in total. On average, values are %{value_chars_avg} - characters long, keys have %{key_segments_avg} segments. cmd: - encourage: - - Good job! - - Well done! - - Perfect! + args: + default_all: 'Default: all' + default_text: 'Default: %{value}' + desc: + confirm: Confirm automatically + data_format: 'Data format: %{valid_text}. %{default_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' + 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}.' + 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}' desc: - normalize: 'normalize translation data: sort and move to the right files' + add_missing: add missing keys to locale data + config: display i18n-tasks configuration data: show locale data data_merge: merge locale data with trees - data_write: replace locale data with tree data_remove: remove keys present in tree from data - health: is everything OK? + data_write: replace locale data with tree + eq_base: show translations equal to base value find: show where keys are used in the code - unused: show unused translations + gem_path: show path to the gem + health: is everything OK? + irb: start REPL session within i18n-tasks context missing: show missing translations - translate_missing: translate missing keys with Google Translate - add_missing: add missing keys to locale data + normalize: 'normalize translation data: sort and move to the right files' remove_unused: remove unused keys - eq_base: show translations equal to base value - tree_translate: Google Translate a tree to root locales - tree_merge: merge trees + translate_missing: translate missing keys with Google Translate + tree_convert: convert tree between formats tree_filter: filter tree by key pattern + tree_merge: merge trees tree_rename_key: rename tree node - tree_subtract: tree A minus the keys in tree B tree_set_value: set values of keys, optionally match a pattern - tree_convert: convert tree between formats - config: display i18n-tasks configuration - gem_path: show path to the gem - irb: start REPL session within i18n-tasks context + tree_subtract: tree A minus the keys in tree B + tree_translate: Google Translate a tree to root locales + unused: show unused translations xlsx_report: save missing and unused translations to an Excel file - args: - default_text: 'Default: %{value}' - default_all: 'Default: all' - desc: - out_format: 'Output format: %{valid_text}. %{default_text}.' - data_format: 'Data format: %{valid_text}. %{default_text}.' - keys: List of keys separated by commas (,), spaces, or newlines. - locales_filter: 'Comma-separated list of locale(s) to process. Default: all. Special: base.' - locale: 'Locale. Default: base' - locale_to_translate_from: 'Locale to translate from (default: base)' - confirm: Confirm automatically - nostdin: Do not read from stdin - strict: Do not infer dynamic key usage such as `t("category.\#{category.name}")` - missing_types: 'Filter by types: %{valid}. Default: all' - key_pattern: Filter by key pattern (e.g. 'common.*') - key_pattern_to_rename: Full key (pattern) to rename. Required - new_key_name: New name, interpolates original name as %{key}. Required - value: 'Value. Interpolates: %{value}, %{human_key}, %{value_or_human_key}' - pattern_router: 'Use pattern router: keys moved per config data.write' - enum_opt: - desc: "%{valid_text}. %{default_text}" - invalid: "%{invalid} is not one of: %{valid}." + encourage: + - Good job! + - 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: - pass_forest: Pass locale forest - invalid_locale: Invalid locale %{invalid} invalid_format: 'Unknown 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 + common: + base_value: Base Value + continue_q: Continue? + details: Details + key: Key + locale: Locale + n_more: "%{count} more" + type: Type + value: Value + data_stats: + text: has %{key_count} keys across %{locale_count} locales. On average, values are %{value_chars_avg} + characters long, keys have %{key_segments_avg} segments, a locale has %{per_locale_avg} keys. + text_single_locale: has %{key_count} keys in total. On average, values are %{value_chars_avg} + characters long, keys have %{key_segments_avg} segments. + title: Forest (%{locales}) + google_translate: + errors: + no_api_key: Set Google API key via GOOGLE_TRANSLATE_API_KEY environment variable or translation.api_key + in config/i18n-tasks.yml. Get the key at https://code.google.com/apis/console. + no_results: Google Translate returned no results. Make sure billing information is set at + https://code.google.com/apis/console. + health: + no_keys_detected: No keys detected. Check data.read in config/i18n-tasks.yml. + missing: + none: No translations are missing. + remove_unused: + confirm: + one: One translations will be removed from %{locales}. + other: "%{count} translation will be removed from %{locales}." + noop: No unused keys to remove + removed: Removed %{count} keys + translate_missing: + translated: Translated %{count} keys + unused: + none: Every translation is in use. + usages: + none: No key usages found. diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 56d630c9..e103e3bd 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1,103 +1,104 @@ --- ru: i18n_tasks: - common: - locale: "Язык" - type: "Тип" - key: "Ключ" - value: "Значение" - base_value: "Исходное значение" - details: "Детали" - continue_q: "Продолжить?" - n_more: "ещё %{count}" - google_translate: - errors: - no_results: Google Translate не дал результатов. Убедитесь в том, что платежная информация - добавлена в в https://code.google.com/apis/console. - remove_unused: - confirm: - one: "Один перевод будут удалён из %{locales}." - other: "Переводы (%{count}) будут удалены из %{locales}." - removed: "Удалены ключи (%{count})" - noop: "Нет неиспользуемых ключей" - translate_missing: - translated: "Переведены ключи (%{count})" add_missing: added: "Добавлены ключи (%{count})" - unused: - none: "Все переводы используются." - missing: - none: "Всё переведено." - usages: - none: "Не найдено использований." - health: - no_keys_detected: "Ключи не обнаружены. Проверьте data.read в config/i18n-tasks.yml." - data_stats: - title: "Данные (%{locales}):" - text: "%{key_count} ключей в %{locale_count} языках. В среднем, длина строки: %{value_chars_avg}, - сегменты ключей: %{key_segments_avg}, ключей в языке %{per_locale_avg}." - text_single_locale: "%{key_count} ключей. В среднем, длина строки: %{value_chars_avg}, сегменты - ключей: %{key_segments_avg}." cmd: - encourage: - - "Хорошая работа!" - - "Отлично!" - - "Прекрасно!" + args: + default_all: "По умолчанию: все" + default_text: "По умолчанию: %{value}" + desc: + confirm: "Подтвердить автоматом" + data_format: "Формат данных: %{valid_text}. %{default_text}." + key_pattern: "Маска ключа (например, common.*)" + key_pattern_to_rename: "Полный ключ (шаблон) для переименования. Необходимый параметр." + locale: "Язык. По умолчанию: base" + locale_to_translate_from: "Язык, с которого переводить (по умолчанию: base)" + locales_filter: "Список языков для обработки, разделенный запятыми (,). По умолчанию: все. + Специальное значение: base." + missing_types: "Типы недостающих переводов: %{valid}. По умолчанию: все" + new_key_name: "Новое имя, интерполирует оригинальное название как %{key}. Необходимый параметр." + nostdin: "Не читать дерево из стандартного ввода" + out_format: "Формат вывода: %{valid_text}. %{default_text}." + pattern_router: "Использовать pattern_router: ключи распределятся по файлам согласно data.write" + strict: Не угадывать динамические использования ключей, например `t("category.#{category.key}")` + value: "Значение, интерполируется с %{value}, %{human_key}, %{value_or_human_key}" desc: - normalize: "нормализовать файлы переводов (сортировка и распределение)" + add_missing: "добавить недостающие ключи к переводам" + config: "показать конфигурацию" data: "показать данные переводов" data_merge: "добавить дерево к переводам" - data_write: "заменить переводы деревом" data_remove: "удалить ключи, которые есть в дереве, из данных" - health: "Всё ОК?" + data_write: "заменить переводы деревом" + eq_base: "показать переводы, равные значениям в основном языке" find: "показать, где ключи используются в коде" - unused: "показать неиспользуемые переводы" + gem_path: "показать путь к ruby gem" + health: "Всё ОК?" + irb: "начать REPL сессию в контексте i18n-tasks" missing: "показать недостающие переводы" - translate_missing: "перевести недостающие переводы с Google Translate" - add_missing: "добавить недостающие ключи к переводам" + normalize: "нормализовать файлы переводов (сортировка и распределение)" remove_unused: "удалить неиспользуемые ключи" - eq_base: "показать переводы, равные значениям в основном языке" - tree_merge: "объединенить деревья" + translate_missing: "перевести недостающие переводы с Google Translate" + tree_convert: "преобразовать дерево между форматами" tree_filter: "фильтровать дерево по ключу" + tree_merge: "объединенить деревья" tree_rename_key: "переименовать узел дерева" - tree_subtract: "дерево A минус ключи в дереве B" tree_set_value: "заменить значения ключей" - tree_convert: "преобразовать дерево между форматами" - config: "показать конфигурацию" - gem_path: "показать путь к ruby gem" - irb: "начать REPL сессию в контексте i18n-tasks" - xlsx_report: "сохранить недостающие и неиспользуемые переводы в Excel-файл" + tree_subtract: "дерево A минус ключи в дереве B" tree_translate: "Перевести дерево при помощи Google Translate на язык корневых узлов" - args: - default_text: "По умолчанию: %{value}" - default_all: "По умолчанию: все" - desc: - out_format: "Формат вывода: %{valid_text}. %{default_text}." - data_format: "Формат данных: %{valid_text}. %{default_text}." - keys: "Список ключей, разделенных запятыми (,), пробелами или символами новой строки." - locales_filter: "Список языков для обработки, разделенный запятыми (,). По умолчанию: все. - Специальное значение: base." - locale_to_translate_from: "Язык, с которого переводить (по умолчанию: base)" - locale: "Язык. По умолчанию: base" - confirm: "Подтвердить автоматом" - nostdin: "Не читать дерево из стандартного ввода" - strict: Не угадывать динамические использования ключей, например `t("category.#{category.key}")` - missing_types: "Типы недостающих переводов: %{valid}. По умолчанию: все" - key_pattern: "Маска ключа (например, common.*)" - key_pattern_to_rename: "Полный ключ (шаблон) для переименования. Необходимый параметр." - new_key_name: "Новое имя, интерполирует оригинальное название как %{key}. Необходимый параметр." - value: "Значение, интерполируется с %{value}, %{human_key}, %{value_or_human_key}" - pattern_router: "Использовать pattern_router: ключи распределятся по файлам согласно data.write" - enum_opt: - desc: "%{valid_text}. %{default_text}" - invalid: "%{invalid} не является одним из: %{valid}." + unused: "показать неиспользуемые переводы" + xlsx_report: "сохранить недостающие и неиспользуемые переводы в Excel-файл" + encourage: + - "Хорошая работа!" + - "Отлично!" + - "Прекрасно!" enum_list_opt: desc: "Разделенных запятыми список: %{valid_text}. %{default_text}" invalid: "%{invalid} не в: %{valid}." + enum_opt: + desc: "%{valid_text}. %{default_text}" + invalid: "%{invalid} не является одним из: %{valid}." errors: - pass_forest: "Передайте дерево" - invalid_locale: "Неверный язык %{invalid}" invalid_format: "Неизвестный формат %{invalid}. Форматы: %{valid}." + invalid_locale: "Неверный язык %{invalid}" invalid_missing_type: one: "Неизвестный тип %{invalid}. Типы: %{valid}." other: "Неизвестные типы: %{invalid}. Типы: %{valid}." + pass_forest: "Передайте дерево" + common: + base_value: "Исходное значение" + continue_q: "Продолжить?" + details: "Детали" + key: "Ключ" + locale: "Язык" + n_more: "ещё %{count}" + type: "Тип" + value: "Значение" + data_stats: + text: "%{key_count} ключей в %{locale_count} языках. В среднем, длина строки: %{value_chars_avg}, + сегменты ключей: %{key_segments_avg}, ключей в языке %{per_locale_avg}." + text_single_locale: "%{key_count} ключей. В среднем, длина строки: %{value_chars_avg}, сегменты + ключей: %{key_segments_avg}." + 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_results: Google Translate не дал результатов. Убедитесь в том, что платежная информация + добавлена в https://code.google.com/apis/console. + health: + no_keys_detected: "Ключи не обнаружены. Проверьте data.read в config/i18n-tasks.yml." + missing: + none: "Всё переведено." + remove_unused: + confirm: + one: "Один перевод будут удалён из %{locales}." + other: "Переводы (%{count}) будут удалены из %{locales}." + noop: "Нет неиспользуемых ключей" + removed: "Удалены ключи (%{count})" + translate_missing: + translated: "Переведены ключи (%{count})" + unused: + none: "Все переводы используются." + usages: + none: "Не найдено использований." diff --git a/i18n-tasks.gemspec b/i18n-tasks.gemspec index 10cfa8db..be0b72f1 100644 --- a/i18n-tasks.gemspec +++ b/i18n-tasks.gemspec @@ -9,12 +9,18 @@ Gem::Specification.new do |s| s.authors = ['glebm'] s.email = ['glex.spb@gmail.com'] s.summary = %q{Manage localization and translation with the awesome power of static analysis} - s.description = %q{ + s.description = <<-TEXT i18n-tasks helps you find and manage missing and unused translations. -It scans calls such as `I18n.t('some.key')` and provides reports on key usage, missing, and unused keys. -It can also can pre-fill missing keys, including from Google Translate, and it can remove unused keys as well. -} +It analyses code statically for key usages, such as `I18n.t('some.key')`, in order to report keys that are missing or unused, +pre-fill missing keys (optionally from Google Translate), and remove unused keys. +TEXT + s.post_install_message = <<-TEXT +# Install default configuration: +cp $(i18n-tasks gem-path)/templates/config/i18n-tasks.yml config/ +# Add an RSpec for missing and unused keys: +cp $(i18n-tasks gem-path)/templates/rspec/i18n_spec.rb spec/ +TEXT s.homepage = 'https://github.com/glebm/i18n-tasks' if s.respond_to?(:metadata=) s.metadata = { 'issue_tracker' => 'https://github.com/glebm/i18n-tasks' } diff --git a/lib/i18n/tasks/command/collection.rb b/lib/i18n/tasks/command/collection.rb index 6e5b3a66..2e6e3b95 100644 --- a/lib/i18n/tasks/command/collection.rb +++ b/lib/i18n/tasks/command/collection.rb @@ -7,7 +7,7 @@ module Command module Collection def self.included(base) base.module_eval do - extend Command::DSL + include Command::DSL include Command::Options::Common include Command::Options::Locales include Command::Options::Trees diff --git a/lib/i18n/tasks/command/commands/data.rb b/lib/i18n/tasks/command/commands/data.rb index c71520bc..c6d45517 100644 --- a/lib/i18n/tasks/command/commands/data.rb +++ b/lib/i18n/tasks/command/commands/data.rb @@ -7,13 +7,13 @@ module Data cmd_opt :pattern_router, { short: :p, long: :pattern_router, - desc: proc { I18n.t('i18n_tasks.cmd.args.desc.pattern_router') }, + desc: t('i18n_tasks.cmd.args.desc.pattern_router'), conf: {argument: false, optional: true} } cmd :normalize, args: '[locale ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.normalize') }, + desc: t('i18n_tasks.cmd.desc.normalize'), opt: cmd_opts(:locales, :pattern_router) def normalize(opt = {}) @@ -22,7 +22,7 @@ def normalize(opt = {}) cmd :data, args: '[locale ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.data') }, + desc: t('i18n_tasks.cmd.desc.data'), opt: cmd_opts(:locales, :out_format) def data(opt = {}) @@ -31,7 +31,7 @@ def data(opt = {}) cmd :data_merge, args: '[tree ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.data_merge') }, + desc: t('i18n_tasks.cmd.desc.data_merge'), opt: cmd_opts(:data_format, :nostdin) def data_merge(opt = {}) @@ -42,7 +42,7 @@ def data_merge(opt = {}) cmd :data_write, args: '[tree]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.data_write') }, + desc: t('i18n_tasks.cmd.desc.data_write'), opt: cmd_opts(:data_format, :nostdin) def data_write(opt = {}) @@ -53,7 +53,7 @@ def data_write(opt = {}) cmd :data_remove, args: '[tree]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.data_remove') }, + desc: t('i18n_tasks.cmd.desc.data_remove'), opt: cmd_opts(:data_format, :nostdin) def data_remove(opt = {}) diff --git a/lib/i18n/tasks/command/commands/eq_base.rb b/lib/i18n/tasks/command/commands/eq_base.rb index 662bfe91..6cd00a40 100644 --- a/lib/i18n/tasks/command/commands/eq_base.rb +++ b/lib/i18n/tasks/command/commands/eq_base.rb @@ -6,7 +6,7 @@ module EqBase cmd :eq_base, args: '[locale ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.eq_base') }, + desc: t('i18n_tasks.cmd.desc.eq_base'), opt: cmd_opts(:locales, :out_format) def eq_base(opt = {}) diff --git a/lib/i18n/tasks/command/commands/health.rb b/lib/i18n/tasks/command/commands/health.rb index f6c42172..693cfad3 100644 --- a/lib/i18n/tasks/command/commands/health.rb +++ b/lib/i18n/tasks/command/commands/health.rb @@ -6,14 +6,14 @@ module Health cmd :health, args: '[locale ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.health') }, + desc: t('i18n_tasks.cmd.desc.health'), opt: cmd_opts(:locales, :out_format) def health(opt = {}) forest = i18n.data_forest(opt[:locales]) stats = i18n.forest_stats(forest) if stats[:key_count].zero? - raise CommandError.new I18n.t('i18n_tasks.health.no_keys_detected') + raise CommandError.new t('i18n_tasks.health.no_keys_detected') end terminal_report.forest_stats forest, stats missing opt diff --git a/lib/i18n/tasks/command/commands/meta.rb b/lib/i18n/tasks/command/commands/meta.rb index 99ae424f..a789db2e 100644 --- a/lib/i18n/tasks/command/commands/meta.rb +++ b/lib/i18n/tasks/command/commands/meta.rb @@ -6,7 +6,7 @@ module Meta cmd :config, args: '[section ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.config') } + desc: t('i18n_tasks.cmd.desc.config') def config(opts = {}) cfg = i18n.config_for_inspect @@ -17,13 +17,13 @@ def config(opts = {}) puts cfg end - cmd :gem_path, desc: proc { I18n.t('i18n_tasks.cmd.desc.gem_path') } + cmd :gem_path, desc: t('i18n_tasks.cmd.desc.gem_path') def gem_path puts I18n::Tasks.gem_path end - cmd :irb, desc: I18n.t('i18n_tasks.cmd.desc.irb') + cmd :irb, desc: t('i18n_tasks.cmd.desc.irb') def irb require 'i18n/tasks/console_context' diff --git a/lib/i18n/tasks/command/commands/missing.rb b/lib/i18n/tasks/command/commands/missing.rb index f969dda5..fbab1ffd 100644 --- a/lib/i18n/tasks/command/commands/missing.rb +++ b/lib/i18n/tasks/command/commands/missing.rb @@ -7,12 +7,12 @@ module Missing 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| I18n.t('i18n_tasks.cmd.args.desc.missing_types', valid: valid, default: default) }, - proc { |invalid, valid| I18n.t('i18n_tasks.cmd.errors.invalid_missing_type', invalid: invalid * ', ', valid: valid * ', ', count: invalid.length) }) + 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) }) cmd :missing, args: '[locale ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.missing') }, + desc: t('i18n_tasks.cmd.desc.missing'), opt: cmd_opts(:locales, :out_format, :missing_types) def missing(opt = {}) @@ -21,14 +21,14 @@ def missing(opt = {}) cmd :translate_missing, args: '[locale ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.translate_missing') }, + desc: t('i18n_tasks.cmd.desc.translate_missing'), opt: cmd_opts(:locales, :locale_to_translate_from) << cmd_opt(:out_format).except(:short) def translate_missing(opt = {}) missing = i18n.missing_diff_forest opt[:locales], opt[:from] translated = i18n.google_translate_forest missing, opt[:from] i18n.data.merge! translated - log_stderr I18n.t('i18n_tasks.translate_missing.translated', count: translated.leaves.count) + log_stderr t('i18n_tasks.translate_missing.translated', count: translated.leaves.count) print_forest translated, opt end @@ -36,14 +36,14 @@ def translate_missing(opt = {}) cmd :add_missing, args: '[locale ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.add_missing') }, + 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}. #{I18n.t('i18n_tasks.cmd.args.default_text', value: DEFAULT_ADD_MISSING_VALUE)}" }) + cmd_opt(:value).merge(desc: proc { "#{cmd_opt(:value)[:desc].call}. #{t('i18n_tasks.cmd.args.default_text', value: DEFAULT_ADD_MISSING_VALUE)}" }) def add_missing(opt = {}) forest = i18n.missing_keys(opt).set_each_value!(opt[:value] || DEFAULT_ADD_MISSING_VALUE) i18n.data.merge! forest - log_stderr I18n.t('i18n_tasks.add_missing.added', count: forest.leaves.count) + log_stderr t('i18n_tasks.add_missing.added', count: forest.leaves.count) print_forest forest, opt end end diff --git a/lib/i18n/tasks/command/commands/tree.rb b/lib/i18n/tasks/command/commands/tree.rb index 7af60888..df8e7490 100644 --- a/lib/i18n/tasks/command/commands/tree.rb +++ b/lib/i18n/tasks/command/commands/tree.rb @@ -6,7 +6,7 @@ module Tree cmd :tree_translate, args: '[tree]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_translate') }, + desc: t('i18n_tasks.cmd.desc.tree_translate'), opt: cmd_opts(:locale_to_translate_from) << cmd_opt(:data_format).except(:short) def tree_translate(opts = {}) @@ -18,7 +18,7 @@ def tree_translate(opts = {}) cmd :tree_merge, args: '[tree ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_merge') }, + desc: t('i18n_tasks.cmd.desc.tree_merge'), opt: cmd_opts(:data_format, :nostdin) def tree_merge(opts = {}) @@ -27,7 +27,7 @@ def tree_merge(opts = {}) cmd :tree_filter, args: '[pattern] [tree]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_filter') }, + desc: t('i18n_tasks.cmd.desc.tree_filter'), opt: cmd_opts(:data_format, :pattern) def tree_filter(opt = {}) @@ -42,12 +42,12 @@ def tree_filter(opt = {}) cmd :tree_rename_key, args: ' [tree]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_rename_key') }, + desc: t('i18n_tasks.cmd.desc.tree_rename_key'), opt: [ cmd_opt(:pattern).merge(short: :k, long: :key=, desc: proc { - I18n.t('i18n_tasks.cmd.args.desc.key_pattern_to_rename') }), + t('i18n_tasks.cmd.args.desc.key_pattern_to_rename') }), cmd_opt(:pattern).merge(short: :n, long: :name=, desc: proc { - I18n.t('i18n_tasks.cmd.args.desc.new_key_name') }) + t('i18n_tasks.cmd.args.desc.new_key_name') }) ] + cmd_opts(:data_format) def tree_rename_key(opt = {}) @@ -62,7 +62,7 @@ def tree_rename_key(opt = {}) cmd :tree_subtract, args: '[tree A] [tree B ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_subtract') }, + desc: t('i18n_tasks.cmd.desc.tree_subtract'), opt: cmd_opts(:data_format, :nostdin) def tree_subtract(opt = {}) @@ -73,7 +73,7 @@ def tree_subtract(opt = {}) cmd :tree_set_value, args: '[value] [tree]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_set_value') }, + desc: t('i18n_tasks.cmd.desc.tree_set_value'), opt: cmd_opts(:value, :data_format, :nostdin, :pattern) def tree_set_value(opt = {}) @@ -87,7 +87,7 @@ def tree_set_value(opt = {}) cmd :tree_convert, args: '', - desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_convert') }, + 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=)] diff --git a/lib/i18n/tasks/command/commands/usages.rb b/lib/i18n/tasks/command/commands/usages.rb index 143f036c..3130a13d 100644 --- a/lib/i18n/tasks/command/commands/usages.rb +++ b/lib/i18n/tasks/command/commands/usages.rb @@ -7,12 +7,12 @@ module Usages cmd_opt :strict, { short: :s, long: :strict, - desc: proc { I18n.t('i18n_tasks.cmd.args.desc.strict') } + desc: t('i18n_tasks.cmd.args.desc.strict') } cmd :find, args: '[pattern]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.find') }, + desc: t('i18n_tasks.cmd.desc.find'), opt: cmd_opts(:out_format, :pattern) def find(opt = {}) @@ -22,7 +22,7 @@ def find(opt = {}) cmd :unused, args: '[locale ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.unused') }, + desc: t('i18n_tasks.cmd.desc.unused'), opt: cmd_opts(:locales, :out_format, :strict) def unused(opt = {}) @@ -31,7 +31,7 @@ def unused(opt = {}) cmd :remove_unused, args: '[locale ...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.remove_unused') }, + desc: t('i18n_tasks.cmd.desc.remove_unused'), opt: cmd_opts(:locales, :out_format, :strict, :confirm) def remove_unused(opt = {}) @@ -40,10 +40,10 @@ def remove_unused(opt = {}) terminal_report.unused_keys(unused_keys) confirm_remove_unused!(unused_keys, opt) removed = i18n.data.remove_by_key!(unused_keys) - log_stderr I18n.t('i18n_tasks.remove_unused.removed', count: unused_keys.leaves.count) + log_stderr t('i18n_tasks.remove_unused.removed', count: unused_keys.leaves.count) print_forest removed, opt else - log_stderr bold green I18n.t('i18n_tasks.remove_unused.noop') + log_stderr bold green t('i18n_tasks.remove_unused.noop') end end @@ -53,8 +53,8 @@ def confirm_remove_unused!(unused_keys, opt) return if ENV['CONFIRM'] || opt[:confirm] locales = bold(opt[:locales] * ', ') msg = [ - red(I18n.t('i18n_tasks.remove_unused.confirm', count: unused_keys.leaves.count, locales: locales)), - yellow(I18n.t('i18n_tasks.common.continue_q')), + red(t('i18n_tasks.remove_unused.confirm', count: unused_keys.leaves.count, locales: locales)), + yellow(t('i18n_tasks.common.continue_q')), yellow('(yes/no)') ] * ' ' exit 1 unless agree msg diff --git a/lib/i18n/tasks/command/commands/xlsx.rb b/lib/i18n/tasks/command/commands/xlsx.rb index 71f62719..5b50b4b3 100644 --- a/lib/i18n/tasks/command/commands/xlsx.rb +++ b/lib/i18n/tasks/command/commands/xlsx.rb @@ -6,7 +6,7 @@ module XLSX cmd :xlsx_report, args: '[locale...]', - desc: proc { I18n.t('i18n_tasks.cmd.desc.xlsx_report') }, + 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'}}] diff --git a/lib/i18n/tasks/command/dsl.rb b/lib/i18n/tasks/command/dsl.rb index d8c6cf40..393f4d7e 100644 --- a/lib/i18n/tasks/command/dsl.rb +++ b/lib/i18n/tasks/command/dsl.rb @@ -5,22 +5,37 @@ module I18n::Tasks module Command module DSL - include DSL::Cmd - include DSL::CmdOpt - include DSL::EnumOpt - - def self.extended(base) - base.instance_variable_set :@dsl, HashWithIndifferentAccess.new { |h, k| - h[k] = HashWithIndifferentAccess.new - } + def self.included(base) + base.module_eval do + @dsl = HashWithIndifferentAccess.new { |h, k| + h[k] = HashWithIndifferentAccess.new + } + extend ClassMethods + end end - def included(base) - base.instance_variable_get(:@dsl).deep_merge!(@dsl) + def t(*args) + I18n.t(*args) end - def dsl(key) - @dsl[key] + module ClassMethods + include DSL::Cmd + include DSL::CmdOpt + include DSL::EnumOpt + + def dsl(key) + @dsl[key] + end + + # late-bound I18n.t for module bodies + def t(*args) + proc { I18n.t(*args) } + end + + # if class is a module, merge DSL definitions when it is included + def included(base) + base.instance_variable_get(:@dsl).deep_merge!(@dsl) + end end end end diff --git a/lib/i18n/tasks/command/options/common.rb b/lib/i18n/tasks/command/options/common.rb index 4370758d..8781e109 100644 --- a/lib/i18n/tasks/command/options/common.rb +++ b/lib/i18n/tasks/command/options/common.rb @@ -5,7 +5,7 @@ module I18n::Tasks module Command module Options module Common - extend Command::DSL + include Command::DSL include Options::EnumOpt include Options::ListOpt @@ -14,28 +14,28 @@ module Common cmd_opt :nostdin, { short: :S, long: :nostdin, - desc: proc { I18n.t('i18n_tasks.cmd.args.desc.nostdin') }, + desc: t('i18n_tasks.cmd.args.desc.nostdin'), conf: {default: false} } cmd_opt :confirm, { short: :y, long: :confirm, - desc: proc { I18n.t('i18n_tasks.cmd.args.desc.confirm') }, + desc: t('i18n_tasks.cmd.args.desc.confirm'), conf: {default: false} } cmd_opt :pattern, { short: :p, long: :pattern=, - desc: proc { I18n.t('i18n_tasks.cmd.args.desc.key_pattern') }, + desc: t('i18n_tasks.cmd.args.desc.key_pattern'), conf: {argument: true, optional: false} } cmd_opt :value, { short: :v, long: :value=, - desc: proc { I18n.t('i18n_tasks.cmd.args.desc.value') }, + desc: t('i18n_tasks.cmd.args.desc.value'), conf: {argument: true, optional: false} } diff --git a/lib/i18n/tasks/command/options/locales.rb b/lib/i18n/tasks/command/options/locales.rb index 219db299..f524a60c 100644 --- a/lib/i18n/tasks/command/options/locales.rb +++ b/lib/i18n/tasks/command/options/locales.rb @@ -2,12 +2,12 @@ module I18n::Tasks module Command module Options module Locales - extend Command::DSL + include Command::DSL cmd_opt :locales, { short: :l, long: :locales=, - desc: proc { I18n.t('i18n_tasks.cmd.args.desc.locales_filter') }, + desc: t('i18n_tasks.cmd.args.desc.locales_filter'), conf: {as: Array, delimiter: /\s*[+:,]\s*/, default: 'all', argument: true, optional: false}, parse: :parse_locales } @@ -15,7 +15,7 @@ module Locales cmd_opt :locale, { short: :l, long: :locale=, - desc: proc { I18n.t('i18n_tasks.cmd.args.desc.locale') }, + desc: t('i18n_tasks.cmd.args.desc.locale'), conf: {default: 'base', argument: true, optional: false}, parse: :parse_locale } @@ -23,7 +23,7 @@ module Locales cmd_opt :locale_to_translate_from, cmd_opt(:locale).merge( short: :f, long: :from=, - desc: proc { I18n.t('i18n_tasks.cmd.args.desc.locale_to_translate_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]) diff --git a/lib/i18n/tasks/command/options/trees.rb b/lib/i18n/tasks/command/options/trees.rb index b2ff23cc..db5ceddf 100644 --- a/lib/i18n/tasks/command/options/trees.rb +++ b/lib/i18n/tasks/command/options/trees.rb @@ -2,7 +2,7 @@ module I18n::Tasks module Command module Options module Trees - extend Command::DSL + include Command::DSL format_opt = proc { |type| enum_opt_attr :f, :format=, enum_opt(type), proc { |valid, default| @@ -29,7 +29,7 @@ def print_forest(forest, opt, version = :show_tree) when 'keys' puts forest.key_names(root: true) when *enum_opt(:data_format) - puts i18n.data.adapter_dump forest, format + puts i18n.data.adapter_dump forest.to_hash(true), format end end diff --git a/lib/i18n/tasks/commands.rb b/lib/i18n/tasks/commands.rb index 44c309a8..937a3d4f 100644 --- a/lib/i18n/tasks/commands.rb +++ b/lib/i18n/tasks/commands.rb @@ -13,7 +13,7 @@ module I18n::Tasks class Commands < Command::Commander - extend Command::DSL + include Command::DSL include Command::Commands::Health include Command::Commands::Missing include Command::Commands::Usages diff --git a/lib/i18n/tasks/console_context.rb b/lib/i18n/tasks/console_context.rb index 60c4fd81..330946d6 100644 --- a/lib/i18n/tasks/console_context.rb +++ b/lib/i18n/tasks/console_context.rb @@ -18,7 +18,7 @@ def start IRB.setup nil ctx = IRB::Irb.new.context IRB.conf[:MAIN_CONTEXT] = ctx - STDERR.puts Messages.banner + $stderr.puts Messages.banner require 'irb/ext/multi-irb' IRB.irb nil, new end diff --git a/lib/i18n/tasks/data/file_formats.rb b/lib/i18n/tasks/data/file_formats.rb index e3f5839c..34c0d536 100644 --- a/lib/i18n/tasks/data/file_formats.rb +++ b/lib/i18n/tasks/data/file_formats.rb @@ -40,7 +40,7 @@ def load_file(path) def write_tree(path, tree) ::FileUtils.mkpath(File.dirname path) ::File.open(path, 'w') { |f| - f.write adapter_dump(tree.to_hash, self.class.adapter_name_for_path(path)) + f.write adapter_dump(tree.to_hash(true), self.class.adapter_name_for_path(path)) } end diff --git a/lib/i18n/tasks/data/tree/node.rb b/lib/i18n/tasks/data/tree/node.rb index fe2e9ed3..be4914dd 100644 --- a/lib/i18n/tasks/data/tree/node.rb +++ b/lib/i18n/tasks/data/tree/node.rb @@ -127,9 +127,9 @@ def to_siblings parent && parent.children || Siblings.new(nodes: [self]) end - def to_hash - @hash ||= begin - children_hash = (children || {}).map(&:to_hash).reduce(:deep_merge) || {} + def to_hash(sort = false) + (@hash ||= {})[sort] ||= begin + children_hash = children ? children.to_hash(sort) : {} if key.nil? children_hash elsif leaf? @@ -144,13 +144,13 @@ def to_hash delegate :to_yaml, to: :to_hash def inspect(level = 0) - if key.nil? - label = Term::ANSIColor.dark '∅' - else - label = [Term::ANSIColor.color(1 + level % 15, self.key), - (": #{Term::ANSIColor.cyan(self.value.to_s)}" if leaf?), - (" #{self.data}" if data?)].compact.join - end + label = if key.nil? + Term::ANSIColor.dark '∅' + else + [Term::ANSIColor.color(1 + level % 15, key), + (": #{Term::ANSIColor.cyan(value.to_s)}" if leaf?), + (" #{data}" if data?)].compact.join + end [' ' * level, label, ("\n" + children.map { |c| c.inspect(level + 1) }.join("\n") if children?)].compact.join end diff --git a/lib/i18n/tasks/data/tree/nodes.rb b/lib/i18n/tasks/data/tree/nodes.rb index 0c664a39..be9d2610 100644 --- a/lib/i18n/tasks/data/tree/nodes.rb +++ b/lib/i18n/tasks/data/tree/nodes.rb @@ -29,8 +29,14 @@ def derive(new_attr = {}) self.class.new(attr) end - def to_hash - @hash ||= map(&:to_hash).reduce(:deep_merge!) || {} + def to_hash(sort = false) + (@hash ||= {})[sort] ||= begin + if sort + self.sort { |a, b| a.key <=> b.key } + else + self + end.map { |node| node.to_hash(sort) }.reduce({}, :deep_merge!) + end end delegate :to_json, to: :to_hash @@ -40,7 +46,7 @@ def inspect if present? map(&:inspect) * "\n" else - Term::ANSIColor.dark '∅' + Term::ANSIColor.dark '{∅}' end end diff --git a/lib/i18n/tasks/data/tree/siblings.rb b/lib/i18n/tasks/data/tree/siblings.rb index b44b3f45..c20ab235 100644 --- a/lib/i18n/tasks/data/tree/siblings.rb +++ b/lib/i18n/tasks/data/tree/siblings.rb @@ -45,7 +45,7 @@ def replace_node!(node, new_node) key_to_node[new_node.key] = new_node end - include SplitKey + include ::I18n::Tasks::SplitKey # @return [Node] by full key def get(full_key) @@ -110,22 +110,7 @@ def append(nodes) def merge!(nodes) nodes = Siblings.from_nested_hash(nodes) if nodes.is_a?(Hash) nodes.each do |node| - if key_to_node.key?(node.key) - our = key_to_node[node.key] - next if our == node - our.value = node.value if node.leaf? - our.data.merge!(node.data) if node.data? - if node.children? - if our.children - our.children.merge!(node.children) - else - warn_add_children_to_leaf our - our.children = node.children - end - end - else - key_to_node[node.key] = node.derive(parent: parent) - end + merge_node! node end @list = key_to_node.values dirty! @@ -161,12 +146,31 @@ def set_root_key!(new_key, data = nil) private + def merge_node!(node) + if key_to_node.key?(node.key) + our = key_to_node[node.key] + return if our == node + our.value = node.value if node.leaf? + our.data.merge!(node.data) if node.data? + if node.children? + if our.children + our.children.merge!(node.children) + else + warn_add_children_to_leaf our + our.children = node.children + end + end + else + key_to_node[node.key] = node.derive(parent: parent) + end + end + def warn_add_children_to_leaf(node) ::I18n::Tasks::Logging.log_warn "'#{node.full_key}' was a leaf, now has children (value <- scope conflict)" end class << self - include SplitKey + include ::I18n::Tasks::SplitKey def null new diff --git a/lib/i18n/tasks/data/tree/traversal.rb b/lib/i18n/tasks/data/tree/traversal.rb index 3a752623..b0e5e17b 100644 --- a/lib/i18n/tasks/data/tree/traversal.rb +++ b/lib/i18n/tasks/data/tree/traversal.rb @@ -21,7 +21,11 @@ def levels(&block) nodes = to_nodes unless nodes.empty? block.yield nodes - Nodes.new(nodes.children).levels(&block) + if nodes.children.size == 1 + first.children + else + Nodes.new(nodes: nodes.children) + end.levels(&block) end self end diff --git a/lib/i18n/tasks/google_translation.rb b/lib/i18n/tasks/google_translation.rb index 46745f4c..a4bc2f03 100644 --- a/lib/i18n/tasks/google_translation.rb +++ b/lib/i18n/tasks/google_translation.rb @@ -5,6 +5,9 @@ module I18n::Tasks module GoogleTranslation + # @param [I18n::Tasks::Tree::Siblings] forest to translate to the locales of its root nodes + # @param [String] from locale + # @return [I18n::Tasks::Tree::Siblings] translated forest def google_translate_forest(forest, from) forest.inject empty_forest do |result, root| translated = google_translate_list(root.key_values(root: true), to: root.key, from: from) @@ -12,7 +15,8 @@ def google_translate_forest(forest, from) end end - # @param [Array] list of [key, value] pairs + # @param [Array<[String, Object]>] list of key-value pairs + # @return [Array<[String, Object]>] translated list def google_translate_list(list, opts) return [] if list.empty? opts = opts.dup @@ -26,6 +30,8 @@ def google_translate_list(list, opts) result end + # @param [Array<[String, Object]>] list of key-value pairs + # @return [Array<[String, Object]>] translated list def fetch_google_translations(list, opts) from_values(list, EasyTranslate.translate(to_values(list), opts)).tap do |result| if result.blank? @@ -38,21 +44,27 @@ def fetch_google_translations(list, opts) def validate_google_translate_api_key!(key) if key.blank? - raise CommandError.new('Set Google API key via GOOGLE_TRANSLATE_API_KEY environment variable or translation.api_key in config/i18n-tasks.yml. -Get the key at https://code.google.com/apis/console.') + raise CommandError.new(I18n.t('i18n_tasks.google_translate.errors.no_api_key')) end end + # @param [Array<[String, Object]>] list of key-value pairs + # @return [Array] values for translation extracted from list def to_values(list) list.map { |l| dump_value l[1] }.flatten.compact end + # @param [Array<[String, Object]>] list of key-value pairs + # @param [Array] list of translated values + # @return [Array<[String, Object]>] translated key-value pairs def from_values(list, translated_values) keys = list.map(&:first) untranslated_values = list.map(&:last) keys.zip parse_value(untranslated_values, translated_values.to_enum) end + # Prepare value for translation. + # @return [String, Array, nil] value for Google Translate or nil for non-string values def dump_value(value) case value when Array @@ -61,10 +73,14 @@ def dump_value(value) when String replace_interpolations value else - value + nil end end + # Parse translated value from the each_translated enumerator + # @param [Object] untranslated + # @param [Enumerator] each_translated + # @return [Object] final translated value def parse_value(untranslated, each_translated) case untranslated when Array @@ -72,10 +88,8 @@ def parse_value(untranslated, each_translated) untranslated.map { |from| parse_value(from, each_translated) } when String restore_interpolations untranslated, each_translated.next - when NilClass - nil else - each_translated.next + untranslated end rescue Exception => e puts "Exception: #{e.to_s}\n\n" @@ -100,7 +114,8 @@ def parse_value(untranslated, each_translated) INTERPOLATION_KEY_RE_TMP => UNTRANSLATABLE_STRING_TMP } - # 'hello, %{name}' => 'hello, ' + # @param [String] value + # @return [String] 'hello, %{name}' => 'hello, ' def replace_interpolations(value) if value =~ INTERPOLATION_KEY_RE value.gsub INTERPOLATION_KEY_RE, UNTRANSLATABLE_STRING @@ -113,6 +128,9 @@ def replace_interpolations(value) end end + # @param [String] untranslated + # @param [String] translated + # @return [String] 'hello, ' => 'hello, %{name}' def restore_interpolations(untranslated, translated) return translated if (untranslated !~ INTERPOLATION_KEY_RE && untranslated !~ INTERPOLATION_KEY_RE_JS && untranslated !~ INTERPOLATION_KEY_RE_TMP) diff --git a/lib/i18n/tasks/logging.rb b/lib/i18n/tasks/logging.rb index 1296106b..e7ae526b 100644 --- a/lib/i18n/tasks/logging.rb +++ b/lib/i18n/tasks/logging.rb @@ -21,6 +21,6 @@ def log_error(message) end def log_stderr(*args) - STDERR.puts(*args) + $stderr.puts(*args) end end diff --git a/lib/i18n/tasks/reports/terminal.rb b/lib/i18n/tasks/reports/terminal.rb index 533ce964..d899928f 100644 --- a/lib/i18n/tasks/reports/terminal.rb +++ b/lib/i18n/tasks/reports/terminal.rb @@ -125,7 +125,7 @@ def key_occurrence(full_key, info) def highlight_key(full_key, line, range = (0..-1)) result = line.dup - result[range] = result[range].sub!(full_key) { |m| underline m } + result[range] = result[range].sub(full_key) { |m| underline m } result end diff --git a/lib/i18n/tasks/scanners/base_scanner.rb b/lib/i18n/tasks/scanners/base_scanner.rb index 67995c16..667b164c 100644 --- a/lib/i18n/tasks/scanners/base_scanner.rb +++ b/lib/i18n/tasks/scanners/base_scanner.rb @@ -8,7 +8,7 @@ class BaseScanner include ::I18n::Tasks::KeyPatternMatching include ::I18n::Tasks::Logging - attr_reader :config, :key_filter, :ignore_lines_re + attr_reader :config, :key_filter, :ignore_lines_res def initialize(config = {}) @config = config.dup.with_indifferent_access.tap do |conf| @@ -21,14 +21,25 @@ def initialize(config = {}) # exclude common binary extensions by default (images and fonts) conf[:exclude] = %w(*.jpg *.png *.gif *.svg *.ico *.eot *.ttf *.woff *.pdf) end - conf[:ignore_lines] ||= %q(^\s*[#/](?!\si18n-tasks-use)).freeze - conf[:ignore_lines] = Array(conf[:ignore_lines]) - @ignore_lines_re = conf[:ignore_lines].map { |line| Regexp.new(line) } + # 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)), + } + @ignore_lines_res = conf[:ignore_lines].inject({}) { |h, (ext, re)| h.update(ext => Regexp.new(re)) } + @key_filter = nil end end - def exclude_line?(line) - ignore_lines_re.any? { |re| re =~ line } + def exclude_line?(line, path) + re = ignore_lines_res[File.extname(path)[1..-1]] + re && re =~ line end def key_filter=(value) diff --git a/lib/i18n/tasks/scanners/pattern_scanner.rb b/lib/i18n/tasks/scanners/pattern_scanner.rb index 36e17e45..1ad7ca91 100644 --- a/lib/i18n/tasks/scanners/pattern_scanner.rb +++ b/lib/i18n/tasks/scanners/pattern_scanner.rb @@ -17,7 +17,7 @@ def scan_file(path, opts = {}) next unless valid_key?(key, strict) key = key + ':' if key.end_with?('.') location = src_location(path, text, src_pos) - unless exclude_line?(location[:line]) + unless exclude_line?(location[:line], path) keys << [key, data: location] end end diff --git a/lib/i18n/tasks/split_key.rb b/lib/i18n/tasks/split_key.rb index 70772aef..96bd4a78 100644 --- a/lib/i18n/tasks/split_key.rb +++ b/lib/i18n/tasks/split_key.rb @@ -1,65 +1,69 @@ -module SplitKey - extend self +module I18n + module Tasks + module SplitKey + extend self - # split a key by dots (.) - # dots inside braces or parenthesis are not split on - # - # split_key 'a.b' # => ['a', 'b'] - # split_key 'a.#{b.c}' # => ['a', '#{b.c}'] - # split_key 'a.b.c', 2 # => ['a', 'b.c'] - def split_key(key, max = Float::INFINITY) - parts = [] - pos = 0 - return [key] if max == 1 - key_parts(key) do |part| - parts << part - pos += part.length + 1 - if parts.length + 1 >= max - parts << key.from(pos) unless pos == key.length - break + # split a key by dots (.) + # dots inside braces or parenthesis are not split on + # + # split_key 'a.b' # => ['a', 'b'] + # split_key 'a.#{b.c}' # => ['a', '#{b.c}'] + # split_key 'a.b.c', 2 # => ['a', 'b.c'] + def split_key(key, max = Float::INFINITY) + parts = [] + pos = 0 + return [key] if max == 1 + key_parts(key) do |part| + parts << part + pos += part.length + 1 + if parts.length + 1 >= max + parts << key.from(pos) unless pos == key.length + break + end + end + parts end - end - parts - end - def last_key_part(key) - last = nil - key_parts(key) { |part| last = part } - last - end + def last_key_part(key) + last = nil + key_parts(key) { |part| last = part } + last + end - # yield each key part - # dots inside braces or parenthesis are not split on - def key_parts(key, &block) - return enum_for(:key_parts, key) unless block - nesting = PARENS - counts = PARENS_ZEROS # dup'd later if key contains parenthesis - delim = '.'.freeze - from = to = 0 - key.each_char do |char| - if char == delim && PARENS_ZEROS == counts - block.yield key[from...to] - from = to = (to + 1) - else - nest_i, nest_inc = nesting[char] - if nest_i - counts = counts.dup if counts.frozen? - counts[nest_i] += nest_inc + # yield each key part + # dots inside braces or parenthesis are not split on + def key_parts(key, &block) + return enum_for(:key_parts, key) unless block + nesting = PARENS + counts = PARENS_ZEROS # dup'd later if key contains parenthesis + delim = '.'.freeze + from = to = 0 + key.each_char do |char| + if char == delim && PARENS_ZEROS == counts + block.yield key[from...to] + from = to = (to + 1) + else + nest_i, nest_inc = nesting[char] + if nest_i + counts = counts.dup if counts.frozen? + counts[nest_i] += nest_inc + end + to += 1 + end end - to += 1 + block.yield(key[from...to]) if from < to && to <= key.length + true end + + PARENS = %w({} [] ()).inject({}) { |h, s| + i = h.size / 2 + h[s[0].freeze] = [i, 1].freeze + h[s[1].freeze] = [i, -1].freeze + h + }.freeze + PARENS_ZEROS = Array.new(PARENS.size, 0).freeze + private_constant :PARENS + private_constant :PARENS_ZEROS end - block.yield(key[from...to]) if from < to && to <= key.length - true end - - PARENS = %w({} [] ()).inject({}) { |h, s| - i = h.size / 2 - h[s[0].freeze] = [i, 1].freeze - h[s[1].freeze] = [i, -1].freeze - h - }.freeze - PARENS_ZEROS = Array.new(PARENS.size, 0).freeze - private_constant :PARENS - private_constant :PARENS_ZEROS end diff --git a/lib/i18n/tasks/version.rb b/lib/i18n/tasks/version.rb index bcf4eac6..a5ca223c 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.5' + VERSION = '0.7.8' end end diff --git a/spec/commands/data_commands_spec.rb b/spec/commands/data_commands_spec.rb index 24a9d46a..5d142b71 100644 --- a/spec/commands/data_commands_spec.rb +++ b/spec/commands/data_commands_spec.rb @@ -20,7 +20,7 @@ def en_data_2 end it '#data' do - expect(JSON.parse(run_cmd :data, format: 'json')).to eq(en_data) + expect(JSON.parse(run_cmd :data, format: 'json', locales: 'en')).to eq(en_data) end it '#data-merge' do diff --git a/spec/google_translate_spec.rb b/spec/google_translate_spec.rb index c77707bb..1fba2d32 100644 --- a/spec/google_translate_spec.rb +++ b/spec/google_translate_spec.rb @@ -9,7 +9,8 @@ nil_value_test = ['nil-value-key', nil, nil], text_test = ['key', "Hello - %{user} O'neill!", "Hola - %{user} O'neill!"], html_test = ['html-key.html', "Hello - %{user} O'neill", "Hola - %{user} O'neill"], - array_test = ['array-key', ['Hello.', nil, '', 'Goodbye.'], ['Hola.', nil, '', 'Adiós.']], + array_test = ['array-key', ['Hello.', nil, '', 'Goodbye.'], ['Hola.', nil, '', '¡Adiós.']], + fixnum_test = ['numeric-key', 1, 1], handlebars_test = ['handlebars-key', "Hello {{username}}!", "Hola {{username}}!"], template_test = ['template-key', "Hello [username]!", "Hola [username]!"], newline_test = ['newline-key', "Hello %{user},\n\nWelcome to %{sitename}.", "Hola %{user},\n\nBienvenido a %{sitename}."] @@ -49,6 +50,7 @@ 'hello_html' => html_test[1], 'array_key' => array_test[1], 'nil-value-key' => nil_value_test[1], + 'fixnum-key' => fixnum_test[1], 'handlebars_key' => handlebars_test[1], 'template_key' => template_test[1], 'newline_key' => newline_test[1] @@ -64,7 +66,8 @@ 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]) - expect(task.t('nil-value-key', 'es')).to eq(nil_value_test[2]) + expect(task.t('common.nil-value-key', 'es')).to eq(nil_value_test[2]) + expect(task.t('common.fixnum-key', 'es')).to eq(fixnum_test[2]) expect(task.t('common.handlebars_key', 'es')).to eq(handlebars_test[2]) expect(task.t('common.template_key', 'es')).to eq(template_test[2]) expect(task.t('common.newline_key', 'es')).to eq(newline_test[2]) diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 0475c9fc..c1b7aeef 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -8,11 +8,11 @@ it 'does not have missing keys' do expect(missing_keys).to be_empty, - "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them" + "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them" end it 'does not have unused keys' do - expect(i18n.unused_keys).to be_empty, - "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" + expect(unused_keys).to be_empty, + "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" end end diff --git a/spec/i18n_tasks_spec.rb b/spec/i18n_tasks_spec.rb index 6612d0e6..b3f7af0e 100644 --- a/spec/i18n_tasks_spec.rb +++ b/spec/i18n_tasks_spec.rb @@ -5,6 +5,17 @@ describe 'i18n-tasks' do delegate :run_cmd, :i18n_task, :in_test_app_dir, to: :TestCodebase + 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| + expect(out).to include(v.to_s) + end + end + end + describe 'missing' do let (:expected_missing_keys) { %w( en.used_but_missing.key en.relative.index.missing @@ -57,7 +68,7 @@ it 'removes unused' do in_test_app_dir do t = i18n_task - unused = expected_unused_keys.map { |k| SplitKey.split_key(k, 2)[1] } + 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 @@ -76,6 +87,21 @@ end describe 'normalize' do + it 'sorts the keys' do + in_test_app_dir do + run_cmd :normalize + en_yml_data = i18n_task.data.reload['en'].select_keys { |_k, node| + node.data[:path] == 'config/locales/en.yml' + } + expect(en_yml_data).to be_present + en_yml_data.nodes { |nodes| + next unless nodes.children + keys = nodes.children.map(&:key) + expect(keys).to eq keys.sort + } + end + end + 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' diff --git a/spec/locale_tree/siblings_spec.rb b/spec/locale_tree/siblings_spec.rb index 5094f24f..46492180 100644 --- a/spec/locale_tree/siblings_spec.rb +++ b/spec/locale_tree/siblings_spec.rb @@ -87,5 +87,10 @@ t['a.b.c.' + node.key] = node expect(t['a.b.c.d'].value).to eq('e') end + + it '#inspect' do + expect(build_tree(a_hash).inspect).to eq "a: 1\nb\n ba: 1\n bb: 2" + expect(build_tree({}).inspect).to eq '{∅}' + end end end diff --git a/spec/split_key_spec.rb b/spec/split_key_spec.rb index 9ee12bf2..7bbc659c 100644 --- a/spec/split_key_spec.rb +++ b/spec/split_key_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'SplitKey' do - include SplitKey + include ::I18n::Tasks::SplitKey [['', %w()], ['a', %w(a)], diff --git a/spec/used_keys_spec.rb b/spec/used_keys_spec.rb index 3ff7698d..6909107a 100644 --- a/spec/used_keys_spec.rb +++ b/spec/used_keys_spec.rb @@ -3,14 +3,18 @@ describe 'UsedKeys' do let!(:task) { I18n::Tasks::BaseTask.new } - - around do |ex| - task.config[:search] = {paths: ['a.html.slim']} - TestCodebase.setup('a.html.slim' => <<-SLIM) + let(:file_name) { 'a.html.slim' } + let(:file_content) do + <<-SLIM div = t 'a' p = t 'a' h1 = t 'b' SLIM + end + + around do |ex| + task.config[:search] = {paths: [file_name]} + TestCodebase.setup(file_name => file_content) TestCodebase.in_test_app_dir { ex.run } TestCodebase.teardown end @@ -45,4 +49,27 @@ [{pos: 29, line_num: 3, line_pos: 6, line: "h1 = t 'b'", src_path: 'a.html.slim'}] ) end + + describe 'when input is haml' do + let(:file_name) { 'a.html.haml' } + let(:file_content) do + <<-HAML +#first{ title: t('a') } +.second{ title: t('a') } +- # t('a') in a comment is ignored + HAML + end + + it '#used_keys(source_occurences: true)' do + used_keys = task.used_tree(source_occurrences: true) + expect(used_keys.size).to eq 1 + expect_node_key_data( + used_keys.leaves.first, + 'a', + source_occurrences: + [{pos: 15, line_num: 1, line_pos: 16, line: "#first{ title: t('a') }", src_path: 'a.html.haml'}, + {pos: 40, line_num: 2, line_pos: 17, line: ".second{ title: t('a') }", src_path: 'a.html.haml'}] + ) + end + end end diff --git a/templates/config/i18n-tasks.yml b/templates/config/i18n-tasks.yml index 6b561442..8d97b7da 100644 --- a/templates/config/i18n-tasks.yml +++ b/templates/config/i18n-tasks.yml @@ -22,7 +22,7 @@ data: # 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}'] + # - ['{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 @@ -59,10 +59,6 @@ search: ## Or, File.fnmatch patterns to include # include: ["*.rb", "*.html.slim"] - ## Lines starting with # or / are ignored by default - # ignore_lines: - # - "^\\s*[#/](?!\\si18n-tasks-use)" - ## Google Translate # translation: # # Get an API key and set billing info at https://code.google.com/apis/console to use Google Translate @@ -70,13 +66,16 @@ search: ## Consider these keys not missing # ignore_missing: -# - pagination.views.* +# - 'errors.messages.{accepted,blank,invalid,too_short,too_long}' +# - '{devise,simple_form}.*' ## Consider these keys used # ignore_unused: -# - 'simple_form.{yes,no}' -# - 'simple_form.{placeholders,hints,labels}.*' -# - 'simple_form.{error_notification,required}.:' +# - 'activerecord.attributes.*' +# - '{devise,kaminari,will_paginate}.*' +# - '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: diff --git a/templates/rspec/i18n_spec.rb b/templates/rspec/i18n_spec.rb index 0475c9fc..c1b7aeef 100644 --- a/templates/rspec/i18n_spec.rb +++ b/templates/rspec/i18n_spec.rb @@ -8,11 +8,11 @@ it 'does not have missing keys' do expect(missing_keys).to be_empty, - "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them" + "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them" end it 'does not have unused keys' do - expect(i18n.unused_keys).to be_empty, - "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" + expect(unused_keys).to be_empty, + "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" end end