lokalise_manager 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/CODE_OF_CONDUCT.md +46 -0
- data/.github/CONTRIBUTING.md +14 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +11 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +5 -0
- data/LICENSE +22 -0
- data/README.md +266 -0
- data/Rakefile +21 -0
- data/lib/lokalise_manager/error.rb +10 -0
- data/lib/lokalise_manager/global_config.rb +89 -0
- data/lib/lokalise_manager/task_definitions/base.rb +98 -0
- data/lib/lokalise_manager/task_definitions/exporter.rb +79 -0
- data/lib/lokalise_manager/task_definitions/importer.rb +106 -0
- data/lib/lokalise_manager/version.rb +5 -0
- data/lib/lokalise_manager.rb +34 -0
- data/lokalise_manager.gemspec +38 -0
- data/spec/lib/lokalise_manager/global_config_spec.rb +99 -0
- data/spec/lib/lokalise_manager/task_definitions/base_spec.rb +102 -0
- data/spec/lib/lokalise_manager/task_definitions/exporter_spec.rb +197 -0
- data/spec/lib/lokalise_manager/task_definitions/importer_spec.rb +168 -0
- data/spec/lib/lokalise_manager_spec.rb +15 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/file_manager.rb +64 -0
- data/spec/support/spec_addons.rb +16 -0
- data/spec/support/vcr.rb +11 -0
- metadata +233 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module LokaliseManager
|
6
|
+
module TaskDefinitions
|
7
|
+
class Exporter < Base
|
8
|
+
# Performs translation file export to Lokalise and returns an array of queued processes
|
9
|
+
#
|
10
|
+
# @return [Array]
|
11
|
+
def export!
|
12
|
+
check_options_errors!
|
13
|
+
|
14
|
+
queued_processes = []
|
15
|
+
each_file do |full_path, relative_path|
|
16
|
+
queued_processes << do_upload(full_path, relative_path)
|
17
|
+
rescue StandardError => e
|
18
|
+
raise e.class, "Error while trying to upload #{full_path}: #{e.message}"
|
19
|
+
end
|
20
|
+
|
21
|
+
$stdout.print 'Task complete!'
|
22
|
+
|
23
|
+
queued_processes
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Performs the actual file uploading to Lokalise. If the API rate limit is exceeed,
|
29
|
+
# applies exponential backoff
|
30
|
+
def do_upload(f_path, r_path)
|
31
|
+
with_exp_backoff(config.max_retries_export) do
|
32
|
+
api_client.upload_file project_id_with_branch, opts(f_path, r_path)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Processes each translation file in the specified directory
|
37
|
+
def each_file
|
38
|
+
return unless block_given?
|
39
|
+
|
40
|
+
loc_path = config.locales_path
|
41
|
+
Dir["#{loc_path}/**/*"].sort.each do |f|
|
42
|
+
full_path = Pathname.new f
|
43
|
+
|
44
|
+
next unless file_matches_criteria? full_path
|
45
|
+
|
46
|
+
relative_path = full_path.relative_path_from Pathname.new(loc_path)
|
47
|
+
|
48
|
+
yield full_path, relative_path
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Generates export options
|
53
|
+
#
|
54
|
+
# @return [Hash]
|
55
|
+
# @param full_p [Pathname]
|
56
|
+
# @param relative_p [Pathname]
|
57
|
+
def opts(full_p, relative_p)
|
58
|
+
content = File.read full_p
|
59
|
+
|
60
|
+
initial_opts = {
|
61
|
+
data: Base64.strict_encode64(content.strip),
|
62
|
+
filename: relative_p,
|
63
|
+
lang_iso: config.lang_iso_inferer.call(content)
|
64
|
+
}
|
65
|
+
|
66
|
+
initial_opts.merge config.export_opts
|
67
|
+
end
|
68
|
+
|
69
|
+
# Checks whether the specified file has to be processed or not
|
70
|
+
#
|
71
|
+
# @return [Boolean]
|
72
|
+
# @param full_path [Pathname]
|
73
|
+
def file_matches_criteria?(full_path)
|
74
|
+
full_path.file? && proper_ext?(full_path) &&
|
75
|
+
!config.skip_file_export.call(full_path)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zip'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module LokaliseManager
|
8
|
+
module TaskDefinitions
|
9
|
+
class Importer < Base
|
10
|
+
# Performs translation files import from Lokalise
|
11
|
+
#
|
12
|
+
# @return [Boolean]
|
13
|
+
def import!
|
14
|
+
check_options_errors!
|
15
|
+
|
16
|
+
unless proceed_when_safe_mode?
|
17
|
+
$stdout.print 'Task cancelled!'
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
open_and_process_zip download_files['bundle_url']
|
22
|
+
|
23
|
+
$stdout.print 'Task complete!'
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Downloads files from Lokalise using the specified config.
|
30
|
+
# Utilizes exponential backoff if "too many requests" error is received
|
31
|
+
#
|
32
|
+
# @return [Hash]
|
33
|
+
def download_files
|
34
|
+
with_exp_backoff(config.max_retries_import) do
|
35
|
+
api_client.download_files project_id_with_branch, config.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 = "#{config.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 config.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 config.import_safe_mode && !Dir.empty?(config.locales_path.to_s)
|
82
|
+
|
83
|
+
$stdout.puts "The target directory #{config.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
|
+
def data_from(zip_entry)
|
102
|
+
config.translations_loader.call zip_entry.get_input_stream.read
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require 'lokalise_manager/version'
|
6
|
+
require 'lokalise_manager/error'
|
7
|
+
require 'lokalise_manager/global_config'
|
8
|
+
require 'lokalise_manager/task_definitions/base'
|
9
|
+
require 'lokalise_manager/task_definitions/importer'
|
10
|
+
require 'lokalise_manager/task_definitions/exporter'
|
11
|
+
|
12
|
+
module LokaliseManager
|
13
|
+
class << self
|
14
|
+
# Initializes a new importer client which is used to download
|
15
|
+
# translation files from Lokalise to the current project
|
16
|
+
#
|
17
|
+
# @return [LokaliseManager::TaskDefinitions::Importer]
|
18
|
+
# @param custom_opts [Hash]
|
19
|
+
# @param global_config [Object]
|
20
|
+
def importer(custom_opts = {}, global_config = LokaliseManager::GlobalConfig)
|
21
|
+
LokaliseManager::TaskDefinitions::Importer.new custom_opts, global_config
|
22
|
+
end
|
23
|
+
|
24
|
+
# Initializes a new exporter client which is used to upload
|
25
|
+
# translation files from the current project to Lokalise
|
26
|
+
#
|
27
|
+
# @return [LokaliseManager::TaskDefinitions::Exporter]
|
28
|
+
# @param custom_opts [Hash]
|
29
|
+
# @param global_config [Object]
|
30
|
+
def exporter(custom_opts = {}, global_config = LokaliseManager::GlobalConfig)
|
31
|
+
LokaliseManager::TaskDefinitions::Exporter.new custom_opts, global_config
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('lib/lokalise_manager/version', __dir__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'lokalise_manager'
|
7
|
+
spec.version = LokaliseManager::VERSION
|
8
|
+
spec.authors = ['Ilya Bodrov']
|
9
|
+
spec.email = ['golosizpru@gmail.com']
|
10
|
+
spec.summary = 'Lokalise integration for Ruby'
|
11
|
+
spec.description = 'This gem contains a collection of some common tasks for Lokalise. Specifically, it allows to import/export translation files from/to Lokalise TMS.'
|
12
|
+
spec.homepage = 'https://github.com/bodrovis/lokalise_manager'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
spec.platform = Gem::Platform::RUBY
|
15
|
+
spec.required_ruby_version = '>= 2.5.0'
|
16
|
+
|
17
|
+
spec.files = Dir['README.md', 'LICENSE',
|
18
|
+
'CHANGELOG.md', 'lib/**/*.rb',
|
19
|
+
'lib/**/*.rake',
|
20
|
+
'lokalise_manager.gemspec', '.github/*.md',
|
21
|
+
'Gemfile', 'Rakefile']
|
22
|
+
spec.test_files = Dir['spec/**/*.rb']
|
23
|
+
spec.extra_rdoc_files = ['README.md']
|
24
|
+
spec.require_paths = ['lib']
|
25
|
+
|
26
|
+
spec.add_dependency 'ruby-lokalise-api', '~> 4.0'
|
27
|
+
spec.add_dependency 'rubyzip', '~> 2.3'
|
28
|
+
|
29
|
+
spec.add_development_dependency 'codecov', '~> 0.2'
|
30
|
+
spec.add_development_dependency 'dotenv', '~> 2.5'
|
31
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
32
|
+
spec.add_development_dependency 'rspec', '~> 3.6'
|
33
|
+
spec.add_development_dependency 'rubocop', '~> 1.0'
|
34
|
+
spec.add_development_dependency 'rubocop-performance', '~> 1.5'
|
35
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 2.5.0'
|
36
|
+
spec.add_development_dependency 'simplecov', '~> 0.16'
|
37
|
+
spec.add_development_dependency 'vcr', '~> 6.0'
|
38
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe LokaliseManager::GlobalConfig do
|
4
|
+
let(:fake_class) { class_double(described_class) }
|
5
|
+
|
6
|
+
it 'is possible to provide config' do
|
7
|
+
described_class.config do |c|
|
8
|
+
expect(c).to eq(described_class)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'is possible to set project_id' do
|
13
|
+
allow(fake_class).to receive(:project_id=).with('123.abc')
|
14
|
+
fake_class.project_id = '123.abc'
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'is possible to set file_ext_regexp' do
|
18
|
+
allow(fake_class).to receive(:file_ext_regexp=).with(Regexp.new('.*'))
|
19
|
+
fake_class.file_ext_regexp = Regexp.new('.*')
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'is possible to set import_opts' do
|
23
|
+
allow(fake_class).to receive(:import_opts=).with(duck_type(:each))
|
24
|
+
fake_class.import_opts = {
|
25
|
+
format: 'json',
|
26
|
+
indentation: '8sp'
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'is possible to set export_opts' do
|
31
|
+
allow(fake_class).to receive(:export_opts=).with(duck_type(:each))
|
32
|
+
fake_class.export_opts = {
|
33
|
+
convert_placeholders: true,
|
34
|
+
detect_icu_plurals: true
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'is possible to set branch' do
|
39
|
+
allow(fake_class).to receive(:branch=).with('custom')
|
40
|
+
fake_class.branch = 'custom'
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'is possible to set timeouts' do
|
44
|
+
allow(fake_class).to receive(:timeouts=).with(duck_type(:each))
|
45
|
+
fake_class.timeouts = {
|
46
|
+
open_timeout: 100,
|
47
|
+
timeout: 500
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'is possible to set import_safe_mode' do
|
52
|
+
allow(fake_class).to receive(:import_safe_mode=).with(true)
|
53
|
+
fake_class.import_safe_mode = true
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'is possible to set max_retries_export' do
|
57
|
+
allow(fake_class).to receive(:max_retries_export=).with(10)
|
58
|
+
fake_class.max_retries_export = 10
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'is possible to set max_retries_import' do
|
62
|
+
allow(fake_class).to receive(:max_retries_import=).with(10)
|
63
|
+
fake_class.max_retries_import = 10
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'is possible to set api_token' do
|
67
|
+
allow(fake_class).to receive(:api_token=).with('abc')
|
68
|
+
fake_class.api_token = 'abc'
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'is possible to override locales_path' do
|
72
|
+
allow(fake_class).to receive(:locales_path=).with('/demo/path')
|
73
|
+
fake_class.locales_path = '/demo/path'
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'is possible to set skip_file_export' do
|
77
|
+
cond = ->(f) { f.nil? }
|
78
|
+
allow(fake_class).to receive(:skip_file_export=).with(cond)
|
79
|
+
fake_class.skip_file_export = cond
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'is possible to set translations_loader' do
|
83
|
+
runner = ->(f) { f.to_json }
|
84
|
+
allow(fake_class).to receive(:translations_loader=).with(runner)
|
85
|
+
fake_class.translations_loader = runner
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'is possible to set translations_converter' do
|
89
|
+
runner = ->(f) { f.to_json }
|
90
|
+
allow(fake_class).to receive(:translations_converter=).with(runner)
|
91
|
+
fake_class.translations_converter = runner
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'is possible to set lang_iso_inferer' do
|
95
|
+
runner = ->(f) { f.to_json }
|
96
|
+
allow(fake_class).to receive(:lang_iso_inferer=).with(runner)
|
97
|
+
fake_class.lang_iso_inferer = runner
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe LokaliseManager::TaskDefinitions::Base do
|
4
|
+
let(:described_object) { described_class.new }
|
5
|
+
|
6
|
+
describe '.new' do
|
7
|
+
it 'allows to override config' do
|
8
|
+
obj = described_class.new token: 'fake'
|
9
|
+
expect(obj.config.token).to eq('fake')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#config' do
|
14
|
+
it 'allows to update config after initialization' do
|
15
|
+
obj = described_class.new token: 'fake', project_id: '123'
|
16
|
+
|
17
|
+
obj.config.project_id = '345'
|
18
|
+
|
19
|
+
expect(obj.config.project_id).to eq('345')
|
20
|
+
expect(obj.config.token).to eq('fake')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
specify '.reset_client!' do
|
25
|
+
expect(described_object.api_client).to be_an_instance_of(Lokalise::Client)
|
26
|
+
described_object.reset_api_client!
|
27
|
+
current_client = described_object.instance_variable_get '@api_client'
|
28
|
+
expect(current_client).to be_nil
|
29
|
+
end
|
30
|
+
|
31
|
+
specify '.project_id_with_branch!' do
|
32
|
+
result = described_object.send :project_id_with_branch
|
33
|
+
expect(result).to be_an_instance_of(String)
|
34
|
+
expect(result).not_to include(':master')
|
35
|
+
|
36
|
+
described_object.config.branch = 'develop'
|
37
|
+
result = described_object.send :project_id_with_branch
|
38
|
+
expect(result).to be_an_instance_of(String)
|
39
|
+
expect(result).to include(':develop')
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '.subdir_and_filename_for' do
|
43
|
+
it 'works properly for longer paths' do
|
44
|
+
path = 'my_path/is/here/file.yml'
|
45
|
+
result = described_object.send(:subdir_and_filename_for, path)
|
46
|
+
expect(result.length).to eq(2)
|
47
|
+
expect(result[0]).to be_an_instance_of(Pathname)
|
48
|
+
expect(result[0].to_s).to eq('my_path/is/here')
|
49
|
+
expect(result[1].to_s).to eq('file.yml')
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'works properly for shorter paths' do
|
53
|
+
path = 'file.yml'
|
54
|
+
result = described_object.send(:subdir_and_filename_for, path)
|
55
|
+
expect(result.length).to eq(2)
|
56
|
+
expect(result[1]).to be_an_instance_of(Pathname)
|
57
|
+
expect(result[0].to_s).to eq('.')
|
58
|
+
expect(result[1].to_s).to eq('file.yml')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '.check_options_errors!' do
|
63
|
+
it 'raises an error when the API key is not set' do
|
64
|
+
allow(LokaliseManager::GlobalConfig).to receive(:api_token).and_return(nil)
|
65
|
+
|
66
|
+
expect(-> { described_object.send(:check_options_errors!) }).to raise_error(LokaliseManager::Error, /API token is not set/i)
|
67
|
+
|
68
|
+
expect(LokaliseManager::GlobalConfig).to have_received(:api_token)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'returns an error when the project_id is not set' do
|
72
|
+
allow_project_id described_object, nil do
|
73
|
+
expect(-> { described_object.send(:check_options_errors!) }).to raise_error(LokaliseManager::Error, /ID is not set/i)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '.proper_ext?' do
|
79
|
+
it 'works properly with path represented as a string' do
|
80
|
+
path = 'my_path/here/file.yml'
|
81
|
+
expect(described_object.send(:proper_ext?, path)).to be true
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'works properly with path represented as a pathname' do
|
85
|
+
path = Pathname.new 'my_path/here/file.json'
|
86
|
+
expect(described_object.send(:proper_ext?, path)).to be false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '.api_client' do
|
91
|
+
it 'is possible to set timeouts' do
|
92
|
+
allow(described_object.config).to receive(:timeouts).and_return({
|
93
|
+
open_timeout: 100,
|
94
|
+
timeout: 500
|
95
|
+
})
|
96
|
+
|
97
|
+
expect(described_object.api_client).to be_an_instance_of(Lokalise::Client)
|
98
|
+
expect(described_object.api_client.open_timeout).to eq(100)
|
99
|
+
expect(described_object.api_client.timeout).to eq(500)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
describe LokaliseManager::TaskDefinitions::Exporter do
|
6
|
+
let(:filename) { 'en.yml' }
|
7
|
+
let(:path) { "#{Dir.getwd}/locales/nested/#{filename}" }
|
8
|
+
let(:relative_name) { "nested/#{filename}" }
|
9
|
+
let(:project_id) { ENV['LOKALISE_PROJECT_ID'] }
|
10
|
+
let(:described_object) do
|
11
|
+
described_class.new project_id: project_id,
|
12
|
+
api_token: ENV['LOKALISE_API_TOKEN'],
|
13
|
+
max_retries_export: 2
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'with many translation files' do
|
17
|
+
before :all do
|
18
|
+
add_translation_files! with_ru: true, additional: 5
|
19
|
+
end
|
20
|
+
|
21
|
+
after :all do
|
22
|
+
rm_translation_files
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '.export!' do
|
26
|
+
it 'sends a proper API request and handles rate limiting' do
|
27
|
+
process = VCR.use_cassette('upload_files_multiple') do
|
28
|
+
described_object.export!
|
29
|
+
end.first
|
30
|
+
|
31
|
+
expect(process.project_id).to eq(project_id)
|
32
|
+
expect(process.status).to eq('queued')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'handles too many requests' do
|
36
|
+
allow(described_object).to receive(:sleep).and_return(0)
|
37
|
+
|
38
|
+
fake_client = instance_double('Lokalise::Client')
|
39
|
+
allow(fake_client).to receive(:upload_file).with(any_args).and_raise(Lokalise::Error::TooManyRequests)
|
40
|
+
allow(described_object).to receive(:api_client).and_return(fake_client)
|
41
|
+
|
42
|
+
expect(-> { described_object.export! }).to raise_error(Lokalise::Error::TooManyRequests, /Gave up after 2 retries/i)
|
43
|
+
|
44
|
+
expect(described_object).to have_received(:sleep).exactly(2).times
|
45
|
+
expect(described_object).to have_received(:api_client).exactly(3).times
|
46
|
+
expect(fake_client).to have_received(:upload_file).exactly(3).times
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'with one translation file' do
|
52
|
+
before :all do
|
53
|
+
add_translation_files!
|
54
|
+
end
|
55
|
+
|
56
|
+
after :all do
|
57
|
+
rm_translation_files
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '.export!' do
|
61
|
+
it 'sends a proper API request' do
|
62
|
+
process = VCR.use_cassette('upload_files') do
|
63
|
+
described_object.export!
|
64
|
+
end.first
|
65
|
+
|
66
|
+
expect(process.project_id).to eq(project_id)
|
67
|
+
expect(process.status).to eq('queued')
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'sends a proper API request when a different branch is provided' do
|
71
|
+
allow(described_object.config).to receive(:branch).and_return('develop')
|
72
|
+
|
73
|
+
process = VCR.use_cassette('upload_files_branch') do
|
74
|
+
described_object.export!
|
75
|
+
end.first
|
76
|
+
|
77
|
+
expect(described_object.config).to have_received(:branch).at_most(2).times
|
78
|
+
expect(process.project_id).to eq(project_id)
|
79
|
+
expect(process.status).to eq('queued')
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'halts when the API key is not set' do
|
83
|
+
allow(described_object.config).to receive(:api_token).and_return(nil)
|
84
|
+
|
85
|
+
expect(-> { described_object.export! }).to raise_error(LokaliseManager::Error, /API token is not set/i)
|
86
|
+
expect(described_object.config).to have_received(:api_token)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'halts when the project_id is not set' do
|
90
|
+
allow_project_id described_object, nil do
|
91
|
+
expect(-> { described_object.export! }).to raise_error(LokaliseManager::Error, /ID is not set/i)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '.each_file' do
|
97
|
+
it 'yield proper arguments' do
|
98
|
+
expect { |b| described_object.send(:each_file, &b) }.to yield_with_args(
|
99
|
+
Pathname.new(path),
|
100
|
+
Pathname.new(relative_name)
|
101
|
+
)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe '.opts' do
|
106
|
+
let(:base64content) { Base64.strict_encode64(File.read(path).strip) }
|
107
|
+
|
108
|
+
it 'generates proper options' do
|
109
|
+
resulting_opts = described_object.send(:opts, path, relative_name)
|
110
|
+
|
111
|
+
expect(resulting_opts[:data]).to eq(base64content)
|
112
|
+
expect(resulting_opts[:filename]).to eq(relative_name)
|
113
|
+
expect(resulting_opts[:lang_iso]).to eq('en')
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'allows to redefine options' do
|
117
|
+
allow(described_object.config).to receive(:export_opts).and_return({
|
118
|
+
detect_icu_plurals: true,
|
119
|
+
convert_placeholders: true
|
120
|
+
})
|
121
|
+
|
122
|
+
resulting_opts = described_object.send(:opts, path, relative_name)
|
123
|
+
|
124
|
+
expect(described_object.config).to have_received(:export_opts)
|
125
|
+
expect(resulting_opts[:data]).to eq(base64content)
|
126
|
+
expect(resulting_opts[:filename]).to eq(relative_name)
|
127
|
+
expect(resulting_opts[:lang_iso]).to eq('en')
|
128
|
+
expect(resulting_opts[:detect_icu_plurals]).to be true
|
129
|
+
expect(resulting_opts[:convert_placeholders]).to be true
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context 'with two translation files' do
|
135
|
+
let(:filename_ru) { 'ru.yml' }
|
136
|
+
let(:path_ru) { "#{Dir.getwd}/locales/#{filename_ru}" }
|
137
|
+
|
138
|
+
before :all do
|
139
|
+
add_translation_files! with_ru: true
|
140
|
+
end
|
141
|
+
|
142
|
+
after :all do
|
143
|
+
rm_translation_files
|
144
|
+
end
|
145
|
+
|
146
|
+
describe '.export!' do
|
147
|
+
it 're-raises export errors' do
|
148
|
+
allow_project_id described_object, '542886116159f798720dc4.94769464'
|
149
|
+
|
150
|
+
VCR.use_cassette('upload_files_error') do
|
151
|
+
expect { described_object.export! }.to raise_error(Lokalise::Error::BadRequest, /Unknown `lang_iso`/)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
describe '.opts' do
|
157
|
+
let(:base64content_ru) { Base64.strict_encode64(File.read(path_ru).strip) }
|
158
|
+
|
159
|
+
it 'generates proper options' do
|
160
|
+
resulting_opts = described_object.send(:opts, path_ru, filename_ru)
|
161
|
+
|
162
|
+
expect(resulting_opts[:data]).to eq(base64content_ru)
|
163
|
+
expect(resulting_opts[:filename]).to eq(filename_ru)
|
164
|
+
expect(resulting_opts[:lang_iso]).to eq('ru_RU')
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
describe '.each_file' do
|
169
|
+
it 'yields every translation file' do
|
170
|
+
expect { |b| described_object.send(:each_file, &b) }.to yield_successive_args(
|
171
|
+
[
|
172
|
+
Pathname.new(path),
|
173
|
+
Pathname.new(relative_name)
|
174
|
+
],
|
175
|
+
[
|
176
|
+
Pathname.new(path_ru),
|
177
|
+
Pathname.new(filename_ru)
|
178
|
+
]
|
179
|
+
)
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'does not yield files that have to be skipped' do
|
183
|
+
allow(described_object.config).to receive(:skip_file_export).twice.and_return(
|
184
|
+
->(f) { f.split[1].to_s.include?('ru') }
|
185
|
+
)
|
186
|
+
expect { |b| described_object.send(:each_file, &b) }.to yield_successive_args(
|
187
|
+
[
|
188
|
+
Pathname.new(path),
|
189
|
+
Pathname.new(relative_name)
|
190
|
+
]
|
191
|
+
)
|
192
|
+
|
193
|
+
expect(described_object.config).to have_received(:skip_file_export).twice
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|