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
|
media_folder: /packages/core/dev-test/backends/proxy/assets/upload
|
||||||
public_folder: /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:
|
collections:
|
||||||
- name: posts
|
- name: posts
|
||||||
label: Posts
|
label: Posts
|
||||||
@ -469,3 +482,24 @@ collections:
|
|||||||
- label: File
|
- label: File
|
||||||
name: file
|
name: file
|
||||||
widget: 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'
|
site_url: 'https://example.com'
|
||||||
media_folder: assets/uploads
|
media_folder: assets/uploads
|
||||||
locale: en
|
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:
|
collections:
|
||||||
- name: posts
|
- name: posts
|
||||||
label: Posts
|
label: Posts
|
||||||
@ -1188,3 +1201,24 @@ collections:
|
|||||||
- label: File
|
- label: File
|
||||||
name: file
|
name: file
|
||||||
widget: 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;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeDraftFieldValidation(path: string, errors: FieldError[]) {
|
export function changeDraftFieldValidation(
|
||||||
|
path: string,
|
||||||
|
errors: FieldError[],
|
||||||
|
i18n?: I18nSettings,
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
type: DRAFT_VALIDATION_ERRORS,
|
type: DRAFT_VALIDATION_ERRORS,
|
||||||
payload: { path, errors },
|
payload: { path, errors, i18n },
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -662,12 +666,12 @@ export function loadEntries(collection: Collection, page = 0) {
|
|||||||
|
|
||||||
const backend = currentBackend(configState.config);
|
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));
|
dispatch(entriesLoading(collection));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loadAllEntries = 'nested' in collection || hasI18n(collection);
|
|
||||||
|
|
||||||
const response: {
|
const response: {
|
||||||
cursor?: Cursor;
|
cursor?: Cursor;
|
||||||
pagination?: number;
|
pagination?: number;
|
||||||
@ -683,7 +687,7 @@ export function loadEntries(collection: Collection, page = 0) {
|
|||||||
? Cursor.create({
|
? Cursor.create({
|
||||||
actions: ['next'],
|
actions: ['next'],
|
||||||
meta: { usingOldPaginationAPI: true },
|
meta: { usingOldPaginationAPI: true },
|
||||||
data: { nextPage: page + 1 },
|
data: { nextPage: loadAllEntries ? -1 : page + 1 },
|
||||||
})
|
})
|
||||||
: Cursor.create(response.cursor),
|
: 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
|
// Handle cursors representing pages in the old, integer-based pagination API
|
||||||
if (cursor.meta?.usingOldPaginationAPI ?? false) {
|
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 {
|
try {
|
||||||
|
@ -92,31 +92,35 @@ const Editor = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [collection, createBackup, slug]);
|
}, [collection, createBackup, slug]);
|
||||||
|
|
||||||
|
console.log('VERSION', version);
|
||||||
|
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
const handlePersistEntry = useCallback(
|
const handlePersistEntry = useCallback(
|
||||||
async (opts: EditorPersistOptions = {}) => {
|
(opts: EditorPersistOptions = {}) => {
|
||||||
const { createNew = false, duplicate = false } = opts;
|
const { createNew = false, duplicate = false } = opts;
|
||||||
|
|
||||||
if (!entryDraft.entry) {
|
if (!entryDraft.entry) {
|
||||||
return;
|
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);
|
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,
|
collection,
|
||||||
|
@ -178,7 +178,7 @@ const EditorControl = ({
|
|||||||
|
|
||||||
const [dirty, setDirty] = useState(!isEmpty(value));
|
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 errors = useAppSelector(fieldErrorsSelector);
|
||||||
|
|
||||||
const hasErrors = (submitted || dirty) && Boolean(errors.length);
|
const hasErrors = (submitted || dirty) && Boolean(errors.length);
|
||||||
@ -192,17 +192,17 @@ const EditorControl = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dirty && !submitted) {
|
if ((!dirty && !submitted) || isHidden) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateValue = async () => {
|
const validateValue = async () => {
|
||||||
const errors = await validate(field, value, widget, t);
|
const errors = await validate(field, value, widget, t);
|
||||||
dispatch(changeDraftFieldValidation(path, errors));
|
dispatch(changeDraftFieldValidation(path, errors, i18n));
|
||||||
};
|
};
|
||||||
|
|
||||||
validateValue();
|
validateValue();
|
||||||
}, [dispatch, field, path, t, value, widget, dirty, submitted]);
|
}, [dirty, dispatch, field, i18n, isHidden, path, submitted, t, value, widget]);
|
||||||
|
|
||||||
const handleChangeDraftField = useCallback(
|
const handleChangeDraftField = useCallback(
|
||||||
(value: ValueOrNestedValue) => {
|
(value: ValueOrNestedValue) => {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
@ -7,7 +8,6 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { changeDraftField as changeDraftFieldAction } from '@staticcms/core/actions/entries';
|
import { changeDraftField as changeDraftFieldAction } from '@staticcms/core/actions/entries';
|
||||||
import confirm from '@staticcms/core/components/UI/Confirm';
|
|
||||||
import {
|
import {
|
||||||
getI18nInfo,
|
getI18nInfo,
|
||||||
getLocaleDataPath,
|
getLocaleDataPath,
|
||||||
@ -18,6 +18,7 @@ import {
|
|||||||
} from '@staticcms/core/lib/i18n';
|
} from '@staticcms/core/lib/i18n';
|
||||||
import EditorControl from './EditorControl';
|
import EditorControl from './EditorControl';
|
||||||
|
|
||||||
|
import type { ButtonProps } from '@mui/material/Button';
|
||||||
import type {
|
import type {
|
||||||
Collection,
|
Collection,
|
||||||
Entry,
|
Entry,
|
||||||
@ -42,24 +43,55 @@ const ControlPaneContainer = styled('div')`
|
|||||||
|
|
||||||
const LocaleRowWrapper = styled('div')`
|
const LocaleRowWrapper = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DefaultLocaleWrittingIn = styled('div')`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 36.5px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface LocaleDropdownProps {
|
interface LocaleDropdownProps {
|
||||||
locales: string[];
|
locales: string[];
|
||||||
|
defaultLocale: string;
|
||||||
dropdownText: 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 [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
const handleClick = useCallback((event: MouseEvent<HTMLButtonElement>) => {
|
const handleClick = useCallback((event: MouseEvent<HTMLButtonElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleLocaleChange = useCallback(
|
||||||
|
(locale: string) => {
|
||||||
|
onLocaleChange?.(locale);
|
||||||
|
handleClose();
|
||||||
|
},
|
||||||
|
[handleClose, onLocaleChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!canChangeLocale) {
|
||||||
|
return <DefaultLocaleWrittingIn>{dropdownText}</DefaultLocaleWrittingIn>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
@ -68,6 +100,9 @@ const LocaleDropdown = ({ locales, dropdownText, onLocaleChange }: LocaleDropdow
|
|||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded={open ? 'true' : undefined}
|
aria-expanded={open ? 'true' : undefined}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
variant="contained"
|
||||||
|
endIcon={<KeyboardArrowDownIcon />}
|
||||||
|
color={color}
|
||||||
>
|
>
|
||||||
{dropdownText}
|
{dropdownText}
|
||||||
</Button>
|
</Button>
|
||||||
@ -80,11 +115,17 @@ const LocaleDropdown = ({ locales, dropdownText, onLocaleChange }: LocaleDropdow
|
|||||||
'aria-labelledby': 'basic-button',
|
'aria-labelledby': 'basic-button',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{locales.map(locale => (
|
{locales
|
||||||
<MenuItem key={locale} onClick={() => onLocaleChange(locale)}>
|
.filter(locale => locale !== defaultLocale)
|
||||||
{locale}
|
.map(locale => (
|
||||||
</MenuItem>
|
<MenuItem
|
||||||
))}
|
key={locale}
|
||||||
|
onClick={() => handleLocaleChange(locale)}
|
||||||
|
sx={{ minWidth: '80px' }}
|
||||||
|
>
|
||||||
|
{locale}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -110,8 +151,8 @@ const EditorControlPane = ({
|
|||||||
fields,
|
fields,
|
||||||
fieldsErrors,
|
fieldsErrors,
|
||||||
submitted,
|
submitted,
|
||||||
changeDraftField,
|
|
||||||
locale,
|
locale,
|
||||||
|
canChangeLocale = false,
|
||||||
onLocaleChange,
|
onLocaleChange,
|
||||||
t,
|
t,
|
||||||
}: TranslatedProps<EditorControlPaneProps>) => {
|
}: TranslatedProps<EditorControlPaneProps>) => {
|
||||||
@ -128,40 +169,6 @@ const EditorControlPane = ({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}, [collection, locale]);
|
}, [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) {
|
if (!collection || !fields) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -176,16 +183,14 @@ const EditorControlPane = ({
|
|||||||
<LocaleRowWrapper>
|
<LocaleRowWrapper>
|
||||||
<LocaleDropdown
|
<LocaleDropdown
|
||||||
locales={i18n.locales}
|
locales={i18n.locales}
|
||||||
|
defaultLocale={i18n.defaultLocale}
|
||||||
dropdownText={t('editor.editorControlPane.i18n.writingInLocale', {
|
dropdownText={t('editor.editorControlPane.i18n.writingInLocale', {
|
||||||
locale: locale?.toUpperCase(),
|
locale: locale?.toUpperCase(),
|
||||||
})}
|
})}
|
||||||
|
color="primary"
|
||||||
|
canChangeLocale={canChangeLocale}
|
||||||
onLocaleChange={onLocaleChange}
|
onLocaleChange={onLocaleChange}
|
||||||
/>
|
/>
|
||||||
<LocaleDropdown
|
|
||||||
locales={i18n.locales.filter(l => l !== locale)}
|
|
||||||
dropdownText={t('editor.editorControlPane.i18n.copyFromLocale')}
|
|
||||||
onLocaleChange={copyFromOtherLocale({ targetLocale: locale })}
|
|
||||||
/>
|
|
||||||
</LocaleRowWrapper>
|
</LocaleRowWrapper>
|
||||||
) : null}
|
) : null}
|
||||||
{fields.map(field => {
|
{fields.map(field => {
|
||||||
@ -222,7 +227,8 @@ export interface EditorControlPaneOwnProps {
|
|||||||
fieldsErrors: FieldsErrors;
|
fieldsErrors: FieldsErrors;
|
||||||
submitted: boolean;
|
submitted: boolean;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
onLocaleChange: (locale: string) => void;
|
canChangeLocale?: boolean;
|
||||||
|
onLocaleChange?: (locale: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(_state: RootState, ownProps: EditorControlPaneOwnProps) {
|
function mapStateToProps(_state: RootState, ownProps: EditorControlPaneOwnProps) {
|
||||||
|
@ -31,15 +31,6 @@ const StyledSplitPane = styled('div')`
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min(864px, 50%) auto;
|
grid-template-columns: min(864px, 50%) auto;
|
||||||
height: calc(100vh - 64px);
|
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')`
|
const NoPreviewContainer = styled('div')`
|
||||||
@ -78,14 +69,23 @@ const PreviewPaneContainer = styled(
|
|||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const ControlPaneContainer = styled(PreviewPaneContainer)`
|
interface ControlPaneContainerProps {
|
||||||
padding: 24px 16px 16px;
|
$hidden?: boolean;
|
||||||
position: relative;
|
}
|
||||||
overflow-x: hidden;
|
|
||||||
display: flex;
|
const ControlPaneContainer = styled(
|
||||||
align-items: flex-start;
|
PreviewPaneContainer,
|
||||||
justify-content: center;
|
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')`
|
const StyledViewControls = styled('div')`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -127,7 +127,7 @@ interface EditorInterfaceProps {
|
|||||||
collection: Collection;
|
collection: Collection;
|
||||||
fields: Field[] | undefined;
|
fields: Field[] | undefined;
|
||||||
fieldsErrors: FieldsErrors;
|
fieldsErrors: FieldsErrors;
|
||||||
onPersist: (opts?: EditorPersistOptions) => Promise<void>;
|
onPersist: (opts?: EditorPersistOptions) => void;
|
||||||
onDelete: () => Promise<void>;
|
onDelete: () => Promise<void>;
|
||||||
onDuplicate: () => void;
|
onDuplicate: () => void;
|
||||||
showDelete: boolean;
|
showDelete: boolean;
|
||||||
@ -177,23 +177,15 @@ const EditorInterface = ({
|
|||||||
}, [loadScroll]);
|
}, [loadScroll]);
|
||||||
|
|
||||||
const { locales, defaultLocale } = useMemo(() => getI18nInfo(collection), [collection]) ?? {};
|
const { locales, defaultLocale } = useMemo(() => getI18nInfo(collection), [collection]) ?? {};
|
||||||
const [selectedLocale, setSelectedLocale] = useState(locales?.[0]);
|
const [selectedLocale, setSelectedLocale] = useState<string>(locales?.[1] ?? 'en');
|
||||||
const switchToDefaultLocale = useCallback(() => {
|
|
||||||
if (hasI18n(collection)) {
|
|
||||||
const { defaultLocale } = getI18nInfo(collection);
|
|
||||||
setSelectedLocale(defaultLocale);
|
|
||||||
}
|
|
||||||
}, [collection]);
|
|
||||||
|
|
||||||
const handleOnPersist = useCallback(
|
const handleOnPersist = useCallback(
|
||||||
async (opts: EditorPersistOptions = {}) => {
|
async (opts: EditorPersistOptions = {}) => {
|
||||||
const { createNew = false, duplicate = false } = opts;
|
const { createNew = false, duplicate = false } = opts;
|
||||||
await switchToDefaultLocale();
|
// await switchToDefaultLocale();
|
||||||
// TODO Trigger field validation on persist
|
|
||||||
// this.controlPaneRef.validate();
|
|
||||||
onPersist({ createNew, duplicate });
|
onPersist({ createNew, duplicate });
|
||||||
},
|
},
|
||||||
[onPersist, switchToDefaultLocale],
|
[onPersist],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTogglePreview = useCallback(() => {
|
const handleTogglePreview = useCallback(() => {
|
||||||
@ -237,37 +229,54 @@ const EditorInterface = ({
|
|||||||
const collectionI18nEnabled = hasI18n(collection);
|
const collectionI18nEnabled = hasI18n(collection);
|
||||||
|
|
||||||
const editor = (
|
const editor = (
|
||||||
<ControlPaneContainer id="control-pane" $overFlow>
|
<ControlPaneContainer key={defaultLocale} id="control-pane">
|
||||||
<EditorControlPane
|
<EditorControlPane
|
||||||
collection={collection}
|
collection={collection}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
fieldsErrors={fieldsErrors}
|
fieldsErrors={fieldsErrors}
|
||||||
locale={selectedLocale}
|
locale={defaultLocale}
|
||||||
onLocaleChange={handleLocaleChange}
|
|
||||||
submitted={submitted}
|
submitted={submitted}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
</ControlPaneContainer>
|
</ControlPaneContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
const editorLocale = (
|
const editorLocale = useMemo(
|
||||||
<ControlPaneContainer $overFlow={!scrollSyncEnabled}>
|
() =>
|
||||||
<EditorControlPane
|
(locales ?? [])
|
||||||
collection={collection}
|
.filter(locale => locale !== defaultLocale)
|
||||||
entry={entry}
|
.map(locale => (
|
||||||
fields={fields}
|
<ControlPaneContainer key={locale} $hidden={locale !== selectedLocale}>
|
||||||
fieldsErrors={fieldsErrors}
|
<EditorControlPane
|
||||||
locale={locales?.[1]}
|
collection={collection}
|
||||||
onLocaleChange={handleLocaleChange}
|
entry={entry}
|
||||||
submitted={submitted}
|
fields={fields}
|
||||||
t={t}
|
fieldsErrors={fieldsErrors}
|
||||||
/>
|
locale={locale}
|
||||||
</ControlPaneContainer>
|
onLocaleChange={handleLocaleChange}
|
||||||
|
submitted={submitted}
|
||||||
|
canChangeLocale
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</ControlPaneContainer>
|
||||||
|
)),
|
||||||
|
[
|
||||||
|
collection,
|
||||||
|
defaultLocale,
|
||||||
|
entry,
|
||||||
|
fields,
|
||||||
|
fieldsErrors,
|
||||||
|
handleLocaleChange,
|
||||||
|
locales,
|
||||||
|
selectedLocale,
|
||||||
|
submitted,
|
||||||
|
t,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const previewEntry = collectionI18nEnabled
|
const previewEntry = collectionI18nEnabled
|
||||||
? getPreviewEntry(entry, selectedLocale, defaultLocale)
|
? getPreviewEntry(entry, selectedLocale[0], defaultLocale)
|
||||||
: entry;
|
: entry;
|
||||||
|
|
||||||
const editorWithPreview = (
|
const editorWithPreview = (
|
||||||
@ -291,7 +300,9 @@ const EditorInterface = ({
|
|||||||
<div>
|
<div>
|
||||||
<StyledSplitPane>
|
<StyledSplitPane>
|
||||||
<ScrollSyncPane>{editor}</ScrollSyncPane>
|
<ScrollSyncPane>{editor}</ScrollSyncPane>
|
||||||
<ScrollSyncPane>{editorLocale}</ScrollSyncPane>
|
<ScrollSyncPane>
|
||||||
|
<>{editorLocale}</>
|
||||||
|
</ScrollSyncPane>
|
||||||
</StyledSplitPane>
|
</StyledSplitPane>
|
||||||
</div>
|
</div>
|
||||||
</ScrollSync>
|
</ScrollSync>
|
||||||
|
@ -44,18 +44,15 @@ export function getI18nFilesDepth(collection: Collection, depth: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isFieldTranslatable(field: Field, locale?: string, defaultLocale?: string) {
|
export function isFieldTranslatable(field: Field, locale?: string, defaultLocale?: string) {
|
||||||
const isTranslatable = locale !== defaultLocale && field.i18n === I18N_FIELD.TRANSLATE;
|
return locale !== defaultLocale && field.i18n === I18N_FIELD.TRANSLATE;
|
||||||
return isTranslatable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFieldDuplicate(field: Field, locale?: string, defaultLocale?: string) {
|
export function isFieldDuplicate(field: Field, locale?: string, defaultLocale?: string) {
|
||||||
const isDuplicate = locale !== defaultLocale && field.i18n === I18N_FIELD.DUPLICATE;
|
return locale !== defaultLocale && field.i18n === I18N_FIELD.DUPLICATE;
|
||||||
return isDuplicate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFieldHidden(field: Field, locale?: string, defaultLocale?: string) {
|
export function isFieldHidden(field: Field, locale?: string, defaultLocale?: string) {
|
||||||
const isHidden = locale !== defaultLocale && field.i18n === I18N_FIELD.NONE;
|
return locale !== defaultLocale && field.i18n === I18N_FIELD.NONE;
|
||||||
return isHidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLocaleDataPath(locale: string) {
|
export function getLocaleDataPath(locale: string) {
|
||||||
@ -205,8 +202,15 @@ export function getI18nBackup(
|
|||||||
if (!data) {
|
if (!data) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
entry.data = data;
|
return {
|
||||||
return { ...acc, [locale]: { raw: entryToRaw(entry) } };
|
...acc,
|
||||||
|
[locale]: {
|
||||||
|
raw: entryToRaw({
|
||||||
|
...entry,
|
||||||
|
data,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
}, {} as Record<string, { raw: string }>);
|
}, {} as Record<string, { raw: string }>);
|
||||||
|
|
||||||
return i18nBackup;
|
return i18nBackup;
|
||||||
@ -386,9 +390,9 @@ export function duplicateI18nFields(
|
|||||||
locales
|
locales
|
||||||
.filter(l => l !== defaultLocale)
|
.filter(l => l !== defaultLocale)
|
||||||
.forEach(l => {
|
.forEach(l => {
|
||||||
entryDraft = get(
|
entryDraft = set(
|
||||||
entryDraft,
|
entryDraft,
|
||||||
['entry', ...getDataPath(l, defaultLocale), ...fieldPath],
|
['entry', ...getDataPath(l, defaultLocale), ...fieldPath].join('.'),
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -414,8 +418,10 @@ export function getPreviewEntry(
|
|||||||
if (!locale || locale === defaultLocale) {
|
if (!locale || locale === defaultLocale) {
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
entry.data = entry.i18n?.[locale]?.data as EntryData;
|
return {
|
||||||
return entry;
|
...entry,
|
||||||
|
data: entry.i18n?.[locale]?.data as EntryData,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeI18n(
|
export function serializeI18n(
|
||||||
|
@ -94,9 +94,6 @@ const de: LocalePhrasesRoot = {
|
|||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
writingInLocale: 'Aktuelle Sprache: %{locale}',
|
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: {
|
editor: {
|
||||||
|
@ -98,10 +98,6 @@ const en: LocalePhrasesRoot = {
|
|||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
writingInLocale: 'Writing in %{locale}',
|
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: {
|
editor: {
|
||||||
|
@ -95,9 +95,6 @@ const tr: LocalePhrasesRoot = {
|
|||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
writingInLocale: '%{locale} için yazılıyor',
|
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: {
|
editor: {
|
||||||
|
@ -181,6 +181,7 @@ function entries(
|
|||||||
const payload = action.payload;
|
const payload = action.payload;
|
||||||
const loadedEntries = payload.entries;
|
const loadedEntries = payload.entries;
|
||||||
const page = payload.page;
|
const page = payload.page;
|
||||||
|
const append = payload.append;
|
||||||
|
|
||||||
const entities = {
|
const entities = {
|
||||||
...state.entities,
|
...state.entities,
|
||||||
@ -196,7 +197,9 @@ function entries(
|
|||||||
|
|
||||||
pages[payload.collection] = {
|
pages[payload.collection] = {
|
||||||
page: page ?? undefined,
|
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,
|
isFetching: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -162,10 +162,14 @@ function entryDraftReducer(
|
|||||||
entry: set(newState.entry, `${dataPath.join('.')}.${path}`, value),
|
entry: set(newState.entry, `${dataPath.join('.')}.${path}`, value),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('BEFORE I18N', { ...newState.entry });
|
||||||
|
|
||||||
if (i18n) {
|
if (i18n) {
|
||||||
newState = duplicateI18nFields(newState, field, i18n.locales, i18n.defaultLocale);
|
newState = duplicateI18nFields(newState, field, i18n.locales, i18n.defaultLocale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('AFTER I18N', { ...newState.entry });
|
||||||
|
|
||||||
const newData = get(newState.entry, dataPath) ?? {};
|
const newData = get(newState.entry, dataPath) ?? {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -175,12 +179,16 @@ function entryDraftReducer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case DRAFT_VALIDATION_ERRORS: {
|
case DRAFT_VALIDATION_ERRORS: {
|
||||||
const { path, errors } = action.payload;
|
const { path, errors, i18n } = action.payload;
|
||||||
const fieldsErrors = { ...state.fieldsErrors };
|
const fieldsErrors = { ...state.fieldsErrors };
|
||||||
|
|
||||||
|
const dataPath = (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data'];
|
||||||
|
const fullPath = `${dataPath.join('.')}.${path}`;
|
||||||
|
|
||||||
if (errors.length === 0) {
|
if (errors.length === 0) {
|
||||||
delete fieldsErrors[path];
|
delete fieldsErrors[fullPath];
|
||||||
} else {
|
} else {
|
||||||
fieldsErrors[path] = action.payload.errors;
|
fieldsErrors[fullPath] = action.payload.errors;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...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';
|
import type { RootState } from '@staticcms/core/store';
|
||||||
|
|
||||||
export const selectFieldErrors = (path: string) => (state: RootState) => {
|
export const selectFieldErrors =
|
||||||
return state.entryDraft.fieldsErrors[path] ?? [];
|
(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) {
|
export function selectEditingDraft(state: RootState) {
|
||||||
return state.entryDraft.entry;
|
return state.entryDraft.entry;
|
||||||
|
@ -168,6 +168,7 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
|
|||||||
label={label}
|
label={label}
|
||||||
value={inputDate}
|
value={inputDate}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
disabled={isDisabled}
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<TextField
|
<TextField
|
||||||
key="mobile-date-input"
|
key="mobile-date-input"
|
||||||
@ -198,6 +199,7 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
|
|||||||
inputFormat={inputFormat}
|
inputFormat={inputFormat}
|
||||||
value={inputDate}
|
value={inputDate}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
disabled={isDisabled}
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<TextField
|
<TextField
|
||||||
key="time-input"
|
key="time-input"
|
||||||
@ -227,6 +229,7 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
|
|||||||
label={label}
|
label={label}
|
||||||
value={inputDate}
|
value={inputDate}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
disabled={isDisabled}
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<TextField
|
<TextField
|
||||||
key="mobile-date-time-input"
|
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.**
|
**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
|
## Folder Collections Path
|
||||||
|
|
||||||
See [Folder Collections Path](/docs/collection-types#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]
|
[build]
|
||||||
command = "yarn build"
|
ignore = "exit 0"
|
||||||
publish = ".next"
|
|
||||||
|
|
||||||
[[plugins]]
|
[[plugins]]
|
||||||
package = "@netlify/plugin-nextjs"
|
package = "@netlify/plugin-nextjs"
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user