Skip to content

Commit

Permalink
Merge pull request #29 from seunbanwo/feature/image-feed/ui/localization
Browse files Browse the repository at this point in the history
Localization - English, Portuguese (pt-BR), and Greek
  • Loading branch information
seunbanwo authored Nov 29, 2023
2 parents ce8f5c1 + cc73b5a commit c44669d
Show file tree
Hide file tree
Showing 17 changed files with 191 additions and 29 deletions.
67 changes: 45 additions & 22 deletions EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -77,6 +77,9 @@
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 */; };
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 */
Expand Down Expand Up @@ -179,7 +182,7 @@
EDF8FE4D2AE2389D00605C5C /* FeedCacheTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCacheTestHelpers.swift; sourceTree = "<group>"; };
EDF8FE4F2AE2390B00605C5C /* SharedTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedTestHelpers.swift; sourceTree = "<group>"; };
EDF8FE512AE2470300605C5C /* FeedCachePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCachePolicy.swift; sourceTree = "<group>"; };
EDFBE23F2B10409C00EFB793 /* FeedViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewControllerTests.swift; sourceTree = "<group>"; };
EDFBE23F2B10409C00EFB793 /* FeedUIIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedUIIntegrationTests.swift; sourceTree = "<group>"; };
EDFBE2482B10534300EFB793 /* FeedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = "<group>"; };
EDFBE24A2B107C8900EFB793 /* FeedImageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImageCell.swift; sourceTree = "<group>"; };
EDFBE24C2B13186200EFB793 /* UIView+Shimmering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Shimmering.swift"; sourceTree = "<group>"; };
Expand All @@ -203,6 +206,11 @@
EDFBE27F2B16B44E00EFB793 /* Feed.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Feed.xcassets; sourceTree = "<group>"; };
EDFBE2812B16C2EB00EFB793 /* UITableView+Dequeueing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Dequeueing.swift"; sourceTree = "<group>"; };
EDFBE2832B16C40800EFB793 /* UIImageView+Animations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Animations.swift"; sourceTree = "<group>"; };
EDFBE2872B17B52700EFB793 /* FeedViewControllerTests+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedViewControllerTests+Localization.swift"; sourceTree = "<group>"; };
EDFBE28A2B17BA2100EFB793 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Feed.strings; sourceTree = "<group>"; };
EDFBE28C2B17BAEF00EFB793 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Feed.strings"; sourceTree = "<group>"; };
EDFBE28D2B17BBDF00EFB793 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Feed.strings; sourceTree = "<group>"; };
EDFBE28F2B17BD1C00EFB793 /* FeedLocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedLocalizationTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -348,7 +356,7 @@
ED0EDEC62B0FB681004857B7 /* EssentialFeediOSTests */ = {
isa = PBXGroup;
children = (
EDFBE2522B14FE9500EFB793 /* Helpers */,
EDFBE28E2B17BC8500EFB793 /* Feed Presentation */,
EDFBE2652B1572A800EFB793 /* Feed UI */,
);
path = EssentialFeediOSTests;
Expand Down Expand Up @@ -506,41 +514,27 @@
path = Views;
sourceTree = "<group>";
};
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 = "<group>";
};
EDFBE25B2B15036400EFB793 /* Controllers */ = {
isa = PBXGroup;
children = (
EDFBE25E2B150A5000EFB793 /* Helpers */,
EDFBE23F2B10409C00EFB793 /* FeedViewControllerTests.swift */,
);
path = Controllers;
sourceTree = "<group>";
};
EDFBE25E2B150A5000EFB793 /* Helpers */ = {
isa = PBXGroup;
children = (
EDFBE25C2B15051100EFB793 /* FeedViewController+TestHelpers.swift */,
EDFBE25F2B150A7200EFB793 /* FeedImageCell+TestHelpers.swift */,
EDFBE2612B150DC600EFB793 /* FeedViewControllerTests+LoaderSpy.swift */,
EDFBE2632B15713C00EFB793 /* FeedViewControllerTests+Assertions.swift */,
EDFBE2872B17B52700EFB793 /* FeedViewControllerTests+Localization.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
EDFBE2652B1572A800EFB793 /* Feed UI */ = {
isa = PBXGroup;
children = (
EDFBE25B2B15036400EFB793 /* Controllers */,
EDFBE25E2B150A5000EFB793 /* Helpers */,
EDFBE23F2B10409C00EFB793 /* FeedUIIntegrationTests.swift */,
);
path = "Feed UI";
sourceTree = "<group>";
Expand All @@ -561,6 +555,15 @@
EDFBE2772B15C8EB00EFB793 /* FeedImagePresenter.swift */,
EDFBE2792B15CC5700EFB793 /* FeedViewModel.swift */,
EDFBE27B2B15CDC600EFB793 /* FeedLoadingViewModel.swift */,
EDFBE28B2B17BA2100EFB793 /* Feed.strings */,
);
path = "Feed Presentation";
sourceTree = "<group>";
};
EDFBE28E2B17BC8500EFB793 /* Feed Presentation */ = {
isa = PBXGroup;
children = (
EDFBE28F2B17BD1C00EFB793 /* FeedLocalizationTests.swift */,
);
path = "Feed Presentation";
sourceTree = "<group>";
Expand Down Expand Up @@ -735,6 +738,8 @@
knownRegions = (
en,
Base,
"pt-BR",
el,
);
mainGroup = 1438188C29A135C0005B41C9;
productRefGroup = 1438189729A135C0005B41C9 /* Products */;
Expand Down Expand Up @@ -779,6 +784,7 @@
files = (
EDFBE27E2B16B37E00EFB793 /* Feed.storyboard in Resources */,
EDFBE2802B16B44E00EFB793 /* Feed.xcassets in Resources */,
EDFBE2892B17BA2100EFB793 /* Feed.strings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -882,14 +888,16 @@
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 */,
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 */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -933,11 +941,25 @@
};
/* End PBXTargetDependency section */

/* Begin PBXVariantGroup section */
EDFBE28B2B17BA2100EFB793 /* Feed.strings */ = {
isa = PBXVariantGroup;
children = (
EDFBE28A2B17BA2100EFB793 /* en */,
EDFBE28C2B17BAEF00EFB793 /* pt-BR */,
EDFBE28D2B17BBDF00EFB793 /* el */,
);
name = Feed.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */

/* Begin XCBuildConfiguration section */
143818A829A135C0005B41C9 /* Debug */ = {
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";
Expand Down Expand Up @@ -1004,6 +1026,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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Oluwaseun Adebanwo on 28/11/2023.
//

import Foundation
import EssentialFeed

protocol FeedLoadingView {
Expand All @@ -24,6 +25,13 @@ final class FeedPresenter {
self.loadingView = loadingView
}

static var title: String {
return NSLocalizedString("FEED_VIEW_TITLE",
tableName: "Feed",
bundle: Bundle(for: FeedPresenter.self),
comment: "Title for the feed view")
}

func didStartLoadingFeed() {
loadingView.display(FeedLoadingViewModel(isLoading: true))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Feed.strings
EssentialFeed

Created by Oluwaseun Adebanwo on 29/11/2023.

*/

"FEED_VIEW_TITLE" = "Το Feed μου";
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Feed.strings
EssentialFeed

Created by Oluwaseun Adebanwo on 29/11/2023.

*/

"FEED_VIEW_TITLE" = "My Feed";
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Feed.strings
EssentialFeed

Created by Oluwaseun Adebanwo on 29/11/2023.

*/

"FEED_VIEW_TITLE" = "Meu Feed";
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +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
let feedController = FeedViewController.makeWith(
delegate: presentationAdapter,
title: FeedPresenter.title)

presentationAdapter.presenter = FeedPresenter(
feedView: FeedViewAdapter(controller: feedController, imageLoader: imageLoader),
Expand All @@ -28,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<T: AnyObject> {
private weak var object: T?

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> {
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))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ import UIKit
import EssentialFeed
import EssentialFeediOS

final class FeedViewControllerTests: XCTestCase {
final class FeedUIIntegrationTests: XCTestCase {

func test_feedView_hasTitle() {
let (sut, _) = makeSUT()

sut.loadViewIfNeeded()

XCTAssertEqual(sut.title, localized("FEED_VIEW_TITLE"))
}

func test_loadFeedActions_requestFeedFromLoader() {
let (sut, loader) = makeSUT()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
import EssentialFeed
import EssentialFeediOS

extension FeedViewControllerTests {
extension FeedUIIntegrationTests {

class LoaderSpy: FeedLoader, FeedImageDataLoader {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// FeedViewControllerTests+Localization.swift
// EssentialFeediOSTests
//
// Created by Oluwaseun Adebanwo on 29/11/2023.
//

import Foundation
import XCTest
import EssentialFeediOS

extension FeedUIIntegrationTests {
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
}
}

0 comments on commit c44669d

Please sign in to comment.