lokalise_rails 0.0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,42 +1,84 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LokaliseRails
3
+ require 'zip'
4
+ require 'yaml'
5
+ require 'open-uri'
6
+ require 'fileutils'
7
+
8
+ module LokaliseRails
4
9
  module TaskDefinition
5
10
  class Importer < Base
6
11
  class << self
12
+ # Performs translation files import from Lokalise to Rails app
13
+ #
14
+ # @return [Boolean]
7
15
  def import!
8
- status_ok, msg = check_required_opts
9
- return msg unless status_ok
10
- return 'Task cancelled!' unless proceed_when_safe_mode?
16
+ errors = opt_errors
17
+
18
+ if errors.any?
19
+ errors.each { |e| $stdout.puts e }
20
+ return false
21
+ end
22
+
23
+ unless proceed_when_safe_mode?
24
+ $stdout.print 'Task cancelled!'
25
+ return false
26
+ end
11
27
 
12
28
  open_and_process_zip download_files['bundle_url']
13
29
 
14
- 'Task complete!'
30
+ $stdout.print 'Task complete!'
31
+ true
15
32
  end
16
33
 
34
+ # Downloads files from Lokalise using the specified options
35
+ #
36
+ # @return [Hash]
17
37
  def download_files
18
- client = ::Lokalise.client LokaliseRails.api_token
19
38
  opts = LokaliseRails.import_opts
20
39
 
21
- client.download_files LokaliseRails.project_id, opts
40
+ api_client.download_files LokaliseRails.project_id, opts
41
+ rescue StandardError => e
42
+ $stdout.puts "There was an error when trying to download files: #{e.inspect}"
22
43
  end
23
44
 
45
+ # Opens ZIP archive (local or remote) with translations and processes its entries
46
+ #
47
+ # @param path [String]
24
48
  def open_and_process_zip(path)
25
- Zip::File.open_buffer(URI.open(path)) { |zip| process_zip zip }
49
+ Zip::File.open_buffer(URI.open(path)) do |zip|
50
+ fetch_zip_entries(zip) { |entry| process!(entry) }
51
+ end
26
52
  end
27
53
 
28
- def process_zip(zip)
54
+ # Iterates over ZIP entries. Each entry may be a file or folder.
55
+ def fetch_zip_entries(zip)
56
+ return unless block_given?
57
+
29
58
  zip.each do |entry|
30
- next unless /\.ya?ml/.match?(entry.name)
59
+ next unless proper_ext? entry.name
60
+
61
+ yield entry
62
+ end
63
+ end
64
+
65
+ # Processes ZIP entry by reading its contents and creating the corresponding translation file
66
+ def process!(zip_entry)
67
+ data = YAML.safe_load zip_entry.get_input_stream.read
68
+ subdir, filename = subdir_and_filename_for zip_entry.name
69
+ full_path = "#{LokaliseRails.locales_path}/#{subdir}"
70
+ FileUtils.mkdir_p full_path
31
71
 
32
- filename = entry.name.include?('/') ? entry.name.split('/')[1] : entry.name
33
- data = YAML.safe_load entry.get_input_stream.read
34
- File.open("#{LokaliseRails.locales_path}/#{filename}", 'w+:UTF-8') do |f|
35
- f.write(data.to_yaml)
36
- end
72
+ File.open(File.join(full_path, filename), 'w+:UTF-8') do |f|
73
+ f.write data.to_yaml
37
74
  end
75
+ rescue StandardError => e
76
+ $stdout.puts "Error when trying to process #{zip_entry&.name}: #{e.inspect}"
38
77
  end
39
78
 
79
+ # Checks whether the user wishes to proceed when safe mode is enabled and the target directory is not empty
80
+ #
81
+ # @return [Boolean]
40
82
  def proceed_when_safe_mode?
41
83
  return true unless LokaliseRails.import_safe_mode && !Dir.empty?(LokaliseRails.locales_path.to_s)
42
84
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LokaliseRails
4
- VERSION = '0.0.2.1'
3
+ module LokaliseRails
4
+ VERSION = '1.0.0'
5
5
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake'
4
+ require "#{Rails.root}/config/lokalise_rails"
5
+
6
+ namespace :lokalise_rails do
7
+ task :import do
8
+ LokaliseRails::TaskDefinition::Importer.import!
9
+ end
10
+
11
+ task :export do
12
+ LokaliseRails::TaskDefinition::Exporter.export!
13
+ end
14
+ end
@@ -16,6 +16,7 @@ Gem::Specification.new do |spec|
16
16
 
