damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 8a9e3ea76b3f92464035967f288d65b453906e0b
parent 4830a6f3b7693076abf3d818c7d5581bb8cf342e
Author: Terry Yiu <git@tyiu.xyz>
Date:   Fri, 14 Jul 2023 00:21:08 -0400

Fix localization issues and export strings for translation

Changelog-Fixed: Fix localization issues and export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 12++++--------
Mdamus/Components/Search/SearchHeaderView.swift | 15+++++----------
Mdamus/Util/LocalizationUtil.swift | 6++++++
Mdamus/Views/ActionBar/EventDetailBar.swift | 24++++++------------------
Mdamus/Views/Events/Components/ContextButton.swift | 2+-
Mdamus/Views/Events/EventShell.swift | 4++--
Mdamus/Views/Events/Longform/LongformPreview.swift | 12+++++++++---
Mdamus/Views/NostrScript/LoadScript.swift | 8++------
Mdamus/Views/NoteContentView.swift | 2+-
Mdamus/Views/Profile/ProfileView.swift | 25++++++-------------------
Mdamus/Views/SearchView.swift | 2+-
Mdamus/Views/Zaps/CustomizeZapView.swift | 8++------
Mdamus/en-US.lproj/Localizable.stringsdict | 16++++++++++++++++
Mdamus/en-US.xcloc/Localized Contents/en-US.xliff | 35++++++++++++++++++++++++++++++-----
Mdamus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings | 0
Mdamus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict | 16++++++++++++++++
DdamusTests/CustomZapViewTests.swift | 34----------------------------------
DdamusTests/EventDetailBarTests.swift | 56--------------------------------------------------------
AdamusTests/LocalizationUtilTests.swift | 41+++++++++++++++++++++++++++++++++++++++++
MdamusTests/NostrScriptTests.swift | 12------------
MdamusTests/ProfileViewTests.swift | 33---------------------------------
21 files changed, 148 insertions(+), 215 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; }; 3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; }; 3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; }; - 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 */; }; @@ -31,12 +30,12 @@ 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; }; 3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; }; 3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; }; + 3AAC7A022A60FE72002B50DF /* LocalizationUtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC7A012A60FE72002B50DF /* LocalizationUtilTests.swift */; }; 3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB72AB8298ECF30004BB58C /* Translator.swift */; }; 3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; }; 3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; }; 3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; }; 3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; }; - 3AFBF3FD29FDA7CC00E79C7C /* CustomZapViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFBF3FC29FDA7CC00E79C7C /* CustomZapViewTests.swift */; }; 4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; }; 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; }; 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; }; @@ -378,7 +377,6 @@ 3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescriptionTests.swift; sourceTree = "<group>"; }; - 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailBarTests.swift; sourceTree = "<group>"; }; 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationUtil.swift; sourceTree = "<group>"; }; 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = "<group>"; }; 3A3040F929A91ED6008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; @@ -442,6 +440,7 @@ 3AA5E70729B9E84A002701ED /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = "<group>"; }; 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLPlan.swift; sourceTree = "<group>"; }; + 3AAC7A012A60FE72002B50DF /* LocalizationUtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationUtilTests.swift; sourceTree = "<group>"; }; 3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 3AB5B86B2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; }; 3AB5B86C2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; @@ -477,7 +476,6 @@ 3AF6336829884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; - 3AFBF3FC29FDA7CC00E79C7C /* CustomZapViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomZapViewTests.swift; sourceTree = "<group>"; }; 4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = "<group>"; }; 4C06670528FCB08600038D2A /* ImageCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarousel.swift; sourceTree = "<group>"; }; 4C06670828FDE64700038D2A /* damus-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "damus-Bridging-Header.h"; sourceTree = "<group>"; }; @@ -1516,17 +1514,16 @@ 4CB883A9297612FF00DC99E7 /* ZapTests.swift */, 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */, 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */, - 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */, 5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */, 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */, 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */, 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */, 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */, - 3AFBF3FC29FDA7CC00E79C7C /* CustomZapViewTests.swift */, 3A5E47C62A4A76C800C0D090 /* TrieTests.swift */, 3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */, 4C4F14A62A2A61A30045A0B9 /* NostrScriptTests.swift */, 4C19AE542A5D977400C90DB7 /* HashtagTests.swift */, + 3AAC7A012A60FE72002B50DF /* LocalizationUtilTests.swift */, ); path = damusTests; sourceTree = "<group>"; @@ -2135,14 +2132,13 @@ 3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */, 4C19AE552A5D977400C90DB7 /* HashtagTests.swift in Sources */, 3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */, + 3AAC7A022A60FE72002B50DF /* LocalizationUtilTests.swift in Sources */, 4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */, 3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */, 501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */, - 3AFBF3FD29FDA7CC00E79C7C /* CustomZapViewTests.swift in Sources */, 3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */, DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */, 4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */, - 3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */, 4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */, 4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */, 4C7D097E2A0C58B900943473 /* WalletConnectTests.swift in Sources */, diff --git a/damus/Components/Search/SearchHeaderView.swift b/damus/Components/Search/SearchHeaderView.swift @@ -31,10 +31,10 @@ struct SearchHeaderView: View { switch described { case .hashtag: - Text("#") + Text(verbatim: "#") .font(.largeTitle.bold()) .foregroundStyle(PinkGradient) - .mask(Text("#") + .mask(Text(verbatim: "#") .font(.largeTitle.bold())) case .unknown: @@ -46,12 +46,7 @@ struct SearchHeaderView: View { } var SearchText: Text { - switch described { - case .hashtag(let ht): - return Text(verbatim: "#" + ht) - case .unknown: - return Text("Search") - } + Text(described.description) } func unfollow(_ hashtag: String) { @@ -66,7 +61,7 @@ struct SearchHeaderView: View { func FollowButton(_ ht: String) -> some View { return Button(action: { follow(ht) }) { - Text("Follow hashtag") + Text("Follow hashtag", comment: "Button to follow a given hashtag.") .font(.footnote.bold()) } .buttonStyle(GradientButtonStyle(padding: 10)) @@ -74,7 +69,7 @@ struct SearchHeaderView: View { func UnfollowButton(_ ht: String) -> some View { return Button(action: { unfollow(ht) }) { - Text("Unfollow hashtag") + Text("Unfollow hashtag", comment: "Button to unfollow a given hashtag.") .font(.footnote.bold()) } .buttonStyle(GradientButtonStyle(padding: 10)) diff --git a/damus/Util/LocalizationUtil.swift b/damus/Util/LocalizationUtil.swift @@ -32,3 +32,9 @@ func localeToLanguage(_ locale: String) -> String? { return NSLocale(localeIdentifier: locale).languageCode } } + +/// Returns a localized string that is pluralized based on a single Int-typed count variable. +func pluralizedString(key: String, count: Int, locale: Locale = Locale.current) -> String { + let format = localizedStringFormat(key: key, locale: locale) + return String(format: format, locale: locale, count) +} diff --git a/damus/Views/ActionBar/EventDetailBar.swift b/damus/Views/ActionBar/EventDetailBar.swift @@ -26,7 +26,8 @@ struct EventDetailBar: View { HStack { if bar.boosts > 0 { NavigationLink(value: Route.Reposts(reposts: RepostsModel(state: state, target: target))) { - let noun = Text(verbatim: repostsCountString(bar.boosts)).foregroundColor(.gray) + let nounString = pluralizedString(key: "reposts_count", count: bar.boosts) + let noun = Text(nounString).foregroundColor(.gray) Text("\(Text(verbatim: bar.boosts.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.") } .buttonStyle(PlainButtonStyle()) @@ -34,7 +35,8 @@ struct EventDetailBar: View { if bar.likes > 0 && !state.settings.onlyzaps_mode { NavigationLink(value: Route.Reactions(reactions: ReactionsModel(state: state, target: target))) { - let noun = Text(verbatim: reactionsCountString(bar.likes)).foregroundColor(.gray) + let nounString = pluralizedString(key: "reactions_count", count: bar.likes) + let noun = Text(nounString).foregroundColor(.gray) Text("\(Text(verbatim: bar.likes.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.") } .buttonStyle(PlainButtonStyle()) @@ -42,7 +44,8 @@ struct EventDetailBar: View { if bar.zaps > 0 { NavigationLink(value: Route.Zaps(target: .note(id: target, author: target_pk))) { - let noun = Text(verbatim: zapsCountString(bar.zaps)).foregroundColor(.gray) + let nounString = pluralizedString(key: "zaps_count", count: bar.zaps) + let noun = Text(nounString).foregroundColor(.gray) Text("\(Text(verbatim: bar.zaps.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.") } .buttonStyle(PlainButtonStyle()) @@ -51,21 +54,6 @@ struct EventDetailBar: View { } } -func repostsCountString(_ count: Int, locale: Locale = Locale.current) -> String { - let format = localizedStringFormat(key: "reposts_count", locale: locale) - return String(format: format, locale: locale, count) -} - -func reactionsCountString(_ count: Int, locale: Locale = Locale.current) -> String { - let format = localizedStringFormat(key: "reactions_count", locale: locale) - return String(format: format, locale: locale, count) -} - -func zapsCountString(_ count: Int, locale: Locale = Locale.current) -> String { - let format = localizedStringFormat(key: "zaps_count", locale: locale) - return String(format: format, locale: locale, count) -} - struct EventDetailBar_Previews: PreviewProvider { static var previews: some View { EventDetailBar(state: test_damus_state(), target: "", target_pk: "") diff --git a/damus/Views/Events/Components/ContextButton.swift b/damus/Views/Events/Components/ContextButton.swift @@ -9,7 +9,7 @@ import SwiftUI struct ContextButton: View { var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + Text(verbatim: /*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) } } diff --git a/damus/Views/Events/EventShell.swift b/damus/Views/Events/EventShell.swift @@ -134,11 +134,11 @@ struct EventShell_Previews: PreviewProvider { static var previews: some View { VStack { EventShell(state: test_damus_state(), event: test_event, options: [.no_action_bar]) { - Text("Hello") + Text(verbatim: "Hello") } EventShell(state: test_damus_state(), event: test_event, options: [.no_action_bar, .wide]) { - Text("Hello") + Text(verbatim: "Hello") } } .frame(height: 300) diff --git a/damus/Views/Events/Longform/LongformPreview.swift b/damus/Views/Events/Longform/LongformPreview.swift @@ -30,7 +30,8 @@ struct LongformPreviewBody: View { } func Words(_ words: Int) -> Text { - Text(verbatim: words.description) + Text(verbatim: " ") + Text("Words") + let wordCount = pluralizedString(key: "word_count", count: words) + return Text(wordCount) } var body: some View { @@ -45,8 +46,13 @@ struct LongformPreviewBody: View { var Main: some View { VStack(alignment: .leading, spacing: 10) { - Text(event.title ?? "Untitled") - .font(.title) + if let title = event.title { + Text(title) + .font(.title) + } else { + Text("Untitled", comment: "Text indicating that the long-form note title is untitled.") + .font(.title) + } Text(event.summary ?? "") .foregroundColor(.gray) diff --git a/damus/Views/NostrScript/LoadScript.swift b/damus/Views/NostrScript/LoadScript.swift @@ -80,11 +80,6 @@ class ScriptModel: ObservableObject { } } -func imports_string(_ count: Int, locale: Locale = Locale.current) -> String { - let format = localizedStringFormat(key: "imports_count", locale: locale) - return String(format: format, locale: locale, count) -} - struct LoadScript: View { let pool: RelayPool @@ -95,7 +90,8 @@ struct LoadScript: View { VStack { let imports = script.script.imports() - let nounText = Text(verbatim: imports_string(imports.count)).font(.title) + let nounString = pluralizedString(key: "imports_count", count: imports.count) + let nounText = Text(nounString).font(.title) Text("\(Text(verbatim: imports.count.formatted())) \(nounText)", comment: "Sentence composed of 2 variables to describe how many imports were performed from loading a NostrScript. In source English, the first variable is the number of imports, and the second variable is 'Import' or 'Imports'.") ForEach(imports.indices, id: \.self) { ind in diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -203,7 +203,7 @@ struct NoteContentView: View { InvoiceView(our_pubkey: damus_state.pubkey, invoice: inv, settings: damus_state.settings) } case .media(let media): - Text("media \(media.url.absoluteString)") + Text(verbatim: "media \(media.url.absoluteString)") } } } diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift @@ -31,21 +31,6 @@ func follow_btn_txt(_ fs: FollowState, follows_you: Bool) -> String { } } -func followersCountString(_ count: Int, locale: Locale = Locale.current) -> String { - let format = localizedStringFormat(key: "followers_count", locale: locale) - return String(format: format, locale: locale, count) -} - -func followingCountString(_ count: Int, locale: Locale = Locale.current) -> String { - let format = localizedStringFormat(key: "following_count", locale: locale) - return String(format: format, locale: locale, count) -} - -func relaysCountString(_ count: Int, locale: Locale = Locale.current) -> String { - let format = localizedStringFormat(key: "relays_count", locale: locale) - return String(format: format, locale: locale, count) -} - func followedByString(_ friend_intersection: [String], profiles: Profiles, locale: Locale = Locale.current) -> String { let bundle = bundleForLocale(locale: locale) let names: [String] = friend_intersection.prefix(3).map { @@ -379,8 +364,9 @@ struct ProfileView: View { .foregroundColor(.gray) } else { let followerCount = followers.count! - let noun_text = Text(verbatim: followersCountString(followerCount)).font(.subheadline).foregroundColor(.gray) - Text("\(Text(verbatim: followerCount.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.") + let nounString = pluralizedString(key: "followers_count", count: followerCount) + let nounText = Text(verbatim: nounString).font(.subheadline).foregroundColor(.gray) + Text("\(Text(verbatim: followerCount.formatted()).font(.subheadline.weight(.medium))) \(nounText)", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.") } } } @@ -405,7 +391,7 @@ struct ProfileView: View { let following_model = FollowingModel(damus_state: damus_state, contacts: contacts) NavigationLink(value: Route.Following(following: following_model)) { HStack { - let noun_text = Text(verbatim: "\(followingCountString(profile.following))").font(.subheadline).foregroundColor(.gray) + let noun_text = Text(verbatim: "\(pluralizedString(key: "following_count", count: profile.following))").font(.subheadline).foregroundColor(.gray) Text("\(Text(verbatim: profile.following.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.") } } @@ -428,7 +414,8 @@ struct ProfileView: View { if let relays = profile.relays { // Only open relay config view if the user is logged in with private key and they are looking at their own profile. - let noun_text = Text(verbatim: relaysCountString(relays.keys.count)).font(.subheadline).foregroundColor(.gray) + let noun_string = pluralizedString(key: "relays_count", count: relays.keys.count) + let noun_text = Text(noun_string).font(.subheadline).foregroundColor(.gray) let relay_text = Text("\(Text(verbatim: relays.keys.count.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.") if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user { NavigationLink(value: Route.RelayConfig) { diff --git a/damus/Views/SearchView.swift b/damus/Views/SearchView.swift @@ -44,7 +44,7 @@ struct SearchView: View { } } -enum DescribedSearch { +enum DescribedSearch: CustomStringConvertible { case hashtag(String) case unknown diff --git a/damus/Views/Zaps/CustomizeZapView.swift b/damus/Views/Zaps/CustomizeZapView.swift @@ -39,11 +39,6 @@ func get_zap_amount_items(_ default_zap_amt: Int) -> [ZapAmountItem] { return entries } -func satsString(_ count: Int, locale: Locale = Locale.current) -> String { - let format = localizedStringFormat(key: "sats", locale: locale) - return String(format: format, locale: locale, count) -} - struct CustomizeZapView: View { let state: DamusState let target: ZapTarget @@ -138,7 +133,8 @@ struct CustomizeZapView: View { model.custom_amount_sats = nil } } - Text(verbatim: satsString(model.custom_amount_sats ?? 0)) + let noun = pluralizedString(key: "sats", count: model.custom_amount_sats ?? 0) + Text(noun) .font(.system(size: 18, weight: .heavy)) } } diff --git a/damus/en-US.lproj/Localizable.stringsdict b/damus/en-US.lproj/Localizable.stringsdict @@ -258,6 +258,22 @@ <string>%2$@ sats</string> </dict> </dict> + <key>word_count</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@WORDS@</string> + <key>WORDS</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>%d Word</string> + <key>other</key> + <string>%d Words</string> + </dict> + </dict> <key>zap_notification_no_message</key> <dict> <key>NSStringLocalizedFormatKey</key> diff --git a/damus/en-US.xcloc/Localized Contents/en-US.xliff b/damus/en-US.xcloc/Localized Contents/en-US.xliff @@ -603,6 +603,11 @@ Sentence composed of 2 variables to describe how many people are following a use <target>Follow Back</target> <note>Button to follow a user back.</note> </trans-unit> + <trans-unit id="Follow hashtag" xml:space="preserve"> + <source>Follow hashtag</source> + <target>Follow hashtag</target> + <note>Button to follow a given hashtag.</note> + </trans-unit> <trans-unit id="Follow me on Nostr" xml:space="preserve"> <source>Follow me on Nostr</source> <target>Follow me on Nostr</target> @@ -769,11 +774,6 @@ Sentence composed of 2 variables to describe how many people are following a use <target>Likes</target> <note>Setting to enable Like Local Notification</note> </trans-unit> - <trans-unit id="Load %lld more" xml:space="preserve"> - <source>Load %lld more</source> - <target>Load %lld more</target> - <note>Button text for loading more events, where the variable is the number of events.</note> - </trans-unit> <trans-unit id="Local Notifications" xml:space="preserve"> <source>Local Notifications</source> <target>Local Notifications</target> @@ -1559,6 +1559,11 @@ Label for filter for seeing your notes and replies (instead of only your notes). <target>Unfollow</target> <note>Button to unfollow a user.</note> </trans-unit> + <trans-unit id="Unfollow hashtag" xml:space="preserve"> + <source>Unfollow hashtag</source> + <target>Unfollow hashtag</target> + <note>Button to unfollow a given hashtag.</note> + </trans-unit> <trans-unit id="Unfollowing..." xml:space="preserve"> <source>Unfollowing...</source> <target>Unfollowing...</target> @@ -1579,6 +1584,11 @@ Label for filter for seeing your notes and replies (instead of only your notes). <target>Unmute conversation</target> <note>Context menu option for unmuting a conversation.</note> </trans-unit> + <trans-unit id="Untitled" xml:space="preserve"> + <source>Untitled</source> + <target>Untitled</target> + <note>Text indicating that the long-form note title is untitled.</note> + </trans-unit> <trans-unit id="Upload" xml:space="preserve"> <source>Upload</source> <target>Upload</target> @@ -2161,6 +2171,21 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY. <target>%2$@ sats</target> <note/> </trans-unit> + <trans-unit id="/word_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve"> + <source>%#@WORDS@</source> + <target>%#@WORDS@</target> + <note/> + </trans-unit> + <trans-unit id="/word_count:dict/WORDS:dict/one:dict/:string" xml:space="preserve"> + <source>%d Word</source> + <target>%d Word</target> + <note/> + </trans-unit> + <trans-unit id="/word_count:dict/WORDS:dict/other:dict/:string" xml:space="preserve"> + <source>%d Words</source> + <target>%d Words</target> + <note/> + </trans-unit> <trans-unit id="/zap_notification_no_message:dict/NOTIFICATION:dict/one:dict/:string" xml:space="preserve"> <source>You received %2$@ sat from %3$@</source> <target>You received %2$@ sat from %3$@</target> 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 Binary files differ. diff --git a/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict b/damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict @@ -258,6 +258,22 @@ <string>%2$@ sats</string> </dict> </dict> + <key>word_count</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@WORDS@</string> + <key>WORDS</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>%d Word</string> + <key>other</key> + <string>%d Words</string> + </dict> + </dict> <key>zap_notification_no_message</key> <dict> <key>NSStringLocalizedFormatKey</key> diff --git a/damusTests/CustomZapViewTests.swift b/damusTests/CustomZapViewTests.swift @@ -1,34 +0,0 @@ -// -// CustomZapViewTests.swift -// damusTests -// -// Created by Terry Yiu on 4/29/23. -// - -import XCTest -@testable import damus - -final class CustomZapViewTests: XCTestCase { - - let enUsLocale = Locale(identifier: "en-US") - - 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 testSatsString() throws { - XCTAssertEqual(satsString(0, locale: enUsLocale), "sats") - XCTAssertEqual(satsString(1, locale: enUsLocale), "sat") - XCTAssertEqual(satsString(2, locale: enUsLocale), "sats") - Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { - for count in 1...10 { - XCTAssertNoThrow(satsString(count, locale: $0)) - } - } - } - -} diff --git a/damusTests/EventDetailBarTests.swift b/damusTests/EventDetailBarTests.swift @@ -1,56 +0,0 @@ -// -// EventDetailBarTests.swift -// damusTests -// -// Created by Terry Yiu on 2/24/23. -// - -import XCTest -@testable import damus - -final class EventDetailBarTests: XCTestCase { - - let enUsLocale = Locale(identifier: "en-US") - - 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 testRepostsCountString() throws { - XCTAssertEqual(repostsCountString(0, locale: enUsLocale), "Reposts") - XCTAssertEqual(repostsCountString(1, locale: enUsLocale), "Repost") - XCTAssertEqual(repostsCountString(2, locale: enUsLocale), "Reposts") - Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { - for count in 1...10 { - XCTAssertNoThrow(repostsCountString(count, locale: $0)) - } - } - } - - func testReactionsCountString() throws { - XCTAssertEqual(reactionsCountString(0, locale: enUsLocale), "Reactions") - XCTAssertEqual(reactionsCountString(1, locale: enUsLocale), "Reaction") - XCTAssertEqual(reactionsCountString(2, locale: enUsLocale), "Reactions") - Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { - for count in 1...10 { - XCTAssertNoThrow(reactionsCountString(count, locale: $0)) - } - } - } - - func testZapssCountString() throws { - XCTAssertEqual(zapsCountString(0, locale: enUsLocale), "Zaps") - XCTAssertEqual(zapsCountString(1, locale: enUsLocale), "Zap") - XCTAssertEqual(zapsCountString(2, locale: enUsLocale), "Zaps") - Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { - for count in 1...10 { - XCTAssertNoThrow(zapsCountString(count, locale: $0)) - } - } - } - -} diff --git a/damusTests/LocalizationUtilTests.swift b/damusTests/LocalizationUtilTests.swift @@ -0,0 +1,41 @@ +// +// LocalizationUtilTests.swift +// damusTests +// +// Created by Terry Yiu on 7/13/23. +// + +import XCTest +@testable import damus + +final class LocalizationUtilTests: XCTestCase { + + func testPluralizedString() throws { + let enUsLocale = Locale(identifier: "en-US") + + // Test cases of the localization string key, and the expected en-US strings for a count of 0, 1, and 2. + let keys = [ + ["followers_count", "Followers", "Follower", "Followers"], + ["following_count", "Following", "Following", "Following"], + ["imports_count", "Imports", "Import", "Imports"], + ["reactions_count", "Reactions", "Reaction", "Reactions"], + ["relays_count", "Relays", "Relay", "Relays"], + ["reposts_count", "Reposts", "Repost", "Reposts"], + ["sats", "sats", "sat", "sats"], + ["zaps_count", "Zaps", "Zap", "Zaps"], + ["word_count", "0 Words", "1 Word", "2 Words"] + ] + + for key in keys { + XCTAssertEqual(pluralizedString(key: key[0], count: 0, locale: enUsLocale), key[1]) + XCTAssertEqual(pluralizedString(key: key[0], count: 1, locale: enUsLocale), key[2]) + XCTAssertEqual(pluralizedString(key: key[0], count: 2, locale: enUsLocale), key[3]) + Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { + for count in 1...10 { + XCTAssertNoThrow(pluralizedString(key: key[0], count: count, locale: $0)) + } + } + } + } + +} diff --git a/damusTests/NostrScriptTests.swift b/damusTests/NostrScriptTests.swift @@ -105,18 +105,6 @@ final class NostrScriptTests: XCTestCase { self.wait(for: [resume_expected], timeout: 10.0) } - func test_imports_string() throws { - let enUsLocale = Locale(identifier: "en-US") - XCTAssertEqual(imports_string(0, locale: enUsLocale), "Imports") - XCTAssertEqual(imports_string(1, locale: enUsLocale), "Import") - XCTAssertEqual(imports_string(2, locale: enUsLocale), "Imports") - Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { - for count in 1...10 { - XCTAssertNoThrow(imports_string(count, locale: $0)) - } - } - } - func testPerformanceExample() throws { // This is an example of a performance test case. self.measure { diff --git a/damusTests/ProfileViewTests.swift b/damusTests/ProfileViewTests.swift @@ -20,39 +20,6 @@ final class ProfileViewTests: XCTestCase { // Put teardown code here. This method is called after the invocation of each test method in the class. } - func testFollowersCountString() throws { - XCTAssertEqual(followersCountString(0, locale: enUsLocale), "Followers") - XCTAssertEqual(followersCountString(1, locale: enUsLocale), "Follower") - XCTAssertEqual(followersCountString(2, locale: enUsLocale), "Followers") - Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { - for count in 1...10 { - XCTAssertNoThrow(followersCountString(count, locale: $0)) - } - } - } - - func testFollowingCountString() throws { - XCTAssertEqual(followingCountString(0, locale: enUsLocale), "Following") - XCTAssertEqual(followingCountString(1, locale: enUsLocale), "Following") - XCTAssertEqual(followingCountString(2, locale: enUsLocale), "Following") - Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { - for count in 1...10 { - XCTAssertNoThrow(followingCountString(count, locale: $0)) - } - } - } - - func testRelaysCountString() throws { - XCTAssertEqual(relaysCountString(0, locale: enUsLocale), "Relays") - XCTAssertEqual(relaysCountString(1, locale: enUsLocale), "Relay") - XCTAssertEqual(relaysCountString(2, locale: enUsLocale), "Relays") - Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { - for count in 1...10 { - XCTAssertNoThrow(relaysCountString(count, locale: $0)) - } - } - } - func testFollowedByString() throws { let profiles = test_damus_state().profiles