Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i18n support for file collections - closes #4483 #4487

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
16f890e
Added i18n support to file collections
Oct 21, 2020
76c0098
Fixed i18n test for file collections
Oct 21, 2020
fcb0853
Typo
Oct 21, 2020
dfc2a81
Updated docs
Oct 21, 2020
77b4612
i18n test collections
Oct 26, 2020
da650c2
Better i18n dev-test collections
Oct 27, 2020
732b27d
Throw on non single_file structured file collections
Oct 27, 2020
c597251
Correctly default entry data on non existing file
Oct 27, 2020
dbb5766
Tweak the i18n limitations
Oct 27, 2020
1075420
Bleh
Oct 27, 2020
f6c2cc1
Tweaked i18n config
Oct 27, 2020
38c9037
Tweaked i18n config
Oct 27, 2020
1b0ef8e
DOH
Oct 27, 2020
4a8238a
Added i18n support to file collections
Oct 21, 2020
cdc208f
Fixed i18n test for file collections
Oct 21, 2020
8fea63c
Typo
Oct 21, 2020
ebafaa9
Updated docs
Oct 21, 2020
d32dcf0
i18n test collections
Oct 26, 2020
2e32920
Better i18n dev-test collections
Oct 27, 2020
53fed9b
Throw on non single_file structured file collections
Oct 27, 2020
84e00c2
Correctly default entry data on non existing file
Oct 27, 2020
bab4112
Tweak the i18n limitations
Oct 27, 2020
6de0c82
Bleh
Oct 27, 2020
4804994
Tweaked i18n config
Oct 27, 2020
962bf4a
Tweaked i18n config
Oct 27, 2020
0bc6694
DOH
Oct 27, 2020
a8d9aad
make tests excplicit by specifying multiple_files / multiple_folders
reimertz Nov 10, 2020
cf49eb0
refactor i18n check into util
reimertz Nov 10, 2020
9fefffa
Merge branch 'i18n-supported-file-collections' into i18n-supported-fi…
reimertz Nov 11, 2020
cacb124
Merge pull request #1 from reimertz/i18n-supported-file-collections
reimertz Nov 11, 2020
113f1a6
run format since resolving commits on github doesn't format the code. :)
reimertz Nov 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions dev-test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ site_url: https://example.com
publish_mode: editorial_workflow
media_folder: assets/uploads

i18n:
erezrokah marked this conversation as resolved.
Show resolved Hide resolved
structure: multiple_files
locales: [en, sv]
default_locale: en

collections: # A list of collections the CMS should be able to edit
- name: 'posts' # Used in routes, ie.: /admin/collections/:slug/edit
label: 'Posts' # Used in the UI
Expand Down Expand Up @@ -259,3 +264,106 @@ collections: # A list of collections the CMS should be able to edit
- { label: 'Date', name: 'date', widget: 'date' }
- { label: 'Image', name: 'image', widget: 'image' }
- { label: 'File', name: 'file', widget: 'file' }

- name: 'i18n_posts' # Used in routes, ie.: /admin/collections/:slug/edit
label: 'i18n Posts' # Used in the UI
label_singular: 'i18n Post' # Used in the UI, ie: "New Post"
description: >
The description is a great place for tone setting, high level information, and editing
guidelines that are specific to a collection.
folder: '_i18n_posts'
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
summary: '{{title}} -- {{year}}/{{month}}/{{day}}'
create: true # Allow users to create new documents in this collection
i18n:
structure: single_file
view_filters:
- label: Posts With Index
field: title
pattern: 'This is post #'
- label: Posts Without Index
field: title
pattern: front matter post
- label: Drafts
field: draft
pattern: true

fields: # The fields each document in this collection have
- { label: 'Title', name: 'title', widget: 'string', tagname: 'h1', i18n: true }
- { label: 'Draft', name: 'draft', widget: 'boolean', default: false, i18n: true }
- {
label: 'Publish Date',
name: 'date',
widget: 'datetime',
date_format: 'YYYY-MM-DD',
time_format: 'HH:mm',
format: 'YYYY-MM-DD HH:mm',
i18n: duplicate,
}
- label: 'Cover Image'
name: 'image'
widget: 'image'
required: false
tagname: ''
i18n: duplicate

- {
label: 'Body',
name: 'body',
widget: 'markdown',
hint: 'Main content goes here.',
i18n: true,
}