17
17
  spec.files = Dir['README.md', 'LICENSE',
18
18
  'CHANGELOG.md', 'lib/**/*.rb',
19
+ 'lib/**/*.rake',
19
20
  'lokalise_rails.gemspec', '.github/*.md',
20
21
  'Gemfile', 'Rakefile']
21
22
  spec.test_files = Dir['spec/**/*.rb']
@@ -39,6 +40,5 @@ Gem::Specification.new do |spec|
39
40
  spec.add_development_dependency 'rubocop-performance', '~> 1.5'
40
41
  spec.add_development_dependency 'rubocop-rspec', '~> 1.37'
41
42
  spec.add_development_dependency 'simplecov', '~> 0.16'
42
- spec.add_development_dependency 'sqlite3', '~> 1.4'
43
43
  spec.add_development_dependency 'vcr', '~> 6.0'
44
44
  end
@@ -4,9 +4,9 @@ require_relative 'boot'
4
4
 
5
5
  require 'rails'
6
6
  # Pick the frameworks you want:
7
- require 'active_model/railtie'
7
+ # require 'active_model/railtie'
8
8
  # require "active_job/railtie"
9
- require 'active_record/railtie'
9
+ # require 'active_record/railtie'
10
10
  # require "active_storage/engine"
11
11
  require 'action_controller/railtie'
12
12
  # require "action_mailer/railtie"
@@ -21,6 +21,8 @@ require 'sprockets/railtie'
21
21
  # you've limited to :test, :development, or :production.
22
22
  Bundler.require(*Rails.groups)
23
23
 
24
+ require 'dotenv/load'
25
+
24
26
  module Dummy
25
27
  class Application < Rails::Application
26
28
  # Settings in config/environments/* take precedence over those specified here.
@@ -0,0 +1,5 @@
1
+ require 'lokalise_rails'
2
+ LokaliseRails.config do |c|
3
+ c.api_token = ENV['LOKALISE_API_TOKEN']
4
+ c.project_id = ENV['LOKALISE_PROJECT_ID']
5
+ end
@@ -9,7 +9,7 @@ describe LokaliseRails::Generators::InstallGenerator do
9
9
 
10
10
  after :all do
11
11
  remove_config
12
- described_class.start
12
+ add_config!
13
13
  end
14
14
 
15
15
  it 'installs config file properly' do
