rails_translation_manager 1.0.0 → 1.1.0

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: 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