i18n-tasks 0.7.1 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1adbf899220bdaffacd8f8b95f74eb2c102268a6
4
- data.tar.gz: 028937e7d90a8af43ad66497a0b3f869c52fb599
3
+ metadata.gz: 1da0127b4f5d5823790e0b3e95609d72bc5d89ce
4
+ data.tar.gz: 4b3444adf44532e8c4cfa4906658f23f95f385c3
5
5
  SHA512:
6
- metadata.gz: 16ee9ac1ea60d4ed1d04881d9c40fdc88478c16c9d0471d8b94d31a77a2a44e915b4236ad8d13a7c3517d40e7179ff9909c5215f828b8d193b19b61b59ee0747
7
- data.tar.gz: 025e2fee7f5590377e4b8ddfb2cec98256e45ff120ef5219d545064d745a23430b15cd071683edf0aa44618e22cdb3b78073be13c1c64a826e18e958db5cc4b7
6
+ metadata.gz: 7d914cabb6ace1ccf7f1eebf8dd68cd885a8929713edc095aebb7073e771cf06bb0ddcce78e1ff6df51562bc68ee11d8e6b1110f3120ad6d3093f985ae44fcb2
7
+ data.tar.gz: 2b8add28adae0c70a297e253dd104bc5877f95a68c4b5d16038f99d313b088b6268da15ca6e052e099be69308cfd2b758428ba276e64d94fdfb6e8cad6fbe2af
data/CHANGES.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.7.2
2
+
3
+ * i18n-tasks now analyses itself! `internal_locale` setting has been added, that controls i18n-tasks reporting language.
4
+ English and Russian are available in this release.
5
+
1
6
  ## 0.7.1
2
7
 
3
8
  * 1.9.3 compatibility
