From 010c6cc9a39d95ed3a357fd0b9e851a7ad3b61ff Mon Sep 17 00:00:00 2001 From: David Elentok <3david@gmail.com> Date: Thu, 7 Feb 2013 13:39:32 +0200 Subject: [PATCH] added missing translations test --- lib/helpers.rb | 24 ++++++++ lib/yaml-validator.rb | 64 +++++++++++++++++++++- pkg/yaml-validator-0.0.1.gem | Bin 0 -> 6656 bytes spec/fixtures/missing_translations/en.yml | 12 ++++ spec/fixtures/missing_translations/he.yml | 7 +++ spec/helpers_spec.rb | 53 ++++++++++++++++++ spec/yaml-validator_spec.rb | 61 ++++++++++++++++++++- 7 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 lib/helpers.rb create mode 100644 pkg/yaml-validator-0.0.1.gem create mode 100644 spec/fixtures/missing_translations/en.yml create mode 100644 spec/fixtures/missing_translations/he.yml create mode 100644 spec/helpers_spec.rb 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 0000000000000000000000000000000000000000..d7f9b4ee9be7afddd83caf72164ac66f36cbb7ba GIT binary patch literal 6656 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 :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