2020-09-20 20:30:46 +03:00

408 lines
12 KiB
TypeScript

import { Map, List } from 'immutable';
import { set, trimEnd, groupBy } from 'lodash';
import { Collection, Entry, EntryDraft, EntryField, EntryMap } from '../types/redux';
import { selectEntrySlug } from '../reducers/collections';
import { EntryValue } from '../valueObjects/Entry';
export const I18N = 'i18n';
export enum I18N_STRUCTURE {
MULTIPLE_FOLDERS = 'multiple_folders',
MULTIPLE_FILES = 'multiple_files',
SINGLE_FILE = 'single_file',
}
export enum I18N_FIELD {
TRANSLATE = 'translate',
DUPLICATE = 'duplicate',
NONE = 'none',
}
export const hasI18n = (collection: Collection) => {
return collection.has(I18N);
};
type I18nInfo = {
locales: string[];
defaultLocale: string;
structure: I18N_STRUCTURE;
};
export const getI18nInfo = (collection: Collection) => {
if (!hasI18n(collection)) {
return {};
}
const { structure, locales, default_locale: defaultLocale } = collection.get(I18N).toJS();
return { structure, locales, defaultLocale } as I18nInfo;
};
export const getI18nFilesDepth = (collection: Collection, depth: number) => {
const { structure } = getI18nInfo(collection) as I18nInfo;
if (structure === I18N_STRUCTURE.MULTIPLE_FOLDERS) {
return depth + 1;
}
return depth;
};
export const isFieldTranslatable = (field: EntryField, locale: string, defaultLocale: string) => {
const isTranslatable = locale !== defaultLocale && field.get(I18N) === I18N_FIELD.TRANSLATE;
return isTranslatable;
};
export const isFieldDuplicate = (field: EntryField, locale: string, defaultLocale: string) => {
const isDuplicate = locale !== defaultLocale && field.get(I18N) === I18N_FIELD.DUPLICATE;
return isDuplicate;
};
export const isFieldHidden = (field: EntryField, locale: string, defaultLocale: string) => {
const isHidden = locale !== defaultLocale && field.get(I18N) === I18N_FIELD.NONE;
return isHidden;
};
export const getLocaleDataPath = (locale: string) => {
return [I18N, locale, 'data'];
};
export const getDataPath = (locale: string, defaultLocale: string) => {
const dataPath = locale !== defaultLocale ? getLocaleDataPath(locale) : ['data'];
return dataPath;
};
export const getFilePath = (
structure: I18N_STRUCTURE,
extension: string,
path: string,
slug: string,
locale: string,
) => {
switch (structure) {
case I18N_STRUCTURE.MULTIPLE_FOLDERS:
return path.replace(`/${slug}`, `/${locale}/${slug}`);
case I18N_STRUCTURE.MULTIPLE_FILES:
return path.replace(extension, `${locale}.${extension}`);
case I18N_STRUCTURE.SINGLE_FILE:
default:
return path;
}
};
export const getLocaleFromPath = (structure: I18N_STRUCTURE, extension: string, path: string) => {
switch (structure) {
case I18N_STRUCTURE.MULTIPLE_FOLDERS: {
const parts = path.split('/');
// filename
parts.pop();
// locale
return parts.pop();
}
case I18N_STRUCTURE.MULTIPLE_FILES: {
const parts = trimEnd(path, `.${extension}`);
return parts.split('.').pop();
}
case I18N_STRUCTURE.SINGLE_FILE:
default:
return '';
}
};
export const getFilePaths = (
collection: Collection,
extension: string,
path: string,
slug: string,
) => {
const { structure, locales } = getI18nInfo(collection) as I18nInfo;
const paths = locales.map(locale =>
getFilePath(structure as I18N_STRUCTURE, extension, path, slug, locale),
);
return paths;
};
export const normalizeFilePath = (structure: I18N_STRUCTURE, path: string, locale: string) => {
switch (structure) {
case I18N_STRUCTURE.MULTIPLE_FOLDERS:
return path.replace(`${locale}/`, '');
case I18N_STRUCTURE.MULTIPLE_FILES:
return path.replace(`.${locale}`, '');
case I18N_STRUCTURE.SINGLE_FILE:
default:
return path;
}
};
export const getI18nFiles = (
collection: Collection,
extension: string,
entryDraft: EntryMap,
entryToRaw: (entryDraft: EntryMap) => string,
path: string,
slug: string,
newPath?: string,
) => {
const { structure, defaultLocale, locales } = getI18nInfo(collection) as I18nInfo;
if (structure === I18N_STRUCTURE.SINGLE_FILE) {
const data = locales.reduce((map, locale) => {
const dataPath = getDataPath(locale, defaultLocale);
return map.set(locale, entryDraft.getIn(dataPath));
}, Map<string, unknown>({}));
const draft = entryDraft.set('data', data);
return [
{
path: getFilePath(structure, extension, path, slug, locales[0]),
slug,
raw: entryToRaw(draft),
...(newPath && {
newPath: getFilePath(structure, extension, newPath, slug, locales[0]),
}),
},
];
}
const dataFiles = locales
.map(locale => {
const dataPath = getDataPath(locale, defaultLocale);
const draft = entryDraft.set('data', entryDraft.getIn(dataPath));
return {
path: getFilePath(structure, extension, path, slug, locale),
slug,
raw: draft.get('data') ? entryToRaw(draft) : '',
...(newPath && {
newPath: getFilePath(structure, extension, newPath, slug, locale),
}),
};
})
.filter(dataFile => dataFile.raw);
return dataFiles;
};
export const getI18nBackup = (
collection: Collection,
entry: EntryMap,
entryToRaw: (entry: EntryMap) => string,
) => {
const { locales, defaultLocale } = getI18nInfo(collection) as I18nInfo;
const i18nBackup = locales
.filter(l => l !== defaultLocale)
.reduce((acc, locale) => {
const dataPath = getDataPath(locale, defaultLocale);
const data = entry.getIn(dataPath);
if (!data) {
return acc;
}
const draft = entry.set('data', data);
return { ...acc, [locale]: { raw: entryToRaw(draft) } };
}, {} as Record<string, { raw: string }>);
return i18nBackup;
};
export const formatI18nBackup = (
i18nBackup: Record<string, { raw: string }>,
formatRawData: (raw: string) => EntryValue,
) => {
const i18n = Object.entries(i18nBackup).reduce((acc, [locale, { raw }]) => {
const entry = formatRawData(raw);
return { ...acc, [locale]: { data: entry.data } };
}, {});
return i18n;
};
const mergeValues = (
collection: Collection,
structure: I18N_STRUCTURE,
defaultLocale: string,
values: { locale: string; value: EntryValue }[],
) => {
let defaultEntry = values.find(e => e.locale === defaultLocale);
if (!defaultEntry) {
defaultEntry = values[0];
console.warn(`Could not locale entry for default locale '${defaultLocale}'`);
}
const i18n = values
.filter(e => e.locale !== defaultEntry!.locale)
.reduce((acc, { locale, value }) => {
const dataPath = getLocaleDataPath(locale);
return set(acc, dataPath, value.data);
}, {});
const path = normalizeFilePath(structure, defaultEntry.value.path, defaultLocale);
const slug = selectEntrySlug(collection, path) as string;
const entryValue: EntryValue = {
...defaultEntry.value,
raw: '',
...i18n,
path,
slug,
};
return entryValue;
};
const mergeSingleFileValue = (entryValue: EntryValue, defaultLocale: string, locales: string[]) => {
const data = entryValue.data[defaultLocale];
const i18n = locales
.filter(l => l !== defaultLocale)
.map(l => ({ locale: l, value: entryValue.data[l] }))
.filter(e => e.value)
.reduce((acc, e) => {
return { ...acc, [e.locale]: { data: e.value } };
}, {});
return {
...entryValue,
data,
i18n,
raw: '',
};
};
export const getI18nEntry = async (
collection: Collection,
extension: string,
path: string,
slug: string,
getEntryValue: (path: string) => Promise<EntryValue>,
) => {
const { structure, locales, defaultLocale } = getI18nInfo(collection) as I18nInfo;
let entryValue: EntryValue;
if (structure === I18N_STRUCTURE.SINGLE_FILE) {
entryValue = mergeSingleFileValue(await getEntryValue(path), defaultLocale, locales);
} else {
const entryValues = await Promise.all(
locales.map(async locale => {
const entryPath = getFilePath(structure, extension, path, slug, locale);
const value = await getEntryValue(entryPath).catch(() => null);
return { value, locale };
}),
);
const nonNullValues = entryValues.filter(e => e.value !== null) as {
value: EntryValue;
locale: string;
}[];
entryValue = mergeValues(collection, structure, defaultLocale, nonNullValues);
}
return entryValue;
};
export const groupEntries = (collection: Collection, extension: string, entries: EntryValue[]) => {
const { structure, defaultLocale, locales } = getI18nInfo(collection) as I18nInfo;
if (structure === I18N_STRUCTURE.SINGLE_FILE) {
return entries.map(e => mergeSingleFileValue(e, defaultLocale, locales));
}
const grouped = groupBy(
entries.map(e => ({
locale: getLocaleFromPath(structure, extension, e.path) as string,
value: e,
})),
({ locale, value: e }) => {
return normalizeFilePath(structure, e.path, locale);
},
);
const groupedEntries = Object.values(grouped).reduce((acc, values) => {
const entryValue = mergeValues(collection, structure, defaultLocale, values);
return [...acc, entryValue];
}, [] as EntryValue[]);
return groupedEntries;
};
export const getI18nDataFiles = (
collection: Collection,
extension: string,
path: string,
slug: string,
diffFiles: { path: string; id: string; newFile: boolean }[],
) => {
const { structure } = getI18nInfo(collection) as I18nInfo;
if (structure === I18N_STRUCTURE.SINGLE_FILE) {
return diffFiles;
}
const paths = getFilePaths(collection, extension, path, slug);
const dataFiles = paths.reduce((acc, path) => {
const dataFile = diffFiles.find(file => file.path === path);
if (dataFile) {
return [...acc, dataFile];
} else {
return [...acc, { path, id: '', newFile: false }];
}
}, [] as { path: string; id: string; newFile: boolean }[]);
return dataFiles;
};
export const duplicateI18nFields = (
entryDraft: EntryDraft,
field: EntryField,
locales: string[],
defaultLocale: string,
fieldPath: string[] = [field.get('name')],
) => {
const value = entryDraft.getIn(['entry', 'data', ...fieldPath]);
if (field.get(I18N) === I18N_FIELD.DUPLICATE) {
locales
.filter(l => l !== defaultLocale)
.forEach(l => {
entryDraft = entryDraft.setIn(
['entry', ...getDataPath(l, defaultLocale), ...fieldPath],
value,
);
});
}
if (field.has('field') && !List.isList(value)) {
const fields = [field.get('field') as EntryField];
fields.forEach(field => {
entryDraft = duplicateI18nFields(entryDraft, field, locales, defaultLocale, [
...fieldPath,
field.get('name'),
]);
});
} else if (field.has('fields') && !List.isList(value)) {
const fields = field.get('fields')!.toArray() as EntryField[];
fields.forEach(field => {
entryDraft = duplicateI18nFields(entryDraft, field, locales, defaultLocale, [
...fieldPath,
field.get('name'),
]);
});
}
return entryDraft;
};
export const getPreviewEntry = (entry: EntryMap, locale: string, defaultLocale: string) => {
if (locale === defaultLocale) {
return entry;
}
return entry.set('data', entry.getIn([I18N, locale, 'data']));
};
export const serializeI18n = (
collection: Collection,
entry: Entry,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
serializeValues: (data: any) => any,
) => {
const { locales, defaultLocale } = getI18nInfo(collection) as I18nInfo;
locales
.filter(locale => locale !== defaultLocale)
.forEach(locale => {
const dataPath = getLocaleDataPath(locale);
entry = entry.setIn(dataPath, serializeValues(entry.getIn(dataPath)));
});
return entry;
};