feat: i18n support (#387)

This commit is contained in:
Daniel Lautzenheiser
2023-01-18 15:08:40 -05:00
committed by GitHub
parent 7372c3735b
commit a01f30ef69
22 changed files with 580 additions and 425 deletions

View File

@ -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,

View File

@ -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) => {

View File

@ -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) {

View File

@ -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>