fastlane-plugin-wpmreleasetoolkit 4.0.0 → 4.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e6b39530c08effa688004cbc0c5d8fa8f4fcf3ae5795cb028ca7b60d84f25e0
4
- data.tar.gz: bccb8c68d90bbb681f2fc41e896f1af775bf141ccd28d83eb3aac1f7ba40e28a
3
+ metadata.gz: ee81192aaa403dc7878821f60017e13f9867c549ba84ba2912d1e3ae0623b7b8
4
+ data.tar.gz: a702bfd98e9134e8d3f4303a1bad07705e6b27c107ead1295f5583a78b40384e
5
5
  SHA512:
6
- metadata.gz: 2d5324c615ca08aba37682e1cd6d613e885245f75f1cea5b459c644f083e8cd43af6ba6e6f93932ee7ce8e2a6bb21ed75a12f655610784494955226e2119fb54
7
- data.tar.gz: d2fbbaeb3d7b753b400d77f33f8bf863a8eafc0dccdab8c4e36886f32af1b7791ca02a9dee75ad9ff09b29659d7af2887c4840d029411f6bc71be098378b8410
6
+ metadata.gz: 87379c5741f84f94127d0388e24080c9e861a37115c16b5e6116afd7e1e13c143c748624870740ff2c25c82eb450d910219abd9c56bddf9fabfcb5494f826df4
7
+ data.tar.gz: 51493b07f6a15c38147e7b25607b63d4f5808ee334ff0ec7a3a65d52f33cf8a4d71a46aa189f680324ca67023d1072f2b9a115899b4cecabe0ad90c92ccdfe91
@@ -32,6 +32,14 @@ module Fastlane
32
32
  end
33
33
 
34
34
  def self.available_options
35
+ libs_hash_description = <<~KEYS
36
+ - `:library`: The library display name.
37
+ - `:strings_path`: The path to the `strings.xml` file of the library.
38
+ - `:exclusions`: An optional `Array` of string keys to exclude from merging.
39
+ - `:source_id`: An optional `String` which will be added as the `a8c-src-lib` XML attribute
40
+ to strings coming from this library, to help identify their source in the merged file.
41
+ - `:add_ignore_attr`: If set to true, will add `tools:ignore="UnusedResources"` to merged strings.
42
+ KEYS
35
43
  [
36
44
  FastlaneCore::ConfigItem.new(key: :app_strings_path,
37
45
  description: 'The path of the main strings file',
@@ -41,10 +49,7 @@ module Fastlane
41
49
  # See `Fastlane::Helper::Android::LocalizeHelper.merge_lib`'s YARD doc for more details on the keys expected for each Hash.
42
50
  FastlaneCore::ConfigItem.new(key: :libs_strings_path,
43
51
  env_name: 'LOCALIZE_LIBS_STRINGS_PATH',
44
- description: 'The list of libs to merge. ' \
45
- + 'Each item in the provided array must be a Hash with the keys `:library` (The library display name),' \
46
- + '`:strings_path` (The path to the `strings.xml` file of the library) and ' \
47
- + '`:exclusions` (Array of string keys to exclude from merging)',
52
+ description: "The list of libs to merge. Each item in the provided array must be a Hash with the following keys:\n#{libs_hash_description}",
48
53
  optional: false,
49
54
  type: Array),
50
55
  ]
@@ -17,8 +17,7 @@ module Fastlane
17
17
  end
18
18
 
19
19
  # Ensure the git repository at `~/.mobile-secrets` is up to date.
20
- # If the secrets repo is in a detached HEAD state, skip the pull,
21
- # since it will fail.
20
+ # If the secrets repo is in a detached HEAD state, skip the pull, since it will fail.
22
21
  def self.update_repository
23
22
  secrets_repo_branch = Fastlane::Helper::ConfigureHelper.repo_branch_name
24
23
 
@@ -15,8 +15,8 @@ module Fastlane
15
15
  # otherwise, the error messaging isn't as helpful.
16
16
  validate_that_secrets_repo_is_clean
17
17
 
18
- # Update the repository to get the latest version of the configuration secrets – that's
19
- # how we'll know if we're behind in subsequent validations
18
+ # Update the repository to get the latest version of the configuration secrets
19
+ # – that's how we'll know if we're behind in subsequent validations
20
20
  ConfigureDownloadAction.run
21
21
 
22
22
  validate_that_branches_match
@@ -22,17 +22,17 @@ module Fastlane
22
22
  install_path: resolve_path(params[:install_path]),
23
23
  version: params[:version]
24
24
  )
