Translate Frontend Components, Episode and Helpers

This commit is contained in:
Stevie Robinson 2023-08-13 23:04:18 +02:00 committed by GitHub
parent 074aa6f445
commit e777b70184
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 482 additions and 177 deletions

View File

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

View File

@ -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}
>
<ModalHeader>
File Browser
{translate('FileBrowser')}
</ModalHeader>
<ModalBody
@ -117,13 +117,13 @@ class FileBrowserModalContent extends Component {
className={styles.mappedDrivesWarning}
kind={kinds.WARNING}
>
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server">FAQ</Link> for more information.
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService')} />
</Alert>
}
<PathInput
className={styles.pathInput}
placeholder="Start typing or select a path below"
placeholder={translate('FileBrowserPlaceholderText')}
hasFileBrowser={false}
{...otherProps}
value={this.state.currentPath}
@ -137,7 +137,7 @@ class FileBrowserModalContent extends Component {
>
{
!!error &&
<div>Error loading contents</div>
<div>{translate('ErrorLoadingContents')}</div>
}
{
@ -151,7 +151,7 @@ class FileBrowserModalContent extends Component {
emptyParent &&
<FileBrowserRow
type="computer"
name="My Computer"
name={translate('MyComputer')}
path={parent}
onPress={this.onRowPress}
/>
@ -212,13 +212,13 @@ class FileBrowserModalContent extends Component {
<Button
onPress={onModalClose}
>
Cancel
{translate('Cancel')}
</Button>
<Button
onPress={this.onOkPress}
>
Ok
{translate('Ok')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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 :
<div className={styles.or}>
or
{translate('or')}
</div>
}
</div>

View File

@ -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) {

View File

@ -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) {

View File

@ -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 {
/>
<SpinnerIconButton
title="Remove filter"
title={translate('RemoveFilter')}
name={icons.REMOVE}
isSpinning={this.state.isDeleting}
onPress={this.onRemovePress}

View File

@ -5,6 +5,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 CustomFilter from './CustomFilter';
import styles from './CustomFiltersModalContent.css';
@ -24,7 +25,7 @@ function CustomFiltersModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Custom Filters
{translate('CustomFilters')}
</ModalHeader>
<ModalBody>
@ -49,7 +50,7 @@ function CustomFiltersModalContent(props) {
<div className={styles.addButtonContainer}>
<Button onPress={onAddCustomFilter}>
Add Custom Filter
{translate('AddCustomFilter')}
</Button>
</div>
</ModalBody>
@ -58,7 +59,7 @@ function CustomFiltersModalContent(props) {
<Button
onPress={onModalClose}
>
Close
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>

View File

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

View File

@ -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) {
<Link
to={helpLink}
>
More Info
{translate('MoreInfo')}
</Link>
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 :
<div className={styles.freeSpace}>
{formatBytes(freeSpace)} Free
{translate('RootFolderSelectFreeSpace', { freeSpace: formatBytes(freeSpace) })}
</div>
}
{
isMissing ?
<div className={styles.isMissing}>
Missing
{translate('Missing')}
</div> :
null
}

View File

@ -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 &&
<div className={styles.freeSpace}>
{formatBytes(freeSpace)} Free
{translate('RootFolderSelectFreeSpace', { freeSpace: formatBytes(freeSpace) })}
</div>
}
</EnhancedSelectInputSelectedValue>

View File

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

View File

@ -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 {
</div>
<div className={styles.details}>
<div>
<label>UMask</label>
<label>{translate('UMask')}</label>
<div className={styles.value}>{umask}</div>
</div>
<div>
<label>Folder</label>
<label>{translate('Folder')}</label>
<div className={styles.value}>{folder}</div>
<div className={styles.unit}>d{formatPermissions(folderNum)}</div>
</div>
<div>
<label>File</label>
<label>{translate('File')}</label>
<div className={styles.value}>{file}</div>
<div className={styles.unit}>{formatPermissions(fileNum)}</div>
</div>

View File

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

View File

@ -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 {
<ButtonComponent
iconName={icons.FILTER}
showIndicator={selectedFilterKey !== 'all'}
text="Filter"
text={translate('Filter')}
isDisabled={isDisabled}
/>

View File

@ -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 &&
<MenuItem onPress={onCustomFiltersPress}>
Custom Filters
{translate('CustomFilters')}
</MenuItem>
}
</MenuContent>

View File

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

View File

@ -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) {
>
<ToolbarMenuButton
iconName={icons.SORT}
text="Sort"
text={translate('Sort')}
isDisabled={isDisabled}
/>
{children}

View File

@ -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) {
>
<ToolbarMenuButton
iconName={icons.VIEW}
text="View"
text={translate('View')}
isDisabled={isDisabled}
/>
{children}

View File

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

View File

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Error
{translate('Error')}
</ModalHeader>
<ModalBody>
@ -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')}
/>
</ModalBody>
@ -33,7 +34,7 @@ function ModalError(props) {
<Button
onPress={onModalClose}
>
Close
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>

View File

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

View File

@ -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 (
<PageContent title="MIA">
<div className={styles.container}>
@ -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;

View File

@ -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) {
</div>
<div className={styles.version}>
Version {version}
{translate('VersionNumber', { version })}
</div>
</div>
);

View File

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Keyboard Shortcuts
{translate('KeyboardShortcuts')}
</ModalHeader>
<ModalBody>
@ -75,7 +76,7 @@ function KeyboardShortcutsModalContent(props) {
<Button
onPress={onModalClose}
>
Close
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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 {
<IconButton
className={styles.donate}
name={icons.HEART}
aria-label="Donate"
aria-label={translate('Donate')}
to="https://sonarr.tv/donate.html"
size={14}
/>

View File

@ -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')}
</MenuItem>
<MenuItemSeparator />
@ -42,7 +43,7 @@ function PageHeaderActionsMenu(props) {
className={styles.itemIcon}
name={icons.RESTART}
/>
Restart
{translate('Restart')}
</MenuItem>
<MenuItem onPress={onShutdownPress}>
@ -51,7 +52,7 @@ function PageHeaderActionsMenu(props) {
name={icons.SHUTDOWN}
kind={kinds.DANGER}
/>
Shutdown
{translate('Shutdown')}
</MenuItem>
{
@ -69,7 +70,7 @@ function PageHeaderActionsMenu(props) {
className={styles.itemIcon}
name={icons.LOGOUT}
/>
Logout
{translate('Logout')}
</MenuItem>
}
</MenuContent>

View File

@ -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 (
<div className={styles.addNewSeriesSuggestion}>
Search for {query}
{translate('SearchForQuery', { query })}
</div>
);
}
@ -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,

View File

@ -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) {
<PageContentBody>
<ErrorBoundaryError
{...props}
message='There was an error loading this page'
message={translate('ErrorLoadingPage')}
/>
</PageContentBody>
</div>

View File

@ -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 {
<ToolbarMenuButton
className={styles.overflowMenuButton}
iconName={icons.OVERFLOW}
text="More"
text={translate('More')}
/>
<MenuContent>

View File

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

View File

@ -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 ?
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Table Options
{translate('TableOptions')}
</ModalHeader>
<ModalBody>
@ -144,13 +145,13 @@ class TableOptionsModal extends Component {
{
hasPageSize ?
<FormGroup>
<FormLabel>Page Size</FormLabel>
<FormLabel>{translate('PageSize')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="pageSize"
value={pageSize || 0}
helpText="Number of items to show on each page"
helpText={translate('TablePageSizeHelpText')}
errors={pageSizeError ? [{ message: pageSizeError }] : undefined}
onChange={this.onPageSizeChange}
/>
@ -168,11 +169,11 @@ class TableOptionsModal extends Component {
{
canModifyColumns ?
<FormGroup>
<FormLabel>Columns</FormLabel>
<FormLabel>{translate('TableColumns')}</FormLabel>
<div>
<FormInputHelpText
text="Choose which columns are visible and which order they appear in"
text={translate('TableColumnsHelpText')}
/>
<div className={styles.columns}>
@ -231,7 +232,7 @@ class TableOptionsModal extends Component {
<Button
onPress={onModalClose}
>
Close
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent> :

View File

@ -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 {
<div className={styles.recordsContainer}>
<div className={styles.records}>
Total records: {totalRecords}
{translate('TotalRecords', { totalRecords })}
</div>
</div>
</div>

View File

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

View File

@ -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')}
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
History
{translate('History')}
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
Search
{translate('Search')}
</Tab>
</TabList>
@ -173,14 +174,14 @@ class EpisodeDetailsModalContent extends Component {
to={seriesLink}
onPress={onModalClose}
>
Open Series
{translate('OpenSeries')}
</Button>
}
<Button
onPress={onModalClose}
>
Close
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -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')}
</Label>
}
title={'Languages'}
title={translate('Languages')}
body={
<ul>
{

View File

@ -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) {
}
</span>
}
title="Scene Information"
title={translate('SceneInformation')}
body={
<SceneInfo
seasonNumber={seasonNumber}

View File

@ -5,6 +5,7 @@ import Icon from 'Components/Icon';
import ProgressBar from 'Components/ProgressBar';
import { icons, kinds, sizes } from 'Helpers/Props';
import isBefore from 'Utilities/Date/isBefore';
import translate from 'Utilities/String/translate';
import EpisodeQuality from './EpisodeQuality';
import styles from './EpisodeStatus.css';
@ -50,7 +51,7 @@ function EpisodeStatus(props) {
<div className={styles.center}>
<Icon
name={icons.DOWNLOADING}
title="Episode is downloading"
title={translate('EpisodeIsDownloading')}
/>
</div>
);
@ -66,7 +67,7 @@ function EpisodeStatus(props) {
quality={quality}
size={episodeFile.size}
isCutoffNotMet={isCutoffNotMet}
title="Episode Downloaded"
title={translate('EpisodeDownloaded')}
/>
</div>
);
@ -77,7 +78,7 @@ function EpisodeStatus(props) {
<div className={styles.center}>
<Icon
name={icons.TBA}
title="TBA"
title={translate('Tba')}
/>
</div>
);
@ -89,7 +90,7 @@ function EpisodeStatus(props) {
<Icon
name={icons.UNMONITORED}
kind={kinds.DISABLED}
title="Episode is not monitored"
title={translate('EpisodeIsNotMonitored')}
/>
</div>
);
@ -100,7 +101,7 @@ function EpisodeStatus(props) {
<div className={styles.center}>
<Icon
name={icons.MISSING}
title="Episode missing from disk"
title={translate('EpisodeMissingFromDisk')}
/>
</div>
);
@ -110,7 +111,7 @@ function EpisodeStatus(props) {
<div className={styles.center}>
<Icon
name={icons.NOT_AIRED}
title="Episode has not aired"
title={translate('EpisodeHasNotAired')}
/>
</div>
);

View File

@ -79,13 +79,13 @@ class EpisodeHistory extends Component {
if (!isFetching && !!error) {
return (
<Alert kind={kinds.DANGER}>Unable to load episode history.</Alert>
<Alert kind={kinds.DANGER}>{translate('EpisodeHistoryLoadError')}</Alert>
);
}
if (isPopulated && !hasItems && !error) {
return (
<Alert kind={kinds.INFO}>No episode history.</Alert>
<Alert kind={kinds.INFO}>{translate('NoEpisodeHistory')}</Alert>
);
}

View File

@ -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' &&
<IconButton
title="Mark as failed"
title={translate('MarkAsFailed')}
name={icons.REMOVE}
onPress={this.onMarkAsFailedPress}
/>
@ -147,9 +148,9 @@ class EpisodeHistoryRow extends Component {
<ConfirmModal
isOpen={isMarkAsFailedModalOpen}
kind={kinds.DANGER}
title="Mark as Failed"
message={`Are you sure you want to mark '${sourceTitle}' as failed?`}
confirmLabel="Mark as Failed"
title={translate('MarkAsFailed')}
message={translate('MarkAsFailedConfirmation', { sourceTitle })}
confirmLabel={translate('MarkAsFailed')}
onConfirm={this.onConfirmMarkAsFailed}
onCancel={this.onMarkAsFailedModalClose}
/>

View File

@ -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) {
<DescriptionListItem
titleClassName={styles.title}
descriptionClassName={styles.description}
title="Season"
title={translate('Season')}
data={sceneSeasonNumber}
/>
}
@ -66,7 +67,7 @@ function SceneInfo(props) {
<DescriptionListItem
titleClassName={styles.title}
descriptionClassName={styles.description}
title="Episode"
title={translate('Episode')}
data={sceneEpisodeNumber}
/>
}
@ -76,7 +77,7 @@ function SceneInfo(props) {
<DescriptionListItem
titleClassName={styles.title}
descriptionClassName={styles.description}
title="Absolute"
title={translate('Absolute')}
data={sceneAbsoluteEpisodeNumber}
/>
}
@ -86,7 +87,7 @@ function SceneInfo(props) {
<DescriptionListItem
titleClassName={styles.title}
descriptionClassName={styles.description}
title={groupedAlternateTitles.length === 1 ? 'Title' : 'Titles'}
title={groupedAlternateTitles.length === 1 ? translate('Title') : translate('Titles')}
data={
<div>
{

View File

@ -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')}
</Button>
</div>
@ -40,7 +41,7 @@ function EpisodeSearch(props) {
name={icons.INTERACTIVE}
/>
Interactive Search
{translate('InteractiveSearch')}
</Button>
</div>
</div>

View File

@ -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) {
</Label>
);
// TODO: Update InlineMarkdown to accept tags and pass in networkLabel object, for now blank string passed into translation
if (!airDateUtc) {
return (
<span>
TBA on {networkLabel}
{translate('AirsTbaOn', { networkLabel: '' })}networkLabel
</span>
);
}
@ -39,7 +41,7 @@ function EpisodeAiring(props) {
if (!showRelativeDates) {
return (
<span>
{moment(airDateUtc).format(shortDateFormat)} at {time} on {networkLabel}
{translate('AirsDateAtTimeOn', { date: moment(airDateUtc).format(shortDateFormat), time, networkLabel: '' })}{networkLabel}
</span>
);
}
@ -47,7 +49,7 @@ function EpisodeAiring(props) {
if (isToday(airDateUtc)) {
return (
<span>
{time} on {networkLabel}
{translate('AirsTimeOn', { time, networkLabel: '' })}{networkLabel}
</span>
);
}
@ -55,7 +57,7 @@ function EpisodeAiring(props) {
if (isTomorrow(airDateUtc)) {
return (
<span>
Tomorrow at {time} on {networkLabel}
{translate('AirsTomorrowOn', { time, networkLabel: '' })}{networkLabel}
</span>
);
}
@ -63,14 +65,14 @@ function EpisodeAiring(props) {
if (isInNextWeek(airDateUtc)) {
return (
<span>
{moment(airDateUtc).format('dddd')} at {time} on {networkLabel}
{translate('AirsDateAtTimeOn', { date: moment(airDateUtc).format('dddd'), time, networkLabel: '' })}{networkLabel}
</span>
);
}
return (
<span>
{moment(airDateUtc).format(shortDateFormat)} at {time} on {networkLabel}
{translate('AirsDateAtTimeOn', { date: moment(airDateUtc).format(shortDateFormat), time, networkLabel: '' })}{networkLabel}
</span>
);
}

View File

@ -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={<MediaInfo {...mediaInfo} />}
position={tooltipPositions.LEFT}
/> :
@ -148,7 +149,7 @@ class EpisodeFileRow extends Component {
}
<IconButton
title="Delete episode from disk"
title={translate('DeleteEpisodeFromDisk')}
name={icons.REMOVE}
onPress={this.onRemoveEpisodeFilePress}
/>
@ -163,9 +164,9 @@ class EpisodeFileRow extends Component {
<ConfirmModal
isOpen={this.state.isRemoveEpisodeFileModalOpen}
kind={kinds.DANGER}
title="Delete Episode File"
message={`Are you sure you want to delete '${path}'?`}
confirmLabel="Delete"
title={translate('DeleteEpisodeFile')}
message={translate('DeleteEpisodeFileMessage', { path })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmRemoveEpisodeFile}
onCancel={this.onRemoveEpisodeFileModalClose}
/>

View File

@ -103,7 +103,7 @@ class EpisodeSummary extends Component {
return (
<div>
<div>
<span className={styles.infoTitle}>Airs</span>
<span className={styles.infoTitle}>{translate('Airs')}</span>
<EpisodeAiringConnector
airDateUtc={airDateUtc}
@ -112,7 +112,7 @@ class EpisodeSummary extends Component {
</div>
<div>
<span className={styles.infoTitle}>Quality Profile</span>
<span className={styles.infoTitle}>{translate('QualityProfile')}</span>
<Label
kind={kinds.PRIMARY}
@ -128,7 +128,7 @@ class EpisodeSummary extends Component {
{
hasOverview ?
overview :
'No episode overview.'
translate('NoEpisodeOverview')
}
</div>
@ -155,9 +155,9 @@ class EpisodeSummary extends Component {
<ConfirmModal
isOpen={this.state.isRemoveEpisodeFileModalOpen}
kind={kinds.DANGER}
title="Delete Episode File"
message={`Are you sure you want to delete '${path}'?`}
confirmLabel="Delete"
title={translate('DeleteEpisodeFile')}
message={translate('DeleteEpisodeFileMessage', { path })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmRemoveEpisodeFile}
onCancel={this.onRemoveEpisodeFileModalClose}
/>

View File

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

View File

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