rails_translation_manager 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2407e2bb0a7f4cf4628d9a9f35bcfb668b085c23bb92418f708c09f571c1b92
4
- data.tar.gz: e0584a3f42216bdc0b9589ab6bb26e7246d6856e0b33f7599ac1e6eed0070af8
3
+ metadata.gz: 0b6a096ee29746cead2529ea0cb83660c0e0217b710e085790969f203f246871
4
+ data.tar.gz: 3c13cb54190c17f1382246827c120cf928e7763a9de4b4724ec36ffe6005fc62
5
5
  SHA512:
6
- metadata.gz: '0249a48778dade714f10f47d4185136da8a8a9d11313273d659817dba1968c01ed5756c42b87e0b1963d3f9a41c1316392317f7214137dca5af8e4962f311f4a'
7
- data.tar.gz: 63553a493e8c36c8daa8fe0bdf388384c2bf80fca3e41fd038f4ed8f92078c4accfe18c9a9561c2dc9faa82a35e9cd4642c7b1894a21a1fbd2fb0302c18092a1
6
+ metadata.gz: 37abc2c32de6245dc3d1cd57da56f14ab3c68f88f9b3802a65735d3c36c02984f823d45ff177729daad6b2234929d03535ca28239a4188889a14be60f35875df
7
+ data.tar.gz: 73f08794d826203ba2be01695751d742bd7a546dee1bf9f8946c98cf57350a48651addd5eef53cfcd9b23303900d6efe0dcd41fb0ae81d67ba060ed851b5061a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 1.1.0
2
+
3
+ Allow multiple files per language to be imported. https://github.com/alphagov/rails_translation_manager/pull/20
4
+
1
5
  ## 1.0.0
2
6
 
3
7
  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
  }
@@ -5,36 +5,39 @@ 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 = CSV.read(csv_path, headers: true, header_converters: :downcase)
19
+
20
+ multiple_files_per_language ? import_csv_into_multiple_files(csv) : import_csv(csv)
21
+ end
22
+
23
+ private
24
+
25
+ def import_csv(csv, import_yml_path = File.join(import_directory, "#{locale}.yml"))
26
+ data = csv.each_with_object({}) do |row, hash|
18
27
  key = row["key"]
19
28
  key_parts = key.split(".")
20
29
  if key_parts.length > 1
21
- leaf_node = (data[key_parts.first] ||= {})
30
+ leaf_node = (hash[key_parts.first] ||= {})
22
31
  key_parts[1..-2].each do |part|
23
32
  leaf_node = (leaf_node[part] ||= {})
24
33
  end
25
34
  leaf_node[key_parts.last] = parse_translation(row["translation"])
26
35
  else
27
- data[key_parts.first] = parse_translation(row["translation"])
36
+ hash[key_parts.first] = parse_translation(row["translation"])
28
37
  end
29
38
  end
30
39
 
31
- write_yaml(import_yml_path, {@locale.to_s => data})
32
- end
33
-
34
- private
35
-
36
- def import_yml_path
37
- File.join(@import_directory, "#{@locale}.yml")
40
+ write_yaml(import_yml_path, { locale.to_s => data })
38
41
  end
39
42
 
40
43
  def parse_translation(translation)
@@ -55,4 +58,19 @@ class RailsTranslationManager::Importer
55
58
  translation
56
59
  end
57
60
  end
61
+
62
+ def import_csv_into_multiple_files(csv)
63
+ group_csv_by_file(csv).each do |group|
64
+ language_dir = File.join(import_directory, locale)
65
+
66
+ Dir.mkdir(language_dir) unless Dir.exists?(language_dir)
67
+
68
+ import_yml_path = File.join(import_directory, locale, "#{group[0]}.yml")
69
+ import_csv(group[1], import_yml_path)
70
+ end
71
+ end
72
+
73
+ def group_csv_by_file(csv)
74
+ csv.group_by { |row| row["key"].split(".").first }
75
+ end
58
76
  end
@@ -1,3 +1,3 @@
1
1
  module RailsTranslationManager
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
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
 
@@ -4,14 +4,6 @@ require_relative "../tasks/translation_helper"
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,50 +30,38 @@ 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 "Imported 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
-
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
63
 
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 "Imported all CSVs from: #{directory} to #{import_dir}"
85
65
  end
86
66
  end
