From 1ecd010c0a6ba776054e9701e3599397cede5403 Mon Sep 17 00:00:00 2001 From: Oluwaseun Adebanwo Date: Wed, 29 Nov 2023 18:49:17 +0100 Subject: [PATCH 01/11] Set `FeedViewController` title --- .../Feed UI/Controllers/FeedViewController.swift | 2 +- .../Feed UI/Controllers/FeedViewControllerTests.swift | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift b/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift index 5e40b8e..1b1fda3 100644 --- a/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift +++ b/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift @@ -20,7 +20,7 @@ public final class FeedViewController: UITableViewController, UITableViewDataSou public override func viewDidLoad() { super.viewDidLoad() - + title = "My Feed" refresh() } diff --git a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift index e164d86..c4328a3 100644 --- a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift +++ b/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift @@ -11,6 +11,14 @@ import EssentialFeed import EssentialFeediOS final class FeedViewControllerTests: XCTestCase { + + func test_feedView_hasTitle() { + let (sut, _) = makeSUT() + + sut.loadViewIfNeeded() + + XCTAssertEqual(sut.title, "My Feed") + } func test_loadFeedActions_requestFeedFromLoader() { let (sut, loader) = makeSUT() From 5b4c9bd85194cdbe11dac23077a30e50e67989dc Mon Sep 17 00:00:00 2001 From: Oluwaseun Adebanwo Date: Wed, 29 Nov 2023 18:51:26 +0100 Subject: [PATCH 02/11] Move title string creation from `FeedViewController` to `FeedPresenter` - In MVP, presentation data should be created by Presenters --- .../EssentialFeediOS/Feed Presentation/FeedPresenter.swift | 4 ++++ .../Feed UI/Controllers/FeedViewController.swift | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/EssentialFeed/EssentialFeediOS/Feed Presentation/FeedPresenter.swift b/EssentialFeed/EssentialFeediOS/Feed Presentation/FeedPresenter.swift index eb6b62b..c5d4b4e 100644 --- a/EssentialFeed/EssentialFeediOS/Feed Presentation/FeedPresenter.swift +++ b/EssentialFeed/EssentialFeediOS/Feed Presentation/FeedPresenter.swift @@ -24,6 +24,10 @@ final class FeedPresenter { self.loadingView = loadingView } + static var title: String { + return "My Feed" + } + func didStartLoadingFeed() { loadingView.display(FeedLoadingViewModel(isLoading: true)) } diff --git a/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift b/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift index 1b1fda3..5eab10f 100644 --- a/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift +++ b/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift @@ -20,7 +20,7 @@ public final class FeedViewController: UITableViewController, UITableViewDataSou public override func viewDidLoad() { super.viewDidLoad() - title = "My Feed" + title = FeedPresenter.title refresh() } From 0b85883585e586ad0ff51775f230ac4e40a351db Mon Sep 17 00:00:00 2001 From: Oluwaseun Adebanwo Date: Wed, 29 Nov 2023 18:53:45 +0100 Subject: [PATCH 03/11] Move title configuration from `FeedViewController` to the `FeedUIComposer` - the View Controllers can be agnostic of Presenters if we move the configuration to composers --- .../EssentialFeediOS/Feed UI/Composers/FeedUIComposer.swift | 1 + .../Feed UI/Controllers/FeedViewController.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/EssentialFeed/EssentialFeediOS/Feed UI/Composers/FeedUIComposer.swift b/EssentialFeed/EssentialFeediOS/Feed UI/Composers/FeedUIComposer.swift index f224090..6a149b6 100644 --- a/EssentialFeed/EssentialFeediOS/Feed UI/Composers/FeedUIComposer.swift +++ b/EssentialFeed/EssentialFeediOS/Feed UI/Composers/FeedUIComposer.swift @@ -18,6 +18,7 @@ public final class FeedUIComposer { let storyboard = UIStoryboard(name: "Feed", bundle: bundle) let feedController = storyboard.instantiateInitialViewController() as! FeedViewController feedController.delegate = presentationAdapter + feedController.title = FeedPresenter.title presentationAdapter.presenter = FeedPresenter( feedView: FeedViewAdapter(controller: feedController, imageLoader: imageLoader), diff --git a/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift b/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift index 5eab10f..5e40b8e 100644 --- a/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift +++ b/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift @@ -20,7 +20,7 @@ public final class FeedViewController: UITableViewController, UITableViewDataSou public override func viewDidLoad() { super.viewDidLoad() - title = FeedPresenter.title + refresh() } From 9c30975d8390fa555eb7287efb1a2b91ac8c0528 Mon Sep 17 00:00:00 2001 From: Oluwaseun Adebanwo Date: Wed, 29 Nov 2023 18:55:55 +0100 Subject: [PATCH 04/11] Extract the `FeedViewController` creation and configuration into a factory method --- .../Feed UI/Composers/FeedUIComposer.swift | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/EssentialFeed/EssentialFeediOS/Feed UI/Composers/FeedUIComposer.swift b/EssentialFeed/EssentialFeediOS/Feed UI/Composers/FeedUIComposer.swift index 6a149b6..5ef08c6 100644 --- a/EssentialFeed/EssentialFeediOS/Feed UI/Composers/FeedUIComposer.swift +++ b/EssentialFeed/EssentialFeediOS/Feed UI/Composers/FeedUIComposer.swift @@ -14,11 +14,9 @@ public final class FeedUIComposer { public static func feedComposedWith(feedLoader: FeedLoader, imageLoader: FeedImageDataLoader) -> FeedViewController { let presentationAdapter = FeedLoaderPresentationAdapter(feedLoader: feedLoader) - let bundle = Bundle(for: FeedViewController.self) - let storyboard = UIStoryboard(name: "Feed", bundle: bundle) - let feedController = storyboard.instantiateInitialViewController() as! FeedViewController - feedController.delegate = presentationAdapter - feedController.title = FeedPresenter.title + let feedController = FeedViewController.makeWith( + delegate: presentationAdapter, + title: FeedPresenter.title) presentationAdapter.presenter = FeedPresenter( feedView: FeedViewAdapter(controller: feedController, imageLoader: imageLoader), @@ -29,6 +27,17 @@ public final class FeedUIComposer { } } +private extension FeedViewController { + static func makeWith(delegate: FeedViewControllerDelegate, title: String) -> FeedViewController { + let bundle = Bundle(for: FeedViewController.self) + let storyboard = UIStoryboard(name: "Feed", bundle: bundle) + let feedController = storyboard.instantiateInitialViewController() as! FeedViewController + feedController.delegate = delegate + feedController.title = title + return feedController + } +} + private final class WeakRefVirtualProxy { private weak var object: T? From ce84ce787b947bbb08eb3e0f0a1634fa754e2f85 Mon Sep 17 00:00:00 2001 From: Oluwaseun Adebanwo Date: Wed, 29 Nov 2023 19:04:21 +0100 Subject: [PATCH 05/11] Localize feed view title string --- EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj | 4 ++++ .../EssentialFeediOS/Feed Presentation/Feed.strings | 9 +++++++++ .../Feed Presentation/FeedPresenter.swift | 6 +++++- .../Feed UI/Controllers/FeedViewControllerTests.swift | 7 ++++++- 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 EssentialFeed/EssentialFeediOS/Feed Presentation/Feed.strings diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 345f5e7..8819fb4 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -77,6 +77,7 @@ EDFBE2802B16B44E00EFB793 /* Feed.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EDFBE27F2B16B44E00EFB793 /* Feed.xcassets */; }; EDFBE2822B16C2EB00EFB793 /* UITableView+Dequeueing.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2812B16C2EB00EFB793 /* UITableView+Dequeueing.swift */; }; EDFBE2842B16C40800EFB793 /* UIImageView+Animations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2832B16C40800EFB793 /* UIImageView+Animations.swift */; }; + EDFBE2862B17B43700EFB793 /* Feed.strings in Resources */ = {isa = PBXBuildFile; fileRef = EDFBE2852B17B43700EFB793 /* Feed.strings */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -203,6 +204,7 @@ EDFBE27F2B16B44E00EFB793 /* Feed.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Feed.xcassets; sourceTree = ""; }; EDFBE2812B16C2EB00EFB793 /* UITableView+Dequeueing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Dequeueing.swift"; sourceTree = ""; }; EDFBE2832B16C40800EFB793 /* UIImageView+Animations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Animations.swift"; sourceTree = ""; }; + EDFBE2852B17B43700EFB793 /* Feed.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Feed.strings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -561,6 +563,7 @@ EDFBE2772B15C8EB00EFB793 /* FeedImagePresenter.swift */, EDFBE2792B15CC5700EFB793 /* FeedViewModel.swift */, EDFBE27B2B15CDC600EFB793 /* FeedLoadingViewModel.swift */, + EDFBE2852B17B43700EFB793 /* Feed.strings */, ); path = "Feed Presentation"; sourceTree = ""; @@ -779,6 +782,7 @@ files = ( EDFBE27E2B16B37E00EFB793 /* Feed.storyboard in Resources */, EDFBE2802B16B44E00EFB793 /* Feed.xcassets in Resources */, + EDFBE2862B17B43700EFB793 /* Feed.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/EssentialFeed/EssentialFeediOS/Feed Presentation/Feed.strings b/EssentialFeed/EssentialFeediOS/Feed Presentation/Feed.strings new file mode 100644 index 0000000..da2404c --- /dev/null +++ b/EssentialFeed/EssentialFeediOS/Feed Presentation/Feed.strings @@ -0,0 +1,9 @@ +/* + Feed.strings + EssentialFeed + + Created by Oluwaseun Adebanwo on 29/11/2023. + +*/ + +"FEED_VIEW_TITLE" = "My Feed"; diff --git a/EssentialFeed/EssentialFeediOS/Feed Presentation/FeedPresenter.swift b/EssentialFeed/EssentialFeediOS/Feed Presentation/FeedPresenter.swift index c5d4b4e..e4f65c9 100644 --- a/EssentialFeed/EssentialFeediOS/Feed Presentation/FeedPresenter.swift +++ b/EssentialFeed/EssentialFeediOS/Feed Presentation/FeedPresenter.swift @@ -5,6 +5,7 @@ // Created by Oluwaseun Adebanwo on 28/11/2023. // +import Foundation import EssentialFeed protocol FeedLoadingView { @@ -25,7 +26,10 @@ final class FeedPresenter { } static var title: String { - return "My Feed" + return NSLocalizedString("FEED_VIEW_TITLE", + tableName: "Feed", + bundle: Bundle(for: FeedPresenter.self), + comment: "Title for the feed view") } func didStartLoadingFeed() { diff --git a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift index c4328a3..22d0216 100644 --- a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift +++ b/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift @@ -17,7 +17,12 @@ final class FeedViewControllerTests: XCTestCase { sut.loadViewIfNeeded() - XCTAssertEqual(sut.title, "My Feed") + let bundle = Bundle(for: FeedViewController.self) + let localizedKey = "FEED_VIEW_TITLE" + let localizedTitle = bundle.localizedString(forKey: localizedKey, value: nil, table: "Feed") + + XCTAssertNotEqual(localizedKey, localizedTitle, "Missing localized string for key: \(localizedKey)") + XCTAssertEqual(sut.title, localizedTitle) } func test_loadFeedActions_requestFeedFromLoader() { From 102c33b0860b064d9163ea5e891fce97e28a64a8 Mon Sep 17 00:00:00 2001 From: Oluwaseun Adebanwo Date: Wed, 29 Nov 2023 19:11:28 +0100 Subject: [PATCH 06/11] Create test helper to find missing localized strings --- .../EssentialFeed.xcodeproj/project.pbxproj | 4 ++++ .../Controllers/FeedViewControllerTests.swift | 7 +----- ...FeedViewControllerTests+Localization.swift | 22 +++++++++++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+Localization.swift diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 8819fb4..5393e4e 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -78,6 +78,7 @@ EDFBE2822B16C2EB00EFB793 /* UITableView+Dequeueing.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2812B16C2EB00EFB793 /* UITableView+Dequeueing.swift */; }; EDFBE2842B16C40800EFB793 /* UIImageView+Animations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2832B16C40800EFB793 /* UIImageView+Animations.swift */; }; EDFBE2862B17B43700EFB793 /* Feed.strings in Resources */ = {isa = PBXBuildFile; fileRef = EDFBE2852B17B43700EFB793 /* Feed.strings */; }; + EDFBE2882B17B52700EFB793 /* FeedViewControllerTests+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2872B17B52700EFB793 /* FeedViewControllerTests+Localization.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -205,6 +206,7 @@ EDFBE2812B16C2EB00EFB793 /* UITableView+Dequeueing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Dequeueing.swift"; sourceTree = ""; }; EDFBE2832B16C40800EFB793 /* UIImageView+Animations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Animations.swift"; sourceTree = ""; }; EDFBE2852B17B43700EFB793 /* Feed.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Feed.strings; sourceTree = ""; }; + EDFBE2872B17B52700EFB793 /* FeedViewControllerTests+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedViewControllerTests+Localization.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -535,6 +537,7 @@ EDFBE25F2B150A7200EFB793 /* FeedImageCell+TestHelpers.swift */, EDFBE2612B150DC600EFB793 /* FeedViewControllerTests+LoaderSpy.swift */, EDFBE2632B15713C00EFB793 /* FeedViewControllerTests+Assertions.swift */, + EDFBE2872B17B52700EFB793 /* FeedViewControllerTests+Localization.swift */, ); path = Helpers; sourceTree = ""; @@ -894,6 +897,7 @@ EDFBE2402B10409C00EFB793 /* FeedViewControllerTests.swift in Sources */, EDFBE2542B14FEE100EFB793 /* UIImage+TestHelpers.swift in Sources */, EDFBE2602B150A7200EFB793 /* FeedImageCell+TestHelpers.swift in Sources */, + EDFBE2882B17B52700EFB793 /* FeedViewControllerTests+Localization.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift index 22d0216..1dfc66d 100644 --- a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift +++ b/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift @@ -17,12 +17,7 @@ final class FeedViewControllerTests: XCTestCase { sut.loadViewIfNeeded() - let bundle = Bundle(for: FeedViewController.self) - let localizedKey = "FEED_VIEW_TITLE" - let localizedTitle = bundle.localizedString(forKey: localizedKey, value: nil, table: "Feed") - - XCTAssertNotEqual(localizedKey, localizedTitle, "Missing localized string for key: \(localizedKey)") - XCTAssertEqual(sut.title, localizedTitle) + XCTAssertEqual(sut.title, localized("FEED_VIEW_TITLE")) } func test_loadFeedActions_requestFeedFromLoader() { diff --git a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+Localization.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+Localization.swift new file mode 100644 index 0000000..5f257f1 --- /dev/null +++ b/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+Localization.swift @@ -0,0 +1,22 @@ +// +// FeedViewControllerTests+Localization.swift +// EssentialFeediOSTests +// +// Created by Oluwaseun Adebanwo on 29/11/2023. +// + +import Foundation +import XCTest +import EssentialFeediOS + +extension FeedViewControllerTests { + func localized(_ key: String, file: StaticString = #file, line: UInt = #line) -> String { + let table = "Feed" + let bundle = Bundle(for: FeedViewController.self) + let value = bundle.localizedString(forKey: key, value: nil, table: table) + if value == key { + XCTFail("Missing localized string for key: \(key) in table: \(table)", file: file, line: line) + } + return value + } +} From 31e27d6e8cd00c8f4170328545990d002d6bfbaa Mon Sep 17 00:00:00 2001 From: Oluwaseun Adebanwo Date: Wed, 29 Nov 2023 19:23:00 +0100 Subject: [PATCH 07/11] Rename `FeedViewControllerTests` to `FeedUIIntegrationTests` since we are testing the composition of multiple UI components in integration --- .../EssentialFeed.xcodeproj/project.pbxproj | 28 ++++--------------- ...sts.swift => FeedUIIntegrationTests.swift} | 2 +- .../Helpers/FeedImageCell+TestHelpers.swift | 0 .../FeedViewController+TestHelpers.swift | 0 .../FeedViewControllerTests+Assertions.swift | 2 +- .../FeedViewControllerTests+LoaderSpy.swift | 2 +- ...FeedViewControllerTests+Localization.swift | 2 +- .../Helpers/UIButton+TestHelpers.swift | 0 .../Helpers/UIControl+TestHelpers.swift | 0 .../Helpers/UIImage+TestHelpers.swift | 0 .../UIRefreshControl+TestHelpers.swift | 0 11 files changed, 10 insertions(+), 26 deletions(-) rename EssentialFeed/EssentialFeediOSTests/Feed UI/{Controllers/FeedViewControllerTests.swift => FeedUIIntegrationTests.swift} (99%) rename EssentialFeed/EssentialFeediOSTests/Feed UI/{Controllers => }/Helpers/FeedImageCell+TestHelpers.swift (100%) rename EssentialFeed/EssentialFeediOSTests/Feed UI/{Controllers => }/Helpers/FeedViewController+TestHelpers.swift (100%) rename EssentialFeed/EssentialFeediOSTests/Feed UI/{Controllers => }/Helpers/FeedViewControllerTests+Assertions.swift (97%) rename EssentialFeed/EssentialFeediOSTests/Feed UI/{Controllers => }/Helpers/FeedViewControllerTests+LoaderSpy.swift (98%) rename EssentialFeed/EssentialFeediOSTests/Feed UI/{Controllers => }/Helpers/FeedViewControllerTests+Localization.swift (94%) rename EssentialFeed/EssentialFeediOSTests/{ => Feed UI}/Helpers/UIButton+TestHelpers.swift (100%) rename EssentialFeed/EssentialFeediOSTests/{ => Feed UI}/Helpers/UIControl+TestHelpers.swift (100%) rename EssentialFeed/EssentialFeediOSTests/{ => Feed UI}/Helpers/UIImage+TestHelpers.swift (100%) rename EssentialFeed/EssentialFeediOSTests/{ => Feed UI}/Helpers/UIRefreshControl+TestHelpers.swift (100%) diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 5393e4e..50ae185 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -50,7 +50,7 @@ EDF8FE4E2AE2389D00605C5C /* FeedCacheTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF8FE4D2AE2389D00605C5C /* FeedCacheTestHelpers.swift */; }; EDF8FE502AE2390B00605C5C /* SharedTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF8FE4F2AE2390B00605C5C /* SharedTestHelpers.swift */; }; EDF8FE522AE2470300605C5C /* FeedCachePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF8FE512AE2470300605C5C /* FeedCachePolicy.swift */; }; - EDFBE2402B10409C00EFB793 /* FeedViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE23F2B10409C00EFB793 /* FeedViewControllerTests.swift */; }; + EDFBE2402B10409C00EFB793 /* FeedUIIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE23F2B10409C00EFB793 /* FeedUIIntegrationTests.swift */; }; EDFBE2422B1046C600EFB793 /* EssentialFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1438189629A135C0005B41C9 /* EssentialFeed.framework */; platformFilter = ios; }; EDFBE2432B1046C600EFB793 /* EssentialFeed.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1438189629A135C0005B41C9 /* EssentialFeed.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; EDFBE2472B1048CF00EFB793 /* XCTTestCase + MemoryLeakTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3FD1222A267833006299CD /* XCTTestCase + MemoryLeakTracking.swift */; }; @@ -181,7 +181,7 @@ EDF8FE4D2AE2389D00605C5C /* FeedCacheTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCacheTestHelpers.swift; sourceTree = ""; }; EDF8FE4F2AE2390B00605C5C /* SharedTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedTestHelpers.swift; sourceTree = ""; }; EDF8FE512AE2470300605C5C /* FeedCachePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCachePolicy.swift; sourceTree = ""; }; - EDFBE23F2B10409C00EFB793 /* FeedViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewControllerTests.swift; sourceTree = ""; }; + EDFBE23F2B10409C00EFB793 /* FeedUIIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedUIIntegrationTests.swift; sourceTree = ""; }; EDFBE2482B10534300EFB793 /* FeedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = ""; }; EDFBE24A2B107C8900EFB793 /* FeedImageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImageCell.swift; sourceTree = ""; }; EDFBE24C2B13186200EFB793 /* UIView+Shimmering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Shimmering.swift"; sourceTree = ""; }; @@ -352,7 +352,6 @@ ED0EDEC62B0FB681004857B7 /* EssentialFeediOSTests */ = { isa = PBXGroup; children = ( - EDFBE2522B14FE9500EFB793 /* Helpers */, EDFBE2652B1572A800EFB793 /* Feed UI */, ); path = EssentialFeediOSTests; @@ -510,29 +509,13 @@ path = Views; sourceTree = ""; }; - EDFBE2522B14FE9500EFB793 /* Helpers */ = { + EDFBE25E2B150A5000EFB793 /* Helpers */ = { isa = PBXGroup; children = ( EDFBE2532B14FEE100EFB793 /* UIImage+TestHelpers.swift */, EDFBE2552B14FFF500EFB793 /* UIRefreshControl+TestHelpers.swift */, EDFBE2572B15005500EFB793 /* UIButton+TestHelpers.swift */, EDFBE2592B1500F900EFB793 /* UIControl+TestHelpers.swift */, - ); - path = Helpers; - sourceTree = ""; - }; - EDFBE25B2B15036400EFB793 /* Controllers */ = { - isa = PBXGroup; - children = ( - EDFBE25E2B150A5000EFB793 /* Helpers */, - EDFBE23F2B10409C00EFB793 /* FeedViewControllerTests.swift */, - ); - path = Controllers; - sourceTree = ""; - }; - EDFBE25E2B150A5000EFB793 /* Helpers */ = { - isa = PBXGroup; - children = ( EDFBE25C2B15051100EFB793 /* FeedViewController+TestHelpers.swift */, EDFBE25F2B150A7200EFB793 /* FeedImageCell+TestHelpers.swift */, EDFBE2612B150DC600EFB793 /* FeedViewControllerTests+LoaderSpy.swift */, @@ -545,7 +528,8 @@ EDFBE2652B1572A800EFB793 /* Feed UI */ = { isa = PBXGroup; children = ( - EDFBE25B2B15036400EFB793 /* Controllers */, + EDFBE25E2B150A5000EFB793 /* Helpers */, + EDFBE23F2B10409C00EFB793 /* FeedUIIntegrationTests.swift */, ); path = "Feed UI"; sourceTree = ""; @@ -894,7 +878,7 @@ EDFBE25A2B1500F900EFB793 /* UIControl+TestHelpers.swift in Sources */, EDFBE2622B150DC600EFB793 /* FeedViewControllerTests+LoaderSpy.swift in Sources */, EDFBE25D2B15051100EFB793 /* FeedViewController+TestHelpers.swift in Sources */, - EDFBE2402B10409C00EFB793 /* FeedViewControllerTests.swift in Sources */, + EDFBE2402B10409C00EFB793 /* FeedUIIntegrationTests.swift in Sources */, EDFBE2542B14FEE100EFB793 /* UIImage+TestHelpers.swift in Sources */, EDFBE2602B150A7200EFB793 /* FeedImageCell+TestHelpers.swift in Sources */, EDFBE2882B17B52700EFB793 /* FeedViewControllerTests+Localization.swift in Sources */, diff --git a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/FeedUIIntegrationTests.swift similarity index 99% rename from EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift rename to EssentialFeed/EssentialFeediOSTests/Feed UI/FeedUIIntegrationTests.swift index 1dfc66d..507471d 100644 --- a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/FeedViewControllerTests.swift +++ b/EssentialFeed/EssentialFeediOSTests/Feed UI/FeedUIIntegrationTests.swift @@ -10,7 +10,7 @@ import UIKit import EssentialFeed import EssentialFeediOS -final class FeedViewControllerTests: XCTestCase { +final class FeedUIIntegrationTests: XCTestCase { func test_feedView_hasTitle() { let (sut, _) = makeSUT() diff --git a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedImageCell+TestHelpers.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedImageCell+TestHelpers.swift similarity index 100% rename from EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedImageCell+TestHelpers.swift rename to EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedImageCell+TestHelpers.swift diff --git a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewController+TestHelpers.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedViewController+TestHelpers.swift similarity index 100% rename from EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewController+TestHelpers.swift rename to EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedViewController+TestHelpers.swift diff --git a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+Assertions.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedViewControllerTests+Assertions.swift similarity index 97% rename from EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+Assertions.swift rename to EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedViewControllerTests+Assertions.swift index 0c53613..4195728 100644 --- a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+Assertions.swift +++ b/EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedViewControllerTests+Assertions.swift @@ -9,7 +9,7 @@ import XCTest import EssentialFeed import EssentialFeediOS -extension FeedViewControllerTests { +extension FeedUIIntegrationTests { func assertThat(_ sut: FeedViewController, isRendering feed: [FeedImage], file: StaticString = #file, line: UInt = #line) { guard sut.numberOfRenderedFeedImageViews() == feed.count else { return XCTFail("Expected \(feed.count) images, got \(sut.numberOfRenderedFeedImageViews()) instead.", file: file, line: line) diff --git a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+LoaderSpy.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedViewControllerTests+LoaderSpy.swift similarity index 98% rename from EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+LoaderSpy.swift rename to EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedViewControllerTests+LoaderSpy.swift index f9562d0..5f87c7b 100644 --- a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+LoaderSpy.swift +++ b/EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedViewControllerTests+LoaderSpy.swift @@ -9,7 +9,7 @@ import Foundation import EssentialFeed import EssentialFeediOS -extension FeedViewControllerTests { +extension FeedUIIntegrationTests { class LoaderSpy: FeedLoader, FeedImageDataLoader { diff --git a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+Localization.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedViewControllerTests+Localization.swift similarity index 94% rename from EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+Localization.swift rename to EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedViewControllerTests+Localization.swift index 5f257f1..ec299d6 100644 --- a/EssentialFeed/EssentialFeediOSTests/Feed UI/Controllers/Helpers/FeedViewControllerTests+Localization.swift +++ b/EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/FeedViewControllerTests+Localization.swift @@ -9,7 +9,7 @@ import Foundation import XCTest import EssentialFeediOS -extension FeedViewControllerTests { +extension FeedUIIntegrationTests { func localized(_ key: String, file: StaticString = #file, line: UInt = #line) -> String { let table = "Feed" let bundle = Bundle(for: FeedViewController.self) diff --git a/EssentialFeed/EssentialFeediOSTests/Helpers/UIButton+TestHelpers.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/UIButton+TestHelpers.swift similarity index 100% rename from EssentialFeed/EssentialFeediOSTests/Helpers/UIButton+TestHelpers.swift rename to EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/UIButton+TestHelpers.swift diff --git a/EssentialFeed/EssentialFeediOSTests/Helpers/UIControl+TestHelpers.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/UIControl+TestHelpers.swift similarity index 100% rename from EssentialFeed/EssentialFeediOSTests/Helpers/UIControl+TestHelpers.swift rename to EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/UIControl+TestHelpers.swift diff --git a/EssentialFeed/EssentialFeediOSTests/Helpers/UIImage+TestHelpers.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/UIImage+TestHelpers.swift similarity index 100% rename from EssentialFeed/EssentialFeediOSTests/Helpers/UIImage+TestHelpers.swift rename to EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/UIImage+TestHelpers.swift diff --git a/EssentialFeed/EssentialFeediOSTests/Helpers/UIRefreshControl+TestHelpers.swift b/EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/UIRefreshControl+TestHelpers.swift similarity index 100% rename from EssentialFeed/EssentialFeediOSTests/Helpers/UIRefreshControl+TestHelpers.swift rename to EssentialFeed/EssentialFeediOSTests/Feed UI/Helpers/UIRefreshControl+TestHelpers.swift From b095b37a0b07032511d96897ad8787be4a6decdd Mon Sep 17 00:00:00 2001 From: Oluwaseun Adebanwo Date: Wed, 29 Nov 2023 19:29:25 +0100 Subject: [PATCH 08/11] Localize `Feed.strings` file --- .../EssentialFeed.xcodeproj/project.pbxproj | 19 +++++++++++++++---- .../{ => en.lproj}/Feed.strings | 0 2 files changed, 15 insertions(+), 4 deletions(-) rename EssentialFeed/EssentialFeediOS/Feed Presentation/{ => en.lproj}/Feed.strings (100%) diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 50ae185..579b2aa 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -77,8 +77,8 @@ EDFBE2802B16B44E00EFB793 /* Feed.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EDFBE27F2B16B44E00EFB793 /* Feed.xcassets */; }; EDFBE2822B16C2EB00EFB793 /* UITableView+Dequeueing.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2812B16C2EB00EFB793 /* UITableView+Dequeueing.swift */; }; EDFBE2842B16C40800EFB793 /* UIImageView+Animations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2832B16C40800EFB793 /* UIImageView+Animations.swift */; }; - EDFBE2862B17B43700EFB793 /* Feed.strings in Resources */ = {isa = PBXBuildFile; fileRef = EDFBE2852B17B43700EFB793 /* Feed.strings */; }; EDFBE2882B17B52700EFB793 /* FeedViewControllerTests+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2872B17B52700EFB793 /* FeedViewControllerTests+Localization.swift */; }; + EDFBE2892B17BA2100EFB793 /* Feed.strings in Resources */ = {isa = PBXBuildFile; fileRef = EDFBE28B2B17BA2100EFB793 /* Feed.strings */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -205,8 +205,8 @@ EDFBE27F2B16B44E00EFB793 /* Feed.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Feed.xcassets; sourceTree = ""; }; EDFBE2812B16C2EB00EFB793 /* UITableView+Dequeueing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Dequeueing.swift"; sourceTree = ""; }; EDFBE2832B16C40800EFB793 /* UIImageView+Animations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Animations.swift"; sourceTree = ""; }; - EDFBE2852B17B43700EFB793 /* Feed.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Feed.strings; sourceTree = ""; }; EDFBE2872B17B52700EFB793 /* FeedViewControllerTests+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedViewControllerTests+Localization.swift"; sourceTree = ""; }; + EDFBE28A2B17BA2100EFB793 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Feed.strings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -550,7 +550,7 @@ EDFBE2772B15C8EB00EFB793 /* FeedImagePresenter.swift */, EDFBE2792B15CC5700EFB793 /* FeedViewModel.swift */, EDFBE27B2B15CDC600EFB793 /* FeedLoadingViewModel.swift */, - EDFBE2852B17B43700EFB793 /* Feed.strings */, + EDFBE28B2B17BA2100EFB793 /* Feed.strings */, ); path = "Feed Presentation"; sourceTree = ""; @@ -769,7 +769,7 @@ files = ( EDFBE27E2B16B37E00EFB793 /* Feed.storyboard in Resources */, EDFBE2802B16B44E00EFB793 /* Feed.xcassets in Resources */, - EDFBE2862B17B43700EFB793 /* Feed.strings in Resources */, + EDFBE2892B17BA2100EFB793 /* Feed.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -925,6 +925,17 @@ }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + EDFBE28B2B17BA2100EFB793 /* Feed.strings */ = { + isa = PBXVariantGroup; + children = ( + EDFBE28A2B17BA2100EFB793 /* en */, + ); + name = Feed.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 143818A829A135C0005B41C9 /* Debug */ = { isa = XCBuildConfiguration; diff --git a/EssentialFeed/EssentialFeediOS/Feed Presentation/Feed.strings b/EssentialFeed/EssentialFeediOS/Feed Presentation/en.lproj/Feed.strings similarity index 100% rename from EssentialFeed/EssentialFeediOS/Feed Presentation/Feed.strings rename to EssentialFeed/EssentialFeediOS/Feed Presentation/en.lproj/Feed.strings From 6bfc368f70839da266e57bba2aeb45d7a67f6ab2 Mon Sep 17 00:00:00 2001 From: Oluwaseun Adebanwo Date: Wed, 29 Nov 2023 19:32:54 +0100 Subject: [PATCH 09/11] Add Portuguese (pt-BR) localization --- EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj | 5 +++++ .../Feed Presentation/pt-BR.lproj/Feed.strings | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 EssentialFeed/EssentialFeediOS/Feed Presentation/pt-BR.lproj/Feed.strings diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 579b2aa..6fcf31a 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -207,6 +207,7 @@ EDFBE2832B16C40800EFB793 /* UIImageView+Animations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Animations.swift"; sourceTree = ""; }; EDFBE2872B17B52700EFB793 /* FeedViewControllerTests+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedViewControllerTests+Localization.swift"; sourceTree = ""; }; EDFBE28A2B17BA2100EFB793 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Feed.strings; sourceTree = ""; }; + EDFBE28C2B17BAEF00EFB793 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Feed.strings"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -725,6 +726,7 @@ knownRegions = ( en, Base, + "pt-BR", ); mainGroup = 1438188C29A135C0005B41C9; productRefGroup = 1438189729A135C0005B41C9 /* Products */; @@ -930,6 +932,7 @@ isa = PBXVariantGroup; children = ( EDFBE28A2B17BA2100EFB793 /* en */, + EDFBE28C2B17BAEF00EFB793 /* pt-BR */, ); name = Feed.strings; sourceTree = ""; @@ -941,6 +944,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -1007,6 +1011,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; diff --git a/EssentialFeed/EssentialFeediOS/Feed Presentation/pt-BR.lproj/Feed.strings b/EssentialFeed/EssentialFeediOS/Feed Presentation/pt-BR.lproj/Feed.strings new file mode 100644 index 0000000..675181c --- /dev/null +++ b/EssentialFeed/EssentialFeediOS/Feed Presentation/pt-BR.lproj/Feed.strings @@ -0,0 +1,9 @@ +/* + Feed.strings + EssentialFeed + + Created by Oluwaseun Adebanwo on 29/11/2023. + +*/ + +"FEED_VIEW_TITLE" = "Meu Feed"; From 88bdab3180dcf0b08ae34a53eb95008cf468943c Mon Sep 17 00:00:00 2001 From: Oluwaseun Adebanwo Date: Wed, 29 Nov 2023 19:36:48 +0100 Subject: [PATCH 10/11] Add Greek (el) localization --- EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj | 3 +++ .../Feed Presentation/el.lproj/Feed.strings | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 EssentialFeed/EssentialFeediOS/Feed Presentation/el.lproj/Feed.strings diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 6fcf31a..0d5293f 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -208,6 +208,7 @@ EDFBE2872B17B52700EFB793 /* FeedViewControllerTests+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedViewControllerTests+Localization.swift"; sourceTree = ""; }; EDFBE28A2B17BA2100EFB793 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Feed.strings; sourceTree = ""; }; EDFBE28C2B17BAEF00EFB793 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Feed.strings"; sourceTree = ""; }; + EDFBE28D2B17BBDF00EFB793 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Feed.strings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -727,6 +728,7 @@ en, Base, "pt-BR", + el, ); mainGroup = 1438188C29A135C0005B41C9; productRefGroup = 1438189729A135C0005B41C9 /* Products */; @@ -933,6 +935,7 @@ children = ( EDFBE28A2B17BA2100EFB793 /* en */, EDFBE28C2B17BAEF00EFB793 /* pt-BR */, + EDFBE28D2B17BBDF00EFB793 /* el */, ); name = Feed.strings; sourceTree = ""; diff --git a/EssentialFeed/EssentialFeediOS/Feed Presentation/el.lproj/Feed.strings b/EssentialFeed/EssentialFeediOS/Feed Presentation/el.lproj/Feed.strings new file mode 100644 index 0000000..7f8910b --- /dev/null +++ b/EssentialFeed/EssentialFeediOS/Feed Presentation/el.lproj/Feed.strings @@ -0,0 +1,9 @@ +/* + Feed.strings + EssentialFeed + + Created by Oluwaseun Adebanwo on 29/11/2023. + +*/ + +"FEED_VIEW_TITLE" = "Το Feed μου"; From cc73b5a60c0381379169555008c1116cd7704ee3 Mon Sep 17 00:00:00 2001 From: Oluwaseun Adebanwo Date: Wed, 29 Nov 2023 19:45:12 +0100 Subject: [PATCH 11/11] Add localization tests to guarantee all localized keys have translations in all supported localizations --- .../EssentialFeed.xcodeproj/project.pbxproj | 12 ++++ .../FeedLocalizationTests.swift | 64 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 EssentialFeed/EssentialFeediOSTests/Feed Presentation/FeedLocalizationTests.swift diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 0d5293f..cdcd5ab 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -79,6 +79,7 @@ EDFBE2842B16C40800EFB793 /* UIImageView+Animations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2832B16C40800EFB793 /* UIImageView+Animations.swift */; }; EDFBE2882B17B52700EFB793 /* FeedViewControllerTests+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2872B17B52700EFB793 /* FeedViewControllerTests+Localization.swift */; }; EDFBE2892B17BA2100EFB793 /* Feed.strings in Resources */ = {isa = PBXBuildFile; fileRef = EDFBE28B2B17BA2100EFB793 /* Feed.strings */; }; + EDFBE2902B17BD1C00EFB793 /* FeedLocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE28F2B17BD1C00EFB793 /* FeedLocalizationTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -209,6 +210,7 @@ EDFBE28A2B17BA2100EFB793 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Feed.strings; sourceTree = ""; }; EDFBE28C2B17BAEF00EFB793 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Feed.strings"; sourceTree = ""; }; EDFBE28D2B17BBDF00EFB793 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Feed.strings; sourceTree = ""; }; + EDFBE28F2B17BD1C00EFB793 /* FeedLocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedLocalizationTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -354,6 +356,7 @@ ED0EDEC62B0FB681004857B7 /* EssentialFeediOSTests */ = { isa = PBXGroup; children = ( + EDFBE28E2B17BC8500EFB793 /* Feed Presentation */, EDFBE2652B1572A800EFB793 /* Feed UI */, ); path = EssentialFeediOSTests; @@ -557,6 +560,14 @@ path = "Feed Presentation"; sourceTree = ""; }; + EDFBE28E2B17BC8500EFB793 /* Feed Presentation */ = { + isa = PBXGroup; + children = ( + EDFBE28F2B17BD1C00EFB793 /* FeedLocalizationTests.swift */, + ); + path = "Feed Presentation"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -877,6 +888,7 @@ files = ( EDFBE2472B1048CF00EFB793 /* XCTTestCase + MemoryLeakTracking.swift in Sources */, EDFBE2642B15713C00EFB793 /* FeedViewControllerTests+Assertions.swift in Sources */, + EDFBE2902B17BD1C00EFB793 /* FeedLocalizationTests.swift in Sources */, EDFBE2582B15005500EFB793 /* UIButton+TestHelpers.swift in Sources */, EDFBE2562B14FFF500EFB793 /* UIRefreshControl+TestHelpers.swift in Sources */, EDFBE25A2B1500F900EFB793 /* UIControl+TestHelpers.swift in Sources */, diff --git a/EssentialFeed/EssentialFeediOSTests/Feed Presentation/FeedLocalizationTests.swift b/EssentialFeed/EssentialFeediOSTests/Feed Presentation/FeedLocalizationTests.swift new file mode 100644 index 0000000..f91fd01 --- /dev/null +++ b/EssentialFeed/EssentialFeediOSTests/Feed Presentation/FeedLocalizationTests.swift @@ -0,0 +1,64 @@ +// +// FeedLocalizationTests.swift +// EssentialFeediOSTests +// +// Created by Oluwaseun Adebanwo on 29/11/2023. +// + +import XCTest +@testable import EssentialFeediOS + +final class FeedLocalizationTests: XCTestCase { + + func test_localizedStrings_haveKeysAndValuesForAllSupportedLocalizations() { + let table = "Feed" + let presentationBundle = Bundle(for: FeedPresenter.self) + let localizationBundles = allLocalizationBundles(in: presentationBundle) + let localizedStringKeys = allLocalizedStringKeys(in: localizationBundles, table: table) + + localizationBundles.forEach { (bundle, localization) in + localizedStringKeys.forEach { key in + let localizedString = bundle.localizedString(forKey: key, value: nil, table: table) + + if localizedString == key { + let language = Locale.current.localizedString(forLanguageCode: localization) ?? "" + + XCTFail("Missing \(language) (\(localization)) localized string for key: '\(key)' in table: '\(table)'") + } + } + } + } + + // MARK: - Helpers + + private typealias LocalizedBundle = (bundle: Bundle, localization: String) + + private func allLocalizationBundles(in bundle: Bundle, file: StaticString = #file, line: UInt = #line) -> [LocalizedBundle] { + return bundle.localizations.compactMap { localization in + guard + let path = bundle.path(forResource: localization, ofType: "lproj"), + let localizedBundle = Bundle(path: path) + else { + XCTFail("Couldn't find bundle for localization: \(localization)", file: file, line: line) + return nil + } + + return (localizedBundle, localization) + } + } + + private func allLocalizedStringKeys(in bundles: [LocalizedBundle], table: String, file: StaticString = #file, line: UInt = #line) -> Set { + return bundles.reduce([]) { (acc, current) in + guard + let path = current.bundle.path(forResource: table, ofType: "strings"), + let strings = NSDictionary(contentsOfFile: path), + let keys = strings.allKeys as? [String] + else { + XCTFail("Couldn't load localized strings for localization: \(current.localization)", file: file, line: line) + return acc + } + + return acc.union(Set(keys)) + } + } +}