Skip to content

Commit

Permalink
fix import for unbalanced pluralizations
Browse files Browse the repository at this point in the history
fixes CNVS-4533

current i18n gem doesn't like pluralization sets
where you specify "other" but not "one".  This
pulls the i18n import into it's own class and
specifically specs out the copying over of the
"other" key into the "one" key in a pluralization
set unless a "one" key already exists.  Importing
any translation set will now solve the pluralization
view errors.

TEST PLAN:
1) confirm that the errors specified in CNVS-4533
are still occurring for the japanese language
set with the current translations.

2) run the i18n import process for the japanese
translations

3) confirm that the view errors are no longer
cropping up under the japanese translation set.

Change-Id: Idb856bd72f4d8e526a645f70a8fcc5556c4a4f98
Reviewed-on: https://gerrit.instructure.com/18526
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Ethan Vizitei <ethan@12spokes.com>
QA-Review: Clare Hetherington <clare@instructure.com>
  • Loading branch information
evizitei committed Mar 12, 2013
1 parent 99ed04a commit f003eec
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 99 deletions.
33 changes: 33 additions & 0 deletions lib/i18n/hash_extensions.rb
@@ -0,0 +1,33 @@
module I18n
module HashExtensions

def flatten_keys(result={}, prefix='')
each_pair do |k, v|
if v.is_a?(Hash)
v.flatten_keys(result, "#{prefix}#{k}.")
else
result["#{prefix}#{k}"] = v
end
end
result
end

def expand_keys(result = {})
each_pair do |k, v|
parts = k.split('.')
last = parts.pop
parts.inject(result){ |h, k2| h[k2] ||= {}}[last] = v
end
result
end

def to_ordered
keys.sort_by(&:to_s).inject ActiveSupport::OrderedHash.new do |h, k|
v = fetch(k)
h[k] = v.is_a?(Hash) ? v.to_ordered : v
h
end
end

end
end
115 changes: 115 additions & 0 deletions lib/i18n_import.rb
@@ -0,0 +1,115 @@
Hash.send(:include, I18n::HashExtensions) unless Hash.new.kind_of?(I18n::HashExtensions)

class I18nImport
attr_reader :source_translations, :new_translations, :language

def initialize(source_translations, new_translations)
@source_translations = init_source(source_translations)
@language = init_language(new_translations)
@new_translations = new_translations[language].flatten_keys
end

def compile_complete_translations(warning)
return nil unless warning.call(missing_keys.sort, "missing translations") if missing_keys.present?
return nil unless warning.call(unexpected_keys.sort, "unexpected translations") if unexpected_keys.present?

find_mismatches

if @placeholder_mismatches.size > 0
return nil unless warning.call(mismatch_diff(@placeholder_mismatches), "placeholder mismatches")
end

if @markdown_mismatches.size > 0
return nil unless warning.call(mismatch_diff(@markdown_mismatches), "markdown/wrapper mismatches")
end

complete_translations
end

def complete_translations
I18n.available_locales
base = (I18n.backend.send(:translations)[language.to_sym] || {})
translations = base.flatten_keys.merge(new_translations)
fix_plural_keys(translations)
translations.expand_keys
end

def fix_plural_keys(flat_hash)
other_keys = flat_hash.keys.grep(/\.other$/)
other_keys.each do |other_key|
one_key = other_key.gsub(/other$/, 'one')
if flat_hash[one_key].nil?
flat_hash[one_key] = flat_hash[other_key]
end
end
end

def missing_keys
source_translations.keys - new_translations.keys
end

def unexpected_keys
new_translations.keys - source_translations.keys
end

def find_mismatches
@placeholder_mismatches = {}
@markdown_mismatches = {}
new_translations.keys.each do |key|
p1 = placeholders(source_translations[key].to_s)
p2 = placeholders(new_translations[key].to_s)
@placeholder_mismatches[key] = [p1, p2] if p1 != p2

m1 = markdown_and_wrappers(source_translations[key].to_s)
m2 = markdown_and_wrappers(new_translations[key].to_s)
@markdown_mismatches[key] = [m1, m2] if m1 != m2
end
end

