fastlane-plugin-wpmreleasetoolkit 2.3.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_localize_libs_action.rb +8 -3
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/buildkite_trigger_build_action.rb +90 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_downloadmetadata_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_download_strings_files_from_glotpress.rb +113 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +5 -5
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_localize_project.rb +2 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_strings_files.rb +75 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_git_helper.rb +0 -20
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb +8 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/git_helper.rb +2 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_git_helper.rb +3 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +108 -173
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_linter_helper.rb +207 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/promo_screenshots_helper.rb +3 -3
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/file_reference.rb +1 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +1 -1
- metadata +23 -39
- data/bin/drawText +0 -20
- data/ext/drawText/drawText/Assets/style.css +0 -1
- data/ext/drawText/drawText/CoreTextStack.swift +0 -113
- data/ext/drawText/drawText/Helpers/CommandLineHelpers.swift +0 -36
- data/ext/drawText/drawText/Helpers/Extensions.swift +0 -27
- data/ext/drawText/drawText/Helpers/FileSystemHelper.swift +0 -24
- data/ext/drawText/drawText/Stylesheet.swift +0 -48
- data/ext/drawText/drawText/TextImage.swift +0 -100
- data/ext/drawText/drawText/main.swift +0 -61
- data/ext/drawText/drawText Tests/DigitParsingTests.swift +0 -21
- data/ext/drawText/drawText Tests/ExtensionsTests.swift +0 -5
- data/ext/drawText/drawText Tests/Info.plist +0 -22
- data/ext/drawText/drawText Tests/StylesheetTests.swift +0 -31
- data/ext/drawText/drawText Tests/Test Cases/default-stylesheet.txt +0 -10
- data/ext/drawText/drawText Tests/Test Cases/external-styles-sample.css +0 -3
- data/ext/drawText/drawText Tests/Test Cases/external-styles-test.txt +0 -13
- data/ext/drawText/drawText Tests/Test Cases/large-text-block.txt +0 -1
- data/ext/drawText/drawText Tests/Test Cases/regular-text-block.txt +0 -2
- data/ext/drawText/drawText Tests/Test Cases/rtl-text-block.txt +0 -2
- data/ext/drawText/drawText Tests/Test Cases/text-size-adjustment-test.txt +0 -10
- data/ext/drawText/drawText Tests/TextImageTests.swift +0 -99
- data/ext/drawText/drawText Tests/drawText_Tests.swift +0 -14
- data/ext/drawText/drawText.xcodeproj/project.pbxproj +0 -508
- data/ext/drawText/drawText.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- data/ext/drawText/drawText.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- data/ext/drawText/drawText.xcodeproj/xcshareddata/xcschemes/drawText Tests.xcscheme +0 -57
- data/ext/drawText/drawText.xcodeproj/xcshareddata/xcschemes/drawText.xcscheme +0 -109
- data/ext/drawText/extconf.rb +0 -36
- data/ext/drawText/makefile.example +0 -8
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_merge_translators_strings.rb +0 -106
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_metadata.rb +0 -52
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_translators_strings.rb +0 -93
@@ -1,205 +1,140 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
1
|
+
require 'fileutils'
|
2
|
+
require 'json'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'open3'
|
5
|
+
require 'open-uri'
|
6
|
+
require 'tempfile'
|
3
7
|
|
4
8
|
module Fastlane
|
5
9
|
module Helper
|
6
10
|
module Ios
|
7
11
|
class L10nHelper
|
8
|
-
|
9
|
-
DEFAULT_BASE_LANG = 'en'
|
10
|
-
CONFIG_FILE_NAME = 'swiftgen-stringtypes.yml'
|
11
|
-
|
12
|
-
attr_reader :install_path, :version
|
13
|
-
|
14
|
-
# @param [String] install_path The path to install SwiftGen to. Usually something like "$PROJECT_DIR/vendor/swiftgen/#{SWIFTGEN_VERSION}".
|
15
|
-
# It's recommended to provide an absolute path here rather than a relative one, to ensure it's not dependant on where the action is run from.
|
16
|
-
# @param [String] version The version of SwiftGen to use. This will be used both:
|
17
|
-
# - to check if the current version located in `install_path`, if it already exists, is the expected one
|
18
|
-
# - to know which version to download if there is not one installed in `install_path` yet
|
19
|
-
#
|
20
|
-
def initialize(install_path:, version: SWIFTGEN_VERSION)
|
21
|
-
@install_path = install_path
|
22
|
-
@version = version || SWIFTGEN_VERSION
|
23
|
-
end
|
24
|
-
|
25
|
-
# Check if SwiftGen is installed in the provided `install_path` and if so if the installed version matches the expected `version`
|
26
|
-
#
|
27
|
-
def check_swiftgen_installed
|
28
|
-
return false unless File.exist?(swiftgen_bin)
|
29
|
-
|
30
|
-
vers_string = `#{swiftgen_bin} --version`
|
31
|
-
# The SwiftGen version string has this format:
|
32
|
-
#
|
33
|
-
# SwiftGen v6.4.0 (Stencil v0.13.1, StencilSwiftKit v2.7.2, SwiftGenKit v6.4.0)
|
34
|
-
return vers_string.include?("SwiftGen v#{version}")
|
35
|
-
rescue
|
36
|
-
return false
|
37
|
-
end
|
38
|
-
|
39
|
-
# Download the ZIP of SwiftGen for the requested `version` and install it in the `install_path`
|
12
|
+
# Returns the type of a `.strings` file (XML, binary or ASCII)
|
40
13
|
#
|
41
|
-
# @
|
14
|
+
# @param [String] path The path to the `.strings` file to check
|
15
|
+
# @return [Symbol] The file format used by the `.strings` file. Can be one of:
|
16
|
+
# - `:text` for the ASCII-plist file format (containing typical `"key" = "value";` lines)
|
17
|
+
# - `:xml` for XML plist file format (can be used if machine-generated, especially since there's no official way/tool to generate the ASCII-plist file format as output)
|
18
|
+
# - `:binary` for binary plist file format (usually only true for `.strings` files converted by Xcode at compile time and included in the final `.app`/`.ipa`)
|
19
|
+
# - `nil` if the file does not exist or is neither of those format (e.g. not a `.strings` file at all)
|
42
20
|
#
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
21
|
+
def self.strings_file_type(path:)
|
22
|
+
# Start by checking it seems like a valid property-list file (and not e.g. an image or plain text file)
|
23
|
+
_, status = Open3.capture2('/usr/bin/plutil', '-lint', path)
|
24
|
+
return nil unless status.success?
|
25
|
+
|
26
|
+
# If it is a valid property-list file, determine the actual format used
|
27
|
+
format_desc, status = Open3.capture2('/usr/bin/file', path)
|
28
|
+
return nil unless status.success?
|
29
|
+
|
30
|
+
case format_desc
|
31
|
+
when /Apple binary property list/ then return :binary
|
32
|
+
when /XML/ then return :xml
|
33
|
+
when /text/ then return :text
|
54
34
|
end
|
55
35
|
end
|
56
36
|
|
57
|
-
#
|
37
|
+
# Merge the content of multiple `.strings` files into a new `.strings` text file.
|
58
38
|
#
|
59
|
-
# @param [String]
|
60
|
-
# @param [String]
|
61
|
-
# @return [
|
62
|
-
# and the values are the output of the `diff` showing these violations.
|
39
|
+
# @param [Array<String>] paths The paths of the `.strings` files to merge together
|
40
|
+
# @param [String] into The path to the merged `.strings` file to generate as a result.
|
41
|
+
# @return [Array<String>] List of duplicate keys found while validating the merge.
|
63
42
|
#
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
##################
|
70
|
-
|
71
|
-
private
|
72
|
-
|
73
|
-
# Path to the swiftgen binary installed at install_path
|
74
|
-
def swiftgen_bin
|
75
|
-
"#{install_path}/bin/swiftgen"
|
76
|
-
end
|
77
|
-
|
78
|
-
# Name to use for the generated files / output files of SwiftGen for each locale. Those files will be generated in the temporary directory to then diff them.
|
79
|
-
def output_filename(lang)
|
80
|
-
"L10nParamsList.#{lang}.txt"
|
81
|
-
end
|
82
|
-
|
83
|
-
# The Stencil template that we want SwiftGen to use to generate the output.
|
84
|
-
# It iterates on every "table" (`.strings` file, in most cases there's only one, `Localizable.strings`),
|
85
|
-
# and for each, iterates on every entry found to print the key and the corresponding types parsed by SwiftGen from the placeholders found in that translation
|
86
|
-
def template_content
|
87
|
-
<<~TEMPLATE
|
88
|
-
{% macro recursiveBlock table item %}
|
89
|
-
{% for string in item.strings %}
|
90
|
-
"{{string.key}}" => [{{string.types|join:","}}]
|
91
|
-
{% endfor %}
|
92
|
-
{% for child in item.children %}
|
93
|
-
{% call recursiveBlock table child %}
|
94
|
-
{% endfor %}
|
95
|
-
{% endmacro %}
|
96
|
-
|
97
|
-
{% for table in tables %}
|
98
|
-
{% call recursiveBlock table.name table.levels %}
|
99
|
-
{% endfor %}
|
100
|
-
TEMPLATE
|
101
|
-
end
|
102
|
-
|
103
|
-
# Create the template file and the config file, in the temp dir, to be used by SwiftGen when parsing the input files.
|
43
|
+
# @note For now, this method only supports merging `.strings` file in `:text` format
|
44
|
+
# and basically concatenates the files (+ checking for duplicates in the process)
|
45
|
+
# @note The method is able to handle input files which are using different encodings,
|
46
|
+
# guessing the encoding of each input file using the BOM (and defaulting to UTF8).
|
47
|
+
# The generated file will always be in utf-8, by convention.
|
104
48
|
#
|
105
|
-
# @
|
49
|
+
# @raise [RuntimeError] If one of the paths provided is not in text format (but XML or binary instead), or if any of the files are missing.
|
106
50
|
#
|
107
|
-
def
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
{
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
'options' => { 'separator' => '____' },
|
127
|
-
'outputs' => [{
|
128
|
-
'templatePath' => template_path,
|
129
|
-
'output' => output_filename(lang)
|
130
|
-
}]
|
131
|
-
}
|
51
|
+
def self.merge_strings(paths:, output_path: nil)
|
52
|
+
duplicates = []
|
53
|
+
Tempfile.create('wpmrt-l10n-merge-', encoding: 'utf-8') do |tmp_file|
|
54
|
+
all_keys_found = []
|
55
|
+
|
56
|
+
tmp_file.write("/* Generated File. Do not edit. */\n\n")
|
57
|
+
paths.each do |input_file|
|
58
|
+
fmt = strings_file_type(path: input_file)
|
59
|
+
raise "The file `#{input_file}` does not exist or is of unknown format." if fmt.nil?
|
60
|
+
raise "The file `#{input_file}` is in #{fmt} format but we currently only support merging `.strings` files in text format." unless fmt == :text
|
61
|
+
|
62
|
+
string_keys = read_strings_file_as_hash(path: input_file).keys
|
63
|
+
duplicates += (string_keys & all_keys_found) # Find duplicates using Array intersection, and add those to duplicates list
|
64
|
+
all_keys_found += string_keys
|
65
|
+
|
66
|
+
tmp_file.write("/* MARK: - #{File.basename(input_file)} */\n\n")
|
67
|
+
# Read line-by-line to reduce memory footprint during content copy; Be sure to guess file encoding using the Byte-Order-Mark.
|
68
|
+
File.readlines(input_file, mode: 'rb:BOM|UTF-8').each { |line| tmp_file.write(line) }
|
69
|
+
tmp_file.write("\n")
|
132
70
|
end
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
File.write(config_file, config.to_yaml)
|
138
|
-
|
139
|
-
return [config_file, langs]
|
71
|
+
tmp_file.close # ensure we flush the content to disk
|
72
|
+
FileUtils.cp(tmp_file.path, output_path)
|
73
|
+
end
|
74
|
+
duplicates
|
140
75
|
end
|
141
76
|
|
142
|
-
#
|
143
|
-
# We need to sort the output again because SwiftGen only sort case-insensitively so that means keys that are
|
144
|
-
# the same except case might be in swapped order for different outputs
|
77
|
+
# Return the list of translations in a `.strings` file.
|
145
78
|
#
|
146
|
-
# @param [String]
|
147
|
-
# @
|
79
|
+
# @param [String] path The path to the `.strings` file to read
|
80
|
+
# @return [Hash<String,String>] A dictionary of key=>translation translations.
|
81
|
+
# @raise [RuntimeError] If the file is not a valid strings file or there was an error in parsing its content.
|
148
82
|
#
|
149
|
-
def
|
150
|
-
|
151
|
-
|
83
|
+
def self.read_strings_file_as_hash(path:)
|
84
|
+
output, status = Open3.capture2e('/usr/bin/plutil', '-convert', 'json', '-o', '-', path)
|
85
|
+
raise output unless status.success?
|
152
86
|
|
153
|
-
|
154
|
-
File.write(file, sorted_lines.join)
|
155
|
-
return file
|
87
|
+
JSON.parse(output)
|
156
88
|
end
|
157
89
|
|
158
|
-
#
|
90
|
+
# Generate a `.strings` file from a dictionary of string translations.
|
159
91
|
#
|
160
|
-
#
|
161
|
-
#
|
162
|
-
# @param [Array<String>] only_langs The list of languages to limit the generation for. Useful to focus only on a couple of issues or just one language
|
163
|
-
# @return [Hash<String, String>] A hash of violations, keyed by language code, whose values are the diff output.
|
92
|
+
# Especially useful to generate `.strings` files not from code, but from keys extracted from another source
|
93
|
+
# (like a JSON file export from GlotPress, or subset of keys extracted from the main `Localizable.strings` to generate an `InfoPlist.strings`)
|
164
94
|
#
|
165
|
-
# @note The
|
95
|
+
# @note The generated file will be in XML-plist format
|
96
|
+
# since ASCII plist is deprecated as an output format by every Apple tool so there's no **safe** way to generate ASCII format.
|
166
97
|
#
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
# and also to serve as a clear visual separator between diff entries in the output.
|
187
|
-
# Those numbers won't be matching the original `.strings` file line numbers because they are line numbers in the SwiftGen-generated intermediate
|
188
|
-
# file instead, but they can still give an indication at the index in the list of keys at which this difference is located.
|
189
|
-
diff.gsub!(/^(---|\+\+\+).*\n/, '')
|
190
|
-
diff.empty? ? nil : [lang, diff]
|
191
|
-
end.compact.to_h
|
98
|
+
# @param [Hash<String,String>] translations The dictionary of key=>translation translations to put in the generated `.strings` file
|
99
|
+
# @param [String] output_path The path to the `.strings` file to generate
|
100
|
+
#
|
101
|
+
def self.generate_strings_file_from_hash(translations:, output_path:)
|
102
|
+
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
103
|
+
xml.doc.create_internal_subset(
|
104
|
+
'plist',
|
105
|
+
'-//Apple//DTD PLIST 1.0//EN',
|
106
|
+
'http://www.apple.com/DTDs/PropertyList-1.0.dtd'
|
107
|
+
)
|
108
|
+
xml.comment('Warning: Auto-generated file, do not edit.')
|
109
|
+
xml.plist(version: '1.0') do
|
110
|
+
xml.dict do
|
111
|
+
translations.each do |k, v|
|
112
|
+
xml.key(k.to_s)
|
113
|
+
xml.string(v.to_s)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
192
117
|
end
|
118
|
+
File.write(output_path, builder.to_xml)
|
193
119
|
end
|
194
120
|
|
195
|
-
#
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
121
|
+
# Downloads the export from GlotPress for a given locale and given filters.
|
122
|
+
#
|
123
|
+
# @param [String] project_url The URL to the GlotPress project to export from, e.g. `"https://translate.wordpress.org/projects/apps/ios/dev"`
|
124
|
+
# @param [String] locale The GlotPress locale code to download strings for.
|
125
|
+
# @param [Hash{Symbol=>String}] filters The hash of filters to apply when exporting from GlotPress.
|
126
|
+
# Typical examples include `{ status: 'current' }` or `{ status: 'review' }`.
|
127
|
+
# @param [String, IO] destination The path or `IO`-like instance, where to write the downloaded file on disk.
|
128
|
+
#
|
129
|
+
def self.download_glotpress_export_file(project_url:, locale:, filters:, destination:)
|
130
|
+
query_params = (filters || {}).transform_keys { |k| "filters[#{k}]" }.merge(format: 'strings')
|
131
|
+
uri = URI.parse("#{project_url.chomp('/')}/#{locale}/default/export-translations?#{URI.encode_www_form(query_params)}")
|
132
|
+
begin
|
133
|
+
IO.copy_stream(uri.open, destination)
|
134
|
+
rescue StandardError => e
|
135
|
+
UI.error "Error downloading locale `#{locale}` — #{e.message}"
|
136
|
+
return nil
|
201
137
|
end
|
202
|
-
return true
|
203
138
|
end
|
204
139
|
end
|
205
140
|
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'tmpdir'
|
3
|
+
|
4
|
+
module Fastlane
|
5
|
+
module Helper
|
6
|
+
module Ios
|
7
|
+
class L10nLinterHelper
|
8
|
+
SWIFTGEN_VERSION = '6.4.0'
|
9
|
+
DEFAULT_BASE_LANG = 'en'
|
10
|
+
CONFIG_FILE_NAME = 'swiftgen-stringtypes.yml'
|
11
|
+
|
12
|
+
attr_reader :install_path, :version
|
13
|
+
|
14
|
+
# @param [String] install_path The path to install SwiftGen to. Usually something like "$PROJECT_DIR/vendor/swiftgen/#{SWIFTGEN_VERSION}".
|
15
|
+
# It's recommended to provide an absolute path here rather than a relative one, to ensure it's not dependant on where the action is run from.
|
16
|
+
# @param [String] version The version of SwiftGen to use. This will be used both:
|
17
|
+
# - to check if the current version located in `install_path`, if it already exists, is the expected one
|
18
|
+
# - to know which version to download if there is not one installed in `install_path` yet
|
19
|
+
#
|
20
|
+
def initialize(install_path:, version: SWIFTGEN_VERSION)
|
21
|
+
@install_path = install_path
|
22
|
+
@version = version || SWIFTGEN_VERSION
|
23
|
+
end
|
24
|
+
|
25
|
+
# Check if SwiftGen is installed in the provided `install_path` and if so if the installed version matches the expected `version`
|
26
|
+
#
|
27
|
+
def check_swiftgen_installed
|
28
|
+
return false unless File.exist?(swiftgen_bin)
|
29
|
+
|
30
|
+
vers_string = `#{swiftgen_bin} --version`
|
31
|
+
# The SwiftGen version string has this format:
|
32
|
+
#
|
33
|
+
# SwiftGen v6.4.0 (Stencil v0.13.1, StencilSwiftKit v2.7.2, SwiftGenKit v6.4.0)
|
34
|
+
return vers_string.include?("SwiftGen v#{version}")
|
35
|
+
rescue
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
|
39
|
+
# Download the ZIP of SwiftGen for the requested `version` and install it in the `install_path`
|
40
|
+
#
|
41
|
+
# @note This action nukes anything at `install_path` – if something already exists – prior to install SwiftGen there
|
42
|
+
#
|
43
|
+
def install_swiftgen!
|
44
|
+
UI.message "Installing SwiftGen #{version} into #{install_path}"
|
45
|
+
Dir.mktmpdir do |tmpdir|
|
46
|
+
zipfile = File.join(tmpdir, "swiftgen-#{version}.zip")
|
47
|
+
Action.sh('curl', '--fail', '--location', '-o', zipfile, "https://github.com/SwiftGen/SwiftGen/releases/download/#{version}/swiftgen-#{version}.zip")
|
48
|
+
extracted_dir = File.join(tmpdir, "swiftgen-#{version}")
|
49
|
+
Action.sh('unzip', zipfile, '-d', extracted_dir)
|
50
|
+
|
51
|
+
FileUtils.rm_rf(install_path) if File.exist?(install_path)
|
52
|
+
FileUtils.mkdir_p(install_path)
|
53
|
+
FileUtils.cp_r("#{extracted_dir}/.", install_path)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Install SwiftGen if necessary (if not installed yet with the expected version), then run the checks and returns the violations found, if any
|
58
|
+
#
|
59
|
+
# @param [String] input_dir The path (ideally absolute) to the directory containing the `.lproj` folders to parse
|
60
|
+
# @param [String] base_lang The code name (i.e the basename of one of the `.lproj` folders) of the locale to use as the baseline
|
61
|
+
# @return [Hash<String, String>] A hash whose keys are the language codes (basename of `.lproj` folders) for which violations were found,
|
62
|
+
# and the values are the output of the `diff` showing these violations.
|
63
|
+
#
|
64
|
+
def run(input_dir:, base_lang: DEFAULT_BASE_LANG, only_langs: nil)
|
65
|
+
check_swiftgen_installed || install_swiftgen!
|
66
|
+
find_diffs(input_dir: input_dir, base_lang: base_lang, only_langs: only_langs)
|
67
|
+
end
|
68
|
+
|
69
|
+
##################
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Path to the swiftgen binary installed at install_path
|
74
|
+
def swiftgen_bin
|
75
|
+
"#{install_path}/bin/swiftgen"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Name to use for the generated files / output files of SwiftGen for each locale. Those files will be generated in the temporary directory to then diff them.
|
79
|
+
def output_filename(lang)
|
80
|
+
"L10nParamsList.#{lang}.txt"
|
81
|
+
end
|
82
|
+
|
83
|
+
# The Stencil template that we want SwiftGen to use to generate the output.
|
84
|
+
# It iterates on every "table" (`.strings` file, in most cases there's only one, `Localizable.strings`),
|
85
|
+
# and for each, iterates on every entry found to print the key and the corresponding types parsed by SwiftGen from the placeholders found in that translation
|
86
|
+
def template_content
|
87
|
+
<<~TEMPLATE
|
88
|
+
{% macro recursiveBlock table item %}
|
89
|
+
{% for string in item.strings %}
|
90
|
+
"{{string.key}}" => [{{string.types|join:","}}]
|
91
|
+
{% endfor %}
|
92
|
+
{% for child in item.children %}
|
93
|
+
{% call recursiveBlock table child %}
|
94
|
+
{% endfor %}
|
95
|
+
{% endmacro %}
|
96
|
+
|
97
|
+
{% for table in tables %}
|
98
|
+
{% call recursiveBlock table.name table.levels %}
|
99
|
+
{% endfor %}
|
100
|
+
TEMPLATE
|
101
|
+
end
|
102
|
+
|
103
|
+
# Create the template file and the config file, in the temp dir, to be used by SwiftGen when parsing the input files.
|
104
|
+
#
|
105
|
+
# @return [(String, Array<String>)] A tuple of (config_file_absolute_path, Array<langs>)
|
106
|
+
#
|
107
|
+
def generate_swiftgen_config!(input_dir, output_dir, only_langs: nil)
|
108
|
+
# Create the template file
|
109
|
+
template_path = File.absolute_path(File.join(output_dir, 'strings-types.stencil'))
|
110
|
+
File.write(template_path, template_content)
|
111
|
+
|
112
|
+
# Dynamically create a SwiftGen config which will cover all supported languages
|
113
|
+
langs = Dir.chdir(input_dir) do
|
114
|
+
Dir.glob('*.lproj/Localizable.strings').map { |loc_file| File.basename(File.dirname(loc_file), '.lproj') }
|
115
|
+
end.sort
|
116
|
+
langs.select! { |lang| only_langs.include?(lang) } unless only_langs.nil?
|
117
|
+
|
118
|
+
config = {
|
119
|
+
'input_dir' => input_dir,
|
120
|
+
'output_dir' => output_dir,
|
121
|
+
'strings' => langs.map do |lang|
|
122
|
+
{
|
123
|
+
'inputs' => ["#{lang}.lproj/Localizable.strings"],
|
124
|
+
# Choose an unlikely separator (instead of the default '.') to avoid creating needlessly complex Stencil Context nested
|
125
|
+
# structure just because we have '.' in the English sentences we use (instead of structured reverse-dns notation) for the keys
|
126
|
+
'options' => { 'separator' => '____' },
|
127
|
+
'outputs' => [{
|
128
|
+
'templatePath' => template_path,
|
129
|
+
'output' => output_filename(lang)
|
130
|
+
}]
|
131
|
+
}
|
132
|
+
end
|
133
|
+
}
|
134
|
+
|
135
|
+
# Write SwiftGen config file
|
136
|
+
config_file = File.join(output_dir, CONFIG_FILE_NAME)
|
137
|
+
File.write(config_file, config.to_yaml)
|
138
|
+
|
139
|
+
return [config_file, langs]
|
140
|
+
end
|
141
|
+
|
142
|
+
# Because we use English copy verbatim as key names, some keys are the same except for the upper/lowercase.
|
143
|
+
# We need to sort the output again because SwiftGen only sort case-insensitively so that means keys that are
|
144
|
+
# the same except case might be in swapped order for different outputs
|
145
|
+
#
|
146
|
+
# @param [String] dir The temporary directory in which the file to sort lines for is located
|
147
|
+
# @param [String] lang The code for the locale we need to sort the output lines for
|
148
|
+
#
|
149
|
+
def sort_file_lines!(dir, lang)
|
150
|
+
file = File.join(dir, output_filename(lang))
|
151
|
+
return nil unless File.exist?(file)
|
152
|
+
|
153
|
+
sorted_lines = File.readlines(file).sort
|
154
|
+
File.write(file, sorted_lines.join)
|
155
|
+
return file
|
156
|
+
end
|
157
|
+
|
158
|
+
# Prepares the template and config files, then run SwiftGen, run `diff` on each generated output against the baseline, and returns a Hash of the violations found.
|
159
|
+
#
|
160
|
+
# @param [String] input_dir The directory where the `.lproj` folders to scan are located
|
161
|
+
# @param [String] base_lang The base language used as source of truth that all other languages will be compared against
|
162
|
+
# @param [Array<String>] only_langs The list of languages to limit the generation for. Useful to focus only on a couple of issues or just one language
|
163
|
+
# @return [Hash<String, String>] A hash of violations, keyed by language code, whose values are the diff output.
|
164
|
+
#
|
165
|
+
# @note The returned Hash contains keys only for locales with violations. Locales parsed but without any violations found will not appear in the resulting hash.
|
166
|
+
#
|
167
|
+
def find_diffs(input_dir:, base_lang:, only_langs: nil)
|
168
|
+
Dir.mktmpdir('a8c-lint-translations-') do |tmpdir|
|
169
|
+
# Run SwiftGen
|
170
|
+
langs = only_langs.nil? ? nil : (only_langs + [base_lang]).uniq
|
171
|
+
(config_file, langs) = generate_swiftgen_config!(input_dir, tmpdir, only_langs: langs)
|
172
|
+
Action.sh(swiftgen_bin, 'config', 'run', '--config', config_file)
|
173
|
+
|
174
|
+
# Run diffs
|
175
|
+
base_file = sort_file_lines!(tmpdir, base_lang)
|
176
|
+
langs.delete(base_lang)
|
177
|
+
return langs.map do |lang|
|
178
|
+
file = sort_file_lines!(tmpdir, lang)
|
179
|
+
# If the lang ends up not having any translation at all (e.g. a `.lproj` without any `.strings` file in it but maybe just a storyboard or assets catalog), ignore it
|
180
|
+
next nil if file.nil? || only_empty_lines?(file)
|
181
|
+
|
182
|
+
# Compute the diff
|
183
|
+
diff = `diff -U0 "#{base_file}" "#{file}"`
|
184
|
+
# Remove the lines starting with `---`/`+++` which contains the file names (which are temp files we don't want to expose in the final diff to users)
|
185
|
+
# Note: We still keep the `@@ from-file-line-numbers to-file-line-numbers @@` lines to help the user know the index of the key to find it faster,
|
186
|
+
# and also to serve as a clear visual separator between diff entries in the output.
|
187
|
+
# Those numbers won't be matching the original `.strings` file line numbers because they are line numbers in the SwiftGen-generated intermediate
|
188
|
+
# file instead, but they can still give an indication at the index in the list of keys at which this difference is located.
|
189
|
+
diff.gsub!(/^(---|\+\+\+).*\n/, '')
|
190
|
+
diff.empty? ? nil : [lang, diff]
|
191
|
+
end.compact.to_h
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Returns true if the file only contains empty lines, i.e. lines that only contains whitespace (space, tab, CR, LF)
|
196
|
+
def only_empty_lines?(file)
|
197
|
+
File.open(file) do |f|
|
198
|
+
while (line = f.gets)
|
199
|
+
return false unless line.strip.empty?
|
200
|
+
end
|
201
|
+
end
|
202
|
+
return true
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -28,6 +28,8 @@ module Fastlane
|
|
28
28
|
message << 'Aborting.'
|
29
29
|
UI.user_error!(message)
|
30
30
|
end
|
31
|
+
|
32
|
+
UI.user_error!('`drawText` not found – install it using `brew install automattic/build-tools/drawText`.') unless system('command -v drawText')
|
31
33
|
end
|
32
34
|
|
33
35
|
def read_json(configFilePath)
|
@@ -233,9 +235,7 @@ module Fastlane
|
|
233
235
|
begin
|
234
236
|
tempTextFile = Tempfile.new()
|
235
237
|
|
236
|
-
|
237
|
-
|
238
|
-
UI.crash!('Unable to draw text') unless system(command)
|
238
|
+
sh('drawText', "html=\"#{text}\"", "maxWidth=#{width}", "maxHeight=#{height}", "output=\"#{tempTextFile.path}\"", "fontSize=#{font_size}", "stylesheet=\"#{stylesheet_path}\"", "alignment=\"#{position}\"")
|
239
239
|
|
240
240
|
text_content = open_image(tempTextFile.path).trim
|
241
241
|
text_frame = create_image(width, height)
|
@@ -10,10 +10,7 @@ module Fastlane
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def source_contents
|
13
|
-
|
14
|
-
# Fastlane::is_ci doesn't work here, so reimplementing the code has been necessary.
|
15
|
-
# (This should be updated if we change CI or if fastlane is updated.)
|
16
|
-
return File.read(secrets_repository_file_path) unless self.encrypt || ENV.key?('CIRCLECI')
|
13
|
+
return File.read(secrets_repository_file_path) unless self.encrypt || FastlaneCore::Helper.is_ci?
|
17
14
|
return nil unless File.file?(encrypted_file_path)
|
18
15
|
|
19
16
|
encrypted = File.read(encrypted_file_path)
|