Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Meta] Automatic String Organization #1372

Merged
merged 9 commits into from
Dec 21, 2024
54 changes: 54 additions & 0 deletions Scripts/Translations/AlphabetizeStrings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Foundation

// Get the English localization file
let fileURL = URL(fileURLWithPath: "./Translations/en.lproj/Localizable.strings")

// This regular expression pattern matches lines of the format:
// "Key" = "Value";
let regex = #/^\"(?<key>[^\"]+)\"\s*=\s*\"(?<value>[^\"]+)\";/#

// Attempt to read the file content.
guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else {
print("Unable to read file: \(fileURL.path)")
exit(1)
}

// Split file content by newlines to process line by line.
let strings = content.components(separatedBy: .newlines)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty && !$0.hasPrefix("//") }

let entries = strings.reduce(into: [String: String]()) {
if let match = $1.firstMatch(of: regex) {
let key = String(match.output.key)
let value = String(match.output.value)
$0[key] = value
} else {
print("Error: Invalid line format in \(fileURL.path): \($1)")
exit(1)
}
}

// Sort the keys alphabetically for consistent ordering.
let sortedKeys = entries.keys.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
let newContent = sortedKeys.map { "/// \(entries[$0]!)\n\"\($0)\" = \"\(entries[$0]!)\";" }.joined(separator: "\n\n")

// Write the updated, sorted, and commented localizations back to the file.
do {
try newContent.write(to: fileURL, atomically: true, encoding: .utf8)

if let derivedFileDirectory = ProcessInfo.processInfo.environment["DERIVED_FILE_DIR"] {
try? "".write(toFile: derivedFileDirectory + "/alphabetizeStrings.txt", atomically: true, encoding: .utf8)
}
} catch {
print("Error: Failed to write to \(fileURL.path)")
exit(1)
}
111 changes: 111 additions & 0 deletions Scripts/Translations/PurgeUnusedStrings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Foundation

// Path to the English localization file
let localizationFile = "./Translations/en.lproj/Localizable.strings"

// Directories to scan for Swift files
let directoriesToScan = ["./Shared", "./Swiftfin", "./Swiftfin tvOS"]

// File to exclude from scanning
let excludedFile = "./Shared/Strings/Strings.swift"

// Regular expressions to match localization entries and usage in Swift files
// Matches lines like "Key" = "Value";
let localizationRegex = #/^\"(?<key>[^\"]+)\"\s*=\s*\"(?<value>[^\"]+)\";$/#

// Matches usage like L10n.key in Swift files
let usageRegex = #/L10n\.(?<key>[a-zA-Z0-9_]+)/#

// Attempt to load the localization file's content
guard let localizationContent = try? String(contentsOfFile: localizationFile, encoding: .utf8) else {
print("Unable to read localization file at \(localizationFile)")
exit(1)
}

// Split the file into lines and initialize a dictionary for localization entries
let localizationLines = localizationContent.components(separatedBy: .newlines)
var localizationEntries = [String: String]()

// Parse each line to extract key-value pairs
for line in localizationLines {
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)

// Skip empty lines or comments
if trimmed.isEmpty || trimmed.hasPrefix("//") { continue }

// Match valid localization entries and add them to the dictionary
if let match = line.firstMatch(of: localizationRegex) {
let key = String(match.output.key)
let value = String(match.output.value)
localizationEntries[key] = value
}
}

// Set to store all keys found in the codebase
var usedKeys = Set<String>()

// Function to scan a directory recursively for Swift files
func scanDirectory(_ path: String) {
let fileManager = FileManager.default
guard let enumerator = fileManager.enumerator(atPath: path) else { return }

for case let file as String in enumerator {
let filePath = "\(path)/\(file)"

// Skip the excluded file
if filePath == excludedFile { continue }

// Process only Swift files
if file.hasSuffix(".swift") {
if let fileContent = try? String(contentsOfFile: filePath, encoding: .utf8) {
for line in fileContent.components(separatedBy: .newlines) {
// Find all matches for L10n.key in each line
let matches = line.matches(of: usageRegex)
for match in matches {
let key = String(match.output.key)
usedKeys.insert(key)
}
}
}
}
}
}

// Scan all specified directories
for directory in directoriesToScan {
scanDirectory(directory)
}

// MARK: - Remove Unused Keys

// Identify keys in the localization file that are not used in the codebase
let unusedKeys = localizationEntries.keys.filter { !usedKeys.contains($0) }

// Remove unused keys from the dictionary
unusedKeys.forEach { localizationEntries.removeValue(forKey: $0) }

// MARK: - Write Updated Localizable.strings

// Sort keys alphabetically for consistent formatting
let sortedKeys = localizationEntries.keys.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }

// Reconstruct the localization file with sorted and updated entries
let updatedContent = sortedKeys.map { "/// \(localizationEntries[$0]!)\n\"\($0)\" = \"\(localizationEntries[$0]!)\";" }
.joined(separator: "\n\n")

