fastlane-plugin-wpmreleasetoolkit 2.3.0 → 3.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_localize_libs_action.rb +8 -3
  3. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/buildkite_trigger_build_action.rb +90 -0
  4. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_downloadmetadata_action.rb +1 -1
  5. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_download_strings_files_from_glotpress.rb +113 -0
  6. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +5 -5
  7. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_localize_project.rb +2 -2
  8. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_strings_files.rb +75 -0
  9. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_git_helper.rb +0 -20
  10. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb +8 -0
  11. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/git_helper.rb +2 -4
  12. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_git_helper.rb +3 -2
  13. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +108 -173
  14. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_linter_helper.rb +207 -0
  15. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/promo_screenshots_helper.rb +3 -3
  16. data/lib/fastlane/plugin/wpmreleasetoolkit/models/file_reference.rb +1 -4
  17. data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +1 -1
  18. metadata +23 -39
  19. data/bin/drawText +0 -20
  20. data/ext/drawText/drawText/Assets/style.css +0 -1
  21. data/ext/drawText/drawText/CoreTextStack.swift +0 -113
  22. data/ext/drawText/drawText/Helpers/CommandLineHelpers.swift +0 -36
  23. data/ext/drawText/drawText/Helpers/Extensions.swift +0 -27
  24. data/ext/drawText/drawText/Helpers/FileSystemHelper.swift +0 -24
  25. data/ext/drawText/drawText/Stylesheet.swift +0 -48
  26. data/ext/drawText/drawText/TextImage.swift +0 -100
  27. data/ext/drawText/drawText/main.swift +0 -61
  28. data/ext/drawText/drawText Tests/DigitParsingTests.swift +0 -21
  29. data/ext/drawText/drawText Tests/ExtensionsTests.swift +0 -5
  30. data/ext/drawText/drawText Tests/Info.plist +0 -22
  31. data/ext/drawText/drawText Tests/StylesheetTests.swift +0 -31
  32. data/ext/drawText/drawText Tests/Test Cases/default-stylesheet.txt +0 -10
  33. data/ext/drawText/drawText Tests/Test Cases/external-styles-sample.css +0 -3
  34. data/ext/drawText/drawText Tests/Test Cases/external-styles-test.txt +0 -13
  35. data/ext/drawText/drawText Tests/Test Cases/large-text-block.txt +0 -1
  36. data/ext/drawText/drawText Tests/Test Cases/regular-text-block.txt +0 -2
  37. data/ext/drawText/drawText Tests/Test Cases/rtl-text-block.txt +0 -2
  38. data/ext/drawText/drawText Tests/Test Cases/text-size-adjustment-test.txt +0 -10
  39. data/ext/drawText/drawText Tests/TextImageTests.swift +0 -99
  40. data/ext/drawText/drawText Tests/drawText_Tests.swift +0 -14
  41. data/ext/drawText/drawText.xcodeproj/project.pbxproj +0 -508
  42. data/ext/drawText/drawText.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  43. data/ext/drawText/drawText.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  44. data/ext/drawText/drawText.xcodeproj/xcshareddata/xcschemes/drawText Tests.xcscheme +0 -57
  45. data/ext/drawText/drawText.xcodeproj/xcshareddata/xcschemes/drawText.xcscheme +0 -109
  46. data/ext/drawText/extconf.rb +0 -36
  47. data/ext/drawText/makefile.example +0 -8
  48. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_merge_translators_strings.rb +0 -106
  49. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_metadata.rb +0 -52
  50. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_translators_strings.rb +0 -93
@@ -1,205 +1,140 @@
1
- require 'yaml'
2
- require 'tmpdir'
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
- 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`
12
+ # Returns the type of a `.strings` file (XML, binary or ASCII)
40
13
  #
41
- # @note This action nukes anything at `install_path` if something already exists – prior to install SwiftGen there
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 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)
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
- # Install SwiftGen if necessary (if not installed yet with the expected version), then run the checks and returns the violations found, if any
37
+ # Merge the content of multiple `.strings` files into a new `.strings` text file.
58
38
  #
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.
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
- 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.
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
- # @return [(String, Array<String>)] A tuple of (config_file_absolute_path, Array<langs>)
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 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
- }
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
- # 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]
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
- # 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
77
+ # Return the list of translations in a `.strings` file.
145
78
  #
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
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 sort_file_lines!(dir, lang)
150
- file = File.join(dir, output_filename(lang))
151
- return nil unless File.exist?(file)
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
- sorted_lines = File.readlines(file).sort
154
- File.write(file, sorted_lines.join)
155
- return file
87
+ JSON.parse(output)
156
88
  end
157
89
 
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.
90
+ # Generate a `.strings` file from a dictionary of string translations.
159
91
  #
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.
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 returned Hash contains keys only for locales with violations. Locales parsed but without any violations found will not appear in the resulting hash.
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
- 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
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
- # 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
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
- command = "bundle exec drawText html=\"#{text}\" maxWidth=#{width} maxHeight=#{height} output=#{tempTextFile.path} fontSize=#{font_size} stylesheet=\"#{stylesheet_path}\" alignment=\"#{position}\""
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
- # TODO: This works only on CircleCI. I chose this variable because it's the one checked by Fastlane::is_ci.
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)
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Wpmreleasetoolkit
3
- VERSION = '2.3.0'
3
+ VERSION = '3.0.0'
4
4
  end
5
5
  end