@@ -0,0 +1,144 @@
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 one translation file' do
11
+ before :all do
12
+ add_translation_files!
13
+ end
14
+
15
+ after :all do
16
+ rm_translation_files
17
+ end
18
+
19
+ describe '.export!' do
20
+ it 'sends a proper API request' do
21
+ process = VCR.use_cassette('upload_files') do
22
+ described_class.export!
23
+ end.first
24
+
25
+ expect(process.project_id).to eq(LokaliseRails.project_id)
26
+ expect(process.status).to eq('queued')
27
+ end
28
+
29
+ it 'halts when the API key is not set' do
30
+ expect(LokaliseRails).to receive(:api_token).and_return(nil)
31
+ expect(described_class).not_to receive(:each_file)
32
+ expect(-> { described_class.export! }).to output(/API token is not set/).to_stdout
33
+ end
34
+
35
+ it 'halts when the project_id is not set' do
36
+ expect(LokaliseRails).to receive(:project_id).and_return(nil)
37
+ expect(described_class).not_to receive(:each_file)
38
+ expect(-> { described_class.export! }).to output(/Project ID is not set/).to_stdout
39
+ end
40
+ end
41
+
42
+ describe '.each_file' do
43
+ it 'yield proper arguments' do
44
+ expect { |b| described_class.each_file(&b) }.to yield_with_args(
45
+ Pathname.new(path),
46
+ Pathname.new(relative_name)
47
+ )
48
+ end
49
+ end
50
+
51
+ describe '.opts' do
52
+ let(:base64content) { Base64.strict_encode64(File.read(path).strip) }
53
+
54
+ it 'generates proper options' do
55
+ resulting_opts = described_class.opts(path, relative_name)
56
+
57
+ expect(resulting_opts[:data]).to eq(base64content)
58
+ expect(resulting_opts[:filename]).to eq(relative_name)
59
+ expect(resulting_opts[:lang_iso]).to eq('en')
60
+ end
61
+
62
+ it 'allows to redefine options' do
63
+ expect(LokaliseRails).to receive(:export_opts).and_return({
64
+ detect_icu_plurals: true,
65
+ convert_placeholders: true
66
+ })
67
+
68
+ resulting_opts = described_class.opts(path, relative_name)
69
+
70
+ expect(resulting_opts[:data]).to eq(base64content)
71
+ expect(resulting_opts[:filename]).to eq(relative_name)
72
+ expect(resulting_opts[:lang_iso]).to eq('en')
73
+ expect(resulting_opts[:detect_icu_plurals]).to be true
74
+ expect(resulting_opts[:convert_placeholders]).to be true
75
+ end
76
+ end
77
+ end
78
+
79
+ context 'with two translation files' do
80
+ let(:filename_ru) { 'ru.yml' }
81
+ let(:path_ru) { "#{Rails.root}/config/locales/#{filename_ru}" }
82
+ let(:relative_name_ru) { filename_ru }
83
+
84
+ before :all do
85
+ add_translation_files! with_ru: true
86
+ end
87
+
88
+ after :all do
89
+ rm_translation_files
90
+ end
91
+
92
+ describe '.export!' do
93
+ it 'rescues from export errors' do
94
+ processes = VCR.use_cassette('upload_files_error') do
95
+ described_class.export!
96
+ end
97
+
98
+ expect(processes.length).to eq(1)
99
+ process = processes.first
100
+ expect(process.project_id).to eq(LokaliseRails.project_id)
101
+ expect(process.status).to eq('queued')
102
+ end
103
+ end
104
+
105
+ describe '.opts' do
106
+ let(:base64content_ru) { Base64.strict_encode64(File.read(path_ru).strip) }
107
+
108
+ it 'generates proper options' do
109
+ resulting_opts = described_class.opts(path_ru, relative_name_ru)
110
+
111
+ expect(resulting_opts[:data]).to eq(base64content_ru)
112
+ expect(resulting_opts[:filename]).to eq(relative_name_ru)
113
+ expect(resulting_opts[:lang_iso]).to eq('ru_RU')
114
+ end
115
+ end
116
+
117
+ describe '.each_file' do
118
+ it 'yields every translation file' do
119
+ expect { |b| described_class.each_file(&b) }.to yield_successive_args(
120
+ [
121
+ Pathname.new(path),
122
+ Pathname.new(relative_name)
123
+ ],
124
+ [
125
+ Pathname.new(path_ru),
126
+ Pathname.new(relative_name_ru)
127
+ ]
128
+ )
129
+ end
130
+
131
+ it 'does not yield files that have to be skipped' do
132
+ expect(LokaliseRails).to receive(:skip_file_export).twice.and_return(
133
+ ->(f) { f.split[1].to_s.include?('ru') }
134
+ )
135
+ expect { |b| described_class.each_file(&b) }.to yield_successive_args(
136
+ [
137
+ Pathname.new(path),
138
+ Pathname.new(relative_name)
139
+ ]
140
+ )
141
+ end
142
+ end
143
+ end
144
+ end
@@ -1,8 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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 'rescues from errors during file processing' do
8
+ expect(-> { described_class.open_and_process_zip(faulty_trans) }).
9
+ to output(/Psych::DisallowedClass/).to_stdout
10
+ end
11
+ end
12
+
4
13
  describe '.download_files' do
5
14
  it 'returns a proper download URL' do
15
+ expect(LokaliseRails).to receive(:project_id).and_return('189934715f57a162257d74.88352370')
6
16
  response = VCR.use_cassette('download_files') do
7
17
  described_class.download_files
8
18
  end
@@ -10,20 +20,27 @@ describe LokaliseRails::TaskDefinition::Importer do
10
20
  expect(response['project_id']).to eq('189934715f57a162257d74.88352370')
11
21
  expect(response['bundle_url']).to include('s3-eu-west-1.amazonaws.com')
12
22
  end
