babelyoda 1.6.0 → 2.0.0

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.
@@ -0,0 +1,72 @@
1
+ require_relative 'localization_key'
2
+ require_relative 'localization_value'
3
+
4
+ module Babelyoda
5
+ class StringsParser
6
+ Bit = Struct.new(:token, :value)
7
+
8
+ def initialize(lexer, language)
9
+ @lexer, @language = lexer, language
10
+ end
11
+
12
+ def parse(str, &block)
13
+ @block = block
14
+ bitstream = []
15
+ @lexer.lex(str) do | token, value |
16
+ bitstream << Bit.new(token, value)
17
+ end
18
+ while bitstream.size > 0
19
+ record = produce(bitstream)
20
+ @block.call(record) if record
21
+ end
22
+ end
23
+
24
+ def produce(bs)
25
+ match_bs(bs, :multiline_comment, :string, :equal_sign, :string, :semicolon) do |bits|
26
+ localization_key = LocalizationKey.new(cleanup_string(bits[1]), cleanup_comment(bits[0]))
27
+ localization_value = LocalizationValue.new(@language, cleanup_string(bits[3]))
28
+ localization_key << localization_value
29
+ return localization_key
30
+ end
31
+ match_bs(bs, :singleline_comment, :string, :equal_sign, :string, :semicolon) do |bits|
32
+ localization_key = LocalizationKey.new(cleanup_string(bits[1]), cleanup_comment(bits[0]))
33
+ localization_value = LocalizationValue.new(@language, cleanup_string(bits[3]))
34
+ localization_key << localization_value
35
+ return localization_key
36
+ end
37
+ match_bs(bs, :string, :equal_sign, :string, :semicolon) do |bits|
38
+ localization_key = LocalizationKey.new(cleanup_string(bits[0]), nil)
39
+ localization_value = LocalizationValue.new(@language, cleanup_string(bits[2]))
40
+ localization_key << localization_value
41
+ return localization_key
42
+ end
43
+ match_bs(bs, :singleline_comment) do |bits|
44
+ return nil
45
+ end
46
+ match_bs(bs, :multiline_comment) do |bits|
47
+ return nil
48
+ end
49
+ raise "Syntax error: #{bs.shift(5).inspect}"
50
+ end
51
+
52
+ def match_bs(bs, *tokens)
53
+ return unless bs.size >= tokens.size
54
+ tokens.each_with_index do |token, idx|
55
+ return unless bs[idx][:token] == token
56
+ end
57
+ yield bs.shift(tokens.size).map { |bit| bit[:value] }
58
+ end
59
+
60
+ def cleanup_comment(str)
61
+ if str.match(/^\/\/\s*/)
62
+ str.sub(/^\/\/\s*/, '')
63
+ else
64
+ str.sub(/^\/\*\s*/, '').sub(/\s*\*\/$/, '')
65
+ end
66
+ end
67
+
68
+ def cleanup_string(str)
69
+ str.sub(/^\"/, '').sub(/\"$/, '')
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,190 @@
1
+ require 'builder'
2
+ require 'net/http'
3
+ require 'nokogiri'
4
+ require 'stringio'
5
+
6
+ require 'babelyoda/specification_loader'
7
+
8
+ module Babelyoda
9
+ class Keyset
10
+ def to_xml(xml, language = nil)
11
+ xml.keyset(:id => name) do
12
+ keys.each_value do |key|
13
+ xml.key(:id => key.id, :is_plural => 'False') do
14
+ xml.context(key.context)
15
+ key.values.each_value do |value|
16
+ next if language && (value.language.to_s != language.to_s)
17
+ xml.value(value.text, :language => value.language, :status => value.status)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ def self.parse_xml(node)
25
+ result = self.new(node[:id])
26
+ node.css('key').each do |key_node|
27
+ result.merge_key!(Babelyoda::LocalizationKey.parse_xml(key_node))
28
+ end
29
+ result
30
+ end
31
+ end
32
+
33
+ class LocalizationKey
34
+ def self.parse_xml(node)
35
+ context = node.css('context').first
36
+ context &&= context.text
37
+ result = self.new(node[:id], context)
38
+ node.css('value').each do |value_node|
39
+ result << Babelyoda::LocalizationValue.parse_xml(value_node)
40
+ end
41
+ result
42
+ end
43
+ end
44
+
45
+ class LocalizationValue
46
+ def self.parse_xml(node)
47
+ self.new(node[:language], node.text, node[:status])
48
+ end
49
+ end
50
+
51
+ class Tanker
52
+ include Babelyoda::SpecificationLoader
53
+
54
+ class FileNameInvalidError < RuntimeError ; end
55
+
56
+ attr_accessor :endpoint
57
+ attr_accessor :token
58
+ attr_accessor :project_id
59
+
60
+ def replace(keyset, language = nil)
61
+ doc = project_xml do |xml|
62
+ keyset.to_xml(xml, language)
63
+ end
64
+ payload = {
65
+ :file => StringIO.new(doc),
66
+ 'project-id' => project_id,
67
+ 'keyset-id' => keyset.name,
68
+ :format => 'xml'
69
+ }
70
+ payload.merge!({:language => language}) if language
71
+ post('/keysets/replace/', payload)
72
+ end
73
+
74
+ def list
75
+ get('/keysets/', { 'project-id' => project_id }).css('keyset').map { |keyset| keyset['id'] }
76
+ end
77
+
78
+ def create(keyset_name)
79
+ post('/keysets/create/', { 'project-id' => project_id, 'keyset-id' => keyset_name })
80
+ end
81
+
82
+ def export(keyset_name = nil, languages = nil, status = nil, safe = false)
83
+ payload = { 'project-id' => project_id }
84
+ payload.merge!({ 'keyset-id' => keyset_name }) if keyset_name
85
+ if languages
86
+ value = languages
87
+ value = languages.join(',') if languages.respond_to?(:join)
88
+ payload.merge!({ 'language' => value })
89
+ end
90
+ payload.merge!({ 'status' => status.to_s }) if status
91
+ payload.merge!({ 'safe' => safe }) if safe
92
+ get('/projects/export/xml/', payload)
93
+ end
94
+
95
+ def load_keyset(keyset_name, languages = nil, status = nil, safe = false)
96
+ doc = export(keyset_name, languages, status, safe)
97
+ doc.css("keyset[@id='#{keyset_name}']").each do |keyset_node|
98
+ keyset = Babelyoda::Keyset.parse_xml(keyset_node)
99
+ return keyset if keyset.name == keyset_name
100
+ end
101
+ Babelyoda::Keyset.new(keyset_name)
102
+ end
103
+
104
+ private
105
+
106
+ MULTIPART_BOUNDARY = '114YANDEXTANKERCLIENTBNDR';
107
+
108
+ def multipart_content_type
109
+ "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}"
110
+ end
111
+
112
+ def method(name)
113
+ "#{endpoint}#{name}"
114
+ end
115
+
116
+ def project_xml(&block)
117
+ xml = Builder::XmlMarkup.new
118
+ xml.instruct!(:xml, :encoding => "UTF-8")
119
+ xml.tanker do
120
+ xml.project(:id => project_id) do
121
+ yield xml
122
+ end
123
+ end
124
+ end
125
+
126
+ def multipart_data(payload = {}, boundary = MULTIPART_BOUNDARY)
127
+ payload.keys.map { |k|
128
+ "--#{boundary}\r\n" + multipart_field(k, payload[k])
129
+ }.join('') + "--#{boundary}--\r\n"
130
+ end
131
+
132
+ def multipart_field(k, v)
133
+ if v.respond_to?(:read)
134
+ "Content-Disposition: form-data; name=\"#{k}\"; filename=\"#{k}.xml\"\r\n" +
135
+ "Content-Type: application/octet-stream\r\n" +
136
+ "Content-Transfer-Encoding: binary\r\n\r\n" +
137
+ "#{v.read}\r\n"
138
+ else
139
+ "Content-Disposition: form-data; name=\"#{k}\"\r\n\r\n" +
140
+ "#{v}\r\n"
141
+ end
142
+ end
143
+
144
+ def post(method_name, payload)
145
+ uri = URI(method(method_name))
146
+ req = Net::HTTP::Post.new(uri.path)
147
+ req['AUTHORIZATION'] = token
148
+ req.content_type = multipart_content_type
149
+ req.body = multipart_data(payload)
150
+
151
+ # puts "POST URI: #{uri}"
152
+ # puts "POST BODY: #{req.body}"
153
+
154
+ res = Net::HTTP.start(uri.host, uri.port) do |http|
155
+ http.request(req)
156
+ end
157
+
158
+ case res
159
+ when Net::HTTPSuccess, Net::HTTPRedirection
160
+ Nokogiri::XML.parse(res.body)
161
+ else
162
+ doc = Nokogiri::XML.parse(res.body)
163
+ error = doc.css('result error')[0].content
164
+ raise RuntimeError.new(error)
165
+ end
166
+ end
167
+
168
+ def get(method_name, payload = nil)
169
+ uri = URI(method(method_name))
170
+ uri.query = URI.encode_www_form(payload) if payload
171
+ req = Net::HTTP::Get.new(uri.request_uri)
172
+ req['AUTHORIZATION'] = token
173
+
174
+ # puts "GET URI: #{uri}"
175
+
176
+ res = Net::HTTP.start(uri.host, uri.port) do |http|
177
+ http.request(req)
178
+ end
179
+
180
+ case res
181
+ when Net::HTTPSuccess, Net::HTTPRedirection
182
+ Nokogiri::XML.parse(res.body)
183
+ else
184
+ doc = Nokogiri::XML.parse(res.body)
185
+ error = doc.css('result error')[0].content
186
+ raise Error.new(error)
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,3 @@
1
+ module Babelyoda
2
+ VERSION = "2.0.0"
3
+ end
@@ -0,0 +1,94 @@
1
+ require 'fileutils'
2
+
3
+ require_relative 'file'
4
+ require_relative 'ibtool'
5
+
6
+ module Babelyoda
7
+ class Xib
8
+ attr_reader :filename
9
+ attr_reader :language
10
+
11
+ def initialize(filename, language)
12
+ @filename, @language = filename, language
13
+ end
14
+
15
+ def extractable?(development_language)
16
+ lproj_part = File.lproj_part(@filename)
17
+ (!lproj_part.nil?) && lproj_part == "#{development_language}.lproj"
18
+ end
19
+
20
+ def dirname
21
+ File.dirname(@filename)
22
+ end
23
+
24
+ def basename
25
+ File.basename(File.split(@filename)[1], '.xib')
26
+ end
27
+
28
+ def resourced?
29
+ !File.lproj_part(@filename).nil?
30
+ end
31
+
32
+ def resource!
33
+ raise "The XIB is already in a resource folder: #{@filename}" unless File.lproj_part(@filename).nil?
34
+ mv(File.localized(@filename, @language))
35
+ end
36
+
37
+ def mv(new_filename)
38
+ FileUtils.mkdir_p(File.dirname(new_filename))
39
+ FileUtils.mv(@filename, new_filename)
40
+ @filename = new_filename
41
+ end
42
+
43
+ def strings?
44
+ !strings.empty?
45
+ end
46
+
47
+ def strings
48
+ Babelyoda::Ibtool.extract_strings(@filename, @language)
49
+ end
50
+
51
+ def localize(language, scm)
52
+ puts "Localizing #{filename} => #{File.localized(filename, language)}..."
53
+ assert_localization_target(language)
54
+ strings_fn = strings_filename(language)
55
+ $logger.error "No strings file found: #{strings_fn}" unless File.exist?(strings_fn)
56
+ Babelyoda::Ibtool.localize(filename, File.localized(filename, language), strings_fn)
57
+ end
58
+
59
+ def localize_incremental(language, scm)
60
+ assert_localization_target(language)
61
+ unless has_incremental_resources?(language, scm)
62
+ localize(language, scm)
63
+ else
64
+ puts "Incrementally localizing #{filename} => #{File.localized(filename, language)}..."
65
+ strings_fn = strings_filename(language)
66
+ $logger.error "No strings file found: #{strings_fn}" unless File.exist?(strings_fn)
67
+
68
+ scm.fetch_versions!(filename, File.localized(filename, language)) do |filenames|
69
+ Babelyoda::Ibtool.localize_incrementally(filename, File.localized(filename, language), strings_fn, filenames[0], filenames[1])
70
+ end
71
+ end
72
+ end
73
+
74
+ def strings_filename(language = nil)
75
+ language ? File.localized(File.join(dirname, "#{basename}.strings"), language) : File.join(dirname, "#{basename}.strings")
76
+ end
77
+
78
+ def import_strings(scm)
79
+ Babelyoda::Ibtool.import_strings(filename, strings_filename)
80
+ end
81
+
82
+ private
83
+
84
+ def has_incremental_resources?(language, scm)
85
+ scm.version_exist?(filename) && scm.version_exist?(File.localized(filename, language))
86
+ end
87
+
88
+ def assert_localization_target(language)
89
+ raise "Can't localize a XIB file that has not been put into an .lproj folder: #{filename}" unless resourced?
90
+ raise "Can't localize #{@language} to #{language} for: #{filename}" if @language == language
91
+ end
92
+
93
+ end
94
+ end
data/lib/babelyoda.rb ADDED
@@ -0,0 +1,207 @@
1
+ BABELYODA_PATH = File.expand_path(File.join(File.dirname(__FILE__), '..'))
2
+
3
+ require 'awesome_print'
4
+
5
+ require_relative 'babelyoda/genstrings'
6
+ require_relative 'babelyoda/git'
7
+ require_relative 'babelyoda/ibtool'
8
+ require_relative 'babelyoda/keyset'
9
+ require_relative 'babelyoda/localization_key'
10
+ require_relative 'babelyoda/localization_value'
11
+ require_relative 'babelyoda/rake'
12
+ require_relative 'babelyoda/specification'
13
+ require_relative 'babelyoda/tanker'
14
+ require_relative 'babelyoda/xib'
15
+
16
+ desc "Do a full localization cycle: push new strings, get translations and merge them"
17
+ task :babelyoda => ['babelyoda:push', 'babelyoda:pull'] do
18
+ end
19
+
20
+ namespace :babelyoda do
21
+
22
+ file 'Babelfile' do
23
+ Babelyoda::Specification.generate_default_babelfile
24
+ end
25
+
26
+ desc "Create a basic bootstrap Babelfile"
27
+ task :init => 'Babelfile' do
28
+ end
29
+
30
+ Babelyoda::Rake.spec do |spec|
31
+
32
+ desc "Extract strings from sources"
33
+ task :extract_strings do
34
+ puts "Extracting strings from sources..."
35
+ dev_lang = spec.development_language
36
+
37
+ spec.scm.transaction("Extract strings from sources") do
38
+ Babelyoda::Genstrings.run(spec.source_files, dev_lang) do |keyset|
39
+ old_strings_filename = strings_filename(keyset.name, dev_lang)
40
+ old_strings = Babelyoda::Strings.new(old_strings_filename, dev_lang).read
41
+ old_strings.merge!(keyset)
42
+ old_strings.save!
43
+ puts " #{old_strings_filename}: #{old_strings.keys.size} keys"
44
+ end
45
+ end
46
+ end
47
+
48
+ desc "Extract strings from XIBs"
49
+ task :extract_xib_strings do
50
+ puts "Extracting .strings from XIBs..."
51
+ spec.scm.transaction("Extract strings from XIBs") do
52
+ spec.xib_files.each do |xib_filename|
53
+ xib = Babelyoda::Xib.new(xib_filename, spec.development_language)
54
+ next unless xib.extractable?(spec.development_language)
55
+ keyset = xib.strings
56
+ puts " #{xib_filename} => #{xib.strings_filename}"
57
+ Babelyoda::Strings.save_keyset(keyset, xib.strings_filename, spec.development_language)
58
+ end
59
+ end
60
+ end
61
+
62
+ desc "Extracts localizable strings into the corresponding .strings files"
63
+ task :extract => [:extract_strings, :extract_xib_strings] do
64
+ end
65
+
66
+ desc "Create remote keysets for local keysets"
67
+ task :create_keysets => :extract do
68
+ # Create remote keysets for each local keyset if they don't exist.
69
+ puts "Creating remote keysets for local keysets..."
70
+ remote_keyset_names = spec.engine.list
71
+ spec.strings_files.each do |filename|
72
+ keyset_name = Babelyoda::Keyset.keyset_name(filename)
73
+ if remote_keyset_names.include?(keyset_name)
74
+ puts " Tanker: An existing keyset found: #{keyset_name}"
75
+ next
76
+ end
77
+ spec.engine.create(keyset_name)
78
+ puts " Tanker: Created NEW keyset: #{keyset_name}"
79
+ end
80
+ end
81
+
82
+ desc "Drops remote keys not found in local keysets"
83
+ task :drop_orphan_keys => :create_keysets do
84
+ puts "Dropping orphan keys..."
85
+ spec.strings_files.each do |filename|
86
+ strings = Babelyoda::Strings.new(filename, spec.development_language).read!
87
+ puts " Processing keyset: #{strings.name}"
88
+ remote_keyset = spec.engine.load_keyset(strings.name)
89
+ keys_to_drop = []
90
+ remote_keyset.keys.each_value do |key|
91
+ unless strings.keys.has_key?(key.id)
92
+ keys_to_drop << key.id
93
+ puts " Found orphan key: #{key.id}"
94
+ end
95
+ end
96
+ keys_to_drop.each do |key|
97
+ remote_keyset.keys.delete(key)
98
+ end
99
+ spec.engine.replace(remote_keyset)
100
+ puts " Dropped keys: #{keys_to_drop.size}"
101
+ end
102
+ end
103
+
104
+ desc "Pushes resources to the translators"
105
+ task :push => :drop_orphan_keys do
106
+ puts "Pushing local keys to the remote..."
107
+ spec.strings_files.each do |filename|
108
+ strings = Babelyoda::Strings.new(filename, spec.development_language).read!
109
+ puts " Processing keyset: #{strings.name}"
110
+ remote_keyset = spec.engine.load_keyset(strings.name, nil, :unapproved)
111
+ result = remote_keyset.merge!(strings, preserve: true)
112
+ remote_keyset.ensure_languages!(spec.all_languages)
113
+ spec.engine.replace(remote_keyset)
114
+ puts " New keys: #{result[:new]} Updated keys: #{result[:updated]}"
115
+ end
116
+ end
117
+
118
+ desc "Fetches remote strings and merges them down into local .string files"
119
+ task :fetch_strings do
120
+ puts "Fetching remote translations..."
121
+ spec.scm.transaction("Merge in remote translations") do
122
+ spec.strings_files.each do |filename|
123
+ keyset_name = Babelyoda::Keyset.keyset_name(filename)
124
+ remote_keyset = spec.engine.load_keyset(keyset_name, nil, :unapproved, true)
125
+ remote_keyset.drop_empty!
126
+ spec.all_languages.each do |language|
127
+ keyset_filename = strings_filename(keyset_name, language)
128
+ Babelyoda::Strings.save_keyset(remote_keyset, keyset_filename, language)
129
+ puts " #{keyset_filename}"
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ desc "Incrementally localizes XIB files"
136
+ task :localize_xibs do
137
+ puts "Translating XIB files..."
138
+
139
+ spec.scm.transaction("Localize XIB files") do
140
+ spec.xib_files.each do |filename|
141
+ xib = Babelyoda::Xib.new(filename, spec.development_language)
142
+
143
+ xib.import_strings(spec.scm)
144
+ spec.localization_languages.each do |language|
145
+ xib.localize_incremental(language, spec.scm)
146
+ end
147
+ end
148
+ end
149
+
150
+ spec.scm.transaction("Update XIB SHA1 version refs") do
151
+ spec.xib_files.each do |filename|
152
+ spec.scm.store_version!(filename)
153
+ spec.localization_languages.each do |language|
154
+ spec.scm.store_version!(File.localized(filename, language))
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ desc "Pull remote translations"
161
+ task :pull => [:fetch_strings, :localize_xibs] do
162
+ end
163
+
164
+ namespace :remote do
165
+
166
+ desc "List remote keysets"
167
+ task :list do
168
+ ap spec.engine.list
169
+ end
170
+
171
+ desc "Drop remote keysets in KEYSETS"
172
+ task :drop_keysets do
173
+ if ENV['KEYSETS']
174
+ keysets = ENV['KEYSETS'].split(',')
175
+ if keysets.include?('*')
176
+ keysets = spec.engine.list
177
+ puts "Dropping ALL keysets: #{keysets}"
178
+ else
179
+ puts "Dropping keysets: #{keysets}"
180
+ end
181
+ keysets.each do |keyset_name|
182
+ puts " Dropping: #{keyset_name}"
183
+ keyset = Babelyoda::Keyset.new(keyset_name)
184
+ key = Babelyoda::LocalizationKey.new("Dummy", "Dummy")
185
+ value = Babelyoda::LocalizationValue.new(:en, "Dummy")
186
+ key << value
187
+ keyset.merge_key!(key)
188
+ spec.engine.replace(keyset)
189
+ end
190
+ puts "All done!"
191
+ else
192
+ puts "Please provide keyset names to drop in the KEYSET environment variable. Separate by commas. Use * for ALL."
193
+ end
194
+ end
195
+
196
+ end
197
+
198
+ end
199
+ end
200
+
201
+ def strings_filename(keyset_name, lang)
202
+ if keyset_name.match(/\//)
203
+ File.join(File.dirname(keyset_name), "#{lang}.lproj", "#{File.basename(keyset_name)}.strings")
204
+ else
205
+ File.join("#{lang}.lproj", "#{keyset_name}.strings")
206
+ end
207
+ end
@@ -0,0 +1,15 @@
1
+ Babelyoda::Specification.new do |s|
2
+ s.name = 'YOUR PROJECT NAME HERE'
3
+ s.development_language = :en
4
+ s.localization_languages = [:ru, :uk, :tr]
5
+ s.engine = Babelyoda::Tanker.new do |t|
6
+ t.token = 'FIX: TANKER TOKEN HERE'
7
+ t.project_id = 'FIX: TANKER PROJECT ID HERE'
8
+ t.endpoint = 'FIX: TANKER END POINT HERE'
9
+ end
10
+ s.scm = Babelyoda::Git.new
11
+ s.source_files = FileList['Classes/**/*.{m,mm,h}']
12
+ s.resources_folder = 'Resources'
13
+ s.xib_files = FileList['{Classes,Resources}/**/en.lproj/*.{xib}']
14
+ s.strings_files = FileList['{Classes,Resources}/**/en.lproj/*.{strings}']
15
+ end