feat: i18n support (#387)
This commit is contained in:
parent
7372c3735b
commit
a01f30ef69
@ -0,0 +1,8 @@
|
||||
---
|
||||
description: Kaffee ist ein kleiner Baum oder Strauch, der in seiner wilden Form
|
||||
im Unterholz des Waldes wächst und traditionell kommerziell unter anderen
|
||||
Bäumen angebaut wurde, die Schatten spendeten. Die waldähnliche Struktur
|
||||
schattiger Kaffeefarmen bietet Lebensraum für eine Vielzahl von wandernden und
|
||||
ansässigen Arten.
|
||||
date: 2023-01-18T14:41:54.252-05:00
|
||||
---
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
slug: file1
|
||||
description: Coffee is a small tree or shrub that grows in the forest understory
|
||||
in its wild form, and traditionally was grown commercially under other trees
|
||||
that provided shade. The forest-like structure of shade coffee farms provides
|
||||
habitat for a great number of migratory and resident species.
|
||||
date: 2023-01-18T14:41:54.252-05:00
|
||||
---
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
description: Le café est un petit arbre ou un arbuste qui pousse dans le
|
||||
sous-étage de la forêt sous sa forme sauvage et qui était traditionnellement
|
||||
cultivé commercialement sous d\'autres arbres qui fournissaient de l\'ombre.
|
||||
La structure forestière des plantations de café d\'ombre fournit un habitat à
|
||||
un grand nombre d\'espèces migratrices et résidentes.
|
||||
date: 2023-01-18T14:41:54.252-05:00
|
||||
---
|
@ -7,6 +7,19 @@ local_backend: true
|
||||
|
||||
media_folder: /packages/core/dev-test/backends/proxy/assets/upload
|
||||
public_folder: /backends/proxy/assets/upload
|
||||
i18n:
|
||||
# Required and can be one of multiple_folders, multiple_files or single_file
|
||||
# multiple_folders - persists files in `<folder>/<locale>/<slug>.<extension>`
|
||||
# multiple_files - persists files in `<folder>/<slug>.<locale>.<extension>`
|
||||
# single_file - persists a single file in `<folder>/<slug>.<extension>`
|
||||
structure: multiple_files
|
||||
|
||||
# Required - a list of locales to show in the editor UI
|
||||
locales: [en, de, fr]
|
||||
|
||||
# Optional, defaults to the first item in locales.
|
||||
# The locale to be used for fields validation and as a baseline for the entry.
|
||||
defaultLocale: en
|
||||
collections:
|
||||
- name: posts
|
||||
label: Posts
|
||||
@ -469,3 +482,24 @@ collections:
|
||||
- label: File
|
||||
name: file
|
||||
widget: file
|
||||
- name: i18n_playground
|
||||
label: i18n Playground
|
||||
i18n: true
|
||||
folder: packages/core/dev-test/backends/proxy/_i18n_playground
|
||||
identifier_field: slug
|
||||
create: true
|
||||
fields:
|
||||
# The slug field will be omitted from the translation.
|
||||
- name: slug
|
||||
label: Slug
|
||||
widget: string
|
||||
# same as 'i18n: translate'. Allows translation of the description field
|
||||
- name: description
|
||||
label: Description
|
||||
widget: text
|
||||
i18n: true
|
||||
# The date field will be duplicated from the default locale.
|
||||
- name: date
|
||||
label: Date
|
||||
widget: datetime
|
||||
i18n: duplicate
|
||||
|
@ -3,6 +3,19 @@ backend:
|
||||
site_url: 'https://example.com'
|
||||
media_folder: assets/uploads
|
||||
locale: en
|
||||
i18n:
|
||||
# Required and can be one of multiple_folders, multiple_files or single_file
|
||||
# multiple_folders - persists files in `<folder>/<locale>/<slug>.<extension>`
|
||||
# multiple_files - persists files in `<folder>/<slug>.<locale>.<extension>`
|
||||
# single_file - persists a single file in `<folder>/<slug>.<extension>`
|
||||
structure: multiple_files
|
||||
|
||||
# Required - a list of locales to show in the editor UI
|
||||
locales: [en, de, fr]
|
||||
|
||||
# Optional, defaults to the first item in locales.
|
||||
# The locale to be used for fields validation and as a baseline for the entry.
|
||||
defaultLocale: en
|
||||
collections:
|
||||
- name: posts
|
||||
label: Posts
|
||||
@ -1188,3 +1201,24 @@ collections:
|
||||
- label: File
|
||||
name: file
|
||||
widget: file
|
||||
- name: i18n_playground
|
||||
label: i18n Playground
|
||||
i18n: true
|
||||
folder: _i18n_playground
|
||||
identifier_field: slug
|
||||
create: true
|
||||
fields:
|
||||
# The slug field will be omitted from the translation.
|
||||
- name: slug
|
||||
label: Slug
|
||||
widget: string
|
||||
# same as 'i18n: translate'. Allows translation of the description field
|
||||
- name: description
|
||||
label: Description
|
||||
widget: text
|
||||
i18n: true
|
||||
# The date field will be duplicated from the default locale.
|
||||
- name: date
|
||||
label: Date
|
||||
widget: datetime
|
||||
i18n: duplicate
|
||||
|
File diff suppressed because one or more lines are too long
@ -469,10 +469,14 @@ export function changeDraftField({
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function changeDraftFieldValidation(path: string, errors: FieldError[]) {
|
||||
export function changeDraftFieldValidation(
|
||||
path: string,
|
||||
errors: FieldError[],
|
||||
i18n?: I18nSettings,
|
||||
) {
|
||||
return {
|
||||
type: DRAFT_VALIDATION_ERRORS,
|
||||
payload: { path, errors },
|
||||
payload: { path, errors, i18n },
|
||||
} as const;
|
||||
}
|
||||
|
||||
@ -662,12 +666,12 @@ export function loadEntries(collection: Collection, page = 0) {
|
||||
|
||||
const backend = currentBackend(configState.config);
|
||||
|
||||
const append = !!(page && !isNaN(page) && page > 0);
|
||||
console.log('Trying to load page', page);
|
||||
const loadAllEntries = 'nested' in collection || hasI18n(collection);
|
||||
const append = !!(page && !isNaN(page) && page > 0) && !loadAllEntries;
|
||||
dispatch(entriesLoading(collection));
|
||||
|
||||
try {
|
||||
const loadAllEntries = 'nested' in collection || hasI18n(collection);
|
||||
|
||||
const response: {
|
||||
cursor?: Cursor;
|
||||
pagination?: number;
|
||||
@ -683,7 +687,7 @@ export function loadEntries(collection: Collection, page = 0) {
|
||||
? Cursor.create({
|
||||
actions: ['next'],
|
||||
meta: { usingOldPaginationAPI: true },
|
||||
data: { nextPage: page + 1 },
|
||||
data: { nextPage: loadAllEntries ? -1 : page + 1 },
|
||||
})
|
||||
: Cursor.create(response.cursor),
|
||||
};
|
||||
@ -749,7 +753,12 @@ export function traverseCollectionCursor(collection: Collection, action: string)
|
||||
|
||||
// Handle cursors representing pages in the old, integer-based pagination API
|
||||
if (cursor.meta?.usingOldPaginationAPI ?? false) {
|
||||
return dispatch(loadEntries(collection, cursor.data!.nextPage as number));
|
||||
const nextPage = (cursor.data!.nextPage as number) ?? -1;
|
||||
if (nextPage < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return dispatch(loadEntries(collection, nextPage));
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -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>
|
||||
|
@ -44,18 +44,15 @@ export function getI18nFilesDepth(collection: Collection, depth: number) {
|
||||
}
|
||||
|
||||
export function isFieldTranslatable(field: Field, locale?: string, defaultLocale?: string) {
|
||||
const isTranslatable = locale !== defaultLocale && field.i18n === I18N_FIELD.TRANSLATE;
|
||||
return isTranslatable;
|
||||
return locale !== defaultLocale && field.i18n === I18N_FIELD.TRANSLATE;
|
||||
}
|
||||
|
||||
export function isFieldDuplicate(field: Field, locale?: string, defaultLocale?: string) {
|
||||
const isDuplicate = locale !== defaultLocale && field.i18n === I18N_FIELD.DUPLICATE;
|
||||
return isDuplicate;
|
||||
return locale !== defaultLocale && field.i18n === I18N_FIELD.DUPLICATE;
|
||||
}
|
||||
|
||||
export function isFieldHidden(field: Field, locale?: string, defaultLocale?: string) {
|
||||
const isHidden = locale !== defaultLocale && field.i18n === I18N_FIELD.NONE;
|
||||
return isHidden;
|
||||
return locale !== defaultLocale && field.i18n === I18N_FIELD.NONE;
|
||||
}
|
||||
|
||||
export function getLocaleDataPath(locale: string) {
|
||||
@ -205,8 +202,15 @@ export function getI18nBackup(
|
||||
if (!data) {
|
||||
return acc;
|
||||
}
|
||||
entry.data = data;
|
||||
return { ...acc, [locale]: { raw: entryToRaw(entry) } };
|
||||
return {
|
||||
...acc,
|
||||
[locale]: {
|
||||
raw: entryToRaw({
|
||||
...entry,
|
||||
data,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}, {} as Record<string, { raw: string }>);
|
||||
|
||||
return i18nBackup;
|
||||
@ -386,9 +390,9 @@ export function duplicateI18nFields(
|
||||
locales
|
||||
.filter(l => l !== defaultLocale)
|
||||
.forEach(l => {
|
||||
entryDraft = get(
|
||||
entryDraft = set(
|
||||
entryDraft,
|
||||
['entry', ...getDataPath(l, defaultLocale), ...fieldPath],
|
||||
['entry', ...getDataPath(l, defaultLocale), ...fieldPath].join('.'),
|
||||
value,
|
||||
);
|
||||
});
|
||||
@ -414,8 +418,10 @@ export function getPreviewEntry(
|
||||
if (!locale || locale === defaultLocale) {
|
||||
return entry;
|
||||
}
|
||||
entry.data = entry.i18n?.[locale]?.data as EntryData;
|
||||
return entry;
|
||||
return {
|
||||
...entry,
|
||||
data: entry.i18n?.[locale]?.data as EntryData,
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeI18n(
|
||||
|
@ -94,9 +94,6 @@ const de: LocalePhrasesRoot = {
|
||||
},
|
||||
i18n: {
|
||||
writingInLocale: 'Aktuelle Sprache: %{locale}',
|
||||
copyFromLocale: 'Aus anderer Sprache übernehmen',
|
||||
copyFromLocaleConfirmBody:
|
||||
'Wollen Sie wirklich die Daten aus der Sprache %{locale} übernehmen?\nAlle bishergen Inhalte werden überschrieben.',
|
||||
},
|
||||
},
|
||||
editor: {
|
||||
|
@ -98,10 +98,6 @@ const en: LocalePhrasesRoot = {
|
||||
},
|
||||
i18n: {
|
||||
writingInLocale: 'Writing in %{locale}',
|
||||
copyFromLocale: 'Fill in from another locale',
|
||||
copyFromLocaleConfirmTitle: 'Fill in data from locale',
|
||||
copyFromLocaleConfirmBody:
|
||||
'Do you want to fill in data from %{locale} locale?\nAll existing content will be overwritten.',
|
||||
},
|
||||
},
|
||||
editor: {
|
||||
|
@ -95,9 +95,6 @@ const tr: LocalePhrasesRoot = {
|
||||
},
|
||||
i18n: {
|
||||
writingInLocale: '%{locale} için yazılıyor',
|
||||
copyFromLocale: 'Başka bir dilden doldurun',
|
||||
copyFromLocaleConfirmBody:
|
||||
'Verileri %{locale} dilinden mi doldurmak istiyorsun?\nVarolan bütün verilerin üzerine yazılacak.',
|
||||
},
|
||||
},
|
||||
editor: {
|
||||
|
@ -181,6 +181,7 @@ function entries(
|
||||
const payload = action.payload;
|
||||
const loadedEntries = payload.entries;
|
||||
const page = payload.page;
|
||||
const append = payload.append;
|
||||
|
||||
const entities = {
|
||||
...state.entities,
|
||||
@ -196,7 +197,9 @@ function entries(
|
||||
|
||||
pages[payload.collection] = {
|
||||
page: page ?? undefined,
|
||||
ids: [...(pages[payload.collection]?.ids ?? []), ...loadedEntries.map(entry => entry.slug)],
|
||||
ids: append
|
||||
? [...(pages[payload.collection]?.ids ?? []), ...loadedEntries.map(entry => entry.slug)]
|
||||
: [...loadedEntries.map(entry => entry.slug)],
|
||||
isFetching: false,
|
||||
};
|
||||
|
||||
|
@ -162,10 +162,14 @@ function entryDraftReducer(
|
||||
entry: set(newState.entry, `${dataPath.join('.')}.${path}`, value),
|
||||
};
|
||||
|
||||
console.log('BEFORE I18N', { ...newState.entry });
|
||||
|
||||
if (i18n) {
|
||||
newState = duplicateI18nFields(newState, field, i18n.locales, i18n.defaultLocale);
|
||||
}
|
||||
|
||||
console.log('AFTER I18N', { ...newState.entry });
|
||||
|
||||
const newData = get(newState.entry, dataPath) ?? {};
|
||||
|
||||
return {
|
||||
@ -175,12 +179,16 @@ function entryDraftReducer(
|
||||
}
|
||||
|
||||
case DRAFT_VALIDATION_ERRORS: {
|
||||
const { path, errors } = action.payload;
|
||||
const { path, errors, i18n } = action.payload;
|
||||
const fieldsErrors = { ...state.fieldsErrors };
|
||||
|
||||
const dataPath = (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data'];
|
||||
const fullPath = `${dataPath.join('.')}.${path}`;
|
||||
|
||||
if (errors.length === 0) {
|
||||
delete fieldsErrors[path];
|
||||
delete fieldsErrors[fullPath];
|
||||
} else {
|
||||
fieldsErrors[path] = action.payload.errors;
|
||||
fieldsErrors[fullPath] = action.payload.errors;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
|
@ -1,9 +1,14 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { getDataPath } from '@staticcms/core/lib/i18n';
|
||||
|
||||
import type { I18nSettings } from '@staticcms/core/interface';
|
||||
import type { RootState } from '@staticcms/core/store';
|
||||
|
||||
export const selectFieldErrors = (path: string) => (state: RootState) => {
|
||||
return state.entryDraft.fieldsErrors[path] ?? [];
|
||||
};
|
||||
export const selectFieldErrors =
|
||||
(path: string, i18n: I18nSettings | undefined) => (state: RootState) => {
|
||||
const dataPath = (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data'];
|
||||
const fullPath = `${dataPath.join('.')}.${path}`;
|
||||
return state.entryDraft.fieldsErrors[fullPath] ?? [];
|
||||
};
|
||||
|
||||
export function selectEditingDraft(state: RootState) {
|
||||
return state.entryDraft.entry;
|
||||
|
@ -168,6 +168,7 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
|
||||
label={label}
|
||||
value={inputDate}
|
||||
onChange={handleChange}
|
||||
disabled={isDisabled}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
key="mobile-date-input"
|
||||
@ -198,6 +199,7 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
|
||||
inputFormat={inputFormat}
|
||||
value={inputDate}
|
||||
onChange={handleChange}
|
||||
disabled={isDisabled}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
key="time-input"
|
||||
@ -227,6 +229,7 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
|
||||
label={label}
|
||||
value={inputDate}
|
||||
onChange={handleChange}
|
||||
disabled={isDisabled}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
key="mobile-date-time-input"
|
||||
|
@ -10,276 +10,6 @@ Static CMS runs new functionality in an open beta format from time to time. That
|
||||
|
||||
**Use these features at your own risk.**
|
||||
|
||||
## i18n Support
|
||||
|
||||
The CMS can provide a side by side interface for authoring content in multiple languages.
|
||||
Configuring the CMS for i18n support requires top level configuration, collection level configuration and field level configuration.
|
||||
|
||||
### Top level configuration
|
||||
|
||||
<CodeTabs>
|
||||
|
||||
```yaml
|
||||
i18n:
|
||||
# Required and can be one of multiple_folders, multiple_files or single_file
|
||||
# multiple_folders - persists files in `<folder>/<locale>/<slug>.<extension>`
|
||||
# multiple_files - persists files in `<folder>/<slug>.<locale>.<extension>`
|
||||
# single_file - persists a single file in `<folder>/<slug>.<extension>`
|
||||
structure: multiple_folders
|
||||
|
||||
# Required - a list of locales to show in the editor UI
|
||||
locales: [en, de, fr]
|
||||
|
||||
# Optional, defaults to the first item in locales.
|
||||
# The locale to be used for fields validation and as a baseline for the entry.
|
||||
defaultLocale: en
|
||||
```
|
||||
|
||||
```js
|
||||
i18n: {
|
||||
/**
|
||||
* Required and can be one of multiple_folders, multiple_files or single_file
|
||||
* multiple_folders - persists files in `<folder>/<locale>/<slug>.<extension>`
|
||||
* multiple_files - persists files in `<folder>/<slug>.<locale>.<extension>`
|
||||
* single_file - persists a single file in `<folder>/<slug>.<extension>`
|
||||
*/
|
||||
structure: 'multiple_folders',
|
||||
|
||||
// Required - a list of locales to show in the editor UI
|
||||
locales: ['en', 'de', 'fr'],
|
||||
|
||||
/**
|
||||
* Optional, defaults to the first item in locales.
|
||||
* The locale to be used for fields validation and as a baseline for the entry.
|
||||
*/
|
||||
defaultLocale: 'en'
|
||||
},
|
||||
```
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
### Collection level configuration
|
||||
|
||||
<CodeTabs>
|
||||
```yaml
|
||||
collections:
|
||||
- name: i18n_content
|
||||
# same as the top level, but all fields are optional and defaults to the top level
|
||||
# can also be a boolean to accept the top level defaults
|
||||
i18n: true
|
||||
```
|
||||
|
||||
```js
|
||||
collections: [
|
||||
{
|
||||
name: 'i18n_content',
|
||||
/**
|
||||
* same as the top level, but all fields are optional and defaults to the top level
|
||||
* can also be a boolean to accept the top level defaults
|
||||
*/
|
||||
i18n: true
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
When using a file collection, you must also enable i18n for each individual file:
|
||||
|
||||
<CodeTabs>
|
||||
```yaml
|
||||
collections:
|
||||
- name: pages
|
||||
label: Pages
|
||||
# Configure i18n for this collection.
|
||||
i18n:
|
||||
structure: single_file
|
||||
locales: [en, de, fr]
|
||||
files:
|
||||
- name: about
|
||||
label: About Page
|
||||
file: site/content/about.yml
|
||||
# Enable i18n for this file.
|
||||
i18n: true
|
||||
fields:
|
||||
- { label: Title, name: title, widget: string, i18n: true }
|
||||
```
|
||||
|
||||
```js
|
||||
collections: [
|
||||
{
|
||||
name: 'pages',
|
||||
label: 'Pages',
|
||||
// Configure i18n for this collection.
|
||||
i18n: {
|
||||
structure: 'single_file',
|
||||
locales: ['en', 'de', 'fr']
|
||||
},
|
||||
files: [
|
||||
{
|
||||
name: 'about',
|
||||
label: 'About Page',
|
||||
file: 'site/content/about.yml',
|
||||
// Enable i18n for this file.
|
||||
i18n: true,
|
||||
fields: [
|
||||
{ label: 'Title', name: 'title', widget: 'string', i18n: true }
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
### Field level configuration
|
||||
|
||||
<CodeTabs>
|
||||
```yaml
|
||||
fields:
|
||||
- label: Title
|
||||
name: title
|
||||
widget: string
|
||||
# same as 'i18n: translate'. Allows translation of the title field
|
||||
i18n: true
|
||||
- label: Date
|
||||
name: date
|
||||
widget: datetime
|
||||
# The date field will be duplicated from the default locale.
|
||||
i18n: duplicate
|
||||
- label: Body
|
||||
name: body
|
||||
# The markdown field will be omitted from the translation.
|
||||
widget: markdown
|
||||
```
|
||||
|
||||
```js
|
||||
fields: [
|
||||
{
|
||||
label: 'Title',
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
// same as 'i18n: translate'. Allows translation of the title field
|
||||
i18n: true
|
||||
},
|
||||
{
|
||||
label: 'Date',
|
||||
name: 'date',
|
||||
widget: 'datetime',
|
||||
// The date field will be duplicated from the default locale.
|
||||
i18n: 'duplicate'
|
||||
},
|
||||
{
|
||||
label: 'Body',
|
||||
name: 'body',
|
||||
// The markdown field will be omitted from the translation.
|
||||
widget: 'markdown'
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
Example configuration:
|
||||
|
||||
<CodeTabs>
|
||||
```yaml
|
||||
i18n:
|
||||
structure: multiple_folders
|
||||
locales: [en, de, fr]
|
||||
|
||||
collections:
|
||||
- name: posts
|
||||
label: Posts
|
||||
folder: content/posts
|
||||
create: true
|
||||
i18n: true
|
||||
fields:
|
||||
- label: Title
|
||||
name: title
|
||||
widget: string
|
||||
i18n: true
|
||||
- label: Date
|
||||
name: date
|
||||
widget: datetime
|
||||
i18n: duplicate
|
||||
- label: Body
|
||||
name: body
|
||||
widget: markdown
|
||||
```
|
||||
|
||||
```js
|
||||
i18n: {
|
||||
structure: 'multiple_folders',
|
||||
locales: ['en', 'de', 'fr']
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
name: 'posts',
|
||||
label: 'Posts',
|
||||
folder: 'content/posts',
|
||||
create: true,
|
||||
i18n: true,
|
||||
fields: [
|
||||
{ label: 'Title', name: 'title', widget: 'string', i18n: true },
|
||||
{ label: 'Date', name: 'date', widget: 'datetime', i18n: 'duplicate' },
|
||||
{ label: 'Body', name: 'body', widget: 'markdown' },
|
||||
],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
### Limitations
|
||||
|
||||
1. File collections support only `structure: single_file`.
|
||||
2. List widgets only support `i18n: true`. `i18n` configuration on sub fields is ignored.
|
||||
3. Object widgets only support `i18n: true` and `i18n` configuration should be done per field:
|
||||
|
||||
<CodeTabs>
|
||||
```yaml
|
||||
- label: 'Object'
|
||||
name: 'object'
|
||||
widget: 'object'
|
||||
i18n: true
|
||||
fields:
|
||||
- { label: 'String', name: 'string', widget: 'string', i18n: true }
|
||||
- { label: 'Date', name: 'date', widget: 'datetime', i18n: duplicate }
|
||||
- { label: 'Boolean', name: 'boolean', widget: 'boolean', i18n: duplicate }
|
||||
- {
|
||||
label: 'Object',
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
i18n: true,
|
||||
field: { label: 'String', name: 'string', widget: 'string', i18n: duplicate },
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
{
|
||||
label: 'Object',
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
i18n: true,
|
||||
fields: [
|
||||
{ label: 'String', name: 'string', widget: 'string', i18n: true },
|
||||
{ label: 'Date', name: 'date', widget: 'datetime', i18n: 'duplicate' },
|
||||
{ label: 'Boolean', name: 'boolean', widget: 'boolean', i18n: 'duplicate' },
|
||||
{
|
||||
label: 'Object',
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
i18n: true,
|
||||
field: { label: 'String', name: 'string', widget: 'string', i18n: 'duplicate' },
|
||||
},
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
## Folder Collections Path
|
||||
|
||||
See [Folder Collections Path](/docs/collection-types#folder-collections-path).
|
||||
|
274
packages/docs/content/docs/i18n-support.mdx
Normal file
274
packages/docs/content/docs/i18n-support.mdx
Normal file
@ -0,0 +1,274 @@
|
||||
---
|
||||
group: Collections
|
||||
title: i18n Support
|
||||
beta: true
|
||||
weight: 30
|
||||
---
|
||||
|
||||
The CMS can provide a side by side interface for authoring content in multiple languages.
|
||||
Configuring the CMS for i18n support requires top level configuration, collection level configuration and field level configuration.
|
||||
|
||||
### Top level configuration
|
||||
|
||||
<CodeTabs>
|
||||
|
||||
```yaml
|
||||
i18n:
|
||||
# Required and can be one of multiple_folders, multiple_files or single_file
|
||||
# multiple_folders - persists files in `<folder>/<locale>/<slug>.<extension>`
|
||||
# multiple_files - persists files in `<folder>/<slug>.<locale>.<extension>`
|
||||
# single_file - persists a single file in `<folder>/<slug>.<extension>`
|
||||
structure: multiple_folders
|
||||
|
||||
# Required - a list of locales to show in the editor UI
|
||||
locales: [en, de, fr]
|
||||
|
||||
# Optional, defaults to the first item in locales.
|
||||
# The locale to be used for fields validation and as a baseline for the entry.
|
||||
defaultLocale: en
|
||||
```
|
||||
|
||||
```js
|
||||
i18n: {
|
||||
/**
|
||||
* Required and can be one of multiple_folders, multiple_files or single_file
|
||||
* multiple_folders - persists files in `<folder>/<locale>/<slug>.<extension>`
|
||||
* multiple_files - persists files in `<folder>/<slug>.<locale>.<extension>`
|
||||
* single_file - persists a single file in `<folder>/<slug>.<extension>`
|
||||
*/
|
||||
structure: 'multiple_folders',
|
||||
|
||||
// Required - a list of locales to show in the editor UI
|
||||
locales: ['en', 'de', 'fr'],
|
||||
|
||||
/**
|
||||
* Optional, defaults to the first item in locales.
|
||||
* The locale to be used for fields validation and as a baseline for the entry.
|
||||
*/
|
||||
defaultLocale: 'en'
|
||||
},
|
||||
```
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
### Collection level configuration
|
||||
|
||||
<CodeTabs>
|
||||
```yaml
|
||||
collections:
|
||||
- name: i18n_content
|
||||
# same as the top level, but all fields are optional and defaults to the top level
|
||||
# can also be a boolean to accept the top level defaults
|
||||
i18n: true
|
||||
```
|
||||
|
||||
```js
|
||||
collections: [
|
||||
{
|
||||
name: 'i18n_content',
|
||||
/**
|
||||
* same as the top level, but all fields are optional and defaults to the top level
|
||||
* can also be a boolean to accept the top level defaults
|
||||
*/
|
||||
i18n: true
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
When using a file collection, you must also enable i18n for each individual file:
|
||||
|
||||
<CodeTabs>
|
||||
```yaml
|
||||
collections:
|
||||
- name: pages
|
||||
label: Pages
|
||||
# Configure i18n for this collection.
|
||||
i18n:
|
||||
structure: single_file
|
||||
locales: [en, de, fr]
|
||||
files:
|
||||
- name: about
|
||||
label: About Page
|
||||
file: site/content/about.yml
|
||||
# Enable i18n for this file.
|
||||
i18n: true
|
||||
fields:
|
||||
- { label: Title, name: title, widget: string, i18n: true }
|
||||
```
|
||||
|
||||
```js
|
||||
collections: [
|
||||
{
|
||||
name: 'pages',
|
||||
label: 'Pages',
|
||||
// Configure i18n for this collection.
|
||||
i18n: {
|
||||
structure: 'single_file',
|
||||
locales: ['en', 'de', 'fr']
|
||||
},
|
||||
files: [
|
||||
{
|
||||
name: 'about',
|
||||
label: 'About Page',
|
||||
file: 'site/content/about.yml',
|
||||
// Enable i18n for this file.
|
||||
i18n: true,
|
||||
fields: [
|
||||
{ label: 'Title', name: 'title', widget: 'string', i18n: true }
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
### Field level configuration
|
||||
|
||||
<CodeTabs>
|
||||
```yaml
|
||||
fields:
|
||||
- label: Title
|
||||
name: title
|
||||
widget: string
|
||||
# same as 'i18n: translate'. Allows translation of the title field
|
||||
i18n: true
|
||||
- label: Date
|
||||
name: date
|
||||
widget: datetime
|
||||
# The date field will be duplicated from the default locale.
|
||||
i18n: duplicate
|
||||
- label: Body
|
||||
name: body
|
||||
# The markdown field will be omitted from the translation.
|
||||
widget: markdown
|
||||
```
|
||||
|
||||
```js
|
||||
fields: [
|
||||
{
|
||||
label: 'Title',
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
// same as 'i18n: translate'. Allows translation of the title field
|
||||
i18n: true
|
||||
},
|
||||
{
|
||||
label: 'Date',
|
||||
name: 'date',
|
||||
widget: 'datetime',
|
||||
// The date field will be duplicated from the default locale.
|
||||
i18n: 'duplicate'
|
||||
},
|
||||
{
|
||||
label: 'Body',
|
||||
name: 'body',
|
||||
// The markdown field will be omitted from the translation.
|
||||
widget: 'markdown'
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
Example configuration:
|
||||
|
||||
<CodeTabs>
|
||||
```yaml
|
||||
i18n:
|
||||
structure: multiple_folders
|
||||
locales: [en, de, fr]
|
||||
|
||||
collections:
|
||||
- name: posts
|
||||
label: Posts
|
||||
folder: content/posts
|
||||
create: true
|
||||
i18n: true
|
||||
fields:
|
||||
- label: Title
|
||||
name: title
|
||||
widget: string
|
||||
i18n: true
|
||||
- label: Date
|
||||
name: date
|
||||
widget: datetime
|
||||
i18n: duplicate
|
||||
- label: Body
|
||||
name: body
|
||||
widget: markdown
|
||||
```
|
||||
|
||||
```js
|
||||
i18n: {
|
||||
structure: 'multiple_folders',
|
||||
locales: ['en', 'de', 'fr']
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
name: 'posts',
|
||||
label: 'Posts',
|
||||
folder: 'content/posts',
|
||||
create: true,
|
||||
i18n: true,
|
||||
fields: [
|
||||
{ label: 'Title', name: 'title', widget: 'string', i18n: true },
|
||||
{ label: 'Date', name: 'date', widget: 'datetime', i18n: 'duplicate' },
|
||||
{ label: 'Body', name: 'body', widget: 'markdown' },
|
||||
],
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
### Limitations
|
||||
|
||||
1. File collections support only `structure: single_file`.
|
||||
2. List widgets only support `i18n: true`. `i18n` configuration on sub fields is ignored.
|
||||
3. Object widgets only support `i18n: true` and `i18n` configuration should be done per field:
|
||||
|
||||
<CodeTabs>
|
||||
```yaml
|
||||
- label: 'Object'
|
||||
name: 'object'
|
||||
widget: 'object'
|
||||
i18n: true
|
||||
fields:
|
||||
- { label: 'String', name: 'string', widget: 'string', i18n: true }
|
||||
- { label: 'Date', name: 'date', widget: 'datetime', i18n: duplicate }
|
||||
- { label: 'Boolean', name: 'boolean', widget: 'boolean', i18n: duplicate }
|
||||
- {
|
||||
label: 'Object',
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
i18n: true,
|
||||
field: { label: 'String', name: 'string', widget: 'string', i18n: duplicate },
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
{
|
||||
label: 'Object',
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
i18n: true,
|
||||
fields: [
|
||||
{ label: 'String', name: 'string', widget: 'string', i18n: true },
|
||||
{ label: 'Date', name: 'date', widget: 'datetime', i18n: 'duplicate' },
|
||||
{ label: 'Boolean', name: 'boolean', widget: 'boolean', i18n: 'duplicate' },
|
||||
{
|
||||
label: 'Object',
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
i18n: true,
|
||||
field: { label: 'String', name: 'string', widget: 'string', i18n: 'duplicate' },
|
||||
},
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
</CodeTabs>
|
@ -1,6 +1,6 @@
|
||||
[build]
|
||||
command = "yarn build"
|
||||
publish = ".next"
|
||||
ignore = "exit 0"
|
||||
|
||||
[[plugins]]
|
||||
package = "@netlify/plugin-nextjs"
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user