Skip to content

Commit 80c6c91

Browse files
committed
Fix ios_l10n_linter to allow missing translations
The previous implementation used a `diff` of textual outputs from SwiftGen, which worked so far because in the host app projects, locales which were missing translations for some keys were previously replaced by `”key” = “key”` entries (this was back when we assumes that our keys were always going to be the English copies) But since the recent improvements in our Localization Tooling, this is not the case anymore, and any key that does not have a translation in a given local is now just omitted and not present in that locale’s `Localizable.strings` — just as it has always been intended. This made the logic of the linter break as it relied on this assumption to make the `diff` it ran on the textual outputs be useful. But now that the `Localizable.strings` files for translations might actually miss a lot of the keys present in the reference locale, we needed to be smarter This changes the approach from doing a plain textual `diff` of the outputs, to parsing the outputs of SwiftGen into a `Hash<Key, ExpectedParametersList>`, so we can then filter and ignore the keys that are missing in each locale, and only raise violations for keys that are present in both the localization and the reference locale (typically English), in other words, only for keys for which there are actual translations to compare the placeholders for.
1 parent 5cf371e commit 80c6c91

File tree

6 files changed

+43
-75
lines changed

6 files changed

+43
-75
lines changed

lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ def self.run_linter(params)
2828
only_langs: params[:only_langs]
2929
)
3030

31-
violations.each do |lang, diff|
32-
UI.error "Inconsistencies found between '#{params[:base_lang]}' and '#{lang}':\n\n#{diff}\n"
31+
violations.each do |lang, violations|
32+
UI.error "Inconsistencies found between '#{params[:base_lang]}' and '#{lang}':\n\n#{violations.join("\n")}\n"
3333
end
3434

3535
violations

lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_linter_helper.rb

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ def install_swiftgen!
5858
#
5959
# @param [String] input_dir The path (ideally absolute) to the directory containing the `.lproj` folders to parse
6060
# @param [String] base_lang The code name (i.e the basename of one of the `.lproj` folders) of the locale to use as the baseline
61-
# @return [Hash<String, String>] A hash whose keys are the language codes (basename of `.lproj` folders) for which violations were found,
62-
# and the values are the output of the `diff` showing these violations.
61+
# @return [Hash<String, Array<String>>] A hash of violations, keyed by language code, whose values are the list of violation messages for that language
6362
#
6463
def run(input_dir:, base_lang: DEFAULT_BASE_LANG, only_langs: nil)
6564
check_swiftgen_installed || install_swiftgen!
@@ -87,7 +86,7 @@ def template_content
8786
<<~TEMPLATE
8887
{% macro recursiveBlock table item %}
8988
{% for string in item.strings %}
90-
"{{string.key}}" => [{{string.types|join:","}}]
89+
{{string.key}} ==> [{{string.types|join:","}}]
9190
{% endfor %}
9291
{% for child in item.children %}
9392
{% call recursiveBlock table child %}
@@ -139,28 +138,27 @@ def generate_swiftgen_config!(input_dir, output_dir, only_langs: nil)
139138
return [config_file, langs]
140139
end
141140

142-
# Because we use English copy verbatim as key names, some keys are the same except for the upper/lowercase.
143-
# We need to sort the output again because SwiftGen only sort case-insensitively so that means keys that are
144-
# the same except case might be in swapped order for different outputs
141+
# Returns a Hash mapping the list of expected parameter types for each of the keys based in the %… placeholders found in their `.strings` files
145142
#
146143
# @param [String] dir The temporary directory in which the file to sort lines for is located
147144
# @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.
148146
#
149-
def sort_file_lines!(dir, lang)
147+
def placeholder_types_for_keys(dir, lang)
150148
file = File.join(dir, output_filename(lang))
151149
return nil unless File.exist?(file)
152150

153-
sorted_lines = File.readlines(file).sort
154-
File.write(file, sorted_lines.join)
155-
return file
151+
File.readlines(file).map do |line|
152+
line.match(/^(.*) ==> (\[[A-Za-z,]*\])$/)&.captures
153+
end.compact.to_h
156154
end
157155

