Translations: Allow to select known languages

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2023-10-18 22:36:26 +02:00
parent 68292e72b7
commit 583ed1215c
16 changed files with 111 additions and 16 deletions

View file

@ -129,6 +129,7 @@
"localforage": "^1.10.0",
"lodash": "^4.7.11",
"mini-css-extract-plugin": "^2.6.0",
"multiselect-react-dropdown": "^2.0.25",
"nostr-machina": "^0.1.0",
"nostr-tools": "^1.14.2",
"path-browserify": "^1.0.1",

View file

@ -46,6 +46,7 @@ const defaultSettings = ImmutableMap({
autoloadMore: true,
preserveSpoilers: false,
autoTranslate: false,
knownLanguages: ImmutableOrderedSet(),
systemFont: false,
demetricator: false,

View file

@ -161,7 +161,6 @@ const expandFollowedHashtagsFail = (error: AxiosError) => ({
error,
});
export {
HASHTAG_FETCH_REQUEST,
HASHTAG_FETCH_SUCCESS,

View file

@ -16,6 +16,7 @@ const List: React.FC<IList> = ({ children }) => (
);
interface IListItem {
className?: string;
label: React.ReactNode;
hint?: React.ReactNode;
to?: string;
@ -25,7 +26,7 @@ interface IListItem {
children?: React.ReactNode;
}
const ListItem: React.FC<IListItem> = ({ label, hint, children, to, onClick, onSelect, isSelected }) => {
const ListItem: React.FC<IListItem> = ({ className, label, hint, children, to, onClick, onSelect, isSelected }) => {
const id = uuidv4();
const domId = `list-group-${id}`;
@ -55,7 +56,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, to, onClick, onS
});
}, [children, domId]);
const className = clsx('flex items-center justify-between overflow-hidden bg-gradient-to-r from-gradient-start/20 to-gradient-end/20 px-4 py-2 first:rounded-t-lg last:rounded-b-lg dark:from-gradient-start/10 dark:to-gradient-end/10', {
const classNames = clsx('flex items-center justify-between overflow-hidden bg-gradient-to-r from-gradient-start/20 to-gradient-end/20 px-4 py-2 first:rounded-t-lg last:rounded-b-lg dark:from-gradient-start/10 dark:to-gradient-end/10', className, {
'cursor-pointer hover:from-gradient-start/30 hover:to-gradient-end/30 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof to !== 'undefined' || typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
});
@ -109,7 +110,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, to, onClick, onS
);
if (to) return (
<Link className={className} to={to}>
<Link className={classNames} to={to}>
{body}
</Link>
);
@ -119,7 +120,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, to, onClick, onS
return (
<Comp
className={className}
className={classNames}
{...linkProps}
>
{body}

View file

@ -42,7 +42,6 @@ const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide' },
});
const withinLimits = (aspectRatio: number) => {
return aspectRatio >= minimumAspectRatio && aspectRatio <= maximumAspectRatio;
};
@ -600,7 +599,6 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
setVisible(!!props.visible);
}, [props.visible]);
return (
<div
className={clsx(className, 'media-gallery', { 'media-gallery--compact': compact })}

View file

@ -7,6 +7,7 @@ import { isLocal } from 'soapbox/utils/accounts';
import { HStack, Icon, Stack, Text } from './ui';
import type { Set as ImmutableSet } from 'immutable';
import type { Account, Status } from 'soapbox/types/entities';
interface ITranslateButton {
@ -20,6 +21,9 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
const instance = useInstance();
const settings = useSettings();
const autoTranslate = settings.get('autoTranslate');
const knownLanguages = autoTranslate ? (settings.get('knownLanguages') as ImmutableSet<string>).add(intl.locale) : [intl.locale];
const me = useAppSelector((state) => state.me);
const {
@ -29,7 +33,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
target_languages: targetLanguages,
} = instance.pleroma.metadata.translation;
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || isLocal(status.account as Account)) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language;
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || isLocal(status.account as Account)) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && !knownLanguages.includes(status.language);
const supportsLanguages = (!sourceLanguages || sourceLanguages.includes(status.language!)) && (!targetLanguages || targetLanguages.includes(intl.locale));
@ -44,6 +48,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
};
useEffect(() => {
if (settings.get('autoTranslate') && features.translations && renderTranslate && supportsLanguages && !status.translation && status.translation !== false) {
dispatch(translateStatus(status.id, intl.locale, true));
}

View file

@ -1,8 +1,15 @@
import clsx from 'clsx';
import React, { useState } from 'react';
import MultiselectReactDropdown from 'multiselect-react-dropdown';
import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { v4 as uuidv4 } from 'uuid';
import { Select } from '../../components/ui';
import { Icon, Select } from '../../components/ui';
const messages = defineMessages({
selectPlaceholder: { id: 'select.placeholder', defaultMessage: 'Select' },
selectNoOptions: { id: 'select.no_options', defaultMessage: 'No options available' },
});
interface IInputContainer {
label?: React.ReactNode;
@ -141,6 +148,47 @@ export const SelectDropdown: React.FC<ISelectDropdown> = (props) => {
) : selectElem;
};
interface IMultiselect {
className?: string;
label?: React.ReactNode;
hint?: React.ReactNode;
items: Record<string, string>;
value?: string[];
onChange?: ((values: string[]) => void);
disabled?: boolean;
}
export const Mutliselect: React.FC<IMultiselect> = (props) => {
const intl = useIntl();
const { label, hint, items, value, onChange, disabled } = props;
const options = useMemo(() => Object.entries(items).map(([key, value]) => ({ key, value })), [items]);
const selectedValues = value?.map(key => options.find(option => option.key === key)).filter(value => value);
const handleChange = (values: Record<'key' | 'value', string>[]) => {
onChange?.(values.map(({ key }) => key));
};
const selectElem = (
<MultiselectReactDropdown
className='bigbuffet-multiselect'
options={options}
selectedValues={selectedValues}
onSelect={handleChange}
onRemove={handleChange}
displayValue='value'
disable={disabled}
customCloseIcon={<Icon className='ml-1 h-4 w-4 hover:cursor-pointer' src={require('@tabler/icons/circle-x.svg')} />}
placeholder={intl.formatMessage(messages.selectPlaceholder)}
emptyRecordMsg={intl.formatMessage(messages.selectNoOptions)}
/>
);
return label ? (
<LabelInputContainer label={label} hint={hint}>{selectElem}</LabelInputContainer>
) : selectElem;
};
interface ITextInput {
name?: string;
onChange?: React.ChangeEventHandler;

View file

@ -13,7 +13,6 @@ import GroupMemberListItem from './components/group-member-list-item';
import type { Group } from 'soapbox/types/entities';
interface IGroupMembers {
params: { groupId: string };
}

View file

@ -33,7 +33,6 @@ const GroupTagTimeline: React.FC<IGroupTimeline> = (props) => {
}
}, [groupId, tag]);
if (isLoading || !tag || !group) {
return null;
}

View file

@ -24,7 +24,6 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
const tag = useAppSelector((state) => state.tags.get(id));
const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next);
const handleLoadMore = (maxId: string) => {
dispatch(expandHashtagTimeline(id, { url: next, maxId }, intl));
};

