Skip to content

Commit

Permalink
[Meta] Automatic String Organization (jellyfin#1372)
Browse files Browse the repository at this point in the history
* Automate String Organization.

* Comment the script so it's easier to maintain? Or messier?

* Linting post comments

* Rename ShellScript -> Alphabetize Strings for tvOS

* use swift regex, add error messages, clean up separators

* Only search for ./Translations/en.lproj/Localizable.strings

* Purge Unused Strings Script

* Organize Translation Scripts into a Folder. Update references at the project level.

* clean up

---------

Co-authored-by: Ethan Pippin <[email protected]>
  • Loading branch information
JPKribs and LePips authored Dec 21, 2024
1 parent a6bd093 commit af602d3
Show file tree
Hide file tree
Showing 5 changed files with 332 additions and 113 deletions.
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

0 comments on commit af602d3

Please sign in to comment.