rails_translation_manager 1.0.0 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2407e2bb0a7f4cf4628d9a9f35bcfb668b085c23bb92418f708c09f571c1b92
4
- data.tar.gz: e0584a3f42216bdc0b9589ab6bb26e7246d6856e0b33f7599ac1e6eed0070af8
3
+ metadata.gz: fbbde3519b97f6c9881189908af474ef24b5ad5dae1a3c6e3a590df8ca4b9885
4
+ data.tar.gz: f6ad0e6bf5584f6fa9db3b39d3a7d52c1b04786368c9297090a480690e2cf1e7
5
5
  SHA512:
6
- metadata.gz: '0249a48778dade714f10f47d4185136da8a8a9d11313273d659817dba1968c01ed5756c42b87e0b1963d3f9a41c1316392317f7214137dca5af8e4962f311f4a'
7
- data.tar.gz: 63553a493e8c36c8daa8fe0bdf388384c2bf80fca3e41fd038f4ed8f92078c4accfe18c9a9561c2dc9faa82a35e9cd4642c7b1894a21a1fbd2fb0302c18092a1
6
+ metadata.gz: befae5c31f13ce323a367033180d58d7969220cde8eded8dd213a0de5405fd82246f0b775c3761d45e2412b25699630538896d6b2ec9692a4f030ea751f9282c
7
+ data.tar.gz: 25979414b9918d47b58e3f87caa2ba967dcca0bc233c37854f270b8e93f0f95940b3c0f54b7f658ea4ed144b463b740569bebe4c0dfd98907a8e82d02c10fd6f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## 1.1.3
2
+
3
+ Handle importing files that contain rows with a blank "key". https://github.com/alphagov/rails_translation_manager/pull/28
4
+
5
+ ## 1.1.2
6
+
7
+ Handle importing files that contain Byte Order Marks. https://github.com/alphagov/rails_translation_manager/pull/27
8
+
9
+ ## 1.1.1
10
+
11
+ Fix Rails Translation Manager / Rails naming clash for class. https://github.com/alphagov/rails_translation_manager/pull/26
12
+
13
+ ## 1.1.0
14
+
15
+ Allow multiple files per language to be imported. https://github.com/alphagov/rails_translation_manager/pull/20
16
+
1
17
  ## 1.0.0
2
18
 
3
19
  Adds logic to verify locale files are in sync with each other and have the
data/Jenkinsfile CHANGED
@@ -3,5 +3,7 @@
3
3
  library("govuk")
4
4
 
5
5
  node {
6
- govuk.buildProject()
6
+ govuk.buildProject(
7
+ cleanWorkspace: true,
8
+ )
7
9
  }
@@ -1,5 +1,4 @@
1
- class TranslationHelper
2
-
1
+ class RailsTranslationManager::I18nTasksOptionParser
3
2
  def initialize(task_options, locale)
4
3
  @task_options = task_options
5
4
  @locale = locale
@@ -5,36 +5,49 @@ require_relative "yaml_writer"
5
5
  class RailsTranslationManager::Importer
6
6
  include YAMLWriter
7
7
 
8
- def initialize(locale, csv_path, import_directory)
9
- @csv_path = csv_path
8
+ attr_reader :locale, :csv_path, :import_directory, :multiple_files_per_language
9
+
10
+ def initialize(locale:, csv_path:, import_directory:, multiple_files_per_language:)
10
11
  @locale = locale
12
+ @csv_path = csv_path
11
13
  @import_directory = import_directory
14
+ @multiple_files_per_language = multiple_files_per_language
12
15
  end
13
16
 
14
17
  def import
15
- csv = CSV.read(@csv_path, headers: true, header_converters: :downcase)
16
- data = {}
17
- csv.each do |row|
18
+ csv = reject_nil_keys(
19
+ CSV.read(csv_path, encoding: "bom|utf-8", headers: true, header_converters: :downcase)
20
+ )
21
+
22
+ multiple_files_per_language ? import_csv_into_multiple_files(csv) : import_csv(csv)
23
+ end
24
+
25
+ private
26
+
27
+ def import_csv(csv, import_yml_path = File.join(import_directory, "#{locale}.yml"))
28
+ data = csv.each_with_object({}) do |row, hash|
18
29
  key = row["key"]
19
30
  key_parts = key.split(".")
20
31
  if key_parts.length > 1
21
- leaf_node = (data[key_parts.first] ||= {})
32
+ leaf_node = (hash[key_parts.first] ||= {})
22
33
  key_parts[1..-2].each do |part|
23
34
  leaf_node = (leaf_node[part] ||= {})
24
35
  end
25
36
  leaf_node[key_parts.last] = parse_translation(row["translation"])
26
37
  else
27
- data[key_parts.first] = parse_translation(row["translation"])
38
+ hash[key_parts.first] = parse_translation(row["translation"])
28
39
  end
29
40
  end
30
41
 
31
- write_yaml(import_yml_path, {@locale.to_s => data})
42
+ write_yaml(import_yml_path, { locale.to_s => data })
32
43
  end
33
44
 
34
- private
35
-
36
- def import_yml_path
37
- File.join(@import_directory, "#{@locale}.yml")
45
+ def reject_nil_keys(csv)
46
+ csv.reject do |row|
47
+ nil_key = row["key"].nil?
48
+ puts "Invalid row: #{row.inspect} for csv_path: #{csv_path}\n" if nil_key == true
49
+ nil_key
50
+ end
38
51
  end
39
52
 
40
53
  def parse_translation(translation)
@@ -55,4 +68,19 @@ class RailsTranslationManager::Importer
55
68
  translation
56
69
  end
57
70
  end
71
+
72
+ def import_csv_into_multiple_files(csv)
73
+ group_csv_by_file(csv).each do |group|
74
+ language_dir = File.join(import_directory, locale)
75
+
76
+ Dir.mkdir(language_dir) unless Dir.exists?(language_dir)
77
+
78
+ import_yml_path = File.join(import_directory, locale, "#{group[0]}.yml")
79
+ import_csv(group[1], import_yml_path)
80
+ end
81
+ end
82
+
83
+ def group_csv_by_file(csv)
84
+ csv.group_by { |row| row["key"].split(".").first }
85
+ end
58
86
  end
