lokalise_rails 0.0.2.3 → 1.1.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,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