Fixed: Improve translation loading

(cherry picked from commit 73c5ec1da4dd00301e1b0dddbcea37590a99b045)
This commit is contained in:
Mark McDowall 2023-07-18 20:20:27 -07:00 committed by Bogdan
parent e691253419
commit ed5c063127
16 changed files with 315 additions and 99 deletions

View File

@ -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 (
<DocumentTitle title={window.Radarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme>
<PageConnector hasTranslationsError={hasTranslationsError}>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ApplyTheme>
@ -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;

View File

@ -7,7 +7,7 @@ function ErrorPage(props) {
const {
version,
isLocalStorageSupported,
hasTranslationsError,
translationsError,
moviesError,
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 (moviesError) {
errorMessage = getErrorMessage(moviesError, 'Failed to load movie 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,
moviesError: PropTypes.object,
customFiltersError: PropTypes.object,
tagsError: PropTypes.object,

View File

@ -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 { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchMovies } from 'Store/Actions/movieActions';
import { fetchMovieCollections } from 'Store/Actions/movieCollectionActions';
@ -53,6 +53,7 @@ const selectIsPopulated = createSelector(
(state) => state.settings.importLists.isPopulated,
(state) => state.system.status.isPopulated,
(state) => state.movieCollections.isPopulated,
(state) => state.app.translations.isPopulated,
(
customFiltersIsPopulated,
tagsIsPopulated,
@ -62,7 +63,8 @@ const selectIsPopulated = createSelector(
indexerFlagsIsPopulated,
importListsIsPopulated,
systemStatusIsPopulated,
movieCollectionsIsPopulated
movieCollectionsIsPopulated,
translationsIsPopulated
) => {
return (
customFiltersIsPopulated &&
@ -73,7 +75,8 @@ const selectIsPopulated = createSelector(
indexerFlagsIsPopulated &&
importListsIsPopulated &&
systemStatusIsPopulated &&
movieCollectionsIsPopulated
movieCollectionsIsPopulated &&
translationsIsPopulated
);
}
);
@ -88,6 +91,7 @@ const selectErrors = createSelector(
(state) => state.settings.importLists.error,
(state) => state.system.status.error,
(state) => state.movieCollections.error,
(state) => state.app.translations.error,
(
customFiltersError,
tagsError,
@ -97,7 +101,8 @@ const selectErrors = createSelector(
indexerFlagsError,
importListsError,
systemStatusError,
movieCollectionsError
movieCollectionsError,
translationsError
) => {
const hasError = !!(
customFiltersError ||
@ -108,7 +113,8 @@ const selectErrors = createSelector(
indexerFlagsError ||
importListsError ||
systemStatusError ||
movieCollectionsError
movieCollectionsError ||
translationsError
);
return {
@ -121,7 +127,8 @@ const selectErrors = createSelector(
indexerFlagsError,
importListsError,
systemStatusError,
movieCollectionsError
movieCollectionsError,
translationsError
};
}
);
@ -183,6 +190,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchStatus() {
dispatch(fetchStatus());
},
dispatchFetchTranslations() {
dispatch(fetchTranslations());
},
onResize(dimensions) {
dispatch(saveDimensions(dimensions));
},
@ -217,6 +227,7 @@ class PageConnector extends Component {
this.props.dispatchFetchImportLists();
this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
}
}
@ -232,7 +243,6 @@ class PageConnector extends Component {
render() {
const {
hasTranslationsError,
isPopulated,
hasError,
dispatchFetchMovies,
@ -244,15 +254,15 @@ class PageConnector extends Component {
dispatchFetchImportLists,
dispatchFetchUISettings,
dispatchFetchStatus,
dispatchFetchTranslations,
...otherProps
} = this.props;
if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) {
if (hasError || !this.state.isLocalStorageSupported) {
return (
<ErrorPage
{...this.state}
{...otherProps}
hasTranslationsError={hasTranslationsError}
/>
);
}
@ -273,7 +283,6 @@ class PageConnector extends Component {
}
PageConnector.propTypes = {
hasTranslationsError: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired,
@ -287,6 +296,7 @@ PageConnector.propTypes = {
dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired
};

View File

@ -21,24 +21,34 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [
{
iconName: icons.MOVIE_CONTINUING,
title: translate('Movies'),
get title() {
return translate('Movies');
},
to: '/',
alias: '/movies',
children: [
{
title: translate('AddNew'),
get title() {
return translate('AddNew');
},
to: '/add/new'
},
{
title: translate('ImportLibrary'),
get title() {
return translate('ImportLibrary');
},
to: '/add/import'
},
{
title: translate('Collections'),
get title() {
return translate('Collections');
},
to: '/collections'
},
{
title: translate('Discover'),
get title() {
return translate('Discover');
},
to: '/add/discover'
}
]
@ -46,26 +56,36 @@ const links = [
{
iconName: icons.CALENDAR,
title: translate('Calendar'),
get title() {
return translate('Calendar');
},
to: '/calendar'
},
{
iconName: icons.ACTIVITY,
title: translate('Activity'),
get title() {
return translate('Activity');
},
to: '/activity/queue',
children: [
{
title: translate('Queue'),
get title() {
return translate('Queue');
},
to: '/activity/queue',
statusComponent: QueueStatusConnector
},
{
title: translate('History'),
get title() {
return translate('History');
},
to: '/activity/history'
},
{
title: translate('Blocklist'),
get title() {
return translate('Blocklist');
},
to: '/activity/blocklist'
}
]
@ -73,55 +93,81 @@ const links = [
{
iconName: icons.SETTINGS,
title: translate('Settings'),
get title() {
return translate('Settings');
},
to: '/settings',
children: [
{
title: translate('MediaManagement'),
get title() {
return translate('MediaManagement');
},
to: '/settings/mediamanagement'
},
{
title: translate('Profiles'),
get title() {
return translate('Profiles');
},
to: '/settings/profiles'
},
{
title: translate('Quality'),
get title() {
return translate('Quality');
},
to: '/settings/quality'
},
{
title: translate('CustomFormats'),
get title() {
return translate('CustomFormats');
},
to: '/settings/customformats'
},
{
title: translate('Indexers'),
get title() {
return translate('Indexers');
},
to: '/settings/indexers'
},
{
title: translate('DownloadClients'),
get title() {
return translate('DownloadClients');
},
to: '/settings/downloadclients'
},
{
title: translate('Lists'),
get title() {
return translate('Lists');
},
to: '/settings/importlists'
},
{
title: translate('Connect'),
get title() {
return translate('Connect');
},
to: '/settings/connect'
},
{
title: translate('Metadata'),
get title() {
return translate('Metadata');
},
to: '/settings/metadata'
},
{
title: translate('Tags'),
get title() {
return translate('Tags');
},
to: '/settings/tags'
},
{
title: translate('General'),
get title() {
return translate('General');
},
to: '/settings/general'
},
{
title: translate('UI'),
get title() {
return translate('UI');
},
to: '/settings/ui'
}
]
@ -129,32 +175,46 @@ const links = [
{
iconName: icons.SYSTEM,
title: translate('System'),
get title() {
return translate('System');
},
to: '/system/status',
children: [
{
title: translate('Status'),
get title() {
return translate('Status');
},
to: '/system/status',
statusComponent: HealthStatusConnector
},
{
title: translate('Tasks'),
get title() {
return translate('Tasks');
},
to: '/system/tasks'
},
{
title: translate('Backup'),
get title() {
return translate('Backup');
},
to: '/system/backup'
},
{
title: translate('Updates'),
get title() {
return translate('Updates');
},
to: '/system/updates'
},
{
title: translate('Events'),
get title() {
return translate('Events');
},
to: '/system/events'
},
{
title: translate('LogFiles'),
get title() {
return translate('LogFiles');
},
to: '/system/logs/files'
}
]

View File

@ -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(

View File

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

View File

@ -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 (

View File

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

View File

@ -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: 'qualityProfileId',
label: translate('QualityProfile'),
get label() {
return translate('QualityProfile');
},
isSortable: true,
isVisible: true,
},
{
name: 'rootFolderPath',
label: translate('RootFolder'),
get label() {
return translate('RootFolder');
},
isSortable: true,
isVisible: true,
},
{
name: 'enableAuto',
label: translate('AutomaticAdd'),
get label() {
return translate('AutomaticAdd');
},
isSortable: true,
isVisible: true,
},
{
name: 'tags',
label: translate('Tags'),
get label() {
return translate('Tags');
},
isSortable: true,
isVisible: true,
},

View File

@ -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 (

View File

@ -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(

View File

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

View File

@ -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 (

View File

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

View File

@ -4,14 +4,14 @@ function getTranslations() {
return createAjaxRequest({
global: false,
dataType: 'json',
url: '/localization'
url: '/localization',
}).request;
}
let translations = {};
let translations: Record<string, string> = {};
export function fetchTranslations() {
return new Promise(async(resolve) => {
export async function fetchTranslations(): Promise<boolean> {
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;
});
}

View File

@ -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(
<App
store={store}
history={history}
hasTranslationsError={hasTranslationsError}
/>,
<App store={store} history={history} />,
document.getElementById('root')
);
}