diff --git a/frontend/src/App/App.js b/frontend/src/App/App.js index d2cf9a3ba..e8911cba5 100644 --- a/frontend/src/App/App.js +++ b/frontend/src/App/App.js @@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector'; import ApplyTheme from './ApplyTheme'; import AppRoutes from './AppRoutes'; -function App({ store, history, hasTranslationsError }) { +function App({ store, history }) { return ( - + @@ -25,8 +25,7 @@ function App({ store, history, hasTranslationsError }) { App.propTypes = { store: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, - hasTranslationsError: PropTypes.bool.isRequired + history: PropTypes.object.isRequired }; export default App; diff --git a/frontend/src/Components/Page/ErrorPage.js b/frontend/src/Components/Page/ErrorPage.js index d6c345e49..4302654d1 100644 --- a/frontend/src/Components/Page/ErrorPage.js +++ b/frontend/src/Components/Page/ErrorPage.js @@ -7,7 +7,7 @@ function ErrorPage(props) { const { version, isLocalStorageSupported, - hasTranslationsError, + translationsError, authorError, customFiltersError, tagsError, @@ -21,8 +21,8 @@ function ErrorPage(props) { if (!isLocalStorageSupported) { errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.'; - } else if (hasTranslationsError) { - errorMessage = 'Failed to load translations from API'; + } else if (translationsError) { + errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API'); } else if (authorError) { errorMessage = getErrorMessage(authorError, 'Failed to load author from API'); } else if (customFiltersError) { @@ -55,7 +55,7 @@ function ErrorPage(props) { ErrorPage.propTypes = { version: PropTypes.string.isRequired, isLocalStorageSupported: PropTypes.bool.isRequired, - hasTranslationsError: PropTypes.bool.isRequired, + translationsError: PropTypes.object, authorError: PropTypes.object, customFiltersError: PropTypes.object, tagsError: PropTypes.object, diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index 7e0216397..d480c824a 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; import { createSelector } from 'reselect'; -import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; +import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { fetchAuthor } from 'Store/Actions/authorActions'; import { fetchBooks } from 'Store/Actions/bookActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; @@ -52,6 +52,7 @@ const selectIsPopulated = createSelector( (state) => state.settings.metadataProfiles.isPopulated, (state) => state.settings.importLists.isPopulated, (state) => state.system.status.isPopulated, + (state) => state.app.translations.isPopulated, ( customFiltersIsPopulated, tagsIsPopulated, @@ -60,7 +61,8 @@ const selectIsPopulated = createSelector( qualityProfilesIsPopulated, metadataProfilesIsPopulated, importListsIsPopulated, - systemStatusIsPopulated + systemStatusIsPopulated, + translationsIsPopulated ) => { return ( customFiltersIsPopulated && @@ -70,7 +72,8 @@ const selectIsPopulated = createSelector( qualityProfilesIsPopulated && metadataProfilesIsPopulated && importListsIsPopulated && - systemStatusIsPopulated + systemStatusIsPopulated && + translationsIsPopulated ); } ); @@ -84,6 +87,7 @@ const selectErrors = createSelector( (state) => state.settings.metadataProfiles.error, (state) => state.settings.importLists.error, (state) => state.system.status.error, + (state) => state.app.translations.error, ( customFiltersError, tagsError, @@ -92,7 +96,8 @@ const selectErrors = createSelector( qualityProfilesError, metadataProfilesError, importListsError, - systemStatusError + systemStatusError, + translationsError ) => { const hasError = !!( customFiltersError || @@ -102,7 +107,8 @@ const selectErrors = createSelector( qualityProfilesError || metadataProfilesError || importListsError || - systemStatusError + systemStatusError || + translationsError ); return { @@ -114,7 +120,8 @@ const selectErrors = createSelector( qualityProfilesError, metadataProfilesError, importListsError, - systemStatusError + systemStatusError, + translationsError }; } ); @@ -176,6 +183,9 @@ function createMapDispatchToProps(dispatch, props) { dispatchFetchStatus() { dispatch(fetchStatus()); }, + dispatchFetchTranslations() { + dispatch(fetchTranslations()); + }, onResize(dimensions) { dispatch(saveDimensions(dimensions)); }, @@ -210,6 +220,7 @@ class PageConnector extends Component { this.props.dispatchFetchImportLists(); this.props.dispatchFetchUISettings(); this.props.dispatchFetchStatus(); + this.props.dispatchFetchTranslations(); } } @@ -225,7 +236,6 @@ class PageConnector extends Component { render() { const { - hasTranslationsError, isPopulated, hasError, dispatchFetchAuthor, @@ -237,15 +247,15 @@ class PageConnector extends Component { dispatchFetchImportLists, dispatchFetchUISettings, dispatchFetchStatus, + dispatchFetchTranslations, ...otherProps } = this.props; - if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) { + if (hasError || !this.state.isLocalStorageSupported) { return ( ); } @@ -266,7 +276,6 @@ class PageConnector extends Component { } PageConnector.propTypes = { - hasTranslationsError: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, hasError: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired, @@ -280,6 +289,7 @@ PageConnector.propTypes = { dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired, + dispatchFetchTranslations: PropTypes.func.isRequired, onSidebarVisibleChange: PropTypes.func.isRequired }; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index fc9f33a0e..ef4bad06a 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -21,28 +21,28 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth); const links = [ { iconName: icons.AUTHOR_CONTINUING, - title: 'Library', + title: () => translate('Library'), to: '/', alias: '/authors', children: [ { - title: 'Authors', + title: () => translate('Authors'), to: '/authors' }, { - title: 'Books', + title: () => translate('Books'), to: '/books' }, { - title: 'Add New', + title: () => translate('AddNew'), to: '/add/search' }, { - title: 'Bookshelf', + title: () => translate('Bookshelf'), to: '/shelf' }, { - title: 'Unmapped Files', + title: () => translate('UnmappedFiles'), to: '/unmapped' } ] @@ -50,26 +50,26 @@ const links = [ { iconName: icons.CALENDAR, - title: 'Calendar', + title: () => translate('Calendar'), to: '/calendar' }, { iconName: icons.ACTIVITY, - title: 'Activity', + title: () => translate('Activity'), to: '/activity/queue', children: [ { - title: 'Queue', + title: () => translate('Queue'), to: '/activity/queue', statusComponent: QueueStatusConnector }, { - title: 'History', + title: () => translate('History'), to: '/activity/history' }, { - title: 'Blocklist', + title: () => translate('Blocklist'), to: '/activity/blocklist' } ] @@ -77,15 +77,15 @@ const links = [ { iconName: icons.WARNING, - title: 'Wanted', + title: () => translate('Wanted'), to: '/wanted/missing', children: [ { - title: 'Missing', + title: () => translate('Missing'), to: '/wanted/missing' }, { - title: 'Cutoff Unmet', + title: () => translate('CutoffUnmet'), to: '/wanted/cutoffunmet' } ] @@ -93,19 +93,19 @@ const links = [ { iconName: icons.SETTINGS, - title: 'Settings', + title: () => translate('Settings'), to: '/settings', children: [ { - title: 'Media Management', + title: () => translate('MediaManagement'), to: '/settings/mediamanagement' }, { - title: 'Profiles', + title: () => translate('Profiles'), to: '/settings/profiles' }, { - title: 'Quality', + title: () => translate('Quality'), to: '/settings/quality' }, { @@ -117,31 +117,31 @@ const links = [ to: '/settings/indexers' }, { - title: 'Download Clients', + title: () => translate('DownloadClients'), to: '/settings/downloadclients' }, { - title: 'Import Lists', + title: () => translate('ImportLists'), to: '/settings/importlists' }, { - title: 'Connect', + title: () => translate('Connect'), to: '/settings/connect' }, { - title: 'Metadata', + title: () => translate('Metadata'), to: '/settings/metadata' }, { - title: 'Tags', + title: () => translate('Tags'), to: '/settings/tags' }, { - title: 'General', + title: () => translate('General'), to: '/settings/general' }, { - title: 'UI', + title: () => translate('Ui'), to: '/settings/ui' } ] @@ -149,32 +149,32 @@ const links = [ { iconName: icons.SYSTEM, - title: 'System', + title: () => translate('System'), to: '/system/status', children: [ { - title: 'Status', + title: () => translate('Status'), to: '/system/status', statusComponent: HealthStatusConnector }, { - title: 'Tasks', + title: () => translate('Tasks'), to: '/system/tasks' }, { - title: 'Backup', + title: () => translate('Backup'), to: '/system/backup' }, { - title: 'Updates', + title: () => translate('Updates'), to: '/system/updates' }, { - title: 'Events', + title: () => translate('Events'), to: '/system/events' }, { - title: 'Log Files', + title: () => translate('LogFiles'), to: '/system/logs/files' } ] diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx index 09961965a..d602f9cd2 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx @@ -27,9 +27,25 @@ interface ManageDownloadClientsEditModalContentProps { const NO_CHANGE = 'noChange'; const enableOptions = [ - { key: NO_CHANGE, value: translate('NoChange'), disabled: true }, - { key: 'enabled', value: translate('Enabled') }, - { key: 'disabled', value: translate('Disabled') }, + { + key: NO_CHANGE, + get value() { + return translate('NoChange'); + }, + disabled: true, + }, + { + key: 'enabled', + get value() { + return translate('Enabled'); + }, + }, + { + key: 'disabled', + get value() { + return translate('Disabled'); + }, + }, ]; function ManageDownloadClientsEditModalContent( diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index dbf4c4fec..a783ef822 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -36,37 +36,49 @@ type OnSelectedChangeCallback = React.ComponentProps< const COLUMNS = [ { name: 'name', - label: translate('Name'), + get label() { + return translate('Name'); + }, isSortable: true, isVisible: true, }, { name: 'implementation', - label: translate('Implementation'), + get label() { + return translate('Implementation'); + }, isSortable: true, isVisible: true, }, { name: 'enable', - label: translate('Enabled'), + get label() { + return translate('Enabled'); + }, isSortable: true, isVisible: true, }, { name: 'priority', - label: translate('Priority'), + get label() { + return translate('Priority'); + }, isSortable: true, isVisible: true, }, { name: 'removeCompletedDownloads', - label: translate('RemoveCompleted'), + get label() { + return translate('RemoveCompleted'); + }, isSortable: true, isVisible: true, }, { name: 'removeFailedDownloads', - label: translate('RemoveFailed'), + get label() { + return translate('RemoveFailed'); + }, isSortable: true, isVisible: true, }, diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.tsx index 98f33c109..e0899ff39 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.tsx @@ -72,9 +72,24 @@ function TagsModalContent(props: TagsModalContentProps) { }, [tags, applyTags, onApplyTagsPress]); const applyTagsOptions = [ - { key: 'add', value: translate('Add') }, - { key: 'remove', value: translate('Remove') }, - { key: 'replace', value: translate('Replace') }, + { + key: 'add', + get value() { + return translate('Add'); + }, + }, + { + key: 'remove', + get value() { + return translate('Remove'); + }, + }, + { + key: 'replace', + get value() { + return translate('Replace'); + }, + }, ]; return ( diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx index 94b3bc8ce..798af88b9 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx @@ -27,9 +27,25 @@ interface ManageImportListsEditModalContentProps { const NO_CHANGE = 'noChange'; const autoAddOptions = [ - { key: NO_CHANGE, value: translate('NoChange'), disabled: true }, - { key: 'enabled', value: translate('Enabled') }, - { key: 'disabled', value: translate('Disabled') }, + { + key: NO_CHANGE, + get value() { + return translate('NoChange'); + }, + disabled: true, + }, + { + key: 'enabled', + get value() { + return translate('Enabled'); + }, + }, + { + key: 'disabled', + get value() { + return translate('Disabled'); + }, + }, ]; function ManageImportListsEditModalContent( diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx index a3ad51fba..8bec6ee76 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx @@ -36,19 +36,25 @@ type OnSelectedChangeCallback = React.ComponentProps< const COLUMNS = [ { name: 'name', - label: translate('Name'), + get label() { + return translate('Name'); + }, isSortable: true, isVisible: true, }, { name: 'implementation', - label: translate('Implementation'), + get label() { + return translate('Implementation'); + }, isSortable: true, isVisible: true, }, { name: 'qualityProfileId', - label: translate('QualityProfile'), + get label() { + return translate('QualityProfile'); + }, isSortable: true, isVisible: true, }, @@ -60,19 +66,25 @@ const COLUMNS = [ }, { name: 'rootFolderPath', - label: translate('RootFolder'), + get label() { + return translate('RootFolder'); + }, isSortable: true, isVisible: true, }, { name: 'enableAutomaticAdd', - label: translate('AutoAdd'), + get label() { + return translate('AutoAdd'); + }, isSortable: true, isVisible: true, }, { name: 'tags', - label: translate('Tags'), + get label() { + return translate('Tags'); + }, isSortable: true, isVisible: true, }, diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/Tags/TagsModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/Tags/TagsModalContent.tsx index 6072be5ff..9d4af820e 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/Tags/TagsModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/Tags/TagsModalContent.tsx @@ -70,9 +70,24 @@ function TagsModalContent(props: TagsModalContentProps) { }, [tags, applyTags, onApplyTagsPress]); const applyTagsOptions = [ - { key: 'add', value: translate('Add') }, - { key: 'remove', value: translate('Remove') }, - { key: 'replace', value: translate('Replace') }, + { + key: 'add', + get value() { + return translate('Add'); + }, + }, + { + key: 'remove', + get value() { + return translate('Remove'); + }, + }, + { + key: 'replace', + get value() { + return translate('Replace'); + }, + }, ]; return ( diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx index b6e9804a1..286a0b0cf 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx @@ -27,9 +27,25 @@ interface ManageIndexersEditModalContentProps { const NO_CHANGE = 'noChange'; const enableOptions = [ - { key: NO_CHANGE, value: translate('NoChange'), disabled: true }, - { key: 'enabled', value: translate('Enabled') }, - { key: 'disabled', value: translate('Disabled') }, + { + key: NO_CHANGE, + get value() { + return translate('NoChange'); + }, + disabled: true, + }, + { + key: 'enabled', + get value() { + return translate('Enabled'); + }, + }, + { + key: 'disabled', + get value() { + return translate('Disabled'); + }, + }, ]; function ManageIndexersEditModalContent( diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx index 850ca2331..7b577d345 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx @@ -36,43 +36,57 @@ type OnSelectedChangeCallback = React.ComponentProps< const COLUMNS = [ { name: 'name', - label: translate('Name'), + get label() { + return translate('Name'); + }, isSortable: true, isVisible: true, }, { name: 'implementation', - label: translate('Implementation'), + get label() { + return translate('Implementation'); + }, isSortable: true, isVisible: true, }, { name: 'enableRss', - label: translate('EnableRSS'), + get label() { + return translate('EnableRSS'); + }, isSortable: true, isVisible: true, }, { name: 'enableAutomaticSearch', - label: translate('EnableAutomaticSearch'), + get label() { + return translate('EnableAutomaticSearch'); + }, isSortable: true, isVisible: true, }, { name: 'enableInteractiveSearch', - label: translate('EnableInteractiveSearch'), + get label() { + return translate('EnableInteractiveSearch'); + }, isSortable: true, isVisible: true, }, { name: 'priority', - label: translate('Priority'), + get label() { + return translate('Priority'); + }, isSortable: true, isVisible: true, }, { name: 'tags', - label: translate('Tags'), + get label() { + return translate('Tags'); + }, isSortable: true, isVisible: true, }, diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx index 32138fb5f..fb1e6b847 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx @@ -70,9 +70,24 @@ function TagsModalContent(props: TagsModalContentProps) { }, [tags, applyTags, onApplyTagsPress]); const applyTagsOptions = [ - { key: 'add', value: translate('Add') }, - { key: 'remove', value: translate('Remove') }, - { key: 'replace', value: translate('Replace') }, + { + key: 'add', + get value() { + return translate('Add'); + }, + }, + { + key: 'remove', + get value() { + return translate('Remove'); + }, + }, + { + key: 'replace', + get value() { + return translate('Replace'); + }, + }, ]; return ( diff --git a/frontend/src/Store/Actions/appActions.js b/frontend/src/Store/Actions/appActions.js index 30ad5e01f..1be49613c 100644 --- a/frontend/src/Store/Actions/appActions.js +++ b/frontend/src/Store/Actions/appActions.js @@ -4,6 +4,7 @@ import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import getSectionState from 'Utilities/State/getSectionState'; import updateSectionState from 'Utilities/State/updateSectionState'; +import { fetchTranslations as fetchAppTranslations } from 'Utilities/String/translate'; import createHandleActions from './Creators/createHandleActions'; function getDimensions(width, height) { @@ -41,7 +42,12 @@ export const defaultState = { isReconnecting: false, isDisconnected: false, isRestarting: false, - isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen + isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen, + translations: { + isFetching: true, + isPopulated: false, + error: null + } }; // @@ -53,6 +59,7 @@ export const SAVE_DIMENSIONS = 'app/saveDimensions'; export const SET_VERSION = 'app/setVersion'; export const SET_APP_VALUE = 'app/setAppValue'; export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible'; +export const FETCH_TRANSLATIONS = 'app/fetchTranslations'; export const PING_SERVER = 'app/pingServer'; @@ -66,6 +73,7 @@ export const setAppValue = createAction(SET_APP_VALUE); export const showMessage = createAction(SHOW_MESSAGE); export const hideMessage = createAction(HIDE_MESSAGE); export const pingServer = createThunk(PING_SERVER); +export const fetchTranslations = createThunk(FETCH_TRANSLATIONS); // // Helpers @@ -127,6 +135,17 @@ function pingServerAfterTimeout(getState, dispatch) { export const actionHandlers = handleThunks({ [PING_SERVER]: function(getState, payload, dispatch) { pingServerAfterTimeout(getState, dispatch); + }, + [FETCH_TRANSLATIONS]: async function(getState, payload, dispatch) { + const isFetchingComplete = await fetchAppTranslations(); + + dispatch(setAppValue({ + translations: { + isFetching: false, + isPopulated: isFetchingComplete, + error: isFetchingComplete ? null : 'Failed to load translations from API' + } + })); } }); diff --git a/frontend/src/Store/Actions/bookActions.js b/frontend/src/Store/Actions/bookActions.js index a441a3a8b..3aa49cfae 100644 --- a/frontend/src/Store/Actions/bookActions.js +++ b/frontend/src/Store/Actions/bookActions.js @@ -24,12 +24,12 @@ export const section = 'books'; export const filters = [ { key: 'all', - label: translate('All'), + label: () => translate('All'), filters: [] }, { key: 'monitored', - label: translate('Monitored'), + label: () => translate('Monitored'), filters: [ { key: 'monitored', @@ -40,7 +40,7 @@ export const filters = [ }, { key: 'unmonitored', - label: translate('Unmonitored'), + label: () => translate('Unmonitored'), filters: [ { key: 'monitored', @@ -51,7 +51,7 @@ export const filters = [ }, { key: 'missing', - label: translate('Missing'), + label: () => translate('Missing'), filters: [ { key: 'monitored', @@ -67,7 +67,7 @@ export const filters = [ }, { key: 'wanted', - label: translate('Wanted'), + label: () => translate('Wanted'), filters: [ { key: 'monitored', diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index a8b0e00de..6938e9dcb 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -60,32 +60,32 @@ export const defaultState = { columns: [ { name: 'status', - columnLabel: translate('Status'), + columnLabel: () => translate('Status'), isSortable: true, isVisible: true, isModifiable: false }, { name: 'authorMetadata.sortName', - label: translate('Author'), + label: () => translate('Author'), isSortable: true, isVisible: true }, { name: 'books.title', - label: translate('BookTitle'), + label: () => translate('BookTitle'), isSortable: true, isVisible: true }, { name: 'books.releaseDate', - label: translate('ReleaseDate'), + label: () => translate('ReleaseDate'), isSortable: true, isVisible: false }, { name: 'quality', - label: translate('Quality'), + label: () => translate('Quality'), isSortable: true, isVisible: true }, @@ -97,64 +97,64 @@ export const defaultState = { }, { name: 'customFormatScore', - columnLabel: translate('CustomFormatScore'), + columnLabel: () => translate('CustomFormatScore'), label: React.createElement(Icon, { name: icons.SCORE, - title: translate('CustomFormatScore') + title: () => translate('CustomFormatScore') }), isVisible: false }, { name: 'protocol', - label: translate('Protocol'), + label: () => translate('Protocol'), isSortable: true, isVisible: false }, { name: 'indexer', - label: translate('Indexer'), + label: () => translate('Indexer'), isSortable: true, isVisible: false }, { name: 'downloadClient', - label: translate('DownloadClient'), + label: () => translate('DownloadClient'), isSortable: true, isVisible: false }, { name: 'title', - label: translate('ReleaseTitle'), + label: () => translate('ReleaseTitle'), isSortable: true, isVisible: false }, { name: 'size', - label: translate('Size'), + label: () => translate('Size'), isSortable: true, isVisible: false }, { name: 'outputPath', - label: translate('OutputPath'), + label: () => translate('OutputPath'), isSortable: false, isVisible: false }, { name: 'estimatedCompletionTime', - label: translate('TimeLeft'), + label: () => translate('TimeLeft'), isSortable: true, isVisible: true }, { name: 'progress', - label: translate('Progress'), + label: () => translate('Progress'), isSortable: true, isVisible: true }, { name: 'actions', - columnLabel: translate('Actions'), + columnLabel: () => translate('Actions'), isVisible: true, isModifiable: false } diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index e5fb0a318..07a4001bb 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -199,7 +199,7 @@ export const defaultState = { }, { name: 'customFormatScore', - label: translate('CustomFormatScore'), + label: () => translate('CustomFormatScore'), type: filterBuilderTypes.NUMBER }, { diff --git a/frontend/src/Store/Actions/systemActions.js b/frontend/src/Store/Actions/systemActions.js index eac93e69c..fefa734ad 100644 --- a/frontend/src/Store/Actions/systemActions.js +++ b/frontend/src/Store/Actions/systemActions.js @@ -82,34 +82,34 @@ export const defaultState = { columns: [ { name: 'level', - columnLabel: translate('Level'), + columnLabel: () => translate('Level'), isSortable: false, isVisible: true, isModifiable: false }, { name: 'time', - label: translate('Time'), + label: () => translate('Time'), isSortable: true, isVisible: true, isModifiable: false }, { name: 'logger', - label: translate('Component'), + label: () => translate('Component'), isSortable: false, isVisible: true, isModifiable: false }, { name: 'message', - label: translate('Message'), + label: () => translate('Message'), isVisible: true, isModifiable: false }, { name: 'actions', - columnLabel: translate('Actions'), + columnLabel: () => translate('Actions'), isSortable: true, isVisible: true, isModifiable: false diff --git a/frontend/src/Utilities/String/translate.js b/frontend/src/Utilities/String/translate.ts similarity index 62% rename from frontend/src/Utilities/String/translate.js rename to frontend/src/Utilities/String/translate.ts index fb0ec0cb5..9c6fa778b 100644 --- a/frontend/src/Utilities/String/translate.js +++ b/frontend/src/Utilities/String/translate.ts @@ -4,14 +4,14 @@ function getTranslations() { return createAjaxRequest({ global: false, dataType: 'json', - url: '/localization' + url: '/localization', }).request; } -let translations = {}; +let translations: Record = {}; -export function fetchTranslations() { - return new Promise(async(resolve) => { +export async function fetchTranslations(): Promise { + return new Promise(async (resolve) => { try { const data = await getTranslations(); translations = data.Strings; @@ -23,12 +23,15 @@ export function fetchTranslations() { }); } -export default function translate(key, args = []) { +export default function translate( + key: string, + args?: (string | number | boolean)[] +) { const translation = translations[key] || key; if (args) { return translation.replace(/\{(\d+)\}/g, (match, index) => { - return args[index]; + return String(args[index]) ?? match; }); } diff --git a/frontend/src/bootstrap.tsx b/frontend/src/bootstrap.tsx index c07e581b5..5e9985ba3 100644 --- a/frontend/src/bootstrap.tsx +++ b/frontend/src/bootstrap.tsx @@ -2,20 +2,14 @@ import { createBrowserHistory } from 'history'; import React from 'react'; import { render } from 'react-dom'; import createAppStore from 'Store/createAppStore'; -import { fetchTranslations } from 'Utilities/String/translate'; import App from './App/App'; export async function bootstrap() { const history = createBrowserHistory(); const store = createAppStore(history); - const hasTranslationsError = !(await fetchTranslations()); render( - , + , document.getElementById('root') ); } diff --git a/src/Readarr.Http/Frontend/InitializeJsonController.cs b/src/Readarr.Http/Frontend/InitializeJsonController.cs index bfb283815..33e766ace 100644 --- a/src/Readarr.Http/Frontend/InitializeJsonController.cs +++ b/src/Readarr.Http/Frontend/InitializeJsonController.cs @@ -10,7 +10,8 @@ namespace Readarr.Http.Frontend { [Authorize(Policy = "UI")] [ApiController] - public class InitializeJsController : Controller + [ApiExplorerSettings(IgnoreApi = true)] + public class InitializeJsonController : Controller { private readonly IConfigFileProvider _configFileProvider; private readonly IAnalyticsService _analyticsService; @@ -19,7 +20,7 @@ public class InitializeJsController : Controller private static string _urlBase; private string _generatedContent; - public InitializeJsController(IConfigFileProvider configFileProvider, + public InitializeJsonController(IConfigFileProvider configFileProvider, IAnalyticsService analyticsService) { _configFileProvider = configFileProvider; @@ -29,11 +30,10 @@ public InitializeJsController(IConfigFileProvider configFileProvider, _urlBase = configFileProvider.UrlBase; } - [HttpGet("/initialize.js")] + [HttpGet("/initialize.json")] public IActionResult Index() { - // TODO: Move away from window.Readarr and prefetch the information returned here when starting the UI - return Content(GetContent(), "application/javascript"); + return Content(GetContent(), "application/json"); } private string GetContent() @@ -44,19 +44,19 @@ private string GetContent() } var builder = new StringBuilder(); - builder.AppendLine("window.Readarr = {"); - builder.AppendLine($" apiRoot: '{_urlBase}/api/v1',"); - builder.AppendLine($" apiKey: '{_apiKey}',"); - builder.AppendLine($" release: '{BuildInfo.Release}',"); - builder.AppendLine($" version: '{BuildInfo.Version.ToString()}',"); - builder.AppendLine($" instanceName: '{_configFileProvider.InstanceName.ToString()}',"); - builder.AppendLine($" theme: '{_configFileProvider.Theme.ToString()}',"); - builder.AppendLine($" branch: '{_configFileProvider.Branch.ToLower()}',"); - builder.AppendLine($" analytics: {_analyticsService.IsEnabled.ToString().ToLowerInvariant()},"); - builder.AppendLine($" userHash: '{HashUtil.AnonymousToken()}',"); - builder.AppendLine($" urlBase: '{_urlBase}',"); - builder.AppendLine($" isProduction: {RuntimeInfo.IsProduction.ToString().ToLowerInvariant()}"); - builder.AppendLine("};"); + builder.AppendLine("{"); + builder.AppendLine($" \"apiRoot\": \"{_urlBase}/api/v1\","); + builder.AppendLine($" \"apiKey\": \"{_apiKey}\","); + builder.AppendLine($" \"release\": \"{BuildInfo.Release}\","); + builder.AppendLine($" \"version\": \"{BuildInfo.Version.ToString()}\","); + builder.AppendLine($" \"instanceName\": \"{_configFileProvider.InstanceName.ToString()}\","); + builder.AppendLine($" \"theme\": \"{_configFileProvider.Theme.ToString()}\","); + builder.AppendLine($" \"branch\": \"{_configFileProvider.Branch.ToLower()}\","); + builder.AppendLine($" \"analytics\": {_analyticsService.IsEnabled.ToString().ToLowerInvariant()},"); + builder.AppendLine($" \"userHash\": \"{HashUtil.AnonymousToken()}\","); + builder.AppendLine($" \"urlBase\": \"{_urlBase}\","); + builder.AppendLine($" \"isProduction\": {RuntimeInfo.IsProduction.ToString().ToLowerInvariant()}"); + builder.AppendLine("}"); _generatedContent = builder.ToString();