lokalise_manager 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LokaliseManager
4
+ VERSION = '1.0.0'
5
+ 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