mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
fix: correctly fall back to English name in LanguageSelector (#1537)
* fix: correctly fall back to English name in LanguageSelector * refactor: clean up language sort & name logic * refactor: also clean up region sort & name logic * refactor: use arrow functions
This commit is contained in:
@@ -14,6 +14,7 @@ export interface Library {
|
|||||||
export interface Region {
|
export interface Region {
|
||||||
iso_3166_1: string;
|
iso_3166_1: string;
|
||||||
english_name: string;
|
english_name: string;
|
||||||
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Language {
|
export interface Language {
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { sortBy } from 'lodash';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import type { OptionsType, OptionTypeBase } from 'react-select';
|
import type { OptionsType, OptionTypeBase } from 'react-select';
|
||||||
|
import useSWR from 'swr';
|
||||||
import { Language } from '../../../server/lib/settings';
|
import { Language } from '../../../server/lib/settings';
|
||||||
import globalMessages from '../../i18n/globalMessages';
|
import globalMessages from '../../i18n/globalMessages';
|
||||||
|
|
||||||
@@ -29,7 +31,6 @@ const selectStyles = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface LanguageSelectorProps {
|
interface LanguageSelectorProps {
|
||||||
languages: Language[];
|
|
||||||
value?: string;
|
value?: string;
|
||||||
setFieldValue: (property: string, value: string) => void;
|
setFieldValue: (property: string, value: string) => void;
|
||||||
serverValue?: string;
|
serverValue?: string;
|
||||||
@@ -37,26 +38,33 @@ interface LanguageSelectorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LanguageSelector: React.FC<LanguageSelectorProps> = ({
|
const LanguageSelector: React.FC<LanguageSelectorProps> = ({
|
||||||
languages,
|
|
||||||
value,
|
value,
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
serverValue,
|
serverValue,
|
||||||
isUserSettings = false,
|
isUserSettings = false,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { data: languages } = useSWR<Language[]>('/api/v1/languages');
|
||||||
|
|
||||||
const defaultLanguageNameFallback = serverValue
|
const sortedLanguages = useMemo(() => {
|
||||||
? languages.find((language) => language.iso_639_1 === serverValue)
|
languages?.forEach((language) => {
|
||||||
?.english_name ?? serverValue
|
language.name =
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const options: OptionType[] =
|
|
||||||
languages.map((language) => ({
|
|
||||||
label:
|
|
||||||
intl.formatDisplayName(language.iso_639_1, {
|
intl.formatDisplayName(language.iso_639_1, {
|
||||||
type: 'language',
|
type: 'language',
|
||||||
fallback: 'none',
|
fallback: 'none',
|
||||||
}) ?? language.english_name,
|
}) ?? language.english_name;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortBy(languages, 'name');
|
||||||
|
}, [intl, languages]);
|
||||||
|
|
||||||
|
const languageName = (languageCode: string) =>
|
||||||
|
sortedLanguages?.find((language) => language.iso_639_1 === languageCode)
|
||||||
|
?.name ?? languageCode;
|
||||||
|
|
||||||
|
const options: OptionType[] =
|
||||||
|
sortedLanguages?.map((language) => ({
|
||||||
|
label: language.name,
|
||||||
value: language.iso_639_1,
|
value: language.iso_639_1,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
@@ -67,13 +75,7 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({
|
|||||||
language: serverValue
|
language: serverValue
|
||||||
? serverValue
|
? serverValue
|
||||||
.split('|')
|
.split('|')
|
||||||
.map(
|
.map((value) => languageName(value))
|
||||||
(value) =>
|
|
||||||
intl.formatDisplayName(value, {
|
|
||||||
type: 'language',
|
|
||||||
fallback: 'none',
|
|
||||||
}) ?? defaultLanguageNameFallback
|
|
||||||
)
|
|
||||||
.reduce((prev, curr) =>
|
.reduce((prev, curr) =>
|
||||||
intl.formatMessage(globalMessages.delimitedlist, {
|
intl.formatMessage(globalMessages.delimitedlist, {
|
||||||
a: prev,
|
a: prev,
|
||||||
@@ -112,13 +114,7 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({
|
|||||||
language: serverValue
|
language: serverValue
|
||||||
? serverValue
|
? serverValue
|
||||||
.split('|')
|
.split('|')
|
||||||
.map(
|
.map((value) => languageName(value))
|
||||||
(value) =>
|
|
||||||
intl.formatDisplayName(value, {
|
|
||||||
type: 'language',
|
|
||||||
fallback: 'none',
|
|
||||||
}) ?? defaultLanguageNameFallback
|
|
||||||
)
|
|
||||||
.reduce((prev, curr) =>
|
.reduce((prev, curr) =>
|
||||||
intl.formatMessage(globalMessages.delimitedlist, {
|
intl.formatMessage(globalMessages.delimitedlist, {
|
||||||
a: prev,
|
a: prev,
|
||||||
@@ -130,7 +126,7 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({
|
|||||||
isFixed: true,
|
isFixed: true,
|
||||||
}
|
}
|
||||||
: value?.split('|').map((code) => {
|
: value?.split('|').map((code) => {
|
||||||
const matchedLanguage = languages.find(
|
const matchedLanguage = sortedLanguages?.find(
|
||||||
(lang) => lang.iso_639_1 === code
|
(lang) => lang.iso_639_1 === code
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -139,11 +135,7 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label:
|
label: matchedLanguage.name,
|
||||||
intl.formatDisplayName(matchedLanguage.iso_639_1, {
|
|
||||||
type: 'language',
|
|
||||||
fallback: 'none',
|
|
||||||
}) ?? matchedLanguage.english_name,
|
|
||||||
value: matchedLanguage.iso_639_1,
|
value: matchedLanguage.iso_639_1,
|
||||||
};
|
};
|
||||||
}) ?? undefined
|
}) ?? undefined
|
||||||
|
@@ -2,6 +2,7 @@ import { Listbox, Transition } from '@headlessui/react';
|
|||||||
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/solid';
|
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/solid';
|
||||||
import { hasFlag } from 'country-flag-icons';
|
import { hasFlag } from 'country-flag-icons';
|
||||||
import 'country-flag-icons/3x2/flags.css';
|
import 'country-flag-icons/3x2/flags.css';
|
||||||
|
import { sortBy } from 'lodash';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
@@ -39,32 +40,21 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortedRegions = useMemo(
|
const sortedRegions = useMemo(() => {
|
||||||
() =>
|
regions?.forEach((region) => {
|
||||||
regions?.sort((region1, region2) => {
|
region.name =
|
||||||
const region1Name =
|
intl.formatDisplayName(region.iso_3166_1, {
|
||||||
intl.formatDisplayName(region1.iso_3166_1, {
|
type: 'region',
|
||||||
type: 'region',
|
fallback: 'none',
|
||||||
fallback: 'none',
|
}) ?? region.english_name;
|
||||||
}) ?? region1.english_name;
|
});
|
||||||
const region2Name =
|
|
||||||
intl.formatDisplayName(region2.iso_3166_1, {
|
|
||||||
type: 'region',
|
|
||||||
fallback: 'none',
|
|
||||||
}) ?? region2.english_name;
|
|
||||||
|
|
||||||
return region1Name === region2Name
|
return sortBy(regions, 'name');
|
||||||
? 0
|
}, [intl, regions]);
|
||||||
: region1Name > region2Name
|
|
||||||
? 1
|
|
||||||
: -1;
|
|
||||||
}),
|
|
||||||
[intl, regions]
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultRegionNameFallback =
|
const regionName = (regionCode: string) =>
|
||||||
regions?.find((region) => region.iso_3166_1 === currentSettings.region)
|
sortedRegions?.find((region) => region.iso_3166_1 === regionCode)?.name ??
|
||||||
?.english_name ?? currentSettings.region;
|
regionCode;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (regions && value) {
|
if (regions && value) {
|
||||||
@@ -110,17 +100,11 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
|
|||||||
)}
|
)}
|
||||||
<span className="block truncate">
|
<span className="block truncate">
|
||||||
{selectedRegion && selectedRegion.iso_3166_1 !== 'all'
|
{selectedRegion && selectedRegion.iso_3166_1 !== 'all'
|
||||||
? intl.formatDisplayName(selectedRegion.iso_3166_1, {
|
? regionName(selectedRegion.iso_3166_1)
|
||||||
type: 'region',
|
|
||||||
fallback: 'none',
|
|
||||||
}) ?? selectedRegion.english_name
|
|
||||||
: isUserSetting && selectedRegion?.iso_3166_1 !== 'all'
|
: isUserSetting && selectedRegion?.iso_3166_1 !== 'all'
|
||||||
? intl.formatMessage(messages.regionServerDefault, {
|
? intl.formatMessage(messages.regionServerDefault, {
|
||||||
region: currentSettings.region
|
region: currentSettings.region
|
||||||
? intl.formatDisplayName(currentSettings.region, {
|
? regionName(currentSettings.region)
|
||||||
type: 'region',
|
|
||||||
fallback: 'none',
|
|
||||||
}) ?? defaultRegionNameFallback
|
|
||||||
: intl.formatMessage(messages.regionDefault),
|
: intl.formatMessage(messages.regionDefault),
|
||||||
})
|
})
|
||||||
: intl.formatMessage(messages.regionDefault)}
|
: intl.formatMessage(messages.regionDefault)}
|
||||||
@@ -168,13 +152,7 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
|
|||||||
>
|
>
|
||||||
{intl.formatMessage(messages.regionServerDefault, {
|
{intl.formatMessage(messages.regionServerDefault, {
|
||||||
region: currentSettings.region
|
region: currentSettings.region
|
||||||
? intl.formatDisplayName(
|
? regionName(currentSettings.region)
|
||||||
currentSettings.region,
|
|
||||||
{
|
|
||||||
type: 'region',
|
|
||||||
fallback: 'none',
|
|
||||||
}
|
|
||||||
) ?? defaultRegionNameFallback
|
|
||||||
: intl.formatMessage(messages.regionDefault),
|
: intl.formatMessage(messages.regionDefault),
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
@@ -241,10 +219,7 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
|
|||||||
selected ? 'font-semibold' : 'font-normal'
|
selected ? 'font-semibold' : 'font-normal'
|
||||||
} block truncate`}
|
} block truncate`}
|
||||||
>
|
>
|
||||||
{intl.formatDisplayName(region.iso_3166_1, {
|
{regionName(region.iso_3166_1)}
|
||||||
type: 'region',
|
|
||||||
fallback: 'none',
|
|
||||||
}) ?? region.english_name}
|
|
||||||
</span>
|
</span>
|
||||||
{selected && (
|
{selected && (
|
||||||
<span
|
<span
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import { RefreshIcon } from '@heroicons/react/solid';
|
import { RefreshIcon } from '@heroicons/react/solid';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import useSWR, { mutate } from 'swr';
|
import useSWR, { mutate } from 'swr';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import type { Language, MainSettings } from '../../../server/lib/settings';
|
import type { MainSettings } from '../../../server/lib/settings';
|
||||||
import { Permission, useUser } from '../../hooks/useUser';
|
import { Permission, useUser } from '../../hooks/useUser';
|
||||||
import globalMessages from '../../i18n/globalMessages';
|
import globalMessages from '../../i18n/globalMessages';
|
||||||
import Badge from '../Common/Badge';
|
import Badge from '../Common/Badge';
|
||||||
@@ -58,9 +58,7 @@ const SettingsMain: React.FC = () => {
|
|||||||
const { data, error, revalidate } = useSWR<MainSettings>(
|
const { data, error, revalidate } = useSWR<MainSettings>(
|
||||||
'/api/v1/settings/main'
|
'/api/v1/settings/main'
|
||||||
);
|
);
|
||||||
const { data: languages, error: languagesError } = useSWR<Language[]>(
|
|
||||||
'/api/v1/languages'
|
|
||||||
);
|
|
||||||
const MainSettingsSchema = Yup.object().shape({
|
const MainSettingsSchema = Yup.object().shape({
|
||||||
applicationTitle: Yup.string().required(
|
applicationTitle: Yup.string().required(
|
||||||
intl.formatMessage(messages.validationApplicationTitle)
|
intl.formatMessage(messages.validationApplicationTitle)
|
||||||
@@ -96,26 +94,7 @@ const SettingsMain: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortedLanguages = useMemo(
|
if (!data && !error) {
|
||||||
() =>
|
|
||||||
languages?.sort((lang1, lang2) => {
|
|
||||||
const lang1Name =
|
|
||||||
intl.formatDisplayName(lang1.iso_639_1, {
|
|
||||||
type: 'language',
|
|
||||||
fallback: 'none',
|
|
||||||
}) ?? lang1.english_name;
|
|
||||||
const lang2Name =
|
|
||||||
intl.formatDisplayName(lang2.iso_639_1, {
|
|
||||||
type: 'language',
|
|
||||||
fallback: 'none',
|
|
||||||
}) ?? lang2.english_name;
|
|
||||||
|
|
||||||
return lang1Name === lang2Name ? 0 : lang1Name > lang2Name ? 1 : -1;
|
|
||||||
}),
|
|
||||||
[intl, languages]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!data && !error && !languages && !languagesError) {
|
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +295,6 @@ const SettingsMain: React.FC = () => {
|
|||||||
<div className="form-input">
|
<div className="form-input">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field">
|
||||||
<LanguageSelector
|
<LanguageSelector
|
||||||
languages={sortedLanguages ?? []}
|
|
||||||
setFieldValue={setFieldValue}
|
setFieldValue={setFieldValue}
|
||||||
value={values.originalLanguage}
|
value={values.originalLanguage}
|
||||||
/>
|
/>
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { UserSettingsGeneralResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
import { UserSettingsGeneralResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||||
import { Language } from '../../../../../server/lib/settings';
|
|
||||||
import {
|
import {
|
||||||
availableLanguages,
|
availableLanguages,
|
||||||
AvailableLocales,
|
AvailableLocales,
|
||||||
@@ -72,38 +71,11 @@ const UserGeneralSettings: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const { data: languages, error: languagesError } = useSWR<Language[]>(
|
|
||||||
'/api/v1/languages'
|
|
||||||
);
|
|
||||||
|
|
||||||
const sortedLanguages = useMemo(
|
|
||||||
() =>
|
|
||||||
languages?.sort((lang1, lang2) => {
|
|
||||||
const lang1Name =
|
|
||||||
intl.formatDisplayName(lang1.iso_639_1, {
|
|
||||||
type: 'language',
|
|
||||||
fallback: 'none',
|
|
||||||
}) ?? lang1.english_name;
|
|
||||||
const lang2Name =
|
|
||||||
intl.formatDisplayName(lang2.iso_639_1, {
|
|
||||||
type: 'language',
|
|
||||||
fallback: 'none',
|
|
||||||
}) ?? lang2.english_name;
|
|
||||||
|
|
||||||
return lang1Name === lang2Name ? 0 : lang1Name > lang2Name ? 1 : -1;
|
|
||||||
}),
|
|
||||||
[intl, languages]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!languages && !languagesError) {
|
if (!data) {
|
||||||
return <LoadingSpinner />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data || !languages) {
|
|
||||||
return <Error statusCode={500} />;
|
return <Error statusCode={500} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +235,6 @@ const UserGeneralSettings: React.FC = () => {
|
|||||||
<div className="form-input">
|
<div className="form-input">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field">
|
||||||
<LanguageSelector
|
<LanguageSelector
|
||||||
languages={sortedLanguages ?? []}
|
|
||||||
setFieldValue={setFieldValue}
|
setFieldValue={setFieldValue}
|
||||||
serverValue={currentSettings.originalLanguage}
|
serverValue={currentSettings.originalLanguage}
|
||||||
value={values.originalLanguage}
|
value={values.originalLanguage}
|
||||||
|
Reference in New Issue
Block a user