@@ -1,3 +1,3 @@
1
1
  module RailsTranslationManager
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.3"
3
3
  end
@@ -13,12 +13,10 @@ require "rails_translation_manager/locale_checker/incompatible_plurals"
13
13
  require "rails_translation_manager/locale_checker/all_locales"
14
14
  require "rails_translation_manager/locale_checker"
15
15
  require "rails_translation_manager/cleaner"
16
+ require "rails_translation_manager/exporter"
17
+ require "rails_translation_manager/importer"
16
18
 
17
19
  module RailsTranslationManager
18
- autoload :Exporter, "rails_translation_manager/exporter"
19
- autoload :Importer, "rails_translation_manager/importer"
20
- autoload :Stealer, "rails_translation_manager/stealer"
21
-
22
20
  rails_i18n_path = Gem::Specification.find_by_name("rails-i18n").gem_dir
23
21
  rails_translation_manager = Gem::Specification.find_by_name("rails_translation_manager").gem_dir
24
22
 
@@ -1,17 +1,9 @@
1
- require "rails_translation_manager"
2
1
  require "i18n/tasks/cli"
3
- require_relative "../tasks/translation_helper"
2
+ require_relative "../rails_translation_manager"
3
+ require_relative "../rails_translation_manager/i18n_tasks_option_parser"
4
4
 
5
5
  namespace :translation do
6
6
 
7
- desc "Regenerate all locales from the EN locale - run this after adding keys"
8
- task(:regenerate, [:directory] => [:environment]) do |t, args|
9
- directory = args[:directory] || "tmp/locale_csv"
10
-
11
- Rake::Task["translation:export:all"].invoke(directory)
12
- Rake::Task["translation:import:all"].invoke(directory)
13
- end
14
-
15
7
  desc "Export a specific locale to CSV."
16
8
  task :export, [:directory, :base_locale, :target_locale] => [:environment] do |t, args|
17
9
  FileUtils.mkdir_p(args[:directory]) unless File.exist?(args[:directory])
@@ -38,62 +30,58 @@ namespace :translation do
38
30
  end
39
31
 
40
32
  desc "Import a specific locale CSV to YAML within the app."
41
- task :import, [:locale, :path] => [:environment] do |t, args|
42
- importer = RailsTranslationManager::Importer.new(args[:locale], args[:path], Rails.root.join("config", "locales"))
33
+ task :import, [:csv_path, :multiple_files_per_language] => [:environment] do |t, args|
34
+ import_dir = Rails.root.join("config", "locales")
35
+ csv_path = args[:csv_path]
36
+
37
+ importer = RailsTranslationManager::Importer.new(
38
+ locale: File.basename(args[:csv_path], ".csv"),
39
+ csv_path: csv_path,
40
+ import_directory: Rails.root.join("config", "locales"),
41
+ multiple_files_per_language: args[:multiple_files_per_language] || false
42
+ )
43
43
  importer.import
44
+
45
+ puts "\nImported CSV from: #{csv_path} to #{import_dir}"
44
46
  end
45
47
 
46
48
  namespace :import do
47
49
  desc "Import all locale CSV files to YAML within the app."
48
- task :all, [:directory] => [:environment] do |t, args|
49
- directory = args[:directory] || "tmp/locale_csv"
50
- Dir[File.join(directory, "*.csv")].each do |csv_path|
51
- locale = File.basename(csv_path, ".csv")
52
- importer = RailsTranslationManager::Importer.new(locale, csv_path, Rails.root.join("config", "locales"))
50
+ task :all, [:csv_directory, :multiple_files_per_language] => [:environment] do |t, args|
51
+ directory = args[:csv_directory] || "tmp/locale_csv"
52
+ import_dir = Rails.root.join("config", "locales")
53
+
54
+ Dir[Rails.root.join(directory, "*.csv")].each do |csv_path|
55
+ importer = RailsTranslationManager::Importer.new(
56
+ locale: File.basename(csv_path, ".csv"),
57
+ csv_path: csv_path,
58
+ import_directory: import_dir,
59
+ multiple_files_per_language: args[:multiple_files_per_language] || false
60
+ )
53
61
  importer.import
54
62
  end
55
- end
56
- end
57
63
 
58
- desc "Check translation files for errors"
59
- task :validate do
60
- require 'rails_translation_manager/validator'
61
- logger = Logger.new(STDOUT)
62
- validator = RailsTranslationManager::Validator.new(Rails.root.join('config', 'locales'), logger)
63
- errors = validator.check!
64
- if errors.any?
65
- puts "Found #{errors.size} errors:"
66
- puts errors.map(&:to_s).join("\n")
67
- else
68
- puts "Success! No unexpected interpolation keys found."
69
- end
70
- end
71
-
72
- desc "Import and convert a locale file from another app."
73
- task :steal, [:locale, :source_app_path, :mapping_file_path] do |t, args|
74
- stealer = RailsTranslationManager::Stealer.new(args[:locale], args[:source_app_path], args[:mapping_file_path], Rails.root.join('config', 'locales'))
75
- stealer.steal_locale
76
- end
77
-
78
- namespace :steal do
79
- desc "Import and convert all locale files from another app."
80
- task :all, [:source_app_path, :mapping_file_path] => [:environment] do |t, args|
81
- I18n.available_locales.reject { |l| l == :en }.each do |locale|
82
- stealer = RailsTranslationManager::Stealer.new(locale.to_s, args[:source_app_path], args[:mapping_file_path], Rails.root.join('config', 'locales'))
83
- stealer.steal_locale
84
- end
64
+ puts "\nImported all CSVs from: #{directory} to #{import_dir}"
85
65
  end
86
66
  end
87
67
 
88
68
  desc "Add missing translations"
89
69
  task(:add_missing, [:locale] => [:environment]) do |t, args|
