bisu 1.2.3 → 1.2.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|