- name: 'i18n_settings'
label: 'i18n Settings'
delete: false # Prevent users from deleting documents in this collection
editor:
preview: false
i18n:
structure: single_file
files:
- name: 'general'
label: 'Site Settings'
file: '_i18n_data/settings.json'
description: 'General Site Settings'
i18n: true
fields:
- { label: 'Global title', name: 'site_title', widget: 'string', i18n: true }
- label: 'Post Settings'
name: posts
widget: 'object'
i18n: true
fields:
- {
label: 'Number of posts on frontpage',
name: front_limit,
widget: number,
min: 1,
max: 10,
i18n: true,
}
- { label: 'Default Author', name: author, widget: string, i18n: true }
- {
label: 'Default Thumbnail',
name: thumb,
widget: image,
class: 'thumb',
required: false,
i18n: true,
}

- name: 'authors'
label: 'Authors'
file: '_i18n_data/authors.yml'
description: 'Author descriptions'
i18n: true
fields:
- name: authors
label: Authors
label_singular: 'Author'
widget: list
i18n: true
fields:
- { label: 'Name', name: 'name', widget: 'string', hint: 'First and Last' }
- { label: 'Description', name: 'description', widget: 'markdown' }
21 changes: 19 additions & 2 deletions dev-test/index.html

Large diffs are not rendered by default.

74 changes: 72 additions & 2 deletions packages/netlify-cms-core/src/actions/__tests__/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ describe('config', () => {
).toEqual({ structure: 'multiple_folders', locales: ['en', 'fr'], default_locale: 'fr' });
});

it('should throw when i18n is set on files collection', () => {
it('should throw when i18n structure is not single_file on files collection', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we explicitly test for an error being thrown when structure is one of multiple_folders, multiple_files?

expect(() =>
applyDefaults(
fromJS({
Expand All @@ -654,7 +654,77 @@ describe('config', () => {
],
}),
),
).toThrow('i18n configuration is not supported for files collection');
).toThrow('i18n configuration for files collections is limited to single_file structures');
});

it('should throw when i18n structure is set to multiple_folders and contains a single file collection', () => {
expect(() =>
applyDefaults(
fromJS({
i18n: {
structure: 'multiple_folders',
locales: ['en', 'de'],
},
collections: [
{
files: [
{ name: 'file', file: 'file', fields: [{ name: 'title', widget: 'string' }] },
],
i18n: true,
},
],
}),
),
).toThrow('i18n configuration for files collections is limited to single_file structures');
});

it('should throw when i18n structure is set to multiple_files and contains a single file collection', () => {
expect(() =>
applyDefaults(
fromJS({
i18n: {
structure: 'multiple_files',
locales: ['en', 'de'],
},
collections: [
{
files: [
{ name: 'file', file: 'file', fields: [{ name: 'title', widget: 'string' }] },
],
i18n: true,
},
],
}),
),
).toThrow('i18n configuration for files collections is limited to single_file structures');
});

it('should set i18n value to translate on field when i18n=true for field in files collection', () => {
expect(
applyDefaults(
fromJS({
i18n: {
structure: 'multiple_folders',
locales: ['en', 'de'],
},
collections: [
{
files: [
{
name: 'file',
file: 'file',
i18n: true,
fields: [{ name: 'title', widget: 'string', i18n: true }],
},
],
i18n: {
structure: 'single_file',
},
},
],
}),
).getIn(['collections', 0, 'files', 0, 'fields', 0, 'i18n']),
).toEqual('translate');
});

it('should set i18n value to translate on field when i18n=true for field', () => {
Expand Down
40 changes: 32 additions & 8 deletions packages/netlify-cms-core/src/actions/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as publishModes from 'Constants/publishModes';
import { validateConfig } from 'Constants/configSchema';
import { selectDefaultSortableFields, traverseFields } from '../reducers/collections';
import { resolveBackend } from 'coreSrc/backend';
import { I18N, I18N_FIELD } from '../lib/i18n';
import { I18N, I18N_FIELD, isI18nAllowed } from '../lib/i18n';

export const CONFIG_REQUEST = 'CONFIG_REQUEST';
export const CONFIG_SUCCESS = 'CONFIG_SUCCESS';
Expand Down Expand Up @@ -90,14 +90,33 @@ const setI18nDefaults = (i18n, collection) => {

if (collectionI18n !== false) {
// set default values for i18n fields
collection = collection.set('fields', traverseFields(collection.get('fields'), setI18nField));
if (collection.has('fields')) {
collection = collection.set(
'fields',
traverseFields(collection.get('fields'), setI18nField),
);
}

if (collection.has('files')) {
collection = collection.set('files', traverseFields(collection.get('files'), setI18nField));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

traverseFields fields expects a list of fields and not of files.

}
}
} else {
collection = collection.delete(I18N);
collection = collection.set(
'fields',
traverseFields(collection.get('fields'), field => field.delete(I18N)),
);

if (collection.has('fields')) {
collection = collection.set(
'fields',
traverseFields(collection.get('fields'), field => field.delete(I18N)),
);
}

if (collection.has('files')) {
collection = collection.set(
'files',
traverseFields(collection.get('files'), field => field.delete(I18N)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

traverseFields fields expects a list of fields and not of files.

);
}
}
return collection;
};
Expand Down Expand Up @@ -244,9 +263,12 @@ export function applyDefaults(config) {

const files = collection.get('files');
if (files) {
if (i18n && collection.has(I18N)) {
throw new Error('i18n configuration is not supported for files collection');
if (isI18nAllowed(i18n, collection)) {
throw new Error(
'i18n configuration for files collections is limited to single_file structures',
);
}

collection = collection.delete('nested');
collection = collection.delete('meta');
collection = collection.set(
Expand All @@ -258,9 +280,11 @@ export function applyDefaults(config) {
'fields',
traverseFields(file.get('fields'), setDefaultPublicFolder),
);
file = setI18nDefaults(i18n, file);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

files should inherit collection level configuration. I think calling setI18nDefaults this way will inherit top level configuration.

This will require re-using some of the logic from here:
https://github.com/netlify/netlify-cms/blob/519cb2d4c2db729d2643c9116f93656b6a9dba23/packages/netlify-cms-core/src/actions/config.js#L74

return file;
}),
);
collection = setI18nDefaults(i18n, collection);
}

if (!collection.has('sortable_fields')) {
Expand Down
27 changes: 27 additions & 0 deletions packages/netlify-cms-core/src/lib/__tests__/i18n.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,33 @@ describe('i18n', () => {
raw: '',
});
});

it('should default to empty data object when file is empty and structure is I18N_STRUCTURE.SINGLE_FILE', async () => {
const data = {
'src/content/index.md': {
slug: 'index',
path: 'src/content/index.md',
data: {},
},
};
const getEntryValue = jest.fn(path => Promise.resolve(data[path]));

await expect(
i18n.getI18nEntry(
fromJS({
i18n: { structure: i18n.I18N_STRUCTURE.SINGLE_FILE, locales, default_locale },
}),
...args,
getEntryValue,
),
).resolves.toEqual({
slug: 'index',
path: 'src/content/index.md',
data: {},
i18n: {},
raw: '',
});
});
});

