lib/missing_t.rb
require "yaml"
#TODO: Should I feel about these 'global' helper functions?
def hashify(segments, value)
return {} if segments.empty?
s, *rest = segments
if rest.empty?
{ s => value }
else
{ s => hashify(rest, value) }
end
end
def print_hash(h, level)
h.each_pair do |k,v|
if v.respond_to?(:each_pair)
puts %(#{" " * (level*2)}#{k}:)
print_hash(v, level+1)
else
puts %(#{" " * (level*2)}#{k}: #{v})
end
end
end
class Hash
# idea snatched from deep_merge in Rails source code
def deep_safe_merge(other_hash)
self.merge(other_hash) do |key, oldval, newval|
oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
newval = newval.to_hash if newval.respond_to?(:to_hash)
if oldval.is_a? Hash
if newval.is_a? Hash
oldval.deep_safe_merge(newval)
else
oldval
end
else
newval
end
end
end
def deep_safe_merge!(other_hash)
replace(deep_safe_merge(other_hash))
end
end
class MissingT
class FileReader
def read(file)
IO.readlines(file).each do |line|
yield line
end
end
end
VERSION = "0.4.1"
def initialize(options={})
@reader = options.fetch(:reader, FileReader.new)
@languages = options[:languages]
@path = options[:path]
end
def run
missing = {}
collect_missing.each do |file, message_strings|
message_strings.each do |message_string, value|
missing.deep_safe_merge! hashify(message_string.split('.'), value)
end
end
missing.each do |language, missing_for_language|
puts
puts "#{language}:"
print_hash(missing_for_language, 1)
end
end
def collect_missing
ts = translation_keys
#TODO: If no translation keys were found and the languages were not given explicitly
# issue a warning and bail out
languages = @languages ? @languages : ts.keys
get_missing_translations(translation_keys, translation_queries, languages)
end
def get_missing_translations(keys, queries, languages)
languages.each_with_object({}) do |lang, missing|
get_missing_translations_for_language(keys, queries, lang).each do |file, queries_for_language|
missing[file] ||= {}
missing[file].merge!(queries_for_language)
end
end
end
def translation_keys
locales_pathes = ["config/locales/**/*.yml", "vendor/plugins/**/config/locales/**/*yml", "vendor/plugins/**/locale/**/*yml"]
locales_pathes.each_with_object({}) do |path, translations|
Dir.glob(path) do |file|
t = open(file) { |f| YAML.load(f.read) }
translations.deep_safe_merge!(t)
end
end
end
def translation_queries
files_with_i18n_queries.each_with_object({}) do |file, queries|
queries_in_file = extract_i18n_queries(file)
if queries_in_file.any?
queries[file] = queries_in_file
end
end
#TODO: remove duplicate queries across files
end
def has_translation?(keys, lang, query)
i18n_label(lang, query).split('.').each do |segment|
return false unless segment =~ /#\{.*\}/ or (keys.respond_to?(:key?) and keys.key?(segment))
keys = keys[segment]
end
true
end
def files_with_i18n_queries
if @path
path = File.expand_path(@path)
if File.file?(path)
[@path]
else
path.chomp!('/')
[
Dir.glob("#{path}/**/*.erb"),
Dir.glob("#{path}/**/*.haml"),
Dir.glob("#{path}/**/*.rb")
]
end
else
[
Dir.glob("app/**/*.erb"),
Dir.glob("app/**/*.haml"),
Dir.glob("app/**/models/**/*.rb"),
Dir.glob("app/**/controllers/**/*.rb"),
Dir.glob("app/**/helpers/**/*.rb")
]
end.flatten
end
def extract_i18n_queries(file)
({}).tap do |queries|
@reader.read(File.expand_path(file)) do |line|
qs = scan_line(line)
queries.merge!(qs)
end
end
end
private
def get_missing_translations_for_language(keys, queries, l)
queries.each_with_object({}) do |(file, queries_in_file), missing_translations|
queries_with_no_translation = queries_in_file.reject { |q, _| has_translation?(keys, l, q) }
if queries_with_no_translation.any?
missing_translations[file] = add_langauge_prefix(queries_with_no_translation, l)
end
end
end
def add_langauge_prefix(qs, l)
qs.each_with_object({}) do |(q, v), with_prefix|
with_prefix["#{l}.#{q}"] = v
end
end
def i18n_label(lang, query)
"#{lang}.#{query}"
end
def scan_line(line)
with_parens = /[^\w]+(?:I18n\.translate|I18n\.t|translate|t)\s*\((['"](.*?)['"].*?)\)/
no_parens = /[^\w]+(?:I18n\.translate|I18n\.t|translate|t)\s+(['"](.*?)['"].*?)/
[with_parens, no_parens].each_with_object({}) do |pattern, extracted_queries|
line.scan(pattern).each do |m|
if m.any?
message_string = m[1]
_, *options = m[0].split(',')
extracted_queries[message_string] = extract_default_value(options)
end
end
end
end
def extract_default_value(message_string_options)
[/:default\s*=>\s*['"](.*)['"]/, /default:\s*['"](.*)['"]/].each do |default_extractor|
message_string_options.each do |option|
if default_key_match=default_extractor.match(option)
return default_key_match[1]
end
end
end
''
end
end