Use named tokens in frontend translate function

This commit is contained in:
Bogdan 2023-08-14 00:58:22 +03:00
commit f83e2ad73c
23 changed files with 92 additions and 84 deletions

View file

@ -146,7 +146,7 @@ class AlbumStudioFooter extends Component {
<div> <div>
<div className={styles.label}> <div className={styles.label}>
{translate('CountArtistsSelected', [selectedCount])} {translate('CountArtistsSelected', { selectedCount })}
</div> </div>
<SpinnerButton <SpinnerButton

View file

@ -289,7 +289,7 @@ class ArtistEditorFooter extends Component {
<div className={styles.buttonContainer}> <div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}> <div className={styles.buttonContainerContent}>
<ArtistEditorFooterLabel <ArtistEditorFooterLabel
label={translate('SelectedCountArtistsSelectedInterp', [selectedCount])} label={translate('SelectedCountArtistsSelectedInterp', { selectedCount })}
isSaving={false} isSaving={false}
/> />

View file

@ -120,7 +120,7 @@ function ArtistIndexPosterInfo(props) {
if (albumCount === 0) { if (albumCount === 0) {
albums = translate('NoAlbums'); albums = translate('NoAlbums');
} else if (albumCount > 1) { } else if (albumCount > 1) {
albums = translate('CountAlbums', [albumCount]); albums = translate('CountAlbums', { albumCount });
} }
return ( return (

View file

@ -152,7 +152,7 @@ class CustomFormat extends Component {
isOpen={this.state.isDeleteCustomFormatModalOpen} isOpen={this.state.isDeleteCustomFormatModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteCustomFormat')} title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', [name])} message={translate('DeleteCustomFormatMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat} onConfirm={this.onConfirmDeleteCustomFormat}

View file

@ -115,7 +115,7 @@ class Specification extends Component {
isOpen={this.state.isDeleteSpecificationModalOpen} isOpen={this.state.isDeleteSpecificationModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteCondition')} title={translate('DeleteCondition')}
message={translate('DeleteConditionMessageText', [name])} message={translate('DeleteConditionMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteSpecification} onConfirm={this.onConfirmDeleteSpecification}
onCancel={this.onDeleteSpecificationModalClose} onCancel={this.onDeleteSpecificationModalClose}

View file

@ -113,7 +113,7 @@ class DownloadClient extends Component {
isOpen={this.state.isDeleteDownloadClientModalOpen} isOpen={this.state.isDeleteDownloadClientModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteDownloadClient')} title={translate('DeleteDownloadClient')}
message={translate('DeleteDownloadClientMessageText', [name])} message={translate('DeleteDownloadClientMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteDownloadClient} onConfirm={this.onConfirmDeleteDownloadClient}
onCancel={this.onDeleteDownloadClientModalClose} onCancel={this.onDeleteDownloadClientModalClose}

View file

@ -164,7 +164,7 @@ function ManageDownloadClientsEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountDownloadClientsSelected', [selectedCount])} {translate('CountDownloadClientsSelected', { selectedCount })}
</div> </div>
<div> <div>

View file

@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent(
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedDownloadClients')} title={translate('DeleteSelectedDownloadClients')}
message={translate('DeleteSelectedDownloadClientsMessageText', [ message={translate('DeleteSelectedDownloadClientsMessageText', {
selectedIds.length, count: selectedIds.length,
])} })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}

View file

@ -107,7 +107,7 @@ class ImportList extends Component {
isOpen={this.state.isDeleteImportListModalOpen} isOpen={this.state.isDeleteImportListModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteImportList')} title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', [name])} message={translate('DeleteImportListMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteImportList} onConfirm={this.onConfirmDeleteImportList}
onCancel={this.onDeleteImportListModalClose} onCancel={this.onDeleteImportListModalClose}

View file

@ -142,7 +142,7 @@ function ManageImportListsEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountImportListsSelected', [selectedCount])} {translate('CountImportListsSelected', { selectedCount })}
</div> </div>
<div> <div>

View file

@ -277,9 +277,9 @@ function ManageImportListsModalContent(
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedImportLists')} title={translate('DeleteSelectedImportLists')}
message={translate('DeleteSelectedImportListsMessageText', [ message={translate('DeleteSelectedImportListsMessageText', {
selectedIds.length, count: selectedIds.length,
])} })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}

View file

@ -152,7 +152,7 @@ class Indexer extends Component {
isOpen={this.state.isDeleteIndexerModalOpen} isOpen={this.state.isDeleteIndexerModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteIndexer')} title={translate('DeleteIndexer')}
message={translate('DeleteIndexerMessageText', [name])} message={translate('DeleteIndexerMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteIndexer} onConfirm={this.onConfirmDeleteIndexer}
onCancel={this.onDeleteIndexerModalClose} onCancel={this.onDeleteIndexerModalClose}

View file

@ -162,7 +162,7 @@ function ManageIndexersEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountIndexersSelected', [selectedCount])} {translate('CountIndexersSelected', { selectedCount })}
</div> </div>
<div> <div>

View file

@ -281,9 +281,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedIndexers')} title={translate('DeleteSelectedIndexers')}
message={translate('DeleteSelectedIndexersMessageText', [ message={translate('DeleteSelectedIndexersMessageText', {
selectedIds.length, count: selectedIds.length,
])} })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}

View file

@ -95,7 +95,7 @@ class RootFolder extends Component {
isOpen={this.state.isDeleteRootFolderModalOpen} isOpen={this.state.isDeleteRootFolderModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteRootFolder')} title={translate('DeleteRootFolder')}
message={translate('DeleteRootFolderMessageText', [name])} message={translate('DeleteRootFolderMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteRootFolder} onConfirm={this.onConfirmDeleteRootFolder}
onCancel={this.onDeleteRootFolderModalClose} onCancel={this.onDeleteRootFolderModalClose}

View file

@ -206,7 +206,7 @@ class Notification extends Component {
isOpen={this.state.isDeleteNotificationModalOpen} isOpen={this.state.isDeleteNotificationModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteNotification')} title={translate('DeleteNotification')}
message={translate('DeleteNotificationMessageText', [name])} message={translate('DeleteNotificationMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteNotification} onConfirm={this.onConfirmDeleteNotification}
onCancel={this.onDeleteNotificationModalClose} onCancel={this.onDeleteNotificationModalClose}

View file

@ -140,7 +140,7 @@ class MetadataProfile extends Component {
isOpen={this.state.isDeleteMetadataProfileModalOpen} isOpen={this.state.isDeleteMetadataProfileModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteMetadataProfile')} title={translate('DeleteMetadataProfile')}
message={translate('DeleteMetadataProfileMessageText', [name])} message={translate('DeleteMetadataProfileMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteMetadataProfile} onConfirm={this.onConfirmDeleteMetadataProfile}

View file

@ -162,7 +162,7 @@ class QualityProfile extends Component {
isOpen={this.state.isDeleteQualityProfileModalOpen} isOpen={this.state.isDeleteQualityProfileModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteQualityProfile')} title={translate('DeleteQualityProfile')}
message={translate('DeleteQualityProfileMessageText', [name])} message={translate('DeleteQualityProfileMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteQualityProfile} onConfirm={this.onConfirmDeleteQualityProfile}

View file

@ -138,7 +138,7 @@ class BackupRow extends Component {
isOpen={isConfirmDeleteModalOpen} isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteBackup')} title={translate('DeleteBackup')}
message={translate('DeleteBackupMessageText', [name])} message={translate('DeleteBackupMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeletePress} onConfirm={this.onConfirmDeletePress}
onCancel={this.onConfirmDeleteModalClose} onCancel={this.onConfirmDeleteModalClose}

View file

@ -146,7 +146,7 @@ class RestoreBackupModalContent extends Component {
<ModalBody> <ModalBody>
{ {
!!id && translate('WouldYouLikeToRestoreBackup', [name]) !!id && translate('WouldYouLikeToRestoreBackup', { name })
} }
{ {

View file

@ -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;
}

View file

@ -0,0 +1,44 @@
import createAjaxRequest from 'Utilities/createAjaxRequest';
function getTranslations() {
return createAjaxRequest({
global: false,
dataType: 'json',
url: '/localization',
}).request;
}
let translations: Record<string, string> = {};
export async function fetchTranslations(): Promise<boolean> {
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<string, string | number | boolean>
) {
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;
}

View file

@ -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", "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.", "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}'", "CouldntFindAnyResultsForTerm": "Couldn't find any results for '{0}'",
"CountAlbums": "{0} albums", "CountAlbums": "{albumCount} albums",
"CountArtistsSelected": "{0} artist(s) selected", "CountArtistsSelected": "{selectedCount} artist(s) selected",
"CountDownloadClientsSelected": "{0} download client(s) selected", "CountDownloadClientsSelected": "{selectedCount} download client(s) selected",
"CountImportListsSelected": "{0} import list(s) selected", "CountImportListsSelected": "{selectedCount} import list(s) selected",
"CountIndexersSelected": "{0} indexer(s) selected", "CountIndexersSelected": "{selectedCount} indexer(s) selected",
"Country": "Country", "Country": "Country",
"CreateEmptyArtistFolders": "Create empty artist folders", "CreateEmptyArtistFolders": "Create empty artist folders",
"CreateEmptyArtistFoldersHelpText": "Create missing artist folders during disk scan", "CreateEmptyArtistFoldersHelpText": "Create missing artist folders during disk scan",
@ -203,45 +203,45 @@
"Delete": "Delete", "Delete": "Delete",
"DeleteArtist": "Delete Selected Artist", "DeleteArtist": "Delete Selected Artist",
"DeleteBackup": "Delete Backup", "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", "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", "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", "DeleteDelayProfile": "Delete Delay Profile",
"DeleteDelayProfileMessageText": "Are you sure you want to delete this delay profile?", "DeleteDelayProfileMessageText": "Are you sure you want to delete this delay profile?",
"DeleteDownloadClient": "Delete Download Client", "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", "DeleteEmptyFolders": "Delete empty folders",
"DeleteEmptyFoldersHelpText": "Delete empty artist and album folders during disk scan and when track files are deleted", "DeleteEmptyFoldersHelpText": "Delete empty artist and album folders during disk scan and when track files are deleted",
"DeleteFilesHelpText": "Delete the track files and artist folder", "DeleteFilesHelpText": "Delete the track files and artist folder",
"DeleteFormat": "Delete Format", "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", "DeleteImportList": "Delete Import List",
"DeleteImportListExclusion": "Delete Import List Exclusion", "DeleteImportListExclusion": "Delete Import List Exclusion",
"DeleteImportListExclusionMessageText": "Are you sure you want to delete this 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", "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", "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", "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", "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", "DeleteReleaseProfile": "Delete ReleaseProfile",
"DeleteReleaseProfileMessageText": "Are you sure you want to delete this releaseProfile?", "DeleteReleaseProfileMessageText": "Are you sure you want to delete this releaseProfile?",
"DeleteRemotePathMapping": "Delete Remote Path Mapping", "DeleteRemotePathMapping": "Delete Remote Path Mapping",
"DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?", "DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?",
"DeleteRootFolder": "Delete Root Folder", "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", "DeleteSelected": "Delete Selected",
"DeleteSelectedDownloadClients": "Delete Download Client(s)", "DeleteSelectedDownloadClients": "Delete Selected 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)", "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)", "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", "DeleteSelectedTrackFiles": "Delete Selected Track Files",
"DeleteSelectedTrackFilesMessageText": "Are you sure you want to delete the selected track files?", "DeleteSelectedTrackFilesMessageText": "Are you sure you want to delete the selected track files?",
"DeleteTag": "Delete Tag", "DeleteTag": "Delete Tag",
@ -846,7 +846,7 @@
"SelectQuality": "Select Quality", "SelectQuality": "Select Quality",
"SelectReleaseGroup": "Select Release Group", "SelectReleaseGroup": "Select Release Group",
"SelectTracks": "Select Tracks", "SelectTracks": "Select Tracks",
"SelectedCountArtistsSelectedInterp": "{0} Artist(s) Selected", "SelectedCountArtistsSelectedInterp": "{selectedCount} Artist(s) Selected",
"SendAnonymousUsageData": "Send Anonymous Usage Data", "SendAnonymousUsageData": "Send Anonymous Usage Data",
"SetPermissions": "Set Permissions", "SetPermissions": "Set Permissions",
"SetPermissionsLinuxHelpText": "Should chmod be run when files are imported/renamed?", "SetPermissionsLinuxHelpText": "Should chmod be run when files are imported/renamed?",
@ -1047,7 +1047,7 @@
"WatchLibraryForChangesHelpText": "Rescan automatically when files change in a root folder", "WatchLibraryForChangesHelpText": "Rescan automatically when files change in a root folder",
"WatchRootFoldersForFileChanges": "Watch Root Folders for file changes", "WatchRootFoldersForFileChanges": "Watch Root Folders for file changes",
"WeekColumnHeader": "Week Column Header", "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.", "WriteAudioTagsHelpTextWarning": "Selecting 'All files' will alter existing files when they are imported.",
"WriteMetadataTags": "Write Metadata Tags", "WriteMetadataTags": "Write Metadata Tags",
"WriteMetadataToAudioFiles": "Write Metadata to Audio Files", "WriteMetadataToAudioFiles": "Write Metadata to Audio Files",