feat(frontend): add french language file

also expanded translation coverage (still lots to do!)
pull/192/head
sct 4 years ago
parent 2aefcfdfb9
commit cd6d8a8216

@ -9,7 +9,7 @@
"build": "yarn build:next && yarn build:server",
"lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\"",
"start": "NODE_ENV=production node dist/index.js",
"i18n:extract": "extract-messages -l=en,ja -o src/i18n/locale -d en --flat true './src/**/!(*.test).{ts,tsx}'",
"i18n:extract": "extract-messages -l=en,ja,fr -o src/i18n/locale -d en --flat true './src/**/!(*.test).{ts,tsx}'",
"migration:generate": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:generate",
"migration:run": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:run",
"format": "prettier --write ."

@ -6,18 +6,8 @@ import TheMovieDb, {
} from '../api/themoviedb';
import { mapMovieResult, mapTvResult, mapPersonResult } from '../models/Search';
import Media from '../entity/Media';
const isMovie = (
movie: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
): movie is TmdbMovieResult => {
return (movie as TmdbMovieResult).title !== undefined;
};
const isPerson = (
person: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
): person is TmdbPersonResult => {
return (person as TmdbPersonResult).known_for !== undefined;
};
import { isMovie, isPerson } from '../utils/typeHelpers';
import { MediaType } from '../constants/media';
const discoverRoutes = Router();
@ -65,7 +55,9 @@ discoverRoutes.get('/movies/upcoming', async (req, res) => {
results: data.results.map((result) =>
mapMovieResult(
result,
media.find((req) => req.tmdbId === result.id)
media.find(
(med) => med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
)
)
),
});
@ -90,7 +82,9 @@ discoverRoutes.get('/tv', async (req, res) => {
results: data.results.map((result) =>
mapTvResult(
result,
media.find((req) => req.tmdbId === result.id)
media.find(
(med) => med.tmdbId === result.id && med.mediaType === MediaType.TV
)
)
),
});
@ -116,13 +110,16 @@ discoverRoutes.get('/trending', async (req, res) => {
isMovie(result)
? mapMovieResult(
result,
media.find((req) => req.tmdbId === result.id)
media.find(
(req) =>
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
)
: isPerson(result)
? mapPersonResult(result)
: mapTvResult(
result,
media.find((req) => req.tmdbId === result.id)
media.find((req) => req.tmdbId === result.id && MediaType.TV)
)
),
});