158156
# 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.
159157
#
160158
# @param [String] input_dir The directory where the `.lproj` folders to scan are located
161159
# @param [String] base_lang The base language used as source of truth that all other languages will be compared against
162160
# @param [Array<String>] only_langs The list of languages to limit the generation for. Useful to focus only on a couple of issues or just one language
163-
# @return [Hash<String, String>] A hash of violations, keyed by language code, whose values are the diff output.
161+
# @return [Hash<String, Array<String>>] A hash of violations, keyed by language code, whose values are the list of violation messages for that language
164162
#
165163
# @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.
166164
#
@@ -172,34 +170,22 @@ def find_diffs(input_dir:, base_lang:, only_langs: nil)
172170
Action.sh(swiftgen_bin, 'config', 'run', '--config', config_file)
173171

174172
# Run diffs
175-
base_file = sort_file_lines!(tmpdir, base_lang)
173+
params_for_base_lang = placeholder_types_for_keys(tmpdir, base_lang)
176174
langs.delete(base_lang)
177175
return langs.map do |lang|
178-
file = sort_file_lines!(tmpdir, lang)
176+
params_for_lang = placeholder_types_for_keys(tmpdir, lang)
177+
179178
# If the lang ends up not having any translation at all (e.g. a `.lproj` without any `.strings` file in it but maybe just a storyboard or assets catalog), ignore it
180-
next nil if file.nil? || only_empty_lines?(file)
181-
182-
# Compute the diff
183-
diff = `diff -U0 "#{base_file}" "#{file}"`
184-
# Remove the lines starting with `---`/`+++` which contains the file names (which are temp files we don't want to expose in the final diff to users)
185-
# Note: We still keep the `@@ from-file-line-numbers to-file-line-numbers @@` lines to help the user know the index of the key to find it faster,
186-
# and also to serve as a clear visual separator between diff entries in the output.
187-
# Those numbers won't be matching the original `.strings` file line numbers because they are line numbers in the SwiftGen-generated intermediate
188-
# file instead, but they can still give an indication at the index in the list of keys at which this difference is located.
189-
diff.gsub!(/^(---|\+\+\+).*\n/, '')
190-
diff.empty? ? nil : [lang, diff]
191-
end.compact.to_h
192-
end
193-
end
179+
next nil if params_for_lang.nil? || params_for_lang.empty?
194180

195-
# Returns true if the file only contains empty lines, i.e. lines that only contains whitespace (space, tab, CR, LF)
196-
def only_empty_lines?(file)
197-
File.open(file) do |f|
198-
while (line = f.gets)
199-
return false unless line.strip.empty?
200-
end
181+
violations = params_for_lang.map do |key, param_types|
182+
next "`#{key}` was unexpected, as it is not present in the base locale." if params_for_base_lang[key].nil?
183+
next "`#{key}` expected placeholders for #{params_for_base_lang[key]} but found #{param_types} instead." if params_for_base_lang[key] != param_types
184+
end.compact
185+
186+
[lang, violations] unless violations.empty?
187+
end.compact.to_h
201188
end
202-
return true
203189
end
204190
end
205191
end

spec/test-data/translations/test-lint-ios-no-violations.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ test_data:
55
"string_int_pos" = "String %1$@ and Int %2$d";
66
"string_int_pos_inv" = "Int %2$d before String %1$@";
77
"many_placeholders_mix_pos" = "String %2$@, Float %4$f, Int %1$d, Long %5$li, Precise %3$.3lf";
8+
"extra_string_not_translated" = "This string might not have a translation yet in %1$d of our locales.";
89
"repeated_placeholder" = "String %1$@, yes, I repeat, that's %1$@ indeed, told you %2$d times.";
910
fr: |
1011
"string_no_pos" = "String %@ here.";

