Skip to content

Commit 626f0d1

Browse files
Validate string-arrays in Android XML (fix #35) (#36)
* Validate string-arrays in Android XML * Print swift version in CI * Nah * Fix build error * Fix tests
1 parent 7f0f913 commit 626f0d1

16 files changed

+191
-4
lines changed

.github/workflows/tests.yml

+5-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ jobs:
1212
steps:
1313
- uses: actions/checkout@v2
1414
# uncomment to pin swift version, but it takes several minutes.
15-
# - uses: fwal/setup-swift@v1
16-
# with:
17-
# swift-version: "5.4"
15+
#- uses: fwal/setup-swift@v1
16+
# with:
17+
# swift-version: "5.6"
1818

1919
- name: Cache SwiftPM
2020
uses: actions/cache@v1
@@ -24,6 +24,8 @@ jobs:
2424
restore-keys: |
2525
${{ runner.os }}-swiftpm-deps-${{ github.workspace }}
2626
27+
- name: Swift Version
28+
run: swift --version
2729
- name: Build
2830
run: swift build
2931
- name: Run tests

Examples/strings-base.xml

+15
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,19 @@
2626

2727
<!-- Missing from translation -->
2828
<string name="missing_from_translation">Missing from translation</string>
29+
30+
<!-- Good string array -->
31+
<string-array name="good_string_array">
32+
<item>Item 1</item>
33+
</string-array>
34+
35+
<!-- String array missing from translation -->
36+
<string-array name="translation_missing_string_array">
37+
<item>Item 1</item>
38+
</string-array>
39+
40+
<!-- String array with wrong item count -->
41+
<string-array name="string_array_wrong_item_count">
42+
<item>Item 1</item>
43+
</string-array>
2944
</resources>

Examples/strings-translation.xml

+16
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,20 @@
2323

2424
<!-- Missing from base -->
2525
<string name="missing_from_base">Missing from base</string>
26+
27+
<!-- Good string array -->
28+
<string-array name="good_string_array">
29+
<item>Item 1</item>
30+
</string-array>
31+
32+
<!-- String array missing from base -->
33+
<string-array name="base_missing_string_array">
34+
<item>Item 1</item>
35+
</string-array>
36+
37+
<!-- String array with wrong item count -->
38+
<string-array name="string_array_wrong_item_count">
39+
<item>Item 1</item>
40+
<item>Item 2</item>
41+
</string-array>
2642
</resources>

Package.swift

+3
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ let package = Package(
3333
.testTarget(
3434
name: "LocheckCommandTests",
3535
dependencies: ["LocheckCommand"]),
36+
.testTarget(
37+
name: "LocheckLogicTests",
38+
dependencies: ["LocheckLogic"]),
3639
])

Sources/LocheckLogic/Problems.swift

+15
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,21 @@ struct StringHasMissingArguments: Problem, StringsProblem, Equatable {
192192
var message: String { "'\(key)' does not include argument(s) at \(args.joined(separator: ", "))" }
193193
}
194194

