From eaa529528cafcfb291009f9f99d296c81e792899 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 24 May 2022 16:29:01 +0200 Subject: [PATCH] Support ICU in TS components --- client/package.json | 1 + .../account-video-channels.component.html | 4 +- .../src/app/+accounts/accounts.component.html | 4 +- .../src/app/+accounts/accounts.component.ts | 10 -- .../edit-configuration.service.ts | 8 +- .../following-list/follow-modal.component.ts | 9 +- .../comments/video-comment-list.component.ts | 9 +- .../users/user-list/user-list.component.ts | 47 ++++++-- .../overview/videos/video-list.component.ts | 41 ++++++- ...ount-notification-preferences.component.ts | 2 +- .../my-history/my-history.component.ts | 10 +- .../my-videos/my-videos.component.ts | 15 ++- client/src/app/+search/search.component.ts | 8 +- .../video-channels.component.html | 4 +- .../videos-list-common-page.component.ts | 25 +++- .../app/core/rest/rest-extractor.service.ts | 111 ++++++++++-------- client/src/app/helpers/i18n-utils.ts | 25 ++++ client/src/app/helpers/utils/upload.ts | 49 ++++---- .../select/select-checkbox-all.component.ts | 8 +- .../instance-features-table.component.ts | 18 ++- .../shared-main/angular/from-now.pipe.ts | 34 ++++-- .../shared/shared-main/video/video.model.ts | 11 +- .../user-ban-modal.component.ts | 21 +++- .../user-moderation-dropdown.component.ts | 3 +- .../video-block.component.ts | 8 +- .../user-video-settings.component.html | 2 +- .../video-miniature.component.ts | 2 +- client/tsconfig.json | 5 +- client/yarn.lock | 54 +++++++++ 29 files changed, 391 insertions(+), 157 deletions(-) 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:` + + '' + 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,49 +34,17 @@ export class RestExtractor { return target } - handleError (err: any) { - let errorMessage + 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 }) + } - 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.` - } + return observableThrowError(() => obj) + } - errorMessage = errorMessage || 'Unknown error.' - console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) - } else { - console.error(err) - errorMessage = err - } + handleError (err: any) { + const errorMessage = this.buildErrorMessage(err) const errorObj: { message: string, status: string, body: string } = { message: errorMessage, @@ -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}` - const minutes = Math.floor(seconds % 3600 / 60) + if (hours !== 0) { + return prepareIcu($localize`~ {hours, plural, =1 {1 hour} other {{hours} hours}}`)( + { hours }, + $localize`~ ${hours} hours` + ) + } - if (minutes === 1) return $localize`~ 1 minute` + const minutes = Math.floor(seconds % 3600 / 60) - 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" -- 2.41.0