diff --git a/src/base/components/inputs/LanguageSelect.tsx b/src/base/components/inputs/LanguageSelect.tsx index f214b772..95dbfab8 100644 --- a/src/base/components/inputs/LanguageSelect.tsx +++ b/src/base/components/inputs/LanguageSelect.tsx @@ -21,7 +21,8 @@ import { useTranslation } from 'react-i18next'; import { Virtuoso } from 'react-virtuoso'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts'; -import { languageSortComparator, toUniqueLanguageCodes } from '@/base/utils/Languages.ts'; +import { languageSortComparator } from '@/base/utils/Languages.ts'; +import { toUniqueISOLanguageCodes } from '@/lib/ISOLanguageUtil.ts'; interface IProps { selectedLanguages: string[]; @@ -33,12 +34,12 @@ export function LanguageSelect(props: IProps) { const { t } = useTranslation(); const { selectedLanguages, setSelectedLanguages, languages } = props; - const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueLanguageCodes(selectedLanguages)); + const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueISOLanguageCodes(selectedLanguages)); const [open, setOpen] = useState(false); const languagesSortedBySelectState = useMemo( () => - toUniqueLanguageCodes([ + toUniqueISOLanguageCodes([ ...tmpSelectedLanguages.toSorted(languageSortComparator), ...languages.toSorted(languageSortComparator), ]), @@ -47,12 +48,12 @@ export function LanguageSelect(props: IProps) { const handleCancel = () => { setOpen(false); - setTmpSelectedLanguages(toUniqueLanguageCodes(selectedLanguages)); + setTmpSelectedLanguages(toUniqueISOLanguageCodes(selectedLanguages)); }; const handleOk = () => { setOpen(false); - setSelectedLanguages(toUniqueLanguageCodes(tmpSelectedLanguages)); + setSelectedLanguages(toUniqueISOLanguageCodes(tmpSelectedLanguages)); }; const handleChange = (language: string, selected: boolean) => { diff --git a/src/base/utils/Languages.ts b/src/base/utils/Languages.ts index e801e132..91be3c99 100644 --- a/src/base/utils/Languages.ts +++ b/src/base/utils/Languages.ts @@ -7,10 +7,10 @@ */ import { t } from 'i18next'; -import { ISOLanguage, IsoLanguages } from '@/base/IsoLanguages.ts'; - import { TranslationKey } from '@/base/Base.types.ts'; +import { getISOLanguage, getPreferredISOLanguageCodes, LanguageObject } from '@/lib/ISOLanguageUtil.ts'; + export enum DefaultLanguage { ALL = 'all', OTHER = 'other', @@ -27,31 +27,6 @@ const DEFAULT_LANGUAGE_TO_TRANSLATION: Record = [DefaultLanguage.LAST_USED_SOURCE]: 'global.label.last_used', }; -type LanguageObject = ISOLanguage & { orgCode: string; isoCode: string }; - -function getISOLanguageFor(code: string, orgCode: string = code, isoCode: string = code): LanguageObject | null { - if (IsoLanguages[code]) { - return { - ...IsoLanguages[code], - orgCode, - isoCode, - }; - } - - return null; -} - -function getISOLanguage(code: string): LanguageObject | null { - return ( - getISOLanguageFor(code) ?? - getISOLanguageFor(code.toLowerCase(), code) ?? - getISOLanguageFor(code.replace('-', '_'), code) ?? - getISOLanguageFor(code.replace('_', '-'), code) ?? - getISOLanguageFor(code.split('-')[0], code) ?? - getISOLanguageFor(code.split('_')[0], code) - ); -} - export function getLanguage(code: string): LanguageObject { const isoLanguage = getISOLanguage(code); @@ -76,31 +51,12 @@ export function languageCodeToName(code: string): string { return getLanguage(code).nativeName; } -export const toUniqueLanguageCodes = (codes: string[]): string[] => { - const languages = codes.map((code) => getLanguage(code)); - const languagesByIsoCode = Object.groupBy(languages, (language) => language.isoCode); - - return Object.entries(languagesByIsoCode) - .filter(([, languagesOfIsoCode]) => !!languagesOfIsoCode?.length) - .map(([, languagesOfIsoCode]) => languagesOfIsoCode![0].orgCode); -}; - export const toComparableLanguage = (code: string): string => getLanguage(code).isoCode; export const toComparableLanguages = (codes: string[]): string[] => codes.map(toComparableLanguage); -function defaultNativeLang(): readonly string[] { - const preferredLanguages = toUniqueLanguageCodes([...navigator.languages]); - - if (!preferredLanguages.length) { - return ['en']; - } - - return preferredLanguages; -} - export function getDefaultLanguages(): string[] { - return [...defaultNativeLang(), DefaultLanguage.ALL]; + return [...getPreferredISOLanguageCodes(), DefaultLanguage.ALL]; } /** diff --git a/src/features/extension/Extensions.utils.ts b/src/features/extension/Extensions.utils.ts index 0448e858..2151b9a3 100644 --- a/src/features/extension/Extensions.utils.ts +++ b/src/features/extension/Extensions.utils.ts @@ -21,7 +21,6 @@ import { languageSpecialSortComparator, toComparableLanguage, toComparableLanguages, - toUniqueLanguageCodes, } from '@/base/utils/Languages.ts'; import { EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP, @@ -31,6 +30,7 @@ import { enhancedCleanup } from '@/base/utils/Strings.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/base/utils/Toast.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { toUniqueISOLanguageCodes } from '@/lib/ISOLanguageUtil.ts'; export const getInstalledState = ( isInstalled: boolean, @@ -131,7 +131,7 @@ export const filterExtensions = ( query?: string | null | undefined; } = {}, ): TExtension[] => { - const normalizedSelectedLanguages = toComparableLanguages(toUniqueLanguageCodes(selectedLanguages ?? [])); + const normalizedSelectedLanguages = toComparableLanguages(toUniqueISOLanguageCodes(selectedLanguages ?? [])); return extensions .filter( diff --git a/src/features/source/components/SourceLanguageSelect.tsx b/src/features/source/components/SourceLanguageSelect.tsx index 2d450e29..13b26a2e 100644 --- a/src/features/source/components/SourceLanguageSelect.tsx +++ b/src/features/source/components/SourceLanguageSelect.tsx @@ -23,7 +23,7 @@ import Box from '@mui/material/Box'; import ListItemAvatar from '@mui/material/ListItemAvatar'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts'; -import { languageSortComparator, toUniqueLanguageCodes } from '@/base/utils/Languages.ts'; +import { languageSortComparator } from '@/base/utils/Languages.ts'; import { SourceDisplayNameInfo, SourceIconInfo, @@ -38,6 +38,7 @@ import { requestManager } from '@/lib/requests/RequestManager.ts'; import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx'; import { makeToast } from '@/base/utils/Toast.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { toUniqueISOLanguageCodes } from '@/lib/ISOLanguageUtil.ts'; export const SourceLanguageSelect = ({ selectedLanguages, @@ -59,14 +60,14 @@ export const SourceLanguageSelect = ({ const [tmpSourceIdToEnabledState, setTmpSourceIdToEnabledState] = useState>({}); - const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueLanguageCodes(selectedLanguages)); + const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueISOLanguageCodes(selectedLanguages)); const [open, setOpen] = useState(false); const sourcesByLanguage = useMemo(() => Sources.groupByLanguage(sources), [sources]); const languagesSortedBySelectState = useMemo( () => - toUniqueLanguageCodes([ + toUniqueISOLanguageCodes([ ...tmpSelectedLanguages.toSorted(languageSortComparator), ...languages.toSorted(languageSortComparator), ]), @@ -77,7 +78,7 @@ export const SourceLanguageSelect = ({ setOpen(false); setTmpSourceIdToEnabledState({}); - setTmpSelectedLanguages(toUniqueLanguageCodes(selectedLanguages)); + setTmpSelectedLanguages(toUniqueISOLanguageCodes(selectedLanguages)); }; const handleOk = () => { @@ -89,7 +90,7 @@ export const SourceLanguageSelect = ({ ), ).catch((e) => makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e))); setTmpSourceIdToEnabledState({}); - setSelectedLanguages(toUniqueLanguageCodes(tmpSelectedLanguages)); + setSelectedLanguages(toUniqueISOLanguageCodes(tmpSelectedLanguages)); }; const handleChange = (language: string, selected: boolean) => { diff --git a/src/features/source/services/Sources.ts b/src/features/source/services/Sources.ts index e57a817a..48283957 100644 --- a/src/features/source/services/Sources.ts +++ b/src/features/source/services/Sources.ts @@ -21,7 +21,6 @@ import { languageSpecialSortComparator, toComparableLanguage, toComparableLanguages, - toUniqueLanguageCodes, } from '@/base/utils/Languages.ts'; import { getSourceMetadata } from '@/features/source/services/SourceMetadata.ts'; import { @@ -30,6 +29,7 @@ import { } from '@/features/settings/services/ServerSettingsMetadata.ts'; import { makeToast } from '@/base/utils/Toast.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { toUniqueISOLanguageCodes } from '@/lib/ISOLanguageUtil.ts'; export class Sources { static readonly LOCAL_SOURCE_ID = '0'; @@ -98,7 +98,7 @@ export class Sources { enabled?: boolean; } = {}, ): Source[] { - const normalizedLanguages = toComparableLanguages(toUniqueLanguageCodes(languages ?? [])); + const normalizedLanguages = toComparableLanguages(toUniqueISOLanguageCodes(languages ?? [])); return sources .filter( diff --git a/src/lib/ISOLanguageUtil.ts b/src/lib/ISOLanguageUtil.ts new file mode 100644 index 00000000..e5df84aa --- /dev/null +++ b/src/lib/ISOLanguageUtil.ts @@ -0,0 +1,55 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { ISOLanguage, IsoLanguages } from '@/base/IsoLanguages.ts'; + +export type LanguageObject = ISOLanguage & { orgCode: string; isoCode: string }; + +export const DEFAULT_LANGUAGE = 'en'; + +export function getISOLanguageFor(code: string, orgCode: string = code, isoCode: string = code): LanguageObject | null { + if (IsoLanguages[code]) { + return { + ...IsoLanguages[code], + orgCode, + isoCode, + }; + } + + return null; +} + +export function getISOLanguage(code: string): LanguageObject | null { + return ( + getISOLanguageFor(code) ?? + getISOLanguageFor(code.toLowerCase(), code) ?? + getISOLanguageFor(code.replace('-', '_'), code) ?? + getISOLanguageFor(code.replace('_', '-'), code) ?? + getISOLanguageFor(code.split('-')[0], code) ?? + getISOLanguageFor(code.split('_')[0], code) + ); +} + +export const toUniqueISOLanguageCodes = (codes: string[]): string[] => { + const languages = codes.map((code) => getISOLanguage(code)).filter((code) => code !== null); + const languagesByIsoCode = Object.groupBy(languages, (language) => language.isoCode); + + return Object.entries(languagesByIsoCode) + .filter(([, languagesOfIsoCode]) => !!languagesOfIsoCode?.length) + .map(([, languagesOfIsoCode]) => languagesOfIsoCode![0].orgCode); +}; + +export function getPreferredISOLanguageCodes(): readonly string[] { + const preferredLanguages = toUniqueISOLanguageCodes([...navigator.languages]); + + if (!preferredLanguages.length) { + return [DEFAULT_LANGUAGE]; + } + + return preferredLanguages; +}