@ -133,6 +133,17 @@ settingsRoutes.post('/radarr', (req, res) => {
const lastItem = settings.radarr[settings.radarr.length - 1];
newRadarr.id = lastItem ? lastItem.id + 1 : 0;
// If we are setting this as the default, clear any previous defaults for the same type first
// ex: if is4k is true, it will only remove defaults for other servers that have is4k set to true
// and are the default
if (req.body.isDefault) {
settings.radarr
.filter((radarrInstance) => radarrInstance.is4k === req.body.is4k)
.forEach((radarrInstance) => {
radarrInstance.isDefault = false;
});
}
settings.radarr = [...settings.radarr, newRadarr];
settings.save();
@ -181,6 +192,17 @@ settingsRoutes.put<{ id: string }>('/radarr/:id', (req, res) => {
.json({ status: '404', message: 'Settings instance not found' });
}
// If we are setting this as the default, clear any previous defaults for the same type first
// ex: if is4k is true, it will only remove defaults for other servers that have is4k set to true
// and are the default
if (req.body.isDefault) {
settings.radarr
.filter((radarrInstance) => radarrInstance.is4k === req.body.is4k)
.forEach((radarrInstance) => {
radarrInstance.isDefault = false;
});
}
settings.radarr[radarrIndex] = {
...req.body,
id: Number(req.params.id),
@ -252,6 +274,17 @@ settingsRoutes.post('/sonarr', (req, res) => {
const lastItem = settings.sonarr[settings.sonarr.length - 1];
newSonarr.id = lastItem ? lastItem.id + 1 : 0;
// If we are setting this as the default, clear any previous defaults for the same type first
// ex: if is4k is true, it will only remove defaults for other servers that have is4k set to true
// and are the default
if (req.body.isDefault) {
settings.sonarr
.filter((sonarrInstance) => sonarrInstance.is4k === req.body.is4k)
.forEach((sonarrInstance) => {
sonarrInstance.isDefault = false;
});
}
settings.sonarr = [...settings.sonarr, newSonarr];
settings.save();
@ -300,6 +333,17 @@ settingsRoutes.put<{ id: string }>('/sonarr/:id', (req, res) => {
.json({ status: '404', message: 'Settings instance not found' });
}
// If we are setting this as the default, clear any previous defaults for the same type first
// ex: if is4k is true, it will only remove defaults for other servers that have is4k set to true
// and are the default
if (req.body.isDefault) {
settings.sonarr
.filter((sonarrInstance) => sonarrInstance.is4k === req.body.is4k)
.forEach((sonarrInstance) => {
sonarrInstance.isDefault = false;
});
}
settings.sonarr[sonarrIndex] = {
...req.body,
id: Number(req.params.id),

@ -0,0 +1,17 @@
import type {
TmdbMovieResult,
TmdbTvResult,
TmdbPersonResult,
} from '../api/themoviedb';
export const isMovie = (
movie: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
): movie is TmdbMovieResult => {
return (movie as TmdbMovieResult).title !== undefined;
};
export const isPerson = (
person: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
): person is TmdbPersonResult => {
return (person as TmdbPersonResult).known_for !== undefined;
};

@ -25,6 +25,10 @@ const availableLanguages: AvailableLanguageObject = {
code: 'ja',
display: '日本語',
},
fr: {
code: 'fr',
display: 'Français',
},
};
const LanguagePicker: React.FC = () => {

@ -3,6 +3,11 @@ import Transition from '../../Transition';
import { useUser } from '../../../hooks/useUser';
import axios from 'axios';
import useClickOutside from '../../../hooks/useClickOutside';
import { defineMessages, FormattedMessage } from 'react-intl';
const messages = defineMessages({
signout: 'Sign Out',
});
const UserDropdown: React.FC = () => {
const dropdownRef = useRef<HTMLDivElement>(null);
@ -56,7 +61,7 @@ const UserDropdown: React.FC = () => {
role="menuitem"
onClick={() => logout()}
>
Sign out
<FormattedMessage {...messages.signout} />
</a>
</div>
</div>

@ -5,6 +5,12 @@ import Sidebar from './Sidebar';
import Notifications from './Notifications';
import LanguagePicker from './LanguagePicker';
import { useRouter } from 'next/router';
import { defineMessages, FormattedMessage } from 'react-intl';
const messages = defineMessages({
alphawarning:
'This is ALPHA software. Almost everything is bound to be nearly broken and/or unstable. Please report issues to the Overseerr Github!',
});
const Layout: React.FC = ({ children }) => {
const [isSidebarOpen, setSidebarOpen] = useState(false);
@ -70,9 +76,7 @@ const Layout: React.FC = ({ children }) => {
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm leading-5 text-white">
This is ALPHA software. Almost everything is bound to be
nearly broken or unstable. Please report issues to the
Overseerr Github!
<FormattedMessage {...messages.alphawarning} />
</p>
<p className="mt-3 text-sm leading-5 md:mt-0 md:ml-6">
<a

@ -4,6 +4,11 @@ import { useUser } from '../../hooks/useUser';
import axios from 'axios';
import { useRouter } from 'next/dist/client/router';
import ImageFader from '../Common/ImageFader';
import { defineMessages, FormattedMessage } from 'react-intl';
const messages = defineMessages({
signinplex: 'Sign in to continue',
});
const Login: React.FC = () => {
const [authToken, setAuthToken] = useState<string | undefined>(undefined);
@ -51,7 +56,7 @@ const Login: React.FC = () => {
alt="Overseerr Logo"
/>
<h2 className="mt-2 text-center text-3xl leading-9 font-extrabold text-gray-100">
Log in to continue
<FormattedMessage {...messages.signinplex} />
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md relative z-50">

@ -1,5 +1,12 @@
import React, { useState } from 'react';
import PlexOAuth from '../../utils/plex';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
loginwithplex: 'Login with Plex',
loading: 'Loading...',
loggingin: 'Logging in...',
});
const plexOAuth = new PlexOAuth();
@ -12,13 +19,16 @@ const PlexLoginButton: React.FC<PlexLoginButtonProps> = ({
onAuthToken,
onError,
}) => {
const [loading, setLoading] = useState<boolean>(false);
const intl = useIntl();
const [loading, setLoading] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const getPlexLogin = async () => {
setLoading(true);
try {
const authToken = await plexOAuth.login();
setLoading(false);
setIsProcessing(true);
onAuthToken(authToken);
} catch (e) {
if (onError) {
@ -35,10 +45,14 @@ const PlexLoginButton: React.FC<PlexLoginButtonProps> = ({
plexOAuth.preparePopup();
setTimeout(() => getPlexLogin(), 1500);
}}
disabled={loading}
disabled={loading || isProcessing}
className="plex-button"
>
{loading ? 'Loading...' : 'Login with Plex'}
{loading
? intl.formatMessage(messages.loading)
: isProcessing
? intl.formatMessage(messages.loggingin)
: intl.formatMessage(messages.loginwithplex)}
</button>
</span>
);

@ -1,11 +1,46 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import Transition from '../Transition';
import Modal from '../Common/Modal';
import Transition from '../../Transition';
import Modal from '../../Common/Modal';
import { Formik, Field } from 'formik';
import type { RadarrSettings } from '../../../server/lib/settings';
import type { RadarrSettings } from '../../../../server/lib/settings';
import * as Yup from 'yup';
import axios from 'axios';
import { useToasts } from 'react-toast-notifications';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
createradarr: 'Create New Radarr Server',
editradarr: 'Edit Radarr Server',
validationHostnameRequired: 'You must provide a hostname/IP',
validationPortRequired: 'You must provide a port',
validationApiKeyRequired: 'You must provide an API key',
validationRootFolderRequired: 'You must select a root folder',
validationProfileRequired: 'You must select a profile',
toastRadarrTestSuccess: 'Radarr connection established!',
toastRadarrTestFailure: 'Failed to connect to Radarr Server',
saving: 'Saving...',
save: 'Save Changes',
add: 'Add Server',
test: 'Test',
testing: 'Testing...',
defaultserver: 'Default Server',
servername: 'Server Name',
servernamePlaceholder: 'A Radarr Server',
hostname: 'Hostname',
port: 'Port',
ssl: 'SSL',
apiKey: 'API Key',
apiKeyPlaceholder: 'Your Radarr API Key',
baseUrl: 'Base URL',
baseUrlPlaceholder: 'Example: /radarr',
qualityprofile: 'Quality Profile',
rootfolder: 'Root Folder',
minimumAvailability: 'Minimum Availability',
server4k: '4K Server',
selectQualityProfile: 'Select a Quality Profile',
selectRootFolder: 'Select a Root Folder',
selectMinimumAvailability: 'Select minimum availability',
});
interface TestResponse {
profiles: {
@ -29,6 +64,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
radarr,
onSave,
}) => {
const intl = useIntl();
const initialLoad = useRef(false);
const { addToast } = useToasts();
const [isValidated, setIsValidated] = useState(radarr ? true : false);
@ -38,11 +74,17 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
rootFolders: [],
});
const RadarrSettingsSchema = Yup.object().shape({
hostname: Yup.string().required('You must provide a hostname/IP'),
port: Yup.number().required('You must provide a port'),
apiKey: Yup.string().required('You must provide an API Key'),
rootFolder: Yup.string().required('You must select a root folder'),
activeProfileId: Yup.string().required('You must select a profile'),
hostname: Yup.string().required(
intl.formatMessage(messages.validationHostnameRequired)
),
port: Yup.number().required(
intl.formatMessage(messages.validationPortRequired)
),
apiKey: Yup.string().required(intl.formatMessage(messages.apiKey)),
rootFolder: Yup.string().required(intl.formatMessage(messages.rootfolder)),
activeProfileId: Yup.string().required(
intl.formatMessage(messages.validationProfileRequired)
),
});
const testConnection = useCallback(
@ -75,7 +117,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
setIsValidated(true);
setTestResponse(response.data);
if (initialLoad.current) {
addToast('Radarr connection established!', {
addToast(intl.formatMessage(messages.toastRadarrTestSuccess), {
appearance: 'success',
autoDismiss: true,
});
@ -83,7 +125,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
} catch (e) {
setIsValidated(false);
if (initialLoad.current) {
addToast('Failed to connect to Radarr server', {
addToast(intl.formatMessage(messages.toastRadarrTestFailure), {
appearance: 'error',
autoDismiss: true,
});
@ -183,13 +225,17 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
okButtonType="primary"
okText={
isSubmitting
? 'Saving...'
? intl.formatMessage(messages.saving)
: !!radarr
? 'Save Changes'
: 'Add Server'
? intl.formatMessage(messages.save)
: intl.formatMessage(messages.add)
}
secondaryButtonType="warning"
secondaryText={isTesting ? 'Testing...' : 'Test'}
secondaryText={
isTesting
? intl.formatMessage(messages.testing)
: intl.formatMessage(messages.test)
}
onSecondary={() => {
if (values.apiKey && values.hostname && values.port) {
testConnection({
@ -207,7 +253,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
okDisabled={!isValidated || isSubmitting || isTesting}
onOk={() => handleSubmit()}
title={
!radarr ? 'Create New Radarr Server' : 'Edit Radarr Server'
!radarr
? intl.formatMessage(messages.createradarr)
: intl.formatMessage(messages.editradarr)
}
>
<div className="mb-6">
@ -216,7 +264,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
htmlFor="isDefault"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Default Server
{intl.formatMessage(messages.defaultserver)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
@ -232,7 +280,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Server Name
{intl.formatMessage(messages.servername)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -240,7 +288,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
id="name"
name="name"
type="text"
placeholder="A Radarr Server"
placeholder={intl.formatMessage(
messages.servernamePlaceholder
)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsValidated(false);
setFieldValue('name', e.target.value);
@ -258,7 +308,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
htmlFor="hostname"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Hostname
{intl.formatMessage(messages.hostname)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -284,7 +334,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
htmlFor="port"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Port
{intl.formatMessage(messages.port)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
@ -308,7 +358,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
htmlFor="ssl"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
SSL
{intl.formatMessage(messages.ssl)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
@ -328,7 +378,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
htmlFor="apiKey"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
API Key
{intl.formatMessage(messages.apiKey)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -336,7 +386,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
id="apiKey"
name="apiKey"
type="text"
placeholder="Your Radarr API Key"
placeholder={intl.formatMessage(
messages.apiKeyPlaceholder
)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsValidated(false);
setFieldValue('apiKey', e.target.value);
@ -354,7 +406,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
htmlFor="baseUrl"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Base URL
{intl.formatMessage(messages.baseUrl)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -362,7 +414,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
id="baseUrl"
name="baseUrl"
type="text"
placeholder="Example: /radarr"
placeholder={intl.formatMessage(
messages.baseUrlPlaceholder
)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsValidated(false);
setFieldValue('baseUrl', e.target.value);
@ -380,7 +434,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
htmlFor="activeProfileId"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Quality Profile
{intl.formatMessage(messages.qualityprofile)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -390,6 +444,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
name="activeProfileId"
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
>
<option value="">
{intl.formatMessage(messages.selectQualityProfile)}
</option>
{testResponse.profiles.length > 0 &&
testResponse.profiles.map((profile) => (
<option
@ -413,7 +470,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
htmlFor="rootFolder"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Root Folder
{intl.formatMessage(messages.rootfolder)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -423,6 +480,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
name="rootFolder"
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
>
<option value="">
{intl.formatMessage(messages.selectRootFolder)}
</option>
{testResponse.rootFolders.length > 0 &&
testResponse.rootFolders.map((folder) => (
<option
@ -446,7 +506,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
htmlFor="minimumAvailability"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Minimum Availability
{intl.formatMessage(messages.minimumAvailability)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -456,6 +516,11 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
name="minimumAvailability"
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
>
<option value="">
{intl.formatMessage(
messages.selectMinimumAvailability
)}
</option>
<option value="announced">Announced</option>
<option value="inCinemas">In Cinemas</option>
<option value="released">Released</option>
@ -469,7 +534,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
htmlFor="is4k"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Ultra HD Server
{intl.formatMessage(messages.server4k)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field

@ -14,6 +14,25 @@ import Transition from '../Transition';
import axios from 'axios';
import SonarrModal from './SonarrModal';
const messages = defineMessages({
radarrsettings: 'Radarr Settings',
radarrSettingsDescription:
'Configure your Radarr connection below. You can have multiple Radarr configurations but only two can be active as defaults at any time (one for standard HD and one for 4K). Administrations can override a titles connection to use in the manage title screen.',
sonarrsettings: 'Sonarr Settings',
sonarrSettingsDescription:
'Configure your Sonarr connection below. You can have multiple Sonarr configurations but only two can be active as defaults at any time (one for standard HD and one for 4K). Administrations can override a titles connection to use in the manage title screen.',
deleteserverconfirm: 'Are you sure you want to delete this server?',
edit: 'Edit',
delete: 'Delete',
ssl: 'SSL',
default: 'Default',
default4k: 'Default 4K',
address: 'Address',
activeProfile: 'Active Profile',
addradarr: 'Add Radarr Server',
addsonarr: 'Add Sonarr Server',
});
interface ServerInstanceProps {
name: string;
isDefault?: boolean;
@ -45,16 +64,33 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
<h3 className="text-white text-sm leading-5 font-medium truncate">
{name}
</h3>
{isDefault && <Badge>Default</Badge>}
{isDefault4K && <Badge badgeType="warning">Default 4K</Badge>}
{isSSL && <Badge badgeType="success">SSL</Badge>}
{isDefault && (
<Badge>
<FormattedMessage {...messages.default} />
</Badge>
)}
{isDefault4K && (
<Badge badgeType="warning">
<FormattedMessage {...messages.default4k} />
</Badge>
)}
{isSSL && (
<Badge badgeType="success">
<FormattedMessage {...messages.ssl} />
</Badge>
)}
</div>
<p className="mt-1 text-gray-300 text-sm leading-5 truncate">
<span className="font-bold mr-2">Address</span>
<span className="font-bold mr-2">
<FormattedMessage {...messages.address} />
</span>
{address}
</p>
<p className="mt-1 text-gray-300 text-sm leading-5 truncate">
<span className="font-bold mr-2">Active Profile</span> {profileName}
<span className="font-bold mr-2">
<FormattedMessage {...messages.activeProfile} />
</span>{' '}
{profileName}
</p>
</div>
<img
@ -78,7 +114,9 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
>
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
<span className="ml-3">Edit</span>
<span className="ml-3">
<FormattedMessage {...messages.edit} />
</span>
</button>
</div>
<div className="-ml-px w-0 flex-1 flex">
@ -98,7 +136,9 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
clipRule="evenodd"
/>
</svg>
<span className="ml-3">Delete</span>
<span className="ml-3">
<FormattedMessage {...messages.delete} />
</span>
</button>
</div>
</div>
@ -155,13 +195,10 @@ const SettingsServices: React.FC = () => {
<>
<div>
<h3 className="text-lg leading-6 font-medium text-gray-200">
Radarr Settings
<FormattedMessage {...messages.radarrsettings} />
</h3>
<p className="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
Configure your Radarr connection below. You can have multiple Radarr
configurations but only two can be active as defaults at any time (one
for standard HD and one for 4K). Administrations can override a titles
connection to use in the manage title screen.
<FormattedMessage {...messages.radarrSettingsDescription} />
</p>
</div>
{editRadarrModal.open && (
@ -206,7 +243,7 @@ const SettingsServices: React.FC = () => {
}
title="Delete Server"
>
Are you sure you want to delete this server?
<FormattedMessage {...messages.deleteserverconfirm} />
</Modal>
</Transition>
<div className="mt-6 sm:mt-5">
@ -252,7 +289,7 @@ const SettingsServices: React.FC = () => {
clipRule="evenodd"
/>
</svg>
Add Radarr Server
<FormattedMessage {...messages.addradarr} />
</Button>
</div>
</li>
@ -261,13 +298,10 @@ const SettingsServices: React.FC = () => {
</div>
<div className="mt-10">
<h3 className="text-lg leading-6 font-medium text-gray-200">
Sonarr Settings
<FormattedMessage {...messages.sonarrsettings} />
</h3>
<p className="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
Configure your Sonarr connection below. You can have multiple Sonarr
configurations but only two can be active as defaults at any time (one
for standard HD and one for 4K). Administrations can override a titles
connection to use in the manage title screen.
<FormattedMessage {...messages.sonarrSettingsDescription} />
</p>
</div>
<div className="mt-6 sm:mt-5">
@ -314,7 +348,7 @@ const SettingsServices: React.FC = () => {
clipRule="evenodd"
/>
</svg>
Add Sonarr Server
<FormattedMessage {...messages.addsonarr} />
</Button>
</div>
</li>

@ -1,11 +1,45 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import Transition from '../Transition';
import Modal from '../Common/Modal';
import Transition from '../../Transition';
import Modal from '../../Common/Modal';
import { Formik, Field } from 'formik';
import type { SonarrSettings } from '../../../server/lib/settings';
import type { SonarrSettings } from '../../../../server/lib/settings';
import * as Yup from 'yup';
import axios from 'axios';
import { useToasts } from 'react-toast-notifications';
import { useIntl, defineMessages } from 'react-intl';
const messages = defineMessages({
createsonarr: 'Create New Sonarr Server',
editsonarr: 'Edit Sonarr Server',
validationHostnameRequired: 'You must provide a hostname/IP',
validationPortRequired: 'You must provide a port',
validationApiKeyRequired: 'You must provide an API key',
validationRootFolderRequired: 'You must select a root folder',
validationProfileRequired: 'You must select a profile',
toastRadarrTestSuccess: 'Sonarr connection established!',
toastRadarrTestFailure: 'Failed to connect to Sonarr Server',
saving: 'Saving...',
save: 'Save Changes',
add: 'Add Server',
test: 'Test',
testing: 'Testing...',
defaultserver: 'Default Server',
servername: 'Server Name',
servernamePlaceholder: 'A Sonarr Server',
hostname: 'Hostname',
port: 'Port',
ssl: 'SSL',
apiKey: 'API Key',
apiKeyPlaceholder: 'Your Sonarr API Key',
baseUrl: 'Base URL',
baseUrlPlaceholder: 'Example: /sonarr',
qualityprofile: 'Quality Profile',
rootfolder: 'Root Folder',
seasonfolders: 'Season Folders',
server4k: '4K Server',
selectQualityProfile: 'Select a Quality Profile',
selectRootFolder: 'Select a Root Folder',
});
interface TestResponse {
profiles: {
@ -29,6 +63,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
sonarr,
onSave,
}) => {
const intl = useIntl();
const initialLoad = useRef(false);
const { addToast } = useToasts();
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
@ -38,11 +73,21 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
rootFolders: [],
});
const SonarrSettingsSchema = Yup.object().shape({
hostname: Yup.string().required('You must provide a hostname/IP'),
port: Yup.number().required('You must provide a port'),
apiKey: Yup.string().required('You must provide an API Key'),
rootFolder: Yup.string().required('You must select a root folder'),
activeProfileId: Yup.string().required('You must select a profile'),
hostname: Yup.string().required(
intl.formatMessage(messages.validationHostnameRequired)
),
port: Yup.number().required(
intl.formatMessage(messages.validationPortRequired)
),
apiKey: Yup.string().required(
intl.formatMessage(messages.validationApiKeyRequired)
),
rootFolder: Yup.string().required(
intl.formatMessage(messages.validationRootFolderRequired)
),
activeProfileId: Yup.string().required(
intl.formatMessage(messages.validationProfileRequired)
),
});
const testConnection = useCallback(
@ -183,13 +228,17 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
okButtonType="primary"
okText={
isSubmitting
? 'Saving...'
? intl.formatMessage(messages.saving)
: !!sonarr
? 'Save Changes'
: 'Add Server'
? intl.formatMessage(messages.save)
: intl.formatMessage(messages.add)
}
secondaryButtonType="warning"
secondaryText={isTesting ? 'Testing...' : 'Test'}
secondaryText={
isTesting
? intl.formatMessage(messages.testing)
: intl.formatMessage(messages.test)
}
onSecondary={() => {
if (values.apiKey && values.hostname && values.port) {
testConnection({
@ -207,7 +256,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
okDisabled={!isValidated || isSubmitting || isTesting}
onOk={() => handleSubmit()}
title={
!sonarr ? 'Create New Sonarr Server' : 'Edit Sonarr Server'
!sonarr
? intl.formatMessage(messages.createsonarr)
: intl.formatMessage(messages.editsonarr)
}
>
<div className="mb-6">
@ -216,7 +267,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
htmlFor="isDefault"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Default Server
{intl.formatMessage(messages.defaultserver)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
@ -232,7 +283,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Server Name
{intl.formatMessage(messages.servername)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -240,7 +291,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
id="name"
name="name"
type="text"
placeholder="A Sonarr Server"
placeholder={intl.formatMessage(
messages.servernamePlaceholder
)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsValidated(false);
setFieldValue('name', e.target.value);
@ -258,7 +311,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
htmlFor="hostname"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Hostname
{intl.formatMessage(messages.hostname)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -284,7 +337,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
htmlFor="port"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Port
{intl.formatMessage(messages.port)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
@ -308,7 +361,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
htmlFor="ssl"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
SSL
{intl.formatMessage(messages.ssl)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
@ -328,7 +381,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
htmlFor="apiKey"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
API Key
{intl.formatMessage(messages.apiKey)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -336,7 +389,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
id="apiKey"
name="apiKey"
type="text"
placeholder="Your Sonarr API Key"
placeholder={intl.formatMessage(
messages.apiKeyPlaceholder
)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsValidated(false);
setFieldValue('apiKey', e.target.value);
@ -354,7 +409,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
htmlFor="baseUrl"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Base URL
{intl.formatMessage(messages.baseUrl)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -362,7 +417,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
id="baseUrl"
name="baseUrl"
type="text"
placeholder="Example: /sonarr"
placeholder={intl.formatMessage(
messages.baseUrlPlaceholder
)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsValidated(false);
setFieldValue('baseUrl', e.target.value);
@ -380,7 +437,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
htmlFor="activeProfileId"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Quality Profile
{intl.formatMessage(messages.qualityprofile)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -390,7 +447,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
name="activeProfileId"
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
>
<option value="">Select a Quality Profile</option>
<option value="">
{intl.formatMessage(messages.selectQualityProfile)}
</option>
{testResponse.profiles.length > 0 &&
testResponse.profiles.map((profile) => (
<option
@ -414,7 +473,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
htmlFor="rootFolder"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Root Folder
{intl.formatMessage(messages.rootfolder)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -424,7 +483,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
name="rootFolder"
className="mt-1 form-select block rounded-md w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
>
<option value="">Select a Root Folder</option>
<option value="">
{intl.formatMessage(messages.selectRootFolder)}
</option>
{testResponse.rootFolders.length > 0 &&
testResponse.rootFolders.map((folder) => (
<option
@ -448,7 +509,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
htmlFor="is4k"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Ultra HD Server
{intl.formatMessage(messages.server4k)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
@ -464,7 +525,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
htmlFor="enableSeasonFolders"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Season Folders
{intl.formatMessage(messages.seasonfolders)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field

@ -2,6 +2,12 @@ import React, { useEffect, useState } from 'react';
import { useUser } from '../../hooks/useUser';
import PlexLoginButton from '../PlexLoginButton';
import axios from 'axios';
import { defineMessages, FormattedMessage } from 'react-intl';
const messages = defineMessages({
welcome: 'Welcome to Overseerr',
signinMessage: 'Get started by logging in with your Plex account',
});
interface LoginWithPlexProps {
onComplete: () => void;
@ -39,10 +45,10 @@ const LoginWithPlex: React.FC<LoginWithPlexProps> = ({ onComplete }) => {
return (
<form>
<div className="flex justify-center font-bold text-xl mb-2">
Welcome to Overseerr
<FormattedMessage {...messages.welcome} />
</div>
<div className="flex justify-center text-sm pb-6 mb-2">
Get started by logging in with your Plex account
<FormattedMessage {...messages.signinMessage} />
</div>
<div className="flex items-center justify-center">
<PlexLoginButton onAuthToken={(authToken) => setAuthToken(authToken)} />

@ -7,15 +7,19 @@ import SettingsServices from '../Settings/SettingsServices';
import LoginWithPlex from './LoginWithPlex';
import SetupSteps from './SetupSteps';
import axios from 'axios';
import { defineMessages, FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
const messages = defineMessages({
finish: 'Finish Setup',
finishing: 'Finishing...',
continue: 'Continue',
loginwithplex: 'Login with Plex',
configureplex: 'Configure Plex',
configureservices: 'Configure Services',
});
const Setup: React.FC = () => {
const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false);
const [currentStep, setCurrentStep] = useState(1);
const [plexSettingsComplete, setPlexSettingsComplete] = useState(false);
@ -43,7 +47,7 @@ const Setup: React.FC = () => {
'/images/rotate4.jpg',
]}
/>
<div className="px-4 sm:px-2 md:px-0 sm:mx-auto sm:w-full sm:max-w-2xl relative z-50">
<div className="px-4 sm:px-2 md:px-0 sm:mx-auto sm:w-full sm:max-w-4xl relative z-50">
<img
src="/logo.png"
className="mx-auto max-h-32 w-auto mb-10"
@ -56,19 +60,19 @@ const Setup: React.FC = () => {
>
<SetupSteps
stepNumber={1}
description={'Login with Plex'}
description={intl.formatMessage(messages.loginwithplex)}
active={currentStep === 1}
completed={currentStep > 1}
/>
<SetupSteps
stepNumber={2}
description={'Configure Plex'}
description={intl.formatMessage(messages.configureplex)}
active={currentStep === 2}
completed={currentStep > 2}
/>
<SetupSteps
stepNumber={3}
description={'Configure Services'}
description={intl.formatMessage(messages.configureservices)}
active={currentStep === 3}
isLastStep
/>

@ -1,6 +1,6 @@
import React, { ReactNode } from 'react';
export type AvailableLocales = 'en' | 'ja';
export type AvailableLocales = 'en' | 'ja' | 'fr';
interface LanguageContextProps {
locale: AvailableLocales;

@ -15,6 +15,9 @@
"components.Layout.Sidebar.requests": "Requests",
"components.Layout.Sidebar.settings": "Settings",
"components.Layout.Sidebar.users": "Users",
"components.Layout.UserDropdown.signout": "Sign Out",
"components.Layout.alphawarning": "This is ALPHA software. Almost everything is bound to be nearly broken and/or unstable. Please report issues to the Overseerr Github!",
"components.Login.signinplex": "Sign in to continue",
"components.MovieDetails.available": "Available",
"components.MovieDetails.budget": "Budget",
"components.MovieDetails.cancelrequest": "Cancel Request",
@ -39,10 +42,85 @@
"components.PendingRequest.decline": "Decline",
"components.PendingRequest.pendingdescription": "This title was requested by {username} ({email}) on {date}",
"components.PendingRequest.pendingtitle": "Pending Request",
"components.PlexLoginButton.loading": "Loading...",
"components.PlexLoginButton.loggingin": "Logging in...",
"components.PlexLoginButton.loginwithplex": "Login with Plex",
"components.RequestModal.cancelrequest": "This will remove your request. Are you sure you want to continue?",
"components.RequestModal.requestadmin": "Your request will be immediately approved.",
"components.Settings.Notifications.save": "Save Changes",
"components.Settings.Notifications.saving": "Saving...",
"components.Settings.RadarrModal.add": "Add Server",
"components.Settings.RadarrModal.apiKey": "API Key",
"components.Settings.RadarrModal.apiKeyPlaceholder": "Your Radarr API Key",
"components.Settings.RadarrModal.baseUrl": "Base URL",
"components.Settings.RadarrModal.baseUrlPlaceholder": "Example: /radarr",
"components.Settings.RadarrModal.createradarr": "Create New Radarr Server",
"components.Settings.RadarrModal.defaultserver": "Default Server",
"components.Settings.RadarrModal.editradarr": "Edit Radarr Server",
"components.Settings.RadarrModal.hostname": "Hostname",
"components.Settings.RadarrModal.minimumAvailability": "Minimum Availability",
"components.Settings.RadarrModal.port": "Port",
"components.Settings.RadarrModal.qualityprofile": "Quality Profile",
"components.Settings.RadarrModal.rootfolder": "Root Folder",
"components.Settings.RadarrModal.save": "Save Changes",
"components.Settings.RadarrModal.saving": "Saving...",
"components.Settings.RadarrModal.selectMinimumAvailability": "Select minimum availability",
"components.Settings.RadarrModal.selectQualityProfile": "Select a Quality Profile",
"components.Settings.RadarrModal.selectRootFolder": "Select a Root Folder",
"components.Settings.RadarrModal.server4k": "4K Server",
"components.Settings.RadarrModal.servername": "Server Name",
"components.Settings.RadarrModal.servernamePlaceholder": "A Radarr Server",
"components.Settings.RadarrModal.ssl": "SSL",
"components.Settings.RadarrModal.test": "Test",
"components.Settings.RadarrModal.testing": "Testing...",
"components.Settings.RadarrModal.toastRadarrTestFailure": "Failed to connect to Radarr Server",
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Radarr connection established!",
"components.Settings.RadarrModal.validationApiKeyRequired": "You must provide an API key",
"components.Settings.RadarrModal.validationHostnameRequired": "You must provide a hostname/IP",
"components.Settings.RadarrModal.validationPortRequired": "You must provide a port",
"components.Settings.RadarrModal.validationProfileRequired": "You must select a profile",
"components.Settings.RadarrModal.validationRootFolderRequired": "You must select a root folder",
"components.Settings.SonarrModal.add": "Add Server",
"components.Settings.SonarrModal.apiKey": "API Key",
"components.Settings.SonarrModal.apiKeyPlaceholder": "Your Sonarr API Key",
"components.Settings.SonarrModal.baseUrl": "Base URL",
"components.Settings.SonarrModal.baseUrlPlaceholder": "Example: /sonarr",
"components.Settings.SonarrModal.createsonarr": "Create New Sonarr Server",
"components.Settings.SonarrModal.defaultserver": "Default Server",
"components.Settings.SonarrModal.editsonarr": "Edit Sonarr Server",
"components.Settings.SonarrModal.hostname": "Hostname",
"components.Settings.SonarrModal.port": "Port",
"components.Settings.SonarrModal.qualityprofile": "Quality Profile",
"components.Settings.SonarrModal.rootfolder": "Root Folder",
"components.Settings.SonarrModal.save": "Save Changes",
"components.Settings.SonarrModal.saving": "Saving...",
"components.Settings.SonarrModal.seasonfolders": "Season Folders",
"components.Settings.SonarrModal.selectQualityProfile": "Select a Quality Profile",
"components.Settings.SonarrModal.selectRootFolder": "Select a Root Folder",
"components.Settings.SonarrModal.server4k": "4K Server",
"components.Settings.SonarrModal.servername": "Server Name",
"components.Settings.SonarrModal.servernamePlaceholder": "A Sonarr Server",
"components.Settings.SonarrModal.ssl": "SSL",
"components.Settings.SonarrModal.test": "Test",
"components.Settings.SonarrModal.testing": "Testing...",
"components.Settings.SonarrModal.toastRadarrTestFailure": "Failed to connect to Sonarr Server",
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Sonarr connection established!",
"components.Settings.SonarrModal.validationApiKeyRequired": "You must provide an API key",
"components.Settings.SonarrModal.validationHostnameRequired": "You must provide a hostname/IP",
"components.Settings.SonarrModal.validationPortRequired": "You must provide a port",
"components.Settings.SonarrModal.validationProfileRequired": "You must select a profile",
"components.Settings.SonarrModal.validationRootFolderRequired": "You must select a root folder",
"components.Settings.activeProfile": "Active Profile",
"components.Settings.addradarr": "Add Radarr Server",
"components.Settings.address": "Address",
"components.Settings.addsonarr": "Add Sonarr Server",
"components.Settings.cancelscan": "Cancel Scan",
"components.Settings.currentlibrary": "Current Library: {name}",
"components.Settings.default": "Default",
"components.Settings.default4k": "Default 4K",
"components.Settings.delete": "Delete",
"components.Settings.deleteserverconfirm": "Are you sure you want to delete this server?",
"components.Settings.edit": "Edit",
"components.Settings.hostname": "Hostname/IP",
"components.Settings.librariesRemaining": "Libraries Remaining: {count}",
"components.Settings.manualscan": "Manual Library Scan",
@ -53,16 +131,26 @@
"components.Settings.plexsettings": "Plex Settings",
"components.Settings.plexsettingsDescription": "Configure the settings for your Plex server. Overseerr uses your Plex server to scan your library at an interval and see what content is available.",
"components.Settings.port": "Port",
"components.Settings.radarrSettingsDescription": "Configure your Radarr connection below. You can have multiple Radarr configurations but only two can be active as defaults at any time (one for standard HD and one for 4K). Administrations can override a titles connection to use in the manage title screen.",
"components.Settings.radarrsettings": "Radarr Settings",
"components.Settings.save": "Save Changes",
"components.Settings.saving": "Saving...",
"components.Settings.servername": "Server Name (Automatically Set)",
"components.Settings.servernamePlaceholder": "Plex Server Name",
"components.Settings.sonarrSettingsDescription": "Configure your Sonarr connection below. You can have multiple Sonarr configurations but only two can be active as defaults at any time (one for standard HD and one for 4K). Administrations can override a titles connection to use in the manage title screen.",
"components.Settings.sonarrsettings": "Sonarr Settings",
"components.Settings.ssl": "SSL",
"components.Settings.startscan": "Start Scan",
"components.Settings.sync": "Sync Plex Libraries",
"components.Settings.syncing": "Syncing",
"components.Setup.configureplex": "Configure Plex",
"components.Setup.configureservices": "Configure Services",
"components.Setup.continue": "Continue",
"components.Setup.finish": "Finish Setup",
"components.Setup.finishing": "Finishing...",
"components.Setup.loginwithplex": "Login with Plex",
"components.Setup.signinMessage": "Get started by logging in with your Plex account",
"components.Setup.welcome": "Welcome to Overseerr",
"components.Slider.noresults": "No Results",
"components.TvDetails.approverequests": "Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}",
"components.TvDetails.available": "Available",

@ -0,0 +1,202 @@
{
"components.Discover.discovermovies": "",
"components.Discover.discovertv": "",
"components.Discover.nopending": "",
"components.Discover.popularmovies": "",
"components.Discover.populartv": "",
"components.Discover.recentlyAdded": "",
"components.Discover.recentrequests": "",
"components.Discover.trending": "",
"components.Discover.upcoming": "",
"components.Discover.upcomingmovies": "",
"components.Layout.LanguagePicker.changelanguage": "",
"components.Layout.SearchInput.searchPlaceholder": "",
"components.Layout.Sidebar.dashboard": "",
"components.Layout.Sidebar.requests": "",
"components.Layout.Sidebar.settings": "",
"components.Layout.Sidebar.users": "",
"components.Layout.UserDropdown.signout": "",
"components.Layout.alphawarning": "",
"components.Login.signinplex": "",
"components.MovieDetails.available": "",
"components.MovieDetails.budget": "",
"components.MovieDetails.cancelrequest": "",
"components.MovieDetails.cast": "",
"components.MovieDetails.originallanguage": "",
"components.MovieDetails.overview": "",
"components.MovieDetails.overviewunavailable": "",
"components.MovieDetails.pending": "",
"components.MovieDetails.recommendations": "",
"components.MovieDetails.recommendationssubtext": "",
"components.MovieDetails.releasedate": "",
"components.MovieDetails.request": "",
"components.MovieDetails.revenue": "",
"components.MovieDetails.runtime": "",
"components.MovieDetails.similar": "",
"components.MovieDetails.similarsubtext": "",
"components.MovieDetails.status": "",
"components.MovieDetails.unavailable": "",
"components.MovieDetails.userrating": "",
"components.MovieDetails.viewrequest": "",
"components.PendingRequest.approve": "",
"components.PendingRequest.decline": "",
"components.PendingRequest.pendingdescription": "",
"components.PendingRequest.pendingtitle": "",
"components.PlexLoginButton.loading": "",
"components.PlexLoginButton.loggingin": "",
"components.PlexLoginButton.loginwithplex": "",
"components.RequestModal.cancelrequest": "",
"components.RequestModal.requestadmin": "",
"components.Settings.Notifications.save": "",
"components.Settings.Notifications.saving": "",
"components.Settings.RadarrModal.add": "",
"components.Settings.RadarrModal.apiKey": "",
"components.Settings.RadarrModal.apiKeyPlaceholder": "",
"components.Settings.RadarrModal.baseUrl": "",
"components.Settings.RadarrModal.baseUrlPlaceholder": "",
"components.Settings.RadarrModal.createradarr": "",
"components.Settings.RadarrModal.defaultserver": "",
"components.Settings.RadarrModal.editradarr": "",
"components.Settings.RadarrModal.hostname": "",
"components.Settings.RadarrModal.minimumAvailability": "",
"components.Settings.RadarrModal.port": "",
"components.Settings.RadarrModal.qualityprofile": "",
"components.Settings.RadarrModal.rootfolder": "",
"components.Settings.RadarrModal.save": "",
"components.Settings.RadarrModal.saving": "",
"components.Settings.RadarrModal.selectMinimumAvailability": "",
"components.Settings.RadarrModal.selectQualityProfile": "",
"components.Settings.RadarrModal.selectRootFolder": "",
"components.Settings.RadarrModal.server4k": "",
"components.Settings.RadarrModal.servername": "",
"components.Settings.RadarrModal.servernamePlaceholder": "",
"components.Settings.RadarrModal.ssl": "",
"components.Settings.RadarrModal.test": "",
"components.Settings.RadarrModal.testing": "",
"components.Settings.RadarrModal.toastRadarrTestFailure": "",
"components.Settings.RadarrModal.toastRadarrTestSuccess": "",
"components.Settings.RadarrModal.validationApiKeyRequired": "",
"components.Settings.RadarrModal.validationHostnameRequired": "",
"components.Settings.RadarrModal.validationPortRequired": "",
"components.Settings.RadarrModal.validationProfileRequired": "",
"components.Settings.RadarrModal.validationRootFolderRequired": "",
"components.Settings.SonarrModal.add": "",
"components.Settings.SonarrModal.apiKey": "",
"components.Settings.SonarrModal.apiKeyPlaceholder": "",
"components.Settings.SonarrModal.baseUrl": "",
"components.Settings.SonarrModal.baseUrlPlaceholder": "",
"components.Settings.SonarrModal.createsonarr": "",
"components.Settings.SonarrModal.defaultserver": "",
"components.Settings.SonarrModal.editsonarr": "",
"components.Settings.SonarrModal.hostname": "",
"components.Settings.SonarrModal.port": "",
"components.Settings.SonarrModal.qualityprofile": "",
"components.Settings.SonarrModal.rootfolder": "",
"components.Settings.SonarrModal.save": "",
"components.Settings.SonarrModal.saving": "",
"components.Settings.SonarrModal.seasonfolders": "",
"components.Settings.SonarrModal.selectQualityProfile": "",
"components.Settings.SonarrModal.selectRootFolder": "",
"components.Settings.SonarrModal.server4k": "",
"components.Settings.SonarrModal.servername": "",
"components.Settings.SonarrModal.servernamePlaceholder": "",
"components.Settings.SonarrModal.ssl": "",
"components.Settings.SonarrModal.test": "",
"components.Settings.SonarrModal.testing": "",
"components.Settings.SonarrModal.toastRadarrTestFailure": "",
"components.Settings.SonarrModal.toastRadarrTestSuccess": "",
"components.Settings.SonarrModal.validationApiKeyRequired": "",
"components.Settings.SonarrModal.validationHostnameRequired": "",
"components.Settings.SonarrModal.validationPortRequired": "",
"components.Settings.SonarrModal.validationProfileRequired": "",
"components.Settings.SonarrModal.validationRootFolderRequired": "",
"components.Settings.activeProfile": "",
"components.Settings.addradarr": "",
"components.Settings.address": "",
"components.Settings.addsonarr": "",
"components.Settings.cancelscan": "",
"components.Settings.currentlibrary": "",
"components.Settings.default": "",
"components.Settings.default4k": "",
"components.Settings.delete": "",
"components.Settings.deleteserverconfirm": "",
"components.Settings.edit": "",
"components.Settings.hostname": "",
"components.Settings.librariesRemaining": "",
"components.Settings.manualscan": "",
"components.Settings.manualscanDescription": "",
"components.Settings.notrunning": "",
"components.Settings.plexlibraries": "",
"components.Settings.plexlibrariesDescription": "",
"components.Settings.plexsettings": "",
"components.Settings.plexsettingsDescription": "",
"components.Settings.port": "",
"components.Settings.radarrSettingsDescription": "",
"components.Settings.radarrsettings": "",
"components.Settings.save": "",
"components.Settings.saving": "",
"components.Settings.servername": "",
"components.Settings.servernamePlaceholder": "",
"components.Settings.sonarrSettingsDescription": "",
"components.Settings.sonarrsettings": "",
"components.Settings.ssl": "",
"components.Settings.startscan": "",
"components.Settings.sync": "",
"components.Settings.syncing": "",
"components.Setup.configureplex": "",
"components.Setup.configureservices": "",
"components.Setup.continue": "",
"components.Setup.finish": "",
"components.Setup.finishing": "",
"components.Setup.loginwithplex": "",
"components.Setup.signinMessage": "",
"components.Setup.welcome": "",
"components.Slider.noresults": "",
"components.TvDetails.approverequests": "",
"components.TvDetails.available": "",
"components.TvDetails.cancelrequest": "",
"components.TvDetails.cast": "",
"components.TvDetails.declinerequests": "",
"components.TvDetails.originallanguage": "",
"components.TvDetails.overview": "",
"components.TvDetails.overviewunavailable": "",
"components.TvDetails.pending": "",
"components.TvDetails.recommendations": "",
"components.TvDetails.recommendationssubtext": "",
"components.TvDetails.request": "",
"components.TvDetails.requestmore": "",
"components.TvDetails.similar": "",
"components.TvDetails.similarsubtext": "",
"components.TvDetails.status": "",
"components.TvDetails.unavailable": "",
"components.TvDetails.userrating": "",
"components.UserEdit.admin": "",
"components.UserEdit.adminDescription": "",
"components.UserEdit.autoapprove": "",
"components.UserEdit.autoapproveDescription": "",
"components.UserEdit.avatar": "",
"components.UserEdit.edituser": "",
"components.UserEdit.email": "",
"components.UserEdit.managerequests": "",
"components.UserEdit.managerequestsDescription": "",
"components.UserEdit.permissions": "",
"components.UserEdit.request": "",
"components.UserEdit.requestDescription": "",
"components.UserEdit.save": "",
"components.UserEdit.saving": "",
"components.UserEdit.settings": "",
"components.UserEdit.settingsDescription": "",
"components.UserEdit.userfail": "",
"components.UserEdit.username": "",
"components.UserEdit.users": "",
"components.UserEdit.usersDescription": "",
"components.UserEdit.usersaved": "",
"components.UserEdit.vote": "",
"components.UserEdit.voteDescription": "",
"pages.internalServerError": "",
"pages.oops": "",
"pages.pageNotFound": "",
"pages.returnHome": "",
"pages.serviceUnavailable": "",
"pages.somethingWentWrong": ""
}

@ -15,6 +15,9 @@
"components.Layout.Sidebar.requests": "リクエスト",
"components.Layout.Sidebar.settings": "設定",
"components.Layout.Sidebar.users": "",
"components.Layout.UserDropdown.signout": "",
"components.Layout.alphawarning": "",
"components.Login.signinplex": "",
"components.MovieDetails.available": "",
"components.MovieDetails.budget": "興行収入",
"components.MovieDetails.cancelrequest": "チャンセルリクエスト",
@ -39,10 +42,85 @@
"components.PendingRequest.decline": "",
"components.PendingRequest.pendingdescription": "",
"components.PendingRequest.pendingtitle": "",
"components.PlexLoginButton.loading": "",
"components.PlexLoginButton.loggingin": "",
"components.PlexLoginButton.loginwithplex": "",
"components.RequestModal.cancelrequest": "このリクエストをキャンセルしてよろしいですか?",
"components.RequestModal.requestadmin": "このリクエストが今すぐ承認致します。よろしいですか?",
"components.Settings.Notifications.save": "",
"components.Settings.Notifications.saving": "",
"components.Settings.RadarrModal.add": "",
"components.Settings.RadarrModal.apiKey": "",
"components.Settings.RadarrModal.apiKeyPlaceholder": "",
"components.Settings.RadarrModal.baseUrl": "",
"components.Settings.RadarrModal.baseUrlPlaceholder": "",
"components.Settings.RadarrModal.createradarr": "",
"components.Settings.RadarrModal.defaultserver": "",
"components.Settings.RadarrModal.editradarr": "",
"components.Settings.RadarrModal.hostname": "",
"components.Settings.RadarrModal.minimumAvailability": "",
"components.Settings.RadarrModal.port": "",
"components.Settings.RadarrModal.qualityprofile": "",
"components.Settings.RadarrModal.rootfolder": "",
"components.Settings.RadarrModal.save": "",
"components.Settings.RadarrModal.saving": "",
"components.Settings.RadarrModal.selectMinimumAvailability": "",
"components.Settings.RadarrModal.selectQualityProfile": "",
"components.Settings.RadarrModal.selectRootFolder": "",
"components.Settings.RadarrModal.server4k": "",
"components.Settings.RadarrModal.servername": "",
"components.Settings.RadarrModal.servernamePlaceholder": "",
"components.Settings.RadarrModal.ssl": "",
"components.Settings.RadarrModal.test": "",
"components.Settings.RadarrModal.testing": "",
"components.Settings.RadarrModal.toastRadarrTestFailure": "",
"components.Settings.RadarrModal.toastRadarrTestSuccess": "",
"components.Settings.RadarrModal.validationApiKeyRequired": "",
"components.Settings.RadarrModal.validationHostnameRequired": "",
"components.Settings.RadarrModal.validationPortRequired": "",
"components.Settings.RadarrModal.validationProfileRequired": "",
"components.Settings.RadarrModal.validationRootFolderRequired": "",
"components.Settings.SonarrModal.add": "",
"components.Settings.SonarrModal.apiKey": "",
"components.Settings.SonarrModal.apiKeyPlaceholder": "",
"components.Settings.SonarrModal.baseUrl": "",
"components.Settings.SonarrModal.baseUrlPlaceholder": "",
"components.Settings.SonarrModal.createsonarr": "",
"components.Settings.SonarrModal.defaultserver": "",
"components.Settings.SonarrModal.editsonarr": "",
"components.Settings.SonarrModal.hostname": "",
"components.Settings.SonarrModal.port": "",
"components.Settings.SonarrModal.qualityprofile": "",
"components.Settings.SonarrModal.rootfolder": "",
"components.Settings.SonarrModal.save": "",
"components.Settings.SonarrModal.saving": "",
"components.Settings.SonarrModal.seasonfolders": "",
"components.Settings.SonarrModal.selectQualityProfile": "",
"components.Settings.SonarrModal.selectRootFolder": "",
"components.Settings.SonarrModal.server4k": "",
"components.Settings.SonarrModal.servername": "",
"components.Settings.SonarrModal.servernamePlaceholder": "",
"components.Settings.SonarrModal.ssl": "",
"components.Settings.SonarrModal.test": "",
"components.Settings.SonarrModal.testing": "",
"components.Settings.SonarrModal.toastRadarrTestFailure": "",
"components.Settings.SonarrModal.toastRadarrTestSuccess": "",
"components.Settings.SonarrModal.validationApiKeyRequired": "",
"components.Settings.SonarrModal.validationHostnameRequired": "",
"components.Settings.SonarrModal.validationPortRequired": "",
"components.Settings.SonarrModal.validationProfileRequired": "",
"components.Settings.SonarrModal.validationRootFolderRequired": "",
"components.Settings.activeProfile": "",
"components.Settings.addradarr": "",
"components.Settings.address": "",
"components.Settings.addsonarr": "",
"components.Settings.cancelscan": "",
"components.Settings.currentlibrary": "",
"components.Settings.default": "",
"components.Settings.default4k": "",
"components.Settings.delete": "",
"components.Settings.deleteserverconfirm": "",
"components.Settings.edit": "",
"components.Settings.hostname": "",
"components.Settings.librariesRemaining": "",
"components.Settings.manualscan": "",
@ -53,16 +131,26 @@
"components.Settings.plexsettings": "",
"components.Settings.plexsettingsDescription": "",
"components.Settings.port": "",
"components.Settings.radarrSettingsDescription": "",
"components.Settings.radarrsettings": "",
"components.Settings.save": "",
"components.Settings.saving": "",
"components.Settings.servername": "",
"components.Settings.servernamePlaceholder": "",
"components.Settings.sonarrSettingsDescription": "",
"components.Settings.sonarrsettings": "",
"components.Settings.ssl": "",
"components.Settings.startscan": "",
"components.Settings.sync": "",
"components.Settings.syncing": "",
"components.Setup.configureplex": "",
"components.Setup.configureservices": "",
"components.Setup.continue": "",
"components.Setup.finish": "",
"components.Setup.finishing": "",
"components.Setup.loginwithplex": "",
"components.Setup.signinMessage": "",
"components.Setup.welcome": "",
"components.Slider.noresults": "",
"components.TvDetails.approverequests": "",
"components.TvDetails.available": "",

Loading…
Cancel
Save