ETLane 0.1.42 → 0.1.46
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Lanes/CommonFastfile +4 -8
- data/Lanes/actions/README.md +47 -0
- data/Lanes/actions/add_keys_to_lokalise.rb +106 -0
- data/Lanes/actions/android/README.md +53 -0
- data/Lanes/actions/android/lokalise_download.rb +144 -0
- data/Lanes/actions/android/lokalise_upload.rb +98 -0
- data/Lanes/actions/lokalise.rb +180 -0
- data/Lanes/actions/lokalise_metadata.rb +624 -0
- data/Lanes/actions/lokalise_upload.rb +165 -0
- data/Lanes/actions/previews.rb +157 -0
- data/Scripts/Sources/Common/Api.swift +142 -0
- data/Scripts/Sources/Common/Array.swift +8 -0
- data/Scripts/Sources/Common/Error.swift +10 -0
- data/Scripts/Sources/Common/MD5.swift +34 -0
- data/Scripts/Sources/Resources/Api+Figma.swift +43 -0
- data/Scripts/Sources/Resources/Deploy.swift +133 -0
- data/Scripts/Sources/Resources/Device.swift +42 -0
- data/Scripts/Sources/Resources/DownloadBatch.swift +108 -0
- data/Scripts/Sources/Resources/FigmaPages.swift +58 -0
- data/Scripts/Sources/Resources/Images.swift +5 -0
- data/Scripts/Sources/Resources/PreviewDownloader.swift +80 -0
- data/Scripts/Sources/Resources/ResourcesParser.swift +25 -0
- data/Scripts/Sources/Resources/ScreenshotDownloader.swift +150 -0
- data/Scripts/Sources/Resources/main.swift +58 -0
- metadata +25 -2
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Fastlane
|
4
|
+
module Actions
|
5
|
+
class LokaliseUploadAction < Action
|
6
|
+
def self.run(params)
|
7
|
+
|
8
|
+
self.snapshot(params)
|
9
|
+
sources = params[:sources]
|
10
|
+
languages = params[:languages]
|
11
|
+
accepted_formats = [".strings", ".stringsdict"]
|
12
|
+
for source in sources do
|
13
|
+
Dir.chdir(source) do
|
14
|
+
Dir.glob('*').select { |f| File.directory? f
|
15
|
+
for lang in languages
|
16
|
+
folder = "#{lang}.lproj"
|
17
|
+
if f == folder
|
18
|
+
Dir.chdir(folder) do
|
19
|
+
Dir.glob('*').select { |f| accepted_formats.include? File.extname(f)
|
20
|
+
upload_name = File.join(source, folder, f)
|
21
|
+
File.open(f, 'rb') do |img|
|
22
|
+
base64 = Base64.strict_encode64(img.read)
|
23
|
+
upload(lang, upload_name, base64, params)
|
24
|
+
end
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# UI.error "Bad response 🉐\n#{response.body}" unless jsonResponse.kind_of? Hash
|
36
|
+
# fileURL = jsonResponse["bundle_url"]
|
37
|
+
# if fileURL.kind_of?(String) then
|
38
|
+
# UI.message "Downloading localizations archive 📦"
|
39
|
+
# FileUtils.mkdir_p("lokalisetmp")
|
40
|
+
# uri = URI(fileURL)
|
41
|
+
# http = Net::HTTP.new(uri.host, uri.port)
|
42
|
+
# http.use_ssl = true
|
43
|
+
# zipRequest = Net::HTTP::Get.new(uri)
|
44
|
+
# response = http.request(zipRequest)
|
45
|
+
# if response.content_type == "application/zip" or response.content_type == "application/octet-stream" then
|
46
|
+
# FileUtils.mkdir_p("lokalisetmp")
|
47
|
+
# open("lokalisetmp/a.zip", "wb") { |file|
|
48
|
+
# file.write(response.body)
|
49
|
+
# }
|
50
|
+
# unzip_file("lokalisetmp/a.zip", destination, clean_destination)
|
51
|
+
# FileUtils.remove_dir("lokalisetmp")
|
52
|
+
# UI.success "Localizations extracted to #{destination} 📗 📕 📘"
|
53
|
+
# else
|
54
|
+
# UI.error "Response did not include ZIP"
|
55
|
+
# end
|
56
|
+
# elsif jsonResponse["response"]["status"] == "error"
|
57
|
+
# code = jsonResponse["response"]["code"]
|
58
|
+
# message = jsonResponse["response"]["message"]
|
59
|
+
# UI.error "Response error code #{code} (#{message}) 📟"
|
60
|
+
# else
|
61
|
+
# UI.error "Bad response 🉐\n#{jsonResponse}"
|
62
|
+
# end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.snapshot(params)
|
66
|
+
require 'net/http'
|
67
|
+
project_identifier = params[:project_identifier]
|
68
|
+
uri = URI("https://api.lokalise.com/api2/projects/#{project_identifier}/snapshots")
|
69
|
+
request_data = {
|
70
|
+
title: params[:snapshot_version],
|
71
|
+
}
|
72
|
+
request = Net::HTTP::Post.new(uri)
|
73
|
+
request.body = request_data.to_json
|
74
|
+
request['X-Api-Token'] = params[:api_token]
|
75
|
+
request['Content-Type'] = 'application/json'
|
76
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
77
|
+
http.use_ssl = true
|
78
|
+
response = http.request(request)
|
79
|
+
jsonResponse = JSON.parse(response.body)
|
80
|
+
puts jsonResponse
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.upload(lang, name, data, params)
|
84
|
+
require 'net/http'
|
85
|
+
token = params[:api_token]
|
86
|
+
project_identifier = params[:project_identifier]
|
87
|
+
if lang == "es-419"
|
88
|
+
lang = "es_419"
|
89
|
+
end
|
90
|
+
puts "Lang: #{lang} #{name}"
|
91
|
+
request_data = {
|
92
|
+
filename: name,
|
93
|
+
data: data,
|
94
|
+
lang_iso: lang,
|
95
|
+
cleanup_mode: true,
|
96
|
+
}
|
97
|
+
uri = URI("https://api.lokalise.com/api2/projects/#{project_identifier}/files/upload")
|
98
|
+
request = Net::HTTP::Post.new(uri)
|
99
|
+
request.body = request_data.to_json
|
100
|
+
request['X-Api-Token'] = token
|
101
|
+
request['Content-Type'] = 'application/json'
|
102
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
103
|
+
http.use_ssl = true
|
104
|
+
response = http.request(request)
|
105
|
+
jsonResponse = JSON.parse(response.body)
|
106
|
+
puts jsonResponse
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
#####################################################
|
111
|
+
# @!group Documentation
|
112
|
+
#####################################################
|
113
|
+
|
114
|
+
def self.description
|
115
|
+
"Upload Lokalise localization"
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.available_options
|
119
|
+
[
|
120
|
+
FastlaneCore::ConfigItem.new(key: :api_token,
|
121
|
+
env_name: "LOKALISE_API_TOKEN",
|
122
|
+
description: "API Token for Lokalise",
|
123
|
+
verify_block: proc do |value|
|
124
|
+
UI.user_error! "No API token for Lokalise given, pass using `api_token: 'token'`" unless (value and not value.empty?)
|
125
|
+
end),
|
126
|
+
FastlaneCore::ConfigItem.new(key: :snapshot_version,
|
127
|
+
env_name: "LOKALISE_SNAPSHOT_VERSION",
|
128
|
+
description: "snapshot version for Lokalise",
|
129
|
+
verify_block: proc do |value|
|
130
|
+
UI.user_error! "No API token for Lokalise given, pass using `api_token: 'token'`" unless (value and not value.empty?)
|
131
|
+
end),
|
132
|
+
FastlaneCore::ConfigItem.new(key: :project_identifier,
|
133
|
+
env_name: "LOKALISE_PROJECT_ID",
|
134
|
+
description: "Lokalise Project ID",
|
135
|
+
verify_block: proc do |value|
|
136
|
+
UI.user_error! "No Project Identifier for Lokalise given, pass using `project_identifier: 'identifier'`" unless (value and not value.empty?)
|
137
|
+
end),
|
138
|
+
FastlaneCore::ConfigItem.new(key: :sources,
|
139
|
+
description: "Localization sources",
|
140
|
+
is_string: false,
|
141
|
+
verify_block: proc do |value|
|
142
|
+
UI.user_error! "Tags should be passed as array" unless value.kind_of? Array
|
143
|
+
end),
|
144
|
+
FastlaneCore::ConfigItem.new(key: :languages,
|
145
|
+
description: "Include only the languages",
|
146
|
+
optional: true,
|
147
|
+
is_string: false,
|
148
|
+
default_value: ["en"],
|
149
|
+
verify_block: proc do |value|
|
150
|
+
UI.user_error! "Tags should be passed as array" unless value.kind_of? Array
|
151
|
+
end),
|
152
|
+
|
153
|
+
]
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.authors
|
157
|
+
"teanet"
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.is_supported?(platform)
|
161
|
+
[:ios, :mac].include? platform
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'credentials_manager/account_manager'
|
2
|
+
require 'fastlane_core/itunes_transporter'
|
3
|
+
require 'rexml/document'
|
4
|
+
# a = CredentialsManager::AccountManager.new(user: user, prefix: "deliver.appspecific", note: "application-specific")
|
5
|
+
# @password = a.password(ask_if_missing: true) # to ask the user for the missing value
|
6
|
+
|
7
|
+
module Fastlane
|
8
|
+
module Actions
|
9
|
+
class PreviewsAction < Action
|
10
|
+
def self.run(params)
|
11
|
+
user = params[:username]
|
12
|
+
product_bundle_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
|
13
|
+
spaceship = Spaceship::Tunes.login(user)
|
14
|
+
spaceship.team_id = CredentialsManager::AppfileConfig.try_fetch_value(:itc_team_id)
|
15
|
+
app = Spaceship::Tunes::Application.find(product_bundle_identifier)
|
16
|
+
app_id = app.apple_id()
|
17
|
+
transporter = FastlaneCore::ItunesTransporter.new(user)
|
18
|
+
destination = "/tmp"
|
19
|
+
itmsp_path = File.join(destination, "#{app_id}.itmsp")
|
20
|
+
transporter.download(app_id, destination)
|
21
|
+
patch_itmsp(itmsp_path)
|
22
|
+
transporter.upload(app_id, destination)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.patch_itmsp(itmsp_path)
|
26
|
+
metadata_path = File.join(itmsp_path, "metadata.xml")
|
27
|
+
doc = REXML::Document.new(File.read(metadata_path))
|
28
|
+
current_version = doc.root.elements["software/software_metadata/versions[1]/version"]
|
29
|
+
|
30
|
+
software_metadata = doc.root.elements["software/software_metadata"]
|
31
|
+
software_metadata.elements.delete("products")
|
32
|
+
software_metadata.elements.delete("in_app_purchases")
|
33
|
+
versions = doc.root.elements["software/software_metadata/versions"]
|
34
|
+
|
35
|
+
old_version = versions.elements[2]
|
36
|
+
if old_version
|
37
|
+
puts "Romove old version"
|
38
|
+
versions.elements.delete(old_version)
|
39
|
+
end
|
40
|
+
|
41
|
+
build_folder = File.join(Dir.pwd, "build")
|
42
|
+
preview_path = File.join(build_folder, "previews")
|
43
|
+
|
44
|
+
current_version.elements.each("locales/locale") do |element|
|
45
|
+
|
46
|
+
element.elements.delete("app_previews")
|
47
|
+
element.elements.delete("software_screenshots")
|
48
|
+
element.elements.delete("title")
|
49
|
+
element.elements.delete("subtitle")
|
50
|
+
element.elements.delete("description")
|
51
|
+
element.elements.delete("version_whats_new")
|
52
|
+
element.elements.delete("privacy_url")
|
53
|
+
element.elements.delete("support_url")
|
54
|
+
element.elements.delete("keywords")
|
55
|
+
|
56
|
+
locale = element.attributes["name"]
|
57
|
+
locale_path = File.join(preview_path, locale)
|
58
|
+
# если нет папки с локалью, значит удаляем видео
|
59
|
+
app_previews = REXML::Element.new('app_previews')
|
60
|
+
if File.directory?(locale_path)
|
61
|
+
locale_files = Dir.entries(locale_path).select {|f| not File.directory?(f) }
|
62
|
+
|
63
|
+
timestamp = "00:00:08:00"
|
64
|
+
for file in locale_files do
|
65
|
+
if file == "timestamp.txt"
|
66
|
+
timestamp = File.read(File.join(locale_path, file))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
for file in locale_files do
|
70
|
+
video_path = File.join(locale_path, file)
|
71
|
+
if File.extname(file).downcase == ".mp4"
|
72
|
+
size = File.size(video_path)
|
73
|
+
if size > 500000
|
74
|
+
app_preview = generate_app_preview(itmsp_path, locale, video_path, timestamp)
|
75
|
+
app_previews.elements.add(app_preview)
|
76
|
+
else
|
77
|
+
puts "#{video_path} too small, #{size}b"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
element.elements.add(app_previews)
|
83
|
+
end
|
84
|
+
formatter = REXML::Formatters::Pretty.new
|
85
|
+
formatter.compact = true
|
86
|
+
formatter.width = 9999
|
87
|
+
File.open(metadata_path,"w"){|file| file.puts formatter.write(doc.root,"")}
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.generate_app_preview(itmsp_path, locale, video_path, timestamp)
|
91
|
+
file = last = File.basename(video_path)
|
92
|
+
file_with_locale = "#{locale}_#{file}"
|
93
|
+
itmsp_video_path = File.join(itmsp_path, file_with_locale)
|
94
|
+
FileUtils.cp(video_path, itmsp_video_path)
|
95
|
+
file_name = File.basename(file, File.extname(file))
|
96
|
+
|
97
|
+
app_preview = REXML::Element.new('app_preview')
|
98
|
+
app_preview.add_attribute(REXML::Attribute.new('display_target', file_name))
|
99
|
+
app_preview.add_attribute(REXML::Attribute.new('position', '1'))
|
100
|
+
|
101
|
+
data_file = REXML::Element.new('data_file')
|
102
|
+
data_file.add_attribute(REXML::Attribute.new('role', 'source'))
|
103
|
+
|
104
|
+
size_node = REXML::Element.new('size')
|
105
|
+
size = File.size(video_path)
|
106
|
+
size_node.add_text("#{size}")
|
107
|
+
data_file.elements.add(size_node)
|
108
|
+
|
109
|
+
file_name_node = REXML::Element.new('file_name')
|
110
|
+
file_name_node.add_text(file_with_locale)
|
111
|
+
data_file.elements.add(file_name_node)
|
112
|
+
|
113
|
+
checksum = REXML::Element.new('checksum')
|
114
|
+
checksum.add_text(Digest::MD5.file(video_path).hexdigest)
|
115
|
+
data_file.elements.add(checksum)
|
116
|
+
|
117
|
+
app_preview.elements.add(data_file)
|
118
|
+
|
119
|
+
preview_image_time = REXML::Element.new('preview_image_time')
|
120
|
+
preview_image_time.add_attribute(REXML::Attribute.new('format', "30/1:1/nonDrop"))
|
121
|
+
preview_image_time.add_text(timestamp)
|
122
|
+
app_preview.elements.add(preview_image_time)
|
123
|
+
|
124
|
+
app_preview
|
125
|
+
end
|
126
|
+
|
127
|
+
#####################################################
|
128
|
+
# @!group Documentation
|
129
|
+
#####################################################
|
130
|
+
|
131
|
+
def self.description
|
132
|
+
"Download Lokalise localization"
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.available_options
|
136
|
+
[
|
137
|
+
FastlaneCore::ConfigItem.new(
|
138
|
+
key: :username,
|
139
|
+
env_name: "PREVIEW_USER_NAME",
|
140
|
+
description: "User",
|
141
|
+
verify_block: proc do |value|
|
142
|
+
UI.user_error! "No API token for Lokalise given, pass using `api_token: 'token'`" unless (value and not value.empty?)
|
143
|
+
end
|
144
|
+
),
|
145
|
+
]
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.authors
|
149
|
+
"teanet"
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.is_supported?(platform)
|
153
|
+
[:ios].include? platform
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
import Foundation
|
2
|
+
|
3
|
+
public final class Api {
|
4
|
+
|
5
|
+
public struct NoContent: Decodable {
|
6
|
+
init() {}
|
7
|
+
}
|
8
|
+
|
9
|
+
public enum ApiError: Error {
|
10
|
+
case nilDataError
|
11
|
+
case pathError
|
12
|
+
case repeatCountLimitReached
|
13
|
+
}
|
14
|
+
|
15
|
+
enum Method: String {
|
16
|
+
case get = "GET"
|
17
|
+
case post = "POST"
|
18
|
+
case put = "PUT"
|
19
|
+
}
|
20
|
+
|
21
|
+
private let session = URLSession(configuration: URLSessionConfiguration.default)
|
22
|
+
private let baseURL: URL
|
23
|
+
|
24
|
+
public init(baseURL: String) {
|
25
|
+
self.baseURL = URL(string: baseURL)!
|
26
|
+
}
|
27
|
+
|
28
|
+
public func get<T: Codable>(
|
29
|
+
path: String,
|
30
|
+
query: [String: String] = [:],
|
31
|
+
headers: [String: String] = [:],
|
32
|
+
timeoutInterval: TimeInterval
|
33
|
+
) throws -> T {
|
34
|
+
return try self.method(.get, path: path, query: query, headers: headers, timeoutInterval: timeoutInterval)
|
35
|
+
}
|
36
|
+
|
37
|
+
public func post<T: Codable, TBody: Encodable>(
|
38
|
+
path: String,
|
39
|
+
body: TBody,
|
40
|
+
query: [String: String] = [:],
|
41
|
+
headers: [String: String] = [:],
|
42
|
+
timeoutInterval: TimeInterval
|
43
|
+
) throws -> T {
|
44
|
+
let body = try JSONEncoder().encode(body)
|
45
|
+
return try self.method(.post, path: path, query: query, headers: headers, timeoutInterval: timeoutInterval, body: body)
|
46
|
+
}
|
47
|
+
|
48
|
+
private func get<T: Codable>(path: String, completion: @escaping (Result<T, Error>) -> Void) {
|
49
|
+
self.method(.get, path: path, completion: completion)
|
50
|
+
}
|
51
|
+
|
52
|
+
private func post<T: Codable, TBody: Encodable>(
|
53
|
+
path: String,
|
54
|
+
body: TBody, headers: [String: String] = [:],
|
55
|
+
completion: @escaping (Result<T, Error>) -> Void
|
56
|
+
) {
|
57
|
+
let body = try? JSONEncoder().encode(body)
|
58
|
+
self.method(.post, path: path, headers: headers, body: body, completion: completion)
|
59
|
+
}
|
60
|
+
|
61
|
+
private func method<T: Decodable>(
|
62
|
+
_ method: Method,
|
63
|
+
path: String,
|
64
|
+
query: [String: String] = [:],
|
65
|
+
headers: [String: String] = [:],
|
66
|
+
timeoutInterval: TimeInterval,
|
67
|
+
body: Data? = nil
|
68
|
+
) throws -> T {
|
69
|
+
let s = DispatchSemaphore(value: 0)
|
70
|
+
var statsResponse: Result<T, Error>!
|
71
|
+
let completion: (Result<T, Error>) -> Void = { result in
|
72
|
+
statsResponse = result
|
73
|
+
s.signal()
|
74
|
+
}
|
75
|
+
self.method(
|
76
|
+
method,
|
77
|
+
path: path,
|
78
|
+
query: query,
|
79
|
+
headers: headers,
|
80
|
+
body: body,
|
81
|
+
timeoutInterval: timeoutInterval,
|
82
|
+
completion: completion
|
83
|
+
)
|
84
|
+
s.wait()
|
85
|
+
return try statsResponse.get()
|
86
|
+
}
|
87
|
+
|
88
|
+
private func method<T: Decodable>(
|
89
|
+
_ method: Method,
|
90
|
+
path: String,
|
91
|
+
query: [String: String] = [:],
|
92
|
+
headers: [String: String] = [:],
|
93
|
+
body: Data? = nil,
|
94
|
+
timeoutInterval: TimeInterval = 60,
|
95
|
+
completion: @escaping (Result<T, Error>) -> Void
|
96
|
+
) {
|
97
|
+
let baseURL = self.baseURL.appendingPathComponent(path)
|
98
|
+
var cmp = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)
|
99
|
+
cmp?.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) }
|
100
|
+
guard let url = cmp?.url else { completion(.failure(ApiError.pathError)); return }
|
101
|
+
var request = URLRequest(url: url)
|
102
|
+
request.timeoutInterval = timeoutInterval
|
103
|
+
request.httpBody = body
|
104
|
+
request.httpMethod = method.rawValue
|
105
|
+
var allHTTPHeaderFields = headers
|
106
|
+
if headers["content-type"] == nil {
|
107
|
+
allHTTPHeaderFields["content-type"] = "application/json"
|
108
|
+
}
|
109
|
+
request.allHTTPHeaderFields = allHTTPHeaderFields
|
110
|
+
print("Start request: \(method.rawValue) \(url)")
|
111
|
+
self.session.dataTask(with: request) { (data, _, error) in
|
112
|
+
|
113
|
+
if let error = error {
|
114
|
+
completion(.failure(error))
|
115
|
+
return
|
116
|
+
}
|
117
|
+
guard let data = data else {
|
118
|
+
completion(.failure(ApiError.nilDataError))
|
119
|
+
return
|
120
|
+
}
|
121
|
+
if data.isEmpty, let noContent = NoContent() as? T {
|
122
|
+
completion(.success(noContent))
|
123
|
+
return
|
124
|
+
}
|
125
|
+
|
126
|
+
if let string = String(data: data, encoding: .utf8)?.prefix(3000) {
|
127
|
+
print("Finish: \(string)...")
|
128
|
+
} else {
|
129
|
+
print("Finish")
|
130
|
+
}
|
131
|
+
|
132
|
+
do {
|
133
|
+
let response = try JSONDecoder().decode(T.self, from: data)
|
134
|
+
completion(.success(response))
|
135
|
+
} catch {
|
136
|
+
print("Decode response \(url) error: \(error)")
|
137
|
+
completion(.failure(error))
|
138
|
+
}
|
139
|
+
}.resume()
|
140
|
+
}
|
141
|
+
|
142
|
+
}
|