Implement Localization (#80)

* Implement localization

* Fix some localization keys

* Adapt to recent changes
This commit is contained in:
Thomas 2023-01-19 18:14:08 +01:00 committed by GitHub
parent e519e9cdff
commit 980b9a5dd6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 709 additions and 346 deletions

View file

@ -64,6 +64,7 @@
9FD542E72962D2FF0045321A /* Lists in Frameworks */ = {isa = PBXBuildFile; productRef = 9FD542E62962D2FF0045321A /* Lists */; };
9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE151A5293C90F900E9683D /* IconSelectorView.swift */; };
9FE3DB57296FEFCA00628CB0 /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE3DB56296FEFCA00628CB0 /* AppAccount */; };
E9B576C329743F4C00BCE646 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E9B576C529743F4C00BCE646 /* Localizable.strings */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -152,6 +153,7 @@
9FD542E52962D2CE0045321A /* Lists */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Lists; path = Packages/Lists; sourceTree = "<group>"; };
9FE151A5293C90F900E9683D /* IconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectorView.swift; sourceTree = "<group>"; };
9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = "<group>"; };
E9B576C429743F4C00BCE646 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -236,6 +238,7 @@
9F2A542D296B1CC0009B2D7C /* glass.wav */,
9F2A542B296B1177009B2D7C /* glass.caf */,
9F24EEB729360C330042359D /* Preview Assets.xcassets */,
E9B576C029743F2A00BCE646 /* Localization */,
);
path = Resources;
sourceTree = "<group>";
@ -345,6 +348,14 @@
path = Settings;
sourceTree = "<group>";
};
E9B576C029743F2A00BCE646 /* Localization */ = {
isa = PBXGroup;
children = (
E9B576C529743F4C00BCE646 /* Localizable.strings */,
);
path = Localization;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -498,6 +509,7 @@
buildActionMask = 2147483647;
files = (
9F2A542C296B1177009B2D7C /* glass.caf in Resources */,
E9B576C329743F4C00BCE646 /* Localizable.strings in Resources */,
9FD34823293D06E800DB0EE9 /* Assets.xcassets in Resources */,
9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */,
9FAD85832971BF7200496AB1 /* Secret.plist in Resources */,
@ -569,12 +581,12 @@
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
9FAD858C29743F7400496AB1 /* MainInterface.storyboard */ = {
E9B576C529743F4C00BCE646 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
9FAD858D29743F7400496AB1 /* Base */,
E9B576C429743F4C00BCE646 /* en */,
);
name = MainInterface.storyboard;
name = Localizable.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */

View file

@ -24,7 +24,7 @@ struct AddAccountView: View {
@State private var isSigninIn = false
@State private var signInClient: Client?
@State private var instances: [InstanceSocial] = []
@State private var instanceFetchError: String?
@State private var instanceFetchError: LocalizedStringKey?
@State private var oauthURL: URL?
private let instanceNamePublisher = PassthroughSubject<String, Never>()
@ -34,7 +34,7 @@ struct AddAccountView: View {
var body: some View {
NavigationStack {
Form {
TextField("Instance URL", text: $instanceName)
TextField("instance.url", text: $instanceName)
.listRowBackground(theme.primaryBackgroundColor)
.keyboardType(.URL)
.textContentType(.URL)
@ -52,7 +52,7 @@ struct AddAccountView: View {
}
}
.formStyle(.grouped)
.navigationTitle("Add account")
.navigationTitle("account.add.navigation-title")
.navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
@ -60,7 +60,7 @@ struct AddAccountView: View {
.toolbar {
if !appAccountsManager.availableAccounts.isEmpty {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel", action: { dismiss() })
Button("action.cancel", action: { dismiss() })
}
}
}
@ -83,7 +83,7 @@ struct AddAccountView: View {
self.instanceFetchError = nil
} catch _ as DecodingError {
self.instance = nil
self.instanceFetchError = "This instance is not currently supported."
self.instanceFetchError = "account.add.error.instance-not-supported"
} catch {
self.instance = nil
}
@ -122,7 +122,7 @@ struct AddAccountView: View {
ProgressView()
.tint(theme.labelColor)
} else {
Text("Sign in")
Text("account.add.sign-in")
.font(.scaledHeadline)
}
Spacer()
@ -134,7 +134,7 @@ struct AddAccountView: View {
}
private var instancesListView: some View {
Section("Suggestions") {
Section("instance.suggestions") {
if instances.isEmpty {
placeholderRow
} else {
@ -149,9 +149,11 @@ struct AddAccountView: View {
Text(instance.info?.shortDescription ?? "")
.font(.scaledBody)
.foregroundColor(.gray)
Text("\(instance.users) users ⸱ \(instance.statuses) posts")
.font(.scaledFootnote)
.foregroundColor(.gray)
(Text("instance.list.users-\(instance.users)")
+ Text("")
+ Text("instance.list.posts-\(instance.statuses)"))
.font(.scaledFootnote)
.foregroundColor(.gray)
}
}
.listRowBackground(theme.primaryBackgroundColor)
@ -162,13 +164,13 @@ struct AddAccountView: View {
private var placeholderRow: some View {
VStack(alignment: .leading, spacing: 4) {
Text("Loading...")
Text("placeholder.loading.short")
.font(.scaledHeadline)
.foregroundColor(.primary)
Text("Loading, loading, loading ....")
Text("placeholder.loading.long")
.font(.scaledBody)
.foregroundColor(.gray)
Text("Loading ...")
Text("placeholder.loading.short")
.font(.scaledFootnote)
.foregroundColor(.gray)
}

View file

@ -12,32 +12,32 @@ struct DisplaySettingsView: View {
var body: some View {
Form {
Section("Theme") {
Section("settings.display.section.theme") {
themeSelectorButton
ColorPicker("Tint color", selection: $theme.tintColor)
ColorPicker("Background color", selection: $theme.primaryBackgroundColor)
ColorPicker("Secondary Background color", selection: $theme.secondaryBackgroundColor)
ColorPicker("settings.display.theme.tint", selection: $theme.tintColor)
ColorPicker("settings.display.theme.background", selection: $theme.primaryBackgroundColor)
ColorPicker("settings.display.theme.secondary-background", selection: $theme.secondaryBackgroundColor)
}
.listRowBackground(theme.primaryBackgroundColor)
Section("Display") {
Picker("Avatar position", selection: $theme.avatarPosition) {
Section("settings.display.section.display") {
Picker("settings.display.avatar.position", selection: $theme.avatarPosition) {
ForEach(Theme.AvatarPosition.allCases, id: \.rawValue) { position in
Text(position.description).tag(position)
}
}
Picker("Avatar shape", selection: $theme.avatarShape) {
Picker("settings.display.avatar.shape", selection: $theme.avatarShape) {
ForEach(Theme.AvatarShape.allCases, id: \.rawValue) { shape in
Text(shape.description).tag(shape)
}
}
Picker("Status actions buttons", selection: $theme.statusActionsDisplay) {
Picker("settings.display.status.action-buttons", selection: $theme.statusActionsDisplay) {
ForEach(Theme.StatusActionsDisplay.allCases, id: \.rawValue) { buttonStyle in
Text(buttonStyle.description).tag(buttonStyle)
}
}
Picker("Status media style", selection: $theme.statusDisplayStyle) {
Picker("settings.display.status.media-style", selection: $theme.statusDisplayStyle) {
ForEach(Theme.StatusDisplayStyle.allCases, id: \.rawValue) { buttonStyle in
Text(buttonStyle.description).tag(buttonStyle)
}
@ -59,12 +59,12 @@ struct DisplaySettingsView: View {
theme.avatarPosition = .top
theme.statusActionsDisplay = .full
} label: {
Text("Restore default")
Text("settings.display.restore")
}
}
.listRowBackground(theme.primaryBackgroundColor)
}
.navigationTitle("Display Settings")
.navigationTitle("settings.display.navigation-title")
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
}
@ -72,7 +72,7 @@ struct DisplaySettingsView: View {
private var themeSelectorButton: some View {
NavigationLink(destination: ThemePreviewView()) {
HStack {
Text("Theme")
Text("settings.display.section.theme")
Spacer()
Text(theme.selectedSet.rawValue)
}

View file

@ -70,7 +70,7 @@ struct IconSelectorView: View {
}
}
.padding(6)
.navigationTitle("Icons")
.navigationTitle("settings.app.icon.navigation-title")
}
.background(theme.primaryBackgroundColor)
}

View file

@ -12,7 +12,7 @@ struct InstanceInfoView: View {
Form {
InstanceInfoSection(instance: instance)
}
.navigationTitle("Instance Info")
.navigationTitle("instance.info.navigation-title")
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
}
@ -24,19 +24,19 @@ public struct InstanceInfoSection: View {
let instance: Instance
public var body: some View {
Section("Instance info") {
LabeledContent("Name", value: instance.title)
Section("instance.info.section.info") {
LabeledContent("instance.info.name", value: instance.title)
Text(instance.shortDescription)
LabeledContent("Email", value: instance.email)
LabeledContent("Version", value: instance.version)
LabeledContent("Users", value: "\(instance.stats.userCount)")
LabeledContent("Posts", value: "\(instance.stats.statusCount)")
LabeledContent("Domains", value: "\(instance.stats.domainCount)")
LabeledContent("instance.info.email", value: instance.email)
LabeledContent("instance.info.version", value: instance.version)
LabeledContent("instance.info.users", value: "\(instance.stats.userCount)")
LabeledContent("instance.info.posts", value: "\(instance.stats.statusCount)")
LabeledContent("instance.info.domains", value: "\(instance.stats.domainCount)")
}
.listRowBackground(theme.primaryBackgroundColor)
if let rules = instance.rules {
Section("Instance rules") {
Section("instance.info.section.rules") {
ForEach(rules) { rule in
Text(rule.text)
}

View file

@ -18,39 +18,39 @@ struct PushNotificationsView: View {
Form {
Section {
Toggle(isOn: $pushNotifications.isPushEnabled) {
Text("Push notifications")
Text("settings.push.main-toggle")
}
} footer: {
Text("Receive push notifications on new activities")
Text("settings.push.main-toggle.description")
}
.listRowBackground(theme.primaryBackgroundColor)
if pushNotifications.isPushEnabled {
Section {
Toggle(isOn: $pushNotifications.isMentionNotificationEnabled) {
Label("Mentions", systemImage: "at")
Label("settings.push.mentions", systemImage: "at")
}
Toggle(isOn: $pushNotifications.isFollowNotificationEnabled) {
Label("Follows", systemImage: "person.badge.plus")
Label("settings.push.follows", systemImage: "person.badge.plus")
}
Toggle(isOn: $pushNotifications.isFavoriteNotificationEnabled) {
Label("Favorites", systemImage: "star")
Label("settings.push.favorites", systemImage: "star")
}
Toggle(isOn: $pushNotifications.isReblogNotificationEnabled) {
Label("Boosts", systemImage: "arrow.left.arrow.right.circle")
Label("settings.push.boosts", systemImage: "arrow.left.arrow.right.circle")
}
Toggle(isOn: $pushNotifications.isPollNotificationEnabled) {
Label("Polls Results", systemImage: "chart.bar")
Label("settings.push.polls", systemImage: "chart.bar")
}
Toggle(isOn: $pushNotifications.isNewPostsNotificationEnabled) {
Label("New Posts", systemImage: "bubble.right")
Label("settings.push.new-posts", systemImage: "bubble.right")
}
}
.listRowBackground(theme.primaryBackgroundColor)
.transition(.move(edge: .bottom))
}
}
.navigationTitle("Push Notifications")
.navigationTitle("settings.push.navigation-title")
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.onAppear {

View file

@ -30,7 +30,7 @@ struct SettingsTabs: View {
}
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.navigationTitle(Text("Settings"))
.navigationTitle(Text("settings.title"))
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(theme.primaryBackgroundColor, for: .navigationBar)
.withAppRouter()
@ -54,7 +54,7 @@ struct SettingsTabs: View {
}
private var accountsSection: some View {
Section("Accounts") {
Section("settings.section.accounts") {
ForEach(appAccountsManager.availableAccounts) { account in
AppAccountView(viewModel: .init(appAccount: account))
}
@ -76,33 +76,33 @@ struct SettingsTabs: View {
@ViewBuilder
private var generalSection: some View {
Section("General") {
Section("settings.section.general") {
NavigationLink(destination: PushNotificationsView()) {
Label("Push notifications", systemImage: "bell.and.waves.left.and.right")
Label("settings.general.push-notifications", systemImage: "bell.and.waves.left.and.right")
}
if let instanceData = currentInstance.instance {
NavigationLink(destination: InstanceInfoView(instance: instanceData)) {
Label("Instance Information", systemImage: "server.rack")
Label("settings.general.instance", systemImage: "server.rack")
}
}
NavigationLink(destination: DisplaySettingsView()) {
Label("Display Settings", systemImage: "paintpalette")
Label("settings.general.display", systemImage: "paintpalette")
}
NavigationLink(destination: remoteLocalTimelinesView) {
Label("Remote Local Timelines", systemImage: "dot.radiowaves.right")
Label("settings.general.remote-timelines", systemImage: "dot.radiowaves.right")
}
if !ProcessInfo.processInfo.isiOSAppOnMac {
Picker(selection: $preferences.preferredBrowser) {
ForEach(PreferredBrowser.allCases, id: \.rawValue) { browser in
switch browser {
case .inAppSafari:
Text("In-App Safari").tag(browser)
Text("settings.general.browser.in-app").tag(browser)
case .safari:
Text("System Safari").tag(browser)
Text("settings.general.browser.system").tag(browser)
}
}
} label: {
Label("Browser", systemImage: "network")
Label("settings.general.browser", systemImage: "network")
}
}
}
@ -110,11 +110,11 @@ struct SettingsTabs: View {
}
private var appSection: some View {
Section("App") {
Section("settings.section.app") {
if !ProcessInfo.processInfo.isiOSAppOnMac {
NavigationLink(destination: IconSelectorView()) {
Label {
Text("App Icon")
Text("settings.app.icon")
} icon: {
if let icon = IconSelectorView.Icon(string: UIApplication.shared.alternateIconName ?? "AppIcon") {
Image(uiImage: .init(named: icon.iconName)!)
@ -127,12 +127,12 @@ struct SettingsTabs: View {
}
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp")!) {
Label("Source (GitHub link)", systemImage: "link")
Label("settings.app.source", systemImage: "link")
}
.tint(theme.labelColor)
NavigationLink(destination: SupportAppView()) {
Label("Support the app", systemImage: "wand.and.stars")
Label("settings.app.support", systemImage: "wand.and.stars")
}
}
.listRowBackground(theme.primaryBackgroundColor)
@ -142,7 +142,7 @@ struct SettingsTabs: View {
Button {
addAccountSheetPresented.toggle()
} label: {
Text("Add account")
Text("settings.account.add")
}
.sheet(isPresented: $addAccountSheetPresented) {
AddAccountView()
@ -162,11 +162,11 @@ struct SettingsTabs: View {
Button {
routerPath.presentedSheet = .addRemoteLocalTimeline
} label: {
Label("Add a local timeline", systemImage: "badge.plus.radiowaves.right")
Label("settings.timeline.add", systemImage: "badge.plus.radiowaves.right")
}
.listRowBackground(theme.primaryBackgroundColor)
}
.navigationTitle("Remote Local Timelines")
.navigationTitle("settings.general.remote-timelines")
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
}

View file

@ -15,26 +15,26 @@ struct SupportAppView: View {
var productId: String {
"icecubes.tipjar.\(rawValue)"
}
var title: String {
var title: LocalizedStringKey {
switch self {
case .one:
return "🍬 Small Tip"
return "settings.support.one.title"
case .two:
return "☕️ Nice Tip"
return "settings.support.two.title"
case .three:
return "🤯 Generous Tip"
return "settings.support.three.title"
}
}
var subtitle: String {
var subtitle: LocalizedStringKey {
switch self {
case .one:
return "Small, but cute, and it taste good!"
return "settings.support.one.subtitle"
case .two:
return "I love the taste of a fancy coffee ❤️"
return "settings.support.two.subtitle"
case .three:
return "You're insane, thank you so much!"
return "settings.support.three.subtitle"
}
}
}
@ -61,7 +61,7 @@ struct SupportAppView: View {
.frame(width: 50, height: 50)
.cornerRadius(4)
}
Text("Hi there! My name is Thomas and I absolutely love creating open source apps. Ice Cubes is definitely one of my proudest projects to date - and let's be real, it's also the one that requires the most maintenance due to the ever-changing world of Mastodon and social media. If you're having a blast using Ice Cubes, consider tossing a little tip my way. It'll make my day (and help keep the app running smoothly for you). 🚀")
Text("settings.support.message-from-dev")
}
}
.listRowBackground(theme.primaryBackgroundColor)
@ -70,9 +70,9 @@ struct SupportAppView: View {
if loadingProducts {
HStack {
VStack(alignment: .leading) {
Text("Loading ...")
Text("placeholder.loading.short.")
.font(.scaledSubheadline)
Text("Loading subtitle...")
Text("settings.support.placeholder.loading-subtitle")
.font(.scaledFootnote)
.foregroundColor(.gray)
}
@ -118,18 +118,18 @@ struct SupportAppView: View {
}
.listRowBackground(theme.primaryBackgroundColor)
}
.navigationTitle("Support Ice Cubes")
.navigationTitle("settings.support.navigation-title")
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.alert("Thanks!", isPresented: $purchaseSuccessDisplayed, actions: {
Button { purchaseSuccessDisplayed = false } label: { Text("Ok") }
.alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: {
Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") }
}, message: {
Text("Thanks you so much for your tip! It's greatly appreciated!")
Text("settings.support.alert.message")
})
.alert("Error!", isPresented: $purchaseErrorDisplayed, actions: {
Button { purchaseErrorDisplayed = false } label: { Text("Ok") }
.alert("alert.error", isPresented: $purchaseErrorDisplayed, actions: {
Button { purchaseErrorDisplayed = false } label: { Text("alert.button.ok") }
}, message: {
Text("Error processing your in app purchase, please try again.")
Text("settings.support.alert.error.message")
})
.onAppear {
loadingProducts = true

View file

@ -55,23 +55,23 @@ enum Tab: Int, Identifiable, Hashable {
var label: some View {
switch self {
case .timeline:
Label("Timeline", systemImage: iconName)
Label("tab.timeline", systemImage: iconName)
case .trending:
Label("Trending", systemImage: iconName)
Label("tab.trending", systemImage: iconName)
case .local:
Label("Local", systemImage: iconName)
Label("tab.local", systemImage: iconName)
case .federated:
Label("Federated", systemImage: iconName)
Label("tab.federated", systemImage: iconName)
case .notifications:
Label("Notifications", systemImage: iconName)
Label("tab.notifications", systemImage: iconName)
case .mentions:
Label("Notifications", systemImage: iconName)
Label("tab.notifications", systemImage: iconName)
case .explore:
Label("Explore", systemImage: iconName)
Label("tab.explore", systemImage: iconName)
case .messages:
Label("Messages", systemImage: iconName)
Label("tab.messages", systemImage: iconName)
case .settings:
Label("Settings", systemImage: iconName)
Label("tab.settings", systemImage: iconName)
case .other, .profile:
EmptyView()
}

View file

@ -24,7 +24,7 @@ struct AddRemoteTimelineView: View {
var body: some View {
NavigationStack {
Form {
TextField("Instance URL", text: $instanceName)
TextField("timeline.add.url", text: $instanceName)
.listRowBackground(theme.primaryBackgroundColor)
.keyboardType(.URL)
.textContentType(.URL)
@ -32,7 +32,7 @@ struct AddRemoteTimelineView: View {
.autocorrectionDisabled()
.focused($isInstanceURLFieldFocused)
if let instance {
Label("\(instance.title) is a valid instance", systemImage: "checkmark.seal.fill")
Label("timeline.\(instance.title)-is-valid", systemImage: "checkmark.seal.fill")
.foregroundColor(.green)
.listRowBackground(theme.primaryBackgroundColor)
}
@ -41,21 +41,21 @@ struct AddRemoteTimelineView: View {
preferences.remoteLocalTimelines.append(instanceName)
dismiss()
} label: {
Text("Add")
Text("timeline.add.action.add")
}
.listRowBackground(theme.primaryBackgroundColor)
instancesListView
}
.formStyle(.grouped)
.navigationTitle("Add remote local timeline")
.navigationTitle("timeline.add-remote.title")
.navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel", action: { dismiss() })
Button("action.cancel", action: { dismiss() })
}
}
.onChange(of: instanceName) { newValue in
@ -78,7 +78,7 @@ struct AddRemoteTimelineView: View {
}
private var instancesListView: some View {
Section("Suggestions") {
Section("instance.suggestions") {
if instances.isEmpty {
ProgressView()
.listRowBackground(theme.primaryBackgroundColor)
@ -94,9 +94,12 @@ struct AddRemoteTimelineView: View {
Text(instance.info?.shortDescription ?? "")
.font(.scaledBody)
.foregroundColor(.gray)
Text("\(instance.users) users ⸱ \(instance.statuses) posts")
.font(.scaledFootnote)
.foregroundColor(.gray)
(Text("instance.list.users-\(instance.users)")
+ Text("")
+ Text("instance.list.posts-\(instance.statuses)"))
.font(.scaledFootnote)
.foregroundColor(.gray)
}
}
.listRowBackground(theme.primaryBackgroundColor)

View file

@ -78,11 +78,11 @@ struct TimelineTab: View {
Button {
self.timeline = timeline
} label: {
Label(timeline.title(), systemImage: timeline.iconName() ?? "")
Label(timeline.localizedTitle(), systemImage: timeline.iconName() ?? "")
}
}
if !currentAccount.lists.isEmpty {
Menu("Lists") {
Menu("timeline.filter.lists") {
ForEach(currentAccount.lists) { list in
Button {
timeline = .list(list: list)
@ -94,7 +94,7 @@ struct TimelineTab: View {
}
if !currentAccount.tags.isEmpty {
Menu("Followed Tags") {
Menu("timeline.filter.tags") {
ForEach(currentAccount.tags) { tag in
Button {
timeline = .hashtag(tag: tag.name, accountId: nil)
@ -104,8 +104,8 @@ struct TimelineTab: View {
}
}
}
Menu("Local Timelines") {
Menu("timeline.filter.local") {
ForEach(preferences.remoteLocalTimelines, id: \.self) { server in
Button {
timeline = .remoteLocal(server: server)
@ -116,7 +116,7 @@ struct TimelineTab: View {
Button {
routerPath.presentedSheet = .addRemoteLocalTimeline
} label: {
Label("Add a local timeline", systemImage: "badge.plus.radiowaves.right")
Label("timeline.filter.add-local", systemImage: "badge.plus.radiowaves.right")
}
}
}

View file

@ -0,0 +1,303 @@
// MARK: Common strings
"action.cancel" = "Cancel";
"action.delete" = "Delete";
"action.save" = "Save";
"action.done" = "Done";
"action.retry" = "Retry";
"alert.button.ok" = "Ok";
"alert.error" = "Error!";
"placeholder.loading.long" = "Loading, loading, loading ....";
"placeholder.loading.short" = "Loading ...";
"see-more" = "See more";
// MARK: Add Account
"account.add.error.instance-not-supported" = "This instance is not currently supported.";
"account.add.navigation-title" = "Add account";
"account.add.sign-in" = "Sign in";
// MARK: Enums
"enum.avatar-position.leading" = "Leading";
"enum.avatar-position.top" = "Top";
"enum.avatar-shape.circle" = "Circle";
"enum.avatar-shape.rounded" = "Rounded";
"enum.status-actions-display.all" = "All";
"enum.status-actions-display.no-buttons" = "No buttons";
"enum.status-actions-display.only-buttons" = "Only buttons";
"enum.status-display-style.compact" = "Compact";
"enum.status-display-style.large" = "Large";
// MARK: Instances
"instance.info.domains" = "Domains";
"instance.info.email" = "Email";
"instance.info.name" = "Name";
"instance.info.navigation-title" = "Instance Info";
"instance.info.posts" = "Posts";
"instance.info.section.info" = "Instance info";
"instance.info.section.rules" = "Instance rules";
"instance.info.users" = "Users";
"instance.info.version" = "Version";
"instance.list.posts-%@" = "%@ posts";
"instance.list.users-%@" = "%@ users";
"instance.suggestions" = "Suggestions";
"instance.url" = "Instance URL";
// MARK: Settings
"settings.account.add" = "Add account";
"settings.app.icon" = "App Icon";
"settings.app.icon.navigation-title" = "Icons";
"settings.app.source" = "Source (GitHub link)";
"settings.app.support" = "Support the app";
"settings.display.avatar.position" = "Avatar position";
"settings.display.avatar.shape" = "Avatar shape";
"settings.display.navigation-title" = "Display Settings";
"settings.display.restore" = "Restore defaults";
"settings.display.section.display" = "Display";
"settings.display.section.theme" = "Theme";
"settings.display.status.action-buttons" = "Status action buttons";
"settings.display.status.media-style" = "Status media style";
"settings.display.theme.background" = "Background color";
"settings.display.theme.secondary-background" = "Secondary Background color";
"settings.display.theme.tint" = "Tint color";
"settings.general.browser" = "Browser";
"settings.general.browser.in-app" = "In-App Safari";
"settings.general.browser.system" = "System Safari";
"settings.general.display" = "Display Settings";
"settings.general.instance" = "Instance Information";
"settings.general.push-notifications" = "Push notification";
"settings.general.remote-timelines" = "Remote Local Timelines";
"settings.push.boosts" = "Boosts";
"settings.push.favorites" = "Favorites";
"settings.push.follows" = "Follows";
"settings.push.main-toggle" = "Push notifications";
"settings.push.main-toggle.description" = "Receive push notifications on new activities";
"settings.push.mentions" = "Mentions";
"settings.push.navigation-title" = "Push Notifications";
"settings.push.new-posts" = "New Posts";
"settings.push.polls" = "Polls Results";
"settings.section.accounts" = "Accounts";
"settings.section.app" = "App";
"settings.section.general" = "General";
"settings.support.alert.error.message" = "Error processing your in app purchase, please try again.";
"settings.support.alert.message" = "Thanks you so much for your tip! It's greatly appreciated!";
"settings.support.alert.title" = "Thanks!";
"settings.support.message-from-dev" = "Hi there! My name is Thomas and I absolutely love creating open source apps. Ice Cubes is definitely one of my proudest projects to date - and let's be real, it's also the one that requires the most maintenance due to the ever-changing world of Mastodon and social media. If you're having a blast using Ice Cubes, consider tossing a little tip my way. It'll make my day (and help keep the app running smoothly for you). 🚀";
"settings.support.navigation-title" = "Support Ice Cubes";
"settings.support.one.subtitle" = "Small, but cute, and it tastes good!";
"settings.support.one.title" = "🍬 Small Tip";
"settings.support.placeholder.loading-subtitle" = "Loading subtitle ...";
"settings.support.three.subtitle" = "You're insane, thank you so much!";
"settings.support.three.title" = "🤯 Generous Tip";
"settings.support.two.subtitle" = "I love the taste of a fancy coffee ❤️";
"settings.support.two.title" = "☕️ Nice Tip";
"settings.timeline.add" = "Add a local timeline";
"settings.title" = "Settings";
// MARK: Tabs
"tab.explore" = "Explore";
"tab.federated" = "Federated";
"tab.local" = "Local";
"tab.messages" = "Messages";
"tab.notifications" = "Notifications";
"tab.settings" = "Settings";
"tab.timeline" = "Timeline";
"tab.trending" = "Trending";
// MARK: Timeline
"timeline.%@-is-valid" = "%@ is a valid instance";
"timeline.add-remote.title" = "Add remote local timeline";
"timeline.add.action.add" = "Add";
"timeline.filter.add-local" = "Add a local timeline";
"timeline.filter.lists" = "Lists";
"timeline.filter.local" = "Local Timelines";
"timeline.filter.tags" = "Followed Tags";
// MARK: Package: AppAccount
"app-account.button.add" = "Add Account";
// MARK: Package: Account
"account.action.add-remove-list" = "Add/Remove from lists";
"account.action.edit-info" = "Edit Info";
"account.action.mention" = "Mention";
"account.action.message" = "Message";
"account.boosted-by" = "Boosted by";
"account.detail.about" = "About";
"account.detail.familiar-followers" = "Also followed by";
"account.detail.n-fields %lld" = "%lld fields";
"account.detail.featured-tags-n-posts %lld" = "%lld posts";
"account.edit.about" = "About";
"account.edit.account-settings.bot" = "Bot account";
"account.edit.account-settings.discoverable" = "Discoverable";
"account.edit.account-settings.private" = "Private";
"account.edit.account-settings.section-title" = "Account settings";
"account.edit.display-name" = "Display Name";
"account.edit.error.save.message" = "Error while saving your profile, please try again.";
"account.edit.error.save.title" = "Error while saving your profile";
"account.edit.navigation-title" = "Edit Profile";
"account.edit.post-settings.privacy" = "Default privacy";
"account.edit.post-settings.section-title" = "Post settings";
"account.edit.post-settings.sensitive" = "Sensitive content";
"account.favorited-by" = "Favorited by";
"account.follow.follow" = "Follow";
"account.follow.following" = "Following";
"account.follow.requested" = "Requested";
"account.followers" = "Followers";
"account.following" = "Following";
"account.list.create" = "Create a new list";
"account.list.create.confirm" = "Create list";
"account.list.create.description" = "Enter the name for your list";
"account.list.delete" = "Delete list";
"account.list.name" = "List name";
"account.post.pinned" = "Pinned post";
"account.posts" = "Posts";
"account.relation.follows-you" = "Follows You";
// MARK: Package: Conversations
"conversations.action.delete" = "Delete";
"conversations.action.mark-read" = "Mark as read";
"conversations.empty.message" = "Looking for some social media love? You'll find all your direct messages and private mentions right here. Happy messaging! 📱❤️";
"conversations.empty.title" = "Inbox Zero";
"conversations.error.button" = "Retry";
"conversations.error.message" = "Error while loading your messages";
"conversations.error.title" = "An error occurred";
"conversations.navigation-title" = "Direct Messages";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld posts from %lld participants";
"design.theme.navigation-title" = "Theme Selector";
"design.theme.toots-preview" = "Toots preview";
// MARK: Package: Explore
"explore.navigation-title" = "Explore";
"explore.search.message-%@" = "From this screen you can search anything on %@";
"explore.search.prompt" = "Search users, posts and tags";
"explore.search.title" = "Search your instance";
"explore.section.posts" = "Posts";
"explore.section.suggested-users" = "Suggested Users";
"explore.section.tags" = "Tags";
"explore.section.trending.links" = "Trending Links";
"explore.section.trending.posts" = "Trending Posts";
"explore.section.trending.tags" = "Trending Tags";
"explore.section.users" = "Users";
// MARK: Package: Env
"env.poll-duration.5m" = "5 minutes";
"env.poll-duration.30m" = "30 minutes";
"env.poll-duration.1h" = "1 hour";
"env.poll-duration.6h" = "6 hours";
"env.poll-duration.1d" = "1 day";
"env.poll-duration.3d" = "3 days";
"env.poll-duration.7d" = "7 days";
"env.poll-vote-frequency.one" = "One Vote";
"env.poll-vote-frequency.multiple" = "Multiple Votes";
// MARK: Package: Lists
"lists.add-remove-%@" = "Add/Remove %@";
"lists.create" = "Create a new list";
"lists.create.confirm" = "Create list";
"lists.edit.users-in-list" = "Users in this list";
"lists.name" = "List name";
"lists.name.message" = "Enter the name for your list";
// MARK: Package: Notifications
"notifications.empty.message" = "Notifications? What notifications? Your notification inbox is looking so empty. Keep on being awesome! 📱😎";
"notifications.empty.title" = "No notifications";
"notifications.error.message" = "An error occured while loading your notifications, please retry.";
"notifications.error.title" = "An error occured";
"notifications.label.favorite" = "starred";
"notifications.label.follow" = "followed you";
"notifications.label.follow-request" = "request to follow you";
"notifications.label.mention" = "mentioned you";
"notifications.label.poll" = "poll ended";
"notifications.label.reblog" = "boosted";
"notifications.label.status" = "posted a status";
"notifications.label.update" = "edited a post";
"notifications.menu-title.favorite" = "Favorite";
"notifications.menu-title.follow" = "Follow";
"notifications.menu-title.follow-request" = "Follow Request";
"notifications.menu-title.mention" = "Mention";
"notifications.menu-title.poll" = "Poll";
"notifications.menu-title.reblog" = "Boost";
"notifications.menu-title.status" = "Post";
"notifications.menu-title.update" = "Post Edited";
"notifications.navigation-title" = "All Notifications";
"notifications.tab.all" = "All";
"notifications.tab.mentions" = "Mentions";
// MARK: Package: Timeline
"timeline.n-new-posts %lld" = "%lld new posts";
"timeline.federated" = "Federated";
"timeline.home" = "Home";
"timeline.local" = "Local";
"timeline.n-recent-from-n-participants %lld %lld" = "%lld recent posts from %lld participants";
"timeline.trending" = "Trending";
// MARK: Package: Status
"status.action.bookmark" = "Bookmark";
"status.action.boost" = "Boost";
"status.action.copy-text" = "Copy Text";
"status.action.delete" = "Delete";
"status.action.edit" = "Edit";
"status.action.favorite" = "Favorite";
"status.action.mention" = "Mention";
"status.action.message" = "Message";
"status.action.pin" = "Pin";
"status.action.post" = "Post";
"status.action.quote" = "Quote this post";
"status.action.reply" = "Reply";
"status.action.section.your-post" = "Your post";
"status.action.share" = "Share this post";
"status.action.unbookmark" = "Unbookmark";
"status.action.unboost" = "Unboost";
"status.action.unfavorite" = "Unfavorite";
"status.action.unpin" = "Unpin";
"status.action.view-in-browser" = "View in Browser";
"status.draft.delete" = "Delete Draft";
"status.draft.save" = "Save Draft";
"status.editor.ai-prompt.correct" = "Correct text";
"status.editor.ai-prompt.emphasize" = "Emphasize text";
"status.editor.ai-prompt.fit" = "Shorten text";
"status.editor.description.add" = "Add description";
"status.editor.description.edit" = "Edit description";
"status.editor.drafts.navigation-title" = "Drafts";
"status.editor.error.upload" = "Error uploading";
"status.editor.language-select.navigation-title" = "Select Language";
"status.editor.media.edit-image" = "Edit Image";
"status.editor.media.image-description" = "Image description";
"status.editor.mode.edit" = "Editing your post";
"status.editor.mode.new" = "New Post";
"status.editor.mode.quote-%@" = "Quote of %@";
"status.editor.mode.reply-%@" = "Replying to %@";
"status.editor.restore-previous" = "Restore previous text";
"status.editor.spoiler" = "Spoiler Text";
"status.editor.text.placeholder" = "What's on your mind?";
"status.editor.visibility" = "Post visibility";
"status.error.loading.message" = "An error occured while loading posts, please try again.";
"status.error.message" = "An error occured while this post context, please try again.";
"status.error.title" = "An error occured";
"status.filter.filtered-by-%@" = "Filtered by: %@";
"status.filter.show-anyway" = "Show anyway";
"status.image.alt-text.abbreviation" = "ALT";
"status.media.content.show" = "Show content";
"status.media.sensitive.show" = "Show sensitive content";
"status.poll.n-votes %lld" = "%lld votes";
"status.poll.closed" = "Closed";
"status.poll.closes-in" = "Closes in ";
"status.poll.duration" = "Poll Duration";
"status.poll.frequency" = "Polling Frequency";
"status.poll.option-n %lld" = "Option %lld";
"status.post-from-%@" = "Post from %@";
"status.row.was-boosted" = "boosted";
"status.row.was-reply" = "Replied to";
"status.row.you-boosted" = "You boosted";
"status.show-less" = "Show less";
"status.show-more" = "Show more";
"status.summary.at-time" = " at ";
"status.summary.n-boosts %lld" = "%lld boosts";
"status.summary.n-favorites %lld" = "%lld favorites";
"status.visibility.direct" = "Private";
"status.visibility.follower" = "Followers";
"status.visibility.public" = "Everyone";
"status.visibility.unlisted" = "Unlisted";

View file

@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "Account",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
],

View file

@ -54,7 +54,7 @@ struct AccountDetailHeaderView: View {
}
if viewModel.relationship?.followedBy == true {
Text("Follows You")
Text("account.relation.follows-you")
.font(.scaledFootnote)
.fontWeight(.semibold)
.padding(4)
@ -89,13 +89,13 @@ struct AccountDetailHeaderView: View {
scrollViewProxy?.scrollTo("status", anchor: .top)
}
} label: {
makeCustomInfoLabel(title: "Posts", count: account.statusesCount)
makeCustomInfoLabel(title: "account.posts", count: account.statusesCount)
}
NavigationLink(value: RouterDestinations.following(id: account.id)) {
makeCustomInfoLabel(title: "Following", count: account.followingCount)
makeCustomInfoLabel(title: "account.following", count: account.followingCount)
}
NavigationLink(value: RouterDestinations.followers(id: account.id)) {
makeCustomInfoLabel(title: "Followers", count: account.followersCount)
makeCustomInfoLabel(title: "account.followers", count: account.followersCount)
}
}.offset(y: 20)
}
@ -132,7 +132,7 @@ struct AccountDetailHeaderView: View {
.offset(y: -40)
}
private func makeCustomInfoLabel(title: String, count: Int) -> some View {
private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int) -> some View {
VStack {
Text("\(count)")
.font(.scaledHeadline)

View file

@ -156,9 +156,9 @@ public struct AccountDetailView: View {
isFieldsSheetDisplayed.toggle()
} label: {
VStack(alignment: .leading, spacing: 0) {
Text("About")
Text("account.detail.about")
.font(.scaledCallout)
Text("\(viewModel.fields.count) fields")
Text("account.detail.n-fields \(viewModel.fields.count)")
.font(.caption2)
}
}
@ -175,7 +175,7 @@ public struct AccountDetailView: View {
VStack(alignment: .leading, spacing: 0) {
Text("#\(tag.name)")
.font(.scaledCallout)
Text("\(tag.statusesCount) posts")
Text("account.detail.featured-tags-n-posts \(tag.statusesCountInt)")
.font(.caption2)
}
}.buttonStyle(.bordered)
@ -191,7 +191,7 @@ public struct AccountDetailView: View {
private var familiarFollowers: some View {
if !viewModel.familiarFollowers.isEmpty {
VStack(alignment: .leading, spacing: 2) {
Text("Also followed by")
Text("account.detail.familiar-followers")
.font(.scaledHeadline)
.padding(.leading, .layoutPadding)
ScrollView(.horizontal, showsIndicators: false) {
@ -234,7 +234,7 @@ public struct AccountDetailView: View {
}
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.navigationTitle("About")
.navigationTitle("account.detail.about")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
@ -284,14 +284,14 @@ public struct AccountDetailView: View {
.foregroundColor(theme.labelColor)
}
.contextMenu {
Button("Delete list", role: .destructive) {
Button("account.list.delete", role: .destructive) {
Task {
await currentAccount.deleteList(list: list)
}
}
}
}
Button("Create a new list") {
Button("account.list.create") {
isCreateListAlertPresented = true
}
.padding(.horizontal, .layoutPadding)
@ -299,13 +299,13 @@ public struct AccountDetailView: View {
.task {
await currentAccount.fetchLists()
}
.alert("Create a new list", isPresented: $isCreateListAlertPresented) {
TextField("List name", text: $createListTitle)
Button("Cancel") {
.alert("account.list.create", isPresented: $isCreateListAlertPresented) {
TextField("account.list.name", text: $createListTitle)
Button("action.cancel") {
isCreateListAlertPresented = false
createListTitle = ""
}
Button("Create List") {
Button("account.list.create.confirm") {
guard !createListTitle.isEmpty else { return }
isCreateListAlertPresented = false
Task {
@ -314,7 +314,7 @@ public struct AccountDetailView: View {
}
}
} message: {
Text("Enter the name for your list")
Text("account.list.create.description")
}
}
@ -323,7 +323,7 @@ public struct AccountDetailView: View {
if !viewModel.pinned.isEmpty {
ForEach(viewModel.pinned) { status in
VStack(alignment: .leading) {
Label("Pinned post", systemImage: "pin.fill")
Label("account.post.pinned", systemImage: "pin.fill")
.font(.scaledFootnote)
.foregroundColor(.gray)
.fontWeight(.semibold)
@ -359,12 +359,12 @@ public struct AccountDetailView: View {
routerPath.presentedSheet = .mentionStatusEditor(account: account,
visibility: preferences.serverPreferences?.postVisibility ?? .pub)
} label: {
Label("Mention", systemImage: "at")
Label("account.action.mention", systemImage: "at")
}
Button {
routerPath.presentedSheet = .mentionStatusEditor(account: account, visibility: .direct)
} label: {
Label("Message", systemImage: "tray.full")
Label("account.action.message", systemImage: "tray.full")
}
Divider()
}
@ -373,7 +373,7 @@ public struct AccountDetailView: View {
Button {
routerPath.presentedSheet = .listAddAccount(account: account)
} label: {
Label("Add/Remove from lists", systemImage: "list.bullet")
Label("account.action.add-remove-list", systemImage: "list.bullet")
}
}
@ -387,7 +387,7 @@ public struct AccountDetailView: View {
Button {
isEditingAccount = true
} label: {
Label("Edit Info", systemImage: "pencil")
Label("account.action.edit-info", systemImage: "pencil")
}
}
}

View file

@ -5,17 +5,17 @@ import SwiftUI
public enum AccountsListMode {
case following(accountId: String), followers(accountId: String)
case favouritedBy(statusId: String), rebloggedBy(statusId: String)
var title: String {
var title: LocalizedStringKey {
switch self {
case .following:
return "Following"
return "account.following"
case .followers:
return "Followers"
return "account.followers"
case .favouritedBy:
return "Favourited by"
return "account.favorited-by"
case .rebloggedBy:
return "Boosted by"
return "account.boosted-by"
}
}
}

View file

@ -23,16 +23,16 @@ struct EditAccountView: View {
}
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.navigationTitle("Edit Profile")
.navigationTitle("account.edit.navigation-title")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
toolbarContent
}
.alert("Error while saving your profile",
.alert("account.edit.error.save.title",
isPresented: $viewModel.saveError,
actions: {
Button("Ok", action: {})
}, message: { Text("Error while saving your profile, please try again.") })
Button("alert.button.ok", action: {})
}, message: { Text("account.edit.error.save.message") })
.task {
viewModel.client = client
await viewModel.fetchAccount()
@ -53,44 +53,44 @@ struct EditAccountView: View {
@ViewBuilder
private var aboutSections: some View {
Section("Display Name") {
TextField("Display Name", text: $viewModel.displayName)
Section("account.edit.display-name") {
TextField("account.edit.display-name", text: $viewModel.displayName)
}
.listRowBackground(theme.primaryBackgroundColor)
Section("About") {
TextField("About", text: $viewModel.note, axis: .vertical)
Section("account.edit.about") {
TextField("account.edit.about", text: $viewModel.note, axis: .vertical)
.frame(maxHeight: 150)
}
.listRowBackground(theme.primaryBackgroundColor)
}
private var postSettingsSection: some View {
Section("Post settings") {
Section("account.edit.post-settings.section-title") {
Picker(selection: $viewModel.postPrivacy) {
ForEach(Models.Visibility.supportDefault, id: \.rawValue) { privacy in
Text(privacy.title).tag(privacy)
}
} label: {
Label("Default privacy", systemImage: "lock")
Label("account.edit.post-settings.privacy", systemImage: "lock")
}
.pickerStyle(.menu)
Toggle(isOn: $viewModel.isSensitive) {
Label("Sensitive content", systemImage: "eye")
Label("account.edit.post-settings.sensitive", systemImage: "eye")
}
}
.listRowBackground(theme.primaryBackgroundColor)
}
private var accountSection: some View {
Section("Account settings") {
Section("account.edit.account-settings.section-title") {
Toggle(isOn: $viewModel.isLocked) {
Label("Private", systemImage: "lock")
Label("account.edit.account-settings.private", systemImage: "lock")
}
Toggle(isOn: $viewModel.isBot) {
Label("Bot account", systemImage: "laptopcomputer.trianglebadge.exclamationmark")
Label("account.edit.account-settings.bot", systemImage: "laptopcomputer.trianglebadge.exclamationmark")
}
Toggle(isOn: $viewModel.isDiscoverable) {
Label("Discoverable", systemImage: "magnifyingglass")
Label("account.edit.account-settings.discoverable", systemImage: "magnifyingglass")
}
}
.listRowBackground(theme.primaryBackgroundColor)
@ -99,7 +99,7 @@ struct EditAccountView: View {
@ToolbarContentBuilder
private var toolbarContent: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
Button("action.cancel") {
dismiss()
}
}
@ -114,7 +114,7 @@ struct EditAccountView: View {
if viewModel.isSaving {
ProgressView()
} else {
Text("Save")
Text("action.save")
}
}
}

View file

@ -70,9 +70,9 @@ public struct FollowButton: View {
}
} label: {
if viewModel.relationship.requested == true {
Text("Requested")
Text("account.follow.requested")
} else {
Text(viewModel.relationship.following ? "Following" : "Follow")
Text(viewModel.relationship.following ? "account.follow.following" : "account.follow.follow")
}
}
.buttonStyle(.bordered)

View file

@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "AppAccount",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
],

View file

@ -81,7 +81,7 @@ public struct AppAccountsSelectorView: View {
Button {
routerPath.presentedSheet = .addAccount
} label: {
Label("Add Account", systemImage: "person.badge.plus")
Label("app-account.button.add", systemImage: "person.badge.plus")
}
}
}

View file

@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "Conversations",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
],

View file

@ -81,7 +81,7 @@ struct ConversationsListRow: View {
await viewModel.markAsRead(conversation: conversation)
}
} label: {
Label("Mark as read", systemImage: "eye")
Label("conversations.action.mark-read", systemImage: "eye")
}
Button(role: .destructive) {
@ -89,7 +89,7 @@ struct ConversationsListRow: View {
await viewModel.delete(conversation: conversation)
}
} label: {
Label("Delete", systemImage: "trash")
Label("conversations.action.delete", systemImage: "trash")
}
}
}

View file

@ -40,12 +40,12 @@ public struct ConversationsListView: View {
}
} else if conversations.isEmpty && !viewModel.isLoadingFirstPage && !viewModel.isError {
EmptyView(iconName: "tray",
title: "Inbox Zero",
message: "Looking for some social media love? You'll find all your direct messages and private mentions right here. Happy messaging! 📱❤️")
title: "conversations.empty.title",
message: "conversations.empty.message")
} else if viewModel.isError {
ErrorView(title: "An error occurred",
message: "Error while loading your messages",
buttonTitle: "Retry") {
ErrorView(title: "conversations.error.title",
message: "conversations.error.message",
buttonTitle: "conversations.error.button") {
Task {
await viewModel.fetchConversations()
}
@ -56,7 +56,7 @@ public struct ConversationsListView: View {
}
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.navigationTitle("Direct Messages")
.navigationTitle("conversations.navigation-title")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
StatusEditorToolbarItem(visibility: .direct)

View file

@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "DesignSystem",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
],

View file

@ -14,9 +14,9 @@ public class Theme: ObservableObject {
public var description: LocalizedStringKey {
switch self {
case .leading:
return "Leading"
return "enum.avatar-position.leading"
case .top:
return "Top"
return "enum.avatar-position.top"
}
}
}
@ -27,9 +27,9 @@ public class Theme: ObservableObject {
public var description: LocalizedStringKey {
switch self {
case .circle:
return "Circle"
return "enum.avatar-shape.circle"
case .rounded:
return "Rounded"
return "enum.avatar-shape.rounded"
}
}
}
@ -40,11 +40,11 @@ public class Theme: ObservableObject {
public var description: LocalizedStringKey {
switch self {
case .full:
return "All"
return "enum.status-actions-display.all"
case .discret:
return "Only buttons"
return "enum.status-actions-display.only-buttons"
case .none:
return "No buttons"
return "enum.status-actions-display.no-buttons"
}
}
}
@ -55,9 +55,9 @@ public class Theme: ObservableObject {
public var description: LocalizedStringKey {
switch self {
case .large:
return "Large"
return "enum.status-display-style.large"
case .compact:
return "Compact"
return "enum.status-display-style.compact"
}
}
}

View file

@ -2,10 +2,10 @@ import SwiftUI
public struct EmptyView: View {
public let iconName: String
public let title: String
public let message: String
public init(iconName: String, title: String, message: String) {
public let title: LocalizedStringKey
public let message: LocalizedStringKey
public init(iconName: String, title: LocalizedStringKey, message: LocalizedStringKey) {
self.iconName = iconName
self.title = title
self.message = message

View file

@ -1,12 +1,12 @@
import SwiftUI
public struct ErrorView: View {
public let title: String
public let message: String
public let buttonTitle: String
public let title: LocalizedStringKey
public let message: LocalizedStringKey
public let buttonTitle: LocalizedStringKey
public let onButtonPress: () -> Void
public init(title: String, message: String, buttonTitle: String, onButtonPress: @escaping (() -> Void)) {
public init(title: LocalizedStringKey, message: LocalizedStringKey, buttonTitle: LocalizedStringKey, onButtonPress: @escaping (() -> Void)) {
self.title = title
self.message = message
self.buttonTitle = buttonTitle

View file

@ -16,7 +16,7 @@ public struct TagRowView: View {
VStack(alignment: .leading) {
Text("#\(tag.name)")
.font(.scaledHeadline)
Text("\(tag.totalUses) posts from \(tag.totalAccounts) participants")
Text("design.tag.n-posts-from-n-participants \(tag.totalUses) \(tag.totalAccounts)")
.font(.scaledFootnote)
.foregroundColor(.gray)
}

View file

@ -30,7 +30,7 @@ public struct ThemePreviewView: View {
.padding(4)
.frame(maxHeight: .infinity)
.background(theme.primaryBackgroundColor)
.navigationTitle("Theme Selector")
.navigationTitle("design.theme.navigation-title")
}
}
@ -54,8 +54,8 @@ struct ThemeBoxView: View {
.foregroundColor(color.tintColor)
.font(.system(size: 20))
.fontWeight(.bold)
Text("Toots preview")
Text("design.theme.toots-preview")
.foregroundColor(color.labelColor)
.frame(maxWidth: .infinity)
.padding()

View file

@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "Env",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
],

View file

@ -1,4 +1,5 @@
import Foundation
import SwiftUI
public enum PollDuration: Int, CaseIterable {
// rawValue == time in seconds; used for sending to the API
@ -10,22 +11,22 @@ public enum PollDuration: Int, CaseIterable {
case threeDays = 259_200
case sevenDays = 604_800
public var displayString: String {
public var displayString: LocalizedStringKey {
switch self {
case .fiveMinutes: return "5 minutes"
case .halfAnHour: return "30 minutes"
case .oneHour: return "1 hour"
case .sixHours: return "6 hours"
case .oneDay: return "1 day"
case .threeDays: return "3 days"
case .sevenDays: return "7 days"
case .fiveMinutes: return "env.poll-duration.5m"
case .halfAnHour: return "env.poll-duration.30m"
case .oneHour: return "env.poll-duration.1h"
case .sixHours: return "env.poll-duration.6h"
case .oneDay: return "env.poll-duration.1d"
case .threeDays: return "env.poll-duration.3d"
case .sevenDays: return "env.poll-duration.7d"
}
}
}
public enum PollVotingFrequency: String, CaseIterable {
case oneVote = "One Vote"
case multipleVotes = "Multiple Votes"
case oneVote = "one-vote"
case multipleVotes = "multiple-votes"
public var canVoteMultipleTimes: Bool {
switch self {
@ -33,4 +34,11 @@ public enum PollVotingFrequency: String, CaseIterable {
case .oneVote: return false
}
}
public var displayString: LocalizedStringKey {
switch self {
case .oneVote: return "env.poll-vote-frequency.one"
case .multipleVotes: return "env.poll-vote-frequency.multiple"
}
}
}

View file

@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "Explore",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
],

View file

@ -28,8 +28,8 @@ public struct ExploreView: View {
loadingView
} else if viewModel.allSectionsEmpty {
EmptyView(iconName: "magnifyingglass",
title: "Search your instance",
message: "From this screen you can search anything on \(client.server)")
title: "explore.search.title",
message: "explore.search.message-\(client.server)")
.listRowBackground(theme.secondaryBackgroundColor)
} else {
if !viewModel.trendingTags.isEmpty {
@ -58,11 +58,11 @@ public struct ExploreView: View {
.listStyle(.grouped)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.navigationTitle("Explore")
.navigationTitle("explore.navigation-title")
.searchable(text: $viewModel.searchQuery,
tokens: $viewModel.tokens,
suggestedTokens: $viewModel.suggestedToken,
prompt: Text("Search users, posts and tags"),
prompt: Text("explore.search.prompt"),
token: { token in
Text(token.rawValue)
})
@ -81,7 +81,7 @@ public struct ExploreView: View {
@ViewBuilder
private func makeSearchResultsView(results: SearchResults) -> some View {
if !results.accounts.isEmpty {
Section("Users") {
Section("explore.section.users") {
ForEach(results.accounts) { account in
if let relationship = results.relationships.first(where: { $0.id == account.id }) {
AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
@ -91,7 +91,7 @@ public struct ExploreView: View {
}
}
if !results.hashtags.isEmpty {
Section("Tags") {
Section("explore.section.tags") {
ForEach(results.hashtags) { tag in
TagRowView(tag: tag)
.listRowBackground(theme.primaryBackgroundColor)
@ -100,7 +100,7 @@ public struct ExploreView: View {
}
}
if !results.statuses.isEmpty {
Section("Posts") {
Section("explore.section.posts") {
ForEach(results.statuses) { status in
StatusRowView(viewModel: .init(status: status))
.listRowBackground(theme.primaryBackgroundColor)
@ -111,7 +111,7 @@ public struct ExploreView: View {
}
private var suggestedAccountsSection: some View {
Section("Suggested Users") {
Section("explore.section.suggested-users") {
ForEach(viewModel.suggestedAccounts
.prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count)) { account in
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
@ -131,10 +131,10 @@ public struct ExploreView: View {
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.listStyle(.plain)
.navigationTitle("Suggested Users")
.navigationTitle("explore.section.suggested-users")
.navigationBarTitleDisplayMode(.inline)
} label: {
Text("See more")
Text("see-more")
.foregroundColor(theme.tintColor)
}
.listRowBackground(theme.primaryBackgroundColor)
@ -142,7 +142,7 @@ public struct ExploreView: View {
}
private var trendingTagsSection: some View {
Section("Trending Tags") {
Section("explore.section.trending.tags") {
ForEach(viewModel.trendingTags
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) { tag in
TagRowView(tag: tag)
@ -160,10 +160,10 @@ public struct ExploreView: View {
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.listStyle(.plain)
.navigationTitle("Trending Tags")
.navigationTitle("explore.section.trending.tags")
.navigationBarTitleDisplayMode(.inline)
} label: {
Text("See more")
Text("see-more")
.foregroundColor(theme.tintColor)
}
.listRowBackground(theme.primaryBackgroundColor)
@ -171,7 +171,7 @@ public struct ExploreView: View {
}
private var trendingPostsSection: some View {
Section("Trending Posts") {
Section("explore.section.trending.posts") {
ForEach(viewModel.trendingStatuses
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in
StatusRowView(viewModel: .init(status: status, isCompact: false))
@ -190,10 +190,10 @@ public struct ExploreView: View {
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.listStyle(.plain)
.navigationTitle("Trending Posts")
.navigationTitle("explore.section.trending.posts")
.navigationBarTitleDisplayMode(.inline)
} label: {
Text("See more")
Text("see-more")
.foregroundColor(theme.tintColor)
}
.listRowBackground(theme.primaryBackgroundColor)
@ -201,7 +201,7 @@ public struct ExploreView: View {
}
private var trendingLinksSection: some View {
Section("Trending Links") {
Section("explore.section.trending.links") {
ForEach(viewModel.trendingLinks
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in
StatusCardView(card: card)
@ -219,10 +219,10 @@ public struct ExploreView: View {
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.listStyle(.plain)
.navigationTitle("Trending Links")
.navigationTitle("explore.section.trending.links")
.navigationBarTitleDisplayMode(.inline)
} label: {
Text("See more")
Text("see-more")
.foregroundColor(theme.tintColor)
}
.listRowBackground(theme.primaryBackgroundColor)

View file

@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "Lists",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
],

View file

@ -39,29 +39,29 @@ public struct ListAddAccountView: View {
}
.listRowBackground(theme.primaryBackgroundColor)
}
Button("Create a new list") {
Button("lists.create") {
isCreateListAlertPresented = true
}
.listRowBackground(theme.primaryBackgroundColor)
}
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.navigationTitle("Add/Remove \(viewModel.account.displayName)")
.navigationTitle("lists.add-remove-\(viewModel.account.displayName)")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem {
Button("Done") {
Button("action.done") {
dismiss()
}
}
}
.alert("Create a new list", isPresented: $isCreateListAlertPresented) {
TextField("List name", text: $createListTitle)
Button("Cancel") {
.alert("lists.create", isPresented: $isCreateListAlertPresented) {
TextField("lists.name", text: $createListTitle)
Button("action.cancel") {
isCreateListAlertPresented = false
createListTitle = ""
}
Button("Create List") {
Button("lists.create.confirm") {
guard !createListTitle.isEmpty else { return }
isCreateListAlertPresented = false
Task {
@ -70,7 +70,7 @@ public struct ListAddAccountView: View {
}
}
} message: {
Text("Enter the name for your list")
Text("lists.name.message")
}
}
.task {

View file

@ -18,7 +18,7 @@ public struct ListEditView: View {
public var body: some View {
NavigationStack {
List {
Section("Users in this list") {
Section("lists.edit.users-in-list") {
if viewModel.isLoadingAccounts {
HStack {
Spacer()
@ -54,7 +54,7 @@ public struct ListEditView: View {
.background(theme.secondaryBackgroundColor)
.toolbar {
ToolbarItem {
Button("Done") {
Button("action.done") {
dismiss()
}
}

View file

@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "Models",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
],

View file

@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "Network",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
],

View file

@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "Notifications",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
],

View file

@ -1,24 +1,25 @@
import Models
import SwiftUI
extension Models.Notification.NotificationType {
func label() -> String {
func label() -> LocalizedStringKey {
switch self {
case .status:
return "posted a status"
return "notifications.label.status"
case .mention:
return "mentioned you"
return "notifications.label.mention"
case .reblog:
return "boosted"
return "notifications.label.reblog"
case .follow:
return "followed you"
return "notifications.label.follow"
case .follow_request:
return "request to follow you"
return "notifications.label.follow-request"
case .favourite:
return "starred"
return "notifications.label.favorite"
case .poll:
return "poll ended"
return "notifications.label.poll"
case .update:
return "edited a post"
return "notifications.label.update"
}
}
@ -40,25 +41,25 @@ extension Models.Notification.NotificationType {
return "pencil.line"
}
}
func menuTitle() -> String {
func menuTitle() -> LocalizedStringKey {
switch self {
case .status:
return "Post"
return "notifications.menu-title.status"
case .mention:
return "Mentions"
return "notifications.menu-title.mention"
case .reblog:
return "Boost"
return "notifications.menu-title.reblog"
case .follow:
return "Follow"
return "notifications.menu-title.follow"
case .follow_request:
return "Follow Request"
return "notifications.menu-title.follow-request"
case .favourite:
return "Favorite"
return "notifications.menu-title.favorite"
case .poll:
return "Poll"
return "notifications.menu-title.poll"
case .update:
return "Post Edited"
return "notifications.menu-title.update"
}
}
}

View file

@ -27,7 +27,7 @@ public struct NotificationsListView: View {
.padding(.top, .layoutPadding + 16)
.background(theme.primaryBackgroundColor)
}
.navigationTitle(lockedType?.menuTitle() ?? viewModel.selectedType?.menuTitle() ?? "All Notifications")
.navigationTitle(lockedType?.menuTitle() ?? viewModel.selectedType?.menuTitle() ?? "notifications.navigation-title")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
if lockedType == nil {
@ -35,7 +35,7 @@ public struct NotificationsListView: View {
Button {
viewModel.selectedType = nil
} label: {
Label("All Notifications", systemImage: "bell.fill")
Label("notifications.navigation-title", systemImage: "bell.fill")
}
Divider()
ForEach(Notification.NotificationType.allCases, id: \.self) { type in
@ -96,8 +96,8 @@ public struct NotificationsListView: View {
case let .display(notifications, nextPageState):
if notifications.isEmpty {
EmptyView(iconName: "bell.slash",
title: "No notifications",
message: "Notifications? What notifications? Your notification inbox is looking so empty. Keep on being awesome! 📱😎")
title: "notifications.empty.title",
message: "notifications.empty.message")
} else {
ForEach(notifications) { notification in
if notification.supportedType != nil {
@ -127,9 +127,9 @@ public struct NotificationsListView: View {
}
case .error:
ErrorView(title: "An error occurred",
message: "An error occurred while loading your notifications, please retry.",
buttonTitle: "Retry") {
ErrorView(title: "notifications.error.title",
message: "notifications.error.message",
buttonTitle: "action.retry") {
Task {
await viewModel.fetchNotifications()
}

View file

@ -15,9 +15,9 @@ class NotificationsViewModel: ObservableObject {
case error(error: Error)
}
public enum Tab: String, CaseIterable {
case all = "All"
case mentions = "Mentions"
public enum Tab: LocalizedStringKey, CaseIterable {
case all = "notifications.tab.all"
case mentions = "notifications.tab.mentions"
}
var client: Client? {

View file

@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "Status",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
],

View file

@ -63,9 +63,9 @@ public struct StatusDetailView: View {
}
case .error:
ErrorView(title: "An error occurred",
message: "An error occurred while this post context, please try again.",
buttonTitle: "Retry") {
ErrorView(title: "status.error.title",
message: "status.error.message",
buttonTitle: "action.retry") {
Task {
await viewModel.fetch()
}

View file

@ -15,8 +15,8 @@ class StatusDetailViewModel: ObservableObject {
}
@Published var state: State = .loading
@Published var title: String = ""
@Published var title: LocalizedStringKey = ""
init(statusId: String) {
state = .loading
self.statusId = statusId
@ -65,7 +65,7 @@ class StatusDetailViewModel: ObservableObject {
do {
let data = try await fetchContextData(client: client, statusId: statusId)
state = .display(status: data.status, context: data.context)
title = "Post from \(data.status.account.displayNameWithoutEmojis)"
title = "status.post-from-\(data.status.account.displayNameWithoutEmojis)"
} catch {
state = .error(error: error)
}

View file

@ -9,11 +9,11 @@ enum StatusEditorAIPrompts: CaseIterable {
var label: some View {
switch self {
case .correct:
Label("Correct text", systemImage: "text.badge.checkmark")
Label("status.editor.ai-prompt.correct", systemImage: "text.badge.checkmark")
case .fit:
Label("Shorten text", systemImage: "text.badge.minus")
Label("status.editor.ai-prompt.fit", systemImage: "text.badge.minus")
case .emphasize:
Label("Emphasize text", systemImage: "text.badge.star")
Label("status.editor.ai-prompt.emphasize", systemImage: "text.badge.star")
}
}

View file

@ -127,10 +127,10 @@ struct StatusEditorAccessoryView: View {
.searchable(text: $languageSearch)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel", action: { isLanguageSheetDisplayed = false })
Button("action.cancel", action: { isLanguageSheetDisplayed = false })
}
}
.navigationTitle("Select Languages")
.navigationTitle("status.editor.language-select.navigation-title")
.navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
@ -157,12 +157,12 @@ struct StatusEditorAccessoryView: View {
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel", action: { isDraftsSheetDisplayed = false })
Button("action.cancel", action: { isDraftsSheetDisplayed = false })
}
}
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.navigationTitle("Drafts")
.navigationTitle("status.editor.drafts.navigation-title")
.navigationBarTitleDisplayMode(.inline)
}
.presentationDetents([.medium])

View file

@ -15,7 +15,7 @@ struct StatusEditorMediaEditView: View {
NavigationStack {
Form {
Section {
TextField("Image description", text: $imageDescription, axis: .horizontal)
TextField("status.editor.media.image-description", text: $imageDescription, axis: .horizontal)
}
.listRowBackground(theme.primaryBackgroundColor)
Section {
@ -45,11 +45,11 @@ struct StatusEditorMediaEditView: View {
.onAppear {
imageDescription = container.mediaAttachment?.description ?? ""
}
.navigationTitle("Edit Image")
.navigationTitle("status.editor.media.edit-image")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
Button("action.done") {
if !imageDescription.isEmpty {
Task {
await viewModel.addDescription(container: container, description: imageDescription)
@ -60,7 +60,7 @@ struct StatusEditorMediaEditView: View {
}
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
Button("action.cancel") {
dismiss()
}
}

View file

@ -47,14 +47,14 @@ struct StatusEditorMediaView: View {
.cornerRadius(8)
if container.error != nil {
VStack {
Text("Error uploading")
Text("status.editor.error.upload")
Button {
withAnimation {
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
}
} label: {
VStack {
Text("Delete")
Text("action.delete")
}
}
.buttonStyle(.bordered)
@ -64,7 +64,7 @@ struct StatusEditorMediaView: View {
}
} label: {
VStack {
Text("Retry")
Text("action.retry")
}
}
.buttonStyle(.bordered)
@ -97,7 +97,7 @@ struct StatusEditorMediaView: View {
editingContainer = container
} label: {
Label(container.mediaAttachment?.description?.isEmpty == false ?
"Edit description" : "Add description",
"status.editor.description.edit" : "status.editor.description.add",
systemImage: "pencil.line")
}
}
@ -106,13 +106,13 @@ struct StatusEditorMediaView: View {
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
}
} label: {
Label("Delete", systemImage: "trash")
Label("action.delete", systemImage: "trash")
}
}
private var altMarker: some View {
Button {} label: {
Text("ALT")
Text("status.image.alt-text.abbreviation")
.font(.caption2)
}
.padding(4)

View file

@ -25,7 +25,7 @@ struct StatusEditorPollView: View {
ForEach(0 ..< count, id: \.self) { index in
VStack {
HStack(spacing: 16) {
TextField("Option \(index + 1)", text: $viewModel.pollOptions[index])
TextField("status.poll.option-n \(index + 1)", text: $viewModel.pollOptions[index])
.textFieldStyle(.roundedBorder)
.focused($focused, equals: .option(index))
.onTapGesture {
@ -66,9 +66,9 @@ struct StatusEditorPollView: View {
}
HStack {
Picker("Polling Frequency", selection: $viewModel.pollVotingFrequency) {
Picker("status.poll.frequency", selection: $viewModel.pollVotingFrequency) {
ForEach(PollVotingFrequency.allCases, id: \.rawValue) {
Text($0.rawValue)
Text($0.displayString)
.tag($0)
}
}
@ -76,7 +76,7 @@ struct StatusEditorPollView: View {
Spacer()
Picker("Poll Duration", selection: $viewModel.pollDuration) {
Picker("status.poll.duration", selection: $viewModel.pollDuration) {
ForEach(PollDuration.allCases, id: \.rawValue) {
Text($0.displayString)
.tag($0)

View file

@ -38,7 +38,7 @@ public struct StatusEditorView: View {
accountHeaderView
.padding(.horizontal, .layoutPadding)
TextView($viewModel.statusText, $viewModel.selectedRange)
.placeholder("What's on your mind")
.placeholder(String(localized: "status.editor.text.placeholder"))
.font(Font.scaledBodyUIFont)
.padding(.horizontal, .layoutPadding)
StatusEditorMediaView(viewModel: viewModel)
@ -110,7 +110,7 @@ public struct StatusEditorView: View {
if viewModel.isPosting {
ProgressView()
} else {
Text("Post")
Text("status.action.post")
}
}
.disabled(!viewModel.canPost)
@ -126,24 +126,24 @@ public struct StatusEditorView: View {
object: nil)
}
} label: {
Text("Cancel")
Text("action.cancel")
}
.keyboardShortcut(.cancelAction)
.confirmationDialog("",
isPresented: $isDismissAlertPresented,
actions: {
Button("Delete Draft", role: .destructive) {
Button("status.draft.delete", role: .destructive) {
dismiss()
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
object: nil)
}
Button("Save Draft") {
Button("status.draft.save") {
preferences.draftsPosts.insert(viewModel.statusText.string, at: 0)
dismiss()
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
object: nil)
}
Button("Cancel", role: .cancel) {}
Button("action.cancel", role: .cancel) {}
})
}
}
@ -155,7 +155,7 @@ public struct StatusEditorView: View {
private var spoilerTextView: some View {
if viewModel.spoilerOn {
VStack {
TextField("Spoiler Text", text: $viewModel.spoilerText)
TextField("status.editor.spoiler", text: $viewModel.spoilerText)
.focused($isSpoilerTextFocused)
.padding(.horizontal, .layoutPadding)
}
@ -185,7 +185,7 @@ public struct StatusEditorView: View {
private var privacyMenu: some View {
Menu {
Section("Post visibility") {
Section("status.editor.visibility") {
ForEach(Models.Visibility.allCases, id: \.self) { visibility in
Button {
viewModel.visibility = visibility
@ -226,7 +226,7 @@ public struct StatusEditorView: View {
viewModel.replaceTextWith(text: backup.string)
viewModel.backupStatusText = nil
} label: {
Label("Restore previous text", systemImage: "arrow.uturn.right")
Label("status.editor.restore-previous", systemImage: "arrow.uturn.right")
}
}
} label: {

View file

@ -1,13 +1,14 @@
import Models
import SwiftUI
import UIKit
public extension StatusEditorViewModel {
enum Mode {
case replyTo(status: Status)
case new(visibility: Visibility)
case new(visibility: Models.Visibility)
case edit(status: Status)
case quote(status: Status)
case mention(account: Account, visibility: Visibility)
case mention(account: Account, visibility: Models.Visibility)
case shareExtension(items: [NSItemProvider])
var isInShareExtension: Bool {
@ -36,17 +37,17 @@ public extension StatusEditorViewModel {
return nil
}
}
var title: String {
var title: LocalizedStringKey {
switch self {
case .new, .mention, .shareExtension:
return "New Post"
return "status.editor.mode.new"
case .edit:
return "Editing your post"
return "status.editor.mode.edit"
case let .replyTo(status):
return "Replying to \(status.reblog?.account.displayNameWithoutEmojis ?? status.account.displayNameWithoutEmojis)"
return "status.editor.mode.reply-\(status.reblog?.account.displayNameWithoutEmojis ?? status.account.displayNameWithoutEmojis)"
case let .quote(status):
return "Quote of \(status.reblog?.account.displayNameWithoutEmojis ?? status.account.displayNameWithoutEmojis)"
return "status.editor.mode.quote-\(status.reblog?.account.displayNameWithoutEmojis ?? status.account.displayNameWithoutEmojis)"
}
}
}

View file

@ -1,7 +1,8 @@
import Models
import SwiftUI
public extension Visibility {
static var supportDefault: [Visibility] {
public extension Models.Visibility {
static var supportDefault: [Self] {
[.pub, .priv, .unlisted]
}
@ -18,16 +19,16 @@ public extension Visibility {
}
}
var title: String {
var title: LocalizedStringKey {
switch self {
case .pub:
return "Everyone"
return "status.visibility.public"
case .unlisted:
return "Unlisted"
return "status.visibility.unlisted"
case .priv:
return "Followers"
return "status.visibility.follower"
case .direct:
return "Private"
return "status.visibility.direct"
}
}
}

View file

@ -25,9 +25,9 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
.padding(.vertical, .dividerPadding)
}
case .error:
ErrorView(title: "An error occurred",
message: "An error occurred while loading posts, please try again.",
buttonTitle: "Retry") {
ErrorView(title: "status.error.title",
message: "status.error.loading.message",
buttonTitle: "action.retry") {
Task {
await fetcher.fetchStatuses()
}

View file

@ -65,12 +65,12 @@ public struct StatusPollView: View {
private var footerView: some View {
HStack(spacing: 0) {
Text("\(viewModel.poll.votesCount) votes")
Text("status.poll.n-votes \(viewModel.poll.votesCount)")
Text("")
if viewModel.poll.expired {
Text("Closed")
Text("status.poll.closed")
} else {
Text("Close in ")
Text("status.poll.closes-in")
Text(viewModel.poll.expiresAt.asDate, style: .timer)
}
}

View file

@ -101,7 +101,7 @@ struct StatusActionsView: View {
Divider()
HStack {
Text(viewModel.status.createdAt.asDate, style: .date) +
Text(" at ") +
Text("status.summary.at-time") +
Text(viewModel.status.createdAt.asDate, style: .time) +
Text(" ·")
Image(systemName: viewModel.status.visibility.iconName)
@ -120,7 +120,7 @@ struct StatusActionsView: View {
if viewModel.favouritesCount > 0 {
Divider()
NavigationLink(value: RouterDestinations.favouritedBy(id: viewModel.status.id)) {
Text("\(viewModel.favouritesCount) favorites")
Text("status.summary.n-favorites \(viewModel.favouritesCount)")
.font(.scaledCallout)
Spacer()
Image(systemName: "chevron.right")
@ -129,7 +129,7 @@ struct StatusActionsView: View {
if viewModel.reblogsCount > 0 {
Divider()
NavigationLink(value: RouterDestinations.rebloggedBy(id: viewModel.status.id)) {
Text("\(viewModel.reblogsCount) boosts")
Text("status.summary.n-boosts \(viewModel.reblogsCount)")
.font(.scaledCallout)
Spacer()
Image(systemName: "chevron.right")

View file

@ -108,9 +108,9 @@ public struct StatusMediaPreviewView: View {
.transition(.opacity)
}
}
.alert("Image description",
.alert("status.editor.media.image-description",
isPresented: $isAltAlertDisplayed) {
Button("Ok", action: {})
Button("alert.button.ok", action: {})
} message: {
Text(altTextDisplayed ?? "")
}
@ -167,7 +167,7 @@ public struct StatusMediaPreviewView: View {
altTextDisplayed = alt
isAltAlertDisplayed = true
} label: {
Text("ALT")
Text("status.image.alt-text.abbreviation")
}
.padding(8)
.background(.thinMaterial)
@ -233,7 +233,7 @@ public struct StatusMediaPreviewView: View {
altTextDisplayed = alt
isAltAlertDisplayed = true
} label: {
Text("ALT")
Text("status.image.alt-text.abbreviation")
.font(.scaledFootnote)
}
.padding(4)
@ -289,9 +289,9 @@ public struct StatusMediaPreviewView: View {
}
} label: {
if sensitive {
Label("Show sensitive content", systemImage: "eye")
Label("status.media.sensitive.show", systemImage: "eye")
} else {
Label("Show content", systemImage: "eye")
Label("status.media.content.show", systemImage: "eye")
}
}
.buttonStyle(.borderedProminent)

View file

@ -19,7 +19,7 @@ struct StatusRowContextMenu: View {
await viewModel.favourite()
}
} } label: {
Label(viewModel.isFavourited ? "Unfavorite" : "Favorite", systemImage: "star")
Label(viewModel.isFavourited ? "status.action.unfavorite" : "status.action.favorite", systemImage: "star")
}
Button { Task {
if viewModel.isReblogged {
@ -28,7 +28,7 @@ struct StatusRowContextMenu: View {
await viewModel.reblog()
}
} } label: {
Label(viewModel.isReblogged ? "Unboost" : "Boost", systemImage: "arrow.left.arrow.right.circle")
Label(viewModel.isReblogged ? "status.action.unboost" : "status.action.boost", systemImage: "arrow.left.arrow.right.circle")
}
Button { Task {
if viewModel.isBookmarked {
@ -37,13 +37,13 @@ struct StatusRowContextMenu: View {
await viewModel.bookmark()
}
} } label: {
Label(viewModel.isReblogged ? "Unbookmark" : "Bookmark",
Label(viewModel.isReblogged ? "status.action.unbookmark" : "status.action.bookmark",
systemImage: "bookmark")
}
Button {
routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.status)
} label: {
Label("Reply", systemImage: "arrowshape.turn.up.left")
Label("status.action.reply", systemImage: "arrowshape.turn.up.left")
}
}
@ -51,7 +51,7 @@ struct StatusRowContextMenu: View {
Button {
routerPath.presentedSheet = .quoteStatusEditor(status: viewModel.status)
} label: {
Label("Quote this post", systemImage: "quote.bubble")
Label("status.action.quote", systemImage: "quote.bubble")
}
}
@ -59,24 +59,24 @@ struct StatusRowContextMenu: View {
if let url = viewModel.status.reblog?.url ?? viewModel.status.url {
ShareLink(item: url) {
Label("Share this post", systemImage: "square.and.arrow.up")
Label("status.action.share", systemImage: "square.and.arrow.up")
}
}
if let url = URL(string: viewModel.status.reblog?.url ?? viewModel.status.url ?? "") {
Button { openURL(url) } label: {
Label("View in Browser", systemImage: "safari")
Label("status.action.view-in-browser", systemImage: "safari")
}
}
Button {
UIPasteboard.general.string = viewModel.status.content.asRawText
} label: {
Label("Copy Text", systemImage: "doc.on.doc")
Label("status.action.copy-text", systemImage: "doc.on.doc")
}
if account.account?.id == viewModel.status.account.id {
Section("Your post") {
Section("status.action.section.your-post") {
Button {
Task {
if viewModel.isPinned {
@ -86,15 +86,15 @@ struct StatusRowContextMenu: View {
}
}
} label: {
Label(viewModel.isPinned ? "Unpin" : "Pin", systemImage: viewModel.isPinned ? "pin.fill" : "pin")
Label(viewModel.isPinned ? "status.action.unpin" : "status.action.pin", systemImage: viewModel.isPinned ? "pin.fill" : "pin")
}
Button {
routerPath.presentedSheet = .editStatusEditor(status: viewModel.status)
} label: {
Label("Edit", systemImage: "pencil")
Label("status.action.edit", systemImage: "pencil")
}
Button(role: .destructive) { Task { await viewModel.delete() } } label: {
Label("Delete", systemImage: "trash")
Label("status.action.delete", systemImage: "trash")
}
}
} else if !viewModel.isRemote {
@ -102,12 +102,12 @@ struct StatusRowContextMenu: View {
Button {
routerPath.presentedSheet = .mentionStatusEditor(account: viewModel.status.account, visibility: .pub)
} label: {
Label("Mention", systemImage: "at")
Label("status.action.mention", systemImage: "at")
}
Button {
routerPath.presentedSheet = .mentionStatusEditor(account: viewModel.status.account, visibility: .direct)
} label: {
Label("Message", systemImage: "tray.full")
Label("status.action.message", systemImage: "tray.full")
}
}
}

View file

@ -82,13 +82,13 @@ public struct StatusRowView: View {
private func makeFilterView(filter: Filter) -> some View {
HStack {
Text("Filtered by: \(filter.title)")
Text("status.filter.filtered-by-\(filter.title)")
Button {
withAnimation {
viewModel.isFiltered = false
}
} label: {
Text("Show anyway")
Text("status.filter.show-anyway")
}
}
}
@ -101,9 +101,9 @@ public struct StatusRowView: View {
AvatarView(url: viewModel.status.account.avatar, size: .boost)
if viewModel.status.account.username != account.account?.username {
EmojiTextApp(viewModel.status.account.safeDisplayName.asMarkdown, emojis: viewModel.status.account.emojis)
Text("boosted")
Text("status.row.was-boosted")
} else {
Text("You boosted")
Text("status.row.you-boosted")
}
}
.font(.scaledFootnote)
@ -128,7 +128,7 @@ public struct StatusRowView: View {
{
HStack(spacing: 2) {
Image(systemName: "arrowshape.turn.up.left.fill")
Text("Replied to")
Text("status.row.was-reply")
Text(mention.username)
}
.font(.scaledFootnote)
@ -185,7 +185,7 @@ public struct StatusRowView: View {
viewModel.displaySpoiler.toggle()
}
} label: {
Text(viewModel.displaySpoiler ? "Show more" : "Show less")
Text(viewModel.displaySpoiler ? "status.show-more" : "status.show-less")
}
.buttonStyle(.bordered)
}

View file

@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "Timeline",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
],

View file

@ -1,11 +1,12 @@
import Foundation
import Models
import Network
import SwiftUI
public enum TimelineFilter: Hashable, Equatable {
case home, local, federated, trending
case hashtag(tag: String, accountId: String?)
case list(list: List)
case list(list: Models.List)
case remoteLocal(server: String)
public func hash(into hasher: inout Hasher) {
@ -37,7 +38,26 @@ public enum TimelineFilter: Hashable, Equatable {
return server
}
}
public func localizedTitle() -> LocalizedStringKey {
switch self {
case .federated:
return "timeline.federated"
case .local:
return "timeline.local"
case .trending:
return "timeline.trending"
case .home:
return "timeline.home"
case let .hashtag(tag, _):
return "#\(tag)"
case let .list(list):
return LocalizedStringKey(list.title)
case let .remoteLocal(server):
return LocalizedStringKey(server)
}
}
public func iconName() -> String? {
switch self {
case .federated:

View file

@ -59,7 +59,7 @@ public struct TimelineView: View {
scrollProxy = proxy
}
}
.navigationTitle(timeline.title())
.navigationTitle(timeline.localizedTitle())
.navigationBarTitleDisplayMode(.inline)
.onAppear {
if viewModel.client == nil {
@ -143,7 +143,7 @@ public struct TimelineView: View {
VStack(alignment: .leading, spacing: 4) {
Text("#\(tag.name)")
.font(.scaledHeadline)
Text("\(tag.totalUses) recent posts from \(tag.totalAccounts) participants")
Text("timeline.n-recent-from-n-participants \(tag.totalUses) \(tag.totalAccounts)")
.font(.scaledFootnote)
.foregroundColor(.gray)
}
@ -157,7 +157,7 @@ public struct TimelineView: View {
}
}
} label: {
Text(tag.following ? "Following" : "Follow")
Text(tag.following ? "account.follow.following" : "account.follow.follow")
}.buttonStyle(.bordered)
}
.padding(.horizontal, .layoutPadding)

View file

@ -46,10 +46,10 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
@Published var pendingStatuses: [Status] = []
@Published var pendingStatusesState: PendingStatusesState = .stream
var pendingStatusesButtonTitle: String {
var pendingStatusesButtonTitle: LocalizedStringKey {
switch pendingStatusesState {
case .stream, .refresh:
return "\(pendingStatuses.count) new posts"
return "timeline.n-new-posts \(pendingStatuses.count)"
}
}