fastlane-plugin-wpmreleasetoolkit 3.1.0 → 4.2.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/android/{android_hotifx_prechecks.rb → android_hotfix_prechecks.rb} +0 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_update_metadata_source.rb +0 -1
- 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_download_strings_files_from_glotpress.rb +1 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_extract_keys_from_strings_files.rb +35 -17
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/{ios_hotifx_prechecks.rb → ios_hotfix_prechecks.rb} +0 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +48 -9
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_strings_files.rb +19 -15
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb +72 -49
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/git_helper.rb +10 -18
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +28 -9
- 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 +2 -3
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/release_notes_helper.rb +4 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/user_agent.rb +5 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/file_reference.rb +2 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +1 -1
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f87869bd0429895cc6f445950c7e6fca7d778021656483f1b0f4cc232487e2a1
|
4
|
+
data.tar.gz: e478fc6f8cd0b3b4baa9431502b7ee6f95e94ab3553bae728d7077e7c9e39adf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c62ae7d95211059423e61feb22558e79a42da9587f76a1ed0c7c5319de2650a0936d1daeb8f7db9c1a359369bab098b52dc3ea188834e6551bd3f1b479414aab
|
7
|
+
data.tar.gz: ed0fc3ef8429091aeb9c83d9e1efeb4909234aa47b733e574546f7297b9c0c7913829889f687cfab55d139f294b00de0eb1f5b4e9064e21c78144697b6683230
|
@@ -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
|
]
|
File without changes
|
@@ -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
|
data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_download_strings_files_from_glotpress.rb
CHANGED
@@ -10,6 +10,7 @@ module Fastlane
|
|
10
10
|
|
11
11
|
locales.each do |glotpress_locale, lproj_name|
|
12
12
|
# Download the export in the proper `.lproj` directory
|
13
|
+
UI.message "Downloading translations for '#{lproj_name}' from GlotPress (#{glotpress_locale}) [#{params[:filters]}]..."
|
13
14
|
lproj_dir = File.join(download_dir, "#{lproj_name}.lproj")
|
14
15
|
destination = File.join(lproj_dir, "#{params[:table_basename]}.strings")
|
15
16
|
FileUtils.mkdir(lproj_dir) unless Dir.exist?(lproj_dir)
|
data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_extract_keys_from_strings_files.rb
CHANGED
@@ -3,8 +3,14 @@ module Fastlane
|
|
3
3
|
class IosExtractKeysFromStringsFilesAction < Action
|
4
4
|
def self.run(params)
|
5
5
|
source_parent_dir = params[:source_parent_dir]
|
6
|
-
target_original_files = params[:target_original_files]
|
7
|
-
keys_to_extract_per_target_file = keys_list_per_target_file(target_original_files)
|
6
|
+
target_original_files = params[:target_original_files].keys # Array [original-file-paths]
|
7
|
+
keys_to_extract_per_target_file = keys_list_per_target_file(target_original_files) # Hash { original-file-path => [keys] }
|
8
|
+
prefix_to_remove_per_target_file = params[:target_original_files] # Hash { original-file-path => prefix }
|
9
|
+
|
10
|
+
UI.message("Extracting keys from `#{source_parent_dir}/*.lproj/#{params[:source_tablename]}.strings` into:")
|
11
|
+
target_original_files.each { |f| UI.message(' - ' + replace_lproj_in_path(f, with_lproj: '*.lproj')) }
|
12
|
+
|
13
|
+
updated_files_list = []
|
8
14
|
|
9
15
|
# For each locale, extract the right translations from `<source_tablename>.strings` into each target `.strings` file
|
10
16
|
Dir.glob('*.lproj', base: source_parent_dir).each do |lproj_dir_name|
|
@@ -12,21 +18,25 @@ module Fastlane
|
|
12
18
|
translations = Fastlane::Helper::Ios::L10nHelper.read_strings_file_as_hash(path: source_strings_file)
|
13
19
|
|
14
20
|
target_original_files.each do |target_original_file|
|
15
|
-
target_strings_file =
|
21
|
+
target_strings_file = replace_lproj_in_path(target_original_file, with_lproj: lproj_dir_name)
|
16
22
|
next if target_strings_file == target_original_file # do not generate/overwrite the original locale itself
|
17
23
|
|
18
|
-
|
19
|
-
|
24
|
+
keys_prefix = prefix_to_remove_per_target_file[target_original_file] || ''
|
25
|
+
keys_to_extract = keys_to_extract_per_target_file[target_original_file].map { |k| "#{keys_prefix}#{k}" }
|
26
|
+
extracted_translations = translations.slice(*keys_to_extract).transform_keys { |k| k.delete_prefix(keys_prefix) }
|
27
|
+
UI.verbose("Extracting #{extracted_translations.count} keys (out of #{keys_to_extract.count} expected) into #{target_strings_file}...")
|
20
28
|
|
21
|
-
extracted_translations = translations.slice(*keys_to_extract)
|
22
29
|
FileUtils.mkdir_p(File.dirname(target_strings_file)) # Ensure path up to parent dir exists, create it if not.
|
23
30
|
Fastlane::Helper::Ios::L10nHelper.generate_strings_file_from_hash(translations: extracted_translations, output_path: target_strings_file)
|
31
|
+
updated_files_list.append(target_strings_file)
|
24
32
|
rescue StandardError => e
|
25
33
|
UI.user_error!("Error while writing extracted translations to `#{target_strings_file}`: #{e.message}")
|
26
34
|
end
|
27
35
|
rescue StandardError => e
|
28
36
|
UI.user_error!("Error while reading the translations from source file `#{source_strings_file}`: #{e.message}")
|
29
37
|
end
|
38
|
+
|
39
|
+
updated_files_list
|
30
40
|
end
|
31
41
|
|
32
42
|
# Pre-load the list of keys to extract for each target file.
|
@@ -43,6 +53,15 @@ module Fastlane
|
|
43
53
|
UI.user_error!("Failed to read the keys to extract from originals file: #{e.message}")
|
44
54
|
end
|
45
55
|
|
56
|
+
# Replaces the `*.lproj` component of the path to a `.strings` file with a different `.lproj` folder
|
57
|
+
#
|
58
|
+
# @param [String] path The path the the `.strings` file, assumed to be in a `.lproj` parent folder
|
59
|
+
# @param [String] with_lproj The new name of the `.lproj` parent folder to point to
|
60
|
+
#
|
61
|
+
def self.replace_lproj_in_path(path, with_lproj:)
|
62
|
+
File.join(File.dirname(File.dirname(path)), with_lproj, File.basename(path))
|
63
|
+
end
|
64
|
+
|
46
65
|
#####################################################
|
47
66
|
# @!group Documentation
|
48
67
|
#####################################################
|
@@ -82,28 +101,27 @@ module Fastlane
|
|
82
101
|
default_value: 'Localizable'),
|
83
102
|
FastlaneCore::ConfigItem.new(key: :target_original_files,
|
84
103
|
env_name: 'FL_IOS_EXTRACT_KEYS_FROM_STRINGS_FILES_TARGET_ORIGINAL_FILES',
|
85
|
-
description: 'The path(s) to the `<base-locale>.lproj/<target-tablename>.strings` file(s) for which we want to extract the keys to. ' \
|
86
|
-
+ 'Each
|
87
|
-
+ 'For each
|
88
|
-
|
104
|
+
description: 'The path(s) to the `<base-locale>.lproj/<target-tablename>.strings` file(s) for which we want to extract the keys to, and the prefix to remove from their keys. ' \
|
105
|
+
+ 'Each key in the Hash should point to a file containing the original strings (typically `en` or `Base` locale), and will be used to determine which keys to extract from the `source_tablename`. ' \
|
106
|
+
+ 'For each key, the associated value is an optional prefix to remove from the keys (which can be useful if you used a prefix during `ios_merge_strings_files` to avoid duplicates). Can be nil or empty if no prefix was used during merge for that file.' \
|
107
|
+
+ 'Note: For each entry, the path(s) in which the translations will be extracted to will be the files with the same basename as the key in each of the other `*.lproj` sibling folders. ',
|
108
|
+
type: Hash,
|
89
109
|
verify_block: proc do |values|
|
90
110
|
UI.user_error!('`target_original_files` must contain at least one path to an original `.strings` file.') if values.empty?
|
91
|
-
values.each do |
|
92
|
-
UI.user_error!("Path `#{
|
93
|
-
UI.user_error! "Expected `#{
|
111
|
+
values.each do |path, _|
|
112
|
+
UI.user_error!("Path `#{path}` (found in `target_original_files`) does not exist.") unless File.exist?(path)
|
113
|
+
UI.user_error! "Expected `#{path}` (found in `target_original_files`) to be a path ending in a `*.lproj/*.strings`." unless File.extname(path) == '.strings' && File.extname(File.dirname(path)) == '.lproj'
|
94
114
|
end
|
95
115
|
end),
|
96
116
|
]
|
97
117
|
end
|
98
118
|
|
99
119
|
def self.return_type
|
100
|
-
|
101
|
-
# see RETURN_TYPES in https://github.com/fastlane/fastlane/blob/master/fastlane/lib/fastlane/action.rb
|
102
|
-
nil
|
120
|
+
:array_of_strings
|
103
121
|
end
|
104
122
|
|
105
123
|
def self.return_value
|
106
|
-
|
124
|
+
'The list of files which have been generated and written to disk by the action'
|
107
125
|
end
|
108
126
|
|
109
127
|
def self.authors
|
File without changes
|
@@ -2,10 +2,20 @@ module Fastlane
|
|
2
2
|
module Actions
|
3
3
|
class IosLintLocalizationsAction < Action
|
4
4
|
def self.run(params)
|
5
|
-
violations =
|
5
|
+
violations = Hash.new([])
|
6
6
|
|
7
7
|
loop do
|
8
|
-
violations = self.
|
8
|
+
# If we did `violations = self.run...` we'd lose the default value for missing key being `[]` that we set above with `Hash.new`.
|
9
|
+
# We want that default value so that we can use `+=` when adding the duplicate keys violations below.
|
10
|
+
violations = violations.merge(self.run_linter(params))
|
11
|
+
|
12
|
+
if params[:check_duplicate_keys]
|
13
|
+
find_duplicated_keys(params).each do |language, duplicates|
|
14
|
+
violations[language] += duplicates
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
report(violations: violations, base_lang: params[:base_lang])
|
9
19
|
break unless !violations.empty? && params[:allow_retry] && UI.confirm(RETRY_MESSAGE)
|
10
20
|
end
|
11
21
|
|
@@ -22,17 +32,38 @@ module Fastlane
|
|
22
32
|
install_path: resolve_path(params[:install_path]),
|
23
33
|
version: params[:version]
|
24
34
|
)
|
25
|
-
|
35
|
+
|
36
|
+
helper.run(
|
26
37
|
input_dir: resolve_path(params[:input_dir]),
|
27
38
|
base_lang: params[:base_lang],
|
28
39
|
only_langs: params[:only_langs]
|
29
40
|
)
|
41
|
+
end
|
30
42
|
|
31
|
-
|
32
|
-
|
43
|
+
def self.report(violations:, base_lang:)
|
44
|
+
violations.each do |lang, lang_violations|
|
45
|
+
UI.error "Inconsistencies found between '#{base_lang}' and '#{lang}':\n\n#{lang_violations.join("\n")}\n"
|
33
46
|
end
|
47
|
+
end
|
34
48
|
|
35
|
-
|
49
|
+
def self.find_duplicated_keys(params)
|
50
|
+
duplicate_keys = {}
|
51
|
+
|
52
|
+
files_to_lint = Dir.chdir(params[:input_dir]) do
|
53
|
+
Dir.glob('*.lproj/Localizable.strings').map do |file|
|
54
|
+
{
|
55
|
+
language: File.basename(File.dirname(file), '.lproj'),
|
56
|
+
path: File.join(params[:input_dir], file)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
files_to_lint.each do |file|
|
62
|
+
duplicates = Fastlane::Helper::Ios::StringsFileValidationHelper.find_duplicated_keys(file: file[:path])
|
63
|
+
duplicate_keys[file[:language]] = duplicates.map { |key, value| "`#{key}` was found at multiple lines: #{value.join(', ')}" } unless duplicates.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
duplicate_keys
|
36
67
|
end
|
37
68
|
|
38
69
|
RETRY_MESSAGE = <<~MSG
|
@@ -140,6 +171,14 @@ module Fastlane
|
|
140
171
|
default_value: false,
|
141
172
|
is_string: false # https://docs.fastlane.tools/advanced/actions/#boolean-parameters
|
142
173
|
),
|
174
|
+
FastlaneCore::ConfigItem.new(
|
175
|
+
key: :check_duplicate_keys,
|
176
|
+
env_name: 'FL_IOS_LINT_TRANSLATIONS_CHECK_DUPLICATE_KEYS',
|
177
|
+
description: 'Checks the input files for duplicate keys',
|
178
|
+
optional: true,
|
179
|
+
default_value: true,
|
180
|
+
is_string: false # https://docs.fastlane.tools/advanced/actions/#boolean-parameters
|
181
|
+
),
|
143
182
|
]
|
144
183
|
end
|
145
184
|
|
@@ -148,15 +187,15 @@ module Fastlane
|
|
148
187
|
end
|
149
188
|
|
150
189
|
def self.return_type
|
151
|
-
:
|
190
|
+
:hash
|
152
191
|
end
|
153
192
|
|
154
193
|
def self.return_value
|
155
|
-
'A hash, keyed by language code, whose values are
|
194
|
+
'A hash, keyed by language code, whose values are arrays of violations found for that language'
|
156
195
|
end
|
157
196
|
|
158
197
|
def self.authors
|
159
|
-
['
|
198
|
+
['Automattic']
|
160
199
|
end
|
161
200
|
|
162
201
|
def self.is_supported?(platform)
|
@@ -2,12 +2,18 @@ module Fastlane
|
|
2
2
|
module Actions
|
3
3
|
class IosMergeStringsFilesAction < Action
|
4
4
|
def self.run(params)
|
5
|
-
|
5
|
+
destination = params[:destination]
|
6
|
+
# Include the destination as the first of the files (without key prefixes) to be included in the merged result
|
7
|
+
all_paths_list = { destination => nil }.merge(params[:paths_to_merge])
|
6
8
|
|
7
|
-
|
9
|
+
UI.message "Merging strings files #{all_paths_list.inspect}"
|
10
|
+
|
11
|
+
File.write(destination, '') unless File.exist?(destination) # Create empty destination file if it does not exist yet
|
12
|
+
duplicates = Fastlane::Helper::Ios::L10nHelper.merge_strings(paths: all_paths_list, output_path: params[:destination])
|
8
13
|
duplicates.each do |dup_key|
|
9
14
|
UI.important "Duplicate key found while merging the `.strings` files: `#{dup_key}`"
|
10
15
|
end
|
16
|
+
UI.important 'Tip: To avoid those key conflicts, you might want to consider providing different prefixes in the `Hash` you used for the `paths:` parameter.' unless duplicates.empty?
|
11
17
|
duplicates
|
12
18
|
end
|
13
19
|
|
@@ -21,12 +27,11 @@ module Fastlane
|
|
21
27
|
|
22
28
|
def self.details
|
23
29
|
<<~DETAILS
|
24
|
-
Merge multiple `.strings` files into one.
|
30
|
+
Merge multiple `.strings` files into another one.
|
25
31
|
|
26
|
-
Especially useful to prepare a single `.strings` file merging
|
27
|
-
|
28
|
-
|
29
|
-
manually maintained by developers.
|
32
|
+
Especially useful to prepare a single `.strings` file merging string files like `InfoPlist.strings` — whose
|
33
|
+
content are typically manually maintained by developers — within the main `Localizable.strings` file — which
|
34
|
+
would have typically been previously generated from the codebase via `ios_generate_strings_file_from_code`.
|
30
35
|
|
31
36
|
The action only supports merging files which are in the OpenStep (`"key" = "value";`) text format (which is
|
32
37
|
the most common format for `.strings` files, and the one generated by `genstrings`), but can handle the case
|
@@ -38,19 +43,18 @@ module Fastlane
|
|
38
43
|
def self.available_options
|
39
44
|
[
|
40
45
|
FastlaneCore::ConfigItem.new(
|
41
|
-
key: :
|
42
|
-
env_name: '
|
43
|
-
description: '
|
44
|
-
type:
|
46
|
+
key: :paths_to_merge,
|
47
|
+
env_name: 'FL_IOS_MERGE_STRINGS_FILES_PATHS_TO_MERGE',
|
48
|
+
description: 'A hash of the paths of all the `.strings` files to merge into the `destination`, with the prefix to be used for their keys as associated value',
|
49
|
+
type: Hash,
|
45
50
|
optional: false
|
46
51
|
),
|
47
52
|
FastlaneCore::ConfigItem.new(
|
48
53
|
key: :destination,
|
49
54
|
env_name: 'FL_IOS_MERGE_STRINGS_FILES_DESTINATION',
|
50
|
-
description: 'The path of the
|
55
|
+
description: 'The path of the `.strings` file to merge the other ones into',
|
51
56
|
type: String,
|
52
|
-
optional:
|
53
|
-
default_value: nil
|
57
|
+
optional: false
|
54
58
|
),
|
55
59
|
]
|
56
60
|
end
|
@@ -60,7 +64,7 @@ module Fastlane
|
|
60
64
|
end
|
61
65
|
|
62
66
|
def self.return_value
|
63
|
-
'The list of duplicate keys found while merging the various `.strings` files'
|
67
|
+
'The list of duplicate keys (after prefix has been added to each) found while merging the various `.strings` files'
|
64
68
|
end
|
65
69
|
|
66
70
|
def self.authors
|
@@ -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
|
@@ -263,9 +282,13 @@ module Fastlane
|
|
263
282
|
#
|
264
283
|
def self.download_glotpress_export_file(project_url:, locale:, filters:)
|
265
284
|
query_params = filters.transform_keys { |k| "filters[#{k}]" }.merge(format: 'android')
|
266
|
-
uri = URI.parse("#{project_url.chomp('/')}/#{locale}/default/export-translations
|
285
|
+
uri = URI.parse("#{project_url.chomp('/')}/#{locale}/default/export-translations/?#{URI.encode_www_form(query_params)}")
|
286
|
+
|
287
|
+
# Set an unambiguous User Agent so GlotPress won't rate-limit us
|
288
|
+
options = { 'User-Agent' => Wpmreleasetoolkit::USER_AGENT }
|
289
|
+
|
267
290
|
begin
|
268
|
-
uri.open { |f| Nokogiri::XML(f.read.gsub("\t", ' '), nil, Encoding::UTF_8.to_s) }
|
291
|
+
uri.open(options) { |f| Nokogiri::XML(f.read.gsub("\t", ' '), nil, Encoding::UTF_8.to_s) }
|
269
292
|
rescue StandardError => e
|
270
293
|
UI.error "Error downloading #{locale} - #{e.message}"
|
271
294
|
return nil
|
@@ -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
|
#
|
@@ -19,6 +19,8 @@ module Fastlane
|
|
19
19
|
# - `nil` if the file does not exist or is neither of those format (e.g. not a `.strings` file at all)
|
20
20
|
#
|
21
21
|
def self.strings_file_type(path:)
|
22
|
+
return :text if File.empty?(path) # If completely empty file, consider it as a valid `.strings` files in textual format
|
23
|
+
|
22
24
|
# Start by checking it seems like a valid property-list file (and not e.g. an image or plain text file)
|
23
25
|
_, status = Open3.capture2('/usr/bin/plutil', '-lint', path)
|
24
26
|
return nil unless status.success?
|
@@ -36,8 +38,8 @@ module Fastlane
|
|
36
38
|
|
37
39
|
# Merge the content of multiple `.strings` files into a new `.strings` text file.
|
38
40
|
#
|
39
|
-
# @param [
|
40
|
-
# @param [String]
|
41
|
+
# @param [Hash<String, String>] paths The paths of the `.strings` files to merge together, associated with the prefix to prepend to each of their respective keys
|
42
|
+
# @param [String] output_path The path to the merged `.strings` file to generate as a result.
|
41
43
|
# @return [Array<String>] List of duplicate keys found while validating the merge.
|
42
44
|
#
|
43
45
|
# @note For now, this method only supports merging `.strings` file in `:text` format
|
@@ -48,24 +50,35 @@ module Fastlane
|
|
48
50
|
#
|
49
51
|
# @raise [RuntimeError] If one of the paths provided is not in text format (but XML or binary instead), or if any of the files are missing.
|
50
52
|
#
|
51
|
-
def self.merge_strings(paths:, output_path:
|
53
|
+
def self.merge_strings(paths:, output_path:)
|
52
54
|
duplicates = []
|
53
55
|
Tempfile.create('wpmrt-l10n-merge-', encoding: 'utf-8') do |tmp_file|
|
54
56
|
all_keys_found = []
|
55
57
|
|
56
58
|
tmp_file.write("/* Generated File. Do not edit. */\n\n")
|
57
|
-
paths.each do |input_file|
|
59
|
+
paths.each do |input_file, prefix|
|
60
|
+
next if File.empty?(input_file) # Skip existing but totally empty files, to avoid adding useless `MARK:` comment for them
|
61
|
+
|
58
62
|
fmt = strings_file_type(path: input_file)
|
59
63
|
raise "The file `#{input_file}` does not exist or is of unknown format." if fmt.nil?
|
60
64
|
raise "The file `#{input_file}` is in #{fmt} format but we currently only support merging `.strings` files in text format." unless fmt == :text
|
61
65
|
|
62
|
-
string_keys = read_strings_file_as_hash(path: input_file).keys
|
66
|
+
string_keys = read_strings_file_as_hash(path: input_file).keys.map { |k| "#{prefix}#{k}" }
|
63
67
|
duplicates += (string_keys & all_keys_found) # Find duplicates using Array intersection, and add those to duplicates list
|
64
68
|
all_keys_found += string_keys
|
65
69
|
|
66
70
|
tmp_file.write("/* MARK: - #{File.basename(input_file)} */\n\n")
|
67
71
|
# Read line-by-line to reduce memory footprint during content copy; Be sure to guess file encoding using the Byte-Order-Mark.
|
68
|
-
File.readlines(input_file, mode: 'rb:BOM|UTF-8').each
|
72
|
+
File.readlines(input_file, mode: 'rb:BOM|UTF-8').each do |line|
|
73
|
+
unless prefix.nil? || prefix.empty?
|
74
|
+
# We need to ensure the line and RegExp are using the same encoding, so we transcode everything to UTF-8.
|
75
|
+
line.encode!(Encoding::UTF_8)
|
76
|
+
# The `/u` modifier on the RegExps is to make them UTF-8
|
77
|
+
line.gsub!(/^(\s*")/u, "\\1#{prefix}") # Lines starting with a quote are considered to be start of a key; add prefix right after the quote
|
78
|
+
line.gsub!(/^(\s*)([A-Z0-9_]+)(\s*=\s*")/ui, "\\1\"#{prefix}\\2\"\\3") # Lines starting with an identifier followed by a '=' are considered to be an unquoted key (typical in InfoPlist.strings files for example)
|
79
|
+
end
|
80
|
+
tmp_file.write(line)
|
81
|
+
end
|
69
82
|
tmp_file.write("\n")
|
70
83
|
end
|
71
84
|
tmp_file.close # ensure we flush the content to disk
|
@@ -81,6 +94,8 @@ module Fastlane
|
|
81
94
|
# @raise [RuntimeError] If the file is not a valid strings file or there was an error in parsing its content.
|
82
95
|
#
|
83
96
|
def self.read_strings_file_as_hash(path:)
|
97
|
+
return {} if File.empty?(path) # Return empty hash if completely empty file
|
98
|
+
|
84
99
|
output, status = Open3.capture2e('/usr/bin/plutil', '-convert', 'json', '-o', '-', path)
|
85
100
|
raise output unless status.success?
|
86
101
|
|
@@ -128,11 +143,15 @@ module Fastlane
|
|
128
143
|
#
|
129
144
|
def self.download_glotpress_export_file(project_url:, locale:, filters:, destination:)
|
130
145
|
query_params = (filters || {}).transform_keys { |k| "filters[#{k}]" }.merge(format: 'strings')
|
131
|
-
uri = URI.parse("#{project_url.chomp('/')}/#{locale}/default/export-translations
|
146
|
+
uri = URI.parse("#{project_url.chomp('/')}/#{locale}/default/export-translations/?#{URI.encode_www_form(query_params)}")
|
147
|
+
|
148
|
+
# Set an unambiguous User Agent so GlotPress won't rate-limit us
|
149
|
+
options = { 'User-Agent' => Wpmreleasetoolkit::USER_AGENT }
|
150
|
+
|
132
151
|
begin
|
133
|
-
IO.copy_stream(uri.open, destination)
|
152
|
+
IO.copy_stream(uri.open(options), destination)
|
134
153
|
rescue StandardError => e
|
135
|
-
UI.error "Error downloading locale `#{locale}` — #{e.message}"
|
154
|
+
UI.error "Error downloading locale `#{locale}` — #{e.message} (#{uri})"
|
136
155
|
return nil
|
137
156
|
end
|
138
157
|
end
|
@@ -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']
|
@@ -235,7 +234,7 @@ module Fastlane
|
|
235
234
|
begin
|
236
235
|
tempTextFile = Tempfile.new()
|
237
236
|
|
238
|
-
sh('drawText', "html
|
237
|
+
Action.sh('drawText', "html=#{text}", "maxWidth=#{width}", "maxHeight=#{height}", "output=#{tempTextFile.path}", "fontSize=#{font_size}", "stylesheet=#{stylesheet_path}", "alignment=#{position}")
|
239
238
|
|
240
239
|
text_content = open_image(tempTextFile.path).trim
|
241
240
|
text_frame = create_image(width, height)
|
@@ -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
|
+
version: 4.2.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-05-31 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
|
@@ -382,7 +382,7 @@ files:
|
|
382
382
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_alpha_version.rb
|
383
383
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_app_version.rb
|
384
384
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_release_version.rb
|
385
|
-
- lib/fastlane/plugin/wpmreleasetoolkit/actions/android/
|
385
|
+
- lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_hotfix_prechecks.rb
|
386
386
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_tag_build.rb
|
387
387
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_release_notes.rb
|
388
388
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/common/buildkite_trigger_build_action.rb
|
@@ -429,7 +429,7 @@ files:
|
|
429
429
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_app_version.rb
|
430
430
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_build_version.rb
|
431
431
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_store_app_sizes.rb
|
432
|
-
- lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/
|
432
|
+
- lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_hotfix_prechecks.rb
|
433
433
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb
|
434
434
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_localize_project.rb
|
435
435
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_strings_files.rb
|
@@ -454,11 +454,13 @@ 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
|
460
461
|
- lib/fastlane/plugin/wpmreleasetoolkit/helper/promo_screenshots_helper.rb
|
461
462
|
- lib/fastlane/plugin/wpmreleasetoolkit/helper/release_notes_helper.rb
|
463
|
+
- lib/fastlane/plugin/wpmreleasetoolkit/helper/user_agent.rb
|
462
464
|
- lib/fastlane/plugin/wpmreleasetoolkit/models/configuration.rb
|
463
465
|
- lib/fastlane/plugin/wpmreleasetoolkit/models/file_reference.rb
|
464
466
|
- lib/fastlane/plugin/wpmreleasetoolkit/version.rb
|