localio 0.1.7 → 0.2.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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +28 -0
- data/.gitignore +4 -1
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile.lock +134 -0
- data/README.md +36 -34
- data/bin/localize +10 -7
- data/docs/plans/2026-02-23-modernization-design.md +91 -0
- data/docs/plans/2026-02-23-modernization.md +1699 -0
- data/docs/plans/2026-02-23-twine-writer-design.md +72 -0
- data/docs/plans/2026-02-23-twine-writer.md +267 -0
- data/lib/localio/localizable_writer.rb +4 -1
- data/lib/localio/processors/csv_processor.rb +1 -1
- data/lib/localio/processors/google_drive_processor.rb +19 -45
- data/lib/localio/processors/xlsx_processor.rb +1 -1
- data/lib/localio/template_handler.rb +3 -1
- data/lib/localio/templates/android_localizable.erb +14 -2
- data/lib/localio/templates/ios_constant_localizable.erb +16 -2
- data/lib/localio/templates/ios_localizable.erb +20 -5
- data/lib/localio/templates/java_properties_localizable.erb +16 -2
- data/lib/localio/templates/json_localizable.erb +6 -5
- data/lib/localio/templates/rails_localizable.erb +15 -3
- data/lib/localio/templates/resx_localizable.erb +14 -2
- data/lib/localio/templates/swift_constant_localizable.erb +15 -2
- data/lib/localio/version.rb +1 -1
- data/lib/localio/writers/ios_writer.rb +3 -3
- data/lib/localio/writers/swift_writer.rb +3 -3
- data/lib/localio/writers/twine_writer.rb +48 -0
- data/localio.gemspec +19 -25
- data/spec/fixtures/sample.csv +11 -0
- data/spec/localio/filter_spec.rb +40 -0
- data/spec/localio/formatter_spec.rb +32 -0
- data/spec/localio/processors/csv_processor_spec.rb +89 -0
- data/spec/localio/processors/google_drive_processor_spec.rb +107 -0
- data/spec/localio/processors/xls_processor_spec.rb +65 -0
- data/spec/localio/processors/xlsx_processor_spec.rb +59 -0
- data/spec/localio/segment_spec.rb +27 -0
- data/spec/localio/segments_list_holder_spec.rb +22 -0
- data/spec/localio/string_helper_spec.rb +49 -0
- data/spec/localio/template_handler_spec.rb +67 -0
- data/spec/localio/term_spec.rb +24 -0
- data/spec/localio/writers/android_writer_spec.rb +71 -0
- data/spec/localio/writers/ios_writer_spec.rb +63 -0
- data/spec/localio/writers/java_properties_writer_spec.rb +35 -0
- data/spec/localio/writers/json_writer_spec.rb +57 -0
- data/spec/localio/writers/rails_writer_spec.rb +47 -0
- data/spec/localio/writers/resx_writer_spec.rb +44 -0
- data/spec/localio/writers/swift_writer_spec.rb +42 -0
- data/spec/localio/writers/twine_writer_spec.rb +68 -0
- data/spec/localio_spec.rb +62 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/shared_terms.rb +35 -0
- metadata +61 -46
|
@@ -8,6 +8,19 @@ Created by localio
|
|
|
8
8
|
|
|
9
9
|
import Foundation
|
|
10
10
|
|
|
11
|
-
<%
|
|
12
|
-
|
|
11
|
+
<%
|
|
12
|
+
node_keys = []
|
|
13
|
+
@segments.each do |term|
|
|
14
|
+
if term.key == '[init-node]' or term.key == '[end-node]'
|
|
15
|
+
node_keys << term.translation if term.key == '[init-node]'
|
|
16
|
+
node_keys.pop if term.key == '[end-node]'
|
|
17
|
+
else
|
|
18
|
+
if node_keys.length() >0
|
|
19
|
+
key_join = node_keys.join("_").capitalize+"_"+term.key.downcase
|
|
20
|
+
else
|
|
21
|
+
key_join = term.key
|
|
22
|
+
end
|
|
23
|
+
%>
|
|
24
|
+
let kLocale<%= key_join %>: String = { return NSLocalizedString("_<%= key_join %>", comment: "") }()<%
|
|
25
|
+
end
|
|
13
26
|
end %>
|
data/lib/localio/version.rb
CHANGED
|
@@ -25,7 +25,7 @@ class IosWriter
|
|
|
25
25
|
|
|
26
26
|
unless term.is_comment?
|
|
27
27
|
constant_key = ios_constant_formatter term.keyword
|
|
28
|
-
constant_value =
|
|
28
|
+
constant_value = translation
|
|
29
29
|
constant_segment = Segment.new(constant_key, constant_value, lang)
|
|
30
30
|
constant_segments.segments << constant_segment
|
|
31
31
|
end
|
|
@@ -45,11 +45,11 @@ class IosWriter
|
|
|
45
45
|
private
|
|
46
46
|
|
|
47
47
|
def self.ios_key_formatter(key)
|
|
48
|
-
|
|
48
|
+
key.space_to_underscore.strip_tag.capitalize
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def self.ios_constant_formatter(key)
|
|
52
|
-
|
|
52
|
+
key.space_to_underscore.strip_tag.camel_case
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
end
|
|
@@ -25,7 +25,7 @@ class SwiftWriter
|
|
|
25
25
|
|
|
26
26
|
unless term.is_comment?
|
|
27
27
|
constant_key = swift_constant_formatter term.keyword
|
|
28
|
-
constant_value =
|
|
28
|
+
constant_value = translation
|
|
29
29
|
constant_segment = Segment.new(constant_key, constant_value, lang)
|
|
30
30
|
constant_segments.segments << constant_segment
|
|
31
31
|
end
|
|
@@ -44,10 +44,10 @@ class SwiftWriter
|
|
|
44
44
|
private
|
|
45
45
|
|
|
46
46
|
def self.swift_key_formatter(key)
|
|
47
|
-
|
|
47
|
+
key.space_to_underscore.strip_tag.capitalize
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def self.swift_constant_formatter(key)
|
|
51
|
-
|
|
51
|
+
key.space_to_underscore.strip_tag.camel_case
|
|
52
52
|
end
|
|
53
53
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'localio/formatter'
|
|
3
|
+
|
|
4
|
+
class TwineWriter
|
|
5
|
+
def self.write(languages, terms, path, formatter, options)
|
|
6
|
+
puts 'Writing Twine translations...'
|
|
7
|
+
|
|
8
|
+
default_language = options[:default_language]
|
|
9
|
+
output_filename = options[:output_file] || 'strings.txt'
|
|
10
|
+
|
|
11
|
+
FileUtils.mkdir_p(path)
|
|
12
|
+
|
|
13
|
+
File.open(File.join(path, output_filename), 'w') do |f|
|
|
14
|
+
pending_comment = nil
|
|
15
|
+
|
|
16
|
+
terms.each do |term|
|
|
17
|
+
if term.is_comment?
|
|
18
|
+
pending_comment = term.values[default_language]
|
|
19
|
+
elsif term.keyword == '[init-node]'
|
|
20
|
+
f.puts "[[#{term.values[default_language]}]]"
|
|
21
|
+
pending_comment = nil
|
|
22
|
+
elsif term.keyword == '[end-node]'
|
|
23
|
+
f.puts ''
|
|
24
|
+
pending_comment = nil
|
|
25
|
+
else
|
|
26
|
+
key = Formatter.format(term.keyword, formatter, method(:twine_key_formatter))
|
|
27
|
+
f.puts "\t[#{key}]"
|
|
28
|
+
languages.keys.each do |lang|
|
|
29
|
+
f.puts "\t\t#{lang} = #{term.values[lang]}"
|
|
30
|
+
end
|
|
31
|
+
if pending_comment
|
|
32
|
+
f.puts "\t\tcomment = #{pending_comment}"
|
|
33
|
+
pending_comment = nil
|
|
34
|
+
end
|
|
35
|
+
f.puts ''
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
puts " > #{output_filename.yellow}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def self.twine_key_formatter(key)
|
|
46
|
+
key.space_to_underscore.strip_tag.downcase
|
|
47
|
+
end
|
|
48
|
+
end
|
data/localio.gemspec
CHANGED
|
@@ -1,35 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
3
|
require 'localio/version'
|
|
5
4
|
|
|
6
5
|
Gem::Specification.new do |spec|
|
|
7
|
-
spec.name
|
|
8
|
-
spec.version
|
|
9
|
-
spec.authors
|
|
10
|
-
spec.email
|
|
11
|
-
spec.description
|
|
12
|
-
spec.summary
|
|
13
|
-
spec.homepage
|
|
14
|
-
spec.license
|
|
6
|
+
spec.name = "localio"
|
|
7
|
+
spec.version = Localio::VERSION
|
|
8
|
+
spec.authors = ["Nacho Lopez"]
|
|
9
|
+
spec.email = ["nacho@nlopez.io"]
|
|
10
|
+
spec.description = %q{Automatic Localizable file generation for multiple platforms}
|
|
11
|
+
spec.summary = %q{Generates Android, iOS, Rails, JSON, Java Properties, and .NET ResX localization files from spreadsheet sources.}
|
|
12
|
+
spec.homepage = "https://github.com/mrmans0n/localio"
|
|
13
|
+
spec.license = "MIT"
|
|
15
14
|
|
|
16
|
-
spec.files
|
|
17
|
-
spec.executables
|
|
18
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
15
|
+
spec.files = `git ls-files`.split("\n")
|
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
19
17
|
spec.require_paths = ["lib"]
|
|
20
18
|
|
|
21
|
-
spec.
|
|
19
|
+
spec.required_ruby_version = ">= 3.2"
|
|
22
20
|
|
|
23
|
-
spec.add_development_dependency "rspec"
|
|
21
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
22
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
24
23
|
|
|
25
|
-
spec.
|
|
26
|
-
|
|
27
|
-
spec.
|
|
28
|
-
spec.
|
|
29
|
-
|
|
30
|
-
spec.add_dependency "micro-optparse", "~> 1.2"
|
|
31
|
-
spec.add_dependency "google_drive", "~> 1.0"
|
|
32
|
-
spec.add_dependency "spreadsheet", "~> 1.0"
|
|
33
|
-
spec.add_dependency "simple_xlsx_reader", "~> 1.0"
|
|
34
|
-
spec.add_dependency "nokogiri", "~> 1.6"
|
|
24
|
+
spec.add_dependency "google_drive", "~> 3.0"
|
|
25
|
+
spec.add_dependency "spreadsheet", "~> 1.3"
|
|
26
|
+
spec.add_dependency "simple_xlsx_reader", "~> 2.0"
|
|
27
|
+
spec.add_dependency "nokogiri", "~> 1.16"
|
|
28
|
+
spec.add_dependency "csv", "~> 3.2"
|
|
35
29
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Title Row,,,
|
|
2
|
+
[key],*en,es,fr
|
|
3
|
+
[comment],Section General,Section General,Section General
|
|
4
|
+
app_name,My App,Mi Aplicación,Mon Application
|
|
5
|
+
greeting,Hello %@ world,Hola %@ mundo,Bonjour %@ monde
|
|
6
|
+
dots_test,Wait...,Espera...,Attendez...
|
|
7
|
+
ampersand_test,Tom & Jerry,Tom & Jerry,Tom & Jerry
|
|
8
|
+
[init-node],module,module,module
|
|
9
|
+
nested_key,Module Key,Clave Módulo,Clé Module
|
|
10
|
+
[end-node],end,end,end
|
|
11
|
+
[end],,,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require 'localio/term'
|
|
2
|
+
require 'localio/filter'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Filter do
|
|
5
|
+
let(:terms) do
|
|
6
|
+
['app_name', 'app_title', 'settings_title', 'settings_back', '[comment]'].map { |kw| Term.new(kw) }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
describe '.apply_filter' do
|
|
10
|
+
it 'returns all terms when no filters set' do
|
|
11
|
+
expect(Filter.apply_filter(terms, nil, nil)).to eq(terms)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
context 'with only filter' do
|
|
15
|
+
it 'keeps terms matching the regex' do
|
|
16
|
+
result = Filter.apply_filter(terms, { keys: 'app_' }, nil)
|
|
17
|
+
expect(result.map(&:keyword)).to contain_exactly('app_name', 'app_title')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'returns empty array when nothing matches' do
|
|
21
|
+
expect(Filter.apply_filter(terms, { keys: 'nonexistent' }, nil)).to be_empty
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context 'with except filter' do
|
|
26
|
+
it 'excludes terms matching the regex' do
|
|
27
|
+
result = Filter.apply_filter(terms, nil, { keys: 'settings_' })
|
|
28
|
+
expect(result.map(&:keyword)).not_to include('settings_title', 'settings_back')
|
|
29
|
+
expect(result.map(&:keyword)).to include('app_name', 'app_title')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context 'with both filters' do
|
|
34
|
+
it 'applies only first then except' do
|
|
35
|
+
result = Filter.apply_filter(terms, { keys: 'app_' }, { keys: 'title' })
|
|
36
|
+
expect(result.map(&:keyword)).to contain_exactly('app_name')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'localio/string_helper'
|
|
2
|
+
require 'localio/formatter'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Formatter do
|
|
5
|
+
let(:smart_callback) { ->(key) { key.upcase } }
|
|
6
|
+
|
|
7
|
+
describe '.format' do
|
|
8
|
+
it ':smart delegates to callback' do
|
|
9
|
+
expect(Formatter.format('hello', :smart, smart_callback)).to eq('HELLO')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it ':none returns key unchanged' do
|
|
13
|
+
expect(Formatter.format('Hello World', :none, smart_callback)).to eq('Hello World')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it ':camel_case converts to CamelCase' do
|
|
17
|
+
expect(Formatter.format('hello world', :camel_case, smart_callback)).to eq('HelloWorld')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it ':camel_case strips single-letter bracket tags' do
|
|
21
|
+
expect(Formatter.format('[a]hello', :camel_case, smart_callback)).to eq('Hello')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it ':snake_case converts spaces to underscores and downcases' do
|
|
25
|
+
expect(Formatter.format('Hello World', :snake_case, smart_callback)).to eq('hello_world')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'raises ArgumentError for unknown formatter' do
|
|
29
|
+
expect { Formatter.format('key', :unknown, smart_callback) }.to raise_error(ArgumentError)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require 'localio/term'
|
|
2
|
+
require 'localio/string_helper'
|
|
3
|
+
require 'localio/processors/csv_processor'
|
|
4
|
+
|
|
5
|
+
RSpec.describe CsvProcessor do
|
|
6
|
+
let(:fixture_path) { File.expand_path('../../../fixtures/sample.csv', __FILE__) }
|
|
7
|
+
let(:platform_options) { {} }
|
|
8
|
+
let(:options) { { path: fixture_path } }
|
|
9
|
+
|
|
10
|
+
describe '.load_localizables' do
|
|
11
|
+
subject(:result) { CsvProcessor.load_localizables(platform_options, options) }
|
|
12
|
+
|
|
13
|
+
it 'returns languages en, es, fr' do
|
|
14
|
+
expect(result[:languages].keys).to contain_exactly('en', 'es', 'fr')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'sets en as default language (marked with *)' do
|
|
18
|
+
expect(result[:default_language]).to eq('en')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'returns 8 terms between [key] and [end]' do
|
|
22
|
+
expect(result[:segments].count).to eq(8)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'parses term keywords correctly' do
|
|
26
|
+
keywords = result[:segments].map(&:keyword)
|
|
27
|
+
expect(keywords).to include('app_name', 'greeting', '[comment]', '[init-node]', '[end-node]')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'parses translations for each language' do
|
|
31
|
+
app_name = result[:segments].find { |t| t.keyword == 'app_name' }
|
|
32
|
+
expect(app_name.values['en']).to eq('My App')
|
|
33
|
+
expect(app_name.values['es']).to eq('Mi Aplicación')
|
|
34
|
+
expect(app_name.values['fr']).to eq('Mon Application')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'identifies comment rows' do
|
|
38
|
+
comment = result[:segments].find { |t| t.keyword == '[comment]' }
|
|
39
|
+
expect(comment.is_comment?).to be true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'raises ArgumentError when :path is missing' do
|
|
43
|
+
expect { CsvProcessor.load_localizables({}, {}) }
|
|
44
|
+
.to raise_error(ArgumentError, /:path attribute is missing/)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'raises IndexError when [key] marker is missing' do
|
|
48
|
+
Dir.mktmpdir do |tmpdir|
|
|
49
|
+
path = File.join(tmpdir, 'bad.csv')
|
|
50
|
+
File.write(path, "no,key,row\ndata,here,\n[end],,,\n")
|
|
51
|
+
expect { CsvProcessor.load_localizables({}, { path: path }) }
|
|
52
|
+
.to raise_error(IndexError, /Could not find any \[key\]/)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'raises IndexError when [end] marker is missing' do
|
|
57
|
+
Dir.mktmpdir do |tmpdir|
|
|
58
|
+
path = File.join(tmpdir, 'bad.csv')
|
|
59
|
+
File.write(path, "title,,,\n[key],*en,es,\ndata,val,val,\n")
|
|
60
|
+
expect { CsvProcessor.load_localizables({}, { path: path }) }
|
|
61
|
+
.to raise_error(IndexError, /Could not find any \[end\]/)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
context 'with override_default option' do
|
|
66
|
+
let(:platform_options) { { override_default: 'es' } }
|
|
67
|
+
|
|
68
|
+
it 'uses the overridden default language' do
|
|
69
|
+
expect(result[:default_language]).to eq('es')
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
context 'with avoid_lang_downcase option' do
|
|
74
|
+
let(:tmpdir) { Dir.mktmpdir }
|
|
75
|
+
let(:options) do
|
|
76
|
+
path = File.join(tmpdir, 'upper.csv')
|
|
77
|
+
File.write(path, "Title,,,\n[key],*EN,ES,FR\napp_name,My App,Mi App,Mon App\n[end],,,\n")
|
|
78
|
+
{ path: path }
|
|
79
|
+
end
|
|
80
|
+
let(:platform_options) { { avoid_lang_downcase: true } }
|
|
81
|
+
|
|
82
|
+
after { FileUtils.rm_rf(tmpdir) }
|
|
83
|
+
|
|
84
|
+
it 'preserves language case' do
|
|
85
|
+
expect(result[:languages].keys).to contain_exactly('EN', 'ES', 'FR')
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# google_drive transitively loads nokogiri. Nokogiri is now built for arm64
|
|
2
|
+
# and loads correctly, so we no longer need to stub it out. We only stub
|
|
3
|
+
# google_drive itself (which requires OAuth flow and network access) by:
|
|
4
|
+
# 1. Defining minimal stub modules for GoogleDrive before anything tries to
|
|
5
|
+
# reference them.
|
|
6
|
+
# 2. Pre-populating $LOADED_FEATURES with the google_drive gem paths so that
|
|
7
|
+
# every subsequent `require 'google_drive'` is treated as already loaded.
|
|
8
|
+
# All runtime calls are intercepted by RSpec doubles.
|
|
9
|
+
|
|
10
|
+
module GoogleDrive
|
|
11
|
+
module Session
|
|
12
|
+
def self.from_config(_config, _opts = {}); end
|
|
13
|
+
def self.from_service_account_key(_path, _scope = nil); end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
_gd_spec = Gem::Specification.find_by_name('google_drive')
|
|
19
|
+
_gd_base = File.join(_gd_spec.gem_dir, 'lib')
|
|
20
|
+
Dir["#{_gd_base}/**/*.rb"].sort.each do |f|
|
|
21
|
+
$LOADED_FEATURES << f unless $LOADED_FEATURES.include?(f)
|
|
22
|
+
end
|
|
23
|
+
["#{_gd_base}/google_drive.rb"].each do |f|
|
|
24
|
+
$LOADED_FEATURES << f unless $LOADED_FEATURES.include?(f)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
require 'localio/term'
|
|
29
|
+
require 'localio/string_helper'
|
|
30
|
+
require 'localio/processors/google_drive_processor'
|
|
31
|
+
|
|
32
|
+
RSpec.describe GoogleDriveProcessor do
|
|
33
|
+
let(:ws_data) do
|
|
34
|
+
{
|
|
35
|
+
[1, 1] => '[key]', [1, 2] => '*en', [1, 3] => 'es',
|
|
36
|
+
[2, 1] => 'app_name', [2, 2] => 'My App', [2, 3] => 'Mi Aplicación',
|
|
37
|
+
[3, 1] => 'greeting', [3, 2] => 'Hello', [3, 3] => 'Hola',
|
|
38
|
+
[4, 1] => '[end]', [4, 2] => '', [4, 3] => '',
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
let(:worksheet) do
|
|
43
|
+
ws = double('worksheet')
|
|
44
|
+
allow(ws).to receive(:[]) { |row, col| ws_data[[row, col]].to_s }
|
|
45
|
+
allow(ws).to receive(:max_rows).and_return(4)
|
|
46
|
+
allow(ws).to receive(:max_cols).and_return(3)
|
|
47
|
+
ws
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
let(:spreadsheet_double) { double('spreadsheet', title: 'My Translations') }
|
|
51
|
+
let(:session_double) { double('session') }
|
|
52
|
+
|
|
53
|
+
before do
|
|
54
|
+
allow(spreadsheet_double).to receive(:worksheets).and_return([worksheet])
|
|
55
|
+
allow(session_double).to receive(:spreadsheets).and_return([spreadsheet_double])
|
|
56
|
+
|
|
57
|
+
# Mock the google_drive 3.x session creation method used by the processor.
|
|
58
|
+
# from_config is used for the OAuth2 client_id/client_secret flow.
|
|
59
|
+
allow(GoogleDrive::Session).to receive(:from_config).and_return(session_double)
|
|
60
|
+
|
|
61
|
+
# Allow File.file? to return false for the :client_token option in OAuth tests
|
|
62
|
+
# so the processor takes the from_config path (not the service-account path).
|
|
63
|
+
allow(File).to receive(:file?).and_call_original
|
|
64
|
+
allow(File).to receive(:file?).with('token').and_return(false)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
let(:options) do
|
|
68
|
+
{ spreadsheet: 'My Translations', client_id: 'id', client_secret: 'secret', client_token: 'token', sheet: 0 }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '.load_localizables' do
|
|
72
|
+
subject(:result) { GoogleDriveProcessor.load_localizables({}, options) }
|
|
73
|
+
|
|
74
|
+
it 'raises when :spreadsheet is missing' do
|
|
75
|
+
expect { GoogleDriveProcessor.load_localizables({}, { client_id: 'a', client_secret: 'b' }) }
|
|
76
|
+
.to raise_error(ArgumentError, /:spreadsheet required/)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'raises when :login is provided (deprecated)' do
|
|
80
|
+
expect { GoogleDriveProcessor.load_localizables({}, { spreadsheet: 'x', login: 'u', client_id: 'a', client_secret: 'b' }) }
|
|
81
|
+
.to raise_error(ArgumentError, /:login is deprecated/)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'raises when :client_id is missing' do
|
|
85
|
+
expect { GoogleDriveProcessor.load_localizables({}, { spreadsheet: 'x', client_secret: 'b' }) }
|
|
86
|
+
.to raise_error(ArgumentError, /:client_id required/)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'raises when :client_secret is missing' do
|
|
90
|
+
expect { GoogleDriveProcessor.load_localizables({}, { spreadsheet: 'x', client_id: 'a' }) }
|
|
91
|
+
.to raise_error(ArgumentError, /:client_secret required/)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'returns languages en and es' do
|
|
95
|
+
expect(result[:languages].keys).to contain_exactly('en', 'es')
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'sets en as default language' do
|
|
99
|
+
expect(result[:default_language]).to eq('en')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'parses translations' do
|
|
103
|
+
app_name = result[:segments].find { |t| t.keyword == 'app_name' }
|
|
104
|
+
expect(app_name.values['en']).to eq('My App')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require 'localio/term'
|
|
2
|
+
require 'localio/string_helper'
|
|
3
|
+
require 'localio/processors/xls_processor'
|
|
4
|
+
|
|
5
|
+
RSpec.describe XlsProcessor do
|
|
6
|
+
let(:data) do
|
|
7
|
+
{
|
|
8
|
+
[0, 0] => '[key]', [0, 1] => '*en', [0, 2] => 'es', [0, 3] => 'fr',
|
|
9
|
+
[1, 0] => 'app_name', [1, 1] => 'My App', [1, 2] => 'Mi Aplicación', [1, 3] => 'Mon Application',
|
|
10
|
+
[2, 0] => 'greeting', [2, 1] => 'Hello', [2, 2] => 'Hola', [2, 3] => 'Bonjour',
|
|
11
|
+
[3, 0] => '[end]', [3, 1] => '', [3, 2] => '', [3, 3] => '',
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
let(:worksheet) do
|
|
16
|
+
ws = double('worksheet')
|
|
17
|
+
allow(ws).to receive(:[]) { |row, col| data[[row, col]].to_s }
|
|
18
|
+
allow(ws).to receive(:row_count).and_return(3)
|
|
19
|
+
allow(ws).to receive(:column_count).and_return(3)
|
|
20
|
+
ws
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
let(:book_double) { double('book') }
|
|
24
|
+
|
|
25
|
+
before do
|
|
26
|
+
allow(Spreadsheet).to receive(:client_encoding=)
|
|
27
|
+
allow(Spreadsheet).to receive(:open).and_return(book_double)
|
|
28
|
+
allow(book_double).to receive(:worksheet).with(0).and_return(worksheet)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
let(:options) { { path: 'fake.xls' } }
|
|
32
|
+
let(:platform_options) { {} }
|
|
33
|
+
|
|
34
|
+
describe '.load_localizables' do
|
|
35
|
+
subject(:result) { XlsProcessor.load_localizables(platform_options, options) }
|
|
36
|
+
|
|
37
|
+
it 'raises ArgumentError when :path is missing' do
|
|
38
|
+
expect { XlsProcessor.load_localizables({}, {}) }
|
|
39
|
+
.to raise_error(ArgumentError, /:path attribute is missing/)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'returns languages en, es, fr' do
|
|
43
|
+
expect(result[:languages].keys).to contain_exactly('en', 'es', 'fr')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'sets en as default language' do
|
|
47
|
+
expect(result[:default_language]).to eq('en')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'returns 2 terms' do
|
|
51
|
+
expect(result[:segments].count).to eq(2)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'parses translations correctly' do
|
|
55
|
+
app_name = result[:segments].find { |t| t.keyword == 'app_name' }
|
|
56
|
+
expect(app_name.values['en']).to eq('My App')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'raises when worksheet is nil' do
|
|
60
|
+
allow(book_double).to receive(:worksheet).and_return(nil)
|
|
61
|
+
expect { XlsProcessor.load_localizables({}, { path: 'fake.xls' }) }
|
|
62
|
+
.to raise_error(RuntimeError, /Unable to retrieve/)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require 'localio/term'
|
|
2
|
+
require 'localio/string_helper'
|
|
3
|
+
require 'localio/processors/xlsx_processor'
|
|
4
|
+
|
|
5
|
+
RSpec.describe XlsxProcessor do
|
|
6
|
+
let(:rows) do
|
|
7
|
+
[
|
|
8
|
+
['Title', nil, nil, nil],
|
|
9
|
+
['[key]', '*en', 'es', 'fr'],
|
|
10
|
+
['[comment]', 'Section General', 'Section General', 'Section General'],
|
|
11
|
+
['app_name', 'My App', 'Mi Aplicación', 'Mon Application'],
|
|
12
|
+
['greeting', 'Hello', 'Hola', 'Bonjour'],
|
|
13
|
+
['[end]', nil, nil, nil],
|
|
14
|
+
]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
let(:sheet_double) { double('sheet', rows: rows) }
|
|
18
|
+
let(:book_double) { double('book', sheets: [sheet_double]) }
|
|
19
|
+
|
|
20
|
+
before { allow(SimpleXlsxReader).to receive(:open).and_return(book_double) }
|
|
21
|
+
|
|
22
|
+
let(:options) { { path: 'fake.xlsx', sheet: 0 } }
|
|
23
|
+
let(:platform_options) { {} }
|
|
24
|
+
|
|
25
|
+
describe '.load_localizables' do
|
|
26
|
+
subject(:result) { XlsxProcessor.load_localizables(platform_options, options) }
|
|
27
|
+
|
|
28
|
+
it 'raises ArgumentError when :path is missing' do
|
|
29
|
+
expect { XlsxProcessor.load_localizables({}, {}) }
|
|
30
|
+
.to raise_error(ArgumentError, /:path attribute is missing/)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'returns languages en, es, fr' do
|
|
34
|
+
expect(result[:languages].keys).to contain_exactly('en', 'es', 'fr')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'sets en as default language' do
|
|
38
|
+
expect(result[:default_language]).to eq('en')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'returns 3 terms' do
|
|
42
|
+
expect(result[:segments].count).to eq(3)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'parses translations correctly' do
|
|
46
|
+
app_name = result[:segments].find { |t| t.keyword == 'app_name' }
|
|
47
|
+
expect(app_name.values['en']).to eq('My App')
|
|
48
|
+
expect(app_name.values['es']).to eq('Mi Aplicación')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'raises when sheet is nil' do
|
|
52
|
+
named_book = double('book', sheets: [])
|
|
53
|
+
allow(SimpleXlsxReader).to receive(:open).and_return(named_book)
|
|
54
|
+
allow(named_book).to receive(:sheets).and_return([])
|
|
55
|
+
expect { XlsxProcessor.load_localizables({}, { path: 'fake.xlsx', sheet: 'Missing' }) }
|
|
56
|
+
.to raise_error(RuntimeError, /Unable to retrieve/)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'localio/string_helper'
|
|
2
|
+
require 'localio/segment'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Segment do
|
|
5
|
+
subject(:segment) { Segment.new('app_name', 'My App', 'en') }
|
|
6
|
+
|
|
7
|
+
it 'stores key, translation, and language' do
|
|
8
|
+
expect(segment.key).to eq('app_name')
|
|
9
|
+
expect(segment.translation).to eq('My App')
|
|
10
|
+
expect(segment.language).to eq('en')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'processes translation through replace_escaped' do
|
|
14
|
+
seg = Segment.new('key', 'hello`+world', 'en')
|
|
15
|
+
expect(seg.translation).to eq('hello+world')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '#is_comment?' do
|
|
19
|
+
it 'returns true when key is nil' do
|
|
20
|
+
segment.key = nil
|
|
21
|
+
expect(segment.is_comment?).to be true
|
|
22
|
+
end
|
|
23
|
+
it 'returns false when key is set' do
|
|
24
|
+
expect(segment.is_comment?).to be false
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'localio/segments_list_holder'
|
|
2
|
+
|
|
3
|
+
RSpec.describe SegmentsListHolder do
|
|
4
|
+
subject(:holder) { SegmentsListHolder.new('en') }
|
|
5
|
+
|
|
6
|
+
it { expect(holder.language).to eq('en') }
|
|
7
|
+
it { expect(holder.segments).to be_empty }
|
|
8
|
+
|
|
9
|
+
describe '#get_binding' do
|
|
10
|
+
it 'returns a Binding' do
|
|
11
|
+
expect(holder.get_binding).to be_a(Binding)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'exposes @language in the binding' do
|
|
15
|
+
expect(eval('@language', holder.get_binding)).to eq('en')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'exposes @segments in the binding' do
|
|
19
|
+
expect(eval('@segments', holder.get_binding)).to eq([])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|