Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion Sources/XcodeGenKit/PBXProjGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1491,6 +1491,7 @@ public class PBXProjGenerator {
var exceptions: Set<String> = Set(
sourceGenerator.syncedFolderExceptions(for: targetSource, at: syncedPath)
.compactMap { try? $0.relativePath(from: syncedPath).string }
.map { syncedFolderLocalizedMembershipException($0) }
)

for infoPlistPath in Set(infoPlistFiles.values) {
Expand All @@ -1514,7 +1515,19 @@ public class PBXProjGenerator {
addObject(exceptionSet)
syncedGroup.exceptions = (syncedGroup.exceptions ?? []) + [exceptionSet]
}


/// In synced folders, Xcode expects localized file membership exceptions to use a
/// `/Localized:` prefix with the variant-group path (stripping the `.lproj` component), e.g.:
/// `Resources/de.lproj/Localizable.strings` → `/Localized: Resources/Localizable.strings`
private func syncedFolderLocalizedMembershipException(_ path: String) -> String {
var components = path.split(separator: "/").map(String.init)
guard let lprojIndex = components.firstIndex(where: { $0.hasSuffix(".lproj") }) else {
return path
}
components.remove(at: lprojIndex)
return "/Localized: " + components.joined(separator: "/")
}

private func makePlatformFilter(for filter: Dependency.PlatformFilter) -> String? {
switch filter {
case .all:
Expand Down
64 changes: 64 additions & 0 deletions Tests/XcodeGenKitTests/SourceGeneratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,70 @@ class SourceGeneratorTests: XCTestCase {
try expect(exceptions.contains("c.swift")) == true
}

$0.it("uses /Localized: prefix for lproj membership exceptions") {
let directories = """
Sources:
- a.swift
- Resources:
- de.lproj:
- Localizable.strings
- AppShortcuts.strings
- fr.lproj:
- Localizable.strings
- AppShortcuts.strings
"""
try createDirectories(directories)

let source = TargetSource(path: "Sources", includes: ["a.swift"], type: .syncedFolder)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [source])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])

let pbxProj = try project.generatePbxProj()
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
let syncedFolder = try unwrap(syncedFolders.first)

let exceptionSet = try unwrap(syncedFolder.exceptions?.first as? PBXFileSystemSynchronizedBuildFileExceptionSet)
let exceptions = try unwrap(exceptionSet.membershipExceptions)

try expect(exceptions.contains("/Localized: Resources/Localizable.strings")) == true
try expect(exceptions.contains("/Localized: Resources/AppShortcuts.strings")) == true
try expect(exceptions.contains("Resources/de.lproj/Localizable.strings")) == false
try expect(exceptions.contains("Resources/fr.lproj/Localizable.strings")) == false
try expect(exceptions.contains("a.swift")) == false
// de + fr variants deduplicate into one /Localized: entry per file
let localizedCount = exceptions.filter { $0.hasPrefix("/Localized:") }.count
try expect(localizedCount) == 2
}

$0.it("preserves non-localized exceptions alongside /Localized: entries") {
let directories = """
Sources:
- a.swift
- b.swift
- Resources:
- de.lproj:
- Localizable.strings
- fr.lproj:
- Localizable.strings
"""
try createDirectories(directories)

let source = TargetSource(path: "Sources", includes: ["a.swift"], type: .syncedFolder)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [source])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])

let pbxProj = try project.generatePbxProj()
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
let syncedFolder = try unwrap(syncedFolders.first)

let exceptionSet = try unwrap(syncedFolder.exceptions?.first as? PBXFileSystemSynchronizedBuildFileExceptionSet)
let exceptions = try unwrap(exceptionSet.membershipExceptions)

try expect(exceptions.contains("b.swift")) == true
try expect(exceptions.contains("/Localized: Resources/Localizable.strings")) == true
try expect(exceptions.contains("a.swift")) == false
}

$0.it("deduplicates synced folders and both targets reference the same group object") {
let directories = """
Sources:
Expand Down