23
+
24
+ it 'rescues from errors during file download' do
25
+ allow(LokaliseRails).to receive(:api_token).and_return('incorrect')
26
+
27
+ VCR.use_cassette('download_files_error') do
28
+ expect(-> { described_class.download_files }).
29
+ to output(/Lokalise::Error::BadRequest/).to_stdout
30
+ end
31
+ end
13
32
  end
14
33
 
15
34
  describe '.import!' do
16
35
  it 'halts when the API key is not set' do
17
36
  expect(LokaliseRails).to receive(:api_token).and_return(nil)
18
- result = described_class.import!
19
- expect(result).to include('API token is not set')
37
+ expect(-> {described_class.import!}).to output(/API token is not set/).to_stdout
20
38
  expect(count_translations).to eq(0)
21
39
  end
22
40
 
23
41
  it 'halts when the project_id is not set' do
24
42
  expect(LokaliseRails).to receive(:project_id).and_return(nil)
25
- result = described_class.import!
26
- expect(result).to include('Project ID is not set')
43
+ expect(-> {described_class.import!}).to output(/Project ID is not set/).to_stdout
27
44
  expect(count_translations).to eq(0)
28
45
  end
29
46
  end
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  describe LokaliseRails do
4
- it 'should return a proper version' do
4
+ it 'returns a proper version' do
5
5
  expect(LokaliseRails::VERSION).to be_a(String)
6
6
  end
7
7
 
8
+ it 'is possible to provide config options' do
9
+ described_class.config do |c|
10
+ expect(c).to eq(described_class)
11
+ end
12
+ end
13
+
8
14
  describe 'parameters' do
9
15
  let(:fake_class) { class_double('LokaliseRails') }
10
16
 
@@ -13,6 +19,11 @@ describe LokaliseRails do
13
19
  fake_class.project_id = '123.abc'
14
20
  end
15
21
 
22
+ it 'is possible to set file_ext_regexp' do
23
+ expect(fake_class).to receive(:file_ext_regexp=).with(Regexp.new('.*'))
24
+ fake_class.file_ext_regexp = Regexp.new('.*')
25
+ end
26
+
16
27
  it 'is possible to set import_opts' do
17
28
  expect(fake_class).to receive(:import_opts=).with(duck_type(:each))
18
29
  fake_class.import_opts = {
@@ -21,6 +32,14 @@ describe LokaliseRails do
21
32
  }
22
33
  end
23
34
 
35
+ it 'is possible to set export_opts' do
36
+ expect(fake_class).to receive(:export_opts=).with(duck_type(:each))
37
+ fake_class.export_opts = {
38
+ convert_placeholders: true,
39
+ detect_icu_plurals: true
40
+ }
41
+ end
42
+
24
43
  it 'is possible to set import_safe_mode' do
25
44
  expect(fake_class).to receive(:import_safe_mode=).with(true)
26
45
  fake_class.import_safe_mode = true
@@ -32,9 +51,14 @@ describe LokaliseRails do
32
51
  end
33
52
 
34
53
  it 'is possible to override locales_path' do
35
- expect(fake_class).to receive(:locales_path).and_return('/demo/path')
54
+ expect(fake_class).to receive(:locales_path=).with('/demo/path')
55
+ fake_class.locales_path = '/demo/path'
56
+ end
36
57
 
37
- expect(fake_class.locales_path).to eq('/demo/path')
58
+ it 'is possible to set skip_file_export' do
59
+ cond = ->(f) { f.nil? }
60
+ expect(fake_class).to receive(:skip_file_export=).with(cond)
61
+ fake_class.skip_file_export = cond
38
62
  end
39
63
  end
40
64
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe LokaliseRails do
4
+ it 'runs export rake task properly' do
5
+ expect(export_executor).to output(/complete!/).to_stdout
6
+ end
7
+ end
@@ -3,6 +3,10 @@
3
3
  require 'fileutils'
4
4
 
5
5
  RSpec.describe LokaliseRails do
6
+ let(:loc_path) { described_class.locales_path }
7
+ let(:local_trans) { "#{Rails.root}/public/trans.zip" }
8
+ let(:remote_trans) { 'https://github.com/bodrovis/lokalise_rails/blob/master/spec/dummy/public/trans.zip?raw=true' }
9
+
6
10
  context 'when directory is empty' do
7
11
  before do
8
12
  mkdir_locales