90
- I18n::Tasks::CLI.start(TranslationHelper.new(["add-missing", "--nil-value"], args[:locale]).with_optional_locale)
70
+ option_parser = RailsTranslationManager::I18nTasksOptionParser.new(
71
+ ["add-missing", "--nil-value"], args[:locale]
72
+ ).with_optional_locale
73
+
74
+ I18n::Tasks::CLI.start(option_parser)
91
75
  RailsTranslationManager::Cleaner.new(Rails.root.join("config", "locales")).clean
92
76
  end
93
77
 
94
78
  desc "Normalize translations"
95
79
  task(:normalize, [:locale] => [:environment]) do |t, args|
96
- I18n::Tasks::CLI.start(TranslationHelper.new(["normalize"], args[:locale]).with_optional_locale)
80
+ option_parser = RailsTranslationManager::I18nTasksOptionParser.new(
81
+ ["normalize"], args[:locale]
82
+ ).with_optional_locale
83
+
84
+ I18n::Tasks::CLI.start(option_parser)
97
85
  RailsTranslationManager::Cleaner.new(Rails.root.join("config", "locales")).clean
98
86
  end
99
87
  end
@@ -18,9 +18,10 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "rails-i18n"
22
21
  spec.add_dependency "activesupport"
22
+ spec.add_dependency "csv", "~> 3.2"
23
23
  spec.add_dependency "i18n-tasks"
24
+ spec.add_dependency "rails-i18n"
24
25
 
25
26
  spec.add_development_dependency "bundler"
26
27
  spec.add_development_dependency "rake", "~> 10.0"
@@ -0,0 +1,9 @@
1
+ key,source,translation
2
+ world_location.type.country,Country,Pays
3
+ world_location.fruit,"[Apples, Bananas, Pears]","[Pommes, Bananes, Poires]"
4
+ world_location.things,nil,nil
5
+ world_location.sentiment,:whatever,:bof
6
+ shared.price,123,123
7
+ shared.key1,is true,true
8
+ shared.key2,is false,false
9
+ ,,
@@ -0,0 +1,4 @@
1
+ key,source,translation
2
+ unpublishing.title,The page you're looking for is no longer available,Ձեր փնտրած էջն այլևս հասանելի չէ
3
+ working_group.contact_details,Contact details,Կոնտակտային տվյալներ
4
+ working_group.policies,Policies,Քաղաքականություններ
@@ -0,0 +1,106 @@
1
+ require "spec_helper"
2
+ require "tmpdir"
3
+
4
+ RSpec.describe RailsTranslationManager::Importer do
5
+ let(:import_directory) { Dir.mktmpdir }
6
+
7
+ it "imports CSV containing a byte order mark" do
8
+ importer = described_class.new(
9
+ locale: "hy",
10
+ csv_path: "spec/locales/importer/hy_with_byte_order_mark.csv",
11
+ import_directory: import_directory,
12
+ multiple_files_per_language: false
13
+ )
14
+ importer.import
15
+
16
+ expect(File).to exist(import_directory + "/hy.yml")
17
+ end
18
+
19
+ it "doesn't try to import a row with a blank key" do
20
+ importer = described_class.new(
21
+ locale: "hy",
22
+ csv_path: "spec/locales/importer/fr.csv",
23
+ import_directory: import_directory,
24
+ multiple_files_per_language: false
25
+ )
26
+
27
+ expect { importer.import }.to output(
28
+ "Invalid row: #<CSV::Row \"key\":nil \"source\":nil \"translation\":nil> for csv_path: spec/locales/importer/fr.csv\n"
29
+ ).to_stdout
30
+ end
31
+
32
+ context "when there is one locale file per language" do
33
+ let(:yaml_translation_data) { YAML.load_file(import_directory + "/fr.yml")["fr"] }
34
+
35
+ before do
36
+ importer = described_class.new(
37
+ locale: "fr",
38
+ csv_path: "spec/locales/importer/fr.csv",
39
+ import_directory: import_directory,
40
+ multiple_files_per_language: false
41
+ )
42
+ importer.import
43
+ end
44
+
45
+ it "creates one YAML file per language" do
46
+ expect(File).to exist(import_directory + "/fr.yml")
47
+ end
48
+
49
+ it "imports nested locales" do
50
+ expected = { "type" => { "country" => "Pays" } }
51
+ expect(yaml_translation_data).to include("world_location" => hash_including(expected))
52
+ end
53
+
54
+ it "imports arrays from CSV as arrays" do
55
+ expected = { "fruit" => ["Pommes", "Bananes", "Poires"] }
56
+ expect(yaml_translation_data).to include("world_location" => hash_including(expected))
57
+ end
58
+
59
+ it "imports string 'nil' as nil" do
60
+ expected = { "things" => nil }
61
+ expect(yaml_translation_data).to include("world_location" => hash_including(expected))
62
+ end
63
+
64
+ it "imports string ':thing' as symbol" do
65
+ expected = { "sentiment" => :bof }
66
+ expect(yaml_translation_data).to include("world_location" => hash_including(expected))
67
+ end
68
+
69
+ it "imports integer strings as integers" do
70
+ expected = { "price" => 123 }
71
+ expect(yaml_translation_data).to include("shared" => hash_including(expected))
72
+ end
73
+
74
+ it "imports boolean values as booleans, not strings" do
75
+ expected = { "key1" => true, "key2" => false }
76
+ expect(yaml_translation_data).to include("shared" => hash_including(expected))
77
+ end
78
+ end
79
+
80
+ context "when there are multiple files per locale" do
81
+ before do
82
+ importer = described_class.new(
83
+ locale: "fr",
84
+ csv_path: "spec/locales/importer/fr.csv",
85
+ import_directory: import_directory,
86
+ multiple_files_per_language: true
87
+ )
88
+ importer.import
89
+ end
90
+
91
+ it "creates multiple YAML files per language in the language's directory" do
92
+ expect(File).to exist(import_directory + "/fr/world_location.yml")
93
+ .and exist(import_directory + "/fr/shared.yml")
94
+ end
95
+
96
+ it "imports only 'world_location' locales to the relevant file" do
97
+ yaml_translation_data = YAML.load_file(import_directory + "/fr/world_location.yml")["fr"]
98
+ expect(yaml_translation_data).to match("world_location" => anything)
99
+ end
100
+
101
+ it "imports only 'shared' locales to the relevant file" do
102
+ yaml_translation_data = YAML.load_file(import_directory + "/fr/shared.yml")["fr"]
103
+ expect(yaml_translation_data).to match("shared" => anything)
104
+ end
105
+ end
106
+ end
@@ -1,38 +1,127 @@
1
- require 'spec_helper'
2
- require_relative '../../spec/support/tasks'
1
+ require "spec_helper"
2
+ require_relative "../../spec/support/tasks"
3
3
 
