aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/angular.json6
-rw-r--r--client/package.json2
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-configuration.service.ts7
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.ts32
-rw-r--r--client/src/app/+admin/follows/following-list/follow-modal.component.ts8
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts14
-rw-r--r--client/src/app/+admin/moderation/registration-list/registration-list.component.ts14
-rw-r--r--client/src/app/+admin/overview/comments/video-comment-list.component.ts8
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.ts26
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.ts32
-rw-r--r--client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts9
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.ts14
-rw-r--r--client/src/app/app.module.ts10
-rw-r--r--client/src/app/helpers/i18n-utils.ts69
-rw-r--r--client/src/app/shared/shared-forms/select/select-checkbox-all.component.ts8
-rw-r--r--client/src/app/shared/shared-instance/instance-features-table.component.ts14
-rw-r--r--client/src/app/shared/shared-main/angular/from-now.pipe.ts17
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts9
-rw-r--r--client/src/app/shared/shared-moderation/user-ban-modal.component.ts14
-rw-r--r--client/src/app/shared/shared-moderation/video-block.component.ts8
-rw-r--r--client/tsconfig.json3
-rw-r--r--client/yarn.lock48
22 files changed, 230 insertions, 142 deletions
diff --git a/client/angular.json b/client/angular.json
index d929248d4..9b069422f 100644
--- a/client/angular.json
+++ b/client/angular.json
@@ -199,7 +199,11 @@
199 "is-plain-object", 199 "is-plain-object",
200 "parse-srcset", 200 "parse-srcset",
201 "deepmerge", 201 "deepmerge",
202 "core-js/features/reflect" 202 "core-js/features/reflect",
203 "@formatjs/intl-locale/polyfill",
204 "@formatjs/intl-locale/should-polyfill",
205 "@formatjs/intl-pluralrules/polyfill-force",
206 "@formatjs/intl-pluralrules/should-polyfill"
203 ], 207 ],
204 "scripts": [], 208 "scripts": [],
205 "vendorChunk": true, 209 "vendorChunk": true,
diff --git a/client/package.json b/client/package.json
index 202a0f836..564e56ae7 100644
--- a/client/package.json
+++ b/client/package.json
@@ -47,6 +47,8 @@
47 "@angular/service-worker": "^16.0.2", 47 "@angular/service-worker": "^16.0.2",
48 "@babel/core": "^7.18.5", 48 "@babel/core": "^7.18.5",
49 "@babel/preset-env": "^7.18.2", 49 "@babel/preset-env": "^7.18.2",
50 "@formatjs/intl-locale": "^3.3.1",
51 "@formatjs/intl-pluralrules": "^5.2.2",
50 "@ng-bootstrap/ng-bootstrap": "^14.0.1", 52 "@ng-bootstrap/ng-bootstrap": "^14.0.1",
51 "@ng-select/ng-select": "^10.0.3", 53 "@ng-select/ng-select": "^10.0.3",
52 "@ngx-loading-bar/core": "^6.0.0", 54 "@ngx-loading-bar/core": "^6.0.0",
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 628c2d102..42c0e6dc2 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,6 +1,6 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { FormGroup } from '@angular/forms' 2import { FormGroup } from '@angular/forms'
3import { prepareIcu } from '@app/helpers' 3import { formatICU } from '@app/helpers'
4 4
5export type ResolutionOption = { 5export type ResolutionOption = {
6 id: string 6 id: string
@@ -99,10 +99,7 @@ export class EditConfigurationService {
99 return { 99 return {
100 value, 100 value,
101 atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible 101 atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
102 unit: prepareIcu($localize`{value, plural, =1 {thread} other {threads}}`)( 102 unit: formatICU($localize`{value, plural, =1 {thread} other {threads}}`, { value })
103 { value },
104 $localize`threads`
105 )
106 } 103 }
107 } 104 }
108} 105}
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
index cebb2e1a2..618892242 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
@@ -1,7 +1,7 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { Component, OnInit } from '@angular/core' 2import { Component, OnInit } from '@angular/core'
3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' 3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
4import { prepareIcu } from '@app/helpers' 4import { formatICU } from '@app/helpers'
5import { AdvancedInputFilter } from '@app/shared/shared-forms' 5import { AdvancedInputFilter } from '@app/shared/shared-forms'
6import { InstanceFollowService } from '@app/shared/shared-instance' 6import { InstanceFollowService } from '@app/shared/shared-instance'
7import { DropdownAction } from '@app/shared/shared-main' 7import { DropdownAction } from '@app/shared/shared-main'
@@ -63,9 +63,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
63 .subscribe({ 63 .subscribe({
64 next: () => { 64 next: () => {
65 // eslint-disable-next-line max-len 65 // eslint-disable-next-line max-len
66 const message = prepareIcu($localize`Accepted {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( 66 const message = formatICU(
67 { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, 67 $localize`Accepted {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`,
68 $localize`Follow requests accepted` 68 { count: follows.length, followerName: this.buildFollowerName(follows[0]) }
69 ) 69 )
70 this.notifier.success(message) 70 this.notifier.success(message)
71 71
@@ -78,9 +78,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
78 78
79 async rejectFollower (follows: ActorFollow[]) { 79 async rejectFollower (follows: ActorFollow[]) {
80 // eslint-disable-next-line max-len 80 // eslint-disable-next-line max-len
81 const message = prepareIcu($localize`Do you really want to reject {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)( 81 const message = formatICU(
82 { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, 82 $localize`Do you really want to reject {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`,
83 $localize`Do you really want to reject these follow requests?` 83 { count: follows.length, followerName: this.buildFollowerName(follows[0]) }
84 ) 84 )
85 85
86 const res = await this.confirmService.confirm(message, $localize`Reject`) 86 const res = await this.confirmService.confirm(message, $localize`Reject`)
@@ -90,9 +90,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
90 .subscribe({ 90 .subscribe({
91 next: () => { 91 next: () => {
92 // eslint-disable-next-line max-len 92 // eslint-disable-next-line max-len
93 const message = prepareIcu($localize`Rejected {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( 93 const message = formatICU(
94 { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, 94 $localize`Rejected {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`,
95 $localize`Follow requests rejected` 95 { count: follows.length, followerName: this.buildFollowerName(follows[0]) }
96 ) 96 )
97 this.notifier.success(message) 97 this.notifier.success(message)
98 98
@@ -110,9 +110,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
110 message += '<br /><br />' 110 message += '<br /><br />'
111 111
112 // eslint-disable-next-line max-len 112 // eslint-disable-next-line max-len
113 message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)( 113 message += formatICU(
114 icuParams, 114 $localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`,
115 $localize`Do you really want to delete these follow requests?` 115 icuParams
116 ) 116 )
117 117
118 const res = await this.confirmService.confirm(message, $localize`Delete`) 118 const res = await this.confirmService.confirm(message, $localize`Delete`)
@@ -122,9 +122,9 @@ export class FollowersListComponent extends RestTable <ActorFollow> implements O
122 .subscribe({ 122 .subscribe({
123 next: () => { 123 next: () => {
124 // eslint-disable-next-line max-len 124 // eslint-disable-next-line max-len
125 const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( 125 const message = formatICU(
126 icuParams, 126 $localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`,
127 $localize`Follow requests removed` 127 icuParams
128 ) 128 )
129 129
130 this.notifier.success(message) 130 this.notifier.success(message)
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 8f74e82a6..54b3cebc5 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,6 +1,6 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { prepareIcu } from '@app/helpers' 3import { formatICU } from '@app/helpers'
4import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' 4import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators'
5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' 5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
6import { InstanceFollowService } from '@app/shared/shared-instance' 6import { InstanceFollowService } from '@app/shared/shared-instance'
@@ -62,9 +62,9 @@ export class FollowModalComponent extends FormReactive implements OnInit {
62 .subscribe({ 62 .subscribe({
63 next: () => { 63 next: () => {
64 this.notifier.success( 64 this.notifier.success(
65 prepareIcu($localize`{count, plural, =1 {Follow request sent!} other {Follow requests sent!}}`)( 65 formatICU(
66 { count: hostsOrHandles.length }, 66 $localize`{count, plural, =1 {Follow request sent!} other {Follow requests sent!}}`,
67 $localize`Follow request(s) sent!` 67 { count: hostsOrHandles.length }
68 ) 68 )
69 ) 69 )
70 70
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts
index 71f2fbe66..6c8723c16 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.ts
+++ b/client/src/app/+admin/follows/following-list/following-list.component.ts
@@ -6,7 +6,7 @@ import { InstanceFollowService } from '@app/shared/shared-instance'
6import { ActorFollow } from '@shared/models' 6import { ActorFollow } from '@shared/models'
7import { FollowModalComponent } from './follow-modal.component' 7import { FollowModalComponent } from './follow-modal.component'
8import { DropdownAction } from '@app/shared/shared-main' 8import { DropdownAction } from '@app/shared/shared-main'
9import { prepareIcu } from '@app/helpers' 9import { formatICU } from '@app/helpers'
10 10
11@Component({ 11@Component({
12 templateUrl: './following-list.component.html', 12 templateUrl: './following-list.component.html',
@@ -64,9 +64,9 @@ export class FollowingListComponent extends RestTable <ActorFollow> implements O
64 async removeFollowing (follows: ActorFollow[]) { 64 async removeFollowing (follows: ActorFollow[]) {
65 const icuParams = { count: follows.length, entryName: this.buildFollowingName(follows[0]) } 65 const icuParams = { count: follows.length, entryName: this.buildFollowingName(follows[0]) }
66 66
67 const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)( 67 const message = formatICU(
68 icuParams, 68 $localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`,
69 $localize`Do you really want to unfollow these entries?` 69 icuParams
70 ) 70 )
71 71
72 const res = await this.confirmService.confirm(message, $localize`Unfollow`) 72 const res = await this.confirmService.confirm(message, $localize`Unfollow`)
@@ -76,9 +76,9 @@ export class FollowingListComponent extends RestTable <ActorFollow> implements O
76 .subscribe({ 76 .subscribe({
77 next: () => { 77 next: () => {
78 // eslint-disable-next-line max-len 78 // eslint-disable-next-line max-len
79 const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)( 79 const message = formatICU(
80 icuParams, 80 $localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`,
81 $localize`You are not following them anymore.` 81 icuParams
82 ) 82 )
83 83
84 this.notifier.success(message) 84 this.notifier.success(message)
diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.ts b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts
index 3ca1ceab8..35d9d13d7 100644
--- a/client/src/app/+admin/moderation/registration-list/registration-list.component.ts
+++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts
@@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api'
2import { Component, OnInit, ViewChild } from '@angular/core' 2import { Component, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' 4import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
5import { prepareIcu } from '@app/helpers' 5import { formatICU } from '@app/helpers'
6import { AdvancedInputFilter } from '@app/shared/shared-forms' 6import { AdvancedInputFilter } from '@app/shared/shared-forms'
7import { DropdownAction } from '@app/shared/shared-main' 7import { DropdownAction } from '@app/shared/shared-main'
8import { UserRegistration, UserRegistrationState } from '@shared/models' 8import { UserRegistration, UserRegistrationState } from '@shared/models'
@@ -121,9 +121,9 @@ export class RegistrationListComponent extends RestTable <UserRegistration> impl
121 const icuParams = { count: registrations.length, username: registrations[0].username } 121 const icuParams = { count: registrations.length, username: registrations[0].username }
122 122
123 // eslint-disable-next-line max-len 123 // eslint-disable-next-line max-len
124 const message = prepareIcu($localize`Do you really want to delete {count, plural, =1 {{username} registration request?} other {{count} registration requests?}}`)( 124 const message = formatICU(
125 icuParams, 125 $localize`Do you really want to delete {count, plural, =1 {{username} registration request?} other {{count} registration requests?}}`,
126 $localize`Do you really want to delete these registration requests?` 126 icuParams
127 ) 127 )
128 128
129 const res = await this.confirmService.confirm(message, $localize`Delete`) 129 const res = await this.confirmService.confirm(message, $localize`Delete`)
@@ -133,9 +133,9 @@ export class RegistrationListComponent extends RestTable <UserRegistration> impl
133 .subscribe({ 133 .subscribe({
134 next: () => { 134 next: () => {
135 // eslint-disable-next-line max-len 135 // eslint-disable-next-line max-len
136 const message = prepareIcu($localize`Removed {count, plural, =1 {{username} registration request} other {{count} registration requests}}`)( 136 const message = formatICU(
137 icuParams, 137 $localize`Removed {count, plural, =1 {{username} registration request} other {{count} registration requests}}`,
138 $localize`Registration requests removed` 138 icuParams
139 ) 139 )
140 140
141 this.notifier.success(message) 141 this.notifier.success(message)
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 28efdc076..b77072665 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,7 +7,7 @@ import { DropdownAction } from '@app/shared/shared-main'
7import { BulkService } from '@app/shared/shared-moderation' 7import { BulkService } from '@app/shared/shared-moderation'
8import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' 8import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment'
9import { FeedFormat, UserRight } from '@shared/models' 9import { FeedFormat, UserRight } from '@shared/models'
10import { prepareIcu } from '@app/helpers' 10import { formatICU } from '@app/helpers'
11 11
12@Component({ 12@Component({
13 selector: 'my-video-comment-list', 13 selector: 'my-video-comment-list',
@@ -146,9 +146,9 @@ export class VideoCommentListComponent extends RestTable <VideoCommentAdmin> imp
146 .subscribe({ 146 .subscribe({
147 next: () => { 147 next: () => {
148 this.notifier.success( 148 this.notifier.success(
149 prepareIcu($localize`{count, plural, =1 {1 comment deleted.} other {{count} comments deleted.}}`)( 149 formatICU(
150 { count: commentArgs.length }, 150 $localize`{count, plural, =1 {1 comment deleted.} other {{count} comments deleted.}}`,
151 $localize`${commentArgs.length} comment(s) deleted.` 151 { count: commentArgs.length }
152 ) 152 )
153 ) 153 )
154 154
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 19420b748..5d5abf6f4 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'
2import { Component, OnInit, ViewChild } from '@angular/core' 2import { Component, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' 4import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
5import { getAPIHost, prepareIcu } from '@app/helpers' 5import { formatICU, getAPIHost } from '@app/helpers'
6import { AdvancedInputFilter } from '@app/shared/shared-forms' 6import { AdvancedInputFilter } from '@app/shared/shared-forms'
7import { Actor, DropdownAction } from '@app/shared/shared-main' 7import { Actor, DropdownAction } from '@app/shared/shared-main'
8import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation' 8import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation'
@@ -210,9 +210,9 @@ export class UserListComponent extends RestTable <User> implements OnInit {
210 210
211 async unbanUsers (users: User[]) { 211 async unbanUsers (users: User[]) {
212 const res = await this.confirmService.confirm( 212 const res = await this.confirmService.confirm(
213 prepareIcu($localize`Do you really want to unban {count, plural, =1 {1 user} other {{count} users}}?`)( 213 formatICU(
214 { count: users.length }, 214 $localize`Do you really want to unban {count, plural, =1 {1 user} other {{count} users}}?`,
215 $localize`Do you really want to unban ${users.length} users?` 215 { count: users.length }
216 ), 216 ),
217 $localize`Unban` 217 $localize`Unban`
218 ) 218 )
@@ -223,9 +223,9 @@ export class UserListComponent extends RestTable <User> implements OnInit {
223 .subscribe({ 223 .subscribe({
224 next: () => { 224 next: () => {
225 this.notifier.success( 225 this.notifier.success(
226 prepareIcu($localize`{count, plural, =1 {1 user unbanned.} other {{count} users unbanned.}}`)( 226 formatICU(
227 { count: users.length }, 227 $localize`{count, plural, =1 {1 user unbanned.} other {{count} users unbanned.}}`,
228 $localize`${users.length} users unbanned.` 228 { count: users.length }
229 ) 229 )
230 ) 230 )
231 this.reloadData() 231 this.reloadData()
@@ -252,9 +252,9 @@ export class UserListComponent extends RestTable <User> implements OnInit {
252 .subscribe({ 252 .subscribe({
253 next: () => { 253 next: () => {
254 this.notifier.success( 254 this.notifier.success(
255 prepareIcu($localize`{count, plural, =1 {1 user deleted.} other {{count} users deleted.}}`)( 255 formatICU(
256 { count: users.length }, 256 $localize`{count, plural, =1 {1 user deleted.} other {{count} users deleted.}}`,
257 $localize`${users.length} users deleted.` 257 { count: users.length }
258 ) 258 )
259 ) 259 )
260 260
@@ -270,9 +270,9 @@ export class UserListComponent extends RestTable <User> implements OnInit {
270 .subscribe({ 270 .subscribe({
271 next: () => { 271 next: () => {
272 this.notifier.success( 272 this.notifier.success(
273 prepareIcu($localize`{count, plural, =1 {1 user email set as verified.} other {{count} user emails set as verified.}}`)( 273 formatICU(
274 { count: users.length }, 274 $localize`{count, plural, =1 {1 user email set as verified.} other {{count} user emails set as verified.}}`,
275 $localize`${users.length} users email set as verified.` 275 { count: users.length }
276 ) 276 )
277 ) 277 )
278 278
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 ebf82ce16..e9c526193 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.ts
+++ b/client/src/app/+admin/overview/videos/video-list.component.ts
@@ -3,7 +3,7 @@ import { finalize } from 'rxjs/operators'
3import { Component, OnInit, ViewChild } from '@angular/core' 3import { Component, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' 5import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
6import { prepareIcu } from '@app/helpers' 6import { formatICU } from '@app/helpers'
7import { AdvancedInputFilter } from '@app/shared/shared-forms' 7import { AdvancedInputFilter } from '@app/shared/shared-forms'
8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
9import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' 9import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation'
@@ -219,9 +219,9 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
219 } 219 }
220 220
221 private async removeVideos (videos: Video[]) { 221 private async removeVideos (videos: Video[]) {
222 const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)( 222 const message = formatICU(
223 { count: videos.length }, 223 $localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`,
224 $localize`Are you sure you want to delete these ${videos.length} videos?` 224 { count: videos.length }
225 ) 225 )
226 226
227 const res = await this.confirmService.confirm(message, $localize`Delete`) 227 const res = await this.confirmService.confirm(message, $localize`Delete`)
@@ -231,9 +231,9 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
231 .subscribe({ 231 .subscribe({
232 next: () => { 232 next: () => {
233 this.notifier.success( 233 this.notifier.success(
234 prepareIcu($localize`Deleted {count, plural, =1 {1 video} other {{count} videos}}.`)( 234 formatICU(
235 { count: videos.length }, 235 $localize`Deleted {count, plural, =1 {1 video} other {{count} videos}}.`,
236 $localize`Deleted ${videos.length} videos.` 236 { count: videos.length }
237 ) 237 )
238 ) 238 )
239 239
@@ -249,9 +249,9 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
249 .subscribe({ 249 .subscribe({
250 next: () => { 250 next: () => {
251 this.notifier.success( 251 this.notifier.success(
252 prepareIcu($localize`Unblocked {count, plural, =1 {1 video} other {{count} videos}}.`)( 252 formatICU(
253 { count: videos.length }, 253 $localize`Unblocked {count, plural, =1 {1 video} other {{count} videos}}.`,
254 $localize`Unblocked ${videos.length} videos.` 254 { count: videos.length }
255 ) 255 )
256 ) 256 )
257 257
@@ -267,15 +267,15 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
267 267
268 if (type === 'hls') { 268 if (type === 'hls') {
269 // eslint-disable-next-line max-len 269 // eslint-disable-next-line max-len
270 message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {1 HLS streaming playlist} other {{count} HLS streaming playlists}}?`)( 270 message = formatICU(
271 { count: videos.length }, 271 $localize`Are you sure you want to delete {count, plural, =1 {1 HLS streaming playlist} other {{count} HLS streaming playlists}}?`,
272 $localize`Are you sure you want to delete ${videos.length} HLS streaming playlists?` 272 { count: videos.length }
273 ) 273 )
274 } else { 274 } else {
275 // eslint-disable-next-line max-len 275 // eslint-disable-next-line max-len
276 message = prepareIcu($localize`Are you sure you want to delete WebTorrent files of {count, plural, =1 {1 video} other {{count} videos}}?`)( 276 message = formatICU(
277 { count: videos.length }, 277 $localize`Are you sure you want to delete WebTorrent files of {count, plural, =1 {1 video} other {{count} videos}}?`,
278 $localize`Are you sure you want to delete WebTorrent files of ${videos.length} videos?` 278 { count: videos.length }
279 ) 279 )
280 } 280 }
281 281
diff --git a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts
index 8ba956eb8..8994c1d00 100644
--- a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts
+++ b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts
@@ -1,7 +1,7 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { Component, OnInit } from '@angular/core' 2import { Component, OnInit } from '@angular/core'
3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' 3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
4import { prepareIcu } from '@app/helpers' 4import { formatICU } from '@app/helpers'
5import { DropdownAction } from '@app/shared/shared-main' 5import { DropdownAction } from '@app/shared/shared-main'
6import { RunnerJob, RunnerJobState } from '@shared/models' 6import { RunnerJob, RunnerJobState } from '@shared/models'
7import { RunnerJobFormatted, RunnerService } from '../runner.service' 7import { RunnerJobFormatted, RunnerService } from '../runner.service'
@@ -57,9 +57,10 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
57 } 57 }
58 58
59 async cancelJobs (jobs: RunnerJob[]) { 59 async cancelJobs (jobs: RunnerJob[]) {
60 const message = prepareIcu( 60 const message = formatICU(
61 $localize`Do you really want to cancel {count, plural, =1 {this job} other {{count} jobs}}? Children jobs will also be cancelled.` 61 $localize`Do you really want to cancel {count, plural, =1 {this job} other {{count} jobs}}? Children jobs will also be cancelled.`,
62 )({ count: jobs.length }, $localize`Do you really want to cancel these jobs? Children jobs will also be cancelled.`) 62 { count: jobs.length }
63 )
63 64
64 const res = await this.confirmService.confirm(message, $localize`Cancel`) 65 const res = await this.confirmService.confirm(message, $localize`Cancel`)
65 66
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 57b8bdf7d..1827d6a0b 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
@@ -5,7 +5,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'
5import { ActivatedRoute, Router } from '@angular/router' 5import { ActivatedRoute, Router } from '@angular/router'
6import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' 6import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core'
7import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' 7import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
8import { immutableAssign, prepareIcu } from '@app/helpers' 8import { immutableAssign, formatICU } from '@app/helpers'
9import { AdvancedInputFilter } from '@app/shared/shared-forms' 9import { AdvancedInputFilter } from '@app/shared/shared-forms'
10import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 10import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
11import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' 11import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
@@ -184,9 +184,9 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
184 .map(([ k, _v ]) => parseInt(k, 10)) 184 .map(([ k, _v ]) => parseInt(k, 10))
185 185
186 const res = await this.confirmService.confirm( 186 const res = await this.confirmService.confirm(
187 prepareIcu($localize`Do you really want to delete {length, plural, =1 {this video} other {{length} videos}}?`)( 187 formatICU(
188 { length: toDeleteVideosIds.length }, 188 $localize`Do you really want to delete {length, plural, =1 {this video} other {{length} videos}}?`,
189 $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?` 189 { length: toDeleteVideosIds.length }
190 ), 190 ),
191 $localize`Delete` 191 $localize`Delete`
192 ) 192 )
@@ -205,9 +205,9 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
205 .subscribe({ 205 .subscribe({
206 next: () => { 206 next: () => {
207 this.notifier.success( 207 this.notifier.success(
208 prepareIcu($localize`{length, plural, =1 {Video has been deleted} other {{length} videos have been deleted}}`)( 208 formatICU(
209 { length: toDeleteVideosIds.length }, 209 $localize`{length, plural, =1 {Video has been deleted} other {{length} videos have been deleted}}`,
210 $localize`${toDeleteVideosIds.length} have been deleted.` 210 { length: toDeleteVideosIds.length }
211 ) 211 )
212 ) 212 )
213 213
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 7e4fac730..9339865f1 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -12,13 +12,14 @@ import { CoreModule, PluginService, RedirectService, ServerService } from './cor
12import { EmptyComponent } from './empty.component' 12import { EmptyComponent } from './empty.component'
13import { HeaderComponent, SearchTypeaheadComponent, SuggestionComponent } from './header' 13import { HeaderComponent, SearchTypeaheadComponent, SuggestionComponent } from './header'
14import { HighlightPipe } from './header/highlight.pipe' 14import { HighlightPipe } from './header/highlight.pipe'
15import { polyfillICU } from './helpers'
15import { LanguageChooserComponent, MenuComponent, NotificationComponent } from './menu' 16import { LanguageChooserComponent, MenuComponent, NotificationComponent } from './menu'
17import { AccountSetupWarningModalComponent } from './modal/account-setup-warning-modal.component'
18import { AdminWelcomeModalComponent } from './modal/admin-welcome-modal.component'
16import { ConfirmComponent } from './modal/confirm.component' 19import { ConfirmComponent } from './modal/confirm.component'
17import { CustomModalComponent } from './modal/custom-modal.component' 20import { CustomModalComponent } from './modal/custom-modal.component'
18import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' 21import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component'
19import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component' 22import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component'
20import { AdminWelcomeModalComponent } from './modal/admin-welcome-modal.component'
21import { AccountSetupWarningModalComponent } from './modal/account-setup-warning-modal.component'
22import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module' 23import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
23import { SharedFormModule } from './shared/shared-forms' 24import { SharedFormModule } from './shared/shared-forms'
24import { SharedGlobalIconModule } from './shared/shared-icons' 25import { SharedGlobalIconModule } from './shared/shared-icons'
@@ -90,6 +91,11 @@ export function loadConfigFactory (server: ServerService, pluginService: PluginS
90 useFactory: loadConfigFactory, 91 useFactory: loadConfigFactory,
91 deps: [ ServerService, PluginService, RedirectService ], 92 deps: [ ServerService, PluginService, RedirectService ],
92 multi: true 93 multi: true
94 },
95 {
96 provide: APP_INITIALIZER,
97 useFactory: () => polyfillICU,
98 multi: true
93 } 99 }
94 ] 100 ]
95}) 101})
diff --git a/client/src/app/helpers/i18n-utils.ts b/client/src/app/helpers/i18n-utils.ts
index b7d73d16b..9e22bb4c1 100644
--- a/client/src/app/helpers/i18n-utils.ts
+++ b/client/src/app/helpers/i18n-utils.ts
@@ -1,4 +1,6 @@
1import IntlMessageFormat from 'intl-messageformat' 1import IntlMessageFormat from 'intl-messageformat'
2import { shouldPolyfill as shouldPolyfillLocale } from '@formatjs/intl-locale/should-polyfill'
3import { shouldPolyfill as shouldPolyfillPlural } from '@formatjs/intl-pluralrules/should-polyfill'
2import { logger } from '@root-helpers/logger' 4import { logger } from '@root-helpers/logger'
3import { environment } from '../../environments/environment' 5import { environment } from '../../environments/environment'
4 6
@@ -10,31 +12,68 @@ function getDevLocale () {
10 return 'fr-FR' 12 return 'fr-FR'
11} 13}
12 14
13function prepareIcu (icu: string) { 15async function polyfillICU () {
14 let alreadyWarned = false 16 // Important to be in this order, Plural needs Locale (https://formatjs.io/docs/polyfills/intl-pluralrules)
17 await polyfillICULocale()
18 await polyfillICUPlural()
19}
15 20
16 try { 21async function polyfillICULocale () {
17 const msg = new IntlMessageFormat(icu, $localize.locale) 22 // This locale is supported
23 if (shouldPolyfillLocale()) {
24 // TODO: remove, it's only needed to support Plural polyfill and so iOS 12
25 console.log('Loading Intl Locale polyfill for ' + $localize.locale)
26
27 await import('@formatjs/intl-locale/polyfill')
28 }
29}
30
31async function polyfillICUPlural () {
32 const unsupportedLocale = shouldPolyfillPlural($localize.locale)
33
34 // This locale is supported
35 if (!unsupportedLocale) {
36 return
37 }
18 38
19 return (context: { [id: string]: number | string }, fallback: string) => { 39 // TODO: remove, it's only needed to support iOS 12
20 try { 40 console.log('Loading Intl Plural rules polyfill for ' + $localize.locale)
21 return msg.format(context) as string
22 } catch (err) {
23 if (!alreadyWarned) logger.warn(`Cannot format ICU ${icu}.`, err)
24 41
25 alreadyWarned = true 42 // Load the polyfill 1st BEFORE loading data
26 return fallback 43 await import('@formatjs/intl-pluralrules/polyfill-force')
27 } 44 // Degraded mode, so only load the en local data
45 await import(`@formatjs/intl-pluralrules/locale-data/en.js`)
46}
47
48// ---------------------------------------------------------------------------
49
50const icuCache = new Map<string, IntlMessageFormat>()
51const icuWarnings = new Set<string>()
52const fallback = 'String translation error'
53
54function formatICU (icu: string, context: { [id: string]: number | string }) {
55 try {
56 let msg = icuCache.get(icu)
57
58 if (!msg) {
59 msg = new IntlMessageFormat(icu, $localize.locale)
60 icuCache.set(icu, msg)
28 } 61 }
62
63 return msg.format(context) as string
29 } catch (err) { 64 } catch (err) {
30 logger.warn(`Cannot build intl message ${icu}.`, err) 65 if (!icuWarnings.has(icu)) {
66 logger.warn(`Cannot format ICU ${icu}.`, err)
67 }
31 68
32 return (_context: unknown, fallback: string) => fallback 69 icuWarnings.add(icu)
70 return fallback
33 } 71 }
34} 72}
35 73
36export { 74export {
37 getDevLocale, 75 getDevLocale,
38 prepareIcu, 76 polyfillICU,
77 formatICU,
39 isOnDevLocale 78 isOnDevLocale
40} 79}
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 2c3226f68..8b6cd091a 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,7 +1,7 @@
1import { Component, forwardRef, Input } from '@angular/core' 1import { Component, forwardRef, Input } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { prepareIcu } from '@app/helpers' 4import { formatICU } from '@app/helpers'
5import { SelectOptionsItem } from '../../../../types/select-options-item.model' 5import { SelectOptionsItem } from '../../../../types/select-options-item.model'
6import { ItemSelectCheckboxValue } from './select-checkbox.component' 6import { ItemSelectCheckboxValue } from './select-checkbox.component'
7 7
@@ -80,9 +80,9 @@ export class SelectCheckboxAllComponent implements ControlValueAccessor {
80 80
81 if (outputItems.length >= this.maxItems) { 81 if (outputItems.length >= this.maxItems) {
82 this.notifier.error( 82 this.notifier.error(
83 prepareIcu($localize`You can't select more than {maxItems, plural, =1 {1 item} other {{maxItems} items}}`)( 83 formatICU(
84 { maxItems: this.maxItems }, 84 $localize`You can't select more than {maxItems, plural, =1 {1 item} other {{maxItems} items}}`,
85 $localize`You can't select more than ${this.maxItems} items` 85 { maxItems: this.maxItems }
86 ) 86 )
87 ) 87 )
88 88
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 2e63f6c17..ab1b1458a 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,6 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
3import { prepareIcu } from '@app/helpers' 3import { formatICU } from '@app/helpers'
4import { ServerConfig } from '@shared/models' 4import { ServerConfig } from '@shared/models'
5 5
6@Component({ 6@Component({
@@ -71,17 +71,17 @@ export class InstanceFeaturesTableComponent implements OnInit {
71 const hours = Math.floor(seconds / 3600) 71 const hours = Math.floor(seconds / 3600)
72 72
73 if (hours !== 0) { 73 if (hours !== 0) {
74 return prepareIcu($localize`~ {hours, plural, =1 {1 hour} other {{hours} hours}}`)( 74 return formatICU(
75 { hours }, 75 $localize`~ {hours, plural, =1 {1 hour} other {{hours} hours}}`,
76 $localize`~ ${hours} hours` 76 { hours }
77 ) 77 )
78 } 78 }
79 79
80 const minutes = Math.floor(seconds % 3600 / 60) 80 const minutes = Math.floor(seconds % 3600 / 60)
81 81
82 return prepareIcu($localize`~ {minutes, plural, =1 {1 minute} other {{minutes} minutes}}`)( 82 return formatICU(
83 { minutes }, 83 $localize`~ {minutes, plural, =1 {1 minute} other {{minutes} minutes}}`,
84 $localize`~ ${minutes} minutes` 84 { minutes }
85 ) 85 )
86 } 86 }
87 87
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 dc6a25e83..4ff244bbb 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,14 +1,9 @@
1import { Pipe, PipeTransform } from '@angular/core' 1import { Pipe, PipeTransform } from '@angular/core'
2import { prepareIcu } from '@app/helpers' 2import { formatICU } from '@app/helpers'
3 3
4// 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
5@Pipe({ name: 'myFromNow' }) 5@Pipe({ name: 'myFromNow' })
6export class FromNowPipe implements PipeTransform { 6export 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 7
13 transform (arg: number | Date | string) { 8 transform (arg: number | Date | string) {
14 const argDate = new Date(arg) 9 const argDate = new Date(arg)
@@ -16,7 +11,7 @@ export class FromNowPipe implements PipeTransform {
16 11
17 let interval = Math.floor(seconds / 31536000) 12 let interval = Math.floor(seconds / 31536000)
18 if (interval >= 1) { 13 if (interval >= 1) {
19 return this.yearICU({ interval }, $localize`${interval} year(s) ago`) 14 return formatICU($localize`{interval, plural, =1 {1 year ago} other {{interval} years ago}}`, { interval })
20 } 15 }
21 16
22 interval = Math.floor(seconds / 2419200) 17 interval = Math.floor(seconds / 2419200)
@@ -25,7 +20,7 @@ export class FromNowPipe implements PipeTransform {
25 if (interval >= 12) return $localize`1 year ago` 20 if (interval >= 12) return $localize`1 year ago`
26 21
27 if (interval >= 1) { 22 if (interval >= 1) {
28 return this.monthICU({ interval }, $localize`${interval} month(s) ago`) 23 return formatICU($localize`{interval, plural, =1 {1 month ago} other {{interval} months ago}}`, { interval })
29 } 24 }
30 25
31 interval = Math.floor(seconds / 604800) 26 interval = Math.floor(seconds / 604800)
@@ -34,17 +29,17 @@ export class FromNowPipe implements PipeTransform {
34 if (interval >= 4) return $localize`1 month ago` 29 if (interval >= 4) return $localize`1 month ago`
35 30
36 if (interval >= 1) { 31 if (interval >= 1) {
37 return this.weekICU({ interval }, $localize`${interval} week(s) ago`) 32 return formatICU($localize`{interval, plural, =1 {1 week ago} other {{interval} weeks ago}}`, { interval })
38 } 33 }
39 34
40 interval = Math.floor(seconds / 86400) 35 interval = Math.floor(seconds / 86400)
41 if (interval >= 1) { 36 if (interval >= 1) {
42 return this.dayICU({ interval }, $localize`${interval} day(s) ago`) 37 return formatICU($localize`{interval, plural, =1 {1 day ago} other {{interval} days ago}}`, { interval })
43 } 38 }
44 39
45 interval = Math.floor(seconds / 3600) 40 interval = Math.floor(seconds / 3600)
46 if (interval >= 1) { 41 if (interval >= 1) {
47 return this.hourICU({ interval }, $localize`${interval} hour(s) ago`) 42 return formatICU($localize`{interval, plural, =1 {1 hour ago} other {{interval} hours ago}}`, { interval })
48 } 43 }
49 44
50 interval = Math.floor(seconds / 60) 45 interval = Math.floor(seconds / 60)
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 24c00c3d5..e94087dbe 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 @@
1import { AuthUser } from '@app/core' 1import { AuthUser } from '@app/core'
2import { User } from '@app/core/users/user.model' 2import { User } from '@app/core/users/user.model'
3import { durationToString, getAbsoluteAPIUrl, getAbsoluteEmbedUrl, prepareIcu } from '@app/helpers' 3import { durationToString, formatICU, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
4import { Actor } from '@app/shared/shared-main/account/actor.model' 4import { Actor } from '@app/shared/shared-main/account/actor.model'
5import { buildVideoWatchPath, getAllFiles } from '@shared/core-utils' 5import { buildVideoWatchPath, getAllFiles } from '@shared/core-utils'
6import { peertubeTranslate } from '@shared/core-utils/i18n' 6import { peertubeTranslate } from '@shared/core-utils/i18n'
@@ -19,9 +19,6 @@ import {
19} from '@shared/models' 19} from '@shared/models'
20 20
21export class Video implements VideoServerModel { 21export 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
25 byVideoChannel: string 22 byVideoChannel: string
26 byAccount: string 23 byAccount: string
27 24
@@ -290,9 +287,9 @@ export class Video implements VideoServerModel {
290 287
291 getExactNumberOfViews () { 288 getExactNumberOfViews () {
292 if (this.isLive) { 289 if (this.isLive) {
293 return Video.viewersICU({ viewers: this.viewers }, $localize`${this.viewers} viewer(s)`) 290 return formatICU($localize`{viewers, plural, =0 {No viewers} =1 {1 viewer} other {{viewers} viewers}}`, { viewers: this.viewers })
294 } 291 }
295 292
296 return Video.viewsICU({ views: this.views }, $localize`{${this.views} view(s)}`) 293 return formatICU($localize`{views, plural, =0 {No view} =1 {1 view} other {{views} views}}`, { views: this.views })
297 } 294 }
298} 295}
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 27dcf043a..34295c34a 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,7 +1,7 @@
1import { forkJoin } from 'rxjs' 1import { forkJoin } from 'rxjs'
2import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 2import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { prepareIcu } from '@app/helpers' 4import { formatICU } from '@app/helpers'
5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' 5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -67,9 +67,9 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
67 let message: string 67 let message: string
68 68
69 if (Array.isArray(this.usersToBan)) { 69 if (Array.isArray(this.usersToBan)) {
70 message = prepareIcu($localize`{count, plural, =1 {1 user banned.} other {{count} users banned.}}`)( 70 message = formatICU(
71 { count: this.usersToBan.length }, 71 $localize`{count, plural, =1 {1 user banned.} other {{count} users banned.}}`,
72 $localize`${this.usersToBan.length} users banned.` 72 { count: this.usersToBan.length }
73 ) 73 )
74 } else { 74 } else {
75 message = $localize`User ${this.usersToBan.username} banned.` 75 message = $localize`User ${this.usersToBan.username} banned.`
@@ -88,9 +88,9 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
88 88
89 getModalTitle () { 89 getModalTitle () {
90 if (Array.isArray(this.usersToBan)) { 90 if (Array.isArray(this.usersToBan)) {
91 return prepareIcu($localize`Ban {count, plural, =1 {1 user} other {{count} users}}`)( 91 return formatICU(
92 { count: this.usersToBan.length }, 92 $localize`Ban {count, plural, =1 {1 user} other {{count} users}}`,
93 $localize`Ban ${this.usersToBan.length} users` 93 { count: this.usersToBan.length }
94 ) 94 )
95 } 95 }
96 96
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 3ff53443a..0137def89 100644
--- a/client/src/app/shared/shared-moderation/video-block.component.ts
+++ b/client/src/app/shared/shared-moderation/video-block.component.ts
@@ -1,6 +1,6 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { prepareIcu } from '@app/helpers' 3import { formatICU } from '@app/helpers'
4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' 4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
5import { Video } from '@app/shared/shared-main' 5import { Video } from '@app/shared/shared-main'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -81,9 +81,9 @@ export class VideoBlockComponent extends FormReactive implements OnInit {
81 this.videoBlocklistService.blockVideo(options) 81 this.videoBlocklistService.blockVideo(options)
82 .subscribe({ 82 .subscribe({
83 next: () => { 83 next: () => {
84 const message = prepareIcu($localize`{count, plural, =1 {Blocked {videoName}.} other {Blocked {count} videos.}}`)( 84 const message = formatICU(
85 { count: this.videos.length, videoName: this.getSingleVideo().name }, 85 $localize`{count, plural, =1 {Blocked {videoName}.} other {Blocked {count} videos.}}`,
86 $localize`Blocked ${this.videos.length} videos.` 86 { count: this.videos.length, videoName: this.getSingleVideo().name }
87 ) 87 )
88 88
89 this.notifier.success(message) 89 this.notifier.success(message)
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 785ed1c6c..f6409402a 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -89,8 +89,7 @@
89 ], 89 ],
90 "exclude": [ 90 "exclude": [
91 "../node_modules", 91 "../node_modules",
92 "../server", 92 "../server"
93 "node_modules"
94 ], 93 ],
95 "angularCompilerOptions": { 94 "angularCompilerOptions": {
96 "strictInjectionParameters": true, 95 "strictInjectionParameters": true,
diff --git a/client/yarn.lock b/client/yarn.lock
index aeb13a7b5..282c851ec 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -1647,6 +1647,14 @@
1647 "@formatjs/intl-localematcher" "0.2.32" 1647 "@formatjs/intl-localematcher" "0.2.32"
1648 tslib "^2.4.0" 1648 tslib "^2.4.0"
1649 1649
1650"@formatjs/ecma402-abstract@1.16.0":
1651 version "1.16.0"
1652 resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.16.0.tgz#15a0baa8401880d4010eb93440d996e896ca251c"
1653 integrity sha512-qIH2cmG/oHGrVdApbqDf6/YR+B2A4NdkBjKLeq369OMVkqMFsC5oPSP1xpiyL1cAn+PbNEZHxwOVMYD/C76c6g==
1654 dependencies:
1655 "@formatjs/intl-localematcher" "0.3.0"
1656 tslib "^2.4.0"
1657
1650"@formatjs/fast-memoize@2.0.1": 1658"@formatjs/fast-memoize@2.0.1":
1651 version "2.0.1" 1659 version "2.0.1"
1652 resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.0.1.tgz#f15aaa73caad5562899c69bdcad8db82adcd3b0b" 1660 resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.0.1.tgz#f15aaa73caad5562899c69bdcad8db82adcd3b0b"
@@ -1671,6 +1679,30 @@
1671 "@formatjs/ecma402-abstract" "1.15.0" 1679 "@formatjs/ecma402-abstract" "1.15.0"
1672 tslib "^2.4.0" 1680 tslib "^2.4.0"
1673 1681
1682"@formatjs/intl-enumerator@1.3.1":
1683 version "1.3.1"
1684 resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.3.1.tgz#32f0a3b5aece244977aad16fe1cd5c205defc7f8"
1685 integrity sha512-UMuD1tNRb8JC+mZo0KeuSntJNie0+TLxXl/1QxRIRMR7z2UuJgphrK/UTUibAx9hjywL1qGdNNhD6QX//pvNyA==
1686 dependencies:
1687 tslib "^2.4.0"
1688
1689"@formatjs/intl-getcanonicallocales@2.2.1":
1690 version "2.2.1"
1691 resolved "https://registry.yarnpkg.com/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.2.1.tgz#0d70251b16bec06c1c4a40b37892ea5b51e3a2bb"
1692 integrity sha512-KooqmyY+Mhq3ioASPzoU6p6Cy9Mx+cWSVQSP6lF+vEW2tiaN90ti08cp82p1dzFschenduOYgPKrNcBpsDi6+g==
1693 dependencies:
1694 tslib "^2.4.0"
1695
1696"@formatjs/intl-locale@^3.3.1":
1697 version "3.3.1"
1698 resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-3.3.1.tgz#2b86e85319913e0bedfcd64884be33bc4bcef73e"
1699 integrity sha512-Rg3BLIjMzVxBcZCsPhvwIrcfc+UVEzPZPKnvoOLh6KNNWrzWnRo0ORQEE/KRDyvYZxAmALV/GHCcRl+qlchKuw==
1700 dependencies:
1701 "@formatjs/ecma402-abstract" "1.16.0"
1702 "@formatjs/intl-enumerator" "1.3.1"
1703 "@formatjs/intl-getcanonicallocales" "2.2.1"
1704 tslib "^2.4.0"
1705
1674"@formatjs/intl-localematcher@0.2.32": 1706"@formatjs/intl-localematcher@0.2.32":
1675 version "0.2.32" 1707 version "0.2.32"
1676 resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz#00d4d307cd7d514b298e15a11a369b86c8933ec1" 1708 resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz#00d4d307cd7d514b298e15a11a369b86c8933ec1"
@@ -1678,6 +1710,22 @@
1678 dependencies: 1710 dependencies:
1679 tslib "^2.4.0" 1711 tslib "^2.4.0"
1680 1712
1713"@formatjs/intl-localematcher@0.3.0":
1714 version "0.3.0"
1715 resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.3.0.tgz#9ad570d90d302b60bcbe78efd5fcd7593c440579"
1716 integrity sha512-NFoxXX3dtZ6B53NlCErq181NxN/noMZOWKHfcEPQRNfV0a19THxyjxu2RTSNS3532wGm6fOdid5qsBQWg0Rhtw==
1717 dependencies:
1718 tslib "^2.4.0"
1719
1720"@formatjs/intl-pluralrules@^5.2.2":
1721 version "5.2.2"
1722 resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.2.tgz#6322d20a6d0172459e4faf4b0f06603c931673aa"
1723 integrity sha512-mEbnbRzsSCIYqaBmrmUlOsPu5MG6KfMcnzekPzUrUucX2dNiI1KWBGHK6IoXl5c8zx60L1NXJ6cSQ7akoc15SQ==
1724 dependencies:
1725 "@formatjs/ecma402-abstract" "1.15.0"
1726 "@formatjs/intl-localematcher" "0.2.32"
1727 tslib "^2.4.0"
1728
1681"@gar/promisify@^1.1.3": 1729"@gar/promisify@^1.1.3":
1682 version "1.1.3" 1730 version "1.1.3"
1683 resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" 1731 resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"