195+
struct StringArrayItemCountMismatch: Problem, StringsProblem, Equatable {
196+
var base: String?
197+
var translation: String?
198+
199+
var kindIdentifier: String { "string_array_item_count_mismatch" }
200+
var uniquifyingInformation: String { "\(language)-\(key)" }
201+
var severity: Severity { .warning }
202+
let key: String
203+
let language: String
204+
let countBase: Int
205+
let countTranslation: Int
206+
207+
var message: String { "'\(key)' item count mismatch in \(language): \(countTranslation) (should be \(countBase))" }
208+
}
209+
195210
struct StringsdictEntryContainsNoVariablesProblem: Problem, StringsdictProblem, Equatable {
196211
var kindIdentifier: String { "stringsdict_entry_contains_no_variables" }
197212
var uniquifyingInformation: String { "\(key)" }

Sources/LocheckLogic/Types/AndroidStringsFile.swift

+33
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@ struct AndroidString: Equatable {
2121
var line: Int { value.line }
2222
}
2323

24+
struct AndroidStringArray: Equatable {
25+
let key: String
26+
let values: [String]
27+
let line: Int
28+
}
29+
2430
public struct AndroidStringsFile: Equatable {
2531
let path: String
2632
let strings: [AndroidString]
2733
let plurals: [AndroidPlural]
34+
let stringArrays: [AndroidStringArray]
2835
}
2936

3037
public extension AndroidStringsFile {
@@ -42,6 +49,7 @@ public extension AndroidStringsFile {
4249
}
4350

4451
var strings = [AndroidString]()
52+
var stringArrays = [AndroidStringArray]()
4553
var plurals = [AndroidPlural]()
4654

4755
var seenKeys = Set<String>()
@@ -87,6 +95,30 @@ public extension AndroidStringsFile {
8795
key: key,
8896
value: FormatString(string: element.text ?? "", path: path, line: element.lineNumberStart)))
8997
}
98+
case "string-array":
99+
var values = [String]()
100+
for child in element.childElements {
101+
guard child.name == "item" else {
102+
problemReporter.report(
103+
XMLSchemaProblem(message: "Item \(i + 1) has a malformed child (not an 'item')"),
104+
path: path,
105+
lineNumber: child.lineNumberStart)
106+
continue
107+
}
108+
if let cdata = child.CDATA {
109+
guard let string = String(data: cdata, encoding: .utf8) else {
110+
problemReporter.report(
111+
CDATACannotBeDecoded(key: key),
112+
path: path,
113+
lineNumber: child.lineNumberStart)
114+
continue
115+
}
116+
values.append(string)
117+
} else {
118+
values.append(child.text ?? "")
119+
}
120+
}
121+
stringArrays.append(AndroidStringArray(key: key, values: values, line: element.lineNumberStart))
90122
case "plurals":
91123
var values = [String: FormatString]()
92124
for child in element.childElements {
@@ -124,5 +156,6 @@ public extension AndroidStringsFile {
124156

125157
self.strings = strings
126158
self.plurals = plurals
159+
self.stringArrays = stringArrays
127160
}
128161
}

Sources/LocheckLogic/Validators/parseAndValidateAndroidStrings.swift

+28
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,18 @@ func validateAndroidStrings(
5151
translationLanguageName: translationLanguageName,
5252
problemReporter: problemReporter)
5353

54+
validateKeyPresence(
55+
basePath: base.path,
56+
baseKeys: Set(base.stringArrays.map(\.key)),
57+
baseLineNumberMap: base.stringArrays.lo_makeDictionary(makeKey: \.key, makeValue: \.line),
58+
translationPath: translation.path,
59+
translationKeys: Set(translation.stringArrays.map(\.key)),
60+
translationLineNumberMap: translation.stringArrays.lo_makeDictionary(makeKey: \.key, makeValue: \.line),
61+
translationLanguageName: translationLanguageName,
62+
problemReporter: problemReporter)
63+
5464
let baseStringMap = base.strings.lo_makeDictionary(makeKey: \.key)
65+
let baseStringArrayMap = base.stringArrays.lo_makeDictionary(makeKey: \.key)
5566

5667
for translationString in translation.strings {
5768
guard let baseString = baseStringMap[translationString.key] else {
@@ -146,4 +157,21 @@ func validateAndroidStrings(
146157
}
147158
}
148159
}
160+
161+
for translationStringArray in translation.stringArrays {
162+
guard let baseStringArray = baseStringArrayMap[translationStringArray.key] else {
163+
continue // We already threw an error for this in validateKeyPresence()
164+
}
165+
166+
if translationStringArray.values.count != baseStringArray.values.count {
167+
problemReporter.report(
168+
StringArrayItemCountMismatch(
169+
key: translationStringArray.key,
170+
language: translationLanguageName,
171+
countBase: baseStringArray.values.count,
172+
countTranslation: translationStringArray.values.count),
173+
path: translation.path,
174+
lineNumber: translationStringArray.line)
175+
}
176+
}
149177
}

Tests/LocheckCommandTests/ExecutableTests.swift

