lokalise_rails 0.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
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
+ initial_opts = {
59
+ data: Base64.strict_encode64(content.strip),
60
+ filename: relative_p,
61
+ lang_iso: LokaliseRails.lang_iso_inferer.call(content)
62
+ }
63
+
64
+ initial_opts.merge LokaliseRails.export_opts
65
+ end
66
+
67
+ # Checks whether the specified file has to be processed or not
68
+ #
69
+ # @return [Boolean]
70
+ # @param full_path [Pathname]
71
+ def file_matches_criteria?(full_path)
72
+ full_path.file? && proper_ext?(full_path) &&
73
+ !LokaliseRails.skip_file_export.call(full_path)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ 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(open_file_or_remote(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
31
60
 
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
61
+ yield entry
37
62
  end
38
63
  end
39
64
 
65
+ # Processes ZIP entry by reading its contents and creating the corresponding translation file
66
+ def process!(zip_entry)
67
+ data = data_from zip_entry
68
+ subdir, filename = subdir_and_filename_for zip_entry.name
69
+ full_path = "#{LokaliseRails.locales_path}/#{subdir}"
70
+ FileUtils.mkdir_p full_path
71
+
72
+ File.open(File.join(full_path, filename), 'w+:UTF-8') do |f|
73
+ f.write LokaliseRails.translations_converter.call(data)
74
+ end
75
+ rescue StandardError => e
76
+ $stdout.puts "Error when trying to process #{zip_entry&.name}: #{e.inspect}"
77
+ end
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
 
@@ -45,6 +87,24 @@ class LokaliseRails
45
87
  answer = $stdin.gets
46
88
  answer.to_s.strip == 'Y'
47
89
  end
90
+
91
+ # Opens a local file or a remote URL using the provided patf
92
+ #
93
+ # @return [String]
94
+ def open_file_or_remote(path)
95
+ parsed_path = URI.parse(path)
96
+ if parsed_path&.scheme&.include?('http')
97
+ parsed_path.open
98
+ else
99
+ File.open path
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def data_from(zip_entry)
106
+ LokaliseRails.translations_loader.call zip_entry.get_input_stream.read
107
+ end
48
108
  end
49
109
  end
50
110
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LokaliseRails
4
- VERSION = '0.2.0'
3
+ module LokaliseRails
4
+ VERSION = '1.3.0'
5
5
  end
@@ -1,15 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rake'
4
- require 'ruby-lokalise-api'
5
- require 'zip'
6
- require 'open-uri'
7
- require 'yaml'
8
4
  require "#{Rails.root}/config/lokalise_rails"
9
5
 
10
6
  namespace :lokalise_rails do
11
7
  task :import do
12
- msg = LokaliseRails::TaskDefinition::Importer.import!
13
- $stdout.print msg
8
+ LokaliseRails::TaskDefinition::Importer.import!
9
+ end
10
+
11
+ task :export do
12
+ LokaliseRails::TaskDefinition::Exporter.export!
14
13
  end
15
14
  end
@@ -23,23 +23,21 @@ Gem::Specification.new do |spec|
23
23
  spec.extra_rdoc_files = ['README.md']
24
24
  spec.require_paths = ['lib']
25
25
 
26
- spec.add_dependency 'ruby-lokalise-api', '~> 3.1'
26
+ spec.add_dependency 'ruby-lokalise-api', '~> 4.0'
27
27
  spec.add_dependency 'rubyzip', '~> 2.3'
28
28
 
29
- spec.add_development_dependency 'codecov', '~> 0.1'
29
+ spec.add_development_dependency 'codecov', '~> 0.2'
30
30
  spec.add_development_dependency 'dotenv', '~> 2.5'
31
31
  if ENV['TEST_RAILS_VERSION'].nil?
32
- spec.add_development_dependency 'rails', '~> 6.0.3'
32
+ spec.add_development_dependency 'rails', '~> 6.1.0'
33
33
  else
34
34
  spec.add_development_dependency 'rails', ENV['TEST_RAILS_VERSION'].to_s
35
35
  end
36
36
  spec.add_development_dependency 'rake', '~> 13.0'
37
37
  spec.add_development_dependency 'rspec', '~> 3.6'
38
- spec.add_development_dependency 'rspec-rails', '~> 4.0'
39
- spec.add_development_dependency 'rubocop', '~> 0.60'
38
+ spec.add_development_dependency 'rubocop', '~> 1.0'
40
39
  spec.add_development_dependency 'rubocop-performance', '~> 1.5'
41
- spec.add_development_dependency 'rubocop-rspec', '~> 1.37'
40
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.1.0'
42
41
  spec.add_development_dependency 'simplecov', '~> 0.16'
43
- spec.add_development_dependency 'sqlite3', '~> 1.4'
44
42
  spec.add_development_dependency 'vcr', '~> 6.0'
45
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"
@@ -17,9 +17,7 @@ Rails.application.configure do
17
17
 
18
18
  # Configure public file server for tests with Cache-Control for performance.
19
19
  config.public_file_server.enabled = true
20
- config.public_file_server.headers = {
21
- 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
22
- }
20
+ config.public_file_server.headers = {'Cache-Control' => 'public, max-age=3600'}
23
21
 
24
22
  # Show full error reports and disable caching.
25
23
  config.consider_all_requests_local = true
@@ -1,29 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'lokalise_rails'
4
-
5
- # These are mandatory options that you must set before running rake tasks:
6
- LokaliseRails.api_token = ENV['LOKALISE_API_TOKEN']
7
- LokaliseRails.project_id = ENV['LOKALISE_PROJECT_ID']
8
-
9
- # Import options have the following defaults:
10
- # LokaliseRails.import_opts = {
11
- # format: 'yaml',
12
- # placeholder_format: :icu,
13
- # yaml_include_root: true,
14
- # original_filenames: true,
15
- # directory_prefix: '',
16
- # indentation: '2sp'
17
- # }
18
-
19
- # Safe mode is disabled by default:
20
- # LokaliseRails.import_safe_mode = false
21
-
22
- # Provide a custom path to the directory with your translation files:
23
- # class LokaliseRails
24
- # class << self
25
- # def locales_path
26
- # "#{Rails.root}/config/locales"
27
- # end
28
- # end
29
- # end
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,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_project_id nil do
49
+ errors = described_class.opt_errors
50
+
51
+ expect(errors.length).to eq(1)
52
+ expect(errors.first).to include('Project ID is not set')
53
+ end
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,165 @@
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_project_id nil do
53
+ expect(-> { described_class.export! }).to output(/Project ID is not set/).to_stdout
54
+ end
55
+ end
56
+ end
57
+
58
+ describe '.each_file' do
59
+ it 'yield proper arguments' do
60
+ expect { |b| described_class.each_file(&b) }.to yield_with_args(
61
+ Pathname.new(path),
62
+ Pathname.new(relative_name)
63
+ )
64
+ end
65
+ end
66
+
67
+ describe '.opts' do
68
+ let(:base64content) { Base64.strict_encode64(File.read(path).strip) }
69
+
70
+ it 'generates proper options' do
71
+ resulting_opts = described_class.opts(path, relative_name)
72
+
73
+ expect(resulting_opts[:data]).to eq(base64content)
74
+ expect(resulting_opts[:filename]).to eq(relative_name)
75
+ expect(resulting_opts[:lang_iso]).to eq('en')
76
+ end
77
+
78
+ it 'allows to redefine options' do
79
+ allow(LokaliseRails).to receive(:export_opts).and_return({
80
+ detect_icu_plurals: true,
81
+ convert_placeholders: true
82
+ })
83
+
84
+ resulting_opts = described_class.opts(path, relative_name)
85
+
86
+ expect(LokaliseRails).to have_received(:export_opts)
87
+ expect(resulting_opts[:data]).to eq(base64content)
88
+ expect(resulting_opts[:filename]).to eq(relative_name)
89
+ expect(resulting_opts[:lang_iso]).to eq('en')
90
+ expect(resulting_opts[:detect_icu_plurals]).to be true
91
+ expect(resulting_opts[:convert_placeholders]).to be true
92
+ end
93
+ end
94
+ end
95
+
96
+ context 'with two translation files' do
97
+ let(:filename_ru) { 'ru.yml' }
98
+ let(:path_ru) { "#{Rails.root}/config/locales/#{filename_ru}" }
99
+ let(:relative_name_ru) { filename_ru }
100
+
101
+ before :all do
102
+ add_translation_files! with_ru: true
103
+ end
104
+
105
+ after :all do
106
+ rm_translation_files
107
+ end
108
+
109
+ describe '.export!' do
110
+ it 'rescues from export errors' do
111
+ allow_project_id
112
+
113
+ processes = VCR.use_cassette('upload_files_error') do
114
+ described_class.export!
115
+ end
116
+
117
+ expect(processes.length).to eq(1)
118
+ process = processes.first
119
+ expect(process.project_id).to eq(LokaliseRails.project_id)
120
+ expect(process.status).to eq('queued')
121
+ end
122
+ end
123
+
124
+ describe '.opts' do
125
+ let(:base64content_ru) { Base64.strict_encode64(File.read(path_ru).strip) }
126
+
127
+ it 'generates proper options' do
128
+ resulting_opts = described_class.opts(path_ru, relative_name_ru)
129
+
130
+ expect(resulting_opts[:data]).to eq(base64content_ru)
131
+ expect(resulting_opts[:filename]).to eq(relative_name_ru)
132
+ expect(resulting_opts[:lang_iso]).to eq('ru_RU')
133
+ end
134
+ end
135
+
136
+ describe '.each_file' do
137
+ it 'yields every translation file' do
138
+ expect { |b| described_class.each_file(&b) }.to yield_successive_args(
139
+ [
140
+ Pathname.new(path),
141
+ Pathname.new(relative_name)
142
+ ],
143
+ [
144
+ Pathname.new(path_ru),
145
+ Pathname.new(relative_name_ru)
146
+ ]
147
+ )
148
+ end
149
+
150
+ it 'does not yield files that have to be skipped' do
151
+ allow(LokaliseRails).to receive(:skip_file_export).twice.and_return(
152
+ ->(f) { f.split[1].to_s.include?('ru') }
153
+ )
154
+ expect { |b| described_class.each_file(&b) }.to yield_successive_args(
155
+ [
156
+ Pathname.new(path),
157
+ Pathname.new(relative_name)
158
+ ]
159
+ )
160
+
161
+ expect(LokaliseRails).to have_received(:skip_file_export).twice
162
+ end
163
+ end
164
+ end
165
+ end