87
67
 
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,81 @@
1
+ require "spec_helper"
2
+ require "tmpdir"
3
+
4
+ RSpec.describe RailsTranslationManager::Importer do
5
+ let(:import_directory) { Dir.mktmpdir }
6
+
7
+ context "when there is one locale file per language" do
8
+ let(:yaml_translation_data) { YAML.load_file(import_directory + "/fr.yml")["fr"] }
9
+
10
+ before do
11
+ importer = described_class.new(
12
+ locale: "fr",
13
+ csv_path: "spec/locales/importer/fr.csv",
14
+ import_directory: import_directory,
15
+ multiple_files_per_language: false
16
+ )
17
+ importer.import
18
+ end
19
+
20
+ it "creates one YAML file per language" do
21
+ expect(File).to exist(import_directory + "/fr.yml")
22
+ end
23
+
24
+ it "imports nested locales" do
25
+ expected = { "type" => { "country" => "Pays" } }
26
+ expect(yaml_translation_data).to include("world_location" => hash_including(expected))
27
+ end
28
+
29
+ it "imports arrays from CSV as arrays" do
30
+ expected = { "fruit" => ["Pommes", "Bananes", "Poires"] }
31
+ expect(yaml_translation_data).to include("world_location" => hash_including(expected))
32
+ end
33
+
34
+ it "imports string 'nil' as nil" do
35
+ expected = { "things" => nil }
36
+ expect(yaml_translation_data).to include("world_location" => hash_including(expected))
37
+ end
38
+
39
+ it "imports string ':thing' as symbol" do
40
+ expected = { "sentiment" => :bof }
41
+ expect(yaml_translation_data).to include("world_location" => hash_including(expected))
42
+ end
43
+
44
+ it "imports integer strings as integers" do
45
+ expected = { "price" => 123 }
46
+ expect(yaml_translation_data).to include("shared" => hash_including(expected))
47
+ end
48
+
49
+ it "imports boolean values as booleans, not strings" do
50
+ expected = { "key1" => true, "key2" => false }
51
+ expect(yaml_translation_data).to include("shared" => hash_including(expected))
52
+ end
53
+ end
54
+
55
+ context "when there are multiple files per locale" do
56
+ before do
57
+ importer = described_class.new(
58
+ locale: "fr",
59
+ csv_path: "spec/locales/importer/fr.csv",
60
+ import_directory: import_directory,
61
+ multiple_files_per_language: true
62
+ )
63
+ importer.import
64
+ end
65
+
66
+ it "creates multiple YAML files per language in the language's directory" do
67
+ expect(File).to exist(import_directory + "/fr/world_location.yml")
68
+ .and exist(import_directory + "/fr/shared.yml")
69
+ end
70
+
71
+ it "imports only 'world_location' locales to the relevant file" do
72
+ yaml_translation_data = YAML.load_file(import_directory + "/fr/world_location.yml")["fr"]
73
+ expect(yaml_translation_data).to match("world_location" => anything)
74
+ end
75
+
76
+ it "imports only 'shared' locales to the relevant file" do
77
+ yaml_translation_data = YAML.load_file(import_directory + "/fr/shared.yml")["fr"]
78
+ expect(yaml_translation_data).to match("shared" => anything)
79
+ end
80
+ end
81
+ end
@@ -1,38 +1,123 @@
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("Imported 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)
16
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("Imported 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
+
49
+ expect(RailsTranslationManager::Importer)
50
+ .to have_received(:new)
51
+ .with(locale: "fr",
52
+ csv_path: "spec/locales/importer/fr.csv",
53
+ import_directory: Rails.root.join("config", "locales"),
54
+ multiple_files_per_language: true)
55
+ expect(importer_instance).to have_received(:import)
56
+ end
57
+ end
58
+
59
+ describe "translation:add_missing", type: :task do
60
+ let(:task) { Rake::Task["translation:add_missing"] }
61
+ let!(:cleaner_instance) { stub_cleaner }
17
62
 
18
- it 'triggers i18n task and allows to receive the right arguments' do
63
+ before do
19
64
  allow(I18n::Tasks::CLI).to receive(:start)
65
+ end
66
+
67
+ it "triggers Cleaner and allows to receive the right arguments" do
20
68
  task.execute
21
- expect(I18n::Tasks::CLI).to have_received(:start).with(["add-missing", "--nil-value"])
69
+ expect(RailsTranslationManager::Cleaner)
70
+ .to have_received(:new)
71
+ .with(Rails.root.join("config", "locales"))
72
+ expect(cleaner_instance).to have_received(:clean)
73
+ end
74
+
75
+ it "triggers i18n task and allows to receive the right arguments" do
76
+ task.execute(locale: "fr")
77
+ expect(I18n::Tasks::CLI).to have_received(:start).with(
78
+ ["add-missing", "--nil-value", ["-l", "fr"]]
79
+ )
22
80
  end
23
81
  end
24
82
 
25
- describe 'translation:normalize', type: :task do
83
+ describe "translation:normalize", type: :task do
26
84
  let(:task) { Rake::Task["translation:normalize"] }
27
-
28
- it 'is executed' do
29
- expect { task.execute }.to_not output.to_stdout
30
- end
85
+ let!(:cleaner_instance) { stub_cleaner }
31
86
 
32
- it 'triggers i18n task and allows to receive the right arguments' do
87
+ before do
33
88
  allow(I18n::Tasks::CLI).to receive(:start)
89
+ end
90
+
91
+ it "triggers Cleaner and allows to receive the right arguments" do
92
+ task.execute(locale_directory: "config/locales")
93
+ expect(RailsTranslationManager::Cleaner)
94
+ .to have_received(:new)
95
+ .with(Rails.root.join("config", "locales"))
96
+ expect(cleaner_instance).to have_received(:clean)
97
+ end
98
+
99
+ it "triggers i18n task and allows to receive the right arguments" do
34
100
  task.execute
35
101
  expect(I18n::Tasks::CLI).to have_received(:start).with(["normalize"])
36
102
  end
37
103
  end
104
+
105
+ def stub_importer
106
+ importer_instance = instance_double(RailsTranslationManager::Importer)
107
+ allow(RailsTranslationManager::Importer).to receive(:new)
108
+ .and_return(importer_instance)
109
+ allow(importer_instance).to receive(:import)
110
+
111
+ importer_instance
112
+ end
113
+
114
+ def stub_cleaner
115
+ cleaner_instance = instance_double(RailsTranslationManager::Cleaner)
116
+ allow(RailsTranslationManager::Cleaner)
117
+ .to receive(:new)
118
+ .and_return(cleaner_instance)
119
+ allow(cleaner_instance).to receive(:clean)
120
+
121
+ cleaner_instance
122
+ end
38
123
  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,14 +1,14 @@
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.0
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-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails-i18n
@@ -153,8 +153,6 @@ files:
153
153
  - lib/rails_translation_manager/locale_checker/missing_foreign_locales.rb
154
154
  - lib/rails_translation_manager/locale_checker/plural_forms.rb
155
155
  - lib/rails_translation_manager/railtie.rb
156
- - lib/rails_translation_manager/stealer.rb
157
- - lib/rails_translation_manager/validator.rb
158
156
  - lib/rails_translation_manager/version.rb
159
157
  - lib/rails_translation_manager/yaml_writer.rb
160
158
  - lib/tasks/translation.rake
@@ -162,11 +160,13 @@ files:
162
160
  - rails_translation_manager.gemspec
163
161
  - spec/locales/cleaner/clean.yml
164
162
  - spec/locales/cleaner/with_whitespace.yml
163
+ - spec/locales/importer/fr.csv
165
164
  - spec/locales/in_sync/cy/browse.yml
166
165
  - spec/locales/in_sync/en/browse.yml
167
166
  - spec/locales/out_of_sync/cy.yml
168
167
  - spec/locales/out_of_sync/en.yml
169
168
  - spec/rails_translation_manager/cleaner_spec.rb
169
+ - spec/rails_translation_manager/importer_spec.rb
170
170
  - spec/rails_translation_manager/locale_checker/all_locales_spec.rb
171
171
  - spec/rails_translation_manager/locale_checker/incompatible_plurals_spec.rb
172
172
  - spec/rails_translation_manager/locale_checker/locale_checker_helper_spec.rb
@@ -178,9 +178,6 @@ files:
178
178
  - spec/support/tasks.rb
179
179
  - spec/tasks/translation_spec.rb
180
180
  - 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
181
  - test/rails_translation_manager/yaml_writer_test.rb
185
182
  - test/test_helper.rb
186
183
  - tmp/.gitkeep
@@ -210,11 +207,13 @@ summary: Tasks to manage translation files
210
207
  test_files:
211
208
  - spec/locales/cleaner/clean.yml
212
209
  - spec/locales/cleaner/with_whitespace.yml
210
+ - spec/locales/importer/fr.csv
213
211
  - spec/locales/in_sync/cy/browse.yml
214
212
  - spec/locales/in_sync/en/browse.yml
215
213
  - spec/locales/out_of_sync/cy.yml
216
214
  - spec/locales/out_of_sync/en.yml
217
215
  - spec/rails_translation_manager/cleaner_spec.rb
216
+ - spec/rails_translation_manager/importer_spec.rb
218
217
  - spec/rails_translation_manager/locale_checker/all_locales_spec.rb
219
218
  - spec/rails_translation_manager/locale_checker/incompatible_plurals_spec.rb
220
219
  - spec/rails_translation_manager/locale_checker/locale_checker_helper_spec.rb
@@ -226,8 +225,5 @@ test_files:
226
225
  - spec/support/tasks.rb
227
226
  - spec/tasks/translation_spec.rb
228
227
  - 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
228
  - test/rails_translation_manager/yaml_writer_test.rb
233
229
  - 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