lokalise_rails 0.1.0 → 1.2.0

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.
@@ -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.1.0'
3
+ module LokaliseRails
4
+ VERSION = '1.2.0'
5
5
  end
@@ -1,14 +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'
4
+ require "#{Rails.root}/config/lokalise_rails"
8
5
 
9
6
  namespace :lokalise_rails do
10
7
  task :import do
11
- msg = LokaliseRails::TaskDefinition::Importer.import!
12
- $stdout.print msg
8
+ LokaliseRails::TaskDefinition::Importer.import!
9
+ end
10
+
11
+ task :export do
12
+ LokaliseRails::TaskDefinition::Exporter.export!
13
13
  end
14
14
  end
@@ -35,11 +35,9 @@ Gem::Specification.new do |spec|
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.0.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"
@@ -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,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