lokalise_rails 0.0.2.2 → 1.0.1

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.
@@ -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.2'
3
+ module LokaliseRails
4
+ VERSION = '1.0.1'
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,148 @@
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
+ allow_project_id
22
+
23
+ process = VCR.use_cassette('upload_files') do
24
+ described_class.export!
25
+ end.first
26
+
27
+ expect(process.project_id).to eq(LokaliseRails.project_id)
28
+ expect(process.status).to eq('queued')
29
+ end
30
+
31
+ it 'halts when the API key is not set' do
32
+ expect(LokaliseRails).to receive(:api_token).and_return(nil)
33
+ expect(described_class).not_to receive(:each_file)
34
+ expect(-> { described_class.export! }).to output(/API token is not set/).to_stdout
35
+ end
36
+
37
+ it 'halts when the project_id is not set' do
38
+ expect(LokaliseRails).to receive(:project_id).and_return(nil)
39
+ expect(described_class).not_to receive(:each_file)
40
+ expect(-> { described_class.export! }).to output(/Project ID is not set/).to_stdout
41
+ end
42
+ end
43
+
44
+ describe '.each_file' do
45
+ it 'yield proper arguments' do
46
+ expect { |b| described_class.each_file(&b) }.to yield_with_args(
47
+ Pathname.new(path),
48
+ Pathname.new(relative_name)
49
+ )
50
+ end
51
+ end
52
+
53
+ describe '.opts' do
54
+ let(:base64content) { Base64.strict_encode64(File.read(path).strip) }
55
+
56
+ it 'generates proper options' do
57
+ resulting_opts = described_class.opts(path, relative_name)
58
+
59
+ expect(resulting_opts[:data]).to eq(base64content)
60
+ expect(resulting_opts[:filename]).to eq(relative_name)
61
+ expect(resulting_opts[:lang_iso]).to eq('en')
62
+ end
63
+
64
+ it 'allows to redefine options' do
65
+ expect(LokaliseRails).to receive(:export_opts).and_return({
66
+ detect_icu_plurals: true,
67
+ convert_placeholders: true
68
+ })
69
+
70
+ resulting_opts = described_class.opts(path, relative_name)
71
+
72
+ expect(resulting_opts[:data]).to eq(base64content)
73
+ expect(resulting_opts[:filename]).to eq(relative_name)
74
+ expect(resulting_opts[:lang_iso]).to eq('en')
75
+ expect(resulting_opts[:detect_icu_plurals]).to be true
76
+ expect(resulting_opts[:convert_placeholders]).to be true
77
+ end
78
+ end
79
+ end
80
+
81
+ context 'with two translation files' do
82
+ let(:filename_ru) { 'ru.yml' }
83
+ let(:path_ru) { "#{Rails.root}/config/locales/#{filename_ru}" }
84
+ let(:relative_name_ru) { filename_ru }
85
+
86
+ before :all do
87
+ add_translation_files! with_ru: true
88
+ end
89
+
90
+ after :all do
91
+ rm_translation_files
92
+ end
93
+
94
+ describe '.export!' do
95
+ it 'rescues from export errors' do
96
+ allow_project_id
97
+
98
+ processes = VCR.use_cassette('upload_files_error') do
99
+ described_class.export!
100
+ end
101
+
102
+ expect(processes.length).to eq(1)
103
+ process = processes.first
104
+ expect(process.project_id).to eq(LokaliseRails.project_id)
105
+ expect(process.status).to eq('queued')
106
+ end
107
+ end
108
+
109
+ describe '.opts' do
110
+ let(:base64content_ru) { Base64.strict_encode64(File.read(path_ru).strip) }
111
+
112
+ it 'generates proper options' do
113
+ resulting_opts = described_class.opts(path_ru, relative_name_ru)
114
+
115
+ expect(resulting_opts[:data]).to eq(base64content_ru)
116
+ expect(resulting_opts[:filename]).to eq(relative_name_ru)
117
+ expect(resulting_opts[:lang_iso]).to eq('ru_RU')
118
+ end
119
+ end
120
+
121
+ describe '.each_file' do
122
+ it 'yields every translation file' do
123
+ expect { |b| described_class.each_file(&b) }.to yield_successive_args(
124
+ [
125
+ Pathname.new(path),
126
+ Pathname.new(relative_name)
127
+ ],
128
+ [
129
+ Pathname.new(path_ru),
130
+ Pathname.new(relative_name_ru)
131
+ ]
132
+ )
133
+ end
134
+
135
+ it 'does not yield files that have to be skipped' do
136
+ expect(LokaliseRails).to receive(:skip_file_export).twice.and_return(
137
+ ->(f) { f.split[1].to_s.include?('ru') }
138
+ )
139
+ expect { |b| described_class.each_file(&b) }.to yield_successive_args(
140
+ [
141
+ Pathname.new(path),
142
+ Pathname.new(relative_name)
143
+ ]
144
+ )
145
+ end
146
+ end
147
+ end
148
+ 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,28 @@ 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_project_id
26
+ allow(LokaliseRails).to receive(:api_token).and_return('incorrect')
27
+
28
+ VCR.use_cassette('download_files_error') do
29
+ expect(-> { described_class.download_files }).
30
+ to output(/Lokalise::Error::BadRequest/).to_stdout
31
+ end
32
+ end
13
33
  end
14
34
 
15
35
  describe '.import!' do
16
36
  it 'halts when the API key is not set' do
17
37
  expect(LokaliseRails).to receive(:api_token).and_return(nil)
18
- result = described_class.import!
19
- expect(result).to include('API token is not set')
38
+ expect(-> { described_class.import! }).to output(/API token is not set/).to_stdout
20
39
  expect(count_translations).to eq(0)
21
40
  end
22
41
 
23
42
  it 'halts when the project_id is not set' do
24
43
  expect(LokaliseRails).to receive(:project_id).and_return(nil)
25
- result = described_class.import!
26
- expect(result).to include('Project ID is not set')
44
+ expect(-> { described_class.import! }).to output(/Project ID is not set/).to_stdout
27
45
  expect(count_translations).to eq(0)
28
46
  end
29
47
  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