diff --git a/frontend/src/AlbumStudio/AlbumStudioFooter.js b/frontend/src/AlbumStudio/AlbumStudioFooter.js index fbdaefa13..8543a0347 100644 --- a/frontend/src/AlbumStudio/AlbumStudioFooter.js +++ b/frontend/src/AlbumStudio/AlbumStudioFooter.js @@ -146,7 +146,7 @@ class AlbumStudioFooter extends Component {
- {translate('CountArtistsSelected', [selectedCount])} + {translate('CountArtistsSelected', { selectedCount })}
diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js b/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js index bad6c63ec..20a34bffd 100644 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js @@ -120,7 +120,7 @@ function ArtistIndexPosterInfo(props) { if (albumCount === 0) { albums = translate('NoAlbums'); } else if (albumCount > 1) { - albums = translate('CountAlbums', [albumCount]); + albums = translate('CountAlbums', { albumCount }); } return ( diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js index d18abcc32..0f72228bb 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js @@ -152,7 +152,7 @@ class CustomFormat extends Component { isOpen={this.state.isDeleteCustomFormatModalOpen} kind={kinds.DANGER} title={translate('DeleteCustomFormat')} - message={translate('DeleteCustomFormatMessageText', [name])} + message={translate('DeleteCustomFormatMessageText', { name })} confirmLabel={translate('Delete')} isSpinning={isDeleting} onConfirm={this.onConfirmDeleteCustomFormat} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.js index 5d06efcf2..0b59f09d5 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.js @@ -115,7 +115,7 @@ class Specification extends Component { isOpen={this.state.isDeleteSpecificationModalOpen} kind={kinds.DANGER} title={translate('DeleteCondition')} - message={translate('DeleteConditionMessageText', [name])} + message={translate('DeleteConditionMessageText', { name })} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeleteSpecification} onCancel={this.onDeleteSpecificationModalClose} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js index 5a86b32d6..a20170662 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js @@ -113,7 +113,7 @@ class DownloadClient extends Component { isOpen={this.state.isDeleteDownloadClientModalOpen} kind={kinds.DANGER} title={translate('DeleteDownloadClient')} - message={translate('DeleteDownloadClientMessageText', [name])} + message={translate('DeleteDownloadClientMessageText', { name })} confirmLabel={translate('Delete')} onConfirm={this.onConfirmDeleteDownloadClient} onCancel={this.onDeleteDownloadClientModalClose} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx index 09961965a..e719a6907 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx @@ -164,7 +164,7 @@ function ManageDownloadClientsEditModalContent(
- {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 05c320d5b..5d8f6dd49 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 b59dccdec..477cbfef3 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx @@ -142,7 +142,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 a4a4a9b7b..fdcc20f8b 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 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 b6e9804a1..dd76376d6 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx @@ -162,7 +162,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 850ca2331..20418c682 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 d82c37867..6e9b01f57 100644 --- a/frontend/src/Settings/Notifications/Notifications/Notification.js +++ b/frontend/src/Settings/Notifications/Notifications/Notification.js @@ -206,7 +206,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 1a0a5059e..c3cfefa1d 100644 --- a/frontend/src/Settings/Profiles/Metadata/MetadataProfile.js +++ b/frontend/src/Settings/Profiles/Metadata/MetadataProfile.js @@ -140,7 +140,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 a7f122e6f..f053ef741 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 a277a5499..d6cfb0c81 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.js b/frontend/src/Utilities/String/translate.js deleted file mode 100644 index 9e7452741..000000000 --- a/frontend/src/Utilities/String/translate.js +++ /dev/null @@ -1,36 +0,0 @@ -import createAjaxRequest from 'Utilities/createAjaxRequest'; - -function getTranslations() { - return createAjaxRequest({ - global: false, - dataType: 'json', - url: '/localization' - }).request; -} - -let translations = {}; - -export function fetchTranslations() { - return new Promise(async(resolve) => { - try { - const data = await getTranslations(); - translations = data.strings; - - resolve(true); - } catch (error) { - resolve(false); - } - }); -} - -export default function translate(key, args = []) { - const translation = translations[key] || key; - - if (args) { - return translation.replace(/\{(\d+)\}/g, (match, index) => { - return args[index]; - }); - } - - return translation; -} diff --git a/frontend/src/Utilities/String/translate.ts b/frontend/src/Utilities/String/translate.ts new file mode 100644 index 000000000..0ccfa839b --- /dev/null +++ b/frontend/src/Utilities/String/translate.ts @@ -0,0 +1,44 @@ +import createAjaxRequest from 'Utilities/createAjaxRequest'; + +function getTranslations() { + return createAjaxRequest({ + global: false, + dataType: 'json', + url: '/localization', + }).request; +} + +let translations: Record = {}; + +export async function fetchTranslations(): Promise { + return new Promise(async (resolve) => { + try { + const data = await getTranslations(); + translations = data.strings; + + resolve(true); + } catch (error) { + resolve(false); + } + }); +} + +export default function translate( + key: string, + tokens?: Record +) { + const translation = translations[key] || key; + + 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 eb96990d4..71da023eb 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -165,11 +165,11 @@ "CopyUsingHardlinksHelpText": "Hardlinks allow Lidarr 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 Lidarr's rename function as a work around.", "CouldntFindAnyResultsForTerm": "Couldn't find any results for '{0}'", - "CountAlbums": "{0} albums", - "CountArtistsSelected": "{0} artist(s) selected", - "CountDownloadClientsSelected": "{0} download client(s) selected", - "CountImportListsSelected": "{0} import list(s) selected", - "CountIndexersSelected": "{0} indexer(s) selected", + "CountAlbums": "{albumCount} albums", + "CountArtistsSelected": "{selectedCount} artist(s) selected", + "CountDownloadClientsSelected": "{selectedCount} download client(s) selected", + "CountImportListsSelected": "{selectedCount} import list(s) selected", + "CountIndexersSelected": "{selectedCount} indexer(s) selected", "Country": "Country", "CreateEmptyArtistFolders": "Create empty artist folders", "CreateEmptyArtistFoldersHelpText": "Create missing artist folders during disk scan", @@ -203,45 +203,45 @@ "Delete": "Delete", "DeleteArtist": "Delete Selected Artist", "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 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 artist and album folders during disk scan and when track files are deleted", "DeleteFilesHelpText": "Delete the track files and artist 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 '{name}'?", "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 ReleaseProfile", "DeleteReleaseProfileMessageText": "Are you sure you want to delete this releaseProfile?", "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}'?", "DeleteSelected": "Delete Selected", - "DeleteSelectedDownloadClients": "Delete Download Client(s)", - "DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {0} selected download client(s)?", + "DeleteSelectedDownloadClients": "Delete 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)?", "DeleteSelectedTrackFiles": "Delete Selected Track Files", "DeleteSelectedTrackFilesMessageText": "Are you sure you want to delete the selected track files?", "DeleteTag": "Delete Tag", @@ -846,7 +846,7 @@ "SelectQuality": "Select Quality", "SelectReleaseGroup": "Select Release Group", "SelectTracks": "Select Tracks", - "SelectedCountArtistsSelectedInterp": "{0} Artist(s) Selected", + "SelectedCountArtistsSelectedInterp": "{selectedCount} Artist(s) Selected", "SendAnonymousUsageData": "Send Anonymous Usage Data", "SetPermissions": "Set Permissions", "SetPermissionsLinuxHelpText": "Should chmod be run when files are imported/renamed?", @@ -1047,7 +1047,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 {0} ?", + "WouldYouLikeToRestoreBackup": "Would you like to restore the backup '{name}'?", "WriteAudioTagsHelpTextWarning": "Selecting 'All files' will alter existing files when they are imported.", "WriteMetadataTags": "Write Metadata Tags", "WriteMetadataToAudioFiles": "Write Metadata to Audio Files",