Translations: Allow to select known languages
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
68292e72b7
commit
583ed1215c
16 changed files with 111 additions and 16 deletions
|
@ -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",
|
||||
|
|
|
@ -46,6 +46,7 @@ const defaultSettings = ImmutableMap({
|
|||
autoloadMore: true,
|
||||
preserveSpoilers: false,
|
||||
autoTranslate: false,
|
||||
knownLanguages: ImmutableOrderedSet(),
|
||||
|
||||
systemFont: false,
|
||||
demetricator: false,
|
||||
|
|
|
@ -161,7 +161,6 @@ const expandFollowedHashtagsFail = (error: AxiosError) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
|
||||
export {
|
||||
HASHTAG_FETCH_REQUEST,
|
||||
HASHTAG_FETCH_SUCCESS,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 })}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -13,7 +13,6 @@ import GroupMemberListItem from './components/group-member-list-item';
|
|||
|
||||
import type { Group } from 'soapbox/types/entities';
|
||||
|
||||
|
||||
interface IGroupMembers {
|
||||
params: { groupId: string };
|
||||
}
|
||||
|
|
|
@ -33,7 +33,6 @@ const GroupTagTimeline: React.FC<IGroupTimeline> = (props) => {
|
|||
}
|
||||
}, [groupId, tag]);
|
||||
|
||||
|
||||
if (isLoading || !tag || !group) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -59,7 +59,6 @@ function immutableizeStore<T, S extends Record<string, T | undefined>>(state: S)
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
immutableizeStore,
|
||||
immutableizeEntity,
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue