Skip to content

Commit

Permalink
New tasks: mv, rm, and tree-mv
Browse files Browse the repository at this point in the history
Move keys easily from the command line!

Fixes #116
  • Loading branch information
glebm committed Dec 27, 2016
1 parent eeed6db commit ebda572
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 18 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,50 @@ Sort the keys, and move them to the respective files as defined by [`config.writ
$ i18n-tasks normalize -p
```

### Move / rename / merge keys

`i18n-tasks mv <pattern> <target>` is a versatile task to move or delete keys matching the given pattern.

All nodes (leafs or subtrees) matching [`<pattern>`](#key-pattern-syntax) are merged together and moved to `<target>`.

Rename a node (leaf or subtree):

``` console
$ i18n-tasks mv user account
```

Move a node:

``` console
$ i18n-tasks mv user_alerts user.alerts
```

Move the children one level up:

``` console
$ i18n-tasks mv 'alerts.{:}' '\1'
```

Merge-move multiple nodes:

``` console
$ i18n-tasks mv '{user,profile}' account
```

Merge (non-leaf) nodes into parent:

``` console
$ i18n-tasks mv '{pages}.{a,b}' '\1'
```

### Delete keys

Delete the keys by using the `rm` task:

```console
$ i18n-tasks rm 'user.{old_profile,old_title}' another_key
```

### Compose tasks

`i18n-tasks` also provides composable tasks for reading, writing and manipulating locale data. Examples below.
Expand Down
3 changes: 3 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,15 @@ en:
health: is everything OK?
irb: start REPL session within i18n-tasks context
missing: show missing translations
mv: rename/merge the keys in locale data that match the given pattern
normalize: 'normalize translation data: sort and move to the right files'
remove_unused: remove unused keys
rm: remove the keys in locale data that match the given pattern
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_mv_key: rename/merge/remove the keys matching the given pattern
tree_rename_key: rename tree node
tree_set_value: set values of keys, optionally match a pattern
tree_subtract: tree A minus the keys in tree B
Expand Down
3 changes: 3 additions & 0 deletions config/locales/ru.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,15 @@ ru:
health: Всё ОК?
irb: начать REPL сессию в контексте i18n-tasks
missing: показать недостающие переводы
mv: переименовать / объединить ключи, которые соответствуют заданному шаблону
normalize: нормализовать файлы переводов (сортировка и распределение)
remove_unused: удалить неиспользуемые ключи
rm: удалить ключи, которые соответствуют заданному шаблону
translate_missing: перевести недостающие переводы с Google Translate
tree_convert: преобразовать дерево между форматами
tree_filter: фильтровать дерево по ключу
tree_merge: объединенить деревья
tree_mv_key: переименованить / объединить / удалить ключи соответствующие заданному шаблону
tree_rename_key: переименовать узел дерева
tree_set_value: заменить значения ключей
tree_subtract: дерево A минус ключи в дереве B
Expand Down
26 changes: 26 additions & 0 deletions lib/i18n/tasks/command/commands/data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,32 @@ def normalize(opt = {})
i18n.normalize_store! opt[:locales], opt[:pattern_router]
end

cmd :mv,
pos: 'FROM_KEY_PATTERN TO_KEY_PATTERN',
desc: t('i18n_tasks.cmd.desc.mv')
def mv(opt = {})
fail CommandError, 'requires FROM_KEY_PATTERN and TO_KEY_PATTERN' if opt[:arguments].size < 2
from_pattern = opt[:arguments].shift
to_pattern = opt[:arguments].shift
forest = i18n.data_forest
results = forest.mv_key!(compile_key_pattern(from_pattern), to_pattern, root: false)
i18n.data.write forest
terminal_report.mv_results results
end

cmd :rm,
pos: 'KEY_PATTERN [KEY_PATTERN...]',
desc: t('i18n_tasks.cmd.desc.rm')
def rm(opt = {})
fail CommandError, 'requires KEY_PATTERN' if opt[:arguments].empty?
forest = i18n.data_forest
results = opt[:arguments].each_with_object({}) do |key_pattern, h|
h.merge! forest.mv_key!(compile_key_pattern(key_pattern), '', root: false)
end
i18n.data.write forest
terminal_report.mv_results results
end

cmd :data,
pos: '[locale ...]',
desc: t('i18n_tasks.cmd.desc.data'),
Expand Down
14 changes: 14 additions & 0 deletions lib/i18n/tasks/command/commands/tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Command
module Commands
module Tree
include Command::Collection
include I18n::Tasks::KeyPatternMatching

cmd :tree_translate,
pos: '[tree (or stdin)]',
Expand Down Expand Up @@ -56,6 +57,19 @@ def tree_rename_key(opt = {})
print_forest forest, opt
end

cmd :tree_mv,
pos: 'FROM_KEY_PATTERN TO_KEY_PATTERN [tree (or stdin)]',
desc: t('i18n_tasks.cmd.desc.tree_mv_key'),
args: [:data_format]
def tree_mv(opt = {})
fail CommandError, 'requires FROM_KEY_PATTERN and TO_KEY_PATTERN' if opt[:arguments].size < 2
from_pattern = opt[:arguments].shift
to_pattern = opt[:arguments].shift
forest = forest_pos_or_stdin!(opt)
forest.mv_key!(compile_key_pattern(from_pattern), to_pattern, root: false)
print_forest forest, opt
end

cmd :tree_subtract,
pos: '[[tree] [tree] ... (or stdin)]',
desc: t('i18n_tasks.cmd.desc.tree_subtract'),
Expand Down
2 changes: 2 additions & 0 deletions lib/i18n/tasks/command/options/data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module Data
# i18n-tasks-use t('i18n_tasks.cmd.args.desc.out_format')
format_arg.call(:out_format, OUT_FORMATS)

# @return [I18n::Tasks::Data::Tree::Siblings]
def forest_pos_or_stdin!(opt, format = opt[:format])
parse_forest(pos_or_stdin!(opt), format)
end
Expand Down Expand Up @@ -59,6 +60,7 @@ def merge_forests_stdin_and_pos!(opt)
end
end

# @return [I18n::Tasks::Data::Tree::Siblings]
def parse_forest(src, format)
unless src
fail CommandError, I18n.t('i18n_tasks.cmd.errors.pass_forest')
Expand Down
3 changes: 1 addition & 2 deletions lib/i18n/tasks/data/tree/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ def append(nodes)
derive.append!(nodes)
end

def full_key(opts = {})
root = opts.key?(:root) ? opts[:root] : true
def full_key(root: true)
@full_key ||= {}
@full_key[root] ||= "#{"#{parent.full_key(root: root)}." if parent? && (root || parent.parent?)}#{key}"
end
Expand Down
38 changes: 38 additions & 0 deletions lib/i18n/tasks/data/tree/siblings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,44 @@ def rename_each_key!(full_key_pattern, new_key_tpl)
self
end

# @param from_pattern [Regexp]
# @param to_pattern [Regexp]
# @param root [Boolean]
# @return {old key => new key}
def mv_key!(from_pattern, to_pattern, root: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
moved_forest = Siblings.new
moved_nodes = []
old_key_to_new_key = {}
nodes do |node|
full_key = node.full_key(root: root)
if from_pattern =~ full_key
moved_nodes << node
if to_pattern.empty?
old_key_to_new_key[full_key] = nil
next
end
match = $~
new_key = to_pattern.gsub(/\\\d+/) { |m| match[m[1..-1].to_i] }
old_key_to_new_key[full_key] = new_key
moved_forest.merge!(Siblings.new.tap do |forest|
forest[[(node.root.try(:key) unless root), new_key].compact.join('.')] =
node.derive(key: split_key(new_key).last)
end)
end
end
# Adjust references
# TODO: support nested references better
nodes do |node|
if node.reference?
new_target = old_key_to_new_key[node.value.to_s]
node.value = new_target.to_sym if new_target
end
end
remove_nodes_and_emptied_ancestors! moved_nodes
merge! moved_forest
old_key_to_new_key
end

def replace_node!(node, new_node)
@list[@list.index(node)] = new_node
key_to_node[new_node.key] = new_node
Expand Down
41 changes: 27 additions & 14 deletions lib/i18n/tasks/data/tree/traversal.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# frozen_string_literal: true
require 'set'

module I18n::Tasks
module Data::Tree
# Any Enumerable that yields nodes can mix in this module
Expand Down Expand Up @@ -51,10 +53,9 @@ def depth_first(&visitor)
end

# @option root include root in full key
def keys(key_opts = {}, &visitor)
key_opts[:root] = false unless key_opts.key?(:root)
return to_enum(:keys, key_opts) unless visitor
leaves { |node| visitor.yield(node.full_key(key_opts), node) }
def keys(root: false, &visitor)
return to_enum(:keys, root: root) unless visitor
leaves { |node| visitor.yield(node.full_key(root: root), node) }
self
end

Expand Down Expand Up @@ -112,24 +113,36 @@ def select_nodes!(&block)
self
end

# @return Siblings
def select_keys(opts = {}, &block)
root = opts.key?(:root) ? opts[:root] : false
ok = {}
# @return [Siblings]
def select_keys(root: false, &block)
matches = get_nodes_by_key_filter(root: root, &block)
select_nodes do |node|
matches.include?(node)
end
end

# @return [Siblings]
def select_keys!(root: false, &block)
matches = get_nodes_by_key_filter(root: root, &block)
select_nodes! do |node|
matches.include?(node)
end
end

# @return [Set<I18n::Tasks::Data::Tree::Node>]
def get_nodes_by_key_filter(root: false, &block)
matches = Set.new
keys(root: root) do |full_key, node|
if block.yield(full_key, node)
node.walk_to_root do |p|
break if ok[p]
ok[p] = true
break unless matches.add?(p)
end
end
end
select_nodes do |node|
ok[node]
end
matches
end

# @return Siblings
# @return [Siblings]
def intersect_keys(other_tree, key_opts = {}, &block)
if block
select_keys(key_opts) do |key, node|
Expand Down
10 changes: 10 additions & 0 deletions lib/i18n/tasks/reports/terminal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ def forest_stats(forest, stats = task.forest_stats(forest))
print_info "#{cyan title} #{cyan text}"
end

def mv_results(results)
results.each do |(from, to)|
if to
print_info "#{cyan from} #{bold(yellow('⮕'))} #{cyan to}"
else
print_info "#{red from}#{bold(red(' 🗑'))}"
end
end
end

private

def missing_key_info(leaf)
Expand Down
8 changes: 7 additions & 1 deletion spec/commands/data_commands_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
require 'spec_helper'

RSpec.describe 'Data commands' do
delegate :run_cmd, to: :TestCodebase
delegate :run_cmd, :in_test_app_dir, to: :TestCodebase
def en_data
{ 'en' => { 'a' => '1', 'common' => { 'hello' => 'Hello' } } }
end
Expand Down Expand Up @@ -37,4 +37,10 @@ def en_data_2
eq('en' => { 'common' => en_data['en']['common'] })
)
end

it '#mv' do
run_cli('mv', 'a', 'b')
expect(in_test_app_dir { YAML.load_file('config/locales/en.yml') })
.to(eq('en' => { 'b' => '1', 'common' => { 'hello' => 'Hello' } }))
end
end
Loading

0 comments on commit ebda572

Please sign in to comment.