lokalise_rails 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'base64'
4
-
5
- module LokaliseRails
6
- module TaskDefinition
7
- class Exporter < Base
8
- class << self
9
- # Performs translation file export from Rails to Lokalise and returns an array of queued processes
10
- #
11
- # @return [Array]
12
- def export!
13
- check_options_errors!
14
-
15
- queued_processes = []
16
- each_file do |full_path, relative_path|
17
- queued_processes << do_upload(full_path, relative_path)
18
- rescue StandardError => e
19
- raise e.class, "Error while trying to upload #{full_path}: #{e.message}"
20
- end
21
-
22
- $stdout.print 'Task complete!'
23
-
24
- queued_processes
25
- end
26
-
27
- # Performs the actual file uploading to Lokalise. If the API rate limit is exceeed,
28
- # applies exponential backoff
29
- def do_upload(f_path, r_path)
30
- with_exp_backoff(LokaliseRails.max_retries_export) do
31
- api_client.upload_file project_id_with_branch, opts(f_path, r_path)
32
- end
33
- end
34
-
35
- # Processes each translation file in the specified directory
36
- def each_file
37
- return unless block_given?
38
-
39
- loc_path = LokaliseRails.locales_path
40
- Dir["#{loc_path}/**/*"].sort.each do |f|
41
- full_path = Pathname.new f
42
-
43
- next unless file_matches_criteria? full_path
44
-
45
- relative_path = full_path.relative_path_from Pathname.new(loc_path)
46
-
47
- yield full_path, relative_path
48
- end
49
- end
50
-
51
- # Generates export options
52
- #
53
- # @return [Hash]
54
- # @param full_p [Pathname]
55
- # @param relative_p [Pathname]
56
- def opts(full_p, relative_p)
57
- content = File.read full_p
58
-
59
- initial_opts = {
60
- data: Base64.strict_encode64(content.strip),
61
- filename: relative_p,
62
- lang_iso: LokaliseRails.lang_iso_inferer.call(content)
63
- }
64
-
65
- initial_opts.merge LokaliseRails.export_opts
66
- end
67
-
68
- # Checks whether the specified file has to be processed or not
69
- #
70
- # @return [Boolean]
71
- # @param full_path [Pathname]
72
- def file_matches_criteria?(full_path)
73
- full_path.file? && proper_ext?(full_path) &&
74
- !LokaliseRails.skip_file_export.call(full_path)
75
- end
76
- end
77
- end
78
- end
79
- end
@@ -1,109 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'zip'
4
- require 'yaml'
5
- require 'open-uri'
6
- require 'fileutils'
7
-
8
- module LokaliseRails
9
- module TaskDefinition
10
- class Importer < Base
11
- class << self
12
- # Performs translation files import from Lokalise to Rails app
13
- #
14
- # @return [Boolean]
15
- def import!
16
- check_options_errors!
17
-
18
- unless proceed_when_safe_mode?
19
- $stdout.print 'Task cancelled!'
20
- return false
21
- end
22
-
23
- open_and_process_zip download_files['bundle_url']
24
-
25
- $stdout.print 'Task complete!'
26
- true
27
- end
28
-
29
- # Downloads files from Lokalise using the specified options.
30
- # Utilizes exponential backoff if "too many requests" error is received
31
- #
32
- # @return [Hash]
33
- def download_files
34
- with_exp_backoff(LokaliseRails.max_retries_import) do
35
- api_client.download_files project_id_with_branch, LokaliseRails.import_opts
36
- end
37
- rescue StandardError => e
38
- raise e.class, "There was an error when trying to download files: #{e.message}"
39
- end
40
-
41
- # Opens ZIP archive (local or remote) with translations and processes its entries
42
- #
43
- # @param path [String]
44
- def open_and_process_zip(path)
45
- Zip::File.open_buffer(open_file_or_remote(path)) do |zip|
46
- fetch_zip_entries(zip) { |entry| process!(entry) }
47
- end
48
- rescue StandardError => e
49
- raise e.class, "There was an error when trying to process the downloaded files: #{e.message}"
50
- end
51
-
52
- # Iterates over ZIP entries. Each entry may be a file or folder.
53
- def fetch_zip_entries(zip)
54
- return unless block_given?
55
-
56
- zip.each do |entry|
57
- next unless proper_ext? entry.name
58
-
59
- yield entry
60
- end
61
- end
62
-
63
- # Processes ZIP entry by reading its contents and creating the corresponding translation file
64
- def process!(zip_entry)
65
- data = data_from zip_entry
66
- subdir, filename = subdir_and_filename_for zip_entry.name
67
- full_path = "#{LokaliseRails.locales_path}/#{subdir}"
68
- FileUtils.mkdir_p full_path
69
-
70
- File.open(File.join(full_path, filename), 'w+:UTF-8') do |f|
71
- f.write LokaliseRails.translations_converter.call(data)
72
- end
73
- rescue StandardError => e
74
- raise e.class, "Error when trying to process #{zip_entry&.name}: #{e.message}"
75
- end
76
-
77
- # Checks whether the user wishes to proceed when safe mode is enabled and the target directory is not empty
78
- #
79
- # @return [Boolean]
80
- def proceed_when_safe_mode?
81
- return true unless LokaliseRails.import_safe_mode && !Dir.empty?(LokaliseRails.locales_path.to_s)
82
-
83
- $stdout.puts "The target directory #{LokaliseRails.locales_path} is not empty!"
84
- $stdout.print 'Enter Y to continue: '
85
- answer = $stdin.gets
86
- answer.to_s.strip == 'Y'
87
- end
88
-
89
- # Opens a local file or a remote URL using the provided patf
90
- #
91
- # @return [String]
92
- def open_file_or_remote(path)
93
- parsed_path = URI.parse(path)
94
- if parsed_path&.scheme&.include?('http')
95
- parsed_path.open
96
- else
97
- File.open path
98
- end
99
- end
100
-
101
- private
102
-
103
- def data_from(zip_entry)
104
- LokaliseRails.translations_loader.call zip_entry.get_input_stream.read
105
- end
106
- end
107
- end
108
- end
109
- end
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe LokaliseRails::TaskDefinition::Base do
4
- specify '.reset_client!' do
5
- expect(described_class.api_client).to be_an_instance_of(Lokalise::Client)
6
- described_class.reset_api_client!
7
- current_client = described_class.instance_variable_get '@api_client'
8
- expect(current_client).to be_nil
9
- end
10
-
11
- specify '.project_id_with_branch!' do
12
- result = described_class.send :project_id_with_branch
13
- expect(result).to be_an_instance_of(String)
14
- expect(result).to include(':master')
15
- end
16
-
17
- describe '.subdir_and_filename_for' do
18
- it 'works properly for longer paths' do
19
- path = 'my_path/is/here/file.yml'
20
- result = described_class.send(:subdir_and_filename_for, path)
21
- expect(result.length).to eq(2)
22
- expect(result[0]).to be_an_instance_of(Pathname)
23
- expect(result[0].to_s).to eq('my_path/is/here')
24
- expect(result[1].to_s).to eq('file.yml')
25
- end
26
-
27
- it 'works properly for shorter paths' do
28
- path = 'file.yml'
29
- result = described_class.send(:subdir_and_filename_for, path)
30
- expect(result.length).to eq(2)
31
- expect(result[1]).to be_an_instance_of(Pathname)
32
- expect(result[0].to_s).to eq('.')
33
- expect(result[1].to_s).to eq('file.yml')
34
- end
35
- end
36
-
37
- describe '.check_options_errors!' do
38
- it 'raises an error when the API key is not set' do
39
- allow(LokaliseRails).to receive(:api_token).and_return(nil)
40
-
41
- expect(-> { described_class.check_options_errors! }).to raise_error(LokaliseRails::Error, /API token is not set/i)
42
-
43
- expect(LokaliseRails).to have_received(:api_token)
44
- end
45
-
46
- it 'returns an error when the project_id is not set' do
47
- allow_project_id nil do
48
- expect(-> { described_class.check_options_errors! }).to raise_error(LokaliseRails::Error, /ID is not set/i)
49
- end
50
- end
51
- end
52
-
53
- describe '.proper_ext?' do
54
- it 'works properly with path represented as a string' do
55
- path = 'my_path/here/file.yml'
56
- expect(described_class.send(:proper_ext?, path)).to be true
57
- end
58
-
59
- it 'works properly with path represented as a pathname' do
60
- path = Pathname.new 'my_path/here/file.json'
61
- expect(described_class.send(:proper_ext?, path)).to be false
62
- end
63
- end
64
-
65
- describe '.api_client' do
66
- before(:all) { described_class.reset_api_client! }
67
-
68
- after(:all) { described_class.reset_api_client! }
69
-
70
- it 'is possible to set timeouts' do
71
- allow(LokaliseRails).to receive(:timeouts).and_return({
72
- open_timeout: 100,
73
- timeout: 500
74
- })
75
-
76
- expect(described_class.api_client).to be_an_instance_of(Lokalise::Client)
77
- expect(described_class.api_client.open_timeout).to eq(100)
78
- expect(described_class.api_client.timeout).to eq(500)
79
- end
80
- end
81
- end
@@ -1,200 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'base64'
4
-
5
- describe LokaliseRails::TaskDefinition::Exporter do
6
- let(:filename) { 'en.yml' }
7
- let(:path) { "#{Rails.root}/config/locales/nested/#{filename}" }
8
- let(:relative_name) { "nested/#{filename}" }
9
-
10
- context 'with many translation files' do
11
- let(:fake_class) { described_class }
12
-
13
- before :all do
14
- add_translation_files! with_ru: true, additional: 5
15
- end
16
-
17
- after :all do
18
- rm_translation_files
19
- end
20
-
21
- describe '.export!' do
22
- it 'sends a proper API request and handles rate limiting' do
23
- allow_project_id '672198945b7d72fc048021.15940510'
24
-
25
- process = VCR.use_cassette('upload_files_multiple') do
26
- described_class.export!
27
- end.first
28
-
29
- expect(process.project_id).to eq(LokaliseRails.project_id)
30
- expect(process.status).to eq('queued')
31
- end
32
-
33
- it 'handles too many requests' do
34
- allow_project_id '672198945b7d72fc048021.15940510'
35
- allow(LokaliseRails).to receive(:max_retries_export).and_return(2)
36
-
37
- fake_client = double
38
- allow(fake_client).to receive(:upload_file).and_raise(Lokalise::Error::TooManyRequests)
39
- allow(fake_class).to receive(:api_client).and_return(fake_client)
40
-
41
- expect(-> { fake_class.export! }).to raise_error(Lokalise::Error::TooManyRequests, /Gave up after 2 retries/i)
42
- expect(LokaliseRails).to have_received(:max_retries_export).exactly(1).times
43
- expect(fake_class).to have_received(:api_client).exactly(3).times
44
- expect(fake_client).to have_received(:upload_file).exactly(3).times
45
- end
46
- end
47
- end
48
-
49
- context 'with one translation file' do
50
- before :all do
51
- add_translation_files!
52
- end
53
-
54
- after :all do
55
- rm_translation_files
56
- end
57
-
58
- describe '.export!' do
59
- it 'sends a proper API request' do
60
- allow_project_id
61
-
62
- process = VCR.use_cassette('upload_files') do
63
- described_class.export!
64
- end.first
65
-
66
- expect(process.project_id).to eq(LokaliseRails.project_id)
67
- expect(process.status).to eq('queued')
68
- expect(LokaliseRails.max_retries_export).to eq(5)
69
- end
70
-
71
- it 'sends a proper API request when a different branch is provided' do
72
- allow_project_id
73
- allow(LokaliseRails).to receive(:branch).and_return('develop')
74
-
75
- process = VCR.use_cassette('upload_files_branch') do
76
- described_class.export!
77
- end.first
78
-
79
- expect(LokaliseRails).to have_received(:branch)
80
- expect(process.project_id).to eq(LokaliseRails.project_id)
81
- expect(process.status).to eq('queued')
82
- end
83
-
84
- it 'halts when the API key is not set' do
85
- allow(LokaliseRails).to receive(:api_token).and_return(nil)
86
-
87
- expect(-> { described_class.export! }).to raise_error(LokaliseRails::Error, /API token is not set/i)
88
- expect(LokaliseRails).to have_received(:api_token)
89
- end
90
-
91
- it 'halts when the project_id is not set' do
92
- allow_project_id nil do
93
- expect(-> { described_class.export! }).to raise_error(LokaliseRails::Error, /ID is not set/i)
94
- end
95
- end
96
- end
97
-
98
- describe '.each_file' do
99
- it 'yield proper arguments' do
100
- expect { |b| described_class.each_file(&b) }.to yield_with_args(
101
- Pathname.new(path),
102
- Pathname.new(relative_name)
103
- )
104
- end
105
- end
106
-
107
- describe '.opts' do
108
- let(:base64content) { Base64.strict_encode64(File.read(path).strip) }
109
-
110
- it 'generates proper options' do
111
- resulting_opts = described_class.opts(path, relative_name)
112
-
113
- expect(resulting_opts[:data]).to eq(base64content)
114
- expect(resulting_opts[:filename]).to eq(relative_name)
115
- expect(resulting_opts[:lang_iso]).to eq('en')
116
- end
117
-
118
- it 'allows to redefine options' do
119
- allow(LokaliseRails).to receive(:export_opts).and_return({
120
- detect_icu_plurals: true,
121
- convert_placeholders: true
122
- })
123
-
124
- resulting_opts = described_class.opts(path, relative_name)
125
-
126
- expect(LokaliseRails).to have_received(:export_opts)
127
- expect(resulting_opts[:data]).to eq(base64content)
128
- expect(resulting_opts[:filename]).to eq(relative_name)
129
- expect(resulting_opts[:lang_iso]).to eq('en')
130
- expect(resulting_opts[:detect_icu_plurals]).to be true
131
- expect(resulting_opts[:convert_placeholders]).to be true
132
- end
133
- end
134
- end
135
-
136
- context 'with two translation files' do
137
- let(:filename_ru) { 'ru.yml' }
138
- let(:path_ru) { "#{Rails.root}/config/locales/#{filename_ru}" }
139
- let(:relative_name_ru) { filename_ru }
140
-
141
- before :all do
142
- add_translation_files! with_ru: true
143
- end
144
-
145
- after :all do
146
- rm_translation_files
147
- end
148
-
149
- describe '.export!' do
150
- it 're-raises export errors' do
151
- allow_project_id
152
-
153
- VCR.use_cassette('upload_files_error') do
154
- expect { described_class.export! }.to raise_error(Lokalise::Error::BadRequest, /Unknown `lang_iso`/)
155
- end
156
- end
157
- end
158
-
159
- describe '.opts' do
160
- let(:base64content_ru) { Base64.strict_encode64(File.read(path_ru).strip) }
161
-
162
- it 'generates proper options' do
163
- resulting_opts = described_class.opts(path_ru, relative_name_ru)
164
-
165
- expect(resulting_opts[:data]).to eq(base64content_ru)
166
- expect(resulting_opts[:filename]).to eq(relative_name_ru)
167
- expect(resulting_opts[:lang_iso]).to eq('ru_RU')
168
- end
169
- end
170
-
171
- describe '.each_file' do
172
- it 'yields every translation file' do
173
- expect { |b| described_class.each_file(&b) }.to yield_successive_args(
174
- [
175
- Pathname.new(path),
176
- Pathname.new(relative_name)
177
- ],
178
- [
179
- Pathname.new(path_ru),
180
- Pathname.new(relative_name_ru)
181
- ]
182
- )
183
- end
184
-
185
- it 'does not yield files that have to be skipped' do
186
- allow(LokaliseRails).to receive(:skip_file_export).twice.and_return(
187
- ->(f) { f.split[1].to_s.include?('ru') }
188
- )
189
- expect { |b| described_class.each_file(&b) }.to yield_successive_args(
190
- [
191
- Pathname.new(path),
192
- Pathname.new(relative_name)
193
- ]
194
- )
195
-
196
- expect(LokaliseRails).to have_received(:skip_file_export).twice
197
- end
198
- end
199
- end
200
- end
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe LokaliseRails::TaskDefinition::Importer do
4
- describe '.open_and_process_zip' do
5
- let(:faulty_trans) { "#{Rails.root}/public/faulty_trans.zip" }
6
-
7
- it 're-raises errors during file processing' do
8
- expect(-> { described_class.open_and_process_zip(faulty_trans) }).
9
- to raise_error(Psych::DisallowedClass, /Error when trying to process fail\.yml/)
10
- end
11
-
12
- it 're-raises errors during file opening' do
13
- expect(-> { described_class.open_and_process_zip('http://fake.url/wrong/path.zip') }).
14
- to raise_error(SocketError, /Failed to open TCP connection/)
15
- end
16
- end
17
-
18
- describe '.download_files' do
19
- it 'returns a proper download URL' do
20
- allow_project_id '672198945b7d72fc048021.15940510' do
21
- response = VCR.use_cassette('download_files') do
22
- described_class.download_files
23
- end
24
-
25
- expect(response['project_id']).to eq('672198945b7d72fc048021.15940510')
26
- expect(response['bundle_url']).to include('s3-eu-west-1.amazonaws.com')
27
- expect(described_class.api_client.enable_compression).to eq(true)
28
- expect(LokaliseRails.max_retries_import).to eq(5)
29
- end
30
- end
31
-
32
- it 're-raises errors during file download' do
33
- allow_project_id 'invalid'
34
- VCR.use_cassette('download_files_error') do
35
- expect(-> { described_class.download_files }).
36
- to raise_error(Lokalise::Error::BadRequest, /Invalid `project_id` parameter/)
37
- end
38
- end
39
- end
40
-
41
- describe '.import!' do
42
- let(:fake_class) { described_class }
43
-
44
- it 'handles too many requests' do
45
- allow_project_id '672198945b7d72fc048021.15940510'
46
- allow(LokaliseRails).to receive(:max_retries_import).and_return(2)
47
-
48
- fake_client = double
49
- allow(fake_client).to receive(:download_files).and_raise(Lokalise::Error::TooManyRequests)
50
- allow(fake_class).to receive(:api_client).and_return(fake_client)
51
-
52
- expect(-> { fake_class.import! }).to raise_error(Lokalise::Error::TooManyRequests, /Gave up after 2 retries/i)
53
- expect(LokaliseRails).to have_received(:max_retries_import).exactly(1).times
54
- expect(fake_class).to have_received(:api_client).exactly(3).times
55
- expect(fake_client).to have_received(:download_files).exactly(3).times
56
- end
57
-
58
- it 'halts when the API key is not set' do
59
- allow(LokaliseRails).to receive(:api_token).and_return(nil)
60
- expect(-> { described_class.import! }).to raise_error(LokaliseRails::Error, /API token is not set/i)
61
- expect(LokaliseRails).to have_received(:api_token)
62
- expect(count_translations).to eq(0)
63
- end
64
-
65
- it 'halts when the project_id is not set' do
66
- allow_project_id nil do
67
- expect(-> { described_class.import! }).to raise_error(LokaliseRails::Error, /ID is not set/i)
68
- expect(count_translations).to eq(0)
69
- end
70
- end
71
- end
72
- end