diff --git a/packages/netlify-cms-core/src/actions/__tests__/config.spec.js b/packages/netlify-cms-core/src/actions/__tests__/config.spec.js index 3420c585..7c5091ce 100644 --- a/packages/netlify-cms-core/src/actions/__tests__/config.spec.js +++ b/packages/netlify-cms-core/src/actions/__tests__/config.spec.js @@ -636,7 +636,33 @@ 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', () => { + 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: true, + }, + ], + }), + ), + ).toThrow('i18n configuration for files collections is limited to single_file structure'); + }); + + it('should throw when i18n structure is set to multiple_folders and contains a single file collection', () => { expect(() => applyDefaults( fromJS({ @@ -654,7 +680,56 @@ describe('config', () => { ], }), ), - ).toThrow('i18n configuration is not supported for files collection'); + ).toThrow('i18n configuration for files collections is limited to single_file structure'); + }); + + 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 structure'); + }); + + 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', () => { diff --git a/packages/netlify-cms-core/src/actions/config.js b/packages/netlify-cms-core/src/actions/config.js index 85956425..9e67ac73 100644 --- a/packages/netlify-cms-core/src/actions/config.js +++ b/packages/netlify-cms-core/src/actions/config.js @@ -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, I18N_STRUCTURE } from '../lib/i18n'; export const CONFIG_REQUEST = 'CONFIG_REQUEST'; export const CONFIG_SUCCESS = 'CONFIG_SUCCESS'; @@ -68,38 +68,53 @@ const setI18nField = field => { return field; }; -const setI18nDefaults = (i18n, collection) => { - if (i18n && collection.has(I18N)) { - const collectionI18n = collection.get(I18N); - if (collectionI18n === true) { - collection = collection.set(I18N, i18n); - } else if (collectionI18n === false) { - collection = collection.delete(I18N); +const setI18nDefaults = (defaultI18n, collectionOrFile) => { + if (defaultI18n && collectionOrFile.has(I18N)) { + const collectionOrFileI18n = collectionOrFile.get(I18N); + if (collectionOrFileI18n === true) { + collectionOrFile = collectionOrFile.set(I18N, defaultI18n); + } else if (collectionOrFileI18n === false) { + collectionOrFile = collectionOrFile.delete(I18N); } else { - const locales = collectionI18n.get('locales', i18n.get('locales')); - const defaultLocale = collectionI18n.get( + const locales = collectionOrFileI18n.get('locales', defaultI18n.get('locales')); + const defaultLocale = collectionOrFileI18n.get( 'default_locale', - collectionI18n.has('locales') ? locales.first() : i18n.get('default_locale'), + collectionOrFileI18n.has('locales') ? locales.first() : defaultI18n.get('default_locale'), ); - collection = collection.set(I18N, i18n.merge(collectionI18n)); - collection = collection.setIn([I18N, 'locales'], locales); - collection = collection.setIn([I18N, 'default_locale'], defaultLocale); + collectionOrFile = collectionOrFile.set(I18N, defaultI18n.merge(collectionOrFileI18n)); + collectionOrFile = collectionOrFile.setIn([I18N, 'locales'], locales); + collectionOrFile = collectionOrFile.setIn([I18N, 'default_locale'], defaultLocale); - throwOnMissingDefaultLocale(collection.get(I18N)); + throwOnMissingDefaultLocale(collectionOrFile.get(I18N)); } - if (collectionI18n !== false) { + if (collectionOrFileI18n !== false) { // set default values for i18n fields - collection = collection.set('fields', traverseFields(collection.get('fields'), setI18nField)); + if (collectionOrFile.has('fields')) { + collectionOrFile = collectionOrFile.set( + 'fields', + traverseFields(collectionOrFile.get('fields'), setI18nField), + ); + } } } else { - collection = collection.delete(I18N); - collection = collection.set( - 'fields', - traverseFields(collection.get('fields'), field => field.delete(I18N)), + collectionOrFile = collectionOrFile.delete(I18N); + if (collectionOrFile.has('fields')) { + collectionOrFile = collectionOrFile.set( + 'fields', + traverseFields(collectionOrFile.get('fields'), field => field.delete(I18N)), + ); + } + } + return collectionOrFile; +}; + +const throwOnInvalidFileCollectionStructure = i18n => { + if (i18n && i18n.get('structure') !== I18N_STRUCTURE.SINGLE_FILE) { + throw new Error( + `i18n configuration for files collections is limited to ${I18N_STRUCTURE.SINGLE_FILE} structure`, ); } - return collection; }; const throwOnMissingDefaultLocale = i18n => { @@ -211,6 +226,8 @@ export function applyDefaults(config) { collection = collection.set('publish', true); } + collection = setI18nDefaults(i18n, collection); + const folder = collection.get('folder'); if (folder) { if (collection.has('path') && !collection.has('media_folder')) { @@ -238,15 +255,13 @@ export function applyDefaults(config) { } else { collection = collection.set('meta', Map()); } - - collection = setI18nDefaults(i18n, collection); } const files = collection.get('files'); if (files) { - if (i18n && collection.has(I18N)) { - throw new Error('i18n configuration is not supported for files collection'); - } + const collectionI18n = collection.get(I18N); + throwOnInvalidFileCollectionStructure(collectionI18n); + collection = collection.delete('nested'); collection = collection.delete('meta'); collection = collection.set( @@ -258,6 +273,8 @@ export function applyDefaults(config) { 'fields', traverseFields(file.get('fields'), setDefaultPublicFolder), ); + file = setI18nDefaults(collectionI18n, file); + throwOnInvalidFileCollectionStructure(file.get(I18N)); return file; }), ); diff --git a/packages/netlify-cms-core/src/lib/__tests__/i18n.spec.js b/packages/netlify-cms-core/src/lib/__tests__/i18n.spec.js index 19f6047f..9fe5d8d8 100644 --- a/packages/netlify-cms-core/src/lib/__tests__/i18n.spec.js +++ b/packages/netlify-cms-core/src/lib/__tests__/i18n.spec.js @@ -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', () => { diff --git a/packages/netlify-cms-core/src/lib/i18n.ts b/packages/netlify-cms-core/src/lib/i18n.ts index 0b11b071..accc0d1f 100644 --- a/packages/netlify-cms-core/src/lib/i18n.ts +++ b/packages/netlify-cms-core/src/lib/i18n.ts @@ -244,7 +244,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] })) diff --git a/website/content/docs/beta-features.md b/website/content/docs/beta-features.md index 0a4a5b54..8467c49f 100644 --- a/website/content/docs/beta-features.md +++ b/website/content/docs/beta-features.md @@ -130,7 +130,7 @@ collections: ### Limitations -1. File collections are not supported. +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: