From cd6d8a8216e7ae183b046d26cd22f3c1dc1d2b35 Mon Sep 17 00:00:00 2001 From: sct Date: Fri, 27 Nov 2020 05:11:45 +0000 Subject: [PATCH] feat(frontend): add french language file also expanded translation coverage (still lots to do!) --- package.json | 2 +- server/routes/discover.ts | 29 ++- server/routes/settings.ts | 44 ++++ server/utils/typeHelpers.ts | 17 ++ .../Layout/LanguagePicker/index.tsx | 4 + src/components/Layout/UserDropdown/index.tsx | 7 +- src/components/Layout/index.tsx | 10 +- src/components/Login/index.tsx | 7 +- src/components/PlexLoginButton/index.tsx | 20 +- .../index.tsx} | 123 ++++++++--- src/components/Settings/SettingsServices.tsx | 74 +++++-- .../index.tsx} | 119 ++++++++--- src/components/Setup/LoginWithPlex.tsx | 10 +- src/components/Setup/index.tsx | 14 +- src/context/LanguageContext.tsx | 2 +- src/i18n/locale/en.json | 88 ++++++++ src/i18n/locale/fr.json | 202 ++++++++++++++++++ src/i18n/locale/ja.json | 88 ++++++++ 18 files changed, 749 insertions(+), 111 deletions(-) create mode 100644 server/utils/typeHelpers.ts rename src/components/Settings/{RadarrModal.tsx => RadarrModal/index.tsx} (81%) rename src/components/Settings/{SonarrModal.tsx => SonarrModal/index.tsx} (82%) create mode 100644 src/i18n/locale/fr.json diff --git a/package.json b/package.json index 9e3b2b63..f1cb8c35 100644 --- a/package.json +++ b/package.json @@ -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 ." diff --git a/server/routes/discover.ts b/server/routes/discover.ts index ab156ebf..ae01802f 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -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) ) ), }); diff --git a/server/routes/settings.ts b/server/routes/settings.ts index 424bb4dc..b4f033fc 100644 --- a/server/routes/settings.ts +++ b/server/routes/settings.ts @@ -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), diff --git a/server/utils/typeHelpers.ts b/server/utils/typeHelpers.ts new file mode 100644 index 00000000..e7d76d78 --- /dev/null +++ b/server/utils/typeHelpers.ts @@ -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; +}; diff --git a/src/components/Layout/LanguagePicker/index.tsx b/src/components/Layout/LanguagePicker/index.tsx index 701fe030..bc6772ba 100644 --- a/src/components/Layout/LanguagePicker/index.tsx +++ b/src/components/Layout/LanguagePicker/index.tsx @@ -25,6 +25,10 @@ const availableLanguages: AvailableLanguageObject = { code: 'ja', display: '日本語', }, + fr: { + code: 'fr', + display: 'Français', + }, }; const LanguagePicker: React.FC = () => { diff --git a/src/components/Layout/UserDropdown/index.tsx b/src/components/Layout/UserDropdown/index.tsx index a6e2287f..16a40a5a 100644 --- a/src/components/Layout/UserDropdown/index.tsx +++ b/src/components/Layout/UserDropdown/index.tsx @@ -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(null); @@ -56,7 +61,7 @@ const UserDropdown: React.FC = () => { role="menuitem" onClick={() => logout()} > - Sign out + diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index b9eea581..a581b667 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -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 }) => {

- This is ALPHA software. Almost everything is bound to be - nearly broken or unstable. Please report issues to the - Overseerr Github! +

{ const [authToken, setAuthToken] = useState(undefined); @@ -51,7 +56,7 @@ const Login: React.FC = () => { alt="Overseerr Logo" />

- Log in to continue +

diff --git a/src/components/PlexLoginButton/index.tsx b/src/components/PlexLoginButton/index.tsx index 4b18b9fe..56883fb3 100644 --- a/src/components/PlexLoginButton/index.tsx +++ b/src/components/PlexLoginButton/index.tsx @@ -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 = ({ onAuthToken, onError, }) => { - const [loading, setLoading] = useState(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 = ({ 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)} ); diff --git a/src/components/Settings/RadarrModal.tsx b/src/components/Settings/RadarrModal/index.tsx similarity index 81% rename from src/components/Settings/RadarrModal.tsx rename to src/components/Settings/RadarrModal/index.tsx index a97ce6b4..2912fa1b 100644 --- a/src/components/Settings/RadarrModal.tsx +++ b/src/components/Settings/RadarrModal/index.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ } 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 = ({ 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 = ({ okDisabled={!isValidated || isSubmitting || isTesting} onOk={() => handleSubmit()} title={ - !radarr ? 'Create New Radarr Server' : 'Edit Radarr Server' + !radarr + ? intl.formatMessage(messages.createradarr) + : intl.formatMessage(messages.editradarr) } >
@@ -216,7 +264,7 @@ const RadarrModal: React.FC = ({ 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)}
= ({ 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)}
@@ -240,7 +288,9 @@ const RadarrModal: React.FC = ({ id="name" name="name" type="text" - placeholder="A Radarr Server" + placeholder={intl.formatMessage( + messages.servernamePlaceholder + )} onChange={(e: React.ChangeEvent) => { setIsValidated(false); setFieldValue('name', e.target.value); @@ -258,7 +308,7 @@ const RadarrModal: React.FC = ({ htmlFor="hostname" className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2" > - Hostname + {intl.formatMessage(messages.hostname)}
@@ -284,7 +334,7 @@ const RadarrModal: React.FC = ({ htmlFor="port" className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2" > - Port + {intl.formatMessage(messages.port)}
= ({ htmlFor="ssl" className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2" > - SSL + {intl.formatMessage(messages.ssl)}
= ({ 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)}
@@ -336,7 +386,9 @@ const RadarrModal: React.FC = ({ id="apiKey" name="apiKey" type="text" - placeholder="Your Radarr API Key" + placeholder={intl.formatMessage( + messages.apiKeyPlaceholder + )} onChange={(e: React.ChangeEvent) => { setIsValidated(false); setFieldValue('apiKey', e.target.value); @@ -354,7 +406,7 @@ const RadarrModal: React.FC = ({ 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)}
@@ -362,7 +414,9 @@ const RadarrModal: React.FC = ({ id="baseUrl" name="baseUrl" type="text" - placeholder="Example: /radarr" + placeholder={intl.formatMessage( + messages.baseUrlPlaceholder + )} onChange={(e: React.ChangeEvent) => { setIsValidated(false); setFieldValue('baseUrl', e.target.value); @@ -380,7 +434,7 @@ const RadarrModal: React.FC = ({ 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)}
@@ -390,6 +444,9 @@ const RadarrModal: React.FC = ({ 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" > + {testResponse.profiles.length > 0 && testResponse.profiles.map((profile) => (