diff --git a/frontend/src/Bookshelf/BookshelfFooter.js b/frontend/src/Bookshelf/BookshelfFooter.js index 63bf84bb1..f01d9198a 100644 --- a/frontend/src/Bookshelf/BookshelfFooter.js +++ b/frontend/src/Bookshelf/BookshelfFooter.js @@ -143,7 +143,7 @@ class BookshelfFooter extends Component {
- {selectedCount} Author(s) Selected + {translate('CountAuthorsSelected', { selectedCount })}
- {translate('CountDownloadClientsSelected', [selectedCount])} + {translate('CountDownloadClientsSelected', { selectedCount })}
diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index 078fed25d..cc8a1eb35 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent( isOpen={isDeleteModalOpen} kind={kinds.DANGER} title={translate('DeleteSelectedDownloadClients')} - message={translate('DeleteSelectedDownloadClientsMessageText', [ - selectedIds.length, - ])} + message={translate('DeleteSelectedDownloadClientsMessageText', { + count: selectedIds.length, + })} confirmLabel={translate('Delete')} onConfirm={onConfirmDelete} onCancel={onDeleteModalClose} diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportList.js b/frontend/src/Settings/ImportLists/ImportLists/ImportList.js index 639d84f58..39bc16f8e 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportList.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportList.js @@ -107,7 +107,7 @@ class ImportList extends Component { isOpen={this.state.isDeleteImportListModalOpen} kind={kinds.DANGER} title={translate('DeleteImportList')} - message={translate('DeleteImportListMessageText', [name])} + message={translate('DeleteImportListMessageText', { name })} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeleteImportList} onCancel={this.onDeleteImportListModalClose} diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx index 798af88b9..6fc3d832a 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx @@ -184,7 +184,7 @@ function ManageImportListsEditModalContent(
- {translate('CountImportListsSelected', [selectedCount])} + {translate('CountImportListsSelected', { selectedCount })}
diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx index ee2887856..bc3712cb3 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx @@ -283,9 +283,9 @@ function ManageImportListsModalContent( isOpen={isDeleteModalOpen} kind={kinds.DANGER} title={translate('DeleteSelectedImportLists')} - message={translate('DeleteSelectedImportListsMessageText', [ - selectedIds.length, - ])} + message={translate('DeleteSelectedImportListsMessageText', { + count: selectedIds.length, + })} confirmLabel={translate('Delete')} onConfirm={onConfirmDelete} onCancel={onDeleteModalClose} diff --git a/frontend/src/Settings/Indexers/Indexers/Indexer.js b/frontend/src/Settings/Indexers/Indexers/Indexer.js index 05198dcf8..e129ea638 100644 --- a/frontend/src/Settings/Indexers/Indexers/Indexer.js +++ b/frontend/src/Settings/Indexers/Indexers/Indexer.js @@ -152,7 +152,7 @@ class Indexer extends Component { isOpen={this.state.isDeleteIndexerModalOpen} kind={kinds.DANGER} title={translate('DeleteIndexer')} - message={translate('DeleteIndexerMessageText', [name])} + message={translate('DeleteIndexerMessageText', { name })} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeleteIndexer} onCancel={this.onDeleteIndexerModalClose} diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx index 286a0b0cf..f9b051986 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx @@ -178,7 +178,7 @@ function ManageIndexersEditModalContent(
- {translate('CountIndexersSelected', [selectedCount])} + {translate('CountIndexersSelected', { selectedCount })}
diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx index 7c1a7db63..37c4a3153 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx @@ -281,9 +281,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { isOpen={isDeleteModalOpen} kind={kinds.DANGER} title={translate('DeleteSelectedIndexers')} - message={translate('DeleteSelectedIndexersMessageText', [ - selectedIds.length, - ])} + message={translate('DeleteSelectedIndexersMessageText', { + count: selectedIds.length, + })} confirmLabel={translate('Delete')} onConfirm={onConfirmDelete} onCancel={onDeleteModalClose} diff --git a/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js b/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js index 2d47872bd..20cefc53f 100644 --- a/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js +++ b/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js @@ -95,7 +95,7 @@ class RootFolder extends Component { isOpen={this.state.isDeleteRootFolderModalOpen} kind={kinds.DANGER} title={translate('DeleteRootFolder')} - message={translate('DeleteRootFolderMessageText', [name])} + message={translate('DeleteRootFolderMessageText', { name })} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeleteRootFolder} onCancel={this.onDeleteRootFolderModalClose} diff --git a/frontend/src/Settings/Notifications/Notifications/Notification.js b/frontend/src/Settings/Notifications/Notifications/Notification.js index 1095a6f5e..6a469d02a 100644 --- a/frontend/src/Settings/Notifications/Notifications/Notification.js +++ b/frontend/src/Settings/Notifications/Notifications/Notification.js @@ -227,7 +227,7 @@ class Notification extends Component { isOpen={this.state.isDeleteNotificationModalOpen} kind={kinds.DANGER} title={translate('DeleteNotification')} - message={translate('DeleteNotificationMessageText', [name])} + message={translate('DeleteNotificationMessageText', { name })} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeleteNotification} onCancel={this.onDeleteNotificationModalClose} diff --git a/frontend/src/Settings/Profiles/Metadata/MetadataProfile.js b/frontend/src/Settings/Profiles/Metadata/MetadataProfile.js index 5e344b623..7cb407eb6 100644 --- a/frontend/src/Settings/Profiles/Metadata/MetadataProfile.js +++ b/frontend/src/Settings/Profiles/Metadata/MetadataProfile.js @@ -144,7 +144,7 @@ class MetadataProfile extends Component { isOpen={this.state.isDeleteMetadataProfileModalOpen} kind={kinds.DANGER} title={translate('DeleteMetadataProfile')} - message={translate('DeleteMetadataProfileMessageText', [name])} + message={translate('DeleteMetadataProfileMessageText', { name })} confirmLabel={translate('Delete')} isSpinning={isDeleting} onConfirm={this.onConfirmDeleteMetadataProfile} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfile.js b/frontend/src/Settings/Profiles/Quality/QualityProfile.js index e67638343..b28d31b98 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfile.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfile.js @@ -162,7 +162,7 @@ class QualityProfile extends Component { isOpen={this.state.isDeleteQualityProfileModalOpen} kind={kinds.DANGER} title={translate('DeleteQualityProfile')} - message={translate('DeleteQualityProfileMessageText', [name])} + message={translate('DeleteQualityProfileMessageText', { name })} confirmLabel={translate('Delete')} isSpinning={isDeleting} onConfirm={this.onConfirmDeleteQualityProfile} diff --git a/frontend/src/System/Backup/BackupRow.js b/frontend/src/System/Backup/BackupRow.js index 185dfa284..c267dbb0a 100644 --- a/frontend/src/System/Backup/BackupRow.js +++ b/frontend/src/System/Backup/BackupRow.js @@ -138,7 +138,7 @@ class BackupRow extends Component { isOpen={isConfirmDeleteModalOpen} kind={kinds.DANGER} title={translate('DeleteBackup')} - message={translate('DeleteBackupMessageText', [name])} + message={translate('DeleteBackupMessageText', { name })} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeletePress} onCancel={this.onConfirmDeleteModalClose} diff --git a/frontend/src/System/Backup/RestoreBackupModalContent.js b/frontend/src/System/Backup/RestoreBackupModalContent.js index 235111bd7..d6aa500d2 100644 --- a/frontend/src/System/Backup/RestoreBackupModalContent.js +++ b/frontend/src/System/Backup/RestoreBackupModalContent.js @@ -146,7 +146,7 @@ class RestoreBackupModalContent extends Component { { - !!id && `Would you like to restore the backup '${name}'?` + !!id && translate('WouldYouLikeToRestoreBackup', { name }) } { diff --git a/frontend/src/Utilities/String/translate.ts b/frontend/src/Utilities/String/translate.ts index 459369a7d..2ee319bc8 100644 --- a/frontend/src/Utilities/String/translate.ts +++ b/frontend/src/Utilities/String/translate.ts @@ -25,18 +25,19 @@ export async function fetchTranslations(): Promise { export default function translate( key: string, - args?: (string | number | boolean)[] + tokens?: Record ) { - if (!(key in translations)) { - console.debug(key); - } - const translation = translations[key] || key; - if (args) { - return translation.replace(/\{(\d+)\}/g, (match, index) => { - return String(args[index]) ?? match; + if (tokens) { + // Fallback to the old behaviour for translations not yet updated to use named tokens + Object.values(tokens).forEach((value, index) => { + tokens[index] = value; }); + + return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) => + String(tokens[tokenMatch] ?? match) + ); } return translation; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 01b15bfe1..31df78c97 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -161,9 +161,10 @@ "CopyUsingHardlinksHelpText": "Hardlinks allow Readarr to import seeding torrents to the the series folder without taking extra disk space or copying the entire contents of the file. Hardlinks will only work if the source and destination are on the same volume", "CopyUsingHardlinksHelpTextWarning": "Occasionally, file locks may prevent renaming files that are being seeded. You may temporarily disable seeding and use Readarr's rename function as a work around.", "CouldntFindAnyResultsForTerm": "Couldn't find any results for '{0}'", - "CountDownloadClientsSelected": "{0} download client(s) selected", - "CountImportListsSelected": "{0} import list(s) selected", - "CountIndexersSelected": "{0} indexer(s) selected", + "CountAuthorsSelected": "{selectedCount} author(s) selected", + "CountDownloadClientsSelected": "{selectedCount} download client(s) selected", + "CountImportListsSelected": "{selectedCount} import list(s) selected", + "CountIndexersSelected": "{selectedCount} indexer(s) selected", "Country": "Country", "CreateEmptyAuthorFolders": "Create empty author folders", "CreateEmptyAuthorFoldersHelpText": "Create missing author folders during disk scan", @@ -202,48 +203,48 @@ "DelayingDownloadUntilInterp": "Delaying download until {0} at {1}", "Delete": "Delete", "DeleteBackup": "Delete Backup", - "DeleteBackupMessageText": "Are you sure you want to delete the backup '{0}'?", + "DeleteBackupMessageText": "Are you sure you want to delete the backup '{name}'?", "DeleteBookFile": "Delete Book File", "DeleteBookFileMessageText": "Are you sure you want to delete {0}?", "DeleteCondition": "Delete Condition", - "DeleteConditionMessageText": "Are you sure you want to delete condition '{0}'?", + "DeleteConditionMessageText": "Are you sure you want to delete condition '{name}'?", "DeleteCustomFormat": "Delete Custom Format", - "DeleteCustomFormatMessageText": "Are you sure you want to delete the custom format '{0}'?", + "DeleteCustomFormatMessageText": "Are you sure you want to delete the custom format '{name}'?", "DeleteDelayProfile": "Delete Delay Profile", "DeleteDelayProfileMessageText": "Are you sure you want to delete this delay profile?", "DeleteDownloadClient": "Delete Download Client", - "DeleteDownloadClientMessageText": "Are you sure you want to delete the download client '{0}'?", + "DeleteDownloadClientMessageText": "Are you sure you want to delete the download client '{name}'?", "DeleteEmptyFolders": "Delete empty folders", "DeleteEmptyFoldersHelpText": "Delete empty author folders during disk scan and when book files are deleted", "DeleteFilesHelpText": "Delete the book files and author folder", "DeleteFormat": "Delete Format", - "DeleteFormatMessageText": "Are you sure you want to delete format tag {0} ?", + "DeleteFormatMessageText": "Are you sure you want to delete format tag '{0}'?", "DeleteImportList": "Delete Import List", "DeleteImportListExclusion": "Delete Import List Exclusion", "DeleteImportListExclusionMessageText": "Are you sure you want to delete this import list exclusion?", - "DeleteImportListMessageText": "Are you sure you want to delete the list '{0}'?", + "DeleteImportListMessageText": "Are you sure you want to delete the list '{name}'?", "DeleteIndexer": "Delete Indexer", - "DeleteIndexerMessageText": "Are you sure you want to delete the indexer '{0}'?", + "DeleteIndexerMessageText": "Are you sure you want to delete the indexer '{name}'?", "DeleteMetadataProfile": "Delete Metadata Profile", - "DeleteMetadataProfileMessageText": "Are you sure you want to delete the metadata profile '{0}'?", + "DeleteMetadataProfileMessageText": "Are you sure you want to delete the metadata profile '{name}'?", "DeleteNotification": "Delete Notification", - "DeleteNotificationMessageText": "Are you sure you want to delete the notification '{0}'?", + "DeleteNotificationMessageText": "Are you sure you want to delete the notification '{name}'?", "DeleteQualityProfile": "Delete Quality Profile", - "DeleteQualityProfileMessageText": "Are you sure you want to delete the quality profile '{0}'?", + "DeleteQualityProfileMessageText": "Are you sure you want to delete the quality profile '{name}'?", "DeleteReleaseProfile": "Delete Release Profile", "DeleteReleaseProfileMessageText": "Are you sure you want to delete this Release Profile?", "DeleteRemotePathMapping": "Delete Remote Path Mapping", "DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?", "DeleteRootFolder": "Delete Root Folder", - "DeleteRootFolderMessageText": "Are you sure you want to delete the root folder '{0}'?", + "DeleteRootFolderMessageText": "Are you sure you want to delete the root folder '{name}'?", "DeleteSelectedBookFiles": "Delete Selected Book Files", "DeleteSelectedBookFilesMessageText": "Are you sure you want to delete the selected book files?", "DeleteSelectedDownloadClients": "Delete Download Client(s)", - "DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {0} selected download client(s)?", + "DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {count} selected download client(s)?", "DeleteSelectedImportLists": "Delete Import List(s)", - "DeleteSelectedImportListsMessageText": "Are you sure you want to delete {0} selected import list(s)?", + "DeleteSelectedImportListsMessageText": "Are you sure you want to delete {count} selected import list(s)?", "DeleteSelectedIndexers": "Delete Indexer(s)", - "DeleteSelectedIndexersMessageText": "Are you sure you want to delete {0} selected indexer(s)?", + "DeleteSelectedIndexersMessageText": "Are you sure you want to delete {count} selected indexer(s)?", "DeleteTag": "Delete Tag", "DeleteTagMessageText": "Are you sure you want to delete the tag '{0}'?", "DestinationPath": "Destination Path", @@ -995,6 +996,7 @@ "WatchLibraryForChangesHelpText": "Rescan automatically when files change in a root folder", "WatchRootFoldersForFileChanges": "Watch Root Folders for file changes", "WeekColumnHeader": "Week Column Header", + "WouldYouLikeToRestoreBackup": "Would you like to restore the backup '{name}'?", "WriteAudioTags": "Tag Audio Files with Metadata", "WriteAudioTagsScrub": "Scrub Existing Tags", "WriteAudioTagsScrubHelp": "Remove existing tags from files, leaving only those added by Readarr.",