diff --git a/client/package.json b/client/package.json
index b0d175b1a..30f1c4cbe 100644
--- a/client/package.json
+++ b/client/package.json
@@ -100,6 +100,7 @@
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.3.1",
"https-browserify": "^1.0.0",
+ "intl-messageformat": "^10.0.1",
"jschannel": "^1.0.2",
"linkify-html": "^3.0.2",
"linkify-plugin-mention": "^3.0.2",
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 @@
-
{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
+
{videoChannel.followersCount, plural, =0 {No subscribers} =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
- {getTotalVideosOf(videoChannel), plural, =1 {1 videos} other {{{ getTotalVideosOf(videoChannel) }} videos}}
+ {getTotalVideosOf(videoChannel), plural, =0 {No videos} =1 {1 video} other {{{ getTotalVideosOf(videoChannel) }} videos}}
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 @@
- {naiveAggregatedSubscribers(), plural, =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}
+ {naiveAggregatedSubscribers(), plural, =0 {No subscribers} =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}
- {accountVideosCount, plural, =1 {1 videos} other {{{ accountVideosCount }} videos}}
+ {accountVideosCount, plural, =0 {No videos} =1 {1 video} other {{{ accountVideosCount }} videos}}
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 {
links: ListOverflowItem[] = []
hideMenu = false
- accountFollowerTitle = ''
-
accountVideosCount: number
accountDescriptionHTML = ''
accountDescriptionExpanded = false
@@ -121,12 +119,6 @@ export class AccountsComponent implements OnInit, OnDestroy {
this.notifier.success($localize`Username copied`)
}
- subscribersDisplayFor (count: number) {
- if (count === 1) return $localize`1 subscriber`
-
- return $localize`${count} subscribers`
- }
-
searchChanged (search: string) {
const queryParams = { search }
@@ -150,8 +142,6 @@ export class AccountsComponent implements OnInit, OnDestroy {
}
private async onAccount (account: Account) {
- this.accountFollowerTitle = $localize`${account.followersCount} direct account followers`
-
this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description)
// 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 @@
import { Injectable } from '@angular/core'
import { FormGroup } from '@angular/forms'
+import { prepareIcu } from '@app/helpers'
export type ResolutionOption = {
id: string
@@ -86,9 +87,10 @@ export class EditConfigurationService {
return {
value,
atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
- unit: value > 1
- ? $localize`threads`
- : $localize`thread`
+ unit: prepareIcu($localize`{value, plural, =1 {thread} other {threads}}`)(
+ { value },
+ $localize`threads`
+ )
}
}
}
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 @@
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
import { Notifier } from '@app/core'
+import { prepareIcu } from '@app/helpers'
import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { InstanceFollowService } from '@app/shared/shared-instance'
@@ -60,7 +61,13 @@ export class FollowModalComponent extends FormReactive implements OnInit {
this.followService.follow(hostsOrHandles)
.subscribe({
next: () => {
- this.notifier.success($localize`Follow request(s) sent!`)
+ this.notifier.success(
+ prepareIcu($localize`{count, plural, =1 {Follow request} other {Follow requests}} sent!`)(
+ { count: hostsOrHandles.length },
+ $localize`Follow request(s) sent!`
+ )
+ )
+
this.newFollow.emit()
},
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'
import { BulkService } from '@app/shared/shared-moderation'
import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment'
import { FeedFormat, UserRight } from '@shared/models'
+import { prepareIcu } from '@app/helpers'
@Component({
selector: 'my-video-comment-list',
@@ -145,7 +146,13 @@ export class VideoCommentListComponent extends RestTable implements OnInit {
this.videoCommentService.deleteVideoComments(commentArgs)
.subscribe({
next: () => {
- this.notifier.success($localize`${commentArgs.length} comments deleted.`)
+ this.notifier.success(
+ prepareIcu($localize`{count, plural, =1 {1 comment} other {{count} comments}} deleted.`)(
+ { count: commentArgs.length },
+ $localize`${commentArgs.length} comment(s) deleted.`
+ )
+ )
+
this.reloadData()
},
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'
import { Component, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
-import { getAPIHost } from '@app/helpers'
+import { prepareIcu, getAPIHost } from '@app/helpers'
import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { Actor, DropdownAction } from '@app/shared/shared-main'
import { AccountMutedStatus, BlocklistService, UserBanModalComponent, UserModerationDisplayType } from '@app/shared/shared-moderation'
@@ -209,13 +209,25 @@ export class UserListComponent extends RestTable implements OnInit {
}
async unbanUsers (users: User[]) {
- const res = await this.confirmService.confirm($localize`Do you really want to unban ${users.length} users?`, $localize`Unban`)
+ const res = await this.confirmService.confirm(
+ prepareIcu($localize`Do you really want to unban {count, plural, =1 {1 user} other {{count} users}}?`)(
+ { count: users.length },
+ $localize`Do you really want to unban ${users.length} users?`
+ ),
+ $localize`Unban`
+ )
+
if (res === false) return
this.userAdminService.unbanUsers(users)
.subscribe({
next: () => {
- this.notifier.success($localize`${users.length} users unbanned.`)
+ this.notifier.success(
+ prepareIcu($localize`{count, plural, =1 {1 user} other {{count} users}} unbanned.`)(
+ { count: users.length },
+ $localize`${users.length} users unbanned.`
+ )
+ )
this.reloadData()
},
@@ -224,21 +236,28 @@ export class UserListComponent extends RestTable implements OnInit {
}
async removeUsers (users: User[]) {
- for (const user of users) {
- if (user.username === 'root') {
- this.notifier.error($localize`You cannot delete root.`)
- return
- }
+ if (users.some(u => u.username === 'root')) {
+ this.notifier.error($localize`You cannot delete root.`)
+ return
}
- const message = $localize`If you remove these users, you will not be able to create others with the same username!`
+ const message = $localize`You can't create users or channels with a username that already used by a deleted user/channel.
` +
+ $localize`It means the following usernames will be permanently deleted and cannot be recovered:` +
+ '' + users.map(u => '- ' + u.username + '
').join('') + '
'
+
const res = await this.confirmService.confirm(message, $localize`Delete`)
if (res === false) return
this.userAdminService.removeUser(users)
.subscribe({
next: () => {
- this.notifier.success($localize`${users.length} users deleted.`)
+ this.notifier.success(
+ prepareIcu($localize`{count, plural, =1 {1 user} other {{count} users}} deleted.`)(
+ { count: users.length },
+ $localize`${users.length} users deleted.`
+ )
+ )
+
this.reloadData()
},
@@ -250,7 +269,13 @@ export class UserListComponent extends RestTable implements OnInit {
this.userAdminService.updateUsers(users, { emailVerified: true })
.subscribe({
next: () => {
- this.notifier.success($localize`${users.length} users email set as verified.`)
+ this.notifier.success(
+ prepareIcu($localize`{count, plural, =1 {1 user} other {{count} users}} email set as verified.`)(
+ { count: users.length },
+ $localize`${users.length} users email set as verified.`
+ )
+ )
+
this.reloadData()
},
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'
import { Component, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
+import { prepareIcu } from '@app/helpers'
import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation'
@@ -196,14 +197,24 @@ export class VideoListComponent extends RestTable implements OnInit {
}
private async removeVideos (videos: Video[]) {
- const message = $localize`Are you sure you want to delete these ${videos.length} videos?`
+ const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)(
+ { count: videos.length },
+ $localize`Are you sure you want to delete these ${videos.length} videos?`
+ )
+
const res = await this.confirmService.confirm(message, $localize`Delete`)
if (res === false) return
this.videoService.removeVideo(videos.map(v => v.id))
.subscribe({
next: () => {
- this.notifier.success($localize`Deleted ${videos.length} videos.`)
+ this.notifier.success(
+ prepareIcu($localize`Deleted {count, plural, =1 {1 video} other {{count} videos}}.`)(
+ { count: videos.length },
+ $localize`Deleted ${videos.length} videos.`
+ )
+ )
+
this.reloadData()
},
@@ -215,7 +226,13 @@ export class VideoListComponent extends RestTable implements OnInit {
this.videoBlockService.unblockVideo(videos.map(v => v.id))
.subscribe({
next: () => {
- this.notifier.success($localize`Unblocked ${videos.length} videos.`)
+ this.notifier.success(
+ prepareIcu($localize`Unblocked {count, plural, =1 {1 video} other {{count} videos}}.`)(
+ { count: videos.length },
+ $localize`Unblocked ${videos.length} videos.`
+ )
+ )
+
this.reloadData()
},
@@ -224,9 +241,21 @@ export class VideoListComponent extends RestTable implements OnInit {
}
private async removeVideoFiles (videos: Video[], type: 'hls' | 'webtorrent') {
- const message = type === 'hls'
- ? $localize`Are you sure you want to delete ${videos.length} HLS streaming playlists?`
- : $localize`Are you sure you want to delete WebTorrent files of ${videos.length} videos?`
+ let message: string
+
+ if (type === 'hls') {
+ // eslint-disable-next-line max-len
+ message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {1 HLS streaming playlist} other {{count} HLS streaming playlists}}?`)(
+ { count: videos.length },
+ $localize`Are you sure you want to delete ${videos.length} HLS streaming playlists?`
+ )
+ } else {
+ // eslint-disable-next-line max-len
+ message = prepareIcu($localize`Are you sure you want to delete WebTorrent files of {count, plural, =1 {1 video} other {{count} videos}}?`)(
+ { count: videos.length },
+ $localize`Are you sure you want to delete WebTorrent files of ${videos.length} videos?`
+ )
+ }
const res = await this.confirmService.confirm(message, $localize`Delete`)
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 {
myVideoPublished: $localize`Video published (after transcoding/scheduled update)`,
myVideoImportFinished: $localize`Video import finished`,
newUserRegistration: $localize`A new user registered on your instance`,
- newFollow: $localize`You or your channel(s) has a new follower`,
+ newFollow: $localize`You or one of your channels has a new follower`,
commentMention: $localize`Someone mentioned you in video comments`,
newInstanceFollower: $localize`Your instance has a new follower`,
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 {
.subscribe({
next: () => {
const message = this.videosHistoryEnabled === true
- ? $localize`Videos history is enabled`
- : $localize`Videos history is disabled`
+ ? $localize`Video history is enabled`
+ : $localize`Video history is disabled`
this.notifier.success(message)
@@ -117,8 +117,8 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook {
}
async clearAllHistory () {
- const title = $localize`Delete videos history`
- const message = $localize`Are you sure you want to delete all your videos history?`
+ const title = $localize`Delete video history`
+ const message = $localize`Are you sure you want to delete all your video history?`
const res = await this.confirmService.confirm(message, title)
if (res !== true) return
@@ -126,7 +126,7 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook {
this.userHistoryService.clearAll()
.subscribe({
next: () => {
- this.notifier.success($localize`Videos history deleted`)
+ this.notifier.success($localize`Video history deleted`)
this.reloadData()
},
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'
import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core'
import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
-import { immutableAssign } from '@app/helpers'
+import { prepareIcu, immutableAssign } from '@app/helpers'
import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
@@ -167,7 +167,10 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
.map(k => parseInt(k, 10))
const res = await this.confirmService.confirm(
- $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?`,
+ prepareIcu($localize`Do you really want to delete {length, plural, =1 {this video} other {{length} videos}}?`)(
+ { length: toDeleteVideosIds.length },
+ $localize`Do you really want to delete ${toDeleteVideosIds.length} videos?`
+ ),
$localize`Delete`
)
if (res === false) return
@@ -184,7 +187,13 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
.pipe(toArray())
.subscribe({
next: () => {
- this.notifier.success($localize`${toDeleteVideosIds.length} videos deleted.`)
+ this.notifier.success(
+ prepareIcu($localize`{length, plural, =1 {Video has been deleted} other {{length} videos have been deleted}}`)(
+ { length: toDeleteVideosIds.length },
+ $localize`${toDeleteVideosIds.length} have been deleted.`
+ )
+ )
+
this.selection = {}
},
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 {
}
private updateTitle () {
- const suffix = this.currentSearch
- ? ' ' + this.currentSearch
- : ''
+ const title = this.currentSearch
+ ? $localize`Search ${this.currentSearch}`
+ : $localize`Search`
- this.metaService.setTitle($localize`Search` + suffix)
+ this.metaService.setTitle(title)
}
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 @@
- {videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
+ {videoChannel.followersCount, plural, =0 {No subscribers} =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
- {channelVideosCount, plural, =1 {1 videos} other {{{ channelVideosCount }} videos}}
+ {channelVideosCount, plural, =0 {No videos} =1 {1 video} other {{{ channelVideosCount }} videos}}
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
if ([ 'hot', 'trending', 'likes', 'views' ].includes(sanitizedSort)) {
this.title = $localize`Trending`
- if (sanitizedSort === 'hot') this.titleTooltip = $localize`Videos with the most interactions for recent videos`
- if (sanitizedSort === 'likes') this.titleTooltip = $localize`Videos that have the most likes`
- if (sanitizedSort === 'views') this.titleTooltip = undefined
+ if (sanitizedSort === 'hot') {
+ this.titleTooltip = $localize`Videos with the most interactions for recent videos`
+ return
+ }
+
+ if (sanitizedSort === 'likes') {
+ this.titleTooltip = $localize`Videos that have the most likes`
+ return
+ }
+
+ if (sanitizedSort === 'views') {
+ this.titleTooltip = undefined
+ return
+ }
if (sanitizedSort === 'trending') {
- if (this.trendingDays === 1) this.titleTooltip = $localize`Videos with the most views during the last 24 hours`
- else this.titleTooltip = $localize`Videos with the most views during the last ${this.trendingDays} days`
+ if (this.trendingDays === 1) {
+ this.titleTooltip = $localize`Videos with the most views during the last 24 hours`
+ return
+ }
+
+ this.titleTooltip = $localize`Videos with the most views during the last ${this.trendingDays} days`
}
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,50 +34,18 @@ export class RestExtractor {
return target
}
- handleError (err: any) {
- let errorMessage
-
- if (err.error instanceof Error) {
- // A client-side or network error occurred. Handle it accordingly.
- errorMessage = err.error.detail || err.error.title
- console.error('An error occurred:', errorMessage)
- } else if (typeof err.error === 'string') {
- errorMessage = err.error
- } else if (err.status !== undefined) {
- // A server-side error occurred.
- if (err.error?.errors) {
- const errors = err.error.errors
- const errorsArray: string[] = []
-
- Object.keys(errors).forEach(key => {
- errorsArray.push(errors[key].msg)
- })
-
- errorMessage = errorsArray.join('. ')
- } else if (err.error?.error) {
- errorMessage = err.error.error
- } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
- // eslint-disable-next-line max-len
- errorMessage = $localize`Media is too large for the server. Please contact you administrator if you want to increase the limit size.`
- } else if (err.status === HttpStatusCode.TOO_MANY_REQUESTS_429) {
- const secondsLeft = err.headers.get('retry-after')
- if (secondsLeft) {
- const minutesLeft = Math.floor(parseInt(secondsLeft, 10) / 60)
- errorMessage = $localize`Too many attempts, please try again after ${minutesLeft} minutes.`
- } else {
- errorMessage = $localize`Too many attempts, please try again later.`
- }
- } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
- errorMessage = $localize`Server error. Please retry later.`
- }
-
- errorMessage = errorMessage || 'Unknown error.'
- console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`)
- } else {
- console.error(err)
- errorMessage = err
+ redirectTo404IfNotFound (obj: { status: number }, type: 'video' | 'other', status = [ HttpStatusCode.NOT_FOUND_404 ]) {
+ if (obj?.status && status.includes(obj.status)) {
+ // Do not use redirectService to avoid circular dependencies
+ this.router.navigate([ '/404' ], { state: { type, obj }, skipLocationChange: true })
}
+ return observableThrowError(() => obj)
+ }
+
+ handleError (err: any) {
+ const errorMessage = this.buildErrorMessage(err)
+
const errorObj: { message: string, status: string, body: string } = {
message: errorMessage,
status: undefined,
@@ -92,12 +60,63 @@ export class RestExtractor {
return observableThrowError(() => errorObj)
}
- redirectTo404IfNotFound (obj: { status: number }, type: 'video' | 'other', status = [ HttpStatusCode.NOT_FOUND_404 ]) {
- if (obj?.status && status.includes(obj.status)) {
- // Do not use redirectService to avoid circular dependencies
- this.router.navigate([ '/404' ], { state: { type, obj }, skipLocationChange: true })
+ private buildErrorMessage (err: any) {
+ if (err.error instanceof Error) {
+ // A client-side or network error occurred. Handle it accordingly.
+ const errorMessage = err.error.detail || err.error.title
+ console.error('An error occurred:', errorMessage)
+
+ return errorMessage
}
- return observableThrowError(() => obj)
+ if (typeof err.error === 'string') {
+ return err.error
+ }
+
+ if (err.status !== undefined) {
+ const errorMessage = this.buildServerErrorMessage(err)
+ console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`)
+
+ return errorMessage
+ }
+
+ console.error(err)
+ return err
+ }
+
+ private buildServerErrorMessage (err: any) {
+ // A server-side error occurred.
+ if (err.error?.errors) {
+ const errors = err.error.errors
+
+ return Object.keys(errors)
+ .map(key => errors[key].msg)
+ .join('. ')
+ }
+
+ if (err.error?.error) {
+ return err.error.error
+ }
+
+ if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
+ return $localize`Media is too large for the server. Please contact you administrator if you want to increase the limit size.`
+ }
+
+ if (err.status === HttpStatusCode.TOO_MANY_REQUESTS_429) {
+ const secondsLeft = err.headers.get('retry-after')
+
+ if (secondsLeft) {
+ const minutesLeft = Math.floor(parseInt(secondsLeft, 10) / 60)
+ return $localize`Too many attempts, please try again after ${minutesLeft} minutes.`
+ }
+
+ return $localize`Too many attempts, please try again later.`
+ }
+
+ if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
+ return $localize`Server error. Please retry later.`
+ }
+
+ return $localize`Unknown server error`
}
}
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 @@
import { environment } from '../../environments/environment'
+import IntlMessageFormat from 'intl-messageformat'
function isOnDevLocale () {
return environment.production === false && window.location.search === '?lang=fr'
@@ -8,7 +9,31 @@ function getDevLocale () {
return 'fr-FR'
}
+function prepareIcu (icu: string) {
+ let alreadyWarned = false
+
+ try {
+ const msg = new IntlMessageFormat(icu, $localize.locale)
+
+ return (context: { [id: string]: number | string }, fallback: string) => {
+ try {
+ return msg.format(context) as string
+ } catch (err) {
+ if (!alreadyWarned) console.warn('Cannot format ICU %s.', icu, err)
+
+ alreadyWarned = true
+ return fallback
+ }
+ }
+ } catch (err) {
+ console.warn('Cannot build intl message %s.', icu, err)
+
+ return (_context: unknown, fallback: string) => fallback
+ }
+}
+
export {
getDevLocale,
+ prepareIcu,
isOnDevLocale
}
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'
import { Notifier } from '@app/core'
import { HttpStatusCode } from '@shared/models'
-function genericUploadErrorHandler (parameters: {
+function genericUploadErrorHandler (options: {
err: Pick
name: string
notifier: Notifier
sticky?: boolean
}) {
- const { err, name, notifier, sticky } = { sticky: false, ...parameters }
- const title = $localize`The upload failed`
- let message = err.message
-
- if (err instanceof ErrorEvent) { // network error
- message = $localize`The connection was interrupted`
- notifier.error(message, title, null, sticky)
- } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
- message = $localize`The server encountered an error`
- notifier.error(message, title, null, sticky)
- } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) {
- message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)`
- notifier.error(message, title, null, sticky)
- } else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
- const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G'
- message = $localize`Your ${name} file was too large (max. size: ${maxFileSize})`
- notifier.error(message, title, null, sticky)
- } else {
- notifier.error(err.message, title)
- }
+ const { err, name, notifier, sticky = false } = options
+ const title = $localize`Upload failed`
+ const message = buildMessage(name, err)
+ notifier.error(message, title, null, sticky)
return message
}
export {
genericUploadErrorHandler
}
+
+// ---------------------------------------------------------------------------
+
+function buildMessage (name: string, err: Pick) {
+ if (err instanceof ErrorEvent) { // network error
+ return $localize`The connection was interrupted`
+ }
+
+ if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
+ return $localize`The server encountered an error`
+ }
+
+ if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) {
+ return $localize`Your ${name} file couldn't be transferred before the server proxy timeout`
+ }
+
+ if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
+ const maxFileSize = err.headers?.get('X-File-Maximum-Size') || '8G'
+ return $localize`Your ${name} file was too large (max. size: ${maxFileSize})`
+ }
+
+ return err.message
+}
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 @@
import { Component, forwardRef, Input } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import { Notifier } from '@app/core'
+import { prepareIcu } from '@app/helpers'
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
import { ItemSelectCheckboxValue } from './select-checkbox.component'
@@ -78,7 +79,12 @@ export class SelectCheckboxAllComponent implements ControlValueAccessor {
if (!outputItems) return true
if (outputItems.length >= this.maxItems) {
- this.notifier.error($localize`You can't select more than ${this.maxItems} items`)
+ this.notifier.error(
+ prepareIcu($localize`You can't select more than {maxItems, plural, =1 {1 item} other {{maxItems} items}}`)(
+ { maxItems: this.maxItems },
+ $localize`You can't select more than ${this.maxItems} items`
+ )
+ )
return false
}
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 @@
import { Component, OnInit } from '@angular/core'
import { ServerService } from '@app/core'
+import { prepareIcu } from '@app/helpers'
import { ServerConfig } from '@shared/models'
import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service'
@@ -65,15 +66,20 @@ export class InstanceFeaturesTableComponent implements OnInit {
private getApproximateTime (seconds: number) {
const hours = Math.floor(seconds / 3600)
- let pluralSuffix = ''
- if (hours > 1) pluralSuffix = 's'
- if (hours > 0) return `~ ${hours} hour${pluralSuffix}`
+
+ if (hours !== 0) {
+ return prepareIcu($localize`~ {hours, plural, =1 {1 hour} other {{hours} hours}}`)(
+ { hours },
+ $localize`~ ${hours} hours`
+ )
+ }
const minutes = Math.floor(seconds % 3600 / 60)
- if (minutes === 1) return $localize`~ 1 minute`
-
- return $localize`~ ${minutes} minutes`
+ return prepareIcu($localize`~ {minutes, plural, =1 {1 minute} other {{minutes} minutes}}`)(
+ { minutes },
+ $localize`~ ${minutes} minutes`
+ )
}
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 @@
import { Pipe, PipeTransform } from '@angular/core'
+import { prepareIcu } from '@app/helpers'
// Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site
@Pipe({ name: 'myFromNow' })
export class FromNowPipe implements PipeTransform {
+ private yearICU = prepareIcu($localize`{interval, plural, =1 {1 year ago} other {{interval} years ago}}`)
+ private monthICU = prepareIcu($localize`{interval, plural, =1 {1 month ago} other {{interval} months ago}}`)
+ private weekICU = prepareIcu($localize`{interval, plural, =1 {1 week ago} other {{interval} weeks ago}}`)
+ private dayICU = prepareIcu($localize`{interval, plural, =1 {1 day ago} other {{interval} days ago}}`)
+ private hourICU = prepareIcu($localize`{interval, plural, =1 {1 hour ago} other {{interval} hours ago}}`)
+
transform (arg: number | Date | string) {
const argDate = new Date(arg)
const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000)
let interval = Math.floor(seconds / 31536000)
- if (interval > 1) return $localize`${interval} years ago`
- if (interval === 1) return $localize`1 year ago`
+ if (interval >= 1) {
+ return this.yearICU({ interval }, $localize`${interval} year(s) ago`)
+ }
interval = Math.floor(seconds / 2419200)
// 12 months = 360 days, but a year ~ 365 days
// Display "1 year ago" rather than "12 months ago"
if (interval >= 12) return $localize`1 year ago`
- if (interval > 1) return $localize`${interval} months ago`
- if (interval === 1) return $localize`1 month ago`
+
+ if (interval >= 1) {
+ return this.monthICU({ interval }, $localize`${interval} month(s) ago`)
+ }
interval = Math.floor(seconds / 604800)
// 4 weeks ~ 28 days, but our month is 30 days
// Display "1 month ago" rather than "4 weeks ago"
if (interval >= 4) return $localize`1 month ago`
- if (interval > 1) return $localize`${interval} weeks ago`
- if (interval === 1) return $localize`1 week ago`
+
+ if (interval >= 1) {
+ return this.weekICU({ interval }, $localize`${interval} week(s) ago`)
+ }
interval = Math.floor(seconds / 86400)
- if (interval > 1) return $localize`${interval} days ago`
- if (interval === 1) return $localize`1 day ago`
+ if (interval >= 1) {
+ return this.dayICU({ interval }, $localize`${interval} day(s) ago`)
+ }
interval = Math.floor(seconds / 3600)
- if (interval > 1) return $localize`${interval} hours ago`
- if (interval === 1) return $localize`1 hour ago`
+ if (interval >= 1) {
+ return this.hourICU({ interval }, $localize`${interval} hour(s) ago`)
+ }
interval = Math.floor(seconds / 60)
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 @@
import { AuthUser } from '@app/core'
import { User } from '@app/core/users/user.model'
-import { durationToString, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
+import { durationToString, prepareIcu, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
import { Actor } from '@app/shared/shared-main/account/actor.model'
import { buildVideoWatchPath } from '@shared/core-utils'
import { peertubeTranslate } from '@shared/core-utils/i18n'
@@ -19,6 +19,9 @@ import {
} from '@shared/models'
export class Video implements VideoServerModel {
+ private static readonly viewsICU = prepareIcu($localize`{views, plural, =0 {No view} =1 {1 view} other {{views} views}}`)
+ private static readonly viewersICU = prepareIcu($localize`{viewers, plural, =0 {No viewers} =1 {1 viewer} other {{viewers} viewers}}`)
+
byVideoChannel: string
byAccount: string
@@ -269,12 +272,10 @@ export class Video implements VideoServerModel {
}
getExactNumberOfViews () {
- if (this.views < 1000) return ''
-
if (this.isLive) {
- return $localize`${this.views} viewers`
+ return Video.viewersICU({ viewers: this.viewers }, $localize`${this.viewers} viewer(s)`)
}
- return $localize`${this.views} views`
+ return Video.viewsICU({ views: this.views }, $localize`{${this.views} view(s)}`)
}
}
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 @@
import { forkJoin } from 'rxjs'
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
import { Notifier } from '@app/core'
+import { prepareIcu } from '@app/helpers'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -63,9 +64,16 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
forkJoin(observables)
.subscribe({
next: () => {
- const message = Array.isArray(this.usersToBan)
- ? $localize`${this.usersToBan.length} users banned.`
- : $localize`User ${this.usersToBan.username} banned.`
+ let message: string
+
+ if (Array.isArray(this.usersToBan)) {
+ message = prepareIcu($localize`{count, plural, =1 {1 user} other {{count} users}} banned.`)(
+ { count: this.usersToBan.length },
+ $localize`${this.usersToBan.length} users banned.`
+ )
+ } else {
+ message = $localize`User ${this.usersToBan.username} banned.`
+ }
this.notifier.success(message)
@@ -79,7 +87,12 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
}
getModalTitle () {
- if (Array.isArray(this.usersToBan)) return $localize`Ban ${this.usersToBan.length} users`
+ if (Array.isArray(this.usersToBan)) {
+ return prepareIcu($localize`Ban {count, plural, =1 {1 user} other {{count} users}}`)(
+ { count: this.usersToBan.length },
+ $localize`Ban ${this.usersToBan.length} users`
+ )
+ }
return $localize`Ban "${this.usersToBan.username}"`
}
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 {
return
}
- const message = $localize`If you remove user ${user.username}, you won't be able to create another with the same username!`
+ // eslint-disable-next-line max-len
+ const message = $localize`If you remove this user, you won't be able to create another user or channel with ${user.username} username!`
const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`)
if (res === false) return
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 @@
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
import { Notifier } from '@app/core'
+import { prepareIcu } from '@app/helpers'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { Video } from '@app/shared/shared-main'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -80,9 +81,10 @@ export class VideoBlockComponent extends FormReactive implements OnInit {
this.videoBlocklistService.blockVideo(options)
.subscribe({
next: () => {
- const message = this.isMultiple
- ? $localize`Blocked ${this.videos.length} videos.`
- : $localize`Blocked ${this.getSingleVideo().name}`
+ const message = prepareIcu($localize`{count, plural, =1 {Blocked {videoName}} other {Blocked {count} videos}}.`)(
+ { count: this.videos.length, videoName: this.getSingleVideo().name },
+ $localize`Blocked ${this.videos.length} videos.`
+ )
this.notifier.success(message)
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 @@
-
+
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 {
if (video.scheduledUpdate) {
const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId)
- return $localize`Publication scheduled on ` + updateAt
+ return $localize`Publication scheduled on ${updateAt}`
}
if (video.state.id === VideoState.TRANSCODING_FAILED) {
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 41814d036..7a0584d5c 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -6,7 +6,7 @@
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
- "module": "esnext",
+ "module": "es2020",
"experimentalDecorators": true,
"noImplicitAny": true,
"noImplicitThis": true,
@@ -15,11 +15,12 @@
"importHelpers": true,
"allowSyntheticDefaultImports": true,
"strictBindCallApply": true,
- "target": "es2015",
+ "target": "es2017",
"typeRoots": [
"node_modules/@types"
],
"lib": [
+ "ES2020.Intl",
"es2018",
"es2017",
"es2016",
diff --git a/client/yarn.lock b/client/yarn.lock
index 0adb80854..9a8be2b08 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -1327,6 +1327,45 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
+"@formatjs/ecma402-abstract@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.6.tgz#0e828ddfed6fb3413ae379e48fb7170fb0795db5"
+ integrity sha512-6TcI+IroIK+GTWXBJ643LBJklmCBsqLt1sUTGWfzdBcI5Y6b1L1iamrJB1B5OAQLnhzWveLbmzPYHYsFEZfeig==
+ dependencies:
+ "@formatjs/intl-localematcher" "0.2.27"
+ tslib "2.4.0"
+
+"@formatjs/fast-memoize@1.2.3":
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.2.3.tgz#5c950bd64c4959e30bbd16b22a17040fbeb9c4d2"
+ integrity sha512-RVI3e4M7mIxAhKbbyS78H8++fsoiSRZgxh0zReHfvV6p1cpfgG2/k2qJYhJq0RXh6orVtUEsQ3xK9i4tDfsOSg==
+ dependencies:
+ tslib "2.4.0"
+
+"@formatjs/icu-messageformat-parser@2.1.2":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.2.tgz#9ff4dfc4f1ed613cca2c188b29f299854b86b7f8"
+ integrity sha512-FYQ2pkgbDJxJlst/U5MU2H7+bR9HrZ4x8J4c0etrya24pJzQxYguVlAhc2S6NoEImlQ2LmIIGsURaBQu9bCtew==
+ dependencies:
+ "@formatjs/ecma402-abstract" "1.11.6"
+ "@formatjs/icu-skeleton-parser" "1.3.8"
+ tslib "2.4.0"
+
+"@formatjs/icu-skeleton-parser@1.3.8":
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.8.tgz#3d150fcb45b4867c1db84237ca1f1f701d598918"
+ integrity sha512-CVdsPMs/KvrIDKhMDw8bSq/Zst2bhdn/bTUfVCHi/c/bj462lChIJmW/JP/FaGKgZzdG8slGyVIFLonpG4uqFA==
+ dependencies:
+ "@formatjs/ecma402-abstract" "1.11.6"
+ tslib "2.4.0"
+
+"@formatjs/intl-localematcher@0.2.27":
+ version "0.2.27"
+ resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.27.tgz#8a837ddca17a55d86e4ab68bcbb25b15f547d61d"
+ integrity sha512-XHYcVas2ebDTh3VtfdluvbTjqyMUHqFHARnuJo5KYF/0MKOTmozVSK7PJGnu1IEHdmRdTWuG6TB+2RnkasaxVw==
+ dependencies:
+ tslib "2.4.0"
+
"@gar/promisify@^1.0.1":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
@@ -6537,6 +6576,16 @@ interpret@^2.2.0:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
+intl-messageformat@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.0.1.tgz#dae7ae81a477e92ea8691dd73c60d5eb5003f866"
+ integrity sha512-oZWDsNbauuWmPd98+zLEfNojuJkBdVpEWIcWQVCTxSJrhag2/czZnwKBsYa8NcVf4t0fWo0k77v+CBCudKEcjw==
+ dependencies:
+ "@formatjs/ecma402-abstract" "1.11.6"
+ "@formatjs/fast-memoize" "1.2.3"
+ "@formatjs/icu-messageformat-parser" "2.1.2"
+ tslib "2.4.0"
+
invert-kv@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
@@ -11055,6 +11104,11 @@ tslib@2.3.1, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
+tslib@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
+ integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
+
tslib@^1.8.1, tslib@^1.9.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"