describe('groupEntries', () => {
Expand Down
20 changes: 18 additions & 2 deletions packages/netlify-cms-core/src/lib/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Map, List } from 'immutable';
import { set, trimEnd, groupBy } from 'lodash';
import { Collection, Entry, EntryDraft, EntryField, EntryMap } from '../types/redux';
import {
Collection,
Entry,
EntryDraft,
EntryField,
EntryMap,
i18n as i18nType,
} from '../types/redux';
import { selectEntrySlug } from '../reducers/collections';
import { EntryValue } from '../valueObjects/Entry';

Expand All @@ -18,6 +25,15 @@ export enum I18N_FIELD {
NONE = 'none',
}

export const isI18nAllowed = (i18n: i18nType, collection: Collection) => {
return (
i18n &&
collection.has(I18N) &&
i18n.get('structure') !== I18N_STRUCTURE.SINGLE_FILE &&
collection.getIn([I18N, 'structure']) !== I18N_STRUCTURE.SINGLE_FILE
);
};

export const hasI18n = (collection: Collection) => {
return collection.has(I18N);
};
Expand Down Expand Up @@ -244,7 +260,7 @@ const mergeValues = (
};

const mergeSingleFileValue = (entryValue: EntryValue, defaultLocale: string, locales: string[]) => {
const data = entryValue.data[defaultLocale];
const data = entryValue.data[defaultLocale] || {};
const i18n = locales
.filter(l => l !== defaultLocale)
.map(l => ({ locale: l, value: entryValue.data[l] }))
Expand Down
2 changes: 1 addition & 1 deletion packages/netlify-cms-core/src/types/redux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ type MetaObject = {

type Meta = StaticallyTypedRecord<MetaObject>;

type i18n = StaticallyTypedRecord<{
export type i18n = StaticallyTypedRecord<{
structure: string;
locales: string[];
default_locale: string;
Expand Down
2 changes: 1 addition & 1 deletion website/content/docs/beta-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ collections:

### Limitations

1. File collections are not supported.
1. File collections currently only support `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:

Expand Down