4
- describe 'rake tasks' do
4
+ describe "rake tasks" do
5
5
  before do
6
6
  fake_rails = double()
7
- fake_rails.stub(:root) { Pathname.new('spec') }
7
+ fake_rails.stub(:root) { Pathname.new("spec") }
8
8
  stub_const("Rails", fake_rails)
9
9
  end
10
10
 
11
- describe 'translation:add_missing', type: :task do
12
- let(:task) { Rake::Task["translation:add_missing"] }
13
-
14
- it 'is executed' do
15
- expect { task.execute }.to output.to_stdout
11
+ describe "translation:import", type: :task do
12
+ let(:task) { Rake::Task["translation:import"] }
13
+ let(:csv_path) { "path/to/import/fr.csv" }
14
+ let!(:importer_instance) { stub_importer }
15
+
16
+ it "outputs to stdout" do
17
+ expect { task.execute(csv_path: csv_path) }
18
+ .to output("\nImported CSV from: #{csv_path} to #{Rails.root.join("config", "locales")}\n")
19
+ .to_stdout
20
+ end
21
+
22
+ it "calls the Importer class with the csv and import paths" do
23
+ task.execute(csv_path: csv_path)
24
+
25
+ expect(RailsTranslationManager::Importer)
26
+ .to have_received(:new)
27
+ .with(locale: "fr",
28
+ csv_path: csv_path,
29
+ import_directory: Rails.root.join("config", "locales"),
30
+ multiple_files_per_language: false)
31
+ expect(importer_instance).to have_received(:import)
32
+ end
33
+ end
34
+
35
+ describe "translation:import:all", type: :task do
36
+ let(:task) { Rake::Task["translation:import:all"] }
37
+ let(:csv_directory) { "locales/importer" }
38
+ let!(:importer_instance) { stub_importer }
39
+
40
+ it "outputs to stdout" do
41
+ expect { task.execute(csv_directory: csv_directory) }
42
+ .to output("\nImported all CSVs from: #{csv_directory} to #{Rails.root.join("config", "locales")}\n")
43
+ .to_stdout
44
+ end
45
+
46
+ it "calls the importer class for each target path" do
47
+ task.execute(csv_directory: csv_directory, multiple_files_per_language: true)
48
+ import_paths = Dir["spec/locales/importer/*.csv"]
49
+
50
+ import_paths.each do |csv_path|
51
+ expect(RailsTranslationManager::Importer)
52
+ .to have_received(:new)
53
+ .with(locale: File.basename(csv_path, ".csv"),
54
+ csv_path: csv_path,
55
+ import_directory: Rails.root.join("config", "locales"),
56
+ multiple_files_per_language: true)
57
+ end
58
+
59
+ expect(importer_instance).to have_received(:import).exactly(import_paths.count)
16
60
  end
61
+ end
62
+
63
+ describe "translation:add_missing", type: :task do
64
+ let(:task) { Rake::Task["translation:add_missing"] }
65
+ let!(:cleaner_instance) { stub_cleaner }
17
66
 
18
- it 'triggers i18n task and allows to receive the right arguments' do
67
+ before do
19
68
  allow(I18n::Tasks::CLI).to receive(:start)
69
+ end
70
+
71
+ it "triggers Cleaner and allows to receive the right arguments" do
20
72
  task.execute
21
- expect(I18n::Tasks::CLI).to have_received(:start).with(["add-missing", "--nil-value"])
73
+ expect(RailsTranslationManager::Cleaner)
74
+ .to have_received(:new)
75
+ .with(Rails.root.join("config", "locales"))
76
+ expect(cleaner_instance).to have_received(:clean)
77
+ end
78
+
79
+ it "triggers i18n task and allows to receive the right arguments" do
80
+ task.execute(locale: "fr")
81
+ expect(I18n::Tasks::CLI).to have_received(:start).with(
82
+ ["add-missing", "--nil-value", ["-l", "fr"]]
83
+ )
22
84
  end
23
85
  end
24
86
 
25
- describe 'translation:normalize', type: :task do
87
+ describe "translation:normalize", type: :task do
26
88
  let(:task) { Rake::Task["translation:normalize"] }
27
-
28
- it 'is executed' do
29
- expect { task.execute }.to_not output.to_stdout
30
- end
89
+ let!(:cleaner_instance) { stub_cleaner }
31
90
 
32
- it 'triggers i18n task and allows to receive the right arguments' do
91
+ before do
33
92
  allow(I18n::Tasks::CLI).to receive(:start)
93
+ end
94
+
95
+ it "triggers Cleaner and allows to receive the right arguments" do
96
+ task.execute(locale_directory: "config/locales")
97
+ expect(RailsTranslationManager::Cleaner)
98
+ .to have_received(:new)
99
+ .with(Rails.root.join("config", "locales"))
100
+ expect(cleaner_instance).to have_received(:clean)
101
+ end
102
+
103
+ it "triggers i18n task and allows to receive the right arguments" do
34
104
  task.execute
35
105
  expect(I18n::Tasks::CLI).to have_received(:start).with(["normalize"])
36
106
  end
37
107
  end
