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