bisu 1.2.3 → 1.2.4
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 +4 -4
- data/.gitignore +1 -0
- data/.rspec +5 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +44 -0
- data/lib/bisu.rb +45 -13
- data/lib/bisu/dictionary.rb +43 -0
- data/lib/bisu/google_sheet.rb +63 -0
- data/lib/bisu/localizer.rb +74 -0
- data/lib/bisu/version.rb +2 -2
- data/spec/bisu/config_spec.rb +35 -0
- data/spec/bisu/dictionary_spec.rb +69 -0
- data/spec/bisu/google_sheet_spec.rb +61 -0
- data/spec/bisu/localizer_spec.rb +149 -0
- data/spec/bisu/logger_spec.rb +36 -0
- data/{test/support → spec/fixtures}/sample_kb_public_info.html +0 -0
- data/{test/support → spec/fixtures}/sample_kb_public_sheet.html +0 -0
- data/{test/support → spec/fixtures}/sample_translatable.yml +0 -0
- data/spec/spec_helper.rb +116 -0
- metadata +29 -23
- data/lib/bisu/knowledge_base.rb +0 -73
- data/lib/bisu/translator.rb +0 -105
- data/test/test_bisu_config.rb +0 -26
- data/test/test_bisu_knowledge_base.rb +0 -42
- data/test/test_bisu_logger.rb +0 -22
- data/test/test_bisu_translator.rb +0 -121
- data/test/test_helper.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91a21fb208a5322f64116f20e4b82e6108085b8d
|
4
|
+
data.tar.gz: 9a8b0eef2a85596da38cc1e51da31f699d5c6f94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc959379b26785e7f50b44461908985ef491eb94d6e765390145ca3416c130305c3ba7877837f9fbafefa9d149bd9816e72bf6d82c92dbc97c0cc9fe5bde58b3
|
7
|
+
data.tar.gz: faa8f17d79bf8579a6a5810e858f19e5adfff2635a213429e641ecbd1f8fe1e8373d27491f10182568b71859abe0349aa3a59c7ddb3c024e264fda38498b409b
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.ruby-version
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
addressable (2.4.0)
|
5
|
+
colorize (0.8.1)
|
6
|
+
crack (0.4.3)
|
7
|
+
safe_yaml (~> 1.0.0)
|
8
|
+
diff-lcs (1.2.5)
|
9
|
+
hashdiff (0.3.0)
|
10
|
+
rspec (3.5.0)
|
11
|
+
rspec-core (~> 3.5.0)
|
12
|
+
rspec-expectations (~> 3.5.0)
|
13
|
+
rspec-mocks (~> 3.5.0)
|
14
|
+
rspec-core (3.5.4)
|
15
|
+
rspec-support (~> 3.5.0)
|
16
|
+
rspec-expectations (3.5.0)
|
17
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
18
|
+
rspec-support (~> 3.5.0)
|
19
|
+
rspec-mocks (3.5.0)
|
20
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
21
|
+
rspec-support (~> 3.5.0)
|
22
|
+
rspec-support (3.5.0)
|
23
|
+
safe_yaml (1.0.4)
|
24
|
+
webmock (1.24.6)
|
25
|
+
addressable (>= 2.3.6)
|
26
|
+
crack (>= 0.3.2)
|
27
|
+
hashdiff
|
28
|
+
xml-simple (1.1.5)
|
29
|
+
|
30
|
+
PLATFORMS
|
31
|
+
ruby
|
32
|
+
|
33
|
+
DEPENDENCIES
|
34
|
+
colorize (~> 0.7)
|
35
|
+
rspec
|
36
|
+
safe_yaml (~> 1.0)
|
37
|
+
webmock (~> 1.20)
|
38
|
+
xml-simple (~> 1.1)
|
39
|
+
|
40
|
+
RUBY VERSION
|
41
|
+
ruby 2.3.1p112
|
42
|
+
|
43
|
+
BUNDLED WITH
|
44
|
+
1.12.5
|
data/lib/bisu.rb
CHANGED
@@ -1,23 +1,30 @@
|
|
1
1
|
require 'bisu/logger'
|
2
2
|
require 'bisu/config'
|
3
|
-
require 'bisu/
|
4
|
-
require 'bisu/
|
3
|
+
require 'bisu/google_sheet'
|
4
|
+
require 'bisu/dictionary'
|
5
|
+
require 'bisu/localizer'
|
5
6
|
require 'bisu/version'
|
6
7
|
require 'optparse'
|
7
8
|
|
8
9
|
module Bisu
|
9
10
|
extend self
|
10
11
|
|
11
|
-
def run(
|
12
|
-
options = command_line_options(
|
12
|
+
def run(options)
|
13
|
+
options = command_line_options(options)
|
13
14
|
|
14
15
|
if config = Bisu::Config.parse("translatable.yml")
|
15
|
-
|
16
|
-
|
16
|
+
google_sheet = Bisu::GoogleSheet.new(config[:sheet_id], config[:keys_column])
|
17
|
+
dictionary = Bisu::Dictionary.new(google_sheet.to_hash)
|
18
|
+
localizer = Bisu::Localizer.new(dictionary, config[:type])
|
17
19
|
|
18
20
|
config[:in].each do |in_path|
|
19
21
|
config[:out].each do |out|
|
20
|
-
|
22
|
+
unless dictionary.has_language?(out[:kb_language])
|
23
|
+
Logger.error("Unknown language #{out[:kb_language]}")
|
24
|
+
return false
|
25
|
+
end
|
26
|
+
|
27
|
+
localize_file(localizer, out[:locale], out[:kb_language], options[:default_language], in_path, out[:path] || config[:out_path])
|
21
28
|
end
|
22
29
|
end
|
23
30
|
end
|
@@ -28,11 +35,11 @@ module Bisu
|
|
28
35
|
private
|
29
36
|
|
30
37
|
def command_line_options(options)
|
31
|
-
|
38
|
+
opts_hash = {}
|
32
39
|
|
33
40
|
opts_parser = OptionParser.new do |opts|
|
34
41
|
opts.on("-d LANGUAGE", "--default LANGUAGE", "Language to use when there is no available translation") do |language|
|
35
|
-
|
42
|
+
opts_hash[:default_language] = language
|
36
43
|
end
|
37
44
|
|
38
45
|
opts.on_tail("-h", "--help", "Show this message") do
|
@@ -45,12 +52,12 @@ module Bisu
|
|
45
52
|
exit
|
46
53
|
end
|
47
54
|
end
|
55
|
+
opts_parser.parse!(options)
|
48
56
|
|
49
|
-
|
50
|
-
options
|
57
|
+
opts_hash
|
51
58
|
end
|
52
59
|
|
53
|
-
def
|
60
|
+
def localize_file(localizer, locale, language, default_language, in_path, out_path)
|
54
61
|
in_name = File.basename(in_path)
|
55
62
|
out_name = in_name.gsub(/\.translatable$/, "")
|
56
63
|
|
@@ -61,7 +68,32 @@ module Bisu
|
|
61
68
|
|
62
69
|
out_path = out_path % { locale: locale, android_locale: locale.gsub("-", "-r"), out_name: out_name }
|
63
70
|
|
64
|
-
|
71
|
+
return false unless in_file = open_file(in_path, "r", true)
|
72
|
+
return false unless out_file = open_file(out_path, "w", false)
|
73
|
+
|
74
|
+
Logger.info("Translating #{in_path} to #{language} > #{out_path}...")
|
75
|
+
|
76
|
+
in_file.each_line do |line|
|
77
|
+
out_file.write(localizer.localize(line, language, locale, default_language))
|
78
|
+
end
|
79
|
+
|
80
|
+
out_file.flush
|
81
|
+
out_file.close
|
82
|
+
in_file.close
|
83
|
+
|
84
|
+
true
|
65
85
|
end
|
66
86
|
|
87
|
+
def open_file(file_name, method, should_exist)
|
88
|
+
if !File.file?(File.expand_path(file_name))
|
89
|
+
if should_exist
|
90
|
+
Logger.error("File #{file_name} not found!")
|
91
|
+
return nil
|
92
|
+
else
|
93
|
+
FileUtils.mkdir_p(File.dirname(file_name))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
File.open(File.expand_path(file_name), method)
|
98
|
+
end
|
67
99
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Bisu
|
2
|
+
class Dictionary
|
3
|
+
def initialize(keys)
|
4
|
+
unless keys.is_a?(Hash)
|
5
|
+
raise ArgumentError.new("keys: expected Hash")
|
6
|
+
end
|
7
|
+
|
8
|
+
keys.each do |key,v|
|
9
|
+
unless v.is_a?(Hash)
|
10
|
+
raise ArgumentError.new("keys['#{key}']: expected Hash")
|
11
|
+
end
|
12
|
+
|
13
|
+
v.each do |lang,v|
|
14
|
+
unless v.is_a?(String) || v.nil?
|
15
|
+
raise ArgumentError.new("keys['#{key}']['#{lang}']: expected String")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
@keys = keys
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_language?(language)
|
24
|
+
languages.include?(language)
|
25
|
+
end
|
26
|
+
|
27
|
+
def localize(key, language)
|
28
|
+
if locals = @keys[key]
|
29
|
+
locals[language]
|
30
|
+
else
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def languages
|
38
|
+
@languages ||= begin
|
39
|
+
@keys.map { |_,v| v.keys }.flatten.uniq
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "net/https"
|
2
|
+
require "xmlsimple"
|
3
|
+
|
4
|
+
module Bisu
|
5
|
+
class GoogleSheet
|
6
|
+
def initialize(sheet_id, keys_column_title)
|
7
|
+
@sheet_id = sheet_id
|
8
|
+
@key_column = keys_column_title
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_hash
|
12
|
+
raw = raw_data(@sheet_id)
|
13
|
+
|
14
|
+
Logger.info("Parsing Google Sheet...")
|
15
|
+
|
16
|
+
remove = ["id", "updated", "category", "title", "content", "link", @key_column]
|
17
|
+
|
18
|
+
kb = {}
|
19
|
+
raw["entry"].each do |entry|
|
20
|
+
hash = entry.select { |d| !remove.include?(d) }
|
21
|
+
hash = hash.each.map { |k, v| v.first == {} ? [k, nil] : [k, v.first] }
|
22
|
+
|
23
|
+
unless (key = entry[@key_column]) && key = key.first
|
24
|
+
raise "Cannot find key column '#{@key_column}'"
|
25
|
+
end
|
26
|
+
|
27
|
+
kb[key] = Hash[hash]
|
28
|
+
end
|
29
|
+
|
30
|
+
Logger.info("Google Sheet parsed successfully!")
|
31
|
+
Logger.info("Found #{kb.count} keys in #{kb.values.first.keys.count} languages.")
|
32
|
+
|
33
|
+
kb
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def xml_data(uri, headers=nil)
|
39
|
+
uri = URI.parse(uri)
|
40
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
41
|
+
http.use_ssl = true
|
42
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
43
|
+
data = http.get(uri.path, headers)
|
44
|
+
|
45
|
+
unless data.code.to_i == 200
|
46
|
+
raise "Cannot access sheet at #{uri} - HTTP #{data.code}"
|
47
|
+
end
|
48
|
+
|
49
|
+
begin
|
50
|
+
XmlSimple.xml_in(data.body, 'KeyAttr' => 'name')
|
51
|
+
rescue
|
52
|
+
raise "Cannot parse. Expected XML at #{uri}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def raw_data(sheet_id)
|
57
|
+
Logger.info("Downloading Google Sheet...")
|
58
|
+
sheet = xml_data("https://spreadsheets.google.com/feeds/worksheets/#{sheet_id}/public/full")
|
59
|
+
url = sheet["entry"][0]["link"][0]["href"]
|
60
|
+
xml_data(url)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Bisu
|
2
|
+
class Localizer
|
3
|
+
def initialize(dictionary, type)
|
4
|
+
@dict = dictionary
|
5
|
+
@type = type.downcase.to_sym
|
6
|
+
|
7
|
+
unless [:ios, :android, :ror].include?(@type)
|
8
|
+
Logger.error("Unknown type #{@type}")
|
9
|
+
raise "Unknown type #{@type}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def localize(text, language, locale, default_language=nil)
|
14
|
+
t = text
|
15
|
+
t = t.gsub("$specialKLanguage$", language)
|
16
|
+
t = t.gsub("$specialKLocale$", locale)
|
17
|
+
t = t.gsub("$specialKComment1$", "This file was automatically generated based on a translation template.")
|
18
|
+
t = t.gsub("$specialKComment2$", "Remember to CHANGE THE TEMPLATE and not this file!")
|
19
|
+
|
20
|
+
to_localize(t).map do |l|
|
21
|
+
if localized = @dict.localize(l[:key], language) || @dict.localize(l[:key], default_language)
|
22
|
+
l[:params].each do |param, value|
|
23
|
+
if localized.match("%{#{param}}")
|
24
|
+
localized = localized.gsub("%{#{param}}", value)
|
25
|
+
else
|
26
|
+
Logger.error("Parameter #{param} not found in translation for #{l[:key]} in #{language}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
t = t.gsub(l[:match], process(localized))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
t.scan(/\$(k[^\$\{]+)(?:\{(.+)\})?\$/) { |match| Logger.warn("Could not find translation for #{match[0]} in #{language}") }
|
34
|
+
unless @type.eql?(:ror)
|
35
|
+
t.scan(/%{[^}]+}/) { |match| Logger.error("Could not find translation param for #{match} in #{language}") }
|
36
|
+
end
|
37
|
+
|
38
|
+
t
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def to_localize(text)
|
44
|
+
all_matches = text.to_enum(:scan, /\$(k[^\$\{]+)(?:\{(.+)\})?\$/).map { Regexp.last_match }
|
45
|
+
all_matches.map do |match|
|
46
|
+
params = if match[2]
|
47
|
+
params = match[2].split(",").map(&:strip).map do |param|
|
48
|
+
key, value = param.split(":", 2).map(&:strip)
|
49
|
+
[key.to_sym, value]
|
50
|
+
end
|
51
|
+
Hash[params]
|
52
|
+
end
|
53
|
+
|
54
|
+
{ match: match[0],
|
55
|
+
key: match[1],
|
56
|
+
params: params || {} }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def process(text)
|
61
|
+
if @type.eql?(:android)
|
62
|
+
text = text.gsub(/[']/, "\\\\\\\\'")
|
63
|
+
text = text.gsub("...", "…")
|
64
|
+
text = text.gsub("& ", "& ")
|
65
|
+
text = text.gsub("@", "\\\\@")
|
66
|
+
|
67
|
+
elsif @type.eql?(:ios)
|
68
|
+
text = text.gsub(/\"/, "\\\\\"")
|
69
|
+
end
|
70
|
+
|
71
|
+
text
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/bisu/version.rb
CHANGED
@@ -0,0 +1,35 @@
|
|
1
|
+
describe Bisu::Config do
|
2
|
+
subject { Bisu::Config.parse(file_path) }
|
3
|
+
|
4
|
+
context "given a yml file" do
|
5
|
+
let(:file_path) { "spec/fixtures/sample_translatable.yml" }
|
6
|
+
|
7
|
+
it "should parse the yml with deep key symbolization" do
|
8
|
+
expect(subject).to eq({
|
9
|
+
type: "BisuOS",
|
10
|
+
sheet_id: "abc1234567890",
|
11
|
+
keys_column: "key_name",
|
12
|
+
in: [
|
13
|
+
"path/to/file/to/1.ext.translatable",
|
14
|
+
"path/to/file/to/2.ext.translatable"
|
15
|
+
],
|
16
|
+
out_path: "path/to/final-%{locale}/%{out_name}",
|
17
|
+
out: [
|
18
|
+
{ locale: "en", kb_language: "english", path: "path/to/default/%{out_name}" },
|
19
|
+
{ locale: "pt", kb_language: "portuguese" },
|
20
|
+
{ locale: "pt-PT", kb_language: "portuguese" }
|
21
|
+
]
|
22
|
+
})
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "given an inexistent file" do
|
27
|
+
let(:file_path) { "does_not_exist" }
|
28
|
+
it { should be nil }
|
29
|
+
end
|
30
|
+
|
31
|
+
context "given no file path" do
|
32
|
+
let(:file_path) { nil }
|
33
|
+
it { should be nil }
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
describe Bisu::Dictionary do
|
2
|
+
subject(:dict) { Bisu::Dictionary.new(keys) }
|
3
|
+
|
4
|
+
describe ".initialize" do
|
5
|
+
let(:keys) { { "kNo1" => { "lang" => "text" } } }
|
6
|
+
it { expect { subject }.not_to raise_error }
|
7
|
+
|
8
|
+
context "when created with invalid type parameters" do
|
9
|
+
let(:keys) { "cenas" }
|
10
|
+
it { expect { subject }.to raise_error /expected Hash$/ }
|
11
|
+
end
|
12
|
+
|
13
|
+
context "when created with an invalid json schema" do
|
14
|
+
let(:keys) { { "kNo1" => "text" } }
|
15
|
+
it { expect { subject }.to raise_error /kNo1.+expected Hash/ }
|
16
|
+
end
|
17
|
+
|
18
|
+
context "when created with an invalid json schema" do
|
19
|
+
let(:keys) { { "kNo1" => { "a-lang" => { "wtvr" => "text" } } } }
|
20
|
+
it { expect { subject }.to raise_error /kNo1.+a-lang.+expected String/ }
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when given empty translations" do
|
24
|
+
let(:keys) { { "kNo1" => { "lang" => nil } } }
|
25
|
+
it { expect { subject }.not_to raise_error }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#has_language?" do
|
30
|
+
let(:keys) { {
|
31
|
+
"kNo1" => { "lang1" => "no1-lang1", "lang2" => "no1-lang2" },
|
32
|
+
"kNo2" => { "lang2" => "no2-lang2", "lang3" => "no2-lang3" }
|
33
|
+
} }
|
34
|
+
|
35
|
+
it "returns true if that language is translated" do
|
36
|
+
expect(dict.has_language?("lang2")).to be true
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns true if that language is only partially translated" do
|
40
|
+
expect(dict.has_language?("lang3")).to be true
|
41
|
+
end
|
42
|
+
|
43
|
+
it "returns false if that language does not exist in any key" do
|
44
|
+
expect(dict.has_language?("lang-no-available")).to be false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#localize" do
|
49
|
+
let(:keys) { {
|
50
|
+
"kCray" => {
|
51
|
+
"english" => "You are crazy!",
|
52
|
+
"portuguese" => "Estás maluco!",
|
53
|
+
"kriolo" => "Bo sta crazy!"
|
54
|
+
}
|
55
|
+
} }
|
56
|
+
|
57
|
+
it "localizes a key" do
|
58
|
+
expect(dict.localize("kCray", "kriolo")).to eq "Bo sta crazy!"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "returns nil when the key does not exist" do
|
62
|
+
expect(dict.localize("kDoesntExist", "kriolo")).to be nil
|
63
|
+
end
|
64
|
+
|
65
|
+
it "returns nil when language is not available" do
|
66
|
+
expect(dict.localize("kCray", "finish")).to be nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|