fastlane-plugin-wpmreleasetoolkit 4.0.0 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_localize_libs_action.rb +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
|