lokalise_rails 0.0.2.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module LokaliseRails
6
+ module TaskDefinition
7
+ class Exporter < Base
8
+ class << self
9
+ # Performs translation file export from Rails to Lokalise and returns an array of queued processes
10
+ #
11
+ # @return [Array]
12
+ def export!
13
+ errors = opt_errors
14
+
15
+ if errors.any?
16
+ errors.each { |e| $stdout.puts e }
17
+ return errors
18
+ end
19
+
20
+ queued_processes = []
21
+ each_file do |full_path, relative_path|
22
+ queued_processes << api_client.upload_file(
23
+ project_id_with_branch, opts(full_path, relative_path)
24
+ )
25
+ rescue StandardError => e
26
+ $stdout.puts "Error while trying to upload #{full_path}: #{e.inspect}"
27
+ end
28
+
29
+ $stdout.print 'Task complete!'
30
+
31
+ queued_processes
32
+ end
33
+
34
+ # Processes each translation file in the specified directory
35
+ def each_file
36
+ return unless block_given?
37
+
38
+ loc_path = LokaliseRails.locales_path
39
+ Dir["#{loc_path}/**/*"].sort.each do |f|
40
+ full_path = Pathname.new f
41
+
42
+ next unless file_matches_criteria? full_path
43
+
44
+ relative_path = full_path.relative_path_from Pathname.new(loc_path)
45
+
46
+ yield full_path, relative_path
47
+ end
48
+ end
49
+
50
+ # Generates export options
51
+ #
52
+ # @return [Hash]
53
+ # @param full_p [Pathname]
54
+ # @param relative_p [Pathname]
55
+ def opts(full_p, relative_p)
56
+ content = File.read full_p
57
+
58
+ lang_iso = YAML.safe_load(content)&.keys&.first
59
+
60
+ initial_opts = {
61
+ data: Base64.strict_encode64(content.strip),
62
+ filename: relative_p,
63
+ lang_iso: lang_iso
64
+ }
65
+
66
+ initial_opts.merge LokaliseRails.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
+ !LokaliseRails.skip_file_export.call(full_path)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -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 project_id_with_branch, 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.3'
3
+ module LokaliseRails
4
+ VERSION = '1.1.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']
@@ -34,11 +35,9 @@ Gem::Specification.new do |spec|
34
35
  end
35
36
  spec.add_development_dependency 'rake', '~> 13.0'
36
37
  spec.add_development_dependency 'rspec', '~> 3.6'
37
- spec.add_development_dependency 'rspec-rails', '~> 4.0'
38
38
  spec.add_development_dependency 'rubocop', '~> 0.60'
39
39
  spec.add_development_dependency 'rubocop-performance', '~> 1.5'
40
40
  spec.add_development_dependency 'rubocop-rspec', '~> 1.37'
41
41
  spec.add_development_dependency 'simplecov', '~> 0.16'
42
- spec.add_development_dependency 'sqlite3', '~> 1.4'
43
42
  spec.add_development_dependency 'vcr', '~> 6.0'
44
43
  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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lokalise_rails'
