[Meta] Automatic String Organization (#1372)

* 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 <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2024-12-21 00:01:11 -07:00 committed by GitHub
parent a6bd093960
commit af602d3d98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 332 additions and 113 deletions

View File

@ -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)
}

View File

@ -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)
}

View File

@ -1346,8 +1346,6 @@ internal enum L10n {
internal static let weekly = L10n.tr("Localizable", "weekly", fallback: "Weekly") internal static let weekly = L10n.tr("Localizable", "weekly", fallback: "Weekly")
/// This will be created as a new item on your Jellyfin Server. /// 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.") 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 /// Writer
internal static let writer = L10n.tr("Localizable", "writer", fallback: "Writer") internal static let writer = L10n.tr("Localizable", "writer", fallback: "Writer")
/// Year /// Year

View File

@ -762,7 +762,6 @@
E1763A292BF3046A004DF6AB /* AddUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A282BF3046A004DF6AB /* AddUserButton.swift */; }; E1763A292BF3046A004DF6AB /* AddUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A282BF3046A004DF6AB /* AddUserButton.swift */; };
E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */; }; E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */; };
E1763A642BF3C9AA004DF6AB /* ListRowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A632BF3C9AA004DF6AB /* ListRowButton.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 */; }; E1763A6A2BF3D177004DF6AB /* PublicUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A692BF3D177004DF6AB /* PublicUserButton.swift */; };
E1763A712BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.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 */; }; E1763A722BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */; };
@ -1272,6 +1271,7 @@
4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionDetailView.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = "<group>"; };
@ -1324,6 +1324,7 @@
4EC2B1A82CC97C0400D866BE /* ServerUserDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserDetailsView.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicDayOfWeek.swift; sourceTree = "<group>"; };
@ -1695,7 +1696,6 @@
E1763A282BF3046A004DF6AB /* AddUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddUserButton.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; E1763A732BF3FA4C004DF6AB /* AppLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLoadingView.swift; sourceTree = "<group>"; };
@ -2462,6 +2462,15 @@
path = ActiveSessionDetailView; path = ActiveSessionDetailView;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
4E75B34D2D16583900D16531 /* Translations */ = {
isa = PBXGroup;
children = (
4EC71FBB2D161FE300D0B3A8 /* AlphabetizeStrings.swift */,
4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */,
);
path = Translations;
sourceTree = "<group>";
};
4E8F74A32CE03D3100CC8969 /* ItemEditorView */ = { 4E8F74A32CE03D3100CC8969 /* ItemEditorView */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -2692,6 +2701,14 @@
path = ServerUserDetailsView; path = ServerUserDetailsView;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
4EC71FBA2D161FD800D0B3A8 /* Scripts */ = {
isa = PBXGroup;
children = (
4E75B34D2D16583900D16531 /* Translations */,
);
path = Scripts;
sourceTree = "<group>";
};
4ECF5D822D0A3D0200F066B1 /* AddAccessScheduleView */ = { 4ECF5D822D0A3D0200F066B1 /* AddAccessScheduleView */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -3004,6 +3021,7 @@
534D4FE126A7D7CC000A7A48 /* Translations */, 534D4FE126A7D7CC000A7A48 /* Translations */,
5377CBF2263B596A003A4E83 /* Products */, 5377CBF2263B596A003A4E83 /* Products */,
53D5E3DB264B47EE00BADDC8 /* Frameworks */, 53D5E3DB264B47EE00BADDC8 /* Frameworks */,
4EC71FBA2D161FD800D0B3A8 /* Scripts */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -4753,6 +4771,7 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 535870712669D21700D05A09 /* Build configuration list for PBXNativeTarget "Swiftfin tvOS" */; buildConfigurationList = 535870712669D21700D05A09 /* Build configuration list for PBXNativeTarget "Swiftfin tvOS" */;
buildPhases = ( buildPhases = (
4EC71FBD2D1620AF00D0B3A8 /* Alphabetize Strings */,
6286F0A3271C0ABA00C40ED5 /* Run Swiftgen.swift */, 6286F0A3271C0ABA00C40ED5 /* Run Swiftgen.swift */,
BD83D7852B55EEB600652C24 /* Run SwiftFormat */, BD83D7852B55EEB600652C24 /* Run SwiftFormat */,
5358705C2669D21600D05A09 /* Sources */, 5358705C2669D21600D05A09 /* Sources */,
@ -4800,6 +4819,7 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "Swiftfin iOS" */; buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "Swiftfin iOS" */;
buildPhases = ( buildPhases = (
4EC71FBC2D16201C00D0B3A8 /* Alphabetize Strings */,
6286F09E271C093000C40ED5 /* Run Swiftgen.swift */, 6286F09E271C093000C40ED5 /* Run Swiftgen.swift */,
BD0BA2282AD64BB200306A8D /* Run SwiftFormat */, BD0BA2282AD64BB200306A8D /* Run SwiftFormat */,
5377CBED263B596A003A4E83 /* Sources */, 5377CBED263B596A003A4E83 /* Sources */,
@ -4989,6 +5009,46 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase 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 */ = { 6286F09E271C093000C40ED5 /* Run Swiftgen.swift */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;

View File

@ -13,6 +13,9 @@
/// Access /// Access
"access" = "Access"; "access" = "Access";
/// Accessibility
"accessibility" = "Accessibility";
/// The End Time must come after the Start Time. /// The End Time must come after the Start Time.
"accessScheduleInvalidTime" = "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. /// 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."; "accessSchedulesDescription" = "Define the allowed hours for usage and restrict access outside those times.";
/// Accessibility
"accessibility" = "Accessibility";
/// Active /// Active
"active" = "Active"; "active" = "Active";
@ -37,11 +37,14 @@
/// Add /// Add
"add" = "Add"; "add" = "Add";
/// Add Access Schedule
"addAccessSchedule" = "Add Access Schedule";
/// Add API key /// Add API key
"addAPIKey" = "Add API key"; "addAPIKey" = "Add API key";
/// Add Access Schedule /// Additional security access for users signed in to this device. This does not change any Jellyfin server user settings.
"addAccessSchedule" = "Add Access Schedule"; "additionalSecurityAccessDescription" = "Additional security access for users signed in to this device. This does not change any Jellyfin server user settings.";
/// Add Server /// Add Server
"addServer" = "Add Server"; "addServer" = "Add Server";
@ -55,9 +58,6 @@
/// Add User /// Add User
"addUser" = "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" = "Administrator"; "administrator" = "Administrator";
@ -67,15 +67,15 @@
/// Age %@ /// Age %@
"agesGroup" = "Age %@"; "agesGroup" = "Age %@";
/// Aired
"aired" = "Aired";
/// Air Time /// Air Time
"airTime" = "Air Time"; "airTime" = "Air Time";
/// Airs %s /// Airs %s
"airWithDate" = "Airs %s"; "airWithDate" = "Airs %s";
/// Aired
"aired" = "Aired";
/// Album Artist /// Album Artist
"albumArtist" = "Album Artist"; "albumArtist" = "Album Artist";
@ -88,12 +88,6 @@
/// All Media /// All Media
"allMedia" = "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 /// Allow collection management
"allowCollectionManagement" = "Allow collection management"; "allowCollectionManagement" = "Allow collection management";
@ -103,6 +97,12 @@
/// Allow media item editing /// Allow media item editing
"allowItemEditing" = "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" = "Alternate"; "alternate" = "Alternate";
@ -127,12 +127,12 @@
/// API Keys /// API Keys
"apiKeysTitle" = "API Keys"; "apiKeysTitle" = "API Keys";
/// App Icon
"appIcon" = "App Icon";
/// Appearance /// Appearance
"appearance" = "Appearance"; "appearance" = "Appearance";
/// App Icon
"appIcon" = "App Icon";
/// Application Name /// Application Name
"applicationName" = "Application Name"; "applicationName" = "Application Name";
@ -202,12 +202,12 @@
/// Tests your server connection to assess internet speed and adjust bandwidth automatically. /// 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."; "birateAutoDescription" = "Tests your server connection to assess internet speed and adjust bandwidth automatically.";
/// Birth year
"birthYear" = "Birth year";
/// Birthday /// Birthday
"birthday" = "Birthday"; "birthday" = "Birthday";
/// Birth year
"birthYear" = "Birth year";
/// Auto /// Auto
"bitrateAuto" = "Auto"; "bitrateAuto" = "Auto";
@ -316,12 +316,12 @@
/// Channels /// Channels
"channels" = "Channels"; "channels" = "Channels";
/// Chapter Slider
"chapterSlider" = "Chapter Slider";
/// Chapters /// Chapters
"chapters" = "Chapters"; "chapters" = "Chapters";
/// Chapter Slider
"chapterSlider" = "Chapter Slider";
/// Cinematic /// Cinematic
"cinematic" = "Cinematic"; "cinematic" = "Cinematic";
@ -418,15 +418,15 @@
/// Cover Artist /// Cover Artist
"coverArtist" = "Cover Artist"; "coverArtist" = "Cover Artist";
/// Create & Join Groups
"createAndJoinGroups" = "Create & Join Groups";
/// Create API Key /// Create API Key
"createAPIKey" = "Create API Key"; "createAPIKey" = "Create API Key";
/// Enter the application name for the new API key. /// Enter the application name for the new API key.
"createAPIKeyMessage" = "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 /// Create a pin to sign in to %@ on this device
"createPinForUser" = "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 /// Custom failed logins
"customFailedLogins" = "Custom failed logins"; "customFailedLogins" = "Custom failed logins";
/// Customize
"customize" = "Customize";
/// Custom Profile /// Custom Profile
"customProfile" = "Custom Profile"; "customProfile" = "Custom Profile";
@ -481,9 +484,6 @@
/// Custom sessions /// Custom sessions
"customSessions" = "Custom sessions"; "customSessions" = "Custom sessions";
/// Customize
"customize" = "Customize";
/// Daily /// Daily
"daily" = "Daily"; "daily" = "Daily";
@ -637,6 +637,9 @@
/// Plays content in its original format. May cause playback issues on unsupported media types. /// 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."; "directDescription" = "Plays content in its original format. May cause playback issues on unsupported media types.";
/// Director
"director" = "Director";
/// Direct Play /// Direct Play
"directPlay" = "Direct Play"; "directPlay" = "Direct Play";
@ -646,9 +649,6 @@
/// Direct Stream /// Direct Stream
"directStream" = "Direct Stream"; "directStream" = "Direct Stream";
/// Director
"director" = "Director";
/// Disabled /// Disabled
"disabled" = "Disabled"; "disabled" = "Disabled";
@ -679,15 +679,15 @@
/// Edit /// Edit
"edit" = "Edit"; "edit" = "Edit";
/// Editor
"editor" = "Editor";
/// Edit Server /// Edit Server
"editServer" = "Edit Server"; "editServer" = "Edit Server";
/// Edit Users /// Edit Users
"editUsers" = "Edit Users"; "editUsers" = "Edit Users";
/// Editor
"editor" = "Editor";
/// Enable all devices /// Enable all devices
"enableAllDevices" = "Enable all devices"; "enableAllDevices" = "Enable all devices";
@ -700,12 +700,12 @@
/// End Date /// End Date
"endDate" = "End Date"; "endDate" = "End Date";
/// End Time
"endTime" = "End Time";
/// Ended /// Ended
"ended" = "Ended"; "ended" = "Ended";
/// End Time
"endTime" = "End Time";
/// Engineer /// Engineer
"engineer" = "Engineer"; "engineer" = "Engineer";
@ -754,12 +754,12 @@
/// Every /// Every
"every" = "Every"; "every" = "Every";
/// Every %1$@
"everyInterval" = "Every %1$@";
/// Everyday /// Everyday
"everyday" = "Everyday"; "everyday" = "Everyday";
/// Every %1$@
"everyInterval" = "Every %1$@";
/// Executed /// Executed
"executed" = "Executed"; "executed" = "Executed";
@ -937,12 +937,12 @@
/// Letter /// Letter
"letter" = "Letter"; "letter" = "Letter";
/// Letter Picker
"letterPicker" = "Letter Picker";
/// Letterer /// Letterer
"letterer" = "Letterer"; "letterer" = "Letterer";
/// Letter Picker
"letterPicker" = "Letter Picker";
/// Library /// Library
"library" = "Library"; "library" = "Library";
@ -958,15 +958,15 @@
/// Live TV /// Live TV
"liveTV" = "Live TV"; "liveTV" = "Live TV";
/// Live TV access
"liveTvAccess" = "Live TV access";
/// Live TV Channels /// Live TV Channels
"liveTVChannels" = "Live TV Channels"; "liveTVChannels" = "Live TV Channels";
/// Live TV Programs /// Live TV Programs
"liveTVPrograms" = "Live TV Programs"; "liveTVPrograms" = "Live TV Programs";
/// Live TV access
"liveTvAccess" = "Live TV access";
/// Live TV recording management /// Live TV recording management
"liveTvRecordingManagement" = "Live TV recording management"; "liveTvRecordingManagement" = "Live TV recording management";
@ -1000,12 +1000,6 @@
/// Management /// Management
"management" = "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 /// Maximum Bitrate
"maximumBitrate" = "Maximum Bitrate"; "maximumBitrate" = "Maximum Bitrate";
@ -1030,6 +1024,12 @@
/// Maximum sessions policy /// Maximum sessions policy
"maximumSessionsPolicy" = "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" = "Media"; "media" = "Media";
@ -1096,12 +1096,12 @@
/// New Password /// New Password
"newPassword" = "New Password"; "newPassword" = "New Password";
/// New User
"newUser" = "New User";
/// News /// News
"news" = "News"; "news" = "News";
/// New User
"newUser" = "New User";
/// Next /// Next
"next" = "Next"; "next" = "Next";
@ -1129,6 +1129,9 @@
/// No local servers found /// No local servers found
"noLocalServersFound" = "No local servers found"; "noLocalServersFound" = "No local servers found";
/// None
"none" = "None";
/// No overview available /// No overview available
"noOverviewAvailable" = "No overview available"; "noOverviewAvailable" = "No overview available";
@ -1138,24 +1141,21 @@
/// No results. /// No results.
"noResults" = "No results."; "noResults" = "No results.";
/// Normal
"normal" = "Normal";
/// No runtime limit /// No runtime limit
"noRuntimeLimit" = "No runtime limit"; "noRuntimeLimit" = "No runtime limit";
/// No session /// No session
"noSession" = "No session"; "noSession" = "No session";
/// No title
"noTitle" = "No title";
/// None
"none" = "None";
/// Normal
"normal" = "Normal";
/// Type: %@ not implemented yet :( /// Type: %@ not implemented yet :(
"notImplementedYetWithType" = "Type: %@ not implemented yet :("; "notImplementedYetWithType" = "Type: %@ not implemented yet :(";
/// No title
"noTitle" = "No title";
/// Official Rating /// Official Rating
"officialRating" = "Official Rating"; "officialRating" = "Official Rating";
@ -1207,12 +1207,12 @@
/// Password /// Password
"password" = "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. /// User password has been changed.
"passwordChangedMessage" = "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. /// New passwords do not match.
"passwordsDoNotMatch" = "New passwords do not match."; "passwordsDoNotMatch" = "New passwords do not match.";
@ -1240,18 +1240,6 @@
/// Play / Pause /// Play / Pause
"playAndPause" = "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" = "Playback"; "playback" = "Playback";
@ -1267,6 +1255,18 @@
/// Played /// Played
"played" = "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" = "Posters"; "posters" = "Posters";
@ -1402,6 +1402,9 @@
/// Replace unlocked metadata with new information. /// Replace unlocked metadata with new information.
"replaceMetadataDescription" = "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. /// Require device authentication when signing in to the user.
"requireDeviceAuthDescription" = "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. /// 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."; "requirePinDescription" = "Require a local pin when signing in to the user. This pin is unrecoverable.";
/// Required
"required" = "Required";
/// Reset /// Reset
"reset" = "Reset"; "reset" = "Reset";
@ -1525,12 +1525,12 @@
/// Server Logs /// Server Logs
"serverLogs" = "Server Logs"; "serverLogs" = "Server Logs";
/// Server URL
"serverURL" = "Server URL";
/// Servers /// Servers
"servers" = "Servers"; "servers" = "Servers";
/// Server URL
"serverURL" = "Server URL";
/// Session /// Session
"session" = "Session"; "session" = "Session";
@ -1660,15 +1660,15 @@
/// Subtitle Offset /// Subtitle Offset
"subtitleOffset" = "Subtitle Offset"; "subtitleOffset" = "Subtitle Offset";
/// Subtitle Size
"subtitleSize" = "Subtitle Size";
/// Subtitles /// Subtitles
"subtitles" = "Subtitles"; "subtitles" = "Subtitles";
/// Settings only affect some subtitle types /// Settings only affect some subtitle types
"subtitlesDisclaimer" = "Settings only affect some subtitle types"; "subtitlesDisclaimer" = "Settings only affect some subtitle types";
/// Subtitle Size
"subtitleSize" = "Subtitle Size";
/// Success /// Success
"success" = "Success"; "success" = "Success";
@ -1720,18 +1720,18 @@
/// Failed /// Failed
"taskFailed" = "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"; "tasks" = "Tasks";
/// Tasks are operations that are scheduled to run periodically or can be triggered manually. /// 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."; "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 /// Tbps
"terabitsPerSecond" = "Tbps"; "terabitsPerSecond" = "Tbps";
@ -1858,18 +1858,18 @@
/// This user will require device authentication. /// This user will require device authentication.
"userDeviceAuthRequiredDescription" = "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" = "Username"; "username" = "Username";
/// A username is required /// A username is required
"usernameRequired" = "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" = "Users"; "users" = "Users";
@ -1927,9 +1927,6 @@
/// This will be created as a new item on your Jellyfin Server. /// 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."; "willBeCreatedOnServer" = "This will be created as a new item on your Jellyfin Server.";
/// WIP
"wip" = "WIP";
/// Writer /// Writer
"writer" = "Writer"; "writer" = "Writer";
@ -1943,5 +1940,4 @@
"yellow" = "Yellow"; "yellow" = "Yellow";
/// Yes /// Yes
"yes" = "Yes"; "yes" = "Yes";