diff options
Diffstat (limited to 'client/src')
26 files changed, 333 insertions, 155 deletions
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html index 379c0443e..34dc52029 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html | |||
@@ -23,10 +23,10 @@ | |||
23 | </h2> | 23 | </h2> |
24 | 24 | ||
25 | <div class="actor-counters"> | 25 | <div class="actor-counters"> |
26 | <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> | 26 | <div class="followers" i18n>{videoChannel.followersCount, plural, =0 {No subscribers} =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> |
27 | 27 | ||
28 | <span class="videos-count" *ngIf="getTotalVideosOf(videoChannel) !== undefined" i18n> | 28 | <span class="videos-count" *ngIf="getTotalVideosOf(videoChannel) !== undefined" i18n> |
29 | {getTotalVideosOf(videoChannel), plural, =1 {1 videos} other {{{ getTotalVideosOf(videoChannel) }} videos}} | 29 | {getTotalVideosOf(videoChannel), plural, =0 {No videos} =1 {1 video} other {{{ getTotalVideosOf(videoChannel) }} videos}} |
30 | </span> | 30 | </span> |
31 | </div> | 31 | </div> |
32 | 32 | ||
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index 8362e6b7e..e235d9689 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -33,10 +33,10 @@ | |||
33 | </div> | 33 | </div> |
34 | 34 | ||
35 | <div class="actor-counters"> | 35 | <div class="actor-counters"> |
36 | <span i18n>{naiveAggregatedSubscribers(), plural, =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span> | 36 | <span i18n>{naiveAggregatedSubscribers(), plural, =0 {No subscribers} =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span> |
37 | 37 | ||
38 | <span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n> | 38 | <span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n> |
39 | {accountVideosCount, plural, =1 {1 videos} other {{{ accountVideosCount }} videos}} | 39 | {accountVideosCount, plural, =0 {No videos} =1 {1 video} other {{{ accountVideosCount }} videos}} |
40 | </span> | 40 | </span> |
41 | </div> | 41 | </div> |
42 | </div> | 42 | </div> |
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index 898325492..cf66b817a 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -30,8 +30,6 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
30 | links: ListOverflowItem[] = [] | 30 | links: ListOverflowItem[] = [] |
31 | hideMenu = false | 31 | hideMenu = false |
32 | 32 | ||
33 | accountFollowerTitle = '' | ||
34 | |||
35 | accountVideosCount: number | 33 | accountVideosCount: number |
36 | accountDescriptionHTML = '' | 34 | accountDescriptionHTML = '' |
37 | accountDescriptionExpanded = false | 35 | accountDescriptionExpanded = false |
@@ -121,12 +119,6 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
121 | this.notifier.success($localize`Username copied`) | 119 | this.notifier.success($localize`Username copied`) |
122 | } | 120 | } |
123 | 121 | ||
124 | subscribersDisplayFor (count: number) { | ||
125 | if (count === 1) return $localize`1 subscriber` | ||
126 | |||
127 | return $localize`${count} subscribers` | ||
128 | } | ||
129 | |||
130 | searchChanged (search: string) { | 122 | searchChanged (search: string) { |
131 | const queryParams = { search } | 123 | const queryParams = { search } |
132 | 124 | ||
@@ -150,8 +142,6 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
150 | } | 142 | } |
151 | 143 | ||
152 | private async onAccount (account: Account) { | 144 | private async onAccount (account: Account) { |
153 | this.accountFollowerTitle = $localize`${account.followersCount} direct account followers` | ||
154 | |||
155 | this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description) | 145 | this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description) |
156 | 146 | ||
157 | // After the markdown renderer to avoid layout changes | 147 | // After the markdown renderer to avoid layout changes |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts b/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts index 9b55cb43c..96f5b830e 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { FormGroup } from '@angular/forms' | 2 | import { FormGroup } from '@angular/forms' |
3 | import { prepareIcu } from '@app/helpers' | ||
3 | 4 | ||
4 | export type ResolutionOption = { | 5 | export type ResolutionOption = { |
5 | id: string | 6 | id: string |
@@ -86,9 +87,10 @@ export class EditConfigurationService { | |||
86 | return { | 87 | return { |
87 | value, | 88 | value, |
88 | atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible | 89 | atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible |
89 | unit: value > 1 | 90 | unit: prepareIcu($localize`{value, plural, =1 {thread} other {threads}}`)( |
90 | ? $localize`threads` | 91 | { value }, |
91 | : $localize`thread` | 92 | $localize`threads` |
93 | ) | ||
92 | } | 94 | } |
93 | } | 95 | } |
94 | } | 96 | } |
diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.ts b/client/src/app/+admin/follows/following-list/follow-modal.component.ts index c40b36e10..bac7b2b01 100644 --- a/client/src/app/+admin/follows/following-list/follow-modal.component.ts +++ b/client/src/app/+admin/follows/following-list/follow-modal.component.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { prepareIcu } from '@app/helpers' | ||
3 | import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' | 4 | import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' |
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
5 | import { InstanceFollowService } from '@app/shared/shared-instance' | 6 | import { InstanceFollowService } from '@app/shared/shared-instance' |
@@ -60,7 +61,13 @@ export class FollowModalComponent extends FormReactive implements OnInit { | |||
60 | this.followService.follow(hostsOrHandles) | 61 | this.followService.follow(hostsOrHandles) |
61 | .subscribe({ | 62 | .subscribe({ |
62 | next: () => { | 63 | next: () => { |
63 | this.notifier.success($localize`Follow request(s) sent!`) | 64 | this.notifier.success( |
65 | prepareIcu($localize`{count, plural, =1 {Follow request} other {Follow requests}} sent!`)( | ||
66 | { count: hostsOrHandles.length }, | ||
67 | $localize`Follow request(s) sent!` | ||
68 | ) | ||
69 | ) | ||
70 | |||
64 | this.newFollow.emit() | 71 | this.newFollow.emit() |
65 | }, | 72 | }, |
66 | 73 | ||
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.ts b/client/src/app/+admin/overview/comments/video-comment-list.component.ts index f3f43a900..f1b27d846 100644 --- a/client/src/app/+admin/overview/comments/video-comment-list.component.ts +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.ts | |||
@@ -7,6 +7,7 @@ import { DropdownAction } from '@app/shared/shared-main' | |||
7 | import { BulkService } from '@app/shared/shared-moderation' | 7 | import { BulkService } from '@app/shared/shared-moderation' |
8 | import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' | 8 | import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' |
9 | import { FeedFormat, UserRight } from '@shared/models' | 9 | import { FeedFormat, UserRight } from '@shared/models' |
10 | import { prepareIcu } from '@app/helpers' | ||
10 | 11 | ||
11 | @Component({ | 12 | @Component({ |
12 | selector: 'my-video-comment-list', | 13 | selector: 'my-video-comment-list', |
@@ -145,7 +146,13 @@ export class VideoCommentListComponent extends RestTable implements OnInit { | |||
145 | this.videoCommentService.deleteVideoComments(commentArgs) | 146 | this.videoCommentService.deleteVideoComments(commentArgs) |
146 | .subscribe({ | 147 | .subscribe({ |
147 | next: () => { | 148 | next: () => { |
148 | this.notifier.success($localize`${commentArgs.length} comments deleted.`) | 149 | this.notifier.success( |
150 | prepareIcu($localize`{count, plural, =1 {1 comment} other {{count} comments}} deleted.`)( | ||
151 | { count: commentArgs.length }, | ||
152 | $localize`${commentArgs.length} comment(s) deleted.` | ||
153 | ) | ||
154 | ) | ||
155 | |||
149 | this.reloadData() | 156 | this.reloadData() |
150 | }, | 157 | }, |
151 | 158 | ||
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.ts b/client/src/app/+admin/overview/users/user-list/user-list.component.ts index 9d11bd02e..f7dc22256 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.ts | |||
@@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api' | |||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | 4 | import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' |
5 | import { getAPIHost } from '@app/helpers' | 5 | import { prepareIcu, getAPIHost } from '@app/helpers' |
6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
7 | import { Actor, DropdownAction } from '@app/shared/shared-main' | 7 | import { Actor, DropdownAction } from '@app/shared/shared-main' |
8 | import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation' | 8 | import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation' |
@@ -209,13 +209,25 @@ export class UserListComponent extends RestTable implements OnInit { | |||
209 | } | 209 | } |
210 | 210 | ||
211 | async unbanUsers (users: User[]) { | 211 | async unbanUsers (users: User[]) { |
212 | const res = await this.confirmService.confirm($localize`Do you really want to unban ${users.length} users?`, $localize`Unban`) | 212 | const res = await this.confirmService.confirm( |
213 | prepareIcu($localize`Do you really want to unban {count, plural, =1 {1 user} other {{count} users}}?`)( | ||
214 | { count: users.length }, | ||
215 | $localize`Do you really want to unban ${users.length} users?` | ||
216 | ), | ||
217 | $localize`Unban` | ||
218 | ) | ||
219 | |||
213 | if (res === false) return | 220 | if (res === false) return |
214 | 221 | ||
215 | this.userAdminService.unbanUsers(users) | 222 | this.userAdminService.unbanUsers(users) |
216 | .subscribe({ | 223 | .subscribe({ |
217 | next: () => { | 224 | next: () => { |
218 | this.notifier.success($localize`${users.length} users unbanned.`) | 225 | this.notifier.success( |
226 | prepareIcu($localize`{count, plural, =1 {1 user} other {{count} users}} unbanned.`)( | ||
227 | { count: users.length }, | ||
228 | $localize`${users.length} users unbanned.` | ||
229 | ) | ||
230 | ) | ||
219 | this.reloadData() | 231 | this.reloadData() |
220 | }, | 232 | }, |
221 | 233 | ||
@@ -224,21 +236,28 @@ export class UserListComponent extends RestTable implements OnInit { | |||
224 | } | 236 | } |
225 | 237 | ||
226 | async removeUsers (users: User[]) { | 238 | async removeUsers (users: User[]) { |
227 | for (const user of users) { | 239 | if (users.some(u => u.username === 'root')) { |
228 | if (user.username === 'root') { | 240 | this.notifier.error($localize`You cannot delete root.`) |
229 | this.notifier.error($localize`You cannot delete root.`) | 241 | return |
230 | return | ||
231 | } | ||
232 | } | 242 | } |
233 | 243 | ||
234 | const message = $localize`If you remove these users, you will not be able to create others with the same username!` | 244 | const message = $localize`<p>You can't create users or channels with a username that already used by a deleted user/channel.</p>` + |
245 | $localize`It means the following usernames will be permanently deleted and cannot be recovered:` + | ||
246 | '<ul>' + users.map(u => '<li>' + u.username + '</li>').join('') + '</ul>' | ||
247 | |||
235 | const res = await this.confirmService.confirm(message, $localize`Delete`) | 248 | const res = await this.confirmService.confirm(message, $localize`Delete`) |
236 | if (res === false) return | 249 | if (res === false) return |
237 | 250 | ||
238 | this.userAdminService.removeUser(users) | 251 | this.userAdminService.removeUser(users) |
239 | .subscribe({ | 252 | .subscribe({ |
240 | next: () => { | 253 | next: () => { |
241 | this.notifier.success($localize`${users.length} users deleted.`) | 254 | this.notifier.success( |
255 | prepareIcu($localize`{count, plural, =1 {1 user} other {{count} users}} deleted.`)( | ||
256 | { count: users.length }, | ||
257 | $localize`${users.length} users deleted.` | ||
258 | ) | ||
259 | ) | ||
260 | |||
242 | this.reloadData() | 261 | this.reloadData() |
243 | }, | 262 | }, |
244 | 263 | ||
@@ -250,7 +269,13 @@ export class UserListComponent extends RestTable implements OnInit { | |||
250 | this.userAdminService.updateUsers(users, { emailVerified: true }) | 269 | this.userAdminService.updateUsers(users, { emailVerified: true }) |
251 | .subscribe({ | 270 | .subscribe({ |
252 | next: () => { | 271 | next: () => { |
253 | this.notifier.success($localize`${users.length} users email set as verified.`) | 272 | this.notifier.success( |
273 | prepareIcu($localize`{count, plural, =1 {1 user} other {{count} users}} email set as verified.`)( | ||
274 | { count: users.length }, | ||
275 | $localize`${users.length} users email set as verified.` | ||
276 | ) | ||
277 | ) | ||
278 | |||
254 | this.reloadData() | 279 | this.reloadData() |
255 | }, | 280 | }, |
256 | 281 | ||
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts index 82ff372aa..67e52d100 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.ts +++ b/client/src/app/+admin/overview/videos/video-list.component.ts | |||
@@ -3,6 +3,7 @@ import { finalize } from 'rxjs/operators' | |||
3 | import { Component, OnInit, ViewChild } from '@angular/core' | 3 | import { Component, OnInit, ViewChild } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' | 5 | import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' |
6 | import { prepareIcu } from '@app/helpers' | ||
6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 7 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
7 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
8 | import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' | 9 | import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' |
@@ -196,14 +197,24 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
196 | } | 197 | } |
197 | 198 | ||
198 | private async removeVideos (videos: Video[]) { | 199 | private async removeVideos (videos: Video[]) { |
199 | const message = $localize`Are you sure you want to delete these ${videos.length} videos?` | 200 | const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)( |
201 | { count: videos.length }, | ||
202 | $localize`Are you sure you want to delete these ${videos.length} videos?` | ||
203 | ) | ||
204 | |||
200 | const res = await this.confirmService.confirm(message, $localize`Delete`) | 205 | const res = await this.confirmService.confirm(message, $localize`Delete`) |
201 | if (res === false) return | 206 | if (res === false) return |
202 | 207 | ||
203 | this.videoService.removeVideo(videos.map(v => v.id)) | 208 | this.videoService.removeVideo(videos.map(v => v.id)) |
204 | .subscribe({ | 209 | .subscribe({ |
205 | next: () => { | 210 | next: () => { |
206 | this.notifier.success($localize`Deleted ${videos.length} videos.`) | 211 | this.notifier.success( |
212 | prepareIcu($localize`Deleted {count, plural, =1 {1 video} other {{count} videos}}.`)( | ||
213 | { count: videos.length }, | ||
214 | $localize`Deleted ${videos.length} videos.` | ||
215 | ) | ||
216 | ) | ||
217 | |||
207 | this.reloadData() | 218 | this.reloadData() |
208 | }, | 219 | }, |
209 | 220 | ||
@@ -215,7 +226,13 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
215 | this.videoBlockService.unblockVideo(videos.map(v => v.id)) | 226 | this.videoBlockService.unblockVideo(videos.map(v => v.id)) |
216 | .subscribe({ | 227 | .subscribe({ |
217 | next: () => { | 228 | next: () => { |
218 | this.notifier.success($localize`Unblocked ${videos.length} videos.`) | 229 | this.notifier.success( |
230 | prepareIcu($localize`Unblocked {count, plural, =1 {1 video} other {{count} videos}}.`)( | ||
231 | { count: videos.length }, | ||
232 | $localize`Unblocked ${videos.length} videos.` | ||
233 | ) | ||
234 | ) | ||
235 | |||
219 | this.reloadData() | 236 | this.reloadData() |
220 | }, | 237 | }, |
221 | 238 | ||
@@ -224,9 +241,21 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
224 | } | 241 | } |
225 | 242 | ||
226 | private async removeVideoFiles (videos: Video[], type: 'hls' | 'webtorrent') { | 243 | private async removeVideoFiles (videos: Video[], type: 'hls' | 'webtorrent') { |
227 | const message = type === 'hls' | 244 | let message: string |
228 | ? $localize`Are you sure you want to delete ${videos.length} HLS streaming playlists?` | 245 | |
229 | : $localize`Are you sure you want to delete WebTorrent files of ${videos.length} videos?` | 246 | if (type === 'hls') { |
247 | // eslint-disable-next-line max-len | ||
248 | message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {1 HLS streaming playlist} other {{count} HLS streaming playlists}}?`)( | ||
249 | { count: videos.length }, | ||
250 | $localize`Are you sure you want to delete ${videos.length} HLS streaming playlists?` | ||
251 | ) | ||
252 | } else { | ||
253 | // eslint-disable-next-line max-len | ||
254 | message = prepareIcu($localize`Are you sure you want to delete WebTorrent files of {count, plural, =1 {1 video} other {{count} videos}}?`)( | ||
255 | { count: videos.length }, | ||
256 | $localize`Are you sure you want to delete WebTorrent files of ${videos.length} videos?` | ||
257 | ) | ||
258 | } | ||
230 | 259 | ||
231 | const res = await this.confirmService.confirm(message, $localize`Delete`) | 260 | const res = await this.confirmService.confirm(message, $localize`Delete`) |
232 | if (res === false) return | 261 | if (res === false) return |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts index 7c13282fa..769ab647a 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts | |||
@@ -37,7 +37,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit { | |||
37 | myVideoPublished: $localize`Video published (after transcoding/scheduled update)`, | 37 | myVideoPublished: $localize`Video published (after transcoding/scheduled update)`, |
38 | myVideoImportFinished: $localize`Video import finished`, | 38 | myVideoImportFinished: $localize`Video import finished`, |
39 | newUserRegistration: $localize`A new user registered on your instance`, | 39 | newUserRegistration: $localize`A new user registered on your instance`, |
40 | newFollow: $localize`You or your channel(s) has a new follower`, | 40 | newFollow: $localize`You or one of your channels has a new follower`, |
41 | commentMention: $localize`Someone mentioned you in video comments`, | 41 | commentMention: $localize`Someone mentioned you in video comments`, |
42 | newInstanceFollower: $localize`Your instance has a new follower`, | 42 | newInstanceFollower: $localize`Your instance has a new follower`, |
43 | autoInstanceFollowing: $localize`Your instance automatically followed another instance`, | 43 | autoInstanceFollowing: $localize`Your instance automatically followed another instance`, |
diff --git a/client/src/app/+my-library/my-history/my-history.component.ts b/client/src/app/+my-library/my-history/my-history.component.ts index f6b712908..766869637 100644 --- a/client/src/app/+my-library/my-history/my-history.component.ts +++ b/client/src/app/+my-library/my-history/my-history.component.ts | |||
@@ -93,8 +93,8 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook { | |||
93 | .subscribe({ | 93 | .subscribe({ |
94 | next: () => { | 94 | next: () => { |
95 | const message = this.videosHistoryEnabled === true | 95 | const message = this.videosHistoryEnabled === true |
96 | ? $localize`Videos history is enabled` | 96 | ? $localize`Video history is enabled` |
97 | : $localize`Videos history is disabled` | 97 | : $localize`Video history is disabled` |
98 | 98 | ||
99 | this.notifier.success(message) | 99 | this.notifier.success(message) |
100 | 100 | ||
@@ -117,8 +117,8 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook { | |||
117 | } | 117 | } |
118 | 118 | ||
119 | async clearAllHistory () { | 119 | async clearAllHistory () { |
120 | const title = $localize`Delete videos history` | 120 | const title = $localize`Delete video history` |
121 | const message = $localize`Are you sure you want to delete all your videos history?` | 121 | const message = $localize`Are you sure you want to delete all your video history?` |
122 | 122 | ||
123 | const res = await this.confirmService.confirm(message, title) | 123 | const res = await this.confirmService.confirm(message, title) |
124 | if (res !== true) return | 124 | if (res !== true) return |
@@ -126,7 +126,7 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook { | |||
126 | this.userHistoryService.clearAll() | 126 | this.userHistoryService.clearAll() |
127 | .subscribe({ | 127 | .subscribe({ |
128 | next: () => { | 128 | next: () => { |
129 | this.notifier.success($localize`Videos history deleted`) | 129 | this.notifier.success($localize`Video history deleted`) |
130 | 130 | ||
131 | this.reloadData() | 131 | this.reloadData() |
132 | }, | 132 | }, |
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts index 64e56a250..c8012ec78 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.ts +++ b/client/src/app/+my-library/my-videos/my-videos.component.ts | |||
@@ -4,7 +4,7 @@ import { Component, OnInit, ViewChild } from '@angular/core' | |||
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' | 5 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' |
6 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | 6 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' |
7 | import { immutableAssign } from '@app/helpers' | 7 | import { prepareIcu, immutableAssign } from '@app/helpers' |
8 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 8 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
9 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 9 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
10 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' | 10 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' |
@@ -167,7 +167,10 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
167 | .map(k => parseInt(k, 10)) | 167 | .map(k => parseInt(k, 10)) |
168 | 168 | ||
169 | const res = await this.confirmService.confirm( | 169 | const res = await this.confirmService.confirm( |
170 | $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?`, | 170 | prepareIcu($localize`Do you really want to delete {length, plural, =1 {this video} other {{length} videos}}?`)( |
171 | { length: toDeleteVideosIds.length }, | ||
172 | $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?` | ||
173 | ), | ||
171 | $localize`Delete` | 174 | $localize`Delete` |
172 | ) | 175 | ) |
173 | if (res === false) return | 176 | if (res === false) return |
@@ -184,7 +187,13 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
184 | .pipe(toArray()) | 187 | .pipe(toArray()) |
185 | .subscribe({ | 188 | .subscribe({ |
186 | next: () => { | 189 | next: () => { |
187 | this.notifier.success($localize`${toDeleteVideosIds.length} videos deleted.`) | 190 | this.notifier.success( |
191 | prepareIcu($localize`{length, plural, =1 {Video has been deleted} other {{length} videos have been deleted}}`)( | ||
192 | { length: toDeleteVideosIds.length }, | ||
193 | $localize`${toDeleteVideosIds.length} have been deleted.` | ||
194 | ) | ||
195 | ) | ||
196 | |||
188 | this.selection = {} | 197 | this.selection = {} |
189 | }, | 198 | }, |
190 | 199 | ||
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts index b9ec6dbcc..62b1c4446 100644 --- a/client/src/app/+search/search.component.ts +++ b/client/src/app/+search/search.component.ts | |||
@@ -248,11 +248,11 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
248 | } | 248 | } |
249 | 249 | ||
250 | private updateTitle () { | 250 | private updateTitle () { |
251 | const suffix = this.currentSearch | 251 | const title = this.currentSearch |
252 | ? ' ' + this.currentSearch | 252 | ? $localize`Search ${this.currentSearch}` |
253 | : '' | 253 | : $localize`Search` |
254 | 254 | ||
255 | this.metaService.setTitle($localize`Search` + suffix) | 255 | this.metaService.setTitle(title) |
256 | } | 256 | } |
257 | 257 | ||
258 | private updateUrlFromAdvancedSearch () { | 258 | private updateUrlFromAdvancedSearch () { |
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html index 212e2f867..780db79b0 100644 --- a/client/src/app/+video-channels/video-channels.component.html +++ b/client/src/app/+video-channels/video-channels.component.html | |||
@@ -72,10 +72,10 @@ | |||
72 | </div> | 72 | </div> |
73 | 73 | ||
74 | <div class="actor-counters"> | 74 | <div class="actor-counters"> |
75 | <span i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span> | 75 | <span i18n>{videoChannel.followersCount, plural, =0 {No subscribers} =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span> |
76 | 76 | ||
77 | <span class="videos-count" *ngIf="channelVideosCount !== undefined" i18n> | 77 | <span class="videos-count" *ngIf="channelVideosCount !== undefined" i18n> |
78 | {channelVideosCount, plural, =1 {1 videos} other {{{ channelVideosCount }} videos}} | 78 | {channelVideosCount, plural, =0 {No videos} =1 {1 video} other {{{ channelVideosCount }} videos}} |
79 | </span> | 79 | </span> |
80 | </div> | 80 | </div> |
81 | </div> | 81 | </div> |
diff --git a/client/src/app/+videos/video-list/videos-list-common-page.component.ts b/client/src/app/+videos/video-list/videos-list-common-page.component.ts index d2782036b..c8fa8ef30 100644 --- a/client/src/app/+videos/video-list/videos-list-common-page.component.ts +++ b/client/src/app/+videos/video-list/videos-list-common-page.component.ts | |||
@@ -204,13 +204,28 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable | |||
204 | if ([ 'hot', 'trending', 'likes', 'views' ].includes(sanitizedSort)) { | 204 | if ([ 'hot', 'trending', 'likes', 'views' ].includes(sanitizedSort)) { |
205 | this.title = $localize`Trending` | 205 | this.title = $localize`Trending` |
206 | 206 | ||
207 | if (sanitizedSort === 'hot') this.titleTooltip = $localize`Videos with the most interactions for recent videos` | 207 | if (sanitizedSort === 'hot') { |
208 | if (sanitizedSort === 'likes') this.titleTooltip = $localize`Videos that have the most likes` | 208 | this.titleTooltip = $localize`Videos with the most interactions for recent videos` |
209 | if (sanitizedSort === 'views') this.titleTooltip = undefined | 209 | return |
210 | } | ||
211 | |||
212 | if (sanitizedSort === 'likes') { | ||
213 | this.titleTooltip = $localize`Videos that have the most likes` | ||
214 | return | ||
215 | } | ||
216 | |||
217 | if (sanitizedSort === 'views') { | ||
218 | this.titleTooltip = undefined | ||
219 | return | ||
220 | } | ||
210 | 221 | ||
211 | if (sanitizedSort === 'trending') { | 222 | if (sanitizedSort === 'trending') { |
212 | if (this.trendingDays === 1) this.titleTooltip = $localize`Videos with the most views during the last 24 hours` | 223 | if (this.trendingDays === 1) { |
213 | else this.titleTooltip = $localize`Videos with the most views during the last ${this.trendingDays} days` | 224 | this.titleTooltip = $localize`Videos with the most views during the last 24 hours` |
225 | return | ||
226 | } | ||
227 | |||
228 | this.titleTooltip = $localize`Videos with the most views during the last ${this.trendingDays} days` | ||
214 | } | 229 | } |
215 | 230 | ||
216 | return | 231 | return |
diff --git a/client/src/app/core/rest/rest-extractor.service.ts b/client/src/app/core/rest/rest-extractor.service.ts index 17053811c..86c7484a5 100644 --- a/client/src/app/core/rest/rest-extractor.service.ts +++ b/client/src/app/core/rest/rest-extractor.service.ts | |||
@@ -34,49 +34,17 @@ export class RestExtractor { | |||
34 | return target | 34 | return target |
35 | } | 35 | } |
36 | 36 | ||
37 | handleError (err: any) { | 37 | redirectTo404IfNotFound (obj: { status: number }, type: 'video' | 'other', status = [ HttpStatusCode.NOT_FOUND_404 ]) { |
38 | let errorMessage | 38 | if (obj?.status && status.includes(obj.status)) { |
39 | // Do not use redirectService to avoid circular dependencies | ||
40 | this.router.navigate([ '/404' ], { state: { type, obj }, skipLocationChange: true }) | ||
41 | } | ||
39 | 42 | ||
40 | if (err.error instanceof Error) { | 43 | return observableThrowError(() => obj) |
41 | // A client-side or network error occurred. Handle it accordingly. | 44 | } |
42 | errorMessage = err.error.detail || err.error.title | ||
43 | console.error('An error occurred:', errorMessage) | ||
44 | } else if (typeof err.error === 'string') { | ||
45 | errorMessage = err.error | ||
46 | } else if (err.status !== undefined) { | ||
47 | // A server-side error occurred. | ||
48 | if (err.error?.errors) { | ||
49 | const errors = err.error.errors | ||
50 | const errorsArray: string[] = [] | ||
51 | |||
52 | Object.keys(errors).forEach(key => { | ||
53 | errorsArray.push(errors[key].msg) | ||
54 | }) | ||
55 | |||
56 | errorMessage = errorsArray.join('. ') | ||
57 | } else if (err.error?.error) { | ||
58 | errorMessage = err.error.error | ||
59 | } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) { | ||
60 | // eslint-disable-next-line max-len | ||
61 | errorMessage = $localize`Media is too large for the server. Please contact you administrator if you want to increase the limit size.` | ||
62 | } else if (err.status === HttpStatusCode.TOO_MANY_REQUESTS_429) { | ||
63 | const secondsLeft = err.headers.get('retry-after') | ||
64 | if (secondsLeft) { | ||
65 | const minutesLeft = Math.floor(parseInt(secondsLeft, 10) / 60) | ||
66 | errorMessage = $localize`Too many attempts, please try again after ${minutesLeft} minutes.` | ||
67 | } else { | ||
68 | errorMessage = $localize`Too many attempts, please try again later.` | ||
69 | } | ||
70 | } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) { | ||
71 | errorMessage = $localize`Server error. Please retry later.` | ||
72 | } | ||
73 | 45 | ||
74 | errorMessage = errorMessage || 'Unknown error.' | 46 | handleError (err: any) { |
75 | console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) | 47 | const errorMessage = this.buildErrorMessage(err) |
76 | } else { | ||
77 | console.error(err) | ||
78 | errorMessage = err | ||
79 | } | ||
80 | 48 | ||
81 | const errorObj: { message: string, status: string, body: string } = { | 49 | const errorObj: { message: string, status: string, body: string } = { |
82 | message: errorMessage, | 50 | message: errorMessage, |
@@ -92,12 +60,63 @@ export class RestExtractor { | |||
92 | return observableThrowError(() => errorObj) | 60 | return observableThrowError(() => errorObj) |
93 | } | 61 | } |
94 | 62 | ||
95 | redirectTo404IfNotFound (obj: { status: number }, type: 'video' | 'other', status = [ HttpStatusCode.NOT_FOUND_404 ]) { | 63 | private buildErrorMessage (err: any) { |
96 | if (obj?.status && status.includes(obj.status)) { | 64 | if (err.error instanceof Error) { |
97 | // Do not use redirectService to avoid circular dependencies | 65 | // A client-side or network error occurred. Handle it accordingly. |
98 | this.router.navigate([ '/404' ], { state: { type, obj }, skipLocationChange: true }) | 66 | const errorMessage = err.error.detail || err.error.title |
67 | console.error('An error occurred:', errorMessage) | ||
68 | |||
69 | return errorMessage | ||
99 | } | 70 | } |
100 | 71 | ||
101 | return observableThrowError(() => obj) | 72 | if (typeof err.error === 'string') { |
73 | return err.error | ||
74 | } | ||
75 | |||
76 | if (err.status !== undefined) { | ||
77 | const errorMessage = this.buildServerErrorMessage(err) | ||
78 | console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) | ||
79 | |||
80 | return errorMessage | ||
81 | } | ||
82 | |||
83 | console.error(err) | ||
84 | return err | ||
85 | } | ||
86 | |||
87 | private buildServerErrorMessage (err: any) { | ||
88 | // A server-side error occurred. | ||
89 | if (err.error?.errors) { | ||
90 | const errors = err.error.errors | ||
91 | |||
92 | return Object.keys(errors) | ||
93 | .map(key => errors[key].msg) | ||
94 | .join('. ') | ||
95 | } | ||
96 | |||
97 | if (err.error?.error) { | ||
98 | return err.error.error | ||
99 | } | ||
100 | |||
101 | if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) { | ||
102 | return $localize`Media is too large for the server. Please contact you administrator if you want to increase the limit size.` | ||
103 | } | ||
104 | |||
105 | if (err.status === HttpStatusCode.TOO_MANY_REQUESTS_429) { | ||
106 | const secondsLeft = err.headers.get('retry-after') | ||
107 | |||
108 | if (secondsLeft) { | ||
109 | const minutesLeft = Math.floor(parseInt(secondsLeft, 10) / 60) | ||
110 | return $localize`Too many attempts, please try again after ${minutesLeft} minutes.` | ||
111 | } | ||
112 | |||
113 | return $localize`Too many attempts, please try again later.` | ||
114 | } | ||
115 | |||
116 | if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) { | ||
117 | return $localize`Server error. Please retry later.` | ||
118 | } | ||
119 | |||
120 | return $localize`Unknown server error` | ||
102 | } | 121 | } |
103 | } | 122 | } |
diff --git a/client/src/app/helpers/i18n-utils.ts b/client/src/app/helpers/i18n-utils.ts index bbfb12959..2017a31ea 100644 --- a/client/src/app/helpers/i18n-utils.ts +++ b/client/src/app/helpers/i18n-utils.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { environment } from '../../environments/environment' | 1 | import { environment } from '../../environments/environment' |
2 | import IntlMessageFormat from 'intl-messageformat' | ||
2 | 3 | ||
3 | function isOnDevLocale () { | 4 | function isOnDevLocale () { |
4 | return environment.production === false && window.location.search === '?lang=fr' | 5 | return environment.production === false && window.location.search === '?lang=fr' |
@@ -8,7 +9,31 @@ function getDevLocale () { | |||
8 | return 'fr-FR' | 9 | return 'fr-FR' |
9 | } | 10 | } |
10 | 11 | ||
12 | function prepareIcu (icu: string) { | ||
13 | let alreadyWarned = false | ||
14 | |||
15 | try { | ||
16 | const msg = new IntlMessageFormat(icu, $localize.locale) | ||
17 | |||
18 | return (context: { [id: string]: number | string }, fallback: string) => { | ||
19 | try { | ||
20 | return msg.format(context) as string | ||
21 | } catch (err) { | ||
22 | if (!alreadyWarned) console.warn('Cannot format ICU %s.', icu, err) | ||
23 | |||
24 | alreadyWarned = true | ||
25 | return fallback | ||
26 | } | ||
27 | } | ||
28 | } catch (err) { | ||
29 | console.warn('Cannot build intl message %s.', icu, err) | ||
30 | |||
31 | return (_context: unknown, fallback: string) => fallback | ||
32 | } | ||
33 | } | ||
34 | |||
11 | export { | 35 | export { |
12 | getDevLocale, | 36 | getDevLocale, |
37 | prepareIcu, | ||
13 | isOnDevLocale | 38 | isOnDevLocale |
14 | } | 39 | } |
diff --git a/client/src/app/helpers/utils/upload.ts b/client/src/app/helpers/utils/upload.ts index a3fce7fee..5c2600a0d 100644 --- a/client/src/app/helpers/utils/upload.ts +++ b/client/src/app/helpers/utils/upload.ts | |||
@@ -2,36 +2,43 @@ import { HttpErrorResponse } from '@angular/common/http' | |||
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { HttpStatusCode } from '@shared/models' | 3 | import { HttpStatusCode } from '@shared/models' |
4 | 4 | ||
5 | function genericUploadErrorHandler (parameters: { | 5 | function genericUploadErrorHandler (options: { |
6 | err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'> | 6 | err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'> |
7 | name: string | 7 | name: string |
8 | notifier: Notifier | 8 | notifier: Notifier |
9 | sticky?: boolean | 9 | sticky?: boolean |
10 | }) { | 10 | }) { |
11 | const { err, name, notifier, sticky } = { sticky: false, ...parameters } | 11 | const { err, name, notifier, sticky = false } = options |
12 | const title = $localize`The upload failed` | 12 | const title = $localize`Upload failed` |
13 | let message = err.message | 13 | const message = buildMessage(name, err) |
14 | |||
15 | if (err instanceof ErrorEvent) { // network error | ||
16 | message = $localize`The connection was interrupted` | ||
17 | notifier.error(message, title, null, sticky) | ||
18 | } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) { | ||
19 | message = $localize`The server encountered an error` | ||
20 | notifier.error(message, title, null, sticky) | ||
21 | } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) { | ||
22 | message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)` | ||
23 | notifier.error(message, title, null, sticky) | ||
24 | } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) { | ||
25 | const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G' | ||
26 | message = $localize`Your ${name} file was too large (max. size: ${maxFileSize})` | ||
27 | notifier.error(message, title, null, sticky) | ||
28 | } else { | ||
29 | notifier.error(err.message, title) | ||
30 | } | ||
31 | 14 | ||
15 | notifier.error(message, title, null, sticky) | ||
32 | return message | 16 | return message |
33 | } | 17 | } |
34 | 18 | ||
35 | export { | 19 | export { |
36 | genericUploadErrorHandler | 20 | genericUploadErrorHandler |
37 | } | 21 | } |
22 | |||
23 | // --------------------------------------------------------------------------- | ||
24 | |||
25 | function buildMessage (name: string, err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'>) { | ||
26 | if (err instanceof ErrorEvent) { // network error | ||
27 | return $localize`The connection was interrupted` | ||
28 | } | ||
29 | |||
30 | if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) { | ||
31 | return $localize`The server encountered an error` | ||
32 | } | ||
33 | |||
34 | if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) { | ||
35 | return $localize`Your ${name} file couldn't be transferred before the server proxy timeout` | ||
36 | } | ||
37 | |||
38 | if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) { | ||
39 | const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G' | ||
40 | return $localize`Your ${name} file was too large (max. size: ${maxFileSize})` | ||
41 | } | ||
42 | |||
43 | return err.message | ||
44 | } | ||
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts b/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts index ebf7b77a6..2c3226f68 100644 --- a/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts +++ b/client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Component, forwardRef, Input } from '@angular/core' | 1 | import { Component, forwardRef, Input } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { prepareIcu } from '@app/helpers' | ||
4 | import { SelectOptionsItem } from '../../../../types/select-options-item.model' | 5 | import { SelectOptionsItem } from '../../../../types/select-options-item.model' |
5 | import { ItemSelectCheckboxValue } from './select-checkbox.component' | 6 | import { ItemSelectCheckboxValue } from './select-checkbox.component' |
6 | 7 | ||
@@ -78,7 +79,12 @@ export class SelectCheckboxAllComponent implements ControlValueAccessor { | |||
78 | if (!outputItems) return true | 79 | if (!outputItems) return true |
79 | 80 | ||
80 | if (outputItems.length >= this.maxItems) { | 81 | if (outputItems.length >= this.maxItems) { |
81 | this.notifier.error($localize`You can't select more than ${this.maxItems} items`) | 82 | this.notifier.error( |
83 | prepareIcu($localize`You can't select more than {maxItems, plural, =1 {1 item} other {{maxItems} items}}`)( | ||
84 | { maxItems: this.maxItems }, | ||
85 | $localize`You can't select more than ${this.maxItems} items` | ||
86 | ) | ||
87 | ) | ||
82 | 88 | ||
83 | return false | 89 | return false |
84 | } | 90 | } |
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts index 6335de450..e405c5790 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.ts +++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ServerService } from '@app/core' | 2 | import { ServerService } from '@app/core' |
3 | import { prepareIcu } from '@app/helpers' | ||
3 | import { ServerConfig } from '@shared/models' | 4 | import { ServerConfig } from '@shared/models' |
4 | import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service' | 5 | import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service' |
5 | 6 | ||
@@ -65,15 +66,20 @@ export class InstanceFeaturesTableComponent implements OnInit { | |||
65 | 66 | ||
66 | private getApproximateTime (seconds: number) { | 67 | private getApproximateTime (seconds: number) { |
67 | const hours = Math.floor(seconds / 3600) | 68 | const hours = Math.floor(seconds / 3600) |
68 | let pluralSuffix = '' | ||
69 | if (hours > 1) pluralSuffix = 's' | ||
70 | if (hours > 0) return `~ ${hours} hour${pluralSuffix}` | ||
71 | 69 | ||
72 | const minutes = Math.floor(seconds % 3600 / 60) | 70 | if (hours !== 0) { |
71 | return prepareIcu($localize`~ {hours, plural, =1 {1 hour} other {{hours} hours}}`)( | ||
72 | { hours }, | ||
73 | $localize`~ ${hours} hours` | ||
74 | ) | ||
75 | } | ||
73 | 76 | ||
74 | if (minutes === 1) return $localize`~ 1 minute` | 77 | const minutes = Math.floor(seconds % 3600 / 60) |
75 | 78 | ||
76 | return $localize`~ ${minutes} minutes` | 79 | return prepareIcu($localize`~ {minutes, plural, =1 {1 minute} other {{minutes} minutes}}`)( |
80 | { minutes }, | ||
81 | $localize`~ ${minutes} minutes` | ||
82 | ) | ||
77 | } | 83 | } |
78 | 84 | ||
79 | private buildQuotaHelpIndication () { | 85 | private buildQuotaHelpIndication () { |
diff --git a/client/src/app/shared/shared-main/angular/from-now.pipe.ts b/client/src/app/shared/shared-main/angular/from-now.pipe.ts index d62c1f88e..dc6a25e83 100644 --- a/client/src/app/shared/shared-main/angular/from-now.pipe.ts +++ b/client/src/app/shared/shared-main/angular/from-now.pipe.ts | |||
@@ -1,37 +1,51 @@ | |||
1 | import { Pipe, PipeTransform } from '@angular/core' | 1 | import { Pipe, PipeTransform } from '@angular/core' |
2 | import { prepareIcu } from '@app/helpers' | ||
2 | 3 | ||
3 | // Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site | 4 | // Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site |
4 | @Pipe({ name: 'myFromNow' }) | 5 | @Pipe({ name: 'myFromNow' }) |
5 | export class FromNowPipe implements PipeTransform { | 6 | export class FromNowPipe implements PipeTransform { |
7 | private yearICU = prepareIcu($localize`{interval, plural, =1 {1 year ago} other {{interval} years ago}}`) | ||
8 | private monthICU = prepareIcu($localize`{interval, plural, =1 {1 month ago} other {{interval} months ago}}`) | ||
9 | private weekICU = prepareIcu($localize`{interval, plural, =1 {1 week ago} other {{interval} weeks ago}}`) | ||
10 | private dayICU = prepareIcu($localize`{interval, plural, =1 {1 day ago} other {{interval} days ago}}`) | ||
11 | private hourICU = prepareIcu($localize`{interval, plural, =1 {1 hour ago} other {{interval} hours ago}}`) | ||
12 | |||
6 | transform (arg: number | Date | string) { | 13 | transform (arg: number | Date | string) { |
7 | const argDate = new Date(arg) | 14 | const argDate = new Date(arg) |
8 | const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) | 15 | const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) |
9 | 16 | ||
10 | let interval = Math.floor(seconds / 31536000) | 17 | let interval = Math.floor(seconds / 31536000) |
11 | if (interval > 1) return $localize`${interval} years ago` | 18 | if (interval >= 1) { |
12 | if (interval === 1) return $localize`1 year ago` | 19 | return this.yearICU({ interval }, $localize`${interval} year(s) ago`) |
20 | } | ||
13 | 21 | ||
14 | interval = Math.floor(seconds / 2419200) | 22 | interval = Math.floor(seconds / 2419200) |
15 | // 12 months = 360 days, but a year ~ 365 days | 23 | // 12 months = 360 days, but a year ~ 365 days |
16 | // Display "1 year ago" rather than "12 months ago" | 24 | // Display "1 year ago" rather than "12 months ago" |
17 | if (interval >= 12) return $localize`1 year ago` | 25 | if (interval >= 12) return $localize`1 year ago` |
18 | if (interval > 1) return $localize`${interval} months ago` | 26 | |
19 | if (interval === 1) return $localize`1 month ago` | 27 | if (interval >= 1) { |
28 | return this.monthICU({ interval }, $localize`${interval} month(s) ago`) | ||
29 | } | ||
20 | 30 | ||
21 | interval = Math.floor(seconds / 604800) | 31 | interval = Math.floor(seconds / 604800) |
22 | // 4 weeks ~ 28 days, but our month is 30 days | 32 | // 4 weeks ~ 28 days, but our month is 30 days |
23 | // Display "1 month ago" rather than "4 weeks ago" | 33 | // Display "1 month ago" rather than "4 weeks ago" |
24 | if (interval >= 4) return $localize`1 month ago` | 34 | if (interval >= 4) return $localize`1 month ago` |
25 | if (interval > 1) return $localize`${interval} weeks ago` | 35 | |
26 | if (interval === 1) return $localize`1 week ago` | 36 | if (interval >= 1) { |
37 | return this.weekICU({ interval }, $localize`${interval} week(s) ago`) | ||
38 | } | ||
27 | 39 | ||
28 | interval = Math.floor(seconds / 86400) | 40 | interval = Math.floor(seconds / 86400) |
29 | if (interval > 1) return $localize`${interval} days ago` | 41 | if (interval >= 1) { |
30 | if (interval === 1) return $localize`1 day ago` | 42 | return this.dayICU({ interval }, $localize`${interval} day(s) ago`) |
43 | } | ||
31 | 44 | ||
32 | interval = Math.floor(seconds / 3600) | 45 | interval = Math.floor(seconds / 3600) |
33 | if (interval > 1) return $localize`${interval} hours ago` | 46 | if (interval >= 1) { |
34 | if (interval === 1) return $localize`1 hour ago` | 47 | return this.hourICU({ interval }, $localize`${interval} hour(s) ago`) |
48 | } | ||
35 | 49 | ||
36 | interval = Math.floor(seconds / 60) | 50 | interval = Math.floor(seconds / 60) |
37 | if (interval >= 1) return $localize`${interval} min ago` | 51 | if (interval >= 1) return $localize`${interval} min ago` |
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts index 022bb95ad..2e4ab87d7 100644 --- a/client/src/app/shared/shared-main/video/video.model.ts +++ b/client/src/app/shared/shared-main/video/video.model.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { AuthUser } from '@app/core' | 1 | import { AuthUser } from '@app/core' |
2 | import { User } from '@app/core/users/user.model' | 2 | import { User } from '@app/core/users/user.model' |
3 | import { durationToString, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers' | 3 | import { durationToString, prepareIcu, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers' |
4 | import { Actor } from '@app/shared/shared-main/account/actor.model' | 4 | import { Actor } from '@app/shared/shared-main/account/actor.model' |
5 | import { buildVideoWatchPath } from '@shared/core-utils' | 5 | import { buildVideoWatchPath } from '@shared/core-utils' |
6 | import { peertubeTranslate } from '@shared/core-utils/i18n' | 6 | import { peertubeTranslate } from '@shared/core-utils/i18n' |
@@ -19,6 +19,9 @@ import { | |||
19 | } from '@shared/models' | 19 | } from '@shared/models' |
20 | 20 | ||
21 | export class Video implements VideoServerModel { | 21 | export class Video implements VideoServerModel { |
22 | private static readonly viewsICU = prepareIcu($localize`{views, plural, =0 {No view} =1 {1 view} other {{views} views}}`) | ||
23 | private static readonly viewersICU = prepareIcu($localize`{viewers, plural, =0 {No viewers} =1 {1 viewer} other {{viewers} viewers}}`) | ||
24 | |||
22 | byVideoChannel: string | 25 | byVideoChannel: string |
23 | byAccount: string | 26 | byAccount: string |
24 | 27 | ||
@@ -269,12 +272,10 @@ export class Video implements VideoServerModel { | |||
269 | } | 272 | } |
270 | 273 | ||
271 | getExactNumberOfViews () { | 274 | getExactNumberOfViews () { |
272 | if (this.views < 1000) return '' | ||
273 | |||
274 | if (this.isLive) { | 275 | if (this.isLive) { |
275 | return $localize`${this.views} viewers` | 276 | return Video.viewersICU({ viewers: this.viewers }, $localize`${this.viewers} viewer(s)`) |
276 | } | 277 | } |
277 | 278 | ||
278 | return $localize`${this.views} views` | 279 | return Video.viewsICU({ views: this.views }, $localize`{${this.views} view(s)}`) |
279 | } | 280 | } |
280 | } | 281 | } |
diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts index 9edfac388..8b483499a 100644 --- a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts +++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { forkJoin } from 'rxjs' | 1 | import { forkJoin } from 'rxjs' |
2 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 2 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { prepareIcu } from '@app/helpers' | ||
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
6 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
@@ -63,9 +64,16 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
63 | forkJoin(observables) | 64 | forkJoin(observables) |
64 | .subscribe({ | 65 | .subscribe({ |
65 | next: () => { | 66 | next: () => { |
66 | const message = Array.isArray(this.usersToBan) | 67 | let message: string |
67 | ? $localize`${this.usersToBan.length} users banned.` | 68 | |
68 | : $localize`User ${this.usersToBan.username} banned.` | 69 | if (Array.isArray(this.usersToBan)) { |
70 | message = prepareIcu($localize`{count, plural, =1 {1 user} other {{count} users}} banned.`)( | ||
71 | { count: this.usersToBan.length }, | ||
72 | $localize`${this.usersToBan.length} users banned.` | ||
73 | ) | ||
74 | } else { | ||
75 | message = $localize`User ${this.usersToBan.username} banned.` | ||
76 | } | ||
69 | 77 | ||
70 | this.notifier.success(message) | 78 | this.notifier.success(message) |
71 | 79 | ||
@@ -79,7 +87,12 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
79 | } | 87 | } |
80 | 88 | ||
81 | getModalTitle () { | 89 | getModalTitle () { |
82 | if (Array.isArray(this.usersToBan)) return $localize`Ban ${this.usersToBan.length} users` | 90 | if (Array.isArray(this.usersToBan)) { |
91 | return prepareIcu($localize`Ban {count, plural, =1 {1 user} other {{count} users}}`)( | ||
92 | { count: this.usersToBan.length }, | ||
93 | $localize`Ban ${this.usersToBan.length} users` | ||
94 | ) | ||
95 | } | ||
83 | 96 | ||
84 | return $localize`Ban "${this.usersToBan.username}"` | 97 | return $localize`Ban "${this.usersToBan.username}"` |
85 | } | 98 | } |
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts index 787318c2c..c69a45c25 100644 --- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts | |||
@@ -100,7 +100,8 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
100 | return | 100 | return |
101 | } | 101 | } |
102 | 102 | ||
103 | const message = $localize`If you remove user ${user.username}, you won't be able to create another with the same username!` | 103 | // eslint-disable-next-line max-len |
104 | const message = $localize`If you remove this user, you won't be able to create another user or channel with <strong>${user.username}</strong> username!` | ||
104 | const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`) | 105 | const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`) |
105 | if (res === false) return | 106 | if (res === false) return |
106 | 107 | ||
diff --git a/client/src/app/shared/shared-moderation/video-block.component.ts b/client/src/app/shared/shared-moderation/video-block.component.ts index 400913f02..e14473b89 100644 --- a/client/src/app/shared/shared-moderation/video-block.component.ts +++ b/client/src/app/shared/shared-moderation/video-block.component.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { prepareIcu } from '@app/helpers' | ||
3 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
4 | import { Video } from '@app/shared/shared-main' | 5 | import { Video } from '@app/shared/shared-main' |
5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
@@ -80,9 +81,10 @@ export class VideoBlockComponent extends FormReactive implements OnInit { | |||
80 | this.videoBlocklistService.blockVideo(options) | 81 | this.videoBlocklistService.blockVideo(options) |
81 | .subscribe({ | 82 | .subscribe({ |
82 | next: () => { | 83 | next: () => { |
83 | const message = this.isMultiple | 84 | const message = prepareIcu($localize`{count, plural, =1 {Blocked {videoName}} other {Blocked {count} videos}}.`)( |
84 | ? $localize`Blocked ${this.videos.length} videos.` | 85 | { count: this.videos.length, videoName: this.getSingleVideo().name }, |
85 | : $localize`Blocked ${this.getSingleVideo().name}` | 86 | $localize`Blocked ${this.videos.length} videos.` |
87 | ) | ||
86 | 88 | ||
87 | this.notifier.success(message) | 89 | this.notifier.success(message) |
88 | this.hide() | 90 | this.hide() |
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.html b/client/src/app/shared/shared-user-settings/user-video-settings.component.html index 446ade445..836972a33 100644 --- a/client/src/app/shared/shared-user-settings/user-video-settings.component.html +++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.html | |||
@@ -30,7 +30,7 @@ | |||
30 | </my-help> | 30 | </my-help> |
31 | 31 | ||
32 | <div> | 32 | <div> |
33 | <my-select-languages formControlName="videoLanguages"></my-select-languages> | 33 | <my-select-languages [maxLanguages]="20" formControlName="videoLanguages"></my-select-languages> |
34 | </div> | 34 | </div> |
35 | </div> | 35 | </div> |
36 | 36 | ||
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts index 42c472579..534a78b3f 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts | |||
@@ -175,7 +175,7 @@ export class VideoMiniatureComponent implements OnInit { | |||
175 | 175 | ||
176 | if (video.scheduledUpdate) { | 176 | if (video.scheduledUpdate) { |
177 | const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId) | 177 | const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId) |
178 | return $localize`Publication scheduled on ` + updateAt | 178 | return $localize`Publication scheduled on ${updateAt}` |
179 | } | 179 | } |
180 | 180 | ||
181 | if (video.state.id === VideoState.TRANSCODING_FAILED) { | 181 | if (video.state.id === VideoState.TRANSCODING_FAILED) { |