diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 556a8a0b..1658f8ce 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; }; 3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; }; 3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; }; + 3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; }; 3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; }; 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; }; 3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; }; @@ -282,6 +283,8 @@ 3A3040FC29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/InfoPlist.strings"; sourceTree = ""; }; 3A3040FD29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = ""; }; 3A3040FE29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/Localizable.strings"; sourceTree = ""; }; + 3A3040FF29AB02D1008A0F29 /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-US"; path = "en-US.lproj/Localizable.strings"; sourceTree = ""; }; + 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupViewTests.swift; sourceTree = ""; }; 3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = ""; }; 3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = ""; }; 3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1037,6 +1040,7 @@ 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */, 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */, 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */, + 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */, ); path = damusTests; sourceTree = ""; @@ -1498,6 +1502,7 @@ buildActionMask = 2147483647; files = ( 3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */, + 3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */, 3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */, DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */, 3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */, @@ -1612,6 +1617,7 @@ 3A827A19299FC69D00C4D171 /* ru */, 3A3040FA29A91EFC008A0F29 /* zh-HK */, 3A3040FE29A91F31008A0F29 /* zh-TW */, + 3A3040FF29AB02D1008A0F29 /* en-US */, ); name = Localizable.strings; sourceTree = ""; diff --git a/damus/Util/LocalizationUtil.swift b/damus/Util/LocalizationUtil.swift index 3abb3e9a..7e2aa938 100644 --- a/damus/Util/LocalizationUtil.swift +++ b/damus/Util/LocalizationUtil.swift @@ -7,7 +7,11 @@ import Foundation -func bundleForLocale(locale: Locale) -> Bundle { - let path = Bundle.main.path(forResource: locale.identifier, ofType: "lproj") +func bundleForLocale(locale: Locale?) -> Bundle { + if locale == nil { + return Bundle.main + } + + let path = Bundle.main.path(forResource: locale!.identifier, ofType: "lproj") return path != nil ? (Bundle(path: path!) ?? Bundle.main) : Bundle.main } diff --git a/damus/Views/Notifications/EventGroupView.swift b/damus/Views/Notifications/EventGroupView.swift index ee36dd1e..d2b466a3 100644 --- a/damus/Views/Notifications/EventGroupView.swift +++ b/damus/Views/Notifications/EventGroupView.swift @@ -46,62 +46,86 @@ func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo { return .tagged_in } -func determine_reacting_to_text(_ r: ReactingTo) -> String { - switch r { - case .tagged_in: - return "a post you were tagged in" - case .your_post: - return "your post" - case .your_profile: - return "your profile" - } -} - func event_author_name(profiles: Profiles, _ ev: NostrEvent) -> String { let alice_pk = ev.pubkey let alice_prof = profiles.lookup(id: alice_pk) return Profile.displayName(profile: alice_prof, pubkey: alice_pk) } -func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?) -> String { +/** + Returns a notification string describing user actions in response to an event group type. + + The localization keys read by this function are the following (although some keys may not actually be used in practice): + + "??" - returned when there are no events associated with the specified event group type. + + "reacted_tagged_in_1" - returned when 1 reaction occurred to a post that the current user was tagged in + "reacted_tagged_in_2" - returned when 2 reactions occurred to a post that the current user was tagged in + "reacted_tagged_in_3" - returned when 3 or more reactions occurred to a post that the current user was tagged in + "reacted_your_post_1" - returned when 1 reaction occurred to the current user's post + "reacted_your_post_2" - returned when 2 reactions occurred to the current user's post + "reacted_your_post_3" - returned when 3 or more reactions occurred to the current user's post + "reacted_your_profile_1" - returned when 1 reaction occurred to the current user's profile + "reacted_your_profile_2" - returned when 2 reactions occurred to the current user's profile + "reacted_your_profile_3" - returned when 3 or more reactions occurred to the current user's profile + + "reposted_tagged_in_1" - returned when 1 repost occurred to a post that the current user was tagged in + "reposted_tagged_in_2" - returned when 2 reposts occurred to a post that the current user was tagged in + "reposted_tagged_in_3" - returned when 3 or more reposts occurred to a post that the current user was tagged in + "reposted_your_post_1" - returned when 1 repost occurred to the current user's post + "reposted_your_post_2" - returned when 2 reposts occurred to the current user's post + "reposted_your_post_3" - returned when 3 or more reposts occurred to the current user's post + "reposted_your_profile_1" - returned when 1 repost occurred to the current user's profile + "reposted_your_profile_2" - returned when 2 reposts occurred to the current user's profile + "reposted_your_profile_3" - returned when 3 or more reposts occurred to the current user's profile + + "zapped_tagged_in_1" - returned when 1 zap occurred to a post that the current user was tagged in + "zapped_tagged_in_2" - returned when 2 zaps occurred to a post that the current user was tagged in + "zapped_tagged_in_3" - returned when 3 or more zaps occurred to a post that the current user was tagged in + "zapped_your_post_1" - returned when 1 zap occurred to the current user's post + "zapped_your_post_2" - returned when 2 zaps occurred to the current user's post + "zapped_your_post_3" - returned when 3 or more zaps occurred to the current user's post + "zapped_your_profile_1" - returned when 1 zap occurred to the current user's profile + "zapped_your_profile_2" - returned when 2 zaps occurred to the current user's profile + "zapped_your_profile_3" - returned when 3 or more zaps occurred to the current user's profile + */ +func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?, locale: Locale? = nil) -> String { let verb = reacting_to_verb(group: group) - let reacting_to = determine_reacting_to(our_pubkey: our_pubkey, ev: ev) - let target = determine_reacting_to_text(reacting_to) - - if group.events.count == 1 { + let localization_key = "\(verb)_\(reacting_to)_\(min(group.events.count, 3))" + let bundle = bundleForLocale(locale: locale) + + switch group.events.count { + case 0: + return NSLocalizedString("??", comment: "") + case 1: let ev = group.events.first! let profile = profiles.lookup(id: ev.pubkey) let display_name = Profile.displayName(profile: profile, pubkey: ev.pubkey) - return String(format: "%@ is %@ %@", display_name, verb, target) - } - - if group.events.count == 2 { + + return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, display_name) + case 2: let alice_name = event_author_name(profiles: profiles, group.events[0]) let bob_name = event_author_name(profiles: profiles, group.events[1]) - - return String(format: "%@ and %@ are %@ %@", alice_name, bob_name, verb, target) - } - - if group.events.count > 2 { + + return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, alice_name, bob_name) + default: let alice_name = event_author_name(profiles: profiles, group.events.first!) let count = group.events.count - 1 - - return String(format: "%@ and %d other people are %@ %@", alice_name, count, verb, target) + + return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, count, alice_name) } - - return "??" } func reacting_to_verb(group: EventGroupType) -> String { switch group { case .reaction: - return "reacting" + return "reacted" case .repost: - return "reposting" + return "reposted" case .zap: fallthrough case .profile_zap: - return "zapping" + return "zapped" } } @@ -111,7 +135,7 @@ struct EventGroupView: View { let group: EventGroupType var GroupDescription: some View { - Text(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event)) + Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event))") } func ZapIcon(_ zapgrp: ZapGroup) -> some View { @@ -168,8 +192,9 @@ struct EventGroupView: View { } let test_encoded_post = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}" -let test_repost = NostrEvent(id: "", content: test_encoded_post, pubkey: "", kind: 6, tags: [], createdAt: 1) -let test_reposts = [test_repost, test_repost] +let test_repost_1 = NostrEvent(id: "", content: test_encoded_post, pubkey: "pk1", kind: 6, tags: [], createdAt: 1) +let test_repost_2 = NostrEvent(id: "", content: test_encoded_post, pubkey: "pk2", kind: 6, tags: [], createdAt: 1) +let test_reposts = [test_repost_1, test_repost_2] let test_event_group = EventGroup(events: test_reposts) struct EventGroupView_Previews: PreviewProvider { diff --git a/damus/en-US.xcloc/Localized Contents/en-US.xliff b/damus/en-US.xcloc/Localized Contents/en-US.xliff index 0ccc8a9b..ba87f69d 100644 --- a/damus/en-US.xcloc/Localized Contents/en-US.xliff +++ b/damus/en-US.xcloc/Localized Contents/en-US.xliff @@ -83,6 +83,11 @@ Sentence composed of 2 variables to describe how many people are following a use '%@' is an invalid NIP-05 identifier. It should look like an email. Description of why the nip05 identifier is invalid. + + ?? + ?? + No comment provided by engineer. + API Key (optional) API Key (optional) @@ -1270,6 +1275,66 @@ Label for filter for seeing your posts and replies (instead of only your posts). optional Label indicating that a form input is optional. + + %@ reacted to a post you were tagged in + %@ reacted to a post you were tagged in + Notification that a user reacted to a post that the current user was tagged in + + + %@ and %@ reacted to a post you were tagged in + %@ and %@ reacted to a post you were tagged in + Notification that 2 users reacted to a post that the current user was tagged in + + + %@ reacted to your post + %@ reacted to your post + Notification that a user reacted to the current user's post + + + %@ and %@ reacted to your post + %@ and %@ reacted to your post + Notification that 2 users reacted to the current user's profile + + + %@ reacted to your profile + %@ reacted to your profile + Notification that a user reacted to the current user's profile + + + %@ and %@ reacted to your profile + %@ and %@ reacted to your profile + Notification that 2 users reacted to the current user's profile + + + %@ reposted a post you were tagged in + %@ reposted a post you were tagged in + Notification that a user reposted a post that the current user was tagged in + + + %@ and %@ reposted a post you were tagged in + %@ and %@ reposted a post you were tagged in + Notification that 2 users reposted a post that the current user was tagged in + + + %@ reposted your post + %@ reposted your post + Notification that a user reposted the current user's post + + + %@ and %@ reposted your post + %@ and %@ reposted your post + Notification that 2 users reposted the current user's post + + + %@ reposted your profile + %@ reposted your profile + Notification that a user reposted the current user's profile + + + %@ and %@ reposted your profile + %@ and %@ reposted your profile + Notification that 2 users reposted the current user's profile + satoshi satoshi @@ -1285,6 +1350,36 @@ Label for filter for seeing your posts and replies (instead of only your posts). you You, in this context, is the person who controls their own social network. You is used in the context of a larger sentence that welcomes the reader to the social network that they control themself. + + %@ zapped a post you were tagged in + %@ zapped a post you were tagged in + Notification that a user zapped a post that the current user was tagged in + + + %@ and %@ zapped a post you were tagged in + %@ and %@ zapped a post you were tagged in + Notification that 2 users zapped a post that the current user was tagged in + + + %@ zapped your post + %@ zapped your post + Notification that a user zapped the current user's post + + + %@ and %@ zapped your post + %@ and %@ zapped your post + Notification that 2 users zapped the current user's post + + + %@ zapped your profile + %@ zapped your profile + Notification that a user zapped the current user's profile + + + %@ and %@ zapped your profile + %@ and %@ zapped your profile + Notification that 2 users zapped the current user's profile + ⚡️ %@ ⚡️ %@ diff --git a/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings b/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings index 873df4c1..10543b39 100644 Binary files a/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings and b/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings differ diff --git a/damusTests/EventGroupViewTests.swift b/damusTests/EventGroupViewTests.swift new file mode 100644 index 00000000..6ccdcf6c --- /dev/null +++ b/damusTests/EventGroupViewTests.swift @@ -0,0 +1,42 @@ +// +// EventGroupViewTests.swift +// damusTests +// +// Created by Terry Yiu on 2/26/23. +// + +import XCTest +@testable import damus + +final class EventGroupViewTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testReactingToText() throws { + let enUsLocale = Locale(identifier: "en-US") + let damusState = test_damus_state() + + let encodedPost = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}" + let repost1 = NostrEvent(id: "", content: encodedPost, pubkey: "pk1", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) + let repost2 = NostrEvent(id: "", content: encodedPost, pubkey: "pk2", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) + + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [])), ev: test_event, locale: enUsLocale), "??") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_event, locale: enUsLocale), "pk1:pk1 reposted a post you were tagged in") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_event, locale: enUsLocale), "pk1:pk1 and pk2:pk2 reposted a post you were tagged in") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost2])), ev: test_event, locale: enUsLocale), "pk1:pk1 and 2 others reposted a post you were tagged in") + + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [])), ev: test_event, locale: $0), "??") + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_event, locale: $0)) + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_event, locale: $0)) + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost2])), ev: test_event, locale: $0)) + } + } + +}