diff --git a/Examples/strings-base.xml b/Examples/strings-base.xml
index b94de9a..747c302 100644
--- a/Examples/strings-base.xml
+++ b/Examples/strings-base.xml
@@ -21,6 +21,26 @@
%s %d
%s %d
+
+ You have %,d points
+ Change: %+d
+ Code: %05d
+ Price: %.2f
+ Value: %10.2f
+ Total: %+,d
+ Points: %,d
+ Amount: %+d
+ Amount: %+d
+
+
+ Value: %10d
+ Price: %.4f
+ Amount: %8.2f
+ Total: %+10d
+ Count: %10d
+ Rate: %.2f
+ Score: %8.3f
+
Photos
diff --git a/Examples/strings-translation.xml b/Examples/strings-translation.xml
index 89f5f6b..73d8183 100644
--- a/Examples/strings-translation.xml
+++ b/Examples/strings-translation.xml
@@ -21,6 +21,29 @@
%s %lu
%s
+
+ Tienes %,d puntos
+ Cambio: %+d
+ Código: %05d
+ Precio: %.2f
+ Valor: %10.2f
+ Total: %+,d
+ Puntos: %d
+ Cantidad: % d
+ Cantidad: %-d
+
+
+ Valor: %10d
+ Precio: %.4f
+ Cantidad: %8.2f
+ Total: %+10d
+
+ Cuenta: %5d
+
+ Tasa: %.4f
+
+ Puntuación: %10.2f
+
Missing from base
diff --git a/Sources/LocheckCommand/FileOrDirectoryArg.swift b/Sources/LocheckCommand/FileOrDirectoryArg.swift
index 7a3574f..c98cd56 100644
--- a/Sources/LocheckCommand/FileOrDirectoryArg.swift
+++ b/Sources/LocheckCommand/FileOrDirectoryArg.swift
@@ -1,5 +1,5 @@
//
-// FileArg.swift
+// FileOrDirectoryArg.swift
//
//
// Created by Steve Landey on 8/18/21.
diff --git a/Sources/LocheckCommand/main.swift b/Sources/LocheckCommand/main.swift
index 7fef20d..7f04a95 100644
--- a/Sources/LocheckCommand/main.swift
+++ b/Sources/LocheckCommand/main.swift
@@ -150,7 +150,7 @@ struct StringCatalog: HasIgnoreWithShorthand, ParsableCommand {
ignore: ignoreWithShorthand,
ignoreWarnings: ignoreWarnings,
treatWarningsAsErrors: treatWarningsAsErrors) { problemReporter in
- let catalogFile = try! File(path: self.catalogFile.argument)
+ let catalogFile = try! File(path: catalogFile.argument)
parseAndValidateStringCatalog(
stringCatalogFile: catalogFile,
problemReporter: problemReporter)
diff --git a/Sources/LocheckLogic/Expressions.swift b/Sources/LocheckLogic/Expressions.swift
index f3845ec..7bfa7c9 100644
--- a/Sources/LocheckLogic/Expressions.swift
+++ b/Sources/LocheckLogic/Expressions.swift
@@ -26,7 +26,7 @@ struct Expressions {
pattern: Expressions.stringPairExpression,
options: .anchorsMatchLines)
- // MARK: Arguments
+ // MARK: - Shared
// https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFStrings/formatSpecifiers.html#//apple_ref/doc/uid/TP40004265
private static let lengthModifiers: [String] = [
@@ -42,8 +42,10 @@ struct Expressions {
]
private static let lengthExpression = lengthModifiers.joined(separator: "|")
+ // MARK: - iOS Format Specifiers
+
// https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFStrings/formatSpecifiers.html#//apple_ref/doc/uid/TP40004265
- private static let specifiers: [String] = [
+ private static let iosSpecifiers: [String] = [
// omit %%, it doesn't affect interpolation
"@",
"d",
@@ -68,16 +70,76 @@ struct Expressions {
"A",
"F",
]
- private static let specifierExpression = specifiers.joined(separator: "|")
+ private static let iosSpecifierExpression = iosSpecifiers.joined(separator: "|")
// Technically length modifiers are invalid for @ and potentially some others, but in practice
// it probably doesn't matter.
- private static let nativeArgumentExpression =
- "%((?\\d+)\\$)?(?(\(lengthExpression))?(\(specifierExpression)))"
- static let nativeArgumentRegex = try! NSRegularExpression(
- pattern: Expressions.nativeArgumentExpression,
+ private static let iosArgumentExpression =
+ "%((?\\d+)\\$)?(?(\(lengthExpression))?(\(iosSpecifierExpression)))"
+ static let iosArgumentRegex = try! NSRegularExpression(
+ pattern: Expressions.iosArgumentExpression,
+ options: [])
+
+ // MARK: - Android Format Specifiers
+
+ // https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Formatter.html
+ private static let androidSpecifiers: [String] = [
+ // General
+ "b", // boolean
+ "B", // boolean (uppercase)
+ "h", // hash code (hexadecimal)
+ "H", // hash code (uppercase)
+ "s", // string
+ "S", // string (uppercase)
+
+ // Character
+ "c", // character
+ "C", // character (uppercase)
+
+ // Integral
+ "d", // decimal integer
+ "o", // octal integer
+ "x", // hexadecimal integer
+ "X", // hexadecimal integer (uppercase)
+
+ // Floating Point
+ "e", // scientific notation
+ "E", // scientific notation (uppercase)
+ "f", // decimal floating point
+ "g", // general (uses e or f)
+ "G", // general (uppercase)
+ "a", // hexadecimal floating point
+ "A", // hexadecimal floating point (uppercase)
+
+ // Date/Time (prefix, followed by date/time conversion suffix)
+ "t", // date/time
+ "T", // date/time (uppercase)
+ ]
+ private static let androidSpecifierExpression = androidSpecifiers.joined(separator: "|")
+
+ // Android/Java Formatter flags: '-', '+', '0', ',', '(', '#'
+ // https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Formatter.html
+ // Flags: '-' (left-justify), '+' (include sign), '0' (zero-pad),
+ // ',' (grouping separator), '(' (negative in parens), '#' (alternate form)
+ // Note: Space flag (' ') is intentionally omitted - it's rarely used in localized
+ // strings and causes false positives with patterns like "80% off" or "%% done"
+ private static let androidFlagsExpression = "[-+0,(#]*"
+ private static let androidWidthExpression = "\\d*"
+ private static let androidPrecisionExpression = "(?:\\.\\d+)?"
+
+ // Full format: %[argument_index$][flags][width][.precision]conversion
+ private static let androidArgumentExpression =
+ "%((?\\d+)\\$)?(?\(androidFlagsExpression)\(androidWidthExpression)\(androidPrecisionExpression)(\(androidSpecifierExpression)))"
+ static let androidArgumentRegex = try! NSRegularExpression(
+ pattern: Expressions.androidArgumentExpression,
options: [])
+ // MARK: - Legacy (backward compatibility)
+
+ static let nativeArgumentRegex = iosArgumentRegex
+
+ // MARK: - Other
+
private static let stringsdictArgumentExpression = #"%[0-9]*\$?#@(?.+?)@"#
static let stringsdictArgumentRegex = try! NSRegularExpression(
pattern: Expressions.stringsdictArgumentExpression,
diff --git a/Sources/LocheckLogic/Extensions/Sequence+locheck.swift b/Sources/LocheckLogic/Extensions/Sequence+locheck.swift
index 8f6e7cf..9dee964 100644
--- a/Sources/LocheckLogic/Extensions/Sequence+locheck.swift
+++ b/Sources/LocheckLogic/Extensions/Sequence+locheck.swift
@@ -1,5 +1,5 @@
//
-// Sequence.swift
+// Sequence+locheck.swift
//
//
// Created by Steve Landey on 8/27/21.
diff --git a/Sources/LocheckLogic/Types/AndroidStringsFile.swift b/Sources/LocheckLogic/Types/AndroidStringsFile.swift
index 9f59ced..334e278 100644
--- a/Sources/LocheckLogic/Types/AndroidStringsFile.swift
+++ b/Sources/LocheckLogic/Types/AndroidStringsFile.swift
@@ -88,12 +88,20 @@ public extension AndroidStringsFile {
strings.append(
AndroidString(
key: key,
- value: FormatString(string: string, path: path, line: element.lineNumberStart)))
+ value: FormatString(
+ string: string,
+ path: path,
+ line: element.lineNumberStart,
+ platform: .android)))
} else {
strings.append(
AndroidString(
key: key,
- value: FormatString(string: element.text ?? "", path: path, line: element.lineNumberStart)))
+ value: FormatString(
+ string: element.text ?? "",
+ path: path,
+ line: element.lineNumberStart,
+ platform: .android)))
}
case "string-array":
var values = [String]()
@@ -143,7 +151,11 @@ public extension AndroidStringsFile {
lineNumber: element.lineNumberStart)
continue
}
- values[childKey] = FormatString(string: child.text ?? "", path: path, line: element.lineNumberStart)
+ values[childKey] = FormatString(
+ string: child.text ?? "",
+ path: path,
+ line: element.lineNumberStart,
+ platform: .android)
}
plurals.append(AndroidPlural(key: key, line: element.lineNumberStart, values: values))
default:
diff --git a/Sources/LocheckLogic/Types/FormatString.swift b/Sources/LocheckLogic/Types/FormatString.swift
index 55c3ddd..fde654d 100644
--- a/Sources/LocheckLogic/Types/FormatString.swift
+++ b/Sources/LocheckLogic/Types/FormatString.swift
@@ -9,7 +9,7 @@ import Foundation
/**
Represents a string containing format specifiers. This type is shared by the iOS
- and Android validators because they use the same syntax for these kinds of strings.
+ and Android validators, but uses platform-specific parsing for format specifiers.
*/
struct FormatString: Equatable {
enum Kind: Equatable {
@@ -17,19 +17,26 @@ struct FormatString: Equatable {
case phrase // https://github.com/square/phrase
}
+ enum Platform: Equatable {
+ case ios
+ case android
+ }
+
let string: String
let arguments: [FormatArgument]
let phraseArguments: [String]
let path: String
let line: Int
let kind: Kind
+ let platform: Platform
- init(string: String, path: String, line: Int) {
+ init(string: String, path: String, line: Int, platform: Platform = .ios) {
self.string = string
self.path = path
self.line = line
+ self.platform = platform
- let nativeArguments = parseNativeArguments(string: string)
+ let nativeArguments = parseNativeArguments(string: string, platform: platform)
arguments = nativeArguments
if nativeArguments.isEmpty {
// Only use Phrase format if native syntax is not present
@@ -43,10 +50,18 @@ struct FormatString: Equatable {
}
/// Transform a single string into parsed `FormatSpecifier` objects
-private func parseNativeArguments(string: String) -> [FormatArgument] {
+private func parseNativeArguments(string: String, platform: FormatString.Platform) -> [FormatArgument] {
var nextImplicitPosition = 1
- return Expressions.nativeArgumentRegex
+ let regex: NSRegularExpression
+ switch platform {
+ case .ios:
+ regex = Expressions.iosArgumentRegex
+ case .android:
+ regex = Expressions.androidArgumentRegex
+ }
+
+ return regex
.lo_matches(in: string)
.enumerated()
.compactMap { (i: Int, match: NSTextCheckingResult) -> FormatArgument? in
diff --git a/Sources/LocheckLogic/Types/LocalizedStringPair.swift b/Sources/LocheckLogic/Types/LocalizedStringPair.swift
index 86ad6ca..59d0b4f 100644
--- a/Sources/LocheckLogic/Types/LocalizedStringPair.swift
+++ b/Sources/LocheckLogic/Types/LocalizedStringPair.swift
@@ -31,14 +31,13 @@ extension LocalizedStringPair {
baseString: String,
translationString: String,
path: String,
- line: Int
- ) {
+ line: Int) {
self.key = key
- self.string = "\"\(key)\" = \"\(translationString)\";"
+ string = "\"\(key)\" = \"\(translationString)\";"
self.path = path
self.line = line
- self.base = FormatString(string: baseString, path: path, line: line)
- self.translation = FormatString(string: translationString, path: path, line: line)
+ base = FormatString(string: baseString, path: path, line: line)
+ translation = FormatString(string: translationString, path: path, line: line)
}
init?(
diff --git a/Sources/LocheckLogic/Types/StringCatalog.swift b/Sources/LocheckLogic/Types/StringCatalog.swift
index 603137f..9f6a523 100644
--- a/Sources/LocheckLogic/Types/StringCatalog.swift
+++ b/Sources/LocheckLogic/Types/StringCatalog.swift
@@ -89,39 +89,37 @@ extension StringEntry {
func getLocalizedStringPairs(
key: String,
basePath: String,
- sourceLanguage: String
- ) -> [LocalizedStringPair] {
+ sourceLanguage: String) -> [LocalizedStringPair] {
var pairs: [LocalizedStringPair] = []
-
+
guard let sourceLocalization = localizations[sourceLanguage] else {
return pairs
}
-
+
let sourceString = sourceLocalization.stringUnit?.value ?? key
-
+
for (language, localization) in localizations {
if language == sourceLanguage {
continue // Skip source language for validation
}
-
+
if let stringUnit = localization.stringUnit {
let pair = LocalizedStringPair(
key: key,
baseString: sourceString,
translationString: stringUnit.value,
path: basePath,
- line: 1
- )
+ line: 1)
pairs.append(pair)
}
-
+
if let variations = localization.variations {
// Handle plural variations
if let pluralForms = variations.plural {
// Get source plural form for comparison (use "other" as fallback)
let sourcePlurals = sourceLocalization.variations?.plural
let fallbackSource = sourcePlurals?["other"]?.stringUnit.value ?? sourceString
-
+
for (pluralForm, pluralVariation) in pluralForms {
let sourceForPluralForm = sourcePlurals?[pluralForm]?.stringUnit.value ?? fallbackSource
let pair = LocalizedStringPair(
@@ -129,13 +127,12 @@ extension StringEntry {
baseString: sourceForPluralForm,
translationString: pluralVariation.stringUnit.value,
path: basePath,
- line: 1
- )
+ line: 1)
pairs.append(pair)
}
}
-
- // Handle device variations
+
+ // Handle device variations
if let deviceForms = variations.device {
for (deviceType, deviceVariation) in deviceForms {
let pair = LocalizedStringPair(
@@ -143,14 +140,13 @@ extension StringEntry {
baseString: sourceString,
translationString: deviceVariation.stringUnit.value,
path: basePath,
- line: 1
- )
+ line: 1)
pairs.append(pair)
}
}
}
}
-
+
return pairs
}
}
diff --git a/Sources/LocheckLogic/Types/StringsdictEntry.swift b/Sources/LocheckLogic/Types/StringsdictEntry.swift
index 6efb5b8..6b461a8 100644
--- a/Sources/LocheckLogic/Types/StringsdictEntry.swift
+++ b/Sources/LocheckLogic/Types/StringsdictEntry.swift
@@ -18,7 +18,7 @@ public struct StringsdictEntry: Equatable {
let rules: [String: StringsdictRule] // derived from XML
func validateRuleVariables(problemReporter: ProblemReporter) {
- let checkRule = { (ruleKey: String, variables: [String]) -> Void in
+ let checkRule = { (ruleKey: String, variables: [String]) in
for variable in variables where rules[variable] == nil {
problemReporter.report(
StringsdictEntryHasMissingVariable(
@@ -57,7 +57,7 @@ public struct StringsdictEntry: Equatable {
// but that would require us to remember which span of each string maps back to which variable,
// which is a lot of extra bookkeeping to do at this stage of the project.
- let report = { (problem: Problem, line: Int) -> Void in
+ let report = { (problem: Problem, line: Int) in
problemReporter.report(problem, path: path, lineNumber: line)
}
@@ -249,7 +249,7 @@ extension StringsdictEntry {
line = node.lineNumberStart
self.path = path
- let report = { (problem: Problem, line: Int) -> Void in
+ let report = { (problem: Problem, line: Int) in
problemReporter.report(problem, path: path, lineNumber: line)
}
diff --git a/Sources/LocheckLogic/Types/StringsdictFile.swift b/Sources/LocheckLogic/Types/StringsdictFile.swift
index a52ae17..20f32ec 100644
--- a/Sources/LocheckLogic/Types/StringsdictFile.swift
+++ b/Sources/LocheckLogic/Types/StringsdictFile.swift
@@ -1,5 +1,5 @@
//
-// Stringsdict.swift
+// StringsdictFile.swift
//
//
// Created by Steve Landey on 8/25/21.
diff --git a/Sources/LocheckLogic/Types/StringsdictRule.swift b/Sources/LocheckLogic/Types/StringsdictRule.swift
index e6cb7a0..04abc7f 100644
--- a/Sources/LocheckLogic/Types/StringsdictRule.swift
+++ b/Sources/LocheckLogic/Types/StringsdictRule.swift
@@ -23,7 +23,7 @@ extension StringsdictRule {
init?(key: String, node: XML.Element, path: String, problemReporter: ProblemReporter) {
line = node.lineNumberStart
- let report = { (problem: Problem) -> Void in
+ let report = { (problem: Problem) in
problemReporter.report(problem, path: path, lineNumber: node.lineNumberStart)
}
diff --git a/Sources/LocheckLogic/Validators/parseAndValidateStringCatalog.swift b/Sources/LocheckLogic/Validators/parseAndValidateStringCatalog.swift
index 5d8be4a..257f8bb 100644
--- a/Sources/LocheckLogic/Validators/parseAndValidateStringCatalog.swift
+++ b/Sources/LocheckLogic/Validators/parseAndValidateStringCatalog.swift
@@ -14,39 +14,36 @@ import Foundation
public func parseAndValidateStringCatalog(
stringCatalogFile: File,
problemReporter: ProblemReporter) {
-
guard let stringCatalog = StringCatalog(path: stringCatalogFile.path, problemReporter: problemReporter) else {
return
}
-
+
var allPairs: [LocalizedStringPair] = []
-
+
// Extract all localized string pairs from the catalog
for (key, entry) in stringCatalog.strings {
let pairs = entry.getLocalizedStringPairs(
key: key,
basePath: stringCatalogFile.path,
- sourceLanguage: stringCatalog.sourceLanguage
- )
+ sourceLanguage: stringCatalog.sourceLanguage)
allPairs.append(contentsOf: pairs)
}
-
+
// Group by language for validation
let pairsByLanguage = Dictionary(grouping: allPairs) { pair in
// Extract language from validation context - we need to infer this from the translation
// For now, we'll validate all pairs together as they already contain base/translation comparison
- return "all"
+ "all"
}
-
+
// Validate each language group
for (_, pairs) in pairsByLanguage {
validateStringCatalogPairs(
pairs: pairs,
sourceLanguage: stringCatalog.sourceLanguage,
- problemReporter: problemReporter
- )
+ problemReporter: problemReporter)
}
-
+
// Additional string catalog specific validations
validateStringCatalogStructure(stringCatalog: stringCatalog, problemReporter: problemReporter)
}
@@ -58,7 +55,6 @@ private func validateStringCatalogPairs(
pairs: [LocalizedStringPair],
sourceLanguage: String,
problemReporter: ProblemReporter) {
-
for pair in pairs {
// Check for format argument consistency
let baseArgumentPositions = Set(pair.base.arguments.map(\.position))
@@ -134,9 +130,8 @@ private func validateStringCatalogPairs(
private func validateStringCatalogStructure(
stringCatalog: StringCatalog,
problemReporter: ProblemReporter) {
-
let sourceLanguage = stringCatalog.sourceLanguage
-
+
// Check that source language exists in all entries
for (key, entry) in stringCatalog.strings {
if entry.localizations[sourceLanguage] == nil {
@@ -147,14 +142,14 @@ private func validateStringCatalogStructure(
path: "StringCatalog",
lineNumber: 1)
}
-
+
// Check for incomplete translations
let sourceLocalization = entry.localizations[sourceLanguage]
for (language, localization) in entry.localizations {
if language == sourceLanguage {
continue
}
-
+
// Check if translation is marked as needs work
if let stringUnit = localization.stringUnit,
stringUnit.state == "needs_work" {
@@ -163,11 +158,10 @@ private func validateStringCatalogStructure(
path: "StringCatalog",
lineNumber: 1)
}
-
+
// Check plural consistency
if let sourcePlurals = sourceLocalization?.variations?.plural,
let translationPlurals = localization.variations?.plural {
-
// Ensure critical plural forms are present
let requiredForms = ["one", "other"]
for form in requiredForms {
diff --git a/Sources/LocheckLogic/parseXML.swift b/Sources/LocheckLogic/parseXML.swift
index 6cbb17a..f6008f9 100644
--- a/Sources/LocheckLogic/parseXML.swift
+++ b/Sources/LocheckLogic/parseXML.swift
@@ -11,7 +11,7 @@ import SwiftyXMLParser
func parseXML(file: File, problemReporter: ProblemReporter) -> XML.Accessor? {
do {
- return XML.parse(try file.read())
+ return try XML.parse(file.read())
} catch {
problemReporter.report(
XMLErrorProblem(message: error.localizedDescription),
diff --git a/Tests/LocheckCommandTests/ExecutableTests.swift b/Tests/LocheckCommandTests/ExecutableTests.swift
index 84fad90..c565552 100644
--- a/Tests/LocheckCommandTests/ExecutableTests.swift
+++ b/Tests/LocheckCommandTests/ExecutableTests.swift
@@ -276,7 +276,7 @@ class ExecutableTests: XCTestCase {
string_array_wrong_item_count:
WARNING: 'string_array_wrong_item_count' item count mismatch in Examples: 2 (should be 1) (string_array_item_count_mismatch)
translation_has_invalid_specifier:
- ERROR: Specifier for argument 2 does not match (should be d, is lu) (string_has_invalid_argument)
+ WARNING: 'translation_has_invalid_specifier' does not include argument(s) at 2 (string_has_missing_arguments)
Base: %s %d
Translation: %s %lu
translation_has_missing_arg:
@@ -287,22 +287,52 @@ class ExecutableTests: XCTestCase {
ERROR: 'translation_has_missing_phrase' does not include argument(s): object_name (phrase_has_missing_arguments)
Base: Could not add {user_name} to \\"{object_name}\\"
Translation: Could not add {user_name}
- 6 warnings, 4 errors
+ translation_missing_comma_flag:
+ ERROR: Specifier for argument 1 does not match (should be ,d, is d) (string_has_invalid_argument)
+ Base: Points: %,d
+ Translation: Puntos: %d
+ translation_wrong_flag:
+ WARNING: 'translation_wrong_flag' does not include argument(s) at 1 (string_has_missing_arguments)
+ Base: Amount: %+d
+ Translation: Cantidad: % d
+ translation_wrong_minus_flag:
+ ERROR: Specifier for argument 1 does not match (should be +d, is -d) (string_has_invalid_argument)
+ Base: Amount: %+d
+ Translation: Cantidad: %-d
+ translation_wrong_precision:
+ ERROR: Specifier for argument 1 does not match (should be .2f, is .4f) (string_has_invalid_argument)
+ Base: Rate: %.2f
+ Translation: Tasa: %.4f
+ translation_wrong_width:
+ ERROR: Specifier for argument 1 does not match (should be 10d, is 5d) (string_has_invalid_argument)
+ Base: Count: %10d
+ Translation: Cuenta: %5d
+ translation_wrong_width_precision:
+ ERROR: Specifier for argument 1 does not match (should be 8.3f, is 10.2f) (string_has_invalid_argument)
+ Base: Score: %8.3f
+ Translation: Puntuación: %10.2f
+ 8 warnings, 8 errors
Errors found
""")
XCTAssertEqual(stderr!, """
- Examples/strings-base.xml:32: error: 'duplicate_entry' appears twice (duplicate_entries)
- Examples/strings-translation.xml:29: error: 'duplicate_entry' appears twice (duplicate_entries)
- Examples/strings-base.xml:28: warning: 'missing_from_translation' is missing from Examples (key_missing_from_translation)
- Examples/strings-translation.xml:25: warning: 'missing_from_base' is missing from the base translation (key_missing_from_base)
- Examples/strings-base.xml:40: warning: 'translation_missing_string_array' is missing from Examples (key_missing_from_translation)
- Examples/strings-translation.xml:37: warning: 'base_missing_string_array' is missing from the base translation (key_missing_from_base)
+ Examples/strings-base.xml:52: error: 'duplicate_entry' appears twice (duplicate_entries)
+ Examples/strings-translation.xml:52: error: 'duplicate_entry' appears twice (duplicate_entries)
+ Examples/strings-base.xml:48: warning: 'missing_from_translation' is missing from Examples (key_missing_from_translation)
+ Examples/strings-translation.xml:48: warning: 'missing_from_base' is missing from the base translation (key_missing_from_base)
+ Examples/strings-base.xml:60: warning: 'translation_missing_string_array' is missing from Examples (key_missing_from_translation)
+ Examples/strings-translation.xml:60: warning: 'base_missing_string_array' is missing from the base translation (key_missing_from_base)
Examples/strings-translation.xml:17: error: 'translation_has_missing_phrase' does not include argument(s): object_name (phrase_has_missing_arguments)
- Examples/strings-translation.xml:21: error: Specifier for argument 2 does not match (should be d, is lu) (string_has_invalid_argument)
+ Examples/strings-translation.xml:21: warning: 'translation_has_invalid_specifier' does not include argument(s) at 2 (string_has_missing_arguments)
Examples/strings-translation.xml:22: warning: 'translation_has_missing_arg' does not include argument(s) at 2 (string_has_missing_arguments)
- Examples/strings-translation.xml:42: warning: 'string_array_wrong_item_count' item count mismatch in Examples: 2 (should be 1) (string_array_item_count_mismatch)
+ Examples/strings-translation.xml:31: error: Specifier for argument 1 does not match (should be ,d, is d) (string_has_invalid_argument)
+ Examples/strings-translation.xml:32: warning: 'translation_wrong_flag' does not include argument(s) at 1 (string_has_missing_arguments)
+ Examples/strings-translation.xml:33: error: Specifier for argument 1 does not match (should be +d, is -d) (string_has_invalid_argument)
+ Examples/strings-translation.xml:41: error: Specifier for argument 1 does not match (should be 10d, is 5d) (string_has_invalid_argument)
+ Examples/strings-translation.xml:43: error: Specifier for argument 1 does not match (should be .2f, is .4f) (string_has_invalid_argument)
+ Examples/strings-translation.xml:45: error: Specifier for argument 1 does not match (should be 8.3f, is 10.2f) (string_has_invalid_argument)
+ Examples/strings-translation.xml:65: warning: 'string_array_wrong_item_count' item count mismatch in Examples: 2 (should be 1) (string_array_item_count_mismatch)
""")
}
diff --git a/Tests/LocheckCommandTests/StringCatalogCommandTests.swift b/Tests/LocheckCommandTests/StringCatalogCommandTests.swift
index 3f6eb31..e029af1 100644
--- a/Tests/LocheckCommandTests/StringCatalogCommandTests.swift
+++ b/Tests/LocheckCommandTests/StringCatalogCommandTests.swift
@@ -5,24 +5,24 @@
// Created by Uladzislau Birukou on 9/22/25.
//
-import XCTest
import Files
import Foundation
+import XCTest
final class StringCatalogCommandTests: XCTestCase {
private var tempFolder: Folder!
-
+
override func setUp() {
super.setUp()
let uuid = UUID().uuidString
tempFolder = try! Folder.home.createSubfolderIfNeeded(withName: "StringCatalogCommandTests_\(uuid)")
}
-
+
override func tearDown() {
try? tempFolder.delete()
super.tearDown()
}
-
+
func testStringCatalogCommandWithValidFile() throws {
let validCatalog = """
{
@@ -49,26 +49,26 @@ final class StringCatalogCommandTests: XCTestCase {
"version" : "1.0"
}
"""
-
+
let catalogFile = try tempFolder.createFile(named: "test.xcstrings", contents: Data(validCatalog.utf8))
-
+
// Test the command via executable
let binary = productsDirectory.appendingPathComponent("locheck")
let process = Process()
process.executableURL = binary
process.arguments = ["stringcatalog", catalogFile.path]
-
+
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
-
+
try process.run()
process.waitUntilExit()
-
+
// Valid file should exit with status 0
XCTAssertEqual(process.terminationStatus, 0)
}
-
+
func testStringCatalogCommandWithProblematicFile() throws {
let problematicCatalog = """
{
@@ -95,28 +95,30 @@ final class StringCatalogCommandTests: XCTestCase {
"version" : "1.0"
}
"""
-
- let catalogFile = try tempFolder.createFile(named: "problematic.xcstrings", contents: Data(problematicCatalog.utf8))
-
+
+ let catalogFile = try tempFolder.createFile(
+ named: "problematic.xcstrings",
+ contents: Data(problematicCatalog.utf8))
+
// Test the command via executable
let binary = productsDirectory.appendingPathComponent("locheck")
let process = Process()
process.executableURL = binary
process.arguments = ["stringcatalog", catalogFile.path]
-
+
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
-
+
try process.run()
process.waitUntilExit()
-
+
// This file only has warnings, which don't cause non-zero exit by default
// So we just check that the process completes successfully
// In real usage, you would use --treat-warnings-as-errors to get non-zero exit
XCTAssertEqual(process.terminationStatus, 0)
}
-
+
func testStringCatalogCommandWithErrorsFile() throws {
let catalogWithErrors = """
{
@@ -143,54 +145,54 @@ final class StringCatalogCommandTests: XCTestCase {
"version" : "1.0"
}
"""
-
+
let catalogFile = try tempFolder.createFile(named: "errors.xcstrings", contents: Data(catalogWithErrors.utf8))
-
+
// Test the command via executable
let binary = productsDirectory.appendingPathComponent("locheck")
let process = Process()
process.executableURL = binary
process.arguments = ["stringcatalog", catalogFile.path]
-
+
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
-
+
try process.run()
process.waitUntilExit()
-
+
// File with errors should exit with non-zero status
XCTAssertNotEqual(process.terminationStatus, 0)
}
-
+
func testStringCatalogCommandHelp() throws {
let binary = productsDirectory.appendingPathComponent("locheck")
let process = Process()
process.executableURL = binary
process.arguments = ["stringcatalog", "--help"]
-
+
let pipe = Pipe()
process.standardOutput = pipe
-
+
try process.run()
process.waitUntilExit()
-
+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? ""
-
+
XCTAssertTrue(output.contains("Validate String Catalog (.xcstrings) files"))
XCTAssertEqual(process.terminationStatus, 0)
}
-
+
/// Returns path to the built products directory.
private var productsDirectory: URL {
#if os(macOS)
- for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
- return bundle.bundleURL.deletingLastPathComponent()
- }
- fatalError("couldn't find the products directory")
+ for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
+ return bundle.bundleURL.deletingLastPathComponent()
+ }
+ fatalError("couldn't find the products directory")
#else
- return Bundle.main.bundleURL
+ return Bundle.main.bundleURL
#endif
}
}
diff --git a/Tests/LocheckLogicTests/FormatStringTests.swift b/Tests/LocheckLogicTests/FormatStringTests.swift
index 00341a1..308f315 100644
--- a/Tests/LocheckLogicTests/FormatStringTests.swift
+++ b/Tests/LocheckLogicTests/FormatStringTests.swift
@@ -19,4 +19,54 @@ class FormatStringTests: XCTestCase {
FormatArgument(specifier: "@", position: 1, isPositionExplicit: false),
])
}
+
+ // MARK: - Escaped percent (%%) tests for Android
+
+ func testAndroidEscapedPercentOnly() {
+ // "%%" is an escaped percent sign, should have no format arguments
+ let formatString = FormatString(string: "100%% complete", path: "", line: 0, platform: .android)
+ XCTAssertEqual(formatString.arguments, [])
+ }
+
+ func testAndroidEscapedPercentFollowedBySpace() {
+ // "%% back" should not match "% b" as a format specifier
+ let formatString = FormatString(string: "[ICON] %s %% back", path: "", line: 0, platform: .android)
+ XCTAssertEqual(
+ formatString.arguments,
+ [
+ FormatArgument(specifier: "s", position: 1, isPositionExplicit: false),
+ ])
+ }
+
+ func testAndroidEscapedPercentThenFormatSpecifier() {
+ // "%%%s" should be "%%" (escaped percent) + "%s" (format specifier)
+ let formatString = FormatString(string: "100%%%s", path: "", line: 0, platform: .android)
+ XCTAssertEqual(
+ formatString.arguments,
+ [
+ FormatArgument(specifier: "s", position: 1, isPositionExplicit: false),
+ ])
+ }
+
+ func testAndroidMultipleEscapedPercents() {
+ // "%%%%" should be two escaped percents, no format arguments
+ let formatString = FormatString(string: "100%%%% done", path: "", line: 0, platform: .android)
+ XCTAssertEqual(formatString.arguments, [])
+ }
+
+ func testAndroidUnescapedPercentBecomesSpecifier() {
+ // If a translator mistakenly changes "%%" to "%" before a valid specifier character,
+ // it becomes a format specifier. This test documents that behavior.
+ // Base: "100%% discount" -> no arguments (escaped percent)
+ let base = FormatString(string: "100%% discount", path: "", line: 0, platform: .android)
+ XCTAssertEqual(base.arguments, [])
+
+ // Translation with typo: "100%d discount" -> HAS an argument (unescaped, %d is a specifier)
+ let translationWithTypo = FormatString(string: "100%d discount", path: "", line: 0, platform: .android)
+ XCTAssertEqual(
+ translationWithTypo.arguments,
+ [
+ FormatArgument(specifier: "d", position: 1, isPositionExplicit: false),
+ ])
+ }
}
diff --git a/Tests/LocheckLogicTests/StringCatalogTests.swift b/Tests/LocheckLogicTests/StringCatalogTests.swift
index aad6608..dc4b515 100644
--- a/Tests/LocheckLogicTests/StringCatalogTests.swift
+++ b/Tests/LocheckLogicTests/StringCatalogTests.swift
@@ -5,8 +5,8 @@
// Created by Uladzislau Birukou on 9/22/25.
//
-import XCTest
@testable import LocheckLogic
+import XCTest
final class StringCatalogTests: XCTestCase {
func testValidStringCatalog() {
@@ -35,20 +35,20 @@ final class StringCatalogTests: XCTestCase {
"version" : "1.0"
}
"""
-
+
let data = json.data(using: .utf8)!
let catalog = try! JSONDecoder().decode(StringCatalog.self, from: data)
-
+
XCTAssertEqual(catalog.sourceLanguage, "en")
XCTAssertEqual(catalog.version, "1.0")
XCTAssertEqual(catalog.strings.count, 1)
-
+
let helloWorldEntry = catalog.strings["hello_world"]!
XCTAssertEqual(helloWorldEntry.localizations.count, 2)
XCTAssertEqual(helloWorldEntry.localizations["en"]?.stringUnit?.value, "Hello, World!")
XCTAssertEqual(helloWorldEntry.localizations["de"]?.stringUnit?.value, "Hallo, Welt!")
}
-
+
func testStringCatalogWithPluralForms() {
let json = """
{
@@ -99,14 +99,14 @@ final class StringCatalogTests: XCTestCase {
"version" : "1.0"
}
"""
-
+
let data = json.data(using: .utf8)!
let catalog = try! JSONDecoder().decode(StringCatalog.self, from: data)
-
+
let itemCountEntry = catalog.strings["item_count"]!
let enPlurals = itemCountEntry.localizations["en"]?.variations?.plural
let dePlurals = itemCountEntry.localizations["de"]?.variations?.plural
-
+
XCTAssertNotNil(enPlurals)
XCTAssertNotNil(dePlurals)
XCTAssertEqual(enPlurals?["one"]?.stringUnit.value, "%d item")
@@ -114,7 +114,7 @@ final class StringCatalogTests: XCTestCase {
XCTAssertEqual(dePlurals?["one"]?.stringUnit.value, "%d Element")
XCTAssertEqual(dePlurals?["other"]?.stringUnit.value, "%d Elemente")
}
-
+
func testStringCatalogGenerateLocalizedPairs() {
let json = """
{
@@ -141,32 +141,31 @@ final class StringCatalogTests: XCTestCase {
"version" : "1.0"
}
"""
-
+
let data = json.data(using: .utf8)!
let catalog = try! JSONDecoder().decode(StringCatalog.self, from: data)
-
+
let greetingEntry = catalog.strings["greeting"]!
let pairs = greetingEntry.getLocalizedStringPairs(
key: "greeting",
basePath: "/test/path",
- sourceLanguage: "en"
- )
-
+ sourceLanguage: "en")
+
XCTAssertEqual(pairs.count, 1) // Only one non-source language (ru)
-
+
let pair = pairs[0]
XCTAssertEqual(pair.key, "greeting")
XCTAssertEqual(pair.base.string, "Hello, %@!")
XCTAssertEqual(pair.translation.string, "Hallo, %@!")
XCTAssertEqual(pair.path, "/test/path")
-
+
// Check format arguments are parsed correctly
XCTAssertEqual(pair.base.arguments.count, 1)
XCTAssertEqual(pair.translation.arguments.count, 1)
XCTAssertEqual(pair.base.arguments[0].specifier, "@")
XCTAssertEqual(pair.translation.arguments[0].specifier, "@")
}
-
+
func testStringCatalogWithFormatArgumentMismatch() {
let json = """
{
@@ -193,28 +192,27 @@ final class StringCatalogTests: XCTestCase {
"version" : "1.0"
}
"""
-
+
let data = json.data(using: .utf8)!
let catalog = try! JSONDecoder().decode(StringCatalog.self, from: data)
-
+
let numbersEntry = catalog.strings["numbers"]!
let pairs = numbersEntry.getLocalizedStringPairs(
key: "numbers",
basePath: "/test/path",
- sourceLanguage: "en"
- )
-
+ sourceLanguage: "en")
+
XCTAssertEqual(pairs.count, 1)
-
+
let pair = pairs[0]
-
+
// Base: %d (position 1), %@ (position 2)
XCTAssertEqual(pair.base.arguments.count, 2)
XCTAssertEqual(pair.base.arguments[0].position, 1)
XCTAssertEqual(pair.base.arguments[0].specifier, "d")
XCTAssertEqual(pair.base.arguments[1].position, 2)
XCTAssertEqual(pair.base.arguments[1].specifier, "@")
-
+
// Translation: %@ (position 1), %s (position 2)
XCTAssertEqual(pair.translation.arguments.count, 2)
XCTAssertEqual(pair.translation.arguments[0].position, 1)
@@ -222,13 +220,13 @@ final class StringCatalogTests: XCTestCase {
XCTAssertEqual(pair.translation.arguments[1].position, 2)
XCTAssertEqual(pair.translation.arguments[1].specifier, "s")
}
-
+
func testInvalidStringCatalogHandling() {
let problemReporter = ProblemReporter(root: "", ignoredProblemIdentifiers: [], ignoreWarnings: false)
-
+
// Test with non-existent file
let catalog = StringCatalog(path: "/non/existent/path.xcstrings", problemReporter: problemReporter)
-
+
XCTAssertNil(catalog)
XCTAssertTrue(problemReporter.hasError)
}
diff --git a/Tests/LocheckLogicTests/StringCatalogValidatorTests.swift b/Tests/LocheckLogicTests/StringCatalogValidatorTests.swift
index aebecd6..da6d330 100644
--- a/Tests/LocheckLogicTests/StringCatalogValidatorTests.swift
+++ b/Tests/LocheckLogicTests/StringCatalogValidatorTests.swift
@@ -5,24 +5,24 @@
// Created by Uladzislau Birukou on 9/22/25.
//
-import XCTest
import Files
@testable import LocheckLogic
+import XCTest
final class StringCatalogValidatorTests: XCTestCase {
private var tempFolder: Folder!
-
+
override func setUp() {
super.setUp()
let uuid = UUID().uuidString
tempFolder = try! Folder.home.createSubfolderIfNeeded(withName: "StringCatalogTests_\(uuid)")
}
-
+
override func tearDown() {
try? tempFolder.delete()
super.tearDown()
}
-
+
func testValidStringCatalogValidation() throws {
let validCatalog = """
{
@@ -49,16 +49,16 @@ final class StringCatalogValidatorTests: XCTestCase {
"version" : "1.0"
}
"""
-
+
let catalogFile = try tempFolder.createFile(named: "test.xcstrings", contents: Data(validCatalog.utf8))
let problemReporter = ProblemReporter(root: "", ignoredProblemIdentifiers: [], ignoreWarnings: false)
-
+
parseAndValidateStringCatalog(stringCatalogFile: catalogFile, problemReporter: problemReporter)
-
+
XCTAssertFalse(problemReporter.hasError)
XCTAssertFalse(problemReporter.hasWarning)
}
-
+
func testStringCatalogWithMissingArguments() throws {
let catalogWithMissingArgs = """
{
@@ -85,20 +85,22 @@ final class StringCatalogValidatorTests: XCTestCase {
"version" : "1.0"
}
"""
-
- let catalogFile = try tempFolder.createFile(named: "test_missing_args.xcstrings", contents: Data(catalogWithMissingArgs.utf8))
+
+ let catalogFile = try tempFolder.createFile(
+ named: "test_missing_args.xcstrings",
+ contents: Data(catalogWithMissingArgs.utf8))
let problemReporter = ProblemReporter(root: "", ignoredProblemIdentifiers: [], ignoreWarnings: false)
-
+
parseAndValidateStringCatalog(stringCatalogFile: catalogFile, problemReporter: problemReporter)
-
+
XCTAssertTrue(problemReporter.hasWarning) // Missing argument should generate warning
-
+
let problems = problemReporter.problems
XCTAssertTrue(problems.contains { localProblem in
localProblem.problem.kindIdentifier == "string_has_missing_arguments"
})
}
-
+
func testStringCatalogWithExtraArguments() throws {
let catalogWithExtraArgs = """
{
@@ -125,20 +127,22 @@ final class StringCatalogValidatorTests: XCTestCase {
"version" : "1.0"
}
"""
-
- let catalogFile = try tempFolder.createFile(named: "test_extra_args.xcstrings", contents: Data(catalogWithExtraArgs.utf8))
+
+ let catalogFile = try tempFolder.createFile(
+ named: "test_extra_args.xcstrings",
+ contents: Data(catalogWithExtraArgs.utf8))
let problemReporter = ProblemReporter(root: "", ignoredProblemIdentifiers: [], ignoreWarnings: false)
-
+
parseAndValidateStringCatalog(stringCatalogFile: catalogFile, problemReporter: problemReporter)
-
+
XCTAssertTrue(problemReporter.hasWarning) // Extra argument should generate warning
-
+
let problems = problemReporter.problems
XCTAssertTrue(problems.contains { localProblem in
localProblem.problem.kindIdentifier == "string_has_extra_arguments"
})
}
-
+
func testStringCatalogWithInvalidArgumentSpecifier() throws {
let catalogWithInvalidSpecifier = """
{
@@ -165,20 +169,22 @@ final class StringCatalogValidatorTests: XCTestCase {
"version" : "1.0"
}
"""
-
- let catalogFile = try tempFolder.createFile(named: "test_invalid_specifier.xcstrings", contents: Data(catalogWithInvalidSpecifier.utf8))
+
+ let catalogFile = try tempFolder.createFile(
+ named: "test_invalid_specifier.xcstrings",
+ contents: Data(catalogWithInvalidSpecifier.utf8))
let problemReporter = ProblemReporter(root: "", ignoredProblemIdentifiers: [], ignoreWarnings: false)
-
+
parseAndValidateStringCatalog(stringCatalogFile: catalogFile, problemReporter: problemReporter)
-
+
XCTAssertTrue(problemReporter.hasError) // Invalid specifier should generate error
-
+
let problems = problemReporter.problems
XCTAssertTrue(problems.contains { localProblem in
localProblem.problem.kindIdentifier == "string_has_invalid_argument"
})
}
-
+
func testStringCatalogWithNeedsWorkTranslation() throws {
let catalogWithNeedsWork = """
{
@@ -205,20 +211,22 @@ final class StringCatalogValidatorTests: XCTestCase {
"version" : "1.0"
}
"""
-
- let catalogFile = try tempFolder.createFile(named: "test_needs_work.xcstrings", contents: Data(catalogWithNeedsWork.utf8))
+
+ let catalogFile = try tempFolder.createFile(
+ named: "test_needs_work.xcstrings",
+ contents: Data(catalogWithNeedsWork.utf8))
let problemReporter = ProblemReporter(root: "", ignoredProblemIdentifiers: [], ignoreWarnings: false)
-
+
parseAndValidateStringCatalog(stringCatalogFile: catalogFile, problemReporter: problemReporter)
-
+
XCTAssertTrue(problemReporter.hasWarning) // "needs work" should generate warning
-
+
let problems = problemReporter.problems
XCTAssertTrue(problems.contains { localProblem in
localProblem.problem.kindIdentifier == "translation_needs_work"
})
}
-
+
func testStringCatalogWithMissingPluralForm() throws {
let catalogWithMissingPlural = """
{
@@ -263,19 +271,21 @@ final class StringCatalogValidatorTests: XCTestCase {
"version" : "1.0"
}
"""
-
- let catalogFile = try tempFolder.createFile(named: "test_missing_plural.xcstrings", contents: Data(catalogWithMissingPlural.utf8))
+
+ let catalogFile = try tempFolder.createFile(
+ named: "test_missing_plural.xcstrings",
+ contents: Data(catalogWithMissingPlural.utf8))
let problemReporter = ProblemReporter(root: "", ignoredProblemIdentifiers: [], ignoreWarnings: false)
-
+
parseAndValidateStringCatalog(stringCatalogFile: catalogFile, problemReporter: problemReporter)
-
+
XCTAssertTrue(problemReporter.hasWarning) // Missing "other" form should generate warning
-
+
let problems = problemReporter.problems
let missingPluralProblems = problems.filter { $0.problem.kindIdentifier == "missing_plural_form" }
XCTAssertTrue(missingPluralProblems.count >= 1) // At least one missing plural form
}
-
+
func testStringCatalogWithPluralArgumentConsistency() throws {
let catalogWithPlurals = """
{
@@ -326,27 +336,30 @@ final class StringCatalogValidatorTests: XCTestCase {
"version" : "1.0"
}
"""
-
- let catalogFile = try tempFolder.createFile(named: "test_valid_plurals.xcstrings", contents: Data(catalogWithPlurals.utf8))
+
+ let catalogFile = try tempFolder.createFile(
+ named: "test_valid_plurals.xcstrings",
+ contents: Data(catalogWithPlurals.utf8))
let problemReporter = ProblemReporter(root: "", ignoredProblemIdentifiers: [], ignoreWarnings: false)
-
+
parseAndValidateStringCatalog(stringCatalogFile: catalogFile, problemReporter: problemReporter)
-
+
// Should not have format specifier errors since all plural forms use %d correctly
- let invalidArgumentProblems = problemReporter.problems.filter { $0.problem.kindIdentifier == "string_has_invalid_argument" }
+ let invalidArgumentProblems = problemReporter.problems
+ .filter { $0.problem.kindIdentifier == "string_has_invalid_argument" }
XCTAssertEqual(invalidArgumentProblems.count, 0)
}
-
+
func testInvalidJSONStringCatalog() throws {
let invalidJSON = "{ invalid json }"
-
+
let catalogFile = try tempFolder.createFile(named: "invalid.xcstrings", contents: Data(invalidJSON.utf8))
let problemReporter = ProblemReporter(root: "", ignoredProblemIdentifiers: [], ignoreWarnings: false)
-
+
parseAndValidateStringCatalog(stringCatalogFile: catalogFile, problemReporter: problemReporter)
-
+
XCTAssertTrue(problemReporter.hasError)
-
+
let problems = problemReporter.problems
XCTAssertTrue(problems.contains { localProblem in
localProblem.problem.kindIdentifier == "invalid_file"
diff --git a/Tests/LocheckLogicTests/parseAndValidateAndroidStringsTests.swift b/Tests/LocheckLogicTests/parseAndValidateAndroidStringsTests.swift
index 0ec45d8..1e6907f 100644
--- a/Tests/LocheckLogicTests/parseAndValidateAndroidStringsTests.swift
+++ b/Tests/LocheckLogicTests/parseAndValidateAndroidStringsTests.swift
@@ -25,7 +25,7 @@ class ParseAndValidateAndroidStringsTests: XCTestCase {
translationLanguageName: "demo",
problemReporter: problemReporter)
- XCTAssertEqual(problemReporter.problems.count, 10)
+ XCTAssertEqual(problemReporter.problems.count, 16)
let problems = problemReporter.problems.map(\.problem)
XCTAssertEqual(problems.map(\.kindIdentifier), [
@@ -36,8 +36,14 @@ class ParseAndValidateAndroidStringsTests: XCTestCase {
"key_missing_from_translation",
"key_missing_from_base",
"phrase_has_missing_arguments",
- "string_has_invalid_argument",
"string_has_missing_arguments",
+ "string_has_missing_arguments",
+ "string_has_invalid_argument", // %,d vs %d
+ "string_has_missing_arguments", // %+d vs % d (space flag not supported, so no specifier found)
+ "string_has_invalid_argument", // %+d vs %-d (wrong flag)
+ "string_has_invalid_argument", // %10d vs %5d (wrong width)
+ "string_has_invalid_argument", // %.2f vs %.4f (wrong precision)
+ "string_has_invalid_argument", // %8.3f vs %10.2f (wrong width and precision)
"string_array_item_count_mismatch",
])