mirror of https://github.com/mastodon/mastodon
Add language picker to server rules section (#34820)
parent
7ede5460d8
commit
d78535eab9
@ -0,0 +1,156 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import type { ChangeEventHandler, FC } from 'react';
|
||||||
|
|
||||||
|
import type { IntlShape } from 'react-intl';
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import type { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
import type { SelectItem } from '@/mastodon/components/dropdown_selector';
|
||||||
|
import type { RootState } from '@/mastodon/store';
|
||||||
|
import { useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
|
import { Section } from './section';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
rules: { id: 'about.rules', defaultMessage: 'Server rules' },
|
||||||
|
defaultLocale: { id: 'about.default_locale', defaultMessage: 'Default' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface RulesSectionProps {
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseRule {
|
||||||
|
text: string;
|
||||||
|
hint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Rule extends BaseRule {
|
||||||
|
id: string;
|
||||||
|
translations: Record<string, BaseRule>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RulesSection: FC<RulesSectionProps> = ({ isLoading = false }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [locale, setLocale] = useState(intl.locale);
|
||||||
|
const rules = useAppSelector((state) => rulesSelector(state, locale));
|
||||||
|
const localeOptions = useAppSelector((state) =>
|
||||||
|
localeOptionsSelector(state, intl),
|
||||||
|
);
|
||||||
|
const handleLocaleChange: ChangeEventHandler<HTMLSelectElement> = useCallback(
|
||||||
|
(e) => {
|
||||||
|
setLocale(e.currentTarget.value);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Section title={intl.formatMessage(messages.rules)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules.length === 0) {
|
||||||
|
return (
|
||||||
|
<Section title={intl.formatMessage(messages.rules)}>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='about.not_available'
|
||||||
|
defaultMessage='This information has not been made available on this server.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section title={intl.formatMessage(messages.rules)}>
|
||||||
|
<ol className='rules-list'>
|
||||||
|
{rules.map((rule) => (
|
||||||
|
<li key={rule.id}>
|
||||||
|
<div className='rules-list__text'>{rule.text}</div>
|
||||||
|
{!!rule.hint && <div className='rules-list__hint'>{rule.hint}</div>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div className='rules-languages'>
|
||||||
|
<label htmlFor='language-select'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='about.language_label'
|
||||||
|
defaultMessage='Language'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<select onChange={handleLocaleChange} id='language-select'>
|
||||||
|
{localeOptions.map((option) => (
|
||||||
|
<option
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
selected={option.value === locale}
|
||||||
|
>
|
||||||
|
{option.text}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectRules = (state: RootState) => {
|
||||||
|
const rules = state.server.getIn([
|
||||||
|
'server',
|
||||||
|
'rules',
|
||||||
|
]) as ImmutableList<Rule> | null;
|
||||||
|
if (!rules) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return rules.toJS() as Rule[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const rulesSelector = createSelector(
|
||||||
|
[selectRules, (_state, locale: string) => locale],
|
||||||
|
(rules, locale): Rule[] => {
|
||||||
|
return rules.map((rule) => {
|
||||||
|
const translations = rule.translations;
|
||||||
|
if (translations[locale]) {
|
||||||
|
rule.text = translations[locale].text;
|
||||||
|
rule.hint = translations[locale].hint;
|
||||||
|
}
|
||||||
|
const partialLocale = locale.split('-')[0];
|
||||||
|
if (partialLocale && translations[partialLocale]) {
|
||||||
|
rule.text = translations[partialLocale].text;
|
||||||
|
rule.hint = translations[partialLocale].hint;
|
||||||
|
}
|
||||||
|
return rule;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const localeOptionsSelector = createSelector(
|
||||||
|
[selectRules, (_state, intl: IntlShape) => intl],
|
||||||
|
(rules, intl): SelectItem[] => {
|
||||||
|
const langs: Record<string, SelectItem> = {
|
||||||
|
default: {
|
||||||
|
value: 'default',
|
||||||
|
text: intl.formatMessage(messages.defaultLocale),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Use the default locale as a target to translate language names.
|
||||||
|
const intlLocale = new Intl.DisplayNames(intl.locale, {
|
||||||
|
type: 'language',
|
||||||
|
});
|
||||||
|
for (const { translations } of rules) {
|
||||||
|
for (const locale in translations) {
|
||||||
|
if (langs[locale]) {
|
||||||
|
continue; // Skip if already added
|
||||||
|
}
|
||||||
|
langs[locale] = {
|
||||||
|
value: locale,
|
||||||
|
text: intlLocale.of(locale) ?? locale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.values(langs);
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import type { FC, MouseEventHandler } from 'react';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { Icon } from '@/mastodon/components/icon';
|
||||||
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
|
import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
|
||||||
|
|
||||||
|
interface SectionProps {
|
||||||
|
title: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
open?: boolean;
|
||||||
|
onOpen?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Section: FC<SectionProps> = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
open = false,
|
||||||
|
onOpen,
|
||||||
|
}) => {
|
||||||
|
const [collapsed, setCollapsed] = useState(!open);
|
||||||
|
const handleClick: MouseEventHandler = useCallback(() => {
|
||||||
|
setCollapsed((prev) => !prev);
|
||||||
|
onOpen?.();
|
||||||
|
}, [onOpen]);
|
||||||
|
return (
|
||||||
|
<div className={classNames('about__section', { active: !collapsed })}>
|
||||||
|
<button
|
||||||
|
className='about__section__title'
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
id={collapsed ? 'chevron-right' : 'chevron-down'}
|
||||||
|
icon={collapsed ? ChevronRightIcon : ExpandMoreIcon}
|
||||||
|
/>{' '}
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{!collapsed && <div className='about__section__body'>{children}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue