lokalise_manager 1.0.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 +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
|