25
- violations = helper.run(
25
+ all_violations = helper.run(
26
26
  input_dir: resolve_path(params[:input_dir]),
27
27
  base_lang: params[:base_lang],
28
28
  only_langs: params[:only_langs]
29
29
  )
30
30
 
31
- violations.each do |lang, diff|
32
- UI.error "Inconsistencies found between '#{params[:base_lang]}' and '#{lang}':\n\n#{diff}\n"
31
+ all_violations.each do |lang, lang_violations|
32
+ UI.error "Inconsistencies found between '#{params[:base_lang]}' and '#{lang}':\n\n#{lang_violations.join("\n")}\n"
33
33
  end
34
34
 
35
- violations
35
+ all_violations
36
36
  end
37
37
 
38
38
  RETRY_MESSAGE = <<~MSG
@@ -148,15 +148,15 @@ module Fastlane
148
148
  end
149
149
 
150
150
  def self.return_type
151
- :hash_of_strings
151
+ :hash
152
152
  end
153
153
 
154
154
  def self.return_value
155
- 'A hash, keyed by language code, whose values are the diff found for said language'
155
+ 'A hash, keyed by language code, whose values are arrays of violations found for that language'
156
156
  end
157
157
 
158
158
  def self.authors
159
- ['AliSoftware']
159
+ ['Automattic']
160
160
  end
161
161
 
162
162
  def self.is_supported?(platform)
@@ -9,86 +9,99 @@ module Fastlane
9
9
  module Helper
10
10
  module Android
11
11
  module LocalizeHelper
12
- # Checks if string_line has the content_override flag set
13
- def self.skip_string_by_tag(string_line)
14
- skip = string_line.attr('content_override') == 'true' unless string_line.attr('content_override').nil?
12
+ LIB_SOURCE_XML_ATTR = 'a8c-src-lib'.freeze
13
+
14
+ # Checks if `string_node` has the `content_override` flag set
15
+ def self.skip_string_by_tag?(string_node)
16
+ skip = string_node.attr('content_override') == 'true' unless string_node.attr('content_override').nil?
15
17
  if skip
16
- puts " - Skipping #{string_line.attr('name')} string"
18
+ UI.message " - Skipping #{string_node.attr('name')} string"
17
19
  return true
18
20
  end
19
21
 
20
22
  return false
21
23
  end
22
24
 
23
- # Checks if string_name is in the excluesion list
24
- def self.skip_string_by_exclusion_list(library, string_name)
25
- return false unless library.key?(:exclusions)
25
+ # Checks if `string_name` is in the exclusion list
26
+ def self.skip_string_by_exclusion_list?(library, string_name)
27
+ return false if library[:exclusions].nil?
26
28
 
27
29
  skip = library[:exclusions].include?(string_name)
28
30
  if skip
29
- puts " - Skipping #{string_name} string"
31
+ UI.message " - Skipping #{string_name} string"
30
32
  return true
31
33
  end
32
34
  end
33
35
 
34
- # Merge string_line into main_string
35
- def self.merge_string(main_strings, library, string_line)
36
- string_name = string_line.attr('name')
37
- string_content = string_line.content
36
+ # Adds the appropriate XML attributes to an XML `<string>` node according to library configuration
37
+ def self.add_xml_attributes!(string_node, library)
38
+ if library[:add_ignore_attr] == true
39
+ existing_ignores = (string_node['tools:ignore'] || '').split(',')
40
+ existing_ignores.append('UnusedResources') unless existing_ignores.include?('UnusedResources')
41
+ string_node['tools:ignore'] = existing_ignores.join(',')
42
+ end
43
+ string_node[LIB_SOURCE_XML_ATTR] = library[:source_id] unless library[:source_id].nil?
44
+ end
45
+
46
+ # Merge a single `lib_string_node` XML node into the `main_strings_xml``
47
+ def self.merge_string_node(main_strings_xml, library, lib_string_node)
48
+ string_name = lib_string_node.attr('name')
49
+ string_content = lib_string_node.content
38
50
 
39
51
  # Skip strings in the exclusions list
40
- return :skipped if skip_string_by_exclusion_list(library, string_name)
52
+ return :skipped if skip_string_by_exclusion_list?(library, string_name)
41
53
 
42
54
  # Search for the string in the main file
43
55
  result = :added
44
- main_strings.xpath('//string').each do |this_string|
45
- if this_string.attr('name') == string_name
56
+ main_strings_xml.xpath('//string').each do |main_string_node|
57
+ if main_string_node.attr('name') == string_name
46
58
  # Skip if the string has the content_override tag
47
- return :skipped if skip_string_by_tag(this_string)
59
+ return :skipped if skip_string_by_tag?(main_string_node)
48
60
 
49
61
  # If nodes are equivalent, skip
50
- return :found if string_line =~ this_string
62
+ return :found if lib_string_node =~ main_string_node
51
63
 
52
64
  # The string needs an update
53
- result = :updated
54
- if this_string.attr('tools:ignore').nil?
55
- # It can be updated, so remove the current one and move ahead
56
- this_string.remove
57
- break
65
+ if main_string_node.attr('tools:ignore').nil?
66
+ # No `tools:ignore` attribute; completely replace existing main string node with lib's one
67
+ add_xml_attributes!(lib_string_node, library)
68
+ main_string_node.replace lib_string_node
58
69
  else
59
- # It has the tools:ignore flag, so update the content without touching the other attributes
60
- this_string.content = string_content
61
- return result
70
+ # Has the `tools:ignore` flag; update the content without touching the other existing attributes
71
+ add_xml_attributes!(main_string_node, library)
72
+ main_string_node.content = string_content
62
73
  end
74
+ return :updated
63
75
  end
64
76
  end
65
77
 
66
78
  # String not found, or removed because needing update and not in the exclusion list: add to the main file
67
- main_strings.xpath('//string').last().add_next_sibling("\n#{' ' * 4}#{string_line.to_xml().strip}")
79
+ add_xml_attributes!(lib_string_node, library)
80
+ main_strings_xml.xpath('//string').last().add_next_sibling("\n#{' ' * 4}#{lib_string_node.to_xml().strip}")
68
81
  return result
69
82
  end
70
83
 
71
- # Verify a string
72
- def self.verify_string(main_strings, library, string_line)
73
- string_name = string_line.attr('name')
74
- string_content = string_line.content
84
+ # Verify a string node from a library has properly been merged into the main one
85
+ def self.verify_string(main_strings_xml, library, lib_string_node)
86
+ string_name = lib_string_node.attr('name')
87
+ string_content = lib_string_node.content
75
88
 
76
89
  # Skip strings in the exclusions list
77
- return if skip_string_by_exclusion_list(library, string_name)
90
+ return if skip_string_by_exclusion_list?(library, string_name)
78
91
 
79
92
  # Search for the string in the main file
80
- main_strings.xpath('//string').each do |this_string|
81
- if this_string.attr('name') == string_name
93
+ main_strings_xml.xpath('//string').each do |main_string_node|
94
+ if main_string_node.attr('name') == string_name
82
95
  # Skip if the string has the content_override tag
83
- return if skip_string_by_tag(this_string)
96
+ return if skip_string_by_tag?(main_string_node)
84
97
 
85
- # Update if needed
86
- UI.user_error!("String #{string_name} [#{string_content}] has been updated in the main file but not in the library #{library[:library]}.") if this_string.content != string_content
98
+ # Check if up-to-date
99
+ UI.user_error!("String #{string_name} [#{string_content}] has been updated in the main file but not in the library #{library[:library]}.") if main_string_node.content != string_content
87
100
  return
88
101
  end
89
102
  end
90
103
 
91
- # String not found and not in the exclusion list:
104
+ # String not found and not in the exclusion list
92
105
  UI.user_error!("String #{string_name} [#{string_content}] was found in library #{library[:library]} but not in the main file.")
93
106
  end
94
107
 
@@ -98,27 +111,33 @@ module Fastlane
98
111
  # @param [Hash] library Hash describing the library to merge. The Hash should contain the following keys:
99
112
  # - `:library`: The human readable name of the library, used to display in console messages
100
113
  # - `: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.
114
+ # - `:exclusions`: An array of strings keys to exclude during merge. Any of those keys from the
115
+ # library's `strings.xml` will be skipped and won't be merged into the main one.
116
+ # - `:source_id`: An optional `String` which will be added as the `a8c-src-lib` XML attribute
117
+ # to strings coming from this library, to help identify their source in the merged file.
118
+ # - `:add_ignore_attr`: If set to `true`, will add `tools:ignore="UnusedResources"` to merged strings.
119
+ #
102
120
  # @return [Boolean] True if at least one string from the library has been added to (or has updated) the main strings file.
121
+ #
103
122
  def self.merge_lib(main, library)
104
123
  UI.message("Merging #{library[:library]} strings into #{main}")
105
- main_strings = File.open(main) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
106
- lib_strings = File.open(library[:strings_path]) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
124
+ main_strings_xml = File.open(main) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
125
+ lib_strings_xml = File.open(library[:strings_path]) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
107
126
 
108
127
  updated_count = 0
109
128
  untouched_count = 0
110
129
  added_count = 0
111
130
  skipped_count = 0
112
- lib_strings.xpath('//string').each do |string_line|
113
- res = merge_string(main_strings, library, string_line)
131
+ lib_strings_xml.xpath('//string').each do |string_node|
132
+ res = merge_string_node(main_strings_xml, library, string_node)
114
133
  case res
115
134
  when :updated
116
- puts "#{string_line.attr('name')} updated."
135
+ UI.verbose "#{string_node.attr('name')} updated."
117
136
  updated_count = updated_count + 1
118
137
  when :found
119
138
  untouched_count = untouched_count + 1
120
139
  when :added
121
- puts "#{string_line.attr('name')} added."
140
+ UI.verbose "#{string_node.attr('name')} added."
122
141
  added_count = added_count + 1
123
142
  when :skipped
124
143
  skipped_count = skipped_count + 1
@@ -128,7 +147,7 @@ module Fastlane
128
147
  end
129
148
 
130
149
  File.open(main, 'w:UTF-8') do |f|
131
- f.write(main_strings.to_xml(indent: 4))
150
+ f.write(main_strings_xml.to_xml(indent: 4))
132
151
  end
133
152
 
134
153
  UI.message("Done (#{added_count} added, #{updated_count} updated, #{untouched_count} untouched, #{skipped_count} skipped).")
@@ -144,8 +163,8 @@ module Fastlane
144
163
 
145
164
  diff_string = diff_string.slice(0..(end_index - 1))
146
165
 
147
- lib_strings.xpath('//string').each do |string_line|
148
- res = verify_string(main_strings, library, string_line) if string_line.attr('name') == diff_string
166
+ lib_strings.xpath('//string').each do |string_node|
167
+ res = verify_string(main_strings, library, string_node) if string_node.attr('name') == diff_string
149
168
  end
150
169
  end
151
170
  end
@@ -8,14 +8,11 @@ module Fastlane
8
8
  # Fallback default branch of the client repository.
9
9
  DEFAULT_GIT_BRANCH = 'trunk'.freeze
10
10
 
11
- # Checks if the given path, or current directory if no path is given, is
12
- # inside a Git repository
11
+ # Checks if the given path, or current directory if no path is given, is inside a Git repository
13
12
  #
14
- # @param [String] path An optional path where to check if a Git repo
15
- # exists.
13
+ # @param [String] path An optional path where to check if a Git repo exists.
16
14
  #
17
- # @return [Bool] True if the current directory is the root of a git repo
18
- # (i.e. a local working copy) or a subdirectory of one.
15
+ # @return [Bool] True if the current directory is the root of a git repo (i.e. a local working copy) or a subdirectory of one.
19
16
  #
20
17
  def self.is_git_repo?(path: Dir.pwd)
21
18
  # If the path doesn't exist, find its first ancestor.
@@ -23,23 +20,19 @@ module Fastlane
23
20
  # Get the path's directory, so we can look in it for the Git folder
24
21
  dir = path.directory? ? path : path.dirname
25
22
 
26
- # Recursively look for the Git folder until it's found or we read the
27
- # the file system root
23
+ # Recursively look for the Git folder until it's found or we read the the file system root
28
24
  dir = dir.parent until Dir.entries(dir).include?('.git') || dir.root?
29
25
 
30
- # If we reached the root, we haven't found a repo. (Technically, there
31
- # could be a repo in the root of the system, but that's a usecase that
32
- # we don't need to support at this time)
26
+ # If we reached the root, we haven't found a repo.
27
+ # (Technically, there could be a repo in the root of the system, but that's a usecase that we don't need to support at this time)
33
28
  return dir.root? == false
34
29
  end
35
30
 
36
- # Travels back the hierarchy of the given path until it finds an existing
37
- # ancestor, or it reaches the root of the file system.
31
+ # Travels back the hierarchy of the given path until it finds an existing ancestor, or it reaches the root of the file system.
38
32
  #
39
33
  # @param [String] path The path to inspect
40
34
  #
41
- # @return [Pathname] The first existing ancestor, or `path` itself if it
42
- # exists
35
+ # @return [Pathname] The first existing ancestor, or `path` itself if it exists
43
36
  #
44
37
  def self.first_existing_ancestor_of(path:)
45
38
  p = Pathname(path).expand_path
@@ -106,7 +99,7 @@ module Fastlane
106
99
  #
107
100
  # @param [String] message The commit message to use
108
101
  # @param [String|Array<String>] files A file or array of files to git-add before creating the commit.
109
- # use `nil` or `[]` if you already added the files in a separate step and don't wan't this method to add any new file before commit.
102
+ # Use `nil` or `[]` if you already added the files in a separate step and don't wan't this method to add any new file before commit.
110
103
  # Also accepts the special symbol `:all` to add all the files (`git commit -a -m …`).
111
104
  # @param [Bool] push If true, will `git push` to `origin` after the commit has been created. Defaults to `false`.
112
105
  #
@@ -209,8 +202,7 @@ module Fastlane
209
202
  UI.user_error!("This command works only on #{branch_name} branch") unless current_branch_name.include?(branch_name)
210
203
  end
211
204
 
212
- # Checks whether a given path is ignored by Git, relying on Git's
213
- # `check-ignore` under the hood.
205
+ # Checks whether a given path is ignored by Git, relying on Git's `check-ignore` under the hood.
214
206
  #
215
207
  # @param [String] path The path to check against `.gitignore`
216
208
  #
@@ -58,8 +58,7 @@ module Fastlane
58
58
  #
59
59
  # @param [String] input_dir The path (ideally absolute) to the directory containing the `.lproj` folders to parse
60
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.
61
+ # @return [Hash<String, Array<String>>] A hash of violations, keyed by language code, whose values are the list of violation messages for that language
63
62
  #
64
63
  def run(input_dir:, base_lang: DEFAULT_BASE_LANG, only_langs: nil)
65
64
  check_swiftgen_installed || install_swiftgen!
@@ -87,7 +86,7 @@ module Fastlane
87
86
  <<~TEMPLATE
88
87
  {% macro recursiveBlock table item %}
89
88
  {% for string in item.strings %}
90
- "{{string.key}}" => [{{string.types|join:","}}]
89
+ {{string.key}} ==> [{{string.types|join:","}}]
91
90
  {% endfor %}
92
91
  {% for child in item.children %}
93
92
  {% call recursiveBlock table child %}
@@ -139,20 +138,19 @@ module Fastlane
139
138
  return [config_file, langs]
140
139
  end
141
140
 
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
141
+ # Returns a Hash mapping the list of expected parameter types for each of the keys based in the %… placeholders found in their `.strings` files
145
142
  #
146
143
  # @param [String] dir The temporary directory in which the file to sort lines for is located
147
144
  # @param [String] lang The code for the locale we need to sort the output lines for
145
+ # @return [Hash<String, String>] A hash whose keys are the strings keys, and corresponding value is a String describing the types expected as parameters.
148
146
  #
149
- def sort_file_lines!(dir, lang)
147
+ def placeholder_types_for_keys(dir, lang)
150
148
  file = File.join(dir, output_filename(lang))
151
149
  return nil unless File.exist?(file)
152
150
 
153
- sorted_lines = File.readlines(file).sort
154
- File.write(file, sorted_lines.join)
155
- return file
151
+ File.readlines(file).map do |line|
152
+ line.match(/^(.*) ==> (\[[A-Za-z,]*\])$/)&.captures
153
+ end.compact.to_h
156
154
  end
157
155
 
158
156
  # 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.
@@ -160,7 +158,7 @@ module Fastlane
160
158
  # @param [String] input_dir The directory where the `.lproj` folders to scan are located
161
159
  # @param [String] base_lang The base language used as source of truth that all other languages will be compared against
162
160
  # @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.
161
+ # @return [Hash<String, Array<String>>] A hash of violations, keyed by language code, whose values are the list of violation messages for that language
164
162
  #
165
163
  # @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
164
  #
@@ -172,34 +170,22 @@ module Fastlane
172
170
  Action.sh(swiftgen_bin, 'config', 'run', '--config', config_file)
173
171
 
174
172
  # Run diffs
175
- base_file = sort_file_lines!(tmpdir, base_lang)
173
+ params_for_base_lang = placeholder_types_for_keys(tmpdir, base_lang)
176
174
  langs.delete(base_lang)
177
175
  return langs.map do |lang|
178
- file = sort_file_lines!(tmpdir, lang)
176
+ params_for_lang = placeholder_types_for_keys(tmpdir, lang)
177
+
179
178
  # 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
179
+ next nil if params_for_lang.nil? || params_for_lang.empty?
194
180
 
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
181
+ violations = params_for_lang.map do |key, param_types|
182
+ next "`#{key}` was unexpected, as it is not present in the base locale." if params_for_base_lang[key].nil?
183
+ next "`#{key}` expected placeholders for #{params_for_base_lang[key]} but found #{param_types} instead." if params_for_base_lang[key] != param_types
184
+ end.compact
185
+
186
+ [lang, violations] unless violations.empty?
187
+ end.compact.to_h
201
188
  end
202
- return true
203
189
  end
204
190
  end
205
191
  end
@@ -0,0 +1,107 @@
1
+ module Fastlane
2
+ module Helper
3
+ module Ios
4
+ class StringsFileValidationHelper
5
+ # context can be one of:
6
+ # :root, :maybe_comment_start, :in_line_comment, :in_block_comment,
7
+ # :maybe_block_comment_end, :in_quoted_key,
8
+ # :after_quoted_key_before_eq, :after_quoted_key_and_equal,
9
+ # :in_quoted_value, :after_quoted_value
10
+ State = Struct.new(:context, :buffer, :in_escaped_ctx, :found_key, keyword_init: true)
11
+
12
+ TRANSITIONS = {
13
+ root: {
14
+ /\s/ => :root,
15
+ '/' => :maybe_comment_start,
16
+ '"' => :in_quoted_key
17
+ },
18
+ maybe_comment_start: {
19
+ '/' => :in_line_comment,
20
+ /\*/ => :in_block_comment
21
+ },
22
+ in_line_comment: {
23
+ "\n" => :root,
24
+ /./ => :in_line_comment
25
+ },
26
+ in_block_comment: {
27
+ /\*/ => :maybe_block_comment_end,
28
+ /./m => :in_block_comment
29
+ },
30
+ maybe_block_comment_end: {
31
+ '/' => :root,
32
+ /./m => :in_block_comment
33
+ },
34
+ in_quoted_key: {
35
+ '"' => lambda do |state, _|
36
+ state.found_key = state.buffer.string.dup
37
+ state.buffer.string = ''
38
+ :after_quoted_key_before_eq
39
+ end,
40
+ /./ => lambda do |state, c|
41
+ state.buffer.write(c)
42
+ :in_quoted_key
43
+ end
44
+ },
45
+ after_quoted_key_before_eq: {
46
+ /\s/ => :after_quoted_key_before_eq,
47
+ '=' => :after_quoted_key_and_eq
48
+ },
49
+ after_quoted_key_and_eq: {
50
+ /\s/ => :after_quoted_key_and_eq,
51
+ '"' => :in_quoted_value
52
+ },
53
+ in_quoted_value: {
54
+ '"' => :after_quoted_value,
55
+ /./m => :in_quoted_value
56
+ },
57
+ after_quoted_value: {
58
+ /\s/ => :after_quoted_value,
59
+ ';' => :root
60
+ }
61
+ }.freeze
62
+
63
+ # Inspects the given `.strings` file for duplicated keys, returning them if any.
64
+ #
65
+ # @param [String] file The path to the file to inspect.
66
+ # @return [Hash<String, Array<Int>] Hash with the dublipcated keys.
67
+ # Each element has the duplicated key (from the `.strings`) as key and an array of line numbers where the key occurs as value.
68
+ def self.find_duplicated_keys(file:)
69
+ keys_with_lines = Hash.new([])
70
+
71
+ state = State.new(context: :root, buffer: StringIO.new, in_escaped_ctx: false, found_key: nil)
72
+
73
+ File.readlines(file).each_with_index do |line, line_no|
74
+ line.chars.each_with_index do |c, col_no|
75
+ # Handle escaped characters at a global level.
76
+ # This is more straightforward than having to account for it in the `TRANSITIONS` table.
77
+ if state.in_escaped_ctx || c == '\\'
78
+ # Just because we check for escaped characters at the global level, it doesn't mean we allow them in every context.
79
+ allowed_contexts_for_escaped_characters = %i[in_quoted_key in_quoted_value in_block_comment in_line_comment]
80
+ raise "Found escaped character outside of allowed contexts on line #{line_no + 1} (current context: #{state.context})" unless allowed_contexts_for_escaped_characters.include?(state.context)
81
+
82
+ state.buffer.write(c) if state.context == :in_quoted_key
83
+ state.in_escaped_ctx = !state.in_escaped_ctx
84
+ next
85
+ end
86
+
87
+ # Look at the transitions table for the current context, and find the first transition matching the current character
88
+ (_, next_context) = TRANSITIONS[state.context].find { |regex, _| c.match?(regex) } || [nil, nil]
89
+ raise "Invalid character `#{c}` found on line #{line_no + 1}, col #{col_no + 1}" if next_context.nil?
90
+
91
+ state.context = next_context.is_a?(Proc) ? next_context.call(state, c) : next_context
92
+ next unless state.found_key
93
+
94
+ # If we just exited the :in_quoted_key context and thus have found a new key, process it
95
+ key = state.found_key.dup
96
+ state.found_key = nil
97
+
98
+ keys_with_lines[key] += [line_no + 1]
99
+ end
100
+ end
101
+
102
+ keys_with_lines.keep_if { |_, lines| lines.count > 1 }
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -12,8 +12,7 @@ module Fastlane
12
12
  @alternates = {}
13
13
  end
14
14
 
15
- # Downloads data from GlotPress,
16
- # in JSON format
15
+ # Downloads data from GlotPress, in JSON format
17
16
  def download(target_locale, glotpress_url, is_source)
18
17
  uri = URI(glotpress_url)
19
18
  response = Net::HTTP.get_response(uri)
@@ -80,8 +79,7 @@ module Fastlane
80
79
  end
81
80
  end
82
81
 
83
- # Writes the downloaded content
84
- # to the target file
82
+ # Writes the downloaded content to the target file
85
83
  def save_metadata(locale, file_name, content)
86
84
  file_path = get_target_file_path(locale, file_name)
87
85
 
@@ -117,8 +117,7 @@ module Fastlane
117
117
  end
118
118
 
119
119
  def draw_screenshot_to_canvas(entry, canvas, device)
120
- # Don't require a screenshot to be present – we can just skip
121
- # this function if one doesn't exist.
120
+ # Don't require a screenshot to be present – we can just skip this function if one doesn't exist.
122
121
  return canvas if entry['screenshot'].nil?
123
122
 
124
123
  device_mask = device['screenshot_mask']
@@ -9,9 +9,11 @@ module Fastlane
9
9
  def self.add_new_section(path:, section_title:)
10
10
  lines = File.readlines(path)
11
11
 
12
- # Find the index of the first non-empty line that is also NOT a comment. That way we keep commment headers as the very top of the file
12
+ # Find the index of the first non-empty line that is also NOT a comment.
13
+ # That way we keep commment headers as the very top of the file
13
14
  line_idx = lines.find_index { |l| !l.start_with?('***') && !l.start_with?('//') && !l.chomp.empty? }
14
- # Put back the header, then the new entry, then the rest (note: '...' excludes the higher bound of the range, unlike '..')
15
+ # Put back the header, then the new entry, then the rest
16
+ # (note: '...' excludes the higher bound of the range, unlike '..')
15
17
  new_lines = lines[0...line_idx] + ["#{section_title}\n", "-----\n", "\n", "\n"] + lines[line_idx..]
16
18
 
17
19
  File.write(path, new_lines.join)
@@ -40,11 +40,9 @@ module Fastlane
40
40
  end
41
41
 
42
42
  # "Applies" the instruction described in the instance to the file system.
43
- # That is, copies the content of the source `file` to the `destination`
44
- # path.
43
+ # That is, copies the content of the source `file` to the `destination` path.
45
44
  #
46
- # @raise [StandardError] For security reasons, it will raise if
47
- # `destination` is not ignored under Git.
45
+ # @raise [StandardError] For security reasons, it will raise if `destination` is not ignored under Git.
48
46
  def apply
49
47
  # Only decrypt the file if the destination is ignored in Git
50
48
  unless Fastlane::Helper::GitHelper.is_ignored?(path: destination_file_path)
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Wpmreleasetoolkit
3
- VERSION = '4.0.0'
3
+ VERSION = '4.1.0'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-wpmreleasetoolkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorenzo Mattei
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-17 00:00:00.000000000 Z
11
+ date: 2022-04-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diffy
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '1.4'
61
+ version: '1.5'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '1.4'
68
+ version: '1.5'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: git
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -454,6 +454,7 @@ files:
454
454
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_git_helper.rb
455
455
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb
456
456
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_linter_helper.rb
457
+ - lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_strings_file_validation_helper.rb
457
458
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_version_helper.rb
458
459
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_download_helper.rb
459
460
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_update_helper.rb