diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx
index 4f64fd76d..e2b45e96f 100644
--- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx
+++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx
@@ -277,9 +277,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 83e5eb99f..9298cd85f 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..5d8180203 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', { count: 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/Notifications/Notifications/Notification.js b/frontend/src/Settings/Notifications/Notifications/Notification.js
index 417675d63..c8316fb63 100644
--- a/frontend/src/Settings/Notifications/Notifications/Notification.js
+++ b/frontend/src/Settings/Notifications/Notifications/Notification.js
@@ -217,7 +217,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/Quality/QualityProfile.js b/frontend/src/Settings/Profiles/Quality/QualityProfile.js
index 7465300f1..55f0dbe75 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('QualityProfileDeleteConfirm', [name])}
+ message={translate('DeleteQualityProfileMessageText', { name })}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteQualityProfile}
diff --git a/frontend/src/Settings/Tags/AutoTagging/AutoTagging.js b/frontend/src/Settings/Tags/AutoTagging/AutoTagging.js
index f735a8e64..760273cb3 100644
--- a/frontend/src/Settings/Tags/AutoTagging/AutoTagging.js
+++ b/frontend/src/Settings/Tags/AutoTagging/AutoTagging.js
@@ -114,7 +114,7 @@ export default function AutoTagging(props) {
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteAutoTag')}
- message={translate('DeleteAutoTagHelpText', [name])}
+ message={translate('DeleteAutoTagHelpText', { name })}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={onConfirmDelete}
diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js
index 3ede0b016..43ff2927f 100644
--- a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js
+++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js
@@ -32,7 +32,7 @@ function TagDetailsModalContent(props) {
return (
- {translate('TagDetails', [label])}
+ {translate('TagDetails', { label })}
diff --git a/frontend/src/Settings/Tags/Tag.js b/frontend/src/Settings/Tags/Tag.js
index 661e632b9..aaf881fe2 100644
--- a/frontend/src/Settings/Tags/Tag.js
+++ b/frontend/src/Settings/Tags/Tag.js
@@ -164,7 +164,7 @@ class Tag extends Component {
isOpen={isDeleteTagModalOpen}
kind={kinds.DANGER}
title={translate('DeleteTag')}
- message={translate('DeleteTagMessageText', [label])}
+ message={translate('DeleteTagMessageText', { label })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteTag}
onCancel={this.onDeleteTagModalClose}
diff --git a/frontend/src/System/Backup/BackupRow.js b/frontend/src/System/Backup/BackupRow.js
index 316180374..b1ea6647e 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 f38121243..73ef7c3a8 100644
--- a/frontend/src/System/Backup/RestoreBackupModalContent.js
+++ b/frontend/src/System/Backup/RestoreBackupModalContent.js
@@ -146,7 +146,7 @@ class RestoreBackupModalContent extends Component {
{
- !!id && translate('WouldYouLikeToRestoreBackup', [name])
+ !!id && translate('WouldYouLikeToRestoreBackup', { name })
}
{
diff --git a/frontend/src/Utilities/String/translate.ts b/frontend/src/Utilities/String/translate.ts
index 9c6fa778b..2ee319bc8 100644
--- a/frontend/src/Utilities/String/translate.ts
+++ b/frontend/src/Utilities/String/translate.ts
@@ -25,14 +25,19 @@ export async function fetchTranslations(): Promise {
export default function translate(
key: string,
- args?: (string | number | boolean)[]
+ tokens?: Record
) {
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 4c4097e7b..e514bd9fb 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -37,7 +37,7 @@
"AllCollectionsHiddenDueToFilter": "All collections are hidden due to applied filter.",
"AllFiles": "All Files",
"AllMoviesHiddenDueToFilter": "All movies are hidden due to applied filter.",
- "AllMoviesInPathHaveBeenImported": "All movies in {0} have been imported",
+ "AllMoviesInPathHaveBeenImported": "All movies in {path} have been imported",
"AllResultsHiddenFilter": "All results are hidden by the applied filter",
"AllTitles": "All Titles",
"AllowHardcodedSubs": "Allow Hardcoded Subs",
@@ -171,10 +171,10 @@
"CopyUsingHardlinksHelpText": "Hardlinks allow Radarr to import seeding torrents to the movie 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 Radarr's rename function as a work around.",
"CouldNotConnectSignalR": "Could not connect to SignalR, UI won't update",
- "CouldNotFindResults": "Couldn't find any results for '{0}'",
- "CountDownloadClientsSelected": "{0} download client(s) selected",
- "CountImportListsSelected": "{0} import list(s) selected",
- "CountIndexersSelected": "{0} indexer(s) selected",
+ "CouldNotFindResults": "Couldn't find any results for '{term}'",
+ "CountDownloadClientsSelected": "{count} download client(s) selected",
+ "CountImportListsSelected": "{count} import list(s) selected",
+ "CountIndexersSelected": "{count} indexer(s) selected",
"CreateEmptyMovieFolders": "Create empty movie folders",
"CreateEmptyMovieFoldersHelpText": "Create missing movie folders during disk scan",
"CreateGroup": "Create group",
@@ -209,17 +209,17 @@
"DelayingDownloadUntilInterp": "Delaying download until {0} at {1}",
"Delete": "Delete",
"DeleteAutoTag": "Delete Auto Tag",
- "DeleteAutoTagHelpText": "Are you sure you want to delete the auto tag '{0}'?",
+ "DeleteAutoTagHelpText": "Are you sure you want to delete the auto tag '{name}'?",
"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}'?",
"DeleteCondition": "Delete Condition",
- "DeleteConditionMessageText": "Are you sure you want to delete the condition '{0}'?",
+ "DeleteConditionMessageText": "Are you sure you want to delete the 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 movie folders during disk scan and when movie files are deleted",
"DeleteFile": "Delete file",
@@ -228,37 +228,36 @@
"DeleteFilesLabel": "Delete {0} Movie Files",
"DeleteFormatMessageText": "Are you sure you want to delete format tag {0} ?",
"DeleteHeader": "Delete - {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 '{name}'?",
"DeleteIndexer": "Delete Indexer",
- "DeleteIndexerMessageText": "Are you sure you want to delete the indexer '{0}'?",
- "DeleteIndexers": "Delete Indexer(s)",
- "DeleteIndexersMessageText": "Are you sure you want to delete {0} indexers(s)?",
- "DeleteList": "Delete List",
- "DeleteListMessageText": "Are you sure you want to delete the list '{0}'?",
+ "DeleteIndexerMessageText": "Are you sure you want to delete the indexer '{name}'?",
"DeleteMovieFolderHelpText": "Delete the movie folder and its contents",
"DeleteMovieFolderLabel": "Delete Movie Folder",
"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 '{name}'?",
"DeleteRemotePathMapping": "Delete Remote Path Mapping",
"DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?",
"DeleteRestriction": "Delete Restriction",
"DeleteRestrictionHelpText": "Are you sure you want to delete this restriction?",
"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 '{path}'?",
"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)?",
"DeleteSelectedMovie": "Delete Selected Movie(s)",
"DeleteSelectedMovieFiles": "Delete Selected Movie Files",
"DeleteSelectedMovieFilesMessage": "Are you sure you want to delete the selected movie files?",
"DeleteTag": "Delete Tag",
- "DeleteTagMessageText": "Are you sure you want to delete the tag '{0}'?",
- "DeleteTheMovieFolder": "The movie folder '{0}' and all its content will be deleted.",
+ "DeleteTagMessageText": "Are you sure you want to delete the tag '{label}'?",
+ "DeleteTheMovieFolder": "The movie folder '{path}' and all its content will be deleted.",
"Deleted": "Deleted",
"DeletedMsg": "Movie was deleted from TMDb",
"DestinationPath": "Destination Path",
@@ -293,12 +292,12 @@
"DownloadClientsSettingsSummary": "Download clients, download handling and remote path mappings",
"DownloadFailed": "Download failed",
"DownloadFailedCheckDownloadClientForMoreDetails": "Download failed: check download client for more details",
- "DownloadFailedInterp": "Download failed: {0}",
+ "DownloadFailedInterp": "Download failed: {errorMessage}",
"DownloadPropersAndRepacks": "Propers and Repacks",
"DownloadPropersAndRepacksHelpText1": "Whether or not to automatically upgrade to Propers/Repacks",
"DownloadPropersAndRepacksHelpText2": "Use 'Do not Prefer' to sort by custom format score over Propers/Repacks",
"DownloadPropersAndRepacksHelpTextWarning": "Use custom formats for automatic upgrades to Propers/Repacks",
- "DownloadWarning": "Download warning: {0}",
+ "DownloadWarning": "Download warning: {warningMessage}",
"DownloadWarningCheckDownloadClientForMoreDetails": "Download warning: check download client for more details",
"Downloaded": "Downloaded",
"DownloadedAndMonitored": "Downloaded (Monitored)",
@@ -441,8 +440,8 @@
"ImportExistingMovies": "Import Existing Movies",
"ImportExtraFiles": "Import Extra Files",
"ImportExtraFilesHelpText": "Import matching extra files (subtitles, nfo, etc) after importing an movie file",
- "ImportFailed": "Import failed: {0}",
- "ImportFailedInterp": "Import failed: {0}",
+ "ImportFailed": "Import failed: {sourceTitle}",
+ "ImportFailedInterp": "Import failed: {errorMessage}",
"ImportHeader": "Import an existing organized library to add movies to Radarr",
"ImportIncludeQuality": "Make sure that your files include the quality in their filenames. e.g. {0}",
"ImportLibrary": "Library Import",
@@ -650,7 +649,7 @@
"MovieYear": "Movie Year",
"MovieYearHelpText": "The year of the movie to exclude",
"Movies": "Movies",
- "MoviesSelectedInterp": "{0} Movie(s) Selected",
+ "MoviesSelectedInterp": "{count} Movie(s) Selected",
"MultiLanguage": "Multi-Language",
"MustContain": "Must Contain",
"MustNotContain": "Must Not Contain",
@@ -726,7 +725,7 @@
"Options": "Options",
"Organize": "Organize",
"OrganizeAndRename": "Organize & Rename",
- "OrganizeConfirm": "Are you sure you want to organize all files in the {0} selected movie(s)?",
+ "OrganizeConfirm": "Are you sure you want to organize all files in the {count} selected movie(s)?",
"OrganizeModalAllPathsRelative": "All paths are relative to:",
"OrganizeModalDisabled": "Renaming is disabled, nothing to rename",
"OrganizeModalNamingPattern": "Naming Pattern:",
@@ -772,7 +771,7 @@
"PreviewRenameHelpText": "Tip: To preview a rename... select 'Cancel' then click any movie title and use the",
"Priority": "Priority",
"PriorityHelpText": "Prioritize multiple Download Clients. Round-Robin is used for clients with the same priority.",
- "PrioritySettings": "Priority: {0}",
+ "PrioritySettings": "Priority: {priority}",
"ProcessingFolders": "Processing Folders",
"Profiles": "Profiles",
"ProfilesSettingsSummary": "Quality, Language and Delay profiles",
@@ -798,7 +797,6 @@
"QualityLimitsHelpText": "Limits are automatically adjusted for the movie runtime.",
"QualityOrLangCutoffHasNotBeenMet": "Quality or Language cutoff has not been met",
"QualityProfile": "Quality Profile",
- "QualityProfileDeleteConfirm": "Are you sure you want to delete the quality profile {0}",
"QualityProfileInUse": "Can't delete a quality profile that is attached to a movie, list, or collection",
"QualityProfiles": "Quality Profiles",
"QualitySettings": "Quality Settings",
@@ -885,17 +883,18 @@
"RemoveFromBlocklist": "Remove from blocklist",
"RemoveFromDownloadClient": "Remove From Download Client",
"RemoveFromQueue": "Remove from queue",
- "RemoveFromQueueText": "Are you sure you want to remove {0} from the queue?",
"RemoveHelpTextWarning": "Removing will remove the download and the file(s) from the download client.",
"RemoveMovieAndDeleteFiles": "Remove Movie and Delete Files",
"RemoveMovieAndKeepFiles": "Remove Movie and Keep Files",
+ "RemoveQueueItem": "Remove - {sourceTitle}",
+ "RemoveQueueItemConfirmation": "Are you sure you want to remove '{sourceTitle}' from the queue?",
"RemoveRootFolder": "Remove root folder",
"RemoveSelected": "Remove Selected",
"RemoveSelectedItem": "Remove Selected Item",
"RemoveSelectedItemBlocklistMessageText": "Are you sure you want to remove the selected items from the blocklist?",
"RemoveSelectedItemQueueMessageText": "Are you sure you want to remove 1 item from the queue?",
"RemoveSelectedItems": "Remove Selected Items",
- "RemoveSelectedItemsQueueMessageText": "Are you sure you want to remove {0} items from the queue?",
+ "RemoveSelectedItemsQueueMessageText": "Are you sure you want to remove {selectedCount} items from the queue?",
"RemoveTagsAutomatically": "Remove Tags Automatically",
"RemoveTagsAutomaticallyHelpText": "Remove tags automatically if conditions are not met",
"RemovedFromTaskQueue": "Removed from task queue",
@@ -1089,7 +1088,7 @@
"TableOptions": "Table Options",
"TableOptionsColumnsMessage": "Choose which columns are visible and which order they appear in",
"TagCannotBeDeletedWhileInUse": "Cannot be deleted while in use",
- "TagDetails": "Tag Details - {0}",
+ "TagDetails": "Tag Details - {label}",
"TagIsNotUsedAndCanBeDeleted": "Tag is not used and can be deleted",
"Tags": "Tags",
"TagsHelpText": "Applies to movies with at least one matching tag",
@@ -1238,7 +1237,7 @@
"WhitelistedHardcodedSubsHelpText": "Subtitle tags set here will not be considered hardcoded",
"WhitelistedSubtitleTags": "Whitelisted Subtitle Tags",
"Wiki": "Wiki",
- "WouldYouLikeToRestoreBackup": "Would you like to restore the backup {0} ?",
+ "WouldYouLikeToRestoreBackup": "Would you like to restore the backup '{name}'?",
"Year": "Year",
"YesCancel": "Yes, Cancel",
"YesMoveFiles": "Yes, Move the Files",