diff --git a/Shared/Coordinators/CustomizeSettingsCoordinator.swift b/Shared/Coordinators/CustomizeSettingsCoordinator.swift index 9c82c810..259bd033 100644 --- a/Shared/Coordinators/CustomizeSettingsCoordinator.swift +++ b/Shared/Coordinators/CustomizeSettingsCoordinator.swift @@ -10,6 +10,7 @@ import Stinsen import SwiftUI final class CustomizeSettingsCoordinator: NavigationCoordinatable { + let stack = NavigationStack(initial: \CustomizeSettingsCoordinator.start) @Root @@ -17,6 +18,8 @@ final class CustomizeSettingsCoordinator: NavigationCoordinatable { @Route(.modal) var indicatorSettings = makeIndicatorSettings + @Route(.modal) + var listColumnSettings = makeListColumnSettings func makeIndicatorSettings() -> NavigationViewCoordinator { NavigationViewCoordinator { @@ -24,6 +27,10 @@ final class CustomizeSettingsCoordinator: NavigationCoordinatable { } } + func makeListColumnSettings(selection: Binding) -> some View { + ListColumnsPickerView(selection: selection) + } + @ViewBuilder func makeStart() -> some View { CustomizeViewsSettings() diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index c5d36cf4..d27716e8 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -82,8 +82,6 @@ final class SettingsCoordinator: NavigationCoordinatable { @Route(.modal) var experimentalSettings = makeExperimentalSettings @Route(.modal) - var indicatorSettings = makeIndicatorSettings - @Route(.modal) var log = makeLog @Route(.modal) var serverDetail = makeServerDetail diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 4433c2d7..6098c600 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -168,6 +168,8 @@ internal enum L10n { internal static let collections = L10n.tr("Localizable", "collections", fallback: "Collections") /// Color internal static let color = L10n.tr("Localizable", "color", fallback: "Color") + /// Columns + internal static let columns = L10n.tr("Localizable", "columns", fallback: "Columns") /// Coming soon internal static let comingSoon = L10n.tr("Localizable", "comingSoon", fallback: "Coming soon") /// Compact diff --git a/Swiftfin tvOS/Components/StepperView.swift b/Swiftfin tvOS/Components/StepperView.swift index e193243b..945b3892 100644 --- a/Swiftfin tvOS/Components/StepperView.swift +++ b/Swiftfin tvOS/Components/StepperView.swift @@ -13,6 +13,11 @@ struct StepperView: View { @Binding private var value: Value + @State + private var updatedValue: Value + @Environment(\.presentationMode) + private var presentationMode + private var title: String private var description: String? private var range: ClosedRange @@ -36,7 +41,7 @@ struct StepperView: View { } .frame(maxHeight: .infinity) - formatter(value).text + formatter(updatedValue).text .font(.title) .frame(height: 250) @@ -44,8 +49,10 @@ struct StepperView: View { HStack { Button { - guard value >= range.lowerBound else { return } - value = value.advanced(by: -step) + if updatedValue > range.lowerBound { + updatedValue = max(updatedValue.advanced(by: -step), range.lowerBound) + value = updatedValue + } } label: { Image(systemName: "minus") .font(.title2.weight(.bold)) @@ -54,8 +61,10 @@ struct StepperView: View { .buttonStyle(.card) Button { - guard value <= range.upperBound else { return } - value = value.advanced(by: step) + if updatedValue < range.upperBound { + updatedValue = min(updatedValue.advanced(by: step), range.upperBound) + value = updatedValue + } } label: { Image(systemName: "plus") .font(.title2.weight(.bold)) @@ -64,10 +73,9 @@ struct StepperView: View { .buttonStyle(.card) } - Button { + Button(L10n.close) { onCloseSelected() - } label: { - Text("Close") + presentationMode.wrappedValue.dismiss() } Spacer() @@ -86,15 +94,14 @@ extension StepperView { range: ClosedRange, step: Value.Stride ) { - self.init( - value: value, - title: title, - description: description, - range: range, - step: step, - formatter: { $0.description }, - onCloseSelected: {} - ) + self._value = value + self._updatedValue = State(initialValue: value.wrappedValue) + self.title = title + self.description = description + self.range = range + self.step = step + self.formatter = { $0.description } + self.onCloseSelected = {} } func valueFormatter(_ formatter: @escaping (Value) -> String) -> Self { diff --git a/Swiftfin tvOS/Views/PagingLibraryView/Components/LibraryRow.swift b/Swiftfin tvOS/Views/PagingLibraryView/Components/LibraryRow.swift new file mode 100644 index 00000000..34939e5a --- /dev/null +++ b/Swiftfin tvOS/Views/PagingLibraryView/Components/LibraryRow.swift @@ -0,0 +1,165 @@ +// +// 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 Defaults +import JellyfinAPI +import SwiftUI + +extension PagingLibraryView { + + struct LibraryRow: View { + + @State + private var contentWidth: CGFloat = 0 + @State + private var focusedItem: Element? + + @FocusState + private var isFocused: Bool + + private let item: Element + private var action: () -> Void + private var contextMenu: () -> any View + private let posterType: PosterDisplayType + + private var onFocusChanged: ((Bool) -> Void)? + + private func imageView(from element: Element) -> ImageView { + switch posterType { + case .landscape: + ImageView(element.landscapeImageSources(maxWidth: 110)) + case .portrait: + ImageView(element.portraitImageSources(maxWidth: 60)) + } + } + + @ViewBuilder + private func itemAccessoryView(item: BaseItemDto) -> some View { + DotHStack { + if item.type == .episode, let seasonEpisodeLocator = item.seasonEpisodeLabel { + Text(seasonEpisodeLocator) + } else if let premiereYear = item.premiereDateYear { + Text(premiereYear) + } + + if let runtime = item.runTimeLabel { + Text(runtime) + } + + if let officialRating = item.officialRating { + Text(officialRating) + } + } + } + + @ViewBuilder + private func personAccessoryView(person: BaseItemPerson) -> some View { + if let subtitle = person.subtitle { + Text(subtitle) + } + } + + @ViewBuilder + private var accessoryView: some View { + switch item { + case let element as BaseItemDto: + itemAccessoryView(item: element) + case let element as BaseItemPerson: + personAccessoryView(person: element) + default: + AssertionFailureView("Used an unexpected type within a `PagingLibaryView`?") + } + } + + @ViewBuilder + private var rowContent: some View { + HStack { + VStack(alignment: .leading, spacing: 5) { + Text(item.displayTitle) + .font(posterType == .landscape ? .subheadline : .callout) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(2) + .multilineTextAlignment(.leading) + + accessoryView + .font(.caption) + .foregroundColor(Color(UIColor.lightGray)) + } + Spacer() + } + } + + @ViewBuilder + private var rowLeading: some View { + ZStack { + Color.clear + + imageView(from: item) + .failure { + SystemImageContentView(systemName: item.systemImage) + } + } + .posterStyle(posterType) + .frame(width: posterType == .landscape ? 110 : 60) + .posterShadow() + .padding(.vertical, 8) + } + + // MARK: body + + var body: some View { + ListRow(insets: .init(horizontal: EdgeInsets.edgePadding)) { + rowLeading + } content: { + rowContent + } + .onSelect(perform: action) + .contextMenu(menuItems: { + contextMenu() + .eraseToAnyView() + }) + .posterShadow() + .ifLet(onFocusChanged) { view, onFocusChanged in + view + .focused($isFocused) + .onChange(of: isFocused) { _, newValue in + onFocusChanged(newValue) + } + } + } + } +} + +extension PagingLibraryView.LibraryRow { + + init(item: Element, posterType: PosterDisplayType) { + self.init( + item: item, + action: {}, + contextMenu: { EmptyView() }, + posterType: posterType, + onFocusChanged: nil + ) + } +} + +extension PagingLibraryView.LibraryRow { + + func onSelect(perform action: @escaping () -> Void) -> Self { + copy(modifying: \.action, with: action) + } + + func contextMenu(@ViewBuilder perform content: @escaping () -> any View) -> Self { + copy(modifying: \.contextMenu, with: content) + } + + func onFocusChanged(perform action: @escaping (Bool) -> Void) -> Self { + copy(modifying: \.onFocusChanged, with: action) + } +} diff --git a/Swiftfin tvOS/Views/PagingLibraryView/Components/ListRow.swift b/Swiftfin tvOS/Views/PagingLibraryView/Components/ListRow.swift new file mode 100644 index 00000000..75348c65 --- /dev/null +++ b/Swiftfin tvOS/Views/PagingLibraryView/Components/ListRow.swift @@ -0,0 +1,78 @@ +// +// 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 SwiftUI + +// TODO: come up with better name along with `ListRowButton` + +// Meant to be used when making a custom list without `List` or `Form` +struct ListRow: View { + + @State + private var contentSize: CGSize = .zero + + private let leading: () -> Leading + private let content: () -> Content + private var action: () -> Void + private var insets: EdgeInsets + private var isSeparatorVisible: Bool + + var body: some View { + ZStack(alignment: .bottomTrailing) { + + Button { + action() + } label: { + HStack(alignment: .center, spacing: EdgeInsets.edgePadding) { + + leading() + + content() + .frame(maxHeight: .infinity) + .trackingSize($contentSize) + } + .padding(.top, insets.top) + .padding(.bottom, insets.bottom) + .padding(.leading, insets.leading) + .padding(.trailing, insets.trailing) + } + .foregroundStyle(.primary, .secondary) + .buttonStyle(.plain) + + Color.secondarySystemFill + .frame(width: contentSize.width, height: 1) + .padding(.trailing, insets.trailing) + .visible(isSeparatorVisible) + } + } +} + +extension ListRow { + + init( + insets: EdgeInsets = .zero, + @ViewBuilder leading: @escaping () -> Leading, + @ViewBuilder content: @escaping () -> Content + ) { + self.init( + leading: leading, + content: content, + action: {}, + insets: insets, + isSeparatorVisible: true + ) + } + + func isSeparatorVisible(_ isVisible: Bool) -> Self { + copy(modifying: \.isSeparatorVisible, with: isVisible) + } + + func onSelect(perform action: @escaping () -> Void) -> Self { + copy(modifying: \.action, with: action) + } +} diff --git a/Swiftfin tvOS/Components/PagingLibraryView.swift b/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift similarity index 85% rename from Swiftfin tvOS/Components/PagingLibraryView.swift rename to Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift index 68021e0f..19f4f4c2 100644 --- a/Swiftfin tvOS/Components/PagingLibraryView.swift +++ b/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift @@ -49,11 +49,13 @@ struct PagingLibraryView: View { let initialPosterType = Defaults[.Customization.Library.posterType] let initialViewType = Defaults[.Customization.Library.displayType] + let listColumnCount = Defaults[.Customization.Library.listColumnCount] self._layout = State( initialValue: Self.makeLayout( posterType: initialPosterType, - viewType: initialViewType + displayType: initialViewType, + listColumnCount: listColumnCount ) ) } @@ -90,15 +92,16 @@ struct PagingLibraryView: View { private static func makeLayout( posterType: PosterDisplayType, - viewType: LibraryDisplayType + displayType: LibraryDisplayType, + listColumnCount: Int ) -> CollectionVGridLayout { - switch (posterType, viewType) { + switch (posterType, displayType) { case (.landscape, .grid): - .columns(5) + return .columns(5, insets: .init(50), itemSpacing: 50, lineSpacing: 50) case (.portrait, .grid): - .columns(7, insets: .init(50), itemSpacing: 50, lineSpacing: 50) + return .columns(7, insets: .init(50), itemSpacing: 50, lineSpacing: 50) case (_, .list): - .columns(1) + return .columns(listColumnCount, insets: .init(50), itemSpacing: 50, lineSpacing: 50) } } @@ -140,8 +143,17 @@ struct PagingLibraryView: View { } } - private func listItemView(item: Element) -> some View { - Button(item.displayTitle) + @ViewBuilder + private func listItemView(item: Element, posterType: PosterDisplayType) -> some View { + LibraryRow(item: item, posterType: posterType) + .onFocusChanged { newValue in + if newValue { + focusedItem = item + } + } + .onSelect { + onSelect(item) + } } @ViewBuilder @@ -156,7 +168,7 @@ struct PagingLibraryView: View { case (.portrait, .grid): portraitGridItemView(item: item) case (_, .list): - listItemView(item: item) + listItemView(item: item, posterType: posterType) } } .onReachedBottomEdge(offset: .rows(3)) { diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/ListColumnsPickerView.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/ListColumnsPickerView.swift new file mode 100644 index 00000000..9d6caa0f --- /dev/null +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/ListColumnsPickerView.swift @@ -0,0 +1,25 @@ +// +// 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 Defaults +import SwiftUI + +struct ListColumnsPickerView: View { + + @Binding + var selection: Int + + var body: some View { + StepperView( + title: "Columns", + value: $selection, + range: 1 ... 3, + step: 1 + ) + } +} diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift index 17d22628..df65220e 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift @@ -37,6 +37,12 @@ struct CustomizeViewsSettings: View { private var libraryRandomImage @Default(.Customization.Library.showFavorites) private var showFavorites + @Default(.Customization.Library.displayType) + private var libraryDisplayType + @Default(.Customization.Library.posterType) + private var libraryPosterType + @Default(.Customization.Library.listColumnCount) + private var listColumnCount @EnvironmentObject private var router: CustomizeSettingsCoordinator.Router @@ -76,8 +82,6 @@ struct CustomizeViewsSettings: View { InlineEnumToggle(title: L10n.recommended, selection: $similarPosterType) InlineEnumToggle(title: L10n.search, selection: $searchPosterType) - - InlineEnumToggle(title: L10n.library, selection: $libraryViewType) } Section(L10n.library) { @@ -87,6 +91,18 @@ struct CustomizeViewsSettings: View { Toggle(L10n.randomImage, isOn: $libraryRandomImage) Toggle(L10n.showFavorites, isOn: $showFavorites) + + InlineEnumToggle(title: L10n.posters, selection: $libraryPosterType) + InlineEnumToggle(title: L10n.library, selection: $libraryDisplayType) + if libraryDisplayType == .list { + ChevronButton( + L10n.columns, + subtitle: listColumnCount.description + ) + .onSelect { + router.route(to: \.listColumnSettings, $listColumnCount) + } + } } HomeSection() diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 12401b08..6dce97af 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -73,6 +73,9 @@ 4ECDAA9E2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; }; 4ECDAA9F2C920A8E0030F2F5 /* TranscodeReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */; }; 4EE141692C8BABDF0045B661 /* ProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ProgressSection.swift */; }; + 4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; }; + 4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; }; + 4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */; }; @@ -1061,6 +1064,9 @@ 4EC6C16A2C92999800FC904B /* TranscodeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeSection.swift; sourceTree = ""; }; 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeReason.swift; sourceTree = ""; }; 4EE141682C8BABDF0045B661 /* ProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressSection.swift; sourceTree = ""; }; + 4EF18B252CB9934700343666 /* LibraryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = ""; }; + 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListColumnsPickerView.swift; sourceTree = ""; }; + 4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = ""; }; 531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 531AC8BE26750DE20091C7EB /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; @@ -1905,6 +1911,7 @@ 4E699BBD2CB34746007CBD5D /* Components */ = { isa = PBXGroup; children = ( + 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */, 4E699BBE2CB3474C007CBD5D /* Sections */, ); path = Components; @@ -1990,6 +1997,24 @@ path = Components; sourceTree = ""; }; + 4EF18B232CB9932F00343666 /* PagingLibraryView */ = { + isa = PBXGroup; + children = ( + 4EF18B242CB9933700343666 /* Components */, + E111D8F928D0400900400001 /* PagingLibraryView.swift */, + ); + path = PagingLibraryView; + sourceTree = ""; + }; + 4EF18B242CB9933700343666 /* Components */ = { + isa = PBXGroup; + children = ( + 4EF18B252CB9934700343666 /* LibraryRow.swift */, + 4EF18B292CB993AD00343666 /* ListRow.swift */, + ); + path = Components; + sourceTree = ""; + }; 5310694F2684E7EE00CFFDBA /* VideoPlayer */ = { isa = PBXGroup; children = ( @@ -2181,7 +2206,6 @@ E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */, E10E842B29A589860064EA49 /* NonePosterButton.swift */, 4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */, - E111D8F928D0400900400001 /* PagingLibraryView.swift */, E1C92617288756BD002A7A66 /* PosterButton.swift */, E1C92619288756BD002A7A66 /* PosterHStack.swift */, E12CC1C428D12D9B00678D5D /* SeeAllPosterButton.swift */, @@ -2923,6 +2947,7 @@ E10231572BCF8AF8009D71FC /* ProgramsView */, E10B1E8C2BD7708900A92EAF /* QuickConnectView.swift */, E1E1643928BAC2EF00323B0A /* SearchView.swift */, + 4EF18B232CB9932F00343666 /* PagingLibraryView */, E193D54A271941D300900D82 /* SelectServerView.swift */, E164A8122BE4995200A54B18 /* SelectUserView */, E193D54F2719430400900D82 /* ServerDetailView.swift */, @@ -4266,6 +4291,7 @@ E190704C2C858CEB0004600E /* VideoPlayerType+Shared.swift in Sources */, E152107D2947ACA000375CC2 /* InvertedLightAppIcon.swift in Sources */, E1549663296CA2EF00C4EF88 /* UserSession.swift in Sources */, + 4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, E145EB232BDCCA43003BF6F3 /* BulletedList.swift in Sources */, E104DC972B9E7E29008F506D /* AssertionFailureView.swift in Sources */, @@ -4476,6 +4502,7 @@ E1575E66293E77B5001665B1 /* Poster.swift in Sources */, E18E021F2887492B0022598C /* SystemImageContentView.swift in Sources */, E19D41B42BF2C0020082B8B2 /* StoredValues+Temp.swift in Sources */, + 4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */, E11BDF7B2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */, 4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */, E1575E8C293E7B1E001665B1 /* UIScreen.swift in Sources */, @@ -4488,6 +4515,7 @@ E1DABAFA2A270E62008AC34A /* OverviewCard.swift in Sources */, E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */, 4E2AC4CC2C6C494E00DD600D /* VideoCodec.swift in Sources */, + 4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */, E129428628F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 4d5157e5..d2d1b6b1 100644 Binary files a/Translations/en.lproj/Localizable.strings and b/Translations/en.lproj/Localizable.strings differ