feat(frontend): added more localized strings

pull/152/head
sct 4 years ago
parent 45d6a1e1c0
commit 659a601877

@ -5,6 +5,8 @@ import { useTransition, animated } from 'react-spring';
import { useLockBodyScroll } from '../../../hooks/useLockBodyScroll';
import LoadingSpinner from '../LoadingSpinner';
import useClickOutside from '../../../hooks/useClickOutside';
import { useIntl } from 'react-intl';
import globalMessages from '../../../i18n/globalMessages';
interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
title?: string;
@ -53,6 +55,7 @@ const Modal: React.FC<ModalProps> = ({
onTertiary,
...props
}) => {
const intl = useIntl();
const modalRef = useRef<HTMLDivElement>(null);
useClickOutside(modalRef, () => {
typeof onCancel === 'function' && backgroundClickable
@ -174,7 +177,9 @@ const Modal: React.FC<ModalProps> = ({
onClick={onCancel}
className="ml-3 sm:ml-0 sm:px-4"
>
{cancelText ? cancelText : 'Cancel'}
{cancelText
? cancelText
: intl.formatMessage(globalMessages.cancel)}
</Button>
)}
</div>

@ -35,7 +35,7 @@ const MovieRecommendations: React.FC = () => {
return `/api/v1/movie/${router.query.movieId}/recommendations?page=${
pageIndex + 1
}`;
}&language=${locale}`;
},
{
initialSize: 3,

@ -35,7 +35,7 @@ const MovieSimilar: React.FC = () => {
return `/api/v1/movie/${router.query.movieId}/similar?page=${
pageIndex + 1
}`;
}&language=${locale}`;
},
{
initialSize: 3,

@ -36,6 +36,7 @@ import RTAudRotten from '../../assets/rt_aud_rotten.svg';
import type { RTRating } from '../../../server/api/rottentomatoes';
import Error from '../../pages/_error';
import Head from 'next/head';
import globalMessages from '../../i18n/globalMessages';
const messages = defineMessages({
releasedate: 'Release Date',
@ -56,6 +57,14 @@ const messages = defineMessages({
viewrequest: 'View Request',
pending: 'Pending',
overviewunavailable: 'Overview unavailable',
manageModalTitle: 'Manage Movie',
manageModalRequests: 'Requests',
manageModalNoRequests: 'No Requests',
manageModalClearMedia: 'Clear All Media Data',
manageModalClearMediaWarning:
'This will remove all media data including all requests for this item. This action is irreversible. If this item exists in your Plex library, the media information will be recreated next sync.',
approve: 'Approve',
decline: 'Decline',
});
interface MovieDetailsProps {
@ -144,11 +153,13 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
/>
<SlideOver
show={showManager}
title="Manage Movie"
title={intl.formatMessage(messages.manageModalTitle)}
onClose={() => setShowManager(false)}
subText={data.title}
>
<h3 className="text-xl mb-2">Requests</h3>
<h3 className="text-xl mb-2">
{intl.formatMessage(messages.manageModalRequests)}
</h3>
<div className="bg-gray-600 shadow overflow-hidden rounded-md">
<ul>
{data.mediaInfo?.requests?.map((request) => (
@ -160,7 +171,9 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</li>
))}
{(data.mediaInfo?.requests ?? []).length === 0 && (
<li className="text-center py-4 text-gray-400">No requests</li>
<li className="text-center py-4 text-gray-400">
{intl.formatMessage(messages.manageModalNoRequests)}
</li>
)}
</ul>
</div>
@ -171,12 +184,10 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
className="w-full text-center"
onClick={() => deleteMedia()}
>
Clear All Media Data
{intl.formatMessage(messages.manageModalClearMedia)}
</Button>
<div className="text-sm text-gray-400 mt-2">
This will remove all media data including all requests for this
item. This action is irreversible. If this item exists in your
Plex library, the media information will be recreated next sync.
{intl.formatMessage(messages.manageModalClearMediaWarning)}
</div>
</div>
)}
@ -192,13 +203,19 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
<div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left">
<div className="mb-2">
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
<Badge badgeType="success">Available</Badge>
<Badge badgeType="success">
{intl.formatMessage(globalMessages.available)}
</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PROCESSING && (
<Badge badgeType="danger">Unavailable</Badge>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.unavailable)}
</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PENDING && (
<Badge badgeType="warning">Pending</Badge>
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.pending)}
</Badge>
)}
</div>
<h1 className="text-2xl md:text-4xl">
@ -309,7 +326,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
clipRule="evenodd"
/>
</svg>
Approve
{intl.formatMessage(messages.approve)}
</ButtonWithDropdown.Item>
<ButtonWithDropdown.Item
onClick={() => modifyRequest('decline')}
@ -326,7 +343,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
clipRule="evenodd"
/>
</svg>
Decline
{intl.formatMessage(messages.decline)}
</ButtonWithDropdown.Item>
</>
)}

@ -1,108 +0,0 @@
import React, { useState } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import Button from '../Common/Button';
import { MediaRequest } from '../../../server/entity/MediaRequest';
import axios from 'axios';
const messages = defineMessages({
pendingtitle: 'Pending Request',
pendingdescription:
'This title was requested by {username} ({email}) on {date}',
approve: 'Approve',
decline: 'Decline',
});
interface PendingRequestProps {
request: MediaRequest;
onUpdate: () => void;
}
const PendingRequest: React.FC<PendingRequestProps> = ({
request,
onUpdate,
}) => {
const intl = useIntl();
const [isLoading, setLoading] = useState(false);
const updateStatus = async (status: 'approve' | 'decline') => {
setLoading(true);
const response = await axios.get(`/api/v1/request/${request.id}/${status}`);
if (response.data) {
onUpdate();
setLoading(false);
}
};
return (
<div className="bg-gray-900 border border-gray-800 sm:rounded-lg mb-6 shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-100">
<FormattedMessage {...messages.pendingtitle} />
</h3>
<div className="mt-2 max-w-xl text-sm leading-5 text-gray-400">
<p>
<FormattedMessage
{...messages.pendingdescription}
values={{
username: request.requestedBy.username,
email: request.requestedBy.email,
date: intl.formatDate(request.updatedAt),
}}
/>
</p>
</div>
<div className="mt-5">
<span className="inline-flex rounded-md shadow-sm mr-2">
<Button
buttonType="success"
disabled={isLoading}
onClick={() => updateStatus('approve')}
>
<svg
className="w-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<FormattedMessage {...messages.approve} />
</Button>
</span>
<span className="inline-flex rounded-md shadow-sm">
<Button
buttonType="danger"
disabled={isLoading}
onClick={() => updateStatus('decline')}
>
<svg
className="w-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<FormattedMessage {...messages.decline} />
</Button>
</span>
</div>
</div>
</div>
);
};
export default PendingRequest;