+63
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,67 @@ class ExecutableTests: XCTestCase {
187187
188188
""")
189189
}
190+
191+
func testExampleOutput_android() throws {
192+
let binary = productsDirectory.appendingPathComponent("locheck")
193+
194+
let process = Process()
195+
process.executableURL = binary
196+
process.arguments = ["androidstrings", "Examples/strings-base.xml", "Examples/strings-translation.xml"]
197+
198+
let stdoutPipe = Pipe()
199+
let stderrPipe = Pipe()
200+
process.standardOutput = stdoutPipe
201+
process.standardError = stderrPipe
202+
203+
try process.run()
204+
process.waitUntilExit()
205+
206+
let stdout = String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
207+
let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
208+
209+
XCTAssertEqual(stdout!, """
210+
211+
Summary:
212+
Examples/strings-base.xml
213+
missing_from_translation:
214+
WARNING: 'missing_from_translation' is missing from Examples (key_missing_from_translation)
215+
translation_missing_string_array:
216+
WARNING: 'translation_missing_string_array' is missing from Examples (key_missing_from_translation)
217+
Examples/strings-translation.xml
218+
base_missing_string_array:
219+
WARNING: 'base_missing_string_array' is missing from the base translation (key_missing_from_base)
220+
missing_from_base:
221+
WARNING: 'missing_from_base' is missing from the base translation (key_missing_from_base)
222+
string_array_wrong_item_count:
223+
WARNING: 'string_array_wrong_item_count' item count mismatch in Examples: 2 (should be 1) (string_array_item_count_mismatch)
224+
translation_has_invalid_specifier:
225+
ERROR: Specifier for argument 2 does not match (should be d, is lu) (string_has_invalid_argument)
226+
Base: %s %d
227+
Translation: %s %lu
228+
translation_has_missing_arg:
229+
WARNING: 'translation_has_missing_arg' does not include argument(s) at 2 (string_has_missing_arguments)
230+
Base: %s %d
231+
Translation: %s
232+
translation_has_missing_phrase:
233+
ERROR: 'translation_has_missing_phrase' does not include argument(s): object_name (phrase_has_missing_arguments)
234+
Base: Could not add {user_name} to \\"{object_name}\\"
235+
Translation: Could not add {user_name}
236+
6 warnings, 2 errors
237+
Errors found
238+
239+
""")
240+
241+
XCTAssertEqual(stderr!, """
242+
Examples/strings-base.xml:28: warning: 'missing_from_translation' is missing from Examples (key_missing_from_translation)
243+
Examples/strings-translation.xml:25: warning: 'missing_from_base' is missing from the base translation (key_missing_from_base)
244+
Examples/strings-base.xml:36: warning: 'translation_missing_string_array' is missing from Examples (key_missing_from_translation)
245+
Examples/strings-translation.xml:33: warning: 'base_missing_string_array' is missing from the base translation (key_missing_from_base)
246+
Examples/strings-translation.xml:17: error: 'translation_has_missing_phrase' does not include argument(s): object_name (phrase_has_missing_arguments)
247+
Examples/strings-translation.xml:21: error: Specifier for argument 2 does not match (should be d, is lu) (string_has_invalid_argument)
248+
Examples/strings-translation.xml:22: warning: 'translation_has_missing_arg' does not include argument(s) at 2 (string_has_missing_arguments)
249+
Examples/strings-translation.xml:38: warning: 'string_array_wrong_item_count' item count mismatch in Examples: 2 (should be 1) (string_array_item_count_mismatch)
250+
251+
""")
252+
}
190253
}
File renamed without changes.

Tests/LocheckCommandTests/parseAndValidateAndroidStringsTests.swift Tests/LocheckLogicTests/parseAndValidateAndroidStringsTests.swift

+13-1
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,20 @@ class ParseAndValidateAndroidStringsTests: XCTestCase {
2525
translationLanguageName: "demo",
2626
problemReporter: problemReporter)
2727

28-
XCTAssertEqual(problemReporter.problems.count, 5)
28+
XCTAssertEqual(problemReporter.problems.count, 8)
2929
let problems = problemReporter.problems.map(\.problem)
30+
31+
XCTAssertEqual(problems.map(\.kindIdentifier), [
32+
"key_missing_from_translation",
33+
"key_missing_from_base",
34+
"key_missing_from_translation",
35+
"key_missing_from_base",
36+
"phrase_has_missing_arguments",
37+
"string_has_invalid_argument",
38+
"string_has_missing_arguments",
39+
"string_array_item_count_mismatch"
40+
])
41+
3042
CastAndAssertEqual(problems[0], KeyMissingFromTranslation(key: "missing_from_translation", language: "demo"))
3143
CastAndAssertEqual(problems[1], KeyMissingFromBase(key: "missing_from_base"))
3244
}

0 commit comments

Comments
 (0)