108
+
109
+ def stub_importer
110
+ importer_instance = instance_double(RailsTranslationManager::Importer)
111
+ allow(RailsTranslationManager::Importer).to receive(:new)
112
+ .and_return(importer_instance)
113
+ allow(importer_instance).to receive(:import)
114
+
115
+ importer_instance
116
+ end
117
+
118
+ def stub_cleaner
119
+ cleaner_instance = instance_double(RailsTranslationManager::Cleaner)
120
+ allow(RailsTranslationManager::Cleaner)
121
+ .to receive(:new)
122
+ .and_return(cleaner_instance)
123
+ allow(cleaner_instance).to receive(:clean)
124
+
125
+ cleaner_instance
126
+ end
38
127
  end
@@ -1,3 +1,5 @@
1
+ require "test_helper"
2
+ require "rails_translation_manager/yaml_writer"
1
3
  module RailsTranslationManager
2
4
  class DummyWriter
3
5
  include YAMLWriter
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_translation_manager
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Edd Sowden
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-12 00:00:00.000000000 Z
11
+ date: 2021-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails-i18n
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,7 +25,21 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: activesupport
28
+ name: csv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: i18n-tasks
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - ">="
@@ -39,7 +53,7 @@ dependencies:
39
53
  - !ruby/object:Gem::Version
40
54
  version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
- name: i18n-tasks
56
+ name: rails-i18n
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - ">="
@@ -143,6 +157,7 @@ files:
143
157
  - lib/rails_translation_manager.rb
144
158
  - lib/rails_translation_manager/cleaner.rb
145
159
  - lib/rails_translation_manager/exporter.rb
160
+ - lib/rails_translation_manager/i18n_tasks_option_parser.rb
146
161
  - lib/rails_translation_manager/importer.rb
147
162
  - lib/rails_translation_manager/locale_checker.rb
148
163
  - lib/rails_translation_manager/locale_checker/all_locales.rb
@@ -153,20 +168,20 @@ files:
153
168
  - lib/rails_translation_manager/locale_checker/missing_foreign_locales.rb
154
169
  - lib/rails_translation_manager/locale_checker/plural_forms.rb
155
170
  - lib/rails_translation_manager/railtie.rb
156
- - lib/rails_translation_manager/stealer.rb
157
- - lib/rails_translation_manager/validator.rb
158
171
  - lib/rails_translation_manager/version.rb
159
172
  - lib/rails_translation_manager/yaml_writer.rb
160
173
  - lib/tasks/translation.rake
161
- - lib/tasks/translation_helper.rb
162
174
  - rails_translation_manager.gemspec
163
175
  - spec/locales/cleaner/clean.yml
164
176
  - spec/locales/cleaner/with_whitespace.yml
177
+ - spec/locales/importer/fr.csv
178
+ - spec/locales/importer/hy_with_byte_order_mark.csv
165
179
  - spec/locales/in_sync/cy/browse.yml
166
180
  - spec/locales/in_sync/en/browse.yml
167
181
  - spec/locales/out_of_sync/cy.yml
168
182
  - spec/locales/out_of_sync/en.yml
169
183
  - spec/rails_translation_manager/cleaner_spec.rb
184
+ - spec/rails_translation_manager/importer_spec.rb
170
185
  - spec/rails_translation_manager/locale_checker/all_locales_spec.rb
171
186
  - spec/rails_translation_manager/locale_checker/incompatible_plurals_spec.rb
172
187
  - spec/rails_translation_manager/locale_checker/locale_checker_helper_spec.rb
@@ -178,9 +193,6 @@ files:
178
193
  - spec/support/tasks.rb
179
194
  - spec/tasks/translation_spec.rb
180
195
  - test/rails_translation_manager/exporter_test.rb
181
- - test/rails_translation_manager/importer_test.rb
182
- - test/rails_translation_manager/stealer_test.rb
183
- - test/rails_translation_manager/validator_test.rb
184
196
  - test/rails_translation_manager/yaml_writer_test.rb
185
197
  - test/test_helper.rb
186
198
  - tmp/.gitkeep
@@ -210,11 +222,14 @@ summary: Tasks to manage translation files
210
222
  test_files:
211
223
  - spec/locales/cleaner/clean.yml
212
224
  - spec/locales/cleaner/with_whitespace.yml
225
+ - spec/locales/importer/fr.csv
226
+ - spec/locales/importer/hy_with_byte_order_mark.csv
213
227
  - spec/locales/in_sync/cy/browse.yml
214
228
  - spec/locales/in_sync/en/browse.yml
215
229
  - spec/locales/out_of_sync/cy.yml
216
230
  - spec/locales/out_of_sync/en.yml
217
231
  - spec/rails_translation_manager/cleaner_spec.rb
232
+ - spec/rails_translation_manager/importer_spec.rb
218
233
  - spec/rails_translation_manager/locale_checker/all_locales_spec.rb
219
234
  - spec/rails_translation_manager/locale_checker/incompatible_plurals_spec.rb
220
235
  - spec/rails_translation_manager/locale_checker/locale_checker_helper_spec.rb
@@ -226,8 +241,5 @@ test_files:
226
241
  - spec/support/tasks.rb
227
242
  - spec/tasks/translation_spec.rb
228
243
  - test/rails_translation_manager/exporter_test.rb
229
- - test/rails_translation_manager/importer_test.rb
230
- - test/rails_translation_manager/stealer_test.rb
231
- - test/rails_translation_manager/validator_test.rb
232
244
  - test/rails_translation_manager/yaml_writer_test.rb
233
245
  - test/test_helper.rb