// Attempt to write the updated content back to the localization file
do {
try updatedContent.write(toFile: localizationFile, atomically: true, encoding: .utf8)
print("Localization file updated. Removed \(unusedKeys.count) unused keys.")
} catch {
print("Error: Failed to write updated localization file.")
exit(1)
}
2 changes: 0 additions & 2 deletions Shared/Strings/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1346,8 +1346,6 @@ internal enum L10n {
internal static let weekly = L10n.tr("Localizable", "weekly", fallback: "Weekly")
/// This will be created as a new item on your Jellyfin Server.
internal static let willBeCreatedOnServer = L10n.tr("Localizable", "willBeCreatedOnServer", fallback: "This will be created as a new item on your Jellyfin Server.")
/// WIP
internal static let wip = L10n.tr("Localizable", "wip", fallback: "WIP")
/// Writer
internal static let writer = L10n.tr("Localizable", "writer", fallback: "Writer")
/// Year
Expand Down
64 changes: 62 additions & 2 deletions Swiftfin.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,6 @@
E1763A292BF3046A004DF6AB /* AddUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A282BF3046A004DF6AB /* AddUserButton.swift */; };
E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */; };
E1763A642BF3C9AA004DF6AB /* ListRowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */; };
E1763A662BF3CA83004DF6AB /* FullScreenMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A652BF3CA83004DF6AB /* FullScreenMenu.swift */; };
E1763A6A2BF3D177004DF6AB /* PublicUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A692BF3D177004DF6AB /* PublicUserButton.swift */; };
E1763A712BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */; };
E1763A722BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */; };
Expand Down Expand Up @@ -1272,6 +1271,7 @@
4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionDetailView.swift; sourceTree = "<group>"; };
4E71D6882C80910900A0174D /* EditCustomDeviceProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomDeviceProfileView.swift; sourceTree = "<group>"; };
4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = "<group>"; };
4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurgeUnusedStrings.swift; sourceTree = "<group>"; };
4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = "<group>"; };
4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreModal.swift; sourceTree = "<group>"; };
4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1324,6 +1324,7 @@
4EC2B1A82CC97C0400D866BE /* ServerUserDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserDetailsView.swift; sourceTree = "<group>"; };
4EC50D602C934B3A00FC3D0E /* ServerTasksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksViewModel.swift; sourceTree = "<group>"; };
4EC6C16A2C92999800FC904B /* TranscodeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeSection.swift; sourceTree = "<group>"; };
4EC71FBB2D161FE300D0B3A8 /* AlphabetizeStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlphabetizeStrings.swift; sourceTree = "<group>"; };
4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeReason.swift; sourceTree = "<group>"; };
4ECF5D812D0A3D0200F066B1 /* AddAccessScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccessScheduleView.swift; sourceTree = "<group>"; };
4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicDayOfWeek.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1695,7 +1696,6 @@
E1763A282BF3046A004DF6AB /* AddUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddUserButton.swift; sourceTree = "<group>"; };
E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGridButton.swift; sourceTree = "<group>"; };
E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowButton.swift; sourceTree = "<group>"; };
E1763A652BF3CA83004DF6AB /* FullScreenMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMenu.swift; sourceTree = "<group>"; };
E1763A692BF3D177004DF6AB /* PublicUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserButton.swift; sourceTree = "<group>"; };
E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftfinStore+Mappings.swift"; sourceTree = "<group>"; };
E1763A732BF3FA4C004DF6AB /* AppLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLoadingView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2462,6 +2462,15 @@
path = ActiveSessionDetailView;
sourceTree = "<group>";
};
4E75B34D2D16583900D16531 /* Translations */ = {
isa = PBXGroup;
children = (
4EC71FBB2D161FE300D0B3A8 /* AlphabetizeStrings.swift */,
4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */,
);
path = Translations;
sourceTree = "<group>";
};
4E8F74A32CE03D3100CC8969 /* ItemEditorView */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2692,6 +2701,14 @@
path = ServerUserDetailsView;
sourceTree = "<group>";
};
4EC71FBA2D161FD800D0B3A8 /* Scripts */ = {
isa = PBXGroup;
children = (
4E75B34D2D16583900D16531 /* Translations */,
);
path = Scripts;
sourceTree = "<group>";
};
4ECF5D822D0A3D0200F066B1 /* AddAccessScheduleView */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3004,6 +3021,7 @@
534D4FE126A7D7CC000A7A48 /* Translations */,
5377CBF2263B596A003A4E83 /* Products */,
53D5E3DB264B47EE00BADDC8 /* Frameworks */,
4EC71FBA2D161FD800D0B3A8 /* Scripts */,
);
sourceTree = "<group>";
};
Expand Down Expand Up @@ -4753,6 +4771,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 535870712669D21700D05A09 /* Build configuration list for PBXNativeTarget "Swiftfin tvOS" */;
buildPhases = (
4EC71FBD2D1620AF00D0B3A8 /* Alphabetize Strings */,
6286F0A3271C0ABA00C40ED5 /* Run Swiftgen.swift */,
BD83D7852B55EEB600652C24 /* Run SwiftFormat */,
5358705C2669D21600D05A09 /* Sources */,
Expand Down Expand Up @@ -4800,6 +4819,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "Swiftfin iOS" */;
buildPhases = (
4EC71FBC2D16201C00D0B3A8 /* Alphabetize Strings */,
6286F09E271C093000C40ED5 /* Run Swiftgen.swift */,
BD0BA2282AD64BB200306A8D /* Run SwiftFormat */,
5377CBED263B596A003A4E83 /* Sources */,
Expand Down Expand Up @@ -4989,6 +5009,46 @@
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
4EC71FBC2D16201C00D0B3A8 /* Alphabetize Strings */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)/Translations/en.lproj/Localizable.strings",
);
name = "Alphabetize Strings";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/alphabetizeStrings.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/Translations/AlphabetizeStrings.swift\"\n";
};
4EC71FBD2D1620AF00D0B3A8 /* Alphabetize Strings */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)/Translations/en.lproj/Localizable.strings",
);
name = "Alphabetize Strings";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/alphabetizeStrings.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/Translations/AlphabetizeStrings.swift\"\n";
};
6286F09E271C093000C40ED5 /* Run Swiftgen.swift */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
Expand Down
Loading