4
+ LokaliseRails.config do |c|
5
+ c.api_token = ENV['LOKALISE_API_TOKEN']
6
+ c.project_id = ENV['LOKALISE_PROJECT_ID']
7
+ 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,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe LokaliseRails::TaskDefinition::Base do
4
+ specify '.reset_client!' do
5
+ expect(described_class.api_client).to be_an_instance_of(Lokalise::Client)
6
+ described_class.reset_api_client!
7
+ current_client = described_class.instance_variable_get '@api_client'
8
+ expect(current_client).to be_nil
9
+ end
10
+
11
+ specify '.project_id_with_branch!' do
12
+ result = described_class.send :project_id_with_branch
13
+ expect(result).to be_an_instance_of(String)
14
+ expect(result).to include(':master')
15
+ end
16
+
17
+ describe '.subdir_and_filename_for' do
18
+ it 'works properly for longer paths' do
19
+ path = 'my_path/is/here/file.yml'
20
+ result = described_class.send(:subdir_and_filename_for, path)
21
+ expect(result.length).to eq(2)
22
+ expect(result[0]).to be_an_instance_of(Pathname)
23
+ expect(result[0].to_s).to eq('my_path/is/here')
24
+ expect(result[1].to_s).to eq('file.yml')
25
+ end
26
+
27
+ it 'works properly for shorter paths' do
28
+ path = 'file.yml'
29
+ result = described_class.send(:subdir_and_filename_for, path)
30
+ expect(result.length).to eq(2)
31
+ expect(result[1]).to be_an_instance_of(Pathname)
32
+ expect(result[0].to_s).to eq('.')
33
+ expect(result[1].to_s).to eq('file.yml')
34
+ end
35
+ end
36
+
37
+ describe '.opt_errors' do
38
+ it 'returns an error when the API key is not set' do
39
+ allow(LokaliseRails).to receive(:api_token).and_return(nil)
40
+ errors = described_class.opt_errors
41
+
42
+ expect(LokaliseRails).to have_received(:api_token)
43
+ expect(errors.length).to eq(1)
44
+ expect(errors.first).to include('API token is not set')
45
+ end
46
+
47
+ it 'returns an error when the project_id is not set' do
48
+ allow(LokaliseRails).to receive(:project_id).and_return(nil)
49
+ errors = described_class.opt_errors
50
+
51
+ expect(LokaliseRails).to have_received(:project_id)
52
+ expect(errors.length).to eq(1)
53
+ expect(errors.first).to include('Project ID is not set')
54
+ end
55
+ end
56
+
57
+ describe '.proper_ext?' do
58
+ it 'works properly with path represented as a string' do
59
+ path = 'my_path/here/file.yml'
60
+ expect(described_class.send(:proper_ext?, path)).to be true
61
+ end
62
+
63
+ it 'works properly with path represented as a pathname' do
64
+ path = Pathname.new 'my_path/here/file.json'
65
+ expect(described_class.send(:proper_ext?, path)).to be false
66
+ end
67
+ end
68
+
69
+ describe '.api_client' do
70
+ before(:all) { described_class.reset_api_client! }
71
+
72
+ after(:all) { described_class.reset_api_client! }
73
+
74
+ it 'is possible to set timeouts' do
75
+ allow(LokaliseRails).to receive(:timeouts).and_return({
76
+ open_timeout: 100,
77
+ timeout: 500
78
+ })
79
+
80
+ expect(described_class.api_client).to be_an_instance_of(Lokalise::Client)
81
+ expect(described_class.api_client.open_timeout).to eq(100)
82
+ expect(described_class.api_client.timeout).to eq(500)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,166 @@
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 'sends a proper API request when a different branch is provided' do
32
+ allow_project_id
33
+ allow(LokaliseRails).to receive(:branch).and_return('develop')
34
+
35
+ process = VCR.use_cassette('upload_files_branch') do
36
+ described_class.export!
37
+ end.first
38
+
39
+ expect(LokaliseRails).to have_received(:branch)
40
+ expect(process.project_id).to eq(LokaliseRails.project_id)
41
+ expect(process.status).to eq('queued')
42
+ end
43
+
44
+ it 'halts when the API key is not set' do
45
+ allow(LokaliseRails).to receive(:api_token).and_return(nil)
46
+
47
+ expect(-> { described_class.export! }).to output(/API token is not set/).to_stdout
48
+ expect(LokaliseRails).to have_received(:api_token)
49
+ end
50
+
51
+ it 'halts when the project_id is not set' do
52
+ allow(LokaliseRails).to receive(:project_id).and_return(nil)
53
+
54
+ expect(-> { described_class.export! }).to output(/Project ID is not set/).to_stdout
55
+ expect(LokaliseRails).to have_received(:project_id)
56
+ end
57
+ end
58
+
59
+ describe '.each_file' do
60
+ it 'yield proper arguments' do
61
+ expect { |b| described_class.each_file(&b) }.to yield_with_args(
62
+ Pathname.new(path),
63
+ Pathname.new(relative_name)
64
+ )
65
+ end
66
+ end
67
+
68
+ describe '.opts' do
69
+ let(:base64content) { Base64.strict_encode64(File.read(path).strip) }
70
+
71
+ it 'generates proper options' do
72
+ resulting_opts = described_class.opts(path, relative_name)
73
+
74
+ expect(resulting_opts[:data]).to eq(base64content)
75
+ expect(resulting_opts[:filename]).to eq(relative_name)
76
+ expect(resulting_opts[:lang_iso]).to eq('en')
77
+ end
78
+
79
+ it 'allows to redefine options' do
80
+ allow(LokaliseRails).to receive(:export_opts).and_return({
81
+ detect_icu_plurals: true,
82
+ convert_placeholders: true
83
+ })
84
+
85
+ resulting_opts = described_class.opts(path, relative_name)
86
+
87
+ expect(LokaliseRails).to have_received(:export_opts)
88
+ expect(resulting_opts[:data]).to eq(base64content)
89
+ expect(resulting_opts[:filename]).to eq(relative_name)
90
+ expect(resulting_opts[:lang_iso]).to eq('en')
91
+ expect(resulting_opts[:detect_icu_plurals]).to be true
92
+ expect(resulting_opts[:convert_placeholders]).to be true
93
+ end
94
+ end
95
+ end
96
+
97
+ context 'with two translation files' do
98
+ let(:filename_ru) { 'ru.yml' }
99
+ let(:path_ru) { "#{Rails.root}/config/locales/#{filename_ru}" }
100
+ let(:relative_name_ru) { filename_ru }
101
+
102
+ before :all do
103
+ add_translation_files! with_ru: true
104
+ end
105
+
106
+ after :all do
107
+ rm_translation_files
108
+ end
109
+
110
+ describe '.export!' do
111
+ it 'rescues from export errors' do
112
+ allow_project_id
113
+
114
+ processes = VCR.use_cassette('upload_files_error') do
115
+ described_class.export!
116
+ end
117
+
118
+ expect(processes.length).to eq(1)
119
+ process = processes.first
120
+ expect(process.project_id).to eq(LokaliseRails.project_id)
121
+ expect(process.status).to eq('queued')
122
+ end
123
+ end
124
+
125
+ describe '.opts' do
126
+ let(:base64content_ru) { Base64.strict_encode64(File.read(path_ru).strip) }
127
+
128
+ it 'generates proper options' do
129
+ resulting_opts = described_class.opts(path_ru, relative_name_ru)
130
+
131
+ expect(resulting_opts[:data]).to eq(base64content_ru)
132
+ expect(resulting_opts[:filename]).to eq(relative_name_ru)
133
+ expect(resulting_opts[:lang_iso]).to eq('ru_RU')
134
+ end
135
+ end
136
+
137
+ describe '.each_file' do
138
+ it 'yields every translation file' do
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
+ Pathname.new(path_ru),
146
+ Pathname.new(relative_name_ru)
147
+ ]
148
+ )
149
+ end
150
+
151
+ it 'does not yield files that have to be skipped' do
152
+ allow(LokaliseRails).to receive(:skip_file_export).twice.and_return(
153
+ ->(f) { f.split[1].to_s.include?('ru') }
154
+ )
155
+ expect { |b| described_class.each_file(&b) }.to yield_successive_args(
156
+ [
157
+ Pathname.new(path),
158
+ Pathname.new(relative_name)
159
+ ]
160
+ )
161
+
162
+ expect(LokaliseRails).to have_received(:skip_file_export).twice
163
+ end
164
+ end
165
+ end
166
+ end