diff --git a/lib/helpers.rb b/lib/helpers.rb new file mode 100644 index 0000000..669bdec --- /dev/null +++ b/lib/helpers.rb @@ -0,0 +1,24 @@ +module Helpers + + def self.normalize_yaml(yaml) + return '' if yaml.nil? + return yaml if yaml.is_a? String + return yaml.to_s if yaml.is_a? Numeric + return yaml.to_s if !!yaml == yaml # if boolean + return ":#{yaml.to_s}" if yaml.is_a? Symbol + yaml = array_to_hash(yaml) if yaml.is_a? Array + + normalized = {} + yaml.each do |key, value| + normalized[key] = normalize_yaml(value) + end + normalized + end + + def self.array_to_hash(array) + hash = {} + array.each_with_index { |val, i| hash[i.to_s] = val } + hash + end + +end diff --git a/lib/yaml-validator.rb b/lib/yaml-validator.rb index 4092457..2c330aa 100644 --- a/lib/yaml-validator.rb +++ b/lib/yaml-validator.rb @@ -1,5 +1,6 @@ require 'yaml' require 'yaml-validator/version' +require_relative './helpers' class YamlValidator @@ -7,12 +8,19 @@ def initialize(root_path) @root_path = root_path end - def en_with_vars + def en + return @en unless @en.nil? + fullpath = File.join(@root_path, 'en.yml') return nil unless File.exists?(fullpath) - @en ||= YAML.load_file(fullpath)['en'] - @en_with_vars ||= get_all_variables(@en) + @en = YAML.load_file(fullpath)['en'] + @en = Helpers.normalize_yaml(@en) + end + + def en_with_vars + return nil if en.nil? + @en_with_vars ||= get_all_variables(en) end def validate() @@ -22,6 +30,7 @@ def validate() yml_files = File.join(@root_path, '*.yml') errors = [] Dir[yml_files].each do |filename| + next if File.basename(filename) == 'en.yml' errors.concat validate_yaml(filename) end errors @@ -29,20 +38,29 @@ def validate() def validate_yaml(filepath) filename = File.basename(filepath) + puts "Validating #{filepath}" begin yaml_object = YAML.load_file(filepath) rescue Psych::SyntaxError => e return [e.message.sub(/^\([^)]+\)/, filename)] end + yaml_object = yaml_object[yaml_object.keys[0]] + yaml_object = Helpers.normalize_yaml(yaml_object) errors = validate_yaml_object('', yaml_object) + errors.concat find_missing_translations(yaml_object) errors.map { |err| "#{filename}: #{err}" } end def validate_yaml_object(full_key, yaml_object) + return [] if yaml_object.nil? errors = [] + + puts "full_key = #{full_key}" + puts "yaml_object = #{yaml_object}" + yaml_object.each do |key, value| full_subkey = (full_key.empty?) ? key : "#{full_key}.#{key}" if value.is_a? String @@ -54,6 +72,41 @@ def validate_yaml_object(full_key, yaml_object) errors end + def find_missing_translations(yaml_object) + find_missing_translations_in_en_object('', en, yaml_object) + end + + def find_missing_translations_in_en_object(full_key, en_yaml_object, yaml_object) + return [] if en_yaml_object.nil? + errors = [] + + en_yaml_object.each do |key, value| + full_subkey = (full_key.empty?) ? key : "#{full_key}.#{key}" + if value.is_a? String or value.is_a? Symbol + if self.class.find_key_in_yaml_object(full_subkey, yaml_object).nil? + errors << "missing translation for #{full_subkey} ('#{value}')" + end + else + errors.concat find_missing_translations_in_en_object(full_subkey, value, yaml_object) + end + end + errors + end + + def self.find_key_in_yaml_object(full_key, yaml_object) + position = yaml_object + full_key.split('.').each do |key| + return nil unless position.is_a? Hash + position = position[key] + end + + if position.is_a? String or position.is_a? Symbol + position + else + nil + end + end + def validate_item(full_key, value) real_vars = get_key_en_vars(full_key) if real_vars.nil? @@ -86,10 +139,14 @@ def get_key_en_vars(full_key) end def get_all_variables(yaml_object) + return {} if yaml_object.nil? with_vars = {} + yaml_object.each do |key, value| if value.is_a? String with_vars[key] = identify_variables(value) + elsif value.is_a? Symbol + with_vars[key] = {} else with_vars[key] = get_all_variables(value) end @@ -100,5 +157,6 @@ def get_all_variables(yaml_object) def identify_variables(string) string.scan(/%{([^}]+)}/).map { |v| v[0] } end + end diff --git a/pkg/yaml-validator-0.0.1.gem b/pkg/yaml-validator-0.0.1.gem new file mode 100644 index 0000000..d7f9b4e Binary files /dev/null and b/pkg/yaml-validator-0.0.1.gem differ diff --git a/spec/fixtures/missing_translations/en.yml b/spec/fixtures/missing_translations/en.yml new file mode 100644 index 0000000..278b5ec --- /dev/null +++ b/spec/fixtures/missing_translations/en.yml @@ -0,0 +1,12 @@ +en: + key1: 'value1' + parent1: + subkey1: 'value1' + key2: 'value2' + parent2: + key3: 'value3' + parent3: + - one + - :two + - three + - diff --git a/spec/fixtures/missing_translations/he.yml b/spec/fixtures/missing_translations/he.yml new file mode 100644 index 0000000..f2fd5a0 --- /dev/null +++ b/spec/fixtures/missing_translations/he.yml @@ -0,0 +1,7 @@ +he: + key1: 'value1' + parent1: + subkey1: 'value1' + parent3: + - 1 + - heb two diff --git a/spec/helpers_spec.rb b/spec/helpers_spec.rb new file mode 100644 index 0000000..91ba97e --- /dev/null +++ b/spec/helpers_spec.rb @@ -0,0 +1,53 @@ +require_relative '../lib/helpers' + +describe Helpers do + describe "#normalize_yaml" do + it "converts symbols to strings (with ':' prefix)" do + yaml = { 'key3' => :bob } + + Helpers.normalize_yaml(yaml).should == { + 'key3' => ':bob' + } + end + + it "converts nil to ''" do + yaml = { 'key3' => nil } + Helpers.normalize_yaml(yaml).should == { + 'key3' => '' + } + end + + it "converts numbers to string" do + yaml = { 'key3' => 123 } + Helpers.normalize_yaml(yaml).should == { + 'key3' => '123' + } + end + + it "converts booleans to string" do + yaml = { 'key3' => true } + Helpers.normalize_yaml(yaml).should == { + 'key3' => 'true' + } + end + + it "converts arrays to hashes" do + yaml = { 'key1' => 'value1', + 'key2' => [ 'value2', nil, :bla ], + 'parent1' => { 'key3' => [ :bob ] } } + + Helpers.normalize_yaml(yaml).should == { + 'key1' => 'value1', + 'key2' => { '0' => 'value2', '1' => '', '2' => ':bla' }, + 'parent1' => { 'key3' => { '0' => ':bob' } } + } + end + end + + describe "#array_to_hash" do + it "returns hash with numeric indexes" do + Helpers.array_to_hash(['a','b']).should == { '0' => 'a', '1' => 'b' } + end + end + +end diff --git a/spec/yaml-validator_spec.rb b/spec/yaml-validator_spec.rb index 9876f82..4dba860 100644 --- a/spec/yaml-validator_spec.rb +++ b/spec/yaml-validator_spec.rb @@ -34,7 +34,9 @@ "he.yml: parent1.key1.subkey1 doesn't exist in en.yml", "he.yml: parent2.key2 doesn't exist in en.yml", "he.yml: key3 doesn't exist in en.yml", - "he.yml: parent3.key4 doesn't exist in en.yml" + "he.yml: parent3.key4 doesn't exist in en.yml", + "he.yml: missing translation for parent1.key1 ('Hello, %{name}, this is %{day_of_week}')", + "he.yml: missing translation for parent2.key2.subkey ('bla bla')" ] end end @@ -49,6 +51,19 @@ ] end end + + describe "missing translations" do + it "returns invalid yaml error" do + validator = YamlValidator.new('spec/fixtures/missing_translations') + errors = validator.validate() + errors.should == [ + "he.yml: missing translation for key2 ('value2')", + "he.yml: missing translation for parent2.key3 ('value3')", + "he.yml: missing translation for parent3.2 ('three')", + "he.yml: missing translation for parent3.3 ('')", + ] + end + end end @@ -143,5 +158,49 @@ end end end + + describe "#find_missing_translations" do + it "returns the missing translation keys" do + validator = YamlValidator.new('spec/fixtures/missing_translations') + + yaml_object = YAML.load_file('spec/fixtures/missing_translations/he.yml')['he'] + yaml_object = Helpers.normalize_yaml(yaml_object) + + errors = validator.find_missing_translations(yaml_object) + errors.should == [ + "missing translation for key2 ('value2')", + "missing translation for parent2.key3 ('value3')", + "missing translation for parent3.2 ('three')", + "missing translation for parent3.3 ('')" + ] + end + end + + describe "#find_key_in_yaml_object" do + it "handles subkeys" do + yaml_object = { 'parent1' => { 'key1' => 'value1' } } + YamlValidator.find_key_in_yaml_object('parent1.key1', yaml_object).should == 'value1' + end + + it "handles root keys" do + yaml_object = { "key2" => 'value2' } + YamlValidator.find_key_in_yaml_object('key2', yaml_object).should == 'value2' + end + + it "returns nil when a root key doesn't exist" do + yaml_object = { "key2" => 'value2' } + YamlValidator.find_key_in_yaml_object('key1', yaml_object).should be_nil + end + + it "returns nil when a subkey doesn't exist" do + yaml_object = { "parent1" => { "key2" => 'value2' } } + YamlValidator.find_key_in_yaml_object('parent1.key1', yaml_object).should be_nil + end + + it "returns nil when a subkey is an object" do + yaml_object = { "parent1" => { "parent2" => { "key1" => 'value1' } } } + YamlValidator.find_key_in_yaml_object('parent1.key2', yaml_object).should be_nil + end + end end