diff --git a/lib/i18n/tasks/base_task.rb b/lib/i18n/tasks/base_task.rb index ffdbd1ce..d798b541 100644 --- a/lib/i18n/tasks/base_task.rb +++ b/lib/i18n/tasks/base_task.rb @@ -4,6 +4,7 @@ require 'i18n/tasks/key_pattern_matching' require 'i18n/tasks/logging' require 'i18n/tasks/plural_keys' +require 'i18n/tasks/references' require 'i18n/tasks/html_keys' require 'i18n/tasks/used_keys' require 'i18n/tasks/ignore_keys' @@ -23,6 +24,7 @@ class BaseTask include SplitKey include KeyPatternMatching include PluralKeys + include References include HtmlKeys include UsedKeys include IgnoreKeys diff --git a/lib/i18n/tasks/references.rb b/lib/i18n/tasks/references.rb new file mode 100644 index 00000000..668b0e5e --- /dev/null +++ b/lib/i18n/tasks/references.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true +module I18n::Tasks + module References + # Given a tree of key usages, return all the reference keys in the tree in their resolved form. + # @param usages [Data::Tree::Siblings] + # @param references [Data::Tree::Siblings] + # @return [Array] a list of all references and their resolutions. + def resolve_references(usages, references) + usages.each.flat_map do |node| + references.key_to_node.flat_map do |ref_key_part, ref_node| + if node.key == ref_key_part + if ref_node.leaf? + [ref_node.full_key(root: false)] + + if node.leaf? + [ref_node.value.to_s] + else + node.children.flat_map { |child| + collect_referenced_keys(child, [ref_node.value.to_s]) + } + end + else + resolve_references(node.children, ref_node.children) + end + else + [] + end + end + end + end + + # Given a node, return the keys of all the leaves up to the given node prefixed with the given prefix. + # @param node [Data::Tree::Node] + # @param prefix [Array] + # @return Array full keys + def collect_referenced_keys(node, prefix) + if node.leaf? + (prefix + [node.key]) * '.' + else + node.children.flat_map { |child| collect_referenced_keys(child, prefix + [node.key]) } + end + end + + # Given a forest of references, merge trees into one tree, ensuring there are no conflicting references. + # @param roots [Data::Tree::Siblings] + # @return [Data::Tree::Siblings] + def merge_reference_trees(roots) + roots.inject(empty_forest) do |forest, root| + root.keys { |full_key, node| + ::I18n::Tasks::Logging.log_warn( + "Self-referencing node: #{node.full_key.inspect} is #{node.value.inspect} in #{node.data[:locale]}" + ) if full_key == node.value.to_s + } + forest.merge!( + root.children, + leaves_merge_guard: -> (node, other) { + ::I18n::Tasks::Logging.log_warn( + "Conflicting references: #{node.full_key.inspect} is #{node.value.inspect} in #{node.data[:locale]}, but #{other.value.inspect} in #{other.data[:locale]}" + ) if node.value != other.value + }) + end + end + end +end diff --git a/spec/references_spec.rb b/spec/references_spec.rb new file mode 100644 index 00000000..c741cada --- /dev/null +++ b/spec/references_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'Reference keys' do + let(:task) { ::I18n::Tasks::BaseTask.new } + + describe '#resolve_references' do + it 'resolves plain references' do + result = task.resolve_references( + build_tree('en' => { + 'reference' => nil, + 'not-a-reference' => nil + }), + build_tree('en' => { + 'reference' => :resolved + })) + expect(result).to(eq %w(reference resolved)) + end + + it 'resolves nested references' do + result = task.resolve_references( + build_tree('en' => { + 'reference' => {'a' => nil, 'b' => {'c' => nil}}, + 'not-a-reference' => nil + }), + build_tree('en' => { + 'reference' => :resolved + })) + expect(result).to(eq %w(reference resolved.a resolved.b.c)) + end + + it 'resolves nested references with nested keys' do + result = task.resolve_references( + build_tree('en' => { + 'nested' => {'reference' => {'a' => nil, 'b' => {'c' => nil}}}, + 'not-a-reference' => nil + }), + build_tree('en' => { + 'nested' => {'reference' => :resolved} + })) + expect(result).to(eq %w(nested.reference resolved.a resolved.b.c)) + end + + it 'returns empty array when nothing to resolve' do + result = task.resolve_references( + build_tree('en' => { + 'not-a-reference' => nil + }), + build_tree('en' => { + 'reference' => :resolved + })) + expect(result).to(eq []) + end + end +end