@ -1,10 +1,15 @@
import React, { useState } from 'react';
import type { MediaRequest } from '../../../server/entity/MediaRequest';
import { FormattedDate } from 'react-intl';
import { FormattedDate, useIntl, defineMessages } from 'react-intl';
import Badge from '../Common/Badge';
import { MediaRequestStatus } from '../../../server/constants/media';
import Button from '../Common/Button';
import axios from 'axios';
import globalMessages from '../../i18n/globalMessages';
const messages = defineMessages({
seasons: 'Seasons',
});
interface RequestBlockProps {
request: MediaRequest;
@ -12,6 +17,7 @@ interface RequestBlockProps {
}
const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false);
const updateRequest = async (type: 'approve' | 'decline'): Promise<void> => {
@ -143,16 +149,24 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
<div className="sm:flex">
<div className="mr-6 flex items-center text-sm leading-5 text-gray-300">
{request.status === MediaRequestStatus.AVAILABLE && (
<Badge badgeType="success">Available</Badge>
<Badge badgeType="success">
{intl.formatMessage(globalMessages.available)}
</Badge>
)}
{request.status === MediaRequestStatus.APPROVED && (
<Badge badgeType="success">Approved</Badge>
<Badge badgeType="success">
{intl.formatMessage(globalMessages.approved)}
</Badge>
)}
{request.status === MediaRequestStatus.DECLINED && (
<Badge badgeType="danger">Declined</Badge>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.declined)}
</Badge>
)}
{request.status === MediaRequestStatus.PENDING && (
<Badge badgeType="warning">Pending</Badge>
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.pending)}
</Badge>
)}
</div>
</div>
@ -176,7 +190,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
</div>
{(request.seasons ?? []).length > 0 && (
<div className="mt-2 text-sm flex items-center">
<span className="mr-2">Seasons</span>
<span className="mr-2">{intl.formatMessage(messages.seasons)}</span>
{request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>

@ -19,6 +19,16 @@ const messages = defineMessages({
'Your request will be immediately approved. Do you wish to continue?',
cancelrequest:
'This will remove your request. Are you sure you want to continue?',
requestSuccess: '<strong>{title}</strong> successfully requested!',
requestCancel: 'Request for <strong>{title}</strong> cancelled',
requesttitle: 'Request {title}',
close: 'Close',
cancel: 'Cancel Request',
cancelling: 'Cancelling...',
pendingrequest: 'Pending request for {title}',
requesting: 'Requesting...',
request: 'Request',
requestfrom: 'There is currently a pending request from {username}',
});
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
@ -62,7 +72,12 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
}
addToast(
<span>
<strong>{data?.title}</strong> succesfully requested!
{intl.formatMessage(messages.requestSuccess, {
title: data?.title,
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
@ -84,7 +99,12 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
}
addToast(
<span>
<strong>{data?.title}</strong> request cancelled!
{intl.formatMessage(messages.cancelrequest, {
title: data?.title,
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
@ -109,15 +129,22 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
onCancel={onCancel}
onOk={isOwner ? () => cancelRequest() : undefined}
okDisabled={isUpdating}
title={`Pending request for ${data?.title}`}
okText={isUpdating ? 'Cancelling...' : 'Cancel Request'}
title={intl.formatMessage(messages.pendingrequest, {
title: data?.title,
})}
okText={
isUpdating
? intl.formatMessage(messages.cancelling)
: intl.formatMessage(messages.cancel)
}
okButtonType={'danger'}
cancelText="Close"
cancelText={intl.formatMessage(messages.close)}
iconSvg={<DownloadIcon className="w-6 h-6" />}
{...props}
>
There is currently a pending request from{' '}
<strong>{activeRequest.requestedBy.username}</strong>.
{intl.formatMessage(messages.requestfrom, {
username: activeRequest.requestedBy.username,
})}
</Modal>
);
}
@ -129,8 +156,12 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
onCancel={onCancel}
onOk={sendRequest}
okDisabled={isUpdating}
title={`Request ${data?.title}`}
okText={isUpdating ? 'Requesting...' : 'Request'}
title={intl.formatMessage(messages.requesttitle)}
okText={
isUpdating
? intl.formatMessage(messages.requesting)
: intl.formatMessage(messages.request)
}
okButtonType={'primary'}
iconSvg={<DownloadIcon className="w-6 h-6" />}
{...props}

@ -14,11 +14,25 @@ import {
import { TvDetails, SeasonWithEpisodes } from '../../../server/models/Tv';
import type SeasonRequest from '../../../server/entity/SeasonRequest';
import Badge from '../Common/Badge';
import globalMessages from '../../i18n/globalMessages';
const messages = defineMessages({
requestadmin: 'Your request will be immediately approved.',
cancelrequest:
'This will remove your request. Are you sure you want to continue?',
requestSuccess: '<strong>{title}</strong> successfully requested!',
requestCancel: 'Request for <strong>{title}</strong> cancelled',
requesttitle: 'Request {title}',
requesting: 'Requesting...',
requestseasons:
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}',
selectseason: 'Select season(s)',
season: 'Season',
numberofepisodes: '# of Episodes',
status: 'Status',
seasonnumber: 'Season {number}',
extras: 'Extras',
notrequested: 'Not Requested',
});
interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
@ -61,7 +75,12 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
}
addToast(
<span>
<strong>{data?.name}</strong> succesfully requested!
{intl.formatMessage(messages.requestSuccess, {
title: data?.name,
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
@ -177,11 +196,13 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
backgroundClickable
onCancel={onCancel}
onOk={() => sendRequest()}
title={`Request ${data?.name}`}
title={intl.formatMessage(messages.requesttitle, { title: data?.name })}
okText={
selectedSeasons.length === 0
? 'Select a season'
: `Request ${selectedSeasons.length} seasons`
? intl.formatMessage(messages.selectseason)
: intl.formatMessage(messages.requestseasons, {
seasonCount: selectedSeasons.length,
})
}
okDisabled={selectedSeasons.length === 0}
okButtonType="primary"
@ -238,13 +259,13 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
</span>
</th>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
Season
{intl.formatMessage(messages.season)}
</th>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
# Of Episodes
{intl.formatMessage(messages.numberofepisodes)}
</th>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
Status
{intl.formatMessage(messages.status)}
</th>
</tr>
</thead>
@ -303,39 +324,55 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm leading-5 font-medium text-gray-100">
{season.seasonNumber === 0
? 'Extras'
: `Season ${season.seasonNumber}`}
? intl.formatMessage(messages.extras)
: intl.formatMessage(messages.seasonnumber, {
number: season.seasonNumber,
})}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm leading-5 text-gray-200">
{season.episodeCount}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm leading-5 text-gray-200">
{!seasonRequest && !mediaSeason && (
<Badge>Not Requested</Badge>
<Badge>
{intl.formatMessage(messages.notrequested)}
</Badge>
)}
{!mediaSeason &&
seasonRequest?.status ===
MediaRequestStatus.PENDING && (
<Badge badgeType="warning">Pending</Badge>
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.pending)}
</Badge>
)}
{!mediaSeason &&
seasonRequest?.status ===
MediaRequestStatus.APPROVED && (
<Badge badgeType="danger">Unavailable</Badge>
<Badge badgeType="danger">
{intl.formatMessage(
globalMessages.unavailable
)}
</Badge>
)}
{!mediaSeason &&
seasonRequest?.status ===
MediaRequestStatus.AVAILABLE && (
<Badge badgeType="success">Available</Badge>
<Badge badgeType="success">
{intl.formatMessage(globalMessages.available)}
</Badge>
)}
{mediaSeason?.status ===
MediaStatus.PARTIALLY_AVAILABLE && (
<Badge badgeType="success">
Partially Available
{intl.formatMessage(
globalMessages.partiallyavailable
)}
</Badge>
)}
{mediaSeason?.status === MediaStatus.AVAILABLE && (
<Badge badgeType="success">Available</Badge>
<Badge badgeType="success">
{intl.formatMessage(globalMessages.available)}
</Badge>
)}
</td>
</tr>

@ -8,6 +8,13 @@ import {
import { useSWRInfinite } from 'swr';
import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../Common/Header';
import globalMessages from '../../i18n/globalMessages';
const messages = defineMessages({
searchresults: 'Search Results',
});
interface SearchResult {
page: number;
@ -17,6 +24,7 @@ interface SearchResult {
}
const Search: React.FC = () => {
const intl = useIntl();
const { locale } = useContext(LanguageContext);
const router = useRouter();
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
@ -58,25 +66,21 @@ const Search: React.FC = () => {
return (
<>
<div className="md:flex md:items-center md:justify-between mb-8 mt-6">
<div className="flex-1 min-w-0">
<h2 className="text-xl leading-7 text-white sm:text-2xl sm:leading-9 sm:truncate">
Search Results
</h2>
</div>
<div className="md:flex md:items-center md:justify-between mb-6 sm:mb-0 mt-6">
<Header>{intl.formatMessage(messages.searchresults)}</Header>
<div className="mt-4 flex md:mt-0 md:ml-4">
<span className="relative z-0 inline-flex shadow-sm rounded-md">
<button
type="button"
className="relative inline-flex items-center px-4 py-2 rounded-l-md border border-indigo-900 bg-indigo-500 hover:bg-indigo-400 text-sm leading-5 font-medium text-gray-100 hover:text-white focus:z-10 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
>
Movies
{intl.formatMessage(globalMessages.movies)}
</button>
<button
type="button"
className="-ml-px relative inline-flex items-center px-4 py-2 rounded-r-md border border-indigo-900 bg-indigo-500 text-sm leading-5 font-medium text-gray-100 hover:text-white focus:z-10 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
>
TV Shows
{intl.formatMessage(globalMessages.tvshows)}
</button>
</span>
</div>

@ -1,8 +1,14 @@
import React, { useEffect } from 'react';
import useClipboard from 'react-use-clipboard';
import { useToasts } from 'react-toast-notifications';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
copied: 'Copied API key to clipboard',
});
const CopyButton: React.FC<{ textToCopy: string }> = ({ textToCopy }) => {
const intl = useIntl();
const [isCopied, setCopied] = useClipboard(textToCopy, {
successDuration: 1000,
});
@ -10,12 +16,12 @@ const CopyButton: React.FC<{ textToCopy: string }> = ({ textToCopy }) => {
useEffect(() => {
if (isCopied) {
addToast('Copied API key to clipboard', {
addToast(intl.formatMessage(messages.copied), {
appearance: 'info',
autoDismiss: true,
});
}
}, [isCopied, addToast]);
}, [isCopied, addToast, intl]);
return (
<button

@ -10,6 +10,10 @@ import * as Yup from 'yup';
const messages = defineMessages({
save: 'Save Changes',
saving: 'Saving...',
agentenabled: 'Agent Enabled',
webhookUrl: 'Webhook URL',
validationWebhookUrlRequired: 'You must provide a webhook URL',
webhookUrlPlaceholder: 'Server Settings -> Integrations -> Webhooks',
});
const NotificationsDiscord: React.FC = () => {
@ -19,7 +23,9 @@ const NotificationsDiscord: React.FC = () => {
);
const NotificationsDiscordSchema = Yup.object().shape({
webhookUrl: Yup.string().required('You must provide a webhook URL'),
webhookUrl: Yup.string().required(
intl.formatMessage(messages.validationWebhookUrlRequired)
),
});
if (!data && !error) {
@ -58,7 +64,7 @@ const NotificationsDiscord: React.FC = () => {
htmlFor="isDefault"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Agent Enabled
{intl.formatMessage(messages.agentenabled)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
@ -74,7 +80,7 @@ const NotificationsDiscord: React.FC = () => {
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Webhook URL
{intl.formatMessage(messages.webhookUrl)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -82,7 +88,9 @@ const NotificationsDiscord: React.FC = () => {
id="webhookUrl"
name="webhookUrl"
type="text"
placeholder="Server Settings -> Integrations -> Webhooks"
placeholder={intl.formatMessage(
messages.webhookUrlPlaceholder
)}
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div>

@ -10,6 +10,16 @@ import * as Yup from 'yup';
const messages = defineMessages({
save: 'Save Changes',
saving: 'Saving...',
validationFromRequired: 'You must provide an email sender address',
validationSmtpHostRequired: 'You must provide an SMTP host',
validationSmtpPortRequired: 'You must provide an SMTP port',
agentenabled: 'Agent Enabled',
emailsender: 'Email Sender Address',
smtpHost: 'SMTP Host',
smtpPort: 'SMTP Port',
enableSsl: 'Enable SSL',
authUser: 'Auth User',
authPass: 'Auth Pass',
});
const NotificationsEmail: React.FC = () => {
@ -20,10 +30,14 @@ const NotificationsEmail: React.FC = () => {
const NotificationsDiscordSchema = Yup.object().shape({
emailFrom: Yup.string().required(
'You must provide an email sender address'
intl.formatMessage(messages.validationFromRequired)
),
smtpHost: Yup.string().required(
intl.formatMessage(messages.validationSmtpHostRequired)
),
smtpPort: Yup.number().required(
intl.formatMessage(messages.validationSmtpPortRequired)
),
smtpHost: Yup.string().required('You must provide an SMTP host'),
smtpPort: Yup.number().required('You must provide an SMTP port'),
});
if (!data && !error) {
@ -88,7 +102,7 @@ const NotificationsEmail: React.FC = () => {
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Email Sender Address
{intl.formatMessage(messages.emailsender)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -110,7 +124,7 @@ const NotificationsEmail: React.FC = () => {
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
SMTP Host
{intl.formatMessage(messages.smtpHost)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -132,7 +146,7 @@ const NotificationsEmail: React.FC = () => {
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
SMTP Port
{intl.formatMessage(messages.smtpPort)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -154,7 +168,7 @@ const NotificationsEmail: React.FC = () => {
htmlFor="isDefault"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Enable SSL
{intl.formatMessage(messages.enableSsl)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
@ -170,7 +184,7 @@ const NotificationsEmail: React.FC = () => {
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Auth User
{intl.formatMessage(messages.authUser)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -178,7 +192,6 @@ const NotificationsEmail: React.FC = () => {
id="authUser"
name="authUser"
type="text"
placeholder="localhost"
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div>
@ -189,7 +202,7 @@ const NotificationsEmail: React.FC = () => {
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Auth Pass
{intl.formatMessage(messages.authPass)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
@ -197,7 +210,6 @@ const NotificationsEmail: React.FC = () => {
id="authPass"
name="authPass"
type="password"
placeholder="localhost"
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div>

@ -1,13 +1,17 @@
import React from 'react';
import useSWR from 'swr';
import LoadingSpinner from '../Common/LoadingSpinner';
import Badge from '../Common/Badge';
import { FormattedDate, FormattedRelativeTime } from 'react-intl';
import { FormattedRelativeTime, defineMessages, useIntl } from 'react-intl';
import Button from '../Common/Button';
import { hasPermission } from '../../../server/lib/permissions';
import { Permission } from '../../hooks/useUser';
const messages = defineMessages({
jobname: 'Job Name',
nextexecution: 'Next Execution',
runnow: 'Run Now',
});
const SettingsJobs: React.FC = () => {
const intl = useIntl();
const { data, error } = useSWR<{ name: string; nextExecutionTime: string }[]>(
'/api/v1/settings/jobs'
);
@ -25,10 +29,10 @@ const SettingsJobs: React.FC = () => {
<thead>
<tr>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
Job Name
{intl.formatMessage(messages.jobname)}
</th>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
Next Execution
{intl.formatMessage(messages.nextexecution)}
</th>
<th className="px-6 py-3 bg-gray-500"></th>
</tr>
@ -54,7 +58,9 @@ const SettingsJobs: React.FC = () => {
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm leading-5 font-medium">
<Button buttonType="primary">Run Now</Button>
<Button buttonType="primary">
{intl.formatMessage(messages.runnow)}
</Button>
</td>
</tr>
))}

@ -1,6 +1,17 @@
import React from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
menuGeneralSettings: 'General Settings',
menuPlexSettings: 'Plex',
menuServices: 'Services',
menuNotifications: 'Notifications',
menuLogs: 'Logs',
menuJobs: 'Jobs',
menuAbout: 'About',
});
interface SettingsRoute {
text: string;
@ -8,46 +19,48 @@ interface SettingsRoute {
regex: RegExp;
}
const settingsRoutes: SettingsRoute[] = [
{
text: 'General Settings',
route: '/settings/main',
regex: /^\/settings(\/main)?$/,
},
{
text: 'Plex',
route: '/settings/plex',
regex: /^\/settings\/plex/,
},
{
text: 'Services',
route: '/settings/services',
regex: /^\/settings\/services/,
},
{
text: 'Notifications',
route: '/settings/notifications/email',
regex: /^\/settings\/notifications/,
},
{
text: 'Logs',
route: '/settings/logs',
regex: /^\/settings\/logs/,
},
{
text: 'Jobs',
route: '/settings/jobs',
regex: /^\/settings\/jobs/,
},
{
text: 'About',
route: '/settings/about',
regex: /^\/settings\/about/,
},
];
const SettingsLayout: React.FC = ({ children }) => {
const router = useRouter();
const intl = useIntl();
const settingsRoutes: SettingsRoute[] = [
{
text: intl.formatMessage(messages.menuGeneralSettings),
route: '/settings/main',
regex: /^\/settings(\/main)?$/,
},
{
text: intl.formatMessage(messages.menuPlexSettings),
route: '/settings/plex',
regex: /^\/settings\/plex/,
},
{
text: intl.formatMessage(messages.menuServices),
route: '/settings/services',
regex: /^\/settings\/services/,
},
{
text: intl.formatMessage(messages.menuNotifications),
route: '/settings/notifications/email',
regex: /^\/settings\/notifications/,
},
{
text: intl.formatMessage(messages.menuLogs),
route: '/settings/logs',
regex: /^\/settings\/logs/,
},
{
text: intl.formatMessage(messages.menuJobs),
route: '/settings/jobs',
regex: /^\/settings\/jobs/,
},
{
text: intl.formatMessage(messages.menuAbout),
route: '/settings/about',
regex: /^\/settings\/about/,
},
];
const activeLinkColor =
'border-indigo-600 text-indigo-500 focus:outline-none focus:text-indigo-500 focus:border-indigo-500';

@ -9,8 +9,13 @@ import Button from '../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
generalsettings: 'General Settings',
generalsettingsDescription:
'These are settings related to general Overseerr configuration.',
save: 'Save Changes',
saving: 'Saving...',
apikey: 'API Key',
applicationurl: 'Application URL',
});
const SettingsMain: React.FC = () => {
@ -27,10 +32,10 @@ const SettingsMain: React.FC = () => {
<>
<div>
<h3 className="text-lg leading-6 font-medium text-gray-200">
General Settings
{intl.formatMessage(messages.generalsettings)}
</h3>
<p className="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
These are settings related to general Overseerr configuration.
{intl.formatMessage(messages.generalsettingsDescription)}
</p>
</div>
<div className="mt-6 sm:mt-5">
@ -59,7 +64,7 @@ const SettingsMain: React.FC = () => {
htmlFor="username"
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">
@ -93,7 +98,7 @@ const SettingsMain: React.FC = () => {
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Application URL
{intl.formatMessage(messages.applicationurl)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">

@ -1,6 +1,13 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
notificationsettings: 'Notification Settings',
notificationsettingsDescription:
'Here you can pick and choose what types of notifications to send and through what types of services.',
});
interface SettingsRoute {
text: string;
@ -23,6 +30,7 @@ const settingsRoutes: SettingsRoute[] = [
const SettingsNotifications: React.FC = ({ children }) => {
const router = useRouter();
const intl = useIntl();
const activeLinkColor = 'bg-gray-700';
@ -55,11 +63,10 @@ const SettingsNotifications: React.FC = ({ children }) => {
<>
<div className="mb-6">
<h3 className="text-lg leading-6 font-medium text-gray-200">
Notification Settings
{intl.formatMessage(messages.notificationsettings)}
</h3>
<p className="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
Here you can pick and choose what types of notifications to send and
through what types of services.
{intl.formatMessage(messages.notificationsettingsDescription)}
</p>
</div>
<div>

@ -9,6 +9,12 @@ import Placeholder from './Placeholder';
import Link from 'next/link';
import { MediaStatus } from '../../../server/constants/media';
import RequestModal from '../RequestModal';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
movie: 'MOVIE',
tvshow: 'SERIES',
});
interface TitleCardProps {
id: number;
@ -32,6 +38,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
mediaType,
canExpand = false,
}) => {
const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false);
const [currentStatus, setCurrentStatus] = useState(status);
const [showDetail, setShowDetail] = useState(false);
@ -87,7 +94,9 @@ const TitleCard: React.FC<TitleCardProps> = ({
}`}
>
<div className="flex items-center text-center text-xs text-white h-4 px-2 py-1 font-normal">
{mediaType === 'movie' ? 'MOVIE' : 'TV SHOW'}
{mediaType === 'movie'
? intl.formatMessage(messages.movie)
: intl.formatMessage(messages.tvshow)}
</div>
</div>

@ -27,6 +27,7 @@ import RTAudFresh from '../../assets/rt_aud_fresh.svg';
import RTAudRotten from '../../assets/rt_aud_rotten.svg';
import type { RTRating } from '../../../server/api/rottentomatoes';
import Head from 'next/head';
import globalMessages from '../../i18n/globalMessages';
const messages = defineMessages({
userrating: 'User Rating',
@ -47,6 +48,14 @@ const messages = defineMessages({
'Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}',
declinerequests:
'Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}',
manageModalTitle: 'Manage Series',
manageModalRequests: 'Requests',
manageModalNoRequests: 'No Requests',
manageModalClearMedia: 'Clear All Media Data',
manageModalClearMediaWarning:
'This will remove all media data including all requests for this item. This action is irreversible. If this item exists in your Plex library, the media information will be recreated next sync.',
approve: 'Approve',
decline: 'Decline',
});
interface TvDetailsProps {
@ -151,7 +160,9 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
onClose={() => setShowManager(false)}
subText={data.name}
>
<h3 className="text-xl mb-2">Requests</h3>
<h3 className="text-xl mb-2">
{intl.formatMessage(messages.manageModalTitle)}
</h3>
<div className="bg-gray-600 shadow overflow-hidden rounded-md">
<ul>
{data.mediaInfo?.requests?.map((request) => (
@ -163,7 +174,9 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
</li>
))}
{(data.mediaInfo?.requests ?? []).length === 0 && (
<li className="text-center py-4 text-gray-400">No requests</li>
<li className="text-center py-4 text-gray-400">
{intl.formatMessage(messages.manageModalNoRequests)}
</li>
)}
</ul>
</div>
@ -174,12 +187,10 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
className="w-full text-center"
onClick={() => deleteMedia()}
>
Clear All Media Data
{intl.formatMessage(messages.manageModalClearMedia)}
</Button>
<div className="text-sm text-gray-400 mt-2">
This will remove all media data including all requests for this
item. This action is irreversible. If this item exists in your
Plex library, the media information will be recreated next sync.
{intl.formatMessage(messages.manageModalClearMediaWarning)}
</div>
</div>
)}
@ -195,16 +206,24 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left">
<div className="mb-2">
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
<Badge badgeType="success">Available</Badge>
<Badge badgeType="success">
{intl.formatMessage(globalMessages.available)}
</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE && (
<Badge badgeType="success">Partially Available</Badge>
<Badge badgeType="success">
{intl.formatMessage(globalMessages.partiallyavailable)}
</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PROCESSING && (
<Badge badgeType="danger">Unavailable</Badge>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.unavailable)}
</Badge>
)}
{data.mediaInfo?.status === MediaStatus.PENDING && (
<Badge badgeType="warning">Pending</Badge>
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.pending)}
</Badge>
)}
</div>
<h1 className="text-2xl md:text-4xl">

@ -145,7 +145,9 @@ const UserEdit: React.FC = () => {
return (
<>
<Header extraMargin={4}>Edit User</Header>
<Header extraMargin={4}>
<FormattedMessage {...messages.edituser} />
</Header>
<div className="px-4 space-y-6 sm:p-6 lg:pb-8">
<div className="flex flex-col space-y-6 lg:flex-row lg:space-y-0 lg:space-x-6 text-white">
<div className="flex-grow space-y-6">

@ -3,14 +3,30 @@ import useSWR from 'swr';
import LoadingSpinner from '../Common/LoadingSpinner';
import type { User } from '../../../server/entity/User';
import Badge from '../Common/Badge';
import { FormattedDate } from 'react-intl';
import { FormattedDate, defineMessages, useIntl } from 'react-intl';
import Button from '../Common/Button';
import { hasPermission } from '../../../server/lib/permissions';
import { Permission } from '../../hooks/useUser';
import { useRouter } from 'next/router';
import Header from '../Common/Header';
const messages = defineMessages({
userlist: 'User List',
username: 'Username',
totalrequests: 'Total Requests',
usertype: 'User Type',
role: 'Role',
created: 'Created',
lastupdated: 'Last Updated',
edit: 'Edit',
delete: 'Delete',
admin: 'Admin',
user: 'User',
plexuser: 'Plex User',
});
const UserList: React.FC = () => {
const intl = useIntl();
const router = useRouter();
const { data, error } = useSWR<User[]>('/api/v1/user');
@ -20,7 +36,7 @@ const UserList: React.FC = () => {
return (
<>
<Header extraMargin={4}>User List</Header>
<Header extraMargin={4}>{intl.formatMessage(messages.userlist)}</Header>
<div className="flex flex-col">
<div className="my-2 overflow-x-auto -mx-6 sm:-mx-6 md:mx-4 lg:mx-4">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
@ -29,22 +45,22 @@ const UserList: React.FC = () => {
<thead>
<tr>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
Name
{intl.formatMessage(messages.username)}
</th>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
Total Requests
{intl.formatMessage(messages.totalrequests)}
</th>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
User Type
{intl.formatMessage(messages.usertype)}
</th>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
Role
{intl.formatMessage(messages.role)}
</th>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
Created
{intl.formatMessage(messages.created)}
</th>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
Last Updated
{intl.formatMessage(messages.lastupdated)}
</th>
<th className="px-6 py-3 bg-gray-500"></th>
</tr>
@ -77,12 +93,14 @@ const UserList: React.FC = () => {
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<Badge badgeType="warning">Plex User</Badge>
<Badge badgeType="warning">
{intl.formatMessage(messages.plexuser)}
</Badge>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm leading-5 text-white">
{hasPermission(Permission.ADMIN, user.permissions)
? 'Admin'
: 'User'}
? intl.formatMessage(messages.admin)
: intl.formatMessage(messages.user)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm leading-5 text-white">
<FormattedDate value={user.createdAt} />
@ -101,9 +119,11 @@ const UserList: React.FC = () => {
)
}
>
Edit
{intl.formatMessage(messages.edit)}
</Button>
<Button buttonType="danger">
{intl.formatMessage(messages.delete)}
</Button>
<Button buttonType="danger">Delete</Button>
</td>
</tr>
))}

@ -0,0 +1,16 @@
import { defineMessages } from 'react-intl';
const globalMessages = defineMessages({
available: 'Available',
partiallyavailable: 'Partially Available',
processing: 'Processing',
unavailable: 'Unavailable',
pending: 'Pending',
declined: 'Declined',
approved: 'Approved',
movies: 'Movies',
tvshows: 'Series',
cancel: 'Cancel',
});
export default globalMessages;

@ -18,10 +18,17 @@
"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.approve": "Approve",
"components.MovieDetails.available": "Available",
"components.MovieDetails.budget": "Budget",
"components.MovieDetails.cancelrequest": "Cancel Request",
"components.MovieDetails.cast": "Cast",
"components.MovieDetails.decline": "Decline",
"components.MovieDetails.manageModalClearMedia": "Clear All Media Data",
"components.MovieDetails.manageModalClearMediaWarning": "This will remove all media data including all requests for this item. This action is irreversible. If this item exists in your Plex library, the media information will be recreated next sync.",
"components.MovieDetails.manageModalNoRequests": "No Requests",
"components.MovieDetails.manageModalRequests": "Requests",
"components.MovieDetails.manageModalTitle": "Manage Movie",
"components.MovieDetails.originallanguage": "Original Language",
"components.MovieDetails.overview": "Overview",
"components.MovieDetails.overviewunavailable": "Overview unavailable",
@ -38,17 +45,46 @@
"components.MovieDetails.unavailable": "Unavailable",
"components.MovieDetails.userrating": "User Rating",
"components.MovieDetails.viewrequest": "View Request",
"components.PendingRequest.approve": "Approve",
"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.RequestBlock.seasons": "Seasons",
"components.RequestModal.cancel": "Cancel Request",
"components.RequestModal.cancelling": "Cancelling...",
"components.RequestModal.cancelrequest": "This will remove your request. Are you sure you want to continue?",
"components.RequestModal.close": "Close",
"components.RequestModal.extras": "Extras",
"components.RequestModal.notrequested": "Not Requested",
"components.RequestModal.numberofepisodes": "# of Episodes",
"components.RequestModal.pendingrequest": "Pending request for {title}",
"components.RequestModal.request": "Request",
"components.RequestModal.requestCancel": "Request for <strong>{title}</strong> cancelled",
"components.RequestModal.requestSuccess": "<strong>{title}</strong> successfully requested!",
"components.RequestModal.requestadmin": "Your request will be immediately approved.",
"components.RequestModal.requestfrom": "There is currently a pending request from {username}",
"components.RequestModal.requesting": "Requesting...",
"components.RequestModal.requestseasons": "Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}",
"components.RequestModal.requesttitle": "Request {title}",
"components.RequestModal.season": "Season",
"components.RequestModal.seasonnumber": "Season {number}",
"components.RequestModal.selectseason": "Select season(s)",
"components.RequestModal.status": "Status",
"components.Search.searchresults": "Search Results",
"components.Settings.Notifications.agentenabled": "Agent Enabled",
"components.Settings.Notifications.authPass": "Auth Pass",
"components.Settings.Notifications.authUser": "Auth User",
"components.Settings.Notifications.emailsender": "Email Sender Address",
"components.Settings.Notifications.enableSsl": "Enable SSL",
"components.Settings.Notifications.save": "Save Changes",
"components.Settings.Notifications.saving": "Saving...",
"components.Settings.Notifications.smtpHost": "SMTP Host",
"components.Settings.Notifications.smtpPort": "SMTP Port",
"components.Settings.Notifications.validationFromRequired": "You must provide an email sender address",
"components.Settings.Notifications.validationSmtpHostRequired": "You must provide an SMTP host",
"components.Settings.Notifications.validationSmtpPortRequired": "You must provide an SMTP port",
"components.Settings.Notifications.validationWebhookUrlRequired": "You must provide a webhook URL",
"components.Settings.Notifications.webhookUrl": "Webhook URL",
"components.Settings.Notifications.webhookUrlPlaceholder": "Server Settings -> Integrations -> Webhooks",
"components.Settings.RadarrModal.add": "Add Server",
"components.Settings.RadarrModal.apiKey": "API Key",
"components.Settings.RadarrModal.apiKeyPlaceholder": "Your Radarr API Key",
@ -114,17 +150,33 @@
"components.Settings.addradarr": "Add Radarr Server",
"components.Settings.address": "Address",
"components.Settings.addsonarr": "Add Sonarr Server",
"components.Settings.apikey": "API Key",
"components.Settings.applicationurl": "Application URL",
"components.Settings.cancelscan": "Cancel Scan",
"components.Settings.copied": "Copied API key to clipboard",
"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.generalsettings": "General Settings",
"components.Settings.generalsettingsDescription": "These are settings related to general Overseerr configuration.",
"components.Settings.hostname": "Hostname/IP",
"components.Settings.jobname": "Job Name",
"components.Settings.librariesRemaining": "Libraries Remaining: {count}",
"components.Settings.manualscan": "Manual Library Scan",
"components.Settings.manualscanDescription": "Normally, this will only be run once every 6 hours. Overseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one time full manual library scan is recommended!",
"components.Settings.menuAbout": "About",
"components.Settings.menuGeneralSettings": "General Settings",
"components.Settings.menuJobs": "Jobs",
"components.Settings.menuLogs": "Logs",
"components.Settings.menuNotifications": "Notifications",
"components.Settings.menuPlexSettings": "Plex",
"components.Settings.menuServices": "Services",
"components.Settings.nextexecution": "Next Execution",
"components.Settings.notificationsettings": "Notification Settings",
"components.Settings.notificationsettingsDescription": "Here you can pick and choose what types of notifications to send and through what types of services.",
"components.Settings.notrunning": "Not Running",
"components.Settings.plexlibraries": "Plex Libraries",
"components.Settings.plexlibrariesDescription": "These are the libraries Overseerr will scan for titles. If you see no libraries listed, you will need to run at least one sync by clicking the button below. You must first configure and save your plex connection settings before you will be able to retrieve your libraries.",
@ -133,6 +185,7 @@
"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.runnow": "Run Now",
"components.Settings.save": "Save Changes",
"components.Settings.saving": "Saving...",
"components.Settings.servername": "Server Name (Automatically Set)",
@ -152,11 +205,20 @@
"components.Setup.signinMessage": "Get started by logging in with your Plex account",
"components.Setup.welcome": "Welcome to Overseerr",
"components.Slider.noresults": "No Results",
"components.TitleCard.movie": "MOVIE",
"components.TitleCard.tvshow": "SERIES",
"components.TvDetails.approve": "Approve",
"components.TvDetails.approverequests": "Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}",
"components.TvDetails.available": "Available",
"components.TvDetails.cancelrequest": "Cancel Request",
"components.TvDetails.cast": "Cast",
"components.TvDetails.decline": "Decline",
"components.TvDetails.declinerequests": "Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}",
"components.TvDetails.manageModalClearMedia": "Clear All Media Data",
"components.TvDetails.manageModalClearMediaWarning": "This will remove all media data including all requests for this item. This action is irreversible. If this item exists in your Plex library, the media information will be recreated next sync.",
"components.TvDetails.manageModalNoRequests": "No Requests",
"components.TvDetails.manageModalRequests": "Requests",
"components.TvDetails.manageModalTitle": "Manage Series",
"components.TvDetails.originallanguage": "Original Language",
"components.TvDetails.overview": "Overview",
"components.TvDetails.overviewunavailable": "Overview unavailable",
@ -193,6 +255,28 @@
"components.UserEdit.usersaved": "User succesfully saved",
"components.UserEdit.vote": "Vote",
"components.UserEdit.voteDescription": "Grants permission to vote on requests (voting not yet implemented)",
"components.UserList.admin": "Admin",
"components.UserList.created": "Created",
"components.UserList.delete": "Delete",
"components.UserList.edit": "Edit",
"components.UserList.lastupdated": "Last Updated",
"components.UserList.plexuser": "Plex User",
"components.UserList.role": "Role",
"components.UserList.totalrequests": "Total Requests",
"components.UserList.user": "User",
"components.UserList.userlist": "User List",
"components.UserList.username": "Username",
"components.UserList.usertype": "User Type",
"i18n.approved": "Approved",
"i18n.available": "Available",
"i18n.cancel": "Cancel",
"i18n.declined": "Declined",
"i18n.movies": "Movies",
"i18n.partiallyavailable": "Partially Available",
"i18n.pending": "Pending",
"i18n.processing": "Processing",
"i18n.tvshows": "Series",
"i18n.unavailable": "Unavailable",
"pages.internalServerError": "{statusCode} - Internal Server Error",
"pages.oops": "Oops",
"pages.pageNotFound": "404 - Page Not Found",

@ -18,10 +18,17 @@
"components.Layout.UserDropdown.signout": "Déconnexion",
"components.Layout.alphawarning": "Ce logiciel est en version ALPHA. Presque tout est succeptible d'être buggé et/ou instalbe. Veuillez signaler tout problème sur le Github d'Overseer!",
"components.Login.signinplex": "S'identifier pour continuer",
"components.MovieDetails.approve": "",
"components.MovieDetails.available": "Disponible",
"components.MovieDetails.budget": "Budget",
"components.MovieDetails.cancelrequest": "Annuler la Requête",
"components.MovieDetails.cast": "Casting",
"components.MovieDetails.decline": "",
"components.MovieDetails.manageModalClearMedia": "",
"components.MovieDetails.manageModalClearMediaWarning": "",
"components.MovieDetails.manageModalNoRequests": "",
"components.MovieDetails.manageModalRequests": "",
"components.MovieDetails.manageModalTitle": "",
"components.MovieDetails.originallanguage": "",
"components.MovieDetails.overview": "",
"components.MovieDetails.overviewunavailable": "",
@ -38,17 +45,46 @@
"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.RequestBlock.seasons": "",
"components.RequestModal.cancel": "",
"components.RequestModal.cancelling": "",
"components.RequestModal.cancelrequest": "",
"components.RequestModal.close": "",
"components.RequestModal.extras": "",
"components.RequestModal.notrequested": "",
"components.RequestModal.numberofepisodes": "",
"components.RequestModal.pendingrequest": "",
"components.RequestModal.request": "",
"components.RequestModal.requestCancel": "",
"components.RequestModal.requestSuccess": "",
"components.RequestModal.requestadmin": "",
"components.RequestModal.requestfrom": "",
"components.RequestModal.requesting": "",
"components.RequestModal.requestseasons": "",
"components.RequestModal.requesttitle": "",
"components.RequestModal.season": "",
"components.RequestModal.seasonnumber": "",
"components.RequestModal.selectseason": "",
"components.RequestModal.status": "",
"components.Search.searchresults": "",
"components.Settings.Notifications.agentenabled": "",
"components.Settings.Notifications.authPass": "",
"components.Settings.Notifications.authUser": "",
"components.Settings.Notifications.emailsender": "",
"components.Settings.Notifications.enableSsl": "",
"components.Settings.Notifications.save": "",
"components.Settings.Notifications.saving": "",
"components.Settings.Notifications.smtpHost": "",
"components.Settings.Notifications.smtpPort": "",
"components.Settings.Notifications.validationFromRequired": "",
"components.Settings.Notifications.validationSmtpHostRequired": "",
"components.Settings.Notifications.validationSmtpPortRequired": "",
"components.Settings.Notifications.validationWebhookUrlRequired": "",
"components.Settings.Notifications.webhookUrl": "",
"components.Settings.Notifications.webhookUrlPlaceholder": "",
"components.Settings.RadarrModal.add": "",
"components.Settings.RadarrModal.apiKey": "",
"components.Settings.RadarrModal.apiKeyPlaceholder": "",
@ -114,17 +150,33 @@
"components.Settings.addradarr": "",
"components.Settings.address": "",
"components.Settings.addsonarr": "",
"components.Settings.apikey": "",
"components.Settings.applicationurl": "",
"components.Settings.cancelscan": "",
"components.Settings.copied": "",
"components.Settings.currentlibrary": "",
"components.Settings.default": "",
"components.Settings.default4k": "",
"components.Settings.delete": "",
"components.Settings.deleteserverconfirm": "",
"components.Settings.edit": "",
"components.Settings.generalsettings": "",
"components.Settings.generalsettingsDescription": "",
"components.Settings.hostname": "",
"components.Settings.jobname": "",
"components.Settings.librariesRemaining": "",
"components.Settings.manualscan": "",
"components.Settings.manualscanDescription": "",
"components.Settings.menuAbout": "",
"components.Settings.menuGeneralSettings": "",
"components.Settings.menuJobs": "",
"components.Settings.menuLogs": "",
"components.Settings.menuNotifications": "",
"components.Settings.menuPlexSettings": "",
"components.Settings.menuServices": "",
"components.Settings.nextexecution": "",
"components.Settings.notificationsettings": "",
"components.Settings.notificationsettingsDescription": "",
"components.Settings.notrunning": "",
"components.Settings.plexlibraries": "",
"components.Settings.plexlibrariesDescription": "",
@ -133,6 +185,7 @@
"components.Settings.port": "",
"components.Settings.radarrSettingsDescription": "",
"components.Settings.radarrsettings": "",
"components.Settings.runnow": "",
"components.Settings.save": "",
"components.Settings.saving": "",
"components.Settings.servername": "",
@ -152,11 +205,20 @@
"components.Setup.signinMessage": "",
"components.Setup.welcome": "",
"components.Slider.noresults": "",
"components.TitleCard.movie": "",
"components.TitleCard.tvshow": "",
"components.TvDetails.approve": "",
"components.TvDetails.approverequests": "",
"components.TvDetails.available": "",
"components.TvDetails.cancelrequest": "",
"components.TvDetails.cast": "",
"components.TvDetails.decline": "",
"components.TvDetails.declinerequests": "",
"components.TvDetails.manageModalClearMedia": "",
"components.TvDetails.manageModalClearMediaWarning": "",
"components.TvDetails.manageModalNoRequests": "",
"components.TvDetails.manageModalRequests": "",
"components.TvDetails.manageModalTitle": "",
"components.TvDetails.originallanguage": "",
"components.TvDetails.overview": "",
"components.TvDetails.overviewunavailable": "",
@ -193,6 +255,28 @@
"components.UserEdit.usersaved": "",
"components.UserEdit.vote": "",
"components.UserEdit.voteDescription": "",
"components.UserList.admin": "",
"components.UserList.created": "",
"components.UserList.delete": "",
"components.UserList.edit": "",
"components.UserList.lastupdated": "",
"components.UserList.plexuser": "",
"components.UserList.role": "",
"components.UserList.totalrequests": "",
"components.UserList.user": "",
"components.UserList.userlist": "",
"components.UserList.username": "",
"components.UserList.usertype": "",
"i18n.approved": "",
"i18n.available": "",
"i18n.cancel": "",
"i18n.declined": "",
"i18n.movies": "",
"i18n.partiallyavailable": "",
"i18n.pending": "",
"i18n.processing": "",
"i18n.tvshows": "",
"i18n.unavailable": "",
"pages.internalServerError": "",
"pages.oops": "",
"pages.pageNotFound": "",

@ -18,10 +18,17 @@
"components.Layout.UserDropdown.signout": "",
"components.Layout.alphawarning": "",
"components.Login.signinplex": "",
"components.MovieDetails.approve": "",
"components.MovieDetails.available": "",
"components.MovieDetails.budget": "興行収入",
"components.MovieDetails.cancelrequest": "チャンセルリクエスト",
"components.MovieDetails.cast": "キャスト",
"components.MovieDetails.decline": "",
"components.MovieDetails.manageModalClearMedia": "",
"components.MovieDetails.manageModalClearMediaWarning": "",
"components.MovieDetails.manageModalNoRequests": "",
"components.MovieDetails.manageModalRequests": "",
"components.MovieDetails.manageModalTitle": "",
"components.MovieDetails.originallanguage": "言語",
"components.MovieDetails.overview": "ストーリー",
"components.MovieDetails.overviewunavailable": "",
@ -38,17 +45,46 @@
"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.RequestBlock.seasons": "",
"components.RequestModal.cancel": "",
"components.RequestModal.cancelling": "",
"components.RequestModal.cancelrequest": "このリクエストをキャンセルしてよろしいですか?",
"components.RequestModal.close": "",
"components.RequestModal.extras": "",
"components.RequestModal.notrequested": "",
"components.RequestModal.numberofepisodes": "",
"components.RequestModal.pendingrequest": "",
"components.RequestModal.request": "",
"components.RequestModal.requestCancel": "",
"components.RequestModal.requestSuccess": "",
"components.RequestModal.requestadmin": "このリクエストが今すぐ承認致します。よろしいですか?",
"components.RequestModal.requestfrom": "",
"components.RequestModal.requesting": "",
"components.RequestModal.requestseasons": "",
"components.RequestModal.requesttitle": "",
"components.RequestModal.season": "",
"components.RequestModal.seasonnumber": "",
"components.RequestModal.selectseason": "",
"components.RequestModal.status": "",
"components.Search.searchresults": "",
"components.Settings.Notifications.agentenabled": "",
"components.Settings.Notifications.authPass": "",
"components.Settings.Notifications.authUser": "",
"components.Settings.Notifications.emailsender": "",
"components.Settings.Notifications.enableSsl": "",
"components.Settings.Notifications.save": "",
"components.Settings.Notifications.saving": "",
"components.Settings.Notifications.smtpHost": "",
"components.Settings.Notifications.smtpPort": "",
"components.Settings.Notifications.validationFromRequired": "",
"components.Settings.Notifications.validationSmtpHostRequired": "",
"components.Settings.Notifications.validationSmtpPortRequired": "",
"components.Settings.Notifications.validationWebhookUrlRequired": "",
"components.Settings.Notifications.webhookUrl": "",
"components.Settings.Notifications.webhookUrlPlaceholder": "",
"components.Settings.RadarrModal.add": "",
"components.Settings.RadarrModal.apiKey": "",
"components.Settings.RadarrModal.apiKeyPlaceholder": "",
@ -114,17 +150,33 @@
"components.Settings.addradarr": "",
"components.Settings.address": "",
"components.Settings.addsonarr": "",
"components.Settings.apikey": "",
"components.Settings.applicationurl": "",
"components.Settings.cancelscan": "",
"components.Settings.copied": "",
"components.Settings.currentlibrary": "",
"components.Settings.default": "",
"components.Settings.default4k": "",
"components.Settings.delete": "",
"components.Settings.deleteserverconfirm": "",
"components.Settings.edit": "",
"components.Settings.generalsettings": "",
"components.Settings.generalsettingsDescription": "",
"components.Settings.hostname": "",
"components.Settings.jobname": "",
"components.Settings.librariesRemaining": "",
"components.Settings.manualscan": "",
"components.Settings.manualscanDescription": "",
"components.Settings.menuAbout": "",
"components.Settings.menuGeneralSettings": "",
"components.Settings.menuJobs": "",
"components.Settings.menuLogs": "",
"components.Settings.menuNotifications": "",
"components.Settings.menuPlexSettings": "",
"components.Settings.menuServices": "",
"components.Settings.nextexecution": "",
"components.Settings.notificationsettings": "",
"components.Settings.notificationsettingsDescription": "",
"components.Settings.notrunning": "",
"components.Settings.plexlibraries": "",
"components.Settings.plexlibrariesDescription": "",
@ -133,6 +185,7 @@
"components.Settings.port": "",
"components.Settings.radarrSettingsDescription": "",
"components.Settings.radarrsettings": "",
"components.Settings.runnow": "",
"components.Settings.save": "",
"components.Settings.saving": "",
"components.Settings.servername": "",
@ -152,11 +205,20 @@
"components.Setup.signinMessage": "",
"components.Setup.welcome": "",
"components.Slider.noresults": "",
"components.TitleCard.movie": "",
"components.TitleCard.tvshow": "",
"components.TvDetails.approve": "",
"components.TvDetails.approverequests": "",
"components.TvDetails.available": "",
"components.TvDetails.cancelrequest": "",
"components.TvDetails.cast": "",
"components.TvDetails.decline": "",
"components.TvDetails.declinerequests": "",
"components.TvDetails.manageModalClearMedia": "",
"components.TvDetails.manageModalClearMediaWarning": "",
"components.TvDetails.manageModalNoRequests": "",
"components.TvDetails.manageModalRequests": "",
"components.TvDetails.manageModalTitle": "",
"components.TvDetails.originallanguage": "",
"components.TvDetails.overview": "",
"components.TvDetails.overviewunavailable": "",
@ -193,6 +255,28 @@
"components.UserEdit.usersaved": "",
"components.UserEdit.vote": "",
"components.UserEdit.voteDescription": "",
"components.UserList.admin": "",
"components.UserList.created": "",
"components.UserList.delete": "",
"components.UserList.edit": "",
"components.UserList.lastupdated": "",
"components.UserList.plexuser": "",
"components.UserList.role": "",
"components.UserList.totalrequests": "",
"components.UserList.user": "",
"components.UserList.userlist": "",
"components.UserList.username": "",
"components.UserList.usertype": "",
"i18n.approved": "",
"i18n.available": "",
"i18n.cancel": "",
"i18n.declined": "",
"i18n.movies": "",
"i18n.partiallyavailable": "",
"i18n.pending": "",
"i18n.processing": "",
"i18n.tvshows": "",
"i18n.unavailable": "",
"pages.internalServerError": "",
"pages.oops": "ああ",
"pages.pageNotFound": "",

@ -13,10 +13,13 @@ import { LanguageContext, AvailableLocales } from '../context/LanguageContext';
import Head from 'next/head';
import Toast from '../components/Toast';
const loadLocaleData = (locale: string) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loadLocaleData = (locale: string): Promise<any> => {
switch (locale) {
case 'ja':
return import('../i18n/locale/ja.json');
case 'fr':
return import('../i18n/locale/fr.json');
default:
return import('../i18n/locale/en.json');
}

Loading…
Cancel
Save