def markdown_and_wrappers(str)
# some stuff this doesn't check (though we don't use):
# blockquotes, e.g. "> some text"
# reference links, e.g. "[an example][id]"
# indented code
(
scan_and_report(str, /\\[\\`\*_\{\}\[\]\(\)#\+\-\.!]/) +
scan_and_report(str, /(\*+|_+|`+)[^\s].*?[^\s]?\1/).map{|m|"#{m}-wrap"} +
scan_and_report(str, /(!?\[)[^\]]+\]\(([^\)"']+).*?\)/).map{|m|"link:#{m.last}"} +
scan_and_report(str, /^((\s*\*\s*){3,}|(\s*-\s*){3,}|(\s*_\s*){3,})$/).map{"hr"} +
scan_and_report(str, /^[^=\-\n]+\n^(=+|-+)$/).map{|m|m.first[0]=='=' ? 'h1' : 'h2'} +
scan_and_report(str, /^(\#{1,6})\s+[^#]*#*$/).map{|m|"h#{m.first.size}"} +
scan_and_report(str, /^ {0,3}(\d+\.|\*|\+|\-)\s/).map{|m|m.first =~ /\d/ ? "1." : "*"}
).sort
end

def placeholders(str)
str.scan(/%h?\{[^\}]+\}/).sort
rescue ArgumentError => e
puts "Unable to scan string: #{str.inspect}"
raise e
end

def scan_and_report(str, re)
str.scan(re)
rescue ArgumentError => e
puts "Unable to scan string: #{str.inspect}"
raise e
end

private
def init_source(translations)
raise "Source does not have any English strings" unless translations.keys.include?('en')
translations['en'].flatten_keys
end

def init_language(translations)
raise "Translation file contains multiple languages" if translations.size > 1
language = translations.keys.first
raise "Translation file appears to have only English strings" if language == 'en'
language
end

def mismatch_diff(mismatches)
mismatches.map{|k,(p1,p2)| "#{k}: expected #{p1.inspect}, got #{p2.inspect}"}.sort
end
end
110 changes: 11 additions & 99 deletions lib/tasks/i18n.rake
@@ -1,34 +1,6 @@
namespace :i18n do require 'i18n/hash_extensions'
module HashExtensions
def flatten_keys(result={}, prefix='')
each_pair do |k, v|
if v.is_a?(Hash)
v.flatten_keys(result, "#{prefix}#{k}.")
else
result["#{prefix}#{k}"] = v
end
end
result
end

def expand_keys(result = {})
each_pair do |k, v|
parts = k.split('.')
last = parts.pop
parts.inject(result){ |h, k2| h[k2] ||= {}}[last] = v
end
result
end

def to_ordered
keys.sort_by(&:to_s).inject ActiveSupport::OrderedHash.new do |h, k|
v = fetch(k)
h[k] = v.is_a?(Hash) ? v.to_ordered : v
h
end
end
end


namespace :i18n do
def infer_scope(filename) def infer_scope(filename)
case filename case filename
when /app\/views\/.*\.handlebars\z/ when /app\/views\/.*\.handlebars\z/
Expand Down Expand Up @@ -181,7 +153,7 @@ namespace :i18n do
I18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] + I18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] +
Dir[Rails.root.join('vendor', 'plugins', '*', 'config', 'locales', '**', '*.{rb,yml}')] Dir[Rails.root.join('vendor', 'plugins', '*', 'config', 'locales', '**', '*.{rb,yml}')]


Hash.send :include, HashExtensions Hash.send :include, I18n::HashExtensions


file_translations = {} file_translations = {}


Expand Down Expand Up @@ -260,7 +232,7 @@ define(['i18nObj', 'jquery'], function(I18n, $) {


desc "Exports new/changed English strings to be translated" desc "Exports new/changed English strings to be translated"
task :export => :environment do task :export => :environment do
Hash.send :include, HashExtensions Hash.send :include, I18n::HashExtensions


begin begin
base_filename = "config/locales/generated/en.yml" base_filename = "config/locales/generated/en.yml"
Expand Down Expand Up @@ -364,55 +336,21 @@ define(['i18nObj', 'jquery'], function(I18n, $) {
desc "Validates and imports new translations" desc "Validates and imports new translations"
task :import => :environment do task :import => :environment do
require 'ya2yaml' require 'ya2yaml'
Hash.send :include, HashExtensions Hash.send(:include, I18n::HashExtensions) unless Hash.new.kind_of?(I18n::HashExtensions)

def placeholders(str)
str.scan(/%h?\{[^\}]+\}/).sort
rescue ArgumentError => e
puts "Unable to scan string: #{str.inspect}"
raise e
end

def scan_and_report(str, re)
str.scan(re)
rescue ArgumentError => e
puts "Unable to scan string: #{str.inspect}"
raise e
end

def markdown_and_wrappers(str)
# some stuff this doesn't check (though we don't use):
# blockquotes, e.g. "> some text"
# reference links, e.g. "[an example][id]"
# indented code
(
scan_and_report(str, /\\[\\`\*_\{\}\[\]\(\)#\+\-\.!]/) +
scan_and_report(str, /(\*+|_+|`+)[^\s].*?[^\s]?\1/).map{|m|"#{m}-wrap"} +
scan_and_report(str, /(!?\[)[^\]]+\]\(([^\)"']+).*?\)/).map{|m|"link:#{m.last}"} +
scan_and_report(str, /^((\s*\*\s*){3,}|(\s*-\s*){3,}|(\s*_\s*){3,})$/).map{"hr"} +
scan_and_report(str, /^[^=\-\n]+\n^(=+|-+)$/).map{|m|m.first[0]=='=' ? 'h1' : 'h2'} +
scan_and_report(str, /^(\#{1,6})\s+[^#]*#*$/).map{|m|"h#{m.first.size}"} +
scan_and_report(str, /^ {0,3}(\d+\.|\*|\+|\-)\s/).map{|m|m.first =~ /\d/ ? "1." : "*"}
).sort
end


begin begin
puts "Enter path to original en.yml file:" puts "Enter path to original en.yml file:"
arg = $stdin.gets.strip arg = $stdin.gets.strip
source_translations = File.exist?(arg) && YAML.safe_load(File.read(arg)) rescue nil source_translations = File.exist?(arg) && YAML.safe_load(File.read(arg)) rescue nil
end until source_translations end until source_translations
raise "Source does not have any English strings" unless source_translations.keys.include?('en')
source_translations = source_translations['en'].flatten_keys


begin begin
puts "Enter path to translated file:" puts "Enter path to translated file:"
arg = $stdin.gets.strip arg = $stdin.gets.strip
new_translations = File.exist?(arg) && YAML.safe_load(File.read(arg)) rescue nil new_translations = File.exist?(arg) && YAML.safe_load(File.read(arg)) rescue nil
end until new_translations end until new_translations
raise "Translation file contains multiple languages" if new_translations.size > 1
language = new_translations.keys.first import = I18nImport.new(source_translations, new_translations)
raise "Translation file appears to have only English strings" if language == 'en'
new_translations = new_translations[language].flatten_keys


item_warning = lambda { |error_items, description| item_warning = lambda { |error_items, description|
begin begin
Expand All @@ -429,37 +367,11 @@ define(['i18nObj', 'jquery'], function(I18n, $) {
true true
} }


missing_keys = source_translations.keys - new_translations.keys complete_translations = import.compile_complete_translations(item_warning)
next unless item_warning.call(missing_keys.sort, "missing translations") if missing_keys.present? next if complete_translations.nil?

unexpected_keys = new_translations.keys - source_translations.keys
next unless item_warning.call(unexpected_keys.sort, "unexpected translations") if unexpected_keys.present?

placeholder_mismatches = {}
markdown_mismatches = {}
new_translations.keys.each do |key|
p1 = placeholders(source_translations[key].to_s)
p2 = placeholders(new_translations[key].to_s)
placeholder_mismatches[key] = [p1, p2] if p1 != p2

m1 = markdown_and_wrappers(source_translations[key].to_s)
m2 = markdown_and_wrappers(new_translations[key].to_s)
markdown_mismatches[key] = [m1, m2] if m1 != m2
end

if placeholder_mismatches.size > 0
next unless item_warning.call(placeholder_mismatches.map{|k,(p1,p2)| "#{k}: expected #{p1.inspect}, got #{p2.inspect}"}.sort, "placeholder mismatches")
end

if markdown_mismatches.size > 0
next unless item_warning.call(markdown_mismatches.map{|k,(p1,p2)| "#{k}: expected #{p1.inspect}, got #{p2.inspect}"}.sort, "markdown/wrapper mismatches")
end

I18n.available_locales


new_translations = (I18n.backend.send(:translations)[language.to_sym] || {}).flatten_keys.merge(new_translations) File.open("config/locales/#{import.language}.yml", "w") { |f|
File.open("config/locales/#{language}.yml", "w") { |f| f.write({import.language => complete_translations}.ya2yaml(:syck_compatible => true))
f.write({language => new_translations.expand_keys}.ya2yaml(:syck_compatible => true))
} }
end end
end end
Expand Down
23 changes: 23 additions & 0 deletions spec/lib/i18n_import_spec.rb
@@ -0,0 +1,23 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
require File.expand_path('../../lib/i18n_import', File.dirname(__FILE__))

describe I18nImport do
describe '#fix_plural_keys' do
it 'copies over the other key if there is no one key' do
import = I18nImport.new({'en'=>{}}, {'ja'=>{}})
hash = { 'some.key.other' => 'value' }
import.fix_plural_keys(hash)
hash.should == { 'some.key.other' => 'value', 'some.key.one' => 'value' }
end

it 'leaves the one key alone if it already exists' do
import = I18nImport.new({'en'=>{}}, {'ja'=>{}})
hash = {
'some.key.other' => 'value',
'some.key.one' => 'other value'
}
import.fix_plural_keys(hash)
hash.should == { 'some.key.other' => 'value', 'some.key.one' => 'other value' }
end
end
end

0 comments on commit f003eec

Please sign in to comment.