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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 90f92046e9e036bbb83b4052ae3599b4cd10042a
4
- data.tar.gz: 90b851d491eb60c8357068344e08c0ac4a4a0150
3
+ metadata.gz: 91a21fb208a5322f64116f20e4b82e6108085b8d
4
+ data.tar.gz: 9a8b0eef2a85596da38cc1e51da31f699d5c6f94
5
5
  SHA512:
6
- metadata.gz: 8e99c6314b1c3455c97c5c7f4baf348f604489439b50b2749e8fdedb4ab1f0f8e8530bb91841b7decdf10ee8dd107623a76d6d06b20dd258bea46982cb46b99f
7
- data.tar.gz: de6b9297d956019d909a597b17a465d6281bb0aefc46e876d270925b52b00094bb948437d4ee1a1311229a810a96508ccfc3b95102ffe27ee50faac91fdd913f
6
+ metadata.gz: cc959379b26785e7f50b44461908985ef491eb94d6e765390145ca3416c130305c3ba7877837f9fbafefa9d149bd9816e72bf6d82c92dbc97c0cc9fe5bde58b3
7
+ data.tar.gz: faa8f17d79bf8579a6a5810e858f19e5adfff2635a213429e641ecbd1f8fe1e8373d27491f10182568b71859abe0349aa3a59c7ddb3c024e264fda38498b409b
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ .ruby-version
data/.rspec ADDED
@@ -0,0 +1,5 @@
1
+ --color
2
+ --format documentation
3
+ --profile
4
+ --drb
5
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+ ruby '2.3.1'
3
+
4
+ gem 'safe_yaml', '~> 1.0'
5
+ gem 'colorize', '~> 0.7'
6
+ gem 'xml-simple', '~> 1.1'
7
+
8
+ gem 'webmock', '~> 1.20'
9
+ gem 'rspec'
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/knowledge_base'
4
- require 'bisu/translator'
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(opts)
12
- options = command_line_options(opts)
12
+ def run(options)
13
+ options = command_line_options(options)
13
14
 
14
15
  if config = Bisu::Config.parse("translatable.yml")
15
- kbase = Bisu::GoogleDriveKB.new(config[:sheet_id], config[:keys_column])
16
- translator = Bisu::Translator.new(kbase, config[:type])
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
- localize(translator, out[:locale], out[:kb_language], options[:default_language], in_path, out[:path] || config[:out_path])
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
- options = {}
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
- options[:default_language] = language
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
- opts_parser.parse!(ARGV)
50
- options
57
+ opts_hash
51
58
  end
52
59
 
53
- def localize(translator, locale, language, default_language, in_path, out_path)
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
- translator.translate(language, locale, in_path, out_path, default_language)
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("& ", "&amp; ")
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
@@ -1,4 +1,4 @@
1
1
  module Bisu
2
- VERSION = '1.2.3'
3
- VERSION_UPDATED_AT = '2016-10-14'
2
+ VERSION = '1.2.4'
3
+ VERSION_UPDATED_AT = '2016-10-23'
4
4
  end
@@ -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