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 +4 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_localize_libs_action.rb +9 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_download_action.rb +1 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_validate_action.rb +2 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +7 -7
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb +66 -47
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/git_helper.rb +10 -18
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_linter_helper.rb +20 -34
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_strings_file_validation_helper.rb +107 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_download_helper.rb +2 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/promo_screenshots_helper.rb +1 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/release_notes_helper.rb +4 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/file_reference.rb +2 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ee81192aaa403dc7878821f60017e13f9867c549ba84ba2912d1e3ae0623b7b8
|
4
|
+
data.tar.gz: a702bfd98e9134e8d3f4303a1bad07705e6b27c107ead1295f5583a78b40384e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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:
|
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
|
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
|
-
|
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
|
-
|
32
|
-
UI.error "Inconsistencies found between '#{params[:base_lang]}' and '#{lang}':\n\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
|
-
|
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
|
-
:
|
151
|
+
:hash
|
152
152
|
end
|
153
153
|
|
154
154
|
def self.return_value
|
155
|
-
'A hash, keyed by language code, whose values are
|
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
|
-
['
|
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
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
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
|
24
|
-
def self.skip_string_by_exclusion_list(library, string_name)
|
25
|
-
return false
|
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
|
-
|
31
|
+
UI.message " - Skipping #{string_name} string"
|
30
32
|
return true
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
34
|
-
#
|
35
|
-
def self.
|
36
|
-
|
37
|
-
|
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
|
-
|
45
|
-
if
|
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(
|
59
|
+
return :skipped if skip_string_by_tag?(main_string_node)
|
48
60
|
|
49
61
|
# If nodes are equivalent, skip
|
50
|
-
return :found if
|
62
|
+
return :found if lib_string_node =~ main_string_node
|
51
63
|
|
52
64
|
# The string needs an update
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
#
|
60
|
-
|
61
|
-
|
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
|
-
|
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(
|
73
|
-
string_name =
|
74
|
-
string_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
|
-
|
81
|
-
if
|
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(
|
96
|
+
return if skip_string_by_tag?(main_string_node)
|
84
97
|
|
85
|
-
#
|
86
|
-
UI.user_error!("String #{string_name} [#{string_content}] has been updated in the main file but not in the library #{library[:library]}.") if
|
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
|
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
|
-
|
106
|
-
|
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
|
-
|
113
|
-
res =
|
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
|
-
|
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
|
-
|
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(
|
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 |
|
148
|
-
res = verify_string(main_strings, library,
|
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.
|
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
|
-
#
|
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
|
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
|
-
|
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
|
-
#
|
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
|
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
|
-
|
154
|
-
|
155
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
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.
|
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
|
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)
|
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.
|
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-
|
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.
|
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.
|
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
|