ETLane 0.1.42 → 0.1.46
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 +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
|
+
}
|