diff --git a/frontend/src/Components/Error/ErrorBoundaryError.tsx b/frontend/src/Components/Error/ErrorBoundaryError.tsx index 42510e5c3..14bd8a87f 100644 --- a/frontend/src/Components/Error/ErrorBoundaryError.tsx +++ b/frontend/src/Components/Error/ErrorBoundaryError.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import StackTrace from 'stacktrace-js'; +import translate from 'Utilities/String/translate'; import styles from './ErrorBoundaryError.css'; interface ErrorBoundaryErrorProps { @@ -18,7 +19,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) { className = styles.container, messageClassName = styles.message, detailsClassName = styles.details, - message = 'There was an error loading this content', + message = translate('ErrorLoadingContent'), error, info, } = props; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js index f285ed529..56b364b56 100644 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js @@ -3,7 +3,6 @@ import React, { Component } from 'react'; import Alert from 'Components/Alert'; import PathInput from 'Components/Form/PathInput'; import Button from 'Components/Link/Button'; -import Link from 'Components/Link/Link'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; @@ -14,6 +13,7 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import { kinds, scrollDirections } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; +import InlineMarkdown from '../Markdown/InlineMarkdown'; import FileBrowserRow from './FileBrowserRow'; import styles from './FileBrowserModalContent.css'; @@ -104,7 +104,7 @@ class FileBrowserModalContent extends Component { onModalClose={onModalClose} > - File Browser + {translate('FileBrowser')} - Mapped network drives are not available when running as a Windows Service, see the FAQ for more information. + } { !!error && - Error loading contents + {translate('ErrorLoadingContents')} } { @@ -151,7 +151,7 @@ class FileBrowserModalContent extends Component { emptyParent && @@ -212,13 +212,13 @@ class FileBrowserModalContent extends Component { - Cancel + {translate('Cancel')} - Ok + {translate('Ok')} diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js index 6b5846594..9492f4577 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import TagInputTag from 'Components/Form/TagInputTag'; import { kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import styles from './FilterBuilderRowValueTag.css'; function FilterBuilderRowValueTag(props) { @@ -18,7 +19,7 @@ function FilterBuilderRowValueTag(props) { props.isLastTag ? null : - or + {translate('or')} } diff --git a/frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.js index dab67a645..3464300f1 100644 --- a/frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.js +++ b/frontend/src/Components/Filter/Builder/SeriesStatusFilterBuilderRowValue.js @@ -1,11 +1,32 @@ import React from 'react'; +import translate from 'Utilities/String/translate'; import FilterBuilderRowValue from './FilterBuilderRowValue'; const seriesStatusList = [ - { id: 'continuing', name: 'Continuing' }, - { id: 'upcoming', name: 'Upcoming' }, - { id: 'ended', name: 'Ended' }, - { id: 'deleted', name: 'Deleted' } + { + id: 'continuing', + get name() { + return translate('Continuing'); + } + }, + { + id: 'upcoming', + get name() { + return translate('Upcoming'); + } + }, + { + id: 'ended', + get name() { + return translate('Ended'); + } + }, + { + id: 'deleted', + get name() { + return translate('Deleted'); + } + } ]; function SeriesStatusFilterBuilderRowValue(props) { diff --git a/frontend/src/Components/Filter/Builder/SeriesTypeFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/SeriesTypeFilterBuilderRowValue.js index 263c9e9da..2e62e558d 100644 --- a/frontend/src/Components/Filter/Builder/SeriesTypeFilterBuilderRowValue.js +++ b/frontend/src/Components/Filter/Builder/SeriesTypeFilterBuilderRowValue.js @@ -1,10 +1,26 @@ import React from 'react'; +import translate from 'Utilities/String/translate'; import FilterBuilderRowValue from './FilterBuilderRowValue'; const seriesTypeList = [ - { id: 'anime', name: 'Anime' }, - { id: 'daily', name: 'Daily' }, - { id: 'standard', name: 'Standard' } + { + id: 'anime', + get name() { + return translate('Anime'); + } + }, + { + id: 'daily', + get name() { + return translate('Daily'); + } + }, + { + id: 'standard', + get name() { + return translate('Standard'); + } + } ]; function SeriesTypeFilterBuilderRowValue(props) { diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js index e87d088b3..7407f729a 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import IconButton from 'Components/Link/IconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import styles from './CustomFilter.css'; class CustomFilter extends Component { @@ -89,7 +90,7 @@ class CustomFilter extends Component { /> - Custom Filters + {translate('CustomFilters')} @@ -49,7 +50,7 @@ function CustomFiltersModalContent(props) { - Add Custom Filter + {translate('AddCustomFilter')} @@ -58,7 +59,7 @@ function CustomFiltersModalContent(props) { - Close + {translate('Close')} diff --git a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js index c89016869..f0ebf534b 100644 --- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js +++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchDownloadClients } from 'Store/Actions/settingsActions'; import sortByName from 'Utilities/Array/sortByName'; +import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { @@ -32,7 +33,7 @@ function createMapStateToProps() { if (includeAny) { values.unshift({ key: 0, - value: '(Any)' + value: `(${translate('Any')})` }); } diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 805b97217..6f3155f5b 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Link from 'Components/Link/Link'; import { inputTypes, kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import AutoCompleteInput from './AutoCompleteInput'; import CaptchaInputConnector from './CaptchaInputConnector'; import CheckInput from './CheckInput'; @@ -215,7 +216,7 @@ function FormInputGroup(props) { - More Info + {translate('MoreInfo')} } diff --git a/frontend/src/Components/Form/IndexerSelectInputConnector.js b/frontend/src/Components/Form/IndexerSelectInputConnector.js index cd58270eb..91c31198f 100644 --- a/frontend/src/Components/Form/IndexerSelectInputConnector.js +++ b/frontend/src/Components/Form/IndexerSelectInputConnector.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchIndexers } from 'Store/Actions/settingsActions'; import sortByName from 'Utilities/Array/sortByName'; +import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { @@ -29,7 +30,7 @@ function createMapStateToProps() { if (includeAny) { values.unshift({ key: 0, - value: '(Any)' + value: `(${translate('Any')})` }); } diff --git a/frontend/src/Components/Form/MonitorEpisodesSelectInput.js b/frontend/src/Components/Form/MonitorEpisodesSelectInput.js index f26693e64..9b80cc587 100644 --- a/frontend/src/Components/Form/MonitorEpisodesSelectInput.js +++ b/frontend/src/Components/Form/MonitorEpisodesSelectInput.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import monitorOptions from 'Utilities/Series/monitorOptions'; +import translate from 'Utilities/String/translate'; import SelectInput from './SelectInput'; function MonitorEpisodesSelectInput(props) { @@ -15,7 +16,9 @@ function MonitorEpisodesSelectInput(props) { if (includeNoChange) { values.unshift({ key: 'noChange', - value: 'No Change', + get value() { + return translate('NoChange'); + }, disabled: true }); } @@ -23,7 +26,9 @@ function MonitorEpisodesSelectInput(props) { if (includeMixed) { values.unshift({ key: 'mixed', - value: '(Mixed)', + get value() { + return `(${translate('Mixed')})`; + }, disabled: true }); } diff --git a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js index 2304a0d67..cc8ffbdb8 100644 --- a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import sortByName from 'Utilities/Array/sortByName'; +import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { @@ -24,7 +25,9 @@ function createMapStateToProps() { if (includeNoChange) { values.unshift({ key: 'noChange', - value: 'No Change', + get value() { + return translate('NoChange'); + }, disabled: includeNoChangeDisabled }); } @@ -32,7 +35,9 @@ function createMapStateToProps() { if (includeMixed) { values.unshift({ key: 'mixed', - value: '(Mixed)', + get value() { + return `(${translate('Mixed')})`; + }, disabled: true }); } diff --git a/frontend/src/Components/Form/RootFolderSelectInputConnector.js b/frontend/src/Components/Form/RootFolderSelectInputConnector.js index 11a00841c..dc5930417 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputConnector.js +++ b/frontend/src/Components/Form/RootFolderSelectInputConnector.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { addRootFolder } from 'Store/Actions/rootFolderActions'; +import translate from 'Utilities/String/translate'; import RootFolderSelectInput from './RootFolderSelectInput'; const ADD_NEW_KEY = 'addNew'; @@ -27,7 +28,9 @@ function createMapStateToProps() { if (includeNoChange) { values.unshift({ key: 'noChange', - value: 'No Change', + get value() { + return translate('NoChange'); + }, isDisabled: includeNoChangeDisabled, isMissing: false }); @@ -53,7 +56,7 @@ function createMapStateToProps() { values.push({ key: ADD_NEW_KEY, - value: 'Add a new path' + value: translate('AddANewPath') }); return { diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.js b/frontend/src/Components/Form/RootFolderSelectInputOption.js index 3197ba875..daac82f34 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputOption.js +++ b/frontend/src/Components/Form/RootFolderSelectInputOption.js @@ -2,6 +2,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; import EnhancedSelectInputOption from './EnhancedSelectInputOption'; import styles from './RootFolderSelectInputOption.css'; @@ -47,14 +48,14 @@ function RootFolderSelectInputOption(props) { freeSpace == null ? null : - {formatBytes(freeSpace)} Free + {translate('RootFolderSelectFreeSpace', { freeSpace: formatBytes(freeSpace) })} } { isMissing ? - Missing + {translate('Missing')} : null } diff --git a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js index 69b1453f3..1c3a4fc9d 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js +++ b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue'; import styles from './RootFolderSelectInputSelectedValue.css'; @@ -39,7 +40,7 @@ function RootFolderSelectInputSelectedValue(props) { { freeSpace != null && includeFreeSpace && - {formatBytes(freeSpace)} Free + {translate('RootFolderSelectFreeSpace', { freeSpace: formatBytes(freeSpace) })} } diff --git a/frontend/src/Components/Form/SeriesTypeSelectInput.tsx b/frontend/src/Components/Form/SeriesTypeSelectInput.tsx index d996e5b34..72f0ed9ac 100644 --- a/frontend/src/Components/Form/SeriesTypeSelectInput.tsx +++ b/frontend/src/Components/Form/SeriesTypeSelectInput.tsx @@ -1,5 +1,6 @@ import React from 'react'; import * as seriesTypes from 'Utilities/Series/seriesTypes'; +import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; import SeriesTypeSelectInputOption from './SeriesTypeSelectInputOption'; import SeriesTypeSelectInputSelectedValue from './SeriesTypeSelectInputSelectedValue'; @@ -21,17 +22,23 @@ const seriesTypeOptions: ISeriesTypeOption[] = [ { key: seriesTypes.STANDARD, value: 'Standard', - format: 'Season and episode numbers (S01E05)', + get format() { + return translate('StandardTypeFormat', { format: 'S01E05' }); + }, }, { key: seriesTypes.DAILY, value: 'Daily / Date', - format: 'Date (2020-05-25)', + get format() { + return translate('DailyTypeFormat', { format: '2020-05-25' }); + }, }, { key: seriesTypes.ANIME, value: 'Anime / Absolute', - format: 'Absolute episode Number (005)', + get format() { + return translate('AnimeTypeFormat', { format: '005' }); + }, }, ]; @@ -47,7 +54,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) { if (includeNoChange) { values.unshift({ key: 'noChange', - value: 'No Change', + value: translate('NoChange'), disabled: includeNoChangeDisabled, }); } @@ -55,7 +62,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) { if (includeMixed) { values.unshift({ key: 'mixed', - value: '(Mixed)', + value: `(${translate('Mixed')})`, disabled: true, }); } diff --git a/frontend/src/Components/Form/UMaskInput.js b/frontend/src/Components/Form/UMaskInput.js index 22f51c8fc..adec30c93 100644 --- a/frontend/src/Components/Form/UMaskInput.js +++ b/frontend/src/Components/Form/UMaskInput.js @@ -1,33 +1,44 @@ /* eslint-disable no-bitwise */ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; import styles from './UMaskInput.css'; const umaskOptions = [ { key: '755', - value: '755 - Owner write, Everyone else read', + get value() { + return translate('Umask755Description', { octal: '755' }); + }, hint: 'drwxr-xr-x' }, { key: '775', - value: '775 - Owner & Group write, Other read', + get value() { + return translate('Umask775Description', { octal: '775' }); + }, hint: 'drwxrwxr-x' }, { key: '770', - value: '770 - Owner & Group write', + get value() { + return translate('Umask770Description', { octal: '770' }); + }, hint: 'drwxrwx---' }, { key: '750', - value: '750 - Owner write, Group read', + get value() { + return translate('Umask750Description', { octal: '750' }); + }, hint: 'drwxr-x---' }, { key: '777', - value: '777 - Everyone write', + get value() { + return translate('Umask777Description', { octal: '777' }); + }, hint: 'drwxrwxrwx' } ]; @@ -101,16 +112,16 @@ class UMaskInput extends Component { - UMask + {translate('UMask')} {umask} - Folder + {translate('Folder')} {folder} d{formatPermissions(folderNum)} - File + {translate('File')} {file} {formatPermissions(fileNum)} diff --git a/frontend/src/Components/Link/IconButton.js b/frontend/src/Components/Link/IconButton.js index 6f5e56d0e..fffbe13e0 100644 --- a/frontend/src/Components/Link/IconButton.js +++ b/frontend/src/Components/Link/IconButton.js @@ -2,6 +2,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; import Icon from 'Components/Icon'; +import translate from 'Utilities/String/translate'; import Link from './Link'; import styles from './IconButton.css'; @@ -23,7 +24,7 @@ function IconButton(props) { className, isDisabled && styles.isDisabled )} - aria-label="Table Options Button" + aria-label={translate('TableOptionsButton')} isDisabled={isDisabled} {...otherProps} > diff --git a/frontend/src/Components/Menu/FilterMenu.js b/frontend/src/Components/Menu/FilterMenu.js index 2ed1235ee..c36c4ae3b 100644 --- a/frontend/src/Components/Menu/FilterMenu.js +++ b/frontend/src/Components/Menu/FilterMenu.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import FilterMenuContent from './FilterMenuContent'; import Menu from './Menu'; import ToolbarMenuButton from './ToolbarMenuButton'; @@ -58,7 +59,7 @@ class FilterMenu extends Component { diff --git a/frontend/src/Components/Menu/FilterMenuContent.js b/frontend/src/Components/Menu/FilterMenuContent.js index 5d978c9ca..516fbb648 100644 --- a/frontend/src/Components/Menu/FilterMenuContent.js +++ b/frontend/src/Components/Menu/FilterMenuContent.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import translate from 'Utilities/String/translate'; import FilterMenuItem from './FilterMenuItem'; import MenuContent from './MenuContent'; import MenuItem from './MenuItem'; @@ -61,7 +62,7 @@ class FilterMenuContent extends Component { { showCustomFilters && - Custom Filters + {translate('CustomFilters')} } diff --git a/frontend/src/Components/Menu/Menu.js b/frontend/src/Components/Menu/Menu.js index 7e5e0b672..8f0b2996c 100644 --- a/frontend/src/Components/Menu/Menu.js +++ b/frontend/src/Components/Menu/Menu.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { Manager, Popper, Reference } from 'react-popper'; import Portal from 'Components/Portal'; import { align } from 'Helpers/Props'; -import getUniqueElememtId from 'Utilities/getUniqueElementId'; +import getUniqueElementId from 'Utilities/getUniqueElementId'; import styles from './Menu.css'; const sharedPopperOptions = { @@ -38,8 +38,8 @@ class Menu extends Component { super(props, context); this._scheduleUpdate = null; - this._menuButtonId = getUniqueElememtId(); - this._menuContentId = getUniqueElememtId(); + this._menuButtonId = getUniqueElementId(); + this._menuContentId = getUniqueElementId(); this.state = { isMenuOpen: false, diff --git a/frontend/src/Components/Menu/SortMenu.js b/frontend/src/Components/Menu/SortMenu.js index ec068fdf9..10a8e162f 100644 --- a/frontend/src/Components/Menu/SortMenu.js +++ b/frontend/src/Components/Menu/SortMenu.js @@ -3,6 +3,7 @@ import React from 'react'; import Menu from 'Components/Menu/Menu'; import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; import { align, icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; function SortMenu(props) { const { @@ -19,7 +20,7 @@ function SortMenu(props) { > {children} diff --git a/frontend/src/Components/Menu/ViewMenu.js b/frontend/src/Components/Menu/ViewMenu.js index 7eb505b8f..a7e56a8c5 100644 --- a/frontend/src/Components/Menu/ViewMenu.js +++ b/frontend/src/Components/Menu/ViewMenu.js @@ -3,6 +3,7 @@ import React from 'react'; import Menu from 'Components/Menu/Menu'; import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; import { align, icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; function ViewMenu(props) { const { @@ -17,7 +18,7 @@ function ViewMenu(props) { > {children} diff --git a/frontend/src/Components/MetadataAttribution.js b/frontend/src/Components/MetadataAttribution.js index 3188a8281..b32eea696 100644 --- a/frontend/src/Components/MetadataAttribution.js +++ b/frontend/src/Components/MetadataAttribution.js @@ -1,5 +1,6 @@ import React from 'react'; import Link from 'Components/Link/Link'; +import translate from 'Utilities/String/translate'; import styles from './MetadataAttribution.css'; export default function MetadataAttribution() { @@ -9,7 +10,7 @@ export default function MetadataAttribution() { className={styles.attribution} to="/settings/metadatasource" > - Metadata is provided by TheTVDB + {translate('MetadataProvidedBy', { provider: 'TheTVDB' })} ); diff --git a/frontend/src/Components/Modal/ModalError.js b/frontend/src/Components/Modal/ModalError.js index d38767ea4..56b26a1d2 100644 --- a/frontend/src/Components/Modal/ModalError.js +++ b/frontend/src/Components/Modal/ModalError.js @@ -6,6 +6,7 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; +import translate from 'Utilities/String/translate'; import styles from './ModalError.css'; function ModalError(props) { @@ -17,7 +18,7 @@ function ModalError(props) { return ( - Error + {translate('Error')} @@ -25,7 +26,7 @@ function ModalError(props) { messageClassName={styles.message} detailsClassName={styles.details} {...otherProps} - message='There was an error loading this item' + message={translate('ErrorLoadingItem')} /> @@ -33,7 +34,7 @@ function ModalError(props) { - Close + {translate('Close')} diff --git a/frontend/src/Components/MonitorToggleButton.js b/frontend/src/Components/MonitorToggleButton.js index 4230a35a9..cb92e43ba 100644 --- a/frontend/src/Components/MonitorToggleButton.js +++ b/frontend/src/Components/MonitorToggleButton.js @@ -3,18 +3,19 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import styles from './MonitorToggleButton.css'; function getTooltip(monitored, isDisabled) { if (isDisabled) { - return 'Cannot toggle monitored state when series is unmonitored'; + return translate('ToggleMonitoredSeriesUnmonitored '); } if (monitored) { - return 'Monitored, click to unmonitor'; + return translate('ToggleMonitoredToUnmonitored'); } - return 'Unmonitored, click to monitor'; + return translate('ToggleUnmonitoredToMonitored'); } class MonitorToggleButton extends Component { diff --git a/frontend/src/Components/NotFound.js b/frontend/src/Components/NotFound.js index 7043da46f..da4221200 100644 --- a/frontend/src/Components/NotFound.js +++ b/frontend/src/Components/NotFound.js @@ -1,9 +1,12 @@ import PropTypes from 'prop-types'; import React from 'react'; import PageContent from 'Components/Page/PageContent'; +import translate from 'Utilities/String/translate'; import styles from './NotFound.css'; -function NotFound({ message }) { +function NotFound(props) { + const { message = translate('DefaultNotFoundMessage') } = props; + return ( @@ -21,11 +24,7 @@ function NotFound({ message }) { } NotFound.propTypes = { - message: PropTypes.string.isRequired -}; - -NotFound.defaultProps = { - message: 'You must be lost, nothing to see here.' + message: PropTypes.string }; export default NotFound; diff --git a/frontend/src/Components/Page/ErrorPage.js b/frontend/src/Components/Page/ErrorPage.js index 214c9dcc9..f76522454 100644 --- a/frontend/src/Components/Page/ErrorPage.js +++ b/frontend/src/Components/Page/ErrorPage.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import translate from 'Utilities/String/translate'; import styles from './ErrorPage.css'; function ErrorPage(props) { @@ -16,24 +17,24 @@ function ErrorPage(props) { systemStatusError } = props; - let errorMessage = 'Failed to load Sonarr'; + let errorMessage = translate('FailedToLoadSonarr'); if (!isLocalStorageSupported) { - errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.'; + errorMessage = translate('LocalStorageIsNotSupported'); } else if (translationsError) { - errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API'); + errorMessage = getErrorMessage(translationsError, translate('FailedToLoadTranslationsFromApi')); } else if (seriesError) { - errorMessage = getErrorMessage(seriesError, 'Failed to load series from API'); + errorMessage = getErrorMessage(seriesError, translate('FailedToLoadSeriesFromApi')); } else if (customFiltersError) { - errorMessage = getErrorMessage(customFiltersError, 'Failed to load custom filters from API'); + errorMessage = getErrorMessage(customFiltersError, translate('FailedToLoadCustomFiltersFromApi')); } else if (tagsError) { - errorMessage = getErrorMessage(tagsError, 'Failed to load tags from API'); + errorMessage = getErrorMessage(tagsError, translate('FailedToLoadTagsFromApi')); } else if (qualityProfilesError) { - errorMessage = getErrorMessage(qualityProfilesError, 'Failed to load quality profiles from API'); + errorMessage = getErrorMessage(qualityProfilesError, translate('FailedToLoadQualityProfilesFromApi')); } else if (uiSettingsError) { - errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API'); + errorMessage = getErrorMessage(uiSettingsError, translate('FailedToLoadUiSettingsFromApi')); } else if (systemStatusError) { - errorMessage = getErrorMessage(uiSettingsError, 'Failed to load system status from API'); + errorMessage = getErrorMessage(uiSettingsError, translate('FailedToLoadSystemStatusFromApi')); } return ( @@ -43,7 +44,7 @@ function ErrorPage(props) { - Version {version} + {translate('VersionNumber', { version })} ); diff --git a/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js index e1a0e19e7..328e65420 100644 --- a/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js +++ b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js @@ -6,6 +6,7 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; +import translate from 'Utilities/String/translate'; import styles from './KeyboardShortcutsModalContent.css'; function getShortcuts() { @@ -47,7 +48,7 @@ function KeyboardShortcutsModalContent(props) { return ( - Keyboard Shortcuts + {translate('KeyboardShortcuts')} @@ -75,7 +76,7 @@ function KeyboardShortcutsModalContent(props) { - Close + {translate('Close')} diff --git a/frontend/src/Components/Page/Header/PageHeader.js b/frontend/src/Components/Page/Header/PageHeader.js index 46abcd013..23d7d7f99 100644 --- a/frontend/src/Components/Page/Header/PageHeader.js +++ b/frontend/src/Components/Page/Header/PageHeader.js @@ -4,6 +4,7 @@ import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import KeyboardShortcutsModal from './KeyboardShortcutsModal'; import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector'; import SeriesSearchInputConnector from './SeriesSearchInputConnector'; @@ -77,7 +78,7 @@ class PageHeader extends Component { diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js index 1df8e4b7a..cb5914aaf 100644 --- a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js @@ -7,6 +7,7 @@ import MenuContent from 'Components/Menu/MenuContent'; import MenuItem from 'Components/Menu/MenuItem'; import MenuItemSeparator from 'Components/Menu/MenuItemSeparator'; import { align, icons, kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import styles from './PageHeaderActionsMenu.css'; function PageHeaderActionsMenu(props) { @@ -32,7 +33,7 @@ function PageHeaderActionsMenu(props) { className={styles.itemIcon} name={icons.KEYBOARD} /> - Keyboard Shortcuts + {translate('KeyboardShortcuts')} @@ -42,7 +43,7 @@ function PageHeaderActionsMenu(props) { className={styles.itemIcon} name={icons.RESTART} /> - Restart + {translate('Restart')} @@ -51,7 +52,7 @@ function PageHeaderActionsMenu(props) { name={icons.SHUTDOWN} kind={kinds.DANGER} /> - Shutdown + {translate('Shutdown')} { @@ -69,7 +70,7 @@ function PageHeaderActionsMenu(props) { className={styles.itemIcon} name={icons.LOGOUT} /> - Logout + {translate('Logout')} } diff --git a/frontend/src/Components/Page/Header/SeriesSearchInput.js b/frontend/src/Components/Page/Header/SeriesSearchInput.js index abaa8f904..a763a3f05 100644 --- a/frontend/src/Components/Page/Header/SeriesSearchInput.js +++ b/frontend/src/Components/Page/Header/SeriesSearchInput.js @@ -6,6 +6,7 @@ import Icon from 'Components/Icon'; import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import FuseWorker from './fuse.worker'; import SeriesSearchResult from './SeriesSearchResult'; import styles from './SeriesSearchInput.css'; @@ -91,7 +92,7 @@ class SeriesSearchInput extends Component { if (item.type === ADD_NEW_TYPE) { return ( - Search for {query} + {translate('SearchForQuery', { query })} ); } @@ -271,14 +272,14 @@ class SeriesSearchInput extends Component { if (suggestions.length || loading) { suggestionGroups.push({ - title: 'Existing Series', + title: translate('ExistingSeries'), loading, suggestions }); } suggestionGroups.push({ - title: 'Add New Series', + title: translate('AddNewSeries'), suggestions: [ { type: ADD_NEW_TYPE, @@ -292,7 +293,7 @@ class SeriesSearchInput extends Component { className: styles.input, name: 'seriesSearch', value, - placeholder: 'Search', + placeholder: translate('Search'), autoComplete: 'off', spellCheck: false, onChange: this.onChange, diff --git a/frontend/src/Components/Page/PageContentError.js b/frontend/src/Components/Page/PageContentError.js index 9a76f2ee3..c645a6771 100644 --- a/frontend/src/Components/Page/PageContentError.js +++ b/frontend/src/Components/Page/PageContentError.js @@ -1,5 +1,6 @@ import React from 'react'; import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError'; +import translate from 'Utilities/String/translate'; import PageContentBody from './PageContentBody'; import styles from './PageContentError.css'; @@ -9,7 +10,7 @@ function PageContentError(props) { diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarSection.js b/frontend/src/Components/Page/Toolbar/PageToolbarSection.js index 2d4aca718..2d50bab8b 100644 --- a/frontend/src/Components/Page/Toolbar/PageToolbarSection.js +++ b/frontend/src/Components/Page/Toolbar/PageToolbarSection.js @@ -8,6 +8,7 @@ import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; import { forEach } from 'Helpers/elementChildren'; import { align, icons } from 'Helpers/Props'; import dimensions from 'Styles/Variables/dimensions'; +import translate from 'Utilities/String/translate'; import PageToolbarOverflowMenuItem from './PageToolbarOverflowMenuItem'; import styles from './PageToolbarSection.css'; @@ -160,7 +161,7 @@ class PageToolbarSection extends Component { diff --git a/frontend/src/Components/ProgressBar.js b/frontend/src/Components/ProgressBar.js index 0cf04da46..171b4c0fa 100644 --- a/frontend/src/Components/ProgressBar.js +++ b/frontend/src/Components/ProgressBar.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { ColorImpairedConsumer } from 'App/ColorImpairedContext'; import { kinds, sizes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import styles from './ProgressBar.css'; function ProgressBar(props) { @@ -57,7 +58,7 @@ function ProgressBar(props) { enableColorImpairedMode && 'colorImpaired' )} role="meter" - aria-label={`Progress Bar at ${progress.toFixed(0)}%`} + aria-label={translate('ProgressBarProgress', { progress: progress.toFixed(0) })} aria-valuenow={progress.toFixed(0)} aria-valuemin="0" aria-valuemax="100" diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsModal.js b/frontend/src/Components/Table/TableOptions/TableOptionsModal.js index 3272989c5..45caee110 100644 --- a/frontend/src/Components/Table/TableOptions/TableOptionsModal.js +++ b/frontend/src/Components/Table/TableOptions/TableOptionsModal.js @@ -15,6 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import TableOptionsColumn from './TableOptionsColumn'; import TableOptionsColumnDragPreview from './TableOptionsColumnDragPreview'; import TableOptionsColumnDragSource from './TableOptionsColumnDragSource'; @@ -50,9 +51,9 @@ class TableOptionsModal extends Component { let pageSizeError = null; if (value < 5) { - pageSizeError = 'Page size must be at least 5'; + pageSizeError = translate('TablePageSizeMinimum', { minimumValue: '5' }); } else if (value > 250) { - pageSizeError = 'Page size must not exceed 250'; + pageSizeError = translate('TablePageSizeMaximum', { maximumValue: '250' }); } else { this.props.onTableOptionChange({ pageSize: value }); } @@ -136,7 +137,7 @@ class TableOptionsModal extends Component { isOpen ? - Table Options + {translate('TableOptions')} @@ -144,13 +145,13 @@ class TableOptionsModal extends Component { { hasPageSize ? - Page Size + {translate('PageSize')} @@ -168,11 +169,11 @@ class TableOptionsModal extends Component { { canModifyColumns ? - Columns + {translate('TableColumns')} @@ -231,7 +232,7 @@ class TableOptionsModal extends Component { - Close + {translate('Close')} : diff --git a/frontend/src/Components/Table/TablePager.js b/frontend/src/Components/Table/TablePager.js index 6f71ee5d2..d58824169 100644 --- a/frontend/src/Components/Table/TablePager.js +++ b/frontend/src/Components/Table/TablePager.js @@ -6,6 +6,7 @@ import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import styles from './TablePager.css'; class TablePager extends Component { @@ -156,7 +157,7 @@ class TablePager extends Component { - Total records: {totalRecords} + {translate('TotalRecords', { totalRecords })} diff --git a/frontend/src/Components/keyboardShortcuts.js b/frontend/src/Components/keyboardShortcuts.js index 912522d49..7ba54ac46 100644 --- a/frontend/src/Components/keyboardShortcuts.js +++ b/frontend/src/Components/keyboardShortcuts.js @@ -1,31 +1,42 @@ import Mousetrap from 'mousetrap'; import React, { Component } from 'react'; import getDisplayName from 'Helpers/getDisplayName'; +import translate from 'Utilities/String/translate'; export const shortcuts = { OPEN_KEYBOARD_SHORTCUTS_MODAL: { key: '?', - name: 'Open This Modal' + get name() { + return translate('KeyboardShortcutsOpenModal'); + } }, CLOSE_MODAL: { key: 'Esc', - name: 'Close Current Modal' + get name() { + return translate('KeyboardShortcutsCloseModal'); + } }, ACCEPT_CONFIRM_MODAL: { key: 'Enter', - name: 'Accept Confirmation Modal' + get name() { + return translate('KeyboardShortcutsConfirmModal'); + } }, SERIES_SEARCH_INPUT: { key: 's', - name: 'Focus Search Box' + get name() { + return translate('KeyboardShortcutsFocusSearchBox'); + } }, SAVE_SETTINGS: { key: 'mod+s', - name: 'Save Settings' + get name() { + return translate('KeyboardShortcutsSaveSettings'); + } } }; diff --git a/frontend/src/Episode/EpisodeDetailsModalContent.js b/frontend/src/Episode/EpisodeDetailsModalContent.js index 75a9e64ad..cd5f37fab 100644 --- a/frontend/src/Episode/EpisodeDetailsModalContent.js +++ b/frontend/src/Episode/EpisodeDetailsModalContent.js @@ -8,6 +8,7 @@ import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import MonitorToggleButton from 'Components/MonitorToggleButton'; import episodeEntities from 'Episode/episodeEntities'; +import translate from 'Utilities/String/translate'; import EpisodeHistoryConnector from './History/EpisodeHistoryConnector'; import EpisodeSearchConnector from './Search/EpisodeSearchConnector'; import SeasonEpisodeNumber from './SeasonEpisodeNumber'; @@ -117,21 +118,21 @@ class EpisodeDetailsModalContent extends Component { className={styles.tab} selectedClassName={styles.selectedTab} > - Details + {translate('Details')} - History + {translate('History')} - Search + {translate('Search')} @@ -173,14 +174,14 @@ class EpisodeDetailsModalContent extends Component { to={seriesLink} onPress={onModalClose} > - Open Series + {translate('OpenSeries')} } - Close + {translate('Close')} diff --git a/frontend/src/Episode/EpisodeLanguages.js b/frontend/src/Episode/EpisodeLanguages.js index 53e3854cc..66f278897 100644 --- a/frontend/src/Episode/EpisodeLanguages.js +++ b/frontend/src/Episode/EpisodeLanguages.js @@ -3,6 +3,7 @@ import React from 'react'; import Label from 'Components/Label'; import Popover from 'Components/Tooltip/Popover'; import { kinds, tooltipPositions } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; function EpisodeLanguages(props) { const { @@ -34,10 +35,10 @@ function EpisodeLanguages(props) { className={className} kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT} > - Multi-Languages + {translate('MultiLanguages')} } - title={'Languages'} + title={translate('Languages')} body={ { diff --git a/frontend/src/Episode/EpisodeNumber.js b/frontend/src/Episode/EpisodeNumber.js index 948f8710c..7f5c448f1 100644 --- a/frontend/src/Episode/EpisodeNumber.js +++ b/frontend/src/Episode/EpisodeNumber.js @@ -5,6 +5,7 @@ import Popover from 'Components/Tooltip/Popover'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import padNumber from 'Utilities/Number/padNumber'; import filterAlternateTitles from 'Utilities/Series/filterAlternateTitles'; +import translate from 'Utilities/String/translate'; import SceneInfo from './SceneInfo'; import styles from './EpisodeNumber.css'; @@ -12,11 +13,11 @@ function getWarningMessage(unverifiedSceneNumbering, seriesType, absoluteEpisode const messages = []; if (unverifiedSceneNumbering) { - messages.push('Scene number hasn\'t been verified yet'); + messages.push(translate('SceneNumberNotVerified')); } if (seriesType === 'anime' && !absoluteEpisodeNumber) { - messages.push('Episode does not have an absolute episode number'); + messages.push(translate('EpisodeMissingAbsoluteNumber')); } return messages.join('\n'); @@ -70,7 +71,7 @@ function EpisodeNumber(props) { } } - title="Scene Information" + title={translate('SceneInformation')} body={ ); @@ -66,7 +67,7 @@ function EpisodeStatus(props) { quality={quality} size={episodeFile.size} isCutoffNotMet={isCutoffNotMet} - title="Episode Downloaded" + title={translate('EpisodeDownloaded')} /> ); @@ -77,7 +78,7 @@ function EpisodeStatus(props) { ); @@ -89,7 +90,7 @@ function EpisodeStatus(props) { ); @@ -100,7 +101,7 @@ function EpisodeStatus(props) { ); @@ -110,7 +111,7 @@ function EpisodeStatus(props) { ); diff --git a/frontend/src/Episode/History/EpisodeHistory.js b/frontend/src/Episode/History/EpisodeHistory.js index c5d2c907d..7a420de26 100644 --- a/frontend/src/Episode/History/EpisodeHistory.js +++ b/frontend/src/Episode/History/EpisodeHistory.js @@ -79,13 +79,13 @@ class EpisodeHistory extends Component { if (!isFetching && !!error) { return ( - Unable to load episode history. + {translate('EpisodeHistoryLoadError')} ); } if (isPopulated && !hasItems && !error) { return ( - No episode history. + {translate('NoEpisodeHistory')} ); } diff --git a/frontend/src/Episode/History/EpisodeHistoryRow.js b/frontend/src/Episode/History/EpisodeHistoryRow.js index 8d50e2e1e..9e46f4e58 100644 --- a/frontend/src/Episode/History/EpisodeHistoryRow.js +++ b/frontend/src/Episode/History/EpisodeHistoryRow.js @@ -15,6 +15,7 @@ import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; +import translate from 'Utilities/String/translate'; import styles from './EpisodeHistoryRow.css'; function getTitle(eventType) { @@ -137,7 +138,7 @@ class EpisodeHistoryRow extends Component { { eventType === 'grabbed' && @@ -147,9 +148,9 @@ class EpisodeHistoryRow extends Component { diff --git a/frontend/src/Episode/SceneInfo.js b/frontend/src/Episode/SceneInfo.js index 154cdce28..dc700c98e 100644 --- a/frontend/src/Episode/SceneInfo.js +++ b/frontend/src/Episode/SceneInfo.js @@ -4,6 +4,7 @@ import React from 'react'; import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; import padNumber from 'Utilities/Number/padNumber'; +import translate from 'Utilities/String/translate'; import styles from './SceneInfo.css'; function SceneInfo(props) { @@ -56,7 +57,7 @@ function SceneInfo(props) { } @@ -66,7 +67,7 @@ function SceneInfo(props) { } @@ -76,7 +77,7 @@ function SceneInfo(props) { } @@ -86,7 +87,7 @@ function SceneInfo(props) { { diff --git a/frontend/src/Episode/Search/EpisodeSearch.js b/frontend/src/Episode/Search/EpisodeSearch.js index cd8d85477..87451e63e 100644 --- a/frontend/src/Episode/Search/EpisodeSearch.js +++ b/frontend/src/Episode/Search/EpisodeSearch.js @@ -3,6 +3,7 @@ import React from 'react'; import Icon from 'Components/Icon'; import Button from 'Components/Link/Button'; import { icons, kinds, sizes } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import styles from './EpisodeSearch.css'; function EpisodeSearch(props) { @@ -24,7 +25,7 @@ function EpisodeSearch(props) { name={icons.QUICK} /> - Quick Search + {translate('QuickSearch')} @@ -40,7 +41,7 @@ function EpisodeSearch(props) { name={icons.INTERACTIVE} /> - Interactive Search + {translate('InteractiveSearch')} diff --git a/frontend/src/Episode/Summary/EpisodeAiring.js b/frontend/src/Episode/Summary/EpisodeAiring.js index e79bc19c2..2b64561dd 100644 --- a/frontend/src/Episode/Summary/EpisodeAiring.js +++ b/frontend/src/Episode/Summary/EpisodeAiring.js @@ -7,6 +7,7 @@ import formatTime from 'Utilities/Date/formatTime'; import isInNextWeek from 'Utilities/Date/isInNextWeek'; import isToday from 'Utilities/Date/isToday'; import isTomorrow from 'Utilities/Date/isTomorrow'; +import translate from '../../Utilities/String/translate'; function EpisodeAiring(props) { const { @@ -26,10 +27,11 @@ function EpisodeAiring(props) { ); + // TODO: Update InlineMarkdown to accept tags and pass in networkLabel object, for now blank string passed into translation if (!airDateUtc) { return ( - TBA on {networkLabel} + {translate('AirsTbaOn', { networkLabel: '' })}networkLabel ); } @@ -39,7 +41,7 @@ function EpisodeAiring(props) { if (!showRelativeDates) { return ( - {moment(airDateUtc).format(shortDateFormat)} at {time} on {networkLabel} + {translate('AirsDateAtTimeOn', { date: moment(airDateUtc).format(shortDateFormat), time, networkLabel: '' })}{networkLabel} ); } @@ -47,7 +49,7 @@ function EpisodeAiring(props) { if (isToday(airDateUtc)) { return ( - {time} on {networkLabel} + {translate('AirsTimeOn', { time, networkLabel: '' })}{networkLabel} ); } @@ -55,7 +57,7 @@ function EpisodeAiring(props) { if (isTomorrow(airDateUtc)) { return ( - Tomorrow at {time} on {networkLabel} + {translate('AirsTomorrowOn', { time, networkLabel: '' })}{networkLabel} ); } @@ -63,14 +65,14 @@ function EpisodeAiring(props) { if (isInNextWeek(airDateUtc)) { return ( - {moment(airDateUtc).format('dddd')} at {time} on {networkLabel} + {translate('AirsDateAtTimeOn', { date: moment(airDateUtc).format('dddd'), time, networkLabel: '' })}{networkLabel} ); } return ( - {moment(airDateUtc).format(shortDateFormat)} at {time} on {networkLabel} + {translate('AirsDateAtTimeOn', { date: moment(airDateUtc).format(shortDateFormat), time, networkLabel: '' })}{networkLabel} ); } diff --git a/frontend/src/Episode/Summary/EpisodeFileRow.js b/frontend/src/Episode/Summary/EpisodeFileRow.js index 0fb44dbad..f23b7c9ce 100644 --- a/frontend/src/Episode/Summary/EpisodeFileRow.js +++ b/frontend/src/Episode/Summary/EpisodeFileRow.js @@ -11,6 +11,7 @@ import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; import MediaInfo from './MediaInfo'; import styles from './EpisodeFileRow.css'; @@ -140,7 +141,7 @@ class EpisodeFileRow extends Component { name={icons.MEDIA_INFO} /> } - title="Media Info" + title={translate('MediaInfo')} body={} position={tooltipPositions.LEFT} /> : @@ -148,7 +149,7 @@ class EpisodeFileRow extends Component { } @@ -163,9 +164,9 @@ class EpisodeFileRow extends Component { diff --git a/frontend/src/Episode/Summary/EpisodeSummary.js b/frontend/src/Episode/Summary/EpisodeSummary.js index f87c884d9..0d454e90e 100644 --- a/frontend/src/Episode/Summary/EpisodeSummary.js +++ b/frontend/src/Episode/Summary/EpisodeSummary.js @@ -103,7 +103,7 @@ class EpisodeSummary extends Component { return ( - Airs + {translate('Airs')} - Quality Profile + {translate('QualityProfile')} @@ -155,9 +155,9 @@ class EpisodeSummary extends Component { diff --git a/frontend/src/Helpers/Props/filterBuilderTypes.js b/frontend/src/Helpers/Props/filterBuilderTypes.js index c0806fabc..9937fbe94 100644 --- a/frontend/src/Helpers/Props/filterBuilderTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderTypes.js @@ -1,3 +1,4 @@ +import translate from 'Utilities/String/translate'; import * as filterTypes from './filterTypes'; export const ARRAY = 'array'; @@ -20,49 +21,127 @@ export const all = [ export const possibleFilterTypes = { [ARRAY]: [ - { key: filterTypes.CONTAINS, value: 'contains' }, - { key: filterTypes.NOT_CONTAINS, value: 'does not contain' } + { + key: filterTypes.CONTAINS, + value: () => translate('FilterContains') + }, + { + key: filterTypes.NOT_CONTAINS, + value: () => translate('FilterDoesNotContain') + } ], [CONTAINS]: [ - { key: filterTypes.CONTAINS, value: 'contains' } + { + key: filterTypes.CONTAINS, + value: () => translate('FilterContains') + } ], [DATE]: [ - { key: filterTypes.LESS_THAN, value: 'is before' }, - { key: filterTypes.GREATER_THAN, value: 'is after' }, - { key: filterTypes.IN_LAST, value: 'in the last' }, - { key: filterTypes.NOT_IN_LAST, value: 'not in the last' }, - { key: filterTypes.IN_NEXT, value: 'in the next' }, - { key: filterTypes.NOT_IN_NEXT, value: 'not in the next' } + { + key: filterTypes.LESS_THAN, + value: () => translate('FilterIsBefore') + }, + { + key: filterTypes.GREATER_THAN, + value: () => translate('FilterIsAfter') + }, + { + key: filterTypes.IN_LAST, + value: () => translate('FilterInLast') + }, + { + key: filterTypes.NOT_IN_LAST, + value: () => translate('FilterNotInLast') + }, + { + key: filterTypes.IN_NEXT, + value: () => translate('FilterInNext') + }, + { + key: filterTypes.NOT_IN_NEXT, + value: () => translate('FilterNotInNext') + } ], [EQUAL]: [ - { key: filterTypes.EQUAL, value: 'is' } + { + key: filterTypes.EQUAL, + value: () => translate('FilterIs') + } ], [EXACT]: [ - { key: filterTypes.EQUAL, value: 'is' }, - { key: filterTypes.NOT_EQUAL, value: 'is not' } + { + key: filterTypes.EQUAL, + value: () => translate('FilterIs') + }, + { + key: filterTypes.NOT_EQUAL, + value: () => translate('FilterIsNot') + } ], [NUMBER]: [ - { key: filterTypes.EQUAL, value: 'equal' }, - { key: filterTypes.GREATER_THAN, value: 'greater than' }, - { key: filterTypes.GREATER_THAN_OR_EQUAL, value: 'greater than or equal' }, - { key: filterTypes.LESS_THAN, value: 'less than' }, - { key: filterTypes.LESS_THAN_OR_EQUAL, value: 'less than or equal' }, - { key: filterTypes.NOT_EQUAL, value: 'not equal' } + { + key: filterTypes.EQUAL, + value: () => translate('FilterEqual') + }, + { + key: filterTypes.GREATER_THAN, + value: () => translate('FilterGreaterThan') + }, + { + key: filterTypes.GREATER_THAN_OR_EQUAL, + value: () => translate('FilterGreaterThanOrEqual') + }, + { + key: filterTypes.LESS_THAN, + value: () => translate('FilterLessThan') + }, + { + key: filterTypes.LESS_THAN_OR_EQUAL, + value: () => translate('FilterLessThanOrEqual') + }, + { + key: filterTypes.NOT_EQUAL, + value: () => translate('FilterNotEqual') + } ], [STRING]: [ - { key: filterTypes.CONTAINS, value: 'contains' }, - { key: filterTypes.NOT_CONTAINS, value: 'does not contain' }, - { key: filterTypes.EQUAL, value: 'equal' }, - { key: filterTypes.NOT_EQUAL, value: 'not equal' }, - { key: filterTypes.STARTS_WITH, value: 'starts with' }, - { key: filterTypes.NOT_STARTS_WITH, value: 'does not start with' }, - { key: filterTypes.ENDS_WITH, value: 'ends with' }, - { key: filterTypes.NOT_ENDS_WITH, value: 'does not end with' } + { + key: filterTypes.CONTAINS, + value: () => translate('FilterContains') + }, + { + key: filterTypes.NOT_CONTAINS, + value: () => translate('FilterDoesNotContain') + }, + { + key: filterTypes.EQUAL, + value: () => translate('FilterEqual') + }, + { + key: filterTypes.NOT_EQUAL, + value: () => translate('FilterNotEqual') + }, + { + key: filterTypes.STARTS_WITH, + value: () => translate('FilterStartsWith') + }, + { + key: filterTypes.NOT_STARTS_WITH, + value: () => translate('FilterDoesNotStartWith') + }, + { + key: filterTypes.ENDS_WITH, + value: () => translate('FilterEndsWith') + }, + { + key: filterTypes.NOT_ENDS_WITH, + value: () => translate('FilterDoesNotEndWith') + } ] }; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index caf7bb617..75a6b4b64 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1,15 +1,18 @@ { "About": "About", + "Absolute": "Absolute", "AbsoluteEpisodeNumber": "Absolute Episode Number", "AbsoluteEpisodeNumbers": "Absolute Episode Number(s)", "Actions": "Actions", "Activity": "Activity", "Add": "Add", + "AddANewPath": "Add a new path", "AddAutoTag": "Add Auto Tag", "AddAutoTagError": "Unable to add a new auto tag, please try again.", "AddCondition": "Add Condition", "AddConditionError": "Unable to add a new condition, please try again.", "AddConnection": "Add Connection", + "AddCustomFilter": "Add Custom Filter", "AddCustomFormat": "Add Custom Format", "AddCustomFormatError": "Unable to add a new custom format, please try again.", "AddDelayProfile": "Add Delay Profile", @@ -48,6 +51,11 @@ "AgeWhenGrabbed": "Age (when grabbed)", "Agenda": "Agenda", "AirDate": "Air Date", + "Airs": "Airs", + "AirsDateAtTimeOn": "{date} at {time} on {networkLabel}", + "AirsTbaOn": "TBA on {networkLabel}", + "AirsTimeOn": "{time} on {networkLabel}", + "AirsTomorrowOn": "Tomorrow at {time} on {networkLabel}", "All": "All", "AllResultsAreHiddenByTheAppliedFilter": "All results are hidden by the applied filter", "AllSeriesInRootFolderHaveBeenImported": "All series in {path} have been imported", @@ -62,6 +70,8 @@ "Anime": "Anime", "AnimeEpisodeFormat": "Anime Episode Format", "AnimeTypeDescription": "Episodes released using an absolute episode number", + "AnimeTypeFormat": "Absolute episode number ({format})", + "Any": "Any", "ApiKey": "API Key", "ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {0} characters long. You can do this via settings or the config file", "AppDataDirectory": "AppData directory", @@ -181,6 +191,7 @@ "ConnectionLostReconnect": "{appName} will try to connect automatically, or you can click reload below.", "ConnectionLostToBackend": "{appName} has lost its connection to the backend and will need to be reloaded to restore functionality.", "Connections": "Connections", + "Continuing": "Continuing", "ContinuingOnly": "Continuing Only", "CopyToClipboard": "Copy to Clipboard", "CopyUsingHardlinksHelpText": "Hardlinks allow Sonarr to import seeding torrents to the series folder without taking extra disk space or copying the entire contents of the file. Hardlinks will only work if the source and destination are on the same volume", @@ -195,6 +206,7 @@ "CreateGroup": "Create Group", "CurrentlyInstalled": "Currently Installed", "Custom": "Custom", + "CustomFilters": "Custom Filters", "CustomFormat": "Custom Format", "CustomFormatHelpText": "Sonarr scores each release using the sum of scores for matching custom formats. If a new release would improve the score, at the same or better quality, then Sonarr will grab it.", "CustomFormatScore": "Custom Format Score", @@ -209,6 +221,7 @@ "Daily": "Daily", "DailyEpisodeFormat": "Daily Episode Format", "DailyTypeDescription": "Episodes released daily or less frequently that use year-month-day (2023-08-04)", + "DailyTypeFormat": "Date ({format})", "Dash": "Dash", "Date": "Date", "Dates": "Dates", @@ -216,6 +229,7 @@ "Debug": "Debug", "DefaultCase": "Default Case", "DefaultDelayProfile": "This is the default profile. It applies to all series that don't have an explicit profile.", + "DefaultNotFoundMessage": "You must be lost, nothing to see here.", "DelayMinutes": "{delay} Minutes", "DelayProfile": "Delay Profile", "DelayProfileProtocol": "Protocol: {preferredProtocol}", @@ -238,6 +252,9 @@ "DeleteDownloadClientMessageText": "Are you sure you want to delete the download client '{name}'?", "DeleteEmptyFolders": "Delete Empty Folders", "DeleteEmptyFoldersHelpText": "Delete empty series and season folders during disk scan and when episode files are deleted", + "DeleteEpisodeFile": "Delete Episode File", + "DeleteEpisodeFileMessage": "Are you sure you want to delete '{path}'?", + "DeleteEpisodeFromDisk": "Delete episode from disk", "DeleteImportList": "Delete Import List", "DeleteImportListExclusion": "Delete Import List Exclusion", "DeleteImportListExclusionMessageText": "Are you sure you want to delete this import list exclusion?", @@ -279,6 +296,7 @@ "DoNotUpgradeAutomatically": "Do not Upgrade Automatically", "Docker": "Docker", "DockerUpdater": "Update the docker container to receive the update", + "Donate": "Donate", "Donations": "Donations", "DoneEditingGroups": "Done Editing Groups", "DotNetVersion": ".NET", @@ -356,15 +374,20 @@ "Episode": "Episode", "EpisodeAirDate": "Episode Air Date", "EpisodeCount": "Episode Count", + "EpisodeDownloaded": "Episode Downloaded", "EpisodeFileDeleted": "Episode File Deleted", "EpisodeFileDeletedTooltip": "Episode file deleted", "EpisodeFileRenamed": "Episode File Renamed", "EpisodeFileRenamedTooltip": "Episode file renamed", + "EpisodeHasNotAired": "Episode has not aired", + "EpisodeHistoryLoadError": "Unable to load episode history", "EpisodeImported": "Episode Imported", "EpisodeImportedTooltip": "Episode downloaded successfully and picked up from download client", "EpisodeInfo": "Episode Info", "EpisodeIsDownloading": "Episode is downloading", + "EpisodeIsNotMonitored": "Episode is not monitored", "EpisodeMissingAbsoluteNumber": "Episode does not have an absolute episode number", + "EpisodeMissingFromDisk": "Episode missing from disk", "EpisodeNaming": "Episode Naming", "EpisodeNumbers": "Episode Number(s)", "EpisodeProgress": "Episode Progress", @@ -374,12 +397,17 @@ "EpisodeTitleRequiredHelpText": "Prevent importing for up to 48 hours if the episode title is in the naming format and the episode title is TBA", "Episodes": "Episodes", "Error": "Error", + "ErrorLoadingContent": "There was an error loading this content", + "ErrorLoadingContents": "Error loading contents", + "ErrorLoadingItem": "There was an error loading this item", + "ErrorLoadingPage": "There was an error loading this page", "ErrorRestoringBackup": "Error restoring backup", "EventType": "Event Type", "Events": "Events", "Example": "Example", "Exception": "Exception", "Existing": "Existing", + "ExistingSeries": "Existing Series", "ExistingTag": "Existing tag", "ExportCustomFormat": "Export Custom Format", "Extend": "Extend", @@ -389,12 +417,44 @@ "ExtraFileExtensionsHelpTextsExamples": "Examples: '.sub, .nfo' or 'sub,nfo'", "Failed": "Failed", "FailedToFetchUpdates": "Failed to fetch updates", + "FailedToLoadCustomFiltersFromApi": "Failed to load custom filters from API", + "FailedToLoadQualityProfilesFromApi": "Failed to load quality profiles from API", + "FailedToLoadSeriesFromApi": "Failed to load series from API", + "FailedToLoadSonarr": "Failed to load Sonarr", + "FailedToLoadSystemStatusFromApi": "Failed to load system status from API", + "FailedToLoadTagsFromApi": "Failed to load tags from API", + "FailedToLoadTranslationsFromApi": "Failed to load translations from API", + "FailedToLoadUiSettingsFromApi": "Failed to load UI settings from API", "FailedToUpdateSettings": "Failed to update settings", "FeatureRequests": "Feature Requests", + "File": "File", + "FileBrowser": "File Browser", + "FileBrowserPlaceholderText": "Start typing or select a path below", "FileManagement": "File Management", "FileNameTokens": "File Name Tokens", "FileNames": "File Names", "Filename": "Filename", + "Filter": "Filter", + "FilterContains": "contains", + "FilterDoesNotContain": "does not contain", + "FilterDoesNotEndWith": "does not end with", + "FilterDoesNotStartWith": "does not start with", + "FilterEndsWith": "ends with", + "FilterEqual": "equal", + "FilterGreaterThan": "greater than", + "FilterGreaterThanOrEqual": "greater than or equal", + "FilterInLast": "in the last", + "FilterInNext": "in the next", + "FilterIs": "is", + "FilterIsAfter": "is after", + "FilterIsBefore": "is before", + "FilterIsNot": "is not", + "FilterLessThan": "less than", + "FilterLessThanOrEqual": "less than or equal", + "FilterNotEqual": "not equal", + "FilterNotInLast": "not in the last", + "FilterNotInNext": "not in the next", + "FilterStartsWith": "starts with", "FinaleTooltip": "Series or season finale", "FirstDayOfWeek": "First Day of Week", "Fixed": "Fixed", @@ -436,6 +496,7 @@ "ICalIncludeUnmonitoredHelpText": "Include unmonitored episodes in the iCal feed", "ICalLink": "iCal Link", "ICalSeasonPremieresOnlyHelpText": "Only the first episode in a season will be in the feed", + "ICalShowAsAllDayEvents": "Show as All-Day Events", "ICalShowAsAllDayEventsHelpText": "Events will appear as all-day events in your calendar", "ICalTagsHelpText": "Feed will only contain series with at least one matching tag", "IRC": "IRC", @@ -514,6 +575,12 @@ "InteractiveSearch": "Interactive Search", "Interval": "Interval", "InvalidFormat": "Invalid Format", + "KeyboardShortcuts": "Keyboard Shortcuts", + "KeyboardShortcutsCloseModal": "Close Current Modal", + "KeyboardShortcutsConfirmModal": "Accept Confirmation Modal", + "KeyboardShortcutsFocusSearchBox": "Focus Search Box", + "KeyboardShortcutsOpenModal": "Open This Modal", + "KeyboardShortcutsSaveSettings": "Save Settings", "Language": "Language", "Languages": "Languages", "LanguagesLoadError": "Unable to load languages", @@ -539,12 +606,14 @@ "Local": "Local", "LocalAirDate": "Local Air Date", "LocalPath": "Local Path", + "LocalStorageIsNotSupported": "Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.", "Location": "Location", "LogFiles": "Log Files", "LogFilesLocation": "Log files are located in: {location}", "LogLevel": "Log Level", "LogLevelTraceHelpTextWarning": "Trace logging should only be enabled temporarily", "Logging": "Logging", + "Logout": "Logout", "Logs": "Logs", "LongDateFormat": "Long Date Format", "Lowercase": "Lowercase", @@ -556,7 +625,9 @@ "ManageLists": "Manage Lists", "Manual": "Manual", "ManualImportItemsLoadError": "Unable to load manual import items", + "MappedNetworkDrivesWindowsService": "Mapped network drives are not available when running as a Windows Service, see the [FAQ](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server) for more information.", "MarkAsFailed": "Mark as Failed", + "MarkAsFailedConfirmation": "Are you sure you want to mark '{sourceTitle}' as failed?", "MatchedToEpisodes": "Matched to Episodes", "MatchedToSeason": "Matched to Season", "MatchedToSeries": "Matched to Series", @@ -577,6 +648,7 @@ "Message": "Message", "Metadata": "Metadata", "MetadataLoadError": "Unable to load Metadata", + "MetadataProvidedBy": "Metadata is provided by {provider}", "MetadataSettings": "Metadata Settings", "MetadataSettingsSummary": "Create metadata files when episodes are imported or series are refreshed", "MetadataSource": "Metadata Source", @@ -595,6 +667,7 @@ "MinutesThirty": "30 Minutes: {thirty}", "Missing": "Missing", "MissingEpisodes": "Missing Episodes", + "Mixed": "Mixed", "Mode": "Mode", "Monday": "Monday", "Monitor": "Monitor", @@ -618,6 +691,7 @@ "MonitoredOnly": "Monitored Only", "MonitoringOptions": "Monitoring Options", "Month": "Month", + "More": "More", "MoreDetails": "More details", "MoreInfo": "More Info", "MountHealthCheckMessage": "Mount containing a series path is mounted read-only: ", @@ -625,11 +699,13 @@ "MultiEpisode": "Multi Episode", "MultiEpisodeInvalidFormat": "Multi Episode: Invalid Format", "MultiEpisodeStyle": "Multi Episode Style", + "MultiLanguages": "Multi-Languages", "MultiSeason": "Multi-Season", "MustContain": "Must Contain", "MustContainHelpText": "The release must contain at least one of these terms (case insensitive)", "MustNotContain": "Must Not Contain", "MustNotContainHelpText": "The release will be rejected if it contains one or more of terms (case insensitive)", + "MyComputer": "My Computer", "Name": "Name", "NamingSettings": "Naming Settings", "NamingSettingsLoadError": "Unable to load Naming settings", @@ -647,6 +723,8 @@ "NoChanges": "No Changes", "NoDelay": "No Delay", "NoDownloadClientsFound": "No download clients found", + "NoEpisodeHistory": "No episode history", + "NoEpisodeOverview": "No episode overview", "NoEventsFound": "No events found", "NoHistoryBlocklist": "No history blocklist", "NoHistoryFound": "No history found", @@ -691,8 +769,10 @@ "OnlyUsenet": "Only Usenet", "OpenBrowserOnStart": "Open browser on start", "OpenBrowserOnStartHelpText": " Open a web browser and navigate to the Sonarr homepage on app start.", + "OpenSeries": "Open Series", "OptionalName": "Optional name", "Options": "Options", + "Or": "or", "Original": "Original", "OriginalLanguage": "Original Language", "Other": "Other", @@ -730,6 +810,7 @@ "Profiles": "Profiles", "ProfilesSettingsSummary": "Quality, Language Delay and Release profiles", "Progress": "Progress", + "ProgressBarProgress": "Progress Bar at {progress}%", "Proper": "Proper", "Protocol": "Protocol", "ProtocolHelpText": "Choose which protocol(s) to use and which one is preferred when choosing between otherwise equal releases", @@ -760,6 +841,7 @@ "QueueIsEmpty": "Queue is empty", "QueueLoadError": "Failed to load Queue", "Queued": "Queued", + "QuickSearch": "Quick Search", "Range": "Range", "Rating": "Rating", "ReadTheWikiForMoreInformation": "Read the Wiki for more information", @@ -821,6 +903,7 @@ "RemoveFailed": "Remove Failed", "RemoveFailedDownloads": "Remove Failed Downloads", "RemoveFailedDownloadsHelpText": "Remove failed downloads from download client history", + "RemoveFilter": "Remove filter", "RemoveFromBlocklist": "Remove from Blocklist", "RemoveFromDownloadClient": "Remove From Download Client", "RemoveFromDownloadClientHelpTextWarning": "Removing will remove the download and the file(s) from the download client.", @@ -885,6 +968,7 @@ "RootFolderMissingHealthCheckMessage": "Missing root folder: {0}", "RootFolderMultipleMissingHealthCheckMessage": "Multiple root folders are missing: {0}", "RootFolderPath": "Root Folder Path", + "RootFolderSelectFreeSpace": "{freeSpace} Free", "RootFolders": "Root Folders", "RootFoldersLoadError": "Unable to load root folders", "Rss": "RSS", @@ -898,16 +982,19 @@ "SaveChanges": "Save Changes", "SaveSettings": "Save Settings", "Scene": "Scene", + "SceneInformation": "Scene Information", "SceneNumberNotVerified": "Scene number hasn't been verified yet", "SceneNumbering": "Scene Numbering", "Scheduled": "Scheduled", "Score": "Score", "Script": "Script", "ScriptPath": "Script Path", + "Search": "Search", "SearchByTvdbId": "You can also search using TVDB ID of a show. eg. tvdb:71663", "SearchFailedError": "Search failed, please try again later.", "SearchForMissing": "Search for Missing", "SearchForMonitoredEpisodes": "Search for monitored episodes", + "SearchForQuery": "Search for {query}", "SearchIsNotSupportedWithThisIndexer": "Search is not supported with this indexer", "Season": "Season", "SeasonCount": "Season Count", @@ -946,7 +1033,6 @@ "Settings": "Settings", "ShortDateFormat": "Short Date Format", "ShowAdvanced": "Show Advanced", - "ICalShowAsAllDayEvents": "Show as All-Day Events", "ShowEpisodeInformation": "Show Episode Information", "ShowEpisodeInformationHelpText": "Show episode title and number", "ShowRelativeDates": "Show Relative Dates", @@ -954,6 +1040,7 @@ "ShownClickToHide": "Shown, click to hide", "ShownUnknownSeriesItems": "Show Unknown Series Items", "ShownUnknownSeriesItemsHelpText": "Show items without a series in the queue, this could include removed series, movies or anything else in Sonarr's category", + "Shutdown": "Shutdown", "SingleEpisode": "Single Episode", "SingleEpisodeInvalidFormat": "Single Episode: Invalid Format", "Size": "Size", @@ -969,6 +1056,7 @@ "Socks5": "Socks5 (Support TOR)", "SomeResultsAreHiddenByTheAppliedFilter": "Some results are hidden by the applied filter", "SonarrTags": "Sonarr Tags", + "Sort": "Sort", "Source": "Source", "SourcePath": "Source Path", "SourceRelativePath": "Source Relative Path", @@ -985,6 +1073,7 @@ "Standard": "Standard", "StandardEpisodeFormat": "Standard Episode Format", "StandardTypeDescription": "Episodes released with SxxEyy pattern", + "StandardTypeFormat": "Season and episode numbers ({format})", "StartImport": "Start Import", "StartProcessing": "Start Processing", "Started": "Started", @@ -1004,6 +1093,14 @@ "SupportedListsMoreInfo": "For more information on the individual lists, click on the more info buttons.", "System": "System", "SystemTimeHealthCheckMessage": "System time is off by more than 1 day. Scheduled tasks may not run correctly until the time is corrected", + "TableColumns": "Columns", + "TableColumnsHelpText": "Choose which columns are visible and which order they appear in", + "TableOptions": "Table Options", + "TableOptionsButton": "Table Options Button", + "TablePageSize": "Page Size", + "TablePageSizeHelpText": "Number of items to show on each page", + "TablePageSizeMaximum": "Page size must not exceed {maximumValue}", + "TablePageSizeMinimum": "Page size must be at least {minimumValue}", "TagCannotBeDeletedWhileInUse": "Tag cannot be deleted while in use", "TagDetails": "Tag Details - {label}", "TagIsNotUsedAndCanBeDeleted": "Tag is not used and can be deleted", @@ -1012,6 +1109,7 @@ "TagsSettingsSummary": "See all tags and how they are used. Unused tags can be removed", "TaskUserAgentTooltip": "User-Agent provided by the app that called the API", "Tasks": "Tasks", + "Tba": "TBA", "TestAll": "Test All", "TestAllClients": "Test All Clients", "TestAllIndexers": "Test All Indexers", @@ -1025,12 +1123,17 @@ "TimeFormat": "Time Format", "TimeLeft": "Time Left", "Title": "Title", + "Titles": "Titles", "Today": "Today", + "ToggleMonitoredSeriesUnmonitored ": "Cannot toggle monitored state when series is unmonitored", + "ToggleMonitoredToUnmonitored": "Monitored, click to unmonitor", + "ToggleUnmonitoredToMonitored": "Unmonitored, click to monitor", "TorrentDelay": "Torrent Delay", "TorrentDelayHelpText": "Delay in minutes to wait before grabbing a torrent", "TorrentDelayTime": "Torrent Delay: {torrentDelay}", "Torrents": "Torrents", "TorrentsDisabled": "Torrents Disabled", + "TotalRecords": "Total records: {totalRecords}", "TotalSpace": "Total Space", "Trace": "Trace", "TvdbId": "TVDB ID", @@ -1044,6 +1147,12 @@ "UiSettings": "UI Settings", "UiSettingsLoadError": "Unable to load UI settings", "UiSettingsSummary": "Calendar, date and color impaired options", + "Umask": "Umask", + "Umask750Description": "{octal} - Owner write, Group read", + "Umask755Description": "{octal} - Owner write, Everyone else read", + "Umask770Description": "{octal} - Owner & Group write", + "Umask775Description": "{octal} - Owner & Group write, Other read", + "Umask777Description": "{octal} - Everyone write", "UnableToLoadAutoTagging": "Unable to load auto tagging", "UnableToLoadBackups": "Unable to load backups", "UnableToLoadRootFolders": "Unable to load root folders", @@ -1095,8 +1204,10 @@ "Username": "Username", "UtcAirDate": "UTC Air Date", "Version": "Version", + "VersionNumber": "Version {version}", "VideoCodec": "Video Codec", "VideoDynamicRange": "Video Dynamic Range", + "View": "View", "VisitTheWikiForMoreDetails": "Visit the wiki for more details: ", "WaitingToImport": "Waiting to Import", "WaitingToProcess": "Waiting to Process",