@@ -13,13 +17,13 @@ RSpec.describe LokaliseRails do
13
17
  rm_translation_files
14
18
  end
15
19
 
16
- it 'is callable' do
20
+ it 'import rake task is callable' do
17
21
  expect(LokaliseRails::TaskDefinition::Importer).to receive(
18
22
  :download_files
19
23
  ).and_return(
20
24
  {
21
25
  'project_id' => '123.abc',
22
- 'bundle_url' => "#{Rails.root}/public/translations.zip"
26
+ 'bundle_url' => local_trans
23
27
  }
24
28
  )
25
29
 
@@ -27,17 +31,18 @@ RSpec.describe LokaliseRails do
27
31
 
28
32
  expect(count_translations).to eq(4)
29
33
 
30
- main_en = File.join described_class.locales_path, 'main_en.yml'
31
- expect(File.file?(main_en)).to be true
34
+ expect_file_exist loc_path, 'en/nested/main_en.yml'
35
+ expect_file_exist loc_path, 'en/nested/deep/secondary_en.yml'
36
+ expect_file_exist loc_path, 'ru/main_ru.yml'
32
37
  end
33
38
 
34
- it 'downloads ZIP archive properly' do
39
+ it 'import rake task downloads ZIP archive properly' do
35
40
  expect(LokaliseRails::TaskDefinition::Importer).to receive(
36
41
  :download_files
37
42
  ).and_return(
38
43
  {
39
44
  'project_id' => '123.abc',
40
- 'bundle_url' => 'https://github.com/bodrovis/lokalise_rails/blob/master/spec/dummy/public/translations.zip?raw=true'
45
+ 'bundle_url' => remote_trans
41
46
  }
42
47
  )
43
48
 
@@ -45,38 +50,53 @@ RSpec.describe LokaliseRails do
45
50
 
46
51
  expect(count_translations).to eq(4)
47
52
 
48
- main_en = File.join described_class.locales_path, 'main_en.yml'
49
- expect(File.file?(main_en)).to be true
53
+ expect_file_exist loc_path, 'en/nested/main_en.yml'
54
+ expect_file_exist loc_path, 'en/nested/deep/secondary_en.yml'
55
+ expect_file_exist loc_path, 'ru/main_ru.yml'
50
56
  end
51
57
  end
52
58
 
53
- context 'when directory is not empty' do
59
+ context 'when directory is not empty and safe mode enabled' do
54
60
  before :all do
55
61
  mkdir_locales
56
- rm_translation_files
57
- temp_file = File.join described_class.locales_path, 'kill.me'
58
- File.open(temp_file, 'w+') { |file| file.write('temp') }
59
62
  described_class.import_safe_mode = true
60
63
  end
61
64
 
65
+ before do
66
+ rm_translation_files
67
+ add_translation_files!
68
+ end
69
+
62
70
  after :all do
63
71
  rm_translation_files
64
72
  described_class.import_safe_mode = false
65
73
  end
66
74
 
67
- it 'returns a success message with default settings' do
75
+ it 'import proceeds when the user agrees' do
68
76
  expect(LokaliseRails::TaskDefinition::Importer).to receive(
69
77
  :download_files
70
78
  ).and_return(
71
79
  {
72
80
  'project_id' => '123.abc',
73
- 'bundle_url' => "#{Rails.root}/public/translations.zip"
81
+ 'bundle_url' => local_trans
74
82
  }
75
83
  )
76
84
  expect($stdin).to receive(:gets).and_return('Y')
77
85
  expect(import_executor).to output(/is not empty/).to_stdout
78
86
 
79
87
  expect(count_translations).to eq(5)
88
+
89
+ expect_file_exist loc_path, 'en/nested/main_en.yml'
90
+ expect_file_exist loc_path, 'en/nested/deep/secondary_en.yml'
91
+ expect_file_exist loc_path, 'ru/main_ru.yml'
92
+ end
93
+
94
+ it 'import halts when a user chooses not to proceed' do
95
+ expect(LokaliseRails::TaskDefinition::Importer).not_to receive(:download_files)
96
+ expect($stdin).to receive(:gets).and_return('N')
97
+ expect(import_executor).to output(/is not empty/).to_stdout
98
+
99
+ expect(count_translations).to eq(1)
80
100
  end
81
101
  end
82
102
  end