data/README.md CHANGED
@@ -25,7 +25,7 @@ i18n-tasks can be used with any project using [i18n][i18n-gem] (default in Rails
25
25
  Add to Gemfile:
26
26
 
27
27
  ```ruby
28
- gem 'i18n-tasks', '~> 0.7.1'
28
+ gem 'i18n-tasks', '~> 0.7.2'
29
29
  ```
30
30
 
31
31
  Copy default [configuration file](#configuration) (optional):
@@ -137,12 +137,12 @@ $ i18n-tasks normalize -p
137
137
  Relative keys (`t '.title'`) and plural keys (`key.{one,many,other,...}`) are fully supported.
138
138
  Scope argument is supported, but only when it is the first keyword argument ([improvements welcome](/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb)):
139
139
 
140
- ```ruby
141
- # this is supported
142
- t :invalid, scope: [:auth, :password], attempts: 5
143
- # but not this
144
- t :invalid, attempts: 5, scope: [:auth, :password]
145
- ```
140
+ ```ruby
141
+ # this is supported
142
+ t :invalid, scope: [:auth, :password], attempts: 5
143
+ # but not this
144
+ t :invalid, attempts: 5, scope: [:auth, :password]
145
+ ```
146
146
 
147
147
  Unused report will detect certain dynamic key forms and not report them, e.g.:
148
148
 
@@ -175,6 +175,12 @@ base_locale: en
175
175
  locales: [es, fr] # This includes base_locale by default
176
176
  ```
177
177
 
178
+ `internal_locale` controls the language i18n-tasks reports in. Locales available are `en` and `ru` (pull request to add more!).
179
+
180
+ ```yaml
181
+ internal_locale: en
182
+ ```
183
+
178
184
  ### Storage
179
185
 
180
186
  The default data adapter supports YAML and JSON files.
@@ -22,21 +22,23 @@ err = proc { |message, exit_code|
22
22
  }
23
23
 
24
24
  begin
25
- ran = false
25
+ ran = false
26
26
  commander = ::I18n::Tasks::Commands
27
+ instance = commander.new
28
+ instance.set_internal_locale!
27
29
  slop_adapter = ::I18n::Tasks::SlopCommand
28
- args = ARGV.dup
29
- args = ['--help'] if args.empty?
30
+ args = ARGV.dup
31
+ args = ['--help'] if args.empty?
30
32
  Slop.parse(args, help: true) do
31
33
  on('-v', '--version', 'Print the version') {
32
34
  puts I18n::Tasks::VERSION
33
35
  exit
34
36
  }
35
37
  commander.cmds.each do |name, attr|
36
- slop_dsl = slop_adapter.slop_command(name, attr) { |name, opts, args|
38
+ slop_dsl = slop_adapter.slop_command(name, attr) { |name, opts|
37
39
  begin
38
40
  ran = true
39
- commander.run_command name, slop_adapter.parse_slop_opts_args(opts, args)
41
+ instance.safe_run name, opts
40
42
  rescue Errno::EPIPE
41
43
  # ignore Errno::EPIPE which is throw when pipe breaks, e.g.:
42
44
  # i18n-tasks missing | head
@@ -5,6 +5,9 @@ base_locale: en
5
5
  ## uncomment to set locales explicitly
6
6
  # locales: [en, es, fr]
7
7
 
8
+ ## i18n-tasks report locale, default: en, available: en, ru
9
+ internal_locale: ru
10
+
8
11
  # Read and write locale data
9
12
  data:
10
13
  ## by default, translation data are read from the file system, or you can provide a custom data adapter
@@ -66,26 +66,32 @@ en:
66
66
  config: display i18n-tasks configuration
67
67
  gem_path: show path to the gem
68
68
  irb: start REPL session within i18n-tasks context
69
+ xlsx_report: save missing and unused translations to an Excel file
69
70
  args:
70
71
  default_text: 'Default: %{value}'
72
+ default_all: 'Default: all'
71
73
  desc:
72
74
  out_format: 'Output format: %{valid_text}. %{default_text}.'
73
75
  data_format: 'Data format: %{valid_text}. %{default_text}.'
74
76
  keys: List of keys separated by commas (,), spaces, or newlines.
75
77
  locales_filter: 'Comma-separated list of locale(s) to process. Default: all. Special: base.'
78
+ locales_to_translate_from: 'Locale to translate from (default: base)'
76
79
  locale: 'Locale. Default: base'
77
80
  confirm: Confirm automatically
78
81
  nostdin: Do not read from stdin
79
82
  strict: Do not infer dynamic key usage such as `t("category.\#{category.name}")`
80
- missing_types: 'Filter by types: %{valid}.'
83
+ missing_types: 'Filter by types: %{valid}. Default: all'
81
84
  key_pattern: Filter by key pattern (e.g. 'common.*')
85
+ key_pattern_to_rename: Full key (pattern) to rename. Required
86
+ new_key_name: New name, interpolates original name as %{key}. Required
82
87
  value: 'Value. Interpolates: %{value}, %{human_key}, %{value_or_human_key}'
83
88
  pattern_router: 'Use pattern router: keys moved per config data.write'
84
89
  enum_opt:
85
- desc:
86
- default: "%{valid_text}. %{default_text}"
87
- invalid_one: "%{invalid} is not one of: %{valid}."
88
- invalid_list: "%{invalid} is not in: %{valid}."
90
+ desc: "%{valid_text}. %{default_text}"
91
+ invalid: "%{invalid} is not one of: %{valid}."
92
+ enum_list_opt:
93
+ desc: 'Comma-separated list of: %{valid_text}. %{default_text}'
94
+ invalid: "%{invalid} is not in: %{valid}."
89
95
  errors:
90
96
  pass_forest: Pass locale forest
91
97
  invalid_locale: Invalid locale %{invalid}
@@ -0,0 +1,102 @@
1
+ ---
2
+ ru:
3
+ i18n_tasks:
4
+ common:
5
+ locale: "Язык"
6
+ type: "Тип"
7
+ key: "Ключ"
8
+ value: "Значение"
9
+ base_value: "Исходное значение"
10
+ details: "Детали"
11
+ continue_q: "Продолжить?"
12
+ n_more: "ещё %{count}"
13
+ google_translate:
14
+ errors:
15
+ no_results: Google Translate не дал результатов. Убедитесь в том, что платежная информация
16
+ добавлена в в https://code.google.com/apis/console.
17
+ remove_unused:
18
+ confirm:
19
+ one: "Один перевод будут удалён из %{locales}."
20
+ other: "Переводы (%{count}) будут удалены из %{locales}."
21
+ removed: "Удалены ключи (%{count})"
22
+ noop: "Нет неиспользуемых ключей"
23
+ translate_missing:
24
+ translated: "Переведены ключи (%{count})"
25
+ add_missing:
26
+ added: "Добавлены ключи (%{count})"
27
+ unused:
28
+ none: "Все переводы используются."
29
+ missing:
30
+ none: "Всё переведено."
31
+ usages:
32
+ none: "Не найдено использований."
33
+ health:
34
+ no_keys_detected: "Ключи не обнаружены. Проверьте data.read в config/i18n-tasks.yml."
35
+ data_stats:
36
+ title: "Данные (%{locales}):"
37
+ text: "%{key_count} ключей в %{locale_count} языках. В среднем, длина строки: %{value_chars_avg},
38
+ сегменты ключей: %{key_segments_avg}, ключей в языке %{per_locale_avg}."
39
+ text_single_locale: "%{key_count} ключей. В среднем, длина строки: %{value_chars_avg}, сегменты
40
+ ключей: %{key_segments_avg}."
41
+ cmd:
42
+ encourage:
43
+ - "Хорошая работа!"
44
+ - "Отлично!"
45
+ - "Прекрасно!"
46
+ desc:
47
+ normalize: "нормализовать файлы переводов (сортировка и распределение)"
48
+ data: "показать данные переводов"
49
+ data_merge: "добавить дерево к переводам"
50
+ data_write: "заменить переводы деревом"
51
+ data_remove: "удалить ключи, которые есть в дереве, из данных"
52
+ health: "Всё ОК?"
53
+ find: "показать, где ключи используются в коде"
54
+ unused: "показать неиспользуемые переводы"
55
+ missing: "показать недостающие переводы"
56
+ translate_missing: "перевести недостающие переводы с Google Translate"
57
+ add_missing: "добавить недостающие ключи к переводам"
58
+ remove_unused: "удалить неиспользуемые ключи"
59
+ eq_base: "показать переводы, равные значениям в основном языке"
60
+ tree_merge: "объединенить деревья"
61
+ tree_filter: "фильтровать дерево по ключу"
62
+ tree_rename_key: "переименовать узел дерева"
63
+ tree_subtract: "дерево A минус ключи в дереве B"
64
+ tree_set_value: "заменить значения ключей"
65
+ tree_convert: "преобразовать дерево между форматами"
66
+ config: "показать конфигурацию"
67
+ gem_path: "показать путь к ruby gem"
68
+ irb: "начать REPL сессию в контексте i18n-tasks"
69
+ xlsx_report: "сохранить недостающие и неиспользуемые переводы в Excel-файл"
70
+ args:
71
+ default_text: "По умолчанию: %{value}"
72
+ default_all: "По умолчанию: все"
73
+ desc:
74
+ out_format: "Формат вывода: %{valid_text}. %{default_text}."
75
+ data_format: "Формат данных: %{valid_text}. %{default_text}."
76
+ keys: "Список ключей, разделенных запятыми (,), пробелами или символами новой строки."
77
+ locales_filter: "Список языков для обработки, разделенный запятыми (,). По умолчанию: все.
78
+ Специальное значение: base."
79
+ locales_to_translate_from: "Язык с которого переводить (по умолчанию: base)"
80
+ locale: "Язык. По умолчанию: base"
81
+ confirm: "Подтвердить автоматом"
82
+ nostdin: "Не читать дерево из стандартного ввода"
83
+ strict: Не угадывать динамические использования ключей, например `t("category.#{category.key}")`
84
+ missing_types: "Типы недостающих переводов: %{valid}. По умолчанию: все"
85
+ key_pattern: "Маска ключа (например, common.*)"
86
+ key_pattern_to_rename: "Полный ключ (шаблон) для переименования. Необходимый параметр."
87
+ new_key_name: "Новое имя, интерполирует оригинальное название как %{key}. Необходимый параметр."
88
+ value: "Значение, интерполируется с %{value}, %{human_key}, %{value_or_human_key}"
89
+ pattern_router: "Использовать pattern_router: ключи распределятся по файлам согласно data.write"
90
+ enum_opt:
91
+ desc: "%{valid_text}. %{default_text}"
92
+ invalid: "%{invalid} не является одним из: %{valid}."
93
+ enum_list_opt:
94
+ desc: "Разделенных запятыми список: %{valid_text}. %{default_text}"
95
+ invalid: "%{invalid} не в: %{valid}."
96
+ errors:
97
+ pass_forest: "Передайте дерево"
98
+ invalid_locale: "Неверный язык %{invalid}"
99
+ invalid_format: "Неизвестный формат %{invalid}. Форматы: %{valid}."
100
+ invalid_missing_type:
101
+ one: "Неизвестный тип %{invalid}. Типы: %{valid}."
102
+ other: "Неизвестные типы: %{invalid}. Типы: %{valid}."
@@ -12,12 +12,6 @@ module I18n::Tasks
12
12
  @i18n = i18n
13
13
  end
14
14
 
15
- def args_with_stdin(opt)
16
- sources = opt[:arguments] || []
17
- sources.unshift $stdin.read unless opt[:nostdin]
18
- sources
19
- end
20
-
21
15
  def safe_run(name, opts)
22
16
  begin
23
17
  coloring_was = Term::ANSIColor.coloring?
@@ -32,16 +26,23 @@ module I18n::Tasks
32
26
  end
33
27
  end
34
28
 
35
- def run(name, opts)
29
+ def run(name, opts = {})
30
+ name = name.to_sym
31
+ public_name = name.to_s.tr '_', '-'
32
+ SlopCommand.parse_opts! opts, self.class.cmds[name][:opt], self
36
33
  if opts.empty?
37
- log_verbose "run #{name.tr('_', '-')} without arguments"
34
+ log_verbose "run #{public_name} without arguments"
38
35
  send name
39
36
  else
40
- log_verbose "run #{name.tr('_', '-')} with #{opts.map { |k, v| "#{k}=#{v}" } * ' '}"
37
+ log_verbose "run #{public_name} with #{opts.map { |k, v| "#{k}=#{v}" } * ' '}"
41
38
  send name, opts
42
39
  end
43
40
  end
44
41
 
42
+ def set_internal_locale!
43
+ I18n.locale = i18n.internal_locale
44
+ end
45
+
45
46
  protected
46
47
 
47
48
  def terminal_report
@@ -52,12 +53,6 @@ module I18n::Tasks
52
53
  @spreadsheet_report ||= I18n::Tasks::Reports::Spreadsheet.new(i18n)
53
54
  end
54
55
 
55
- class << self
56
- def run_command(name, opts)
57
- ::I18n::Tasks::Commands.new.safe_run(name, opts)
58
- end
59
- end
60
-
61
56
  def desc(name)
62
57
  self.class.cmds.try(:[], name).try(:desc)
63
58
  end
@@ -7,38 +7,34 @@ module I18n::Tasks
7
7
  cmd_opt :pattern_router, {
8
8
  short: :p,
9
9
  long: :pattern_router,
10
- desc: I18n.t('i18n_tasks.cmd.args.desc.pattern_router'),
10
+ desc: proc { I18n.t('i18n_tasks.cmd.args.desc.pattern_router') },
11
11
  conf: {argument: false, optional: true}
12
12
  }
13
13
 
14
14
  cmd :normalize,
15
15
  args: '[locale ...]',
16
- desc: I18n.t('i18n_tasks.cmd.desc.normalize'),
16
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.normalize') },
17
17
  opt: cmd_opts(:locales, :pattern_router)
18
18
 
19
19
  def normalize(opt = {})
20
- opt_locales! opt
21
20
  i18n.normalize_store! opt[:locales], opt[:pattern_router]
22
21
  end
23
22
 
24
23
  cmd :data,
25
24
  args: '[locale ...]',
26
- desc: I18n.t('i18n_tasks.cmd.desc.data'),
25
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.data') },
27
26
  opt: cmd_opts(:locales, :out_format)
28
27
 
29
28
  def data(opt = {})
30
- opt_locales! opt
31
- opt_output_format! opt
32
29
  print_forest i18n.data_forest(opt[:locales]), opt
33
30
  end
34
31
 
35
32
  cmd :data_merge,
36
33
  args: '[tree ...]',
37
- desc: I18n.t('i18n_tasks.cmd.desc.data_merge'),
34
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.data_merge') },
38
35
  opt: cmd_opts(:data_format, :nostdin)
39
36
 
40
37
  def data_merge(opt = {})
41
- opt_data_format! opt
42
38
  forest = opt_forests_merged_stdin_args!(opt)
43
39
  merged = i18n.data.merge!(forest)
44
40
  print_forest merged, opt
@@ -46,11 +42,10 @@ module I18n::Tasks
46
42
 
47
43
  cmd :data_write,
48
44
  args: '[tree]',
49
- desc: I18n.t('i18n_tasks.cmd.desc.data_write'),
45
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.data_write') },
50
46
  opt: cmd_opts(:data_format, :nostdin)
51
47
 
52
48
  def data_write(opt = {})
53
- opt_data_format! opt
54
49
  forest = opt_forest_arg_or_stdin!(opt)
55
50
  i18n.data.write forest
56
51
  print_forest forest, opt
@@ -58,11 +53,10 @@ module I18n::Tasks
58
53
 
59
54
  cmd :data_remove,
60
55
  args: '[tree]',
61
- desc: I18n.t('i18n_tasks.cmd.desc.data_remove'),
56
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.data_remove') },
62
57
  opt: cmd_opts(:data_format, :nostdin)
63
58
 
64
59
  def data_remove(opt = {})
65
- opt_data_format! opt
66
60
  removed = i18n.data.remove_by_key!(opt_forest_arg_or_stdin!(opt))
67
61
  log_stderr 'Removed:'
68
62
  print_forest removed, opt
@@ -6,12 +6,10 @@ module I18n::Tasks
6
6
 
7
7
  cmd :eq_base,
8
8
  args: '[locale ...]',
9
- desc: I18n.t('i18n_tasks.cmd.desc.eq_base'),
9
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.eq_base') },
10
10
  opt: cmd_opts(:locales, :out_format)
11
11
 
12
12
  def eq_base(opt = {})
13
- opt_locales! opt
14
- opt_output_format! opt
15
13
  print_forest i18n.eq_base_keys(opt), opt, :eq_base_keys
16
14
  end
17
15
  end
@@ -6,11 +6,10 @@ module I18n::Tasks
6
6
 
7
7
  cmd :health,
8
8
  args: '[locale ...]',
9
- desc: I18n.t('i18n_tasks.cmd.desc.health'),
9
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.health') },
10
10
  opt: cmd_opts(:locales, :out_format)
11
11
 
12
12
  def health(opt = {})
13
- opt_locales! opt
14
13
  forest = i18n.data_forest(opt[:locales])
15
14
  stats = i18n.forest_stats(forest)
16
15
  if stats[:key_count].zero?
@@ -6,7 +6,7 @@ module I18n::Tasks
6
6
 
7
7
  cmd :config,
8
8
  args: '[section ...]',
9
- desc: I18n.t('i18n_tasks.cmd.desc.config')
9
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.config') }
10
10
 
11
11
  def config(opts = {})
12
12
  cfg = i18n.config_for_inspect
@@ -17,7 +17,7 @@ module I18n::Tasks
17
17
  puts cfg
18
18
  end
19
19
 
20
- cmd :gem_path, desc: I18n.t('i18n_tasks.cmd.desc.gem_path')
20
+ cmd :gem_path, desc: proc { I18n.t('i18n_tasks.cmd.desc.gem_path') }
21
21
 
22
22
  def gem_path
23
23
  puts I18n::Tasks.gem_path
@@ -4,38 +4,31 @@ module I18n::Tasks
4
4
  module Missing
5
5
  include Command::Collection
6
6
 
7
- cmd_opt :missing_types, {
8
- short: :t,
9
- long: :types=,
10
- desc: I18n.t('i18n_tasks.cmd.args.desc.missing_types', valid: I18n::Tasks::MissingKeys.missing_keys_types * ', '),
11
- conf: {as: Array, delimiter: /\s*[+:,]\s*/}
12
- }
13
-
14
- DEFAULT_ADD_MISSING_VALUE = '%{value_or_human_key}'
7
+ enum_opt :missing_types, I18n::Tasks::MissingKeys.missing_keys_types
8
+ cmd_opt :missing_types, enum_list_opt_attr(
9
+ :t, :types=, enum_opt(:missing_types),
10
+ proc { |valid, default| I18n.t('i18n_tasks.cmd.args.desc.missing_types', valid: valid, default: default) },
11
+ proc { |invalid, valid| I18n.t('i18n_tasks.cmd.errors.invalid_missing_type', invalid: invalid * ', ', valid: valid * ', ', count: invalid.length) })
15
12
 
16
13
  cmd :missing,
17
14
  args: '[locale ...]',
18
- desc: I18n.t('i18n_tasks.cmd.desc.missing'),
15
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.missing') },
19
16
  opt: cmd_opts(:locales, :out_format, :missing_types)
20
17
 
21
18
  def missing(opt = {})
22
- opt_locales!(opt)
23
- opt_output_format!(opt)
24
- opt_missing_types!(opt)
25
19
  print_forest i18n.missing_keys(opt), opt, :missing_keys
26
20
  end
27
21
 
28
22
  cmd :translate_missing,
29
23
  args: '[locale ...]',
30
- desc: I18n.t('i18n_tasks.cmd.desc.translate_missing'),
24
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.translate_missing') },
31
25
  opt: [cmd_opt(:locales),
32
- cmd_opt(:locale).merge(short: :f, long: :from=, desc: 'Locale to translate from (default: base)'),
26
+ cmd_opt(:locale).merge(short: :f, long: :from=, desc: proc {
27
+ I18n.t('i18n_tasks.cmd.args.desc.locales_to_translate_from') }),
33
28
  cmd_opt(:out_format).except(:short)]
34
29
 
35
30
  def translate_missing(opt = {})
36
- opt_locales! opt
37
- opt_output_format! opt
38
- from = opt_locale! opt, :from
31
+ from = opt[:from]
39
32
  translated = (opt[:locales] - [from]).inject i18n.empty_forest do |result, locale|
40
33
  result.merge! i18n.google_translate_forest i18n.missing_tree(locale, from), from, locale
41
34
  end
@@ -44,29 +37,20 @@ module I18n::Tasks
44
37
  print_forest translated, opt
45
38
  end
46
39
 
40
+ DEFAULT_ADD_MISSING_VALUE = '%{value_or_human_key}'
41
+
47
42
  cmd :add_missing,
48
43
  args: '[locale ...]',
49
- desc: I18n.t('i18n_tasks.cmd.desc.add_missing'),
44
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.add_missing') },
50
45
  opt: cmd_opts(:locales, :out_format) <<
51
46
  cmd_opt(:value).merge(desc: "#{cmd_opt(:value)[:desc]}. #{I18n.t('i18n_tasks.cmd.args.default_text', value: DEFAULT_ADD_MISSING_VALUE)}")
52
47
 
53
48
  def add_missing(opt = {})
54
- opt_locales! opt
55
- opt_output_format! opt
56
49
  forest = i18n.missing_keys(opt).set_each_value!(opt[:value] || DEFAULT_ADD_MISSING_VALUE)
57
50
  i18n.data.merge! forest
58
51
  log_stderr I18n.t('i18n_tasks.add_missing.added', count: forest.leaves.count)
59
52
  print_forest forest, opt
60
53
  end
61
-
62
- private
63
-
64
- def opt_missing_types!(opt)
65
- parse_enum_list_opt(opt[:types], I18n::Tasks::MissingKeys.missing_keys_types) do |invalid, valid|
66
- I18n.t('i18n_tasks.cmd.errors.invalid_missing_type',
67
- invalid: invalid * ', ', valid: valid * ', ', count: invalid.length)
68
- end
69
- end
70
54
  end
71
55
  end
72
56
  end
@@ -6,7 +6,7 @@ module I18n::Tasks
6
6
 
7
7
  cmd :tree_merge,
8
8
  args: '[tree ...]',
9
- desc: I18n.t('i18n_tasks.cmd.desc.tree_merge'),
9
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_merge') },
10
10
  opt: cmd_opts(:data_format, :nostdin)
11
11
 
12
12
  def tree_merge(opts = {})
@@ -15,12 +15,11 @@ module I18n::Tasks
15
15
 
16
16
  cmd :tree_filter,
17
17
  args: '[pattern] [tree]',
18
- desc: I18n.t('i18n_tasks.cmd.desc.tree_filter'),
18
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_filter') },
19
19
  opt: cmd_opts(:data_format, :pattern)
20
20
 
21
21
  def tree_filter(opt = {})
22
- opt_data_format! opt
23
- pattern = opt_or_arg!(:pattern, opt)
22
+ pattern = opt_or_arg! :pattern, opt
24
23
  forest = opt_forest_arg_or_stdin!(opt)
25
24
  unless pattern.blank?
26
25
  pattern_re = i18n.compile_key_pattern(pattern)
@@ -31,15 +30,17 @@ module I18n::Tasks
31
30
 
32
31
  cmd :tree_rename_key,
33
32
  args: '<key> <name> [tree]',
34
- desc: I18n.t('i18n_tasks.cmd.desc.tree_rename_key'),
35
- opt: cmd_opts(:data_format) + [
36
- cmd_opt(:pattern).merge(short: :k, long: :key=, desc: 'Full key (pattern) to rename. Required'),
37
- cmd_opt(:pattern).merge(short: :n, long: :name=, desc: 'New name, interpolates original name as %{key}. Required')]
33
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_rename_key') },
34
+ opt: [
35
+ cmd_opt(:pattern).merge(short: :k, long: :key=, desc: proc {
36
+ I18n.t('i18n_tasks.cmd.args.desc.key_pattern_to_rename') }),
37
+ cmd_opt(:pattern).merge(short: :n, long: :name=, desc: proc {
38
+ I18n.t('i18n_tasks.cmd.args.desc.new_key_name') })
39
+ ] + cmd_opts(:data_format)
38
40
 
39
41
  def tree_rename_key(opt = {})
40
42
  key = opt_or_arg! :key, opt
41
43
  name = opt_or_arg! :name, opt
42
- opt_data_format! opt
43
44
  forest = opt_forest_arg_or_stdin! opt
44
45
  raise CommandError.new('pass full key to rename (-k, --key)') if key.blank?
45
46
  raise CommandError.new('pass new name (-n, --name)') if name.blank?
@@ -49,11 +50,10 @@ module I18n::Tasks
49
50
 
50
51
  cmd :tree_subtract,
51
52
  args: '[tree A] [tree B ...]',
52
- desc: I18n.t('i18n_tasks.cmd.desc.tree_subtract'),
53
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_subtract') },
53
54
  opt: cmd_opts(:data_format, :nostdin)
54
55
 
55
56
  def tree_subtract(opt = {})
56
- opt_data_format! opt
57
57
  forests = opt_forests_stdin_args! opt, 2
58
58
  forest = forests.reduce(:subtract_by_key) || empty_forest
59
59
  print_forest forest, opt
@@ -61,11 +61,10 @@ module I18n::Tasks
61
61
 
62
62
  cmd :tree_set_value,
63
63
  args: '[value] [tree]',
64
- desc: I18n.t('i18n_tasks.cmd.desc.tree_set_value'),
64
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_set_value') },
65
65
  opt: cmd_opts(:value, :data_format, :nostdin, :pattern)
66
66
 
67
67
  def tree_set_value(opt = {})
68
- opt_data_format! opt
69
68
  value = opt_or_arg! :value, opt
70
69
  forest = opt_forest_arg_or_stdin!(opt)
71
70
  key_pattern = opt[:pattern]
@@ -76,13 +75,11 @@ module I18n::Tasks
76
75
 
77
76
  cmd :tree_convert,
78
77
  args: '<tree>',
79
- desc: I18n.t('i18n_tasks.cmd.desc.tree_convert'),
78
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.tree_convert') },
80
79
  opt: [cmd_opt(:data_format).merge(short: :f, long: :from=),
81
80
  cmd_opt(:out_format).merge(short: :t, long: :to=)]
82
81
 
83
82
  def tree_convert(opt = {})
84
- opt_data_format! opt, :from
85
- opt_output_format! opt, :to
86
83
  forest = opt_forest_arg_or_stdin! opt.merge(format: opt[:from])
87
84
  print_forest forest, opt.merge(format: opt[:to])
88
85
  end
@@ -7,39 +7,34 @@ module I18n::Tasks
7
7
  cmd_opt :strict, {
8
8
  short: :s,
9
9
  long: :strict,
10
- desc: I18n.t('i18n_tasks.cmd.args.desc.strict')
10
+ desc: proc { I18n.t('i18n_tasks.cmd.args.desc.strict') }
11
11
  }
12
12
 
13
13
  cmd :find,
14
14
  args: '[pattern]',
15
- desc: I18n.t('i18n_tasks.cmd.desc.find'),
15
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.find') },
16
16
  opt: cmd_opts(:out_format, :pattern)
17
17
 
18
18
  def find(opt = {})
19
- opt_output_format! opt
20
19
  opt[:filter] ||= opt.delete(:pattern) || opt[:arguments].try(:first)
21
20
  print_forest i18n.used_tree(key_filter: opt[:filter].presence, source_occurrences: true), opt, :used_keys
22
21
  end
23
22
 
24
23
  cmd :unused,
25
24
  args: '[locale ...]',
26
- desc: I18n.t('i18n_tasks.cmd.desc.unused'),
25
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.unused') },
27
26
  opt: cmd_opts(:locales, :out_format, :strict)
28
27
 
29
28
  def unused(opt = {})
30
- opt_locales! opt
31
- opt_output_format! opt
32
29
  print_forest i18n.unused_keys(opt), opt, :unused_keys
33
30
  end
34
31
 
35
32
  cmd :remove_unused,
36
33
  args: '[locale ...]',
37
- desc: I18n.t('i18n_tasks.cmd.desc.remove_unused'),
34
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.remove_unused') },
38
35
  opt: cmd_opts(:locales, :out_format, :strict, :confirm)
39
36
 
40
37
  def remove_unused(opt = {})
41
- opt_locales! opt
42
- opt_output_format! opt
43
38
  unused_keys = i18n.unused_keys(opt)
44
39
  if unused_keys.present?
45
40
  terminal_report.unused_keys(unused_keys)
@@ -6,7 +6,7 @@ module I18n::Tasks
6
6
 
7
7
  cmd :xlsx_report,
8
8
  args: '[locale...]',
9
- desc: 'save missing and unused translations to an Excel file',
9
+ desc: proc { I18n.t('i18n_tasks.cmd.desc.xlsx_report') },
10
10
  opt: [cmd_opt(:locales),
11
11
  {short: :p, long: :path=, desc: 'Destination path', conf: {default: 'tmp/i18n-report.xlsx'}}]
12
12
 
@@ -18,7 +18,6 @@ module I18n::Tasks
18
18
  log_stderr Term::ANSIColor.red Term::ANSIColor.bold message
19
19
  exit 1
20
20
  end
21
- opt_locales! opt
22
21
  spreadsheet_report.save_report opt[:path], opt.except(:path)
23
22
  end
24
23
  end
@@ -11,14 +11,33 @@ module I18n::Tasks
11
11
  end
12
12
 
13
13
  DEFAULT_ENUM_OPT_DESC = proc { |valid, default|
14
- I18n.t('i18n_tasks.cmd.enum_opt.desc.default', valid_text: valid, default_text: default)
14
+ I18n.t('i18n_tasks.cmd.enum_opt.desc', valid_text: valid, default_text: default)
15
15
  }
16
16
 
17
- def enum_opt_attr(short, long, valid, &desc)
17
+ def enum_opt_attr(short, long, valid, desc, error_msg)
18
18
  desc ||= DEFAULT_ENUM_OPT_DESC
19
- {short: short, long: long.to_sym,
20
- desc: desc.call(valid * ', ', I18n.t('i18n_tasks.cmd.args.default_text', value: valid.first)),
21
- conf: {default: valid.first, argument: true, optional: false}}
19
+ desc_proc = proc { desc.call(valid * ', ', I18n.t('i18n_tasks.cmd.args.default_text', value: valid.first)) }
20
+ {short: short, long: long, desc: desc_proc,
21
+ conf: {default: valid.first, argument: true, optional: false},
22
+ parse: enum_parse_proc(:parse_enum_opt, valid, &error_msg)}
23
+ end
24
+
25
+ DEFAULT_LIST_OPT_DESC = proc { |valid, default|
26
+ I18n.t('i18n_tasks.cmd.enum_list_opt.desc', valid_text: valid, default_text: default)
27
+ }
28
+
29
+ def enum_list_opt_attr(short, long, valid, desc, error_msg)
30
+ desc ||= DEFAULT_LIST_OPT_DESC
31
+ desc_proc = proc { desc.call(valid * ', ', I18n.t('i18n_tasks.cmd.args.default_all')) }
32
+ {short: short, long: long, desc: desc_proc,
33
+ conf: {as: Array, delimiter: /\s*[+:,]\s*/},
34
+ parse: enum_parse_proc(:parse_enum_list_opt, valid, &error_msg)}
35
+ end
36
+
37
+ def enum_parse_proc(method, valid, &error)
38
+ proc { |opt, key|
39
+ opt[key] = send(method, opt[key], valid, &error)
40
+ }
22
41
  end
23
42
  end
24
43
  end
@@ -14,28 +14,28 @@ module I18n::Tasks
14
14
  cmd_opt :nostdin, {
15
15
  short: :S,
16
16
  long: :nostdin,
17
- desc: I18n.t('i18n_tasks.cmd.args.desc.nostdin'),
17
+ desc: proc { I18n.t('i18n_tasks.cmd.args.desc.nostdin') },
18
18
  conf: {default: false}
19
19
  }
20
20
 
21
21
  cmd_opt :confirm, {
22
22
  short: :y,
23
23
  long: :confirm,
24
- desc: I18n.t('i18n_tasks.cmd.args.desc.confirm'),
24
+ desc: proc { I18n.t('i18n_tasks.cmd.args.desc.confirm') },
25
25
  conf: {default: false}
26
26
  }
27
27
 
28
28
  cmd_opt :pattern, {
29
29
  short: :p,
30
30
  long: :pattern=,
31
- desc: I18n.t('i18n_tasks.cmd.args.desc.key_pattern'),
31
+ desc: proc { I18n.t('i18n_tasks.cmd.args.desc.key_pattern') },
32
32
  conf: {argument: true, optional: false}
33
33
  }
34
34
 
35
35
  cmd_opt :value, {
36
36
  short: :v,
37
37
  long: :value=,
38
- desc: I18n.t('i18n_tasks.cmd.args.desc.value'),
38
+ desc: proc { I18n.t('i18n_tasks.cmd.args.desc.value') },
39
39
  conf: {argument: true, optional: false}
40
40
  }
41
41
 
@@ -3,7 +3,7 @@ module I18n::Tasks
3
3
  module Options
4
4
  module EnumOpt
5
5
  DEFAULT_ENUM_OPT_ERROR = proc { |bad, good|
6
- I18n.t('i18n_tasks.cmd.enum_opt.invalid_one', invalid: bad, valid: good * ', ')
6
+ I18n.t('i18n_tasks.cmd.enum_opt.invalid', invalid: bad, valid: good * ', ')
7
7
  }
8
8
 
9
9
  def parse_enum_opt(value, valid, &error_msg)
@@ -23,14 +23,18 @@ module I18n::Tasks
23
23
  end
24
24
 
25
25
  DEFAULT_ENUM_LIST_ERROR = proc { |bad, good|
26
- I18n.t('i18n_tasks.cmd.enum_opt.invalid_list', invalid: bad * ', ', valid: good * ', ')
26
+ I18n.t('i18n_tasks.cmd.enum_list_opt.invalid', invalid: bad * ', ', valid: good * ', ')
27
27
  }
28
28
 
29
29
  def parse_enum_list_opt(values, valid, &error_msg)
30
30
  values = explode_list_opt(values)
31
31
  invalid = values - valid.map(&:to_s)
32
32
  if invalid.empty?
33
- values
33
+ if values.empty?
34
+ valid
35
+ else
36
+ values
37
+ end
34
38
  else
35
39
  error_msg ||= DEFAULT_ENUM_LIST_ERROR
36
40
  raise CommandError.new error_msg.call(invalid, valid)
@@ -8,17 +8,20 @@ module I18n::Tasks
8
8
  short: :l,
9
9
  long: :locales=,
10
10
  desc: I18n.t('i18n_tasks.cmd.args.desc.locales_filter'),
11
- conf: {as: Array, delimiter: /\s*[+:,]\s*/, default: 'all', argument: true, optional: false}
11
+ conf: {as: Array, delimiter: /\s*[+:,]\s*/, default: 'all', argument: true, optional: false},
12
+ parse: :parse_locales
12
13
  }
14
+
13
15
  cmd_opt :locale, {
14
16
  short: :l,
15
17
  long: :locale=,
16
18
  desc: I18n.t('i18n_tasks.cmd.args.desc.locale'),
17
- conf: {default: 'base', argument: true, optional: false}
19
+ conf: {default: 'base', argument: true, optional: false},
20
+ parse: :parse_locale
18
21
  }
19
22
 
20
- def opt_locales!(opt)
21
- argv = Array(opt[:arguments]) + Array(opt[:locales])
23
+ def parse_locales(opt, key = :locales)
24
+ argv = Array(opt[:arguments]) + Array(opt[key])
22
25
  locales = if argv == ['all'] || argv == 'all' || argv.blank?
23
26
  i18n.locales
24
27
  else
@@ -26,16 +29,15 @@ module I18n::Tasks
26
29
  end
27
30
  locales.each { |locale| validate_locale!(locale) }
28
31
  log_verbose "locales for the command are #{locales.inspect}"
29
- opt[:locales] = locales
32
+ opt[key] = locales
30
33
  end
31
34
 
32
- def opt_locale!(opt, key = :locale)
35
+ def parse_locale(opt, key = :locale)
33
36
  val = opt[key]
34
37
  opt[key] = base_locale if val.blank? || val == 'base'
35
38
  opt[key]
36
39
  end
37
40
 
38
-
39
41
  VALID_LOCALE_RE = /\A\w[\w\-_\.]*\z/i
40
42
 
41
43
  def validate_locale!(locale)
@@ -3,27 +3,24 @@ module I18n::Tasks
3
3
  module Options
4
4
  module Trees
5
5
  extend Command::DSL
6
+ format_opt = proc { |type|
7
+ enum_opt_attr :f, :format=, enum_opt(type),
8
+ proc { |valid, default|
9
+ I18n.t("i18n_tasks.cmd.args.desc.#{type}", valid_text: valid, default_text: default) },
10
+ proc { |value, valid|
11
+ I18n.t('i18n_tasks.cmd.errors.invalid_format', invalid: value, valid: valid * ', ') }
12
+ }
6
13
 
7
14
  enum_opt :data_format, %w(yaml json keys)
8
- cmd_opt :data_format, enum_opt_attr(:f, :format=, enum_opt(:data_format)) { |valid_text, default_text|
9
- I18n.t('i18n_tasks.cmd.args.desc.data_format', valid_text: valid_text, default_text: default_text)
10
- }
15
+ # i18n-tasks-use t('i18n_tasks.cmd.args.desc.data_format')
16
+ cmd_opt :data_format, format_opt.call(:data_format)
11
17
 
12
18
  enum_opt :out_format, ['terminal-table', *enum_opt(:data_format), 'inspect']
13
- cmd_opt :out_format, enum_opt_attr(:f, :format=, enum_opt(:out_format)) { |valid_text, default_text|
14
- I18n.t('i18n_tasks.cmd.args.desc.out_format', valid_text: valid_text, default_text: default_text)
15
- }
16
-
17
- cmd_opt :keys, {
18
- short: :k,
19
- long: :keys=,
20
- desc: I18n.t('i18n_tasks.cmd.args.desc.keys'),
21
- conf: {as: Array, delimiter: /[+:,]/, argument: true, optional: false}
22
- }
19
+ # i18n-tasks-use t('i18n_tasks.cmd.args.desc.out_format')
20
+ cmd_opt :out_format, format_opt.call(:out_format)
23
21
 
24
22
  def print_forest(forest, opt, version = :show_tree)
25
23
  format = opt[:format].to_s
26
-
27
24
  case format
28
25
  when 'terminal-table'
29
26
  terminal_report.send(version, forest)
@@ -36,34 +33,18 @@ module I18n::Tasks
36
33
  end
37
34
  end
38
35
 
39
- INVALID_FORMAT_MSG = proc do |value, valid|
40
- I18n.t('i18n_tasks.cmd.errors.invalid_format', invalid: value, valid: valid * ', ')
41
- end
42
-
43
- def opt_output_format!(opt = {}, key = :format)
44
- opt[key] = parse_enum_opt opt[key], :out_format, &INVALID_FORMAT_MSG
45
- end
46
-
47
- def opt_data_format!(opt = {}, key = :format)
48
- opt[key] = parse_enum_opt opt[key], :data_format, &INVALID_FORMAT_MSG
49
- end
50
-
51
- def opt_args_keys!(opt = {})
52
- opt[:keys] = explode_list_opt(opt[:keys]) + Array(opt[:arguments])
53
- end
54
-
55
- def opt_forest_arg_or_stdin!(opt)
36
+ def opt_forest_arg_or_stdin!(opt, format = opt[:format])
56
37
  src = opt[:arguments].try(:shift) || $stdin.read
57
- parse_forest(src, opt)
38
+ parse_forest(src, format)
58
39
  end
59
40
 
60
- def opt_forests_stdin_args!(opt, num = false)
41
+ def opt_forests_stdin_args!(opt, num = false, format = opt[:format])
61
42
  args = opt[:arguments] || []
62
43
  if opt[:nostdin]
63
44
  sources = []
64
45
  else
65
46
  sources = [$stdin.read]
66
- num -= 1 if num
47
+ num -= 1 if num
67
48
  end
68
49
  if num
69
50
  num.times { sources << args.shift }
@@ -71,7 +52,7 @@ module I18n::Tasks
71
52
  sources += args
72
53
  args.clear
73
54
  end
74
- sources.map { |src| parse_forest(src, opt) }
55
+ sources.map { |src| parse_forest(src, format) }
75
56
  end
76
57
 
77
58
  def opt_forests_merged_stdin_args!(opt)
@@ -80,11 +61,10 @@ module I18n::Tasks
80
61
  }
81
62
  end
82
63
 
83
- def parse_forest(src, opt = {})
64
+ def parse_forest(src, format)
84
65
  if !src
85
66
  raise CommandError.new(I18n.t('i18n_tasks.cmd.errors.pass_forest'))
86
67
  end
87
- format = opt_data_format!(opt)
88
68
  if format == 'keys'
89
69
  Data::Tree::Siblings.from_key_names parse_keys(src)
90
70
  else
@@ -70,6 +70,9 @@ module I18n::Tasks::Configuration
70
70
  @config_sections[:base_locale] ||= (config[:base_locale] || 'en').to_s
71
71
  end
72
72
 
73
+ def internal_locale
74
+ @config_sections[:internal_locale] ||= (config[:internal_locale] || 'en').to_s
75
+ end
73
76
 
74
77
  def ignore_config(type = nil)
75
78
  key = type ? "ignore_#{type}" : 'ignore'
@@ -81,6 +84,7 @@ module I18n::Tasks::Configuration
81
84
  def config_sections
82
85
  # init all sections
83
86
  base_locale
87
+ internal_locale
84
88
  locales
85
89
  data_config
86
90
  search_config
@@ -4,24 +4,37 @@ module I18n::Tasks::SlopCommand
4
4
  def slop_command(name, attr, &block)
5
5
  proc {
6
6
  command name.tr('_', '-') do
7
- opts = attr[:opt]
8
7
  args = attr[:args]
9
8
  banner "Usage: i18n-tasks #{name} [options] #{args}" if args.present?
10
9
  desc = attr[:desc]
10
+ desc = desc.call if desc.respond_to?(:call)
11
11
  description desc if desc
12
- if opts
13
- opts.each do |opt|
14
- on *[:short, :long, :desc, :conf].map { |k| opt[k] }.compact
15
- end
12
+ attr[:opt].try :each do |opt|
13
+ on *opt.values_at(:short, :long, :desc, :conf).compact.map { |v| v.respond_to?(:call) ? v.call : v }
16
14
  end
17
- run { |opts, args| block.call(name, opts, args) }
15
+ run { |slop_opts, slop_args|
16
+ slop_opts = slop_opts.to_hash(true).reject { |k, v| v.nil? }
17
+ slop_opts.merge!(arguments: slop_args) unless slop_args.empty?
18
+ block.call name, slop_opts
19
+ }
18
20
  end
19
21
  }
20
22
  end
21
23
 
22
- def parse_slop_opts_args(opts, args)
23
- opts = opts.to_hash(true).reject { |k, v| v.nil? }
24
- opts.merge!(arguments: args) unless args.empty?
25
- opts
24
+ def parse_opts!(opts, opts_conf, context)
25
+ return if !opts_conf
26
+ opts_conf.each do |opt_conf|
27
+ parse = opt_conf[:parse]
28
+ if parse
29
+ key = opt_conf[:long].to_s.sub(/=\z/, '').to_sym
30
+ if parse.respond_to?(:call)
31
+ context.instance_exec opts, key, &parse
32
+ elsif Symbol === parse
33
+ context.instance_exec do
34
+ send parse, opts, key
35
+ end
36
+ end
37
+ end
38
+ end
26
39
  end
27
40
  end
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
2
  module I18n
3
3
  module Tasks
4
- VERSION = '0.7.1'
4
+ VERSION = '0.7.2'
5
5
  end
6
6
  end
@@ -53,7 +53,7 @@ describe 'Google Translation' do
53
53
  }
54
54
  })
55
55
 
56
- cmd.translate_missing
56
+ cmd.run :translate_missing
57
57
  expect(task.t('common.hello', 'es')).to eq(text_test[2])
58
58
  expect(task.t('common.hello_html', 'es')).to eq(html_test[2])
59
59
  expect(task.t('common.array_key', 'es')).to eq(array_test[2])
@@ -24,7 +24,7 @@ module TestCodebase
24
24
  def run_cmd(name, *args, &block)
25
25
  in_test_app_dir do
26
26
  silence_stderr {
27
- capture_stdout { i18n_cmd.send(name, *args, &block) }
27
+ capture_stdout { i18n_cmd.run(name, *args, &block) }
28
28
  }
29
29
  end
30
30
  end
@@ -5,6 +5,9 @@ base_locale: en
5
5
  ## uncomment to set locales explicitly
6
6
  # locales: [en, es, fr]
7
7
 
8
+ ## i18n-tasks report locale, default: en, available: en, ru
9
+ # internal_locale: ru
10
+
8
11
  # Read and write locale data
9
12
  data:
10
13
  ## by default, translation data are read from the file system, or you can provide a custom data adapter
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n-tasks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - glebm
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-30 00:00:00.000000000 Z
11
+ date: 2014-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: erubis
@@ -217,6 +217,7 @@ files:
217
217
  - bin/i18n-tasks
218
218
  - config/i18n-tasks.yml
219
219
  - config/locales/en.yml
220
+ - config/locales/ru.yml
220
221
  - i18n-tasks.gemspec
221
222
  - lib/i18n/tasks.rb
222
223
  - lib/i18n/tasks/base_task.rb