fastlane-plugin-wpmreleasetoolkit 2.3.0 → 4.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.
- 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/common/upload_to_s3.rb +112 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_download_strings_files_from_glotpress.rb +114 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_extract_keys_from_strings_files.rb +136 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_generate_strings_file_from_code.rb +1 -1
- 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 +79 -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 +120 -170
- 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 +25 -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
@@ -27,26 +27,6 @@ module Fastlane
|
|
27
27
|
)
|
28
28
|
end
|
29
29
|
end
|
30
|
-
|
31
|
-
# Calls the `tools/update-translations.sh` script from the project repo, then lint them using the provided gradle task
|
32
|
-
#
|
33
|
-
# Deprecated. Use the `android_download_translations` action instead.
|
34
|
-
#
|
35
|
-
# @env PROJECT_ROOT_FOLDER The path to the git root of the project
|
36
|
-
# @env PROJECT_NAME The name of the directory containing the project code (especially containing the `build.gradle` file)
|
37
|
-
#
|
38
|
-
# @param [String] validate_translation_command The name of the gradle task to run to validate the translations
|
39
|
-
#
|
40
|
-
# @todo Remove this once every client has migrated to `android_download_translations` and we got rid of that legacy action.
|
41
|
-
#
|
42
|
-
def self.update_metadata(validate_translation_command)
|
43
|
-
Action.sh('./tools/update-translations.sh')
|
44
|
-
Action.sh('git', 'submodule', 'update', '--init', '--recursive')
|
45
|
-
Action.sh('./gradlew', validate_translation_command)
|
46
|
-
|
47
|
-
res_dir = File.join(ENV['PROJECT_ROOT_FOLDER'], ENV['PROJECT_NAME'], 'src', 'main', 'res')
|
48
|
-
Fastlane::Helper::GitHelper.commit(message: 'Update translations', files: res_dir, push: true)
|
49
|
-
end
|
50
30
|
end
|
51
31
|
end
|
52
32
|
end
|
@@ -92,6 +92,14 @@ module Fastlane
|
|
92
92
|
UI.user_error!("String #{string_name} [#{string_content}] was found in library #{library[:library]} but not in the main file.")
|
93
93
|
end
|
94
94
|
|
95
|
+
# Merge strings from a library into the strings.xml of the main app
|
96
|
+
#
|
97
|
+
# @param [String] main Path to the main strings.xml file (something like `…/res/values/strings.xml`)
|
98
|
+
# @param [Hash] library Hash describing the library to merge. The Hash should contain the following keys:
|
99
|
+
# - `:library`: The human readable name of the library, used to display in console messages
|
100
|
+
# - `:strings_path`: The path to the strings.xml file of the library to merge into the main one
|
101
|
+
# - `:exclusions`: An array of strings keys to exclude during merge. Any of those keys from the library's `strings.xml` will be skipped and won't be merged into the main one.
|
102
|
+
# @return [Boolean] True if at least one string from the library has been added to (or has updated) the main strings file.
|
95
103
|
def self.merge_lib(main, library)
|
96
104
|
UI.message("Merging #{library[:library]} strings into #{main}")
|
97
105
|
main_strings = File.open(main) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
|
@@ -5,10 +5,8 @@ module Fastlane
|
|
5
5
|
# Helper methods to execute git-related operations
|
6
6
|
#
|
7
7
|
module GitHelper
|
8
|
-
# Fallback default branch of the client repository.
|
9
|
-
|
10
|
-
# TODO: Set to 'trunk' for the next major release.
|
11
|
-
DEFAULT_GIT_BRANCH = 'develop'.freeze
|
8
|
+
# Fallback default branch of the client repository.
|
9
|
+
DEFAULT_GIT_BRANCH = 'trunk'.freeze
|
12
10
|
|
13
11
|
# Checks if the given path, or current directory if no path is given, is
|
14
12
|
# inside a Git repository
|
@@ -35,8 +35,9 @@ module Fastlane
|
|
35
35
|
# @env PROJECT_ROOT_FOLDER The path to the git root of the project
|
36
36
|
# @env PROJECT_NAME The name of the directory containing the project code (especially containing the `build.gradle` file)
|
37
37
|
#
|
38
|
-
# @
|
39
|
-
#
|
38
|
+
# @deprecated This method is only used by the `ios_localize_project` action, which is itself deprecated
|
39
|
+
# in favor of the new `ios_generate_strings_file_from_code` action
|
40
|
+
# @todo [Next Major] Remove this method once we fully remove `ios_localize_project`
|
40
41
|
#
|
41
42
|
def self.localize_project
|
42
43
|
Action.sh("cd #{get_from_env!(key: 'PROJECT_ROOT_FOLDER')} && ./Scripts/localize.py")
|
@@ -1,205 +1,155 @@
|
|
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
|
12
|
+
# Returns the type of a `.strings` file (XML, binary or ASCII)
|
19
13
|
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
#
|
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)
|
26
20
|
#
|
27
|
-
def
|
28
|
-
return
|
21
|
+
def self.strings_file_type(path:)
|
22
|
+
return :text if File.empty?(path) # If completely empty file, consider it as a valid `.strings` files in textual format
|
29
23
|
|
30
|
-
|
31
|
-
|
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
|
24
|
+
# Start by checking it seems like a valid property-list file (and not e.g. an image or plain text file)
|
25
|
+
_, status = Open3.capture2('/usr/bin/plutil', '-lint', path)
|
26
|
+
return nil unless status.success?
|
38
27
|
|
39
|
-
|
40
|
-
|
41
|
-
|
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)
|
28
|
+
# If it is a valid property-list file, determine the actual format used
|
29
|
+
format_desc, status = Open3.capture2('/usr/bin/file', path)
|
30
|
+
return nil unless status.success?
|
50
31
|
|
51
|
-
|
52
|
-
|
53
|
-
|
32
|
+
case format_desc
|
33
|
+
when /Apple binary property list/ then return :binary
|
34
|
+
when /XML/ then return :xml
|
35
|
+
when /text/ then return :text
|
54
36
|
end
|
55
37
|
end
|
56
38
|
|
57
|
-
#
|
39
|
+
# Merge the content of multiple `.strings` files into a new `.strings` text file.
|
58
40
|
#
|
59
|
-
# @param [String]
|
60
|
-
# @param [String]
|
61
|
-
# @return [
|
62
|
-
# and the values are the output of the `diff` showing these violations.
|
41
|
+
# @param [Hash<String, String>] paths The paths of the `.strings` files to merge together, associated with the prefix to prepend to each of their respective keys
|
42
|
+
# @param [String] output_path The path to the merged `.strings` file to generate as a result.
|
43
|
+
# @return [Array<String>] List of duplicate keys found while validating the merge.
|
63
44
|
#
|
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.
|
45
|
+
# @note For now, this method only supports merging `.strings` file in `:text` format
|
46
|
+
# and basically concatenates the files (+ checking for duplicates in the process)
|
47
|
+
# @note The method is able to handle input files which are using different encodings,
|
48
|
+
# guessing the encoding of each input file using the BOM (and defaulting to UTF8).
|
49
|
+
# The generated file will always be in utf-8, by convention.
|
104
50
|
#
|
105
|
-
# @
|
51
|
+
# @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
52
|
#
|
107
|
-
def
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
53
|
+
def self.merge_strings(paths:, output_path:)
|
54
|
+
duplicates = []
|
55
|
+
Tempfile.create('wpmrt-l10n-merge-', encoding: 'utf-8') do |tmp_file|
|
56
|
+
all_keys_found = []
|
57
|
+
|
58
|
+
tmp_file.write("/* Generated File. Do not edit. */\n\n")
|
59
|
+
paths.each do |input_file, prefix|
|
60
|
+
next if File.empty?(input_file) # Skip existing but totally empty files, to avoid adding useless `MARK:` comment for them
|
61
|
+
|
62
|
+
fmt = strings_file_type(path: input_file)
|
63
|
+
raise "The file `#{input_file}` does not exist or is of unknown format." if fmt.nil?
|
64
|
+
raise "The file `#{input_file}` is in #{fmt} format but we currently only support merging `.strings` files in text format." unless fmt == :text
|
65
|
+
|
66
|
+
string_keys = read_strings_file_as_hash(path: input_file).keys.map { |k| "#{prefix}#{k}" }
|
67
|
+
duplicates += (string_keys & all_keys_found) # Find duplicates using Array intersection, and add those to duplicates list
|
68
|
+
all_keys_found += string_keys
|
69
|
+
|
70
|
+
tmp_file.write("/* MARK: - #{File.basename(input_file)} */\n\n")
|
71
|
+
# Read line-by-line to reduce memory footprint during content copy; Be sure to guess file encoding using the Byte-Order-Mark.
|
72
|
+
File.readlines(input_file, mode: 'rb:BOM|UTF-8').each do |line|
|
73
|
+
unless prefix.nil? || prefix.empty?
|
74
|
+
# We need to ensure the line and RegExp are using the same encoding, so we transcode everything to UTF-8.
|
75
|
+
line.encode!(Encoding::UTF_8)
|
76
|
+
# The `/u` modifier on the RegExps is to make them UTF-8
|
77
|
+
line.gsub!(/^(\s*")/u, "\\1#{prefix}") # Lines starting with a quote are considered to be start of a key; add prefix right after the quote
|
78
|
+
line.gsub!(/^(\s*)([A-Z0-9_]+)(\s*=\s*")/ui, "\\1\"#{prefix}\\2\"\\3") # Lines starting with an identifier followed by a '=' are considered to be an unquoted key (typical in InfoPlist.strings files for example)
|
79
|
+
end
|
80
|
+
tmp_file.write(line)
|
81
|
+
end
|
82
|
+
tmp_file.write("\n")
|
132
83
|
end
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
File.write(config_file, config.to_yaml)
|
138
|
-
|
139
|
-
return [config_file, langs]
|
84
|
+
tmp_file.close # ensure we flush the content to disk
|
85
|
+
FileUtils.cp(tmp_file.path, output_path)
|
86
|
+
end
|
87
|
+
duplicates
|
140
88
|
end
|
141
89
|
|
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
|
90
|
+
# Return the list of translations in a `.strings` file.
|
145
91
|
#
|
146
|
-
# @param [String]
|
147
|
-
# @
|
92
|
+
# @param [String] path The path to the `.strings` file to read
|
93
|
+
# @return [Hash<String,String>] A dictionary of key=>translation translations.
|
94
|
+
# @raise [RuntimeError] If the file is not a valid strings file or there was an error in parsing its content.
|
148
95
|
#
|
149
|
-
def
|
150
|
-
|
151
|
-
|
96
|
+
def self.read_strings_file_as_hash(path:)
|
97
|
+
return {} if File.empty?(path) # Return empty hash if completely empty file
|
98
|
+
|
99
|
+
output, status = Open3.capture2e('/usr/bin/plutil', '-convert', 'json', '-o', '-', path)
|
100
|
+
raise output unless status.success?
|
152
101
|
|
153
|
-
|
154
|
-
File.write(file, sorted_lines.join)
|
155
|
-
return file
|
102
|
+
JSON.parse(output)
|
156
103
|
end
|
157
104
|
|
158
|
-
#
|
105
|
+
# Generate a `.strings` file from a dictionary of string translations.
|
159
106
|
#
|
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.
|
107
|
+
# Especially useful to generate `.strings` files not from code, but from keys extracted from another source
|
108
|
+
# (like a JSON file export from GlotPress, or subset of keys extracted from the main `Localizable.strings` to generate an `InfoPlist.strings`)
|
164
109
|
#
|
165
|
-
# @note The
|
110
|
+
# @note The generated file will be in XML-plist format
|
111
|
+
# since ASCII plist is deprecated as an output format by every Apple tool so there's no **safe** way to generate ASCII format.
|
166
112
|
#
|
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
|
113
|
+
# @param [Hash<String,String>] translations The dictionary of key=>translation translations to put in the generated `.strings` file
|
114
|
+
# @param [String] output_path The path to the `.strings` file to generate
|
115
|
+
#
|
116
|
+
def self.generate_strings_file_from_hash(translations:, output_path:)
|
117
|
+
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
118
|
+
xml.doc.create_internal_subset(
|
119
|
+
'plist',
|
120
|
+
'-//Apple//DTD PLIST 1.0//EN',
|
121
|
+
'http://www.apple.com/DTDs/PropertyList-1.0.dtd'
|
122
|
+
)
|
123
|
+
xml.comment('Warning: Auto-generated file, do not edit.')
|
124
|
+
xml.plist(version: '1.0') do
|
125
|
+
xml.dict do
|
126
|
+
translations.sort.each do |k, v| # NOTE: use `sort` just in order to be deterministic over various runs
|
127
|
+
xml.key(k.to_s)
|
128
|
+
xml.string(v.to_s)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
192
132
|
end
|
133
|
+
File.write(output_path, builder.to_xml)
|
193
134
|
end
|
194
135
|
|
195
|
-
#
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
136
|
+
# Downloads the export from GlotPress for a given locale and given filters.
|
137
|
+
#
|
138
|
+
# @param [String] project_url The URL to the GlotPress project to export from, e.g. `"https://translate.wordpress.org/projects/apps/ios/dev"`
|
139
|
+
# @param [String] locale The GlotPress locale code to download strings for.
|
140
|
+
# @param [Hash{Symbol=>String}] filters The hash of filters to apply when exporting from GlotPress.
|
141
|
+
# Typical examples include `{ status: 'current' }` or `{ status: 'review' }`.
|
142
|
+
# @param [String, IO] destination The path or `IO`-like instance, where to write the downloaded file on disk.
|
143
|
+
#
|
144
|
+
def self.download_glotpress_export_file(project_url:, locale:, filters:, destination:)
|
145
|
+
query_params = (filters || {}).transform_keys { |k| "filters[#{k}]" }.merge(format: 'strings')
|
146
|
+
uri = URI.parse("#{project_url.chomp('/')}/#{locale}/default/export-translations?#{URI.encode_www_form(query_params)}")
|
147
|
+
begin
|
148
|
+
IO.copy_stream(uri.open, destination)
|
149
|
+
rescue StandardError => e
|
150
|
+
UI.error "Error downloading locale `#{locale}` — #{e.message}"
|
151
|
+
return nil
|
201
152
|
end
|
202
|
-
return true
|
203
153
|
end
|
204
154
|
end
|
205
155
|
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)
|