spec/test-data/translations/test-lint-ios-tricky-chars.yaml

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,9 @@ test_data:
2323
"repeated_placeholder" = "السلسلة %1$@ ، نعم ، أكرر ، هذه %1$@ بالفعل ، وليست %2$d."; // OK
2424
2525
result:
26-
fr: |
27-
@@ -3 +3 @@
28-
-"many_placeholders_mix_pos" => [Int,String,Float,Float,Int]
29-
+"many_placeholders_mix_pos" => [String,Float,Float,Int]
30-
@@ -5 +5 @@
31-
-"string_int_pos" => [String,Int]
32-
+"string_int_pos" => [Int]
33-
@@ -7 +7 @@
34-
-"string_no_pos" => [String]
35-
+"string_no_pos" => []
36-
ar: |
37-
@@ -8 +8 @@
38-
-"string_pos" => [String]
39-
+"string_pos" => []
26+
fr:
27+
- "`many_placeholders_mix_pos` expected placeholders for [Int,String,Float,Float,Int] but found [String,Float,Float,Int] instead."
28+
- "`string_int_pos` expected placeholders for [String,Int] but found [Int] instead."
29+
- "`string_no_pos` expected placeholders for [String] but found [] instead."
30+
ar:
31+
- "`string_pos` expected placeholders for [String] but found [] instead."

spec/test-data/translations/test-lint-ios-wrong-placeholder-count.yaml

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ test_data:
1212
"string_int_pos" = "String %1$@ and Int %2$d";
1313
"string_int_pos_inv" = "Int %2$d before String %1$@";
1414
"many_placeholders_mix_pos" = "String %2$@, Float %4$f, Int %1$d, Long %5$li, Precise %3$.3lf";
15+
"unexpected_key_not_present_in_base" = "This key should not exist in the translation given it does not exist in English";
1516
"repeated_placeholder" = "String %1$@, yes, that's it indeed, %@.";
1617
it: |
1718
"string_no_pos" = "String %@ here.";
@@ -21,12 +22,9 @@ test_data:
2122
"many_placeholders_mix_pos" = "String %2$@, Float %4$f, Int %1$d, Long %5$li, Precise %3$.3lf";
2223
"repeated_placeholder" = "String %1$@, yes, and not %2%@, I repeat, that's not %2$@.";
2324
24-
result:
25-
fr: |
26-
@@ -7 +7 @@
27-
-"string_no_pos" => [String]
28-
+"string_no_pos" => [String,String]
29-
it: |
30-
@@ -4 +4 @@
31-
-"repeated_placeholder" => [String]
32-
+"repeated_placeholder" => [String,String]
25+
result:
26+
fr:
27+
- "`string_no_pos` expected placeholders for [String] but found [String,String] instead."
28+
- "`unexpected_key_not_present_in_base` was unexpected, as it is not present in the base locale."
29+
it:
30+
- "`repeated_placeholder` expected placeholders for [String] but found [String,String] instead."

spec/test-data/translations/test-lint-ios-wrong-placeholder-types.yaml

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,10 @@ test_data:
2121
"many_placeholders_mix_pos" = "String %2$@, Float %4$f, Int %1$d, Long %5$li, Precise %3$.3lf";
2222
"repeated_placeholder" = "String %1$d, yes, I repeat, that's %1$d indeed, I said %d.";
2323
24-
result:
25-
fr: |
26-
@@ -3 +3 @@
27-
-"many_placeholders_mix_pos" => [Int,String,Float,Int]
28-
+"many_placeholders_mix_pos" => [String,Float,Int,Int]
29-
@@ -5 +5 @@
30-
-"string_int_pos" => [String,Int]
31-
+"string_int_pos" => [Int,String]
32-
@@ -7 +7 @@
33-
-"string_no_pos" => [String]
34-
+"string_no_pos" => [Int]
35-
36-
it: |
37-
@@ -4 +4 @@
38-
-"repeated_placeholder" => [String]
39-
+"repeated_placeholder" => [Int]
24+
result:
25+
fr:
26+
- "`many_placeholders_mix_pos` expected placeholders for [Int,String,Float,Int] but found [String,Float,Int,Int] instead."
27+
- "`string_int_pos` expected placeholders for [String,Int] but found [Int,String] instead."
28+
- "`string_no_pos` expected placeholders for [String] but found [Int] instead."
29+
it:
30+
- "`repeated_placeholder` expected placeholders for [String] but found [Int] instead."

0 commit comments

Comments
 (0)