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 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