lib/tasks/locale.rake
namespace :locale do
desc "Extract strings from en.yml and store them in a ruby file for gettext:find"
task :store_dictionary_strings => :environment do
output_strings = [
"# This is automatically generated file (rake locale:store_dictionary_strings).",
"# The file contains strings extracted from en.yml for gettext to find."
]
no_plurals = %w[NFS OS] # strings which we don't want to create automatic plurals for
dict = YAML.safe_load(File.open(Rails.root.join("locale/en.yml")))["en"]["dictionary"]
dict.each_key do |tree|
next unless %w[column model table].include?(tree) # subtrees of interest
dict[tree].each_key do |item|
if dict[tree][item].kind_of?(String) # leaf node
output_strings.push("# TRANSLATORS: en.yml key: dictionary.#{tree}.#{item}")
value = dict[tree][item]
output_strings.push('_("%{value}")' % {:value => value})
if %w[model table].include?(tree) && # create automatic plurals for model and table subtrees
!no_plurals.include?(value)
m = /(.+)(\s+\(.+\))/.match(value) # strings like: "Infrastructure Provider (Openstack)"
value_plural = m ? "#{m[1].pluralize}#{m[2]}" : value.pluralize
if value != value_plural
output_strings.push("# TRANSLATORS: en.yml key: dictionary.#{tree}.#{item} (plural form)")
output_strings.push('_("%{plural}")' % {:plural => value_plural})
end
end
elsif dict[tree][item].kind_of?(Hash) # subtree
dict[tree][item].each_key do |subitem|
output_strings.push("# TRANSLATORS: en.yml key: dictionary.#{tree}.#{item}.#{subitem}")
output_strings.push('_("%{item}")' % {:item => dict[tree][item][subitem]})
end
end
end
end
File.open(Rails.root.join("config/dictionary_strings.rb"), "w+") do |f|
f.puts(output_strings)
end
end
desc "Extract strings from various yaml files and store them in a ruby file for gettext:find"
task :extract_yaml_strings, [:root] => :environment do |_t, args|
def update_output(string, file, output, root)
file.gsub!(root + '/', "")
return if string.blank?
if output.key?(string)
output[string].append(file)
else
output[string] = [file]
end
end
def parse_object(object, keys, file, output, root)
if object.kind_of?(Hash)
object.each_key do |key|
if keys.include?(key) || keys.include?(key.to_s)
if object[key].kind_of?(Array)
object[key].each { |i| update_output(i, file, output, root) }
else
update_output(object[key], file, output, root)
end
end
parse_object(object[key], keys, file, output, root)
end
elsif object.kind_of?(Array)
object.each do |item|
parse_object(item, keys, file, output, root)
end
end
end
def key_from_yaml(yaml_key_value)
case yaml_key_value
when Array
yaml_key_value.first
else
yaml_key_value
end
end
root_path = args[:root] || Rails.root
config_file = root_path.join('config/locale_task_config.yaml')
next unless config_file.exist?
yamls = YAML.load_file(config_file)['yaml_strings_to_extract']
output = {}
yamls.each_key do |yaml_glob|
yaml_glob_full = root_path.join(yaml_glob)
Dir.glob(yaml_glob_full).sort.each do |file|
yml = YAML.load_file(file)
parse_object(yml, yamls[yaml_glob], file, output, root_path.to_s)
end
end
next if output.empty? # no yaml strings were found
File.open(root_path.join("config/yaml_strings.rb"), "w+") do |f|
f.puts "# This is automatically generated file (rake locale:extract_yaml_strings)."
f.puts "# The file contains strings extracted from various yaml files for gettext to find."
output.each_key do |key|
output[key].sort.uniq.each do |file|
f.puts "# TRANSLATORS: file: #{file}"
end
f.puts '_("%{key}")' % {:key => key_from_yaml(key)}
end
end
end
desc "Extract human locale names from translation catalogs and store them in a yaml file"
task :extract_locale_names => :environment do
require 'yaml/store'
Vmdb::FastGettextHelper.register_locales
locale_hash = {}
FastGettext.available_locales.each do |locale|
FastGettext.locale = locale
# TRANSLATORS: Provide locale name in native language (e.g. English, Deutsch or Português)
human_locale = Vmdb::FastGettextHelper.locale_name
human_locale = locale if human_locale == "locale_name"
locale_hash[locale] = human_locale
end
store = YAML::Store.new("config/human_locale_names.yaml")
store.transaction do
store['human_locale_names'] = locale_hash
end
end
desc "Extract model attribute names and virtual column names"
task "store_model_attributes" => :environment do
require 'gettext_i18n_rails/model_attributes_finder'
require_relative 'model_attribute_override'
attributes_file = 'locale/model_attributes.rb'
File.unlink(attributes_file) if File.exist?(attributes_file)
Rake::Task['gettext:store_model_attributes'].invoke
FileUtils.mv(attributes_file, 'config/model_attributes.rb')
end
desc "Run store_model_attributes task in i18n environment"
task "run_store_model_attributes" do
system({"RAILS_ENV" => "i18n"}, "bundle exec rake locale:store_model_attributes")
end
task "delete_pot_file", :root do |_, args|
pot_file = Dir.glob(Pathname(args[:root]).join("locale/*.pot")).first
FileUtils.rm_f(pot_file) if pot_file
end
desc "Update ManageIQ gettext catalogs"
task "update" do
Rake::Task['locale:store_dictionary_strings'].invoke
Rake::Task['locale:run_store_model_attributes'].invoke
Rake::Task['locale:extract_yaml_strings'].invoke(Rails.root)
Rake::Task['locale:model_display_names'].invoke
Rake::Task['locale:delete_pot_file'].invoke(Rails.root)
Rake::Task['gettext:find'].invoke
Dir["config/dictionary_strings.rb", "config/model_attributes.rb", "config/model_display_names.rb", "config/yaml_strings.rb", "locale/**/*.edit.po", "locale/**/*.po.time_stamp"].each do |file|
File.unlink(file)
end
end
desc "Update all ManageIQ gettext catalogs and merge them into one"
task "update_all" do
Rake::Task['locale:update'].invoke
pot_files = []
Vmdb::Plugins.each do |plugin|
# HACK: Rake tasks aren't re-invoked by default. We need to reenable non-time based(FileTask) rake tasks
# so they can be run again in the context of each plugin.
#
# TODO: Rake tasks such as delete_pot_file, plugin:find, and report_changes take arguments and conflict with
# the assumption that rake makes: already invoked tasks should not be invoked again as the result should be the same.
# We should make these methods with arguments and not use rake tasks in this way.
Rake.application.tasks.each { |t| t.reenable if t.already_invoked && !t.kind_of?(Rake::FileTask) }
Rake::Task['locale:delete_pot_file'].invoke(plugin.root) # Delete plugin's pot file if it exists to avoid weird file timestamp issues
Rake::Task['locale:plugin:find'].invoke(plugin.to_s.sub('::Engine', '')) # will warn and exit 1 if any engine fails
pot_file = Dir.glob("#{plugin.root.join('locale')}/*.pot")[0]
pot_files << pot_file if pot_file.present?
end
checkout_branch = ENV['BRANCH'].presence || 'master'
extra_pots = [
"https://raw.githubusercontent.com/ManageIQ/ui-components/#{checkout_branch}/locale/ui-components.pot",
"https://raw.githubusercontent.com/ManageIQ/react-ui-components/#{checkout_branch}/locale/react-ui-components.pot"
]
tmp_dir = Rails.root.join('locale/tmp')
tmp_dir.rmtree if tmp_dir.exist?
tmp_dir.mkpath
extra_pots.each do |url|
pot_file = tmp_dir.join("#{url.split('/')[-1]}").to_s
ManageIQ::Environment.system!('curl', '-f', '-o', pot_file, url)
pot_files << pot_file
end
locale_dir = Rails.root.join("locale")
locale_tmp_dir = locale_dir.join("tmp")
system('rmsgcat', '--sort-by-msgid', '--no-all-comments', '-o', locale_tmp_dir.join('manageiq-all.pot').to_s, locale_dir.join('manageiq.pot').to_s, *pot_files)
system('mv', '-v', locale_tmp_dir.join('manageiq-all.pot').to_s, locale_dir.join('manageiq.pot').to_s)
system('rmsgmerge', '--sort-by-msgid', '--no-location', '--no-fuzzy-matching', '-o', locale_tmp_dir.join('manageiq-all.po').to_s, locale_dir.join('en/manageiq.po').to_s, locale_dir.join('manageiq.pot').to_s)
system('mv', '-v', locale_tmp_dir.join('manageiq-all.po').to_s, locale_dir.join('en/manageiq.po').to_s)
tmp_dir.rmtree
end
desc "Show changes in gettext strings since last catalog update"
task "report_changes", [:verbose] do |_t, args|
require 'poparser'
old_pot = PoParser.parse(File.read(Rails.root.join("locale/manageiq.pot"))).to_h.collect { |item| item[:msgid] }.sort
Rake::Task['locale:update_all'].invoke
new_pot = PoParser.parse(File.read(Rails.root.join("locale/manageiq.pot"))).to_h.collect { |item| item[:msgid] }.sort
diff = new_pot - old_pot
puts "--------------------------------------------------"
puts "Current string / word count: %{str} / %{word}" % {:str => old_pot.length, :word => old_pot.join(' ').split.size}
puts "Updated string / word count: %{str} / %{word}" % {:str => new_pot.length, :word => new_pot.join(' ').split.size}
puts
puts "New string / word count: %{str} / %{word}" % {:str => diff.length, :word => diff.join(' ').split.size}
puts "--------------------------------------------------"
puts "New strings: ", diff if args.verbose == 'verbose'
end
desc "Extract plugin strings - execute as: rake locale:plugin:find[plugin_name]"
task "plugin:find", :engine do |_, args|
unless args[:engine]
warn "You need to specify a plugin name: rake locale:plugin:find[plugin_name]"
exit 1
end
@domain = args[:engine].gsub('::', '_')
begin
@engine = "#{args[:engine].camelize}::Engine".constantize
rescue NameError
warn "The specified plugin #{args[:engine]} does not exist."
exit 1
end
@engine_root = @engine.root
# extract plugin's yaml strings
Rake::Task['locale:extract_yaml_strings'].invoke(@engine_root)
namespace :gettext do
def locale_path
@engine_root.join('locale').to_s
end
def files_to_translate
Dir.glob("#{@engine_root}/{app,db,lib,config,locale}/**/*.{rb,erb,haml,slim,rhtml,js,jsx}").sort
end
def text_domain
@domain
end
end
system('mkdir', '-p', "#{@engine_root}/locale/en") # create initial locale/en directories if they don't exist
FastGettext.add_text_domain(@domain,
:path => @engine_root.join('locale').to_s,
:type => :po,
:ignore_fuzzy => true,
:report_warning => false)
Rake::Task['gettext:find'].invoke
Dir["#{@engine.root}/locale/**/*.edit.po", "#{@engine.root}/locale/**/*.po.time_stamp", "#{@engine.root}/config/yaml_strings.rb"].each do |file|
File.unlink(file)
end
end
desc "Convert PO files from all plugins to JS files"
task "po_to_json" => :environment do
begin
require_relative 'gettext_task_override'
require_relative 'po_to_json_override'
require Rails.root.join('lib/manageiq/environment')
require Rails.root.join("lib/vmdb/gettext/domains")
po_files = {}
Vmdb::Gettext::Domains.po_paths.each do |path|
files = Pathname.glob(File.join(path, "**", "*.po")).sort
files.each do |file|
locale = file.dirname.basename.to_s
po_files[locale] ||= []
po_files[locale].push(file)
end
end
combined_dir = Rails.root.join("locale/combined").to_s
Dir.mkdir(combined_dir, 0o700)
po_files.each do |locale, files|
files.each do |file|
system("msgfmt --check #{file}", :exception => true)
end
dir = File.join(combined_dir, locale)
po = File.join(dir, 'manageiq.po')
Dir.mkdir(dir, 0o700)
puts "Generating po from\n#{files.sort.map { |f| "- #{f}" }.join("\n")}"
system "rmsgcat --sort-by-msgid -o #{po} #{files.join(' ')}"
puts
end
# create webpack file for including bootstrap-datepicker language packs
File.open(ManageIQ::UI::Classic::Engine.root.join('app/javascript/packs/bootstrap-datepicker-languages.js'), "w+") do |f|
f.puts("// This file is automatically generated by rake task 'locale:po_to_json'")
po_files.keys.sort.each do |lang|
next if lang == 'en'
f.puts("require('bootstrap-datepicker/dist/locales/bootstrap-datepicker." + lang.sub('_', '-') + ".min.js');")
end
end
# This depends on PoToJson overrides as defined in lib/tasks/po_to_json_override.rb
Rake::Task[defined?(ENGINE_ROOT) ? "app:gettext:po_to_json" : "gettext:po_to_json"].invoke
ensure
system "rm -rf #{combined_dir}"
end
end
desc "Create display names for models"
task "model_display_names" => :environment do
f = File.open(Rails.root.join("config/model_display_names.rb"), "w+")
Rails.application.eager_load!
ApplicationRecord.descendants.select { |ar| ar.respond_to?(:display_name) }.sort_by(&:display_name).collect do |model|
next if model.model_name.singular.titleize != model.display_name || model.display_name.start_with?('ManageIQ')
f.puts "n_('#{model.display_name}', '#{model.display_name 2}', n)"
end
f.close
end
end