Use named tokens in frontend translate function

This commit is contained in:
Bogdan
2023-08-13 21:03:38 +03:00
parent 5dbb59dfaa
commit 31261f66ad
22 changed files with 57 additions and 51 deletions

View File

@@ -288,7 +288,7 @@ class AddIndexerModalContent extends Component {
<div className={styles.available}> <div className={styles.available}>
{ {
isPopulated ? isPopulated ?
translate('CountIndexersAvailable', [filteredIndexers.length]) : translate('CountIndexersAvailable', { count: filteredIndexers.length }) :
null null
} }
</div> </div>

View File

@@ -37,7 +37,7 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
{translate('AreYouSureYouWantToDeleteIndexer', [name])} {translate('AreYouSureYouWantToDeleteIndexer', { name })}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>

View File

@@ -48,7 +48,9 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
<ModalBody> <ModalBody>
<div className={styles.message}> <div className={styles.message}>
{translate('DeleteSelectedIndexersMessageText', [indexers.length])} {translate('DeleteSelectedIndexersMessageText', {
count: indexers.length,
})}
</div> </div>
<ul> <ul>

View File

@@ -257,7 +257,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountIndexersSelected', [selectedCount])} {translate('CountIndexersSelected', { count: selectedCount })}
</div> </div>
<div> <div>

View File

@@ -165,7 +165,7 @@ function IndexerIndexSelectFooter() {
</div> </div>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountIndexersSelected', [selectedCount])} {translate('CountIndexersSelected', { count: selectedCount })}
</div> </div>
<EditIndexerModal <EditIndexerModal

View File

@@ -191,10 +191,10 @@ class SearchFooter extends Component {
icon = icons.SEARCH; icon = icons.SEARCH;
} }
let footerLabel = searchIndexerIds.length === 0 ? translate('SearchAllIndexers') : translate('SearchCountIndexers', [searchIndexerIds.length]); let footerLabel = searchIndexerIds.length === 0 ? translate('SearchAllIndexers') : translate('SearchCountIndexers', { count: searchIndexerIds.length });
if (isPopulated) { if (isPopulated) {
footerLabel = selectedCount === 0 ? translate('FoundCountReleases', [itemCount]) : translate('SelectedCountOfCountReleases', [selectedCount, itemCount]); footerLabel = selectedCount === 0 ? translate('FoundCountReleases', { itemCount }) : translate('SelectedCountOfCountReleases', { selectedCount, itemCount });
} }
return ( return (

View File

@@ -127,7 +127,7 @@ class Application extends Component {
isOpen={this.state.isDeleteApplicationModalOpen} isOpen={this.state.isDeleteApplicationModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteApplication')} title={translate('DeleteApplication')}
message={translate('DeleteApplicationMessageText', [name])} message={translate('DeleteApplicationMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteApplication} onConfirm={this.onConfirmDeleteApplication}
onCancel={this.onDeleteApplicationModalClose} onCancel={this.onDeleteApplicationModalClose}

View File

@@ -114,7 +114,7 @@ function ManageApplicationsEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountApplicationsSelected', [selectedCount])} {translate('CountApplicationsSelected', { count: selectedCount })}
</div> </div>
<div> <div>

View File

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

View File

@@ -88,7 +88,7 @@ class Category extends Component {
message={ message={
<div> <div>
<div> <div>
{translate('AreYouSureYouWantToDeleteCategory', [name])} {translate('AreYouSureYouWantToDeleteCategory')}
</div> </div>
</div> </div>
} }

View File

@@ -105,7 +105,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

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

View File

@@ -226,9 +226,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

@@ -122,7 +122,7 @@ class IndexerProxy extends Component {
isOpen={this.state.isDeleteIndexerProxyModalOpen} isOpen={this.state.isDeleteIndexerProxyModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteIndexerProxy')} title={translate('DeleteIndexerProxy')}
message={translate('DeleteIndexerProxyMessageText', [name])} message={translate('DeleteIndexerProxyMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteIndexerProxy} onConfirm={this.onConfirmDeleteIndexerProxy}
onCancel={this.onDeleteIndexerProxyModalClose} onCancel={this.onDeleteIndexerProxyModalClose}

View File

@@ -137,7 +137,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

@@ -130,7 +130,7 @@ class AppProfile extends Component {
isOpen={this.state.isDeleteAppProfileModalOpen} isOpen={this.state.isDeleteAppProfileModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteAppProfile')} title={translate('DeleteAppProfile')}
message={translate('AppProfileDeleteConfirm', [name])} message={translate('DeleteAppProfileMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteAppProfile} onConfirm={this.onConfirmDeleteAppProfile}

View File

@@ -137,7 +137,7 @@ class Tag extends Component {
isOpen={isDeleteTagModalOpen} isOpen={isDeleteTagModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteTag')} title={translate('DeleteTag')}
message={translate('DeleteTagMessageText', [label])} message={translate('DeleteTagMessageText', { label })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteTag} onConfirm={this.onConfirmDeleteTag}
onCancel={this.onDeleteTagModalClose} onCancel={this.onDeleteTagModalClose}

View File

@@ -147,7 +147,7 @@ class UISettings extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.SELECT}
name="theme" name="theme"
helpText={translate('ThemeHelpText', ['Theme.Park'])} helpText={translate('ThemeHelpText', { inspiredBy: 'Theme.Park' })}
values={themeOptions} values={themeOptions}
onChange={onInputChange} onChange={onInputChange}
{...settings.theme} {...settings.theme}

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

@@ -114,7 +114,7 @@ class Updates extends Component {
/> />
<div className={styles.message}> <div className={styles.message}>
{translate('TheLatestVersionIsAlreadyInstalled', ['Prowlarr'])} {translate('TheLatestVersionIsAlreadyInstalled', { appName: 'Prowlarr' })}
</div> </div>
{ {

View File

@@ -25,14 +25,19 @@ export async function fetchTranslations(): Promise<boolean> {
export default function translate( export default function translate(
key: string, key: string,
args?: (string | number | boolean)[] tokens?: Record<string, string | number | boolean>
) { ) {
const translation = translations[key] || key; const translation = translations[key] || key;
if (args) { if (tokens) {
return translation.replace(/\{(\d+)\}/g, (match, index) => { // Fallback to the old behaviour for translations not yet updated to use named tokens
return String(args[index]) ?? match; Object.values(tokens).forEach((value, index) => {
tokens[index] = value;
}); });
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
String(tokens[tokenMatch] ?? match)
);
} }
return translation; return translation;

View File

@@ -28,7 +28,6 @@
"ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {0} characters long. You can do this via settings or the config file", "ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {0} characters long. You can do this via settings or the config file",
"AppDataDirectory": "AppData directory", "AppDataDirectory": "AppData directory",
"AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update", "AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update",
"AppProfileDeleteConfirm": "Are you sure you want to delete {0}?",
"AppProfileInUse": "App Profile in Use", "AppProfileInUse": "App Profile in Use",
"AppProfileSelectHelpText": "App profiles are used to control RSS, Automatic Search and Interactive Search settings on application sync", "AppProfileSelectHelpText": "App profiles are used to control RSS, Automatic Search and Interactive Search settings on application sync",
"AppSettingsSummary": "Applications and settings to configure how Prowlarr interacts with your PVR programs", "AppSettingsSummary": "Applications and settings to configure how Prowlarr interacts with your PVR programs",
@@ -52,7 +51,7 @@
"AppsMinimumSeeders": "Apps Minimum Seeders", "AppsMinimumSeeders": "Apps Minimum Seeders",
"AppsMinimumSeedersHelpText": "Minimum seeders required by the Applications for the indexer to grab, empty is Sync profile's default", "AppsMinimumSeedersHelpText": "Minimum seeders required by the Applications for the indexer to grab, empty is Sync profile's default",
"AreYouSureYouWantToDeleteCategory": "Are you sure you want to delete mapped category?", "AreYouSureYouWantToDeleteCategory": "Are you sure you want to delete mapped category?",
"AreYouSureYouWantToDeleteIndexer": "Are you sure you want to delete '{0}' from Prowlarr?", "AreYouSureYouWantToDeleteIndexer": "Are you sure you want to delete '{name}' from Prowlarr?",
"AreYouSureYouWantToResetYourAPIKey": "Are you sure you want to reset your API Key?", "AreYouSureYouWantToResetYourAPIKey": "Are you sure you want to reset your API Key?",
"Artist": "Artist", "Artist": "Artist",
"AudioSearch": "Audio Search", "AudioSearch": "Audio Search",
@@ -108,10 +107,10 @@
"ConnectionLostMessage": "Prowlarr has lost its connection to the backend and will need to be reloaded to restore functionality.", "ConnectionLostMessage": "Prowlarr has lost its connection to the backend and will need to be reloaded to restore functionality.",
"Connections": "Connections", "Connections": "Connections",
"CouldNotConnectSignalR": "Could not connect to SignalR, UI won't update", "CouldNotConnectSignalR": "Could not connect to SignalR, UI won't update",
"CountApplicationsSelected": "{0} application(s) selected", "CountApplicationsSelected": "{count} application(s) selected",
"CountDownloadClientsSelected": "{0} download client(s) selected", "CountDownloadClientsSelected": "{count} download client(s) selected",
"CountIndexersAvailable": "{0} indexer(s) available", "CountIndexersAvailable": "{count} indexer(s) available",
"CountIndexersSelected": "{0} indexer(s) selected", "CountIndexersSelected": "{count} indexer(s) selected",
"Custom": "Custom", "Custom": "Custom",
"CustomFilters": "Custom Filters", "CustomFilters": "Custom Filters",
"DBMigration": "DB Migration", "DBMigration": "DB Migration",
@@ -120,26 +119,27 @@
"Dates": "Dates", "Dates": "Dates",
"Delete": "Delete", "Delete": "Delete",
"DeleteAppProfile": "Delete App Profile", "DeleteAppProfile": "Delete App Profile",
"DeleteAppProfileMessageText": "Are you sure you want to delete the app profile '{name}'?",
"DeleteApplication": "Delete Application", "DeleteApplication": "Delete Application",
"DeleteApplicationMessageText": "Are you sure you want to delete the application '{0}'?", "DeleteApplicationMessageText": "Are you sure you want to delete the application '{name}'?",
"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}'?",
"DeleteClientCategory": "Delete Download Client Category", "DeleteClientCategory": "Delete Download Client Category",
"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}'?",
"DeleteIndexerProxy": "Delete Indexer Proxy", "DeleteIndexerProxy": "Delete Indexer Proxy",
"DeleteIndexerProxyMessageText": "Are you sure you want to delete the proxy '{0}'?", "DeleteIndexerProxyMessageText": "Are you sure you want to delete the indexer proxy '{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}'?",
"DeleteSelectedApplications": "Delete Selected Applications", "DeleteSelectedApplications": "Delete Selected Applications",
"DeleteSelectedApplicationsMessageText": "Are you sure you want to delete {0} selected application(s)?", "DeleteSelectedApplicationsMessageText": "Are you sure you want to delete {count} selected application(s)?",
"DeleteSelectedDownloadClients": "Delete Download Client(s)", "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)?",
"DeleteSelectedIndexer": "Delete Selected Indexer", "DeleteSelectedIndexer": "Delete Selected Indexer",
"DeleteSelectedIndexers": "Delete Selected Indexers", "DeleteSelectedIndexers": "Delete Selected Indexers",
"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", "DeleteTag": "Delete Tag",
"DeleteTagMessageText": "Are you sure you want to delete the tag '{0}'?", "DeleteTagMessageText": "Are you sure you want to delete the tag '{label}'?",
"Description": "Description", "Description": "Description",
"Details": "Details", "Details": "Details",
"DevelopmentSettings": "Development Settings", "DevelopmentSettings": "Development Settings",
@@ -195,7 +195,7 @@
"FocusSearchBox": "Focus Search Box", "FocusSearchBox": "Focus Search Box",
"Folder": "Folder", "Folder": "Folder",
"ForMoreInformationOnTheIndividualDownloadClients": "For more information on the individual download clients, click on the info buttons.", "ForMoreInformationOnTheIndividualDownloadClients": "For more information on the individual download clients, click on the info buttons.",
"FoundCountReleases": "Found {0} releases", "FoundCountReleases": "Found {itemCount} releases",
"FullSync": "Full Sync", "FullSync": "Full Sync",
"General": "General", "General": "General",
"GeneralSettings": "General Settings", "GeneralSettings": "General Settings",
@@ -257,7 +257,6 @@
"IndexerVipCheckExpiredClientMessage": "Indexer VIP benefits have expired: {0}", "IndexerVipCheckExpiredClientMessage": "Indexer VIP benefits have expired: {0}",
"IndexerVipCheckExpiringClientMessage": "Indexer VIP benefits expiring soon: {0}", "IndexerVipCheckExpiringClientMessage": "Indexer VIP benefits expiring soon: {0}",
"Indexers": "Indexers", "Indexers": "Indexers",
"IndexersSelectedInterp": "{0} Indexer(s) Selected",
"Info": "Info", "Info": "Info",
"InitialFailure": "Initial Failure", "InitialFailure": "Initial Failure",
"InstanceName": "Instance Name", "InstanceName": "Instance Name",
@@ -358,7 +357,7 @@
"Proxies": "Proxies", "Proxies": "Proxies",
"Proxy": "Proxy", "Proxy": "Proxy",
"ProxyBypassFilterHelpText": "Use ',' as a separator, and '*.' as a wildcard for subdomains", "ProxyBypassFilterHelpText": "Use ',' as a separator, and '*.' as a wildcard for subdomains",
"ProxyCheckBadRequestMessage": "Failed to test proxy. StatusCode: {0}", "ProxyCheckBadRequestMessage": "Failed to test proxy. Status code: {0}",
"ProxyCheckFailedToTestMessage": "Failed to test proxy: {0}", "ProxyCheckFailedToTestMessage": "Failed to test proxy: {0}",
"ProxyCheckResolveIpMessage": "Failed to resolve the IP Address for the Configured Proxy Host {0}", "ProxyCheckResolveIpMessage": "Failed to resolve the IP Address for the Configured Proxy Host {0}",
"ProxyPasswordHelpText": "You only need to enter a username and password if one is required. Leave them blank otherwise.", "ProxyPasswordHelpText": "You only need to enter a username and password if one is required. Leave them blank otherwise.",
@@ -415,7 +414,7 @@
"Search": "Search", "Search": "Search",
"SearchAllIndexers": "Search all indexers", "SearchAllIndexers": "Search all indexers",
"SearchCapabilities": "Search Capabilities", "SearchCapabilities": "Search Capabilities",
"SearchCountIndexers": "Search {0} indexers", "SearchCountIndexers": "Search {count} indexer(s)",
"SearchIndexers": "Search Indexers", "SearchIndexers": "Search Indexers",
"SearchQueries": "Search Queries", "SearchQueries": "Search Queries",
"SearchType": "Search Type", "SearchType": "Search Type",
@@ -429,7 +428,7 @@
"Seeders": "Seeders", "Seeders": "Seeders",
"SelectAll": "Select All", "SelectAll": "Select All",
"SelectIndexers": "Select Indexers", "SelectIndexers": "Select Indexers",
"SelectedCountOfCountReleases": "Selected {0} of {1} releases", "SelectedCountOfCountReleases": "Selected {selectedCount} of {itemCount} releases",
"SemiPrivate": "Semi-Private", "SemiPrivate": "Semi-Private",
"SendAnonymousUsageData": "Send Anonymous Usage Data", "SendAnonymousUsageData": "Send Anonymous Usage Data",
"SetTags": "Set Tags", "SetTags": "Set Tags",
@@ -487,9 +486,9 @@
"TestAllApps": "Test All Apps", "TestAllApps": "Test All Apps",
"TestAllClients": "Test All Clients", "TestAllClients": "Test All Clients",
"TestAllIndexers": "Test All Indexers", "TestAllIndexers": "Test All Indexers",
"TheLatestVersionIsAlreadyInstalled": "The latest version of {0} is already installed", "TheLatestVersionIsAlreadyInstalled": "The latest version of {appName} is already installed",
"Theme": "Theme", "Theme": "Theme",
"ThemeHelpText": "Change Application UI Theme, 'Auto' Theme will use your OS Theme to set Light or Dark mode. Inspired by {0}", "ThemeHelpText": "Change Application UI Theme, 'Auto' Theme will use your OS Theme to set Light or Dark mode. Inspired by {inspiredBy}.",
"Time": "Time", "Time": "Time",
"Title": "Title", "Title": "Title",
"Today": "Today", "Today": "Today",