Add some validation for stringsdict files
This commit is contained in:
parent
1a07a9a252
commit
12440064a7
3
.github/workflows/translation-check.yml
vendored
3
.github/workflows/translation-check.yml
vendored
@ -28,3 +28,6 @@ jobs:
|
||||
|
||||
- name: Validate Localizable.strings
|
||||
run: Scripts/translation-validator/.build/debug/translation-validator Signal/translations/en.lproj/Localizable.strings
|
||||
|
||||
- name: Validate PluralAware.stringsdict
|
||||
run: Scripts/translation-validator/.build/debug/translation-validator Signal/translations/en.lproj/PluralAware.stringsdict
|
||||
|
||||
@ -10,9 +10,18 @@ struct TranslationValidator {
|
||||
static func main() {
|
||||
let sourcePath = CommandLine.arguments.dropFirst().first!
|
||||
let sourceUrl = URL(filePath: sourcePath, relativeTo: URL.currentDirectory()).standardizedFileURL
|
||||
switch sourceUrl.pathExtension {
|
||||
case "strings":
|
||||
checkStrings(at: sourceUrl)
|
||||
case "stringsdict":
|
||||
checkStringsDict(at: sourceUrl)
|
||||
default:
|
||||
fatalError("invalid file extension")
|
||||
}
|
||||
}
|
||||
|
||||
private static func checkStrings(at sourceUrl: URL) {
|
||||
let sourceStrings = fetchStrings(at: sourceUrl)
|
||||
let sourceDir = sourceUrl.deletingLastPathComponent().deletingLastPathComponent()
|
||||
let enumerator = FileManager.default.enumerator(at: sourceDir, includingPropertiesForKeys: nil, errorHandler: nil)!
|
||||
|
||||
var hasError = false
|
||||
|
||||
@ -32,38 +41,82 @@ struct TranslationValidator {
|
||||
// Keys that only appear in the translations.
|
||||
var translatedOnly = [String: Set<String>]()
|
||||
|
||||
for fileUrl in enumerator {
|
||||
let fileUrl = fileUrl as! URL
|
||||
if fileUrl.lastPathComponent == sourceUrl.lastPathComponent {
|
||||
let localeName = fileUrl.deletingLastPathComponent().deletingPathExtension().lastPathComponent
|
||||
let translatedStrings = fetchStrings(at: fileUrl)
|
||||
for key in Set(sourceStrings.keys).subtracting(translatedStrings.keys) {
|
||||
sourceOnly[key, default: []].insert(localeName)
|
||||
enumerateLocalizedFiles(sourceUrl: sourceUrl) { fileUrl, localeName in
|
||||
let translatedStrings = fetchStrings(at: fileUrl)
|
||||
for key in Set(sourceStrings.keys).subtracting(translatedStrings.keys) {
|
||||
sourceOnly[key, default: []].insert(localeName)
|
||||
}
|
||||
for key in Set(translatedStrings.keys).subtracting(sourceStrings.keys) {
|
||||
translatedOnly[key, default: []].insert(localeName)
|
||||
}
|
||||
for (key, translatedString) in translatedStrings {
|
||||
guard let sourceSpecifiers = sourceSpecifiers[key] else {
|
||||
continue
|
||||
}
|
||||
for key in Set(translatedStrings.keys).subtracting(sourceStrings.keys) {
|
||||
translatedOnly[key, default: []].insert(localeName)
|
||||
let translatedSpecifiers: [String]
|
||||
do {
|
||||
translatedSpecifiers = try formatSpecifiers(in: translatedString)
|
||||
} catch {
|
||||
print("The translation for", key, "in", localeName, "has a malformed format specifier:", error)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
for (key, translatedString) in translatedStrings {
|
||||
guard let sourceSpecifiers = sourceSpecifiers[key] else {
|
||||
continue
|
||||
}
|
||||
let translatedSpecifiers: [String]
|
||||
do {
|
||||
translatedSpecifiers = try formatSpecifiers(in: translatedString)
|
||||
} catch {
|
||||
print("The translation for", key, "in", localeName, "has a malformed format specifier:", error)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
guard sourceSpecifiers.sorted() == translatedSpecifiers.sorted() else {
|
||||
print("The translation for", key, "in", localeName, "has an incorrect set of format specifiers:", translatedSpecifiers, "vs.", sourceSpecifiers)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
guard sourceSpecifiers.sorted() == translatedSpecifiers.sorted() else {
|
||||
print("The translation for", key, "in", localeName, "has an incorrect set of format specifiers:", translatedSpecifiers, "vs.", sourceSpecifiers)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printMissingExtraSummary(sourceOnly: sourceOnly, translatedOnly: translatedOnly, hasError: &hasError)
|
||||
|
||||
if hasError {
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
private static func checkStringsDict(at sourceUrl: URL) {
|
||||
let sourceStrings = fetchStringsDict(at: sourceUrl)
|
||||
|
||||
var hasError = false
|
||||
|
||||
// Keys that only appear in the source.
|
||||
var sourceOnly = [String: Set<String>]()
|
||||
|
||||
// Keys that only appear in the translations.
|
||||
var translatedOnly = [String: Set<String>]()
|
||||
|
||||
enumerateLocalizedFiles(sourceUrl: sourceUrl) { fileUrl, localeName in
|
||||
let translatedStrings = fetchStringsDict(at: fileUrl)
|
||||
for key in Set(sourceStrings.keys).subtracting(translatedStrings.keys) {
|
||||
sourceOnly[key, default: []].insert(localeName)
|
||||
}
|
||||
for key in Set(translatedStrings.keys).subtracting(sourceStrings.keys) {
|
||||
translatedOnly[key, default: []].insert(localeName)
|
||||
}
|
||||
}
|
||||
|
||||
printMissingExtraSummary(sourceOnly: sourceOnly, translatedOnly: translatedOnly, hasError: &hasError)
|
||||
|
||||
if hasError {
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
private static func enumerateLocalizedFiles(sourceUrl: URL, block: (URL, String) -> Void) {
|
||||
let sourceDir = sourceUrl.deletingLastPathComponent().deletingLastPathComponent()
|
||||
let enumerator = FileManager.default.enumerator(at: sourceDir, includingPropertiesForKeys: nil, errorHandler: nil)!
|
||||
for fileUrl in enumerator {
|
||||
let fileUrl = fileUrl as! URL
|
||||
if fileUrl.lastPathComponent == sourceUrl.lastPathComponent {
|
||||
let localeName = fileUrl.deletingLastPathComponent().deletingPathExtension().lastPathComponent
|
||||
block(fileUrl, localeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func printMissingExtraSummary(sourceOnly: [String: Set<String>], translatedOnly: [String: Set<String>], hasError: inout Bool) {
|
||||
if !sourceOnly.isEmpty {
|
||||
hasError = true
|
||||
print("The following strings are missing from some translated files:")
|
||||
@ -81,16 +134,42 @@ struct TranslationValidator {
|
||||
}
|
||||
print()
|
||||
}
|
||||
|
||||
if hasError {
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
private static func fetchStrings(at url: URL) -> [String: String] {
|
||||
return try! PropertyListSerialization.propertyList(from: Data(contentsOf: url), format: nil) as! [String: String]
|
||||
}
|
||||
|
||||
private static func fetchStringsDict(at url: URL) -> [String: PluralTranslation] {
|
||||
let translations = try! PropertyListSerialization.propertyList(from: Data(contentsOf: url), format: nil) as! [String: [String: Any]]
|
||||
var results = [String: PluralTranslation]()
|
||||
for (key, translation) in translations {
|
||||
var translation = translation
|
||||
let format = translation.removeValue(forKey: "NSStringLocalizedFormatKey") as! String
|
||||
let replacements = translation.mapValues { replacement in
|
||||
var replacement = replacement as! [String: String]
|
||||
return PluralTranslation.Replacement(
|
||||
specType: replacement.removeValue(forKey: "NSStringFormatSpecTypeKey")!,
|
||||
valueType: replacement.removeValue(forKey: "NSStringFormatValueTypeKey")!,
|
||||
cases: replacement,
|
||||
)
|
||||
}
|
||||
results[key] = PluralTranslation(format: format, replacements: replacements)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
struct PluralTranslation {
|
||||
var format: String
|
||||
var replacements: [String: Replacement]
|
||||
|
||||
struct Replacement {
|
||||
var specType: String
|
||||
var valueType: String
|
||||
var cases: [String: String]
|
||||
}
|
||||
}
|
||||
|
||||
private enum FormatSpecifierError: Error {
|
||||
case notTerminated
|
||||
case unknownCharacter(Character)
|
||||
|
||||
@ -10,25 +10,8 @@ cd $REPO_ROOT
|
||||
swift run --package-path Scripts/translation-tool translation-tool download-resources
|
||||
swift build --package-path Scripts/translation-validator
|
||||
|
||||
LOCALIZATION_ROOT=$REPO_ROOT/Signal/translations
|
||||
cd $LOCALIZATION_ROOT
|
||||
LOCALIZATION_ROOT=Signal/translations
|
||||
|
||||
# Parse the PluralAware.stringsdict files to ensure they're not malformed.
|
||||
lang_errors=()
|
||||
for dir in *.lproj
|
||||
do
|
||||
pushd "$dir"
|
||||
if [ -e PluralAware.stringsdict ]; then
|
||||
plutil PluralAware.stringsdict || lang_errors+=("$dir")
|
||||
fi
|
||||
popd
|
||||
done
|
||||
if [ "${#lang_errors[@]}" -gt 0 ]; then
|
||||
1>&2 echo "Some languages have malformed .stringsdict files: ${lang_errors[*]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
../../Scripts/translation-validator/.build/debug/translation-validator $LOCALIZATION_ROOT/en.lproj/Localizable.strings || true
|
||||
../../Scripts/translation-validator/.build/debug/translation-validator $LOCALIZATION_ROOT/en.lproj/InfoPlist.strings || true
|
||||
|
||||
echo "Make sure you register any new localizations in XCode! (Go to Project > Signal > Localizations > Add Localizations)"
|
||||
Scripts/translation-validator/.build/debug/translation-validator $LOCALIZATION_ROOT/en.lproj/Localizable.strings || true
|
||||
Scripts/translation-validator/.build/debug/translation-validator $LOCALIZATION_ROOT/en.lproj/InfoPlist.strings || true
|
||||
Scripts/translation-validator/.build/debug/translation-validator $LOCALIZATION_ROOT/en.lproj/PluralAware.stringsdict || true
|
||||
|
||||
Loading…
Reference in New Issue
Block a user