From 137043b475353326c64ba5c81b9e3b0c77e64870 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 20 Dec 2024 15:04:56 -0700 Subject: [PATCH 1/9] Automate String Organization. --- Scripts/AlphabetizeStrings.swift | 59 ++++++ Swiftfin.xcodeproj/project.pbxproj | 51 +++++- Translations/en.lproj/Localizable.strings | 208 +++++++++++----------- 3 files changed, 212 insertions(+), 106 deletions(-) create mode 100644 Scripts/AlphabetizeStrings.swift diff --git a/Scripts/AlphabetizeStrings.swift b/Scripts/AlphabetizeStrings.swift new file mode 100644 index 000000000..baace26ad --- /dev/null +++ b/Scripts/AlphabetizeStrings.swift @@ -0,0 +1,59 @@ +// +// 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 + +let rootURL = URL(fileURLWithPath: "./Translations") +guard FileManager.default.fileExists(atPath: rootURL.path) else { + exit(0) +} + +guard let enumerator = FileManager.default.enumerator(at: rootURL, includingPropertiesForKeys: nil) else { + exit(1) +} + +var files = [URL]() +for case let fileURL as URL in enumerator { + if fileURL.pathExtension == "strings" { + files.append(fileURL) + } +} + +let regex = try! NSRegularExpression(pattern: "^\"([^\"]+)\"\\s*=\\s*\"([^\"]+)\";", options: []) + +for fileURL in files { + guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else { + continue + } + + let lines = content.components(separatedBy: .newlines) + var entries = [String: String]() + + for line in lines { + let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty || trimmed.hasPrefix("//") { continue } + + if let match = regex.firstMatch(in: line, options: [], range: NSRange(line.startIndex..., in: line)) { + let keyRange = Range(match.range(at: 1), in: line)! + let valueRange = Range(match.range(at: 2), in: line)! + let key = String(line[keyRange]) + let value = String(line[valueRange]) + entries[key] = value + } + } + + let sortedKeys = entries.keys.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } + + var newContent = "" + for key in sortedKeys { + let value = entries[key]! + newContent += "/// \(value)\n\"\(key)\" = \"\(value)\";\n\n" + } + + try? newContent.write(to: fileURL, atomically: true, encoding: .utf8) +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 074a8bc3d..46170618d 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -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 */; }; @@ -1324,6 +1323,7 @@ 4EC2B1A82CC97C0400D866BE /* ServerUserDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserDetailsView.swift; sourceTree = ""; }; 4EC50D602C934B3A00FC3D0E /* ServerTasksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksViewModel.swift; sourceTree = ""; }; 4EC6C16A2C92999800FC904B /* TranscodeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeSection.swift; sourceTree = ""; }; + 4EC71FBB2D161FE300D0B3A8 /* AlphabetizeStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlphabetizeStrings.swift; sourceTree = ""; }; 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeReason.swift; sourceTree = ""; }; 4ECF5D812D0A3D0200F066B1 /* AddAccessScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccessScheduleView.swift; sourceTree = ""; }; 4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicDayOfWeek.swift; sourceTree = ""; }; @@ -1695,7 +1695,6 @@ E1763A282BF3046A004DF6AB /* AddUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddUserButton.swift; sourceTree = ""; }; E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGridButton.swift; sourceTree = ""; }; E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowButton.swift; sourceTree = ""; }; - E1763A652BF3CA83004DF6AB /* FullScreenMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMenu.swift; sourceTree = ""; }; E1763A692BF3D177004DF6AB /* PublicUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserButton.swift; sourceTree = ""; }; E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftfinStore+Mappings.swift"; sourceTree = ""; }; E1763A732BF3FA4C004DF6AB /* AppLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLoadingView.swift; sourceTree = ""; }; @@ -2692,6 +2691,14 @@ path = ServerUserDetailsView; sourceTree = ""; }; + 4EC71FBA2D161FD800D0B3A8 /* Scripts */ = { + isa = PBXGroup; + children = ( + 4EC71FBB2D161FE300D0B3A8 /* AlphabetizeStrings.swift */, + ); + path = Scripts; + sourceTree = ""; + }; 4ECF5D822D0A3D0200F066B1 /* AddAccessScheduleView */ = { isa = PBXGroup; children = ( @@ -3004,6 +3011,7 @@ 534D4FE126A7D7CC000A7A48 /* Translations */, 5377CBF2263B596A003A4E83 /* Products */, 53D5E3DB264B47EE00BADDC8 /* Frameworks */, + 4EC71FBA2D161FD800D0B3A8 /* Scripts */, ); sourceTree = ""; }; @@ -4753,6 +4761,7 @@ isa = PBXNativeTarget; buildConfigurationList = 535870712669D21700D05A09 /* Build configuration list for PBXNativeTarget "Swiftfin tvOS" */; buildPhases = ( + 4EC71FBD2D1620AF00D0B3A8 /* ShellScript */, 6286F0A3271C0ABA00C40ED5 /* Run Swiftgen.swift */, BD83D7852B55EEB600652C24 /* Run SwiftFormat */, 5358705C2669D21600D05A09 /* Sources */, @@ -4800,6 +4809,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 */, @@ -4989,6 +4999,43 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 4EC71FBC2D16201C00D0B3A8 /* Alphabetize Strings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/Translations/*.strings", + ); + name = "Alphabetize Strings"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/AlphabetizeStrings.swift\"\n"; + }; + 4EC71FBD2D1620AF00D0B3A8 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/Translations/*.strings", + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/AlphabetizeStrings.swift\"\n"; + }; 6286F09E271C093000C40ED5 /* Run Swiftgen.swift */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 9e9e0b01b..736e90bea 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -13,6 +13,9 @@ /// Access "access" = "Access"; +/// Accessibility +"accessibility" = "Accessibility"; + /// The End Time must come after the Start Time. "accessScheduleInvalidTime" = "The End Time must come after the Start Time."; @@ -22,9 +25,6 @@ /// Define the allowed hours for usage and restrict access outside those times. "accessSchedulesDescription" = "Define the allowed hours for usage and restrict access outside those times."; -/// Accessibility -"accessibility" = "Accessibility"; - /// Active "active" = "Active"; @@ -37,11 +37,14 @@ /// Add "add" = "Add"; +/// Add Access Schedule +"addAccessSchedule" = "Add Access Schedule"; + /// Add API key "addAPIKey" = "Add API key"; -/// Add Access Schedule -"addAccessSchedule" = "Add Access Schedule"; +/// Additional security access for users signed in to this device. This does not change any Jellyfin server user settings. +"additionalSecurityAccessDescription" = "Additional security access for users signed in to this device. This does not change any Jellyfin server user settings."; /// Add Server "addServer" = "Add Server"; @@ -55,9 +58,6 @@ /// Add User "addUser" = "Add User"; -/// Additional security access for users signed in to this device. This does not change any Jellyfin server user settings. -"additionalSecurityAccessDescription" = "Additional security access for users signed in to this device. This does not change any Jellyfin server user settings."; - /// Administrator "administrator" = "Administrator"; @@ -67,15 +67,15 @@ /// Age %@ "agesGroup" = "Age %@"; +/// Aired +"aired" = "Aired"; + /// Air Time "airTime" = "Air Time"; /// Airs %s "airWithDate" = "Airs %s"; -/// Aired -"aired" = "Aired"; - /// Album Artist "albumArtist" = "Album Artist"; @@ -88,12 +88,6 @@ /// All Media "allMedia" = "All Media"; -/// All Servers -"allServers" = "All Servers"; - -/// View and manage all registered users on the server, including their permissions and activity status. -"allUsersDescription" = "View and manage all registered users on the server, including their permissions and activity status."; - /// Allow collection management "allowCollectionManagement" = "Allow collection management"; @@ -103,6 +97,12 @@ /// Allow media item editing "allowItemEditing" = "Allow media item editing"; +/// All Servers +"allServers" = "All Servers"; + +/// View and manage all registered users on the server, including their permissions and activity status. +"allUsersDescription" = "View and manage all registered users on the server, including their permissions and activity status."; + /// Alternate "alternate" = "Alternate"; @@ -127,12 +127,12 @@ /// API Keys "apiKeysTitle" = "API Keys"; -/// App Icon -"appIcon" = "App Icon"; - /// Appearance "appearance" = "Appearance"; +/// App Icon +"appIcon" = "App Icon"; + /// Application Name "applicationName" = "Application Name"; @@ -202,12 +202,12 @@ /// Tests your server connection to assess internet speed and adjust bandwidth automatically. "birateAutoDescription" = "Tests your server connection to assess internet speed and adjust bandwidth automatically."; -/// Birth year -"birthYear" = "Birth year"; - /// Birthday "birthday" = "Birthday"; +/// Birth year +"birthYear" = "Birth year"; + /// Auto "bitrateAuto" = "Auto"; @@ -316,12 +316,12 @@ /// Channels "channels" = "Channels"; -/// Chapter Slider -"chapterSlider" = "Chapter Slider"; - /// Chapters "chapters" = "Chapters"; +/// Chapter Slider +"chapterSlider" = "Chapter Slider"; + /// Cinematic "cinematic" = "Cinematic"; @@ -418,15 +418,15 @@ /// Cover Artist "coverArtist" = "Cover Artist"; +/// Create & Join Groups +"createAndJoinGroups" = "Create & Join Groups"; + /// Create API Key "createAPIKey" = "Create API Key"; /// Enter the application name for the new API key. "createAPIKeyMessage" = "Enter the application name for the new API key."; -/// Create & Join Groups -"createAndJoinGroups" = "Create & Join Groups"; - /// Create a pin to sign in to %@ on this device "createPinForUser" = "Create a pin to sign in to %@ on this device"; @@ -472,6 +472,9 @@ /// Custom failed logins "customFailedLogins" = "Custom failed logins"; +/// Customize +"customize" = "Customize"; + /// Custom Profile "customProfile" = "Custom Profile"; @@ -481,9 +484,6 @@ /// Custom sessions "customSessions" = "Custom sessions"; -/// Customize -"customize" = "Customize"; - /// Daily "daily" = "Daily"; @@ -637,6 +637,9 @@ /// Plays content in its original format. May cause playback issues on unsupported media types. "directDescription" = "Plays content in its original format. May cause playback issues on unsupported media types."; +/// Director +"director" = "Director"; + /// Direct Play "directPlay" = "Direct Play"; @@ -646,9 +649,6 @@ /// Direct Stream "directStream" = "Direct Stream"; -/// Director -"director" = "Director"; - /// Disabled "disabled" = "Disabled"; @@ -679,15 +679,15 @@ /// Edit "edit" = "Edit"; +/// Editor +"editor" = "Editor"; + /// Edit Server "editServer" = "Edit Server"; /// Edit Users "editUsers" = "Edit Users"; -/// Editor -"editor" = "Editor"; - /// Enable all devices "enableAllDevices" = "Enable all devices"; @@ -700,12 +700,12 @@ /// End Date "endDate" = "End Date"; -/// End Time -"endTime" = "End Time"; - /// Ended "ended" = "Ended"; +/// End Time +"endTime" = "End Time"; + /// Engineer "engineer" = "Engineer"; @@ -754,12 +754,12 @@ /// Every "every" = "Every"; -/// Every %1$@ -"everyInterval" = "Every %1$@"; - /// Everyday "everyday" = "Everyday"; +/// Every %1$@ +"everyInterval" = "Every %1$@"; + /// Executed "executed" = "Executed"; @@ -937,12 +937,12 @@ /// Letter "letter" = "Letter"; -/// Letter Picker -"letterPicker" = "Letter Picker"; - /// Letterer "letterer" = "Letterer"; +/// Letter Picker +"letterPicker" = "Letter Picker"; + /// Library "library" = "Library"; @@ -958,15 +958,15 @@ /// Live TV "liveTV" = "Live TV"; +/// Live TV access +"liveTvAccess" = "Live TV access"; + /// Live TV Channels "liveTVChannels" = "Live TV Channels"; /// Live TV Programs "liveTVPrograms" = "Live TV Programs"; -/// Live TV access -"liveTvAccess" = "Live TV access"; - /// Live TV recording management "liveTvRecordingManagement" = "Live TV recording management"; @@ -1000,12 +1000,6 @@ /// Management "management" = "Management"; -/// Maximum parental rating -"maxParentalRating" = "Maximum parental rating"; - -/// Content with a higher rating will be hidden from this user. -"maxParentalRatingDescription" = "Content with a higher rating will be hidden from this user."; - /// Maximum Bitrate "maximumBitrate" = "Maximum Bitrate"; @@ -1030,6 +1024,12 @@ /// Maximum sessions policy "maximumSessionsPolicy" = "Maximum sessions policy"; +/// Maximum parental rating +"maxParentalRating" = "Maximum parental rating"; + +/// Content with a higher rating will be hidden from this user. +"maxParentalRatingDescription" = "Content with a higher rating will be hidden from this user."; + /// Media "media" = "Media"; @@ -1096,12 +1096,12 @@ /// New Password "newPassword" = "New Password"; -/// New User -"newUser" = "New User"; - /// News "news" = "News"; +/// New User +"newUser" = "New User"; + /// Next "next" = "Next"; @@ -1129,6 +1129,9 @@ /// No local servers found "noLocalServersFound" = "No local servers found"; +/// None +"none" = "None"; + /// No overview available "noOverviewAvailable" = "No overview available"; @@ -1138,24 +1141,21 @@ /// No results. "noResults" = "No results."; +/// Normal +"normal" = "Normal"; + /// No runtime limit "noRuntimeLimit" = "No runtime limit"; /// No session "noSession" = "No session"; -/// No title -"noTitle" = "No title"; - -/// None -"none" = "None"; - -/// Normal -"normal" = "Normal"; - /// Type: %@ not implemented yet :( "notImplementedYetWithType" = "Type: %@ not implemented yet :("; +/// No title +"noTitle" = "No title"; + /// Official Rating "officialRating" = "Official Rating"; @@ -1207,12 +1207,12 @@ /// Password "password" = "Password"; -/// Changes the Jellyfin server user password. This does not change any Swiftfin settings. -"passwordChangeWarning" = "Changes the Jellyfin server user password. This does not change any Swiftfin settings."; - /// User password has been changed. "passwordChangedMessage" = "User password has been changed."; +/// Changes the Jellyfin server user password. This does not change any Swiftfin settings. +"passwordChangeWarning" = "Changes the Jellyfin server user password. This does not change any Swiftfin settings."; + /// New passwords do not match. "passwordsDoNotMatch" = "New passwords do not match."; @@ -1240,18 +1240,6 @@ /// Play / Pause "playAndPause" = "Play / Pause"; -/// Play From Beginning -"playFromBeginning" = "Play From Beginning"; - -/// Play Next Item -"playNextItem" = "Play Next Item"; - -/// Play on active -"playOnActive" = "Play on active"; - -/// Play Previous Item -"playPreviousItem" = "Play Previous Item"; - /// Playback "playback" = "Playback"; @@ -1267,6 +1255,18 @@ /// Played "played" = "Played"; +/// Play From Beginning +"playFromBeginning" = "Play From Beginning"; + +/// Play Next Item +"playNextItem" = "Play Next Item"; + +/// Play on active +"playOnActive" = "Play on active"; + +/// Play Previous Item +"playPreviousItem" = "Play Previous Item"; + /// Posters "posters" = "Posters"; @@ -1402,6 +1402,9 @@ /// Replace unlocked metadata with new information. "replaceMetadataDescription" = "Replace unlocked metadata with new information."; +/// Required +"required" = "Required"; + /// Require device authentication when signing in to the user. "requireDeviceAuthDescription" = "Require device authentication when signing in to the user."; @@ -1414,9 +1417,6 @@ /// Require a local pin when signing in to the user. This pin is unrecoverable. "requirePinDescription" = "Require a local pin when signing in to the user. This pin is unrecoverable."; -/// Required -"required" = "Required"; - /// Reset "reset" = "Reset"; @@ -1525,12 +1525,12 @@ /// Server Logs "serverLogs" = "Server Logs"; -/// Server URL -"serverURL" = "Server URL"; - /// Servers "servers" = "Servers"; +/// Server URL +"serverURL" = "Server URL"; + /// Session "session" = "Session"; @@ -1660,15 +1660,15 @@ /// Subtitle Offset "subtitleOffset" = "Subtitle Offset"; -/// Subtitle Size -"subtitleSize" = "Subtitle Size"; - /// Subtitles "subtitles" = "Subtitles"; /// Settings only affect some subtitle types "subtitlesDisclaimer" = "Settings only affect some subtitle types"; +/// Subtitle Size +"subtitleSize" = "Subtitle Size"; + /// Success "success" = "Success"; @@ -1720,18 +1720,18 @@ /// Failed "taskFailed" = "Failed"; -/// Sets the duration (in minutes) in between task triggers. -"taskTriggerInterval" = "Sets the duration (in minutes) in between task triggers."; - -/// Sets the maximum runtime (in hours) for this task trigger. -"taskTriggerTimeLimit" = "Sets the maximum runtime (in hours) for this task trigger."; - /// Tasks "tasks" = "Tasks"; /// Tasks are operations that are scheduled to run periodically or can be triggered manually. "tasksDescription" = "Tasks are operations that are scheduled to run periodically or can be triggered manually."; +/// Sets the duration (in minutes) in between task triggers. +"taskTriggerInterval" = "Sets the duration (in minutes) in between task triggers."; + +/// Sets the maximum runtime (in hours) for this task trigger. +"taskTriggerTimeLimit" = "Sets the maximum runtime (in hours) for this task trigger."; + /// Tbps "terabitsPerSecond" = "Tbps"; @@ -1858,18 +1858,18 @@ /// This user will require device authentication. "userDeviceAuthRequiredDescription" = "This user will require device authentication."; -/// This user will require a pin. -"userPinRequiredDescription" = "This user will require a pin."; - -/// User %@ requires device authentication -"userRequiresDeviceAuthentication" = "User %@ requires device authentication"; - /// Username "username" = "Username"; /// A username is required "usernameRequired" = "A username is required"; +/// This user will require a pin. +"userPinRequiredDescription" = "This user will require a pin."; + +/// User %@ requires device authentication +"userRequiresDeviceAuthentication" = "User %@ requires device authentication"; + /// Users "users" = "Users"; From fc9f5247bdd74c3a989dcc47ccc9e8133ab875a4 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 20 Dec 2024 15:25:13 -0700 Subject: [PATCH 2/9] Comment the script so it's easier to maintain? Or messier? --- Scripts/AlphabetizeStrings.swift | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Scripts/AlphabetizeStrings.swift b/Scripts/AlphabetizeStrings.swift index baace26ad..c52560060 100644 --- a/Scripts/AlphabetizeStrings.swift +++ b/Scripts/AlphabetizeStrings.swift @@ -8,36 +8,50 @@ import Foundation +// Look for all Localizable.strings files in Translations directory let rootURL = URL(fileURLWithPath: "./Translations") + +// Exit early if the Translations directory does not exist. guard FileManager.default.fileExists(atPath: rootURL.path) else { exit(0) } +// Enumerate through the Translations directory to find all .strings files. guard let enumerator = FileManager.default.enumerator(at: rootURL, includingPropertiesForKeys: nil) else { exit(1) } - var files = [URL]() + +// Collect all files with the .strings extension found in ./Translations. for case let fileURL as URL in enumerator { if fileURL.pathExtension == "strings" { files.append(fileURL) } } +// This regular expression pattern matches lines of the format: +// "Key" = "Value"; let regex = try! NSRegularExpression(pattern: "^\"([^\"]+)\"\\s*=\\s*\"([^\"]+)\";", options: []) +// Process each .strings file found. for fileURL in files { + // Attempt to read the file content. guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else { continue } + // Split file content by newlines to process line by line. let lines = content.components(separatedBy: .newlines) var entries = [String: String]() + // Extract key-value pairs from each valid line. for line in lines { let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) + + // Ignore empty lines and lines starting with comments. if trimmed.isEmpty || trimmed.hasPrefix("//") { continue } + // Use regex to find and capture the key and value from the line. if let match = regex.firstMatch(in: line, options: [], range: NSRange(line.startIndex..., in: line)) { let keyRange = Range(match.range(at: 1), in: line)! let valueRange = Range(match.range(at: 2), in: line)! @@ -47,13 +61,17 @@ for fileURL in files { } } + // Sort the keys alphabetically for consistent ordering. let sortedKeys = entries.keys.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } + // Build the new file content with a descriptive comment before each entry. var newContent = "" for key in sortedKeys { let value = entries[key]! - newContent += "/// \(value)\n\"\(key)\" = \"\(value)\";\n\n" + // Insert a comment line above each localization entry describing the value. + newContent += "// \(value)\n\"\(key)\" = \"\(value)\";\n\n" } + // Write the updated, sorted, and commented localizations back to the file. try? newContent.write(to: fileURL, atomically: true, encoding: .utf8) } From 5fdc46c3236cae13b908276da5713578cd067847 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 20 Dec 2024 15:27:21 -0700 Subject: [PATCH 3/9] Linting post comments --- Scripts/AlphabetizeStrings.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Scripts/AlphabetizeStrings.swift b/Scripts/AlphabetizeStrings.swift index c52560060..334136f55 100644 --- a/Scripts/AlphabetizeStrings.swift +++ b/Scripts/AlphabetizeStrings.swift @@ -20,6 +20,8 @@ guard FileManager.default.fileExists(atPath: rootURL.path) else { guard let enumerator = FileManager.default.enumerator(at: rootURL, includingPropertiesForKeys: nil) else { exit(1) } + +// All .strings files in Translations directory var files = [URL]() // Collect all files with the .strings extension found in ./Translations. From 20dec4645b1e04e7848bfb9d082e99aeb1fcc84a Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 20 Dec 2024 15:55:34 -0700 Subject: [PATCH 4/9] Rename ShellScript -> Alphabetize Strings for tvOS --- Swiftfin.xcodeproj/project.pbxproj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 46170618d..6a7e8d7a5 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -4761,7 +4761,7 @@ isa = PBXNativeTarget; buildConfigurationList = 535870712669D21700D05A09 /* Build configuration list for PBXNativeTarget "Swiftfin tvOS" */; buildPhases = ( - 4EC71FBD2D1620AF00D0B3A8 /* ShellScript */, + 4EC71FBD2D1620AF00D0B3A8 /* Alphabetize Strings */, 6286F0A3271C0ABA00C40ED5 /* Run Swiftgen.swift */, BD83D7852B55EEB600652C24 /* Run SwiftFormat */, 5358705C2669D21600D05A09 /* Sources */, @@ -5018,7 +5018,7 @@ shellPath = /bin/sh; shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/AlphabetizeStrings.swift\"\n"; }; - 4EC71FBD2D1620AF00D0B3A8 /* ShellScript */ = { + 4EC71FBD2D1620AF00D0B3A8 /* Alphabetize Strings */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -5028,6 +5028,7 @@ inputPaths = ( "$(SRCROOT)/Translations/*.strings", ); + name = "Alphabetize Strings"; outputFileListPaths = ( ); outputPaths = ( From 656b10f22434a88b8f21bfcfb587071cd192d1dc Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Fri, 20 Dec 2024 16:21:47 -0700 Subject: [PATCH 5/9] use swift regex, add error messages, clean up separators --- Scripts/AlphabetizeStrings.swift | 35 ++++++++++++----------- Swiftfin.xcodeproj/project.pbxproj | 2 ++ Translations/en.lproj/Localizable.strings | 3 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Scripts/AlphabetizeStrings.swift b/Scripts/AlphabetizeStrings.swift index 334136f55..249bdaab1 100644 --- a/Scripts/AlphabetizeStrings.swift +++ b/Scripts/AlphabetizeStrings.swift @@ -13,11 +13,13 @@ let rootURL = URL(fileURLWithPath: "./Translations") // Exit early if the Translations directory does not exist. guard FileManager.default.fileExists(atPath: rootURL.path) else { - exit(0) + print("Error: Translations directory not found.") + exit(1) } // Enumerate through the Translations directory to find all .strings files. guard let enumerator = FileManager.default.enumerator(at: rootURL, includingPropertiesForKeys: nil) else { + print("Error: Failed to enumerate Translations directory.") exit(1) } @@ -33,12 +35,13 @@ for case let fileURL as URL in enumerator { // This regular expression pattern matches lines of the format: // "Key" = "Value"; -let regex = try! NSRegularExpression(pattern: "^\"([^\"]+)\"\\s*=\\s*\"([^\"]+)\";", options: []) +let regex = #/^\"(?[^\"]+)\"\s*=\s*\"(?[^\"]+)\";/# // Process each .strings file found. for fileURL in files { // Attempt to read the file content. guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else { + print("Skipping unreadable file: \(fileURL.path)") continue } @@ -53,27 +56,25 @@ for fileURL in files { // Ignore empty lines and lines starting with comments. if trimmed.isEmpty || trimmed.hasPrefix("//") { continue } - // Use regex to find and capture the key and value from the line. - if let match = regex.firstMatch(in: line, options: [], range: NSRange(line.startIndex..., in: line)) { - let keyRange = Range(match.range(at: 1), in: line)! - let valueRange = Range(match.range(at: 2), in: line)! - let key = String(line[keyRange]) - let value = String(line[valueRange]) + if let match = line.firstMatch(of: regex) { + let key = String(match.output.key) + let value = String(match.output.value) entries[key] = value + } else { + print("Error: Invalid line format in \(fileURL.path): \(line)") + exit(1) } } // Sort the keys alphabetically for consistent ordering. let sortedKeys = entries.keys.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } - - // Build the new file content with a descriptive comment before each entry. - var newContent = "" - for key in sortedKeys { - let value = entries[key]! - // Insert a comment line above each localization entry describing the value. - newContent += "// \(value)\n\"\(key)\" = \"\(value)\";\n\n" - } + 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. - try? newContent.write(to: fileURL, atomically: true, encoding: .utf8) + do { + try newContent.write(to: fileURL, atomically: true, encoding: .utf8) + } catch { + print("Error: Failed to write to \(fileURL.path)") + exit(1) + } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 6a7e8d7a5..2c2d760a9 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -5001,6 +5001,7 @@ /* Begin PBXShellScriptBuildPhase section */ 4EC71FBC2D16201C00D0B3A8 /* Alphabetize Strings */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -5020,6 +5021,7 @@ }; 4EC71FBD2D1620AF00D0B3A8 /* Alphabetize Strings */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 736e90bea..762a383d8 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -1943,5 +1943,4 @@ "yellow" = "Yellow"; /// Yes -"yes" = "Yes"; - +"yes" = "Yes"; \ No newline at end of file From 4e48016c42a0cbe858b541a9da4514ff61641913 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 20 Dec 2024 16:45:00 -0700 Subject: [PATCH 6/9] Only search for ./Translations/en.lproj/Localizable.strings --- Scripts/AlphabetizeStrings.swift | 89 ++++++++++++-------------------- 1 file changed, 34 insertions(+), 55 deletions(-) diff --git a/Scripts/AlphabetizeStrings.swift b/Scripts/AlphabetizeStrings.swift index 249bdaab1..0de976b9d 100644 --- a/Scripts/AlphabetizeStrings.swift +++ b/Scripts/AlphabetizeStrings.swift @@ -8,73 +8,52 @@ import Foundation -// Look for all Localizable.strings files in Translations directory -let rootURL = URL(fileURLWithPath: "./Translations") - -// Exit early if the Translations directory does not exist. -guard FileManager.default.fileExists(atPath: rootURL.path) else { - print("Error: Translations directory not found.") - exit(1) -} - -// Enumerate through the Translations directory to find all .strings files. -guard let enumerator = FileManager.default.enumerator(at: rootURL, includingPropertiesForKeys: nil) else { - print("Error: Failed to enumerate Translations directory.") - exit(1) -} - -// All .strings files in Translations directory -var files = [URL]() - -// Collect all files with the .strings extension found in ./Translations. -for case let fileURL as URL in enumerator { - if fileURL.pathExtension == "strings" { - files.append(fileURL) - } -} +// Get the English localization file +var fileURL = URL(fileURLWithPath: "./Translations/en.lproj/Localizable.strings") // This regular expression pattern matches lines of the format: // "Key" = "Value"; let regex = #/^\"(?[^\"]+)\"\s*=\s*\"(?[^\"]+)\";/# -// Process each .strings file found. -for fileURL in files { - // Attempt to read the file content. - guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else { - print("Skipping unreadable file: \(fileURL.path)") - continue - } +// 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 lines = content.components(separatedBy: .newlines) - var entries = [String: String]() +// Split file content by newlines to process line by line. +let lines = content.components(separatedBy: .newlines) +var entries = [String: String]() - // Extract key-value pairs from each valid line. - for line in lines { - let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) +// Extract key-value pairs from each valid line. +for line in lines { + let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) - // Ignore empty lines and lines starting with comments. - if trimmed.isEmpty || trimmed.hasPrefix("//") { continue } + // Ignore empty lines and lines starting with comments. + if trimmed.isEmpty || trimmed.hasPrefix("//") { continue } - if let match = line.firstMatch(of: regex) { - let key = String(match.output.key) - let value = String(match.output.value) + if let match = line.firstMatch(of: regex) { + let key = String(match.output.key) + let value = String(match.output.value) + + // Add the key-value pair if the key is not already in the dictionary. + if entries[key] == nil { entries[key] = value - } else { - print("Error: Invalid line format in \(fileURL.path): \(line)") - exit(1) } + } else { + print("Error: Invalid line format in \(fileURL.path): \(line)") + 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") +// 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) - } catch { - print("Error: Failed to write to \(fileURL.path)") - exit(1) - } +// Write the updated, sorted, and commented localizations back to the file. +do { + try newContent.write(to: fileURL, atomically: true, encoding: .utf8) +} catch { + print("Error: Failed to write to \(fileURL.path)") + exit(1) } From 291355cae29ee1f15eb65bb251f0312c79c28bc8 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 20 Dec 2024 18:48:05 -0700 Subject: [PATCH 7/9] Purge Unused Strings Script --- Scripts/PurgeUnusedStrings.swift | 111 ++++++++++++++++++++++ Shared/Strings/Strings.swift | 2 - Swiftfin.xcodeproj/project.pbxproj | 46 ++++++++- Translations/en.lproj/Localizable.strings | 3 - 4 files changed, 155 insertions(+), 7 deletions(-) create mode 100755 Scripts/PurgeUnusedStrings.swift diff --git a/Scripts/PurgeUnusedStrings.swift b/Scripts/PurgeUnusedStrings.swift new file mode 100755 index 000000000..cddfbadc3 --- /dev/null +++ b/Scripts/PurgeUnusedStrings.swift @@ -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 = #/^\"(?[^\"]+)\"\s*=\s*\"(?[^\"]+)\";$/# + +// Matches usage like L10n.key in Swift files +let usageRegex = #/L10n\.(?[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() + +// 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) +} diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index ba31685c6..f64386563 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -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 diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 2c2d760a9..f6794eb94 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -1271,6 +1271,7 @@ 4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionDetailView.swift; sourceTree = ""; }; 4E71D6882C80910900A0174D /* EditCustomDeviceProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomDeviceProfileView.swift; sourceTree = ""; }; 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = ""; }; + 4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurgeUnusedStrings.swift; sourceTree = ""; }; 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = ""; }; 4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreModal.swift; sourceTree = ""; }; 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = ""; }; @@ -2695,6 +2696,7 @@ isa = PBXGroup; children = ( 4EC71FBB2D161FE300D0B3A8 /* AlphabetizeStrings.swift */, + 4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */, ); path = Scripts; sourceTree = ""; @@ -4761,6 +4763,7 @@ isa = PBXNativeTarget; buildConfigurationList = 535870712669D21700D05A09 /* Build configuration list for PBXNativeTarget "Swiftfin tvOS" */; buildPhases = ( + 4E75B34C2D1650C700D16531 /* Purge Unused Strings */, 4EC71FBD2D1620AF00D0B3A8 /* Alphabetize Strings */, 6286F0A3271C0ABA00C40ED5 /* Run Swiftgen.swift */, BD83D7852B55EEB600652C24 /* Run SwiftFormat */, @@ -4809,6 +4812,7 @@ isa = PBXNativeTarget; buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "Swiftfin iOS" */; buildPhases = ( + 4E75B34B2D164BC800D16531 /* Purge Unused Strings */, 4EC71FBC2D16201C00D0B3A8 /* Alphabetize Strings */, 6286F09E271C093000C40ED5 /* Run Swiftgen.swift */, BD0BA2282AD64BB200306A8D /* Run SwiftFormat */, @@ -4999,6 +5003,44 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 4E75B34B2D164BC800D16531 /* Purge Unused Strings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/Translations/en.lproj/Localizable.strings", + ); + name = "Purge Unused Strings"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/PurgeUnusedStrings.swift\"\n"; + }; + 4E75B34C2D1650C700D16531 /* Purge Unused Strings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/Translations/en.lproj/Localizable.strings", + ); + name = "Purge Unused Strings"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/AlphabetizeStrings.swift\"\n"; + }; 4EC71FBC2D16201C00D0B3A8 /* Alphabetize Strings */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -5008,7 +5050,7 @@ inputFileListPaths = ( ); inputPaths = ( - "$(SRCROOT)/Translations/*.strings", + "$(SRCROOT)/Translations/en.lproj/Localizable.strings", ); name = "Alphabetize Strings"; outputFileListPaths = ( @@ -5028,7 +5070,7 @@ inputFileListPaths = ( ); inputPaths = ( - "$(SRCROOT)/Translations/*.strings", + "$(SRCROOT)/Translations/en.lproj/Localizable.strings", ); name = "Alphabetize Strings"; outputFileListPaths = ( diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 762a383d8..8eccb96c2 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -1927,9 +1927,6 @@ /// This will be created as a new item on your Jellyfin Server. "willBeCreatedOnServer" = "This will be created as a new item on your Jellyfin Server."; -/// WIP -"wip" = "WIP"; - /// Writer "writer" = "Writer"; From 83885ca16a8a4525c33e5b4b2dce8d712cda362e Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 20 Dec 2024 19:01:41 -0700 Subject: [PATCH 8/9] Organize Translation Scripts into a Folder. Update references at the project level. --- .../AlphabetizeStrings.swift | 0 .../PurgeUnusedStrings.swift | 0 Swiftfin.xcodeproj/project.pbxproj | 20 +++++++++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) rename Scripts/{ => Translations}/AlphabetizeStrings.swift (100%) rename Scripts/{ => Translations}/PurgeUnusedStrings.swift (100%) diff --git a/Scripts/AlphabetizeStrings.swift b/Scripts/Translations/AlphabetizeStrings.swift similarity index 100% rename from Scripts/AlphabetizeStrings.swift rename to Scripts/Translations/AlphabetizeStrings.swift diff --git a/Scripts/PurgeUnusedStrings.swift b/Scripts/Translations/PurgeUnusedStrings.swift similarity index 100% rename from Scripts/PurgeUnusedStrings.swift rename to Scripts/Translations/PurgeUnusedStrings.swift diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index f6794eb94..13505090c 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -2462,6 +2462,15 @@ path = ActiveSessionDetailView; sourceTree = ""; }; + 4E75B34D2D16583900D16531 /* Translations */ = { + isa = PBXGroup; + children = ( + 4EC71FBB2D161FE300D0B3A8 /* AlphabetizeStrings.swift */, + 4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */, + ); + path = Translations; + sourceTree = ""; + }; 4E8F74A32CE03D3100CC8969 /* ItemEditorView */ = { isa = PBXGroup; children = ( @@ -2695,8 +2704,7 @@ 4EC71FBA2D161FD800D0B3A8 /* Scripts */ = { isa = PBXGroup; children = ( - 4EC71FBB2D161FE300D0B3A8 /* AlphabetizeStrings.swift */, - 4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */, + 4E75B34D2D16583900D16531 /* Translations */, ); path = Scripts; sourceTree = ""; @@ -5020,7 +5028,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/PurgeUnusedStrings.swift\"\n"; + shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/Translations/PurgeUnusedStrings.swift\"\n"; }; 4E75B34C2D1650C700D16531 /* Purge Unused Strings */ = { isa = PBXShellScriptBuildPhase; @@ -5039,7 +5047,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/AlphabetizeStrings.swift\"\n"; + shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/Translations/AlphabetizeStrings.swift\"\n"; }; 4EC71FBC2D16201C00D0B3A8 /* Alphabetize Strings */ = { isa = PBXShellScriptBuildPhase; @@ -5059,7 +5067,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/AlphabetizeStrings.swift\"\n"; + shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/Translations/AlphabetizeStrings.swift\"\n"; }; 4EC71FBD2D1620AF00D0B3A8 /* Alphabetize Strings */ = { isa = PBXShellScriptBuildPhase; @@ -5079,7 +5087,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/AlphabetizeStrings.swift\"\n"; + shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/Translations/AlphabetizeStrings.swift\"\n"; }; 6286F09E271C093000C40ED5 /* Run Swiftgen.swift */ = { isa = PBXShellScriptBuildPhase; From 0f827261de75b03ecd6fdd366c230bce2c1634fc Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Fri, 20 Dec 2024 23:51:58 -0700 Subject: [PATCH 9/9] clean up --- Scripts/Translations/AlphabetizeStrings.swift | 29 +++++------- Swiftfin.xcodeproj/project.pbxproj | 44 +------------------ 2 files changed, 14 insertions(+), 59 deletions(-) diff --git a/Scripts/Translations/AlphabetizeStrings.swift b/Scripts/Translations/AlphabetizeStrings.swift index 0de976b9d..ffd94657a 100644 --- a/Scripts/Translations/AlphabetizeStrings.swift +++ b/Scripts/Translations/AlphabetizeStrings.swift @@ -9,7 +9,7 @@ import Foundation // Get the English localization file -var fileURL = URL(fileURLWithPath: "./Translations/en.lproj/Localizable.strings") +let fileURL = URL(fileURLWithPath: "./Translations/en.lproj/Localizable.strings") // This regular expression pattern matches lines of the format: // "Key" = "Value"; @@ -22,26 +22,17 @@ guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else { } // Split file content by newlines to process line by line. -let lines = content.components(separatedBy: .newlines) -var entries = [String: String]() +let strings = content.components(separatedBy: .newlines) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty && !$0.hasPrefix("//") } -// Extract key-value pairs from each valid line. -for line in lines { - let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) - - // Ignore empty lines and lines starting with comments. - if trimmed.isEmpty || trimmed.hasPrefix("//") { continue } - - if let match = line.firstMatch(of: regex) { +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) - - // Add the key-value pair if the key is not already in the dictionary. - if entries[key] == nil { - entries[key] = value - } + $0[key] = value } else { - print("Error: Invalid line format in \(fileURL.path): \(line)") + print("Error: Invalid line format in \(fileURL.path): \($1)") exit(1) } } @@ -53,6 +44,10 @@ let newContent = sortedKeys.map { "/// \(entries[$0]!)\n\"\($0)\" = \"\(entries[ // 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) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 13505090c..288ca5416 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -4771,7 +4771,6 @@ isa = PBXNativeTarget; buildConfigurationList = 535870712669D21700D05A09 /* Build configuration list for PBXNativeTarget "Swiftfin tvOS" */; buildPhases = ( - 4E75B34C2D1650C700D16531 /* Purge Unused Strings */, 4EC71FBD2D1620AF00D0B3A8 /* Alphabetize Strings */, 6286F0A3271C0ABA00C40ED5 /* Run Swiftgen.swift */, BD83D7852B55EEB600652C24 /* Run SwiftFormat */, @@ -4820,7 +4819,6 @@ isa = PBXNativeTarget; buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "Swiftfin iOS" */; buildPhases = ( - 4E75B34B2D164BC800D16531 /* Purge Unused Strings */, 4EC71FBC2D16201C00D0B3A8 /* Alphabetize Strings */, 6286F09E271C093000C40ED5 /* Run Swiftgen.swift */, BD0BA2282AD64BB200306A8D /* Run SwiftFormat */, @@ -5011,47 +5009,8 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 4E75B34B2D164BC800D16531 /* Purge Unused Strings */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$(SRCROOT)/Translations/en.lproj/Localizable.strings", - ); - name = "Purge Unused Strings"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/Translations/PurgeUnusedStrings.swift\"\n"; - }; - 4E75B34C2D1650C700D16531 /* Purge Unused Strings */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$(SRCROOT)/Translations/en.lproj/Localizable.strings", - ); - name = "Purge Unused Strings"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/Translations/AlphabetizeStrings.swift\"\n"; - }; 4EC71FBC2D16201C00D0B3A8 /* Alphabetize Strings */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -5064,6 +5023,7 @@ outputFileListPaths = ( ); outputPaths = ( + "$(DERIVED_FILE_DIR)/alphabetizeStrings.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -5071,7 +5031,6 @@ }; 4EC71FBD2D1620AF00D0B3A8 /* Alphabetize Strings */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -5084,6 +5043,7 @@ outputFileListPaths = ( ); outputPaths = ( + "$(DERIVED_FILE_DIR)/alphabetizeStrings.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh;