feat: i18n support (#387)
This commit is contained in:
committed by
GitHub
parent
7372c3735b
commit
a01f30ef69
@ -92,31 +92,35 @@ const Editor = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [collection, createBackup, slug]);
|
||||
|
||||
console.log('VERSION', version);
|
||||
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const handlePersistEntry = useCallback(
|
||||
async (opts: EditorPersistOptions = {}) => {
|
||||
(opts: EditorPersistOptions = {}) => {
|
||||
const { createNew = false, duplicate = false } = opts;
|
||||
|
||||
if (!entryDraft.entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await persistEntry(collection);
|
||||
setVersion(version + 1);
|
||||
|
||||
deleteBackup();
|
||||
|
||||
if (createNew) {
|
||||
navigateToNewEntry(collection.name);
|
||||
if (duplicate && entryDraft.entry) {
|
||||
createDraftDuplicateFromEntry(entryDraft.entry);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
|
||||
setSubmitted(true);
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await persistEntry(collection);
|
||||
setVersion(version + 1);
|
||||
|
||||
deleteBackup();
|
||||
|
||||
if (createNew) {
|
||||
navigateToNewEntry(collection.name);
|
||||
if (duplicate && entryDraft.entry) {
|
||||
createDraftDuplicateFromEntry(entryDraft.entry);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}, 100);
|
||||
},
|
||||
[
|
||||
collection,
|
||||
|
@ -178,7 +178,7 @@ const EditorControl = ({
|
||||
|
||||
const [dirty, setDirty] = useState(!isEmpty(value));
|
||||
|
||||
const fieldErrorsSelector = useMemo(() => selectFieldErrors(path), [path]);
|
||||
const fieldErrorsSelector = useMemo(() => selectFieldErrors(path, i18n), [i18n, path]);
|
||||
const errors = useAppSelector(fieldErrorsSelector);
|
||||
|
||||
const hasErrors = (submitted || dirty) && Boolean(errors.length);
|
||||
@ -192,17 +192,17 @@ const EditorControl = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dirty && !submitted) {
|
||||
if ((!dirty && !submitted) || isHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validateValue = async () => {
|
||||
const errors = await validate(field, value, widget, t);
|
||||
dispatch(changeDraftFieldValidation(path, errors));
|
||||
dispatch(changeDraftFieldValidation(path, errors, i18n));
|
||||
};
|
||||
|
||||
validateValue();
|
||||
}, [dispatch, field, path, t, value, widget, dirty, submitted]);
|
||||
}, [dirty, dispatch, field, i18n, isHidden, path, submitted, t, value, widget]);
|
||||
|
||||
const handleChangeDraftField = useCallback(
|
||||
(value: ValueOrNestedValue) => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import Button from '@mui/material/Button';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
@ -7,7 +8,6 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeDraftField as changeDraftFieldAction } from '@staticcms/core/actions/entries';
|
||||
import confirm from '@staticcms/core/components/UI/Confirm';
|
||||
import {
|
||||
getI18nInfo,
|
||||
getLocaleDataPath,
|
||||
@ -18,6 +18,7 @@ import {
|
||||
} from '@staticcms/core/lib/i18n';
|
||||
import EditorControl from './EditorControl';
|
||||
|
||||
import type { ButtonProps } from '@mui/material/Button';
|
||||
import type {
|
||||
Collection,
|
||||
Entry,
|
||||
@ -42,24 +43,55 @@ const ControlPaneContainer = styled('div')`
|
||||
|
||||
const LocaleRowWrapper = styled('div')`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const DefaultLocaleWrittingIn = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36.5px;
|
||||
`;
|
||||
|
||||
interface LocaleDropdownProps {
|
||||
locales: string[];
|
||||
defaultLocale: string;
|
||||
dropdownText: string;
|
||||
onLocaleChange: (locale: string) => void;
|
||||
color: ButtonProps['color'];
|
||||
canChangeLocale: boolean;
|
||||
onLocaleChange?: (locale: string) => void;
|
||||
}
|
||||
|
||||
const LocaleDropdown = ({ locales, dropdownText, onLocaleChange }: LocaleDropdownProps) => {
|
||||
const LocaleDropdown = ({
|
||||
locales,
|
||||
defaultLocale,
|
||||
dropdownText,
|
||||
color,
|
||||
canChangeLocale,
|
||||
onLocaleChange,
|
||||
}: LocaleDropdownProps) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = useCallback((event: MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
}, []);
|
||||
|
||||
const handleLocaleChange = useCallback(
|
||||
(locale: string) => {
|
||||
onLocaleChange?.(locale);
|
||||
handleClose();
|
||||
},
|
||||
[handleClose, onLocaleChange],
|
||||
);
|
||||
|
||||
if (!canChangeLocale) {
|
||||
return <DefaultLocaleWrittingIn>{dropdownText}</DefaultLocaleWrittingIn>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
@ -68,6 +100,9 @@ const LocaleDropdown = ({ locales, dropdownText, onLocaleChange }: LocaleDropdow
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
onClick={handleClick}
|
||||
variant="contained"
|
||||
endIcon={<KeyboardArrowDownIcon />}
|
||||
color={color}
|
||||
>
|
||||
{dropdownText}
|
||||
</Button>
|
||||
@ -80,11 +115,17 @@ const LocaleDropdown = ({ locales, dropdownText, onLocaleChange }: LocaleDropdow
|
||||
'aria-labelledby': 'basic-button',
|
||||
}}
|
||||
>
|
||||
{locales.map(locale => (
|
||||
<MenuItem key={locale} onClick={() => onLocaleChange(locale)}>
|
||||
{locale}
|
||||
</MenuItem>
|
||||
))}
|
||||
{locales
|
||||
.filter(locale => locale !== defaultLocale)
|
||||
.map(locale => (
|
||||
<MenuItem
|
||||
key={locale}
|
||||
onClick={() => handleLocaleChange(locale)}
|
||||
sx={{ minWidth: '80px' }}
|
||||
>
|
||||
{locale}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
@ -110,8 +151,8 @@ const EditorControlPane = ({
|
||||
fields,
|
||||
fieldsErrors,
|
||||
submitted,
|
||||
changeDraftField,
|
||||
locale,
|
||||
canChangeLocale = false,
|
||||
onLocaleChange,
|
||||
t,
|
||||
}: TranslatedProps<EditorControlPaneProps>) => {
|
||||
@ -128,40 +169,6 @@ const EditorControlPane = ({
|
||||
return undefined;
|
||||
}, [collection, locale]);
|
||||
|
||||
const copyFromOtherLocale = useCallback(
|
||||
({ targetLocale }: { targetLocale?: string }) =>
|
||||
async (sourceLocale: string) => {
|
||||
if (!targetLocale) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!(await confirm({
|
||||
title: 'editor.editorControlPane.i18n.copyFromLocaleConfirmTitle',
|
||||
body: {
|
||||
key: 'editor.editorControlPane.i18n.copyFromLocaleConfirmBody',
|
||||
options: { locale: sourceLocale.toUpperCase() },
|
||||
},
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fields.forEach(field => {
|
||||
if (isFieldTranslatable(field, targetLocale, sourceLocale)) {
|
||||
const copyValue = getFieldValue(
|
||||
field,
|
||||
entry,
|
||||
sourceLocale !== i18n?.defaultLocale,
|
||||
sourceLocale,
|
||||
);
|
||||
changeDraftField({ path: field.name, field, value: copyValue, i18n });
|
||||
}
|
||||
});
|
||||
},
|
||||
[fields, entry, i18n, changeDraftField],
|
||||
);
|
||||
|
||||
if (!collection || !fields) {
|
||||
return null;
|
||||
}
|
||||
@ -176,16 +183,14 @@ const EditorControlPane = ({
|
||||
<LocaleRowWrapper>
|
||||
<LocaleDropdown
|
||||
locales={i18n.locales}
|
||||
defaultLocale={i18n.defaultLocale}
|
||||
dropdownText={t('editor.editorControlPane.i18n.writingInLocale', {
|
||||
locale: locale?.toUpperCase(),
|
||||
})}
|
||||
color="primary"
|
||||
canChangeLocale={canChangeLocale}
|
||||
onLocaleChange={onLocaleChange}
|
||||
/>
|
||||
<LocaleDropdown
|
||||
locales={i18n.locales.filter(l => l !== locale)}
|
||||
dropdownText={t('editor.editorControlPane.i18n.copyFromLocale')}
|
||||
onLocaleChange={copyFromOtherLocale({ targetLocale: locale })}
|
||||
/>
|
||||
</LocaleRowWrapper>
|
||||
) : null}
|
||||
{fields.map(field => {
|
||||
@ -222,7 +227,8 @@ export interface EditorControlPaneOwnProps {
|
||||
fieldsErrors: FieldsErrors;
|
||||
submitted: boolean;
|
||||
locale?: string;
|
||||
onLocaleChange: (locale: string) => void;
|
||||
canChangeLocale?: boolean;
|
||||
onLocaleChange?: (locale: string) => void;
|
||||
}
|
||||
|
||||
function mapStateToProps(_state: RootState, ownProps: EditorControlPaneOwnProps) {
|
||||
|
@ -31,15 +31,6 @@ const StyledSplitPane = styled('div')`
|
||||
display: grid;
|
||||
grid-template-columns: min(864px, 50%) auto;
|
||||
height: calc(100vh - 64px);
|
||||
|
||||
> div:nth-of-type(2)::before {
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: calc(100vh - 64px);
|
||||
position: relative;
|
||||
background-color: rgb(223, 223, 227);
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const NoPreviewContainer = styled('div')`
|
||||
@ -78,14 +69,23 @@ const PreviewPaneContainer = styled(
|
||||
`,
|
||||
);
|
||||
|
||||
const ControlPaneContainer = styled(PreviewPaneContainer)`
|
||||
padding: 24px 16px 16px;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
`;
|
||||
interface ControlPaneContainerProps {
|
||||
$hidden?: boolean;
|
||||
}
|
||||
|
||||
const ControlPaneContainer = styled(
|
||||
PreviewPaneContainer,
|
||||
transientOptions,
|
||||
)<ControlPaneContainerProps>(
|
||||
({ $hidden = false }) => `
|
||||
padding: 24px 16px 16px;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
display: ${$hidden ? 'none' : 'flex'};
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledViewControls = styled('div')`
|
||||
position: fixed;
|
||||
@ -127,7 +127,7 @@ interface EditorInterfaceProps {
|
||||
collection: Collection;
|
||||
fields: Field[] | undefined;
|
||||
fieldsErrors: FieldsErrors;
|
||||
onPersist: (opts?: EditorPersistOptions) => Promise<void>;
|
||||
onPersist: (opts?: EditorPersistOptions) => void;
|
||||
onDelete: () => Promise<void>;
|
||||
onDuplicate: () => void;
|
||||
showDelete: boolean;
|
||||
@ -177,23 +177,15 @@ const EditorInterface = ({
|
||||
}, [loadScroll]);
|
||||
|
||||
const { locales, defaultLocale } = useMemo(() => getI18nInfo(collection), [collection]) ?? {};
|
||||
const [selectedLocale, setSelectedLocale] = useState(locales?.[0]);
|
||||
const switchToDefaultLocale = useCallback(() => {
|
||||
if (hasI18n(collection)) {
|
||||
const { defaultLocale } = getI18nInfo(collection);
|
||||
setSelectedLocale(defaultLocale);
|
||||
}
|
||||
}, [collection]);
|
||||
const [selectedLocale, setSelectedLocale] = useState<string>(locales?.[1] ?? 'en');
|
||||
|
||||
const handleOnPersist = useCallback(
|
||||
async (opts: EditorPersistOptions = {}) => {
|
||||
const { createNew = false, duplicate = false } = opts;
|
||||
await switchToDefaultLocale();
|
||||
// TODO Trigger field validation on persist
|
||||
// this.controlPaneRef.validate();
|
||||
// await switchToDefaultLocale();
|
||||
onPersist({ createNew, duplicate });
|
||||
},
|
||||
[onPersist, switchToDefaultLocale],
|
||||
[onPersist],
|
||||
);
|
||||
|
||||
const handleTogglePreview = useCallback(() => {
|
||||
@ -237,37 +229,54 @@ const EditorInterface = ({
|
||||
const collectionI18nEnabled = hasI18n(collection);
|
||||
|
||||
const editor = (
|
||||
<ControlPaneContainer id="control-pane" $overFlow>
|
||||
<ControlPaneContainer key={defaultLocale} id="control-pane">
|
||||
<EditorControlPane
|
||||
collection={collection}
|
||||
entry={entry}
|
||||
fields={fields}
|
||||
fieldsErrors={fieldsErrors}
|
||||
locale={selectedLocale}
|
||||
onLocaleChange={handleLocaleChange}
|
||||
locale={defaultLocale}
|
||||
submitted={submitted}
|
||||
t={t}
|
||||
/>
|
||||
</ControlPaneContainer>
|
||||
);
|
||||
|
||||
const editorLocale = (
|
||||
<ControlPaneContainer $overFlow={!scrollSyncEnabled}>
|
||||
<EditorControlPane
|
||||
collection={collection}
|
||||
entry={entry}
|
||||
fields={fields}
|
||||
fieldsErrors={fieldsErrors}
|
||||
locale={locales?.[1]}
|
||||
onLocaleChange={handleLocaleChange}
|
||||
submitted={submitted}
|
||||
t={t}
|
||||
/>
|
||||
</ControlPaneContainer>
|
||||
const editorLocale = useMemo(
|
||||
() =>
|
||||
(locales ?? [])
|
||||
.filter(locale => locale !== defaultLocale)
|
||||
.map(locale => (
|
||||
<ControlPaneContainer key={locale} $hidden={locale !== selectedLocale}>
|
||||
<EditorControlPane
|
||||
collection={collection}
|
||||
entry={entry}
|
||||
fields={fields}
|
||||
fieldsErrors={fieldsErrors}
|
||||
locale={locale}
|
||||
onLocaleChange={handleLocaleChange}
|
||||
submitted={submitted}
|
||||
canChangeLocale
|
||||
t={t}
|
||||
/>
|
||||
</ControlPaneContainer>
|
||||
)),
|
||||
[
|
||||
collection,
|
||||
defaultLocale,
|
||||
entry,
|
||||
fields,
|
||||
fieldsErrors,
|
||||
handleLocaleChange,
|
||||
locales,
|
||||
selectedLocale,
|
||||
submitted,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
const previewEntry = collectionI18nEnabled
|
||||
? getPreviewEntry(entry, selectedLocale, defaultLocale)
|
||||
? getPreviewEntry(entry, selectedLocale[0], defaultLocale)
|
||||
: entry;
|
||||
|
||||
const editorWithPreview = (
|
||||
@ -291,7 +300,9 @@ const EditorInterface = ({
|
||||
<div>
|
||||
<StyledSplitPane>
|
||||
<ScrollSyncPane>{editor}</ScrollSyncPane>
|
||||
<ScrollSyncPane>{editorLocale}</ScrollSyncPane>
|
||||
<ScrollSyncPane>
|
||||
<>{editorLocale}</>
|
||||
</ScrollSyncPane>
|
||||
</StyledSplitPane>
|
||||
</div>
|
||||
</ScrollSync>
|
||||
|
Reference in New Issue
Block a user