View file

@ -1,10 +1,11 @@
import { Set as ImmutableSet } from 'immutable';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { changeSetting } from 'soapbox/actions/settings';
import List, { ListItem } from 'soapbox/components/list';
import { Form } from 'soapbox/components/ui';
import { SelectDropdown } from 'soapbox/features/forms';
import { Mutliselect, SelectDropdown } from 'soapbox/features/forms';
import SettingToggle from 'soapbox/features/notifications/components/setting-toggle';
import { useAppDispatch, useFeatures, useSettings } from 'soapbox/hooks';
@ -96,6 +97,10 @@ const Preferences = () => {
dispatch(changeSetting(path, event.target.value, { showAlert: true }));
};
const onSelectMultiple = (selectedList: string[], path: string[]) => {
dispatch(changeSetting(path, ImmutableSet(selectedList.sort((a, b) => a.localeCompare(b))), { showAlert: true }));
};
const onToggleChange = (key: string[], checked: boolean) => {
dispatch(changeSetting(key, checked, { showAlert: true }));
};
@ -213,6 +218,16 @@ const Preferences = () => {
<ListItem label={<FormattedMessage id='preferences.fields.auto_translate_label' defaultMessage='Automatically translate posts in unknown languages' />}>
<SettingToggle settings={settings} settingPath={['autoTranslate']} onChange={onToggleChange} />
</ListItem>
<ListItem className='!overflow-visible' label={<FormattedMessage id='preferences.fields.known_languages_label' defaultMessage='Languages you know' />}>
<Mutliselect
className='max-w-[200px]'
items={languages}
value={settings.get('knownLanguages').toJS() as string[] | undefined}
onChange={(selectedList) => onSelectMultiple(selectedList, ['knownLanguages'])}
disabled={!settings.get('autoTranslate')}
/>
</ListItem>
</List>
</Form>
);

View file

@ -29,6 +29,7 @@ const settingsSchema = z.object({
systemFont: z.boolean().catch(false),
demetricator: z.boolean().catch(false),
isDeveloper: z.boolean().catch(false),
knownLanguages: z.array(z.string()).catch([]),
});
type Settings = z.infer<typeof settingsSchema>;

View file

@ -310,7 +310,6 @@ const getRemoteInstanceFederation = (state: RootState, host: string): HostFedera
) as HostFederation;
};
export const makeGetHosts = () => {
return createSelector([getSimplePolicy], (simplePolicy) => {
const { accept, reject_deletes, report_removal, ...rest } = simplePolicy;

View file

@ -27,3 +27,29 @@ select {
font-size: 14px;
align-items: center;
}
.bigbuffet-multiselect {
.chip {
@apply bg-primary-600 my-1;
}
.search-wrapper {
@apply rounded-md border-gray-300 bg-white min-h-[38px] max-w-[400px] py-0 pl-3 pr-10 text-base focus:border-primary-500 focus:outline-none focus:ring-primary-500 disabled:opacity-50 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:border-primary-500 dark:focus:ring-primary-500 sm:text-sm w-auto;
> input {
@apply first:pl-0 p-1.5 m-0 focus:ring-0 text-base sm:text-sm;
}
}
.optionContainer {
@apply border-gray-300 dark:border-gray-800 dark:bg-gray-900;
}
.option {
@apply hover:bg-primary-600;
}
.highlightOption {
@apply bg-primary-600;
}
}

View file

@ -59,7 +59,6 @@ function immutableizeStore<T, S extends Record<string, T | undefined>>(state: S)
};
}
export {
immutableizeStore,
immutableizeEntity,

View file

@ -7361,6 +7361,11 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
multiselect-react-dropdown@^2.0.25:
version "2.0.25"
resolved "https://registry.yarnpkg.com/multiselect-react-dropdown/-/multiselect-react-dropdown-2.0.25.tgz#0c8d16f20d78023d5be2f3af4f15a4a164b6b427"
integrity sha512-z8kUSyBNOuV7vn4Dk35snzXWtIfTdSEEXhgDdLMvOmR+xJFx35vc1voUlSuOvrk3khb+hXC219Qs9szOvNm25Q==
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"