babelyoda 1.6.0 → 2.0.0

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