@@ -1,85 +0,0 @@
1
- require "yaml"
2
- require "i18n"
3
- require_relative "yaml_writer"
4
-
5
- class RailsTranslationManager::Stealer
6
- include YAMLWriter
7
-
8
- # locale is the locale name as a string.
9
- # source_app_path is the path to the root of the app to steal from.
10
- # mapping_file_path is the path to a YAML file mapping translation keys in
11
- # the source app to those in the target app. For example:
12
- # document.type: content_item.format
13
- # document.published: content_item.metadata.published
14
- # which will import everything under "document.type" and "document.published"
15
- # in the source app, and write it to "content_item.format" and
16
- # "content_item.metadata.published" in the target app.
17
- # locales_path is the path to the locale files to output, which is usually
18
- # Rails.root.join('config/locales').
19
- # The process will preserve data already in the output file if it is not
20
- # referenced in the mapping, but will always override data belonging to keys
21
- # that are in the mapping.
22
- def initialize(locale, source_app_path, mapping_file_path, locales_path)
23
- @locale = locale
24
- @source_app_path = source_app_path
25
- @mapping_file_path = mapping_file_path
26
- @locales_path = locales_path
27
- end
28
-
29
- def steal_locale
30
- target_data = convert_locale(get_target_data)
31
- write_yaml(target_locale_path, target_data)
32
- end
33
-
34
- def convert_locale(target_data)
35
- mapping_data.each do |source, target|
36
- data = source_data[@locale]
37
- source.split('.').each { |key| data = data.fetch(key, {}) }
38
- set_recursive(target_data[@locale], target.split("."), data)
39
- end
40
- target_data
41
- end
42
-
43
- private
44
-
45
- def set_recursive(hash, keys, data)
46
- if keys.empty?
47
- data
48
- else
49
- key = keys.shift
50
- hash.tap do |h|
51
- h.merge!({ key => set_recursive(hash.fetch(key, {}), keys, data)})
52
- end
53
- end
54
- end
55
-
56
- def source_locale_path
57
- File.join(@source_app_path, 'config', 'locales', "#{@locale}.yml")
58
- end
59
-
60
- def source_data
61
- @source_data ||= YAML.load_file(source_locale_path)
62
- end
63
-
64
- def target_locale_path
65
- File.join(@locales_path, "#{@locale}.yml")
66
- end
67
-
68
- def default_target_data
69
- { @locale => {} }
70
- end
71
-
72
- def get_target_data
73
- if File.exist?(target_locale_path)
74
- YAML.load_file(target_locale_path) || default_target_data
75
- else
76
- default_target_data
77
- end
78
- end
79
-
80
- def mapping_data
81
- @mapping_data ||= YAML.load_file(@mapping_file_path)
82
- end
83
-
84
- end
85
-
@@ -1,92 +0,0 @@
1
- require 'yaml'
2
- require 'logger'
3
-
4
- class RailsTranslationManager::Validator
5
- def initialize(translation_file_path, logger = Logger.new(nil))
6
- @translation_file_path = translation_file_path
7
- @logger = logger
8
- end
9
-
10
- def check!
11
- @logger.info "Checking translation files in '#{@translation_file_path}' for unexpected interpolation keys"
12
- @logger.info "Loading reference file (#{reference_file_name})"
13
- @logger.info "Checking..."
14
- reference = load_translation_file("#{@translation_file_path}/#{reference_file_name}")
15
- Dir["#{@translation_file_path}/*.yml"].reject do |entry|
16
- File.basename(entry) == reference_file_name
17
- end.inject([]) do |errors, entry|
18
- translation_file = load_translation_file(entry)
19
- errors + unexpected_substitution_keys(reference, translation_file)
20
- end
21
- end
22
-
23
- def unexpected_substitution_keys(reference, translation_file)
24
- reference_substitutions = substitutions_in(reference)
25
- target_substitutions = substitutions_in(translation_file)
26
-
27
- targets_by_path = target_substitutions.each_with_object({}) do |target, hash|
28
- hash[exclude_locale_from_path(target.path)] = target
29
- end
30
-
31
- reference_substitutions.each_with_object([]) do |reference, unexpected_substitutions|
32
- target = targets_by_path[exclude_locale_from_path(reference.path)]
33
- next if target.nil? || reference.has_all_substitutions?(target)
34
- unexpected_substitutions << UnexpectedSubstition.new(target, reference)
35
- end
36
- end
37
-
38
- def substitutions_in(translation_file)
39
- flatten(translation_file).reject do |translation|
40
- translation.substitutions.empty?
41
- end
42
- end
43
-
44
- class TranslationEntry < Struct.new(:path, :value)
45
- def substitutions
46
- @substitutions ||= self.value.scan(/%{([^}]*)}/)
47
- end
48
-
49
- def has_all_substitutions?(other)
50
- (other.substitutions - self.substitutions).empty?
51
- end
52
- end
53
-
54
- class UnexpectedSubstition < Struct.new(:target, :reference)
55
- def to_s
56
- missing = (self.reference.substitutions - self.target.substitutions)
57
- extras = (self.target.substitutions - self.reference.substitutions)
58
- message = %Q{Key "#{target.path.join('.')}":}
59
- if extras.any?
60
- message << %Q{ Extra substitutions: ["#{extras.join('", "')}"].}
61
- end
62
- if missing.any?
63
- message << %Q{ Missing substitutions: ["#{missing.join('", "')}"].}
64
- end
65
- message
66
- end
67
- end
68
-
69
- def flatten(translation_file, path = [])
70
- translation_file.map do |key, value|
71
- case value
72
- when Hash
73
- flatten(value, path + [key])
74
- else
75
- TranslationEntry.new(path + [key], value || "")
76
- end
77
- end.flatten
78
- end
79
-
80
- def load_translation_file(filename)
81
- YAML.load_file(filename)
82
- end
83
-
84
- def reference_file_name
85
- "en.yml"
86
- end
87
-
88
- private
89
- def exclude_locale_from_path(path)
90
- path[1..-1]
91
- end
92
- end
@@ -1,132 +0,0 @@
1
- require "test_helper"
2
- require "rails_translation_manager/importer"
3
- require "tmpdir"
4
- require "csv"
5
-
6
- module RailsTranslationManager
7
- class ImporterTest < Minitest::Test
8
- test 'should create a new locale file for a filled in translation csv file' do
9
- given_csv(:fr,
10
- [:key, :source, :translation],
11
- ["world_location.type.country", "Country", "Pays"],
12
- ["world_location.country", "Germany", "Allemange"],
13
- ["other.nested.key", "original", "translated"]
14
- )
15
-
16
- Importer.new(:fr, csv_path(:fr), import_directory).import
17
-
18
- yaml_translation_data = YAML.load_file(File.join(import_directory, "fr.yml"))
19
- expected = {"fr" => {
20
- "world_location" => {
21
- "country" => "Allemange",
22
- "type" => {
23
- "country" => "Pays"
24
- }
25
- },
26
- "other" => {
27
- "nested" => {
28
- "key" => "translated"
29
- }
30
- }
31
- }}
32
- assert_equal expected, yaml_translation_data
33
- end
34
-
35
- test 'imports arrays from CSV as arrays' do
36
- given_csv(:fr,
37
- [:key, :source, :translation],
38
- ["fruit", ["Apples", "Bananas", "Pears"], ["Pommes", "Bananes", "Poires"]]
39
- )
40
-
41
- Importer.new(:fr, csv_path(:fr), import_directory).import
42
-
43
- yaml_translation_data = YAML.load_file(File.join(import_directory, "fr.yml"))
44
- expected = {"fr" => {
45
- "fruit" => ["Pommes", "Bananes", "Poires"]
46
- }}
47
- assert_equal expected, yaml_translation_data
48
- end
49
-
50
- test 'interprets string "nil" as nil' do
51
- given_csv(:fr,
52
- [:key, :source, :translation],
53
- ["things", ["one", nil, "two"], ["une", nil, "deux"]]
54
- )
55
-
56
- Importer.new(:fr, csv_path(:fr), import_directory).import
57
-
58
- yaml_translation_data = YAML.load_file(File.join(import_directory, "fr.yml"))
59
- expected = {"fr" => {
60
- "things" => ["une", nil, "deux"]
61
- }}
62
- assert_equal expected, yaml_translation_data
63
- end
64
-
65
- test 'interprets string ":thing" as symbol' do
66
- given_csv(:fr,
67
- [:key, :source, :translation],
68
- ["sentiment", ":whatever", ":bof"]
69
- )
70
-
71
- Importer.new(:fr, csv_path(:fr), import_directory).import
72
-
73
- yaml_translation_data = YAML.load_file(File.join(import_directory, "fr.yml"))
74
- expected = {"fr" => {
75
- "sentiment" => :bof
76
- }}
77
- assert_equal expected, yaml_translation_data
78
- end
79
-
80
- test 'interprets integer strings as integers' do
81
- given_csv(:fr,
82
- [:key, :source, :translation],
83
- ["price", "123", "123"]
84
- )
85
-
86
- Importer.new(:fr, csv_path(:fr), import_directory).import
87
-
88
- yaml_translation_data = YAML.load_file(File.join(import_directory, "fr.yml"))
89
- expected = {"fr" => {
90
- "price" => 123
91
- }}
92
- assert_equal expected, yaml_translation_data
93
- end
94
-
95
- test 'interprets boolean values as booleans, not strings' do
96
- given_csv(:fr,
97
- [:key, :source, :translation],
98
- ["key1", "is true", "true"],
99
- ["key2", "is false", "false"]
100
- )
101
-
102
- Importer.new(:fr, csv_path(:fr), import_directory).import
103
-
104
- yaml_translation_data = YAML.load_file(File.join(import_directory, "fr.yml"))
105
- expected = {"fr" => {
106
- "key1" => true,
107
- "key2" => false
108
- }}
109
- assert_equal expected, yaml_translation_data
110
- end
111
-
112
- private
113
-
114
- def csv_path(locale)
115
- File.join(import_directory, "#{locale}.csv")
116
- end
117
-
118
- def given_csv(locale, header_row, *rows)
119
- csv = CSV.generate do |csv|
120
- csv << CSV::Row.new(["key", "source", "translation"], ["key", "source", "translation"], true)
121
- rows.each do |row|
122
- csv << CSV::Row.new(["key", "source", "translation"], row)
123
- end
124
- end
125
- File.open(csv_path(locale), "w") { |f| f.write csv.to_s }
126
- end
127
-
128
- def import_directory
129
- @import_directory ||= Dir.mktmpdir
130
- end
131
- end
132
- end
@@ -1,251 +0,0 @@
1
- require "test_helper"
2
-
3
- require "rails_translation_manager/stealer"
4
- require "fileutils"
5
- require "tmpdir"
6
- require "csv"
7
- require "i18n"
8
-
9
- module RailsTranslationManager
10
- class StealerTest < Minitest::Test
11
-
12
- test "converts subtree of items to the same depth" do
13
-
14
- original = {
15
- "fr" => {
16
- "document" => {
17
- "type" => {
18
- "type1" => 'premier genre',
19
- "type2" => 'deuxième genre',
20
- "type3" => 'troisième genre'
21
- }
22
- }
23
- }
24
- }
25
-
26
- conversion_mapping = {
27
- "document.type" => "content_item.format",
28
- }
29
-
30
- expected = {
31
- "fr" => {
32
- "content_item" => {
33
- "format" => {
34
- "type1" => 'premier genre',
35
- "type2" => 'deuxième genre',
36
- "type3" => 'troisième genre'
37
- }
38
- }
39
- }
40
- }
41
- stealer_test(original, conversion_mapping, expected)
42
- end
43
-
44
- test "converts a subtree of items to a different depth" do
45
- original = {
46
- "fr" => {
47
- "document" => {
48
- "published" => 'publiée',
49
- }
50
- }
51
- }
52
- conversion_mapping = {
53
- "document.published" => "content_item.metadata.published"
54
- }
55
-
56
- expected = {
57
- "fr" => {
58
- "content_item" => {
59
- "metadata" => {
60
- "published" => 'publiée'
61
- }
62
- }
63
- }
64
- }
65
-
66
- stealer_test(original, conversion_mapping, expected)
67
- end
68
-
69
- test "combines multiple mappings" do
70
- original = {
71
- "fr" => {
72
- "document" => {
73
- "type" => {
74
- "type1" => 'premier genre',
75
- },
76
- "published" => 'publiée',
77
- }
78
- }
79
- }
80
-
81
- conversion_mapping = {
82
- "document.type" => "content_item.format",
83
- "document.published" => "content_item.metadata.published"
84
- }
85
- expected = {
86
- "fr" => {
87
- "content_item" => {
88
- "format" => {
89
- "type1" => 'premier genre',
90
- },
91
- "metadata" => {
92
- "published" => 'publiée',
93
- }
94
- }
95
- }
96
- }
97
- stealer_test(original, conversion_mapping, expected)
98
- end
99
-
100
- test "does not copy over keys with no mapping" do
101
- original = {
102
- "fr" => {
103
- "document" => {
104
- "published" => 'publiée',
105
- "do_not_want" => 'non voulu'
106
- }
107
- }
108
- }
109
- conversion_mapping = {
110
- "document.published" => "content_item.metadata.published"
111
- }
112
-
113
- expected = {
114
- "fr" => {
115
- "content_item" => {
116
- "metadata" => {
117
- "published" => 'publiée'
118
- }
119
- }
120
- }
121
- }
122
-
123
- stealer_test(original, conversion_mapping, expected)
124
- end
125
-
126
- test "overrides existing translations present in mapping" do
127
- original = {
128
- "fr" => {
129
- "document" => {
130
- "published" => 'publiée',
131
- "updated" => 'mise au jour',
132
- }
133
- }
134
- }
135
-
136
- conversion_mapping = {
137
- "document.published" => "content_item.metadata.published",
138
- "document.updated" => "content_item.metadata.updated"
139
- }
140
-
141
- existing = {
142
- "fr" => {
143
- "content_item" => {
144
- "metadata" => {
145
- "updated" => 'mauvaise traduction'
146
- }
147
- }
148
- }
149
- }
150
-
151
- expected = {
152
- "fr" => {
153
- "content_item" => {
154
- "metadata" => {
155
- "published" => 'publiée',
156
- "updated" => 'mise au jour',
157
- }
158
- }
159
- }
160
- }
161
- stealer_test(original, conversion_mapping, expected, existing)
162
- end
163
-
164
- test "does not override existing translations not in mapping" do
165
- original = {
166
- "fr" => {
167
- "document" => {
168
- "published" => 'publiée',
169
- }
170
- }
171
- }
172
-
173
- conversion_mapping = {
174
- "document.published" => "content_item.metadata.published"
175
- }
176
-
177
- existing = {
178
- "fr" => {
179
- "content_item" => {
180
- "metadata" => {
181
- "updated" => 'mise au jour',
182
- }
183
- }
184
- }
185
- }
186
-
187
- expected = {
188
- "fr" => {
189
- "content_item" => {
190
- "metadata" => {
191
- "published" => 'publiée',
192
- "updated" => 'mise au jour',
193
- }
194
- }
195
- }
196
- }
197
- stealer_test(original, conversion_mapping, expected, existing)
198
- end
199
-
200
- private
201
-
202
- def stealer_test(original, mapping, expected, existing=nil)
203
- write_source_data(original)
204
- write_converter_data(mapping)
205
-
206
- if existing.present?
207
- File.open(locale_file, 'w') do |f|
208
- f.puts(existing.to_yaml)
209
- end
210
- end
211
-
212
- stealer = RailsTranslationManager::Stealer.new("fr", source_dir, converter_path, locales_dir)
213
- stealer.steal_locale
214
-
215
- assert_equal expected, YAML.load_file(locale_file)
216
- end
217
-
218
- def source_dir
219
- @source_dir ||= Dir.mktmpdir
220
- end
221
-
222
- def source_locale_path
223
- File.join(source_dir, 'config/locales')
224
- end
225
-
226
- def write_source_data(data)
227
- FileUtils.mkdir_p(source_locale_path)
228
- File.open(File.join(source_locale_path, 'fr.yml'), 'w') do |f|
229
- f.puts(data.to_yaml)
230
- end
231
- end
232
-
233
- def write_converter_data(data)
234
- File.open(converter_path, 'w') do |f|
235
- f.puts(data.to_yaml)
236
- end
237
- end
238
-
239
- def converter_path
240
- @converter_path ||= Tempfile.new('fr').path
241
- end
242
-
243
- def locales_dir
244
- @locales_dir ||= Dir.mktmpdir
245
- end
246
-
247
- def locale_file
248
- File.join(locales_dir, 'fr.yml')
249
- end
250
- end
251
- end
@@ -1,51 +0,0 @@
1
- # encoding: utf-8
2
- require 'test_helper'
3
- require 'rails_translation_manager/validator'
4
- require 'tmpdir'
5
- require 'fileutils'
6
-
7
- module RailsTranslationManager
8
- class ValidatorTest < Minitest::Test
9
- def setup
10
- @translation_path = Dir.mktmpdir
11
- @translation_validator = Validator.new(@translation_path)
12
- end
13
-
14
- def teardown
15
- FileUtils.remove_entry_secure(@translation_path)
16
- end
17
-
18
- def create_translation_file(locale, content)
19
- File.open(File.join(@translation_path, "#{locale}.yml"), "w") do |f|
20
- f.write(content.lstrip)
21
- end
22
- end
23
-
24
- test "can create a flattened list of substitutions" do
25
- translation_file = YAML.load(%q{
26
- en:
27
- view: View '%{title}'
28
- test: foo
29
- })
30
- expected = [Validator::TranslationEntry.new(%w{en view}, "View '%{title}'")]
31
- assert_equal expected, @translation_validator.substitutions_in(translation_file)
32
- end
33
-
34
- test "detects extra substitution keys" do
35
- create_translation_file("en", %q{
36
- en:
37
- document:
38
- view: View '%{title}'
39
- })
40
- create_translation_file("sr", %q{
41
- sr:
42
- document:
43
- view: Pročitajte '%{naslov}'
44
- })
45
- errors = Validator.new(@translation_path).check!
46
-
47
- expected = %q{Key "sr.document.view": Extra substitutions: ["naslov"]. Missing substitutions: ["title"].}
48
- assert_equal [expected], errors.map(&:to_s)
49
- end
50
- end
51
- end